@ekwo-ai/factur-x 0.11.1 → 0.12.1
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/CHANGELOG.md +25 -0
- package/README.md +79 -3
- package/dist/cii-read.d.ts +48 -0
- package/dist/cii-read.d.ts.map +1 -0
- package/dist/cii-read.js +462 -0
- package/dist/cii-read.js.map +1 -0
- package/dist/decimal.d.ts +52 -0
- package/dist/decimal.d.ts.map +1 -0
- package/dist/decimal.js +121 -0
- package/dist/decimal.js.map +1 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/pdf.d.ts +33 -6
- package/dist/pdf.d.ts.map +1 -1
- package/dist/pdf.js +81 -18
- package/dist/pdf.js.map +1 -1
- package/dist/received-checks.d.ts +34 -0
- package/dist/received-checks.d.ts.map +1 -0
- package/dist/received-checks.js +198 -0
- package/dist/received-checks.js.map +1 -0
- package/dist/received.d.ts +321 -0
- package/dist/received.d.ts.map +1 -0
- package/dist/received.js +20 -0
- package/dist/received.js.map +1 -0
- package/dist/xml.d.ts +43 -0
- package/dist/xml.d.ts.map +1 -0
- package/dist/xml.js +274 -0
- package/dist/xml.js.map +1 -0
- package/dist/xmp.d.ts +15 -1
- package/dist/xmp.d.ts.map +1 -1
- package/dist/xmp.js +14 -4
- package/dist/xmp.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,31 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
### Added
|
|
10
|
+
- **Reading a received invoice.** `readCii()` reads a CII D16B
|
|
11
|
+
`CrossIndustryInvoice` of any profile — MINIMUM, BASIC WL, BASIC, EN 16931,
|
|
12
|
+
EXTENDED, ZUGFeRD 2.x spellings included — and says which profile it
|
|
13
|
+
declares. `readFacturX()` of `@ekwo-ai/factur-x/pdf` reads it out of the
|
|
14
|
+
PDF. The result is the `ReceivedInvoice` that `readUbl()` of
|
|
15
|
+
`@ekwo-ai/peppol-ubl` returns, from byte-for-byte copies of the same files:
|
|
16
|
+
figures as the decimal text the file wrote, the EN 16931 arithmetic reported
|
|
17
|
+
in `violations`, an `InvoiceFileError` with a code for what is not an
|
|
18
|
+
invoice.
|
|
19
|
+
- `extractFacturX()` also finds `xrechnung.xml`, the name ZUGFeRD gives the
|
|
20
|
+
XML of its XRECHNUNG profile.
|
|
21
|
+
- `buildXmpMetadata()` takes `author`, `subject`, `keywords` and
|
|
22
|
+
`creationDate`, written as `dc:creator`, `dc:description`, `pdf:Keywords`
|
|
23
|
+
and `xmp:CreateDate`. Without them it writes what it wrote before.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **The XMP of `embedFacturX()` now says what the information dictionary
|
|
27
|
+
says.** It wrote the tool as `dc:creator` beside an `Author` naming the
|
|
28
|
+
seller, the time of embedding as `xmp:CreateDate` beside the PDF's own
|
|
29
|
+
`CreationDate`, and nothing for `Subject` or `Keywords`; PDF/A wants each
|
|
30
|
+
entry of the dictionary and its XMP counterpart to agree. The packet is now
|
|
31
|
+
written from the dictionary of the PDF it is given, after the title, creator,
|
|
32
|
+
producer and dates of the options are set in it.
|
|
33
|
+
|
|
9
34
|
## [0.2.0] — 2026-09-14
|
|
10
35
|
|
|
11
36
|
Released with Ekwo OS `v0.2.0`, the first tagged release of the repository this
|
package/README.md
CHANGED
|
@@ -7,7 +7,9 @@ Factur-X / ZUGFeRD electronic invoices in TypeScript.
|
|
|
7
7
|
`BR-IC`, `BR-G`, `BR-O`). Profiles MINIMUM, BASIC WL, BASIC (default), EN 16931, EXTENDED.
|
|
8
8
|
- **PDF/A-3 embedding.** Attaches the XML as `factur-x.xml` with `AFRelationship /Alternative`
|
|
9
9
|
and writes the XMP packet with the Factur-X extension schema.
|
|
10
|
-
- **Extraction.** Reads `factur-x.xml
|
|
10
|
+
- **Extraction.** Reads `factur-x.xml`, `zugferd-invoice.xml` or `xrechnung.xml` back from any PDF.
|
|
11
|
+
- **Reading a received invoice.** `readCii` and `readFacturX` turn the CII of any profile
|
|
12
|
+
into the same plain result `@ekwo-ai/peppol-ubl` returns from UBL — see [Reading](#reading).
|
|
11
13
|
- **Totals you can trust.** One VAT group per category and rate, VAT computed on the rounded
|
|
12
14
|
basis, prepayments, credit notes referencing the original invoice.
|
|
13
15
|
- Zero dependencies for the XML part; `pdf-lib` only for the PDF part (separate entry point).
|
|
@@ -92,17 +94,91 @@ back to `C62` (unit). See `UNIT_CODES` and `toUnitCode`.
|
|
|
92
94
|
| `buildXmpMetadata`, `CONFORMANCE_LEVELS`, `GUIDELINES`, `DOCUMENT_TYPE_CODES` | Constants of the specification. |
|
|
93
95
|
| `embedFacturX(pdf, xml, { profile?, title?, creator?, producer?, date? })` *(`/pdf`)* | Returns a new PDF with the XML attached and the PDF/A-3 XMP packet. |
|
|
94
96
|
| `extractFacturX(pdf)` *(`/pdf`)* | `{ filename, xml }` or `null`. |
|
|
97
|
+
| `readCii(xml, { maxBytes?, maxDepth?, maxElements? })` | `{ invoice, violations, profile }`, see [Reading](#reading). |
|
|
98
|
+
| `readFacturX(pdf, options?)` *(`/pdf`)* | The same, read out of the PDF, with the `filename` of the attachment. |
|
|
99
|
+
| `InvoiceFileError`, `profileOf`, `checkReceived`, `isValidIban`, `CREDIT_NOTE_TYPE_CODES` | What reading refuses, and the helpers it uses. |
|
|
95
100
|
|
|
96
101
|
## Scope and limitations
|
|
97
102
|
|
|
98
103
|
- Allowances and charges (BG-20, BG-21, BG-27, BG-28), gross prices with discounts and
|
|
99
104
|
multiple deliveries are not modelled yet.
|
|
100
|
-
- The PDF part embeds the data
|
|
101
|
-
|
|
105
|
+
- The PDF part embeds the data, declares PDF/A-3 and writes an XMP packet that agrees with
|
|
106
|
+
the document information dictionary; it does not convert the visual PDF to PDF/A (fonts,
|
|
107
|
+
colour profiles, output intent). A PDF rendered by
|
|
108
|
+
[`@ekwo-ai/invoice-pdf`](../invoice-pdf/README.md#pdfa-3) has them, and veraPDF finds no
|
|
109
|
+
failed rule of PDF/A-3b in it once embedded; validate any other PDF with the tool of your
|
|
102
110
|
platform before going live.
|
|
103
111
|
- No XSD or Schematron validation is performed. Test against a validator such as the FNFE
|
|
104
112
|
Factur-X validator or the Mustang project.
|
|
105
113
|
|
|
114
|
+
## Reading
|
|
115
|
+
|
|
116
|
+
The invoice a supplier sends you as Factur-X or ZUGFeRD, turned into a plain object a
|
|
117
|
+
purchase draft can be made from.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { readCii, InvoiceFileError } from '@ekwo-ai/factur-x';
|
|
121
|
+
import { readFacturX } from '@ekwo-ai/factur-x/pdf';
|
|
122
|
+
|
|
123
|
+
const { invoice, violations, profile, filename } = await readFacturX(pdfBytes);
|
|
124
|
+
profile; // 'minimum' | 'basic-wl' | 'basic' | 'en16931' | 'extended' | null
|
|
125
|
+
invoice.kind; // 'invoice' | 'credit_note', from the type code
|
|
126
|
+
invoice.totals.payable; // '648.40', a decimal as text, never a number
|
|
127
|
+
|
|
128
|
+
const same = readCii(xmlBytesOrText); // the XML alone, without pdf-lib
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- `readCii(input: string | Uint8Array, options?: ReadOptions): ReceivedCiiFile` reads a
|
|
132
|
+
CII D16B `CrossIndustryInvoice` — Factur-X 1.0, ZUGFeRD 2.x, an XRechnung in CII — and
|
|
133
|
+
returns `{ invoice, violations, profile }`.
|
|
134
|
+
- `readFacturX(pdf: Uint8Array | ArrayBuffer, options?: ReadOptions): Promise<ReceivedFacturX>`
|
|
135
|
+
*(`/pdf`)* finds the XML attached to the PDF and reads it with `readCii`; the result
|
|
136
|
+
adds the `filename` it was attached under.
|
|
137
|
+
- `profileOf(guideline)` is the profile a guideline identifier (BT-24) declares. An
|
|
138
|
+
EN 16931 CIUS (`urn:cen.eu:en16931:2017#compliant#…`, XRechnung for one) reads as
|
|
139
|
+
`en16931`; an identifier this package does not know is `null`, and
|
|
140
|
+
`invoice.customizationId` keeps what the file wrote.
|
|
141
|
+
|
|
142
|
+
**The shape is the UBL reader's.** `invoice` is a `ReceivedInvoice`, the type
|
|
143
|
+
`readUbl` of `@ekwo-ai/peppol-ubl` returns: each field names the EN 16931 business term it
|
|
144
|
+
holds, and a business term is in the same field whichever syntax carried it — the test
|
|
145
|
+
suite reads one invoice written in both syntaxes and gets the same object from both. The
|
|
146
|
+
two bricks do not depend on each other; they hold byte-for-byte copies of the files that
|
|
147
|
+
define the shape, the checks, the XML reader and the decimals, and a test keeps them
|
|
148
|
+
identical. `syntax` says which one was read.
|
|
149
|
+
|
|
150
|
+
**Nothing is recomputed, nothing is defaulted.** A figure is the text the file wrote,
|
|
151
|
+
checked to be a decimal; an absent element is `null` — a MINIMUM or BASIC WL file has no
|
|
152
|
+
line and no line total, and comes back so. The arithmetic of EN 16931 (BR-CO-10 to
|
|
153
|
+
BR-CO-17) is checked on exact decimals and reported in `violations`. CII-specific
|
|
154
|
+
findings are there too: `invalid_iban` (an `IBANID` whose check digits fail, still
|
|
155
|
+
returned as written), `invalid_attachment`, `unsupported_date_format` (a date in a
|
|
156
|
+
format other than 102, returned as `null`). Where CII writes something once for the
|
|
157
|
+
document that UBL writes per means of payment — the payment reference (BT-83) and the
|
|
158
|
+
direct debit mandate (BT-89) — it is returned on every means of payment.
|
|
159
|
+
|
|
160
|
+
**What is not an invoice throws an `InvoiceFileError`** with a `code`: the codes of the
|
|
161
|
+
UBL reader (`malformed_xml`, `unsupported_encoding`, `doctype_forbidden`,
|
|
162
|
+
`undefined_entity`, `too_large`, `too_deep`, `too_many_elements`, `not_an_invoice`,
|
|
163
|
+
`missing_element`, `invalid_value`), plus `not_a_pdf` and `no_embedded_invoice` from
|
|
164
|
+
`readFacturX`. A line is required except in the MINIMUM and BASIC WL profiles. No raw
|
|
165
|
+
error of the XML parser or of `pdf-lib` escapes. The XML is read by a strict reader of
|
|
166
|
+
the package's own: no DOCTYPE and so no entity expansion, UTF-8 only, size, depth and
|
|
167
|
+
element limits.
|
|
168
|
+
|
|
169
|
+
**Limits.**
|
|
170
|
+
|
|
171
|
+
- The PDF is opened with `pdf-lib`, already the dependency of the `/pdf` entry point:
|
|
172
|
+
the attachment is found in the `EmbeddedFiles` name tree of the catalogue, under one
|
|
173
|
+
of the three names above. An attachment reachable only from a page annotation, an
|
|
174
|
+
encrypted PDF, and a stream in a filter `pdf-lib` does not decode are not read: they
|
|
175
|
+
are refused with one of the codes above, never read wrongly. The XMP metadata is not read: the profile
|
|
176
|
+
comes from the XML.
|
|
177
|
+
- ZUGFeRD 1.0 (`CrossIndustryDocument`, another namespace) is refused as
|
|
178
|
+
`not_an_invoice`.
|
|
179
|
+
- Elements of EXTENDED beyond EN 16931 (several deliveries, line sub-structures…) are not
|
|
180
|
+
read. Nothing is validated against the CII schema or a Schematron.
|
|
181
|
+
|
|
106
182
|
## Development
|
|
107
183
|
|
|
108
184
|
```sh
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A received CII invoice or credit note, read.
|
|
3
|
+
*
|
|
4
|
+
* The XML of a Factur-X or ZUGFeRD file — UN/CEFACT Cross Industry Invoice
|
|
5
|
+
* D16B — turned into the plain shape of `received.ts`, which the UBL reader of
|
|
6
|
+
* the Peppol brick returns too: a business term is in the same field whichever
|
|
7
|
+
* syntax carried it. The reader follows the CII binding of EN 16931 (which
|
|
8
|
+
* business term lives in which element) and reads every profile of Factur-X
|
|
9
|
+
* 1.0 and ZUGFeRD 2.x, from MINIMUM to EXTENDED; `profile` says which one the
|
|
10
|
+
* file declares, and what a lighter profile does not carry comes back null or
|
|
11
|
+
* empty, never invented. EXTENDED elements beyond EN 16931 are not read.
|
|
12
|
+
*
|
|
13
|
+
* It reads what the file says and judges only the arithmetic (see
|
|
14
|
+
* `received-checks.ts`). A file that is not an invoice at all — not XML, not
|
|
15
|
+
* CII, no number, an amount that is not one — is an {@link InvoiceFileError},
|
|
16
|
+
* never a raw error of the parser.
|
|
17
|
+
*/
|
|
18
|
+
import type { ReadOptions, ReceivedInvoiceFile } from './received.js';
|
|
19
|
+
import type { Profile } from './types.js';
|
|
20
|
+
/**
|
|
21
|
+
* The profile a guideline identifier declares. Another specification built on
|
|
22
|
+
* EN 16931 by restriction — a CIUS such as XRechnung, written
|
|
23
|
+
* `urn:cen.eu:en16931:2017#compliant#…` — is EN 16931 for a reader: it holds
|
|
24
|
+
* nothing EN 16931 does not. Anything else is null, and `customizationId`
|
|
25
|
+
* keeps what the file wrote.
|
|
26
|
+
*/
|
|
27
|
+
export declare function profileOf(guideline: string | null): Profile | null;
|
|
28
|
+
/** What {@link readCii} returns: the invoice, what does not add up, and the profile the file declares. */
|
|
29
|
+
export interface ReceivedCiiFile extends ReceivedInvoiceFile {
|
|
30
|
+
/** From the guideline identifier (BT-24); null where it names no profile this reader knows. */
|
|
31
|
+
profile: Profile | null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reads the XML of a Factur-X or ZUGFeRD invoice: a CII D16B
|
|
35
|
+
* `CrossIndustryInvoice`, whatever its profile. To read it out of the PDF,
|
|
36
|
+
* `readFacturX` of `@ekwo-ai/factur-x/pdf`.
|
|
37
|
+
*
|
|
38
|
+
* Throws an {@link InvoiceFileError} when there is no invoice to return: the
|
|
39
|
+
* bytes are not XML or not UTF-8, the document is not CII, or it lacks a
|
|
40
|
+
* number, a type code, a date, a currency or its totals, or holds an amount
|
|
41
|
+
* or a date that is not one. Everything else comes back, with `violations`
|
|
42
|
+
* naming what does not add up.
|
|
43
|
+
*
|
|
44
|
+
* Whether the document is a credit note is said by its type code (BT-3)
|
|
45
|
+
* alone, as CII has a single root for both; the figures stay as written.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readCii(input: string | Uint8Array, options?: ReadOptions): ReceivedCiiFile;
|
|
48
|
+
//# sourceMappingURL=cii-read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cii-read.d.ts","sourceRoot":"","sources":["../src/cii-read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAEV,WAAW,EAOX,mBAAmB,EAMpB,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAkC1C;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,GAAG,IAAI,CAKlE;AAED,0GAA0G;AAC1G,MAAM,WAAW,eAAgB,SAAQ,mBAAmB;IAC1D,+FAA+F;IAC/F,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACzB;AA+PD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,OAAO,GAAE,WAAgB,GAAG,eAAe,CAqK9F"}
|
package/dist/cii-read.js
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A received CII invoice or credit note, read.
|
|
3
|
+
*
|
|
4
|
+
* The XML of a Factur-X or ZUGFeRD file — UN/CEFACT Cross Industry Invoice
|
|
5
|
+
* D16B — turned into the plain shape of `received.ts`, which the UBL reader of
|
|
6
|
+
* the Peppol brick returns too: a business term is in the same field whichever
|
|
7
|
+
* syntax carried it. The reader follows the CII binding of EN 16931 (which
|
|
8
|
+
* business term lives in which element) and reads every profile of Factur-X
|
|
9
|
+
* 1.0 and ZUGFeRD 2.x, from MINIMUM to EXTENDED; `profile` says which one the
|
|
10
|
+
* file declares, and what a lighter profile does not carry comes back null or
|
|
11
|
+
* empty, never invented. EXTENDED elements beyond EN 16931 are not read.
|
|
12
|
+
*
|
|
13
|
+
* It reads what the file says and judges only the arithmetic (see
|
|
14
|
+
* `received-checks.ts`). A file that is not an invoice at all — not XML, not
|
|
15
|
+
* CII, no number, an amount that is not one — is an {@link InvoiceFileError},
|
|
16
|
+
* never a raw error of the parser.
|
|
17
|
+
*/
|
|
18
|
+
import { InvoiceFileError } from './errors.js';
|
|
19
|
+
import { CREDIT_NOTE_TYPE_CODES, checkReceived, decimalText, decodeBase64, isValidIban } from './received-checks.js';
|
|
20
|
+
import { parseXml } from './xml.js';
|
|
21
|
+
const NS = {
|
|
22
|
+
rsm: 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
|
|
23
|
+
ram: 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
|
|
24
|
+
udt: 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100',
|
|
25
|
+
qdt: 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100',
|
|
26
|
+
ublInvoice: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
|
|
27
|
+
ublCreditNote: 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2',
|
|
28
|
+
};
|
|
29
|
+
const CII_NAMESPACES = new Set([NS.rsm, NS.ram, NS.udt, NS.qdt]);
|
|
30
|
+
const DEFAULTS = { maxBytes: 64 * 1024 * 1024, maxDepth: 64, maxElements: 500_000 };
|
|
31
|
+
/**
|
|
32
|
+
* The guideline identifiers (BT-24) of Factur-X 1.0 and ZUGFeRD 2.x, by
|
|
33
|
+
* profile, as the specifications of the two (FNFE-MPE and FeRD) list them.
|
|
34
|
+
* The `urn:zugferd.de:2p0` spellings are those of ZUGFeRD 2.0, written before
|
|
35
|
+
* the two specifications converged on the Factur-X ones.
|
|
36
|
+
*/
|
|
37
|
+
const PROFILE_GUIDELINES = {
|
|
38
|
+
'urn:factur-x.eu:1p0:minimum': 'minimum',
|
|
39
|
+
'urn:zugferd.de:2p0:minimum': 'minimum',
|
|
40
|
+
'urn:factur-x.eu:1p0:basicwl': 'basic-wl',
|
|
41
|
+
'urn:zugferd.de:2p0:basicwl': 'basic-wl',
|
|
42
|
+
'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic': 'basic',
|
|
43
|
+
'urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic': 'basic',
|
|
44
|
+
'urn:cen.eu:en16931:2017': 'en16931',
|
|
45
|
+
'urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended': 'extended',
|
|
46
|
+
'urn:cen.eu:en16931:2017#conformant#urn:zugferd.de:2p0:extended': 'extended',
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The profile a guideline identifier declares. Another specification built on
|
|
50
|
+
* EN 16931 by restriction — a CIUS such as XRechnung, written
|
|
51
|
+
* `urn:cen.eu:en16931:2017#compliant#…` — is EN 16931 for a reader: it holds
|
|
52
|
+
* nothing EN 16931 does not. Anything else is null, and `customizationId`
|
|
53
|
+
* keeps what the file wrote.
|
|
54
|
+
*/
|
|
55
|
+
export function profileOf(guideline) {
|
|
56
|
+
if (guideline === null)
|
|
57
|
+
return null;
|
|
58
|
+
const known = PROFILE_GUIDELINES[guideline];
|
|
59
|
+
if (known !== undefined)
|
|
60
|
+
return known;
|
|
61
|
+
return guideline.startsWith('urn:cen.eu:en16931:2017#compliant#') ? 'en16931' : null;
|
|
62
|
+
}
|
|
63
|
+
function isCii(element, name) {
|
|
64
|
+
return element.name === name && CII_NAMESPACES.has(element.namespace);
|
|
65
|
+
}
|
|
66
|
+
function find(parent, path) {
|
|
67
|
+
let current = parent;
|
|
68
|
+
for (const name of path) {
|
|
69
|
+
if (current === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
current = current.children.find((each) => isCii(each, name));
|
|
72
|
+
}
|
|
73
|
+
return current;
|
|
74
|
+
}
|
|
75
|
+
function all(parent, name) {
|
|
76
|
+
if (parent === undefined)
|
|
77
|
+
return [];
|
|
78
|
+
return parent.children.filter((each) => isCii(each, name));
|
|
79
|
+
}
|
|
80
|
+
/** The trimmed text at a path, or null when absent or blank. */
|
|
81
|
+
function text(parent, ...path) {
|
|
82
|
+
const value = find(parent, path)?.text.trim();
|
|
83
|
+
return value === undefined || value === '' ? null : value;
|
|
84
|
+
}
|
|
85
|
+
function attribute(element, name) {
|
|
86
|
+
const value = element?.attributes[name]?.trim();
|
|
87
|
+
return value === undefined || value === '' ? null : value;
|
|
88
|
+
}
|
|
89
|
+
function required(parent, what, ...path) {
|
|
90
|
+
const value = text(parent, ...path);
|
|
91
|
+
if (value === null)
|
|
92
|
+
throw new InvoiceFileError('missing_element', `the document has no ${what} (${path.join('/')})`);
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
function amount(parent, what, ...path) {
|
|
96
|
+
const value = text(parent, ...path);
|
|
97
|
+
return value === null ? null : decimalText(value, what);
|
|
98
|
+
}
|
|
99
|
+
function requiredAmount(parent, what, ...path) {
|
|
100
|
+
return decimalText(required(parent, what, ...path), what);
|
|
101
|
+
}
|
|
102
|
+
function identifier(element) {
|
|
103
|
+
const value = element?.text.trim();
|
|
104
|
+
if (element === undefined || value === undefined || value === '')
|
|
105
|
+
return null;
|
|
106
|
+
return { value, scheme: attribute(element, 'schemeID') };
|
|
107
|
+
}
|
|
108
|
+
const DAY_102 = /^([0-9]{4})([0-9]{2})([0-9]{2})$/;
|
|
109
|
+
/**
|
|
110
|
+
* One reading, holding the violations it finds on the way: a date in a format
|
|
111
|
+
* EN 16931 does not use is reported and returned as null rather than refused.
|
|
112
|
+
*/
|
|
113
|
+
class Reader {
|
|
114
|
+
violations = [];
|
|
115
|
+
/**
|
|
116
|
+
* A date of CII: a `DateTimeString` (or `DateString`) of format 102,
|
|
117
|
+
* `YYYYMMDD` — the only format the CII binding of EN 16931 (EN 16931-3-3)
|
|
118
|
+
* uses for a date. `holder` is the element that contains the string,
|
|
119
|
+
* `IssueDateTime` and the like.
|
|
120
|
+
*/
|
|
121
|
+
date(holder, what) {
|
|
122
|
+
if (holder === undefined)
|
|
123
|
+
return null;
|
|
124
|
+
const string = holder.children.find((each) => isCii(each, 'DateTimeString') || isCii(each, 'DateString'));
|
|
125
|
+
const raw = string?.text.trim() ?? '';
|
|
126
|
+
if (string === undefined || raw === '')
|
|
127
|
+
return null;
|
|
128
|
+
const format = attribute(string, 'format');
|
|
129
|
+
if (format !== null && format !== '102') {
|
|
130
|
+
this.violations.push({
|
|
131
|
+
code: 'unsupported_date_format',
|
|
132
|
+
message: `${what} is written in format ${format}; EN 16931 dates are format 102 (YYYYMMDD), and it was not read.`,
|
|
133
|
+
});
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const match = DAY_102.exec(raw);
|
|
137
|
+
const year = Number(match?.[1]);
|
|
138
|
+
const month = Number(match?.[2]);
|
|
139
|
+
const day = Number(match?.[3]);
|
|
140
|
+
const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
141
|
+
const length = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
|
|
142
|
+
if (match === null || length === undefined || day < 1 || day > length) {
|
|
143
|
+
throw new InvoiceFileError('invalid_value', `${what} is not a date: ${JSON.stringify(raw)}`);
|
|
144
|
+
}
|
|
145
|
+
return `${match[1]}-${match[2]}-${match[3]}`;
|
|
146
|
+
}
|
|
147
|
+
period(element, what) {
|
|
148
|
+
if (element === undefined)
|
|
149
|
+
return null;
|
|
150
|
+
return {
|
|
151
|
+
start: this.date(find(element, ['StartDateTime']), `the start of ${what}`),
|
|
152
|
+
end: this.date(find(element, ['EndDateTime']), `the end of ${what}`),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** CII says which kind of account it holds; an IBAN whose digits fail is still the IBAN the file wrote, and reported. */
|
|
156
|
+
iban(value, what) {
|
|
157
|
+
if (value === null)
|
|
158
|
+
return null;
|
|
159
|
+
const compact = value.replace(/\s+/g, '').toUpperCase();
|
|
160
|
+
if (!isValidIban(compact)) {
|
|
161
|
+
this.violations.push({ code: 'invalid_iban', message: `${what}, ${value}, is written as an IBAN and its check digits fail.` });
|
|
162
|
+
}
|
|
163
|
+
return { kind: 'iban', value: compact };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// --- the parts -------------------------------------------------------------
|
|
167
|
+
function address(element) {
|
|
168
|
+
if (element === undefined)
|
|
169
|
+
return null;
|
|
170
|
+
const lines = [text(element, 'LineOne'), text(element, 'LineTwo'), text(element, 'LineThree')].filter((line) => line !== null);
|
|
171
|
+
const result = {
|
|
172
|
+
lines,
|
|
173
|
+
city: text(element, 'CityName'),
|
|
174
|
+
postalCode: text(element, 'PostcodeCode'),
|
|
175
|
+
region: text(element, 'CountrySubDivisionName'),
|
|
176
|
+
country: text(element, 'CountryID'),
|
|
177
|
+
};
|
|
178
|
+
return lines.length > 0 || Object.values(result).some((value) => typeof value === 'string') ? result : null;
|
|
179
|
+
}
|
|
180
|
+
function party(element, who) {
|
|
181
|
+
let vatId = null;
|
|
182
|
+
let taxRegistrationId = null;
|
|
183
|
+
for (const registration of all(element, 'SpecifiedTaxRegistration')) {
|
|
184
|
+
const id = find(registration, ['ID']);
|
|
185
|
+
const value = id?.text.trim();
|
|
186
|
+
if (value === undefined || value === '')
|
|
187
|
+
continue;
|
|
188
|
+
const scheme = (attribute(id, 'schemeID') ?? '').toUpperCase();
|
|
189
|
+
if (scheme === 'VA')
|
|
190
|
+
vatId ??= value;
|
|
191
|
+
else if (scheme === 'FC' && who === 'seller')
|
|
192
|
+
taxRegistrationId ??= value;
|
|
193
|
+
}
|
|
194
|
+
const legal = find(element, ['SpecifiedLegalOrganization']);
|
|
195
|
+
const contact = find(element, ['DefinedTradeContact']);
|
|
196
|
+
const contactFields = {
|
|
197
|
+
name: text(contact, 'PersonName') ?? text(contact, 'DepartmentName'),
|
|
198
|
+
phone: text(contact, 'TelephoneUniversalCommunication', 'CompleteNumber'),
|
|
199
|
+
email: text(contact, 'EmailURIUniversalCommunication', 'URIID'),
|
|
200
|
+
};
|
|
201
|
+
return {
|
|
202
|
+
name: text(element, 'Name'),
|
|
203
|
+
tradeName: text(legal, 'TradingBusinessName'),
|
|
204
|
+
// BT-29 / BT-46: GlobalID where the identifier has a scheme, ID where it has none.
|
|
205
|
+
identifiers: [...all(element, 'ID'), ...all(element, 'GlobalID')]
|
|
206
|
+
.map((each) => identifier(each))
|
|
207
|
+
.filter((each) => each !== null),
|
|
208
|
+
legalId: identifier(find(legal, ['ID'])),
|
|
209
|
+
vatId,
|
|
210
|
+
taxRegistrationId,
|
|
211
|
+
legalForm: who === 'seller' ? text(element, 'Description') : null,
|
|
212
|
+
electronicAddress: identifier(find(element, ['URIUniversalCommunication', 'URIID'])),
|
|
213
|
+
address: address(find(element, ['PostalTradeAddress'])),
|
|
214
|
+
contact: Object.values(contactFields).some((value) => value !== null) ? contactFields : null,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function allowanceCharge(element, where, onLine) {
|
|
218
|
+
const indicator = text(element, 'ChargeIndicator', 'Indicator');
|
|
219
|
+
if (indicator !== 'true' && indicator !== 'false') {
|
|
220
|
+
throw new InvoiceFileError('invalid_value', `an allowance or charge of ${where} says neither true nor false: ${JSON.stringify(indicator)}`);
|
|
221
|
+
}
|
|
222
|
+
const charge = indicator === 'true';
|
|
223
|
+
const what = `${charge ? 'a charge' : 'an allowance'} of ${where}`;
|
|
224
|
+
return {
|
|
225
|
+
charge,
|
|
226
|
+
amount: requiredAmount(element, `the amount of ${what}`, 'ActualAmount'),
|
|
227
|
+
baseAmount: amount(element, `the base of ${what}`, 'BasisAmount'),
|
|
228
|
+
percent: amount(element, `the percentage of ${what}`, 'CalculationPercent'),
|
|
229
|
+
reason: text(element, 'Reason'),
|
|
230
|
+
reasonCode: text(element, 'ReasonCode'),
|
|
231
|
+
vatCategory: onLine ? null : text(element, 'CategoryTradeTax', 'CategoryCode'),
|
|
232
|
+
vatRate: onLine ? null : amount(element, `the VAT rate of ${what}`, 'CategoryTradeTax', 'RateApplicablePercent'),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function line(reader, element, position) {
|
|
236
|
+
const document = find(element, ['AssociatedDocumentLineDocument']);
|
|
237
|
+
const id = text(document, 'LineID');
|
|
238
|
+
if (id === null)
|
|
239
|
+
throw new InvoiceFileError('missing_element', `line ${position} has no identifier (BT-126)`);
|
|
240
|
+
const where = `line ${id}`;
|
|
241
|
+
const product = find(element, ['SpecifiedTradeProduct']);
|
|
242
|
+
const agreement = find(element, ['SpecifiedLineTradeAgreement']);
|
|
243
|
+
const gross = find(agreement, ['GrossPriceProductTradePrice']);
|
|
244
|
+
const net = find(agreement, ['NetPriceProductTradePrice']);
|
|
245
|
+
const settlement = find(element, ['SpecifiedLineTradeSettlement']);
|
|
246
|
+
const tax = find(settlement, ['ApplicableTradeTax']);
|
|
247
|
+
const quantity = find(element, ['SpecifiedLineTradeDelivery', 'BilledQuantity']);
|
|
248
|
+
// BT-149 and BT-150 are written on the net price, and may be on the gross one.
|
|
249
|
+
const baseQuantity = find(net, ['BasisQuantity']) ?? find(gross, ['BasisQuantity']);
|
|
250
|
+
const summation = find(settlement, ['SpecifiedTradeSettlementLineMonetarySummation']);
|
|
251
|
+
return {
|
|
252
|
+
id,
|
|
253
|
+
note: text(document, 'IncludedNote', 'Content'),
|
|
254
|
+
quantity: quantity === undefined || quantity.text.trim() === '' ? null : decimalText(quantity.text, `the quantity of ${where}`),
|
|
255
|
+
unitCode: attribute(quantity, 'unitCode'),
|
|
256
|
+
netAmount: requiredAmount(summation, `the net amount of ${where}`, 'LineTotalAmount'),
|
|
257
|
+
orderLineReference: text(agreement, 'BuyerOrderReferencedDocument', 'LineID'),
|
|
258
|
+
period: reader.period(find(settlement, ['BillingSpecifiedPeriod']), where),
|
|
259
|
+
allowances: all(settlement, 'SpecifiedTradeAllowanceCharge').map((each) => allowanceCharge(each, where, true)),
|
|
260
|
+
netPrice: amount(net, `the net price of ${where}`, 'ChargeAmount'),
|
|
261
|
+
priceDiscount: amount(gross, `the price discount of ${where}`, 'AppliedTradeAllowanceCharge', 'ActualAmount'),
|
|
262
|
+
grossPrice: amount(gross, `the gross price of ${where}`, 'ChargeAmount'),
|
|
263
|
+
priceBaseQuantity: baseQuantity === undefined || baseQuantity.text.trim() === ''
|
|
264
|
+
? null
|
|
265
|
+
: decimalText(baseQuantity.text, `the base quantity of the price of ${where}`),
|
|
266
|
+
priceBaseUnitCode: attribute(baseQuantity, 'unitCode'),
|
|
267
|
+
vatCategory: text(tax, 'CategoryCode'),
|
|
268
|
+
vatRate: amount(tax, `the VAT rate of ${where}`, 'RateApplicablePercent'),
|
|
269
|
+
name: text(product, 'Name'),
|
|
270
|
+
description: text(product, 'Description'),
|
|
271
|
+
sellerItemId: text(product, 'SellerAssignedID'),
|
|
272
|
+
buyerItemId: text(product, 'BuyerAssignedID'),
|
|
273
|
+
standardItemId: identifier(find(product, ['GlobalID'])),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function paymentMeans(reader, element, reference, mandateReference) {
|
|
277
|
+
const payee = find(element, ['PayeePartyCreditorFinancialAccount']);
|
|
278
|
+
const payeeIban = text(payee, 'IBANID');
|
|
279
|
+
const proprietary = text(payee, 'ProprietaryID');
|
|
280
|
+
return {
|
|
281
|
+
code: text(element, 'TypeCode'),
|
|
282
|
+
text: text(element, 'Information'),
|
|
283
|
+
reference,
|
|
284
|
+
account: payeeIban !== null
|
|
285
|
+
? reader.iban(payeeIban, 'The account to pay')
|
|
286
|
+
: proprietary === null
|
|
287
|
+
? null
|
|
288
|
+
: { kind: 'other', value: proprietary },
|
|
289
|
+
accountName: text(payee, 'AccountName'),
|
|
290
|
+
bic: text(element, 'PayeeSpecifiedCreditorFinancialInstitution', 'BICID'),
|
|
291
|
+
cardNumber: text(element, 'ApplicableTradeSettlementFinancialCard', 'ID'),
|
|
292
|
+
mandateReference,
|
|
293
|
+
debitedAccount: reader.iban(text(element, 'PayerPartyDebtorFinancialAccount', 'IBANID'), 'The account to debit'),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Reads the XML of a Factur-X or ZUGFeRD invoice: a CII D16B
|
|
298
|
+
* `CrossIndustryInvoice`, whatever its profile. To read it out of the PDF,
|
|
299
|
+
* `readFacturX` of `@ekwo-ai/factur-x/pdf`.
|
|
300
|
+
*
|
|
301
|
+
* Throws an {@link InvoiceFileError} when there is no invoice to return: the
|
|
302
|
+
* bytes are not XML or not UTF-8, the document is not CII, or it lacks a
|
|
303
|
+
* number, a type code, a date, a currency or its totals, or holds an amount
|
|
304
|
+
* or a date that is not one. Everything else comes back, with `violations`
|
|
305
|
+
* naming what does not add up.
|
|
306
|
+
*
|
|
307
|
+
* Whether the document is a credit note is said by its type code (BT-3)
|
|
308
|
+
* alone, as CII has a single root for both; the figures stay as written.
|
|
309
|
+
*/
|
|
310
|
+
export function readCii(input, options = {}) {
|
|
311
|
+
const limits = { ...DEFAULTS, ...options };
|
|
312
|
+
const root = parseXml(input, limits);
|
|
313
|
+
if (root.namespace === NS.ublInvoice || root.namespace === NS.ublCreditNote) {
|
|
314
|
+
throw new InvoiceFileError('not_an_invoice', 'the file is a UBL invoice (Peppol), not CII: read it with the UBL reader');
|
|
315
|
+
}
|
|
316
|
+
if (root.name !== 'CrossIndustryInvoice' || root.namespace !== NS.rsm) {
|
|
317
|
+
throw new InvoiceFileError('not_an_invoice', `the document is <${root.name}> in ${root.namespace === '' ? 'no namespace' : root.namespace}, not a CII D16B CrossIndustryInvoice`);
|
|
318
|
+
}
|
|
319
|
+
const reader = new Reader();
|
|
320
|
+
const context = find(root, ['ExchangedDocumentContext']);
|
|
321
|
+
const exchanged = find(root, ['ExchangedDocument']);
|
|
322
|
+
if (exchanged === undefined)
|
|
323
|
+
throw new InvoiceFileError('missing_element', 'the document has no header (ExchangedDocument)');
|
|
324
|
+
const transaction = find(root, ['SupplyChainTradeTransaction']);
|
|
325
|
+
const agreement = find(transaction, ['ApplicableHeaderTradeAgreement']);
|
|
326
|
+
const delivery = find(transaction, ['ApplicableHeaderTradeDelivery']);
|
|
327
|
+
const settlement = find(transaction, ['ApplicableHeaderTradeSettlement']);
|
|
328
|
+
if (settlement === undefined) {
|
|
329
|
+
throw new InvoiceFileError('missing_element', 'the document has no settlement (ApplicableHeaderTradeSettlement)');
|
|
330
|
+
}
|
|
331
|
+
const guideline = text(context, 'GuidelineSpecifiedDocumentContextParameter', 'ID');
|
|
332
|
+
const typeCode = required(exchanged, 'type code (BT-3)', 'TypeCode');
|
|
333
|
+
const number = required(exchanged, 'number', 'ID');
|
|
334
|
+
const issueDate = reader.date(find(exchanged, ['IssueDateTime']), 'the issue date');
|
|
335
|
+
if (issueDate === null)
|
|
336
|
+
throw new InvoiceFileError('missing_element', 'the document has no issue date (IssueDateTime)');
|
|
337
|
+
const currency = required(settlement, 'currency', 'InvoiceCurrencyCode');
|
|
338
|
+
const taxCurrency = text(settlement, 'TaxCurrencyCode');
|
|
339
|
+
// BT-110 and BT-111 are both TaxTotalAmount, told apart by their currency.
|
|
340
|
+
const summation = find(settlement, ['SpecifiedTradeSettlementHeaderMonetarySummation']);
|
|
341
|
+
if (summation === undefined) {
|
|
342
|
+
throw new InvoiceFileError('missing_element', 'the document has no totals (SpecifiedTradeSettlementHeaderMonetarySummation)');
|
|
343
|
+
}
|
|
344
|
+
const taxTotals = all(summation, 'TaxTotalAmount');
|
|
345
|
+
const inCurrency = (each, code) => {
|
|
346
|
+
const written = attribute(each, 'currencyID');
|
|
347
|
+
return written === null ? code === currency : written === code;
|
|
348
|
+
};
|
|
349
|
+
const documentTax = taxTotals.find((each) => inCurrency(each, currency)) ?? (taxCurrency === null ? taxTotals[0] : undefined);
|
|
350
|
+
const taxCurrencyTax = taxCurrency !== null && taxCurrency !== currency
|
|
351
|
+
? taxTotals.find((each) => each !== documentTax && inCurrency(each, taxCurrency))
|
|
352
|
+
: undefined;
|
|
353
|
+
const totalOf = (element, what) => element === undefined || element.text.trim() === '' ? null : decimalText(element.text, what);
|
|
354
|
+
const taxes = all(settlement, 'ApplicableTradeTax').map((group) => {
|
|
355
|
+
const label = `the VAT group ${text(group, 'CategoryCode') ?? '?'} ${text(group, 'RateApplicablePercent') ?? ''}`.trim();
|
|
356
|
+
return {
|
|
357
|
+
category: text(group, 'CategoryCode'),
|
|
358
|
+
rate: amount(group, `the rate of ${label}`, 'RateApplicablePercent'),
|
|
359
|
+
base: requiredAmount(group, `the base of ${label}`, 'BasisAmount'),
|
|
360
|
+
tax: requiredAmount(group, `the tax of ${label}`, 'CalculatedAmount'),
|
|
361
|
+
exemptionReasonCode: text(group, 'ExemptionReasonCode'),
|
|
362
|
+
exemptionReason: text(group, 'ExemptionReason'),
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
// BT-7 is written on the VAT breakdown in CII, once per group and the same in each.
|
|
366
|
+
const taxPointDate = all(settlement, 'ApplicableTradeTax')
|
|
367
|
+
.map((group) => reader.date(find(group, ['TaxPointDate']), 'the tax point date'))
|
|
368
|
+
.find((each) => each !== null) ?? null;
|
|
369
|
+
// BG-24 is an additional document of type 916; type 130 is an invoiced
|
|
370
|
+
// object (BT-18) and type 50 a tender or lot (BT-17), neither an attachment.
|
|
371
|
+
const attachments = [];
|
|
372
|
+
for (const reference of all(agreement, 'AdditionalReferencedDocument')) {
|
|
373
|
+
const documentType = text(reference, 'TypeCode');
|
|
374
|
+
if (documentType === '130' || documentType === '50')
|
|
375
|
+
continue;
|
|
376
|
+
const embedded = find(reference, ['AttachmentBinaryObject']);
|
|
377
|
+
let content = null;
|
|
378
|
+
if (embedded !== undefined) {
|
|
379
|
+
content = decodeBase64(embedded.text);
|
|
380
|
+
if (content === null) {
|
|
381
|
+
reader.violations.push({
|
|
382
|
+
code: 'invalid_attachment',
|
|
383
|
+
message: `The document ${text(reference, 'IssuerAssignedID') ?? 'attached'} is not base64, and was not decoded.`,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
attachments.push({
|
|
388
|
+
id: text(reference, 'IssuerAssignedID'),
|
|
389
|
+
description: text(reference, 'Name'),
|
|
390
|
+
filename: attribute(embedded, 'filename'),
|
|
391
|
+
mimeType: attribute(embedded, 'mimeCode'),
|
|
392
|
+
content,
|
|
393
|
+
uri: text(reference, 'URIID'),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
// BT-9, BT-20 and BT-89 are in the payment terms; BT-83 is one reference for
|
|
397
|
+
// the document. Each is returned on every means of payment, as UBL has them.
|
|
398
|
+
const terms = all(settlement, 'SpecifiedTradePaymentTerms');
|
|
399
|
+
const dueDate = terms.map((each) => reader.date(find(each, ['DueDateDateTime']), 'the due date')).find((each) => each !== null) ?? null;
|
|
400
|
+
const descriptions = terms.map((each) => text(each, 'Description')).filter((each) => each !== null);
|
|
401
|
+
const mandate = terms.map((each) => text(each, 'DirectDebitMandateID')).find((each) => each !== null) ?? null;
|
|
402
|
+
const paymentReference = text(settlement, 'PaymentReference');
|
|
403
|
+
const shipTo = find(delivery, ['ShipToTradeParty']);
|
|
404
|
+
const profile = profileOf(guideline);
|
|
405
|
+
const lineElements = all(transaction, 'IncludedSupplyChainTradeLineItem');
|
|
406
|
+
if (lineElements.length === 0 && profile !== 'minimum' && profile !== 'basic-wl') {
|
|
407
|
+
throw new InvoiceFileError('missing_element', 'the document has no line, and its profile is not one that carries none (MINIMUM, BASIC WL)');
|
|
408
|
+
}
|
|
409
|
+
const invoice = {
|
|
410
|
+
syntax: 'cii',
|
|
411
|
+
customizationId: guideline,
|
|
412
|
+
profileId: text(context, 'BusinessProcessSpecifiedDocumentContextParameter', 'ID'),
|
|
413
|
+
kind: CREDIT_NOTE_TYPE_CODES.has(typeCode) ? 'credit_note' : 'invoice',
|
|
414
|
+
typeCode,
|
|
415
|
+
number,
|
|
416
|
+
issueDate,
|
|
417
|
+
dueDate,
|
|
418
|
+
taxPointDate,
|
|
419
|
+
currency,
|
|
420
|
+
taxCurrency,
|
|
421
|
+
buyerReference: text(agreement, 'BuyerReference'),
|
|
422
|
+
orderReference: text(agreement, 'BuyerOrderReferencedDocument', 'IssuerAssignedID'),
|
|
423
|
+
salesOrderReference: text(agreement, 'SellerOrderReferencedDocument', 'IssuerAssignedID'),
|
|
424
|
+
contractReference: text(agreement, 'ContractReferencedDocument', 'IssuerAssignedID'),
|
|
425
|
+
projectReference: text(agreement, 'SpecifiedProcuringProject', 'ID'),
|
|
426
|
+
accountingCost: text(settlement, 'ReceivableSpecifiedTradeAccountingAccount', 'ID'),
|
|
427
|
+
precedingInvoices: all(settlement, 'InvoiceReferencedDocument')
|
|
428
|
+
.filter((each) => text(each, 'IssuerAssignedID') !== null)
|
|
429
|
+
.map((each) => ({
|
|
430
|
+
number: text(each, 'IssuerAssignedID'),
|
|
431
|
+
issueDate: reader.date(find(each, ['FormattedIssueDateTime']), 'the date of a preceding invoice'),
|
|
432
|
+
})),
|
|
433
|
+
notes: all(exchanged, 'IncludedNote')
|
|
434
|
+
.map((each) => text(each, 'Content'))
|
|
435
|
+
.filter((each) => each !== null),
|
|
436
|
+
seller: party(find(agreement, ['SellerTradeParty']), 'seller'),
|
|
437
|
+
buyer: party(find(agreement, ['BuyerTradeParty']), 'buyer'),
|
|
438
|
+
deliveryDate: reader.date(find(delivery, ['ActualDeliverySupplyChainEvent', 'OccurrenceDateTime']), 'the delivery date'),
|
|
439
|
+
deliveryAddress: address(find(shipTo, ['PostalTradeAddress'])),
|
|
440
|
+
invoicingPeriod: reader.period(find(settlement, ['BillingSpecifiedPeriod']), 'the invoicing period'),
|
|
441
|
+
paymentTerms: descriptions.length === 0 ? null : descriptions.join('\n'),
|
|
442
|
+
paymentMeans: all(settlement, 'SpecifiedTradeSettlementPaymentMeans').map((each) => paymentMeans(reader, each, paymentReference, mandate)),
|
|
443
|
+
allowances: all(settlement, 'SpecifiedTradeAllowanceCharge').map((each) => allowanceCharge(each, 'the document', false)),
|
|
444
|
+
taxes,
|
|
445
|
+
totals: {
|
|
446
|
+
lineTotal: amount(summation, 'the sum of the lines', 'LineTotalAmount'),
|
|
447
|
+
allowanceTotal: amount(summation, 'the sum of the allowances', 'AllowanceTotalAmount'),
|
|
448
|
+
chargeTotal: amount(summation, 'the sum of the charges', 'ChargeTotalAmount'),
|
|
449
|
+
taxExclusive: requiredAmount(summation, 'the total without VAT', 'TaxBasisTotalAmount'),
|
|
450
|
+
taxTotal: totalOf(documentTax, 'the total VAT'),
|
|
451
|
+
taxTotalInTaxCurrency: totalOf(taxCurrencyTax, 'the total VAT in the tax currency'),
|
|
452
|
+
taxInclusive: requiredAmount(summation, 'the total with VAT', 'GrandTotalAmount'),
|
|
453
|
+
prepaid: amount(summation, 'the amount paid', 'TotalPrepaidAmount'),
|
|
454
|
+
rounding: amount(summation, 'the rounding amount', 'RoundingAmount'),
|
|
455
|
+
payable: requiredAmount(summation, 'the amount due', 'DuePayableAmount'),
|
|
456
|
+
},
|
|
457
|
+
lines: lineElements.map((each, index) => line(reader, each, index + 1)),
|
|
458
|
+
attachments,
|
|
459
|
+
};
|
|
460
|
+
return { invoice, violations: [...reader.violations, ...checkReceived(invoice)], profile };
|
|
461
|
+
}
|
|
462
|
+
//# sourceMappingURL=cii-read.js.map
|