mikrowerk-edi-invoicing 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.2
2
+ Name: mikrowerk_edi_invoicing
3
+ Version: 0.1.0
4
+ Summary: Parser for EDI invoices in CII or UBL format
5
+ Author: Mikrowerk a Gammadata Division
6
+ Author-email: info@mikrowerk.com
7
+ License: Affero-3
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
10
+ Classifier: Operating System :: OS Independent
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: lxml
14
+ Requires-Dist: pypdf
15
+ Requires-Dist: pytest
16
+ Requires-Dist: flake8
17
+ Requires-Dist: isort
18
+ Requires-Dist: black
19
+ Requires-Dist: coverage
20
+ Requires-Dist: codecov
21
+ Requires-Dist: drafthorse~=2.4.0
22
+ Requires-Dist: factur-x==3.6
23
+ Requires-Dist: jsonpickle~=4.0.1
24
+ Requires-Dist: parameterized
25
+ Requires-Dist: schwifty
26
+ Dynamic: author
27
+ Dynamic: author-email
28
+ Dynamic: classifier
29
+ Dynamic: description
30
+ Dynamic: description-content-type
31
+ Dynamic: license
32
+ Dynamic: requires-dist
33
+ Dynamic: summary
34
+
35
+ # Mikrowerk EDI Parser for CII and UBL Invoices
@@ -0,0 +1,18 @@
1
+ model/__init__.py,sha256=eM8kFNSQHvV3RbGnrOPECSNcdOZIU_FJYkqOJ9gZ5-0,59
2
+ model/x_rechnung.py,sha256=qklcfdmihPZL9Ny7rqvP_KgvcnEsNGXLJUEO8iafZas,9790
3
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ tests/test_iban_handling.py,sha256=suRaB9gxbNc2Dc7spjHmQyPBdXva98HF1js85wQWqPM,662
5
+ tests/test_parse_x_rechnung.py,sha256=ChVUcDp3P1o8IzQmRJJWwkIlrqZel3xYmoE3V1ukytE,3584
6
+ util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ util/file_helper.py,sha256=4gdWbv8L9LSMraLKvGI1Z3NMcuGGy7JB1qFvNaW-yo4,767
8
+ x_mapper/__init__.py,sha256=Ee222PGj2y9wgsjx53yP9InOwkig31SZd8ra29gkPMc,536
9
+ x_mapper/cross_industry_invoice_mapper.py,sha256=HmIDFJhJf4iB5efFZoPhs04c2qvwvTn0pfbsrPA9EJk,827
10
+ x_mapper/drafthorse_elements_helper.py,sha256=gO8VobJnBqKEdRDcVsz4XixCvSCQEYqhcZeb7v51jGE,3756
11
+ x_mapper/xml_abstract_x_rechnung_parser.py,sha256=puxCSC02zIXpfMY9xNLqY-w0aRv0y5ROHmNtmaV16o4,673
12
+ x_mapper/xml_cii_dom_parser.py,sha256=q_0osUHGloVkG0ZL782SBMWAGtMaBdEI8xblyGrDlmw,10980
13
+ x_mapper/xml_ubl_sax_parser.py,sha256=VZcZSQo-iHiVA3IUtZu9xAQwWglgk34lD_Y-Hw3Sq5M,14008
14
+ mikrowerk_edi_invoicing-0.1.0.dist-info/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
15
+ mikrowerk_edi_invoicing-0.1.0.dist-info/METADATA,sha256=9kizoXNxoAkO0FHNyu10gjLF10aJr6VYsPlQpPQgjBM,1004
16
+ mikrowerk_edi_invoicing-0.1.0.dist-info/WHEEL,sha256=nn6H5-ilmfVryoAQl3ZQ2l8SH5imPWFpm1A5FgEuFV4,91
17
+ mikrowerk_edi_invoicing-0.1.0.dist-info/top_level.txt,sha256=pchuH1VFSrOMiUHBeuZxMaHNvC6mgZ59aac8eM_PXgs,26
18
+ mikrowerk_edi_invoicing-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ model
2
+ tests
3
+ util
4
+ x_mapper
model/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .x_rechnung import XRechnung
2
+
3
+ __all__ = ["XRechnung"]
model/x_rechnung.py ADDED
@@ -0,0 +1,260 @@
1
+ from datetime import datetime
2
+ from dataclasses import dataclass, asdict
3
+ from decimal import Decimal
4
+
5
+ __all__ = ['XRechnung', "XRechnungCurrency", "XRechnungTradeParty", "XRechnungTradeAddress", "XRechnungTradeContact",
6
+ "XRechnungPaymentMeans", "XRechnungBankAccount", "XRechnungAppliedTradeTax", "XRechnungTradeLine",
7
+ "XRechnungFinancialCard"]
8
+
9
+
10
+ @dataclass
11
+ class XRechnungCurrency:
12
+ amount: Decimal
13
+ currency_code: str
14
+
15
+ @staticmethod
16
+ def from_currency_tuple(currency_tuple: tuple) -> 'XRechnungCurrency':
17
+ return XRechnungCurrency(*currency_tuple)
18
+
19
+
20
+ @dataclass
21
+ class XRechnungTradeAddress:
22
+ post_code: str = None # 'Post-Code'
23
+ city_name: str = None # 'City'
24
+ country_id: str = None # 'Country ID
25
+ country_subdivision_id: str = None # 'Country Subdivision ID'
26
+ address_line_1: str = None # 'Address Line 1'
27
+ address_line_2: str = None # 'Address Line 2'
28
+ address_line_3: str = None # 'Address Line 3'
29
+
30
+
31
+ @dataclass
32
+ class XRechnungTradeContact:
33
+ name: str = None # 'Person Name'
34
+ department_name: str = None # 'Department Name'
35
+ telephone: str = None # 'Telephone Number'
36
+ fax: str = None # 'Fax'
37
+ email: str = None # 'Email'
38
+
39
+
40
+ @dataclass
41
+ class XRechnungTradeParty:
42
+ global_id: int = 0 # 'Global ID'
43
+ global_id_schema: str = None # 'Global Schema'
44
+ id: str = None # 'id'
45
+ name: str = None # 'Name'
46
+ description: str = None # 'Description'
47
+ postal_address: XRechnungTradeAddress = None
48
+ email: str = None # 'Email'
49
+ trade_contact: XRechnungTradeContact = None
50
+ vat_registration_number: str = None # 'VAT Registration Number'
51
+ fiscal_registration_number: str = None # 'Fiscal Registration Number'
52
+ legal_registration_number: str = None
53
+
54
+
55
+ # @dataclass
56
+ # class XRechnungSpecifiedTradeSettlementPaymentMeans:
57
+ # name: str = None # 'Name'
58
+ # type_code: str = None # 'Type Code'
59
+ # information: str = None # 'Information'
60
+ # iban: str = None # 'IBAN'
61
+ # bicid: str = None # 'BICID'
62
+ # account_name: str = None # 'Account Name'
63
+
64
+
65
+ @dataclass
66
+ class XRechnungAppliedTradeTax:
67
+ name: str = None # 'Name'
68
+ type_code: str = None
69
+ category_code: str = None
70
+ applicable_percent: float = 0.0
71
+ basis_amount: float = 0.0 # 'Basis Amount'
72
+ calculated_amount: float = 0.0 # 'Calculated Tax Amount'
73
+
74
+
75
+ @dataclass
76
+ class XRechnungTradeLine:
77
+ name: str = None # 'Name')
78
+ description: str = None
79
+ line_id: str = None # 'Line ID')
80
+ global_product_id: str = None # 'Global Product ID')
81
+ global_product_scheme_id: str = None # 'Global Product Scheme ID')
82
+ seller_assigned_id: str = None # 'Seller Assigned ID')
83
+ buyer_assigned_id: str = None # 'Buyer Assigned ID')
84
+ price_unit: float = 0.0 # 'Net Price')
85
+ quantity_billed: float = 0.0 # 'Billed Quantity')
86
+ quantity_unit_code: str = None # 'Quantity Code')
87
+ total_amount_net: float = 0.0 # 'Total Amount Net')
88
+ price_unit_gross: float = 0.0
89
+ total_allowance_charge: float = 0.0
90
+ trade_tax: any = None
91
+ note: str = None
92
+ lot_number_id: str = None
93
+ expiry_date: datetime = None
94
+
95
+
96
+ @dataclass
97
+ class XRechnungFinancialCard:
98
+ id: str | None = None
99
+ cardholder_name: str | None = None
100
+
101
+
102
+ @dataclass
103
+ class XRechnungBankAccount:
104
+ iban: str | None = None
105
+ bic: str | None = None
106
+
107
+
108
+ @dataclass
109
+ class XRechnungPaymentMeans:
110
+ id: str = None
111
+ type_code: str = None
112
+ information: str = None
113
+ financial_card: XRechnungFinancialCard = None
114
+ payee_account: XRechnungBankAccount = None
115
+
116
+
117
+ @dataclass
118
+ class XRechnung:
119
+ name: str = None # 'Name'
120
+ doc_id: str = None # 'Document ID'
121
+ doc_type_code: str = None # 'Subject Code'
122
+ issued_date_time: datetime = None # 'Date'
123
+ delivered_date_time: datetime = None # 'Delivered Date'
124
+ languages: str = None # 'Languages'
125
+ notes: str = None # 'Notes'
126
+ buyer_reference: str = None # 'Buyer Reference'
127
+ order_reference: str = None
128
+ dispatch_reference: str = None
129
+ sales_order_reference: str = None
130
+ seller: XRechnungTradeParty = None
131
+ payee: XRechnungTradeParty = None
132
+ buyer: XRechnungTradeParty = None
133
+ invoicee: XRechnungTradeParty = None
134
+ currency_code: str = None # 'Currency Code'
135
+ payment_means: XRechnungPaymentMeans = None
136
+ payment_terms: str = None # 'Payment Terms'
137
+ line_total_amount: Decimal = None # 'Line Total Amount'
138
+ charge_total_amount: Decimal = None # 'Charge Total Amount'
139
+ allowance_total_amount: Decimal = None # 'Allowance Total Amount'
140
+ tax_basis_total_amount: XRechnungCurrency = None
141
+ tax_total_amount: [XRechnungCurrency] = None # 'Tax Grand Total Amount'
142
+ grand_total_amount: XRechnungCurrency = None # 'Grand Total Amount'
143
+ total_prepaid_amount: Decimal = None # 'Total Prepaid Amount'
144
+ due_payable_amount: Decimal = None # 'Due Payable Amount'
145
+ trade_line_items: [XRechnungTradeLine] = None
146
+ applicable_trade_taxes: [XRechnungAppliedTradeTax] = None
147
+
148
+ def map_to_dict(self) -> dict:
149
+ """
150
+ maps a XRechnung to a dict suited for generation odoo entities
151
+ Note: this is not a 1:1 mapping of the XRechnung model, some adjustments and simplifications made
152
+ :param self: XRechnung.XRechnung
153
+ :return: dict
154
+ """
155
+
156
+ _dict = asdict(self)
157
+ _dict.update({
158
+ 'line_total_amount': float(self.line_total_amount) if self.line_total_amount is not None else 0,
159
+ 'charge_total_amount': float(self.charge_total_amount) if self.charge_total_amount else 0,
160
+ 'allowance_total_amount': float(self.allowance_total_amount) if self.allowance_total_amount else 0,
161
+ 'tax_basis_total_amount': float(
162
+ self.tax_basis_total_amount.amount) if self.tax_basis_total_amount else 0,
163
+ 'tax_total_amount': self.sum_x_rechnung_currency(self.tax_total_amount) if self.tax_total_amount else 0,
164
+ 'grand_total_amount': float(self.grand_total_amount.amount) if self.grand_total_amount else 0,
165
+ 'total_prepaid_amount': float(self.total_prepaid_amount) if self.total_prepaid_amount else 0.0,
166
+ 'due_payable_amount': float(self.due_payable_amount) if self.due_payable_amount else 0.0,
167
+ 'seller': self.map_trade_party(self.seller) if self.seller else None,
168
+ 'payee': self.map_trade_party(self.payee) if self.payee else None,
169
+ 'buyer': self.map_trade_party(self.buyer) if self.buyer else None,
170
+ 'invoicee': self.map_trade_party(self.invoicee) if self.invoicee else None,
171
+ 'payment_means': self.map_payment_means(self.payment_means) if self.payment_means else None,
172
+ 'trade_line_items': self.map_tradeline_items_to_dict(self.trade_line_items),
173
+ 'applicable_trade_taxes': self.map_trade_taxes_to_dict(self.applicable_trade_taxes),
174
+ })
175
+ return _dict
176
+
177
+ @classmethod
178
+ def map_tradeline_to_dict(cls, x_trade_line: XRechnungTradeLine) -> dict:
179
+ _dict = asdict(x_trade_line)
180
+ _dict.update(
181
+ {'trade_tax': asdict(x_trade_line.trade_tax),
182
+ 'price_unit': float(x_trade_line.price_unit),
183
+ 'quantity_billed': float(x_trade_line.quantity_billed),
184
+ 'total_amount_net': float(x_trade_line.total_amount_net),
185
+ })
186
+ return _dict
187
+
188
+ @classmethod
189
+ def map_tradeline_items_to_dict(cls, tradeline_items: list) -> list:
190
+ res = []
191
+ if tradeline_items:
192
+ for tradeline_item in tradeline_items:
193
+ res.append(cls.map_tradeline_to_dict(tradeline_item))
194
+ return res
195
+
196
+ @classmethod
197
+ def map_trade_taxes_to_dict(cls, trade_taxes: list) -> list:
198
+ res = []
199
+ if trade_taxes:
200
+ for tax in trade_taxes:
201
+ res.append(asdict(tax))
202
+ return res
203
+
204
+ @classmethod
205
+ def map_trade_party(cls, trade_party: XRechnungTradeParty) -> dict:
206
+ _dict = asdict(trade_party)
207
+ _dict.update({
208
+ 'postal_address': cls.map_trade_address(trade_party.postal_address),
209
+ 'trade_contact': cls.map_trade_contact(trade_party.trade_contact)
210
+ })
211
+ return _dict
212
+
213
+ @classmethod
214
+ def map_trade_address(cls, trade_address: XRechnungTradeAddress) -> dict | None:
215
+ if trade_address is None:
216
+ return None
217
+ _dict = asdict(trade_address)
218
+ return _dict
219
+
220
+ @classmethod
221
+ def map_trade_contact(cls, trade_contact: XRechnungTradeContact) -> dict | None:
222
+ if trade_contact is None:
223
+ return None
224
+ _dict = asdict(trade_contact)
225
+ return _dict
226
+
227
+ @classmethod
228
+ def map_payment_means(cls, payment_means: XRechnungPaymentMeans) -> dict:
229
+ _dict = {
230
+ 'type_code': payment_means.type_code,
231
+ 'information': payment_means.information,
232
+ 'financial_card': cls.map_financial_card(
233
+ payment_means.financial_card) if payment_means.financial_card else None,
234
+ 'bank_account': cls.map_bank_account(payment_means.payee_account) if payment_means.payee_account else None,
235
+ }
236
+ return _dict
237
+
238
+ @classmethod
239
+ def map_financial_card(cls, card: XRechnungFinancialCard) -> dict:
240
+ _dict = {
241
+ 'card_number': card.id,
242
+ 'card_holder_name': card.cardholder_name
243
+ }
244
+ return _dict
245
+
246
+ @classmethod
247
+ def map_bank_account(cls, bank: XRechnungBankAccount) -> dict:
248
+ _dict = {
249
+ 'iban': bank.iban,
250
+ 'bic': bank.bic
251
+ }
252
+ return _dict
253
+
254
+ @classmethod
255
+ def sum_x_rechnung_currency(cls, x_currencies: [XRechnungCurrency]) -> float:
256
+ res = 0.0
257
+ if x_currencies:
258
+ for x_currency in x_currencies:
259
+ res += float(x_currency.amount)
260
+ return res
tests/__init__.py ADDED
File without changes
@@ -0,0 +1,24 @@
1
+ import unittest
2
+ from schwifty import IBAN
3
+
4
+ _iban_1 = 'DE43500105175451887913'
5
+ _bic_expected = 'INGDDEFFXXX'
6
+
7
+
8
+ class IbanTestCase(unittest.TestCase):
9
+ def test_iban_decoding(self):
10
+ _ib_1: IBAN = IBAN(_iban_1)
11
+ self.assertIsNotNone(_ib_1)
12
+ _bank_code = _ib_1.bank_code
13
+ self.assertIsNotNone(_bank_code)
14
+ _account_code = _ib_1.account_code
15
+ self.assertIsNotNone(_account_code)
16
+ _country_code = _ib_1.country_code
17
+ self.assertIsNotNone(_country_code)
18
+ _bic = _ib_1.bic
19
+ self.assertIsNotNone(_bic)
20
+ self.assertEqual(_bic, _bic_expected)
21
+
22
+
23
+ if __name__ == '__main__':
24
+ unittest.main()
@@ -0,0 +1,82 @@
1
+ import unittest
2
+ import jsonpickle
3
+ from parameterized import parameterized
4
+
5
+ from facturx import get_facturx_xml_from_pdf
6
+
7
+ from ..util.file_helper import get_checked_file_path
8
+ from ..model.x_rechnung import XRechnung
9
+ from ..x_mapper.cross_industry_invoice_mapper import parse_and_map_x_rechnung
10
+
11
+
12
+ class XRechnungEinfachTestCase(unittest.TestCase):
13
+
14
+ def setUp(self) -> None:
15
+ jsonpickle.set_encoder_options("json", indent=2, ensure_ascii=False)
16
+ pass
17
+
18
+ @parameterized.expand([
19
+ ('xml', 'zugferd/XRECHNUNG_Einfach/xrechnung.xml'),
20
+ ('xml', 'real_invoice_samples/AKD-736116091815.xml'),
21
+ ('xml', 'griffity_exapmles/385_2025.xml'),
22
+ ('xml', 'e_invoicing_EN16931/CII-BR-CO-10-RoundingIssue.xml'),
23
+ ('xml', 'e_invoicing_EN16931/CII_business_example_01.xml'),
24
+ ('xml', 'e_invoicing_EN16931/CII_business_example_02.xml'),
25
+ ('xml', 'e_invoicing_EN16931/CII_business_example_Z.xml'),
26
+ ('xml', 'e_invoicing_EN16931/CII_example1.xml'),
27
+ # # ('xml', 'e_invoicing_EN16931/CII_example2.xml'), # throws error
28
+ ('xml', 'e_invoicing_EN16931/CII_example3.xml'),
29
+ ('xml', 'e_invoicing_EN16931/CII_example4.xml'),
30
+ # # ('xml', 'e_invoicing_EN16931/CII_example5.xml'), # throws error
31
+ ('xml', 'e_invoicing_EN16931/CII_example6.xml'),
32
+ ('xml', 'e_invoicing_EN16931/CII_example7.xml'),
33
+ ('xml', 'e_invoicing_EN16931/CII_example8.xml'),
34
+ ('xml', 'e_invoicing_EN16931/CII_example9.xml'),
35
+ ('xml', 'ubl/RG00343552.xml'),
36
+ ('xml', 'ubl/ubl_invoice_example.xml'),
37
+ ('xml', 'ubl/UBL-Invoice-2.1-Example.xml'),
38
+ ('pdf', 'zugferd/BASIC-WL_Einfach/BASIC-WL_Einfach.pdf'),
39
+ # ('pdf', 'zugferd/XRECHNUNG_Einfach/XRECHNUNG_Einfach.pdf'), # needs some checks, why test failed
40
+ # ("pdf", "zugferd/XRECHNUNG_Elektron/XRECHNUNG_Elektron.pdf"), # needs some checks, why test failed
41
+ ('pdf', 'odoo_generated/INV_2025_00001.pdf')
42
+ ])
43
+ def test_x_rechnung_files(self, file_type, file_path):
44
+ print(f"start testing with file: {file_path}")
45
+ if file_type == 'xml':
46
+ _parsed = self._parse_xml(file_path)
47
+ elif file_type == 'pdf':
48
+ _parsed = self._parse_pdf(file_path)
49
+ else:
50
+ raise AssertionError(f'File type {file_type} not supported')
51
+ assert _parsed is not None
52
+ res_dict = _parsed.map_to_dict()
53
+ print(jsonpickle.dumps(res_dict))
54
+
55
+ def _parse_xml(self, filepath) -> XRechnung:
56
+ _file_path, _exists, _is_dir = get_checked_file_path(filepath, __file__)
57
+ self.assertTrue(_exists)
58
+ print(f"\n_parse_xml: file_path={_file_path}")
59
+ with open(_file_path, "rb") as _file:
60
+ samplexml = _file.read()
61
+ res = parse_and_map_x_rechnung(samplexml)
62
+ self.assertIsNotNone(res)
63
+ return res
64
+
65
+ def _parse_pdf(self, filepath) -> XRechnung:
66
+ _file_path, _exists, _is_dir = get_checked_file_path(filepath, __file__)
67
+ self.assertTrue(_exists)
68
+ print(f"\n_parse_pdf: file_path={_file_path}")
69
+ with open(_file_path, "rb") as _file:
70
+ sample_pdf = _file.read()
71
+ filename, xml = get_facturx_xml_from_pdf(sample_pdf, False)
72
+ print(xml)
73
+ if not xml or len(xml) == 0:
74
+ raise FileNotFoundError(
75
+ f"Could not extraxt XML from PDF file: {filepath}")
76
+ res = parse_and_map_x_rechnung(xml)
77
+ self.assertIsNotNone(res)
78
+ return res
79
+
80
+
81
+ if __name__ == '__main__':
82
+ unittest.main()
util/__init__.py ADDED
File without changes
util/file_helper.py ADDED
@@ -0,0 +1,24 @@
1
+ """
2
+ Helper functions for working with files and directories.
3
+ """
4
+
5
+ from os import path as os_path
6
+
7
+
8
+ def get_checked_file_path(file_path: str, rel_file=None) -> (str, bool, bool):
9
+ """
10
+ get relative or absolute file path
11
+ Args:
12
+ file_path: path to file, relative or absolute
13
+ rel_file: if not none then this is the base dir-path to the file_path
14
+
15
+ Returns: (str, bool, bool) resulting path to file, file exists, is directory
16
+
17
+ """
18
+ if rel_file is None or file_path.startswith("/"):
19
+ _path = os_path.normpath(file_path) # absolute
20
+ else:
21
+ rel_dir = os_path.dirname(rel_file)
22
+ fp = os_path.normpath(file_path)
23
+ _path = os_path.join(rel_dir, fp)
24
+ return _path, os_path.exists(_path), os_path.isdir(_path)
x_mapper/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ from .cross_industry_invoice_mapper import parse_and_map_x_rechnung
2
+ from .xml_abstract_x_rechnung_parser import XMLAbstractXRechnungParser
3
+ from .xml_cii_dom_parser import XRechnungCIIXMLParser
4
+ from .xml_ubl_sax_parser import XRechnungUblXMLParser
5
+ from .drafthorse_elements_helper import get_string_from_text as get_string_from_text
6
+ __all__ = ["parse_and_map_x_rechnung", "cross_industry_invoice_mapper", "drafthorse_elements_helper",
7
+ "XMLAbstractXRechnungParser",
8
+ "XRechnungCIIXMLParser", "XRechnungUblXMLParser"]
@@ -0,0 +1,20 @@
1
+ """
2
+ This implements a mapper from a drafthorse parsed x-rechnung-xml to the internal XRechnung object
3
+ """
4
+ from lxml import etree
5
+
6
+ from ..model.x_rechnung import XRechnung
7
+ from ..x_mapper.xml_cii_dom_parser import XRechnungCIIXMLParser
8
+ from ..x_mapper.xml_ubl_sax_parser import XRechnungUblXMLParser
9
+
10
+
11
+ def parse_and_map_x_rechnung(_xml: any) -> XRechnung:
12
+ _parser = None
13
+ tree = etree.fromstring(_xml)
14
+ if tree.tag == '{urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100}CrossIndustryInvoice':
15
+ _parser = XRechnungCIIXMLParser()
16
+ elif tree.tag == '{urn:oasis:names:specification:ubl:schema:xsd:Invoice-2}Invoice':
17
+ _parser = XRechnungUblXMLParser()
18
+ if _parser is None:
19
+ raise ValueError(f'xml format not supported: "{tree.tag}"')
20
+ return _parser.parse_and_map_x_rechnung(_xml)
@@ -0,0 +1,132 @@
1
+ from drafthorse.models.elements import (StringElement, DirectDateTimeElement, DateTimeElement, DecimalElement,
2
+ IndicatorElement, QuantityElement, CurrencyElement, ClassificationElement,
3
+ Container)
4
+ from drafthorse.models.party import EmailURI, PhoneNumber, FaxNumber
5
+ from drafthorse.models.note import IncludedNote
6
+ from drafthorse.models.payment import PaymentTerms
7
+ from drafthorse.models.container import IDContainer, StringContainer, CurrencyContainer
8
+
9
+ """
10
+ Code zur Beschreibung des Datumsformats
11
+ Codeliste:
12
+ 102 = CCYYMMDD
13
+ 201 = YYMMDDHHMM
14
+ 615 = YYWW
15
+ 616 = CCYYWW
16
+ 720 = THHMMTHHMM
17
+ 804 = Anzahl Tage
18
+ Beispiel: <DocumentDate FormatCode="102">20160331</DocumentDate>
19
+ """
20
+
21
+
22
+ def get_value(self) -> any:
23
+ return self._value
24
+
25
+
26
+ def get_value_from_amount(self) -> any:
27
+ return self._amount
28
+
29
+
30
+ def get_currency(self) -> (any, any):
31
+ return self._amount, self._currency
32
+
33
+
34
+ def get_currencies(self) -> [(any, any)]:
35
+ res = []
36
+ if self.children:
37
+ for currency in self.children:
38
+ res.append(currency)
39
+ return res
40
+
41
+
42
+ def get_string_from_text(self) -> str:
43
+ return self._text
44
+
45
+
46
+ def get_string_from_payment_terms(self, separator) -> str:
47
+ return str(self.description)
48
+
49
+
50
+ def get_string_from_value(self) -> str:
51
+ return str(self._value)
52
+
53
+
54
+ def get_string_from_data(self) -> str:
55
+ return str(self._data)
56
+
57
+
58
+ def get_string_from_address(self) -> str:
59
+ return str(self.address)
60
+
61
+
62
+ def get_string_from_number(self) -> str:
63
+ return str(self.number)
64
+
65
+
66
+ def get_string_from_date(self) -> str:
67
+ return str(self._value)
68
+
69
+
70
+ def get_string_from_quantity(self) -> str:
71
+ return str(f"{self._amount} {self._unit_code}")
72
+
73
+
74
+ def get_string_from_currency(self) -> str:
75
+ return str(f"{self._amount} {self._currency}")
76
+
77
+
78
+ def get_string_from_content(self, separator) -> str:
79
+ if not self.content:
80
+ return ""
81
+ return self.content.get_string_elements(separator)
82
+
83
+
84
+ def get_string_elements(self, separator: str = ';') -> str:
85
+ res = ""
86
+ _separator = ""
87
+ if self.children:
88
+ for child in self.children:
89
+ if isinstance(child, str):
90
+ res += _separator + child
91
+ elif isinstance(child, tuple):
92
+ p1, p2 = child[:]
93
+ res += f"{p1} {p2} {_separator}"
94
+ else:
95
+ res += _separator + child.get_string(separator)
96
+ _separator = separator
97
+ return res
98
+
99
+
100
+ def get_ids(self) -> list:
101
+ res = list()
102
+ if self.children:
103
+ for text, _id, in self.children:
104
+ res.append({
105
+ 'id': _id,
106
+ 'schema': text,
107
+ })
108
+ return res
109
+
110
+
111
+ StringElement.get_string = get_string_from_text
112
+ StringContainer.get_string = get_string_elements
113
+ DirectDateTimeElement.get_string = get_string_from_date
114
+ DateTimeElement.get_string = get_string_from_date
115
+ DateTimeElement.get_value = get_value
116
+ DecimalElement.get_string = get_string_from_value
117
+ DecimalElement.get_value = get_value
118
+ IndicatorElement.get_string = get_string_from_value
119
+ QuantityElement.get_string = get_string_from_quantity
120
+ QuantityElement.get_value = get_value_from_amount
121
+ CurrencyElement.get_currency = get_currency
122
+ CurrencyContainer.get_string = get_string_elements
123
+ CurrencyContainer.get_currencies = get_currencies
124
+ ClassificationElement.get_string = get_string_from_text
125
+ IDContainer.get_object = get_ids
126
+ EmailURI.get_string = get_string_from_address
127
+ PhoneNumber.get_string = get_string_from_number
128
+ FaxNumber.get_string = get_string_from_number
129
+ Container.get_string_elements = get_string_elements
130
+ IncludedNote.get_string = get_string_from_content
131
+ IncludedNote.get_string_elements = get_string_elements
132
+ PaymentTerms.get_string = get_string_from_payment_terms
@@ -0,0 +1,21 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from ..model.x_rechnung import XRechnung
4
+
5
+
6
+ class XMLAbstractXRechnungParser(ABC):
7
+ TYPE_CODES = {
8
+ '326': 'Partial invoice',
9
+ '380': 'Commercial invoice', # Rechnung
10
+ '384': 'Corrected invoice', # Rechnungskorrektur
11
+ '389': 'Self-billed invoice', # Selbst erstelle Rechnung
12
+ '381': 'Credit note', # Gutschrift
13
+ '875': 'Partial Invoice', # Abschlagsrechnung
14
+ '876': 'Partial final invoice', # Teilschlussrechnung
15
+ '877': 'Final invoice' # Schlussrechnung
16
+ }
17
+
18
+ @staticmethod
19
+ @abstractmethod
20
+ def parse_and_map_x_rechnung(_xml: any) -> XRechnung:
21
+ pass