@open-nav/invoicing 0.1.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/LICENSE +29 -0
- package/README.md +190 -0
- package/dist/color.d.ts +31 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +150 -0
- package/dist/color.js.map +1 -0
- package/dist/export.d.ts +89 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +115 -0
- package/dist/export.js.map +1 -0
- package/dist/format.d.ts +23 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +52 -0
- package/dist/format.js.map +1 -0
- package/dist/html.d.ts +43 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +355 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.d.ts +8 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +199 -0
- package/dist/labels.js.map +1 -0
- package/dist/markings.d.ts +28 -0
- package/dist/markings.d.ts.map +1 -0
- package/dist/markings.js +69 -0
- package/dist/markings.js.map +1 -0
- package/dist/pdf-native.d.ts +32 -0
- package/dist/pdf-native.d.ts.map +1 -0
- package/dist/pdf-native.js +601 -0
- package/dist/pdf-native.js.map +1 -0
- package/dist/pdf.d.ts +92 -0
- package/dist/pdf.d.ts.map +1 -0
- package/dist/pdf.js +203 -0
- package/dist/pdf.js.map +1 -0
- package/dist/theme.d.ts +85 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +210 -0
- package/dist/theme.js.map +1 -0
- package/package.json +54 -0
- package/src/color.ts +170 -0
- package/src/export.ts +204 -0
- package/src/format.ts +66 -0
- package/src/html.ts +446 -0
- package/src/index.ts +9 -0
- package/src/labels.ts +222 -0
- package/src/markings.ts +102 -0
- package/src/pdf-native.ts +754 -0
- package/src/pdf.ts +302 -0
- package/src/theme.ts +305 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 open-nav contributors
|
|
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.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This repository vendors XML schemas, sample documents and validation message
|
|
26
|
+
catalogues published by the Hungarian Tax and Customs Administration
|
|
27
|
+
(Nemzeti Adó- és Vámhivatal) under the MIT license. Those files live under
|
|
28
|
+
`schemas/` and `conformance/`, retain NAV's copyright, and are covered by
|
|
29
|
+
`schemas/NAV-LICENCE.md`. See `schemas/README.md` for provenance.
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# @open-nav/invoicing
|
|
2
|
+
|
|
3
|
+
Turn NAV invoice data into the two documents an invoicing program has to be
|
|
4
|
+
able to produce: a printable invoice, and the tax authority data export.
|
|
5
|
+
|
|
6
|
+
## The printable invoice
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
open-nav render invoice.xml --pdf invoice.pdf --theme theme.json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { renderInvoicePdf, renderInvoiceHtml, loadTheme } from '@open-nav/invoicing';
|
|
14
|
+
|
|
15
|
+
const theme = loadTheme('theme.json');
|
|
16
|
+
const pdf = await renderInvoicePdf(invoice, { theme }); // Buffer
|
|
17
|
+
const html = renderInvoiceHtml(invoice, { theme }); // string
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Styling
|
|
21
|
+
|
|
22
|
+
Everything visual comes from a theme, which can live in a JSON file next to
|
|
23
|
+
its logo — see [`examples/invoice-theme.json`](../../examples/invoice-theme.json).
|
|
24
|
+
|
|
25
|
+
| Field | Purpose |
|
|
26
|
+
| ----------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
27
|
+
| `accentColor` | Title, the grand total, the rule above it |
|
|
28
|
+
| `inkColor`, `mutedColor`, `panelColor`, `borderColor` | The rest of the palette |
|
|
29
|
+
| `fontFamily`, `baseFontSize` | Typography; everything scales from the base size |
|
|
30
|
+
| `pageSize`, `pageMargin` | `@page` setup, e.g. `A4` and `16mm 14mm` |
|
|
31
|
+
| `logo` / `logoFile` | Header logo. `logoFile` is read relative to the theme file and inlined |
|
|
32
|
+
| `issuerContact` | Extra lines in the supplier block: phone, email, web |
|
|
33
|
+
| `footerLines` | Footer: payment terms, company registration, bank details |
|
|
34
|
+
| `zebraRows` | Tint alternate table rows |
|
|
35
|
+
| `provenanceNote` | Whether to print the "rendered from reported data" line |
|
|
36
|
+
| `customCss` | Appended after the generated stylesheet. The escape hatch |
|
|
37
|
+
|
|
38
|
+
Theme values end up inside the stylesheet, so they are **validated rather
|
|
39
|
+
than interpolated**: a colour must look like a CSS colour, a font stack may
|
|
40
|
+
not contain parentheses (no font name does, and allowing them would admit
|
|
41
|
+
`url(...)`), and a logo must be a `data:` URI or an `http(s)` URL. A bad value
|
|
42
|
+
names the field it came from.
|
|
43
|
+
|
|
44
|
+
`--logo` on the command line overrides the theme's, so one branded theme can
|
|
45
|
+
be shared and a single document still overridden.
|
|
46
|
+
|
|
47
|
+
### How the PDF is made, and why
|
|
48
|
+
|
|
49
|
+
`ő` and `ű` are the whole difficulty. Both lie outside WinAnsi, the encoding
|
|
50
|
+
the PDF core fonts use, so **a font has to be embedded** for a Hungarian
|
|
51
|
+
invoice to spell itself. (This is not hypothetical: of the fonts installed on
|
|
52
|
+
the machine this was developed on, one — Loma — is missing exactly those four
|
|
53
|
+
characters and nothing else.)
|
|
54
|
+
|
|
55
|
+
There are two engines, and the default needs nothing installed.
|
|
56
|
+
|
|
57
|
+
**`native`** — the default. pdfmake lays the document out, pdfkit writes the
|
|
58
|
+
PDF, and the Roboto files pdfmake bundles supply the glyphs. Roboto covers
|
|
59
|
+
Latin Extended-A, is Apache-2.0, and embeds as a subset with a correct
|
|
60
|
+
ToUnicode map, so the text stays searchable and copyable. Output is around a
|
|
61
|
+
quarter the size of a browser's.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const pdf = await renderInvoicePdf(invoice, { theme }); // native
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Bring your own font if the brand needs one:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const pdf = await renderInvoicePdf(invoice, {
|
|
71
|
+
theme,
|
|
72
|
+
font: { name: 'Inter', normal: 'fonts/Inter-Regular.ttf', bold: 'fonts/Inter-SemiBold.ttf' },
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**`browser`** — renders the HTML document and converts it with Chrome,
|
|
77
|
+
Chromium or Edge. It follows the HTML exactly, which matters if you have
|
|
78
|
+
styled it with `customCss` beyond what the native layout reproduces.
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
open-nav render invoice.xml --pdf invoice.pdf --engine browser --no-sandbox
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`findBrowser()` locates one: an explicit `OPEN_NAV_BROWSER`, `CHROME_PATH` or
|
|
85
|
+
`PUPPETEER_EXECUTABLE_PATH`, then a Playwright install, then the usual
|
|
86
|
+
locations, then `PATH` — preferring a full build over a headless shell, which
|
|
87
|
+
cannot print. Or supply `convert` and use Playwright directly:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { chromium } from 'playwright';
|
|
91
|
+
|
|
92
|
+
const convert = async (html: string) => {
|
|
93
|
+
const browser = await chromium.launch();
|
|
94
|
+
const page = await browser.newPage();
|
|
95
|
+
await page.setContent(html, { waitUntil: 'load' });
|
|
96
|
+
const pdf = await page.pdf({ printBackground: true });
|
|
97
|
+
await browser.close();
|
|
98
|
+
return pdf;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const pdf = await renderInvoicePdf(invoice, { convert }); // implies browser
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**The browser sandbox stays on by default.** Invoice data is input, and the
|
|
105
|
+
sandbox is what contains a malicious document, so conversion fails as root —
|
|
106
|
+
usual in a container. Pass `sandbox: false`, or `--no-sandbox`, deliberately.
|
|
107
|
+
|
|
108
|
+
The native engine has no such exposure: it fetches nothing and reads nothing
|
|
109
|
+
but the font files it registered, both enforced through pdfmake's access
|
|
110
|
+
policies rather than left at their permissive defaults.
|
|
111
|
+
|
|
112
|
+
### The two engines are two layouts
|
|
113
|
+
|
|
114
|
+
They follow the same theme, but the native engine builds the page from
|
|
115
|
+
pdfmake's document model rather than CSS, so **they will not match pixel for
|
|
116
|
+
pixel**. Known differences: party boxes have square corners rather than
|
|
117
|
+
rounded, an SVG logo is skipped because pdfmake cannot rasterise one (use PNG
|
|
118
|
+
or JPEG), and `customCss` applies only to HTML. The native engine adds page
|
|
119
|
+
numbers when a document runs to more than one page.
|
|
120
|
+
|
|
121
|
+
The HTML has no external references — no fonts, scripts or images fetched — so
|
|
122
|
+
it renders the same offline and archives as a single file. Keep the logo
|
|
123
|
+
inlined for that reason; a logo fetched over the network is the usual cause of
|
|
124
|
+
a conversion that hangs, or a PDF that silently comes out logo-less.
|
|
125
|
+
|
|
126
|
+
### It derives the markings the law requires
|
|
127
|
+
|
|
128
|
+
Section 169 of the VAT Act prescribes more than figures. An exempt supply must
|
|
129
|
+
state its legal ground, a domestic reverse charge must say _fordított adózás_,
|
|
130
|
+
cash accounting must say _pénzforgalmi elszámolás_, a margin scheme must name
|
|
131
|
+
which one. These are derived from the invoice data and printed with the
|
|
132
|
+
provision they come from, because a missing phrase makes an invoice defective
|
|
133
|
+
even when every number on it is right:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
JOGSZABÁLYI JELÖLÉSEK
|
|
137
|
+
• fordított adózás (Áfa tv. 169. § n))
|
|
138
|
+
• különbözet szerinti szabályozás – használt cikkek (Áfa tv. 169. § o) p))
|
|
139
|
+
• Adómentes — TAM — Mentes ÁFA tv. 85.§ (1) i) (Áfa tv. 169. § m))
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Also handled: simplified and aggregate invoices get their own title, a
|
|
143
|
+
modification document names the invoice it amends, a foreign-currency invoice
|
|
144
|
+
shows the rate and the forint total, a private person is never named, and a
|
|
145
|
+
batch document renders each invoice on its own page.
|
|
146
|
+
|
|
147
|
+
Amounts are formatted from the exact decimal strings, never through a
|
|
148
|
+
JavaScript number, so an 18 digit total survives to the page.
|
|
149
|
+
|
|
150
|
+
## The data export
|
|
151
|
+
|
|
152
|
+
Decree **23/2014. (VI. 30.) NGM** requires every invoicing program to have a
|
|
153
|
+
built-in _adóhatósági ellenőrzési adatszolgáltatás_ function that exports the
|
|
154
|
+
invoices issued in a date range, or in an invoice number range, as XML.
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
open-nav export invoices/*.xml --out export/ --from 2024-01-01 --to 2024-12-31
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Two structures are permitted. Annexes 2 and 3 of the decree define one
|
|
161
|
+
(`szamla.xsd`); **section 13/A(1)** permits the other — the structure
|
|
162
|
+
published for the online invoice data service, which is `invoiceData.xsd`.
|
|
163
|
+
This produces the second, because it is the schema this project already
|
|
164
|
+
generates, validates and round-trips against NAV's own documents.
|
|
165
|
+
|
|
166
|
+
That choice belongs to the taxpayer. **If your auditor asks for the Annex 3
|
|
167
|
+
structure specifically, this export is not it.**
|
|
168
|
+
|
|
169
|
+
The export writes one XML per invoice plus a `manifest.json` recording what
|
|
170
|
+
was selected, the structure used with the provision that permits it, and the
|
|
171
|
+
SHA-256 of every file — so the export can later be shown to be the one that
|
|
172
|
+
was produced.
|
|
173
|
+
|
|
174
|
+
### Invoice number ranges order naturally
|
|
175
|
+
|
|
176
|
+
Selecting `2024/1` to `2024/10` by plain text comparison excludes `2024/9`,
|
|
177
|
+
because `"2024/9" > "2024/10"` as text. Digit runs are therefore compared as
|
|
178
|
+
numbers by default, so a requested range does not silently lose invoices from
|
|
179
|
+
the middle of it. Pass `compareInvoiceNumbers` if your numbering needs
|
|
180
|
+
something else.
|
|
181
|
+
|
|
182
|
+
## What this does not claim
|
|
183
|
+
|
|
184
|
+
It does not make anyone's invoicing program compliant. Gapless numbering,
|
|
185
|
+
immutable storage and the audit trail are the issuer's obligations, and a
|
|
186
|
+
rendered document is a presentation of reported data, not the legal original.
|
|
187
|
+
|
|
188
|
+
## Licence
|
|
189
|
+
|
|
190
|
+
MIT. Not affiliated with NAV.
|
package/dist/color.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colour conversion for the PDF engine.
|
|
3
|
+
*
|
|
4
|
+
* A browser accepts every CSS colour form; PDF does not — pdfkit understands
|
|
5
|
+
* hex and the CSS colour keywords. Converting here means a theme behaves the
|
|
6
|
+
* same whichever engine renders it, rather than silently losing a colour in
|
|
7
|
+
* one of them.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Convert any accepted theme colour to `#rrggbb`.
|
|
11
|
+
*
|
|
12
|
+
* Alpha is dropped: PDF has no per-colour alpha in this position, and a
|
|
13
|
+
* silently half-transparent rule is worse than an opaque one.
|
|
14
|
+
*/
|
|
15
|
+
export declare function toHexColor(value: string, fallback?: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Convert a CSS length to PDF points.
|
|
18
|
+
*
|
|
19
|
+
* `em`, `rem` and `%` are relative to a layout that does not exist here, so
|
|
20
|
+
* they fall back rather than being guessed at.
|
|
21
|
+
*/
|
|
22
|
+
export declare function toPoints(value: string, fallback: number): number;
|
|
23
|
+
/**
|
|
24
|
+
* Expand a CSS margin shorthand into PDF's `[left, top, right, bottom]`.
|
|
25
|
+
*
|
|
26
|
+
* CSS orders shorthand clockwise from the top; pdfmake starts at the left.
|
|
27
|
+
* Getting that wrong swaps the margins on a non-square page, which is exactly
|
|
28
|
+
* the kind of error nobody notices until something prints.
|
|
29
|
+
*/
|
|
30
|
+
export declare function toMargins(value: string, fallback: number): [number, number, number, number];
|
|
31
|
+
//# sourceMappingURL=color.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../src/color.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAuBH;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAY,GAAG,MAAM,CA+BtE;AAqED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQhE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAU3F"}
|
package/dist/color.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colour conversion for the PDF engine.
|
|
3
|
+
*
|
|
4
|
+
* A browser accepts every CSS colour form; PDF does not — pdfkit understands
|
|
5
|
+
* hex and the CSS colour keywords. Converting here means a theme behaves the
|
|
6
|
+
* same whichever engine renders it, rather than silently losing a colour in
|
|
7
|
+
* one of them.
|
|
8
|
+
*/
|
|
9
|
+
/** CSS colour keywords, kept to the ones a document is plausibly styled with. */
|
|
10
|
+
const KEYWORDS = {
|
|
11
|
+
black: '#000000',
|
|
12
|
+
white: '#ffffff',
|
|
13
|
+
red: '#ff0000',
|
|
14
|
+
green: '#008000',
|
|
15
|
+
blue: '#0000ff',
|
|
16
|
+
navy: '#000080',
|
|
17
|
+
teal: '#008080',
|
|
18
|
+
olive: '#808000',
|
|
19
|
+
purple: '#800080',
|
|
20
|
+
maroon: '#800000',
|
|
21
|
+
gray: '#808080',
|
|
22
|
+
grey: '#808080',
|
|
23
|
+
silver: '#c0c0c0',
|
|
24
|
+
orange: '#ffa500',
|
|
25
|
+
yellow: '#ffff00',
|
|
26
|
+
rebeccapurple: '#663399',
|
|
27
|
+
transparent: '#ffffff',
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Convert any accepted theme colour to `#rrggbb`.
|
|
31
|
+
*
|
|
32
|
+
* Alpha is dropped: PDF has no per-colour alpha in this position, and a
|
|
33
|
+
* silently half-transparent rule is worse than an opaque one.
|
|
34
|
+
*/
|
|
35
|
+
export function toHexColor(value, fallback = '#000000') {
|
|
36
|
+
const input = value.trim().toLowerCase();
|
|
37
|
+
const keyword = KEYWORDS[input];
|
|
38
|
+
if (keyword)
|
|
39
|
+
return keyword;
|
|
40
|
+
if (input.startsWith('#')) {
|
|
41
|
+
const digits = input.slice(1);
|
|
42
|
+
if (digits.length === 3 || digits.length === 4) {
|
|
43
|
+
const [r, g, b] = [...digits.slice(0, 3)];
|
|
44
|
+
return `#${r}${r}${g}${g}${b}${b}`;
|
|
45
|
+
}
|
|
46
|
+
if (digits.length === 6 || digits.length === 8)
|
|
47
|
+
return `#${digits.slice(0, 6)}`;
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
const rgb = /^rgba?\(([^)]+)\)$/.exec(input);
|
|
51
|
+
if (rgb) {
|
|
52
|
+
const parts = splitComponents(rgb[1]);
|
|
53
|
+
if (parts.length < 3)
|
|
54
|
+
return fallback;
|
|
55
|
+
return hex(channel(parts[0]), channel(parts[1]), channel(parts[2]));
|
|
56
|
+
}
|
|
57
|
+
const hsl = /^hsla?\(([^)]+)\)$/.exec(input);
|
|
58
|
+
if (hsl) {
|
|
59
|
+
const parts = splitComponents(hsl[1]);
|
|
60
|
+
if (parts.length < 3)
|
|
61
|
+
return fallback;
|
|
62
|
+
return hslToHex(angle(parts[0]), percent(parts[1]), percent(parts[2]));
|
|
63
|
+
}
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
function splitComponents(body) {
|
|
67
|
+
return body
|
|
68
|
+
.split(/[,\s/]+/)
|
|
69
|
+
.map((part) => part.trim())
|
|
70
|
+
.filter((part) => part !== '');
|
|
71
|
+
}
|
|
72
|
+
/** An rgb channel: 0-255, or a percentage. */
|
|
73
|
+
function channel(part) {
|
|
74
|
+
const value = part.endsWith('%')
|
|
75
|
+
? (Number.parseFloat(part) / 100) * 255
|
|
76
|
+
: Number.parseFloat(part);
|
|
77
|
+
return clamp(Math.round(value), 0, 255);
|
|
78
|
+
}
|
|
79
|
+
function angle(part) {
|
|
80
|
+
const value = Number.parseFloat(part);
|
|
81
|
+
return Number.isFinite(value) ? ((value % 360) + 360) % 360 : 0;
|
|
82
|
+
}
|
|
83
|
+
function percent(part) {
|
|
84
|
+
const value = Number.parseFloat(part);
|
|
85
|
+
return clamp(Number.isFinite(value) ? value / 100 : 0, 0, 1);
|
|
86
|
+
}
|
|
87
|
+
function clamp(value, min, max) {
|
|
88
|
+
return Math.min(Math.max(value, min), max);
|
|
89
|
+
}
|
|
90
|
+
function hex(r, g, b) {
|
|
91
|
+
return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`;
|
|
92
|
+
}
|
|
93
|
+
function hslToHex(hue, saturation, lightness) {
|
|
94
|
+
const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
|
|
95
|
+
const sector = hue / 60;
|
|
96
|
+
const second = chroma * (1 - Math.abs((sector % 2) - 1));
|
|
97
|
+
const [r, g, b] = sector < 1
|
|
98
|
+
? [chroma, second, 0]
|
|
99
|
+
: sector < 2
|
|
100
|
+
? [second, chroma, 0]
|
|
101
|
+
: sector < 3
|
|
102
|
+
? [0, chroma, second]
|
|
103
|
+
: sector < 4
|
|
104
|
+
? [0, second, chroma]
|
|
105
|
+
: sector < 5
|
|
106
|
+
? [second, 0, chroma]
|
|
107
|
+
: [chroma, 0, second];
|
|
108
|
+
const offset = lightness - chroma / 2;
|
|
109
|
+
return hex(Math.round((r + offset) * 255), Math.round((g + offset) * 255), Math.round((b + offset) * 255));
|
|
110
|
+
}
|
|
111
|
+
/** Units CSS uses that convert to PDF points without a layout context. */
|
|
112
|
+
const UNIT_TO_POINTS = {
|
|
113
|
+
pt: 1,
|
|
114
|
+
px: 0.75,
|
|
115
|
+
pc: 12,
|
|
116
|
+
mm: 72 / 25.4,
|
|
117
|
+
cm: 72 / 2.54,
|
|
118
|
+
in: 72,
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Convert a CSS length to PDF points.
|
|
122
|
+
*
|
|
123
|
+
* `em`, `rem` and `%` are relative to a layout that does not exist here, so
|
|
124
|
+
* they fall back rather than being guessed at.
|
|
125
|
+
*/
|
|
126
|
+
export function toPoints(value, fallback) {
|
|
127
|
+
const match = /^([0-9.]+)([a-z%]*)$/.exec(value.trim());
|
|
128
|
+
if (!match)
|
|
129
|
+
return fallback;
|
|
130
|
+
const size = Number.parseFloat(match[1]);
|
|
131
|
+
if (!Number.isFinite(size))
|
|
132
|
+
return fallback;
|
|
133
|
+
const unit = match[2] === '' ? 'pt' : match[2];
|
|
134
|
+
const factor = UNIT_TO_POINTS[unit];
|
|
135
|
+
return factor === undefined ? fallback : size * factor;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Expand a CSS margin shorthand into PDF's `[left, top, right, bottom]`.
|
|
139
|
+
*
|
|
140
|
+
* CSS orders shorthand clockwise from the top; pdfmake starts at the left.
|
|
141
|
+
* Getting that wrong swaps the margins on a non-square page, which is exactly
|
|
142
|
+
* the kind of error nobody notices until something prints.
|
|
143
|
+
*/
|
|
144
|
+
export function toMargins(value, fallback) {
|
|
145
|
+
const parts = value.trim().split(/\s+/).filter(Boolean);
|
|
146
|
+
const points = parts.map((part) => toPoints(part, fallback));
|
|
147
|
+
const [top = fallback, right = points[0] ?? fallback, bottom = points[0] ?? fallback, left = points[1] ?? points[0] ?? fallback,] = points;
|
|
148
|
+
return [left, top, right, bottom];
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=color.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"color.js","sourceRoot":"","sources":["../src/color.ts"],"names":[],"mappings":"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"}
|
package/dist/export.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type InvoiceData } from '@open-nav/core';
|
|
2
|
+
/**
|
|
3
|
+
* The data export an invoicing program must be able to produce.
|
|
4
|
+
*
|
|
5
|
+
* Decree 23/2014. (VI. 30.) NGM requires every invoicing program to have a
|
|
6
|
+
* built-in "adóhatósági ellenőrzési adatszolgáltatás" function that exports
|
|
7
|
+
* the invoices issued in a given date range, or in a given invoice number
|
|
8
|
+
* range, as XML.
|
|
9
|
+
*
|
|
10
|
+
* Two structures are permitted. Annexes 2 and 3 of the decree define one
|
|
11
|
+
* (`szamla.xsd`); section 13/A(1) permits the other — the structure published
|
|
12
|
+
* for the online invoice data service, which is `invoiceData.xsd`. This
|
|
13
|
+
* implementation produces the second, because it is the same schema this
|
|
14
|
+
* project already generates, validates and round-trips against NAV's own
|
|
15
|
+
* sample documents.
|
|
16
|
+
*
|
|
17
|
+
* That choice belongs to the taxpayer, not to this library. If your auditor
|
|
18
|
+
* asks for the Annex 3 structure specifically, this export is not it.
|
|
19
|
+
*/
|
|
20
|
+
export interface DataExportSelection {
|
|
21
|
+
/** First issue date to include, inclusive, as `yyyy-mm-dd`. */
|
|
22
|
+
issueDateFrom?: string;
|
|
23
|
+
/** Last issue date to include, inclusive. */
|
|
24
|
+
issueDateTo?: string;
|
|
25
|
+
/** First invoice number to include, inclusive. */
|
|
26
|
+
invoiceNumberFrom?: string;
|
|
27
|
+
/** Last invoice number to include, inclusive. */
|
|
28
|
+
invoiceNumberTo?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface DataExportOptions extends DataExportSelection {
|
|
31
|
+
/**
|
|
32
|
+
* How invoice numbers are ordered when a number range is given.
|
|
33
|
+
*
|
|
34
|
+
* The default compares digit runs numerically, so `2024/9` sorts before
|
|
35
|
+
* `2024/10`. Plain text ordering puts them the other way round and would
|
|
36
|
+
* silently drop invoices from the middle of a requested range — which is
|
|
37
|
+
* the kind of mistake an audit is precisely designed to find.
|
|
38
|
+
*/
|
|
39
|
+
compareInvoiceNumbers?: (left: string, right: string) => number;
|
|
40
|
+
/** Injectable clock, so an export is reproducible in tests. */
|
|
41
|
+
now?: () => Date;
|
|
42
|
+
}
|
|
43
|
+
export interface ExportedFile {
|
|
44
|
+
/** Path within the export, e.g. `invoices/2024-000123.xml`. */
|
|
45
|
+
name: string;
|
|
46
|
+
contents: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ExportedInvoice {
|
|
49
|
+
invoiceNumber: string;
|
|
50
|
+
invoiceIssueDate: string;
|
|
51
|
+
file: string;
|
|
52
|
+
/** SHA-256 of the file, so the export can be shown to be unaltered. */
|
|
53
|
+
sha256: string;
|
|
54
|
+
}
|
|
55
|
+
export interface DataExportManifest {
|
|
56
|
+
/** What was asked for. */
|
|
57
|
+
selection: DataExportSelection;
|
|
58
|
+
/** The structure used, and the provision that permits it. */
|
|
59
|
+
structure: {
|
|
60
|
+
schema: string;
|
|
61
|
+
namespace: string;
|
|
62
|
+
basis: string;
|
|
63
|
+
};
|
|
64
|
+
generatedAt: string;
|
|
65
|
+
invoiceCount: number;
|
|
66
|
+
invoices: ExportedInvoice[];
|
|
67
|
+
}
|
|
68
|
+
export interface DataExportResult {
|
|
69
|
+
/** Every file in the export, manifest included. */
|
|
70
|
+
files: ExportedFile[];
|
|
71
|
+
manifest: DataExportManifest;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Natural ordering for invoice numbers: digit runs compare as numbers.
|
|
75
|
+
*
|
|
76
|
+
* `2024/9` before `2024/10`, and `A-2` before `A-10`.
|
|
77
|
+
*/
|
|
78
|
+
export declare function compareInvoiceNumbersNaturally(left: string, right: string): number;
|
|
79
|
+
/** Whether an invoice falls inside the requested selection. */
|
|
80
|
+
export declare function isSelected(invoice: InvoiceData, options: DataExportOptions): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Build the export from a set of invoices.
|
|
83
|
+
*
|
|
84
|
+
* One XML file per invoice, plus a manifest recording what was selected and
|
|
85
|
+
* the SHA-256 of every file, so the export can later be shown to be the one
|
|
86
|
+
* that was produced.
|
|
87
|
+
*/
|
|
88
|
+
export declare function createDataExport(invoices: InvoiceData[], options?: DataExportOptions): DataExportResult;
|
|
89
|
+
//# sourceMappingURL=export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../src/export.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAErE;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iDAAiD;IACjD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC5D;;;;;;;OAOG;IACH,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAChE,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,SAAS,EAAE,mBAAmB,CAAC;IAC/B,6DAA6D;IAC7D,SAAS,EAAE;QACT,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,QAAQ,EAAE,kBAAkB,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAsBlF;AAED,+DAA+D;AAC/D,wBAAgB,UAAU,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAYpF;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,GAAE,iBAAsB,GAC9B,gBAAgB,CA+ClB"}
|
package/dist/export.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { serializeDocument } from '@open-nav/core';
|
|
3
|
+
/**
|
|
4
|
+
* Natural ordering for invoice numbers: digit runs compare as numbers.
|
|
5
|
+
*
|
|
6
|
+
* `2024/9` before `2024/10`, and `A-2` before `A-10`.
|
|
7
|
+
*/
|
|
8
|
+
export function compareInvoiceNumbersNaturally(left, right) {
|
|
9
|
+
const chunk = /(\d+|\D+)/g;
|
|
10
|
+
const leftParts = left.match(chunk) ?? [];
|
|
11
|
+
const rightParts = right.match(chunk) ?? [];
|
|
12
|
+
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
|
|
13
|
+
const a = leftParts[index];
|
|
14
|
+
const b = rightParts[index];
|
|
15
|
+
if (a === undefined)
|
|
16
|
+
return -1;
|
|
17
|
+
if (b === undefined)
|
|
18
|
+
return 1;
|
|
19
|
+
const aNumeric = /^\d+$/.test(a);
|
|
20
|
+
const bNumeric = /^\d+$/.test(b);
|
|
21
|
+
if (aNumeric && bNumeric) {
|
|
22
|
+
// BigInt, because an invoice number can be longer than a safe integer.
|
|
23
|
+
const difference = BigInt(a) - BigInt(b);
|
|
24
|
+
if (difference !== 0n)
|
|
25
|
+
return difference < 0n ? -1 : 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (a !== b)
|
|
29
|
+
return a < b ? -1 : 1;
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
/** Whether an invoice falls inside the requested selection. */
|
|
34
|
+
export function isSelected(invoice, options) {
|
|
35
|
+
const compare = options.compareInvoiceNumbers ?? compareInvoiceNumbersNaturally;
|
|
36
|
+
if (options.issueDateFrom && invoice.invoiceIssueDate < options.issueDateFrom)
|
|
37
|
+
return false;
|
|
38
|
+
if (options.issueDateTo && invoice.invoiceIssueDate > options.issueDateTo)
|
|
39
|
+
return false;
|
|
40
|
+
if (options.invoiceNumberFrom && compare(invoice.invoiceNumber, options.invoiceNumberFrom) < 0) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (options.invoiceNumberTo && compare(invoice.invoiceNumber, options.invoiceNumberTo) > 0) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build the export from a set of invoices.
|
|
50
|
+
*
|
|
51
|
+
* One XML file per invoice, plus a manifest recording what was selected and
|
|
52
|
+
* the SHA-256 of every file, so the export can later be shown to be the one
|
|
53
|
+
* that was produced.
|
|
54
|
+
*/
|
|
55
|
+
export function createDataExport(invoices, options = {}) {
|
|
56
|
+
const now = options.now ?? (() => new Date());
|
|
57
|
+
const compare = options.compareInvoiceNumbers ?? compareInvoiceNumbersNaturally;
|
|
58
|
+
const selected = invoices
|
|
59
|
+
.filter((invoice) => isSelected(invoice, options))
|
|
60
|
+
.sort((left, right) => compare(left.invoiceNumber, right.invoiceNumber));
|
|
61
|
+
const files = [];
|
|
62
|
+
const entries = [];
|
|
63
|
+
const usedNames = new Set();
|
|
64
|
+
for (const invoice of selected) {
|
|
65
|
+
const name = uniqueName(`invoices/${safeFileName(invoice.invoiceNumber)}.xml`, usedNames);
|
|
66
|
+
const contents = serializeDocument('InvoiceData', invoice, { indent: ' ' });
|
|
67
|
+
files.push({ name, contents });
|
|
68
|
+
entries.push({
|
|
69
|
+
invoiceNumber: invoice.invoiceNumber,
|
|
70
|
+
invoiceIssueDate: invoice.invoiceIssueDate,
|
|
71
|
+
file: name,
|
|
72
|
+
sha256: createHash('sha256').update(contents, 'utf8').digest('hex'),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const selection = {
|
|
76
|
+
...(options.issueDateFrom ? { issueDateFrom: options.issueDateFrom } : {}),
|
|
77
|
+
...(options.issueDateTo ? { issueDateTo: options.issueDateTo } : {}),
|
|
78
|
+
...(options.invoiceNumberFrom ? { invoiceNumberFrom: options.invoiceNumberFrom } : {}),
|
|
79
|
+
...(options.invoiceNumberTo ? { invoiceNumberTo: options.invoiceNumberTo } : {}),
|
|
80
|
+
};
|
|
81
|
+
const manifest = {
|
|
82
|
+
selection,
|
|
83
|
+
structure: {
|
|
84
|
+
schema: 'invoiceData.xsd',
|
|
85
|
+
namespace: 'http://schemas.nav.gov.hu/OSA/3.0/data',
|
|
86
|
+
basis: '23/2014. (VI. 30.) NGM rendelet 13/A. § (1) — the data structure published ' +
|
|
87
|
+
'for the online invoice data service',
|
|
88
|
+
},
|
|
89
|
+
generatedAt: now().toISOString(),
|
|
90
|
+
invoiceCount: entries.length,
|
|
91
|
+
invoices: entries,
|
|
92
|
+
};
|
|
93
|
+
files.push({ name: 'manifest.json', contents: `${JSON.stringify(manifest, null, 2)}\n` });
|
|
94
|
+
return { files, manifest };
|
|
95
|
+
}
|
|
96
|
+
/** An invoice number can contain `/`, which a file name cannot. */
|
|
97
|
+
function safeFileName(invoiceNumber) {
|
|
98
|
+
const cleaned = invoiceNumber.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
99
|
+
return cleaned === '' ? 'invoice' : cleaned;
|
|
100
|
+
}
|
|
101
|
+
/** Two invoice numbers can sanitise to the same name; keep them distinct. */
|
|
102
|
+
function uniqueName(name, used) {
|
|
103
|
+
if (!used.has(name)) {
|
|
104
|
+
used.add(name);
|
|
105
|
+
return name;
|
|
106
|
+
}
|
|
107
|
+
for (let suffix = 2;; suffix += 1) {
|
|
108
|
+
const candidate = name.replace(/\.xml$/, `-${suffix}.xml`);
|
|
109
|
+
if (!used.has(candidate)) {
|
|
110
|
+
used.add(candidate);
|
|
111
|
+
return candidate;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=export.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export.js","sourceRoot":"","sources":["../src/export.ts"],"names":[],"mappings":"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"}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type DocumentLanguage = 'hu' | 'en';
|
|
2
|
+
/**
|
|
3
|
+
* Format an exact decimal for display without going through a JavaScript
|
|
4
|
+
* number.
|
|
5
|
+
*
|
|
6
|
+
* The amounts on an invoice are exact decimal strings precisely so they do
|
|
7
|
+
* not lose precision; rendering them through `Number` to get thousands
|
|
8
|
+
* separators would throw that away at the top of NAV's 18 digit range.
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatAmount(value: string, language: DocumentLanguage, fractionDigits?: number): string;
|
|
11
|
+
/** Format a VAT rate like `0.27` as a percentage. */
|
|
12
|
+
export declare function formatPercentage(value: string, language: DocumentLanguage): string;
|
|
13
|
+
/** Dates are ISO in the data; Hungarian documents write them with dots. */
|
|
14
|
+
export declare function formatDate(value: string, language: DocumentLanguage): string;
|
|
15
|
+
/** Compose a tax number from its parts, in the written form. */
|
|
16
|
+
export declare function formatTaxNumber(parts: {
|
|
17
|
+
taxpayerId: string;
|
|
18
|
+
vatCode?: string;
|
|
19
|
+
countyCode?: string;
|
|
20
|
+
}): string;
|
|
21
|
+
/** Escape text for HTML. */
|
|
22
|
+
export declare function escapeHtml(value: string): string;
|
|
23
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3C;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,gBAAgB,EAC1B,cAAc,SAAI,GACjB,MAAM,CAaR;AAED,qDAAqD;AACrD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,GAAG,MAAM,CAOlF;AAED,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,GAAG,MAAM,CAK5E;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,MAAM,CAET;AAED,4BAA4B;AAC5B,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMhD"}
|