diff --git a/document.py b/document.py index 8389c98..54dbc61 100644 --- a/document.py +++ b/document.py @@ -28,7 +28,7 @@ xml_types = [ (['xsd', 'Factur-X_1.07.2_BASIC', 'Factur-X_1.07.2_BASIC.xsd'], 'Factur-X basic', 'facturx_basic'), (['xsd', 'Factur-X_1.07.2_BASICWL', 'Factur-X_1.07.2_BASICWL.xsd'], - 'Factur-X basic-wl', ''), + 'Factur-X basicwl', 'facturx_basicwl'), (['xsd', 'Factur-X_1.07.2_EN16931', 'Factur-X_1.07.2_EN16931.xsd'], 'Factur-X EN16931', ''), (['xsd', 'Factur-X_1.07.2_EXTENDED', 'Factur-X_1.07.2_EXTENDED.xsd'], @@ -63,20 +63,25 @@ class Incoming(metaclass=PoolMeta): with_children (bool, optional): convert sub-documents. Defaults to False. """ + print('\n## process:', documents) for document in documents: - cls._readxml_check_content(document) + document._facturx_detect_content() + print('-- process-2:') super().process(documents, with_children) + print('-- process-3:') def _process_supplier_invoice(self): """ try to detect content of 'data', read values """ invoice = super()._process_supplier_invoice() + print('\n## _process_supplier_invoice:', self) if self.mime_type == 'application/xml': # detect xml-content xml_info = self._facturx_detect_content() if xml_info: (xsd_type, funcname, xmltree) = xml_info + print('-- _process_supplier_invoice-xml_info:', xml_info) xml_read_func = getattr(self, '_readxml_%s' % funcname, None) if not xml_read_func: raise UserError(gettext( @@ -90,6 +95,7 @@ class Incoming(metaclass=PoolMeta): invoice.save() self._readxml_check_invoice(invoice) # raise ValueError('stop') + print('-- _process_supplier_invoice-FIN:', invoice) return invoice def _readxml_check_invoice(self, invoice): @@ -422,6 +428,14 @@ class Incoming(metaclass=PoolMeta): """ self._readxml_facturx_extended(xmltree) + def _readxml_facturx_basicwl(self, xmltree): + """ read facturx - basic-wl + """ + # deny usage of factur-x basicwl because it contains no invoice-lines + raise UserError(gettext( + 'document_incoming_invoice_xml.msg_convert_error', + msg='factur-x basic-wl not supported')) + def _readxml_facturx_extended(self, xmltree): """ read factur-x extended """ @@ -1016,29 +1030,7 @@ class Incoming(metaclass=PoolMeta): msg='cannot convert %(val)s' % { 'val': date_string})) - @classmethod - def _readxml_check_content(cls, document): - """ try to detect content, fire exception if fail - - Args: - document (record): model document.incoming - """ - xml_data = etree.fromstring(document.data) - try: - fx_flavour = facturx.get_flavor(xml_data) - fx_level = facturx.get_level(xml_data) - document._facturx_detect_content(fx_flavour, fx_level) - document.save() - except Exception as e1: - raise UserError(gettext( - 'document_incoming_invoice_xml.msg_convert_error', - msg='%(name)s [%(flavour)s|%(level)s] %(msg)s' % { - 'name': document.name or '-', - 'flavour': fx_flavour, - 'level': fx_level, - 'msg': str(e1)})) - - def _facturx_detect_content(self, flavour=None, level=None): + def _facturx_detect_content(self): """ check xml-data against xsd of XRechnung and FacturX, begin with extended, goto minimal, then xrechnung @@ -1047,11 +1039,13 @@ class Incoming(metaclass=PoolMeta): defaults to None if not detected """ xml_data = etree.fromstring(self.data) + fx_flavour = facturx.get_flavor(xml_data) + fx_level = facturx.get_level(xml_data) for xsdpath, xsdtype, funcname in xml_types: - if flavour == 'factur-x' and level: + if fx_flavour == 'factur-x' and fx_level: if not (xsdtype.lower().startswith('factur-x') and - xsdtype.lower().endswith(level)): + xsdtype.lower().endswith(fx_level)): # skip check if xml-content is already known continue @@ -1063,8 +1057,14 @@ class Incoming(metaclass=PoolMeta): return (xsdtype, funcname, xml_data) except etree.DocumentInvalid as e1: # fire exception for knows xml-content - if flavour == 'factur-x' and level: - raise e1 + if fx_flavour == 'factur-x' and fx_level: + raise UserError(gettext( + 'document_incoming_invoice_xml.msg_convert_error', + msg='%(name)s [%(flavour)s|%(level)s] %(msg)s' % { + 'name': self.name or '-', + 'flavour': fx_flavour, + 'level': fx_level, + 'msg': str(e1)})) pass return None diff --git a/tests/document.py b/tests/document.py index 3ba1074..699e1f9 100644 --- a/tests/document.py +++ b/tests/document.py @@ -12,149 +12,8 @@ from trytond.pool import Pool from trytond.exceptions import UserError from trytond.modules.company.tests import create_company, set_company from trytond.modules.account.tests import create_chart, get_fiscalyear - - -parsed_data_facturx_extended = { - 'invoice_number': 'RE2024.01234', - 'invoice_date': date(2024, 6, 17), - 'note_list': [{ - 'Content': 'Description of invoice', - 'ContentCode': None, - 'SubjectCode': None, - }, { - 'Content': 'Some notes to the customer.', - 'ContentCode': '1', - 'SubjectCode': None, - }, { - 'Content': 'Goes to field comment.', - 'ContentCode': '22', - 'SubjectCode': '42'}], - 'seller_party': { - 'name': 'Name of the Supplier', - 'postal_code': '12345', - 'street': 'Street of Supplier No 1', - 'city': 'Berlin'}, - 'buyer_party': { - 'name': 'Our Company', - 'postal_code': '23456', - 'street': 'Address Line 1\nAddress Line 2', - 'city': 'Potsdam'}, - 'lines_data': [{ - 'line_no': '1', - 'name': 'Name of Product 1', - 'description': 'Description of Product 1', - 'unit_net_price': {'amount': Decimal('1350.00')}, - 'quantity': {'billed': Decimal('1.0'), 'unit_code': 'KGM'}, - 'taxes': [{ - 'type': 'VAT', - 'category_code': 'S', - 'percent': Decimal('19.00')}], - 'total': {'amount': Decimal('1350.00')}, - }, { - 'convert_note': [ - 'skip: /rsm:CrossIndustryInvoice/' + - 'rsm:SupplyChainTradeTransaction/' + - 'ram:IncludedSupplyChainTradeLineItem[2]/' + - 'ram:SpecifiedLineTradeDelivery/' + - 'ram:ActualDeliverySupplyChainEvent'], - 'line_no': '2', - 'line_note': 'Description of Line 2\n' + - 'Description of Line 2, line 2', - 'prod_id': '2', - 'glob_id': '3', - 'seller_id': '4', - 'buyer_id': '5', - 'industy_id': '6', - 'model_id': '7', - 'name': 'Name of Product 2', - 'description': 'Description of Product 2', - 'lot': 'batch23', - 'brand_name': 'Brand-Name', - 'model_name': 'Model-Name', - 'trade_country': 'DE', - 'attributes': [{ - 'code': '123', - 'description': 'Kilogram', - 'uom': 'kg', - 'value': Decimal('123.0')}], - 'classification': [{ - 'code': '3c', 'name': 'product-class 1'}], - 'serialno': [{'lot': '22', 'serial': '1234'}], - 'refprod': [{ - 'id': '1', - 'global_id': '2', - 'seller_id': '3', - 'buyer_id': '4', - 'name': 'ref-prod-1', - 'description': 'description of ref-prod-1', - 'quantity': Decimal('1.0')}], - 'unit_net_price': { - 'amount': Decimal('800.00'), - 'basequantity': Decimal('1.0')}, - 'unit_gross_price': { - 'amount': Decimal('950.00'), - 'basequantity': Decimal('1.0')}, - 'quantity': { - 'billed': Decimal('1.5'), - 'unit_code': 'KGM', - 'package': Decimal('1.5')}, - 'taxes': [{ - 'type': 'VAT', - 'category_code': 'S', - 'percent': Decimal('19.00')}], - 'total': {'amount': Decimal('1200.00')}, - }, { - 'line_no': '3', - 'name': 'Name of Product 3', - 'description': 'Description of Product 3', - 'unit_net_price': {'amount': Decimal('150.00')}, - 'quantity': {'billed': Decimal('2.0'), 'unit_code': 'MTR'}, - 'taxes': [{ - 'type': 'VAT', - 'category_code': 'S', - 'percent': Decimal('7.00')}], - 'total': {'amount': Decimal('300.00')}, - }], - 'payment': { - 'reference': 'RE2024.01234', - 'currency': 'EUR', - 'bank': [{ - 'info': 'Wire transfer', - 'type': '30', - 'debitor_iban': 'DE02300209000106531065', - 'creditor_iban': 'DE02300209000106531065', - 'creditor_name': 'mbs', - 'card_id': 'DE02300209000106531065', - 'card_holder_name': 'Card Holder', - 'institution': 'WELADED1PMB'}], - 'taxes': [{ - 'amount': Decimal('484.5'), - 'type': 'VAT', - 'base': Decimal('2550.0'), - 'category_code': 'S', - 'percent': Decimal('19.00'), - }, { - 'amount': Decimal('21.0'), - 'type': 'VAT', - 'base': Decimal('300.0'), - 'category_code': 'S', - 'percent': Decimal('7.00')}], - 'terms': [{ - 'description': 'Payment description', - 'duedate': date(2024, 7, 1), - 'mandat_id': 'mandat id', - 'amount': Decimal('3355.50'), - 'discount_date': date(2024, 7, 2), - 'discount_measure': Decimal('10.0'), - 'discount_base': Decimal('3355.0'), - 'discount_perc': Decimal('2.0'), - 'discount_amount': Decimal('70.0')}]}, - 'total': { - 'amount': Decimal('1350.00'), - 'taxbase': Decimal('2850.00'), - 'taxtotal': Decimal('505.5'), - 'grand': Decimal('3355.50'), - 'duepayable': Decimal('3355.50')}} +from .parsed_data import ( + parsed_data_facturx_extended, parsed_data_facturx_basic) def set_invoice_sequences(fiscalyear): @@ -289,7 +148,8 @@ class DocumentTestCase(object): @with_transaction() def test_xmldoc_check_xml_read_facturx_extended(self): - """ add incoming-dcument in memory, read xml into 'parsed_data' + """ add incoming-dcument 'factur-x-extended' in memory, + read xml into 'parsed_data' """ pool = Pool() IncDocument = pool.get('document.incoming') @@ -305,11 +165,13 @@ class DocumentTestCase(object): self.assertEqual(funcname, 'facturx_extended') incoming._readxml_facturx_extended(xml_data) - self.assertEqual(incoming.parsed_data, parsed_data_facturx_extended) + self.assertEqual( + self.prep_sorted_dict(incoming.parsed_data), + parsed_data_facturx_extended) @with_transaction() def test_xmldoc_import_facturx_extended(self): - """ create incoming-document, load xml, detect type + """ create incoming-document, load factur-x-extended xml, detect type """ pool = Pool() IncDocument = pool.get('document.incoming') @@ -435,4 +297,119 @@ class DocumentTestCase(object): self.assertEqual(attachment.data, document.data) self.assertEqual(attachment.name, 'facturx-extended.xml') + @with_transaction() + def test_xmldoc_check_xml_read_facturx_basic(self): + """ add incoming-dcument 'facturx-basic' in memory, + read xml into 'parsed_data' + """ + pool = Pool() + IncDocument = pool.get('document.incoming') + + with open(os.path.join( + os.path.split(__file__)[0], + 'facturx-basic.xml'), 'rb') as fhdl: + xml_txt = fhdl.read() + + incoming = IncDocument(data=xml_txt) + (xsdtype, funcname, xml_data) = incoming._facturx_detect_content() + self.assertEqual(xsdtype, 'Factur-X basic') + self.assertEqual(funcname, 'facturx_basic') + + incoming._readxml_facturx_basic(xml_data) + self.assertEqual(incoming.parsed_data, parsed_data_facturx_basic) + + @with_transaction() + def test_xmldoc_import_facturx_basic(self): + """ create incoming-document, load factur-x-basic xml, detect type + """ + pool = Pool() + IncDocument = pool.get('document.incoming') + Configuration = pool.get('document.incoming.configuration') + Party = pool.get('party.party') + IrAttachment = pool.get('ir.attachment') + Invoice = pool.get('account.invoice') + + company = create_company('m-ds') + with set_company(company): + + create_chart(company=company, tax=True) + self.prep_fiscalyear(company) + + product_categories = self.prep_prodcat_category(company) + config = Configuration( + product_category=product_categories) + config.save() + + self.assertEqual(config.create_supplier, True) + self.assertEqual(config.accept_other_company, False) + self.assertEqual(config.number_target, 'reference') + self.assertEqual(len(config.product_category), 3) + self.assertEqual(config.product_category[0].name, 'Accounting') + self.assertEqual(config.product_category[1].name, 'Accounting 19%') + self.assertEqual(config.product_category[2].name, 'Accounting 7%') + + to_create = [] + with open(os.path.join( + os.path.split(__file__)[0], + 'facturx-basic.xml'), 'rb') as fhdl: + to_create.append({ + 'data': fhdl.read(), + 'name': 'facturx-basic.xml', + 'type': 'supplier_invoice'}) + + document, = IncDocument.create(to_create) + self.assertEqual(document.mime_type, 'application/xml') + self.assertEqual(document.company.id, company.id) + self.assertTrue(document.data.startswith( + b'\n' + + b'