From: Raphael Michel Date: Mon, 15 Oct 2018 20:20:24 +0000 (+0200) Subject: Add simple parser X-Git-Tag: 1.0.0~20 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=25e032e4fb35cb491d9f57406a99f22540cc4aee;p=thirdparty%2Fpython-drafthorse.git Add simple parser --- diff --git a/demo.py b/demo.py index b37994c..d7fe3b6 100644 --- a/demo.py +++ b/demo.py @@ -18,3 +18,10 @@ doc.header.notes.add(n) doc.trade.agreement.seller.name = "Lieferant GmbH" print(prettify(doc.serialize())) + +samplexml = open("sample.xml", "rb").read() +doc = Document.parse(samplexml) + +print(doc.header.name) +print(doc.trade.agreement.seller.name) +print([str(a.content) for a in doc.header.notes.children]) \ No newline at end of file diff --git a/drafthorse/models/accounting.py b/drafthorse/models/accounting.py index af0cce6..56a452c 100644 --- a/drafthorse/models/accounting.py +++ b/drafthorse/models/accounting.py @@ -31,6 +31,10 @@ class ApplicableTradeTax(LineApplicableTradeTax): required=False, profile=EXTENDED, _d="Gesamtbetrag Zu- und Abschläge des Steuersatzes") + class Meta: + namespace = NS_RAM + tag = "ApplicableTradeTax" + class AccountingAccount(Element): id = StringField(NS_RAM, "ID", required=True, profile=EXTENDED, _d="Buchungsreferenz") diff --git a/drafthorse/models/container.py b/drafthorse/models/container.py new file mode 100644 index 0000000..58f073d --- /dev/null +++ b/drafthorse/models/container.py @@ -0,0 +1,90 @@ +class Container: + def __init__(self, child_type): + super().__init__() + self.children = [] + self.child_type = child_type + + def add(self, item): + if isinstance(self.child_type, type) and not isinstance(item, self.child_type): + raise TypeError("{} is not of type {}".format(item, self.child_type)) + self.children.append(item) + + def append_to(self, node): + for child in self.children: + child.append_to(node) + + def get_tag(self): + return '{%s}%s' % (self.child_type.Meta.namespace, self.child_type.Meta.tag) + + def empty_element(self): + return self.child_type() + + def add_from_etree(self, root): + childel = self.empty_element() + childel.from_etree(root) + self.add(childel) + + +class SimpleContainer(Container): + def __init__(self, child_type, namespace, tag): + super().__init__(child_type) + self.namespace = namespace + self.tag = tag + + def get_tag(self): + return '{%s}%s' % (self.namespace, self.tag) + + def empty_element(self): + raise NotImplemented() + + def set_element(self, el, child): + raise NotImplemented() + + def append_to(self, node): + for child in self.children: + el = self.empty_element() + self.set_element(el, child) + el.append_to(node) + + def add_from_etree(self, root): + self.add(root.text) + + +class CurrencyContainer(SimpleContainer): + + def empty_element(self): + from .elements import CurrencyElement + return CurrencyElement(namespace=self.namespace, tag=self.tag) + + def set_element(self, el, child): + el.amount = child[0] + el.currency = child[1] + + def add_from_etree(self, root): + self.add((root.text, root.attrib['currencyID'])) + + +class IDContainer(SimpleContainer): + def empty_element(self): + from .elements import IDElement + return IDElement(namespace=self.namespace, tag=self.tag) + + def set_element(self, el, child): + el.text = child[1] + el.scheme_id = child[0] + + def add_from_etree(self, root): + self.add((root.attrib['schemeID'], root.text)) + + +class StringContainer(SimpleContainer): + + def empty_element(self): + from .elements import StringElement + return StringElement(namespace=self.namespace, tag=self.tag) + + def set_element(self, el, child): + el.text = child + + def add_from_etree(self, root): + self.add(root.text) diff --git a/drafthorse/models/delivery.py b/drafthorse/models/delivery.py index be6d9c7..8bec84d 100644 --- a/drafthorse/models/delivery.py +++ b/drafthorse/models/delivery.py @@ -8,7 +8,7 @@ from .references import (DeliveryNoteReferencedDocument, class SupplyChainEvent(Element): - occurrence = DateTimeField(NS_RAM, "OccurenceDateTime", + occurrence = DateTimeField(NS_RAM, "OccurrenceDateTime", required=False, profile=BASIC, _d="Tatsächlicher Lieferungszeitpunkt") diff --git a/drafthorse/models/elements.py b/drafthorse/models/elements.py index 18a339b..2d9101f 100644 --- a/drafthorse/models/elements.py +++ b/drafthorse/models/elements.py @@ -1,4 +1,8 @@ import sys +from decimal import Decimal + +from datetime import date, datetime + import xml.etree.cElementTree as ET from collections import OrderedDict @@ -6,12 +10,13 @@ from drafthorse.utils import validate_xml from . import NS_UDT from .fields import Field +from .container import Container class BaseElementMeta(type): def __new__(mcls, name, bases, attrs): cls = super(BaseElementMeta, mcls).__new__(mcls, name, bases, attrs) - fields = [] + fields = list(cls._fields) if hasattr(cls, '_fields') else [] for attr, obj in attrs.items(): if isinstance(obj, Field): if sys.version_info < (3, 6): @@ -30,8 +35,8 @@ class Element(metaclass=BaseElementMeta): setattr(self, k, v) def _etree_node(self): - node = ET.Element("{%s}%s" % (self.Meta.namespace, self.Meta.tag)) - if hasattr(self.Meta, 'attributes'): + node = ET.Element(self.get_tag()) + if hasattr(self, 'Meta') and hasattr(self.Meta, 'attributes'): for k, v in self.Meta.attributes.items(): node.set(k, v) return node @@ -43,6 +48,9 @@ class Element(metaclass=BaseElementMeta): v.append_to(node) return node + def get_tag(self): + return "{%s}%s" % (self.Meta.namespace, self.Meta.tag) + def append_to(self, node): node.append(self.to_etree()) @@ -51,6 +59,30 @@ class Element(metaclass=BaseElementMeta): validate_xml(xmlout=xml, schema="ZUGFeRD1p0") return xml + def from_etree(self, root): + if hasattr(self, 'Meta') and hasattr(self.Meta, 'namespace') and root.tag != "{%s}%s" % (self.Meta.namespace, self.Meta.tag): + raise TypeError("Invalid XML, found tag {} where {} was expected".format(root.tag, "{%s}%s" % (self.Meta.namespace, self.Meta.tag))) + field_index = {} + for field in self._fields: + element = getattr(self, field.name) + field_index[element.get_tag()] = (field.name, element) + for child in root: + if child.tag in field_index: + name, childel = field_index[child.tag] + if isinstance(getattr(self, name), Container): + getattr(self, name).add_from_etree(child) + else: + getattr(self, name).from_etree(child) + else: + raise TypeError("Unknown element {}".format(child.tag)) + return self + + @classmethod + def parse(cls, xmlinput): + from lxml import etree + root = etree.fromstring(xmlinput) + return cls().from_etree(root) + class StringElement(Element): def __init__(self, namespace, tag, text=""): @@ -59,14 +91,21 @@ class StringElement(Element): self.tag = tag self.text = text - def _etree_node(self): - return ET.Element("{%s}%s" % (self.namespace, self.tag)) + def __str__(self): + return self.text + + def get_tag(self): + return "{%s}%s" % (self.namespace, self.tag) def to_etree(self): node = self._etree_node() node.text = self.text return node + def from_etree(self, root): + self.text = root.text + return self + class DecimalElement(StringElement): def __init__(self, namespace, tag, value=0): @@ -78,6 +117,13 @@ class DecimalElement(StringElement): node.text = str(self.value) return node + def __str__(self): + return self.value + + def from_etree(self, root): + self.value = Decimal(root.text) + return self + class QuantityElement(StringElement): def __init__(self, namespace, tag, amount="", unit_code=""): @@ -91,6 +137,14 @@ class QuantityElement(StringElement): node.attrib["unitCode"] = self.unit_code return node + def __str__(self): + return "{} {}".format(self.amount, self.unit_code) + + def from_etree(self, root): + self.amount = Decimal(root.text) + self.unit_code = root.attrib['unitCode'] + return self + class CurrencyElement(StringElement): def __init__(self, namespace, tag, amount="", currency="EUR"): @@ -104,6 +158,14 @@ class CurrencyElement(StringElement): node.attrib["currencyID"] = self.currency return node + def from_etree(self, root): + self.amount = Decimal(root.text) + self.currency = root.attrib['currencyID'] + return self + + def __str__(self): + return "{} {}".format(self.amount, self.currency) + class ClassificationElement(StringElement): def __init__(self, namespace, tag, text="", list_id="", list_version_id=""): @@ -119,6 +181,15 @@ class ClassificationElement(StringElement): node.attrib['listVersionID'] = self.list_version_id return node + def from_etree(self, root): + self.text = Decimal(root.text) + self.list_id = root.attrib['listID'] + self.list_version_id = root.attrib['listVersionID'] + return self + + def __str__(self): + return "{} ({} {})".format(self.text, self.list_id, self.list_version_id) + class AgencyIDElement(StringElement): def __init__(self, namespace, tag, text="", scheme_id=""): @@ -132,6 +203,14 @@ class AgencyIDElement(StringElement): node.attrib['schemeAgencyID'] = self.scheme_id return node + def from_etree(self, root): + self.text = Decimal(root.text) + self.scheme_id = root.attrib['schemeAgencyID'] + return self + + def __str__(self): + return "{} ({})".format(self.text, self.scheme_id) + class IDElement(StringElement): def __init__(self, namespace, tag, text="", scheme_id=""): @@ -145,43 +224,57 @@ class IDElement(StringElement): node.attrib['schemeID'] = self.scheme_id return node + def from_etree(self, root): + self.text = root.text + self.scheme_id = root.attrib['schemeID'] + return self -class DateTimeElement(Element): - def __init__(self, namespace, tag, value=None): - super().__init__() - self.value = None - self.namespace = namespace - self.tag = tag + def __str__(self): + return "{} ({})".format(self.text, self.scheme_id) + + +class DateTimeElement(StringElement): + def __init__(self, namespace, tag, value=None, format='102'): + super().__init__(namespace, tag) + self.value = value + self.format = format def to_etree(self): - t = ET.Element("{%s}%s" % (self.namespace, self.tag)) - node = self._etree_node() + t = self._etree_node() + node = ET.Element("{%s}%s" % (NS_UDT, "DateTimeString")) node.text = self.value.strftime("%Y%m%d") + node.attrib['format'] = self.format t.append(node) return t - class Meta: - namespace = NS_UDT - tag = "DateTimeString" - attributes = { - "format": "102" - } + 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"): + raise TypeError("Tag %s not recognized" % root[0].tag) + if root[0].attrib['format'] != '102': + raise TypeError("Date format %s cannot be parsed" % root[0].attrib['format']) + self.value = datetime.strptime(root[0].text, '%Y%m%d').date() + self.format = root[0].attrib['format'] + return self + + def __str__(self): + return "{}".format(self.value) -class IndicatorElement(Element): +class IndicatorElement(StringElement): def __init__(self, namespace, tag, value=None): - super().__init__() - self.value = None - self.namespace = namespace - self.tag = tag + super().__init__(namespace, tag) + self.value = value + + def get_tag(self): + return "{%s}%s" % (self.namespace, self.tag) def to_etree(self): - t = ET.Element("{%s}%s" % (self.namespace, self.tag)) - node = self._etree_node() - node.text = str(self.value).lower() + t = self._etree_node() + node = ET.Element("{%s}%s" % (NS_UDT, "Indicator")) + node.text = self.value t.append(node) - return t - class Meta: - namespace = NS_UDT - tag = "Indicator" + def __str__(self): + return "{}".format(self.value) diff --git a/drafthorse/models/fields.py b/drafthorse/models/fields.py index 1119b4e..0da21fe 100644 --- a/drafthorse/models/fields.py +++ b/drafthorse/models/fields.py @@ -1,3 +1,4 @@ +from .container import Container, StringContainer, CurrencyContainer, IDContainer from . import BASIC @@ -188,22 +189,6 @@ class DateTimeField(Field): return self.cls(self.namespace, self.tag) -class Container: - def __init__(self, child_type): - super().__init__() - self.children = [] - self.child_type = child_type - - def add(self, item): - if isinstance(self.child_type, type) and not isinstance(item, self.child_type): - raise TypeError("{} is not of type {}".format(item, self.child_type)) - self.children.append(item) - - def append_to(self, node): - for child in self.children: - child.append_to(node) - - class MultiField(Field): def __init__(self, inner_type, default=False, required=False, profile=BASIC, _d=None): super().__init__(Container, default, required, profile, _d) @@ -213,19 +198,6 @@ class MultiField(Field): return self.cls(child_type=self.inner_type) -class StringContainer(Container): - def __init__(self, child_type, namespace, tag): - super().__init__(child_type) - self.namespace = namespace - self.tag = tag - - def append_to(self, node): - for child in self.children: - from .elements import StringElement - cnode = StringElement(namespace=self.namespace, tag=self.tag, text=child) - cnode.append_to(node) - - class MultiStringField(Field): def __init__(self, namespace, tag, default=False, required=False, profile=BASIC, _d=None): super().__init__(StringContainer, default, required, profile, _d) @@ -236,19 +208,6 @@ class MultiStringField(Field): return self.cls(child_type=str, namespace=self.namespace, tag=self.tag) -class CurrencyContainer(Container): - def __init__(self, child_type, namespace, tag): - super().__init__(child_type) - self.namespace = namespace - self.tag = tag - - def append_to(self, node): - for child in self.children: - from .elements import CurrencyElement - cnode = CurrencyElement(namespace=self.namespace, tag=self.tag, amount=child[0], currency=child[1]) - cnode.append_to(node) - - class MultiCurrencyField(Field): def __init__(self, namespace, tag, default=False, required=False, profile=BASIC, _d=None): super().__init__(CurrencyContainer, default, required, profile, _d) @@ -259,19 +218,6 @@ class MultiCurrencyField(Field): return self.cls(child_type=(tuple, list), namespace=self.namespace, tag=self.tag) -class IDContainer(Container): - def __init__(self, child_type, namespace, tag): - super().__init__(child_type) - self.namespace = namespace - self.tag = tag - - def append_to(self, node): - for child in self.children: - from .elements import IDElement - cnode = IDElement(namespace=self.namespace, tag=self.tag, scheme_id=child[0], text=child[1]) - cnode.append_to(node) - - class MultiIDField(Field): def __init__(self, namespace, tag, default=False, required=False, profile=BASIC, _d=None): super().__init__(IDContainer, default, required, profile, _d) diff --git a/drafthorse/models/payment.py b/drafthorse/models/payment.py index c6c6232..bb6bcc6 100644 --- a/drafthorse/models/payment.py +++ b/drafthorse/models/payment.py @@ -70,6 +70,10 @@ class PaymentPenaltyTerms(Element): actual_amount = CurrencyField(NS_RAM, "ActualPenaltyAmount", required=False, profile=EXTENDED, _d="Betrag des Zahlungszuschlags") + class Meta: + namespace = NS_RAM + tag = "ApplicableTradePaymentPenaltyTerms" + class PaymentDiscountTerms(Element): basis_date_time = DateTimeField(NS_RAM, "BasisDateTime", required=False, @@ -83,6 +87,10 @@ class PaymentDiscountTerms(Element): actual_amount = CurrencyField(NS_RAM, "ActualDiscountAmount", required=False, profile=EXTENDED, _d="Betrag des Zahlungsabschlags") + class Meta: + namespace = NS_RAM + tag = "ApplicableTradePaymentDiscountTerms" + class PaymentTerms(Element): description = StringField(NS_RAM, "Description", required=True, profile=COMFORT, diff --git a/drafthorse/models/product.py b/drafthorse/models/product.py index 62c0126..d93d053 100644 --- a/drafthorse/models/product.py +++ b/drafthorse/models/product.py @@ -12,17 +12,29 @@ class ProductCharacteristic(Element): profile=EXTENDED, _d="Numerische Messgröße") value = StringField(NS_RAM, "Value", required=False, profile=EXTENDED) + class Meta: + namespace = NS_RAM + tag = "ApplicableProductCharacteristic" + class ProductClassification(Element): class_code = ClassificationField(NS_RAM, "ClassCode", required=True, profile=EXTENDED) value = StringField(NS_RAM, "ClassName", required=True, profile=EXTENDED) + class Meta: + namespace = NS_RAM + tag = "DesignatedProductClassification" + class OriginCountry(Element): id = StringField(NS_RAM, "ID", required=True, profile=EXTENDED, _d="Land der Produktherkunft") + class Meta: + namespace = NS_RAM + tag = "OriginTradeCountry" + class ReferencedProduct(Element): name = StringField(NS_RAM, "Name", required=False, profile=EXTENDED) @@ -35,6 +47,10 @@ class ReferencedProduct(Element): unit_quantity = QuantityField(NS_RAM, "UnitQuantity", required=False, profile=EXTENDED) + class Meta: + namespace = NS_RAM + tag = "IncludedReferencedProduct" + class TradeProduct(Element): name = StringField(NS_RAM, "Name", required=False, profile=COMFORT) @@ -48,3 +64,7 @@ class TradeProduct(Element): classifications = MultiField(ProductClassification, required=False, profile=EXTENDED) origins = MultiField(OriginCountry, required=False, profile=EXTENDED) included_products = MultiField(ReferencedProduct, required=False, profile=EXTENDED) + + class Meta: + namespace = NS_RAM + tag = "SpecifiedTradeProduct" diff --git a/drafthorse/models/tradelines.py b/drafthorse/models/tradelines.py index ad0e0db..d4d3d0b 100644 --- a/drafthorse/models/tradelines.py +++ b/drafthorse/models/tradelines.py @@ -1,8 +1,7 @@ -from drafthorse.models.delivery import SupplyChainEvent - from . import BASIC, COMFORT, EXTENDED, NS_RAM from .accounting import (AccountingAccount, BillingSpecifiedPeriod, - TradeAllowanceCharge) + TradeAllowanceCharge, ApplicableTradeTax) +from .delivery import SupplyChainEvent from .elements import Element from .fields import (CurrencyField, Field, MultiField, QuantityField, StringField) @@ -104,7 +103,7 @@ class LineSummation(Element): class LineSettlement(Element): - trade_tax = Field(LineAdditionalReferencedDocument, required=False, profile=COMFORT) + trade_tax = Field(ApplicableTradeTax, required=False, profile=COMFORT) period = Field(BillingSpecifiedPeriod, required=False, profile=EXTENDED) accounting_account = Field(AccountingAccount, required=False, profile=EXTENDED, _d="Kostenstelle")