]> 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.
 
 
 ``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::
 
 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 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()
     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.id = "RE1337"
     doc.header.type_code = "380"
-    doc.header.name = "RECHNUNG"
     doc.header.issue_date_time = date.today()
     doc.header.issue_date_time = date.today()
-    doc.header.language = "ger"
 
     doc.header.notes.add(IncludedNote(content="Test Note 1"))
 
 
     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.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.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(
     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 = 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.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()
     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.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.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")
 
 
     # 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
     # 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
 lxml
 pypdf
 pytest
index 556b7c4663c829838b0f00caa781ba91cdc36357..75fe735fa2d03680be1d480b8098e80d59138b2a 100644 (file)
@@ -1,79 +1,10 @@
 import os
 import pytest
 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):
 
 
 @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
 
 
     return doc