@fin.cx/einvoice 10.0.1 → 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/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/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 +3 -3
- package/readme.md +55 -19
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +8 -14
- 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/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
|
@@ -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,
|
|
@@ -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
|
+
};
|
|
@@ -4,7 +4,14 @@ import { DecimalCurrencyCalculator } from './currency.calculator.decimal.js';
|
|
|
4
4
|
import { toPlainDecimalString } from './number.text.js';
|
|
5
5
|
import { EInvoiceFormatError } from '../../errors.js';
|
|
6
6
|
import type { ValidationResult } from '../validation/validation.types.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getDefaultVatExemption,
|
|
9
|
+
getItemVatCategory,
|
|
10
|
+
getItemVatExemption,
|
|
11
|
+
mergeVatExemption,
|
|
12
|
+
type IVatExemption,
|
|
13
|
+
type TVatCategoryCode,
|
|
14
|
+
} from './vat.category.js';
|
|
8
15
|
|
|
9
16
|
/** An item amount that is no finite number, so no total can be computed from it */
|
|
10
17
|
export interface IInvalidItemAmount {
|
|
@@ -49,6 +56,14 @@ export const getTotalsSkippedResult = (invalid: IInvalidItemAmount[], source: st
|
|
|
49
56
|
field: invalid.map((entry) => `items[${entry.index}].${entry.field}`).join(', '),
|
|
50
57
|
});
|
|
51
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The VAT categories whose tax amount (BT-117) is the taxable amount × rate:
|
|
61
|
+
* standard rated (BR-S-09), IGIC (BR-AF-09) and IPSI (BR-AG-09). Every other
|
|
62
|
+
* category states the tax 0 (BR-Z-09, BR-E-09, BR-AE-09, BR-IC-09, BR-G-09,
|
|
63
|
+
* BR-O-09).
|
|
64
|
+
*/
|
|
65
|
+
const TAXED_CATEGORIES: readonly TVatCategoryCode[] = ['S', 'L', 'M'];
|
|
66
|
+
|
|
52
67
|
/** The most decimals an amount may have in EN 16931 (BR-DEC-09 to BR-DEC-23) */
|
|
53
68
|
export const EN16931_MAX_AMOUNT_DECIMALS = 2;
|
|
54
69
|
|
|
@@ -65,8 +80,8 @@ export const getDocumentCalculator = (currency: TAccountingDoc['currency']): Dec
|
|
|
65
80
|
/**
|
|
66
81
|
* The VAT breakdown of one VAT category and rate (BG-23): the category
|
|
67
82
|
* (BT-118), the rate (BT-119), the taxable amount (BT-116), the tax on it
|
|
68
|
-
* (BT-117) and
|
|
69
|
-
* (
|
|
83
|
+
* (BT-117) and the exemption reason (BT-121, BT-120): the one its items state,
|
|
84
|
+
* else the one its category states by default (`getDefaultVatExemption`).
|
|
70
85
|
*/
|
|
71
86
|
export interface IDocumentVatGroup {
|
|
72
87
|
category: TVatCategoryCode;
|
|
@@ -86,7 +101,7 @@ export interface IDocumentTotals {
|
|
|
86
101
|
minorUnits: number;
|
|
87
102
|
/** the Invoice line net amount (BT-131) of each item, in item order: quantity × net price, rounded */
|
|
88
103
|
lineNetAmounts: Decimal[];
|
|
89
|
-
/** the VAT category (BT-151) of each item, in item order */
|
|
104
|
+
/** the VAT category (BT-151) of each item, in item order: the one it states, else AE or S (`getItemVatCategory`) */
|
|
90
105
|
lineVatCategories: TVatCategoryCode[];
|
|
91
106
|
/** Sum of Invoice line net amount (BT-106) */
|
|
92
107
|
lineTotal: Decimal;
|
|
@@ -107,7 +122,8 @@ export interface IDocumentTotals {
|
|
|
107
122
|
* rounded; the sum of line net amounts (BT-106) is their sum (BR-CO-10);
|
|
108
123
|
* each VAT category taxable amount (BT-116) is the sum of the line net amounts
|
|
109
124
|
* of that category and rate, and its tax amount (BT-117) is BT-116 × rate / 100
|
|
110
|
-
* rounded (BR-CO-17), 0 for
|
|
125
|
+
* rounded (BR-CO-17), 0 for the categories whose rate is 0 or that state no tax
|
|
126
|
+
* (BR-Z-09, BR-E-09, BR-AE-09, BR-IC-09, BR-G-09, BR-O-09); the total VAT (BT-110) is the sum of the category tax amounts
|
|
111
127
|
* (BR-CO-14); the total with VAT (BT-112) is BT-109 + BT-110 (BR-CO-15).
|
|
112
128
|
* @param accountingDoc The document
|
|
113
129
|
*/
|
|
@@ -116,10 +132,12 @@ export const computeDocumentTotals = (
|
|
|
116
132
|
): IDocumentTotals => {
|
|
117
133
|
const calculator = getDocumentCalculator(accountingDoc.currency);
|
|
118
134
|
const minorUnits = calculator.getCurrencyInfo().minorUnits;
|
|
119
|
-
const category = getVatCategory(accountingDoc);
|
|
120
135
|
const lineNetAmounts: Decimal[] = [];
|
|
121
136
|
const lineVatCategories: TVatCategoryCode[] = [];
|
|
122
|
-
const taxableByGroup = new Map<
|
|
137
|
+
const taxableByGroup = new Map<
|
|
138
|
+
string,
|
|
139
|
+
{ category: TVatCategoryCode; rate: number; taxable: Decimal; exemptions: IVatExemption[] }
|
|
140
|
+
>();
|
|
123
141
|
// an amount that is no number cannot be computed with; it is refused, not treated as 0
|
|
124
142
|
const [firstInvalid] = findInvalidItemAmounts(accountingDoc.items);
|
|
125
143
|
if (firstInvalid) {
|
|
@@ -136,21 +154,30 @@ export const computeDocumentTotals = (
|
|
|
136
154
|
toPlainDecimalString(item.unitNetPrice),
|
|
137
155
|
);
|
|
138
156
|
lineNetAmounts.push(lineNet);
|
|
157
|
+
const category = getItemVatCategory(accountingDoc, item);
|
|
139
158
|
lineVatCategories.push(category);
|
|
140
159
|
// grouped by VAT category and rate: the same rate under two categories is two groups
|
|
141
160
|
const key = `${category}|${item.vatPercentage}`;
|
|
142
|
-
const group = taxableByGroup.get(key) ?? { category, rate: item.vatPercentage, taxable: Decimal.ZERO };
|
|
161
|
+
const group = taxableByGroup.get(key) ?? { category, rate: item.vatPercentage, taxable: Decimal.ZERO, exemptions: [] };
|
|
143
162
|
group.taxable = group.taxable.add(lineNet);
|
|
163
|
+
const exemption = getItemVatExemption(item);
|
|
164
|
+
if (exemption) {
|
|
165
|
+
group.exemptions.push(exemption);
|
|
166
|
+
}
|
|
144
167
|
taxableByGroup.set(key, group);
|
|
145
168
|
}
|
|
146
169
|
const vatGroups: IDocumentVatGroup[] = [...taxableByGroup.values()].map((group) => {
|
|
147
|
-
|
|
170
|
+
// the reason the items state; items of one breakdown that state different ones are refused
|
|
171
|
+
// on export and reported by the validators (findVatCategoryViolations)
|
|
172
|
+
const exemption = mergeVatExemption(group.exemptions[0], getDefaultVatExemption(group.category, accountingDoc.language));
|
|
148
173
|
return {
|
|
149
174
|
category: group.category,
|
|
150
175
|
rate: group.rate,
|
|
151
176
|
taxableAmount: calculator.round(group.taxable),
|
|
152
|
-
// reverse charge states
|
|
153
|
-
taxAmount:
|
|
177
|
+
// only a taxed category states tax; reverse charge states none, the recipient owes it (BR-AE-09)
|
|
178
|
+
taxAmount: TAXED_CATEGORIES.includes(group.category)
|
|
179
|
+
? calculator.calculateVAT(group.taxable, group.rate)
|
|
180
|
+
: calculator.round(Decimal.ZERO),
|
|
154
181
|
...(exemption ? { exemption } : {}),
|
|
155
182
|
};
|
|
156
183
|
});
|
|
@@ -485,8 +485,11 @@ export const findEnvelopeDifferences = (
|
|
|
485
485
|
}
|
|
486
486
|
});
|
|
487
487
|
values.vatBreakdown.forEach((group, index) => {
|
|
488
|
+
// a breakdown not subject to VAT (O) may state no rate; the envelope's has the rate 0
|
|
488
489
|
const computed = totals.vatGroups.find(
|
|
489
|
-
(candidate) =>
|
|
490
|
+
(candidate) =>
|
|
491
|
+
candidate.category === group.categoryCode &&
|
|
492
|
+
(group.rate === undefined ? group.categoryCode === 'O' : decimal(candidate.rate).equals(decimal(group.rate))),
|
|
490
493
|
);
|
|
491
494
|
if (!computed) {
|
|
492
495
|
differs('BG-23', `vatBreakdown[${index}]`, `${group.categoryCode ?? '?'} ${group.rate ?? ''}`.trim(), 'no breakdown of that category and rate');
|