@stackforge-eu/factur-x 1.3.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/index.cjs +36 -2
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +36 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.4.1] — 2026-09-14
|
|
11
|
+
|
|
12
|
+
- **Added the rounding amount (BT-114).** A new optional `roundingAmount`
|
|
13
|
+
field on `InvoiceTotalsInput` emits `ram:RoundingAmount` between the tax
|
|
14
|
+
total and the grand total, so an invoice can state the amount that takes
|
|
15
|
+
the grand total (BT-112) to the payable figure (BT-115). Emitted for
|
|
16
|
+
EN 16931 and EXTENDED only, as the lower-profile XSDs do not define the
|
|
17
|
+
element. `validateInput` now also checks BR-CO-16
|
|
18
|
+
(`duePayableAmount = grandTotal - prepaidAmount + roundingAmount`), which
|
|
19
|
+
the schematron already enforced. Omitting the field keeps the output
|
|
20
|
+
byte-for-byte identical (#17).
|
|
21
|
+
|
|
22
|
+
## [1.4.0] — 2026-09-03
|
|
23
|
+
|
|
24
|
+
- **Added the item price discount (BT-147).** A new optional `priceDiscount`
|
|
25
|
+
field on `InvoiceLineInput` emits `ram:AppliedTradeAllowanceCharge` inside
|
|
26
|
+
the gross price element, so a line can state the discount that connects the
|
|
27
|
+
gross unit price (BT-148) to the net unit price (BT-146) instead of leaving
|
|
28
|
+
it implied by their difference. Requires `grossUnitPrice` to be set; the
|
|
29
|
+
amount keeps up to four decimals for sub-cent discounts. Omitting the field
|
|
30
|
+
keeps the output byte-for-byte identical (#15).
|
|
31
|
+
|
|
10
32
|
## [1.3.1] — 2026-09-01
|
|
11
33
|
|
|
12
34
|
- **Dropped the unused `schema/cii-d22b/` code-list XSDs from the npm and JSR
|
|
@@ -235,6 +257,8 @@ This element is not expected`. Reordered the three blocks in
|
|
|
235
257
|
- Deno compatibility test suite
|
|
236
258
|
|
|
237
259
|
[Unreleased]: https://github.com/StackForge-EU/factur-x/commits/main
|
|
260
|
+
[1.4.1]: https://github.com/StackForge-EU/factur-x/compare/v1.4.0...v1.4.1
|
|
261
|
+
[1.4.0]: https://github.com/StackForge-EU/factur-x/compare/v1.3.1...v1.4.0
|
|
238
262
|
[1.3.1]: https://github.com/StackForge-EU/factur-x/compare/v1.3.0...v1.3.1
|
|
239
263
|
[1.3.0]: https://github.com/StackForge-EU/factur-x/compare/v1.2.1...v1.3.0
|
|
240
264
|
[1.2.1]: https://github.com/StackForge-EU/factur-x/compare/v1.2.0...v1.2.1
|
package/dist/index.cjs
CHANGED
|
@@ -307,6 +307,12 @@ function fmtAmt(n) {
|
|
|
307
307
|
}
|
|
308
308
|
return n.toFixed(2);
|
|
309
309
|
}
|
|
310
|
+
function fmtPrice(n) {
|
|
311
|
+
if (!Number.isFinite(n)) {
|
|
312
|
+
throw new Error(`Invalid amount value: ${n}. Expected a finite number.`);
|
|
313
|
+
}
|
|
314
|
+
return n.toFixed(4).replace(/(\.\d{2}\d*?)0+$/, "$1");
|
|
315
|
+
}
|
|
310
316
|
function tag(name, content, attrs) {
|
|
311
317
|
const a = attrs ? Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeXml(v)}"`).join("") : "";
|
|
312
318
|
return `<${name}${a}>${content}</${name}>`;
|
|
@@ -406,11 +412,21 @@ function buildLineItem(line, profile) {
|
|
|
406
412
|
const basisQuantity = line.basisQuantity !== void 0 ? tag("ram:BasisQuantity", line.basisQuantity.toString(), {
|
|
407
413
|
unitCode: line.basisQuantityUnitCode ?? unitCode
|
|
408
414
|
}) : "";
|
|
409
|
-
if (line.
|
|
415
|
+
if (line.priceDiscount !== void 0 && line.grossUnitPrice === void 0) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`buildXml: line "${line.id}" sets "priceDiscount" (BT-147) without "grossUnitPrice" (BT-148).`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (line.grossUnitPrice !== void 0) {
|
|
421
|
+
const discount = line.priceDiscount !== void 0 ? tag(
|
|
422
|
+
"ram:AppliedTradeAllowanceCharge",
|
|
423
|
+
tag("ram:ChargeIndicator", tag("udt:Indicator", "false")) + tag("ram:ActualAmount", fmtPrice(line.priceDiscount))
|
|
424
|
+
) : "";
|
|
410
425
|
agree += tag(
|
|
411
426
|
"ram:GrossPriceProductTradePrice",
|
|
412
|
-
tag("ram:ChargeAmount", fmtAmt(line.grossUnitPrice)) + basisQuantity
|
|
427
|
+
tag("ram:ChargeAmount", fmtAmt(line.grossUnitPrice)) + basisQuantity + discount
|
|
413
428
|
);
|
|
429
|
+
}
|
|
414
430
|
agree += tag(
|
|
415
431
|
"ram:NetPriceProductTradePrice",
|
|
416
432
|
tag("ram:ChargeAmount", fmtAmt(line.unitPrice)) + basisQuantity
|
|
@@ -480,6 +496,8 @@ function buildMonetarySummation(t, profile) {
|
|
|
480
496
|
x += tag("ram:TaxTotalAmount", fmtAmt(t.taxTotal), {
|
|
481
497
|
currencyID: t.currency
|
|
482
498
|
});
|
|
499
|
+
if (atLeast(profile, "EN16931" /* EN16931 */) && t.roundingAmount !== void 0)
|
|
500
|
+
x += tag("ram:RoundingAmount", fmtAmt(t.roundingAmount));
|
|
483
501
|
x += tag("ram:GrandTotalAmount", fmtAmt(t.grandTotal));
|
|
484
502
|
if (atLeast(profile, "BASIC_WL" /* BASIC_WL */) && t.prepaidAmount !== void 0)
|
|
485
503
|
x += tag("ram:TotalPrepaidAmount", fmtAmt(t.prepaidAmount));
|
|
@@ -920,6 +938,22 @@ function validateInput(input, profile, flavor) {
|
|
|
920
938
|
}
|
|
921
939
|
}
|
|
922
940
|
}
|
|
941
|
+
if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
|
|
942
|
+
const t = input.totals;
|
|
943
|
+
if (t.grandTotal != null && t.duePayableAmount != null) {
|
|
944
|
+
const prepaid = t.prepaidAmount ?? 0;
|
|
945
|
+
const rounding = t.roundingAmount ?? 0;
|
|
946
|
+
const expected = round2(t.grandTotal - prepaid + rounding);
|
|
947
|
+
if (Math.abs(round2(t.duePayableAmount) - expected) > 0.01) {
|
|
948
|
+
addError(
|
|
949
|
+
errors,
|
|
950
|
+
"totals.duePayableAmount",
|
|
951
|
+
`duePayableAmount ${t.duePayableAmount} does not equal grandTotal - prepaidAmount + roundingAmount (${expected}) (BR-CO-16).`,
|
|
952
|
+
"BASIC_WL" /* BASIC_WL */
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
923
957
|
if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
|
|
924
958
|
const t = input.totals;
|
|
925
959
|
if (t.lineTotal != null && t.taxBasisTotal != null) {
|
package/dist/index.d.cts
CHANGED
|
@@ -573,6 +573,14 @@ interface InvoiceLineInput {
|
|
|
573
573
|
* @see EN 16931 BT-148
|
|
574
574
|
*/
|
|
575
575
|
grossUnitPrice?: number;
|
|
576
|
+
/**
|
|
577
|
+
* Item price discount — the amount subtracted from `grossUnitPrice`
|
|
578
|
+
* (BT-148) to arrive at `unitPrice` (BT-146). Emitted as
|
|
579
|
+
* `ram:AppliedTradeAllowanceCharge` inside the gross price element,
|
|
580
|
+
* so `grossUnitPrice` must be set alongside it.
|
|
581
|
+
* @see EN 16931 BT-147
|
|
582
|
+
*/
|
|
583
|
+
priceDiscount?: number;
|
|
576
584
|
/**
|
|
577
585
|
* Item price base quantity — the quantity the unit price (BT-146)
|
|
578
586
|
* applies to, e.g. a price per 10 square metres or per 1000 prints.
|
|
@@ -851,6 +859,14 @@ interface InvoiceTotalsInput {
|
|
|
851
859
|
* @see EN 16931 BT-113
|
|
852
860
|
*/
|
|
853
861
|
prepaidAmount?: number;
|
|
862
|
+
/**
|
|
863
|
+
* Rounding amount — the amount added to the grand total (BT-112) to round
|
|
864
|
+
* the amount due (BT-115) to a payable figure, e.g. `0.02` to take
|
|
865
|
+
* `29.95` to `29.97`. Emitted as `ram:RoundingAmount` for EN 16931 and
|
|
866
|
+
* EXTENDED only; the BASIC / BASIC WL XSDs do not define the element.
|
|
867
|
+
* @see EN 16931 BT-114, BR-CO-16
|
|
868
|
+
*/
|
|
869
|
+
roundingAmount?: number;
|
|
854
870
|
/**
|
|
855
871
|
* Due payable amount.
|
|
856
872
|
* @see EN 16931 BT-115
|
package/dist/index.d.ts
CHANGED
|
@@ -573,6 +573,14 @@ interface InvoiceLineInput {
|
|
|
573
573
|
* @see EN 16931 BT-148
|
|
574
574
|
*/
|
|
575
575
|
grossUnitPrice?: number;
|
|
576
|
+
/**
|
|
577
|
+
* Item price discount — the amount subtracted from `grossUnitPrice`
|
|
578
|
+
* (BT-148) to arrive at `unitPrice` (BT-146). Emitted as
|
|
579
|
+
* `ram:AppliedTradeAllowanceCharge` inside the gross price element,
|
|
580
|
+
* so `grossUnitPrice` must be set alongside it.
|
|
581
|
+
* @see EN 16931 BT-147
|
|
582
|
+
*/
|
|
583
|
+
priceDiscount?: number;
|
|
576
584
|
/**
|
|
577
585
|
* Item price base quantity — the quantity the unit price (BT-146)
|
|
578
586
|
* applies to, e.g. a price per 10 square metres or per 1000 prints.
|
|
@@ -851,6 +859,14 @@ interface InvoiceTotalsInput {
|
|
|
851
859
|
* @see EN 16931 BT-113
|
|
852
860
|
*/
|
|
853
861
|
prepaidAmount?: number;
|
|
862
|
+
/**
|
|
863
|
+
* Rounding amount — the amount added to the grand total (BT-112) to round
|
|
864
|
+
* the amount due (BT-115) to a payable figure, e.g. `0.02` to take
|
|
865
|
+
* `29.95` to `29.97`. Emitted as `ram:RoundingAmount` for EN 16931 and
|
|
866
|
+
* EXTENDED only; the BASIC / BASIC WL XSDs do not define the element.
|
|
867
|
+
* @see EN 16931 BT-114, BR-CO-16
|
|
868
|
+
*/
|
|
869
|
+
roundingAmount?: number;
|
|
854
870
|
/**
|
|
855
871
|
* Due payable amount.
|
|
856
872
|
* @see EN 16931 BT-115
|
package/dist/index.js
CHANGED
|
@@ -277,6 +277,12 @@ function fmtAmt(n) {
|
|
|
277
277
|
}
|
|
278
278
|
return n.toFixed(2);
|
|
279
279
|
}
|
|
280
|
+
function fmtPrice(n) {
|
|
281
|
+
if (!Number.isFinite(n)) {
|
|
282
|
+
throw new Error(`Invalid amount value: ${n}. Expected a finite number.`);
|
|
283
|
+
}
|
|
284
|
+
return n.toFixed(4).replace(/(\.\d{2}\d*?)0+$/, "$1");
|
|
285
|
+
}
|
|
280
286
|
function tag(name, content, attrs) {
|
|
281
287
|
const a = attrs ? Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeXml(v)}"`).join("") : "";
|
|
282
288
|
return `<${name}${a}>${content}</${name}>`;
|
|
@@ -376,11 +382,21 @@ function buildLineItem(line, profile) {
|
|
|
376
382
|
const basisQuantity = line.basisQuantity !== void 0 ? tag("ram:BasisQuantity", line.basisQuantity.toString(), {
|
|
377
383
|
unitCode: line.basisQuantityUnitCode ?? unitCode
|
|
378
384
|
}) : "";
|
|
379
|
-
if (line.
|
|
385
|
+
if (line.priceDiscount !== void 0 && line.grossUnitPrice === void 0) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`buildXml: line "${line.id}" sets "priceDiscount" (BT-147) without "grossUnitPrice" (BT-148).`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
if (line.grossUnitPrice !== void 0) {
|
|
391
|
+
const discount = line.priceDiscount !== void 0 ? tag(
|
|
392
|
+
"ram:AppliedTradeAllowanceCharge",
|
|
393
|
+
tag("ram:ChargeIndicator", tag("udt:Indicator", "false")) + tag("ram:ActualAmount", fmtPrice(line.priceDiscount))
|
|
394
|
+
) : "";
|
|
380
395
|
agree += tag(
|
|
381
396
|
"ram:GrossPriceProductTradePrice",
|
|
382
|
-
tag("ram:ChargeAmount", fmtAmt(line.grossUnitPrice)) + basisQuantity
|
|
397
|
+
tag("ram:ChargeAmount", fmtAmt(line.grossUnitPrice)) + basisQuantity + discount
|
|
383
398
|
);
|
|
399
|
+
}
|
|
384
400
|
agree += tag(
|
|
385
401
|
"ram:NetPriceProductTradePrice",
|
|
386
402
|
tag("ram:ChargeAmount", fmtAmt(line.unitPrice)) + basisQuantity
|
|
@@ -450,6 +466,8 @@ function buildMonetarySummation(t, profile) {
|
|
|
450
466
|
x += tag("ram:TaxTotalAmount", fmtAmt(t.taxTotal), {
|
|
451
467
|
currencyID: t.currency
|
|
452
468
|
});
|
|
469
|
+
if (atLeast(profile, "EN16931" /* EN16931 */) && t.roundingAmount !== void 0)
|
|
470
|
+
x += tag("ram:RoundingAmount", fmtAmt(t.roundingAmount));
|
|
453
471
|
x += tag("ram:GrandTotalAmount", fmtAmt(t.grandTotal));
|
|
454
472
|
if (atLeast(profile, "BASIC_WL" /* BASIC_WL */) && t.prepaidAmount !== void 0)
|
|
455
473
|
x += tag("ram:TotalPrepaidAmount", fmtAmt(t.prepaidAmount));
|
|
@@ -890,6 +908,22 @@ function validateInput(input, profile, flavor) {
|
|
|
890
908
|
}
|
|
891
909
|
}
|
|
892
910
|
}
|
|
911
|
+
if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
|
|
912
|
+
const t = input.totals;
|
|
913
|
+
if (t.grandTotal != null && t.duePayableAmount != null) {
|
|
914
|
+
const prepaid = t.prepaidAmount ?? 0;
|
|
915
|
+
const rounding = t.roundingAmount ?? 0;
|
|
916
|
+
const expected = round2(t.grandTotal - prepaid + rounding);
|
|
917
|
+
if (Math.abs(round2(t.duePayableAmount) - expected) > 0.01) {
|
|
918
|
+
addError(
|
|
919
|
+
errors,
|
|
920
|
+
"totals.duePayableAmount",
|
|
921
|
+
`duePayableAmount ${t.duePayableAmount} does not equal grandTotal - prepaidAmount + roundingAmount (${expected}) (BR-CO-16).`,
|
|
922
|
+
"BASIC_WL" /* BASIC_WL */
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
893
927
|
if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
|
|
894
928
|
const t = input.totals;
|
|
895
929
|
if (t.lineTotal != null && t.taxBasisTotal != null) {
|