@stackforge-eu/factur-x 1.4.4 → 1.4.5
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 +11 -0
- package/dist/index.cjs +14 -3
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +14 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.4.5] — 2026-11-14
|
|
11
|
+
|
|
12
|
+
- **Fixed derived line net amount ignoring `basisQuantity`.** When a line
|
|
13
|
+
omits `lineTotal`, `buildXml` and the BR-CO-10 check in `validateInput`
|
|
14
|
+
computed it as `quantity * unitPrice`, so a price per 100 units produced a
|
|
15
|
+
line total 100× too high and a correct `totals.lineTotal` was rejected. The
|
|
16
|
+
amount is now `quantity * unitPrice / basisQuantity` (EN 16931 BT-131),
|
|
17
|
+
rounded per line in both places. Reported by @trackoor (#26).
|
|
18
|
+
- **`validateInput` rejects a `basisQuantity` that is not greater than zero.**
|
|
19
|
+
|
|
10
20
|
## [1.4.4] — 2026-11-09
|
|
11
21
|
|
|
12
22
|
- **Fixed header reference element order.** `buildXml` emitted
|
|
@@ -291,6 +301,7 @@ This element is not expected`. Reordered the three blocks in
|
|
|
291
301
|
- Deno compatibility test suite
|
|
292
302
|
|
|
293
303
|
[Unreleased]: https://github.com/StackForge-EU/factur-x/commits/main
|
|
304
|
+
[1.4.5]: https://github.com/StackForge-EU/factur-x/compare/v1.4.4...v1.4.5
|
|
294
305
|
[1.4.4]: https://github.com/StackForge-EU/factur-x/compare/v1.4.3...v1.4.4
|
|
295
306
|
[1.4.3]: https://github.com/StackForge-EU/factur-x/compare/v1.4.2...v1.4.3
|
|
296
307
|
[1.4.2]: https://github.com/StackForge-EU/factur-x/compare/v1.4.1...v1.4.2
|
package/dist/index.cjs
CHANGED
|
@@ -289,6 +289,9 @@ function toCents(n) {
|
|
|
289
289
|
function roundHalfUp(n) {
|
|
290
290
|
return toCents(n) / 100;
|
|
291
291
|
}
|
|
292
|
+
function deriveLineNet(line) {
|
|
293
|
+
return roundHalfUp(line.quantity * line.unitPrice / (line.basisQuantity ?? 1));
|
|
294
|
+
}
|
|
292
295
|
|
|
293
296
|
// src/core/xml-builder.ts
|
|
294
297
|
var PROFILE_LEVEL = {
|
|
@@ -451,7 +454,7 @@ function buildLineItem(line, profile) {
|
|
|
451
454
|
tx += tag("ram:RateApplicablePercent", line.vatRatePercent.toString());
|
|
452
455
|
settle += tag("ram:ApplicableTradeTax", tx);
|
|
453
456
|
}
|
|
454
|
-
const lineTotal = line.lineTotal ??
|
|
457
|
+
const lineTotal = line.lineTotal ?? deriveLineNet(line);
|
|
455
458
|
settle += tag(
|
|
456
459
|
"ram:SpecifiedTradeSettlementLineMonetarySummation",
|
|
457
460
|
tag("ram:LineTotalAmount", fmtAmt(lineTotal))
|
|
@@ -879,6 +882,14 @@ function validateInput(input, profile, flavor) {
|
|
|
879
882
|
if (line.unitPrice == null) {
|
|
880
883
|
addError(errors, `lines[${i}].unitPrice`, "Line unit price is required", "BASIC" /* BASIC */);
|
|
881
884
|
}
|
|
885
|
+
if (line.basisQuantity != null && !(line.basisQuantity > 0)) {
|
|
886
|
+
addError(
|
|
887
|
+
errors,
|
|
888
|
+
`lines[${i}].basisQuantity`,
|
|
889
|
+
"Line price base quantity (BT-149) must be greater than zero",
|
|
890
|
+
"BASIC" /* BASIC */
|
|
891
|
+
);
|
|
892
|
+
}
|
|
882
893
|
});
|
|
883
894
|
}
|
|
884
895
|
}
|
|
@@ -975,8 +986,8 @@ function validateInput(input, profile, flavor) {
|
|
|
975
986
|
}
|
|
976
987
|
if (req("BASIC" /* BASIC */) && input.totals && input.lines) {
|
|
977
988
|
const sumLineNets = input.lines.reduce((sum, l) => {
|
|
978
|
-
const lineNet = l.lineTotal ?? (
|
|
979
|
-
return sum + lineNet;
|
|
989
|
+
const lineNet = l.lineTotal ?? deriveLineNet(l);
|
|
990
|
+
return sum + (Number.isFinite(lineNet) ? lineNet : 0);
|
|
980
991
|
}, 0);
|
|
981
992
|
if (input.totals.lineTotal != null) {
|
|
982
993
|
if (amountsDiffer(input.totals.lineTotal, sumLineNets)) {
|
package/dist/index.d.cts
CHANGED
|
@@ -596,7 +596,8 @@ interface InvoiceLineInput {
|
|
|
596
596
|
*/
|
|
597
597
|
basisQuantityUnitCode?: UnitCode | (string & Record<never, never>);
|
|
598
598
|
/**
|
|
599
|
-
* Line total (net)
|
|
599
|
+
* Line total (net). Derived as `quantity * unitPrice / (basisQuantity ?? 1)`,
|
|
600
|
+
* rounded to two decimals, when omitted.
|
|
600
601
|
* @see EN 16931 BT-131
|
|
601
602
|
*/
|
|
602
603
|
lineTotal?: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -596,7 +596,8 @@ interface InvoiceLineInput {
|
|
|
596
596
|
*/
|
|
597
597
|
basisQuantityUnitCode?: UnitCode | (string & Record<never, never>);
|
|
598
598
|
/**
|
|
599
|
-
* Line total (net)
|
|
599
|
+
* Line total (net). Derived as `quantity * unitPrice / (basisQuantity ?? 1)`,
|
|
600
|
+
* rounded to two decimals, when omitted.
|
|
600
601
|
* @see EN 16931 BT-131
|
|
601
602
|
*/
|
|
602
603
|
lineTotal?: number;
|
package/dist/index.js
CHANGED
|
@@ -259,6 +259,9 @@ function toCents(n) {
|
|
|
259
259
|
function roundHalfUp(n) {
|
|
260
260
|
return toCents(n) / 100;
|
|
261
261
|
}
|
|
262
|
+
function deriveLineNet(line) {
|
|
263
|
+
return roundHalfUp(line.quantity * line.unitPrice / (line.basisQuantity ?? 1));
|
|
264
|
+
}
|
|
262
265
|
|
|
263
266
|
// src/core/xml-builder.ts
|
|
264
267
|
var PROFILE_LEVEL = {
|
|
@@ -421,7 +424,7 @@ function buildLineItem(line, profile) {
|
|
|
421
424
|
tx += tag("ram:RateApplicablePercent", line.vatRatePercent.toString());
|
|
422
425
|
settle += tag("ram:ApplicableTradeTax", tx);
|
|
423
426
|
}
|
|
424
|
-
const lineTotal = line.lineTotal ??
|
|
427
|
+
const lineTotal = line.lineTotal ?? deriveLineNet(line);
|
|
425
428
|
settle += tag(
|
|
426
429
|
"ram:SpecifiedTradeSettlementLineMonetarySummation",
|
|
427
430
|
tag("ram:LineTotalAmount", fmtAmt(lineTotal))
|
|
@@ -849,6 +852,14 @@ function validateInput(input, profile, flavor) {
|
|
|
849
852
|
if (line.unitPrice == null) {
|
|
850
853
|
addError(errors, `lines[${i}].unitPrice`, "Line unit price is required", "BASIC" /* BASIC */);
|
|
851
854
|
}
|
|
855
|
+
if (line.basisQuantity != null && !(line.basisQuantity > 0)) {
|
|
856
|
+
addError(
|
|
857
|
+
errors,
|
|
858
|
+
`lines[${i}].basisQuantity`,
|
|
859
|
+
"Line price base quantity (BT-149) must be greater than zero",
|
|
860
|
+
"BASIC" /* BASIC */
|
|
861
|
+
);
|
|
862
|
+
}
|
|
852
863
|
});
|
|
853
864
|
}
|
|
854
865
|
}
|
|
@@ -945,8 +956,8 @@ function validateInput(input, profile, flavor) {
|
|
|
945
956
|
}
|
|
946
957
|
if (req("BASIC" /* BASIC */) && input.totals && input.lines) {
|
|
947
958
|
const sumLineNets = input.lines.reduce((sum, l) => {
|
|
948
|
-
const lineNet = l.lineTotal ?? (
|
|
949
|
-
return sum + lineNet;
|
|
959
|
+
const lineNet = l.lineTotal ?? deriveLineNet(l);
|
|
960
|
+
return sum + (Number.isFinite(lineNet) ? lineNet : 0);
|
|
950
961
|
}, 0);
|
|
951
962
|
if (input.totals.lineTotal != null) {
|
|
952
963
|
if (amountsDiffer(input.totals.lineTotal, sumLineNets)) {
|