@fin.cx/compatibility 2.0.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/belegbilder/index.d.ts +27 -7
- package/dist_ts/belegbilder/index.js +101 -11
- package/dist_ts/error.d.ts +3 -2
- package/dist_ts/error.js +1 -1
- package/package.json +1 -1
- package/readme.md +18 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/belegbilder/index.ts +150 -15
- package/ts/error.ts +3 -2
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@fin.cx/compatibility',
|
|
6
|
-
version: '
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
15
|
-
* named by its GUID
|
|
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
|
|
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, '&')
|
|
7
23
|
.replace(/</gu, '<')
|
|
@@ -9,23 +25,97 @@ const escapeXml = (valueArg) => valueArg
|
|
|
9
25
|
.replace(/"/gu, '"')
|
|
10
26
|
.replace(/'/gu, ''');
|
|
11
27
|
/**
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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:
|
|
28
|
-
...
|
|
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,
|
|
121
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9iZWxlZ2JpbGRlci9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUN4QyxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFxQmpELE1BQU0sQ0FBQyxNQUFNLGlCQUFpQixHQUFnQyxDQUFDLEtBQUssRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsS0FBSyxDQUFDLENBQUM7QUFFM0csTUFBTSxVQUFVLEdBQStDO0lBQzdELEdBQUcsRUFBRSxLQUFLO0lBQ1YsSUFBSSxFQUFFLEtBQUs7SUFDWCxHQUFHLEVBQUUsS0FBSztJQUNWLEdBQUcsRUFBRSxLQUFLO0lBQ1YsSUFBSSxFQUFFLEtBQUs7SUFDWCxHQUFHLEVBQUUsS0FBSztDQUNYLENBQUM7QUF5QkYsTUFBTSxDQUFDLE1BQU0sd0JBQXdCLEdBQUcsaUJBQWlCLENBQUM7QUFFMUQsMEVBQTBFO0FBQzFFLE1BQU0sQ0FBQyxNQUFNLG1CQUFtQixHQUFHLENBQUMsUUFBZ0IsRUFBVSxFQUFFLENBQUMsZ0JBQWdCLFFBQVEsTUFBTSxDQUFDO0FBRWhHLHdIQUF3SDtBQUN4SCxNQUFNLENBQUMsTUFBTSxpQkFBaUIsR0FBRyxDQUFDLE9BQWUsRUFBRSxZQUE4QixLQUFLLEVBQVUsRUFBRSxDQUFDLEdBQUcsT0FBTyxJQUFJLFVBQVUsQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFDO0FBRXpJLE1BQU0sWUFBWSxHQUFHLGdGQUFnRixDQUFDO0FBQ3RHLHVFQUF1RTtBQUN2RSxNQUFNLGFBQWEsR0FBRyxJQUFJLENBQUM7QUFDM0IsTUFBTSxlQUFlLEdBQUcsRUFBRSxDQUFDO0FBRTNCLE1BQU0sU0FBUyxHQUFHLENBQUMsUUFBZ0IsRUFBVSxFQUFFLENBQzdDLFFBQVE7S0FDTCxPQUFPLENBQUMsS0FB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
|
package/dist_ts/error.d.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
|
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,
|
|
8
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9lcnJvci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFRQSxNQUFNLE9BQU8sa0JBQW1CLFNBQVEsS0FBSztJQUczQyxZQUFZLE9BQWdDLEVBQUUsVUFBa0I7UUFDOUQsS0FBSyxDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBQ2xCLElBQUksQ0FBQyxJQUFJLEdBQUcsb0JBQW9CLENBQUM7UUFDakMsSUFBSSxDQUFDLElBQUksR0FBRyxPQUFPLENBQUM7SUFDdEIsQ0FBQztDQUNGIn0=
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fin.cx/compatibility",
|
|
3
|
-
"version": "
|
|
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",
|
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`
|
|
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
|
|
|
@@ -49,15 +49,31 @@ file.replacedCharacters; // [{ offset, character: 'ő', row: 1, field: 14 }, …
|
|
|
49
49
|
|
|
50
50
|
- **A draft the account policy refuses** throws `CompatibilityError` with code `invalid_bookings` and a message naming the draft.
|
|
51
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`.
|
|
52
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.
|
|
53
54
|
|
|
54
55
|
```typescript
|
|
55
56
|
import { belegbilder } from '@fin.cx/compatibility';
|
|
56
57
|
|
|
57
|
-
const zip = await belegbilder.createBelegbilderZip(
|
|
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
|
+
);
|
|
58
65
|
// served as belegbilder.BELEGBILDER_CONTENT_TYPE under belegbilder.belegbilderFilename('2026-03')
|
|
59
66
|
```
|
|
60
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
|
+
|
|
61
77
|
## Windows-1252
|
|
62
78
|
|
|
63
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 € ‚ ƒ „ … † ‡ ˆ ‰ Š ‹ Œ Ž ‘ ’ “ ” • – — ˜ ™ š › œ ž Ÿ.
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@fin.cx/compatibility',
|
|
6
|
-
version: '
|
|
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
|
}
|
package/ts/belegbilder/index.ts
CHANGED
|
@@ -1,21 +1,72 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import { CompatibilityError } from '../error.js';
|
|
2
3
|
|
|
3
|
-
|
|
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
|
|
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, '&')
|
|
@@ -25,18 +76,98 @@ const escapeXml = (valueArg: string): string =>
|
|
|
25
76
|
.replace(/'/gu, ''');
|
|
26
77
|
|
|
27
78
|
/**
|
|
28
|
-
* The
|
|
29
|
-
*
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
` <
|
|
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
|
|
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:
|
|
52
|
-
...
|
|
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;
|