@stackforge-eu/factur-x 1.4.4 → 1.4.6
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 +21 -0
- package/dist/index.cjs +20 -8
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +20 -8
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.4.6] — 2026-11-14
|
|
11
|
+
|
|
12
|
+
- **Fixed MINIMUM XML failing XSD validation when optional party fields are
|
|
13
|
+
set.** `buildXml` wrote a party's `id`, `globalId`,
|
|
14
|
+
`legalOrganization.tradingName` and `electronicAddress` in every profile,
|
|
15
|
+
but the MINIMUM schema allows none of them. They are now dropped for
|
|
16
|
+
MINIMUM, like `address` and `contact` already were, and still written from
|
|
17
|
+
BASIC WL upwards (#28).
|
|
18
|
+
|
|
19
|
+
## [1.4.5] — 2026-11-14
|
|
20
|
+
|
|
21
|
+
- **Fixed derived line net amount ignoring `basisQuantity`.** When a line
|
|
22
|
+
omits `lineTotal`, `buildXml` and the BR-CO-10 check in `validateInput`
|
|
23
|
+
computed it as `quantity * unitPrice`, so a price per 100 units produced a
|
|
24
|
+
line total 100× too high and a correct `totals.lineTotal` was rejected. The
|
|
25
|
+
amount is now `quantity * unitPrice / basisQuantity` (EN 16931 BT-131),
|
|
26
|
+
rounded per line in both places. Reported by @trackoor (#26).
|
|
27
|
+
- **`validateInput` rejects a `basisQuantity` that is not greater than zero.**
|
|
28
|
+
|
|
10
29
|
## [1.4.4] — 2026-11-09
|
|
11
30
|
|
|
12
31
|
- **Fixed header reference element order.** `buildXml` emitted
|
|
@@ -291,6 +310,8 @@ This element is not expected`. Reordered the three blocks in
|
|
|
291
310
|
- Deno compatibility test suite
|
|
292
311
|
|
|
293
312
|
[Unreleased]: https://github.com/StackForge-EU/factur-x/commits/main
|
|
313
|
+
[1.4.6]: https://github.com/StackForge-EU/factur-x/compare/v1.4.5...v1.4.6
|
|
314
|
+
[1.4.5]: https://github.com/StackForge-EU/factur-x/compare/v1.4.4...v1.4.5
|
|
294
315
|
[1.4.4]: https://github.com/StackForge-EU/factur-x/compare/v1.4.3...v1.4.4
|
|
295
316
|
[1.4.3]: https://github.com/StackForge-EU/factur-x/compare/v1.4.2...v1.4.3
|
|
296
317
|
[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 = {
|
|
@@ -352,11 +355,12 @@ function buildContact(c) {
|
|
|
352
355
|
}
|
|
353
356
|
function buildParty(p, profile) {
|
|
354
357
|
let x = "";
|
|
355
|
-
|
|
358
|
+
const beyondMinimum = atLeast(profile, "BASIC_WL" /* BASIC_WL */);
|
|
359
|
+
if (p.id && beyondMinimum) {
|
|
356
360
|
const ids = Array.isArray(p.id) ? p.id : [p.id];
|
|
357
361
|
for (const id of ids) x += tag("ram:ID", escapeXml(id));
|
|
358
362
|
}
|
|
359
|
-
if (p.globalId) {
|
|
363
|
+
if (p.globalId && beyondMinimum) {
|
|
360
364
|
x += tag("ram:GlobalID", escapeXml(p.globalId.value), { schemeID: p.globalId.schemeID });
|
|
361
365
|
}
|
|
362
366
|
x += tag("ram:Name", escapeXml(p.name));
|
|
@@ -371,13 +375,13 @@ function buildParty(p, profile) {
|
|
|
371
375
|
p.legalOrganization.schemeID ? { schemeID: p.legalOrganization.schemeID } : void 0
|
|
372
376
|
);
|
|
373
377
|
}
|
|
374
|
-
if (p.legalOrganization.tradingName)
|
|
378
|
+
if (p.legalOrganization.tradingName && beyondMinimum)
|
|
375
379
|
lo += tag("ram:TradingBusinessName", escapeXml(p.legalOrganization.tradingName));
|
|
376
380
|
x += tag("ram:SpecifiedLegalOrganization", lo);
|
|
377
381
|
}
|
|
378
382
|
if (p.contact && atLeast(profile, "EN16931" /* EN16931 */)) x += buildContact(p.contact);
|
|
379
|
-
if (p.address &&
|
|
380
|
-
if (p.electronicAddress)
|
|
383
|
+
if (p.address && beyondMinimum) x += buildAddress(p.address);
|
|
384
|
+
if (p.electronicAddress && beyondMinimum)
|
|
381
385
|
x += tag(
|
|
382
386
|
"ram:URIUniversalCommunication",
|
|
383
387
|
tag("ram:URIID", escapeXml(p.electronicAddress.value), {
|
|
@@ -451,7 +455,7 @@ function buildLineItem(line, profile) {
|
|
|
451
455
|
tx += tag("ram:RateApplicablePercent", line.vatRatePercent.toString());
|
|
452
456
|
settle += tag("ram:ApplicableTradeTax", tx);
|
|
453
457
|
}
|
|
454
|
-
const lineTotal = line.lineTotal ??
|
|
458
|
+
const lineTotal = line.lineTotal ?? deriveLineNet(line);
|
|
455
459
|
settle += tag(
|
|
456
460
|
"ram:SpecifiedTradeSettlementLineMonetarySummation",
|
|
457
461
|
tag("ram:LineTotalAmount", fmtAmt(lineTotal))
|
|
@@ -879,6 +883,14 @@ function validateInput(input, profile, flavor) {
|
|
|
879
883
|
if (line.unitPrice == null) {
|
|
880
884
|
addError(errors, `lines[${i}].unitPrice`, "Line unit price is required", "BASIC" /* BASIC */);
|
|
881
885
|
}
|
|
886
|
+
if (line.basisQuantity != null && !(line.basisQuantity > 0)) {
|
|
887
|
+
addError(
|
|
888
|
+
errors,
|
|
889
|
+
`lines[${i}].basisQuantity`,
|
|
890
|
+
"Line price base quantity (BT-149) must be greater than zero",
|
|
891
|
+
"BASIC" /* BASIC */
|
|
892
|
+
);
|
|
893
|
+
}
|
|
882
894
|
});
|
|
883
895
|
}
|
|
884
896
|
}
|
|
@@ -975,8 +987,8 @@ function validateInput(input, profile, flavor) {
|
|
|
975
987
|
}
|
|
976
988
|
if (req("BASIC" /* BASIC */) && input.totals && input.lines) {
|
|
977
989
|
const sumLineNets = input.lines.reduce((sum, l) => {
|
|
978
|
-
const lineNet = l.lineTotal ?? (
|
|
979
|
-
return sum + lineNet;
|
|
990
|
+
const lineNet = l.lineTotal ?? deriveLineNet(l);
|
|
991
|
+
return sum + (Number.isFinite(lineNet) ? lineNet : 0);
|
|
980
992
|
}, 0);
|
|
981
993
|
if (input.totals.lineTotal != null) {
|
|
982
994
|
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 = {
|
|
@@ -322,11 +325,12 @@ function buildContact(c) {
|
|
|
322
325
|
}
|
|
323
326
|
function buildParty(p, profile) {
|
|
324
327
|
let x = "";
|
|
325
|
-
|
|
328
|
+
const beyondMinimum = atLeast(profile, "BASIC_WL" /* BASIC_WL */);
|
|
329
|
+
if (p.id && beyondMinimum) {
|
|
326
330
|
const ids = Array.isArray(p.id) ? p.id : [p.id];
|
|
327
331
|
for (const id of ids) x += tag("ram:ID", escapeXml(id));
|
|
328
332
|
}
|
|
329
|
-
if (p.globalId) {
|
|
333
|
+
if (p.globalId && beyondMinimum) {
|
|
330
334
|
x += tag("ram:GlobalID", escapeXml(p.globalId.value), { schemeID: p.globalId.schemeID });
|
|
331
335
|
}
|
|
332
336
|
x += tag("ram:Name", escapeXml(p.name));
|
|
@@ -341,13 +345,13 @@ function buildParty(p, profile) {
|
|
|
341
345
|
p.legalOrganization.schemeID ? { schemeID: p.legalOrganization.schemeID } : void 0
|
|
342
346
|
);
|
|
343
347
|
}
|
|
344
|
-
if (p.legalOrganization.tradingName)
|
|
348
|
+
if (p.legalOrganization.tradingName && beyondMinimum)
|
|
345
349
|
lo += tag("ram:TradingBusinessName", escapeXml(p.legalOrganization.tradingName));
|
|
346
350
|
x += tag("ram:SpecifiedLegalOrganization", lo);
|
|
347
351
|
}
|
|
348
352
|
if (p.contact && atLeast(profile, "EN16931" /* EN16931 */)) x += buildContact(p.contact);
|
|
349
|
-
if (p.address &&
|
|
350
|
-
if (p.electronicAddress)
|
|
353
|
+
if (p.address && beyondMinimum) x += buildAddress(p.address);
|
|
354
|
+
if (p.electronicAddress && beyondMinimum)
|
|
351
355
|
x += tag(
|
|
352
356
|
"ram:URIUniversalCommunication",
|
|
353
357
|
tag("ram:URIID", escapeXml(p.electronicAddress.value), {
|
|
@@ -421,7 +425,7 @@ function buildLineItem(line, profile) {
|
|
|
421
425
|
tx += tag("ram:RateApplicablePercent", line.vatRatePercent.toString());
|
|
422
426
|
settle += tag("ram:ApplicableTradeTax", tx);
|
|
423
427
|
}
|
|
424
|
-
const lineTotal = line.lineTotal ??
|
|
428
|
+
const lineTotal = line.lineTotal ?? deriveLineNet(line);
|
|
425
429
|
settle += tag(
|
|
426
430
|
"ram:SpecifiedTradeSettlementLineMonetarySummation",
|
|
427
431
|
tag("ram:LineTotalAmount", fmtAmt(lineTotal))
|
|
@@ -849,6 +853,14 @@ function validateInput(input, profile, flavor) {
|
|
|
849
853
|
if (line.unitPrice == null) {
|
|
850
854
|
addError(errors, `lines[${i}].unitPrice`, "Line unit price is required", "BASIC" /* BASIC */);
|
|
851
855
|
}
|
|
856
|
+
if (line.basisQuantity != null && !(line.basisQuantity > 0)) {
|
|
857
|
+
addError(
|
|
858
|
+
errors,
|
|
859
|
+
`lines[${i}].basisQuantity`,
|
|
860
|
+
"Line price base quantity (BT-149) must be greater than zero",
|
|
861
|
+
"BASIC" /* BASIC */
|
|
862
|
+
);
|
|
863
|
+
}
|
|
852
864
|
});
|
|
853
865
|
}
|
|
854
866
|
}
|
|
@@ -945,8 +957,8 @@ function validateInput(input, profile, flavor) {
|
|
|
945
957
|
}
|
|
946
958
|
if (req("BASIC" /* BASIC */) && input.totals && input.lines) {
|
|
947
959
|
const sumLineNets = input.lines.reduce((sum, l) => {
|
|
948
|
-
const lineNet = l.lineTotal ?? (
|
|
949
|
-
return sum + lineNet;
|
|
960
|
+
const lineNet = l.lineTotal ?? deriveLineNet(l);
|
|
961
|
+
return sum + (Number.isFinite(lineNet) ? lineNet : 0);
|
|
950
962
|
}, 0);
|
|
951
963
|
if (input.totals.lineTotal != null) {
|
|
952
964
|
if (amountsDiffer(input.totals.lineTotal, sumLineNets)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackforge-eu/factur-x",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Embed Factur-X, ZUGFeRD, and XRechnung structured invoice XML into PDF/A-3 documents",
|
|
6
6
|
"keywords": [
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"type": "git",
|
|
42
42
|
"url": "git+https://github.com/StackForge-EU/factur-x.git"
|
|
43
43
|
},
|
|
44
|
-
"homepage": "https://
|
|
44
|
+
"homepage": "https://factur-x.deployo.eu",
|
|
45
45
|
"bugs": {
|
|
46
46
|
"url": "https://github.com/StackForge-EU/factur-x/issues"
|
|
47
47
|
},
|