@stackforge-eu/factur-x 1.4.2 → 1.4.4
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 +28 -0
- package/dist/index.cjs +62 -37
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +62 -37
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.4.4] — 2026-11-09
|
|
11
|
+
|
|
12
|
+
- **Fixed header reference element order.** `buildXml` emitted
|
|
13
|
+
`input.references` in input order, so an `order` reference listed before a
|
|
14
|
+
`seller-order` (or `contract`) reference produced XML that failed XSD
|
|
15
|
+
validation. References are now emitted in the CII sequence (SellerOrder →
|
|
16
|
+
BuyerOrder → Contract → SpecifiedProcuringProject); input order is only
|
|
17
|
+
kept among references of the same type. Reported by @SCPAU100 (#23).
|
|
18
|
+
- **Fixed `project` references failing XSD validation.** `SpecifiedProcuringProject`
|
|
19
|
+
requires a `ram:Name` child that was never emitted. A new optional `name`
|
|
20
|
+
field on `DocumentReferenceInput` fills it, defaulting to `"Project"`.
|
|
21
|
+
- **`validateInput` rejects duplicate single-occurrence references.** The
|
|
22
|
+
schema allows `order`, `contract`, `seller-order`, `project` and `despatch`
|
|
23
|
+
references at most once each, and `delivery.despatchAdviceReference` and a
|
|
24
|
+
`despatch` reference target the same element. Both cases used to surface
|
|
25
|
+
only as an XSD error after building.
|
|
26
|
+
|
|
27
|
+
## [1.4.3] — 2026-09-28
|
|
28
|
+
|
|
29
|
+
- **Fixed half-cent rounding.** `Math.round(n * 100) / 100` turns
|
|
30
|
+
`402.50 × 19%` (`76.475`) into `76.47`, because `76.475 * 100` is
|
|
31
|
+
`7647.499999999999`, so `validateInput` rejected the correctly rounded
|
|
32
|
+
`76.48` (BR-CO-17). Amounts are now rounded half away from zero on a
|
|
33
|
+
noise-free value and compared in integer cents, in the validator and in
|
|
34
|
+
the XML builder alike. Reported by @johannesspohr (#21).
|
|
35
|
+
|
|
10
36
|
## [1.4.2] — 2026-09-25
|
|
11
37
|
|
|
12
38
|
- **Fixed the embedded XML MIME type back to `text/xml`.** 1.2.1 switched the
|
|
@@ -265,6 +291,8 @@ This element is not expected`. Reordered the three blocks in
|
|
|
265
291
|
- Deno compatibility test suite
|
|
266
292
|
|
|
267
293
|
[Unreleased]: https://github.com/StackForge-EU/factur-x/commits/main
|
|
294
|
+
[1.4.4]: https://github.com/StackForge-EU/factur-x/compare/v1.4.3...v1.4.4
|
|
295
|
+
[1.4.3]: https://github.com/StackForge-EU/factur-x/compare/v1.4.2...v1.4.3
|
|
268
296
|
[1.4.2]: https://github.com/StackForge-EU/factur-x/compare/v1.4.1...v1.4.2
|
|
269
297
|
[1.4.1]: https://github.com/StackForge-EU/factur-x/compare/v1.4.0...v1.4.1
|
|
270
298
|
[1.4.0]: https://github.com/StackForge-EU/factur-x/compare/v1.3.1...v1.4.0
|
package/dist/index.cjs
CHANGED
|
@@ -281,6 +281,15 @@ function resolveBusinessProcessUrn(input, flavor) {
|
|
|
281
281
|
return FLAVOR_CONFIGS[flavor].businessProcessUrn;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// src/core/rounding.ts
|
|
285
|
+
function toCents(n) {
|
|
286
|
+
const cents = Math.round(Number((Math.abs(n) * 100).toPrecision(15)));
|
|
287
|
+
return n < 0 ? -cents : cents;
|
|
288
|
+
}
|
|
289
|
+
function roundHalfUp(n) {
|
|
290
|
+
return toCents(n) / 100;
|
|
291
|
+
}
|
|
292
|
+
|
|
284
293
|
// src/core/xml-builder.ts
|
|
285
294
|
var PROFILE_LEVEL = {
|
|
286
295
|
MINIMUM: 0,
|
|
@@ -305,7 +314,7 @@ function fmtAmt(n) {
|
|
|
305
314
|
if (!Number.isFinite(n)) {
|
|
306
315
|
throw new Error(`Invalid amount value: ${n}. Expected a finite number.`);
|
|
307
316
|
}
|
|
308
|
-
return n.toFixed(2);
|
|
317
|
+
return roundHalfUp(n).toFixed(2);
|
|
309
318
|
}
|
|
310
319
|
function fmtPrice(n) {
|
|
311
320
|
if (!Number.isFinite(n)) {
|
|
@@ -442,7 +451,7 @@ function buildLineItem(line, profile) {
|
|
|
442
451
|
tx += tag("ram:RateApplicablePercent", line.vatRatePercent.toString());
|
|
443
452
|
settle += tag("ram:ApplicableTradeTax", tx);
|
|
444
453
|
}
|
|
445
|
-
const lineTotal = line.lineTotal ??
|
|
454
|
+
const lineTotal = line.lineTotal ?? roundHalfUp(line.quantity * line.unitPrice);
|
|
446
455
|
settle += tag(
|
|
447
456
|
"ram:SpecifiedTradeSettlementLineMonetarySummation",
|
|
448
457
|
tag("ram:LineTotalAmount", fmtAmt(lineTotal))
|
|
@@ -561,27 +570,21 @@ function buildXml(input, profile, flavor) {
|
|
|
561
570
|
buildParty(input.sellerTaxRepresentative, profile)
|
|
562
571
|
);
|
|
563
572
|
if (input.references && atLeast(profile, "BASIC_WL" /* BASIC_WL */)) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
case "project":
|
|
580
|
-
if (atLeast(profile, "EN16931" /* EN16931 */))
|
|
581
|
-
agree += tag("ram:SpecifiedProcuringProject", tag("ram:ID", escapeXml(ref.id)));
|
|
582
|
-
break;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
573
|
+
const refsOfType = (type) => input.references.filter((ref) => (ref.type ?? "order") === type);
|
|
574
|
+
const refDoc = (ref) => tag("ram:IssuerAssignedID", escapeXml(ref.id)) + (ref.issueDate ? dateEl("ram:FormattedIssueDateTime", ref.issueDate) : "");
|
|
575
|
+
if (atLeast(profile, "EN16931" /* EN16931 */))
|
|
576
|
+
for (const ref of refsOfType("seller-order"))
|
|
577
|
+
agree += tag("ram:SellerOrderReferencedDocument", refDoc(ref));
|
|
578
|
+
for (const ref of refsOfType("order"))
|
|
579
|
+
agree += tag("ram:BuyerOrderReferencedDocument", refDoc(ref));
|
|
580
|
+
for (const ref of refsOfType("contract"))
|
|
581
|
+
agree += tag("ram:ContractReferencedDocument", refDoc(ref));
|
|
582
|
+
if (atLeast(profile, "EN16931" /* EN16931 */))
|
|
583
|
+
for (const ref of refsOfType("project"))
|
|
584
|
+
agree += tag(
|
|
585
|
+
"ram:SpecifiedProcuringProject",
|
|
586
|
+
tag("ram:ID", escapeXml(ref.id)) + tag("ram:Name", escapeXml(ref.name ?? "Project"))
|
|
587
|
+
);
|
|
585
588
|
}
|
|
586
589
|
tx += tag("ram:ApplicableHeaderTradeAgreement", agree);
|
|
587
590
|
let del = "";
|
|
@@ -927,8 +930,8 @@ function validateInput(input, profile, flavor) {
|
|
|
927
930
|
if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
|
|
928
931
|
const t = input.totals;
|
|
929
932
|
if (t.taxBasisTotal != null && t.taxTotal != null && t.grandTotal != null) {
|
|
930
|
-
const expected =
|
|
931
|
-
if (
|
|
933
|
+
const expected = roundHalfUp(t.taxBasisTotal + t.taxTotal);
|
|
934
|
+
if (amountsDiffer(t.grandTotal, expected)) {
|
|
932
935
|
addError(
|
|
933
936
|
errors,
|
|
934
937
|
"totals.grandTotal",
|
|
@@ -943,8 +946,8 @@ function validateInput(input, profile, flavor) {
|
|
|
943
946
|
if (t.grandTotal != null && t.duePayableAmount != null) {
|
|
944
947
|
const prepaid = t.prepaidAmount ?? 0;
|
|
945
948
|
const rounding = t.roundingAmount ?? 0;
|
|
946
|
-
const expected =
|
|
947
|
-
if (
|
|
949
|
+
const expected = roundHalfUp(t.grandTotal - prepaid + rounding);
|
|
950
|
+
if (amountsDiffer(t.duePayableAmount, expected)) {
|
|
948
951
|
addError(
|
|
949
952
|
errors,
|
|
950
953
|
"totals.duePayableAmount",
|
|
@@ -959,8 +962,8 @@ function validateInput(input, profile, flavor) {
|
|
|
959
962
|
if (t.lineTotal != null && t.taxBasisTotal != null) {
|
|
960
963
|
const allowance = t.allowanceTotal ?? 0;
|
|
961
964
|
const charge = t.chargeTotal ?? 0;
|
|
962
|
-
const expected =
|
|
963
|
-
if (
|
|
965
|
+
const expected = roundHalfUp(t.lineTotal - allowance + charge);
|
|
966
|
+
if (amountsDiffer(t.taxBasisTotal, expected)) {
|
|
964
967
|
addError(
|
|
965
968
|
errors,
|
|
966
969
|
"totals.taxBasisTotal",
|
|
@@ -976,11 +979,11 @@ function validateInput(input, profile, flavor) {
|
|
|
976
979
|
return sum + lineNet;
|
|
977
980
|
}, 0);
|
|
978
981
|
if (input.totals.lineTotal != null) {
|
|
979
|
-
if (
|
|
982
|
+
if (amountsDiffer(input.totals.lineTotal, sumLineNets)) {
|
|
980
983
|
addError(
|
|
981
984
|
errors,
|
|
982
985
|
"totals.lineTotal",
|
|
983
|
-
`lineTotal ${input.totals.lineTotal} does not equal \u03A3 line nets (${
|
|
986
|
+
`lineTotal ${input.totals.lineTotal} does not equal \u03A3 line nets (${roundHalfUp(sumLineNets)}) (BR-CO-10).`,
|
|
984
987
|
"BASIC" /* BASIC */
|
|
985
988
|
);
|
|
986
989
|
}
|
|
@@ -988,11 +991,11 @@ function validateInput(input, profile, flavor) {
|
|
|
988
991
|
}
|
|
989
992
|
if (req("BASIC_WL" /* BASIC_WL */) && input.totals?.taxTotal != null && input.vatBreakdown) {
|
|
990
993
|
const sumVat = input.vatBreakdown.reduce((s, v) => s + (v.taxAmount ?? 0), 0);
|
|
991
|
-
if (
|
|
994
|
+
if (amountsDiffer(input.totals.taxTotal, sumVat)) {
|
|
992
995
|
addError(
|
|
993
996
|
errors,
|
|
994
997
|
"totals.taxTotal",
|
|
995
|
-
`taxTotal ${input.totals.taxTotal} does not equal \u03A3 vatBreakdown.taxAmount (${
|
|
998
|
+
`taxTotal ${input.totals.taxTotal} does not equal \u03A3 vatBreakdown.taxAmount (${roundHalfUp(sumVat)}) (BR-CO-14).`,
|
|
996
999
|
"BASIC_WL" /* BASIC_WL */
|
|
997
1000
|
);
|
|
998
1001
|
}
|
|
@@ -1000,8 +1003,8 @@ function validateInput(input, profile, flavor) {
|
|
|
1000
1003
|
if (req("BASIC_WL" /* BASIC_WL */) && input.vatBreakdown) {
|
|
1001
1004
|
input.vatBreakdown.forEach((vb, i) => {
|
|
1002
1005
|
if (vb.taxableAmount != null && vb.taxAmount != null && vb.ratePercent != null) {
|
|
1003
|
-
const expected =
|
|
1004
|
-
if (
|
|
1006
|
+
const expected = roundHalfUp(vb.taxableAmount * vb.ratePercent / 100);
|
|
1007
|
+
if (amountsDiffer(vb.taxAmount, expected)) {
|
|
1005
1008
|
addError(
|
|
1006
1009
|
errors,
|
|
1007
1010
|
`vatBreakdown[${i}].taxAmount`,
|
|
@@ -1027,6 +1030,28 @@ function validateInput(input, profile, flavor) {
|
|
|
1027
1030
|
}
|
|
1028
1031
|
}
|
|
1029
1032
|
}
|
|
1033
|
+
if (req("BASIC_WL" /* BASIC_WL */) && input.references) {
|
|
1034
|
+
const singleOccurrence = ["order", "contract", "seller-order", "project", "despatch"];
|
|
1035
|
+
for (const type of singleOccurrence) {
|
|
1036
|
+
const count = input.references.filter((ref) => (ref.type ?? "order") === type).length;
|
|
1037
|
+
if (count > 1) {
|
|
1038
|
+
addError(
|
|
1039
|
+
errors,
|
|
1040
|
+
"references",
|
|
1041
|
+
`Only one "${type}" reference is allowed, got ${count}.`,
|
|
1042
|
+
"BASIC_WL" /* BASIC_WL */
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (input.delivery?.despatchAdviceReference && input.references.some((ref) => ref.type === "despatch")) {
|
|
1047
|
+
addError(
|
|
1048
|
+
errors,
|
|
1049
|
+
"references",
|
|
1050
|
+
'Use either delivery.despatchAdviceReference or a "despatch" reference, not both.',
|
|
1051
|
+
"BASIC_WL" /* BASIC_WL */
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1030
1055
|
if (flavor === "xrechnung" /* XRECHNUNG */) {
|
|
1031
1056
|
if (!input.document?.buyerReference) {
|
|
1032
1057
|
addError(
|
|
@@ -1084,8 +1109,8 @@ function validateInput(input, profile, flavor) {
|
|
|
1084
1109
|
errors
|
|
1085
1110
|
};
|
|
1086
1111
|
}
|
|
1087
|
-
function
|
|
1088
|
-
return Math.
|
|
1112
|
+
function amountsDiffer(actual, expected) {
|
|
1113
|
+
return Math.abs(toCents(actual) - toCents(expected)) > 1;
|
|
1089
1114
|
}
|
|
1090
1115
|
function isValidIban(iban) {
|
|
1091
1116
|
const normalized = iban.replace(/\s+/g, "").toUpperCase();
|
package/dist/index.d.cts
CHANGED
|
@@ -947,6 +947,12 @@ interface DocumentReferenceInput {
|
|
|
947
947
|
type?: "order" | "contract" | "despatch" | "preceding" | "seller-order" | "project";
|
|
948
948
|
/** Issue date of the referenced document */
|
|
949
949
|
issueDate?: string;
|
|
950
|
+
/**
|
|
951
|
+
* Project name, only used for `"project"` references. The CII schema
|
|
952
|
+
* requires a `ram:Name` next to the project ID (BT-11); falls back to
|
|
953
|
+
* `"Project"` when omitted.
|
|
954
|
+
*/
|
|
955
|
+
name?: string;
|
|
950
956
|
}
|
|
951
957
|
/**
|
|
952
958
|
* Billing/invoicing period.
|
package/dist/index.d.ts
CHANGED
|
@@ -947,6 +947,12 @@ interface DocumentReferenceInput {
|
|
|
947
947
|
type?: "order" | "contract" | "despatch" | "preceding" | "seller-order" | "project";
|
|
948
948
|
/** Issue date of the referenced document */
|
|
949
949
|
issueDate?: string;
|
|
950
|
+
/**
|
|
951
|
+
* Project name, only used for `"project"` references. The CII schema
|
|
952
|
+
* requires a `ram:Name` next to the project ID (BT-11); falls back to
|
|
953
|
+
* `"Project"` when omitted.
|
|
954
|
+
*/
|
|
955
|
+
name?: string;
|
|
950
956
|
}
|
|
951
957
|
/**
|
|
952
958
|
* Billing/invoicing period.
|
package/dist/index.js
CHANGED
|
@@ -251,6 +251,15 @@ function resolveBusinessProcessUrn(input, flavor) {
|
|
|
251
251
|
return FLAVOR_CONFIGS[flavor].businessProcessUrn;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
// src/core/rounding.ts
|
|
255
|
+
function toCents(n) {
|
|
256
|
+
const cents = Math.round(Number((Math.abs(n) * 100).toPrecision(15)));
|
|
257
|
+
return n < 0 ? -cents : cents;
|
|
258
|
+
}
|
|
259
|
+
function roundHalfUp(n) {
|
|
260
|
+
return toCents(n) / 100;
|
|
261
|
+
}
|
|
262
|
+
|
|
254
263
|
// src/core/xml-builder.ts
|
|
255
264
|
var PROFILE_LEVEL = {
|
|
256
265
|
MINIMUM: 0,
|
|
@@ -275,7 +284,7 @@ function fmtAmt(n) {
|
|
|
275
284
|
if (!Number.isFinite(n)) {
|
|
276
285
|
throw new Error(`Invalid amount value: ${n}. Expected a finite number.`);
|
|
277
286
|
}
|
|
278
|
-
return n.toFixed(2);
|
|
287
|
+
return roundHalfUp(n).toFixed(2);
|
|
279
288
|
}
|
|
280
289
|
function fmtPrice(n) {
|
|
281
290
|
if (!Number.isFinite(n)) {
|
|
@@ -412,7 +421,7 @@ function buildLineItem(line, profile) {
|
|
|
412
421
|
tx += tag("ram:RateApplicablePercent", line.vatRatePercent.toString());
|
|
413
422
|
settle += tag("ram:ApplicableTradeTax", tx);
|
|
414
423
|
}
|
|
415
|
-
const lineTotal = line.lineTotal ??
|
|
424
|
+
const lineTotal = line.lineTotal ?? roundHalfUp(line.quantity * line.unitPrice);
|
|
416
425
|
settle += tag(
|
|
417
426
|
"ram:SpecifiedTradeSettlementLineMonetarySummation",
|
|
418
427
|
tag("ram:LineTotalAmount", fmtAmt(lineTotal))
|
|
@@ -531,27 +540,21 @@ function buildXml(input, profile, flavor) {
|
|
|
531
540
|
buildParty(input.sellerTaxRepresentative, profile)
|
|
532
541
|
);
|
|
533
542
|
if (input.references && atLeast(profile, "BASIC_WL" /* BASIC_WL */)) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
case "project":
|
|
550
|
-
if (atLeast(profile, "EN16931" /* EN16931 */))
|
|
551
|
-
agree += tag("ram:SpecifiedProcuringProject", tag("ram:ID", escapeXml(ref.id)));
|
|
552
|
-
break;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
543
|
+
const refsOfType = (type) => input.references.filter((ref) => (ref.type ?? "order") === type);
|
|
544
|
+
const refDoc = (ref) => tag("ram:IssuerAssignedID", escapeXml(ref.id)) + (ref.issueDate ? dateEl("ram:FormattedIssueDateTime", ref.issueDate) : "");
|
|
545
|
+
if (atLeast(profile, "EN16931" /* EN16931 */))
|
|
546
|
+
for (const ref of refsOfType("seller-order"))
|
|
547
|
+
agree += tag("ram:SellerOrderReferencedDocument", refDoc(ref));
|
|
548
|
+
for (const ref of refsOfType("order"))
|
|
549
|
+
agree += tag("ram:BuyerOrderReferencedDocument", refDoc(ref));
|
|
550
|
+
for (const ref of refsOfType("contract"))
|
|
551
|
+
agree += tag("ram:ContractReferencedDocument", refDoc(ref));
|
|
552
|
+
if (atLeast(profile, "EN16931" /* EN16931 */))
|
|
553
|
+
for (const ref of refsOfType("project"))
|
|
554
|
+
agree += tag(
|
|
555
|
+
"ram:SpecifiedProcuringProject",
|
|
556
|
+
tag("ram:ID", escapeXml(ref.id)) + tag("ram:Name", escapeXml(ref.name ?? "Project"))
|
|
557
|
+
);
|
|
555
558
|
}
|
|
556
559
|
tx += tag("ram:ApplicableHeaderTradeAgreement", agree);
|
|
557
560
|
let del = "";
|
|
@@ -897,8 +900,8 @@ function validateInput(input, profile, flavor) {
|
|
|
897
900
|
if (req("BASIC_WL" /* BASIC_WL */) && input.totals) {
|
|
898
901
|
const t = input.totals;
|
|
899
902
|
if (t.taxBasisTotal != null && t.taxTotal != null && t.grandTotal != null) {
|
|
900
|
-
const expected =
|
|
901
|
-
if (
|
|
903
|
+
const expected = roundHalfUp(t.taxBasisTotal + t.taxTotal);
|
|
904
|
+
if (amountsDiffer(t.grandTotal, expected)) {
|
|
902
905
|
addError(
|
|
903
906
|
errors,
|
|
904
907
|
"totals.grandTotal",
|
|
@@ -913,8 +916,8 @@ function validateInput(input, profile, flavor) {
|
|
|
913
916
|
if (t.grandTotal != null && t.duePayableAmount != null) {
|
|
914
917
|
const prepaid = t.prepaidAmount ?? 0;
|
|
915
918
|
const rounding = t.roundingAmount ?? 0;
|
|
916
|
-
const expected =
|
|
917
|
-
if (
|
|
919
|
+
const expected = roundHalfUp(t.grandTotal - prepaid + rounding);
|
|
920
|
+
if (amountsDiffer(t.duePayableAmount, expected)) {
|
|
918
921
|
addError(
|
|
919
922
|
errors,
|
|
920
923
|
"totals.duePayableAmount",
|
|
@@ -929,8 +932,8 @@ function validateInput(input, profile, flavor) {
|
|
|
929
932
|
if (t.lineTotal != null && t.taxBasisTotal != null) {
|
|
930
933
|
const allowance = t.allowanceTotal ?? 0;
|
|
931
934
|
const charge = t.chargeTotal ?? 0;
|
|
932
|
-
const expected =
|
|
933
|
-
if (
|
|
935
|
+
const expected = roundHalfUp(t.lineTotal - allowance + charge);
|
|
936
|
+
if (amountsDiffer(t.taxBasisTotal, expected)) {
|
|
934
937
|
addError(
|
|
935
938
|
errors,
|
|
936
939
|
"totals.taxBasisTotal",
|
|
@@ -946,11 +949,11 @@ function validateInput(input, profile, flavor) {
|
|
|
946
949
|
return sum + lineNet;
|
|
947
950
|
}, 0);
|
|
948
951
|
if (input.totals.lineTotal != null) {
|
|
949
|
-
if (
|
|
952
|
+
if (amountsDiffer(input.totals.lineTotal, sumLineNets)) {
|
|
950
953
|
addError(
|
|
951
954
|
errors,
|
|
952
955
|
"totals.lineTotal",
|
|
953
|
-
`lineTotal ${input.totals.lineTotal} does not equal \u03A3 line nets (${
|
|
956
|
+
`lineTotal ${input.totals.lineTotal} does not equal \u03A3 line nets (${roundHalfUp(sumLineNets)}) (BR-CO-10).`,
|
|
954
957
|
"BASIC" /* BASIC */
|
|
955
958
|
);
|
|
956
959
|
}
|
|
@@ -958,11 +961,11 @@ function validateInput(input, profile, flavor) {
|
|
|
958
961
|
}
|
|
959
962
|
if (req("BASIC_WL" /* BASIC_WL */) && input.totals?.taxTotal != null && input.vatBreakdown) {
|
|
960
963
|
const sumVat = input.vatBreakdown.reduce((s, v) => s + (v.taxAmount ?? 0), 0);
|
|
961
|
-
if (
|
|
964
|
+
if (amountsDiffer(input.totals.taxTotal, sumVat)) {
|
|
962
965
|
addError(
|
|
963
966
|
errors,
|
|
964
967
|
"totals.taxTotal",
|
|
965
|
-
`taxTotal ${input.totals.taxTotal} does not equal \u03A3 vatBreakdown.taxAmount (${
|
|
968
|
+
`taxTotal ${input.totals.taxTotal} does not equal \u03A3 vatBreakdown.taxAmount (${roundHalfUp(sumVat)}) (BR-CO-14).`,
|
|
966
969
|
"BASIC_WL" /* BASIC_WL */
|
|
967
970
|
);
|
|
968
971
|
}
|
|
@@ -970,8 +973,8 @@ function validateInput(input, profile, flavor) {
|
|
|
970
973
|
if (req("BASIC_WL" /* BASIC_WL */) && input.vatBreakdown) {
|
|
971
974
|
input.vatBreakdown.forEach((vb, i) => {
|
|
972
975
|
if (vb.taxableAmount != null && vb.taxAmount != null && vb.ratePercent != null) {
|
|
973
|
-
const expected =
|
|
974
|
-
if (
|
|
976
|
+
const expected = roundHalfUp(vb.taxableAmount * vb.ratePercent / 100);
|
|
977
|
+
if (amountsDiffer(vb.taxAmount, expected)) {
|
|
975
978
|
addError(
|
|
976
979
|
errors,
|
|
977
980
|
`vatBreakdown[${i}].taxAmount`,
|
|
@@ -997,6 +1000,28 @@ function validateInput(input, profile, flavor) {
|
|
|
997
1000
|
}
|
|
998
1001
|
}
|
|
999
1002
|
}
|
|
1003
|
+
if (req("BASIC_WL" /* BASIC_WL */) && input.references) {
|
|
1004
|
+
const singleOccurrence = ["order", "contract", "seller-order", "project", "despatch"];
|
|
1005
|
+
for (const type of singleOccurrence) {
|
|
1006
|
+
const count = input.references.filter((ref) => (ref.type ?? "order") === type).length;
|
|
1007
|
+
if (count > 1) {
|
|
1008
|
+
addError(
|
|
1009
|
+
errors,
|
|
1010
|
+
"references",
|
|
1011
|
+
`Only one "${type}" reference is allowed, got ${count}.`,
|
|
1012
|
+
"BASIC_WL" /* BASIC_WL */
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (input.delivery?.despatchAdviceReference && input.references.some((ref) => ref.type === "despatch")) {
|
|
1017
|
+
addError(
|
|
1018
|
+
errors,
|
|
1019
|
+
"references",
|
|
1020
|
+
'Use either delivery.despatchAdviceReference or a "despatch" reference, not both.',
|
|
1021
|
+
"BASIC_WL" /* BASIC_WL */
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1000
1025
|
if (flavor === "xrechnung" /* XRECHNUNG */) {
|
|
1001
1026
|
if (!input.document?.buyerReference) {
|
|
1002
1027
|
addError(
|
|
@@ -1054,8 +1079,8 @@ function validateInput(input, profile, flavor) {
|
|
|
1054
1079
|
errors
|
|
1055
1080
|
};
|
|
1056
1081
|
}
|
|
1057
|
-
function
|
|
1058
|
-
return Math.
|
|
1082
|
+
function amountsDiffer(actual, expected) {
|
|
1083
|
+
return Math.abs(toCents(actual) - toCents(expected)) > 1;
|
|
1059
1084
|
}
|
|
1060
1085
|
function isValidIban(iban) {
|
|
1061
1086
|
const normalized = iban.replace(/\s+/g, "").toUpperCase();
|