111 lines
5.7 KiB
Python
111 lines
5.7 KiB
Python
"""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
|