Skip to content

Commit

Permalink
fixes Issue #20, Issue #22, Issue #26 and Issue #29. It replaces PR #21
Browse files Browse the repository at this point in the history
… and PR #23 and contains alignments to the v2.2 specification (#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
  • Loading branch information
MAKOMO authored Mar 10, 2024
1 parent 4019506 commit cdfd45a
Show file tree
Hide file tree
Showing 14 changed files with 790 additions and 150 deletions.
8 changes: 8 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ Generating::
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_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"
Expand Down
2 changes: 1 addition & 1 deletion drafthorse/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
NS_RAM = (
"urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
)
NS_QDT = "urn:un:unece:uncefact:data:standard:QualifiedDataType:10"
NS_QDT = "urn:un:unece:uncefact:data:standard:QualifiedDataType:100"
BASIC = "BASIC"
COMFORT = "COMFORT"
EXTENDED = "EXTENDED"
71 changes: 15 additions & 56 deletions drafthorse/models/accounting.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,47 +28,6 @@ class Meta:
tag = "BillingSpecifiedPeriod"


class SellerOrderReferencedDocument(Element):
issuer_ID = StringField(NS_RAM, "IssuerAssignedID", profile=COMFORT)
issue_date_time = DateTimeField(
NS_RAM, "FormattedIssueDateTime", required=True, profile=EXTENDED
)

class Meta:
namespace = NS_RAM
tag = "SellerOrderReferencedDocument"


class LineApplicableTradeTax(Element):
calculated_amount = DecimalField(
NS_RAM, "CalculatedAmount", required=True, profile=BASIC, _d="Steuerbetrag"
)
type_code = StringField(
NS_RAM, "TypeCode", required=True, profile=BASIC, _d="Steuerart (Code)"
)
exemption_reason = StringField(
NS_RAM,
"ExemptionReason",
required=False,
profile=COMFORT,
_d="Grund der Steuerbefreiung (Freitext)",
)
category_code = StringField(
NS_RAM,
"CategoryCode",
required=False,
profile=COMFORT,
_d="Steuerkategorie (Wert)",
)
rate_applicable_percent = DecimalField(
NS_RAM, "RateApplicablePercent", required=True, profile=BASIC
)

class Meta:
namespace = NS_RAM
tag = "ApplicableTradeTax"


class ApplicableTradeTax(Element):
calculated_amount = DecimalField(
NS_RAM, "CalculatedAmount", required=True, profile=BASIC, _d="Steuerbetrag"
Expand All @@ -80,17 +39,8 @@ class ApplicableTradeTax(Element):
NS_RAM,
"ExemptionReason",
required=False,
profile=COMFORT,
_d="Grund der Steuerbefreiung (Freitext)",
)
tax_point_date = DateTimeField(
NS_RAM, "TaxPointDate", required=False, profile=COMFORT
)
due_date_type_code = StringField(
NS_RAM,
"DueDateTypeCode",
required=False,
profile=BASIC,
_d="Grund der Steuerbefreiung (Freitext)",
)
basis_amount = DecimalField(
NS_RAM,
Expand All @@ -117,16 +67,25 @@ class ApplicableTradeTax(Element):
NS_RAM,
"CategoryCode",
required=False,
profile=COMFORT,
profile=BASIC,
_d="Steuerkategorie (Wert)",
)
exemption_reason_code = StringField(
NS_RAM,
"ExemptionReasonCode",
required=False,
profile=EXTENDED,
profile=BASIC,
_d="Grund der Steuerbefreiung (Code)",
)
tax_point_date = DateTimeField(
NS_RAM, "TaxPointDate", required=False, profile=COMFORT
)
due_date_type_code = StringField(
NS_RAM,
"DueDateTypeCode",
required=False,
profile=BASIC,
)
rate_applicable_percent = DecimalField(
NS_RAM, "RateApplicablePercent", required=True, profile=BASIC
)
Expand Down Expand Up @@ -254,14 +213,14 @@ class TradeAllowanceCharge(Element):
NS_RAM,
"CalculationPercent",
required=False,
profile=EXTENDED,
profile=COMFORT,
_d="Rabatt in Prozent",
)
basis_amount = DecimalField( # TODO: Should be deprecated?
NS_RAM,
"BasisAmount",
required=False,
profile=EXTENDED,
profile=COMFORT,
_d="Basisbetrag des Rabatts",
)
basis_quantity = QuantityField(
Expand All @@ -278,7 +237,7 @@ class TradeAllowanceCharge(Element):
profile=COMFORT,
_d="Betrag des Zu-/Abschlags",
)
reason_code = StringField(NS_RAM, "ReasonCode", required=False, profile=EXTENDED)
reason_code = StringField(NS_RAM, "ReasonCode", required=False, profile=COMFORT)
reason = StringField(NS_RAM, "Reason", required=False, profile=COMFORT)
trade_tax = MultiField(CategoryTradeTax, required=False, profile=COMFORT)

Expand Down
14 changes: 9 additions & 5 deletions drafthorse/models/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def __setattr__(self, key, value):
if (
not hasattr(self, key)
and not key.startswith("_")
and not key in ("required",)
and key not in ("required",)
):
raise AttributeError(
f"Element {type(self)} has no attribute '{key}'. If you set it, it would not be included in the output."
Expand Down Expand Up @@ -301,7 +301,8 @@ def __init__(self, namespace, tag, text="", scheme_id=""):
def to_etree(self):
node = self._etree_node()
node.text = self._text
node.attrib["schemeID"] = self._scheme_id
if self._scheme_id != "":
node.attrib["schemeID"] = self._scheme_id
return node

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


class DateTimeElement(StringElement):
def __init__(self, namespace, tag, value=None, format="102"):
def __init__(
self, namespace, tag, value=None, format="102", date_time_namespace=NS_UDT
):
super().__init__(namespace, tag)
self._value = value
self._format = format
self._date_time_namespace = date_time_namespace

def to_etree(self):
t = self._etree_node()
node = ET.Element("{%s}%s" % (NS_UDT, "DateTimeString"))
node = ET.Element("{%s}%s" % (self._date_time_namespace, "DateTimeString"))
if self._value:
if self._format == "102":
node.text = self._value.strftime("%Y%m%d")
Expand All @@ -344,7 +348,7 @@ def to_etree(self):
def from_etree(self, root):
if len(root) != 1:
raise TypeError("Date containers should have one child")
if root[0].tag != "{%s}%s" % (NS_UDT, "DateTimeString"):
if root[0].tag != "{%s}%s" % (self._date_time_namespace, "DateTimeString"):
raise TypeError("Tag %s not recognized" % root[0].tag)
self._format = root[0].attrib["format"]
if self._format == "102":
Expand Down
34 changes: 26 additions & 8 deletions drafthorse/models/fields.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from decimal import Decimal

from . import BASIC
from . import BASIC, NS_UDT
from .container import (
Container,
CurrencyContainer,
Expand Down Expand Up @@ -119,10 +119,16 @@ def __set__(self, instance, value):
if instance._data.get(self.name, None) is None:
instance._data[self.name] = self.initialize()

if not isinstance(value, (tuple, list)):
raise TypeError("Please pass a 2-tuple of including scheme ID and ID.")
instance._data[self.name]._text = value[1]
instance._data[self.name]._scheme_id = value[0]
if isinstance(value, (tuple, list)):
if len(value) == 2:
instance._data[self.name]._text = value[1]
instance._data[self.name]._scheme_id = value[0]
else:
raise TypeError(
"Please pass a 2-tuple of including scheme ID and ID, or just an ID."
)
else:
instance._data[self.name]._text = value


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

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

class DateTimeField(Field):
def __init__(
self, namespace, tag, default=False, required=False, profile=BASIC, _d=None
self,
namespace,
tag,
default=False,
required=False,
profile=BASIC,
_d=None,
date_time_namespace=NS_UDT,
):
from .elements import DateTimeElement

super().__init__(DateTimeElement, default, required, profile, _d)
self.namespace = namespace
self.tag = tag
self._date_time_namespace = date_time_namespace

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

def initialize(self):
return self.cls(self.namespace, self.tag)
return self.cls(
self.namespace, self.tag, date_time_namespace=self._date_time_namespace
)


class DirectDateTimeField(Field):
Expand Down
6 changes: 6 additions & 0 deletions drafthorse/models/party.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ class Meta:
tag = "PayeeTradeParty"


class PayerTradeParty(TradeParty):
class Meta:
namespace = NS_RAM
tag = "PayerTradeParty"


class InvoicerTradeParty(TradeParty):
class Meta:
namespace = NS_RAM
Expand Down
37 changes: 23 additions & 14 deletions drafthorse/models/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ class ProductCharacteristic(Element):
type_code = StringField(
NS_RAM,
"TypeCode",
required=True,
required=False,
profile=EXTENDED,
_d="Art der Produkteigenschaft",
)
description = StringField(NS_RAM, "Description", required=True, profile=EXTENDED)
description = StringField(NS_RAM, "Description", required=True, profile=COMFORT)
value_measure = QuantityField(
NS_RAM,
"ValueMeasure",
required=False,
profile=EXTENDED,
_d="Numerische Messgröße",
)
value = StringField(NS_RAM, "Value", required=False, profile=EXTENDED)
value = StringField(NS_RAM, "Value", required=False, profile=COMFORT)

class Meta:
namespace = NS_RAM
Expand All @@ -34,15 +34,26 @@ class Meta:

class ProductClassification(Element):
class_code = ClassificationField(
NS_RAM, "ClassCode", required=True, profile=EXTENDED
NS_RAM, "ClassCode", required=False, profile=COMFORT
)
value = StringField(NS_RAM, "ClassName", required=True, profile=EXTENDED)
value = StringField(NS_RAM, "ClassName", required=False, profile=EXTENDED)

class Meta:
namespace = NS_RAM
tag = "DesignatedProductClassification"


class ProductInstance(Element):
batch_id = IDField(NS_RAM, "BatchID", required=False, profile=EXTENDED)
serial_id = StringField(
NS_RAM, "SupplierAssignedSerialID", required=False, profile=EXTENDED
)

class Meta:
namespace = NS_RAM
tag = "IndividualTradeProductInstance"


class OriginCountry(Element):
id = StringField(
NS_RAM, "ID", required=True, profile=EXTENDED, _d="Land der Produktherkunft"
Expand Down Expand Up @@ -73,22 +84,20 @@ class Meta:


class TradeProduct(Element):
global_id = IDField(NS_RAM, "GlobalID", required=False, profile=COMFORT)
id = IDField(NS_RAM, "ID", required=False, profile=EXTENDED)
global_id = IDField(NS_RAM, "GlobalID", required=False)
seller_assigned_id = StringField(
NS_RAM, "SellerAssignedID", required=False, profile=COMFORT
)
buyer_assigned_id = StringField(
NS_RAM, "BuyerAssignedID", required=False, profile=COMFORT
)
name = StringField(NS_RAM, "Name", required=False, profile=COMFORT)
name = StringField(NS_RAM, "Name", required=False)
description = StringField(NS_RAM, "Description", required=False, profile=COMFORT)
characteristics = MultiField(
ProductCharacteristic, required=False, profile=EXTENDED
)
classifications = MultiField(
ProductClassification, required=False, profile=EXTENDED
)
origins = MultiField(OriginCountry, required=False, profile=EXTENDED)
characteristics = MultiField(ProductCharacteristic, required=False, profile=COMFORT)
classifications = MultiField(ProductClassification, required=False, profile=COMFORT)
instance = MultiField(ProductInstance, required=False, profile=EXTENDED)
origins = MultiField(OriginCountry, required=False, profile=COMFORT)
included_products = MultiField(ReferencedProduct, required=False, profile=EXTENDED)

class Meta:
Expand Down
Loading

0 comments on commit cdfd45a

Please sign in to comment.