From 536ca73735afc3add4c4e4694b5bc9551a0604db Mon Sep 17 00:00:00 2001 From: Julien Palard Date: Thu, 27 Nov 2025 21:06:23 +0100 Subject: [PATCH] Make README example pass validation with EN16931 (cog alternative). (#103) * Make README example pass validation with EN16931. * Avoid copy/pasting the README example, it's too easily out of sync. * Apply fixes from #102. * Alternative implementation using cog. * Run black --------- Co-authored-by: Raphael Michel --- README.rst | 80 ++++++++++++++++++++++++++---------------- example.py | 83 ++++++++++++++++++++++++++++++++++++++++++++ requirements_dev.txt | 1 + tests/conftest.py | 71 +------------------------------------ 4 files changed, 135 insertions(+), 100 deletions(-) create mode 100644 example.py diff --git a/README.rst b/README.rst index 47eca20..b9dd812 100644 --- a/README.rst +++ b/README.rst @@ -49,85 +49,105 @@ Parsing:: ``Document.parse()`` taskes a boolean parameter ``strict`` which defaults to ``True``. This means that the parser will raise an error if it encounters any unknown element. If you set it to ``False``, the parser will not raise an error and parse whatever it can. +.. [[[cog + # Re-run this with `cog -r README.rst` + + from pathlib import Path + from textwrap import indent + + import cog + + cog.outl("Generating::\n") + cog.outl(indent(Path("example.py").read_text(encoding="UTF-8"), " "), dedent=False) +.. ]]] Generating:: - from datetime import date, datetime, timezone + from datetime import date, datetime, timedelta, timezone from decimal import Decimal from drafthorse.models.accounting import ApplicableTradeTax from drafthorse.models.document import Document from drafthorse.models.note import IncludedNote from drafthorse.models.party import TaxRegistration + from drafthorse.models.payment import PaymentMeans, PaymentTerms + from drafthorse.models.trade import AdvancePayment, IncludedTradeTax from drafthorse.models.tradelines import LineItem from drafthorse.pdf import attach_xml # Build data structure doc = Document() - doc.context.guideline_parameter.id = "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended" + doc.context.guideline_parameter.id = "urn:cen.eu:en16931:2017" doc.header.id = "RE1337" doc.header.type_code = "380" - doc.header.name = "RECHNUNG" doc.header.issue_date_time = date.today() - doc.header.language = "ger" doc.header.notes.add(IncludedNote(content="Test Note 1")) - doc.trade.agreement.seller.name = "Lieferant GmbH" - doc.trade.settlement.payee.name = "Lieferant GmbH" - doc.trade.agreement.buyer.name = "Kunde GmbH" - doc.trade.settlement.invoicee.name = "Kunde GmbH" + doc.trade.agreement.buyer.address.country_id = "DE" doc.trade.settlement.currency_code = "EUR" - doc.trade.settlement.payment_means.type_code = "ZZZ" + doc.trade.settlement.payment_means.add(PaymentMeans(type_code="ZZZ")) + doc.trade.agreement.seller.name = "Lieferant GmbH" doc.trade.agreement.seller.address.country_id = "DE" doc.trade.agreement.seller.address.country_subdivision = "Bayern" doc.trade.agreement.seller.tax_registrations.add( - TaxRegistration( - id=("VA", "DE000000000") - ) + TaxRegistration(id=("VA", "DE000000000")) ) - doc.trade.agreement.seller_order.issue_date_time = datetime.now(timezone.utc) - doc.trade.agreement.buyer_order.issue_date_time = datetime.now(timezone.utc) - doc.trade.settlement.advance_payment.received_date = datetime.now(timezone.utc) - doc.trade.agreement.customer_order.issue_date_time = datetime.now(timezone.utc) + advance = AdvancePayment( + received_date=datetime.now(timezone.utc), paid_amount=Decimal(42) + ) + advance.included_trade_tax.add( + IncludedTradeTax( + calculated_amount=Decimal(0), + type_code="VAT", + category_code="E", + rate_applicable_percent=Decimal(0), + ) + ) + doc.trade.settlement.advance_payment.add(advance) li = LineItem() li.document.line_id = "1" li.product.name = "Rainbow" - li.agreement.gross.amount = Decimal("999.00") - li.agreement.gross.basis_quantity = (Decimal("1.0000"), "H87") # H87 == pieces - li.agreement.net.amount = Decimal("999.00") - li.agreement.net.basis_quantity = (Decimal("999.00"), "EUR") - li.delivery.billed_quantity = (Decimal("1.0000"), "H87") # H87 == pieces + li.agreement.gross.amount = Decimal("1198.8") + li.agreement.gross.basis_quantity = (Decimal("1.0000"), "C62") # C62 == unit + li.agreement.net.amount = Decimal("999") + li.agreement.net.basis_quantity = (Decimal("1.0000"), "C62") # C62 == unit + li.delivery.billed_quantity = (Decimal("1.0000"), "C62") # C62 == unit li.settlement.trade_tax.type_code = "VAT" - li.settlement.trade_tax.category_code = "E" - li.settlement.trade_tax.rate_applicable_percent = Decimal("0.00") + li.settlement.trade_tax.category_code = "S" + li.settlement.trade_tax.rate_applicable_percent = Decimal("20.00") li.settlement.monetary_summation.total_amount = Decimal("999.00") doc.trade.items.add(li) trade_tax = ApplicableTradeTax() - trade_tax.calculated_amount = Decimal("0.00") + trade_tax.calculated_amount = Decimal("199.80") trade_tax.basis_amount = Decimal("999.00") trade_tax.type_code = "VAT" - trade_tax.category_code = "AE" - trade_tax.exemption_reason_code = 'VATEX-EU-AE' - trade_tax.rate_applicable_percent = Decimal("0.00") + trade_tax.category_code = "S" + trade_tax.rate_applicable_percent = Decimal("20.00") doc.trade.settlement.trade_tax.add(trade_tax) doc.trade.settlement.monetary_summation.line_total = Decimal("999.00") doc.trade.settlement.monetary_summation.charge_total = Decimal("0.00") doc.trade.settlement.monetary_summation.allowance_total = Decimal("0.00") doc.trade.settlement.monetary_summation.tax_basis_total = Decimal("999.00") - doc.trade.settlement.monetary_summation.tax_total = Decimal("0.00") - doc.trade.settlement.monetary_summation.grand_total = Decimal("999.00") - doc.trade.settlement.monetary_summation.due_amount = Decimal("999.00") + doc.trade.settlement.monetary_summation.tax_total = (Decimal("199.80"), "EUR") + doc.trade.settlement.monetary_summation.grand_total = Decimal("1198.8") + doc.trade.settlement.monetary_summation.due_amount = Decimal("1198.8") + + terms = PaymentTerms() + terms.due = datetime.now(timezone.utc) + timedelta(days=30) + doc.trade.settlement.terms.add(terms) # Generate XML file xml = doc.serialize(schema="FACTUR-X_EXTENDED") +.. [[[end]]] + # Attach XML to an existing PDF. # Note that the existing PDF should be compliant to PDF/A-3! # You can validate this here: https://www.pdf-online.com/osa/validate.aspx diff --git a/example.py b/example.py new file mode 100644 index 0000000..95f90e5 --- /dev/null +++ b/example.py @@ -0,0 +1,83 @@ +from datetime import date, datetime, timedelta, timezone +from decimal import Decimal + +from drafthorse.models.accounting import ApplicableTradeTax +from drafthorse.models.document import Document +from drafthorse.models.note import IncludedNote +from drafthorse.models.party import TaxRegistration +from drafthorse.models.payment import PaymentMeans, PaymentTerms +from drafthorse.models.trade import AdvancePayment, IncludedTradeTax +from drafthorse.models.tradelines import LineItem +from drafthorse.pdf import attach_xml + +# Build data structure +doc = Document() +doc.context.guideline_parameter.id = "urn:cen.eu:en16931:2017" +doc.header.id = "RE1337" +doc.header.type_code = "380" +doc.header.issue_date_time = date.today() + +doc.header.notes.add(IncludedNote(content="Test Note 1")) + +doc.trade.agreement.buyer.name = "Kunde GmbH" +doc.trade.agreement.buyer.address.country_id = "DE" + +doc.trade.settlement.currency_code = "EUR" +doc.trade.settlement.payment_means.add(PaymentMeans(type_code="ZZZ")) + +doc.trade.agreement.seller.name = "Lieferant GmbH" +doc.trade.agreement.seller.address.country_id = "DE" +doc.trade.agreement.seller.address.country_subdivision = "Bayern" +doc.trade.agreement.seller.tax_registrations.add( + TaxRegistration(id=("VA", "DE000000000")) +) + +advance = AdvancePayment( + received_date=datetime.now(timezone.utc), paid_amount=Decimal(42) +) +advance.included_trade_tax.add( + IncludedTradeTax( + calculated_amount=Decimal(0), + type_code="VAT", + category_code="E", + rate_applicable_percent=Decimal(0), + ) +) +doc.trade.settlement.advance_payment.add(advance) + +li = LineItem() +li.document.line_id = "1" +li.product.name = "Rainbow" +li.agreement.gross.amount = Decimal("1198.8") +li.agreement.gross.basis_quantity = (Decimal("1.0000"), "C62") # C62 == unit +li.agreement.net.amount = Decimal("999") +li.agreement.net.basis_quantity = (Decimal("1.0000"), "C62") # C62 == unit +li.delivery.billed_quantity = (Decimal("1.0000"), "C62") # C62 == unit +li.settlement.trade_tax.type_code = "VAT" +li.settlement.trade_tax.category_code = "S" +li.settlement.trade_tax.rate_applicable_percent = Decimal("20.00") +li.settlement.monetary_summation.total_amount = Decimal("999.00") +doc.trade.items.add(li) + +trade_tax = ApplicableTradeTax() +trade_tax.calculated_amount = Decimal("199.80") +trade_tax.basis_amount = Decimal("999.00") +trade_tax.type_code = "VAT" +trade_tax.category_code = "S" +trade_tax.rate_applicable_percent = Decimal("20.00") +doc.trade.settlement.trade_tax.add(trade_tax) + +doc.trade.settlement.monetary_summation.line_total = Decimal("999.00") +doc.trade.settlement.monetary_summation.charge_total = Decimal("0.00") +doc.trade.settlement.monetary_summation.allowance_total = Decimal("0.00") +doc.trade.settlement.monetary_summation.tax_basis_total = Decimal("999.00") +doc.trade.settlement.monetary_summation.tax_total = (Decimal("199.80"), "EUR") +doc.trade.settlement.monetary_summation.grand_total = Decimal("1198.8") +doc.trade.settlement.monetary_summation.due_amount = Decimal("1198.8") + +terms = PaymentTerms() +terms.due = datetime.now(timezone.utc) + timedelta(days=30) +doc.trade.settlement.terms.add(terms) + +# Generate XML file +xml = doc.serialize(schema="FACTUR-X_EXTENDED") diff --git a/requirements_dev.txt b/requirements_dev.txt index fdc09b4..b581056 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,4 @@ +cogapp lxml pypdf pytest diff --git a/tests/conftest.py b/tests/conftest.py index 556b7c4..75fe735 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,79 +1,10 @@ import os import pytest -from datetime import date, datetime, timezone -from decimal import Decimal - -from drafthorse.models.accounting import ApplicableTradeTax -from drafthorse.models.document import Document -from drafthorse.models.note import IncludedNote -from drafthorse.models.party import TaxRegistration -from drafthorse.models.tradelines import LineItem +from example import doc @pytest.fixture def invoice_document(request): - doc = Document() - doc.context.guideline_parameter.id = ( - "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended" - ) - doc.header.id = "RE1337" - doc.header.type_code = request.param - doc.header.name = "RECHNUNG" - doc.header.issue_date_time = date.today() - doc.header.language = "ger" - - doc.header.notes.add(IncludedNote(content="Test Note 1")) - - doc.trade.agreement.seller.name = "Lieferant GmbH" - doc.trade.settlement.payee.name = "Lieferant GmbH" - - doc.trade.agreement.buyer.name = "Kunde GmbH" - doc.trade.settlement.invoicee.name = "Kunde GmbH" - - doc.trade.settlement.currency_code = "EUR" - doc.trade.settlement.payment_means.type_code = "ZZZ" - - doc.trade.agreement.seller.address.country_id = "DE" - doc.trade.agreement.seller.address.country_subdivision = "Bayern" - doc.trade.agreement.seller.tax_registrations.add( - TaxRegistration(id=("VA", "DE000000000")) - ) - - doc.trade.agreement.seller_order.issue_date_time = datetime.now(timezone.utc) - doc.trade.agreement.buyer_order.issue_date_time = datetime.now(timezone.utc) - doc.trade.settlement.advance_payment.received_date = datetime.now(timezone.utc) - doc.trade.agreement.customer_order.issue_date_time = datetime.now(timezone.utc) - - li = LineItem() - li.document.line_id = "1" - li.product.name = "Rainbow" - li.agreement.gross.amount = Decimal("999.00") - li.agreement.gross.basis_quantity = (Decimal("1.0000"), "C62") # C62 == pieces - li.agreement.net.amount = Decimal("999.00") - li.agreement.net.basis_quantity = (Decimal("999.00"), "C62") - li.delivery.billed_quantity = (Decimal("1.0000"), "C62") # C62 == pieces - li.settlement.trade_tax.type_code = "VAT" - li.settlement.trade_tax.category_code = "E" - li.settlement.trade_tax.rate_applicable_percent = Decimal("0.00") - li.settlement.monetary_summation.total_amount = Decimal("999.00") - doc.trade.items.add(li) - - trade_tax = ApplicableTradeTax() - trade_tax.calculated_amount = Decimal("0.00") - trade_tax.basis_amount = Decimal("999.00") - trade_tax.type_code = "VAT" - trade_tax.category_code = "E" - trade_tax.rate_applicable_percent = Decimal("0.00") - doc.trade.settlement.trade_tax.add(trade_tax) - - doc.trade.settlement.monetary_summation.line_total = Decimal("999.00") - doc.trade.settlement.monetary_summation.charge_total = Decimal("0.00") - doc.trade.settlement.monetary_summation.allowance_total = Decimal("0.00") - doc.trade.settlement.monetary_summation.tax_basis_total = Decimal("999.00") - doc.trade.settlement.monetary_summation.tax_total = (Decimal("0.00"), "EUR") - doc.trade.settlement.monetary_summation.grand_total = Decimal("999.00") - doc.trade.settlement.monetary_summation.due_amount = Decimal("999.00") - return doc -- 2.47.3