@fin.cx/einvoice 10.0.0 → 10.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/einvoice.d.ts +2 -0
- package/dist_ts/einvoice.js +7 -13
- package/dist_ts/formats/base/base.decoder.d.ts +9 -1
- package/dist_ts/formats/base/base.decoder.js +24 -12
- package/dist_ts/formats/cii/cii.decoder.d.ts +11 -3
- package/dist_ts/formats/cii/cii.decoder.js +27 -8
- package/dist_ts/formats/cii/cii.encoder.d.ts +12 -0
- package/dist_ts/formats/cii/cii.encoder.js +36 -1
- package/dist_ts/formats/cii/cii.types.js +2 -1
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +12 -8
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +12 -8
- package/dist_ts/formats/semantic/semantic.adapter.d.ts +0 -4
- package/dist_ts/formats/semantic/semantic.adapter.js +7 -18
- package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +6 -1
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +40 -8
- package/dist_ts/formats/ubl/ubl.decoder.d.ts +11 -0
- package/dist_ts/formats/ubl/ubl.decoder.js +30 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +3 -3
- package/dist_ts/formats/utils/date.value.d.ts +24 -0
- package/dist_ts/formats/utils/date.value.js +77 -12
- package/dist_ts/formats/utils/delivery.address.d.ts +18 -0
- package/dist_ts/formats/utils/delivery.address.js +30 -0
- package/dist_ts/formats/utils/document.totals.d.ts +5 -4
- package/dist_ts/formats/utils/document.totals.js +24 -8
- package/dist_ts/formats/utils/stated.values.js +4 -2
- package/dist_ts/formats/utils/vat.category.d.ts +146 -55
- package/dist_ts/formats/utils/vat.category.js +357 -69
- package/dist_ts/formats/validation/vat-categories.validator.d.ts +14 -15
- package/dist_ts/formats/validation/vat-categories.validator.js +123 -85
- package/package.json +5 -5
- package/readme.md +61 -20
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +8 -14
- package/ts/formats/base/base.decoder.ts +24 -17
- package/ts/formats/cii/cii.decoder.ts +28 -7
- package/ts/formats/cii/cii.encoder.ts +36 -0
- package/ts/formats/cii/cii.types.ts +1 -0
- package/ts/formats/cii/facturx/facturx.encoder.ts +12 -7
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +12 -7
- package/ts/formats/semantic/semantic.adapter.ts +6 -15
- package/ts/formats/ubl/generic/ubl.encoder.ts +40 -7
- package/ts/formats/ubl/ubl.decoder.ts +31 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +2 -2
- package/ts/formats/utils/date.value.ts +84 -11
- package/ts/formats/utils/delivery.address.ts +36 -0
- package/ts/formats/utils/document.totals.ts +38 -11
- package/ts/formats/utils/stated.values.ts +4 -1
- package/ts/formats/utils/vat.category.ts +426 -95
- package/ts/formats/validation/vat-categories.validator.ts +169 -88
|
@@ -8,6 +8,8 @@ import type { TPrecedingInvoiceReference } from '../utils/preceding.invoice.js';
|
|
|
8
8
|
import type { IEInvoiceMetadata } from '../../interfaces/en16931-metadata.js';
|
|
9
9
|
import type { IEInvoiceStatedValues } from '../../interfaces/stated.values.js';
|
|
10
10
|
import { readCiiStatedValues } from '../utils/stated.values.js';
|
|
11
|
+
import { applyStatedVatCategories } from '../utils/vat.category.js';
|
|
12
|
+
import { deliveryAddressOf, withDeliveryAddress, type TDeliveryAddress } from '../utils/delivery.address.js';
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* The invoicing period (BG-14) a CII document states: its start date (BT-73),
|
|
@@ -79,10 +81,29 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
|
|
|
79
81
|
const accountingDocType = getAccountingDocType(
|
|
80
82
|
this.getText('/rsm:CrossIndustryInvoice/rsm:ExchangedDocument/ram:TypeCode'),
|
|
81
83
|
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return this.
|
|
84
|
+
const document = accountingDocType === 'creditnote' ? await this.decodeCreditNote() : await this.decodeInvoice(accountingDocType);
|
|
85
|
+
// every item with the VAT category its line states and its breakdown's exemption reason
|
|
86
|
+
applyStatedVatCategories(document.items, this.statedValues);
|
|
87
|
+
return withDeliveryAddress(document, this.readDeliveryAddress());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Reads the deliver to address (BG-15) of the header delivery: line one and
|
|
92
|
+
* two as the street (BT-75) and the house number, the city (BT-77), the post
|
|
93
|
+
* code (BT-78), the country subdivision (BT-79) and the country (BT-80);
|
|
94
|
+
* undefined when the document states no address with a country
|
|
95
|
+
*/
|
|
96
|
+
protected readDeliveryAddress(): TDeliveryAddress | undefined {
|
|
97
|
+
const address =
|
|
98
|
+
'/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeDelivery/ram:ShipToTradeParty/ram:PostalTradeAddress';
|
|
99
|
+
return deliveryAddressOf({
|
|
100
|
+
streetName: this.getText(`${address}/ram:LineOne`),
|
|
101
|
+
houseNumber: this.getText(`${address}/ram:LineTwo`),
|
|
102
|
+
city: this.getText(`${address}/ram:CityName`),
|
|
103
|
+
postalCode: this.getText(`${address}/ram:PostcodeCode`),
|
|
104
|
+
countrySubdivision: this.getText(`${address}/ram:CountrySubDivisionName`),
|
|
105
|
+
countryCode: this.getText(`${address}/ram:CountryID`),
|
|
106
|
+
});
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
/**
|
|
@@ -158,9 +179,9 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
|
|
|
158
179
|
}
|
|
159
180
|
|
|
160
181
|
/**
|
|
161
|
-
* Whether every line of the document has the VAT category reverse charge (AE, BT-151);
|
|
162
|
-
*
|
|
163
|
-
*
|
|
182
|
+
* Whether every line of the document has the VAT category reverse charge (AE, BT-151); a
|
|
183
|
+
* document mixing AE lines with others is not, and its items carry their categories
|
|
184
|
+
* (`applyStatedVatCategories`)
|
|
164
185
|
*/
|
|
165
186
|
protected isReverseChargeDocument(): boolean {
|
|
166
187
|
const categories = this.select(
|
|
@@ -284,6 +284,42 @@ export abstract class CIIBaseEncoder extends BaseEncoder {
|
|
|
284
284
|
agreementElement.appendChild(buyerReferenceElement);
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Adds the deliver to address (BG-15) of `metadata.deliveryAddress` as
|
|
289
|
+
* `ram:ShipToTradeParty/ram:PostalTradeAddress` of the header delivery, when
|
|
290
|
+
* it states a country (BT-80, which BR-57 requires of the address): the
|
|
291
|
+
* street and house number as line one and two (BT-75, BT-76), the city
|
|
292
|
+
* (BT-77), the post code (BT-78), the country subdivision (BT-79) and the
|
|
293
|
+
* country. An intra-community supply states it (BR-IC-12).
|
|
294
|
+
* @param doc XML document
|
|
295
|
+
* @param deliveryElement The header trade delivery
|
|
296
|
+
* @param invoice Invoice data
|
|
297
|
+
*/
|
|
298
|
+
protected addDeliverToAddress(doc: Document, deliveryElement: Element, invoice: TAccountingDoc): void {
|
|
299
|
+
const address = (invoice as TAccountingDoc & { metadata?: IEInvoiceMetadata }).metadata?.deliveryAddress;
|
|
300
|
+
const countryCode = address?.countryCode?.trim();
|
|
301
|
+
if (!address || !countryCode) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const shipTo = doc.createElement('ram:ShipToTradeParty');
|
|
305
|
+
const postal = doc.createElement('ram:PostalTradeAddress');
|
|
306
|
+
const append = (name: string, text: string | undefined) => {
|
|
307
|
+
if (text?.trim()) {
|
|
308
|
+
const element = doc.createElement(name);
|
|
309
|
+
element.textContent = text.trim();
|
|
310
|
+
postal.appendChild(element);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
append('ram:PostcodeCode', address.postalCode);
|
|
314
|
+
append('ram:LineOne', address.streetName);
|
|
315
|
+
append('ram:LineTwo', address.houseNumber);
|
|
316
|
+
append('ram:CityName', address.city);
|
|
317
|
+
append('ram:CountryID', countryCode);
|
|
318
|
+
append('ram:CountrySubDivisionName', address.countrySubdivision);
|
|
319
|
+
shipTo.appendChild(postal);
|
|
320
|
+
deliveryElement.insertBefore(shipTo, deliveryElement.firstChild);
|
|
321
|
+
}
|
|
322
|
+
|
|
287
323
|
/**
|
|
288
324
|
* Adds the invoicing period (BG-14) as `ram:BillingSpecifiedPeriod` of the
|
|
289
325
|
* header settlement, where the CII schema puts it: the start date (BT-73) and
|
|
@@ -59,6 +59,7 @@ export const CII_CHILD_SEQUENCES: Readonly<Record<string, readonly string[]>> =
|
|
|
59
59
|
ApplicableHeaderTradeAgreement: ['Reference', 'BuyerReference', 'SellerTradeParty', 'BuyerTradeParty', 'SalesAgentTradeParty', 'BuyerRequisitionerTradeParty', 'BuyerAssignedAccountantTradeParty', 'SellerAssignedAccountantTradeParty', 'BuyerTaxRepresentativeTradeParty', 'SellerTaxRepresentativeTradeParty', 'ProductEndUserTradeParty', 'ApplicableTradeDeliveryTerms', 'SellerOrderReferencedDocument', 'BuyerOrderReferencedDocument', 'QuotationReferencedDocument', 'OrderResponseReferencedDocument', 'ContractReferencedDocument', 'DemandForecastReferencedDocument', 'SupplyInstructionReferencedDocument', 'PromotionalDealReferencedDocument', 'PriceListReferencedDocument', 'AdditionalReferencedDocument', 'RequisitionerReferencedDocument', 'BuyerAgentTradeParty', 'PurchaseConditionsReferencedDocument', 'SpecifiedProcuringProject', 'UltimateCustomerOrderReferencedDocument'],
|
|
60
60
|
SellerTradeParty: ['ID', 'GlobalID', 'Name', 'RoleCode', 'Description', 'SpecifiedLegalOrganization', 'DefinedTradeContact', 'PostalTradeAddress', 'URIUniversalCommunication', 'SpecifiedTaxRegistration', 'EndPointURIUniversalCommunication', 'LogoAssociatedSpecifiedBinaryFile'],
|
|
61
61
|
BuyerTradeParty: ['ID', 'GlobalID', 'Name', 'RoleCode', 'Description', 'SpecifiedLegalOrganization', 'DefinedTradeContact', 'PostalTradeAddress', 'URIUniversalCommunication', 'SpecifiedTaxRegistration', 'EndPointURIUniversalCommunication', 'LogoAssociatedSpecifiedBinaryFile'],
|
|
62
|
+
ShipToTradeParty: ['ID', 'GlobalID', 'Name', 'RoleCode', 'Description', 'SpecifiedLegalOrganization', 'DefinedTradeContact', 'PostalTradeAddress', 'URIUniversalCommunication', 'SpecifiedTaxRegistration', 'EndPointURIUniversalCommunication', 'LogoAssociatedSpecifiedBinaryFile'],
|
|
62
63
|
PostalTradeAddress: ['ID', 'PostcodeCode', 'PostOfficeBox', 'BuildingName', 'LineOne', 'LineTwo', 'LineThree', 'LineFour', 'LineFive', 'StreetName', 'CityName', 'CitySubDivisionName', 'CountryID', 'CountryName', 'CountrySubDivisionID', 'CountrySubDivisionName', 'AttentionOf', 'CareOf', 'BuildingNumber', 'DepartmentName', 'AdditionalStreetName'],
|
|
63
64
|
DefinedTradeContact: ['ID', 'PersonName', 'DepartmentName', 'TypeCode', 'JobTitle', 'Responsibility', 'PersonID', 'TelephoneUniversalCommunication', 'DirectTelephoneUniversalCommunication', 'MobileTelephoneUniversalCommunication', 'FaxUniversalCommunication', 'EmailURIUniversalCommunication', 'TelexUniversalCommunication', 'VOIPUniversalCommunication', 'InstantMessagingUniversalCommunication', 'SpecifiedNote', 'SpecifiedContactPerson'],
|
|
64
65
|
SpecifiedLegalOrganization: ['LegalClassificationCode', 'Name', 'ID', 'TradingBusinessName', 'PostalTradeAddress', 'AuthorizedLegalRegistration'],
|
|
@@ -308,6 +308,9 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
308
308
|
const deliveryElement = doc.createElement('ram:ApplicableHeaderTradeDelivery');
|
|
309
309
|
transactionElement.appendChild(deliveryElement);
|
|
310
310
|
|
|
311
|
+
// Deliver to address (BG-15), when the document states its country (BT-80)
|
|
312
|
+
this.addDeliverToAddress(doc, deliveryElement, invoice);
|
|
313
|
+
|
|
311
314
|
// Add delivery date if available
|
|
312
315
|
if (invoice.deliveryDate !== undefined) {
|
|
313
316
|
const deliveryDateElement = doc.createElement('ram:ActualDeliverySupplyChainEvent');
|
|
@@ -351,12 +354,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
351
354
|
};
|
|
352
355
|
appendText('ram:CalculatedAmount', taxAmount.toFixed(totals.minorUnits)); // BT-117
|
|
353
356
|
appendText('ram:TypeCode', 'VAT');
|
|
354
|
-
if (exemption) {
|
|
355
|
-
appendText('ram:ExemptionReason', exemption.reason); // BT-120 (BR-
|
|
357
|
+
if (exemption?.reason) {
|
|
358
|
+
appendText('ram:ExemptionReason', exemption.reason); // BT-120 (BR-x-10)
|
|
356
359
|
}
|
|
357
360
|
appendText('ram:BasisAmount', taxableAmount.toFixed(totals.minorUnits)); // BT-116
|
|
358
361
|
appendText('ram:CategoryCode', category); // BT-118
|
|
359
|
-
if (exemption) {
|
|
362
|
+
if (exemption?.code) {
|
|
360
363
|
appendText('ram:ExemptionReasonCode', exemption.code); // BT-121
|
|
361
364
|
}
|
|
362
365
|
appendText('ram:RateApplicablePercent', rate.toString()); // BT-119
|
|
@@ -483,10 +486,12 @@ export class FacturXEncoder extends CIIBaseEncoder {
|
|
|
483
486
|
taxCategoryCodeElement.textContent = totals.lineVatCategories[index]; // BT-151
|
|
484
487
|
taxElement.appendChild(taxCategoryCodeElement);
|
|
485
488
|
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
489
|
+
// Invoiced item VAT rate (BT-152); an item not subject to VAT has none (BR-O-05)
|
|
490
|
+
if (totals.lineVatCategories[index] !== 'O') {
|
|
491
|
+
const taxRateElement = doc.createElement('ram:RateApplicablePercent');
|
|
492
|
+
taxRateElement.textContent = item.vatPercentage.toString();
|
|
493
|
+
taxElement.appendChild(taxRateElement);
|
|
494
|
+
}
|
|
490
495
|
|
|
491
496
|
settlementElement.appendChild(taxElement);
|
|
492
497
|
|
|
@@ -320,6 +320,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
320
320
|
const deliveryElement = doc.createElement('ram:ApplicableHeaderTradeDelivery');
|
|
321
321
|
transactionElement.appendChild(deliveryElement);
|
|
322
322
|
|
|
323
|
+
// Deliver to address (BG-15), when the document states its country (BT-80)
|
|
324
|
+
this.addDeliverToAddress(doc, deliveryElement, invoice);
|
|
325
|
+
|
|
323
326
|
// Add delivery date if available
|
|
324
327
|
if (invoice.deliveryDate !== undefined) {
|
|
325
328
|
const deliveryDateElement = doc.createElement('ram:ActualDeliverySupplyChainEvent');
|
|
@@ -449,8 +452,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
449
452
|
typeCodeElement.textContent = 'VAT';
|
|
450
453
|
taxElement.appendChild(typeCodeElement);
|
|
451
454
|
|
|
452
|
-
// VAT exemption reason text (BT-120), for
|
|
453
|
-
if (exemption) {
|
|
455
|
+
// VAT exemption reason text (BT-120), for the categories that state one (BR-x-10)
|
|
456
|
+
if (exemption?.reason) {
|
|
454
457
|
const exemptionReasonElement = doc.createElement('ram:ExemptionReason');
|
|
455
458
|
exemptionReasonElement.textContent = exemption.reason;
|
|
456
459
|
taxElement.appendChild(exemptionReasonElement);
|
|
@@ -467,7 +470,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
467
470
|
taxElement.appendChild(categoryCodeElement);
|
|
468
471
|
|
|
469
472
|
// VAT exemption reason code (BT-121)
|
|
470
|
-
if (exemption) {
|
|
473
|
+
if (exemption?.code) {
|
|
471
474
|
const exemptionReasonCodeElement = doc.createElement('ram:ExemptionReasonCode');
|
|
472
475
|
exemptionReasonCodeElement.textContent = exemption.code;
|
|
473
476
|
taxElement.appendChild(exemptionReasonCodeElement);
|
|
@@ -591,10 +594,12 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
591
594
|
taxCategoryCodeElement.textContent = totals.lineVatCategories[index]; // BT-151
|
|
592
595
|
taxElement.appendChild(taxCategoryCodeElement);
|
|
593
596
|
|
|
594
|
-
//
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
597
|
+
// Invoiced item VAT rate (BT-152); an item not subject to VAT has none (BR-O-05)
|
|
598
|
+
if (totals.lineVatCategories[index] !== 'O') {
|
|
599
|
+
const taxRateElement = doc.createElement('ram:RateApplicablePercent');
|
|
600
|
+
taxRateElement.textContent = item.vatPercentage.toString();
|
|
601
|
+
taxElement.appendChild(taxRateElement);
|
|
602
|
+
}
|
|
598
603
|
|
|
599
604
|
settlementElement.appendChild(taxElement);
|
|
600
605
|
|
|
@@ -7,6 +7,7 @@ import { EInvoice } from '../../einvoice.js';
|
|
|
7
7
|
import { getAccountingDocType, getDocumentTypeCode } from '../utils/document.typecode.js';
|
|
8
8
|
import { computeDocumentTotals } from '../utils/document.totals.js';
|
|
9
9
|
import { getPaymentTotals } from '../utils/paid.amount.js';
|
|
10
|
+
import { getItemVatCategory } from '../utils/vat.category.js';
|
|
10
11
|
import type {
|
|
11
12
|
EN16931SemanticModel,
|
|
12
13
|
Seller,
|
|
@@ -119,7 +120,7 @@ export class SemanticModelAdapter {
|
|
|
119
120
|
additionalDocuments: invoice.metadata?.extensions?.supportingDocuments,
|
|
120
121
|
|
|
121
122
|
// Invoice lines
|
|
122
|
-
invoiceLines: this.mapInvoiceLines(invoice
|
|
123
|
+
invoiceLines: this.mapInvoiceLines(invoice)
|
|
123
124
|
};
|
|
124
125
|
}
|
|
125
126
|
|
|
@@ -443,9 +444,8 @@ export class SemanticModelAdapter {
|
|
|
443
444
|
/**
|
|
444
445
|
* Map invoice lines
|
|
445
446
|
*/
|
|
446
|
-
private mapInvoiceLines(
|
|
447
|
-
|
|
448
|
-
|
|
447
|
+
private mapInvoiceLines(invoice: EInvoice): InvoiceLine[] {
|
|
448
|
+
const items = invoice.items ?? [];
|
|
449
449
|
return items.map((item, index) => ({
|
|
450
450
|
identifier: (index + 1).toString(),
|
|
451
451
|
note: (item as any).description || (item as any).text || '',
|
|
@@ -464,7 +464,8 @@ export class SemanticModelAdapter {
|
|
|
464
464
|
itemPriceBaseQuantity: (item as any).priceBaseQuantity || 1
|
|
465
465
|
},
|
|
466
466
|
vatInformation: {
|
|
467
|
-
|
|
467
|
+
// the category the encoders write: the item's own, else AE or S (BT-151)
|
|
468
|
+
categoryCode: getItemVatCategory(invoice, item),
|
|
468
469
|
rate: item.vatPercentage
|
|
469
470
|
},
|
|
470
471
|
itemInformation: {
|
|
@@ -480,16 +481,6 @@ export class SemanticModelAdapter {
|
|
|
480
481
|
}));
|
|
481
482
|
}
|
|
482
483
|
|
|
483
|
-
/**
|
|
484
|
-
* Map VAT category from percentage
|
|
485
|
-
*/
|
|
486
|
-
private mapVATCategory(percentage?: number): string {
|
|
487
|
-
if (percentage === undefined || percentage === null) return 'S';
|
|
488
|
-
if (percentage === 0) return 'Z';
|
|
489
|
-
if (percentage > 0) return 'S';
|
|
490
|
-
return 'E'; // Exempt
|
|
491
|
-
}
|
|
492
|
-
|
|
493
484
|
/**
|
|
494
485
|
* Reverse map seller
|
|
495
486
|
*/
|
|
@@ -11,6 +11,7 @@ import { getPaymentTotals } from '../../utils/paid.amount.js';
|
|
|
11
11
|
import { toPlainDecimalString } from '../../utils/number.text.js';
|
|
12
12
|
import { resolveCountryCode } from '../../utils/country.code.js';
|
|
13
13
|
import { getPartyIdentifiers } from '../../utils/party.identifier.js';
|
|
14
|
+
import type { IEInvoiceMetadata } from '../../../interfaces/en16931-metadata.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* UBL Encoder implementation
|
|
@@ -189,17 +190,45 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
/**
|
|
192
|
-
* Writes the actual delivery date (BT-72) the
|
|
193
|
+
* Writes the actual delivery date (BT-72) and the deliver to address (BG-15)
|
|
194
|
+
* the document states. The address is `metadata.deliveryAddress` when it
|
|
195
|
+
* states a country (BT-80, which BR-57 requires of the address): the street
|
|
196
|
+
* (BT-75), the house number, the city (BT-77), the post code (BT-78), the
|
|
197
|
+
* country subdivision (BT-79) and the country. An intra-community supply
|
|
198
|
+
* states it (BR-IC-12).
|
|
193
199
|
* @param doc XML document
|
|
194
200
|
* @param root Document root
|
|
195
201
|
* @param invoice Accounting document
|
|
196
202
|
*/
|
|
197
203
|
private addDelivery(doc: Document, root: Element, invoice: TAccountingDoc): void {
|
|
198
|
-
|
|
204
|
+
const address = (invoice as TAccountingDoc & { metadata?: IEInvoiceMetadata }).metadata?.deliveryAddress;
|
|
205
|
+
const countryCode = address?.countryCode?.trim();
|
|
206
|
+
if (invoice.deliveryDate === undefined && !countryCode) {
|
|
199
207
|
return;
|
|
200
208
|
}
|
|
201
209
|
const delivery = doc.createElement('cac:Delivery');
|
|
202
|
-
|
|
210
|
+
if (invoice.deliveryDate !== undefined) {
|
|
211
|
+
this.appendElement(doc, delivery, 'cbc:ActualDeliveryDate', this.formatDate(invoice.deliveryDate, 'BT-72 actual delivery date'));
|
|
212
|
+
}
|
|
213
|
+
if (address && countryCode) {
|
|
214
|
+
const location = doc.createElement('cac:DeliveryLocation');
|
|
215
|
+
const addressNode = doc.createElement('cac:Address');
|
|
216
|
+
const append = (name: string, text: string | undefined) => {
|
|
217
|
+
if (text?.trim()) {
|
|
218
|
+
this.appendElement(doc, addressNode, name, text.trim());
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
append('cbc:StreetName', address.streetName);
|
|
222
|
+
append('cbc:BuildingNumber', address.houseNumber);
|
|
223
|
+
append('cbc:CityName', address.city);
|
|
224
|
+
append('cbc:PostalZone', address.postalCode);
|
|
225
|
+
append('cbc:CountrySubentity', address.countrySubdivision);
|
|
226
|
+
const country = doc.createElement('cac:Country');
|
|
227
|
+
this.appendElement(doc, country, 'cbc:IdentificationCode', countryCode);
|
|
228
|
+
addressNode.appendChild(country);
|
|
229
|
+
location.appendChild(addressNode);
|
|
230
|
+
delivery.appendChild(location);
|
|
231
|
+
}
|
|
203
232
|
root.appendChild(delivery);
|
|
204
233
|
}
|
|
205
234
|
|
|
@@ -456,9 +485,11 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
456
485
|
this.appendElement(doc, taxCategoryNode, 'cbc:ID', category);
|
|
457
486
|
this.appendElement(doc, taxCategoryNode, 'cbc:Percent', rate.toFixed(2));
|
|
458
487
|
|
|
459
|
-
// VAT exemption reason code (BT-121) and text (BT-120), for
|
|
460
|
-
if (exemption) {
|
|
488
|
+
// VAT exemption reason code (BT-121) and text (BT-120), for the categories that state one (BR-x-10)
|
|
489
|
+
if (exemption?.code) {
|
|
461
490
|
this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReasonCode', exemption.code);
|
|
491
|
+
}
|
|
492
|
+
if (exemption?.reason) {
|
|
462
493
|
this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReason', exemption.reason);
|
|
463
494
|
}
|
|
464
495
|
|
|
@@ -573,8 +604,10 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
573
604
|
// Invoiced item VAT category code (BT-151)
|
|
574
605
|
this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:ID', totals.lineVatCategories[index]);
|
|
575
606
|
|
|
576
|
-
//
|
|
577
|
-
|
|
607
|
+
// Invoiced item VAT rate (BT-152) with 2 decimal places; an item not subject to VAT has none (BR-O-05)
|
|
608
|
+
if (totals.lineVatCategories[index] !== 'O') {
|
|
609
|
+
this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:Percent', item.vatPercentage.toFixed(2));
|
|
610
|
+
}
|
|
578
611
|
|
|
579
612
|
// Tax scheme
|
|
580
613
|
const taxSchemeNode = doc.createElement('cac:TaxScheme');
|
|
@@ -5,6 +5,8 @@ import { DOMParser, xpath } from '../../plugins.js';
|
|
|
5
5
|
import { EInvoiceParsingError } from '../../errors.js';
|
|
6
6
|
import { getAccountingDocType } from '../utils/document.typecode.js';
|
|
7
7
|
import { readUblStatedValues } from '../utils/stated.values.js';
|
|
8
|
+
import { applyStatedVatCategories } from '../utils/vat.category.js';
|
|
9
|
+
import { deliveryAddressOf, withDeliveryAddress, type TDeliveryAddress } from '../utils/delivery.address.js';
|
|
8
10
|
import type { IEInvoiceStatedValues } from '../../interfaces/stated.values.js';
|
|
9
11
|
|
|
10
12
|
const ublParser = new DOMParser();
|
|
@@ -54,6 +56,35 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
|
|
|
54
56
|
// the values the document states, read first: an amount that is no number is refused
|
|
55
57
|
this.statedValues = this.assertStatedValuesReadable(readUblStatedValues(this.doc), 'ubl');
|
|
56
58
|
|
|
59
|
+
// a CreditNote root is a credit note whatever its type code says
|
|
60
|
+
const document = await this.decodeByType();
|
|
61
|
+
// every item with the VAT category its line states and its breakdown's exemption reason
|
|
62
|
+
applyStatedVatCategories(document.items, this.statedValues);
|
|
63
|
+
return withDeliveryAddress(document, this.readDeliveryAddress());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reads the deliver to address (BG-15) of the first delivery: the street
|
|
68
|
+
* (BT-75), the house number, the city (BT-77), the post code (BT-78), the
|
|
69
|
+
* country subdivision (BT-79) and the country (BT-80); undefined when the
|
|
70
|
+
* document states no address with a country
|
|
71
|
+
*/
|
|
72
|
+
private readDeliveryAddress(): TDeliveryAddress | undefined {
|
|
73
|
+
const address = '/*/cac:Delivery[1]/cac:DeliveryLocation/cac:Address';
|
|
74
|
+
return deliveryAddressOf({
|
|
75
|
+
streetName: this.getText(`${address}/cbc:StreetName`, this.doc),
|
|
76
|
+
houseNumber: this.getText(`${address}/cbc:BuildingNumber`, this.doc),
|
|
77
|
+
city: this.getText(`${address}/cbc:CityName`, this.doc),
|
|
78
|
+
postalCode: this.getText(`${address}/cbc:PostalZone`, this.doc),
|
|
79
|
+
countrySubdivision: this.getText(`${address}/cbc:CountrySubentity`, this.doc),
|
|
80
|
+
countryCode: this.getText(`${address}/cac:Country/cbc:IdentificationCode`, this.doc),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Decodes the document by its root and document type code (BT-3)
|
|
86
|
+
*/
|
|
87
|
+
private async decodeByType(): Promise<TAccountingDoc> {
|
|
57
88
|
// a CreditNote root is a credit note whatever its type code says
|
|
58
89
|
if (this.getDocumentType() === UBLDocumentType.CREDIT_NOTE) {
|
|
59
90
|
return this.decodeCreditNote();
|
|
@@ -265,8 +265,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
|
|
265
265
|
subject: subject,
|
|
266
266
|
items: items,
|
|
267
267
|
dueInDays: dueInDays,
|
|
268
|
-
// reverse charge when every line is VAT category AE;
|
|
269
|
-
//
|
|
268
|
+
// reverse charge when every line is VAT category AE; in a document mixing AE lines with
|
|
269
|
+
// others each item carries its category (applyStatedVatCategories)
|
|
270
270
|
reverseCharge: this.isReverseChargeDocument(),
|
|
271
271
|
currency: currencyCode as finance.TCurrency,
|
|
272
272
|
notes: notes,
|
|
@@ -51,6 +51,42 @@ export const getWritableDueDate = (issueTimestamp: unknown, dueInDays: unknown,
|
|
|
51
51
|
return dueDate;
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The UTC midnight of a calendar day, or undefined when there is no such day
|
|
56
|
+
* @param year The year, 1 or later
|
|
57
|
+
* @param month The month, 1 to 12
|
|
58
|
+
* @param day The day of the month
|
|
59
|
+
*/
|
|
60
|
+
const utcMidnightOf = (year: number, month: number, day: number): number | undefined => {
|
|
61
|
+
if (year < 1) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const date = new Date(0);
|
|
65
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
66
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return date.getTime();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether a time of day is one: hours 0 to 23, minutes and seconds 0 to 59
|
|
74
|
+
* @param hours The hours
|
|
75
|
+
* @param minutes The minutes
|
|
76
|
+
* @param seconds The seconds, 0 when not stated
|
|
77
|
+
*/
|
|
78
|
+
const isTimeOfDay = (hours: number, minutes: number, seconds: number): boolean =>
|
|
79
|
+
hours <= 23 && minutes <= 59 && seconds <= 59;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether a time zone offset is one an `xsd:date` or `xsd:dateTime` may carry:
|
|
83
|
+
* at most 14:00
|
|
84
|
+
* @param hours The hours of the offset
|
|
85
|
+
* @param minutes The minutes of the offset
|
|
86
|
+
*/
|
|
87
|
+
const isZoneOffset = (hours: number, minutes: number): boolean =>
|
|
88
|
+
hours < 14 ? minutes <= 59 : hours === 14 && minutes === 0;
|
|
89
|
+
|
|
54
90
|
/**
|
|
55
91
|
* The calendar day a UBL `xsd:date` names (BT-2, BT-9, BT-26, BT-72, BT-73,
|
|
56
92
|
* BT-74), as the UTC midnight of that day; undefined when the value is no such
|
|
@@ -66,20 +102,57 @@ export const parseXsdDateDay = (value: string): number | undefined => {
|
|
|
66
102
|
if (!match) {
|
|
67
103
|
return undefined;
|
|
68
104
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const [zoneHours, zoneMinutes] = [Number(match[4]), Number(match[5])];
|
|
72
|
-
if (zoneHours > 14 || zoneMinutes > 59 || (zoneHours === 14 && zoneMinutes !== 0)) {
|
|
73
|
-
return undefined;
|
|
74
|
-
}
|
|
105
|
+
if (match[4] !== undefined && !isZoneOffset(Number(match[4]), Number(match[5]))) {
|
|
106
|
+
return undefined;
|
|
75
107
|
}
|
|
76
|
-
|
|
108
|
+
return utcMidnightOf(Number(match[1]), Number(match[2]), Number(match[3]));
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The calendar day an ISO 8601 date, or date and time, names, as the UTC
|
|
113
|
+
* midnight of that day; undefined when the value is no such date. A CII date
|
|
114
|
+
* without a format code may be stated so. The value is `YYYY-MM-DD`,
|
|
115
|
+
* optionally followed by a time of day `Thh:mm`, `Thh:mm:ss` or
|
|
116
|
+
* `Thh:mm:ss.fff`, and then optionally by a time zone: `Z`, or `+hh:mm` /
|
|
117
|
+
* `-hh:mm` of at most 14:00. Neither the time of day nor the zone moves the
|
|
118
|
+
* day the document states: `2026-09-11T00:30:00+02:00` is 22:30 UTC on
|
|
119
|
+
* 10 September, but the document states 11 September, and that is the day an
|
|
120
|
+
* invoice is dated with (§ 14 Abs. 4 Satz 1 Nr. 3 UStG asks for the issue date,
|
|
121
|
+
* a calendar day).
|
|
122
|
+
* @param value The date as the document states it
|
|
123
|
+
*/
|
|
124
|
+
export const parseIsoDateTimeDay = (value: string): number | undefined => {
|
|
125
|
+
const match =
|
|
126
|
+
/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?)?(?:Z|[+-](\d{2}):(\d{2}))?$/.exec(value.trim());
|
|
127
|
+
if (!match) {
|
|
77
128
|
return undefined;
|
|
78
129
|
}
|
|
79
|
-
|
|
80
|
-
date.setUTCFullYear(year, month - 1, day);
|
|
81
|
-
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
|
|
130
|
+
if (match[4] !== undefined && !isTimeOfDay(Number(match[4]), Number(match[5]), Number(match[6] ?? 0))) {
|
|
82
131
|
return undefined;
|
|
83
132
|
}
|
|
84
|
-
|
|
133
|
+
if (match[7] !== undefined && !isZoneOffset(Number(match[7]), Number(match[8]))) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
return utcMidnightOf(Number(match[1]), Number(match[2]), Number(match[3]));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The calendar day a CII date in format 102 (`YYYYMMDD`), 203
|
|
141
|
+
* (`YYYYMMDDhhmm`) or 204 (`YYYYMMDDhhmmss`) names, as the UTC midnight of that
|
|
142
|
+
* day; undefined when the value is no such date in that format. A time of day
|
|
143
|
+
* in formats 203 and 204 does not move the day stated; the formats carry no
|
|
144
|
+
* time zone.
|
|
145
|
+
* @param value The date as the document states it
|
|
146
|
+
* @param format The format code: '102', '203' or '204'
|
|
147
|
+
*/
|
|
148
|
+
export const parseCiiDateDay = (value: string, format: '102' | '203' | '204'): number | undefined => {
|
|
149
|
+
const timeDigits = { '102': '', '203': '(\\d{2})(\\d{2})', '204': '(\\d{2})(\\d{2})(\\d{2})' }[format];
|
|
150
|
+
const match = new RegExp(`^(\\d{4})(\\d{2})(\\d{2})${timeDigits}$`).exec(value.trim());
|
|
151
|
+
if (!match) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
if (format !== '102' && !isTimeOfDay(Number(match[4]), Number(match[5]), Number(match[6] ?? 0))) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
return utcMidnightOf(Number(match[1]), Number(match[2]), Number(match[3]));
|
|
85
158
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TAccountingDoc } from '../../interfaces/common.js';
|
|
2
|
+
import type { IEInvoiceMetadata } from '../../interfaces/en16931-metadata.js';
|
|
3
|
+
|
|
4
|
+
/** The deliver to address (BG-15) of a document, as `metadata.deliveryAddress` carries it */
|
|
5
|
+
export type TDeliveryAddress = NonNullable<IEInvoiceMetadata['deliveryAddress']>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The deliver to address from the texts a document states, each trimmed and
|
|
9
|
+
* left out when empty; undefined when it states no country (BT-80), which
|
|
10
|
+
* every deliver to address has (BR-57)
|
|
11
|
+
* @param texts The texts read, '' where the document states none
|
|
12
|
+
*/
|
|
13
|
+
export const deliveryAddressOf = (texts: Record<keyof TDeliveryAddress, string>): TDeliveryAddress | undefined => {
|
|
14
|
+
const address: TDeliveryAddress = {};
|
|
15
|
+
for (const [field, text] of Object.entries(texts) as Array<[keyof TDeliveryAddress, string]>) {
|
|
16
|
+
if (text.trim()) {
|
|
17
|
+
address[field] = text.trim();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return address.countryCode ? address : undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The decoded document with the deliver to address in its metadata, or
|
|
25
|
+
* unchanged when it states none
|
|
26
|
+
* @param document The decoded document
|
|
27
|
+
* @param address The deliver to address read
|
|
28
|
+
*/
|
|
29
|
+
export const withDeliveryAddress = <TDocument extends TAccountingDoc>(document: TDocument, address: TDeliveryAddress | undefined): TDocument => {
|
|
30
|
+
if (!address) {
|
|
31
|
+
return document;
|
|
32
|
+
}
|
|
33
|
+
const withMetadata = document as TDocument & { metadata?: IEInvoiceMetadata };
|
|
34
|
+
withMetadata.metadata = { ...withMetadata.metadata, deliveryAddress: address };
|
|
35
|
+
return withMetadata;
|
|
36
|
+
};
|