UNPKG

lbx-invoice

Version:

Provides functionality around generating invoices.

233 lines (211 loc) 11.8 kB
import { create as createXml } from 'xmlbuilder2'; import { XMLBuilder, XMLBuilderCreateOptions } from 'xmlbuilder2/lib/interfaces'; import { InvoiceCalcUtilities } from './invoice-calc.utilities'; import { dateTo102 } from '../functions'; import { getCustomerName } from '../functions/get-customer-name.function'; import { BaseCompanyInfo, BaseInvoice, InvoiceItem, Vat } from '../models'; /** * Represents a wrapper around XML nodes to implement easy to use and chainable document builder methods. */ export type XML = XMLBuilder; /** * Utility class for handling xml files. */ export abstract class XmlUtilities { /** * Creates an XML document without any child nodes with the given options. * @param options - Builder options. * @returns Document node. */ static create(options: XMLBuilderCreateOptions = { version: '1.0', encoding: 'utf8' }): XML { return createXml(options); } /** * Creates an cross industry invoice compliant xml file. * @param invoice - The invoice data to generate the xml from. * @param currency - The currency code, eg. 'USD'. * @param companyInfo - Info about the seller. * @param tradeContact - The trade contact. Defaults to companyInfo.ceo. * @param documentContextId - The identifier for the GuidelineSpecifiedDocumentContextParameter ID. * @returns The finished xml. * @throws When the provided invoice data cannot be transformed to an valid xml. */ static createCrossIndustryInvoiceXml<Invoice extends BaseInvoice>( invoice: Invoice, currency: string, companyInfo: BaseCompanyInfo, tradeContact: string | undefined = companyInfo.ceo, documentContextId: string = 'urn:cen.eu:en16931:2017' ): XML { if (!tradeContact) { throw new Error('No trade contact has been provided. (This defaults to companyInfo.CEO)'); } const root: XML = XmlUtilities.create({ version: '1.0', encoding: 'utf8' }).ele( 'rsm:CrossIndustryInvoice', { 'xmlns:rsm': 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100', 'xmlns:qdt': 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100', 'xmlns:ram': 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', 'xmlns:xs': 'http://www.w3.org/2001/XMLSchema', 'xmlns:udt': 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100' } ); const documentContext: XML = root.ele('rsm:ExchangedDocumentContext'); documentContext.ele('ram:BusinessProcessSpecifiedDocumentContextParameter') .ele('ram:ID') .txt('urn:fdc:peppol.eu:2017:poacc:billing:01:1.0'); documentContext.ele('ram:GuidelineSpecifiedDocumentContextParameter') .ele('ram:ID') .txt(documentContextId); const exchangedDocument: XML = root.ele('rsm:ExchangedDocument'); exchangedDocument.ele('ram:ID').txt(invoice.number); // eslint-disable-next-line sonar/no-duplicate-string exchangedDocument.ele('ram:TypeCode').txt('380'); // 380 = Commercial invoice exchangedDocument.ele('ram:IssueDateTime') // eslint-disable-next-line sonar/no-duplicate-string .ele('udt:DateTimeString', { format: '102' }) .txt(dateTo102(invoice.date)); const tradeTransaction: XML = root.ele('rsm:SupplyChainTradeTransaction'); this.buildTradeLineItems(invoice, tradeTransaction); const tradeAgreement: XML = tradeTransaction.ele('ram:ApplicableHeaderTradeAgreement'); tradeAgreement.ele('ram:BuyerReference').txt('999'); this.buildSellerParty(tradeAgreement, companyInfo, tradeContact); this.buildBuyerParty<Invoice>(tradeAgreement, invoice); tradeTransaction.ele('ram:ApplicableHeaderTradeDelivery') .ele('ram:ActualDeliverySupplyChainEvent') .ele('ram:OccurrenceDateTime') .ele('udt:DateTimeString', { format: '102' }) .txt(dateTo102(invoice.performanceDate)); const tradeSettlement: XML = tradeTransaction.ele('ram:ApplicableHeaderTradeSettlement'); tradeSettlement.ele('ram:InvoiceCurrencyCode').txt(currency); // payment options const paymentMeans: XML = tradeSettlement.ele('ram:SpecifiedTradeSettlementPaymentMeans'); paymentMeans.ele('ram:TypeCode').txt('58'); if (!companyInfo.iban || !companyInfo.bic) { throw new Error('At least the iban and bic need to be provided for payment information.'); } paymentMeans.ele('ram:PayeePartyCreditorFinancialAccount').ele('ram:IBANID') .txt(companyInfo.iban); paymentMeans.ele('ram:PayeeSpecifiedCreditorFinancialInstitution').ele('ram:BICID') .txt(companyInfo.bic); // taxes const taxGroups: Vat[] = this.getTaxGroups<Invoice>(invoice); for (const taxGroup of taxGroups) { const tax: XML = tradeSettlement.ele('ram:ApplicableTradeTax'); tax.ele('ram:CalculatedAmount').txt(InvoiceCalcUtilities.getTotalTaxForTaxGroup(invoice, taxGroup, false).toString()); tax.ele('ram:TypeCode').txt('VAT'); tax.ele('ram:BasisAmount').txt(InvoiceCalcUtilities.getTotalBeforeTaxForTaxGroup(invoice, taxGroup, false).toString()); tax.ele('ram:CategoryCode').txt(taxGroup.categoryCode as string); tax.ele('ram:RateApplicablePercent').txt(taxGroup.rate.toString()); if (taxGroup.categoryCode === 'E') { if (!taxGroup.exemptionReason) { throw new Error('No exemption reason has been provided.'); } tax.ele('ram:ExemptionReason').txt(taxGroup.exemptionReason); } } // payment terms tradeSettlement.ele('ram:SpecifiedTradePaymentTerms') .ele('ram:DueDateDateTime') .ele('udt:DateTimeString', { format: '102' }) .txt(dateTo102(invoice.dueDate)); // summary const summary: XML = tradeSettlement.ele('ram:SpecifiedTradeSettlementHeaderMonetarySummation'); summary.ele('ram:LineTotalAmount').txt(InvoiceCalcUtilities.getTotalBeforeTax(invoice).toString()); // ChargeTotalAmount // AllowanceTotalAmount summary.ele('ram:TaxBasisTotalAmount').txt(InvoiceCalcUtilities.getTotalBeforeTax(invoice).toString()); summary.ele('ram:TaxTotalAmount').txt(InvoiceCalcUtilities.getTotalTax(invoice).toString()) .att('currencyID', currency); summary.ele('ram:GrandTotalAmount').txt(InvoiceCalcUtilities.getTotalAfterTax(invoice).toString()); // TotalPrepaidAmount summary.ele('ram:DuePayableAmount').txt(InvoiceCalcUtilities.getTotalAfterTax(invoice).toString()); return root; } private static buildBuyerParty<Invoice extends BaseInvoice>(tradeAgreement: XML, invoice: Invoice): void { const buyerParty: XML = tradeAgreement.ele('ram:BuyerTradeParty'); buyerParty.ele('ram:Name').txt(getCustomerName(invoice)); if (!invoice.customerAddressData.email) { throw new Error('No customer email has been provided (invoice.customerAddressData.email).'); } buyerParty.ele('ram:URIUniversalCommunication') .ele('ram:URIID', { schemeID: 'EM' }) .txt(invoice.customerAddressData.email); const buyerPostalTradeAddress: XML = buyerParty.ele('ram:PostalTradeAddress'); buyerPostalTradeAddress.ele('ram:PostcodeCode').txt(invoice.customerAddressData.postcode); buyerPostalTradeAddress.ele('ram:LineOne').txt(`${invoice.customerAddressData.street} ${invoice.customerAddressData.number}`); buyerPostalTradeAddress.ele('ram:CityName').txt(invoice.customerAddressData.city); buyerPostalTradeAddress.ele('ram:CountryID').txt(invoice.customerAddressData.countryId); } private static buildSellerParty(tradeAgreement: XML, companyInfo: BaseCompanyInfo, tradeContact: string): void { const sellerParty: XML = tradeAgreement.ele('ram:SellerTradeParty'); sellerParty.ele('ram:Name').txt(companyInfo.fullName); if (!companyInfo.taxNumber) { throw new Error('No tax number has been provided.'); } sellerParty.ele('ram:SpecifiedLegalOrganization') .ele('ram:ID') .txt(companyInfo.taxNumber); sellerParty.ele('ram:URIUniversalCommunication') .ele('ram:URIID', { schemeID: 'EM' }) .txt(companyInfo.email); const sellerContact: XML = sellerParty.ele('ram:DefinedTradeContact'); sellerContact.ele('ram:PersonName').txt(tradeContact); sellerContact.ele('ram:TelephoneUniversalCommunication') .ele('ram:CompleteNumber') .txt(companyInfo.phone); sellerContact.ele('ram:EmailURIUniversalCommunication') .ele('ram:URIID') .txt(companyInfo.email); const sellerPostalTradeAddress: XML = sellerParty.ele('ram:PostalTradeAddress'); sellerPostalTradeAddress.ele('ram:PostcodeCode').txt(companyInfo.address.postcode); sellerPostalTradeAddress.ele('ram:LineOne').txt(`${companyInfo.address.street} ${companyInfo.address.number}`); sellerPostalTradeAddress.ele('ram:CityName').txt(companyInfo.address.city); sellerPostalTradeAddress.ele('ram:CountryID').txt(companyInfo.address.countryId); if (companyInfo.taxNumber) { sellerParty.ele('ram:SpecifiedTaxRegistration') .ele('ram:ID', { schemeID: 'FC' }) .txt(companyInfo.taxNumber); } if (companyInfo.vatNumber) { sellerParty.ele('ram:SpecifiedTaxRegistration') .ele('ram:ID', { schemeID: 'VA' }) .txt(companyInfo.vatNumber); } } private static buildTradeLineItems<Invoice extends BaseInvoice>(invoice: Invoice, tradeTransaction: XML): void { for (let i: number = 0; i < invoice.items.length; i++) { const invoiceItem: InvoiceItem = invoice.items[i]; const lineItem: XML = tradeTransaction.ele('ram:IncludedSupplyChainTradeLineItem'); lineItem.ele('ram:AssociatedDocumentLineDocument') .ele('ram:LineID') .txt((i + 1).toString()); lineItem.ele('ram:SpecifiedTradeProduct') .ele('ram:Name') .txt(invoiceItem.name); lineItem.ele('ram:SpecifiedLineTradeAgreement') .ele('ram:NetPriceProductTradePrice') .ele('ram:ChargeAmount') .txt(invoiceItem.price.toString()); lineItem.ele('ram:SpecifiedLineTradeDelivery') .ele('ram:BilledQuantity', { unitCode: invoiceItem.amountUnit.code }) .txt(invoiceItem.amount.toString()); const tradeSettlement: XML = lineItem.ele('ram:SpecifiedLineTradeSettlement'); const tradeTax: XML = tradeSettlement.ele('ram:ApplicableTradeTax'); tradeTax.ele('ram:TypeCode').txt('VAT'); tradeTax.ele('ram:CategoryCode').txt(invoiceItem.vat.categoryCode as string); tradeTax.ele('ram:RateApplicablePercent').txt(invoiceItem.vat.rate.toString()); tradeSettlement.ele('ram:SpecifiedTradeSettlementLineMonetarySummation') .ele('ram:LineTotalAmount') .txt(InvoiceCalcUtilities.getItemTotalPriceBeforeTax(invoiceItem).toString()); } } private static getTaxGroups<Invoice extends BaseInvoice>(invoice: Invoice): Vat[] { return [...new Set(invoice.items.map(item => item.vat))]; } }