]> git.ipfire.org Git - thirdparty/python-drafthorse.git/commitdiff
Add simple parser
authorRaphael Michel <mail@raphaelmichel.de>
Mon, 15 Oct 2018 20:20:24 +0000 (22:20 +0200)
committerRaphael Michel <mail@raphaelmichel.de>
Mon, 15 Oct 2018 20:20:24 +0000 (22:20 +0200)
demo.py
drafthorse/models/accounting.py
drafthorse/models/container.py [new file with mode: 0644]
drafthorse/models/delivery.py
drafthorse/models/elements.py
drafthorse/models/fields.py
drafthorse/models/payment.py
drafthorse/models/product.py
drafthorse/models/tradelines.py

diff --git a/demo.py b/demo.py
index b37994c04aafd55c6c9f7e615de47352e8f5b387..d7fe3b63a425d9f18379f9aae2ae6099502d8f6d 100644 (file)
--- 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
index af0cce6a1f5ef14505320b4505732cfe767bf6e9..56a452cf0ece18d764ae521143dac6ef19f36b0b 100644 (file)
@@ -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 (file)
index 0000000..58f073d
--- /dev/null
@@ -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)
index be6d9c72bc649525e909558388e953d722e60d29..8bec84d83e9a778c5dc0dad62682729312ac1910 100644 (file)
@@ -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")
 
index 18a339bbc4b4294c7078703539d00d812724dd53..2d9101f2ac9d48ad4751b927bf1a6b77e904b59e 100644 (file)
@@ -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)
index 1119b4e8a7015af1124afabc1943947291c3a1b4..0da21fe164bb38b7717559c2ff94cc07457b39d1 100644 (file)
@@ -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)
index c6c62321537295d75372339525047e5bf1e4c06b..bb6bcc634109364ff302f3eff7a8cd345bdce46b 100644 (file)
@@ -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,
index 62c0126302d85ebbf00133f3f946e7d4946e539a..d93d05338454010e6af701ec707b0f870a31ce12 100644 (file)
@@ -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"
index ad0e0dbbf3aea4de16bcb5edb141de951a2c56d6..d4d3d0b9a71339524df98c05a617f410260d5d30 100644 (file)
@@ -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")