diff --git a/purchase_order_import_ubl/README.rst b/purchase_order_import_ubl/README.rst new file mode 100644 index 0000000000..efde43328d --- /dev/null +++ b/purchase_order_import_ubl/README.rst @@ -0,0 +1,59 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +========================= +Purchase Order Import UBL +========================= + +This module adds support for the import of electronic quotations that comply with the `Universal Business Language (UBL) `_ standard. The UBL standard became the `ISO/IEC 19845 `_ standard in December 2015 (cf the `official announce _`). The file can be in two formats: + +* UBL XML file, +* PDF file with an embedded UBL XML file. + +You can use the OCA module *sale_order_ubl* to generate PDF quotations with an embedded UBL XML file. + +Configuration +============= + +No configuration is needed. + +Usage +===== + +Refer to the README.rst of the module *purchase_order_import* for a detailed usage description. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/226/10.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Alexis de Lattre + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/purchase_order_import_ubl/__init__.py b/purchase_order_import_ubl/__init__.py new file mode 100644 index 0000000000..40272379f7 --- /dev/null +++ b/purchase_order_import_ubl/__init__.py @@ -0,0 +1 @@ +from . import wizard diff --git a/purchase_order_import_ubl/__manifest__.py b/purchase_order_import_ubl/__manifest__.py new file mode 100644 index 0000000000..fce981f1d6 --- /dev/null +++ b/purchase_order_import_ubl/__manifest__.py @@ -0,0 +1,15 @@ +# © 2016-2017 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Quotation Order UBL Import", + "version": "14.0.1.0.0", + "category": "Purchase Management", + "license": "AGPL-3", + "summary": "Import UBL XML quotation files", + "author": "Akretion,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/edi", + "depends": ["purchase_order_import", "base_ubl"], + "demo": ["demo/demo_data.xml"], + "installable": True, +} diff --git a/purchase_order_import_ubl/demo/demo_data.xml b/purchase_order_import_ubl/demo/demo_data.xml new file mode 100644 index 0000000000..5aaaf87461 --- /dev/null +++ b/purchase_order_import_ubl/demo/demo_data.xml @@ -0,0 +1,133 @@ + + + + + + Johnssons byggvaror + + 1 + 0 + + 5 Rådhusgatan + PoBox123 + 11000 + Stockholm + + + + + + + Pelle Svensson + Boss + pelle@johnsson.se + 1 + contact + + + + + Swedish trucking + + 0 + 0 + bill@svetruck.se + 5 Rådhusgatan + 2nd floor + 11000 + Stockholm + + + + + + IYT Corporation + + 1 + 0 + + 56A Avon Way + Thereabouts + ZZ99 1ZZ + Bridgtow + + + + + + + Fred Churchill + fred@iytcorporation.gov.uk + 1 + contact + + + + + The Terminus + + 1 + 0 + + 56A Avon Way + Thereabouts + ZZ99 1ZZ + Bridgtow + + + + + + S Massiah + smassiah@the-email.co.uk + 1 + contact + + + + + + Gentofte Kommune + + 1 + 0 + + 161 Bernstorffsvej + 2920 + Charlottenlund + + + + + + + Joe Delivery + 1 + contact + + + + Delta PC + + 1 + info@yourcompany.example.com + + + + + + DDU + DELIVERED DUTY UNPAID + + + + PROD_DEL02 + PROD_DEL02 + + + + MBi9 + MBi9 + + + diff --git a/purchase_order_import_ubl/i18n/es.po b/purchase_order_import_ubl/i18n/es.po new file mode 100644 index 0000000000..6c790a80b0 --- /dev/null +++ b/purchase_order_import_ubl/i18n/es.po @@ -0,0 +1,42 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_order_import_ubl +# +# Translators: +# enjolras , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-03-12 01:43+0000\n" +"PO-Revision-Date: 2018-03-12 01:43+0000\n" +"Last-Translator: enjolras , 2018\n" +"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: purchase_order_import_ubl +#: model:ir.model,name:purchase_order_import_ubl.model_purchase_order_import +msgid "Purchase Order Import from Files" +msgstr "Importar pedido de compra desde archivos" + +#. module: purchase_order_import_ubl +#: model:ir.model,name:purchase_order_import_ubl.model_order_response_import +#, fuzzy +msgid "Purchase Order Response Import from Files" +msgstr "Importar pedido de compra desde archivos" + +#. module: purchase_order_import_ubl +#: code:addons/purchase_order_import_ubl/wizard/order_response_import.py:60 +#, python-format +msgid "Unknown response code found '%s'" +msgstr "" + +#. module: purchase_order_import_ubl +#: code:addons/purchase_order_import_ubl/wizard/order_response_import.py:70 +#, python-format +msgid "Unsupported line status code found '%s'" +msgstr "" diff --git a/purchase_order_import_ubl/i18n/purchase_order_import_ubl.pot b/purchase_order_import_ubl/i18n/purchase_order_import_ubl.pot new file mode 100644 index 0000000000..0b46e71d17 --- /dev/null +++ b/purchase_order_import_ubl/i18n/purchase_order_import_ubl.pot @@ -0,0 +1,37 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_order_import_ubl +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: purchase_order_import_ubl +#: model:ir.model,name:purchase_order_import_ubl.model_purchase_order_import +msgid "Purchase Order Import from Files" +msgstr "" + +#. module: purchase_order_import_ubl +#: model:ir.model,name:purchase_order_import_ubl.model_order_response_import +msgid "Purchase Order Response Import from Files" +msgstr "" + +#. module: purchase_order_import_ubl +#: code:addons/purchase_order_import_ubl/wizard/order_response_import.py:60 +#, python-format +msgid "Unknown response code found '%s'" +msgstr "" + +#. module: purchase_order_import_ubl +#: code:addons/purchase_order_import_ubl/wizard/order_response_import.py:70 +#, python-format +msgid "Unsupported line status code found '%s'" +msgstr "" + diff --git a/purchase_order_import_ubl/readme/CONTRIBUTORS.rst b/purchase_order_import_ubl/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..53a1c0a2b6 --- /dev/null +++ b/purchase_order_import_ubl/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Alexis de Lattre +* Robin Conjour diff --git a/purchase_order_import_ubl/readme/DESCRIPTION.rst b/purchase_order_import_ubl/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..d3bd76d415 --- /dev/null +++ b/purchase_order_import_ubl/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This module adds support for the import of electronic quotations that comply with the `Universal Business Language (UBL) `_ standard. The UBL standard became the `ISO/IEC 19845 `_ standard in December 2015 (cf the `official announce _`). The file can be in two formats: + +* UBL XML file, +* PDF file with an embedded UBL XML file. + +You can use the OCA module *sale_order_ubl* to generate PDF quotations with an embedded UBL XML file. diff --git a/purchase_order_import_ubl/static/description/icon.png b/purchase_order_import_ubl/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/purchase_order_import_ubl/static/description/icon.png differ diff --git a/purchase_order_import_ubl/tests/__init__.py b/purchase_order_import_ubl/tests/__init__.py new file mode 100644 index 0000000000..6a208fe0da --- /dev/null +++ b/purchase_order_import_ubl/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_ubl_order_import +from . import test_order_response_import diff --git a/purchase_order_import_ubl/tests/files/order_response_tmpl.xml b/purchase_order_import_ubl/tests/files/order_response_tmpl.xml new file mode 100644 index 0000000000..abe1d544eb --- /dev/null +++ b/purchase_order_import_ubl/tests/files/order_response_tmpl.xml @@ -0,0 +1,149 @@ + + + 2.1 + PO07337 + 2020-02-04 + 22:10:30 + %(order_response_code)s + Note1 + Note2 + EUR + + %(order_id)s + + + 79201 + + + BE0477472701 + + + My supplier company BELGIUM + + + French (BE) / Français (BE) + fr_BE + + + straat 20 + ZAVENTEM + 1930 + + BE + Belgique + + + + My supplier company BELGIUM + BE0401953350 + + VAT + + + + 12345 + 12345 + orderahbelux@my-supplier-company.com + + + + + + http://www.my-company.be + + BE0421801233 + + + My Componay Belux SA + + + English + en_US + + + Rue , 17 + Villers-le-Bouillet + 4530 + + BE + Belgique + + + + My Componay Belux SA + BE0421801233 + + VAT + + + + My Componay Belux SA + + Rue , 17 + Villers-le-Bouillet + 4530 + + BE + Belgique + + + + + +32 (0)4 123 34 90 + +32 (0)4 123 27 83 + secretariat@my-compy.be + + + + + + %(line_1_id)s + line_1 Note1 + line_1 Note2 + %(line_1_qty)s + %(line_1_backorder_qty)s + %(line_1_status_code)s + 228.2 + + 45.64 + 1 + + + [10016098] CYTOPOINT 10MG 2x1ml + CYTOPOINT 10MG 2x1ml + + 10016098 + + + + + + + + %(line_2_id)s + line_2 Note1 + line_2 Note2 + %(line_2_qty)s + %(line_2_backorder_qty)s + %(line_2_status_code)s + 1117.07 + + 65.71 + 1 + + + [10016099] CYTOPOINT 20MG 2x1ml + CYTOPOINT 20MG 2x1ml + + 10016099 + + + + + diff --git a/purchase_order_import_ubl/tests/files/quote-PO00004.pdf b/purchase_order_import_ubl/tests/files/quote-PO00004.pdf new file mode 100644 index 0000000000..acef818fb0 Binary files /dev/null and b/purchase_order_import_ubl/tests/files/quote-PO00004.pdf differ diff --git a/purchase_order_import_ubl/tests/test_order_response_import.py b/purchase_order_import_ubl/tests/test_order_response_import.py new file mode 100644 index 0000000000..ac8a9654e4 --- /dev/null +++ b/purchase_order_import_ubl/tests/test_order_response_import.py @@ -0,0 +1,94 @@ +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tools import file_open + +from odoo.addons.purchase_order_import.tests.test_order_response_import import ( + TestOrderResponseImportCommon, +) +from odoo.addons.purchase_order_import.wizard.order_response_import import ( + LINE_STATUS_ACCEPTED, + ORDER_RESPONSE_STATUS_ACK, +) + +from ..wizard.order_response_import import ( + _ORDER_LINE_STATUS_TO_STATUS, + _ORDER_RESPONSE_CODE_TO_STATUS, +) + +_STATUS_TO_RESPONSE_CODE = {p[1]: p[0] for p in _ORDER_RESPONSE_CODE_TO_STATUS.items()} + +_STATUS_TO_LINE_STATUS = {p[1]: p[0] for p in _ORDER_LINE_STATUS_TO_STATUS.items()} + + +class TestOrderResponseImport(TestOrderResponseImportCommon): + @classmethod + def setUpClass(cls): + super(TestOrderResponseImport, cls).setUpClass() + with file_open( + "purchase_order_import_ubl/tests/files/order_response_tmpl.xml", + "rb", + ) as f: + cls.order_response_xml = f.read() + + def test_01(self): + """ + Data: + An UBL2 OrderResponse with all the information expected by the + parser + Test case: + Convert to xml document to the internal data structure + Expected result: + All the fields are filled into the internal data structure. + """ + xml_content = self.order_response_xml % { + b"order_response_code": _STATUS_TO_RESPONSE_CODE[ + ORDER_RESPONSE_STATUS_ACK + ].encode("utf8"), + b"order_id": self.purchase_order.name.encode("utf8"), + b"line_1_id": str(self.line1.id).encode("utf8"), + b"line_1_qty": str(self.line1.product_qty).encode("utf8"), + b"line_1_backorder_qty": b"0", + b"line_1_status_code": _STATUS_TO_LINE_STATUS[LINE_STATUS_ACCEPTED].encode( + "utf8" + ), + b"line_2_id": str(self.line2.id).encode("utf8"), + b"line_2_qty": str(self.line2.product_qty).encode("utf8"), + b"line_2_backorder_qty": b"0", + b"line_2_status_code": _STATUS_TO_LINE_STATUS[LINE_STATUS_ACCEPTED].encode( + "utf8" + ), + } + result = self.OrderResponseImport.parse_order_response(xml_content, "test.xml") + attachments = result.pop("attachments") + self.assertTrue(attachments.get("test.xml")) + expected = { + "status": ORDER_RESPONSE_STATUS_ACK, + "company": {"vat": "BE0421801233"}, + "currency": {"iso": "EUR"}, + "date": "2020-02-04", + "chatter_msg": [], + "lines": [ + { + "status": LINE_STATUS_ACCEPTED, + "backorder_qty": 0, + "qty": self.line1.product_qty, + "note": "line_1 Note1\nline_1 Note2", + "line_id": str(self.line1.id), + "uom": {"unece_code": "C62"}, + }, + { + "status": LINE_STATUS_ACCEPTED, + "backorder_qty": 0, + "qty": self.line2.product_qty, + "note": "line_2 Note1\nline_2 Note2", + "line_id": str(self.line2.id), + "uom": {"unece_code": "C62"}, + }, + ], + "note": "Note1\nNote2", + "time": "22:10:30", + "supplier": {"vat": "BE0401953350"}, + "ref": str(self.purchase_order.name), + } + self.assertDictEqual(expected, result) diff --git a/purchase_order_import_ubl/tests/test_ubl_order_import.py b/purchase_order_import_ubl/tests/test_ubl_order_import.py new file mode 100644 index 0000000000..ede59eee86 --- /dev/null +++ b/purchase_order_import_ubl/tests/test_ubl_order_import.py @@ -0,0 +1,39 @@ +# © 2016-2017 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import base64 + +from odoo.tests.common import TransactionCase +from odoo.tools import file_open + + +class TestUblOrderImport(TransactionCase): + def test_ubl_order_import(self): + # Modify partner of used purchase order + self.env.ref("purchase.purchase_order_4").write( + {"partner_id": self.env.ref("purchase_order_import_ubl.deltapc").id} + ) + tests = { + "quote-PO00004.pdf": { + "po_to_update": self.env.ref("purchase.purchase_order_4"), + "incoterm": self.env.ref("purchase_order_import_ubl.incoterm_DDU"), + }, + } + poio = self.env["purchase.order.import"] + for filename, res in tests.items(): + po = res["po_to_update"] + + f = file_open("purchase_order_import_ubl/tests/files/" + filename, "rb") + quote_file = f.read() + wiz = poio.with_context( + active_model="purchase.order", active_id=po.id + ).create( + { + "quote_file": base64.b64encode(quote_file), + "quote_filename": filename, + } + ) + f.close() + self.assertEqual(wiz.purchase_id, po) + wiz.update_rfq_button() + self.assertEqual(po.incoterm_id, res["incoterm"]) diff --git a/purchase_order_import_ubl/wizard/__init__.py b/purchase_order_import_ubl/wizard/__init__.py new file mode 100644 index 0000000000..5d8ab4abcd --- /dev/null +++ b/purchase_order_import_ubl/wizard/__init__.py @@ -0,0 +1,2 @@ +from . import purchase_order_import +from . import order_response_import diff --git a/purchase_order_import_ubl/wizard/order_response_import.py b/purchase_order_import_ubl/wizard/order_response_import.py new file mode 100644 index 0000000000..87c1129c0f --- /dev/null +++ b/purchase_order_import_ubl/wizard/order_response_import.py @@ -0,0 +1,171 @@ +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import _, api, models +from odoo.exceptions import UserError + +from odoo.addons.purchase_order_import.wizard.order_response_import import ( + LINE_STATUS_ACCEPTED, + LINE_STATUS_AMEND, + LINE_STATUS_REJECTED, + ORDER_RESPONSE_STATUS_ACCEPTED, + ORDER_RESPONSE_STATUS_ACK, + ORDER_RESPONSE_STATUS_CONDITIONAL, + ORDER_RESPONSE_STATUS_REJECTED, +) + +logger = logging.getLogger(__name__) + + +_ORDER_RESPONSE_CODE_TO_STATUS = { + "AB": ORDER_RESPONSE_STATUS_ACK, + "AP": ORDER_RESPONSE_STATUS_ACCEPTED, + "RE": ORDER_RESPONSE_STATUS_REJECTED, + "CA": ORDER_RESPONSE_STATUS_CONDITIONAL, +} + +_ORDER_LINE_STATUS_TO_STATUS = { + "5": LINE_STATUS_ACCEPTED, + "7": LINE_STATUS_REJECTED, + "3": LINE_STATUS_AMEND, +} + + +class OrderResponseImport(models.TransientModel): + _name = "order.response.import" + _inherit = ["order.response.import", "base.ubl"] + + @api.model + def parse_xml_order_document(self, xml_root): + start_tag = "{urn:oasis:names:specification:ubl:schema:xsd:" + if xml_root.tag == start_tag + "OrderResponse-2}OrderResponse": + return self.parse_ubl_order_response(xml_root) + else: + return super(OrderResponseImport, self).parse_xml_order_document(xml_root) + + @api.model + def parse_note_path(self, note_xpath): + return "\n".join([n.text for n in note_xpath or [] if n.text]) + + @api.model + def parse_response_code(self, xml_root, ns): + code_xpath = xml_root.xpath( + "/main:OrderResponse/cbc:OrderResponseCode", namespaces=ns + ) + code = code_xpath and len(code_xpath) and code_xpath[0].text + status = _ORDER_RESPONSE_CODE_TO_STATUS.get(code) + if not status: + raise UserError(_("Unknown response code found '%s'") % code) + return status + + @api.model + def parse_line_status_code(self, line, ns): + code_xpath = line.xpath("cbc:LineStatusCode", namespaces=ns) + code = code_xpath and len(code_xpath) and code_xpath[0].text + status = _ORDER_LINE_STATUS_TO_STATUS.get(code) + if not status: + raise UserError(_("Unsupported line status code found '%s'") % code) + return status + + @api.model + def parse_ubl_order_response_line(self, line, ns): + line_item = line.xpath("cac:LineItem", namespaces=ns)[0] + line_id_xpath = line_item.xpath("cbc:ID", namespaces=ns) + qty_xpath = line_item.xpath("cbc:Quantity", namespaces=ns) + qty = float(qty_xpath[0].text) + note_xpath = line_item.xpath("cbc:Note", namespaces=ns) + backorder_qty_xpath = line_item.xpath( + "cbc:MaximumBackorderQuantity", namespaces=ns + ) + backorder_qty = None + if backorder_qty_xpath and len(backorder_qty_xpath): + backorder_qty = float(backorder_qty_xpath[0].text) + + res_line = { + "line_id": line_id_xpath[0].text, + "qty": qty, + "uom": {"unece_code": qty_xpath[0].attrib.get("unitCode")}, + "note": self.parse_note_path(note_xpath), + "status": self.parse_line_status_code(line_item, ns), + "backorder_qty": backorder_qty, + } + return res_line + + # Format of parsed order response + # { + # 'ref': 'SO01234' # the buyer party identifier + # # (specified into the Order document -> po's name) + # 'supplier': {'vat': 'FR25499247138'}, + # 'company': {'vat': 'FR12123456789'}, # Only used to check we are not + # # importing the quote in the + # # wrong company by mistake + # 'status': 'acknowledgement | accepted | rejected | + # conditionally_accepted' + # 'currency': {'iso': 'EUR', 'symbol': u'€'}, + # 'note': 'some notes', + # 'chatter_msg': ['msg1', 'msg2'] + # 'lines': [{ + # 'id': 123456, + # 'qty': 2.5, + # 'uom': {'unece_code': 'C62'}, + # 'status': 5, + # 'note': 'my note' + # 'backorder_qty: None # if provided and qty != expected + # # the backorder qty will be delivered + # # in a next shipping + # }] + @api.model + def parse_ubl_order_response(self, xml_root): + ns = xml_root.nsmap + main_xmlns = ns.pop(None) + ns["main"] = main_xmlns + date_xpath = xml_root.xpath("/main:OrderResponse/cbc:IssueDate", namespaces=ns) + time_xpath = xml_root.xpath("/main:OrderResponse/cbc:IssueTime", namespaces=ns) + order_reference_xpath = xml_root.xpath( + "/main:OrderResponse/cac:OrderReference/cbc:ID", namespaces=ns + ) + + currency_xpath = xml_root.xpath( + "/main:OrderResponse/cbc:DocumentCurrencyCode", namespaces=ns + ) + currency_code = False + if currency_xpath: + currency_code = currency_xpath[0].text + else: + currency_xpath = xml_root.xpath("//cbc:LineExtensionAmount", namespaces=ns) + if currency_xpath: + currency_code = currency_xpath[0].attrib.get("currencyID") + supplier_xpath = xml_root.xpath( + "/main:OrderResponse/cac:SellerSupplierParty", namespaces=ns + ) + supplier_dict = self.ubl_parse_supplier_party(supplier_xpath[0], ns) + # We only take the "official references" for supplier_dict + supplier_dict = {"vat": supplier_dict.get("vat")} + customer_xpath_party = xml_root.xpath( + "/main:OrderResponse/cac:BuyerCustomerParty/cac:Party", + namespaces=ns, + ) + company_dict_full = self.ubl_parse_party(customer_xpath_party[0], ns) + company_dict = {} + # We only take the "official references" for company_dict + if company_dict_full.get("vat"): + company_dict = {"vat": company_dict_full["vat"]} + note_xpath = xml_root.xpath("/main:OrderResponse/cbc:Note", namespaces=ns) + lines_xpath = xml_root.xpath("/main:OrderResponse/cac:OrderLine", namespaces=ns) + res_lines = [] + for line in lines_xpath: + res_lines.append(self.parse_ubl_order_response_line(line, ns)) + res = { + "ref": order_reference_xpath[0].text, + "supplier": supplier_dict, + "company": company_dict, + "currency": {"iso": currency_code}, + "date": len(date_xpath) and date_xpath[0].text, + "time": len(time_xpath) and time_xpath[0].text, + "status": self.parse_response_code(xml_root, ns), + "note": self.parse_note_path(note_xpath), + "lines": res_lines, + } + return res diff --git a/purchase_order_import_ubl/wizard/purchase_order_import.py b/purchase_order_import_ubl/wizard/purchase_order_import.py new file mode 100644 index 0000000000..01e1404273 --- /dev/null +++ b/purchase_order_import_ubl/wizard/purchase_order_import.py @@ -0,0 +1,107 @@ +# © 2016-2017 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api, models +from odoo.tools import float_is_zero + +logger = logging.getLogger(__name__) + + +class PurchaseOrderImport(models.TransientModel): + _name = "purchase.order.import" + _inherit = ["purchase.order.import", "base.ubl"] + + @api.model + def parse_xml_quote(self, xml_root): + start_tag = "{urn:oasis:names:specification:ubl:schema:xsd:" + if xml_root.tag == start_tag + "Quotation-2}Quotation": + return self.parse_ubl_quote(xml_root) + else: + return super(PurchaseOrderImport, self).parse_xml_quote(xml_root) + + @api.model + def parse_ubl_quote_line(self, line, ns): + qty_prec = self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ) + line_item = line.xpath("cac:LineItem", namespaces=ns)[0] + # line_id_xpath = line_item.xpath('cbc:ID', namespaces=ns) + # line_id = line_id_xpath[0].text + qty_xpath = line_item.xpath("cbc:Quantity", namespaces=ns) + qty = float(qty_xpath[0].text) + price_unit = 0.0 + subtotal_without_tax_xpath = line_item.xpath( + "cbc:LineExtensionAmount", namespaces=ns + ) + if subtotal_without_tax_xpath: + subtotal_without_tax = float(subtotal_without_tax_xpath[0].text) + if not float_is_zero(qty, precision_digits=qty_prec): + price_unit = subtotal_without_tax / qty + else: + price_xpath = line_item.xpath("cac:Price/cbc:PriceAmount", namespaces=ns) + if price_xpath: + price_unit = float(price_xpath[0].text) + res_line = { + "product": self.ubl_parse_product(line_item, ns), + "qty": qty, + "uom": {"unece_code": qty_xpath[0].attrib.get("unitCode")}, + "price_unit": price_unit, + } + return res_line + + @api.model + def parse_ubl_quote(self, xml_root): + ns = xml_root.nsmap + main_xmlns = ns.pop(None) + ns["main"] = main_xmlns + date_xpath = xml_root.xpath("/main:Quotation/cbc:IssueDate", namespaces=ns) + currency_xpath = xml_root.xpath( + "/main:Quotation/cbc:PricingCurrencyCode", namespaces=ns + ) + currency_code = False + if currency_xpath: + currency_code = currency_xpath[0].text + else: + currency_xpath = xml_root.xpath("//cbc:LineExtensionAmount", namespaces=ns) + if currency_xpath: + currency_code = currency_xpath[0].attrib.get("currencyID") + supplier_xpath = xml_root.xpath( + "/main:Quotation/cac:SellerSupplierParty", namespaces=ns + ) + supplier_dict = self.ubl_parse_supplier_party(supplier_xpath[0], ns) + customer_xpath_party = xml_root.xpath( + "/main:Quotation/cac:BuyerCustomerParty/cac:Party", namespaces=ns + ) + company_dict_full = self.ubl_parse_party(customer_xpath_party[0], ns) + company_dict = {} + # We only take the "official references" for company_dict + if company_dict_full.get("vat"): + company_dict = {"vat": company_dict_full["vat"]} + delivery_term_xpath = xml_root.xpath( + "/main:Quotation/cac:DeliveryTerms", namespaces=ns + ) + if delivery_term_xpath: + incoterm_dict = self.ubl_parse_incoterm(delivery_term_xpath[0], ns) + else: + incoterm_dict = {} + note_xpath = xml_root.xpath("/main:Quotation/cbc:Note", namespaces=ns) + lines_xpath = xml_root.xpath("/main:Quotation/cac:QuotationLine", namespaces=ns) + res_lines = [] + for line in lines_xpath: + res_lines.append(self.parse_ubl_quote_line(line, ns)) + # TODO : add charges + res = { + "partner": supplier_dict, + "company": company_dict, + "currency": {"iso": currency_code}, + "date": date_xpath[0].text, + "incoterm": incoterm_dict, + "note": note_xpath and note_xpath[0].text or False, + "lines": res_lines, + } + # Stupid hack to remove invalid VAT of sample files + if res["partner"]["vat"] in ["DK18296799"]: + res["partner"].pop("vat") + return res diff --git a/setup/purchase_order_import_ubl/odoo/addons/purchase_order_import_ubl b/setup/purchase_order_import_ubl/odoo/addons/purchase_order_import_ubl new file mode 120000 index 0000000000..b23d88086c --- /dev/null +++ b/setup/purchase_order_import_ubl/odoo/addons/purchase_order_import_ubl @@ -0,0 +1 @@ +../../../../purchase_order_import_ubl \ No newline at end of file diff --git a/setup/purchase_order_import_ubl/setup.py b/setup/purchase_order_import_ubl/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/purchase_order_import_ubl/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)