"""UN/CEFACT CII D16B, EN16931: positive, taxable service invoices.""" from pathlib import Path from decimal import Decimal import xml.etree.ElementTree as ET from .calc import compute_totals, line_net from .models import Invoice NS = {k: f'urn:un:unece:uncefact:data:standard:{v}:100' for k, v in { 'rsm': 'CrossIndustryInvoice', 'ram': 'ReusableAggregateBusinessInformationEntity', 'udt': 'UnqualifiedDataType'}.items()} for prefix, uri in NS.items(): ET.register_namespace(prefix, uri) def node(parent, name, value=None, **attrs): prefix, local = name.split(':') el = ET.SubElement(parent, f'{{{NS[prefix]}}}{local}', attrs) if value is not None: el.text = format(value, "f") if isinstance(value, Decimal) else str(value) return el def ram(parent, name, value=None, **attrs): return node(parent, 'ram:' + name, value, **attrs) def date_node(parent, name, value): node(ram(parent, name), 'udt:DateTimeString', value.strftime('%Y%m%d'), format='102') def party(parent, name, data): p = ram(parent, name) ram(p, 'Name', data.name) address = ram(p, 'PostalTradeAddress') for key, value in [('PostcodeCode', data.address.postal_code), ('LineOne', data.address.street), ('CityName', data.address.city), ('CountryID', data.address.country)]: ram(address, key, value) if data.email: ram(ram(p, 'URIUniversalCommunication'), 'URIID', data.email, schemeID='EM') for value, scheme in [(data.vat_id, 'VA'), (data.tax_number, 'FC')]: if value: ram(ram(p, 'SpecifiedTaxRegistration'), 'ID', value, schemeID=scheme) def generate_facturx_xml(inv: Invoice, out_xml: Path, logger) -> Path: totals = compute_totals(inv) root = ET.Element(f'{{{NS["rsm"]}}}CrossIndustryInvoice') context = node(root, 'rsm:ExchangedDocumentContext') ram(ram(context, 'GuidelineSpecifiedDocumentContextParameter'), 'ID', 'urn:cen.eu:en16931:2017') doc = node(root, 'rsm:ExchangedDocument') ram(doc, 'ID', inv.meta.invoice_number) ram(doc, 'TypeCode', '380') date_node(doc, 'IssueDateTime', inv.meta.invoice_date) if inv.meta.notes: ram(ram(doc, 'IncludedNote'), 'Content', inv.meta.notes) transaction = node(root, 'rsm:SupplyChainTradeTransaction') for index, item in enumerate(inv.items, 1): line = ram(transaction, 'IncludedSupplyChainTradeLineItem') ram(ram(line, 'AssociatedDocumentLineDocument'), 'LineID', index) product = ram(line, 'SpecifiedTradeProduct') ram(product, 'Name', item.name) if item.description: ram(product, 'Description', item.description) agreement = ram(line, 'SpecifiedLineTradeAgreement') ram(ram(agreement, 'NetPriceProductTradePrice'), 'ChargeAmount', item.unit_price_net) ram(ram(line, 'SpecifiedLineTradeDelivery'), 'BilledQuantity', item.quantity, unitCode=item.unit) settlement = ram(line, 'SpecifiedLineTradeSettlement') tax = ram(settlement, 'ApplicableTradeTax') ram(tax, 'TypeCode', 'VAT') ram(tax, 'CategoryCode', 'S') ram(tax, 'RateApplicablePercent', item.tax_rate) ram(ram(settlement, 'SpecifiedTradeSettlementLineMonetarySummation'), 'LineTotalAmount', line_net(item)) agreement = ram(transaction, 'ApplicableHeaderTradeAgreement') if inv.meta.buyer_reference: ram(agreement, 'BuyerReference', inv.meta.buyer_reference) party(agreement, 'SellerTradeParty', inv.seller) party(agreement, 'BuyerTradeParty', inv.buyer) if inv.meta.order_reference: ram(ram(agreement, 'BuyerOrderReferencedDocument'), 'IssuerAssignedID', inv.meta.order_reference) delivery = ram(transaction, 'ApplicableHeaderTradeDelivery') date_node(ram(delivery, 'ActualDeliverySupplyChainEvent'), 'OccurrenceDateTime', inv.meta.service_date) settlement = ram(transaction, 'ApplicableHeaderTradeSettlement') ram(settlement, 'PaymentReference', inv.payment.reference) ram(settlement, 'InvoiceCurrencyCode', inv.meta.currency) if inv.payment.iban: means = ram(settlement, 'SpecifiedTradeSettlementPaymentMeans') ram(means, 'TypeCode', '58') ram(ram(means, 'PayeePartyCreditorFinancialAccount'), 'IBANID', inv.payment.iban.replace(' ', '')) if inv.payment.bic: ram(ram(means, 'PayeeSpecifiedCreditorFinancialInstitution'), 'BICID', inv.payment.bic) for rate, base in totals.basis_by_rate.items(): tax = ram(settlement, 'ApplicableTradeTax') ram(tax, 'CalculatedAmount', totals.tax_by_rate[rate]) ram(tax, 'TypeCode', 'VAT') ram(tax, 'BasisAmount', base) ram(tax, 'CategoryCode', 'S') ram(tax, 'RateApplicablePercent', rate) if inv.meta.service_period_start: period = ram(settlement, 'BillingSpecifiedPeriod') date_node(period, 'StartDateTime', inv.meta.service_period_start) date_node(period, 'EndDateTime', inv.meta.service_period_end) terms = ram(settlement, 'SpecifiedTradePaymentTerms') date_node(terms, 'DueDateDateTime', inv.payment.due_date) summary = ram(settlement, 'SpecifiedTradeSettlementHeaderMonetarySummation') for name, amount in [('LineTotalAmount', totals.net_total), ('ChargeTotalAmount', '0.00'), ('AllowanceTotalAmount', '0.00'), ('TaxBasisTotalAmount', totals.net_total)]: ram(summary, name, amount) ram(summary, 'TaxTotalAmount', totals.tax_total, currencyID=inv.meta.currency) ram(summary, 'GrandTotalAmount', totals.gross_total) ram(summary, 'DuePayableAmount', totals.gross_total) ET.indent(root) out_xml.parent.mkdir(parents=True, exist_ok=True) ET.ElementTree(root).write(out_xml, encoding='utf-8', xml_declaration=True) logger.info('Wrote EN16931 XML: %s', out_xml) return out_xml