654 lines
25 KiB
TypeScript
654 lines
25 KiB
TypeScript
import { CIIBaseEncoder } from '../cii.encoder.js';
|
|
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
|
|
import { ZUGFERD_PROFILE_IDS } from './zugferd.types.js';
|
|
import { CIIProfile } from '../cii.types.js';
|
|
import { DOMParser, XMLSerializer } from '../../../plugins.js';
|
|
|
|
/**
|
|
* Encoder for ZUGFeRD invoice format
|
|
*/
|
|
export class ZUGFeRDEncoder extends CIIBaseEncoder {
|
|
constructor() {
|
|
super();
|
|
// Set default profile to BASIC
|
|
this.profile = CIIProfile.BASIC;
|
|
}
|
|
|
|
/**
|
|
* Encodes a credit note into ZUGFeRD XML
|
|
* @param creditNote Credit note to encode
|
|
* @returns ZUGFeRD XML string
|
|
*/
|
|
protected async encodeCreditNote(creditNote: TCreditNote): Promise<string> {
|
|
// Create base XML
|
|
const xmlDoc = this.createBaseXml();
|
|
|
|
// Set document type code to credit note (381)
|
|
this.setDocumentTypeCode(xmlDoc, '381');
|
|
|
|
// Add common invoice data
|
|
this.addCommonInvoiceData(xmlDoc, creditNote);
|
|
|
|
// Serialize to string
|
|
return new XMLSerializer().serializeToString(xmlDoc);
|
|
}
|
|
|
|
/**
|
|
* Encodes a debit note (invoice) into ZUGFeRD XML
|
|
* @param debitNote Debit note to encode
|
|
* @returns ZUGFeRD XML string
|
|
*/
|
|
protected async encodeDebitNote(debitNote: TDebitNote): Promise<string> {
|
|
// Create base XML
|
|
const xmlDoc = this.createBaseXml();
|
|
|
|
// Set document type code to invoice (380)
|
|
this.setDocumentTypeCode(xmlDoc, '380');
|
|
|
|
// Add common invoice data
|
|
this.addCommonInvoiceData(xmlDoc, debitNote);
|
|
|
|
// Serialize to string
|
|
return new XMLSerializer().serializeToString(xmlDoc);
|
|
}
|
|
|
|
/**
|
|
* Creates a base ZUGFeRD XML document
|
|
* @returns XML document with basic structure
|
|
*/
|
|
private createBaseXml(): Document {
|
|
// Create XML document from template
|
|
const xmlString = this.createXmlRoot();
|
|
const doc = new DOMParser().parseFromString(xmlString, 'application/xml');
|
|
|
|
// Add ZUGFeRD profile
|
|
this.addProfile(doc);
|
|
|
|
return doc;
|
|
}
|
|
|
|
/**
|
|
* Adds ZUGFeRD profile information to the XML document
|
|
* @param doc XML document
|
|
*/
|
|
private addProfile(doc: Document): void {
|
|
// Get root element
|
|
const root = doc.documentElement;
|
|
|
|
// Create context element if it doesn't exist
|
|
let contextElement = root.getElementsByTagName('rsm:ExchangedDocumentContext')[0];
|
|
if (!contextElement) {
|
|
contextElement = doc.createElement('rsm:ExchangedDocumentContext');
|
|
root.appendChild(contextElement);
|
|
}
|
|
|
|
// Create guideline parameter element
|
|
const guidelineElement = doc.createElement('ram:GuidelineSpecifiedDocumentContextParameter');
|
|
contextElement.appendChild(guidelineElement);
|
|
|
|
// Add ID element with profile
|
|
const idElement = doc.createElement('ram:ID');
|
|
|
|
// Set profile based on the selected profile
|
|
let profileId = ZUGFERD_PROFILE_IDS.BASIC;
|
|
if (this.profile === CIIProfile.COMFORT) {
|
|
profileId = ZUGFERD_PROFILE_IDS.COMFORT;
|
|
} else if (this.profile === CIIProfile.EXTENDED) {
|
|
profileId = ZUGFERD_PROFILE_IDS.EXTENDED;
|
|
}
|
|
|
|
idElement.textContent = profileId;
|
|
guidelineElement.appendChild(idElement);
|
|
}
|
|
|
|
/**
|
|
* Sets the document type code in the XML document
|
|
* @param doc XML document
|
|
* @param typeCode Document type code (380 for invoice, 381 for credit note)
|
|
*/
|
|
private setDocumentTypeCode(doc: Document, typeCode: string): void {
|
|
// Get root element
|
|
const root = doc.documentElement;
|
|
|
|
// Create document element if it doesn't exist
|
|
let documentElement = root.getElementsByTagName('rsm:ExchangedDocument')[0];
|
|
if (!documentElement) {
|
|
documentElement = doc.createElement('rsm:ExchangedDocument');
|
|
root.appendChild(documentElement);
|
|
}
|
|
|
|
// Add type code element
|
|
const typeCodeElement = doc.createElement('ram:TypeCode');
|
|
typeCodeElement.textContent = typeCode;
|
|
documentElement.appendChild(typeCodeElement);
|
|
}
|
|
|
|
/**
|
|
* Adds common invoice data to the XML document
|
|
* @param doc XML document
|
|
* @param invoice Invoice data
|
|
*/
|
|
private addCommonInvoiceData(doc: Document, invoice: TInvoice): void {
|
|
// Get root element
|
|
const root = doc.documentElement;
|
|
|
|
// Get document element or create it
|
|
let documentElement = root.getElementsByTagName('rsm:ExchangedDocument')[0];
|
|
if (!documentElement) {
|
|
documentElement = doc.createElement('rsm:ExchangedDocument');
|
|
root.appendChild(documentElement);
|
|
}
|
|
|
|
// Add ID element
|
|
const idElement = doc.createElement('ram:ID');
|
|
idElement.textContent = invoice.id;
|
|
documentElement.appendChild(idElement);
|
|
|
|
// Add issue date element
|
|
const issueDateElement = doc.createElement('ram:IssueDateTime');
|
|
const dateStringElement = doc.createElement('udt:DateTimeString');
|
|
dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.date);
|
|
issueDateElement.appendChild(dateStringElement);
|
|
documentElement.appendChild(issueDateElement);
|
|
|
|
// Add notes if available
|
|
if (invoice.notes && invoice.notes.length > 0) {
|
|
for (const note of invoice.notes) {
|
|
const noteElement = doc.createElement('ram:IncludedNote');
|
|
const contentElement = doc.createElement('ram:Content');
|
|
contentElement.textContent = note;
|
|
noteElement.appendChild(contentElement);
|
|
documentElement.appendChild(noteElement);
|
|
}
|
|
}
|
|
|
|
// Create transaction element if it doesn't exist
|
|
let transactionElement = root.getElementsByTagName('rsm:SupplyChainTradeTransaction')[0];
|
|
if (!transactionElement) {
|
|
transactionElement = doc.createElement('rsm:SupplyChainTradeTransaction');
|
|
root.appendChild(transactionElement);
|
|
}
|
|
|
|
// Add agreement section with seller and buyer
|
|
this.addAgreementSection(doc, transactionElement, invoice);
|
|
|
|
// Add delivery section
|
|
this.addDeliverySection(doc, transactionElement, invoice);
|
|
|
|
// Add settlement section with payment terms and totals
|
|
this.addSettlementSection(doc, transactionElement, invoice);
|
|
|
|
// Add line items
|
|
this.addLineItems(doc, transactionElement, invoice);
|
|
}
|
|
|
|
/**
|
|
* Adds agreement section with seller and buyer information
|
|
* @param doc XML document
|
|
* @param transactionElement Transaction element
|
|
* @param invoice Invoice data
|
|
*/
|
|
private addAgreementSection(doc: Document, transactionElement: Element, invoice: TInvoice): void {
|
|
// Create agreement element
|
|
const agreementElement = doc.createElement('ram:ApplicableHeaderTradeAgreement');
|
|
transactionElement.appendChild(agreementElement);
|
|
|
|
// Add buyer reference if available
|
|
if (invoice.buyerReference) {
|
|
const buyerRefElement = doc.createElement('ram:BuyerReference');
|
|
buyerRefElement.textContent = invoice.buyerReference;
|
|
agreementElement.appendChild(buyerRefElement);
|
|
}
|
|
|
|
// Add seller
|
|
const sellerElement = doc.createElement('ram:SellerTradeParty');
|
|
this.addPartyInfo(doc, sellerElement, invoice.from);
|
|
|
|
// Add seller electronic address if available
|
|
if (invoice.electronicAddress && invoice.from.type === 'company') {
|
|
const contactElement = doc.createElement('ram:DefinedTradeContact');
|
|
const uriElement = doc.createElement('ram:URIID');
|
|
uriElement.setAttribute('schemeID', invoice.electronicAddress.scheme);
|
|
uriElement.textContent = invoice.electronicAddress.value;
|
|
contactElement.appendChild(uriElement);
|
|
sellerElement.appendChild(contactElement);
|
|
}
|
|
|
|
agreementElement.appendChild(sellerElement);
|
|
|
|
// Add buyer
|
|
const buyerElement = doc.createElement('ram:BuyerTradeParty');
|
|
this.addPartyInfo(doc, buyerElement, invoice.to);
|
|
agreementElement.appendChild(buyerElement);
|
|
}
|
|
|
|
/**
|
|
* Adds party information to an element
|
|
* @param doc XML document
|
|
* @param partyElement Party element
|
|
* @param party Party data
|
|
*/
|
|
private addPartyInfo(doc: Document, partyElement: Element, party: any): void {
|
|
// Add name
|
|
const nameElement = doc.createElement('ram:Name');
|
|
nameElement.textContent = party.name;
|
|
partyElement.appendChild(nameElement);
|
|
|
|
// Add postal address
|
|
const addressElement = doc.createElement('ram:PostalTradeAddress');
|
|
|
|
// Add address line 1 (street)
|
|
if (party.address.streetName) {
|
|
const line1Element = doc.createElement('ram:LineOne');
|
|
line1Element.textContent = party.address.streetName;
|
|
addressElement.appendChild(line1Element);
|
|
}
|
|
|
|
// Add address line 2 (house number) if present
|
|
if (party.address.houseNumber && party.address.houseNumber !== '0') {
|
|
const line2Element = doc.createElement('ram:LineTwo');
|
|
line2Element.textContent = party.address.houseNumber;
|
|
addressElement.appendChild(line2Element);
|
|
}
|
|
|
|
// Add postal code
|
|
if (party.address.postalCode) {
|
|
const postalCodeElement = doc.createElement('ram:PostcodeCode');
|
|
postalCodeElement.textContent = party.address.postalCode;
|
|
addressElement.appendChild(postalCodeElement);
|
|
}
|
|
|
|
// Add city
|
|
if (party.address.city) {
|
|
const cityElement = doc.createElement('ram:CityName');
|
|
cityElement.textContent = party.address.city;
|
|
addressElement.appendChild(cityElement);
|
|
}
|
|
|
|
// Add country
|
|
if (party.address.country || party.address.countryCode) {
|
|
const countryElement = doc.createElement('ram:CountryID');
|
|
countryElement.textContent = party.address.countryCode || party.address.country;
|
|
addressElement.appendChild(countryElement);
|
|
}
|
|
|
|
partyElement.appendChild(addressElement);
|
|
|
|
// Add VAT ID if available
|
|
if (party.registrationDetails && party.registrationDetails.vatId) {
|
|
const taxRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
|
|
const taxIdElement = doc.createElement('ram:ID');
|
|
taxIdElement.setAttribute('schemeID', 'VA');
|
|
taxIdElement.textContent = party.registrationDetails.vatId;
|
|
taxRegistrationElement.appendChild(taxIdElement);
|
|
partyElement.appendChild(taxRegistrationElement);
|
|
}
|
|
|
|
// Add registration ID if available
|
|
if (party.registrationDetails && party.registrationDetails.registrationId) {
|
|
const regRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
|
|
const regIdElement = doc.createElement('ram:ID');
|
|
regIdElement.setAttribute('schemeID', 'FC');
|
|
regIdElement.textContent = party.registrationDetails.registrationId;
|
|
regRegistrationElement.appendChild(regIdElement);
|
|
partyElement.appendChild(regRegistrationElement);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds delivery section with delivery information
|
|
* @param doc XML document
|
|
* @param transactionElement Transaction element
|
|
* @param invoice Invoice data
|
|
*/
|
|
private addDeliverySection(doc: Document, transactionElement: Element, invoice: TInvoice): void {
|
|
// Create delivery element
|
|
const deliveryElement = doc.createElement('ram:ApplicableHeaderTradeDelivery');
|
|
transactionElement.appendChild(deliveryElement);
|
|
|
|
// Add delivery date if available
|
|
if (invoice.deliveryDate) {
|
|
const deliveryDateElement = doc.createElement('ram:ActualDeliverySupplyChainEvent');
|
|
const occurrenceDateElement = doc.createElement('ram:OccurrenceDateTime');
|
|
const dateStringElement = doc.createElement('udt:DateTimeString');
|
|
dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.deliveryDate);
|
|
occurrenceDateElement.appendChild(dateStringElement);
|
|
deliveryDateElement.appendChild(occurrenceDateElement);
|
|
deliveryElement.appendChild(deliveryDateElement);
|
|
}
|
|
|
|
// Add period of performance if available
|
|
if (invoice.periodOfPerformance) {
|
|
const periodElement = doc.createElement('ram:BillingSpecifiedPeriod');
|
|
|
|
// Start date
|
|
if (invoice.periodOfPerformance.from) {
|
|
const startDateElement = doc.createElement('ram:StartDateTime');
|
|
const startDateStringElement = doc.createElement('udt:DateTimeString');
|
|
startDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
startDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.from);
|
|
startDateElement.appendChild(startDateStringElement);
|
|
periodElement.appendChild(startDateElement);
|
|
}
|
|
|
|
// End date
|
|
if (invoice.periodOfPerformance.to) {
|
|
const endDateElement = doc.createElement('ram:EndDateTime');
|
|
const endDateStringElement = doc.createElement('udt:DateTimeString');
|
|
endDateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
endDateStringElement.textContent = this.formatDateYYYYMMDD(invoice.periodOfPerformance.to);
|
|
endDateElement.appendChild(endDateStringElement);
|
|
periodElement.appendChild(endDateElement);
|
|
}
|
|
|
|
deliveryElement.appendChild(periodElement);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds settlement section with payment terms and totals
|
|
* @param doc XML document
|
|
* @param transactionElement Transaction element
|
|
* @param invoice Invoice data
|
|
*/
|
|
private addSettlementSection(doc: Document, transactionElement: Element, invoice: TInvoice): void {
|
|
// Create settlement element
|
|
const settlementElement = doc.createElement('ram:ApplicableHeaderTradeSettlement');
|
|
transactionElement.appendChild(settlementElement);
|
|
|
|
// Add currency
|
|
const currencyElement = doc.createElement('ram:InvoiceCurrencyCode');
|
|
currencyElement.textContent = invoice.currency;
|
|
settlementElement.appendChild(currencyElement);
|
|
|
|
// Add payment terms
|
|
const paymentTermsElement = doc.createElement('ram:SpecifiedTradePaymentTerms');
|
|
|
|
// Add payment instructions if available
|
|
if (invoice.paymentOptions) {
|
|
// Add payment instructions as description - this is generic enough to work with any payment type
|
|
const descriptionElement = doc.createElement('ram:Description');
|
|
descriptionElement.textContent = `Due in ${invoice.dueInDays} days. ${invoice.paymentOptions.info || ''}`;
|
|
paymentTermsElement.appendChild(descriptionElement);
|
|
}
|
|
|
|
// Add due date
|
|
const dueDateElement = doc.createElement('ram:DueDateDateTime');
|
|
const dateStringElement = doc.createElement('udt:DateTimeString');
|
|
dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
|
|
|
|
// Calculate due date
|
|
const dueDate = new Date(invoice.date);
|
|
dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
|
|
|
|
dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime());
|
|
dueDateElement.appendChild(dateStringElement);
|
|
paymentTermsElement.appendChild(dueDateElement);
|
|
|
|
settlementElement.appendChild(paymentTermsElement);
|
|
|
|
// Add payment means if available (using a generic approach)
|
|
if (invoice.paymentOptions) {
|
|
const paymentMeansElement = doc.createElement('ram:SpecifiedTradeSettlementPaymentMeans');
|
|
|
|
// Payment type code (58 for SEPA transfer as default)
|
|
const typeCodeElement = doc.createElement('ram:TypeCode');
|
|
typeCodeElement.textContent = '58';
|
|
paymentMeansElement.appendChild(typeCodeElement);
|
|
|
|
// Information (optional)
|
|
if (invoice.paymentOptions.info) {
|
|
const infoElement = doc.createElement('ram:Information');
|
|
infoElement.textContent = invoice.paymentOptions.info;
|
|
paymentMeansElement.appendChild(infoElement);
|
|
}
|
|
|
|
// If payment details are available in a standard format
|
|
if (invoice.paymentOptions.sepaConnection.iban) {
|
|
// Payee account
|
|
const payeeAccountElement = doc.createElement('ram:PayeePartyCreditorFinancialAccount');
|
|
const ibanElement = doc.createElement('ram:IBANID');
|
|
ibanElement.textContent = invoice.paymentOptions.sepaConnection.iban;
|
|
payeeAccountElement.appendChild(ibanElement);
|
|
paymentMeansElement.appendChild(payeeAccountElement);
|
|
|
|
// Payee financial institution if BIC available
|
|
if (invoice.paymentOptions.sepaConnection.bic) {
|
|
const institutionElement = doc.createElement('ram:PayeeSpecifiedCreditorFinancialInstitution');
|
|
const bicElement = doc.createElement('ram:BICID');
|
|
bicElement.textContent = invoice.paymentOptions.sepaConnection.bic;
|
|
institutionElement.appendChild(bicElement);
|
|
paymentMeansElement.appendChild(institutionElement);
|
|
}
|
|
}
|
|
|
|
settlementElement.appendChild(paymentMeansElement);
|
|
}
|
|
|
|
// Add tax details
|
|
this.addTaxDetails(doc, settlementElement, invoice);
|
|
|
|
// Add totals
|
|
this.addMonetarySummation(doc, settlementElement, invoice);
|
|
}
|
|
|
|
/**
|
|
* Adds tax details to the settlement section
|
|
* @param doc XML document
|
|
* @param settlementElement Settlement element
|
|
* @param invoice Invoice data
|
|
*/
|
|
private addTaxDetails(doc: Document, settlementElement: Element, invoice: TInvoice): void {
|
|
// Calculate tax categories and totals
|
|
const taxCategories = new Map<number, number>(); // Map of VAT rate to net amount
|
|
|
|
// Calculate from items
|
|
if (invoice.items) {
|
|
for (const item of invoice.items) {
|
|
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
const vatRate = item.vatPercentage;
|
|
|
|
const currentAmount = taxCategories.get(vatRate) || 0;
|
|
taxCategories.set(vatRate, currentAmount + itemNetAmount);
|
|
}
|
|
}
|
|
|
|
// Add each tax category
|
|
for (const [rate, baseAmount] of taxCategories.entries()) {
|
|
const taxElement = doc.createElement('ram:ApplicableTradeTax');
|
|
|
|
// Calculate tax amount
|
|
const taxAmount = baseAmount * (rate / 100);
|
|
|
|
// Add calculated amount
|
|
const calculatedAmountElement = doc.createElement('ram:CalculatedAmount');
|
|
calculatedAmountElement.textContent = taxAmount.toFixed(2);
|
|
taxElement.appendChild(calculatedAmountElement);
|
|
|
|
// Add type code (VAT)
|
|
const typeCodeElement = doc.createElement('ram:TypeCode');
|
|
typeCodeElement.textContent = 'VAT';
|
|
taxElement.appendChild(typeCodeElement);
|
|
|
|
// Add basis amount
|
|
const basisAmountElement = doc.createElement('ram:BasisAmount');
|
|
basisAmountElement.textContent = baseAmount.toFixed(2);
|
|
taxElement.appendChild(basisAmountElement);
|
|
|
|
// Add category code
|
|
const categoryCodeElement = doc.createElement('ram:CategoryCode');
|
|
categoryCodeElement.textContent = invoice.reverseCharge ? 'AE' : 'S';
|
|
taxElement.appendChild(categoryCodeElement);
|
|
|
|
// Add rate
|
|
const rateElement = doc.createElement('ram:RateApplicablePercent');
|
|
rateElement.textContent = rate.toString();
|
|
taxElement.appendChild(rateElement);
|
|
|
|
settlementElement.appendChild(taxElement);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds monetary summation to the settlement section
|
|
* @param doc XML document
|
|
* @param settlementElement Settlement element
|
|
* @param invoice Invoice data
|
|
*/
|
|
private addMonetarySummation(doc: Document, settlementElement: Element, invoice: TInvoice): void {
|
|
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
|
|
|
|
// Calculate totals
|
|
let totalNetAmount = 0;
|
|
let totalTaxAmount = 0;
|
|
|
|
// Calculate from items
|
|
if (invoice.items) {
|
|
for (const item of invoice.items) {
|
|
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
const itemTaxAmount = itemNetAmount * (item.vatPercentage / 100);
|
|
|
|
totalNetAmount += itemNetAmount;
|
|
totalTaxAmount += itemTaxAmount;
|
|
}
|
|
}
|
|
|
|
const totalGrossAmount = totalNetAmount + totalTaxAmount;
|
|
|
|
// Add line total amount
|
|
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
lineTotalElement.textContent = totalNetAmount.toFixed(2);
|
|
monetarySummationElement.appendChild(lineTotalElement);
|
|
|
|
// Add tax total amount
|
|
const taxTotalElement = doc.createElement('ram:TaxTotalAmount');
|
|
taxTotalElement.textContent = totalTaxAmount.toFixed(2);
|
|
taxTotalElement.setAttribute('currencyID', invoice.currency);
|
|
monetarySummationElement.appendChild(taxTotalElement);
|
|
|
|
// Add grand total amount
|
|
const grandTotalElement = doc.createElement('ram:GrandTotalAmount');
|
|
grandTotalElement.textContent = totalGrossAmount.toFixed(2);
|
|
monetarySummationElement.appendChild(grandTotalElement);
|
|
|
|
// Add due payable amount
|
|
const duePayableElement = doc.createElement('ram:DuePayableAmount');
|
|
duePayableElement.textContent = totalGrossAmount.toFixed(2);
|
|
monetarySummationElement.appendChild(duePayableElement);
|
|
|
|
settlementElement.appendChild(monetarySummationElement);
|
|
}
|
|
|
|
/**
|
|
* Adds line items to the XML document
|
|
* @param doc XML document
|
|
* @param transactionElement Transaction element
|
|
* @param invoice Invoice data
|
|
*/
|
|
private addLineItems(doc: Document, transactionElement: Element, invoice: TInvoice): void {
|
|
// Add each line item
|
|
if (invoice.items) {
|
|
for (const item of invoice.items) {
|
|
// Create line item element
|
|
const lineItemElement = doc.createElement('ram:IncludedSupplyChainTradeLineItem');
|
|
|
|
// Add line ID
|
|
const lineIdElement = doc.createElement('ram:AssociatedDocumentLineDocument');
|
|
const lineIdValueElement = doc.createElement('ram:LineID');
|
|
lineIdValueElement.textContent = item.position.toString();
|
|
lineIdElement.appendChild(lineIdValueElement);
|
|
lineItemElement.appendChild(lineIdElement);
|
|
|
|
// Add product information
|
|
const productElement = doc.createElement('ram:SpecifiedTradeProduct');
|
|
|
|
// Add name
|
|
const nameElement = doc.createElement('ram:Name');
|
|
nameElement.textContent = item.name;
|
|
productElement.appendChild(nameElement);
|
|
|
|
// Add article number if available
|
|
if (item.articleNumber) {
|
|
const articleNumberElement = doc.createElement('ram:SellerAssignedID');
|
|
articleNumberElement.textContent = item.articleNumber;
|
|
productElement.appendChild(articleNumberElement);
|
|
}
|
|
|
|
lineItemElement.appendChild(productElement);
|
|
|
|
// Add agreement information (price)
|
|
const agreementElement = doc.createElement('ram:SpecifiedLineTradeAgreement');
|
|
const priceElement = doc.createElement('ram:NetPriceProductTradePrice');
|
|
const chargeAmountElement = doc.createElement('ram:ChargeAmount');
|
|
chargeAmountElement.textContent = item.unitNetPrice.toFixed(2);
|
|
priceElement.appendChild(chargeAmountElement);
|
|
agreementElement.appendChild(priceElement);
|
|
lineItemElement.appendChild(agreementElement);
|
|
|
|
// Add delivery information (quantity)
|
|
const deliveryElement = doc.createElement('ram:SpecifiedLineTradeDelivery');
|
|
const quantityElement = doc.createElement('ram:BilledQuantity');
|
|
quantityElement.textContent = item.unitQuantity.toString();
|
|
quantityElement.setAttribute('unitCode', item.unitType);
|
|
deliveryElement.appendChild(quantityElement);
|
|
lineItemElement.appendChild(deliveryElement);
|
|
|
|
// Add settlement information (tax)
|
|
const settlementElement = doc.createElement('ram:SpecifiedLineTradeSettlement');
|
|
|
|
// Add tax information
|
|
const taxElement = doc.createElement('ram:ApplicableTradeTax');
|
|
|
|
// Add tax type code
|
|
const taxTypeCodeElement = doc.createElement('ram:TypeCode');
|
|
taxTypeCodeElement.textContent = 'VAT';
|
|
taxElement.appendChild(taxTypeCodeElement);
|
|
|
|
// Add tax category code
|
|
const taxCategoryCodeElement = doc.createElement('ram:CategoryCode');
|
|
taxCategoryCodeElement.textContent = invoice.reverseCharge ? 'AE' : 'S';
|
|
taxElement.appendChild(taxCategoryCodeElement);
|
|
|
|
// Add tax rate
|
|
const taxRateElement = doc.createElement('ram:RateApplicablePercent');
|
|
taxRateElement.textContent = item.vatPercentage.toString();
|
|
taxElement.appendChild(taxRateElement);
|
|
|
|
settlementElement.appendChild(taxElement);
|
|
|
|
// Add monetary summation
|
|
const monetarySummationElement = doc.createElement('ram:SpecifiedLineTradeSettlementMonetarySummation');
|
|
|
|
// Calculate item total
|
|
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
|
|
|
|
// Add line total amount
|
|
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
|
|
lineTotalElement.textContent = itemNetAmount.toFixed(2);
|
|
monetarySummationElement.appendChild(lineTotalElement);
|
|
|
|
settlementElement.appendChild(monetarySummationElement);
|
|
|
|
lineItemElement.appendChild(settlementElement);
|
|
|
|
// Add line item to transaction
|
|
transactionElement.appendChild(lineItemElement);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formats a date as YYYYMMDD
|
|
* @param timestamp Timestamp to format
|
|
* @returns Formatted date string
|
|
*/
|
|
private formatDateYYYYMMDD(timestamp: number): string {
|
|
const date = new Date(timestamp);
|
|
const year = date.getFullYear();
|
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
const day = date.getDate().toString().padStart(2, '0');
|
|
return `${year}${month}${day}`;
|
|
}
|
|
} |