@ekwo-ai/factur-x 0.4.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/cii.d.ts +17 -0
- package/dist/cii.d.ts.map +1 -0
- package/dist/cii.js +204 -0
- package/dist/cii.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/pdf.d.ts +32 -0
- package/dist/pdf.d.ts.map +1 -0
- package/dist/pdf.js +89 -0
- package/dist/pdf.js.map +1 -0
- package/dist/rounding.d.ts +25 -0
- package/dist/rounding.d.ts.map +1 -0
- package/dist/rounding.js +35 -0
- package/dist/rounding.js.map +1 -0
- package/dist/totals.d.ts +13 -0
- package/dist/totals.d.ts.map +1 -0
- package/dist/totals.js +55 -0
- package/dist/totals.js.map +1 -0
- package/dist/types.d.ts +170 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/units.d.ts +32 -0
- package/dist/units.d.ts.map +1 -0
- package/dist/units.js +61 -0
- package/dist/units.js.map +1 -0
- package/dist/vat.d.ts +23 -0
- package/dist/vat.d.ts.map +1 -0
- package/dist/vat.js +43 -0
- package/dist/vat.js.map +1 -0
- package/dist/xmp.d.ts +17 -0
- package/dist/xmp.d.ts.map +1 -0
- package/dist/xmp.js +70 -0
- package/dist/xmp.js.map +1 -0
- package/package.json +68 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.0] — 2026-09-14
|
|
10
|
+
|
|
11
|
+
Released with Ekwo OS `v0.2.0`, the first tagged release of the repository this
|
|
12
|
+
package lives in.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **A negative half was rounded the wrong way.** The totals used
|
|
16
|
+
`Math.round((v + EPSILON) * 100) / 100`, which goes towards positive infinity
|
|
17
|
+
and turns `-0.005` into `-0.00` — so a credit note was not its invoice with
|
|
18
|
+
the sign flipped, and the cent was found by whoever filed the return. The new
|
|
19
|
+
`roundCurrency` rounds half up on the absolute value, which is symmetric by
|
|
20
|
+
construction, and is the same file in the three packages that hold a copy.
|
|
21
|
+
|
|
22
|
+
## [0.1.0] — 2026-09-11
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- `generateCiiXml`: EN 16931 CII XML for the MINIMUM, BASIC WL, BASIC, EN 16931 and
|
|
26
|
+
EXTENDED Factur-X profiles, with VAT breakdown rules, credit notes and prepayments.
|
|
27
|
+
- `computeTotals`, VAT category and unit-code helpers.
|
|
28
|
+
- `@ekwo-ai/factur-x/pdf`: `embedFacturX` (PDF/A-3 attachment + XMP) and `extractFacturX`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ekwo AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @ekwo-ai/factur-x
|
|
2
|
+
|
|
3
|
+
Factur-X / ZUGFeRD electronic invoices in TypeScript.
|
|
4
|
+
|
|
5
|
+
- **CII XML from a plain invoice object.** EN 16931 semantic model, UN/CEFACT CII D16B
|
|
6
|
+
syntax, element order of the schema, VAT breakdown rules (`BR-S`, `BR-Z`, `BR-E`, `BR-AE`,
|
|
7
|
+
`BR-IC`, `BR-G`, `BR-O`). Profiles MINIMUM, BASIC WL, BASIC (default), EN 16931, EXTENDED.
|
|
8
|
+
- **PDF/A-3 embedding.** Attaches the XML as `factur-x.xml` with `AFRelationship /Alternative`
|
|
9
|
+
and writes the XMP packet with the Factur-X extension schema.
|
|
10
|
+
- **Extraction.** Reads `factur-x.xml` or `zugferd-invoice.xml` back from any PDF.
|
|
11
|
+
- **Totals you can trust.** One VAT group per category and rate, VAT computed on the rounded
|
|
12
|
+
basis, prepayments, credit notes referencing the original invoice.
|
|
13
|
+
- Zero dependencies for the XML part; `pdf-lib` only for the PDF part (separate entry point).
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install @ekwo-ai/factur-x
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { generateCiiXml, computeTotals } from '@ekwo-ai/factur-x';
|
|
25
|
+
import { embedFacturX, extractFacturX } from '@ekwo-ai/factur-x/pdf';
|
|
26
|
+
|
|
27
|
+
const invoice = {
|
|
28
|
+
number: 'INV-2026-0042',
|
|
29
|
+
issueDate: '2026-03-31',
|
|
30
|
+
dueDate: '2026-04-30',
|
|
31
|
+
seller: {
|
|
32
|
+
name: 'Exemple Conseil SA',
|
|
33
|
+
vatId: 'BE0123456749',
|
|
34
|
+
legalId: { scheme: '0208', value: '0123456749' }, // Belgian enterprise number
|
|
35
|
+
address: { line1: "Rue de l'Exemple 1", postalCode: '1000', city: 'Bruxelles', country: 'BE' },
|
|
36
|
+
},
|
|
37
|
+
buyer: {
|
|
38
|
+
name: 'Société Fictive SAS',
|
|
39
|
+
vatId: 'FR12345678901',
|
|
40
|
+
legalId: { scheme: '0002', value: '345678901' }, // SIREN
|
|
41
|
+
address: { line1: '10 avenue des Tests', postalCode: '75001', city: 'Paris', country: 'FR' },
|
|
42
|
+
},
|
|
43
|
+
buyerReference: 'SERVICE-EXEC-42',
|
|
44
|
+
lines: [
|
|
45
|
+
{ name: 'Consulting days', quantity: 3, unitCode: 'day', unitPrice: 950, vatRate: 0 },
|
|
46
|
+
{ name: 'Software licence', quantity: 1, unitPrice: 1200, vatRate: 0 },
|
|
47
|
+
],
|
|
48
|
+
payment: { iban: 'BE71 0961 2345 6769' },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const xml = generateCiiXml(invoice); // BASIC profile
|
|
52
|
+
const totals = computeTotals(invoice); // { lineTotal, taxTotal, grandTotal, duePayable, breakdown }
|
|
53
|
+
|
|
54
|
+
const pdf = await embedFacturX(visualPdfBytes, xml, { title: 'Invoice INV-2026-0042' });
|
|
55
|
+
const back = await extractFacturX(pdf); // { filename: 'factur-x.xml', xml }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`npm run example` runs [`examples/basic.ts`](examples/basic.ts) end to end.
|
|
59
|
+
|
|
60
|
+
### VAT categories
|
|
61
|
+
|
|
62
|
+
Each line carries a `vatRate`. The category (BT-151) is derived when you do not state it:
|
|
63
|
+
positive rate → `S`; zero rate at home → `Z`; zero rate to an EU business with a VAT id → `K`
|
|
64
|
+
(intra-community); to an EU buyer without one → `E`; outside the EU → `G`. Set `vatCategory`
|
|
65
|
+
on the line to override (`AE` reverse charge, `O` not subject to VAT…). Exempt groups get a
|
|
66
|
+
reason text (BT-120, override with `invoice.exemptionReasons`) and the VATEX code where one
|
|
67
|
+
exists.
|
|
68
|
+
|
|
69
|
+
### Identifiers
|
|
70
|
+
|
|
71
|
+
| Field | Example | XML |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `vatId` | `BE0123456749`, `FR12345678901` | `SpecifiedTaxRegistration/ID[@schemeID="VA"]` |
|
|
74
|
+
| `legalId` | `{ scheme: '0208', value: '0123456749' }` (BE), `{ scheme: '0002', value: '345678901' }` (FR SIREN) | `SpecifiedLegalOrganization/ID[@schemeID]` |
|
|
75
|
+
| `buyerReference` | French « service exécutant » code | `BuyerReference` (BT-10) |
|
|
76
|
+
| `orderReference` | purchase order | `BuyerOrderReferencedDocument` (BT-13) |
|
|
77
|
+
|
|
78
|
+
### Units
|
|
79
|
+
|
|
80
|
+
`unitCode` accepts a UN/ECE Recommendation 20 code (`HUR`, `DAY`, `KWH`…) or an everyday
|
|
81
|
+
word in French, Dutch, English or German (`heures`, `dagen`, `m²`, `Stück`). Unknown values fall
|
|
82
|
+
back to `C62` (unit). See `UNIT_CODES` and `toUnitCode`.
|
|
83
|
+
|
|
84
|
+
## API
|
|
85
|
+
|
|
86
|
+
| Export | Description |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `generateCiiXml(invoice, { profile?, businessProcess? })` | CII XML string. |
|
|
89
|
+
| `computeTotals(invoice)` | VAT breakdown and document totals (BT-106 … BT-117). |
|
|
90
|
+
| `defaultVatCategory`, `EXEMPTION_REASON_CODES`, `DEFAULT_EXEMPTION_REASONS` | VAT helpers. |
|
|
91
|
+
| `toUnitCode`, `UNIT_CODES` | Unit helpers. |
|
|
92
|
+
| `buildXmpMetadata`, `CONFORMANCE_LEVELS`, `GUIDELINES`, `DOCUMENT_TYPE_CODES` | Constants of the specification. |
|
|
93
|
+
| `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
|
+
| `extractFacturX(pdf)` *(`/pdf`)* | `{ filename, xml }` or `null`. |
|
|
95
|
+
|
|
96
|
+
## Scope and limitations
|
|
97
|
+
|
|
98
|
+
- Allowances and charges (BG-20, BG-21, BG-27, BG-28), gross prices with discounts and
|
|
99
|
+
multiple deliveries are not modelled yet.
|
|
100
|
+
- The PDF part embeds the data and declares PDF/A-3; it does not convert the visual PDF to
|
|
101
|
+
PDF/A (fonts, colour profiles, output intent). Validate the result with the tool of your
|
|
102
|
+
platform before going live.
|
|
103
|
+
- No XSD or Schematron validation is performed. Test against a validator such as the FNFE
|
|
104
|
+
Factur-X validator or the Mustang project.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
npm install
|
|
110
|
+
npm run typecheck
|
|
111
|
+
npm test
|
|
112
|
+
npm run build
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
[MIT](LICENSE) © Ekwo AI
|
package/dist/cii.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross Industry Invoice (CII, UN/CEFACT D16B) XML for Factur-X / ZUGFeRD,
|
|
3
|
+
* following the element order of the EN 16931 schema and its business rules
|
|
4
|
+
* for VAT breakdowns.
|
|
5
|
+
*/
|
|
6
|
+
import type { DocumentType, GenerateOptions, Invoice, Profile } from './types.js';
|
|
7
|
+
/** Guideline identifiers (BT-24) per conformance level. */
|
|
8
|
+
export declare const GUIDELINES: Readonly<Record<Profile, string>>;
|
|
9
|
+
/** UNTDID 1001 document type codes (BT-3). */
|
|
10
|
+
export declare const DOCUMENT_TYPE_CODES: Readonly<Record<DocumentType, string>>;
|
|
11
|
+
export declare function escapeXml(s: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* The CII XML document for an invoice, as a UTF-8 string ready to be
|
|
14
|
+
* embedded as `factur-x.xml`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function generateCiiXml(invoice: Invoice, options?: GenerateOptions): string;
|
|
17
|
+
//# sourceMappingURL=cii.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cii.d.ts","sourceRoot":"","sources":["../src/cii.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,OAAO,EAAS,OAAO,EAAE,MAAM,YAAY,CAAC;AAKzF,2DAA2D;AAC3D,eAAO,MAAM,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAMxD,CAAC;AAEF,8CAA8C;AAC9C,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAMtE,CAAC;AAEF,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAO3C;AA2CD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CAgJtF"}
|
package/dist/cii.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { computeTotals, lineNetAmount, lineVatCategory } from './totals.js';
|
|
2
|
+
import { toUnitCode } from './units.js';
|
|
3
|
+
import { DEFAULT_EXEMPTION_REASONS, EXEMPTION_REASON_CODES, EXEMPT_CATEGORIES } from './vat.js';
|
|
4
|
+
/** Guideline identifiers (BT-24) per conformance level. */
|
|
5
|
+
export const GUIDELINES = {
|
|
6
|
+
minimum: 'urn:factur-x.eu:1p0:minimum',
|
|
7
|
+
'basic-wl': 'urn:factur-x.eu:1p0:basicwl',
|
|
8
|
+
basic: 'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic',
|
|
9
|
+
en16931: 'urn:cen.eu:en16931:2017',
|
|
10
|
+
extended: 'urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended',
|
|
11
|
+
};
|
|
12
|
+
/** UNTDID 1001 document type codes (BT-3). */
|
|
13
|
+
export const DOCUMENT_TYPE_CODES = {
|
|
14
|
+
invoice: '380',
|
|
15
|
+
'credit-note': '381',
|
|
16
|
+
'corrected-invoice': '384',
|
|
17
|
+
'self-billed-invoice': '389',
|
|
18
|
+
'prepayment-invoice': '386',
|
|
19
|
+
};
|
|
20
|
+
export function escapeXml(s) {
|
|
21
|
+
return s
|
|
22
|
+
.replace(/&/g, '&')
|
|
23
|
+
.replace(/</g, '<')
|
|
24
|
+
.replace(/>/g, '>')
|
|
25
|
+
.replace(/"/g, '"')
|
|
26
|
+
.replace(/'/g, ''');
|
|
27
|
+
}
|
|
28
|
+
const amount = (n) => n.toFixed(2);
|
|
29
|
+
const quantity = (n) => n.toFixed(4);
|
|
30
|
+
const ymd = (iso) => iso.replace(/-/g, '').slice(0, 8);
|
|
31
|
+
const date102 = (iso) => `<udt:DateTimeString format="102">${ymd(iso)}</udt:DateTimeString>`;
|
|
32
|
+
const tag = (name, value) => (value ? `<${name}>${escapeXml(value)}</${name}>` : '');
|
|
33
|
+
function partyXml(name, party, profile) {
|
|
34
|
+
const a = party.address;
|
|
35
|
+
const legal = party.legalId
|
|
36
|
+
? `<ram:SpecifiedLegalOrganization><ram:ID schemeID="${escapeXml(party.legalId.scheme)}">${escapeXml(party.legalId.value.replace(/\s/g, ''))}</ram:ID></ram:SpecifiedLegalOrganization>`
|
|
37
|
+
: '';
|
|
38
|
+
const contact = profile === 'en16931' || profile === 'extended'
|
|
39
|
+
? party.contact
|
|
40
|
+
? `<ram:DefinedTradeContact>${tag('ram:PersonName', party.contact.name)}${party.contact.phone ? `<ram:TelephoneUniversalCommunication>${tag('ram:CompleteNumber', party.contact.phone)}</ram:TelephoneUniversalCommunication>` : ''}${party.contact.email ? `<ram:EmailURIUniversalCommunication>${tag('ram:URIID', party.contact.email)}</ram:EmailURIUniversalCommunication>` : ''}</ram:DefinedTradeContact>`
|
|
41
|
+
: ''
|
|
42
|
+
: '';
|
|
43
|
+
const electronic = party.electronicAddress
|
|
44
|
+
? `<ram:URIUniversalCommunication><ram:URIID schemeID="${escapeXml(party.electronicAddress.scheme)}">${escapeXml(party.electronicAddress.value)}</ram:URIID></ram:URIUniversalCommunication>`
|
|
45
|
+
: '';
|
|
46
|
+
const vat = party.vatId
|
|
47
|
+
? `<ram:SpecifiedTaxRegistration><ram:ID schemeID="VA">${escapeXml(party.vatId.replace(/\s/g, ''))}</ram:ID></ram:SpecifiedTaxRegistration>`
|
|
48
|
+
: '';
|
|
49
|
+
return `<ram:${name}>
|
|
50
|
+
${tag('ram:Name', party.name)}
|
|
51
|
+
${legal}${contact}
|
|
52
|
+
<ram:PostalTradeAddress>
|
|
53
|
+
${tag('ram:PostcodeCode', a.postalCode)}
|
|
54
|
+
${tag('ram:LineOne', a.line1)}
|
|
55
|
+
${tag('ram:LineTwo', a.line2)}
|
|
56
|
+
${tag('ram:LineThree', a.line3)}
|
|
57
|
+
${tag('ram:CityName', a.city)}
|
|
58
|
+
<ram:CountryID>${escapeXml(a.country.toUpperCase())}</ram:CountryID>
|
|
59
|
+
${tag('ram:CountrySubDivisionName', a.subdivision)}
|
|
60
|
+
</ram:PostalTradeAddress>
|
|
61
|
+
${electronic}${vat}
|
|
62
|
+
</ram:${name}>`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The CII XML document for an invoice, as a UTF-8 string ready to be
|
|
66
|
+
* embedded as `factur-x.xml`.
|
|
67
|
+
*/
|
|
68
|
+
export function generateCiiXml(invoice, options = {}) {
|
|
69
|
+
const profile = options.profile ?? 'basic';
|
|
70
|
+
const currency = (invoice.currency ?? 'EUR').toUpperCase();
|
|
71
|
+
const totals = computeTotals(invoice);
|
|
72
|
+
const typeCode = DOCUMENT_TYPE_CODES[invoice.type ?? 'invoice'];
|
|
73
|
+
const lines = invoice.lines
|
|
74
|
+
.map((line, i) => {
|
|
75
|
+
const category = lineVatCategory(line, invoice);
|
|
76
|
+
const rate = category === 'S' ? line.vatRate : 0;
|
|
77
|
+
return `
|
|
78
|
+
<ram:IncludedSupplyChainTradeLineItem>
|
|
79
|
+
<ram:AssociatedDocumentLineDocument>
|
|
80
|
+
<ram:LineID>${escapeXml(line.id ?? String(i + 1))}</ram:LineID>
|
|
81
|
+
${line.note ? `<ram:IncludedNote>${tag('ram:Content', line.note)}</ram:IncludedNote>` : ''}
|
|
82
|
+
</ram:AssociatedDocumentLineDocument>
|
|
83
|
+
<ram:SpecifiedTradeProduct>
|
|
84
|
+
${tag('ram:SellerAssignedID', line.sellerItemId)}
|
|
85
|
+
${tag('ram:Name', line.name)}
|
|
86
|
+
${tag('ram:Description', line.description)}
|
|
87
|
+
</ram:SpecifiedTradeProduct>
|
|
88
|
+
<ram:SpecifiedLineTradeAgreement>
|
|
89
|
+
<ram:NetPriceProductTradePrice>
|
|
90
|
+
<ram:ChargeAmount>${amount(line.unitPrice)}</ram:ChargeAmount>
|
|
91
|
+
</ram:NetPriceProductTradePrice>
|
|
92
|
+
</ram:SpecifiedLineTradeAgreement>
|
|
93
|
+
<ram:SpecifiedLineTradeDelivery>
|
|
94
|
+
<ram:BilledQuantity unitCode="${escapeXml(toUnitCode(line.unitCode))}">${quantity(line.quantity)}</ram:BilledQuantity>
|
|
95
|
+
</ram:SpecifiedLineTradeDelivery>
|
|
96
|
+
<ram:SpecifiedLineTradeSettlement>
|
|
97
|
+
<ram:ApplicableTradeTax>
|
|
98
|
+
<ram:TypeCode>VAT</ram:TypeCode>
|
|
99
|
+
<ram:CategoryCode>${category}</ram:CategoryCode>
|
|
100
|
+
${category === 'O' ? '' : `<ram:RateApplicablePercent>${amount(rate)}</ram:RateApplicablePercent>`}
|
|
101
|
+
</ram:ApplicableTradeTax>
|
|
102
|
+
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
|
103
|
+
<ram:LineTotalAmount>${amount(lineNetAmount(line))}</ram:LineTotalAmount>
|
|
104
|
+
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
|
105
|
+
</ram:SpecifiedLineTradeSettlement>
|
|
106
|
+
</ram:IncludedSupplyChainTradeLineItem>`;
|
|
107
|
+
})
|
|
108
|
+
.join('');
|
|
109
|
+
// Header VAT breakdown. Element order of the schema: CalculatedAmount,
|
|
110
|
+
// TypeCode, ExemptionReason, BasisAmount, CategoryCode, ExemptionReasonCode,
|
|
111
|
+
// DueDateTypeCode, RateApplicablePercent.
|
|
112
|
+
const breakdown = totals.breakdown
|
|
113
|
+
.map((g) => {
|
|
114
|
+
const exempt = EXEMPT_CATEGORIES.has(g.category);
|
|
115
|
+
const reason = exempt
|
|
116
|
+
? invoice.exemptionReasons?.[g.category] ??
|
|
117
|
+
DEFAULT_EXEMPTION_REASONS[g.category]
|
|
118
|
+
: undefined;
|
|
119
|
+
const reasonCode = exempt ? EXEMPTION_REASON_CODES[g.category] : undefined;
|
|
120
|
+
return `
|
|
121
|
+
<ram:ApplicableTradeTax>
|
|
122
|
+
<ram:CalculatedAmount>${amount(g.tax)}</ram:CalculatedAmount>
|
|
123
|
+
<ram:TypeCode>VAT</ram:TypeCode>
|
|
124
|
+
${tag('ram:ExemptionReason', reason)}
|
|
125
|
+
<ram:BasisAmount>${amount(g.basis)}</ram:BasisAmount>
|
|
126
|
+
<ram:CategoryCode>${g.category}</ram:CategoryCode>
|
|
127
|
+
${tag('ram:ExemptionReasonCode', reasonCode)}
|
|
128
|
+
${g.category === 'O' ? '' : `<ram:RateApplicablePercent>${amount(g.rate)}</ram:RateApplicablePercent>`}
|
|
129
|
+
</ram:ApplicableTradeTax>`;
|
|
130
|
+
})
|
|
131
|
+
.join('');
|
|
132
|
+
const p = invoice.payment;
|
|
133
|
+
const paymentMeans = p && (p.iban || p.meansCode)
|
|
134
|
+
? `
|
|
135
|
+
<ram:SpecifiedTradeSettlementPaymentMeans>
|
|
136
|
+
<ram:TypeCode>${escapeXml(p.meansCode ?? '58')}</ram:TypeCode>
|
|
137
|
+
${p.iban ? `<ram:PayeePartyCreditorFinancialAccount><ram:IBANID>${escapeXml(p.iban.replace(/\s/g, ''))}</ram:IBANID></ram:PayeePartyCreditorFinancialAccount>` : ''}
|
|
138
|
+
${p.bic && (profile === 'en16931' || profile === 'extended') ? `<ram:PayeeSpecifiedCreditorFinancialInstitution><ram:BICID>${escapeXml(p.bic)}</ram:BICID></ram:PayeeSpecifiedCreditorFinancialInstitution>` : ''}
|
|
139
|
+
</ram:SpecifiedTradeSettlementPaymentMeans>`
|
|
140
|
+
: '';
|
|
141
|
+
const paymentTerms = p?.terms || invoice.dueDate
|
|
142
|
+
? `
|
|
143
|
+
<ram:SpecifiedTradePaymentTerms>
|
|
144
|
+
${tag('ram:Description', p?.terms)}
|
|
145
|
+
${invoice.dueDate ? `<ram:DueDateDateTime>${date102(invoice.dueDate)}</ram:DueDateDateTime>` : ''}
|
|
146
|
+
</ram:SpecifiedTradePaymentTerms>`
|
|
147
|
+
: '';
|
|
148
|
+
const preceding = invoice.precedingInvoice
|
|
149
|
+
? `
|
|
150
|
+
<ram:InvoiceReferencedDocument>
|
|
151
|
+
${tag('ram:IssuerAssignedID', invoice.precedingInvoice.number)}
|
|
152
|
+
${invoice.precedingInvoice.issueDate ? `<ram:FormattedIssueDateTime><qdt:DateTimeString format="102">${ymd(invoice.precedingInvoice.issueDate)}</qdt:DateTimeString></ram:FormattedIssueDateTime>` : ''}
|
|
153
|
+
</ram:InvoiceReferencedDocument>`
|
|
154
|
+
: '';
|
|
155
|
+
const note = invoice.note?.replace(/\s+/g, ' ').trim();
|
|
156
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
157
|
+
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
|
|
158
|
+
<rsm:ExchangedDocumentContext>
|
|
159
|
+
<ram:BusinessProcessSpecifiedDocumentContextParameter>
|
|
160
|
+
<ram:ID>${escapeXml(options.businessProcess ?? 'A1')}</ram:ID>
|
|
161
|
+
</ram:BusinessProcessSpecifiedDocumentContextParameter>
|
|
162
|
+
<ram:GuidelineSpecifiedDocumentContextParameter>
|
|
163
|
+
<ram:ID>${GUIDELINES[profile]}</ram:ID>
|
|
164
|
+
</ram:GuidelineSpecifiedDocumentContextParameter>
|
|
165
|
+
</rsm:ExchangedDocumentContext>
|
|
166
|
+
<rsm:ExchangedDocument>
|
|
167
|
+
<ram:ID>${escapeXml(invoice.number)}</ram:ID>
|
|
168
|
+
<ram:TypeCode>${typeCode}</ram:TypeCode>
|
|
169
|
+
<ram:IssueDateTime>${date102(invoice.issueDate)}</ram:IssueDateTime>
|
|
170
|
+
${note ? `<ram:IncludedNote>${tag('ram:Content', note)}</ram:IncludedNote>` : ''}
|
|
171
|
+
</rsm:ExchangedDocument>
|
|
172
|
+
<rsm:SupplyChainTradeTransaction>${lines}
|
|
173
|
+
<ram:ApplicableHeaderTradeAgreement>
|
|
174
|
+
${tag('ram:BuyerReference', invoice.buyerReference)}
|
|
175
|
+
${partyXml('SellerTradeParty', invoice.seller, profile)}
|
|
176
|
+
${partyXml('BuyerTradeParty', invoice.buyer, profile)}
|
|
177
|
+
${invoice.orderReference ? `<ram:BuyerOrderReferencedDocument>${tag('ram:IssuerAssignedID', invoice.orderReference)}</ram:BuyerOrderReferencedDocument>` : ''}
|
|
178
|
+
${invoice.contractReference ? `<ram:ContractReferencedDocument>${tag('ram:IssuerAssignedID', invoice.contractReference)}</ram:ContractReferencedDocument>` : ''}
|
|
179
|
+
</ram:ApplicableHeaderTradeAgreement>
|
|
180
|
+
<ram:ApplicableHeaderTradeDelivery>
|
|
181
|
+
<ram:ActualDeliverySupplyChainEvent>
|
|
182
|
+
<ram:OccurrenceDateTime>${date102(invoice.deliveryDate ?? invoice.issueDate)}</ram:OccurrenceDateTime>
|
|
183
|
+
</ram:ActualDeliverySupplyChainEvent>
|
|
184
|
+
</ram:ApplicableHeaderTradeDelivery>
|
|
185
|
+
<ram:ApplicableHeaderTradeSettlement>
|
|
186
|
+
${tag('ram:PaymentReference', p?.reference)}
|
|
187
|
+
<ram:InvoiceCurrencyCode>${escapeXml(currency)}</ram:InvoiceCurrencyCode>${paymentMeans}${breakdown}${paymentTerms}
|
|
188
|
+
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
|
189
|
+
<ram:LineTotalAmount>${amount(totals.lineTotal)}</ram:LineTotalAmount>
|
|
190
|
+
<ram:TaxBasisTotalAmount>${amount(totals.taxBasisTotal)}</ram:TaxBasisTotalAmount>
|
|
191
|
+
<ram:TaxTotalAmount currencyID="${escapeXml(currency)}">${amount(totals.taxTotal)}</ram:TaxTotalAmount>
|
|
192
|
+
<ram:GrandTotalAmount>${amount(totals.grandTotal)}</ram:GrandTotalAmount>
|
|
193
|
+
${totals.prepaid ? `<ram:TotalPrepaidAmount>${amount(totals.prepaid)}</ram:TotalPrepaidAmount>` : ''}
|
|
194
|
+
<ram:DuePayableAmount>${amount(totals.duePayable)}</ram:DuePayableAmount>
|
|
195
|
+
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>${preceding}
|
|
196
|
+
</ram:ApplicableHeaderTradeSettlement>
|
|
197
|
+
</rsm:SupplyChainTradeTransaction>
|
|
198
|
+
</rsm:CrossIndustryInvoice>
|
|
199
|
+
`
|
|
200
|
+
.split('\n')
|
|
201
|
+
.filter((l) => l.trim() !== '')
|
|
202
|
+
.join('\n') + '\n';
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=cii.js.map
|
package/dist/cii.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cii.js","sourceRoot":"","sources":["../src/cii.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { generateCiiXml, escapeXml, GUIDELINES, DOCUMENT_TYPE_CODES } from './cii.js';
|
|
2
|
+
export { computeTotals, lineNetAmount, lineVatCategory, round2 } from './totals.js';
|
|
3
|
+
export { defaultVatCategory, isEuCountry, EU_COUNTRIES, EXEMPT_CATEGORIES, EXEMPTION_REASON_CODES, DEFAULT_EXEMPTION_REASONS, } from './vat.js';
|
|
4
|
+
export { toUnitCode, UNIT_CODES } from './units.js';
|
|
5
|
+
export { buildXmpMetadata, CONFORMANCE_LEVELS } from './xmp.js';
|
|
6
|
+
export type { XmpOptions } from './xmp.js';
|
|
7
|
+
export type { CurrencyCode, DocumentType, GenerateOptions, Invoice, InvoiceLine, InvoiceTotals, IsoDate, LegalIdentifier, Party, PaymentInstructions, PostalAddress, Profile, VatBreakdown, VatCategory, } from './types.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,OAAO,EACP,WAAW,EACX,aAAa,EACb,OAAO,EACP,eAAe,EACf,KAAK,EACL,mBAAmB,EACnB,aAAa,EACb,OAAO,EACP,YAAY,EACZ,WAAW,GACZ,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { generateCiiXml, escapeXml, GUIDELINES, DOCUMENT_TYPE_CODES } from './cii.js';
|
|
2
|
+
export { computeTotals, lineNetAmount, lineVatCategory, round2 } from './totals.js';
|
|
3
|
+
export { defaultVatCategory, isEuCountry, EU_COUNTRIES, EXEMPT_CATEGORIES, EXEMPTION_REASON_CODES, DEFAULT_EXEMPTION_REASONS, } from './vat.js';
|
|
4
|
+
export { toUnitCode, UNIT_CODES } from './units.js';
|
|
5
|
+
export { buildXmpMetadata, CONFORMANCE_LEVELS } from './xmp.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/pdf.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Profile } from './types.js';
|
|
2
|
+
export declare const FACTURX_FILENAME = "factur-x.xml";
|
|
3
|
+
/** File name used by ZUGFeRD 2.x documents; accepted when reading. */
|
|
4
|
+
export declare const ZUGFERD_FILENAME = "zugferd-invoice.xml";
|
|
5
|
+
export interface EmbedOptions {
|
|
6
|
+
/** Defaults to `"basic"`. Must match the guideline used for the XML. */
|
|
7
|
+
profile?: Profile;
|
|
8
|
+
title?: string;
|
|
9
|
+
creator?: string;
|
|
10
|
+
producer?: string;
|
|
11
|
+
/** Timestamp for the attachment and the XMP packet. Defaults to now. */
|
|
12
|
+
date?: Date;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns a copy of `pdf` carrying `xml` as `factur-x.xml`.
|
|
16
|
+
*
|
|
17
|
+
* The visual PDF itself is left untouched: fonts, colour profiles and output
|
|
18
|
+
* intents are the responsibility of the producer of the original file. Strict
|
|
19
|
+
* PDF/A-3 validators may still flag those; the invoice data is embedded and
|
|
20
|
+
* readable, which is what e-invoicing platforms check.
|
|
21
|
+
*/
|
|
22
|
+
export declare function embedFacturX(pdf: Uint8Array | ArrayBuffer, xml: string, options?: EmbedOptions): Promise<Uint8Array>;
|
|
23
|
+
export interface ExtractedInvoice {
|
|
24
|
+
filename: string;
|
|
25
|
+
xml: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Reads the embedded invoice XML (`factur-x.xml` or `zugferd-invoice.xml`)
|
|
29
|
+
* from a PDF. Returns `null` when the file carries none.
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractFacturX(pdf: Uint8Array | ArrayBuffer): Promise<ExtractedInvoice | null>;
|
|
32
|
+
//# sourceMappingURL=pdf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdf.d.ts","sourceRoot":"","sources":["../src/pdf.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,eAAO,MAAM,gBAAgB,iBAAiB,CAAC;AAC/C,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,wBAAwB,CAAC;AAEtD,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,IAAI,CAAC,EAAE,IAAI,CAAC;CACb;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,UAAU,GAAG,WAAW,EAC7B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,UAAU,CAAC,CAuBrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,WAAW,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAiBpG"}
|
package/dist/pdf.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF side of Factur-X: attach the CII XML to a PDF as `factur-x.xml` with
|
|
3
|
+
* `AFRelationship /Alternative`, declare the document as PDF/A-3 in XMP, and
|
|
4
|
+
* read the XML back from an existing Factur-X / ZUGFeRD file.
|
|
5
|
+
*
|
|
6
|
+
* Only this module depends on `pdf-lib`; import it from `@ekwo-ai/factur-x/pdf`.
|
|
7
|
+
*/
|
|
8
|
+
import { AFRelationship, PDFArray, PDFDict, PDFDocument, PDFHexString, PDFName, PDFRawStream, PDFString, decodePDFRawStream, } from 'pdf-lib';
|
|
9
|
+
import { buildXmpMetadata } from './xmp.js';
|
|
10
|
+
export const FACTURX_FILENAME = 'factur-x.xml';
|
|
11
|
+
/** File name used by ZUGFeRD 2.x documents; accepted when reading. */
|
|
12
|
+
export const ZUGFERD_FILENAME = 'zugferd-invoice.xml';
|
|
13
|
+
/**
|
|
14
|
+
* Returns a copy of `pdf` carrying `xml` as `factur-x.xml`.
|
|
15
|
+
*
|
|
16
|
+
* The visual PDF itself is left untouched: fonts, colour profiles and output
|
|
17
|
+
* intents are the responsibility of the producer of the original file. Strict
|
|
18
|
+
* PDF/A-3 validators may still flag those; the invoice data is embedded and
|
|
19
|
+
* readable, which is what e-invoicing platforms check.
|
|
20
|
+
*/
|
|
21
|
+
export async function embedFacturX(pdf, xml, options = {}) {
|
|
22
|
+
const profile = options.profile ?? 'basic';
|
|
23
|
+
const date = options.date ?? new Date();
|
|
24
|
+
const doc = await PDFDocument.load(pdf, { updateMetadata: false });
|
|
25
|
+
await doc.attach(new TextEncoder().encode(xml), FACTURX_FILENAME, {
|
|
26
|
+
mimeType: 'text/xml',
|
|
27
|
+
description: 'Factur-X invoice data (CII)',
|
|
28
|
+
creationDate: date,
|
|
29
|
+
modificationDate: date,
|
|
30
|
+
afRelationship: AFRelationship.Alternative,
|
|
31
|
+
});
|
|
32
|
+
const xmp = new TextEncoder().encode(buildXmpMetadata({ profile, ...options, date }));
|
|
33
|
+
const stream = doc.context.stream(xmp, { Type: 'Metadata', Subtype: 'XML', Length: xmp.length });
|
|
34
|
+
doc.catalog.set(PDFName.of('Metadata'), doc.context.register(stream));
|
|
35
|
+
if (options.title)
|
|
36
|
+
doc.setTitle(options.title);
|
|
37
|
+
if (options.creator)
|
|
38
|
+
doc.setCreator(options.creator);
|
|
39
|
+
doc.setProducer(options.producer ?? '@ekwo-ai/factur-x');
|
|
40
|
+
doc.setModificationDate(date);
|
|
41
|
+
return doc.save({ useObjectStreams: false });
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Reads the embedded invoice XML (`factur-x.xml` or `zugferd-invoice.xml`)
|
|
45
|
+
* from a PDF. Returns `null` when the file carries none.
|
|
46
|
+
*/
|
|
47
|
+
export async function extractFacturX(pdf) {
|
|
48
|
+
const doc = await PDFDocument.load(pdf, { updateMetadata: false, ignoreEncryption: true });
|
|
49
|
+
const names = doc.catalog.lookupMaybe(PDFName.of('Names'), PDFDict);
|
|
50
|
+
const embedded = names?.lookupMaybe(PDFName.of('EmbeddedFiles'), PDFDict);
|
|
51
|
+
if (!embedded)
|
|
52
|
+
return null;
|
|
53
|
+
const wanted = new Set([FACTURX_FILENAME, ZUGFERD_FILENAME]);
|
|
54
|
+
for (const [name, spec] of walkNameTree(doc, embedded)) {
|
|
55
|
+
if (!wanted.has(name))
|
|
56
|
+
continue;
|
|
57
|
+
const ef = spec.lookupMaybe(PDFName.of('EF'), PDFDict);
|
|
58
|
+
const candidate = ef?.lookup(PDFName.of('UF')) ?? ef?.lookup(PDFName.of('F'));
|
|
59
|
+
if (!(candidate instanceof PDFRawStream))
|
|
60
|
+
continue;
|
|
61
|
+
const stream = candidate;
|
|
62
|
+
const bytes = decodePDFRawStream(stream).decode();
|
|
63
|
+
return { filename: name, xml: new TextDecoder('utf-8').decode(bytes) };
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/** Yields `[fileName, fileSpecification]` pairs of an EmbeddedFiles name tree. */
|
|
68
|
+
function* walkNameTree(doc, node) {
|
|
69
|
+
const names = node.lookupMaybe(PDFName.of('Names'), PDFArray);
|
|
70
|
+
if (names) {
|
|
71
|
+
for (let i = 0; i + 1 < names.size(); i += 2) {
|
|
72
|
+
const spec = names.lookup(i + 1);
|
|
73
|
+
if (!(spec instanceof PDFDict))
|
|
74
|
+
continue;
|
|
75
|
+
const text = (o) => (o instanceof PDFString || o instanceof PDFHexString ? o.decodeText() : undefined);
|
|
76
|
+
const name = text(spec.lookup(PDFName.of('UF'))) ?? text(spec.lookup(PDFName.of('F'))) ?? text(names.lookup(i)) ?? '';
|
|
77
|
+
yield [name, spec];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const kids = node.lookupMaybe(PDFName.of('Kids'), PDFArray);
|
|
81
|
+
if (kids) {
|
|
82
|
+
for (let i = 0; i < kids.size(); i++) {
|
|
83
|
+
const kid = kids.lookupMaybe(i, PDFDict);
|
|
84
|
+
if (kid)
|
|
85
|
+
yield* walkNameTree(doc, kid);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=pdf.js.map
|
package/dist/pdf.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdf.js","sourceRoot":"","sources":["../src/pdf.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one rounding rule of Ekwo: **half up, on the absolute value, at the
|
|
3
|
+
* currency's decimals**.
|
|
4
|
+
*
|
|
5
|
+
* Three packages used to answer this differently — `Math.round((v + EPSILON) *
|
|
6
|
+
* 100) / 100`, `Math.round(v * 100) / 100` and `Number.prototype.toFixed(2)` —
|
|
7
|
+
* and the three disagreed on the two cases that matter: a negative half, where
|
|
8
|
+
* `Math.round` goes towards positive infinity and turns -0.005 into -0.00, and
|
|
9
|
+
* a value a binary float cannot hold, where 2.675 becomes 2.67 because
|
|
10
|
+
* `2.675 * 100` is really 267.49999999999994. A credit note is a sale with the
|
|
11
|
+
* sign flipped; a rule that treats the two differently puts a cent between an
|
|
12
|
+
* invoice and its credit note, and the cent is found by the person filing the
|
|
13
|
+
* return.
|
|
14
|
+
*
|
|
15
|
+
* Half up on the absolute value is what an invoice, a VAT return and a set of
|
|
16
|
+
* annual accounts are written with, and it is symmetric by construction: the
|
|
17
|
+
* rounding of -x is the rounding of x with the sign put back.
|
|
18
|
+
*
|
|
19
|
+
* The file is deliberately identical in every package that holds a copy. A
|
|
20
|
+
* format brick may not import the core or another brick, so the rule is
|
|
21
|
+
* duplicated rather than shared — and `tests/rounding.test.ts` reads the
|
|
22
|
+
* copies and refuses one that has drifted. See `docs/decisions.md`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function roundCurrency(value: number, decimals?: number): number;
|
|
25
|
+
//# sourceMappingURL=rounding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rounding.d.ts","sourceRoot":"","sources":["../src/rounding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAUjE"}
|
package/dist/rounding.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one rounding rule of Ekwo: **half up, on the absolute value, at the
|
|
3
|
+
* currency's decimals**.
|
|
4
|
+
*
|
|
5
|
+
* Three packages used to answer this differently — `Math.round((v + EPSILON) *
|
|
6
|
+
* 100) / 100`, `Math.round(v * 100) / 100` and `Number.prototype.toFixed(2)` —
|
|
7
|
+
* and the three disagreed on the two cases that matter: a negative half, where
|
|
8
|
+
* `Math.round` goes towards positive infinity and turns -0.005 into -0.00, and
|
|
9
|
+
* a value a binary float cannot hold, where 2.675 becomes 2.67 because
|
|
10
|
+
* `2.675 * 100` is really 267.49999999999994. A credit note is a sale with the
|
|
11
|
+
* sign flipped; a rule that treats the two differently puts a cent between an
|
|
12
|
+
* invoice and its credit note, and the cent is found by the person filing the
|
|
13
|
+
* return.
|
|
14
|
+
*
|
|
15
|
+
* Half up on the absolute value is what an invoice, a VAT return and a set of
|
|
16
|
+
* annual accounts are written with, and it is symmetric by construction: the
|
|
17
|
+
* rounding of -x is the rounding of x with the sign put back.
|
|
18
|
+
*
|
|
19
|
+
* The file is deliberately identical in every package that holds a copy. A
|
|
20
|
+
* format brick may not import the core or another brick, so the rule is
|
|
21
|
+
* duplicated rather than shared — and `tests/rounding.test.ts` reads the
|
|
22
|
+
* copies and refuses one that has drifted. See `docs/decisions.md`.
|
|
23
|
+
*/
|
|
24
|
+
export function roundCurrency(value, decimals = 2) {
|
|
25
|
+
const factor = 10 ** decimals;
|
|
26
|
+
const scaled = Math.abs(value) * factor;
|
|
27
|
+
// The nudge is proportional to the number, which is what makes it work at
|
|
28
|
+
// 1.005 and at 1000000.005 alike, and far too small to move a value that is
|
|
29
|
+
// not already within a bit of the halfway point.
|
|
30
|
+
const rounded = Math.round(scaled + Number.EPSILON * scaled);
|
|
31
|
+
// `-0` is a value of its own in JavaScript and it prints as "-0.00".
|
|
32
|
+
// Adding zero turns it back into `0`.
|
|
33
|
+
return (value < 0 ? -rounded : rounded) / factor + 0;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=rounding.js.map
|