@fin.cx/einvoice 8.0.0 → 8.2.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 +14 -1
- package/dist_ts/einvoice.js +48 -3
- package/dist_ts/formats/base/base.encoder.d.ts +1 -1
- package/dist_ts/formats/base/base.validator.d.ts +5 -0
- package/dist_ts/formats/base/base.validator.js +28 -1
- package/dist_ts/formats/cii/cii.decoder.d.ts +12 -1
- package/dist_ts/formats/cii/cii.decoder.js +34 -1
- package/dist_ts/formats/cii/cii.encoder.d.ts +14 -3
- package/dist_ts/formats/cii/cii.encoder.js +58 -6
- package/dist_ts/formats/cii/cii.types.d.ts +1 -0
- package/dist_ts/formats/cii/cii.types.js +3 -2
- package/dist_ts/formats/cii/cii.validator.d.ts +11 -0
- package/dist_ts/formats/cii/cii.validator.js +35 -1
- package/dist_ts/formats/cii/facturx/facturx.decoder.d.ts +1 -1
- package/dist_ts/formats/cii/facturx/facturx.decoder.js +11 -5
- package/dist_ts/formats/cii/facturx/facturx.encoder.d.ts +2 -2
- package/dist_ts/formats/cii/facturx/facturx.encoder.js +17 -11
- package/dist_ts/formats/cii/facturx/facturx.validator.js +8 -4
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.d.ts +1 -1
- package/dist_ts/formats/cii/zugferd/zugferd.decoder.js +11 -5
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.d.ts +1 -1
- package/dist_ts/formats/cii/zugferd/zugferd.encoder.js +20 -14
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.d.ts +7 -1
- package/dist_ts/formats/cii/zugferd/zugferd.v1.decoder.js +29 -5
- package/dist_ts/formats/cii/zugferd/zugferd.validator.js +5 -4
- package/dist_ts/formats/converters/xml-to-einvoice.converter.d.ts +5 -0
- package/dist_ts/formats/converters/xml-to-einvoice.converter.js +11 -2
- package/dist_ts/formats/semantic/semantic.adapter.js +2 -2
- package/dist_ts/formats/ubl/en16931.ubl.validator.js +5 -1
- package/dist_ts/formats/ubl/generic/ubl.encoder.d.ts +23 -1
- package/dist_ts/formats/ubl/generic/ubl.encoder.js +71 -12
- package/dist_ts/formats/ubl/ubl.decoder.d.ts +1 -1
- package/dist_ts/formats/ubl/ubl.encoder.d.ts +16 -3
- package/dist_ts/formats/ubl/ubl.encoder.js +75 -13
- package/dist_ts/formats/ubl/ubl.validator.d.ts +11 -0
- package/dist_ts/formats/ubl/ubl.validator.js +35 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.d.ts +6 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.decoder.js +39 -8
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +1 -1
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +4 -2
- package/dist_ts/formats/utils/date.value.d.ts +10 -0
- package/dist_ts/formats/utils/date.value.js +21 -0
- package/dist_ts/formats/utils/document.typecode.d.ts +3 -3
- package/dist_ts/formats/utils/document.typecode.js +8 -4
- package/dist_ts/formats/utils/preceding.invoice.d.ts +26 -0
- package/dist_ts/formats/utils/preceding.invoice.js +52 -0
- package/dist_ts/formats/utils/unit.codes.d.ts +40 -0
- package/dist_ts/formats/utils/unit.codes.js +193 -0
- package/dist_ts/formats/validation/codelist.validator.d.ts +1 -1
- package/dist_ts/formats/validation/codelist.validator.js +7 -9
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +13 -13
- package/dist_ts/formats/validation/validation.types.d.ts +0 -4
- package/dist_ts/formats/validation/validation.types.js +1 -28
- package/dist_ts/index.d.ts +5 -1
- package/dist_ts/index.js +6 -1
- package/dist_ts/interfaces/common.d.ts +6 -2
- package/dist_ts_install/download-schematron.js +3 -5
- package/package.json +8 -8
- package/readme.md +103 -10
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +51 -2
- package/ts/formats/base/base.encoder.ts +1 -1
- package/ts/formats/base/base.validator.ts +28 -0
- package/ts/formats/cii/cii.decoder.ts +43 -1
- package/ts/formats/cii/cii.encoder.ts +62 -6
- package/ts/formats/cii/cii.types.ts +2 -1
- package/ts/formats/cii/cii.validator.ts +47 -0
- package/ts/formats/cii/facturx/facturx.decoder.ts +11 -4
- package/ts/formats/cii/facturx/facturx.encoder.ts +17 -10
- package/ts/formats/cii/facturx/facturx.validator.ts +9 -3
- package/ts/formats/cii/zugferd/zugferd.decoder.ts +11 -4
- package/ts/formats/cii/zugferd/zugferd.encoder.ts +20 -13
- package/ts/formats/cii/zugferd/zugferd.v1.decoder.ts +32 -4
- package/ts/formats/cii/zugferd/zugferd.validator.ts +4 -3
- package/ts/formats/converters/xml-to-einvoice.converter.ts +11 -1
- package/ts/formats/semantic/semantic.adapter.ts +1 -1
- package/ts/formats/ubl/en16931.ubl.validator.ts +6 -0
- package/ts/formats/ubl/generic/ubl.encoder.ts +78 -11
- package/ts/formats/ubl/ubl.decoder.ts +1 -1
- package/ts/formats/ubl/ubl.encoder.ts +77 -13
- package/ts/formats/ubl/ubl.validator.ts +47 -0
- package/ts/formats/ubl/xrechnung/xrechnung.decoder.ts +47 -7
- package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +4 -1
- package/ts/formats/utils/date.value.ts +21 -0
- package/ts/formats/utils/document.typecode.ts +7 -3
- package/ts/formats/utils/preceding.invoice.ts +74 -0
- package/ts/formats/utils/unit.codes.ts +215 -0
- package/ts/formats/validation/codelist.validator.ts +8 -19
- package/ts/formats/validation/en16931.business-rules.validator.ts +13 -13
- package/ts/formats/validation/validation.types.ts +0 -28
- package/ts/index.ts +12 -0
- package/ts/interfaces/common.ts +6 -1
|
@@ -4,6 +4,7 @@ import { ZUGFERD_PROFILE_IDS } from './zugferd.types.js';
|
|
|
4
4
|
import { CIIProfile } from '../cii.types.js';
|
|
5
5
|
import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
6
6
|
import { getDocumentTypeCode } from '../../utils/document.typecode.js';
|
|
7
|
+
import { getWritableDate } from '../../utils/date.value.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Encoder for ZUGFeRD invoice format
|
|
@@ -35,7 +36,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
|
-
* Encodes an invoice document (invoice, debit note or self-billed invoice) into ZUGFeRD XML
|
|
39
|
+
* Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into ZUGFeRD XML
|
|
39
40
|
* @param invoice Invoice document to encode
|
|
40
41
|
* @returns ZUGFeRD XML string
|
|
41
42
|
*/
|
|
@@ -43,7 +44,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
43
44
|
// Create base XML
|
|
44
45
|
const xmlDoc = this.createBaseXml();
|
|
45
46
|
|
|
46
|
-
// Set document type code (380 invoice, 383 debit note, 389 self-billed invoice)
|
|
47
|
+
// Set document type code (380 invoice, 383 debit note, 384 corrected invoice, 389 self-billed invoice)
|
|
47
48
|
this.setDocumentTypeCode(xmlDoc, getDocumentTypeCode(invoice.accountingDocType));
|
|
48
49
|
|
|
49
50
|
// Add common invoice data
|
|
@@ -149,7 +150,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
149
150
|
const issueDateElement = doc.createElement('ram:IssueDateTime');
|
|
150
151
|
const dateStringElement = doc.createElement('udt:DateTimeString');
|
|
151
152
|
dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
152
|
-
dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.date);
|
|
153
|
+
dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.date, 'BT-2 invoice issue date');
|
|
153
154
|
issueDateElement.appendChild(dateStringElement);
|
|
154
155
|
documentElement.appendChild(issueDateElement);
|
|
155
156
|
|
|
@@ -187,6 +188,9 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
187
188
|
// Add settlement section with payment terms and totals
|
|
188
189
|
this.addSettlementSection(doc, transactionElement, invoice);
|
|
189
190
|
|
|
191
|
+
// Add the preceding invoice references (BG-3) the document states
|
|
192
|
+
this.addPrecedingInvoiceReferences(doc, invoice);
|
|
193
|
+
|
|
190
194
|
// Add line items
|
|
191
195
|
this.addLineItems(doc, transactionElement, invoice);
|
|
192
196
|
}
|
|
@@ -316,12 +320,12 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
316
320
|
transactionElement.appendChild(deliveryElement);
|
|
317
321
|
|
|
318
322
|
// Add delivery date if available
|
|
319
|
-
if (invoice.deliveryDate) {
|
|
323
|
+
if (invoice.deliveryDate !== undefined) {
|
|
320
324
|
const deliveryDateElement = doc.createElement('ram:ActualDeliverySupplyChainEvent');
|
|
321
325
|
const occurrenceDateElement = doc.createElement('ram:OccurrenceDateTime');
|
|
322
326
|
const dateStringElement = doc.createElement('udt:DateTimeString');
|
|
323
327
|
dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
324
|
-
dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.deliveryDate);
|
|
328
|
+
dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.deliveryDate, 'BT-72 actual delivery date');
|
|
325
329
|
occurrenceDateElement.appendChild(dateStringElement);
|
|
326
330
|
deliveryDateElement.appendChild(occurrenceDateElement);
|
|
327
331
|
deliveryElement.appendChild(deliveryDateElement);
|
|
@@ -332,21 +336,21 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
332
336
|
const periodElement = doc.createElement('ram:BillingSpecifiedPeriod');
|
|
333
337
|
|
|
334
338
|
// Start date
|
|
335
|
-
if (invoice.periodOfPerformance.from) {
|
|
339
|
+
if (invoice.periodOfPerformance.from !== undefined) {
|
|
336
340
|
const startDateElement = doc.createElement('ram:StartDateTime');
|
|
337
341
|
const startDateStringElement = doc.createElement('udt:DateTimeString');
|
|
338
342
|
startDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
339
|
-
startDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.from);
|
|
343
|
+
startDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.from, 'BT-73 invoicing period start date');
|
|
340
344
|
startDateElement.appendChild(startDateStringElement);
|
|
341
345
|
periodElement.appendChild(startDateElement);
|
|
342
346
|
}
|
|
343
347
|
|
|
344
348
|
// End date
|
|
345
|
-
if (invoice.periodOfPerformance.to) {
|
|
349
|
+
if (invoice.periodOfPerformance.to !== undefined) {
|
|
346
350
|
const endDateElement = doc.createElement('ram:EndDateTime');
|
|
347
351
|
const endDateStringElement = doc.createElement('udt:DateTimeString');
|
|
348
352
|
endDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
349
|
-
endDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.to);
|
|
353
|
+
endDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.to, 'BT-74 invoicing period end date');
|
|
350
354
|
endDateElement.appendChild(endDateStringElement);
|
|
351
355
|
periodElement.appendChild(endDateElement);
|
|
352
356
|
}
|
|
@@ -391,7 +395,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
391
395
|
const dueDate = new Date(invoice.date);
|
|
392
396
|
dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
|
|
393
397
|
|
|
394
|
-
dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime());
|
|
398
|
+
dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime(), 'BT-9 payment due date');
|
|
395
399
|
dueDateElement.appendChild(dateStringElement);
|
|
396
400
|
paymentTermsElement.appendChild(dueDateElement);
|
|
397
401
|
|
|
@@ -599,7 +603,10 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
599
603
|
const deliveryElement = doc.createElement('ram:SpecifiedLineTradeDelivery');
|
|
600
604
|
const quantityElement = doc.createElement('ram:BilledQuantity');
|
|
601
605
|
quantityElement.textContent = item.unitQuantity.toString();
|
|
602
|
-
|
|
606
|
+
// a line without a unit is written without one, so that BR-23 reports it
|
|
607
|
+
if (item.unitType) {
|
|
608
|
+
quantityElement.setAttribute('unitCode', item.unitType);
|
|
609
|
+
}
|
|
603
610
|
deliveryElement.appendChild(quantityElement);
|
|
604
611
|
lineItemElement.appendChild(deliveryElement);
|
|
605
612
|
|
|
@@ -652,8 +659,8 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
|
652
659
|
* @param timestamp Timestamp to format
|
|
653
660
|
* @returns Formatted date string
|
|
654
661
|
*/
|
|
655
|
-
private formatDateYYYYMMDD(timestamp: number): string {
|
|
656
|
-
const date =
|
|
662
|
+
private formatDateYYYYMMDD(timestamp: number, field: string): string {
|
|
663
|
+
const date = getWritableDate(timestamp, field, 'cii');
|
|
657
664
|
const year = date.getFullYear();
|
|
658
665
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
659
666
|
const day = date.getDate().toString().padStart(2, '0');
|
|
@@ -6,9 +6,16 @@ import type {
|
|
|
6
6
|
TInvoiceDocument,
|
|
7
7
|
} from '../../../interfaces/common.js';
|
|
8
8
|
import { ZUGFERD_V1_NAMESPACES } from '../cii.types.js';
|
|
9
|
-
import { business, finance } from '../../../plugins.js';
|
|
9
|
+
import { business, finance, xpath } from '../../../plugins.js';
|
|
10
10
|
import { EN16931Validator } from '../../validation/en16931.validator.js';
|
|
11
11
|
|
|
12
|
+
/** XPath selection with the ZUGFeRD v1 namespaces */
|
|
13
|
+
const zugferdV1Select = xpath.useNamespaces({
|
|
14
|
+
rsm: ZUGFERD_V1_NAMESPACES.RSM,
|
|
15
|
+
ram: ZUGFERD_V1_NAMESPACES.RAM,
|
|
16
|
+
udt: ZUGFERD_V1_NAMESPACES.UDT,
|
|
17
|
+
});
|
|
18
|
+
|
|
12
19
|
/**
|
|
13
20
|
* Decoder for ZUGFeRD v1 invoice format
|
|
14
21
|
*/
|
|
@@ -28,6 +35,22 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
28
35
|
};
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Reads the actual delivery date of the v1 header delivery
|
|
40
|
+
* (`ram:ApplicableSupplyChainTradeDelivery`); undefined when the document
|
|
41
|
+
* does not state one.
|
|
42
|
+
*/
|
|
43
|
+
protected override extractDeliveryDate(): number | undefined {
|
|
44
|
+
const deliveryDatePath =
|
|
45
|
+
'/rsm:CrossIndustryDocument/rsm:SpecifiedSupplyChainTradeTransaction/ram:ApplicableSupplyChainTradeDelivery/ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString';
|
|
46
|
+
const deliveryDate = String(zugferdV1Select(`string(${deliveryDatePath})`, this.doc)).trim();
|
|
47
|
+
if (!deliveryDate) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const format = String(zugferdV1Select(`string(${deliveryDatePath}/@format)`, this.doc)).trim();
|
|
51
|
+
return this.parseRequiredCIIDate(deliveryDate, format);
|
|
52
|
+
}
|
|
53
|
+
|
|
31
54
|
/**
|
|
32
55
|
* Decodes a ZUGFeRD v1 credit note
|
|
33
56
|
* @returns Promise resolving to a TCreditNote object
|
|
@@ -37,7 +60,7 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
37
60
|
}
|
|
38
61
|
|
|
39
62
|
/**
|
|
40
|
-
* Decodes a ZUGFeRD v1 invoice document: an invoice, a debit note or a self-billed invoice
|
|
63
|
+
* Decodes a ZUGFeRD v1 invoice document: an invoice, a corrected invoice, a debit note or a self-billed invoice
|
|
41
64
|
* @param accountingDocType The type the document type code stands for
|
|
42
65
|
* @returns Promise resolving to the invoice document
|
|
43
66
|
*/
|
|
@@ -99,6 +122,9 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
99
122
|
notes = allNotes.slice(1); // Remove subject from notes
|
|
100
123
|
}
|
|
101
124
|
|
|
125
|
+
// Extract the actual delivery date, if stated
|
|
126
|
+
const deliveryDate = this.extractDeliveryDate();
|
|
127
|
+
|
|
102
128
|
// Check for reverse charge
|
|
103
129
|
const reverseCharge = this.exists('//ram:SpecifiedTradeAllowanceCharge/ram:ReasonCode[text()="62"]');
|
|
104
130
|
|
|
@@ -126,7 +152,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
126
152
|
reverseCharge: reverseCharge,
|
|
127
153
|
currency: currencyCode as finance.TCurrency,
|
|
128
154
|
notes: notes,
|
|
129
|
-
|
|
155
|
+
// the day of supply as the document states it (BT-72), never the issue date in its place
|
|
156
|
+
...(deliveryDate === undefined ? {} : { deliveryDate }),
|
|
130
157
|
objectActions: []
|
|
131
158
|
};
|
|
132
159
|
|
|
@@ -206,7 +233,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
|
|
|
206
233
|
const name = this.getText('ram:SpecifiedTradeProduct/ram:Name', itemNode);
|
|
207
234
|
const articleNumber = this.getText('ram:SpecifiedTradeProduct/ram:SellerAssignedID', itemNode);
|
|
208
235
|
const unitQuantity = this.getNumber('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', itemNode);
|
|
209
|
-
|
|
236
|
+
// an absent unit code (BT-130) stays absent, so that BR-23 reports it
|
|
237
|
+
const unitType = this.getText('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode', itemNode);
|
|
210
238
|
const unitNetPrice = this.getNumber('ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount', itemNode);
|
|
211
239
|
const vatPercentage = this.getNumber('ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent', itemNode);
|
|
212
240
|
|
|
@@ -24,8 +24,9 @@ export class ZUGFeRDValidator extends CIIBaseValidator {
|
|
|
24
24
|
* @returns True if business validation passed
|
|
25
25
|
*/
|
|
26
26
|
protected validateBusinessRules(): boolean {
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
// BR-23: every line has a unit code
|
|
28
|
+
const linesHaveUnits = this.validateLineUnitCodePresence();
|
|
29
|
+
// BR-CL-23: unit codes of UN/ECE Recommendation 20 with the Recommendation 21 extension
|
|
30
|
+
return this.validateUnitCodes() && linesHaveUnits;
|
|
30
31
|
}
|
|
31
32
|
}
|
|
@@ -100,7 +100,8 @@ export class XMLToEInvoiceConverter {
|
|
|
100
100
|
position: i,
|
|
101
101
|
name: this.getElementTextFromNode(line, 'cbc:Name') || `Item ${i + 1}`,
|
|
102
102
|
unitQuantity: parseFloat(this.getElementTextFromNode(line, 'cbc:InvoicedQuantity') || '1'),
|
|
103
|
-
|
|
103
|
+
// the unit code (BT-130) the document states, or none, which BR-23 reports
|
|
104
|
+
unitType: this.getAttributeFromNode(line, 'cbc:InvoicedQuantity', 'unitCode'),
|
|
104
105
|
unitNetPrice: parseFloat(this.getElementTextFromNode(line, 'cbc:PriceAmount') || '100'),
|
|
105
106
|
vatPercentage: parseFloat(this.getElementTextFromNode(line, 'cbc:Percent') || '19')
|
|
106
107
|
};
|
|
@@ -118,6 +119,15 @@ export class XMLToEInvoiceConverter {
|
|
|
118
119
|
return mockInvoice as unknown as EInvoice;
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Helper to get an attribute of the first matching element below a node, or '' when the element
|
|
124
|
+
* or the attribute is absent
|
|
125
|
+
*/
|
|
126
|
+
private getAttributeFromNode(node: Element, tagName: string, attributeName: string): string {
|
|
127
|
+
const element = node.getElementsByTagName(tagName)[0];
|
|
128
|
+
return element?.getAttribute(attributeName) ?? '';
|
|
129
|
+
}
|
|
130
|
+
|
|
121
131
|
/**
|
|
122
132
|
* Helper to get element text from a node
|
|
123
133
|
*/
|
|
@@ -442,7 +442,7 @@ export class SemanticModelAdapter {
|
|
|
442
442
|
identifier: (index + 1).toString(),
|
|
443
443
|
note: (item as any).description || (item as any).text || '',
|
|
444
444
|
invoicedQuantity: item.unitQuantity,
|
|
445
|
-
invoicedQuantityUnitOfMeasureCode: item.unitType
|
|
445
|
+
invoicedQuantityUnitOfMeasureCode: item.unitType,
|
|
446
446
|
lineExtensionAmount: item.unitNetPrice * item.unitQuantity,
|
|
447
447
|
purchaseOrderLineReference: (item as any).purchaseOrderLineRef,
|
|
448
448
|
buyerAccountingReference: (item as any).buyerAccountingRef,
|
|
@@ -148,6 +148,12 @@ export class EN16931UBLValidator extends UBLBaseValidator {
|
|
|
148
148
|
valid = false;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// BR-23: every line has a unit code
|
|
152
|
+
valid = this.validateLineUnitCodePresence() && valid;
|
|
153
|
+
|
|
154
|
+
// BR-CL-23: unit codes of UN/ECE Recommendation 20 with the Recommendation 21 extension
|
|
155
|
+
valid = this.validateUnitCodes() && valid;
|
|
156
|
+
|
|
151
157
|
// Validate calculation rules if we have the necessary data
|
|
152
158
|
if (this.exists('//cac:LegalMonetaryTotal/cbc:LineExtensionAmount')) {
|
|
153
159
|
valid = this.validateCalculationRules() && valid;
|
|
@@ -3,6 +3,8 @@ import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../../int
|
|
|
3
3
|
import { UBLDocumentType } from '../ubl.types.js';
|
|
4
4
|
import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
5
5
|
import { getDocumentTypeCode } from '../../utils/document.typecode.js';
|
|
6
|
+
import { getPrecedingInvoiceReferences } from '../../utils/preceding.invoice.js';
|
|
7
|
+
import { getWritableDate } from '../../utils/date.value.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* UBL Encoder implementation
|
|
@@ -24,13 +26,15 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
24
26
|
|
|
25
27
|
// Add credit note specific data
|
|
26
28
|
this.addCreditNoteSpecificData(doc, creditNote);
|
|
29
|
+
|
|
30
|
+
this.orderRootElements(doc);
|
|
27
31
|
|
|
28
32
|
// Serialize to string
|
|
29
33
|
return new XMLSerializer().serializeToString(doc);
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/**
|
|
33
|
-
* Encodes an invoice document (invoice, debit note or self-billed invoice) into UBL XML
|
|
37
|
+
* Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into UBL XML
|
|
34
38
|
* @param invoice Invoice document to encode
|
|
35
39
|
* @returns UBL XML string
|
|
36
40
|
*/
|
|
@@ -44,6 +48,8 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
44
48
|
|
|
45
49
|
// Add invoice specific data
|
|
46
50
|
this.addInvoiceSpecificData(doc, invoice);
|
|
51
|
+
|
|
52
|
+
this.orderRootElements(doc);
|
|
47
53
|
|
|
48
54
|
// Serialize to string
|
|
49
55
|
return new XMLSerializer().serializeToString(doc);
|
|
@@ -71,16 +77,15 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
71
77
|
this.appendElement(doc, root, 'cbc:ID', invoice.id);
|
|
72
78
|
|
|
73
79
|
// Issue Date
|
|
74
|
-
this.appendElement(doc, root, 'cbc:IssueDate', this.formatDate(invoice.date));
|
|
80
|
+
this.appendElement(doc, root, 'cbc:IssueDate', this.formatDate(invoice.date, 'BT-2 invoice issue date'));
|
|
75
81
|
|
|
76
82
|
// Due Date - the CreditNote schema has no DueDate element; its payment due date lives in cac:PaymentMeans
|
|
77
83
|
if (documentType === UBLDocumentType.INVOICE) {
|
|
78
|
-
const
|
|
79
|
-
const dueDate = new Date(issueTimestamp);
|
|
84
|
+
const dueDate = getWritableDate(invoice.date, 'BT-2 invoice issue date', 'ubl');
|
|
80
85
|
dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
|
|
81
|
-
this.appendElement(doc, root, 'cbc:DueDate', this.formatDate(dueDate.getTime()));
|
|
86
|
+
this.appendElement(doc, root, 'cbc:DueDate', this.formatDate(dueDate.getTime(), 'BT-9 payment due date'));
|
|
82
87
|
}
|
|
83
|
-
// Document Type Code (380 invoice, 381 credit note, 383 debit note, 389 self-billed invoice; the element differs per schema)
|
|
88
|
+
// Document Type Code (380 invoice, 381 credit note, 383 debit note, 384 corrected invoice, 389 self-billed invoice; the element differs per schema)
|
|
84
89
|
this.appendElement(
|
|
85
90
|
doc,
|
|
86
91
|
root,
|
|
@@ -104,12 +109,21 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
104
109
|
|
|
105
110
|
// Document Currency Code
|
|
106
111
|
this.appendElement(doc, root, 'cbc:DocumentCurrencyCode', invoice.currency);
|
|
112
|
+
|
|
113
|
+
// Invoicing period (BG-14), in schema order before the document references
|
|
114
|
+
this.addInvoicePeriod(doc, root, invoice);
|
|
115
|
+
|
|
116
|
+
// Preceding invoice references (BG-3)
|
|
117
|
+
this.addBillingReferences(doc, root, invoice);
|
|
107
118
|
|
|
108
119
|
// Add accounting supplier party (seller)
|
|
109
120
|
this.addParty(doc, root, 'cac:AccountingSupplierParty', invoice.from);
|
|
110
121
|
|
|
111
122
|
// Add accounting customer party (buyer)
|
|
112
123
|
this.addParty(doc, root, 'cac:AccountingCustomerParty', invoice.to);
|
|
124
|
+
|
|
125
|
+
// Actual delivery date (BT-72), in schema order after the parties
|
|
126
|
+
this.addDelivery(doc, root, invoice);
|
|
113
127
|
|
|
114
128
|
// Add payment terms
|
|
115
129
|
this.addPaymentTerms(doc, root, invoice);
|
|
@@ -127,6 +141,57 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
127
141
|
this.preserveMetadata(doc, root, invoice);
|
|
128
142
|
}
|
|
129
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Writes the invoicing period (BG-14: BT-73 and BT-74) the document states.
|
|
146
|
+
* @param doc XML document
|
|
147
|
+
* @param root Document root
|
|
148
|
+
* @param invoice Accounting document
|
|
149
|
+
*/
|
|
150
|
+
private addInvoicePeriod(doc: Document, root: Element, invoice: TAccountingDoc): void {
|
|
151
|
+
if (!invoice.periodOfPerformance) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const invoicePeriod = doc.createElement('cac:InvoicePeriod');
|
|
155
|
+
this.appendElement(doc, invoicePeriod, 'cbc:StartDate', this.formatDate(invoice.periodOfPerformance.from, 'BT-73 invoicing period start date'));
|
|
156
|
+
this.appendElement(doc, invoicePeriod, 'cbc:EndDate', this.formatDate(invoice.periodOfPerformance.to, 'BT-74 invoicing period end date'));
|
|
157
|
+
root.appendChild(invoicePeriod);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Writes one preceding invoice reference (BG-3) per invoice the document
|
|
162
|
+
* states it corrects: the number (BT-25) and, when stated, the issue date (BT-26).
|
|
163
|
+
* @param doc XML document
|
|
164
|
+
* @param root Document root
|
|
165
|
+
* @param invoice Accounting document
|
|
166
|
+
*/
|
|
167
|
+
private addBillingReferences(doc: Document, root: Element, invoice: TAccountingDoc): void {
|
|
168
|
+
for (const reference of getPrecedingInvoiceReferences(invoice)) {
|
|
169
|
+
const billingReference = doc.createElement('cac:BillingReference');
|
|
170
|
+
const invoiceDocumentReference = doc.createElement('cac:InvoiceDocumentReference');
|
|
171
|
+
this.appendElement(doc, invoiceDocumentReference, 'cbc:ID', reference.documentId);
|
|
172
|
+
if (reference.issueDate !== undefined) {
|
|
173
|
+
this.appendElement(doc, invoiceDocumentReference, 'cbc:IssueDate', this.formatDate(reference.issueDate, 'BT-26 preceding invoice issue date'));
|
|
174
|
+
}
|
|
175
|
+
billingReference.appendChild(invoiceDocumentReference);
|
|
176
|
+
root.appendChild(billingReference);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Writes the actual delivery date (BT-72) the document states.
|
|
182
|
+
* @param doc XML document
|
|
183
|
+
* @param root Document root
|
|
184
|
+
* @param invoice Accounting document
|
|
185
|
+
*/
|
|
186
|
+
private addDelivery(doc: Document, root: Element, invoice: TAccountingDoc): void {
|
|
187
|
+
if (invoice.deliveryDate === undefined) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const delivery = doc.createElement('cac:Delivery');
|
|
191
|
+
this.appendElement(doc, delivery, 'cbc:ActualDeliveryDate', this.formatDate(invoice.deliveryDate, 'BT-72 actual delivery date'));
|
|
192
|
+
root.appendChild(delivery);
|
|
193
|
+
}
|
|
194
|
+
|
|
130
195
|
/**
|
|
131
196
|
* Adds credit note specific data to the document
|
|
132
197
|
* @param doc XML document
|
|
@@ -280,11 +345,10 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
280
345
|
// Payment means code - default to credit transfer
|
|
281
346
|
this.appendElement(doc, paymentMeansNode, 'cbc:PaymentMeansCode', '30');
|
|
282
347
|
|
|
283
|
-
// Payment due date
|
|
284
|
-
const
|
|
285
|
-
const dueDate = new Date(issueTimestamp);
|
|
348
|
+
// Payment due date, counted from the issue date the document states
|
|
349
|
+
const dueDate = getWritableDate(invoice.date, 'BT-2 invoice issue date', 'ubl');
|
|
286
350
|
dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
|
|
287
|
-
this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime()));
|
|
351
|
+
this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime(), 'BT-9 payment due date'));
|
|
288
352
|
|
|
289
353
|
// Add payment channel code if available
|
|
290
354
|
if (paymentOptions.description) {
|
|
@@ -459,7 +523,10 @@ export class UBLEncoder extends UBLBaseEncoder {
|
|
|
459
523
|
|
|
460
524
|
// Invoiced quantity
|
|
461
525
|
const quantityElement = doc.createElement(creditNote ? 'cbc:CreditedQuantity' : 'cbc:InvoicedQuantity');
|
|
462
|
-
|
|
526
|
+
// a line without a unit is written without one, so that BR-23 reports it
|
|
527
|
+
if (item.unitType) {
|
|
528
|
+
quantityElement.setAttribute('unitCode', item.unitType);
|
|
529
|
+
}
|
|
463
530
|
quantityElement.textContent = item.unitQuantity.toString();
|
|
464
531
|
invoiceLineNode.appendChild(quantityElement);
|
|
465
532
|
|
|
@@ -89,7 +89,7 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
|
|
|
89
89
|
protected abstract decodeCreditNote(): Promise<TCreditNote>;
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
* Decodes a UBL invoice document: an invoice, a debit note or a self-billed invoice
|
|
92
|
+
* Decodes a UBL invoice document: an invoice, a corrected invoice, a debit note or a self-billed invoice
|
|
93
93
|
* @param accountingDocType The type the document type code stands for
|
|
94
94
|
* @returns Promise resolving to the invoice document
|
|
95
95
|
*/
|
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
import { BaseEncoder } from '../base/base.encoder.js';
|
|
2
2
|
import type { TAccountingDoc, TCreditNote, TInvoiceDocument } from '../../interfaces/common.js';
|
|
3
3
|
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
|
|
4
|
+
import { getWritableDate } from '../utils/date.value.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The children of the document root in the order the UBL 2.1 schema requires
|
|
8
|
+
* them (UBL-Invoice-2.1.xsd and UBL-CreditNote-2.1.xsd, local names).
|
|
9
|
+
*/
|
|
10
|
+
const UBL_ROOT_SEQUENCES: Record<UBLDocumentType, readonly string[]> = {
|
|
11
|
+
[UBLDocumentType.INVOICE]: [
|
|
12
|
+
'UBLExtensions', 'UBLVersionID', 'CustomizationID', 'ProfileID', 'ProfileExecutionID', 'ID',
|
|
13
|
+
'CopyIndicator', 'UUID', 'IssueDate', 'IssueTime', 'DueDate', 'InvoiceTypeCode', 'Note',
|
|
14
|
+
'TaxPointDate', 'DocumentCurrencyCode', 'TaxCurrencyCode', 'PricingCurrencyCode',
|
|
15
|
+
'PaymentCurrencyCode', 'PaymentAlternativeCurrencyCode', 'AccountingCostCode', 'AccountingCost',
|
|
16
|
+
'LineCountNumeric', 'BuyerReference', 'InvoicePeriod', 'OrderReference', 'BillingReference',
|
|
17
|
+
'DespatchDocumentReference', 'ReceiptDocumentReference', 'StatementDocumentReference',
|
|
18
|
+
'OriginatorDocumentReference', 'ContractDocumentReference', 'AdditionalDocumentReference',
|
|
19
|
+
'ProjectReference', 'Signature', 'AccountingSupplierParty', 'AccountingCustomerParty',
|
|
20
|
+
'PayeeParty', 'BuyerCustomerParty', 'SellerSupplierParty', 'TaxRepresentativeParty', 'Delivery',
|
|
21
|
+
'DeliveryTerms', 'PaymentMeans', 'PaymentTerms', 'PrepaidPayment', 'AllowanceCharge',
|
|
22
|
+
'TaxExchangeRate', 'PricingExchangeRate', 'PaymentExchangeRate', 'PaymentAlternativeExchangeRate',
|
|
23
|
+
'TaxTotal', 'WithholdingTaxTotal', 'LegalMonetaryTotal', 'InvoiceLine',
|
|
24
|
+
],
|
|
25
|
+
[UBLDocumentType.CREDIT_NOTE]: [
|
|
26
|
+
'UBLExtensions', 'UBLVersionID', 'CustomizationID', 'ProfileID', 'ProfileExecutionID', 'ID',
|
|
27
|
+
'CopyIndicator', 'UUID', 'IssueDate', 'IssueTime', 'TaxPointDate', 'CreditNoteTypeCode', 'Note',
|
|
28
|
+
'DocumentCurrencyCode', 'TaxCurrencyCode', 'PricingCurrencyCode', 'PaymentCurrencyCode',
|
|
29
|
+
'PaymentAlternativeCurrencyCode', 'AccountingCostCode', 'AccountingCost', 'LineCountNumeric',
|
|
30
|
+
'BuyerReference', 'InvoicePeriod', 'DiscrepancyResponse', 'OrderReference', 'BillingReference',
|
|
31
|
+
'DespatchDocumentReference', 'ReceiptDocumentReference', 'ContractDocumentReference',
|
|
32
|
+
'AdditionalDocumentReference', 'StatementDocumentReference', 'OriginatorDocumentReference',
|
|
33
|
+
'Signature', 'AccountingSupplierParty', 'AccountingCustomerParty', 'PayeeParty',
|
|
34
|
+
'BuyerCustomerParty', 'SellerSupplierParty', 'TaxRepresentativeParty', 'Delivery',
|
|
35
|
+
'DeliveryTerms', 'PaymentMeans', 'PaymentTerms', 'TaxExchangeRate', 'PricingExchangeRate',
|
|
36
|
+
'PaymentExchangeRate', 'PaymentAlternativeExchangeRate', 'AllowanceCharge', 'TaxTotal',
|
|
37
|
+
'LegalMonetaryTotal', 'CreditNoteLine',
|
|
38
|
+
],
|
|
39
|
+
};
|
|
4
40
|
|
|
5
41
|
/**
|
|
6
42
|
* Base encoder for UBL-based invoice formats
|
|
@@ -27,7 +63,7 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
|
|
|
27
63
|
protected abstract encodeCreditNote(creditNote: TCreditNote): Promise<string>;
|
|
28
64
|
|
|
29
65
|
/**
|
|
30
|
-
* Encodes an invoice document (invoice, debit note or self-billed invoice) into UBL XML
|
|
66
|
+
* Encodes an invoice document (invoice, corrected invoice, debit note or self-billed invoice) into UBL XML
|
|
31
67
|
* @param invoice Invoice document to encode
|
|
32
68
|
* @returns UBL XML string
|
|
33
69
|
*/
|
|
@@ -47,20 +83,48 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
|
|
|
47
83
|
}
|
|
48
84
|
|
|
49
85
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
86
|
+
* Puts the children of the document root into the order the schema requires.
|
|
87
|
+
* Elements are added in several passes (the generic encoder, a customization,
|
|
88
|
+
* preserved metadata), each inserting where it can; this final pass makes the
|
|
89
|
+
* result follow the schema sequence. The sort is stable, so repeated elements
|
|
90
|
+
* (notes, references, lines) keep their order, and an element the sequence
|
|
91
|
+
* does not name stays behind the element it followed.
|
|
92
|
+
* @param doc XML document
|
|
53
93
|
*/
|
|
54
|
-
protected
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
94
|
+
protected orderRootElements(doc: Document): void {
|
|
95
|
+
const root = doc.documentElement;
|
|
96
|
+
const sequence = UBL_ROOT_SEQUENCES[root.localName as UBLDocumentType];
|
|
97
|
+
if (!sequence) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const children: Element[] = [];
|
|
101
|
+
for (let node = root.firstChild; node; node = node.nextSibling) {
|
|
102
|
+
if (node.nodeType === 1) {
|
|
103
|
+
children.push(node as Element);
|
|
104
|
+
}
|
|
58
105
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
106
|
+
let lastRank = -1;
|
|
107
|
+
const ranked = children.map((element, position) => {
|
|
108
|
+
// an element made with createElement('cac:X') carries its prefix in localName as well
|
|
109
|
+
const rank = sequence.indexOf(element.nodeName.slice(element.nodeName.indexOf(':') + 1));
|
|
110
|
+
lastRank = rank === -1 ? lastRank : rank;
|
|
111
|
+
return { element, rank: lastRank, position };
|
|
112
|
+
});
|
|
113
|
+
ranked.sort((a, b) => a.rank - b.rank || a.position - b.position);
|
|
114
|
+
for (const { element } of ranked) {
|
|
115
|
+
root.appendChild(element);
|
|
63
116
|
}
|
|
64
|
-
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Formats a date as an ISO string (YYYY-MM-DD, UTC). A value that is no
|
|
121
|
+
* valid timestamp is refused with an `EInvoiceFormatError` naming the field;
|
|
122
|
+
* no date is put in its place.
|
|
123
|
+
* @param timestamp Timestamp to format
|
|
124
|
+
* @param field The business term the date is written as
|
|
125
|
+
* @returns Formatted date string
|
|
126
|
+
*/
|
|
127
|
+
protected formatDate(timestamp: number, field: string): string {
|
|
128
|
+
return getWritableDate(timestamp, field, 'ubl').toISOString().split('T')[0];
|
|
65
129
|
}
|
|
66
130
|
}
|
|
@@ -3,6 +3,7 @@ import { ValidationLevel } from '../../interfaces/common.js';
|
|
|
3
3
|
import type { ValidationResult } from '../../interfaces/common.js';
|
|
4
4
|
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
|
|
5
5
|
import { DOMParser, xpath } from '../../plugins.js';
|
|
6
|
+
import { meetsUnitCodeRule } from '../utils/unit.codes.js';
|
|
6
7
|
|
|
7
8
|
const ublValidatorParser = new DOMParser();
|
|
8
9
|
const ublValidatorNamespaces = {
|
|
@@ -104,6 +105,52 @@ export abstract class UBLBaseValidator extends BaseValidator {
|
|
|
104
105
|
*/
|
|
105
106
|
protected abstract validateStructure(): boolean;
|
|
106
107
|
|
|
108
|
+
/**
|
|
109
|
+
* BR-23: every invoice or credit note line has a unit code on its invoiced or credited quantity.
|
|
110
|
+
* @returns True if every line has one
|
|
111
|
+
*/
|
|
112
|
+
protected validateLineUnitCodePresence(): boolean {
|
|
113
|
+
// BR-23: every invoice or credit note line (BG-25) has an invoiced quantity unit of measure code (BT-130)
|
|
114
|
+
const lines = this.select('//cac:InvoiceLine | //cac:CreditNoteLine', this.doc) as Element[];
|
|
115
|
+
let valid = true;
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
if (!this.exists('./cbc:InvoicedQuantity/@unitCode | ./cbc:CreditedQuantity/@unitCode', line)) {
|
|
118
|
+
this.addError(
|
|
119
|
+
'BR-23',
|
|
120
|
+
'An Invoice line (BG-25) shall have an Invoiced quantity unit of measure code (BT-130)',
|
|
121
|
+
this.locationOf(line)
|
|
122
|
+
);
|
|
123
|
+
valid = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return valid;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* BR-CL-23: every unit code of an invoiced, credited or base quantity (BT-130, BT-150) is a
|
|
131
|
+
* code of UN/ECE Recommendation 20 with the Recommendation 21 extension.
|
|
132
|
+
* @returns True if every unit code passed
|
|
133
|
+
*/
|
|
134
|
+
protected validateUnitCodes(): boolean {
|
|
135
|
+
const quantities = this.select(
|
|
136
|
+
'//cbc:InvoicedQuantity[@unitCode] | //cbc:CreditedQuantity[@unitCode] | //cbc:BaseQuantity[@unitCode]',
|
|
137
|
+
this.doc
|
|
138
|
+
) as Element[];
|
|
139
|
+
let valid = true;
|
|
140
|
+
for (const quantity of quantities) {
|
|
141
|
+
const unitCode = quantity.getAttribute('unitCode') ?? '';
|
|
142
|
+
if (!meetsUnitCodeRule(unitCode)) {
|
|
143
|
+
this.addError(
|
|
144
|
+
'BR-CL-23',
|
|
145
|
+
`Unit code MUST be coded according to the UN/ECE Recommendation 20 with Rec 21 extension; found "${unitCode}"`,
|
|
146
|
+
`${this.locationOf(quantity)}/@unitCode`
|
|
147
|
+
);
|
|
148
|
+
valid = false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return valid;
|
|
152
|
+
}
|
|
153
|
+
|
|
107
154
|
/**
|
|
108
155
|
* Gets a text value from an XPath expression
|
|
109
156
|
* @param xpath XPath expression
|