]> git.ipfire.org Git - thirdparty/python-drafthorse.git/commitdiff
Make README example pass validation with EN16931 (cog alternative). (#103) master
authorJulien Palard <julien@palard.fr>
Thu, 27 Nov 2025 20:06:23 +0000 (21:06 +0100)
committerGitHub <noreply@github.com>
Thu, 27 Nov 2025 20:06:23 +0000 (21:06 +0100)
* 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 <michel@rami.io>
README.rst
example.py [new file with mode: 0644]
requirements_dev.txt
tests/conftest.py

index 47eca20f29af22e63a96187440e99e0e3739332e..b9dd812b8dd67347631839d7b5fc14602182c02a 100644 (file)
@@ -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 (file)
index 0000000..95f90e5
--- /dev/null
@@ -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")
index fdc09b404335fff5685502f9882b999627532087..b581056eddbe4c2a5a52303a0f3ed226b7fd6edd 100644 (file)
@@ -1,3 +1,4 @@
+cogapp
 lxml
 pypdf
 pytest
index 556b7c4663c829838b0f00caa781ba91cdc36357..75fe735fa2d03680be1d480b8098e80d59138b2a 100644 (file)
@@ -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