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,
+)