Skip to content

Commit cdfd45a

Browse files
authored
fixes Issue pretix#20, Issue pretix#22, Issue pretix#26 and Issue pretix#29. It replaces PR pretix#21 and PR pretix#23 and contains alignments to the v2.2 specification (pretix#27)
* - corrects the NS_QDT name according to the zugferd22 specification - extends elements.py:DateTimeElement to allow the adjustment of the inner DateTimes namespace - updates references.py:ReferencedDocument to use NS_QDT for its DateTimeElement - corrects profiles in reference.py for various attributes according to the zugferd22 schemata - adds test zugferd_2p2_EN16931_ElektronischeAdresse2.xml as a variation of the official zugferd22 sample EN16931_ElektronischeAdresse.xml adding a FormattedIssueDateTime to the BuyerOrderReferencedDocument - moves SellerOrderReferencedDocument from trade.py to references.py - fixes DateTimeField namespace of AdvancePayment.received_date * This commit also adds the class ProductInstance (IndividualTradeProductInstance) to product.py and adds the missing fields "id" (IDField) and "instance" (ProductInstance) to product.py:TradeProduct. This commit also adds class PayerTradeParty (PayerTradeParty) to party.py and adds the missing field "payer" (PayerTradeParty) to trade.py:TradeSettlement. This commit also removes the unused (and not needed) party.py:LineApplicableTradeTax class. This commit also corrects profile and required tags according to the v2.2 specification as follows: - references.py:LineAdditionalReferencedDocument .name profile COMFORT => BASIC (was COMFORT) - tradelines.py:LineSettlement .trade_tax profile COMFORT => BASIC .period profile COMFORT => BASIC .allowance_charge profile COMFORT => BASIC .monetary_summation profile COMFORT => BASIC .additional_referenced_document profile EXTENDED => COMFORT .accounting_account profile EXTENDED => COMFORT - product.py:ProductCharacteristic .type_code required => optional .description profile EXTENDED => COMFORT .value profile EXTENDED => COMFORT - product.py:ProductClassification .class_code required => optional; profile EXTENDED => COMFORT .value required => optional - product.py:TradeProduct .name profile COMFORT => BASIC .characteristics EXTENDED => COMFORT .classifications EXTENDED => COMFORT .origins EXTENDED => COMFORT - trade.py:TradeSettlement .tax_currency_code profile COMFORT => BASIC .invoicer profile COMFORT => EXTENDED .invoicee profile COMFORT => EXTENDED .payee profile COMFORT => BASIC .allowance_charge profile COMFORT => BASIC .service_charge profile COMFORT => EXTENDED .terms profile COMFORT => BASIC .accounting_account EXTENDED => BASIC - accounting.py:ApplicableTradeTax .exemption_reason COMFORT => BASIC .category_code COMFORT => BASIC .exemption_reason_code EXTENDED => BASIC * fixes profile annotations in TradeAllowanceCharge * SpecifiedTradeAllowanceCharge:ReasonCode in COMFORT
1 parent 4019506 commit cdfd45a

14 files changed

+790
-150
lines changed

README.rst

+8
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ Generating::
6666
doc.trade.settlement.currency_code = "EUR"
6767
doc.trade.settlement.payment_means.type_code = "ZZZ"
6868

69+
doc.trade.agreement.seller.address.country_id = "DE"
70+
doc.trade.agreement.seller.address.country_subdivision = "Bayern"
71+
72+
doc.trade.agreement.seller_order.issue_date_time = datetime.now(timezone.utc)
73+
doc.trade.agreement.buyer_order.issue_date_time = datetime.now(timezone.utc)
74+
doc.trade.settlement.advance_payment.received_date = datetime.now(timezone.utc)
75+
doc.trade.agreement.customer_order.issue_date_time = datetime.now(timezone.utc)
76+
6977
li = LineItem()
7078
li.document.line_id = "1"
7179
li.product.name = "Rainbow"

drafthorse/models/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
NS_RAM = (
55
"urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
66
)
7-
NS_QDT = "urn:un:unece:uncefact:data:standard:QualifiedDataType:10"
7+
NS_QDT = "urn:un:unece:uncefact:data:standard:QualifiedDataType:100"
88
BASIC = "BASIC"
99
COMFORT = "COMFORT"
1010
EXTENDED = "EXTENDED"

drafthorse/models/accounting.py

+15-56
Original file line numberDiff line numberDiff line change
@@ -28,47 +28,6 @@ class Meta:
2828
tag = "BillingSpecifiedPeriod"
2929

3030

31-
class SellerOrderReferencedDocument(Element):
32-
issuer_ID = StringField(NS_RAM, "IssuerAssignedID", profile=COMFORT)
33-
issue_date_time = DateTimeField(
34-
NS_RAM, "FormattedIssueDateTime", required=True, profile=EXTENDED
35-
)
36-
37-
class Meta:
38-
namespace = NS_RAM
39-
tag = "SellerOrderReferencedDocument"
40-
41-
42-
class LineApplicableTradeTax(Element):
43-
calculated_amount = DecimalField(
44-
NS_RAM, "CalculatedAmount", required=True, profile=BASIC, _d="Steuerbetrag"
45-
)
46-
type_code = StringField(
47-
NS_RAM, "TypeCode", required=True, profile=BASIC, _d="Steuerart (Code)"
48-
)
49-
exemption_reason = StringField(
50-
NS_RAM,
51-
"ExemptionReason",
52-
required=False,
53-
profile=COMFORT,
54-
_d="Grund der Steuerbefreiung (Freitext)",
55-
)
56-
category_code = StringField(
57-
NS_RAM,
58-
"CategoryCode",
59-
required=False,
60-
profile=COMFORT,
61-
_d="Steuerkategorie (Wert)",
62-
)
63-
rate_applicable_percent = DecimalField(
64-
NS_RAM, "RateApplicablePercent", required=True, profile=BASIC
65-
)
66-
67-
class Meta:
68-
namespace = NS_RAM
69-
tag = "ApplicableTradeTax"
70-
71-
7231
class ApplicableTradeTax(Element):
7332
calculated_amount = DecimalField(
7433
NS_RAM, "CalculatedAmount", required=True, profile=BASIC, _d="Steuerbetrag"
@@ -80,17 +39,8 @@ class ApplicableTradeTax(Element):
8039
NS_RAM,
8140
"ExemptionReason",
8241
required=False,
83-
profile=COMFORT,
84-
_d="Grund der Steuerbefreiung (Freitext)",
85-
)
86-
tax_point_date = DateTimeField(
87-
NS_RAM, "TaxPointDate", required=False, profile=COMFORT
88-
)
89-
due_date_type_code = StringField(
90-
NS_RAM,
91-
"DueDateTypeCode",
92-
required=False,
9342
profile=BASIC,
43+
_d="Grund der Steuerbefreiung (Freitext)",
9444
)
9545
basis_amount = DecimalField(
9646
NS_RAM,
@@ -117,16 +67,25 @@ class ApplicableTradeTax(Element):
11767
NS_RAM,
11868
"CategoryCode",
11969
required=False,
120-
profile=COMFORT,
70+
profile=BASIC,
12171
_d="Steuerkategorie (Wert)",
12272
)
12373
exemption_reason_code = StringField(
12474
NS_RAM,
12575
"ExemptionReasonCode",
12676
required=False,
127-
profile=EXTENDED,
77+
profile=BASIC,
12878
_d="Grund der Steuerbefreiung (Code)",
12979
)
80+
tax_point_date = DateTimeField(
81+
NS_RAM, "TaxPointDate", required=False, profile=COMFORT
82+
)
83+
due_date_type_code = StringField(
84+
NS_RAM,
85+
"DueDateTypeCode",
86+
required=False,
87+
profile=BASIC,
88+
)
13089
rate_applicable_percent = DecimalField(
13190
NS_RAM, "RateApplicablePercent", required=True, profile=BASIC
13291
)
@@ -254,14 +213,14 @@ class TradeAllowanceCharge(Element):
254213
NS_RAM,
255214
"CalculationPercent",
256215
required=False,
257-
profile=EXTENDED,
216+
profile=COMFORT,
258217
_d="Rabatt in Prozent",
259218
)
260219
basis_amount = DecimalField( # TODO: Should be deprecated?
261220
NS_RAM,
262221
"BasisAmount",
263222
required=False,
264-
profile=EXTENDED,
223+
profile=COMFORT,
265224
_d="Basisbetrag des Rabatts",
266225
)
267226
basis_quantity = QuantityField(
@@ -278,7 +237,7 @@ class TradeAllowanceCharge(Element):
278237
profile=COMFORT,
279238
_d="Betrag des Zu-/Abschlags",
280239
)
281-
reason_code = StringField(NS_RAM, "ReasonCode", required=False, profile=EXTENDED)
240+
reason_code = StringField(NS_RAM, "ReasonCode", required=False, profile=COMFORT)
282241
reason = StringField(NS_RAM, "Reason", required=False, profile=COMFORT)
283242
trade_tax = MultiField(CategoryTradeTax, required=False, profile=COMFORT)
284243

drafthorse/models/elements.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def __setattr__(self, key, value):
8080
if (
8181
not hasattr(self, key)
8282
and not key.startswith("_")
83-
and not key in ("required",)
83+
and key not in ("required",)
8484
):
8585
raise AttributeError(
8686
f"Element {type(self)} has no attribute '{key}'. If you set it, it would not be included in the output."
@@ -301,7 +301,8 @@ def __init__(self, namespace, tag, text="", scheme_id=""):
301301
def to_etree(self):
302302
node = self._etree_node()
303303
node.text = self._text
304-
node.attrib["schemeID"] = self._scheme_id
304+
if self._scheme_id != "":
305+
node.attrib["schemeID"] = self._scheme_id
305306
return node
306307

307308
def from_etree(self, root):
@@ -319,14 +320,17 @@ def __str__(self):
319320

320321

321322
class DateTimeElement(StringElement):
322-
def __init__(self, namespace, tag, value=None, format="102"):
323+
def __init__(
324+
self, namespace, tag, value=None, format="102", date_time_namespace=NS_UDT
325+
):
323326
super().__init__(namespace, tag)
324327
self._value = value
325328
self._format = format
329+
self._date_time_namespace = date_time_namespace
326330

327331
def to_etree(self):
328332
t = self._etree_node()
329-
node = ET.Element("{%s}%s" % (NS_UDT, "DateTimeString"))
333+
node = ET.Element("{%s}%s" % (self._date_time_namespace, "DateTimeString"))
330334
if self._value:
331335
if self._format == "102":
332336
node.text = self._value.strftime("%Y%m%d")
@@ -344,7 +348,7 @@ def to_etree(self):
344348
def from_etree(self, root):
345349
if len(root) != 1:
346350
raise TypeError("Date containers should have one child")
347-
if root[0].tag != "{%s}%s" % (NS_UDT, "DateTimeString"):
351+
if root[0].tag != "{%s}%s" % (self._date_time_namespace, "DateTimeString"):
348352
raise TypeError("Tag %s not recognized" % root[0].tag)
349353
self._format = root[0].attrib["format"]
350354
if self._format == "102":

drafthorse/models/fields.py

+26-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from decimal import Decimal
22

3-
from . import BASIC
3+
from . import BASIC, NS_UDT
44
from .container import (
55
Container,
66
CurrencyContainer,
@@ -119,10 +119,16 @@ def __set__(self, instance, value):
119119
if instance._data.get(self.name, None) is None:
120120
instance._data[self.name] = self.initialize()
121121

122-
if not isinstance(value, (tuple, list)):
123-
raise TypeError("Please pass a 2-tuple of including scheme ID and ID.")
124-
instance._data[self.name]._text = value[1]
125-
instance._data[self.name]._scheme_id = value[0]
122+
if isinstance(value, (tuple, list)):
123+
if len(value) == 2:
124+
instance._data[self.name]._text = value[1]
125+
instance._data[self.name]._scheme_id = value[0]
126+
else:
127+
raise TypeError(
128+
"Please pass a 2-tuple of including scheme ID and ID, or just an ID."
129+
)
130+
else:
131+
instance._data[self.name]._text = value
126132

127133

128134
class CurrencyField(Field):
@@ -208,7 +214,9 @@ def __set__(self, instance, value):
208214
instance._data[self.name] = self.initialize()
209215

210216
if not isinstance(value, (tuple, list)):
211-
raise TypeError("Please pass a 2-tuple of including amount and unit code.")
217+
raise TypeError(
218+
"Please pass a 3-tuple of mimeCode, filename and base64-encoded binary."
219+
)
212220
instance._data[self.name]._text = value[2]
213221
instance._data[self.name]._mime_code = value[0]
214222
instance._data[self.name]._filename = value[1]
@@ -238,21 +246,31 @@ def initialize(self):
238246

239247
class DateTimeField(Field):
240248
def __init__(
241-
self, namespace, tag, default=False, required=False, profile=BASIC, _d=None
249+
self,
250+
namespace,
251+
tag,
252+
default=False,
253+
required=False,
254+
profile=BASIC,
255+
_d=None,
256+
date_time_namespace=NS_UDT,
242257
):
243258
from .elements import DateTimeElement
244259

245260
super().__init__(DateTimeElement, default, required, profile, _d)
246261
self.namespace = namespace
247262
self.tag = tag
263+
self._date_time_namespace = date_time_namespace
248264

249265
def __set__(self, instance, value):
250266
if instance._data.get(self.name, None) is None:
251267
instance._data[self.name] = self.initialize()
252268
instance._data[self.name]._value = value
253269

254270
def initialize(self):
255-
return self.cls(self.namespace, self.tag)
271+
return self.cls(
272+
self.namespace, self.tag, date_time_namespace=self._date_time_namespace
273+
)
256274

257275

258276
class DirectDateTimeField(Field):

drafthorse/models/party.py

+6
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ class Meta:
117117
tag = "PayeeTradeParty"
118118

119119

120+
class PayerTradeParty(TradeParty):
121+
class Meta:
122+
namespace = NS_RAM
123+
tag = "PayerTradeParty"
124+
125+
120126
class InvoicerTradeParty(TradeParty):
121127
class Meta:
122128
namespace = NS_RAM

drafthorse/models/product.py

+23-14
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@ class ProductCharacteristic(Element):
1313
type_code = StringField(
1414
NS_RAM,
1515
"TypeCode",
16-
required=True,
16+
required=False,
1717
profile=EXTENDED,
1818
_d="Art der Produkteigenschaft",
1919
)
20-
description = StringField(NS_RAM, "Description", required=True, profile=EXTENDED)
20+
description = StringField(NS_RAM, "Description", required=True, profile=COMFORT)
2121
value_measure = QuantityField(
2222
NS_RAM,
2323
"ValueMeasure",
2424
required=False,
2525
profile=EXTENDED,
2626
_d="Numerische Messgröße",
2727
)
28-
value = StringField(NS_RAM, "Value", required=False, profile=EXTENDED)
28+
value = StringField(NS_RAM, "Value", required=False, profile=COMFORT)
2929

3030
class Meta:
3131
namespace = NS_RAM
@@ -34,15 +34,26 @@ class Meta:
3434

3535
class ProductClassification(Element):
3636
class_code = ClassificationField(
37-
NS_RAM, "ClassCode", required=True, profile=EXTENDED
37+
NS_RAM, "ClassCode", required=False, profile=COMFORT
3838
)
39-
value = StringField(NS_RAM, "ClassName", required=True, profile=EXTENDED)
39+
value = StringField(NS_RAM, "ClassName", required=False, profile=EXTENDED)
4040

4141
class Meta:
4242
namespace = NS_RAM
4343
tag = "DesignatedProductClassification"
4444

4545

46+
class ProductInstance(Element):
47+
batch_id = IDField(NS_RAM, "BatchID", required=False, profile=EXTENDED)
48+
serial_id = StringField(
49+
NS_RAM, "SupplierAssignedSerialID", required=False, profile=EXTENDED
50+
)
51+
52+
class Meta:
53+
namespace = NS_RAM
54+
tag = "IndividualTradeProductInstance"
55+
56+
4657
class OriginCountry(Element):
4758
id = StringField(
4859
NS_RAM, "ID", required=True, profile=EXTENDED, _d="Land der Produktherkunft"
@@ -73,22 +84,20 @@ class Meta:
7384

7485

7586
class TradeProduct(Element):
76-
global_id = IDField(NS_RAM, "GlobalID", required=False, profile=COMFORT)
87+
id = IDField(NS_RAM, "ID", required=False, profile=EXTENDED)
88+
global_id = IDField(NS_RAM, "GlobalID", required=False)
7789
seller_assigned_id = StringField(
7890
NS_RAM, "SellerAssignedID", required=False, profile=COMFORT
7991
)
8092
buyer_assigned_id = StringField(
8193
NS_RAM, "BuyerAssignedID", required=False, profile=COMFORT
8294
)
83-
name = StringField(NS_RAM, "Name", required=False, profile=COMFORT)
95+
name = StringField(NS_RAM, "Name", required=False)
8496
description = StringField(NS_RAM, "Description", required=False, profile=COMFORT)
85-
characteristics = MultiField(
86-
ProductCharacteristic, required=False, profile=EXTENDED
87-
)
88-
classifications = MultiField(
89-
ProductClassification, required=False, profile=EXTENDED
90-
)
91-
origins = MultiField(OriginCountry, required=False, profile=EXTENDED)
97+
characteristics = MultiField(ProductCharacteristic, required=False, profile=COMFORT)
98+
classifications = MultiField(ProductClassification, required=False, profile=COMFORT)
99+
instance = MultiField(ProductInstance, required=False, profile=EXTENDED)
100+
origins = MultiField(OriginCountry, required=False, profile=COMFORT)
92101
included_products = MultiField(ReferencedProduct, required=False, profile=EXTENDED)
93102

94103
class Meta:

0 commit comments

Comments
 (0)