@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/src/html.ts
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import type { AddressType, InvoiceData, InvoiceType, LineType, VatRateType } from '@open-nav/core';
|
|
2
|
+
import {
|
|
3
|
+
escapeHtml,
|
|
4
|
+
formatAmount,
|
|
5
|
+
formatDate,
|
|
6
|
+
formatPercentage,
|
|
7
|
+
formatTaxNumber,
|
|
8
|
+
type DocumentLanguage,
|
|
9
|
+
} from './format.js';
|
|
10
|
+
import { documentTitle, label, paymentMethodLabel, unitLabel } from './labels.js';
|
|
11
|
+
import { deriveMarkings } from './markings.js';
|
|
12
|
+
import { buildStyles, resolveTheme, type InvoiceTheme, type ResolvedTheme } from './theme.js';
|
|
13
|
+
|
|
14
|
+
export interface RenderOptions {
|
|
15
|
+
/** Document language. Hungarian by default, as the law prescribes. */
|
|
16
|
+
language?: DocumentLanguage;
|
|
17
|
+
/** Extra note printed under the totals, e.g. payment instructions. */
|
|
18
|
+
note?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Branding and layout: logo, colours, fonts, page setup, footer.
|
|
21
|
+
*
|
|
22
|
+
* See {@link InvoiceTheme}. Anything omitted falls back to a restrained
|
|
23
|
+
* default that prints well in black and white.
|
|
24
|
+
*/
|
|
25
|
+
theme?: InvoiceTheme;
|
|
26
|
+
/**
|
|
27
|
+
* State that the document was rendered from reported data.
|
|
28
|
+
*
|
|
29
|
+
* On by default. An invoice's legal original is whatever the issuer issued;
|
|
30
|
+
* this rendering is a faithful presentation of the reported data, and
|
|
31
|
+
* saying so avoids passing it off as something it is not. Can also be set
|
|
32
|
+
* through the theme.
|
|
33
|
+
*/
|
|
34
|
+
provenanceNote?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render an invoice as a self-contained, printable HTML document.
|
|
39
|
+
*
|
|
40
|
+
* HTML rather than PDF on purpose. A Hungarian invoice needs `ő` and `ű`,
|
|
41
|
+
* which are outside the WinAnsi encoding the PDF core fonts use, so a
|
|
42
|
+
* dependency-free PDF would need a bundled and licensed font to spell the
|
|
43
|
+
* language correctly. HTML gets the typography right, needs nothing bundled,
|
|
44
|
+
* and turns into a PDF with any browser:
|
|
45
|
+
*
|
|
46
|
+
* ```sh
|
|
47
|
+
* chromium --headless --print-to-pdf=invoice.pdf invoice.html
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* The output carries no external references, so it renders identically
|
|
51
|
+
* offline and archives as a single file.
|
|
52
|
+
*/
|
|
53
|
+
export function renderInvoiceHtml(document: InvoiceData, options: RenderOptions = {}): string {
|
|
54
|
+
const language = options.language ?? 'hu';
|
|
55
|
+
const theme = resolveTheme(options.theme);
|
|
56
|
+
const invoices = document.invoiceMain.invoice
|
|
57
|
+
? [document.invoiceMain.invoice]
|
|
58
|
+
: (document.invoiceMain.batchInvoice ?? []).map((entry) => entry.invoice);
|
|
59
|
+
|
|
60
|
+
const body = invoices
|
|
61
|
+
.map((invoice) => renderInvoice(document, invoice, language, options, theme))
|
|
62
|
+
.join('\n<div class="page-break"></div>\n');
|
|
63
|
+
|
|
64
|
+
return `<!doctype html>
|
|
65
|
+
<html lang="${language}">
|
|
66
|
+
<head>
|
|
67
|
+
<meta charset="utf-8">
|
|
68
|
+
<title>${escapeHtml(document.invoiceNumber)}</title>
|
|
69
|
+
${buildStyles(theme)}
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
${body}
|
|
73
|
+
</body>
|
|
74
|
+
</html>
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function renderInvoice(
|
|
79
|
+
document: InvoiceData,
|
|
80
|
+
invoice: InvoiceType,
|
|
81
|
+
language: DocumentLanguage,
|
|
82
|
+
options: RenderOptions,
|
|
83
|
+
theme: ResolvedTheme,
|
|
84
|
+
): string {
|
|
85
|
+
const detail = invoice.invoiceHead.invoiceDetail;
|
|
86
|
+
const currency = detail.currencyCode;
|
|
87
|
+
const isModification = invoice.invoiceReference !== undefined;
|
|
88
|
+
const lines = invoice.invoiceLines?.line ?? [];
|
|
89
|
+
const markings = deriveMarkings(invoice, language);
|
|
90
|
+
|
|
91
|
+
const metaRows: Array<[string, string]> = [
|
|
92
|
+
[label('invoiceNumber', language), document.invoiceNumber],
|
|
93
|
+
[label('issueDate', language), formatDate(document.invoiceIssueDate, language)],
|
|
94
|
+
];
|
|
95
|
+
if (detail.invoiceDeliveryDate) {
|
|
96
|
+
metaRows.push([
|
|
97
|
+
label('deliveryDate', language),
|
|
98
|
+
formatDate(detail.invoiceDeliveryDate, language),
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
if (detail.invoiceDeliveryPeriodStart && detail.invoiceDeliveryPeriodEnd) {
|
|
102
|
+
metaRows.push([
|
|
103
|
+
label('deliveryPeriod', language),
|
|
104
|
+
`${formatDate(detail.invoiceDeliveryPeriodStart, language)} – ${formatDate(detail.invoiceDeliveryPeriodEnd, language)}`,
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
if (detail.paymentDate) {
|
|
108
|
+
metaRows.push([label('paymentDate', language), formatDate(detail.paymentDate, language)]);
|
|
109
|
+
}
|
|
110
|
+
if (detail.paymentMethod) {
|
|
111
|
+
metaRows.push([
|
|
112
|
+
label('paymentMethod', language),
|
|
113
|
+
paymentMethodLabel(detail.paymentMethod, language),
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
metaRows.push([label('currency', language), currency]);
|
|
117
|
+
if (detail.exchangeRate && currency !== 'HUF') {
|
|
118
|
+
metaRows.push([label('exchangeRate', language), `${detail.exchangeRate} HUF/${currency}`]);
|
|
119
|
+
}
|
|
120
|
+
if (invoice.invoiceReference) {
|
|
121
|
+
metaRows.push([
|
|
122
|
+
label('originalInvoiceNumber', language),
|
|
123
|
+
invoice.invoiceReference.originalInvoiceNumber,
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const logo = theme.logo
|
|
128
|
+
? `<img class="logo" src="${escapeHtml(theme.logo.src)}" alt="${escapeHtml(theme.logo.alt ?? invoice.invoiceHead.supplierInfo.supplierName)}"` +
|
|
129
|
+
`${theme.logo.width ? ` style="width:${escapeHtml(theme.logo.width)}"` : ''}>`
|
|
130
|
+
: '';
|
|
131
|
+
|
|
132
|
+
return `<article class="invoice">
|
|
133
|
+
<header>
|
|
134
|
+
<div class="brand">
|
|
135
|
+
${logo}
|
|
136
|
+
<div>
|
|
137
|
+
<h1>${escapeHtml(documentTitle(detail.invoiceCategory, isModification, language))}</h1>
|
|
138
|
+
${markings.length > 0 ? `<div class="subtitle">${markings.map((marking) => escapeHtml(marking.text)).join(' · ')}</div>` : ''}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="meta">
|
|
142
|
+
${metaRows
|
|
143
|
+
.map(
|
|
144
|
+
([name, value]) =>
|
|
145
|
+
`<div>${escapeHtml(name)}: <span class="value">${escapeHtml(value)}</span></div>`,
|
|
146
|
+
)
|
|
147
|
+
.join('\n ')}
|
|
148
|
+
</div>
|
|
149
|
+
</header>
|
|
150
|
+
|
|
151
|
+
<section class="parties">
|
|
152
|
+
${renderParty(label('supplier', language), supplierFields(invoice, language), theme.issuerContact)}
|
|
153
|
+
${renderParty(label('customer', language), customerFields(invoice, language), [])}
|
|
154
|
+
</section>
|
|
155
|
+
|
|
156
|
+
${renderLines(lines, currency, language)}
|
|
157
|
+
${renderTotals(invoice, currency, language)}
|
|
158
|
+
${renderVatSummary(invoice, currency, language)}
|
|
159
|
+
${renderMarkings(markings, language)}
|
|
160
|
+
${options.note ? `<div class="note">${escapeHtml(options.note)}</div>` : ''}
|
|
161
|
+
${renderFooter(theme, options, language)}
|
|
162
|
+
</article>`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function renderParty(
|
|
166
|
+
heading: string,
|
|
167
|
+
fields: { name: string; address: string; rows: Array<[string, string]> },
|
|
168
|
+
contact: string[],
|
|
169
|
+
): string {
|
|
170
|
+
return `<div class="party">
|
|
171
|
+
<h2>${escapeHtml(heading)}</h2>
|
|
172
|
+
<div class="name">${escapeHtml(fields.name)}</div>
|
|
173
|
+
${fields.address ? `<div class="address">${escapeHtml(fields.address)}</div>` : ''}
|
|
174
|
+
<dl>
|
|
175
|
+
${fields.rows
|
|
176
|
+
.map(([key, value]) => `<dt>${escapeHtml(key)}</dt><dd>${escapeHtml(value)}</dd>`)
|
|
177
|
+
.join('\n ')}
|
|
178
|
+
</dl>
|
|
179
|
+
${
|
|
180
|
+
contact.length > 0
|
|
181
|
+
? `<div class="contact">${contact.map((line) => escapeHtml(line)).join('<br>')}</div>`
|
|
182
|
+
: ''
|
|
183
|
+
}
|
|
184
|
+
</div>`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The footer carries the theme's own lines plus the provenance note.
|
|
189
|
+
*
|
|
190
|
+
* `provenanceNote` can be set on either the options or the theme; the option
|
|
191
|
+
* wins, so a single document can drop it without editing shared branding.
|
|
192
|
+
*/
|
|
193
|
+
function renderFooter(
|
|
194
|
+
theme: ResolvedTheme,
|
|
195
|
+
options: RenderOptions,
|
|
196
|
+
language: DocumentLanguage,
|
|
197
|
+
): string {
|
|
198
|
+
const showProvenance = options.provenanceNote ?? theme.provenanceNote;
|
|
199
|
+
const lines = [
|
|
200
|
+
...theme.footerLines.map((line) => escapeHtml(line)),
|
|
201
|
+
...(showProvenance ? [escapeHtml(label('notReported', language))] : []),
|
|
202
|
+
];
|
|
203
|
+
if (lines.length === 0) return '';
|
|
204
|
+
return `<footer>${lines.map((line) => `<div>${line}</div>`).join('')}</footer>`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function formatAddress(address: AddressType | undefined): string {
|
|
208
|
+
if (!address) return '';
|
|
209
|
+
if ('simpleAddress' in address && address.simpleAddress) {
|
|
210
|
+
const simple = address.simpleAddress;
|
|
211
|
+
return [simple.postalCode, simple.city, simple.additionalAddressDetail, simple.countryCode]
|
|
212
|
+
.filter(Boolean)
|
|
213
|
+
.join(', ');
|
|
214
|
+
}
|
|
215
|
+
if ('detailedAddress' in address && address.detailedAddress) {
|
|
216
|
+
const detailed = address.detailedAddress;
|
|
217
|
+
const street = [
|
|
218
|
+
detailed.streetName,
|
|
219
|
+
detailed.publicPlaceCategory,
|
|
220
|
+
detailed.number,
|
|
221
|
+
detailed.building,
|
|
222
|
+
detailed.floor,
|
|
223
|
+
detailed.door,
|
|
224
|
+
]
|
|
225
|
+
.filter(Boolean)
|
|
226
|
+
.join(' ');
|
|
227
|
+
return [detailed.postalCode, detailed.city, street, detailed.countryCode]
|
|
228
|
+
.filter(Boolean)
|
|
229
|
+
.join(', ');
|
|
230
|
+
}
|
|
231
|
+
return '';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function supplierFields(
|
|
235
|
+
invoice: InvoiceType,
|
|
236
|
+
language: DocumentLanguage,
|
|
237
|
+
): { name: string; address: string; rows: Array<[string, string]> } {
|
|
238
|
+
const supplier = invoice.invoiceHead.supplierInfo;
|
|
239
|
+
const rows: Array<[string, string]> = [];
|
|
240
|
+
rows.push([label('taxNumber', language), formatTaxNumber(supplier.supplierTaxNumber)]);
|
|
241
|
+
if (supplier.groupMemberTaxNumber) {
|
|
242
|
+
rows.push([
|
|
243
|
+
label('groupMemberTaxNumber', language),
|
|
244
|
+
formatTaxNumber(supplier.groupMemberTaxNumber),
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
if (supplier.communityVatNumber) {
|
|
248
|
+
rows.push([label('communityVatNumber', language), supplier.communityVatNumber]);
|
|
249
|
+
}
|
|
250
|
+
if (supplier.supplierBankAccountNumber) {
|
|
251
|
+
rows.push([label('bankAccount', language), supplier.supplierBankAccountNumber]);
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
name: supplier.supplierName,
|
|
255
|
+
address: formatAddress(supplier.supplierAddress),
|
|
256
|
+
rows,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function customerFields(
|
|
261
|
+
invoice: InvoiceType,
|
|
262
|
+
language: DocumentLanguage,
|
|
263
|
+
): { name: string; address: string; rows: Array<[string, string]> } {
|
|
264
|
+
const customer = invoice.invoiceHead.customerInfo;
|
|
265
|
+
if (!customer) return { name: '—', address: '', rows: [] };
|
|
266
|
+
|
|
267
|
+
const rows: Array<[string, string]> = [];
|
|
268
|
+
const vatData = customer.customerVatData;
|
|
269
|
+
if (vatData?.customerTaxNumber) {
|
|
270
|
+
rows.push([label('taxNumber', language), formatTaxNumber(vatData.customerTaxNumber)]);
|
|
271
|
+
}
|
|
272
|
+
if (vatData?.communityVatNumber) {
|
|
273
|
+
rows.push([label('communityVatNumber', language), vatData.communityVatNumber]);
|
|
274
|
+
}
|
|
275
|
+
if (vatData?.thirdStateTaxId) {
|
|
276
|
+
rows.push([label('thirdStateTaxId', language), vatData.thirdStateTaxId]);
|
|
277
|
+
}
|
|
278
|
+
if (customer.customerBankAccountNumber) {
|
|
279
|
+
rows.push([label('bankAccount', language), customer.customerBankAccountNumber]);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// A private person is deliberately not named on the reported data.
|
|
283
|
+
const name =
|
|
284
|
+
customer.customerName ?? (customer.customerVatStatus === 'PRIVATE_PERSON' ? '—' : '');
|
|
285
|
+
return { name, address: formatAddress(customer.customerAddress), rows };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function vatRateText(rate: VatRateType | undefined, language: DocumentLanguage): string {
|
|
289
|
+
if (!rate) return '';
|
|
290
|
+
if (rate.vatPercentage !== undefined) return formatPercentage(rate.vatPercentage, language);
|
|
291
|
+
if (rate.vatContent !== undefined) return formatPercentage(rate.vatContent, language);
|
|
292
|
+
if (rate.vatExemption) return `${label('exempt', language)} (${rate.vatExemption.case})`;
|
|
293
|
+
if (rate.vatOutOfScope) return `${label('outOfScope', language)} (${rate.vatOutOfScope.case})`;
|
|
294
|
+
if (rate.vatDomesticReverseCharge) return label('reverseCharge', language);
|
|
295
|
+
if (rate.marginSchemeIndicator) return label('exempt', language);
|
|
296
|
+
return '';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function renderLines(lines: LineType[], currency: string, language: DocumentLanguage): string {
|
|
300
|
+
if (lines.length === 0) return '';
|
|
301
|
+
|
|
302
|
+
const rows = lines
|
|
303
|
+
.map((line) => {
|
|
304
|
+
const normal = line.lineAmountsNormal;
|
|
305
|
+
const simplified = line.lineAmountsSimplified;
|
|
306
|
+
const rate = normal?.lineVatRate ?? simplified?.lineVatRate;
|
|
307
|
+
const net = normal?.lineNetAmountData.lineNetAmount;
|
|
308
|
+
const vat = normal?.lineVatData?.lineVatAmount;
|
|
309
|
+
const gross =
|
|
310
|
+
normal?.lineGrossAmountData?.lineGrossAmountNormal ?? simplified?.lineGrossAmountSimplified;
|
|
311
|
+
|
|
312
|
+
return `<tr>
|
|
313
|
+
<td class="num">${line.lineNumber}</td>
|
|
314
|
+
<td>${escapeHtml(line.lineDescription ?? '')}</td>
|
|
315
|
+
<td class="num">${line.quantity ? escapeHtml(formatAmount(line.quantity, language, decimalsOf(line.quantity))) : ''}</td>
|
|
316
|
+
<td>${escapeHtml(unitLabel(line.unitOfMeasure, line.unitOfMeasureOwn, language))}</td>
|
|
317
|
+
<td class="num">${line.unitPrice ? escapeHtml(formatAmount(line.unitPrice, language, decimalsOf(line.unitPrice))) : ''}</td>
|
|
318
|
+
<td class="num">${net ? escapeHtml(formatAmount(net, language)) : ''}</td>
|
|
319
|
+
<td class="num">${escapeHtml(vatRateText(rate, language))}</td>
|
|
320
|
+
<td class="num">${vat ? escapeHtml(formatAmount(vat, language)) : ''}</td>
|
|
321
|
+
<td class="num">${gross ? escapeHtml(formatAmount(gross, language)) : ''}</td>
|
|
322
|
+
</tr>`;
|
|
323
|
+
})
|
|
324
|
+
.join('\n ');
|
|
325
|
+
|
|
326
|
+
return `<table>
|
|
327
|
+
<thead>
|
|
328
|
+
<tr>
|
|
329
|
+
<th class="num">${escapeHtml(label('lineNumber', language))}</th>
|
|
330
|
+
<th>${escapeHtml(label('description', language))}</th>
|
|
331
|
+
<th class="num">${escapeHtml(label('quantity', language))}</th>
|
|
332
|
+
<th>${escapeHtml(label('unit', language))}</th>
|
|
333
|
+
<th class="num">${escapeHtml(label('unitPrice', language))}</th>
|
|
334
|
+
<th class="num">${escapeHtml(label('netAmount', language))} (${escapeHtml(currency)})</th>
|
|
335
|
+
<th class="num">${escapeHtml(label('vatRate', language))}</th>
|
|
336
|
+
<th class="num">${escapeHtml(label('vatAmount', language))}</th>
|
|
337
|
+
<th class="num">${escapeHtml(label('grossAmount', language))}</th>
|
|
338
|
+
</tr>
|
|
339
|
+
</thead>
|
|
340
|
+
<tbody>
|
|
341
|
+
${rows}
|
|
342
|
+
</tbody>
|
|
343
|
+
</table>`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Keep a quantity's own precision rather than forcing two decimals. */
|
|
347
|
+
function decimalsOf(value: string): number {
|
|
348
|
+
const fraction = value.split('.')[1];
|
|
349
|
+
if (!fraction) return 0;
|
|
350
|
+
return fraction.replace(/0+$/, '').length;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function renderTotals(invoice: InvoiceType, currency: string, language: DocumentLanguage): string {
|
|
354
|
+
const summary = invoice.invoiceSummary;
|
|
355
|
+
const normal = summary.summaryNormal;
|
|
356
|
+
const gross = summary.summaryGrossData;
|
|
357
|
+
const rows: string[] = [];
|
|
358
|
+
|
|
359
|
+
if (normal) {
|
|
360
|
+
rows.push(
|
|
361
|
+
row(label('totalNet', language), formatAmount(normal.invoiceNetAmount, language), currency),
|
|
362
|
+
row(label('totalVat', language), formatAmount(normal.invoiceVatAmount, language), currency),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (gross) {
|
|
366
|
+
rows.push(
|
|
367
|
+
`<tr class="grand"><td>${escapeHtml(label('totalGross', language))}</td>` +
|
|
368
|
+
`<td class="num">${escapeHtml(formatAmount(gross.invoiceGrossAmount, language))}</td>` +
|
|
369
|
+
`<td class="num">${escapeHtml(currency)}</td></tr>`,
|
|
370
|
+
);
|
|
371
|
+
if (currency !== 'HUF') {
|
|
372
|
+
rows.push(
|
|
373
|
+
row(label('inHuf', language), formatAmount(gross.invoiceGrossAmountHUF, language), 'HUF'),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (rows.length === 0) return '';
|
|
378
|
+
|
|
379
|
+
return `<div class="totals"><table><tbody>${rows.join('')}</tbody></table></div>`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function row(name: string, value: string, currency: string): string {
|
|
383
|
+
return (
|
|
384
|
+
`<tr><td>${escapeHtml(name)}</td>` +
|
|
385
|
+
`<td class="num">${escapeHtml(value)}</td>` +
|
|
386
|
+
`<td class="num">${escapeHtml(currency)}</td></tr>`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function renderVatSummary(
|
|
391
|
+
invoice: InvoiceType,
|
|
392
|
+
currency: string,
|
|
393
|
+
language: DocumentLanguage,
|
|
394
|
+
): string {
|
|
395
|
+
const byRate = invoice.invoiceSummary.summaryNormal?.summaryByVatRate ?? [];
|
|
396
|
+
if (byRate.length < 2) return '';
|
|
397
|
+
|
|
398
|
+
const rows = byRate
|
|
399
|
+
.map(
|
|
400
|
+
(entry) => `<tr>
|
|
401
|
+
<td>${escapeHtml(vatRateText(entry.vatRate, language))}</td>
|
|
402
|
+
<td class="num">${escapeHtml(formatAmount(entry.vatRateNetData.vatRateNetAmount, language))}</td>
|
|
403
|
+
<td class="num">${escapeHtml(formatAmount(entry.vatRateVatData.vatRateVatAmount, language))}</td>
|
|
404
|
+
<td class="num">${entry.vatRateGrossData ? escapeHtml(formatAmount(entry.vatRateGrossData.vatRateGrossAmount, language)) : ''}</td>
|
|
405
|
+
</tr>`,
|
|
406
|
+
)
|
|
407
|
+
.join('\n ');
|
|
408
|
+
|
|
409
|
+
return `<section class="vat-summary">
|
|
410
|
+
<h2>${escapeHtml(label('summaryByVatRate', language))} (${escapeHtml(currency)})</h2>
|
|
411
|
+
<table>
|
|
412
|
+
<thead>
|
|
413
|
+
<tr>
|
|
414
|
+
<th>${escapeHtml(label('vatRate', language))}</th>
|
|
415
|
+
<th class="num">${escapeHtml(label('netAmount', language))}</th>
|
|
416
|
+
<th class="num">${escapeHtml(label('vatAmount', language))}</th>
|
|
417
|
+
<th class="num">${escapeHtml(label('grossAmount', language))}</th>
|
|
418
|
+
</tr>
|
|
419
|
+
</thead>
|
|
420
|
+
<tbody>
|
|
421
|
+
${rows}
|
|
422
|
+
</tbody>
|
|
423
|
+
</table>
|
|
424
|
+
</section>`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function renderMarkings(
|
|
428
|
+
markings: ReturnType<typeof deriveMarkings>,
|
|
429
|
+
language: DocumentLanguage,
|
|
430
|
+
): string {
|
|
431
|
+
if (markings.length === 0) return '';
|
|
432
|
+
const items = markings
|
|
433
|
+
.map(
|
|
434
|
+
(marking) =>
|
|
435
|
+
`<li>${escapeHtml(marking.text)}` +
|
|
436
|
+
(marking.detail ? ` — ${escapeHtml(marking.detail)}` : '') +
|
|
437
|
+
` <span class="reference">(${escapeHtml(marking.reference)})</span></li>`,
|
|
438
|
+
)
|
|
439
|
+
.join('\n ');
|
|
440
|
+
return `<section class="markings">
|
|
441
|
+
<h2>${escapeHtml(label('markings', language))}</h2>
|
|
442
|
+
<ul>
|
|
443
|
+
${items}
|
|
444
|
+
</ul>
|
|
445
|
+
</section>`;
|
|
446
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './format.js';
|
|
2
|
+
export * from './labels.js';
|
|
3
|
+
export * from './markings.js';
|
|
4
|
+
export * from './theme.js';
|
|
5
|
+
export * from './html.js';
|
|
6
|
+
export * from './color.js';
|
|
7
|
+
export * from './pdf-native.js';
|
|
8
|
+
export * from './pdf.js';
|
|
9
|
+
export * from './export.js';
|
package/src/labels.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InvoiceCategoryType,
|
|
3
|
+
MarginSchemeType,
|
|
4
|
+
PaymentMethodType,
|
|
5
|
+
UnitOfMeasureType,
|
|
6
|
+
} from '@open-nav/core';
|
|
7
|
+
import type { DocumentLanguage } from './format.js';
|
|
8
|
+
|
|
9
|
+
type Dictionary = Record<string, string>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Document labels.
|
|
13
|
+
*
|
|
14
|
+
* Hungarian is the reference: an invoice issued in Hungary is a Hungarian
|
|
15
|
+
* document, and the phrases the VAT Act requires on it are prescribed in
|
|
16
|
+
* Hungarian. The English column is a courtesy translation for a foreign
|
|
17
|
+
* recipient, not a substitute.
|
|
18
|
+
*/
|
|
19
|
+
const LABELS: Record<DocumentLanguage, Dictionary> = {
|
|
20
|
+
hu: {
|
|
21
|
+
invoice: 'SZÁMLA',
|
|
22
|
+
simplifiedInvoice: 'EGYSZERŰSÍTETT SZÁMLA',
|
|
23
|
+
aggregateInvoice: 'GYŰJTŐSZÁMLA',
|
|
24
|
+
modificationDocument: 'MÓDOSÍTÓ OKIRAT',
|
|
25
|
+
invoiceNumber: 'Számla sorszáma',
|
|
26
|
+
issueDate: 'Számla kelte',
|
|
27
|
+
deliveryDate: 'Teljesítés dátuma',
|
|
28
|
+
deliveryPeriod: 'Elszámolási időszak',
|
|
29
|
+
paymentDate: 'Fizetési határidő',
|
|
30
|
+
paymentMethod: 'Fizetési mód',
|
|
31
|
+
supplier: 'Eladó',
|
|
32
|
+
customer: 'Vevő',
|
|
33
|
+
taxNumber: 'Adószám',
|
|
34
|
+
communityVatNumber: 'Közösségi adószám',
|
|
35
|
+
thirdStateTaxId: 'Harmadik országbeli adószám',
|
|
36
|
+
groupMemberTaxNumber: 'Csoporttag adószáma',
|
|
37
|
+
bankAccount: 'Bankszámlaszám',
|
|
38
|
+
fiscalRepresentative: 'Pénzügyi képviselő',
|
|
39
|
+
originalInvoiceNumber: 'Eredeti számla sorszáma',
|
|
40
|
+
lineNumber: 'Sor',
|
|
41
|
+
description: 'Megnevezés',
|
|
42
|
+
quantity: 'Mennyiség',
|
|
43
|
+
unit: 'Egység',
|
|
44
|
+
unitPrice: 'Egységár',
|
|
45
|
+
netAmount: 'Nettó érték',
|
|
46
|
+
vatRate: 'Áfa',
|
|
47
|
+
vatAmount: 'Áfa érték',
|
|
48
|
+
grossAmount: 'Bruttó érték',
|
|
49
|
+
summary: 'Összesítés',
|
|
50
|
+
summaryByVatRate: 'Áfa-összesítő',
|
|
51
|
+
totalNet: 'Nettó összesen',
|
|
52
|
+
totalVat: 'Áfa összesen',
|
|
53
|
+
totalGross: 'Bruttó összesen',
|
|
54
|
+
currency: 'Pénznem',
|
|
55
|
+
exchangeRate: 'Árfolyam',
|
|
56
|
+
inHuf: 'Forintban',
|
|
57
|
+
exempt: 'Adómentes',
|
|
58
|
+
outOfScope: 'Áfa tv. hatályán kívüli',
|
|
59
|
+
reverseCharge: 'fordított adózás',
|
|
60
|
+
cashAccounting: 'pénzforgalmi elszámolás',
|
|
61
|
+
selfBilling: 'önszámlázás',
|
|
62
|
+
marginTravel: 'különbözet szerinti szabályozás – utazási irodák',
|
|
63
|
+
marginSecondHand: 'különbözet szerinti szabályozás – használt cikkek',
|
|
64
|
+
marginArtwork: 'különbözet szerinti szabályozás – műalkotások',
|
|
65
|
+
marginAntiques: 'különbözet szerinti szabályozás – gyűjteménydarabok és régiségek',
|
|
66
|
+
newTransportMean: 'új közlekedési eszköz értékesítése',
|
|
67
|
+
markings: 'Jogszabályi jelölések',
|
|
68
|
+
notReported: 'Ez a dokumentum a NAV felé beküldött adatokból készült megjelenítés.',
|
|
69
|
+
page: 'oldal',
|
|
70
|
+
},
|
|
71
|
+
en: {
|
|
72
|
+
invoice: 'INVOICE',
|
|
73
|
+
simplifiedInvoice: 'SIMPLIFIED INVOICE',
|
|
74
|
+
aggregateInvoice: 'AGGREGATE INVOICE',
|
|
75
|
+
modificationDocument: 'MODIFICATION DOCUMENT',
|
|
76
|
+
invoiceNumber: 'Invoice number',
|
|
77
|
+
issueDate: 'Date of issue',
|
|
78
|
+
deliveryDate: 'Date of supply',
|
|
79
|
+
deliveryPeriod: 'Settlement period',
|
|
80
|
+
paymentDate: 'Payment due',
|
|
81
|
+
paymentMethod: 'Payment method',
|
|
82
|
+
supplier: 'Supplier',
|
|
83
|
+
customer: 'Customer',
|
|
84
|
+
taxNumber: 'Tax number',
|
|
85
|
+
communityVatNumber: 'Community VAT number',
|
|
86
|
+
thirdStateTaxId: 'Third state tax id',
|
|
87
|
+
groupMemberTaxNumber: 'Group member tax number',
|
|
88
|
+
bankAccount: 'Bank account',
|
|
89
|
+
fiscalRepresentative: 'Fiscal representative',
|
|
90
|
+
originalInvoiceNumber: 'Original invoice number',
|
|
91
|
+
lineNumber: 'No.',
|
|
92
|
+
description: 'Description',
|
|
93
|
+
quantity: 'Quantity',
|
|
94
|
+
unit: 'Unit',
|
|
95
|
+
unitPrice: 'Unit price',
|
|
96
|
+
netAmount: 'Net amount',
|
|
97
|
+
vatRate: 'VAT',
|
|
98
|
+
vatAmount: 'VAT amount',
|
|
99
|
+
grossAmount: 'Gross amount',
|
|
100
|
+
summary: 'Summary',
|
|
101
|
+
summaryByVatRate: 'VAT summary',
|
|
102
|
+
totalNet: 'Total net',
|
|
103
|
+
totalVat: 'Total VAT',
|
|
104
|
+
totalGross: 'Total gross',
|
|
105
|
+
currency: 'Currency',
|
|
106
|
+
exchangeRate: 'Exchange rate',
|
|
107
|
+
inHuf: 'In HUF',
|
|
108
|
+
exempt: 'Exempt',
|
|
109
|
+
outOfScope: 'Outside the scope of the VAT Act',
|
|
110
|
+
reverseCharge: 'reverse charge',
|
|
111
|
+
cashAccounting: 'cash accounting',
|
|
112
|
+
selfBilling: 'self-billing',
|
|
113
|
+
marginTravel: 'margin scheme — travel agents',
|
|
114
|
+
marginSecondHand: 'margin scheme — second-hand goods',
|
|
115
|
+
marginArtwork: 'margin scheme — works of art',
|
|
116
|
+
marginAntiques: 'margin scheme — collectors items and antiques',
|
|
117
|
+
newTransportMean: 'supply of a new means of transport',
|
|
118
|
+
markings: 'Statutory markings',
|
|
119
|
+
notReported: 'Rendered from the data reported to NAV.',
|
|
120
|
+
page: 'page',
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export function label(key: string, language: DocumentLanguage): string {
|
|
125
|
+
return LABELS[language][key] ?? LABELS.hu[key] ?? key;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const UNITS: Record<DocumentLanguage, Record<UnitOfMeasureType, string>> = {
|
|
129
|
+
hu: {
|
|
130
|
+
PIECE: 'db',
|
|
131
|
+
KILOGRAM: 'kg',
|
|
132
|
+
TON: 't',
|
|
133
|
+
KWH: 'kWh',
|
|
134
|
+
DAY: 'nap',
|
|
135
|
+
HOUR: 'óra',
|
|
136
|
+
MINUTE: 'perc',
|
|
137
|
+
MONTH: 'hónap',
|
|
138
|
+
LITER: 'l',
|
|
139
|
+
KILOMETER: 'km',
|
|
140
|
+
CUBIC_METER: 'm³',
|
|
141
|
+
METER: 'm',
|
|
142
|
+
LINEAR_METER: 'fm',
|
|
143
|
+
CARTON: 'karton',
|
|
144
|
+
PACK: 'csomag',
|
|
145
|
+
OWN: '',
|
|
146
|
+
},
|
|
147
|
+
en: {
|
|
148
|
+
PIECE: 'pcs',
|
|
149
|
+
KILOGRAM: 'kg',
|
|
150
|
+
TON: 't',
|
|
151
|
+
KWH: 'kWh',
|
|
152
|
+
DAY: 'day',
|
|
153
|
+
HOUR: 'hour',
|
|
154
|
+
MINUTE: 'min',
|
|
155
|
+
MONTH: 'month',
|
|
156
|
+
LITER: 'l',
|
|
157
|
+
KILOMETER: 'km',
|
|
158
|
+
CUBIC_METER: 'm³',
|
|
159
|
+
METER: 'm',
|
|
160
|
+
LINEAR_METER: 'lm',
|
|
161
|
+
CARTON: 'carton',
|
|
162
|
+
PACK: 'pack',
|
|
163
|
+
OWN: '',
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export function unitLabel(
|
|
168
|
+
unit: UnitOfMeasureType | undefined,
|
|
169
|
+
own: string | undefined,
|
|
170
|
+
language: DocumentLanguage,
|
|
171
|
+
): string {
|
|
172
|
+
if (unit === 'OWN') return own ?? '';
|
|
173
|
+
return unit ? UNITS[language][unit] : '';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const PAYMENT_METHODS: Record<DocumentLanguage, Record<PaymentMethodType, string>> = {
|
|
177
|
+
hu: {
|
|
178
|
+
TRANSFER: 'átutalás',
|
|
179
|
+
CASH: 'készpénz',
|
|
180
|
+
CARD: 'bankkártya',
|
|
181
|
+
VOUCHER: 'utalvány',
|
|
182
|
+
OTHER: 'egyéb',
|
|
183
|
+
},
|
|
184
|
+
en: {
|
|
185
|
+
TRANSFER: 'bank transfer',
|
|
186
|
+
CASH: 'cash',
|
|
187
|
+
CARD: 'card',
|
|
188
|
+
VOUCHER: 'voucher',
|
|
189
|
+
OTHER: 'other',
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export function paymentMethodLabel(
|
|
194
|
+
method: PaymentMethodType | undefined,
|
|
195
|
+
language: DocumentLanguage,
|
|
196
|
+
): string {
|
|
197
|
+
return method ? PAYMENT_METHODS[language][method] : '';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function documentTitle(
|
|
201
|
+
category: InvoiceCategoryType | undefined,
|
|
202
|
+
isModification: boolean,
|
|
203
|
+
language: DocumentLanguage,
|
|
204
|
+
): string {
|
|
205
|
+
if (isModification) return label('modificationDocument', language);
|
|
206
|
+
if (category === 'SIMPLIFIED') return label('simplifiedInvoice', language);
|
|
207
|
+
if (category === 'AGGREGATE') return label('aggregateInvoice', language);
|
|
208
|
+
return label('invoice', language);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function marginSchemeLabel(scheme: MarginSchemeType, language: DocumentLanguage): string {
|
|
212
|
+
switch (scheme) {
|
|
213
|
+
case 'TRAVEL_AGENCY':
|
|
214
|
+
return label('marginTravel', language);
|
|
215
|
+
case 'SECOND_HAND':
|
|
216
|
+
return label('marginSecondHand', language);
|
|
217
|
+
case 'ARTWORK':
|
|
218
|
+
return label('marginArtwork', language);
|
|
219
|
+
default:
|
|
220
|
+
return label('marginAntiques', language);
|
|
221
|
+
}
|
|
222
|
+
}
|