@fin.cx/compatibility 1.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/compatibility',
6
- version: '1.1.0',
6
+ version: '3.0.0',
7
7
  description: 'Exchange formats of third-party accounting systems: the EXTF Buchungsstapel in Windows-1252 on the rows of @fin.cx/skr, and the document-image archive (document.xml and ZIP)'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx1QkFBdUI7SUFDN0IsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLCtLQUErSztDQUM3TCxDQUFBIn0=
@@ -1,19 +1,39 @@
1
- /** A document image of the archive, linked to its bookings through the GUID in their Beleglink (field 20). */
1
+ /** The formats a document image can have here: PDF and the picture formats DATEV takes as a File (not WebP, AVIF or HEIC). */
2
+ export type TBelegbildFormat = 'pdf' | 'jpeg' | 'png' | 'gif' | 'tiff' | 'bmp';
3
+ export declare const BELEGBILD_FORMATS: readonly TBelegbildFormat[];
4
+ /** A document of the archive, linked to its bookings through the GUID in their Beleglink (field 20). */
2
5
  export interface IBelegbilderDocument {
6
+ /** RFC 4122, `8-4-4-4-12` hexadecimal digits, as DATEV takes it */
3
7
  guid: string;
8
+ /** cut to DATEV's 40 characters; empty, none is written */
4
9
  description: string;
10
+ /** what the file is; a PDF when absent */
11
+ format?: TBelegbildFormat;
5
12
  }
6
13
  export interface IBelegbilderEntry extends IBelegbilderDocument {
7
- /** the document as PDF */
14
+ /** the file as it is: the PDF, or the picture in `format` */
8
15
  bytes: Uint8Array;
9
16
  }
17
+ /** What the archive says of itself. */
18
+ export interface IBelegbilderArchive {
19
+ /** when the archive was written (DATEV's header `date`, required); its UTC clock time is written, as the Buchungsstapel header writes its own. The package reads no clock. */
20
+ createdAt: Date;
21
+ /** the software that wrote it (DATEV's `generatingSystem`, up to 40 characters) */
22
+ generatingSystem?: string;
23
+ }
10
24
  export declare const BELEGBILDER_CONTENT_TYPE = "application/zip";
11
25
  /** `DATEV_Belege_<label>.zip`, for example `DATEV_Belege_2026-03.zip`. */
12
26
  export declare const belegbilderFilename: (labelArg: string) => string;
27
+ /** The name of a document's file in the archive: its GUID and the extension of its format, for example `<GUID>.jpg`. */
28
+ export declare const belegbildFileName: (guidArg: string, formatArg?: TBelegbildFormat) => string;
13
29
  /**
14
- * The manifest of the archive, `document.xml`: one `<document>` per PDF,
15
- * named by its GUID. UTF-8, unlike the Buchungsstapel.
30
+ * The manifest of the archive, `document.xml` after DATEV's `Document_v060.xsd`:
31
+ * one `<document>` per file, named by its GUID, with its description and one
32
+ * `File` extension. A GUID listed twice for the same format is one document;
33
+ * characters XML 1.0 does not allow are left out of the texts. UTF-8, unlike
34
+ * the Buchungsstapel. Refuses, as a `CompatibilityError` `invalid_documents`,
35
+ * what DATEV would refuse.
16
36
  */
17
- export declare const buildDocumentXml: (documentsArg: readonly IBelegbilderDocument[]) => string;
18
- /** `document.xml` and one `<GUID>.pdf` per document, as one ZIP. */
19
- export declare const createBelegbilderZip: (entriesArg: readonly IBelegbilderEntry[]) => Promise<Uint8Array>;
37
+ export declare const buildDocumentXml: (documentsArg: readonly IBelegbilderDocument[], archiveArg: IBelegbilderArchive) => string;
38
+ /** `document.xml` and each document's file under its name (`belegbildFileName`), as one ZIP: a DATEV Document-Package. */
39
+ export declare const createBelegbilderZip: (entriesArg: readonly IBelegbilderEntry[], archiveArg: IBelegbilderArchive) => Promise<Uint8Array>;
@@ -1,7 +1,23 @@
1
1
  import * as plugins from './plugins.js';
2
+ import { CompatibilityError } from '../error.js';
3
+ export const BELEGBILD_FORMATS = ['pdf', 'jpeg', 'png', 'gif', 'tiff', 'bmp'];
4
+ const EXTENSIONS = {
5
+ pdf: 'pdf',
6
+ jpeg: 'jpg',
7
+ png: 'png',
8
+ gif: 'gif',
9
+ tiff: 'tif',
10
+ bmp: 'bmp',
11
+ };
2
12
  export const BELEGBILDER_CONTENT_TYPE = 'application/zip';
3
13
  /** `DATEV_Belege_<label>.zip`, for example `DATEV_Belege_2026-03.zip`. */
4
14
  export const belegbilderFilename = (labelArg) => `DATEV_Belege_${labelArg}.zip`;
15
+ /** The name of a document's file in the archive: its GUID and the extension of its format, for example `<GUID>.jpg`. */
16
+ export const belegbildFileName = (guidArg, formatArg = 'pdf') => `${guidArg}.${EXTENSIONS[formatArg]}`;
17
+ const GUID_PATTERN = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/u;
18
+ /** DATEV's `document` of `content`: 1 to 4999 (`Document_v060.xsd`) */
19
+ const MAX_DOCUMENTS = 4999;
20
+ const MAX_DESCRIPTION = 40;
5
21
  const escapeXml = (valueArg) => valueArg
6
22
  .replace(/&/gu, '&amp;')
7
23
  .replace(/</gu, '&lt;')
@@ -9,23 +25,97 @@ const escapeXml = (valueArg) => valueArg
9
25
  .replace(/"/gu, '&quot;')
10
26
  .replace(/'/gu, '&apos;');
11
27
  /**
12
- * The manifest of the archive, `document.xml`: one `<document>` per PDF,
13
- * named by its GUID. UTF-8, unlike the Buchungsstapel.
28
+ * The characters XML 1.0 does not allow (its production `Char`): C0 controls
29
+ * other than tab, line feed and carriage return, a surrogate not in a pair,
30
+ * U+FFFE and U+FFFF. Left out of a text before it is cut.
31
+ */
32
+ const XML_FORBIDDEN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/gu;
33
+ /** The characters XML allows, at most `maxArg` of them (characters, not UTF-16 units), trimmed. */
34
+ const clip = (valueArg, maxArg) => [...valueArg.replace(XML_FORBIDDEN, '').trim()].slice(0, maxArg).join('').trim();
35
+ const pad = (valueArg) => String(valueArg).padStart(2, '0');
36
+ /** `YYYY-MM-DDThh:mm:ss` of the date's UTC clock time, without a zone, as DATEV's `p10048` takes it. */
37
+ const headerDate = (dateArg) => `${dateArg.getUTCFullYear()}-${pad(dateArg.getUTCMonth() + 1)}-${pad(dateArg.getUTCDate())}`
38
+ + `T${pad(dateArg.getUTCHours())}:${pad(dateArg.getUTCMinutes())}:${pad(dateArg.getUTCSeconds())}`;
39
+ /**
40
+ * The documents once each: a GUID names one file, so a document linked by
41
+ * two bookings is listed once, under its first description; a GUID that names
42
+ * two different files (another format, or other bytes when `sameFileArg` can
43
+ * tell) is refused.
44
+ */
45
+ const distinctDocuments = (documentsArg, sameFileArg = () => true) => {
46
+ const firstOf = new Map();
47
+ const distinct = [];
48
+ documentsArg.forEach((documentArg, indexArg) => {
49
+ const key = documentArg.guid.toUpperCase();
50
+ const first = firstOf.get(key);
51
+ if (!first) {
52
+ firstOf.set(key, { document: documentArg, index: indexArg });
53
+ distinct.push(documentArg);
54
+ return;
55
+ }
56
+ if ((first.document.format ?? 'pdf') !== (documentArg.format ?? 'pdf') || !sameFileArg(first.document, documentArg)) {
57
+ throw new CompatibilityError('invalid_documents', `The GUID "${documentArg.guid}" names two different files (documents ${first.index + 1} and ${indexArg + 1}).`);
58
+ }
59
+ });
60
+ return distinct;
61
+ };
62
+ const sameBytes = (leftArg, rightArg) => leftArg.bytes.length === rightArg.bytes.length && leftArg.bytes.every((byteArg, indexArg) => byteArg === rightArg.bytes[indexArg]);
63
+ /** Refuses what DATEV does not take: no document or too many, a GUID not of the RFC 4122 form, a format it does not take. */
64
+ const checkDocuments = (documentsArg) => {
65
+ if (documentsArg.length === 0) {
66
+ throw new CompatibilityError('invalid_documents', 'A document package holds at least one document.');
67
+ }
68
+ if (documentsArg.length > MAX_DOCUMENTS) {
69
+ throw new CompatibilityError('invalid_documents', `A document package holds at most ${MAX_DOCUMENTS} documents, not ${documentsArg.length}.`);
70
+ }
71
+ documentsArg.forEach((documentArg, indexArg) => {
72
+ if (!GUID_PATTERN.test(documentArg.guid)) {
73
+ throw new CompatibilityError('invalid_documents', `The GUID "${documentArg.guid}" of document ${indexArg + 1} is not the 8-4-4-4-12 hexadecimal form DATEV takes.`);
74
+ }
75
+ if (documentArg.format !== undefined && !BELEGBILD_FORMATS.includes(documentArg.format)) {
76
+ throw new CompatibilityError('invalid_documents', `Document ${indexArg + 1} is in the format "${String(documentArg.format)}", which a DATEV Document-Package does not take.`);
77
+ }
78
+ });
79
+ };
80
+ /**
81
+ * The manifest of the archive, `document.xml` after DATEV's `Document_v060.xsd`:
82
+ * one `<document>` per file, named by its GUID, with its description and one
83
+ * `File` extension. A GUID listed twice for the same format is one document;
84
+ * characters XML 1.0 does not allow are left out of the texts. UTF-8, unlike
85
+ * the Buchungsstapel. Refuses, as a `CompatibilityError` `invalid_documents`,
86
+ * what DATEV would refuse.
14
87
  */
15
- export const buildDocumentXml = (documentsArg) => {
16
- const lines = ['<?xml version="1.0" encoding="UTF-8"?>', '<archive>', ' <content>'];
17
- for (const document of documentsArg) {
18
- lines.push(` <document guid="${escapeXml(document.guid)}">`, ' <extension>PDF</extension>', ` <filename>${escapeXml(document.guid)}.pdf</filename>`, ' <beleg_type>1</beleg_type>', ...(document.description ? [` <description>${escapeXml(document.description)}</description>`] : []), ' </document>');
88
+ export const buildDocumentXml = (documentsArg, archiveArg) => {
89
+ checkDocuments(documentsArg);
90
+ const documents = distinctDocuments(documentsArg);
91
+ const generatingSystem = archiveArg.generatingSystem === undefined ? '' : clip(archiveArg.generatingSystem, MAX_DESCRIPTION);
92
+ const lines = [
93
+ '<?xml version="1.0" encoding="UTF-8"?>',
94
+ '<archive xmlns="http://xml.datev.de/bedi/tps/document/v06.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
95
+ + ' xsi:schemaLocation="http://xml.datev.de/bedi/tps/document/v06.0 Document_v060.xsd" version="6.0"'
96
+ + `${generatingSystem ? ` generatingSystem="${escapeXml(generatingSystem)}"` : ''}>`,
97
+ ' <header>',
98
+ ` <date>${headerDate(archiveArg.createdAt)}</date>`,
99
+ ' </header>',
100
+ ' <content>',
101
+ ];
102
+ for (const document of documents) {
103
+ const description = clip(document.description, MAX_DESCRIPTION);
104
+ lines.push(` <document guid="${escapeXml(document.guid)}">`, ...(description ? [` <description>${escapeXml(description)}</description>`] : []), ` <extension xsi:type="File" name="${escapeXml(belegbildFileName(document.guid, document.format))}"/>`, ' </document>');
19
105
  }
20
106
  lines.push(' </content>', '</archive>', '');
21
107
  return lines.join('\n');
22
108
  };
23
- /** `document.xml` and one `<GUID>.pdf` per document, as one ZIP. */
24
- export const createBelegbilderZip = async (entriesArg) => {
109
+ /** `document.xml` and each document's file under its name (`belegbildFileName`), as one ZIP: a DATEV Document-Package. */
110
+ export const createBelegbilderZip = async (entriesArg, archiveArg) => {
111
+ checkDocuments(entriesArg);
112
+ // one file per GUID: the same document linked by two bookings once, one GUID for two different files refused
113
+ const entries = distinctDocuments(entriesArg, sameBytes);
114
+ const manifest = buildDocumentXml(entries, archiveArg);
25
115
  const zipTools = new plugins.smartarchiveZip.ZipTools();
26
116
  return zipTools.createZip([
27
- { archivePath: 'document.xml', content: buildDocumentXml(entriesArg) },
28
- ...entriesArg.map((entryArg) => ({ archivePath: `${entryArg.guid}.pdf`, content: entryArg.bytes })),
117
+ { archivePath: 'document.xml', content: manifest },
118
+ ...entries.map((entryArg) => ({ archivePath: belegbildFileName(entryArg.guid, entryArg.format), content: entryArg.bytes })),
29
119
  ]);
30
120
  };
31
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9iZWxlZ2JpbGRlci9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQWF4QyxNQUFNLENBQUMsTUFBTSx3QkFBd0IsR0FBRyxpQkFBaUIsQ0FBQztBQUUxRCwwRUFBMEU7QUFDMUUsTUFBTSxDQUFDLE1BQU0sbUJBQW1CLEdBQUcsQ0FBQyxRQUFnQixFQUFVLEVBQUUsQ0FBQyxnQkFBZ0IsUUFBUSxNQUFNLENBQUM7QUFFaEcsTUFBTSxTQUFTLEdBQUcsQ0FBQyxRQUFnQixFQUFVLEVBQUUsQ0FDN0MsUUFBUTtLQUNMLE9BQU8sQ0FBQyxLQUFLLEVBQUUsT0FBTyxDQUFDO0tBQ3ZCLE9BQU8sQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDO0tBQ3RCLE9BQU8sQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDO0tBQ3RCLE9BQU8sQ0FBQyxLQUFLLEVBQUUsUUFBUSxDQUFDO0tBQ3hCLE9BQU8sQ0FBQyxLQUFLLEVBQUUsUUFBUSxDQUFDLENBQUM7QUFFOUI7OztHQUdHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sZ0JBQWdCLEdBQUcsQ0FBQyxZQUE2QyxFQUFVLEVBQUU7SUFDeEYsTUFBTSxLQUFLLEdBQUcsQ0FBQyx3Q0FBd0MsRUFBRSxXQUFXLEVBQUUsYUFBYSxDQUFDLENBQUM7SUFDckYsS0FBSyxNQUFNLFFBQVEsSUFBSSxZQUFZLEVBQUUsQ0FBQztRQUNwQyxLQUFLLENBQUMsSUFBSSxDQUNSLHVCQUF1QixTQUFTLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQ25ELGtDQUFrQyxFQUNsQyxtQkFBbUIsU0FBUyxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsaUJBQWlCLEVBQzVELGtDQUFrQyxFQUNsQyxHQUFHLENBQUMsUUFBUSxDQUFDLFdBQVcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxzQkFBc0IsU0FBUyxDQUFDLFFBQVEsQ0FBQyxXQUFXLENBQUMsZ0JBQWdCLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLEVBQ3hHLGlCQUFpQixDQUNsQixDQUFDO0lBQ0osQ0FBQztJQUNELEtBQUssQ0FBQyxJQUFJLENBQUMsY0FBYyxFQUFFLFlBQVksRUFBRSxFQUFFLENBQUMsQ0FBQztJQUM3QyxPQUFPLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7QUFDMUIsQ0FBQyxDQUFDO0FBRUYsb0VBQW9FO0FBQ3BFLE1BQU0sQ0FBQyxNQUFNLG9CQUFvQixHQUFHLEtBQUssRUFBRSxVQUF3QyxFQUF1QixFQUFFO0lBQzFHLE1BQU0sUUFBUSxHQUFHLElBQUksT0FBTyxDQUFDLGVBQWUsQ0FBQyxRQUFRLEVBQUUsQ0FBQztJQUN4RCxPQUFPLFFBQVEsQ0FBQyxTQUFTLENBQUM7UUFDeEIsRUFBRSxXQUFXLEVBQUUsY0FBYyxFQUFFLE9BQU8sRUFBRSxnQkFBZ0IsQ0FBQyxVQUFVLENBQUMsRUFBRTtRQUN0RSxHQUFHLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUMsRUFBRSxXQUFXLEVBQUUsR0FBRyxRQUFRLENBQUMsSUFBSSxNQUFNLEVBQUUsT0FBTyxFQUFFLFFBQVEsQ0FBQyxLQUFLLEVBQUUsQ0FBQyxDQUFDO0tBQ3BHLENBQUMsQ0FBQztBQUNMLENBQUMsQ0FBQyJ9
121
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9iZWxlZ2JpbGRlci9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUN4QyxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFxQmpELE1BQU0sQ0FBQyxNQUFNLGlCQUFpQixHQUFnQyxDQUFDLEtBQUssRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsS0FBSyxDQUFDLENBQUM7QUFFM0csTUFBTSxVQUFVLEdBQStDO0lBQzdELEdBQUcsRUFBRSxLQUFLO0lBQ1YsSUFBSSxFQUFFLEtBQUs7SUFDWCxHQUFHLEVBQUUsS0FBSztJQUNWLEdBQUcsRUFBRSxLQUFLO0lBQ1YsSUFBSSxFQUFFLEtBQUs7SUFDWCxHQUFHLEVBQUUsS0FBSztDQUNYLENBQUM7QUF5QkYsTUFBTSxDQUFDLE1BQU0sd0JBQXdCLEdBQUcsaUJBQWlCLENBQUM7QUFFMUQsMEVBQTBFO0FBQzFFLE1BQU0sQ0FBQyxNQUFNLG1CQUFtQixHQUFHLENBQUMsUUFBZ0IsRUFBVSxFQUFFLENBQUMsZ0JBQWdCLFFBQVEsTUFBTSxDQUFDO0FBRWhHLHdIQUF3SDtBQUN4SCxNQUFNLENBQUMsTUFBTSxpQkFBaUIsR0FBRyxDQUFDLE9BQWUsRUFBRSxZQUE4QixLQUFLLEVBQVUsRUFBRSxDQUFDLEdBQUcsT0FBTyxJQUFJLFVBQVUsQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFDO0FBRXpJLE1BQU0sWUFBWSxHQUFHLGdGQUFnRixDQUFDO0FBQ3RHLHVFQUF1RTtBQUN2RSxNQUFNLGFBQWEsR0FBRyxJQUFJLENBQUM7QUFDM0IsTUFBTSxlQUFlLEdBQUcsRUFBRSxDQUFDO0FBRTNCLE1BQU0sU0FBUyxHQUFHLENBQUMsUUFBZ0IsRUFBVSxFQUFFLENBQzdDLFFBQVE7S0FDTCxPQUFPLENBQUMsS0FB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
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Why an exchange file could not be written:
3
3
  * - `invalid_bookings`: a draft the account policy refuses; the message names it;
4
- * - `export_failed`: the conversion into rows failed.
4
+ * - `export_failed`: the conversion into rows failed;
5
+ * - `invalid_documents`: a document of a document-image archive DATEV does not take (its GUID, its format), or none.
5
6
  */
6
- export type TCompatibilityErrorCode = 'invalid_bookings' | 'export_failed';
7
+ export type TCompatibilityErrorCode = 'invalid_bookings' | 'export_failed' | 'invalid_documents';
7
8
  export declare class CompatibilityError extends Error {
8
9
  readonly code: TCompatibilityErrorCode;
9
10
  constructor(codeArg: TCompatibilityErrorCode, messageArg: string);
package/dist_ts/error.js CHANGED
@@ -5,4 +5,4 @@ export class CompatibilityError extends Error {
5
5
  this.code = codeArg;
6
6
  }
7
7
  }
8
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9lcnJvci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFPQSxNQUFNLE9BQU8sa0JBQW1CLFNBQVEsS0FBSztJQUczQyxZQUFZLE9BQWdDLEVBQUUsVUFBa0I7UUFDOUQsS0FBSyxDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBQ2xCLElBQUksQ0FBQyxJQUFJLEdBQUcsb0JBQW9CLENBQUM7UUFDakMsSUFBSSxDQUFDLElBQUksR0FBRyxPQUFPLENBQUM7SUFDdEIsQ0FBQztDQUNGIn0=
8
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9lcnJvci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFRQSxNQUFNLE9BQU8sa0JBQW1CLFNBQVEsS0FBSztJQUczQyxZQUFZLE9BQWdDLEVBQUUsVUFBa0I7UUFDOUQsS0FBSyxDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBQ2xCLElBQUksQ0FBQyxJQUFJLEdBQUcsb0JBQW9CLENBQUM7UUFDakMsSUFBSSxDQUFDLElBQUksR0FBRyxPQUFPLENBQUM7SUFDdEIsQ0FBQztDQUNGIn0=
@@ -33,7 +33,9 @@ export declare const bookingsOfRows: (rowsArg: readonly IDatevBuchungsstapelRow[
33
33
  export declare const differingBookingCount: (leftArg: readonly IExportBooking[], rightArg: readonly IExportBooking[]) => number;
34
34
  /**
35
35
  * Checks every draft against the account policy and turns the drafts into
36
- * Buchungsstapel rows (`@fin.cx/skr` `journalDraftsToDatevRows`, not fixed).
36
+ * Buchungsstapel rows (`@fin.cx/skr` `journalDraftsToDatevRows` under that
37
+ * policy, not fixed): its Automatikkonten and VAT accounts decide which VAT
38
+ * lines fold into their base.
37
39
  * A draft the policy refuses throws `invalid_bookings`, naming the draft; a
38
40
  * failure of the conversion throws `export_failed`.
39
41
  */
@@ -47,6 +49,32 @@ export interface IBuchungsstapelOptions {
47
49
  /** the file's name, for example `extfFilename('2026-03')` */
48
50
  filename: string;
49
51
  }
52
+ /**
53
+ * A character a Buchungsstapel cannot hold in Windows-1252, written as `?`,
54
+ * with where it stands: `offset` in the whole text, and for a character of a
55
+ * booking row the index of that row in `rows` (from 0) and the number of its
56
+ * DATEV field (from 1, 14 is the Buchungstext). A character of the header
57
+ * line has neither.
58
+ */
59
+ export interface IBuchungsstapelReplacement extends IReplacedCharacter {
60
+ row?: number;
61
+ field?: number;
62
+ }
63
+ /** A character of one booking row the file cannot hold, as `replacedCharactersOfRows` reports it. */
64
+ export interface IRowReplacement {
65
+ /** the index of the row in the rows given, from 0 */
66
+ row: number;
67
+ /** the number of the DATEV field it stands in, from 1 (14 is the Buchungstext) */
68
+ field: number;
69
+ character: string;
70
+ }
71
+ /**
72
+ * The characters of each row that a Buchungsstapel in Windows-1252 cannot
73
+ * hold, in row order, without writing a file: the rows' lines as the writer
74
+ * writes them (`@fin.cx/skr` `datevRowFields`), so a character a field is cut
75
+ * off before is not named. A row the writer refuses throws as it does.
76
+ */
77
+ export declare const replacedCharactersOfRows: (rowsArg: readonly IDatevBuchungsstapelRow[]) => IRowReplacement[];
50
78
  export interface IBuchungsstapelFile {
51
79
  filename: string;
52
80
  /** the file as text, lines ending in CRLF */
@@ -55,8 +83,8 @@ export interface IBuchungsstapelFile {
55
83
  bytes: Uint8Array;
56
84
  rows: IDatevBuchungsstapelRow[];
57
85
  bookings: IExportBooking[];
58
- /** the characters Windows-1252 cannot write, each written as `?` */
59
- replacedCharacters: IReplacedCharacter[];
86
+ /** the characters Windows-1252 cannot write, each written as `?`, with the row and field it stands in */
87
+ replacedCharacters: IBuchungsstapelReplacement[];
60
88
  }
61
89
  /** Writes a Buchungsstapel: the rows of the drafts, the header and the caption row, encoded in Windows-1252. */
62
90
  export declare const writeBuchungsstapel: (optionsArg: IBuchungsstapelOptions) => IBuchungsstapelFile;
@@ -50,7 +50,9 @@ export const differingBookingCount = (leftArg, rightArg) => {
50
50
  };
51
51
  /**
52
52
  * Checks every draft against the account policy and turns the drafts into
53
- * Buchungsstapel rows (`@fin.cx/skr` `journalDraftsToDatevRows`, not fixed).
53
+ * Buchungsstapel rows (`@fin.cx/skr` `journalDraftsToDatevRows` under that
54
+ * policy, not fixed): its Automatikkonten and VAT accounts decide which VAT
55
+ * lines fold into their base.
54
56
  * A draft the policy refuses throws `invalid_bookings`, naming the draft; a
55
57
  * failure of the conversion throws `export_failed`.
56
58
  */
@@ -62,17 +64,79 @@ export const datevRowsOf = (draftsArg, policyArg) => {
62
64
  }
63
65
  }
64
66
  try {
65
- return plugins.skrCore.journalDraftsToDatevRows([...draftsArg], { festschreibung: false });
67
+ // the rows consolidate under the same policy the drafts are checked against, not the chart's default one
68
+ return plugins.skrCore.journalDraftsToDatevRows([...draftsArg], { festschreibung: false, policy: policyArg });
66
69
  }
67
70
  catch (error) {
68
71
  throw new CompatibilityError('export_failed', error instanceof Error ? error.message : String(error));
69
72
  }
70
73
  };
74
+ /**
75
+ * Where the characters of one row stand in its line: the line is the fields
76
+ * joined with `;` (`@fin.cx/skr` `datevRowFields`), so field n begins after
77
+ * the lengths of the fields before it and one separator each.
78
+ */
79
+ const fieldStartsOf = (fieldsArg) => {
80
+ const starts = [];
81
+ let offset = 0;
82
+ for (const field of fieldsArg) {
83
+ starts.push(offset);
84
+ offset += field.length + 1;
85
+ }
86
+ return starts;
87
+ };
88
+ const fieldAt = (startsArg, offsetArg) => {
89
+ let field = 0;
90
+ while (field + 1 < startsArg.length && startsArg[field + 1] <= offsetArg) {
91
+ field += 1;
92
+ }
93
+ return field + 1;
94
+ };
95
+ /**
96
+ * The characters of each row that a Buchungsstapel in Windows-1252 cannot
97
+ * hold, in row order, without writing a file: the rows' lines as the writer
98
+ * writes them (`@fin.cx/skr` `datevRowFields`), so a character a field is cut
99
+ * off before is not named. A row the writer refuses throws as it does.
100
+ */
101
+ export const replacedCharactersOfRows = (rowsArg) => {
102
+ const replacements = [];
103
+ rowsArg.forEach((rowArg, rowIndex) => {
104
+ const fields = plugins.skrCore.datevRowFields(rowArg);
105
+ const starts = fieldStartsOf(fields);
106
+ for (const replaced of encodeWindows1252(fields.join(';')).replaced) {
107
+ replacements.push({ row: rowIndex, field: fieldAt(starts, replaced.offset), character: replaced.character });
108
+ }
109
+ });
110
+ return replacements;
111
+ };
71
112
  /** Writes a Buchungsstapel: the rows of the drafts, the header and the caption row, encoded in Windows-1252. */
72
113
  export const writeBuchungsstapel = (optionsArg) => {
73
114
  const rows = datevRowsOf(optionsArg.drafts, optionsArg.policy);
74
115
  const { csv } = plugins.skrCore.writeExtfBuchungsstapel(rows, optionsArg.header);
75
116
  const { bytes, replaced } = encodeWindows1252(csv);
76
- return { filename: optionsArg.filename, csv, bytes, rows, bookings: bookingsOfRows(rows), replacedCharacters: replaced };
117
+ // the file is the header line, the caption line and one line per row, each ending in CRLF (`writeExtfBuchungsstapel`)
118
+ const rowLines = rows.map((rowArg) => plugins.skrCore.datevRowFields(rowArg));
119
+ const [headerLine = '', captionLine = ''] = csv.split('\r\n', 2);
120
+ const rowsStart = headerLine.length + 2 + captionLine.length + 2;
121
+ const lineStarts = [];
122
+ let offset = rowsStart;
123
+ for (const fields of rowLines) {
124
+ lineStarts.push(offset);
125
+ offset += fields.join(';').length + 2;
126
+ }
127
+ if (offset !== csv.length || rowLines.some((fieldsArg, indexArg) => csv.slice(lineStarts[indexArg], lineStarts[indexArg] + fieldsArg.join(';').length) !== fieldsArg.join(';'))) {
128
+ throw new CompatibilityError('export_failed', 'the rows of the Buchungsstapel are not the lines @fin.cx/skr writes for them');
129
+ }
130
+ const replacedCharacters = replaced.map((replacedArg) => {
131
+ if (replacedArg.offset < rowsStart) {
132
+ return { ...replacedArg };
133
+ }
134
+ let row = 0;
135
+ while (row + 1 < lineStarts.length && lineStarts[row + 1] <= replacedArg.offset) {
136
+ row += 1;
137
+ }
138
+ return { ...replacedArg, row, field: fieldAt(fieldStartsOf(rowLines[row]), replacedArg.offset - lineStarts[row]) };
139
+ });
140
+ return { filename: optionsArg.filename, csv, bytes, rows, bookings: bookingsOfRows(rows), replacedCharacters };
77
141
  };
78
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9leHRmL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBQ3hDLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUNqRCxPQUFPLEVBQUUsaUJBQWlCLEVBQTJCLE1BQU0sc0JBQXNCLENBQUM7QUFFbEYsT0FBTyxFQUFFLGtCQUFrQixFQUFnQyxNQUFNLGFBQWEsQ0FBQztBQUsvRSxzRkFBc0Y7QUFDdEYsTUFBTSxDQUFDLE1BQU0saUJBQWlCLEdBQUcsZ0NBQWdDLENBQUM7QUFFbEUsd0ZBQXdGO0FBQ3hGLE1BQU0sQ0FBQyxNQUFNLFlBQVksR0FBRyxDQUFDLFFBQWdCLEVBQVUsRUFBRSxDQUFDLHVCQUF1QixRQUFRLE1BQU0sQ0FBQztBQWtCaEcsMEVBQTBFO0FBQzFFLE1BQU0sVUFBVSxHQUFHLENBQUMsT0FBZSxFQUFVLEVBQUUsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLFlBQVksRUFBRSxFQUFFLENBQUMsQ0FBQyxPQUFPLENBQUMsV0FBVyxFQUFFLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDO0FBRWxILDRDQUE0QztBQUM1QyxNQUFNLENBQUMsTUFBTSxjQUFjLEdBQUcsQ0FBQyxPQUEyQyxFQUFvQixFQUFFLENBQzlGLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDdkIsV0FBVyxFQUFFLE1BQU0sQ0FBQyxXQUFXO0lBQy9CLFNBQVMsRUFBRSxNQUFNLENBQUMsU0FBUztJQUMzQixLQUFLLEVBQUUsTUFBTSxDQUFDLEtBQUs7SUFDbkIsVUFBVSxFQUFFLE1BQU0sQ0FBQyxVQUFVO0lBQzdCLEtBQUssRUFBRSxNQUFNLENBQUMsWUFBWTtJQUMxQixJQUFJLEVBQUUsTUFBTSxDQUFDLFVBQVUsQ0FBQyxXQUFXLEVB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
142
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9leHRmL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBQ3hDLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUNqRCxPQUFPLEVBQUUsaUJBQWlCLEVBQTJCLE1BQU0sc0JBQXNCLENBQUM7QUFFbEYsT0FBTyxFQUFFLGtCQUFrQixFQUFnQyxNQUFNLGFBQWEsQ0FBQztBQUsvRSxzRkFBc0Y7QUFDdEYsTUFBTSxDQUFDLE1BQU0saUJBQWlCLEdBQUcsZ0NBQWdDLENBQUM7QUFFbEUsd0ZBQXdGO0FBQ3hGLE1BQU0sQ0FBQyxNQUFNLFlBQVksR0FBRyxDQUFDLFFBQWdCLEVBQVUsRUFBRSxDQUFDLHVCQUF1QixRQUFRLE1BQU0sQ0FBQztBQWtCaEcsMEVBQTBFO0FBQzFFLE1BQU0sVUFBVSxHQUFHLENBQUMsT0FBZSxFQUFVLEVBQUUsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLFlBQVksRUFBRSxFQUFFLENBQUMsQ0FBQyxPQUFPLENBQUMsV0FBVyxFQUFFLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDO0FBRWxILDRDQUE0QztBQUM1QyxNQUFNLENBQUMsTUFBTSxjQUFjLEdBQUcsQ0FBQyxPQUEyQyxFQUFvQixFQUFFLENBQzlGLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDdkIsV0FBVyxFQUFFLE1BQU0sQ0FBQyxXQUFXO0lBQy9CLFNBQVMsRUFBRSxNQUFNLENBQUMsU0FBUztJQUMzQixLQUFLLEVBQUUsTUFBTSxDQUFDLEtBQUs7SUFDbkIsVUFBVSxFQUFFLE1BQU0sQ0FBQyxVQUFVO0lBQzdCLEtBQUssRUFBRSxNQUFNLENBQUMsWUFBWTtJQUMxQixJQUFJLEVBQUUsTUFBTSxDQUFDLFVBQVUsQ0FBQyxXQUFXLEVB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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fin.cx/compatibility",
3
- "version": "1.1.0",
3
+ "version": "3.0.0",
4
4
  "private": false,
5
5
  "description": "Exchange formats of third-party accounting systems: the EXTF Buchungsstapel in Windows-1252 on the rows of @fin.cx/skr, and the document-image archive (document.xml and ZIP)",
6
6
  "main": "dist_ts/index.js",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "homepage": "https://code.foss.global/fin.cx/compatibility#readme",
44
44
  "dependencies": {
45
- "@fin.cx/skr": "^4.0.0",
45
+ "@fin.cx/skr": "^5.2.0",
46
46
  "@push.rocks/smartarchive": "^5.4.1"
47
47
  },
48
48
  "devDependencies": {
package/readme.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Exchange formats of third-party accounting systems: the files a German tax advisor's system imports. The package's name carries no vendor's name; the formats and files keep the names the importing system expects (EXTF, `DATEV_Belege_<label>.zip`, the Datev field names in `@fin.cx/skr`'s rows). Two formats, each on its own entry point:
4
4
 
5
5
  - `@fin.cx/compatibility/extf`: the **EXTF Buchungsstapel**. The rows and the text come from `@fin.cx/skr` (`journalDraftsToDatevRows`, `writeExtfBuchungsstapel`); this module checks the drafts, writes the file in Windows-1252 and says which characters the encoding could not hold.
6
- - `@fin.cx/compatibility/belegbilder`: the **document-image archive**, a `document.xml` manifest plus one `<GUID>.pdf` per document, as one ZIP, linked to the bookings through the GUID in their Beleglink (field 20).
6
+ - `@fin.cx/compatibility/belegbilder`: the **document-image archive**, a DATEV Document-Package: `document.xml` after DATEV's `Document_v060.xsd` plus one `<GUID>.<extension>` per document (a PDF or a picture), as one ZIP, linked to the bookings through the GUID in their Beleglink (field 20, `BEDI "GUID"`).
7
7
 
8
8
  It reads no clock, keeps no state and writes nothing anywhere: files go back to the caller as bytes.
9
9
 
@@ -41,20 +41,39 @@ const file = extf.writeBuchungsstapel({
41
41
  });
42
42
  file.bytes; // Uint8Array in Windows-1252, served as extf.EXTF_CONTENT_TYPE
43
43
  file.bookings; // what each row says, for keeping and comparing
44
- file.replacedCharacters; // [{ offset, character: 'ő' }, …]: written as "?"
44
+ file.replacedCharacters; // [{ offset, character: 'ő', row: 1, field: 14 }, …]: written as "?"
45
45
  ```
46
46
 
47
+ - **Where a character that became `?` stands.** Each entry of `replacedCharacters` names its `offset` in the text and, for a booking row, the index of that row in `rows` (from 0) and the number of its DATEV field (from 1; 14 is the Buchungstext). A character of the header line (the label) has neither.
48
+ - **The same without a file.** `extf.replacedCharactersOfRows(rows)` names the characters of each row the file could not hold (`{ row, field, character }`), without a header and without writing anything: for a preview, before the consultant and client numbers of the header are known. It reads each row's line as the writer writes it (`@fin.cx/skr` `datevRowFields`), so a character a field is cut off before (the Buchungstext keeps 60 characters) is not named.
49
+
47
50
  - **A draft the account policy refuses** throws `CompatibilityError` with code `invalid_bookings` and a message naming the draft.
48
51
  - **A failed conversion** throws code `export_failed`.
52
+ - **A document-image archive DATEV would not take** (no document, a GUID of another form, a format it does not take) throws code `invalid_documents`.
49
53
  - **`differingBookingCount(left, right)`** counts the rows two Stapel of one period do not share. It compares each booking's day, amount, side, accounts, BU key and Belegfeld 1, as a multiset, and ignores wording and document links.
50
54
 
51
55
  ```typescript
52
56
  import { belegbilder } from '@fin.cx/compatibility';
53
57
 
54
- const zip = await belegbilder.createBelegbilderZip([{ guid, description: 'Rechnung 7', bytes: pdfBytes }]);
58
+ const zip = await belegbilder.createBelegbilderZip(
59
+ [
60
+ { guid, description: 'Rechnung 7', bytes: pdfBytes },
61
+ { guid: photoGuid, description: 'Kassenbon', bytes: jpegBytes, format: 'jpeg' },
62
+ ],
63
+ { createdAt: new Date(), generatingSystem: 'My app' },
64
+ );
55
65
  // served as belegbilder.BELEGBILDER_CONTENT_TYPE under belegbilder.belegbilderFilename('2026-03')
56
66
  ```
57
67
 
68
+ The archive is what DATEV's XML-Schnittstelle online takes as a Document-Package (developer.datev.de, "DATEV XML interface online", format specification and XSD files, read 2026-09-28):
69
+
70
+ - `document.xml` in the namespace `http://xml.datev.de/bedi/tps/document/v06.0`, `version="6.0"`, with the header's `date` (`createdAt`'s UTC clock time without a zone, as the Buchungsstapel header writes its own; the package reads no clock) and, when given, `generatingSystem` (up to 40 characters).
71
+ - One `<document guid="…">` per entry, its `description` cut to DATEV's 40 characters (none when empty), and one `<extension xsi:type="File" name="<GUID>.<extension>"/>`. The GUID is what DATEV Unternehmen online files the document under and what the Beleglink names; it must have the RFC 4122 form `8-4-4-4-12` hexadecimal digits.
72
+ - `format` is `pdf` (the default), `jpeg`, `png`, `gif`, `tiff` or `bmp` (`BELEGBILD_FORMATS`), written as `.pdf`, `.jpg`, `.png`, `.gif`, `.tif`, `.bmp` (`belegbildFileName`). DATEV takes these among its File types (PDF, XML, TIF, TIFF, BMP, CSV, DOC, DOCX, GIF, JPEG, JPG, ODS, ODT, PKCS7, PNG, RTF, TXT, XLS, XLSX); it does not take WebP, AVIF or HEIC, so a picture in one of them must be converted by the caller first.
73
+ - One file per GUID: a document listed twice (the same document linked by two bookings: the same GUID, format and bytes) is in the archive once, under its first description.
74
+ - Characters XML 1.0 does not allow (C0 controls other than tab, line feed and carriage return, a surrogate not in a pair, U+FFFE, U+FFFF) are left out of a description and of `generatingSystem` before they are cut.
75
+ - Refused with `CompatibilityError` `invalid_documents`: no document, more than 4999 (the schema's bound), a GUID of another form, a format outside `BELEGBILD_FORMATS`, one GUID for two different files (another format, or other bytes).
76
+
58
77
  ## Windows-1252
59
78
 
60
79
  `encodeWindows1252(text)` follows the WHATWG Encoding Standard. Bytes 0x00–0x7F and 0xA0–0xFF are the code points of the same number, and 0x80–0x9F carry the 27 characters € ‚ ƒ „ … † ‡ ˆ ‰ Š ‹ Œ Ž ‘ ’ “ ” • – — ˜ ™ š › œ ž Ÿ.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/compatibility',
6
- version: '1.1.0',
6
+ version: '3.0.0',
7
7
  description: 'Exchange formats of third-party accounting systems: the EXTF Buchungsstapel in Windows-1252 on the rows of @fin.cx/skr, and the document-image archive (document.xml and ZIP)'
8
8
  }
@@ -1,21 +1,72 @@
1
1
  import * as plugins from './plugins.js';
2
+ import { CompatibilityError } from '../error.js';
2
3
 
3
- /** A document image of the archive, linked to its bookings through the GUID in their Beleglink (field 20). */
4
+ /*
5
+ * The document-image archive is a DATEV Document-Package of the DATEV
6
+ * XML-Schnittstelle online: a ZIP of `document.xml`, written after DATEV's
7
+ * schema `Document_v060.xsd` (namespace `http://xml.datev.de/bedi/tps/document/v06.0`),
8
+ * and the files it names. Source: developer.datev.de, "DATEV XML interface
9
+ * online", format specification and XSD files (read 2026-09-28).
10
+ * - Each `<document>` carries its GUID (`guid`, RFC 4122, `[0-9A-F]{8}-…-[0-9A-F]{12}`),
11
+ * under which DATEV Unternehmen online files the document; a booking's
12
+ * Beleglink (EXTF field 20, `BEDI "GUID"`) names it.
13
+ * - Its file is an `<extension xsi:type="File" name="…"/>`. DATEV takes as a
14
+ * File: PDF, XML, TIF, TIFF, BMP, CSV, DOC, DOCX, GIF, JPEG, JPG, ODS, ODT,
15
+ * PKCS7, PNG, RTF, TXT, XLS, XLSX; file names in ASCII (the GUID is).
16
+ * - The header's `date` is a date-time without a zone (`.+T[^Z+-]+`);
17
+ * a document's `description` has 1 to 40 characters.
18
+ */
19
+
20
+ /** The formats a document image can have here: PDF and the picture formats DATEV takes as a File (not WebP, AVIF or HEIC). */
21
+ export type TBelegbildFormat = 'pdf' | 'jpeg' | 'png' | 'gif' | 'tiff' | 'bmp';
22
+
23
+ export const BELEGBILD_FORMATS: readonly TBelegbildFormat[] = ['pdf', 'jpeg', 'png', 'gif', 'tiff', 'bmp'];
24
+
25
+ const EXTENSIONS: Readonly<Record<TBelegbildFormat, string>> = {
26
+ pdf: 'pdf',
27
+ jpeg: 'jpg',
28
+ png: 'png',
29
+ gif: 'gif',
30
+ tiff: 'tif',
31
+ bmp: 'bmp',
32
+ };
33
+
34
+ /** A document of the archive, linked to its bookings through the GUID in their Beleglink (field 20). */
4
35
  export interface IBelegbilderDocument {
36
+ /** RFC 4122, `8-4-4-4-12` hexadecimal digits, as DATEV takes it */
5
37
  guid: string;
38
+ /** cut to DATEV's 40 characters; empty, none is written */
6
39
  description: string;
40
+ /** what the file is; a PDF when absent */
41
+ format?: TBelegbildFormat;
7
42
  }
8
43
 
9
44
  export interface IBelegbilderEntry extends IBelegbilderDocument {
10
- /** the document as PDF */
45
+ /** the file as it is: the PDF, or the picture in `format` */
11
46
  bytes: Uint8Array;
12
47
  }
13
48
 
49
+ /** What the archive says of itself. */
50
+ export interface IBelegbilderArchive {
51
+ /** when the archive was written (DATEV's header `date`, required); its UTC clock time is written, as the Buchungsstapel header writes its own. The package reads no clock. */
52
+ createdAt: Date;
53
+ /** the software that wrote it (DATEV's `generatingSystem`, up to 40 characters) */
54
+ generatingSystem?: string;
55
+ }
56
+
14
57
  export const BELEGBILDER_CONTENT_TYPE = 'application/zip';
15
58
 
16
59
  /** `DATEV_Belege_<label>.zip`, for example `DATEV_Belege_2026-03.zip`. */
17
60
  export const belegbilderFilename = (labelArg: string): string => `DATEV_Belege_${labelArg}.zip`;
18
61
 
62
+ /** The name of a document's file in the archive: its GUID and the extension of its format, for example `<GUID>.jpg`. */
63
+ export const belegbildFileName = (guidArg: string, formatArg: TBelegbildFormat = 'pdf'): string => `${guidArg}.${EXTENSIONS[formatArg]}`;
64
+
65
+ const GUID_PATTERN = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/u;
66
+ /** DATEV's `document` of `content`: 1 to 4999 (`Document_v060.xsd`) */
67
+ const MAX_DOCUMENTS = 4999;
68
+ const MAX_DESCRIPTION = 40;
69
+
19
70
  const escapeXml = (valueArg: string): string =>
20
71
  valueArg
21
72
  .replace(/&/gu, '&amp;')
@@ -25,18 +76,98 @@ const escapeXml = (valueArg: string): string =>
25
76
  .replace(/'/gu, '&apos;');
26
77
 
27
78
  /**
28
- * The manifest of the archive, `document.xml`: one `<document>` per PDF,
29
- * named by its GUID. UTF-8, unlike the Buchungsstapel.
79
+ * The characters XML 1.0 does not allow (its production `Char`): C0 controls
80
+ * other than tab, line feed and carriage return, a surrogate not in a pair,
81
+ * U+FFFE and U+FFFF. Left out of a text before it is cut.
82
+ */
83
+ const XML_FORBIDDEN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/gu;
84
+
85
+ /** The characters XML allows, at most `maxArg` of them (characters, not UTF-16 units), trimmed. */
86
+ const clip = (valueArg: string, maxArg: number): string => [...valueArg.replace(XML_FORBIDDEN, '').trim()].slice(0, maxArg).join('').trim();
87
+
88
+ const pad = (valueArg: number): string => String(valueArg).padStart(2, '0');
89
+
90
+ /** `YYYY-MM-DDThh:mm:ss` of the date's UTC clock time, without a zone, as DATEV's `p10048` takes it. */
91
+ const headerDate = (dateArg: Date): string =>
92
+ `${dateArg.getUTCFullYear()}-${pad(dateArg.getUTCMonth() + 1)}-${pad(dateArg.getUTCDate())}`
93
+ + `T${pad(dateArg.getUTCHours())}:${pad(dateArg.getUTCMinutes())}:${pad(dateArg.getUTCSeconds())}`;
94
+
95
+ /**
96
+ * The documents once each: a GUID names one file, so a document linked by
97
+ * two bookings is listed once, under its first description; a GUID that names
98
+ * two different files (another format, or other bytes when `sameFileArg` can
99
+ * tell) is refused.
100
+ */
101
+ const distinctDocuments = <TDocument extends IBelegbilderDocument>(
102
+ documentsArg: readonly TDocument[],
103
+ sameFileArg: (leftArg: TDocument, rightArg: TDocument) => boolean = () => true,
104
+ ): TDocument[] => {
105
+ const firstOf = new Map<string, { document: TDocument; index: number }>();
106
+ const distinct: TDocument[] = [];
107
+ documentsArg.forEach((documentArg, indexArg) => {
108
+ const key = documentArg.guid.toUpperCase();
109
+ const first = firstOf.get(key);
110
+ if (!first) {
111
+ firstOf.set(key, { document: documentArg, index: indexArg });
112
+ distinct.push(documentArg);
113
+ return;
114
+ }
115
+ if ((first.document.format ?? 'pdf') !== (documentArg.format ?? 'pdf') || !sameFileArg(first.document, documentArg)) {
116
+ throw new CompatibilityError('invalid_documents', `The GUID "${documentArg.guid}" names two different files (documents ${first.index + 1} and ${indexArg + 1}).`);
117
+ }
118
+ });
119
+ return distinct;
120
+ };
121
+
122
+ const sameBytes = (leftArg: IBelegbilderEntry, rightArg: IBelegbilderEntry): boolean =>
123
+ leftArg.bytes.length === rightArg.bytes.length && leftArg.bytes.every((byteArg, indexArg) => byteArg === rightArg.bytes[indexArg]);
124
+
125
+ /** Refuses what DATEV does not take: no document or too many, a GUID not of the RFC 4122 form, a format it does not take. */
126
+ const checkDocuments = (documentsArg: readonly IBelegbilderDocument[]): void => {
127
+ if (documentsArg.length === 0) {
128
+ throw new CompatibilityError('invalid_documents', 'A document package holds at least one document.');
129
+ }
130
+ if (documentsArg.length > MAX_DOCUMENTS) {
131
+ throw new CompatibilityError('invalid_documents', `A document package holds at most ${MAX_DOCUMENTS} documents, not ${documentsArg.length}.`);
132
+ }
133
+ documentsArg.forEach((documentArg, indexArg) => {
134
+ if (!GUID_PATTERN.test(documentArg.guid)) {
135
+ throw new CompatibilityError('invalid_documents', `The GUID "${documentArg.guid}" of document ${indexArg + 1} is not the 8-4-4-4-12 hexadecimal form DATEV takes.`);
136
+ }
137
+ if (documentArg.format !== undefined && !BELEGBILD_FORMATS.includes(documentArg.format)) {
138
+ throw new CompatibilityError('invalid_documents', `Document ${indexArg + 1} is in the format "${String(documentArg.format)}", which a DATEV Document-Package does not take.`);
139
+ }
140
+ });
141
+ };
142
+
143
+ /**
144
+ * The manifest of the archive, `document.xml` after DATEV's `Document_v060.xsd`:
145
+ * one `<document>` per file, named by its GUID, with its description and one
146
+ * `File` extension. A GUID listed twice for the same format is one document;
147
+ * characters XML 1.0 does not allow are left out of the texts. UTF-8, unlike
148
+ * the Buchungsstapel. Refuses, as a `CompatibilityError` `invalid_documents`,
149
+ * what DATEV would refuse.
30
150
  */
31
- export const buildDocumentXml = (documentsArg: readonly IBelegbilderDocument[]): string => {
32
- const lines = ['<?xml version="1.0" encoding="UTF-8"?>', '<archive>', ' <content>'];
33
- for (const document of documentsArg) {
151
+ export const buildDocumentXml = (documentsArg: readonly IBelegbilderDocument[], archiveArg: IBelegbilderArchive): string => {
152
+ checkDocuments(documentsArg);
153
+ const documents = distinctDocuments(documentsArg);
154
+ const generatingSystem = archiveArg.generatingSystem === undefined ? '' : clip(archiveArg.generatingSystem, MAX_DESCRIPTION);
155
+ const lines = [
156
+ '<?xml version="1.0" encoding="UTF-8"?>',
157
+ '<archive xmlns="http://xml.datev.de/bedi/tps/document/v06.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
158
+ + ' xsi:schemaLocation="http://xml.datev.de/bedi/tps/document/v06.0 Document_v060.xsd" version="6.0"'
159
+ + `${generatingSystem ? ` generatingSystem="${escapeXml(generatingSystem)}"` : ''}>`,
160
+ ' <header>',
161
+ ` <date>${headerDate(archiveArg.createdAt)}</date>`,
162
+ ' </header>',
163
+ ' <content>',
164
+ ];
165
+ for (const document of documents) {
166
+ const description = clip(document.description, MAX_DESCRIPTION);
34
167
  lines.push(
35
168
  ` <document guid="${escapeXml(document.guid)}">`,
36
- ' <extension>PDF</extension>',
37
- ` <filename>${escapeXml(document.guid)}.pdf</filename>`,
38
- ' <beleg_type>1</beleg_type>',
39
- ...(document.description ? [` <description>${escapeXml(document.description)}</description>`] : []),
169
+ ...(description ? [` <description>${escapeXml(description)}</description>`] : []),
170
+ ` <extension xsi:type="File" name="${escapeXml(belegbildFileName(document.guid, document.format))}"/>`,
40
171
  ' </document>',
41
172
  );
42
173
  }
@@ -44,11 +175,15 @@ export const buildDocumentXml = (documentsArg: readonly IBelegbilderDocument[]):
44
175
  return lines.join('\n');
45
176
  };
46
177
 
47
- /** `document.xml` and one `<GUID>.pdf` per document, as one ZIP. */
48
- export const createBelegbilderZip = async (entriesArg: readonly IBelegbilderEntry[]): Promise<Uint8Array> => {
178
+ /** `document.xml` and each document's file under its name (`belegbildFileName`), as one ZIP: a DATEV Document-Package. */
179
+ export const createBelegbilderZip = async (entriesArg: readonly IBelegbilderEntry[], archiveArg: IBelegbilderArchive): Promise<Uint8Array> => {
180
+ checkDocuments(entriesArg);
181
+ // one file per GUID: the same document linked by two bookings once, one GUID for two different files refused
182
+ const entries = distinctDocuments(entriesArg, sameBytes);
183
+ const manifest = buildDocumentXml(entries, archiveArg);
49
184
  const zipTools = new plugins.smartarchiveZip.ZipTools();
50
185
  return zipTools.createZip([
51
- { archivePath: 'document.xml', content: buildDocumentXml(entriesArg) },
52
- ...entriesArg.map((entryArg) => ({ archivePath: `${entryArg.guid}.pdf`, content: entryArg.bytes })),
186
+ { archivePath: 'document.xml', content: manifest },
187
+ ...entries.map((entryArg) => ({ archivePath: belegbildFileName(entryArg.guid, entryArg.format), content: entryArg.bytes })),
53
188
  ]);
54
189
  };
package/ts/error.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Why an exchange file could not be written:
3
3
  * - `invalid_bookings`: a draft the account policy refuses; the message names it;
4
- * - `export_failed`: the conversion into rows failed.
4
+ * - `export_failed`: the conversion into rows failed;
5
+ * - `invalid_documents`: a document of a document-image archive DATEV does not take (its GUID, its format), or none.
5
6
  */
6
- export type TCompatibilityErrorCode = 'invalid_bookings' | 'export_failed';
7
+ export type TCompatibilityErrorCode = 'invalid_bookings' | 'export_failed' | 'invalid_documents';
7
8
 
8
9
  export class CompatibilityError extends Error {
9
10
  public readonly code: TCompatibilityErrorCode;
package/ts/extf/index.ts CHANGED
@@ -77,7 +77,9 @@ export const differingBookingCount = (leftArg: readonly IExportBooking[], rightA
77
77
 
78
78
  /**
79
79
  * Checks every draft against the account policy and turns the drafts into
80
- * Buchungsstapel rows (`@fin.cx/skr` `journalDraftsToDatevRows`, not fixed).
80
+ * Buchungsstapel rows (`@fin.cx/skr` `journalDraftsToDatevRows` under that
81
+ * policy, not fixed): its Automatikkonten and VAT accounts decide which VAT
82
+ * lines fold into their base.
81
83
  * A draft the policy refuses throws `invalid_bookings`, naming the draft; a
82
84
  * failure of the conversion throws `export_failed`.
83
85
  */
@@ -89,7 +91,8 @@ export const datevRowsOf = (draftsArg: readonly plugins.skrCore.IJournalDraft[],
89
91
  }
90
92
  }
91
93
  try {
92
- return plugins.skrCore.journalDraftsToDatevRows([...draftsArg], { festschreibung: false });
94
+ // the rows consolidate under the same policy the drafts are checked against, not the chart's default one
95
+ return plugins.skrCore.journalDraftsToDatevRows([...draftsArg], { festschreibung: false, policy: policyArg });
93
96
  } catch (error) {
94
97
  throw new CompatibilityError('export_failed', error instanceof Error ? error.message : String(error));
95
98
  }
@@ -105,6 +108,68 @@ export interface IBuchungsstapelOptions {
105
108
  filename: string;
106
109
  }
107
110
 
111
+ /**
112
+ * A character a Buchungsstapel cannot hold in Windows-1252, written as `?`,
113
+ * with where it stands: `offset` in the whole text, and for a character of a
114
+ * booking row the index of that row in `rows` (from 0) and the number of its
115
+ * DATEV field (from 1, 14 is the Buchungstext). A character of the header
116
+ * line has neither.
117
+ */
118
+ export interface IBuchungsstapelReplacement extends IReplacedCharacter {
119
+ row?: number;
120
+ field?: number;
121
+ }
122
+
123
+ /** A character of one booking row the file cannot hold, as `replacedCharactersOfRows` reports it. */
124
+ export interface IRowReplacement {
125
+ /** the index of the row in the rows given, from 0 */
126
+ row: number;
127
+ /** the number of the DATEV field it stands in, from 1 (14 is the Buchungstext) */
128
+ field: number;
129
+ character: string;
130
+ }
131
+
132
+ /**
133
+ * Where the characters of one row stand in its line: the line is the fields
134
+ * joined with `;` (`@fin.cx/skr` `datevRowFields`), so field n begins after
135
+ * the lengths of the fields before it and one separator each.
136
+ */
137
+ const fieldStartsOf = (fieldsArg: readonly string[]): number[] => {
138
+ const starts: number[] = [];
139
+ let offset = 0;
140
+ for (const field of fieldsArg) {
141
+ starts.push(offset);
142
+ offset += field.length + 1;
143
+ }
144
+ return starts;
145
+ };
146
+
147
+ const fieldAt = (startsArg: readonly number[], offsetArg: number): number => {
148
+ let field = 0;
149
+ while (field + 1 < startsArg.length && startsArg[field + 1] <= offsetArg) {
150
+ field += 1;
151
+ }
152
+ return field + 1;
153
+ };
154
+
155
+ /**
156
+ * The characters of each row that a Buchungsstapel in Windows-1252 cannot
157
+ * hold, in row order, without writing a file: the rows' lines as the writer
158
+ * writes them (`@fin.cx/skr` `datevRowFields`), so a character a field is cut
159
+ * off before is not named. A row the writer refuses throws as it does.
160
+ */
161
+ export const replacedCharactersOfRows = (rowsArg: readonly IDatevBuchungsstapelRow[]): IRowReplacement[] => {
162
+ const replacements: IRowReplacement[] = [];
163
+ rowsArg.forEach((rowArg, rowIndex) => {
164
+ const fields = plugins.skrCore.datevRowFields(rowArg);
165
+ const starts = fieldStartsOf(fields);
166
+ for (const replaced of encodeWindows1252(fields.join(';')).replaced) {
167
+ replacements.push({ row: rowIndex, field: fieldAt(starts, replaced.offset), character: replaced.character });
168
+ }
169
+ });
170
+ return replacements;
171
+ };
172
+
108
173
  export interface IBuchungsstapelFile {
109
174
  filename: string;
110
175
  /** the file as text, lines ending in CRLF */
@@ -113,8 +178,8 @@ export interface IBuchungsstapelFile {
113
178
  bytes: Uint8Array;
114
179
  rows: IDatevBuchungsstapelRow[];
115
180
  bookings: IExportBooking[];
116
- /** the characters Windows-1252 cannot write, each written as `?` */
117
- replacedCharacters: IReplacedCharacter[];
181
+ /** the characters Windows-1252 cannot write, each written as `?`, with the row and field it stands in */
182
+ replacedCharacters: IBuchungsstapelReplacement[];
118
183
  }
119
184
 
120
185
  /** Writes a Buchungsstapel: the rows of the drafts, the header and the caption row, encoded in Windows-1252. */
@@ -122,5 +187,28 @@ export const writeBuchungsstapel = (optionsArg: IBuchungsstapelOptions): IBuchun
122
187
  const rows = datevRowsOf(optionsArg.drafts, optionsArg.policy);
123
188
  const { csv } = plugins.skrCore.writeExtfBuchungsstapel(rows, optionsArg.header);
124
189
  const { bytes, replaced } = encodeWindows1252(csv);
125
- return { filename: optionsArg.filename, csv, bytes, rows, bookings: bookingsOfRows(rows), replacedCharacters: replaced };
190
+ // the file is the header line, the caption line and one line per row, each ending in CRLF (`writeExtfBuchungsstapel`)
191
+ const rowLines = rows.map((rowArg) => plugins.skrCore.datevRowFields(rowArg));
192
+ const [headerLine = '', captionLine = ''] = csv.split('\r\n', 2);
193
+ const rowsStart = headerLine.length + 2 + captionLine.length + 2;
194
+ const lineStarts: number[] = [];
195
+ let offset = rowsStart;
196
+ for (const fields of rowLines) {
197
+ lineStarts.push(offset);
198
+ offset += fields.join(';').length + 2;
199
+ }
200
+ if (offset !== csv.length || rowLines.some((fieldsArg, indexArg) => csv.slice(lineStarts[indexArg], lineStarts[indexArg] + fieldsArg.join(';').length) !== fieldsArg.join(';'))) {
201
+ throw new CompatibilityError('export_failed', 'the rows of the Buchungsstapel are not the lines @fin.cx/skr writes for them');
202
+ }
203
+ const replacedCharacters = replaced.map((replacedArg): IBuchungsstapelReplacement => {
204
+ if (replacedArg.offset < rowsStart) {
205
+ return { ...replacedArg };
206
+ }
207
+ let row = 0;
208
+ while (row + 1 < lineStarts.length && lineStarts[row + 1] <= replacedArg.offset) {
209
+ row += 1;
210
+ }
211
+ return { ...replacedArg, row, field: fieldAt(fieldStartsOf(rowLines[row]), replacedArg.offset - lineStarts[row]) };
212
+ });
213
+ return { filename: optionsArg.filename, csv, bytes, rows, bookings: bookingsOfRows(rows), replacedCharacters };
126
214
  };