From 8691a1c3c42653441de82a96c4efc6863f38eaeb Mon Sep 17 00:00:00 2001
From: thien
Date: Fri, 3 May 2024 09:34:21 +0700
Subject: [PATCH 01/10] [ADD] edi_purchase_edifact_oca
---
edi_purchase_edifact_oca/README.rst | 96 +++
edi_purchase_edifact_oca/__init__.py | 3 +
edi_purchase_edifact_oca/__manifest__.py | 29 +
.../components/__init__.py | 3 +
.../components/generate_edifact_output.py | 21 +
.../components/listener_edifact_output.py | 41 ++
.../components/process_edifact_input.py | 35 +
edi_purchase_edifact_oca/data/edi_backend.xml | 11 +
.../data/edi_exchange_type.xml | 49 ++
edi_purchase_edifact_oca/models/__init__.py | 4 +
.../models/business_document_import.py | 51 ++
.../models/edi_exchange_record.py | 14 +
edi_purchase_edifact_oca/models/purchase.py | 254 +++++++
.../models/res_partner.py | 21 +
.../readme/CONTRIBUTORS.rst | 1 +
edi_purchase_edifact_oca/readme/CREDITS.rst | 3 +
.../readme/DESCRIPTION.rst | 9 +
.../security/ir.model.access.csv | 2 +
.../static/description/index.html | 443 +++++++++++
edi_purchase_edifact_oca/tests/__init__.py | 1 +
.../tests/test_edifact_purchase.py | 275 +++++++
edi_purchase_edifact_oca/views/purchase.xml | 16 +
.../views/res_partner.xml | 17 +
edi_purchase_edifact_oca/wizard/__init__.py | 1 +
.../wizard/purchase_order_import.py | 692 ++++++++++++++++++
.../wizard/purchase_order_import_view.xml | 49 ++
26 files changed, 2141 insertions(+)
create mode 100644 edi_purchase_edifact_oca/README.rst
create mode 100644 edi_purchase_edifact_oca/__init__.py
create mode 100644 edi_purchase_edifact_oca/__manifest__.py
create mode 100644 edi_purchase_edifact_oca/components/__init__.py
create mode 100644 edi_purchase_edifact_oca/components/generate_edifact_output.py
create mode 100644 edi_purchase_edifact_oca/components/listener_edifact_output.py
create mode 100644 edi_purchase_edifact_oca/components/process_edifact_input.py
create mode 100644 edi_purchase_edifact_oca/data/edi_backend.xml
create mode 100644 edi_purchase_edifact_oca/data/edi_exchange_type.xml
create mode 100644 edi_purchase_edifact_oca/models/__init__.py
create mode 100644 edi_purchase_edifact_oca/models/business_document_import.py
create mode 100644 edi_purchase_edifact_oca/models/edi_exchange_record.py
create mode 100644 edi_purchase_edifact_oca/models/purchase.py
create mode 100644 edi_purchase_edifact_oca/models/res_partner.py
create mode 100644 edi_purchase_edifact_oca/readme/CONTRIBUTORS.rst
create mode 100644 edi_purchase_edifact_oca/readme/CREDITS.rst
create mode 100644 edi_purchase_edifact_oca/readme/DESCRIPTION.rst
create mode 100644 edi_purchase_edifact_oca/security/ir.model.access.csv
create mode 100644 edi_purchase_edifact_oca/static/description/index.html
create mode 100644 edi_purchase_edifact_oca/tests/__init__.py
create mode 100644 edi_purchase_edifact_oca/tests/test_edifact_purchase.py
create mode 100644 edi_purchase_edifact_oca/views/purchase.xml
create mode 100644 edi_purchase_edifact_oca/views/res_partner.xml
create mode 100644 edi_purchase_edifact_oca/wizard/__init__.py
create mode 100644 edi_purchase_edifact_oca/wizard/purchase_order_import.py
create mode 100644 edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml
diff --git a/edi_purchase_edifact_oca/README.rst b/edi_purchase_edifact_oca/README.rst
new file mode 100644
index 000000000..dbba976d1
--- /dev/null
+++ b/edi_purchase_edifact_oca/README.rst
@@ -0,0 +1,96 @@
+========================
+EDI PURCHASE EDIFACT OCA
+========================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:d2e2602bb615321d18583caff366876c642564652bdfcd412395f69355aa8d72
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Alpha
+.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi-lightgray.png?logo=github
+ :target: https://github.com/OCA/edi/tree/12.0/edi_purchase_edifact_oca
+ :alt: OCA/edi
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/edi-12-0/edi-12-0-edi_purchase_edifact_oca
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/edi&target_branch=12.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+UN/EDIFACT
+ United Nations rules for Electronic Data Interchange for Administration, Commerce and Transport
+
+This module will support exporting and confirming orders in EDIFACT format.
+
+https://www.stedi.com/edi/edifact/D01B/messages/ORDERS
+https://www.stedi.com/edi/edifact/D96A/messages/ORDERS
+https://www.stedi.com/edi/edifact/D01B/messages/DESADV
+https://www.stedi.com/edi/edifact/D96A/messages/DESADV
+
+.. IMPORTANT::
+ This is an alpha version, the data model and design can change at any time without warning.
+ Only for development or testing purpose, do not use in production.
+ `More details on development status `_
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+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 to smash it by providing a detailed and welcomed
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+~~~~~~~
+
+* Trobz
+
+Contributors
+~~~~~~~~~~~~
+
+* Thien (Vo Hong)
+
+Other credits
+~~~~~~~~~~~~~
+
+The development of this module has been financially supported by:
+
+* Trobz
+
+Maintainers
+~~~~~~~~~~~
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+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.
+
+This module is part of the `OCA/edi `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/edi_purchase_edifact_oca/__init__.py b/edi_purchase_edifact_oca/__init__.py
new file mode 100644
index 000000000..29b138774
--- /dev/null
+++ b/edi_purchase_edifact_oca/__init__.py
@@ -0,0 +1,3 @@
+from . import models
+from . import components
+from . import wizard
diff --git a/edi_purchase_edifact_oca/__manifest__.py b/edi_purchase_edifact_oca/__manifest__.py
new file mode 100644
index 000000000..f45d271b2
--- /dev/null
+++ b/edi_purchase_edifact_oca/__manifest__.py
@@ -0,0 +1,29 @@
+# Copyright 2024 Trobz
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0)
+{
+ "name": "EDI PURCHASE EDIFACT OCA",
+ "summary": "Create and send EDIFACT order files",
+ "version": "12.0.1.0.0",
+ "development_status": "Alpha",
+ "website": "https://github.com/OCA/edi",
+ "author": "Trobz, Odoo Community Association (OCA)",
+ "license": "AGPL-3",
+ "application": False,
+ "installable": True,
+ "depends": [
+ "base_edifact",
+ "stock",
+ "edi_storage_oca",
+ "edi_purchase_oca",
+ "partner_identification_gln",
+ "base_business_document_import",
+ ],
+ "data": [
+ "security/ir.model.access.csv",
+ "views/purchase.xml",
+ "views/res_partner.xml",
+ "data/edi_backend.xml",
+ "data/edi_exchange_type.xml",
+ "wizard/purchase_order_import_view.xml",
+ ],
+}
diff --git a/edi_purchase_edifact_oca/components/__init__.py b/edi_purchase_edifact_oca/components/__init__.py
new file mode 100644
index 000000000..a386caa57
--- /dev/null
+++ b/edi_purchase_edifact_oca/components/__init__.py
@@ -0,0 +1,3 @@
+from . import listener_edifact_output
+from . import generate_edifact_output
+from . import process_edifact_input
diff --git a/edi_purchase_edifact_oca/components/generate_edifact_output.py b/edi_purchase_edifact_oca/components/generate_edifact_output.py
new file mode 100644
index 000000000..77ab19864
--- /dev/null
+++ b/edi_purchase_edifact_oca/components/generate_edifact_output.py
@@ -0,0 +1,21 @@
+# Copyright 2024 Trobz
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.addons.component.core import Component
+
+
+class EDIExchangeEDIFACTOutGenerate(Component):
+ _name = "edi.output.edifact.out.generate"
+ _inherit = "edi.component.output.mixin"
+ _usage = "output.generate.edifact"
+
+ def generate(self):
+ data = False
+ exchange_record = self.exchange_record
+
+ if exchange_record:
+ if exchange_record.model == "purchase.order" and exchange_record.res_id:
+ order = self.env["purchase.order"].browse(exchange_record.res_id)
+ if order:
+ data = order.edifact_purchase_generate_data(exchange_record)
+ return data
diff --git a/edi_purchase_edifact_oca/components/listener_edifact_output.py b/edi_purchase_edifact_oca/components/listener_edifact_output.py
new file mode 100644
index 000000000..52896685e
--- /dev/null
+++ b/edi_purchase_edifact_oca/components/listener_edifact_output.py
@@ -0,0 +1,41 @@
+# Copyright 2024 Trobz
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import logging
+
+from odoo.addons.component.core import Component
+
+_logger = logging.getLogger(__name__)
+
+
+class PurchaseOrderEdifactListener(Component):
+ _name = "purchase.order.event.listener.edifact"
+ _inherit = "base.event.listener"
+ _apply_on = ["purchase.order"]
+
+ def on_button_confirm_purchase_order(self, order):
+ if not self._should_create_exchange_record(order):
+ return None
+ exchange_type = self.env.ref(
+ "edi_purchase_edifact_oca.edi_exchange_type_purchase_order_out"
+ )
+ record = exchange_type.backend_id.create_record(
+ exchange_type.code, self._storage_new_exchange_record_vals()
+ )
+ # Set related record
+ record._set_related_record(order)
+ _logger.info(
+ "Exchange record for purchase order %s was created: %s",
+ order.name,
+ record.identifier,
+ )
+
+ def _should_create_exchange_record(self, order):
+ if self.env.context.get("skip_send_edifact", False):
+ return False
+
+ partner = order.partner_id
+ return (partner and partner.edifact_purchase_order_out)
+
+ def _storage_new_exchange_record_vals(self):
+ return {"edi_exchange_state": "new"}
diff --git a/edi_purchase_edifact_oca/components/process_edifact_input.py b/edi_purchase_edifact_oca/components/process_edifact_input.py
new file mode 100644
index 000000000..bb8a2f65d
--- /dev/null
+++ b/edi_purchase_edifact_oca/components/process_edifact_input.py
@@ -0,0 +1,35 @@
+# Copyright 2024 Trobz
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import base64
+from odoo.addons.component.core import Component
+
+
+class EDIExchangeEDIFACTInput(Component):
+
+ _name = "edi.input.process.edifact.input"
+ _inherit = "edi.component.input.mixin"
+ _usage = "input.process.edifact.input"
+
+ def process(self):
+ """Process incoming EDIFACT record and confirm record."""
+ file_content = self.exchange_record._get_file_content()
+ wizard = self.env["purchase.order.import"].create(
+ {
+ "import_type": "edifact",
+ "order_file": base64.b64encode(file_content.encode()),
+ "order_filename": self.exchange_record.exchange_filename,
+ }
+ )
+ file_name = self.exchange_record.exchange_filename
+ action = wizard.import_order_button()
+ if action and action.get("res_model", False):
+ self.exchange_record.update(
+ {
+ "model": action["res_model"],
+ "res_id": action["res_id"],
+ }
+ )
+ self.exchange_record.exchange_filename = file_name
+
+ return True
diff --git a/edi_purchase_edifact_oca/data/edi_backend.xml b/edi_purchase_edifact_oca/data/edi_backend.xml
new file mode 100644
index 000000000..87c85268d
--- /dev/null
+++ b/edi_purchase_edifact_oca/data/edi_backend.xml
@@ -0,0 +1,11 @@
+
+
+
+ EDIFACT
+ edifact
+
+
+ EDIFACT
+
+
+
diff --git a/edi_purchase_edifact_oca/data/edi_exchange_type.xml b/edi_purchase_edifact_oca/data/edi_exchange_type.xml
new file mode 100644
index 000000000..4db1f6738
--- /dev/null
+++ b/edi_purchase_edifact_oca/data/edi_exchange_type.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+ EDIFACT-OUT-ORDER
+ edifact_out_order
+ output
+ D{dt}
+ txt
+ True
+ iso-8859-1
+ strict
+ strict
+
+
+ components:
+ generate:
+ usage: output.generate.edifact
+ env_ctx:
+ msg_type: Picking
+ filename_pattern:
+ force_tz: Europe/Zurich
+ date_pattern: "%Y%m%d%H%M%S%f"
+
+
+
+
+
+
+ EDIFACT-IN-DESPATCH-ADVICE
+ edifact_in_despatch_advice
+ output.*
+ edi
+ input
+
+ True
+ iso-8859-1
+ strict
+ strict
+
+ components:
+ process:
+ usage: input.process.edifact.input
+ env_ctx:
+ msg_type: "EDIFACT Input"
+
+
+
diff --git a/edi_purchase_edifact_oca/models/__init__.py b/edi_purchase_edifact_oca/models/__init__.py
new file mode 100644
index 000000000..baa643a91
--- /dev/null
+++ b/edi_purchase_edifact_oca/models/__init__.py
@@ -0,0 +1,4 @@
+from . import purchase
+from . import res_partner
+from . import business_document_import
+from . import edi_exchange_record
diff --git a/edi_purchase_edifact_oca/models/business_document_import.py b/edi_purchase_edifact_oca/models/business_document_import.py
new file mode 100644
index 000000000..c7bad30dc
--- /dev/null
+++ b/edi_purchase_edifact_oca/models/business_document_import.py
@@ -0,0 +1,51 @@
+# Copyright 2024 Trobz
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import _, api, exceptions, models
+
+
+class BusinessDocumentImport(models.AbstractModel):
+ _inherit = "business.document.import"
+
+ @api.model
+ def _hook_match_partner(self, partner_dict, chatter_msg, domain, order):
+ """
+ 2 types
+ partner_dict = {'gln':""}
+ partner_dict = {'partner': {'gln':""}, 'address':{'country_code':"ES",...}}
+ """
+ partner = partner_dict.get("partner", partner_dict)
+ partner_dict.get("address", False)
+ if not partner.get("gln"):
+ return super()._hook_match_partner(partner_dict, chatter_msg, domain, order)
+ party_id = partner["gln"]
+
+ partner_id_category = self.env.ref(
+ "partner_identification_gln.partner_identification_gln_number_category"
+ )
+ if not partner_id_category:
+ raise exceptions.UserError(
+ _(
+ "partner_identification_gln module"
+ " should be installed with a xmlid: "
+ "partner_identification_gln_number_category"
+ )
+ )
+ id_number = self.env["res.partner.id_number"].search(
+ [("category_id", "=", int(partner_id_category)), ("name", "=", party_id)],
+ limit=1,
+ )
+ if not id_number:
+ ctx = partner.get(
+ "edi_ctx", {"order_filename": _("Unknown"), "rff_va": _("Unknown")}
+ )
+ raise exceptions.UserError(
+ _("Partner GLN Code: {party} not found in order file: '{file}' "
+ "from VAT registration number '{vat}'.").format(
+ party=party_id,
+ file=ctx.get("order_filename"),
+ vat=ctx.get("rff_va"),
+ )
+ )
+
+ return id_number.partner_id
diff --git a/edi_purchase_edifact_oca/models/edi_exchange_record.py b/edi_purchase_edifact_oca/models/edi_exchange_record.py
new file mode 100644
index 000000000..a381ad456
--- /dev/null
+++ b/edi_purchase_edifact_oca/models/edi_exchange_record.py
@@ -0,0 +1,14 @@
+# Copyright 2024 Trobz
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import models
+from odoo.addons.edi_oca.models.edi_exchange_record import EDIExchangeRecord
+
+
+class EDIExchangeRecord(models.Model):
+ _inherit = "edi.exchange.record"
+
+ _rollback_state_mapping = {
+ **EDIExchangeRecord._rollback_state_mapping,
+ "input_processed": "input_received",
+ }
diff --git a/edi_purchase_edifact_oca/models/purchase.py b/edi_purchase_edifact_oca/models/purchase.py
new file mode 100644
index 000000000..7cb13e8dc
--- /dev/null
+++ b/edi_purchase_edifact_oca/models/purchase.py
@@ -0,0 +1,254 @@
+# Copyright 2024 Trobz
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0)
+
+from odoo import _, models, fields
+from datetime import datetime
+from odoo.exceptions import UserError
+
+
+class PurchaseOrder(models.Model):
+ _inherit = "purchase.order"
+
+ edifact_version = fields.Selection(
+ [
+ ("d96a", "D.96A"),
+ ("d01b", "D.01B"),
+ ],
+ default="d96a",
+ string="Edifact Version",
+ )
+
+ def _replace_edifact_delimiters(self, data):
+ edifact_delimiters = {"+", ":", ".", "?", "*"}
+
+ def replace_in_string(s):
+ return "".join(
+ char if char not in edifact_delimiters else "_" for char in s
+ )
+
+ def process_element(element):
+ if isinstance(element, str):
+ return (
+ replace_in_string(element)
+ if not element.replace(".", "", 1).isdigit()
+ else element
+ )
+ elif isinstance(element, (list, tuple)):
+ result = map(process_element, element)
+ return type(element)(result)
+ else:
+ return element
+
+ return process_element(data)
+
+ def edifact_purchase_generate_data(self, exchange_record=None):
+ self.ensure_one()
+ edifact_model = self.env["base.edifact"]
+ lines = []
+ interchange = self._edifact_purchase_get_interchange()
+
+ header = self._edifact_purchase_get_header(exchange_record)
+ product, vals = self._edifact_purchase_get_product()
+ summary = self._edifact_purchase_get_summary(vals, exchange_record)
+ lines += header + product + summary
+ for segment in lines:
+ segment = self._replace_edifact_delimiters(segment)
+ interchange.add_segment(edifact_model.create_segment(*segment))
+ return f"UNA:+.? '{interchange.serialize()}"
+
+ def _edifact_purchase_get_interchange(self):
+ id_number = self.env["res.partner.id_number"]
+ # Because the person placing the order will be
+ # the representative of the ordering company.
+ # So we will check the code of the company's `id number` here
+ sender = id_number.search(
+ [("partner_id", "=", self.user_id.company_id.partner_id.id)], limit=1
+ )
+ recipient = id_number.search([("partner_id", "=", self.partner_id.id)], limit=1)
+ # if current supplier does not have `Id number`
+ # then check current supplier's parent
+ if not recipient and self.partner_id.parent_id:
+ recipient = id_number.search(
+ [("partner_id", "=", self.partner_id.parent_id.id)], limit=1
+ )
+ if not sender or not recipient:
+ raise UserError(_("Partner is not allowed to use the feature."))
+ sender_edifact = [sender.name, "14"]
+ recipient_edifact = [recipient.name, "14"]
+ syntax_identifier = ["UNOC", "3"]
+
+ return self.env["base.edifact"].create_interchange(
+ sender_edifact, recipient_edifact, self.name, syntax_identifier
+ )
+
+ def _edifact_purchase_get_address(self, partner):
+ # We apply the same logic as:
+ # https://github.com/OCA/edi/blob/
+ # c41829a8d986c6751c07299807c808d15adbf4db/base_ubl/models/ubl.py#L39
+
+ # oca/partner-contact/partner_address_street3 is installed
+ if hasattr(partner, "street3"):
+ return partner.street3 or partner.street2 or partner.street
+ else:
+ return partner.street2 or partner.street
+
+ def _edifact_get_name_and_address(self, partner, code, id_number=""):
+ street = self._edifact_purchase_get_address(partner)
+ return [
+ # partner information
+ (
+ "NAD",
+ code,
+ [id_number, "", "9"],
+ "",
+ partner.commercial_company_name,
+ [street, ""],
+ partner.city,
+ partner.state_id.name,
+ partner.zip,
+ partner.country_id.code,
+ ),
+ # VAT registration number
+ ("RFF", ["VA", partner.vat]),
+ # Purchasing contact
+ ("CTA", "PD", [partner.name, ""]),
+ ]
+
+ def _edifact_purchase_get_header(self, exchange_record=None):
+ today = datetime.now().date().strftime("%Y%m%d")
+ id_number = self.env["res.partner.id_number"]
+ buyer_id_number = id_number.search(
+ [("partner_id", "=", self.user_id.company_id.partner_id.id)], limit=1
+ )
+ seller_id_number = id_number.search([("partner_id", "=", self.partner_id.id)])
+ if not seller_id_number and self.partner_id.parent_id:
+ seller_id_number = id_number.search(
+ [("partner_id", "=", self.partner_id.parent_id.id)], limit=1
+ )
+ message_id = exchange_record.id if exchange_record else ""
+ warehouse_name = (
+ self.picking_type_id.warehouse_id.name if self.picking_type_id else ""
+ )
+
+ header = [
+ ("UNH", message_id, ["ORDERS", "D", "96A", "UN", "EAN008"]),
+ # Order
+ ("BGM", ["220", "", "9", "ORDERS"], self.name, "9"),
+ # 137: Document/message date/time
+ ("DTM", ["137", today, "102"]),
+ # 2: Delivery date/time, requested
+ ("DTM", ["2", self.date_planned.strftime("%Y%m%d"), "102"]),
+ # Delivery note number
+ ("RFF", ["DQ", self.id]),
+ # Telephone
+ ("COM", [self.user_id.partner_id.phone or "", "TE"]),
+ # Reference currency
+ ("CUX", ["2", self.currency_id.name, "4"]),
+ # Rate of exchange
+ ("DTM", ["134", today, "102"]),
+ # Main-carriage transport
+ ("TDT", "20", "", "30", "31"), # TODO: add detail of transport
+ # Warehouse
+ (
+ "LOC",
+ "18",
+ [warehouse_name, "", "", "", ""],
+ ),
+ ]
+ if self.edifact_version == "d01b":
+ header[0] = (
+ "UNH",
+ message_id,
+ ["ORDERS", "D", "01B", "UN", "EAN010"],
+ )
+
+ if not self.user_id.partner_id.phone:
+ header = header[:5] + header[6:]
+
+ header = (
+ header[:5]
+ + self._edifact_get_name_and_address(
+ self.user_id.partner_id, "BY", buyer_id_number.name
+ )
+ + self._edifact_get_name_and_address(
+ self.partner_id, "SU", seller_id_number.name
+ )
+ + self._edifact_get_name_and_address(
+ self.picking_type_id.warehouse_id.partner_id, "DP", buyer_id_number.name
+ )
+ + header[5:]
+ )
+
+ return header
+
+ def _edifact_purchase_get_product(self):
+ number = 0
+ segments = []
+ vals = {}
+ tax = {}
+ for line in self.order_line:
+ number += 1
+ product_tax = 0
+ product = line.product_id
+ if line.taxes_id and line.taxes_id.amount_type == "percent":
+ product_tax = line.taxes_id.amount
+ if product_tax not in tax:
+ tax[product_tax] = line.price_total
+ else:
+ tax[product_tax] += line.price_total
+ product_type = "EN"
+ if self.edifact_version == "d01b":
+ product_type = "SRV"
+ product_seg = [
+ # Line item number
+ ("LIN", number, "", [product.barcode, product_type]),
+ # Product identification of Supplier's article number
+ ("PIA", "1", [product.default_code, "SA", "", "91"]),
+ # Product identification of Buyer's part number
+ ("PIA", "1", [product.default_code, "BP", "", "92"]),
+ # Ordered quantity
+ ("QTY", ["21", line.product_uom_qty, ""]),
+ # Quantity per pack
+ (
+ "QTY",
+ [
+ "52",
+ line.package_qty if "package_qty" in line._fields else "",
+ "",
+ ],
+ ),
+ # Delivery date/time, requested
+ ("DTM", ["2", line.date_planned.strftime("%Y%m%d"), "102"]),
+ # Line item amount
+ ("MOA", ["203", line.price_total]),
+ # Mutually defined
+ # ("FTX", "ZZZ", "1", ["", "", "91"]),
+ # Calculation net
+ ("PRI", ["AAA", round(line.price_total / line.product_uom_qty, 2)]),
+ ("PRI", ["AAB", round(line.price_total / line.product_uom_qty, 2)]),
+ # Order number of line item
+ ("RFF", ["PL", self.id]),
+ # TODO: This place can add delivery to multiple locations
+ # Tax information
+ ("TAX", "7", "VAT", "", "", ["", "", "", product_tax]),
+ ]
+ segments.extend(product_seg)
+ # Pass tax information to summary
+ # TODO: can be used to create TAX, MOA segments
+ vals["tax"] = tax
+ vals["total_line_item"] = number
+ return segments, vals
+
+ def _edifact_purchase_get_summary(self, vals, exchange_record=None):
+ message_id = exchange_record.id if exchange_record else ""
+ total_line_item = vals["total_line_item"]
+ len_header = 22
+ if not self.user_id.partner_id.phone:
+ len_header -= 1
+ summary = [
+ ("UNS", "S"),
+ # Number of line items in message
+ ("CNT", ["2", total_line_item]),
+ ("UNT", len_header + 9 * total_line_item, message_id),
+ ]
+ return summary
diff --git a/edi_purchase_edifact_oca/models/res_partner.py b/edi_purchase_edifact_oca/models/res_partner.py
new file mode 100644
index 000000000..c2f0409f7
--- /dev/null
+++ b/edi_purchase_edifact_oca/models/res_partner.py
@@ -0,0 +1,21 @@
+# Copyright 2024 Trobz
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+from odoo import fields, models
+
+
+class ResPartner(models.Model):
+ _inherit = "res.partner"
+
+ edifact_purchase_order_out = fields.Boolean(
+ string="Export Purchase Order with EDIFACT",
+ default=False
+ )
+
+ edifact_despatch_advice_ignore_lines_with_unknown_products = fields.Boolean(
+ default=True,
+ help="""When this option is enabled,
+ we will ignore lines with unknown products
+ when processing a Despatch Advice of this supplier.
+ """,
+ )
diff --git a/edi_purchase_edifact_oca/readme/CONTRIBUTORS.rst b/edi_purchase_edifact_oca/readme/CONTRIBUTORS.rst
new file mode 100644
index 000000000..42455569c
--- /dev/null
+++ b/edi_purchase_edifact_oca/readme/CONTRIBUTORS.rst
@@ -0,0 +1 @@
+* Thien (Vo Hong)
diff --git a/edi_purchase_edifact_oca/readme/CREDITS.rst b/edi_purchase_edifact_oca/readme/CREDITS.rst
new file mode 100644
index 000000000..b777d8a49
--- /dev/null
+++ b/edi_purchase_edifact_oca/readme/CREDITS.rst
@@ -0,0 +1,3 @@
+The development of this module has been financially supported by:
+
+* Trobz
diff --git a/edi_purchase_edifact_oca/readme/DESCRIPTION.rst b/edi_purchase_edifact_oca/readme/DESCRIPTION.rst
new file mode 100644
index 000000000..81a0bb18d
--- /dev/null
+++ b/edi_purchase_edifact_oca/readme/DESCRIPTION.rst
@@ -0,0 +1,9 @@
+UN/EDIFACT
+ United Nations rules for Electronic Data Interchange for Administration, Commerce and Transport
+
+This module will support exporting and confirming orders in EDIFACT format.
+
+https://www.stedi.com/edi/edifact/D01B/messages/ORDERS
+https://www.stedi.com/edi/edifact/D96A/messages/ORDERS
+https://www.stedi.com/edi/edifact/D01B/messages/DESADV
+https://www.stedi.com/edi/edifact/D96A/messages/DESADV
diff --git a/edi_purchase_edifact_oca/security/ir.model.access.csv b/edi_purchase_edifact_oca/security/ir.model.access.csv
new file mode 100644
index 000000000..ee5b62eeb
--- /dev/null
+++ b/edi_purchase_edifact_oca/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_purchase_order_import,access_purchase_order_import,model_purchase_order_import,base.group_user,1,1,1,1
diff --git a/edi_purchase_edifact_oca/static/description/index.html b/edi_purchase_edifact_oca/static/description/index.html
new file mode 100644
index 000000000..c8a0e64f7
--- /dev/null
+++ b/edi_purchase_edifact_oca/static/description/index.html
@@ -0,0 +1,443 @@
+
+
+
+
+
+
+EDI PURCHASE EDIFACT OCA
+
+
+
+
+
+
diff --git a/edi_purchase_edifact_oca/tests/__init__.py b/edi_purchase_edifact_oca/tests/__init__.py
new file mode 100644
index 000000000..f80e62715
--- /dev/null
+++ b/edi_purchase_edifact_oca/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_edifact_purchase
diff --git a/edi_purchase_edifact_oca/tests/test_edifact_purchase.py b/edi_purchase_edifact_oca/tests/test_edifact_purchase.py
new file mode 100644
index 000000000..d78c8f03e
--- /dev/null
+++ b/edi_purchase_edifact_oca/tests/test_edifact_purchase.py
@@ -0,0 +1,275 @@
+# Copyright 2024 Trobz
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import base64
+from odoo.addons.component.tests.common import TransactionComponentCase
+from odoo.addons.edi_oca.tests.common import EDIBackendTestMixin
+
+from odoo import fields
+from base64 import b64encode
+import re
+
+
+class TestEdifactPurchaseOrder(TransactionComponentCase, EDIBackendTestMixin):
+ def setUp(self):
+ super(TestEdifactPurchaseOrder, self).setUp()
+ self.env = self.env(context=dict(self.env.context, tracking_disable=True))
+ self.base_edifact_model = self.env["base.edifact"]
+ self.company = self.env.ref("base.main_company")
+ self.product_1 = self.env.ref("product.product_product_1")
+ self.product_1.default_code = "FURN_66668"
+ self.product_1.type = "product"
+ self.product_2 = self.env.ref("product.product_product_4")
+ self.product_2.default_code = "FURN_88558"
+ self.product_3 = self.env.ref("product.product_product_5")
+ self.product_3.default_code = "FURN_667777"
+ self.product_3.type = "product"
+ partner_id_number = self.env["res.partner.id_number"]
+ self.partner_1 = self.env.ref("base.res_partner_1")
+ self.partner_1.edifact_purchase_order_out = True
+ self.partner_2 = self.env.ref("base.res_partner_12")
+ self.exc_type_input = self.env.ref(
+ "edi_purchase_edifact_oca.edi_exchange_type_purchase_order_input"
+ )
+ partner_id_number_data_1 = {
+ "category_id": self.env.ref(
+ "partner_identification_gln.partner_identification_gln_number_category"
+ ).id,
+ "partner_id": self.partner_1.id,
+ "name": "9780201379624",
+ }
+
+ partner_id_number_data_2 = {
+ "category_id": self.env.ref(
+ "partner_identification_gln.partner_identification_gln_number_category"
+ ).id,
+ "partner_id": self.partner_2.id,
+ "name": "9780201379174",
+ }
+ partner_id_number.create(partner_id_number_data_1)
+ partner_id_number.create(partner_id_number_data_2)
+ self.env.user.partner_id = self.partner_2
+ self.env.user.company_id.partner_id = self.partner_2
+
+ self.datetime = fields.Datetime.now()
+ self.purchase = self.env["purchase.order"].create(
+ {
+ "partner_id": self.partner_1.id,
+ "date_order": self.datetime,
+ "date_planned": self.datetime,
+ }
+ )
+ self.po_line1 = self.purchase.order_line.create(
+ {
+ "order_id": self.purchase.id,
+ "product_id": self.product_1.id,
+ "name": self.product_1.name,
+ "date_planned": self.datetime,
+ "product_qty": 12,
+ "product_uom": self.product_1.uom_id.id,
+ "price_unit": 42.42,
+ }
+ )
+ self.po_line2 = self.purchase.order_line.create(
+ {
+ "order_id": self.purchase.id,
+ "product_id": self.product_2.id,
+ "name": self.product_2.name,
+ "date_planned": self.datetime,
+ "product_qty": 2,
+ "product_uom": self.product_2.uom_id.id,
+ "price_unit": 12.34,
+ }
+ )
+
+ def test_edifact_purchase_generate_data(self):
+ edifact_data = self.purchase.edifact_purchase_generate_data()
+ self.assertTrue(edifact_data)
+ self.assertEqual(isinstance(edifact_data, str), True)
+
+ def test_edifact_purchase_get_interchange(self):
+ interchange = self.purchase._edifact_purchase_get_interchange()
+ self.assertEqual(interchange.sender, ["9780201379174", "14"])
+ self.assertEqual(interchange.recipient, ["9780201379624", "14"])
+ self.assertEqual(interchange.syntax_identifier, ["UNOC", "3"])
+
+ def test_edifact_purchase_get_header(self):
+ segments = self.purchase._edifact_purchase_get_header()
+ seg = ("UNH", "", ["ORDERS", "D", "96A", "UN", "EAN008"])
+ self.assertEqual(segments[0], seg)
+ self.assertEqual(len(segments), 19)
+
+ def test_edifact_purchase_get_product(self):
+ segments, vals = self.purchase._edifact_purchase_get_product()
+ self.assertEqual(len(segments), 22)
+ self.assertEqual(len(vals), 2)
+
+ def test_edifact_purchase_get_summary(self):
+ vals = {"total_line_item": 2}
+ segments = self.purchase._edifact_purchase_get_summary(vals)
+ self.assertEqual(len(segments), 3)
+
+ def test_edifact_purchase_get_address(self):
+ partner = self.purchase.partner_id
+ if hasattr(partner, "street3"):
+ partner.street3 = "Address"
+ self.assertEqual(
+ self.purchase._edifact_purchase_get_address(partner), partner.street3
+ )
+ else:
+ self.assertEqual(
+ self.purchase._edifact_purchase_get_address(partner), partner.street
+ )
+
+ def test_action_confirm(self):
+ self.purchase.button_confirm()
+ exchange_record = self.env["edi.exchange.record"].search(
+ [
+ ("model", "=", "purchase.order"),
+ ("res_id", "=", self.purchase.id),
+ ]
+ )
+ exchange_record.action_exchange_generate()
+ self.assertNotEqual(exchange_record.exchange_file, False)
+ self.assertEqual(exchange_record.edi_exchange_state, "output_pending")
+ self.assertEqual(exchange_record.exchanged_on, False)
+
+ # Compare data after generating
+ expected_data = self.purchase.edifact_purchase_generate_data()
+ expected_data = expected_data.replace(
+ "UNH++ORDERS", f"UNH+{exchange_record.id}+ORDERS"
+ )
+ # Add edi_exchange_record.id to UNT segment in expected_data
+ pattern = r"(UNT\+[^']*\+)[^']*(?=(?:'|$))"
+ expected_data = re.sub(
+ pattern,
+ lambda match: f"{match.group(1)}{exchange_record.id}",
+ expected_data,
+ )
+ self.assertEqual(
+ exchange_record.exchange_file, b64encode(expected_data.encode())
+ )
+
+ def test_edifact_purchase_wizard_import(self):
+ self.partner_1.edifact_purchase_order_out = False
+ edifact_data = self.purchase.edifact_purchase_generate_data()
+ self.assertTrue(edifact_data)
+ self.assertEqual(isinstance(edifact_data, str), True)
+ self.purchase.button_confirm()
+ edifact_data = edifact_data.replace("UNH++ORDERS", "UNH+1+DESADV")
+ edifact_data = edifact_data.replace(
+ "'UNS+S'",
+ (
+ "'LIN+3++:EN'PIA+1+FURN_667777:SA::91'PIA+1+FURN_667777:BP::92'"
+ "QTY+21:4.0:'QTY+52::'DTM+2:20250110:102'MOA+203:0.0'PRI+AAA:0.0'"
+ "PRI+AAB:0.0'RFF+PL:338'TAX+7+VAT+++:::0'UNS+S'"
+ )
+ )
+ wiz = self.env["purchase.order.import"].create(
+ {
+ "import_type": "edifact",
+ "order_file": base64.b64encode(edifact_data.encode()),
+ "order_filename": "test_edifact.txt",
+ }
+ )
+ self.assertEqual(self.purchase.state, "purchase")
+ # Import file to confirm purchase order
+ wiz.import_order_button()
+
+ new_order = self.env["purchase.order.line"].search(
+ [("product_id.default_code", "=", self.product_3.default_code)]
+ )
+
+ self.assertTrue(new_order.order_id)
+ self.assertEqual(new_order.move_ids.quantity_done, 4)
+
+ sum_quantity_done = sum(
+ self.purchase.order_line.mapped("move_ids.quantity_done")
+ )
+ self.assertEqual(sum_quantity_done, 14.0)
+
+ def test_edifact_purchase_wizard_import_new_po(self):
+ self.partner_1.edifact_purchase_order_out = False
+ edifact_data = self.purchase.edifact_purchase_generate_data()
+ self.assertTrue(edifact_data)
+ self.assertEqual(isinstance(edifact_data, str), True)
+ self.purchase.button_confirm()
+ self.purchase.picking_ids.action_cancel()
+ edifact_data = edifact_data.replace("UNH++ORDERS", "UNH+1+DESADV")
+
+ wiz = self.env["purchase.order.import"].create(
+ {
+ "import_type": "edifact",
+ "order_file": base64.b64encode(edifact_data.encode()),
+ "order_filename": "test_edifact.txt",
+ }
+ )
+ self.assertEqual(self.purchase.state, "purchase")
+ # Import file to confirm purchase order
+ action = wiz.import_order_button()
+
+ new_order = self.env["purchase.order"].search([("id", "=", action["res_id"])])
+
+ sum_quantity_done = sum(new_order.order_line.mapped("move_ids.quantity_done"))
+
+ self.assertNotEqual(self.purchase.id, new_order.id)
+ self.assertEqual(len(new_order.order_line), 2)
+ self.assertEqual(sum_quantity_done, 14.0)
+
+ def test_edifact_purchase_wizard_import_ignore_unknown(self):
+ self.partner_1.edifact_purchase_order_out = False
+ edifact_data = self.purchase.edifact_purchase_generate_data()
+ self.assertTrue(edifact_data)
+ self.assertEqual(isinstance(edifact_data, str), True)
+ self.purchase.button_confirm()
+ edifact_data = edifact_data.replace("UNH++ORDERS", "UNH+1+DESADV").replace(
+ "LIN+1++:EN'PIA+1+FURN_66668:SA::91", "LIN+1++:EN'PIA+1+FURN_666688:SA::91"
+ )
+
+ wiz = self.env["purchase.order.import"].create(
+ {
+ "import_type": "edifact",
+ "order_file": base64.b64encode(edifact_data.encode()),
+ "order_filename": "test_edifact.txt",
+ }
+ )
+ self.assertEqual(self.purchase.state, "purchase")
+ # Import file to confirm purchase order
+ wiz.import_order_button()
+
+ sum_quantity_done = sum(
+ self.purchase.order_line.mapped("move_ids.quantity_done")
+ )
+ self.assertEqual(sum_quantity_done, 2.0)
+
+ def test_edifact_purchase_exchange_record_input(self):
+ self.partner_1.edifact_purchase_order_out = False
+ edifact_data = self.purchase.edifact_purchase_generate_data()
+ self.assertTrue(edifact_data)
+ self.assertEqual(isinstance(edifact_data, str), True)
+
+ self.purchase.button_confirm()
+ edifact_data = edifact_data.replace("UNH++ORDERS", "UNH+1+DESADV")
+
+ record = self.exc_type_input.backend_id.create_record(
+ self.exc_type_input.code,
+ {
+ "edi_exchange_state": "input_received",
+ "exchange_file": base64.b64encode(edifact_data.encode()),
+ },
+ )
+ record.exchange_filename = "output-1.edi"
+ self.assertEqual(self.purchase.state, "purchase")
+
+ # Run `exchange_process`, It will run through the def `process` of
+ # the component edi.input.process.edifact.input
+ record.action_exchange_process()
+
+ self.assertEqual(record.exchange_filename, "output-1.edi")
+ self.assertEqual(record.res_id, self.purchase.id)
+ self.assertEqual(record.related_name, self.purchase.name)
+
+ sum_quantity_done = sum(
+ self.purchase.order_line.mapped("move_ids.quantity_done")
+ )
+ self.assertEqual(sum_quantity_done, 14.0)
diff --git a/edi_purchase_edifact_oca/views/purchase.xml b/edi_purchase_edifact_oca/views/purchase.xml
new file mode 100644
index 000000000..df590f2df
--- /dev/null
+++ b/edi_purchase_edifact_oca/views/purchase.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ edi.purchase.edifact.oca.purchase.order.form
+ purchase.order
+
+
+
+
+
+
+
+
+
+
diff --git a/edi_purchase_edifact_oca/views/res_partner.xml b/edi_purchase_edifact_oca/views/res_partner.xml
new file mode 100644
index 000000000..69baf68bf
--- /dev/null
+++ b/edi_purchase_edifact_oca/views/res_partner.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ view.partner.form.inherit
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
diff --git a/edi_purchase_edifact_oca/wizard/__init__.py b/edi_purchase_edifact_oca/wizard/__init__.py
new file mode 100644
index 000000000..00c66fca6
--- /dev/null
+++ b/edi_purchase_edifact_oca/wizard/__init__.py
@@ -0,0 +1 @@
+from . import purchase_order_import
diff --git a/edi_purchase_edifact_oca/wizard/purchase_order_import.py b/edi_purchase_edifact_oca/wizard/purchase_order_import.py
new file mode 100644
index 000000000..4956c6592
--- /dev/null
+++ b/edi_purchase_edifact_oca/wizard/purchase_order_import.py
@@ -0,0 +1,692 @@
+# Copyright 2024 Trobz
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import logging
+import mimetypes
+import json
+from base64 import b64decode, b64encode
+from collections import defaultdict
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+from odoo.tools import config, float_is_zero
+
+logger = logging.getLogger(__name__)
+
+
+class PurchaseOrderImport(models.TransientModel):
+ _name = "purchase.order.import"
+ _description = "Purchase Order Import from Files"
+
+ partner_id = fields.Many2one("res.partner", string="Customer")
+
+ import_type = fields.Selection(
+ [("edifact", "EDIFACT")],
+ required=True,
+ default="edifact",
+ help="Select a type which you want to import",
+ )
+
+ order_file = fields.Binary(
+ string="Request for Quotation or Order",
+ required=True,
+ )
+ order_filename = fields.Char(string="Filename")
+
+ def _get_supported_types(self):
+ supported_types = {
+ "edifact": ("text/plain", None),
+ }
+ return supported_types
+
+ def _parse_file(self, filename, filecontent):
+ assert filename, "Missing filename"
+ assert filecontent, "Missing file content"
+ filetype = mimetypes.guess_type(filename)
+ logger.debug("Order file mimetype: %s", filetype)
+ mimetype = filetype[0]
+ supported_types = self._get_supported_types()
+ # Check if the selected import type is supported
+ if self.import_type not in supported_types:
+ raise UserError(_("Please select a valid import type before importing!"))
+
+ # Check if the detected MIME type is supported for the selected import type
+ if mimetype not in supported_types[self.import_type]:
+ raise UserError(
+ _(
+ "This file '%(filename)s' is not recognized as a %(type)s file. "
+ "Please check the file and its extension.",
+ filename=filename,
+ type=self.import_type.upper(),
+ )
+ )
+ if hasattr(self, "parse_%s_order" % self.import_type):
+ return getattr(self, "parse_%s_order" % self.import_type)(filecontent)
+ else:
+ raise UserError(
+ _(
+ "This Import Type is not supported. Did you install "
+ "the module to support this type?"
+ )
+ )
+
+ @api.model
+ def parse_order(self, order_file, order_filename):
+ parsed_order = self._parse_file(order_filename, order_file)
+ logger.debug("Result of order parsing: %s", parsed_order)
+ defaults = (
+ ("attachments", {}),
+ ("chatter_msg", []),
+ )
+ for key, val in defaults:
+ parsed_order.setdefault(key, val)
+
+ parsed_order["attachments"][order_filename] = b64encode(order_file)
+ if (
+ parsed_order.get("company")
+ and not config["test_enable"]
+ and not self._context.get("edi_skip_company_check")
+ ):
+ self.env["business.document.import"]._check_company(
+ parsed_order["company"], parsed_order["chatter_msg"]
+ )
+ return parsed_order
+
+ def import_order_button(self):
+ self.ensure_one()
+ order_file_decoded = b64decode(self.order_file)
+ parsed_order = self.parse_order(order_file_decoded, self.order_filename)
+
+ if not parsed_order.get("lines"):
+ raise UserError(_("This order doesn't have any line !"))
+ order = parsed_order.get("order", False)
+ if order and not order.picking_ids.filtered(
+ lambda picking: picking.state not in ("done", "cancel")
+ ):
+ order = order.copy(default={"order_line": [(5, 0, 0)]})
+ parsed_order["order"] = order
+
+ return self.update_purchase_order(order, parsed_order)
+
+ @api.model
+ def parse_edifact_order(self, filecontent):
+ edifact_model = self.env["base.edifact"]
+ interchange = edifact_model._loads_edifact(filecontent)
+ header = interchange.get_header_segment()
+ # > UNB segment: [['UNOA', '2'], ['5450534000000', '14'],
+ # ['8435337000003', '14'], ['230306', '0435'], '5506']
+
+ msg_type, __ = edifact_model._get_msg_type(interchange)
+
+ supported = ["ORDERS", "DESADV"]
+ if msg_type not in supported:
+ raise UserError(
+ _("{msg_type} document is not a Purchase Order document").format(
+ msg_type=msg_type
+ )
+ )
+
+ bgm = interchange.get_segment("BGM")
+ # Supplier PO number
+ # BGM segment: ['220', '1LP6WZGF', '9']
+ order_ref = bgm[1]
+
+ rd = {
+ # Supplier PO number
+ "order_ref": order_ref,
+ "edi_ctx": {"sender": header[1], "recipient": header[2]},
+ "msg_type": msg_type,
+ "reception_lines": len(list(interchange.get_segments("LIN"))),
+ }
+ parties = self._prepare_edifact_parties(interchange)
+ order_dict = {
+ **rd,
+ **self._prepare_edifact_dates(interchange),
+ **self._prepare_edifact_currencies(interchange),
+ **parties,
+ }
+
+ existing_quotations = self.env["purchase.order"].search(
+ [
+ ("state", "=", "purchase"),
+ ("name", "=", order_dict["order_ref"]),
+ ]
+ )
+ if existing_quotations:
+ order_dict["order"] = existing_quotations[0]
+ else:
+ raise UserError(
+ _("Purchase Order Id {id} is not found").format(
+ id=order_dict["order_ref"]
+ )
+ )
+
+ lines = self._prepare_edifact_lines(interchange, order_dict)
+ if lines:
+ order_dict["lines"] = lines
+ return order_dict
+
+ @api.model
+ def _prepare_edifact_parties(self, interchange):
+ references = self._prepare_edifact_references(interchange)
+ parties = self._prepare_edifact_name_and_address(interchange)
+ if references.get("vat") and parties.get("invoice_to"):
+ # just for check vat
+ if parties["invoice_to"].get("partner"):
+ parties["invoice_to"]["partner"]["rff_va"] = references["vat"]
+ if parties.get("invoice_to") and parties["invoice_to"].get("partner"):
+ newpartner = parties["invoice_to"]["partner"].copy()
+ if parties.get("partner") and parties["partner"].get("gln"):
+ # To see if NAD_BY is different NAD_IV
+ newpartner["gln_by"] = parties["partner"]["gln"]
+ parties["partner"] = newpartner
+ # add context information
+ for pval in parties.values():
+ partner_dict = pval.get("partner", pval)
+ partner_dict["edi_ctx"] = {
+ "order_filename": self.order_filename,
+ }
+ if references.get("vat"):
+ partner_dict["edi_ctx"]["rff_va"] = references["vat"]
+ if parties.get("company"):
+ parties["company"]["edi_ctx"]["vendor_code"] = references.get("vendor_code")
+ if references.get("order_ref"):
+ parties["order_ref"] = references["order_ref"]
+ return parties
+
+ @api.model
+ def _prepare_edifact_dates(self, interchange):
+ dates = defaultdict(dict)
+ edifact_model = self.env["base.edifact"]
+ for seg in interchange.get_segments("DTM"):
+ date_meaning_code = seg[0][0]
+ if date_meaning_code == "137":
+ dates["date"] = edifact_model.map2odoo_date(seg[0])
+ elif date_meaning_code == "63":
+ # latest delivery date
+ dates["delivery_detail"]["validity_date"] = edifact_model.map2odoo_date(
+ seg[0]
+ )
+ elif date_meaning_code == "2":
+ # Date planned
+ dates["delivery_detail"]["date_planned"] = edifact_model.map2odoo_date(
+ seg[0]
+ )
+
+ return dates
+
+ @api.model
+ def _prepare_edifact_references(self, interchange):
+ """
+ RFF segment: [['CR', 'IK142']]
+ """
+ refs = {}
+ for seg in interchange.get_segments("RFF"):
+ reference = seg[0]
+ reference_code = reference[0]
+ if reference_code == "ADE":
+ # ['firstorder','backorder','advantage','nyp']
+ refs["account_reference"] = reference[1]
+ elif reference_code == "CR":
+ # Customer reference Number
+ refs["vendor_code"] = reference[1]
+ elif reference_code == "PD":
+ # Promotion Deal Number
+ # Number assigned by a vendor to a special promotion activity
+ refs["promotion_number"] = reference[1]
+ elif reference_code == "VA":
+ # Unique number assigned by the relevant tax authority to identify a
+ # party for use in relation to Value Added Tax (VAT).
+ refs["vat"] = reference[1]
+ elif reference_code == "ON":
+ # Order reference number
+ refs["order_ref"] = reference[1]
+
+ return refs
+
+ @api.model
+ def _prepare_edifact_name_and_address(self, interchange):
+ nads = {}
+ edifact_model = self.env["base.edifact"]
+ for seg in interchange.get_segments("NAD"):
+ reference_code = seg[0]
+ if reference_code == "BY":
+ # NAD segment: ['BY', ['5450534001649', '', '9']]
+ # Customer (Buyer's GLN)
+ nads["partner"] = edifact_model.map2odoo_partner(seg)
+ elif reference_code == "SU":
+ # Our number of Supplier's GLN
+ # Can be used to check that we are not importing the order
+ # in the wrong company by mistake
+ nads["company"] = edifact_model.map2odoo_partner(seg)
+ elif reference_code == "DP":
+ # Delivery Party
+ nads["ship_to"] = edifact_model.map2odoo_address(seg)
+ elif reference_code == "IV":
+ # Invoice Party
+ nads["invoice_to"] = edifact_model.map2odoo_address(seg)
+ return nads
+
+ @api.model
+ def _prepare_edifact_currencies(self, interchange):
+ currencies = {}
+ edifact_model = self.env["base.edifact"]
+ for seg in interchange.get_segments("CUX"):
+ usage_code = seg[0][0]
+ if usage_code == "2":
+ currencies["currency"] = edifact_model.map2odoo_currency(seg[0])
+ return currencies
+
+ @api.model
+ def _prepare_edifact_lines(self, interchange, order_dict):
+ edifact_model = self.env["base.edifact"]
+ bdio = self.env["business.document.import"]
+ order = order_dict["order"]
+ partner_id_number = order_dict["company"]
+ order_dict["unknown_products"] = []
+ partner = bdio._match_partner(
+ partner_id_number,
+ "",
+ partner_type="customer",
+ )
+ lines = []
+ pia_list = []
+ qty_list = []
+ pri_list = []
+ imd_list = []
+
+ for i in interchange.get_segments("PIA"):
+ if i[1][1] == "SA":
+ pia_list.append(i)
+ for i in interchange.get_segments("QTY"):
+ if i[0][0] == "21" or i[0][0] == "12":
+ qty_list.append(i)
+ for i in interchange.get_segments("PRI"):
+ pri_list.append(i)
+ for i in interchange.get_segments("IMD"):
+ if i[0] == "A" and i[1] == "":
+ imd_list.append(i)
+
+ for linseg in interchange.get_segments("LIN"):
+
+ piaseg = pia_list.pop(0) if pia_list else None
+ qtyseg = qty_list.pop(0) if qty_list else None
+ priseg = pri_list.pop(0) if pri_list else None
+ imdseg = imd_list.pop(0) if imd_list else None
+
+ line = {
+ "sequence": int(linseg[0]),
+ "product": edifact_model.map2odoo_product(linseg, piaseg),
+ "qty": edifact_model.map2odoo_qty(qtyseg),
+ }
+
+ price_unit = edifact_model.map2odoo_unit_price(priseg)
+ # If the product price is not provided,
+ # the price will be taken from the system
+ if price_unit != 0.0:
+ line["price_unit"] = price_unit
+
+ description = edifact_model.map2odoo_description(imdseg)
+ if description:
+ line["name"] = description
+
+ try:
+ # Check product
+ bdio._match_product(line["product"], "")
+ except UserError as error:
+ if (
+ order
+ and
+ partner.edifact_despatch_advice_ignore_lines_with_unknown_products
+ ):
+ order.message_post(body=_(error.name))
+ order_dict["unknown_products"].append(line)
+ continue
+ else:
+ raise error
+
+ lines.append(line)
+
+ return lines
+
+ @api.model
+ def _prepare_create_order_line(self, product, uom, order, import_line):
+ """the 'order' arg can be a recordset (in case of an update of a purchase order)
+ or a dict (in case of the creation of a new purchase order)"""
+ polo = self.env["purchase.order.line"]
+ vals = {}
+ # Ensure the company is loaded before we play onchanges.
+ # Yes, `company_id` is related to `order_id.company_id`
+ # but when we call `play_onchanges` it will be empty
+ # w/out this precaution.
+ company_id = self._prepare_order_line_get_company_id(order)
+ vals.update(
+ {
+ "name": product.display_name,
+ "product_id": product.id,
+ "product_uom_qty": import_line["qty"],
+ "product_qty": import_line["qty"],
+ "product_uom": uom.id,
+ "company_id": company_id,
+ "order_id": order.id,
+ }
+ )
+ # Handle additional fields dynamically if available.
+ # This way, if you add a field to a record
+ # and it's value is injected by a parser
+ # you won't have to override `_prepare_create_order_line`
+ # to let it propagate.
+ for k, v in import_line.items():
+ if k not in vals and k in polo._fields:
+ vals[k] = v
+
+ defaults = self.env.context.get("purchase_order_import__default_vals", {}).get(
+ "lines", {}
+ )
+ vals.update(defaults)
+ return vals
+
+ def _prepare_update_order_line_vals(self, change_dict):
+ # Allows other module to update some fields on the line
+ return {}
+
+ def _prepare_order_line_get_company_id(self, order):
+ company_id = self.env.user.company_id
+ if isinstance(order, models.Model):
+ company_id = order.company_id.id
+ elif isinstance(order, dict):
+ company_id = order.get("company_id") or company_id
+ return company_id
+
+ def _add_order_line_from_compare_res(self, order, compare_res, parsed_order):
+ chatter = parsed_order["chatter_msg"]
+ polo = self.env["purchase.order.line"]
+ to_create_label = []
+ for add in compare_res["to_add"]:
+ line_vals = self._prepare_create_order_line(
+ add["product"], add["uom"], order, add["import_line"]
+ )
+ line_vals["date_planned"] = parsed_order["delivery_detail"]["date_planned"]
+ new_line = polo.create(line_vals)
+ to_create_label.append(
+ "%s %s x %s"
+ % (
+ new_line.product_uom_qty,
+ new_line.product_uom.name,
+ new_line.name,
+ )
+ )
+ chatter.append(
+ _("%(orders)s new order line(s) created: %(label)s").format(
+ orders=len(compare_res["to_add"]), label=", ".join(to_create_label)
+ )
+ )
+ # Update quantity_done with product_uom_qty with PO created based on
+ # information from Despatch Advice
+ if order.state not in ["purchase", "done", "cancel"]:
+ order.with_context(skip_send_edifact=True).button_confirm()
+ for picking in order.picking_ids.filtered(
+ lambda p: p.state not in ["done", "cancel"]
+ ):
+ for move in picking.move_lines:
+ move.quantity_done = move.product_uom_qty
+ self._update_qty_done_package(picking.move_lines)
+
+ def _remove_order_line_from_compare_res(self, compare_res, parsed_order):
+ chatter = parsed_order["chatter_msg"]
+ to_remove_label = [
+ "%s %s x %s"
+ % (line.product_uom_qty, line.product_uom.name, line.product_id.name)
+ for line in compare_res["to_remove"]
+ ]
+ chatter.append(
+ _("{orders} order line(s) deleted: {label}").format(
+ orders=len(compare_res["to_remove"]),
+ label=", ".join(to_remove_label),
+ )
+ )
+ compare_res["to_remove"].unlink()
+
+ @api.model
+ def update_order_lines(self, parsed_order, order):
+ chatter = parsed_order["chatter_msg"]
+ qty_diff_list = parsed_order["qty_diff"] = []
+ dpo = self.env["decimal.precision"]
+ bdio = self.env["business.document.import"]
+ qty_prec = dpo.precision_get("Product UoS")
+ existing_lines = []
+ for oline in order.order_line:
+ # compute price unit without tax
+ price_unit = 0.0
+ if not float_is_zero(oline.product_uom_qty, precision_digits=qty_prec):
+ qty = float(oline.product_uom_qty)
+ price_unit = oline.price_subtotal / qty
+ existing_lines.append(
+ {
+ "product": oline.product_id or False,
+ "name": oline.name,
+ # Assign to 0 to get update qty data in `compare_res``
+ "qty": 0,
+ "uom": oline.product_uom,
+ "line": oline,
+ "price_unit": price_unit,
+ }
+ )
+ compare_res = bdio.compare_lines(
+ existing_lines,
+ parsed_order["lines"],
+ chatter,
+ qty_precision=qty_prec,
+ seller=False,
+ )
+
+ # NOW, we start to write/delete/create the order lines
+ number_line_updated = 0
+ picking_dict = {}
+ for oline, cdict in compare_res["to_update"].items():
+ write_vals = {}
+ if cdict.get("qty"):
+ write_vals.update(self._prepare_update_order_line_vals(cdict))
+ if oline.product_id.type == "product":
+ delivery_qty = cdict["qty"][1]
+ if oline.product_qty != delivery_qty + oline.qty_received:
+ qty_diff_list.append(
+ {
+ "message": "Mismatch between ordered quantity"
+ " ({}) and quantity being delivered ({})".format(
+ oline.product_qty, delivery_qty
+ ),
+ "Order Line Info": {
+ "id": oline.id,
+ "product_id": oline.product_id.id,
+ "barcode": oline.product_id.barcode,
+ "default_code": oline.product_id.default_code,
+ "description": oline.name,
+ "product_qty": oline.product_qty,
+ "qty_recieved": oline.qty_received,
+ "incoming_qty": delivery_qty,
+ },
+ }
+ )
+ updated_picking_dict = self._update_stock_moves(
+ oline, delivery_qty, picking_dict
+ )
+ if updated_picking_dict:
+ picking_dict = updated_picking_dict
+ number_line_updated += 1
+
+ if write_vals:
+ oline.write(write_vals)
+ for picking, move_ids in picking_dict.items():
+ message = (
+ "Record has been updated automatically via the import Despatch Advice."
+ f" Done quantities were updated on {len(move_ids)} lines out of "
+ f"the {len(picking.move_line_ids)} Reception lines."
+ )
+ picking.message_post(body=_(message))
+
+ if compare_res["to_remove"] and order.state not in ["purchase", "done"]:
+ self._remove_order_line_from_compare_res(compare_res, parsed_order)
+
+ if compare_res["to_add"]:
+ if order.state in ["purchase", "done"]:
+ sub_order = order.copy(default={"order_line": [(5, 0, 0)]})
+ order.message_post(
+ body=_(
+ "Received some unexpected products. "
+ "Created a new Purchase Order for it in "
+ "%s."
+ )
+ % (sub_order.id, sub_order.name)
+ )
+ order = sub_order
+ self._add_order_line_from_compare_res(order, compare_res, parsed_order)
+ return number_line_updated
+
+ def _update_qty_done_package(self, moves):
+ if hasattr(moves, "qty_done_package"):
+ for move in moves:
+ if (
+ move.purchase_line_id
+ and move.quantity_done > 0
+ and move.package_qty
+ ):
+ move.qty_done_package = move.quantity_done / move.package_qty
+ return moves
+
+ @api.model
+ def _update_stock_moves(self, order_line, new_qty, picking_dict=None):
+ if picking_dict is None:
+ picking_dict = {}
+ moves = order_line.move_ids.filtered(
+ lambda move: move.state not in ("done", "cancel", "draft")
+ )
+ if not moves:
+ raise UserError(f"No valid moves to update for the line {order_line.name}")
+ total_qty_update = (
+ new_qty - order_line.qty_received - sum(moves.mapped("quantity_done"))
+ )
+ if total_qty_update <= 0:
+ order_line.order_id.message_post(
+ body=_(
+ "The quantity delivered for product "
+ "%s "
+ "is less than or equal to the quantity received."
+ )
+ % (order_line.product_id.id, order_line.product_id.name)
+ )
+ if total_qty_update > 0:
+ for move in moves:
+ if total_qty_update <= 0:
+ break
+
+ available_qty = move.product_uom_qty - move.quantity_done
+
+ if available_qty > 0:
+ if total_qty_update >= available_qty:
+ move.quantity_done = move.product_uom_qty
+ total_qty_update -= available_qty
+ else:
+ move.quantity_done += total_qty_update
+ total_qty_update = 0
+
+ if not picking_dict.get(move.picking_id, False):
+ picking_dict[move.picking_id] = []
+ picking_dict[move.picking_id].append(move.id)
+
+ self._update_qty_done_package(moves)
+
+ # Create new stock.move if still available
+ if total_qty_update > 0:
+ new_move = moves[-1].copy(
+ {
+ "product_uom_qty": total_qty_update,
+ "quantity_done": total_qty_update,
+ "state": "draft",
+ }
+ )
+ new_move = self._update_qty_done_package(new_move._action_confirm())
+ new_move._action_assign()
+
+ # Return the pickings with updated moves to print the message
+ return picking_dict
+ return False
+
+ @api.model
+ def _prepare_update_order_vals(self, parsed_order):
+ bdio = self.env["business.document.import"]
+ partner = bdio._match_partner(
+ parsed_order["company"],
+ parsed_order["chatter_msg"],
+ partner_type="customer",
+ )
+ vals = {"partner_id": partner.id}
+ return vals
+
+ def update_purchase_order(self, order, parsed_order):
+ self.ensure_one()
+ bdio = self.env["business.document.import"]
+ currency = bdio._match_currency(
+ parsed_order.get("currency"), parsed_order["chatter_msg"]
+ )
+ if currency != order.currency_id:
+ raise UserError(
+ _(
+ "The currency of the imported order {old} is different from "
+ "the currency of the existing order {new}"
+ ).format(
+ old=currency.name,
+ new=order.currency_id.name,
+ )
+ )
+ vals = self._prepare_update_order_vals(parsed_order)
+ if vals:
+ order.write(vals)
+ number_line_updated = self.update_order_lines(parsed_order, order)
+ bdio.post_create_or_update(parsed_order, order)
+ logger.info(
+ "Order ID %d updated via import of file %s",
+ order.id,
+ self.order_filename,
+ )
+ action = self.env.ref("purchase.purchase_form_action").read()[0]
+ action.update(
+ {
+ "view_mode": "form,tree,calendar,graph",
+ "views": False,
+ "view_id": False,
+ "res_id": order.id,
+ "unknown_products": parsed_order["unknown_products"],
+ "qty_diff": parsed_order["qty_diff"],
+ "reception_lines": parsed_order["reception_lines"],
+ "number_line_updated": number_line_updated,
+ }
+ )
+ message = self._create_expected_reception_message(action)
+ order.message_post(body=_(message))
+
+ return action
+
+ def _create_expected_reception_message(self, action):
+ message = """
+ \nThis order has been updated automatically via the import of file {}
+ \nDone quantities were updated on {} lines out of the {} Reception lines
+ """.format(
+ self.order_filename,
+ action.get("number_line_updated", 0),
+ action.get("reception_lines", 0),
+ )
+
+ unknown_products = action.get("unknown_products", False)
+ if unknown_products:
+ message += "\nUnknow Product: \n"
+ message += " * " + "\n * ".join(
+ json.dumps(rec, indent=4) for rec in unknown_products
+ )
+ qty_diff = action.get("qty_diff", False)
+ if qty_diff:
+ message += "\nDifference of Qty: \n"
+ message += " * " + "\n * ".join(
+ json.dumps(rec, indent=4) for rec in qty_diff
+ )
+ return message
diff --git a/edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml b/edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml
new file mode 100644
index 000000000..b9be11893
--- /dev/null
+++ b/edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml
@@ -0,0 +1,49 @@
+
+
+
+
+ purchase.order.import.form.dev
+ purchase.order.import
+
+
+
+
+
+ Import Purchase Order EDIFACT
+ purchase.order.import
+ form
+ new
+
+
+
+
From dfd8db961b4a461e2aa1553728513277ef8d93f1 Mon Sep 17 00:00:00 2001
From: Mathias Francke
Date: Mon, 14 Apr 2025 13:06:59 +0200
Subject: [PATCH 02/10] [IMP] edi_purchase_edifact_oca: black, isort, prettier
---
edi_purchase_edifact_oca/README.rst | 10 +++++-----
.../components/listener_edifact_output.py | 2 +-
.../components/process_edifact_input.py | 1 +
.../models/business_document_import.py | 6 ++++--
.../models/edi_exchange_record.py | 1 +
edi_purchase_edifact_oca/models/purchase.py | 3 ++-
edi_purchase_edifact_oca/models/res_partner.py | 3 +--
.../static/description/index.html | 18 ++++++++++--------
.../tests/test_edifact_purchase.py | 11 ++++++-----
edi_purchase_edifact_oca/views/purchase.xml | 5 ++---
edi_purchase_edifact_oca/views/res_partner.xml | 11 ++++++-----
.../wizard/purchase_order_import.py | 5 ++---
.../wizard/purchase_order_import_view.xml | 6 ++----
13 files changed, 43 insertions(+), 39 deletions(-)
diff --git a/edi_purchase_edifact_oca/README.rst b/edi_purchase_edifact_oca/README.rst
index dbba976d1..c8a0ab817 100644
--- a/edi_purchase_edifact_oca/README.rst
+++ b/edi_purchase_edifact_oca/README.rst
@@ -17,13 +17,13 @@ EDI PURCHASE EDIFACT OCA
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi-lightgray.png?logo=github
- :target: https://github.com/OCA/edi/tree/12.0/edi_purchase_edifact_oca
+ :target: https://github.com/OCA/edi/tree/14.0/edi_purchase_edifact_oca
:alt: OCA/edi
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
- :target: https://translation.odoo-community.org/projects/edi-12-0/edi-12-0-edi_purchase_edifact_oca
+ :target: https://translation.odoo-community.org/projects/edi-14-0/edi-14-0-edi_purchase_edifact_oca
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
- :target: https://runboat.odoo-community.org/builds?repo=OCA/edi&target_branch=12.0
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/edi&target_branch=14.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
@@ -54,7 +54,7 @@ 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 to smash it by providing a detailed and welcomed
-`feedback `_.
+`feedback `_.
Do not contact contributors directly about support or help with technical issues.
@@ -91,6 +91,6 @@ 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.
-This module is part of the `OCA/edi `_ project on GitHub.
+This module is part of the `OCA/edi `_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/edi_purchase_edifact_oca/components/listener_edifact_output.py b/edi_purchase_edifact_oca/components/listener_edifact_output.py
index 52896685e..8d3336d75 100644
--- a/edi_purchase_edifact_oca/components/listener_edifact_output.py
+++ b/edi_purchase_edifact_oca/components/listener_edifact_output.py
@@ -35,7 +35,7 @@ def _should_create_exchange_record(self, order):
return False
partner = order.partner_id
- return (partner and partner.edifact_purchase_order_out)
+ return partner and partner.edifact_purchase_order_out
def _storage_new_exchange_record_vals(self):
return {"edi_exchange_state": "new"}
diff --git a/edi_purchase_edifact_oca/components/process_edifact_input.py b/edi_purchase_edifact_oca/components/process_edifact_input.py
index bb8a2f65d..bf57a08db 100644
--- a/edi_purchase_edifact_oca/components/process_edifact_input.py
+++ b/edi_purchase_edifact_oca/components/process_edifact_input.py
@@ -2,6 +2,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import base64
+
from odoo.addons.component.core import Component
diff --git a/edi_purchase_edifact_oca/models/business_document_import.py b/edi_purchase_edifact_oca/models/business_document_import.py
index c7bad30dc..29357b69f 100644
--- a/edi_purchase_edifact_oca/models/business_document_import.py
+++ b/edi_purchase_edifact_oca/models/business_document_import.py
@@ -40,8 +40,10 @@ def _hook_match_partner(self, partner_dict, chatter_msg, domain, order):
"edi_ctx", {"order_filename": _("Unknown"), "rff_va": _("Unknown")}
)
raise exceptions.UserError(
- _("Partner GLN Code: {party} not found in order file: '{file}' "
- "from VAT registration number '{vat}'.").format(
+ _(
+ "Partner GLN Code: {party} not found in order file: '{file}' "
+ "from VAT registration number '{vat}'."
+ ).format(
party=party_id,
file=ctx.get("order_filename"),
vat=ctx.get("rff_va"),
diff --git a/edi_purchase_edifact_oca/models/edi_exchange_record.py b/edi_purchase_edifact_oca/models/edi_exchange_record.py
index a381ad456..1e2b057ec 100644
--- a/edi_purchase_edifact_oca/models/edi_exchange_record.py
+++ b/edi_purchase_edifact_oca/models/edi_exchange_record.py
@@ -2,6 +2,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models
+
from odoo.addons.edi_oca.models.edi_exchange_record import EDIExchangeRecord
diff --git a/edi_purchase_edifact_oca/models/purchase.py b/edi_purchase_edifact_oca/models/purchase.py
index 7cb13e8dc..5e7a8d6c6 100644
--- a/edi_purchase_edifact_oca/models/purchase.py
+++ b/edi_purchase_edifact_oca/models/purchase.py
@@ -1,8 +1,9 @@
# Copyright 2024 Trobz
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0)
-from odoo import _, models, fields
from datetime import datetime
+
+from odoo import _, fields, models
from odoo.exceptions import UserError
diff --git a/edi_purchase_edifact_oca/models/res_partner.py b/edi_purchase_edifact_oca/models/res_partner.py
index c2f0409f7..9a4aa5473 100644
--- a/edi_purchase_edifact_oca/models/res_partner.py
+++ b/edi_purchase_edifact_oca/models/res_partner.py
@@ -8,8 +8,7 @@ class ResPartner(models.Model):
_inherit = "res.partner"
edifact_purchase_order_out = fields.Boolean(
- string="Export Purchase Order with EDIFACT",
- default=False
+ string="Export Purchase Order with EDIFACT", default=False
)
edifact_despatch_advice_ignore_lines_with_unknown_products = fields.Boolean(
diff --git a/edi_purchase_edifact_oca/static/description/index.html b/edi_purchase_edifact_oca/static/description/index.html
index c8a0e64f7..d74ed04c7 100644
--- a/edi_purchase_edifact_oca/static/description/index.html
+++ b/edi_purchase_edifact_oca/static/description/index.html
@@ -1,4 +1,3 @@
-
@@ -9,10 +8,11 @@
/*
:Author: David Goodger (goodger@python.org)
-:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
+:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
+Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
@@ -275,7 +275,7 @@
margin-left: 2em ;
margin-right: 2em }
-pre.code .ln { color: grey; } /* line numbers */
+pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
@@ -301,7 +301,7 @@
span.pre {
white-space: pre }
-span.problematic {
+span.problematic, pre.problematic {
color: red }
span.section-subtitle {
@@ -369,7 +369,7 @@ EDI PURCHASE EDIFACT OCA
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:d2e2602bb615321d18583caff366876c642564652bdfcd412395f69355aa8d72
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
-

+

- UN/EDIFACT
- United Nations rules for Electronic Data Interchange for Administration, Commerce and Transport
@@ -403,7 +403,7 @@
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 to smash it by providing a detailed and welcomed
-feedback.
+feedback.
Do not contact contributors directly about support or help with technical issues.
@@ -430,11 +430,13 @@
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.
-
This module is part of the OCA/edi project on GitHub.
+
This module is part of the OCA/edi project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/edi_purchase_edifact_oca/tests/test_edifact_purchase.py b/edi_purchase_edifact_oca/tests/test_edifact_purchase.py
index d78c8f03e..1d621e934 100644
--- a/edi_purchase_edifact_oca/tests/test_edifact_purchase.py
+++ b/edi_purchase_edifact_oca/tests/test_edifact_purchase.py
@@ -2,12 +2,13 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import base64
-from odoo.addons.component.tests.common import TransactionComponentCase
-from odoo.addons.edi_oca.tests.common import EDIBackendTestMixin
+import re
+from base64 import b64encode
from odoo import fields
-from base64 import b64encode
-import re
+
+from odoo.addons.component.tests.common import TransactionComponentCase
+from odoo.addons.edi_oca.tests.common import EDIBackendTestMixin
class TestEdifactPurchaseOrder(TransactionComponentCase, EDIBackendTestMixin):
@@ -163,7 +164,7 @@ def test_edifact_purchase_wizard_import(self):
"'LIN+3++:EN'PIA+1+FURN_667777:SA::91'PIA+1+FURN_667777:BP::92'"
"QTY+21:4.0:'QTY+52::'DTM+2:20250110:102'MOA+203:0.0'PRI+AAA:0.0'"
"PRI+AAB:0.0'RFF+PL:338'TAX+7+VAT+++:::0'UNS+S'"
- )
+ ),
)
wiz = self.env["purchase.order.import"].create(
{
diff --git a/edi_purchase_edifact_oca/views/purchase.xml b/edi_purchase_edifact_oca/views/purchase.xml
index df590f2df..dbb82a742 100644
--- a/edi_purchase_edifact_oca/views/purchase.xml
+++ b/edi_purchase_edifact_oca/views/purchase.xml
@@ -1,10 +1,9 @@
-
-
+
edi.purchase.edifact.oca.purchase.order.form
purchase.order
-
+
diff --git a/edi_purchase_edifact_oca/views/res_partner.xml b/edi_purchase_edifact_oca/views/res_partner.xml
index 69baf68bf..8762fda5e 100644
--- a/edi_purchase_edifact_oca/views/res_partner.xml
+++ b/edi_purchase_edifact_oca/views/res_partner.xml
@@ -1,15 +1,16 @@
-
-
+
view.partner.form.inherit
res.partner
-
+
-
-
+
+
diff --git a/edi_purchase_edifact_oca/wizard/purchase_order_import.py b/edi_purchase_edifact_oca/wizard/purchase_order_import.py
index 4956c6592..0f2f74884 100644
--- a/edi_purchase_edifact_oca/wizard/purchase_order_import.py
+++ b/edi_purchase_edifact_oca/wizard/purchase_order_import.py
@@ -1,9 +1,9 @@
# Copyright 2024 Trobz
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+import json
import logging
import mimetypes
-import json
from base64 import b64decode, b64encode
from collections import defaultdict
@@ -336,8 +336,7 @@ def _prepare_edifact_lines(self, interchange, order_dict):
except UserError as error:
if (
order
- and
- partner.edifact_despatch_advice_ignore_lines_with_unknown_products
+ and partner.edifact_despatch_advice_ignore_lines_with_unknown_products
):
order.message_post(body=_(error.name))
order_dict["unknown_products"].append(line)
diff --git a/edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml b/edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml
index b9be11893..c9a24f5a7 100644
--- a/edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml
+++ b/edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml
@@ -1,5 +1,4 @@
-
purchase.order.import.form.dev
@@ -8,8 +7,7 @@
From b18d616f575f5366db654127e303fdf047c5cf8d Mon Sep 17 00:00:00 2001
From: fkantelberg
Date: Tue, 22 Jul 2025 08:01:12 +0200
Subject: [PATCH 04/10] [IMP] edi_purchase_edifact_oca: Allow to only activate
edifact on the parent contact
---
.../components/listener_edifact_output.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/edi_purchase_edifact_oca/components/listener_edifact_output.py b/edi_purchase_edifact_oca/components/listener_edifact_output.py
index 8d3336d75..9c0e56ad1 100644
--- a/edi_purchase_edifact_oca/components/listener_edifact_output.py
+++ b/edi_purchase_edifact_oca/components/listener_edifact_output.py
@@ -35,7 +35,13 @@ def _should_create_exchange_record(self, order):
return False
partner = order.partner_id
- return partner and partner.edifact_purchase_order_out
+ if not partner:
+ return False
+
+ return (
+ partner.edifact_purchase_order_out
+ or partner.parent_id.edifact_purchase_order_out
+ )
def _storage_new_exchange_record_vals(self):
return {"edi_exchange_state": "new"}
From 795b9bc003eb8a9e0a5278c1051bb2a9ab6a171a Mon Sep 17 00:00:00 2001
From: fkantelberg
Date: Tue, 22 Jul 2025 08:35:18 +0200
Subject: [PATCH 05/10] [IMP] Use company as buyer
---
edi_purchase_edifact_oca/models/purchase.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/edi_purchase_edifact_oca/models/purchase.py b/edi_purchase_edifact_oca/models/purchase.py
index 435d9d8c2..d317a420f 100644
--- a/edi_purchase_edifact_oca/models/purchase.py
+++ b/edi_purchase_edifact_oca/models/purchase.py
@@ -119,7 +119,7 @@ def _edifact_purchase_get_header(self, exchange_record=None):
today = datetime.now().date().strftime("%Y%m%d")
id_number = self.env["res.partner.id_number"]
buyer_id_number = id_number.search(
- [("partner_id", "=", self.user_id.company_id.partner_id.id)], limit=1
+ [("partner_id", "=", self.company_id.partner_id.id)], limit=1
)
seller_id_number = id_number.search([("partner_id", "=", self.partner_id.id)])
if not seller_id_number and self.partner_id.parent_id:
@@ -169,7 +169,7 @@ def _edifact_purchase_get_header(self, exchange_record=None):
header = (
header[:5]
+ self._edifact_get_name_and_address(
- self.user_id.partner_id, "BY", buyer_id_number.name
+ self.company_id.partner_id, "BY", buyer_id_number.name
)
+ self._edifact_get_name_and_address(
self.partner_id, "SU", seller_id_number.name
From cded00d1c6cca762317f8c65ae85bad351b47ac6 Mon Sep 17 00:00:00 2001
From: fkantelberg
Date: Mon, 28 Jul 2025 08:36:44 +0200
Subject: [PATCH 06/10] [IMP][14.0] edi_purchase_edifact_oca: Use commercial
partner id. Use default_code from product.supplierinfo
---
.../data/edi_exchange_type.xml | 2 +-
edi_purchase_edifact_oca/models/purchase.py | 32 +++++++++++++++----
2 files changed, 27 insertions(+), 7 deletions(-)
diff --git a/edi_purchase_edifact_oca/data/edi_exchange_type.xml b/edi_purchase_edifact_oca/data/edi_exchange_type.xml
index 4db1f6738..7a61ea71f 100644
--- a/edi_purchase_edifact_oca/data/edi_exchange_type.xml
+++ b/edi_purchase_edifact_oca/data/edi_exchange_type.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/edi_purchase_edifact_oca/models/purchase.py b/edi_purchase_edifact_oca/models/purchase.py
index d317a420f..b76b6cc48 100644
--- a/edi_purchase_edifact_oca/models/purchase.py
+++ b/edi_purchase_edifact_oca/models/purchase.py
@@ -68,9 +68,9 @@ def _edifact_purchase_get_interchange(self):
recipient = id_number.search([("partner_id", "=", self.partner_id.id)], limit=1)
# if current supplier does not have `Id number`
# then check current supplier's parent
- if not recipient and self.partner_id.parent_id:
+ if not recipient and self.partner_id.commercial_partner_id:
recipient = id_number.search(
- [("partner_id", "=", self.partner_id.parent_id.id)], limit=1
+ [("partner_id", "=", self.partner_id.commercial_partner_id.id)], limit=1
)
if not sender or not recipient:
raise UserError(_("Partner is not allowed to use the feature."))
@@ -82,6 +82,22 @@ def _edifact_purchase_get_interchange(self):
sender_edifact, recipient_edifact, self.name, syntax_identifier
)
+ def _edifact_purchase_get_supplier_code(self, product):
+ # Make it hookable and use the product.supplierinfo if a code is set
+ supplierinfo = product.seller_ids.filtered_domain(
+ [("name", "=", self.partner_id.id)]
+ )
+ if not supplierinfo:
+ supplierinfo = product.seller_ids.filtered_domain(
+ [("name", "=", self.partner_id.commercial_partner_id.id)]
+ )
+
+ return supplierinfo[:1].product_code or product.default_code
+
+ def _edifact_purchase_get_barcode(self, product):
+ # Make it hookable for additional modules
+ return product.barcode
+
def _edifact_purchase_get_address(self, partner):
# We apply the same logic as:
# https://github.com/OCA/edi/blob/
@@ -122,9 +138,9 @@ def _edifact_purchase_get_header(self, exchange_record=None):
[("partner_id", "=", self.company_id.partner_id.id)], limit=1
)
seller_id_number = id_number.search([("partner_id", "=", self.partner_id.id)])
- if not seller_id_number and self.partner_id.parent_id:
+ if not seller_id_number and self.partner_id.commercial_partner_id:
seller_id_number = id_number.search(
- [("partner_id", "=", self.partner_id.parent_id.id)], limit=1
+ [("partner_id", "=", self.partner_id.commercial_partner_id.id)], limit=1
)
message_id = exchange_record.id if exchange_record else ""
warehouse_name = (
@@ -200,11 +216,15 @@ def _edifact_purchase_get_product(self):
product_type = "EN"
if self.edifact_version == "d01b":
product_type = "SRV"
+
+ supplier_code = self._edifact_purchase_get_supplier_code(product)
+ barcode = self._edifact_purchase_get_barcode(product)
+
product_seg = [
# Line item number
- ("LIN", number, "", [product.barcode, product_type]),
+ ("LIN", number, "", [barcode, product_type]),
# Product identification of Supplier's article number
- ("PIA", "1", [product.default_code, "SA", "", "91"]),
+ ("PIA", "1", [supplier_code, "SA", "", "91"]),
# Product identification of Buyer's part number
("PIA", "1", [product.default_code, "BP", "", "92"]),
# Ordered quantity
From 618f6b78b026cfecaf6f96a45557905abd447e96 Mon Sep 17 00:00:00 2001
From: phucph
Date: Mon, 18 Aug 2025 09:39:00 +0700
Subject: [PATCH 07/10] [DONT MERGE] test-requirements.txt
---
test-requirements.txt | 3 +++
1 file changed, 3 insertions(+)
diff --git a/test-requirements.txt b/test-requirements.txt
index a8133e4b5..505024994 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,2 +1,5 @@
odoo-test-helper
xmlunittest
+odoo-addon-base_edifact @ git+https://github.com/OCA/edi.git@refs/pull/1185/head#subdirectory=base_edifact
+odoo-addon-edi_storage_oca @ git+https://github.com/OCA/edi-framework.git@refs/pull/135/head#subdirectory=edi_storage_oca
+odoo-addon-edi_purchase_oca @ git+https://github.com/OCA/edi-framework.git@refs/pull/180/head#subdirectory=edi_purchase_oca
From 48fd8d99bb0d68465f5ee4ca7d5c70a77c99bfae Mon Sep 17 00:00:00 2001
From: phucph
Date: Mon, 18 Aug 2025 10:30:24 +0700
Subject: [PATCH 08/10] [IMP] edi_purchase_edifact_oca: pre-commit auto fixes
---
edi_purchase_edifact_oca/README.rst | 37 ++++++++++---------
edi_purchase_edifact_oca/__manifest__.py | 4 +-
edi_purchase_edifact_oca/pyproject.toml | 3 ++
.../readme/CONTRIBUTORS.md | 2 +
.../readme/CONTRIBUTORS.rst | 1 -
.../readme/{CREDITS.rst => CREDITS.md} | 2 +-
.../readme/DESCRIPTION.md | 11 ++++++
.../readme/DESCRIPTION.rst | 9 -----
.../static/description/index.html | 21 ++++++-----
9 files changed, 51 insertions(+), 39 deletions(-)
create mode 100644 edi_purchase_edifact_oca/pyproject.toml
create mode 100644 edi_purchase_edifact_oca/readme/CONTRIBUTORS.md
delete mode 100644 edi_purchase_edifact_oca/readme/CONTRIBUTORS.rst
rename edi_purchase_edifact_oca/readme/{CREDITS.rst => CREDITS.md} (89%)
create mode 100644 edi_purchase_edifact_oca/readme/DESCRIPTION.md
delete mode 100644 edi_purchase_edifact_oca/readme/DESCRIPTION.rst
diff --git a/edi_purchase_edifact_oca/README.rst b/edi_purchase_edifact_oca/README.rst
index c8a0ab817..c67eaf8a5 100644
--- a/edi_purchase_edifact_oca/README.rst
+++ b/edi_purchase_edifact_oca/README.rst
@@ -16,22 +16,24 @@ EDI PURCHASE EDIFACT OCA
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
-.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi-lightgray.png?logo=github
- :target: https://github.com/OCA/edi/tree/14.0/edi_purchase_edifact_oca
- :alt: OCA/edi
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi--framework-lightgray.png?logo=github
+ :target: https://github.com/OCA/edi-framework/tree/18.0/edi_purchase_edifact_oca
+ :alt: OCA/edi-framework
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
- :target: https://translation.odoo-community.org/projects/edi-14-0/edi-14-0-edi_purchase_edifact_oca
+ :target: https://translation.odoo-community.org/projects/edi-framework-18-0/edi-framework-18-0-edi_purchase_edifact_oca
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
- :target: https://runboat.odoo-community.org/builds?repo=OCA/edi&target_branch=14.0
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/edi-framework&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
-UN/EDIFACT
- United Nations rules for Electronic Data Interchange for Administration, Commerce and Transport
+| UN/EDIFACT
+| United Nations rules for Electronic Data Interchange for
+ Administration, Commerce and Transport
-This module will support exporting and confirming orders in EDIFACT format.
+This module will support exporting and confirming orders in EDIFACT
+format.
https://www.stedi.com/edi/edifact/D01B/messages/ORDERS
https://www.stedi.com/edi/edifact/D96A/messages/ORDERS
@@ -51,10 +53,10 @@ https://www.stedi.com/edi/edifact/D96A/messages/DESADV
Bug Tracker
===========
-Bugs are tracked on `GitHub Issues `_.
+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 to smash it by providing a detailed and welcomed
-`feedback `_.
+`feedback `_.
Do not contact contributors directly about support or help with technical issues.
@@ -62,24 +64,25 @@ Credits
=======
Authors
-~~~~~~~
+-------
* Trobz
Contributors
-~~~~~~~~~~~~
+------------
-* Thien (Vo Hong)
+- Thien (Vo Hong)
+- Phuc (Phan Hong)
Other credits
-~~~~~~~~~~~~~
+-------------
The development of this module has been financially supported by:
-* Trobz
+- Trobz
Maintainers
-~~~~~~~~~~~
+-----------
This module is maintained by the OCA.
@@ -91,6 +94,6 @@ 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.
-This module is part of the `OCA/edi `_ project on GitHub.
+This module is part of the `OCA/edi-framework `_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/edi_purchase_edifact_oca/__manifest__.py b/edi_purchase_edifact_oca/__manifest__.py
index 1e0ef8541..2f213853e 100644
--- a/edi_purchase_edifact_oca/__manifest__.py
+++ b/edi_purchase_edifact_oca/__manifest__.py
@@ -3,9 +3,9 @@
{
"name": "EDI PURCHASE EDIFACT OCA",
"summary": "Create and send EDIFACT order files",
- "version": "14.0.1.0.0",
+ "version": "18.0.1.0.0",
"development_status": "Alpha",
- "website": "https://github.com/OCA/edi",
+ "website": "https://github.com/OCA/edi-framework",
"author": "Trobz, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
diff --git a/edi_purchase_edifact_oca/pyproject.toml b/edi_purchase_edifact_oca/pyproject.toml
new file mode 100644
index 000000000..4231d0ccc
--- /dev/null
+++ b/edi_purchase_edifact_oca/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/edi_purchase_edifact_oca/readme/CONTRIBUTORS.md b/edi_purchase_edifact_oca/readme/CONTRIBUTORS.md
new file mode 100644
index 000000000..582d911bc
--- /dev/null
+++ b/edi_purchase_edifact_oca/readme/CONTRIBUTORS.md
@@ -0,0 +1,2 @@
+- Thien (Vo Hong) \
+- Phuc (Phan Hong) \
diff --git a/edi_purchase_edifact_oca/readme/CONTRIBUTORS.rst b/edi_purchase_edifact_oca/readme/CONTRIBUTORS.rst
deleted file mode 100644
index 42455569c..000000000
--- a/edi_purchase_edifact_oca/readme/CONTRIBUTORS.rst
+++ /dev/null
@@ -1 +0,0 @@
-* Thien (Vo Hong)
diff --git a/edi_purchase_edifact_oca/readme/CREDITS.rst b/edi_purchase_edifact_oca/readme/CREDITS.md
similarity index 89%
rename from edi_purchase_edifact_oca/readme/CREDITS.rst
rename to edi_purchase_edifact_oca/readme/CREDITS.md
index b777d8a49..b5330f661 100644
--- a/edi_purchase_edifact_oca/readme/CREDITS.rst
+++ b/edi_purchase_edifact_oca/readme/CREDITS.md
@@ -1,3 +1,3 @@
The development of this module has been financially supported by:
-* Trobz
+- Trobz
diff --git a/edi_purchase_edifact_oca/readme/DESCRIPTION.md b/edi_purchase_edifact_oca/readme/DESCRIPTION.md
new file mode 100644
index 000000000..b2753a3c1
--- /dev/null
+++ b/edi_purchase_edifact_oca/readme/DESCRIPTION.md
@@ -0,0 +1,11 @@
+UN/EDIFACT
+United Nations rules for Electronic Data Interchange for Administration,
+Commerce and Transport
+
+This module will support exporting and confirming orders in EDIFACT
+format.
+
+
+
+
+
diff --git a/edi_purchase_edifact_oca/readme/DESCRIPTION.rst b/edi_purchase_edifact_oca/readme/DESCRIPTION.rst
deleted file mode 100644
index 81a0bb18d..000000000
--- a/edi_purchase_edifact_oca/readme/DESCRIPTION.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-UN/EDIFACT
- United Nations rules for Electronic Data Interchange for Administration, Commerce and Transport
-
-This module will support exporting and confirming orders in EDIFACT format.
-
-https://www.stedi.com/edi/edifact/D01B/messages/ORDERS
-https://www.stedi.com/edi/edifact/D96A/messages/ORDERS
-https://www.stedi.com/edi/edifact/D01B/messages/DESADV
-https://www.stedi.com/edi/edifact/D96A/messages/DESADV
diff --git a/edi_purchase_edifact_oca/static/description/index.html b/edi_purchase_edifact_oca/static/description/index.html
index d74ed04c7..7ad6fc1ae 100644
--- a/edi_purchase_edifact_oca/static/description/index.html
+++ b/edi_purchase_edifact_oca/static/description/index.html
@@ -369,12 +369,14 @@ EDI PURCHASE EDIFACT OCA
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:d2e2602bb615321d18583caff366876c642564652bdfcd412395f69355aa8d72
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
-

-
-- UN/EDIFACT
-- United Nations rules for Electronic Data Interchange for Administration, Commerce and Transport
-
-This module will support exporting and confirming orders in EDIFACT format.
+

+
+
UN/EDIFACT
+
United Nations rules for Electronic Data Interchange for
+Administration, Commerce and Transport
+
+This module will support exporting and confirming orders in EDIFACT
+format.
https://www.stedi.com/edi/edifact/D01B/messages/ORDERS
https://www.stedi.com/edi/edifact/D96A/messages/ORDERS
https://www.stedi.com/edi/edifact/D01B/messages/DESADV
@@ -400,10 +402,10 @@
EDI PURCHASE EDIFACT OCA
-
Bugs are tracked on GitHub Issues.
+
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 to smash it by providing a detailed and welcomed
-feedback.
+
feedback.
Do not contact contributors directly about support or help with technical issues.
@@ -436,7 +439,7 @@
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.
-
This module is part of the OCA/edi project on GitHub.
+
This module is part of the OCA/edi-framework project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
From 810c10fb629ed65b8e170aa217d26a6eb5d046de Mon Sep 17 00:00:00 2001
From: phucph
Date: Mon, 18 Aug 2025 10:34:37 +0700
Subject: [PATCH 09/10] [MIG] edi_purchase_edifact_oca: Migration to 18.0
---
edi_purchase_edifact_oca/README.rst | 1 +
edi_purchase_edifact_oca/__manifest__.py | 2 +
.../components/generate_edifact_output.py | 31 +-
.../components/process_edifact_input.py | 23 +-
.../data/edi_exchange_type.xml | 15 +-
.../models/edi_exchange_record.py | 4 +-
edi_purchase_edifact_oca/models/purchase.py | 7 +-
.../readme/CONTRIBUTORS.md | 1 +
.../security/ir.model.access.csv | 2 +
.../static/description/index.html | 1 +
.../tests/test_edifact_purchase.py | 158 +++++----
.../wizard/purchase_order_import.py | 302 ++++++++++--------
.../wizard/purchase_order_import_view.xml | 2 +-
test-requirements.txt | 3 -
14 files changed, 316 insertions(+), 236 deletions(-)
diff --git a/edi_purchase_edifact_oca/README.rst b/edi_purchase_edifact_oca/README.rst
index c67eaf8a5..0dbe5b0af 100644
--- a/edi_purchase_edifact_oca/README.rst
+++ b/edi_purchase_edifact_oca/README.rst
@@ -72,6 +72,7 @@ Contributors
------------
- Thien (Vo Hong)
+- Mathias Francke
- Phuc (Phan Hong)
Other credits
diff --git a/edi_purchase_edifact_oca/__manifest__.py b/edi_purchase_edifact_oca/__manifest__.py
index 2f213853e..0b5c95d3b 100644
--- a/edi_purchase_edifact_oca/__manifest__.py
+++ b/edi_purchase_edifact_oca/__manifest__.py
@@ -13,8 +13,10 @@
"depends": [
"base_edifact",
"stock",
+ "edi_core_oca",
"edi_storage_oca",
"edi_purchase_oca",
+ "edi_exchange_template_oca",
"partner_identification_gln",
"base_business_document_import",
],
diff --git a/edi_purchase_edifact_oca/components/generate_edifact_output.py b/edi_purchase_edifact_oca/components/generate_edifact_output.py
index 77ab19864..b51898c4a 100644
--- a/edi_purchase_edifact_oca/components/generate_edifact_output.py
+++ b/edi_purchase_edifact_oca/components/generate_edifact_output.py
@@ -1,21 +1,34 @@
# Copyright 2024 Trobz
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-from odoo.addons.component.core import Component
+# from odoo.addons.component.core import Component
+from odoo import models
+from odoo.addons.edi_core_oca.exceptions import EDINotImplementedError
-class EDIExchangeEDIFACTOutGenerate(Component):
- _name = "edi.output.edifact.out.generate"
- _inherit = "edi.component.output.mixin"
- _usage = "output.generate.edifact"
- def generate(self):
- data = False
- exchange_record = self.exchange_record
+class EDIExchangeEDIFACTOutGenerate(models.AbstractModel):
+ _name = "edi.output.edifact.out.generate"
+ _inherit = [
+ "edi.oca.handler.generate",
+ ]
+ _description = "Process Generate Output EDIFact"
+ def generate(self, exchange_record):
+ exchange_record = self.env["edi.exchange.record"].browse(exchange_record.id)
+ tmpl = exchange_record.backend_id._get_output_template(exchange_record)
+ if tmpl:
+ exchange_record = exchange_record.with_context(
+ edi_framework_action="generate"
+ )
+ tmpl = tmpl.with_context(edi_framework_action="generate")
if exchange_record:
if exchange_record.model == "purchase.order" and exchange_record.res_id:
order = self.env["purchase.order"].browse(exchange_record.res_id)
if order:
data = order.edifact_purchase_generate_data(exchange_record)
- return data
+ return data
+
+ raise EDINotImplementedError(
+ self.env._("EDIFact generation process is not implemented yet")
+ )
diff --git a/edi_purchase_edifact_oca/components/process_edifact_input.py b/edi_purchase_edifact_oca/components/process_edifact_input.py
index bf57a08db..4b4278fcc 100644
--- a/edi_purchase_edifact_oca/components/process_edifact_input.py
+++ b/edi_purchase_edifact_oca/components/process_edifact_input.py
@@ -3,34 +3,33 @@
import base64
-from odoo.addons.component.core import Component
+from odoo import models
-class EDIExchangeEDIFACTInput(Component):
-
+class EDIExchangeEDIFACTInput(models.Model):
_name = "edi.input.process.edifact.input"
- _inherit = "edi.component.input.mixin"
- _usage = "input.process.edifact.input"
+ _inherit = ["edi.oca.handler.process"]
+ _description = "Input process despatch advice from EDIFact"
- def process(self):
+ def process(self, exchange_record):
"""Process incoming EDIFACT record and confirm record."""
- file_content = self.exchange_record._get_file_content()
+ file_content = exchange_record._get_file_content()
wizard = self.env["purchase.order.import"].create(
{
"import_type": "edifact",
"order_file": base64.b64encode(file_content.encode()),
- "order_filename": self.exchange_record.exchange_filename,
+ "order_filename": exchange_record.exchange_filename,
}
)
- file_name = self.exchange_record.exchange_filename
+ file_name = exchange_record.exchange_filename
action = wizard.import_order_button()
if action and action.get("res_model", False):
- self.exchange_record.update(
+ exchange_record.update(
{
"model": action["res_model"],
"res_id": action["res_id"],
}
)
- self.exchange_record.exchange_filename = file_name
+ exchange_record.exchange_filename = file_name
- return True
+ return self.env._("Process incoming EDIFACT completed!")
diff --git a/edi_purchase_edifact_oca/data/edi_exchange_type.xml b/edi_purchase_edifact_oca/data/edi_exchange_type.xml
index 7a61ea71f..54fb118c8 100644
--- a/edi_purchase_edifact_oca/data/edi_exchange_type.xml
+++ b/edi_purchase_edifact_oca/data/edi_exchange_type.xml
@@ -12,13 +12,9 @@
iso-8859-1
strict
strict
+
- components:
- generate:
- usage: output.generate.edifact
- env_ctx:
- msg_type: Picking
filename_pattern:
force_tz: Europe/Zurich
date_pattern: "%Y%m%d%H%M%S%f"
@@ -38,12 +34,7 @@
iso-8859-1
strict
strict
-
- components:
- process:
- usage: input.process.edifact.input
- env_ctx:
- msg_type: "EDIFACT Input"
-
+
+
diff --git a/edi_purchase_edifact_oca/models/edi_exchange_record.py b/edi_purchase_edifact_oca/models/edi_exchange_record.py
index 1e2b057ec..4eeae1701 100644
--- a/edi_purchase_edifact_oca/models/edi_exchange_record.py
+++ b/edi_purchase_edifact_oca/models/edi_exchange_record.py
@@ -3,10 +3,10 @@
from odoo import models
-from odoo.addons.edi_oca.models.edi_exchange_record import EDIExchangeRecord
+from odoo.addons.edi_core_oca.models.edi_exchange_record import EDIExchangeRecord
-class EDIExchangeRecord(models.Model):
+class EDIExchangeRecordEDIFact(models.Model):
_inherit = "edi.exchange.record"
_rollback_state_mapping = {
diff --git a/edi_purchase_edifact_oca/models/purchase.py b/edi_purchase_edifact_oca/models/purchase.py
index b76b6cc48..aacd9f2fa 100644
--- a/edi_purchase_edifact_oca/models/purchase.py
+++ b/edi_purchase_edifact_oca/models/purchase.py
@@ -16,7 +16,6 @@ class PurchaseOrder(models.Model):
("d01b", "D.01B"),
],
default="d96a",
- string="Edifact Version",
)
def _replace_edifact_delimiters(self, data):
@@ -34,7 +33,7 @@ def process_element(element):
if not element.replace(".", "", 1).isdigit()
else element
)
- elif isinstance(element, (list, tuple)):
+ elif isinstance(element, list | tuple):
result = map(process_element, element)
return type(element)(result)
else:
@@ -85,11 +84,11 @@ def _edifact_purchase_get_interchange(self):
def _edifact_purchase_get_supplier_code(self, product):
# Make it hookable and use the product.supplierinfo if a code is set
supplierinfo = product.seller_ids.filtered_domain(
- [("name", "=", self.partner_id.id)]
+ [("partner_id", "=", self.partner_id.id)]
)
if not supplierinfo:
supplierinfo = product.seller_ids.filtered_domain(
- [("name", "=", self.partner_id.commercial_partner_id.id)]
+ [("partner_id", "=", self.partner_id.commercial_partner_id.id)]
)
return supplierinfo[:1].product_code or product.default_code
diff --git a/edi_purchase_edifact_oca/readme/CONTRIBUTORS.md b/edi_purchase_edifact_oca/readme/CONTRIBUTORS.md
index 582d911bc..51e107e45 100644
--- a/edi_purchase_edifact_oca/readme/CONTRIBUTORS.md
+++ b/edi_purchase_edifact_oca/readme/CONTRIBUTORS.md
@@ -1,2 +1,3 @@
- Thien (Vo Hong) \
+- Mathias Francke \
- Phuc (Phan Hong) \
diff --git a/edi_purchase_edifact_oca/security/ir.model.access.csv b/edi_purchase_edifact_oca/security/ir.model.access.csv
index ee5b62eeb..44281e251 100644
--- a/edi_purchase_edifact_oca/security/ir.model.access.csv
+++ b/edi_purchase_edifact_oca/security/ir.model.access.csv
@@ -1,2 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_purchase_order_import,access_purchase_order_import,model_purchase_order_import,base.group_user,1,1,1,1
+access_edi_output_edifact_out_generate,access_edi_output_edifact_out_generate,model_edi_output_edifact_out_generate,base.group_user,1,1,1,1
+access_edi_input_process_edifact_input,access_edi_input_process_edifact_input,model_edi_input_process_edifact_input,base.group_user,1,1,1,1
diff --git a/edi_purchase_edifact_oca/static/description/index.html b/edi_purchase_edifact_oca/static/description/index.html
index 7ad6fc1ae..65a7ef6eb 100644
--- a/edi_purchase_edifact_oca/static/description/index.html
+++ b/edi_purchase_edifact_oca/static/description/index.html
@@ -420,6 +420,7 @@
diff --git a/edi_purchase_edifact_oca/tests/test_edifact_purchase.py b/edi_purchase_edifact_oca/tests/test_edifact_purchase.py
index 1d621e934..82302a406 100644
--- a/edi_purchase_edifact_oca/tests/test_edifact_purchase.py
+++ b/edi_purchase_edifact_oca/tests/test_edifact_purchase.py
@@ -12,73 +12,84 @@
class TestEdifactPurchaseOrder(TransactionComponentCase, EDIBackendTestMixin):
- def setUp(self):
- super(TestEdifactPurchaseOrder, self).setUp()
- self.env = self.env(context=dict(self.env.context, tracking_disable=True))
- self.base_edifact_model = self.env["base.edifact"]
- self.company = self.env.ref("base.main_company")
- self.product_1 = self.env.ref("product.product_product_1")
- self.product_1.default_code = "FURN_66668"
- self.product_1.type = "product"
- self.product_2 = self.env.ref("product.product_product_4")
- self.product_2.default_code = "FURN_88558"
- self.product_3 = self.env.ref("product.product_product_5")
- self.product_3.default_code = "FURN_667777"
- self.product_3.type = "product"
- partner_id_number = self.env["res.partner.id_number"]
- self.partner_1 = self.env.ref("base.res_partner_1")
- self.partner_1.edifact_purchase_order_out = True
- self.partner_2 = self.env.ref("base.res_partner_12")
- self.exc_type_input = self.env.ref(
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
+ cls.base_edifact_model = cls.env["base.edifact"]
+ cls.company = cls.env.ref("base.main_company")
+ cls.product_1 = cls.env.ref("product.product_delivery_01")
+ cls.product_2 = cls.env.ref("product.product_delivery_02")
+ cls.product_3 = cls.env.ref("product.product_order_01")
+ partner_id_number = cls.env["res.partner.id_number"]
+ cls.partner_1 = cls.env.ref("base.res_partner_1")
+ cls.partner_1.edifact_purchase_order_out = True
+ cls.partner_2 = cls.env.ref("base.res_partner_12")
+ cls.exc_type_input = cls.env.ref(
"edi_purchase_edifact_oca.edi_exchange_type_purchase_order_input"
)
+ cls.edi_config_out_po = cls.env.ref(
+ "edi_purchase_oca.demo_edi_configuration_confirmed"
+ )
+ cls.edi_config_out_po.write(
+ {
+ "backend_id": cls.env.ref(
+ "edi_purchase_edifact_oca.edi_backend_edifact"
+ ),
+ "type_id": cls.env.ref(
+ "edi_purchase_edifact_oca.edi_exchange_type_purchase_order_out"
+ ),
+ }
+ )
+ cls.partner_1.write(
+ {"edi_purchase_conf_ids": [(6, 0, cls.edi_config_out_po.ids)]}
+ )
partner_id_number_data_1 = {
- "category_id": self.env.ref(
+ "category_id": cls.env.ref(
"partner_identification_gln.partner_identification_gln_number_category"
).id,
- "partner_id": self.partner_1.id,
+ "partner_id": cls.partner_1.id,
"name": "9780201379624",
}
-
partner_id_number_data_2 = {
- "category_id": self.env.ref(
+ "category_id": cls.env.ref(
"partner_identification_gln.partner_identification_gln_number_category"
).id,
- "partner_id": self.partner_2.id,
+ "partner_id": cls.partner_2.id,
"name": "9780201379174",
}
partner_id_number.create(partner_id_number_data_1)
partner_id_number.create(partner_id_number_data_2)
- self.env.user.partner_id = self.partner_2
- self.env.user.company_id.partner_id = self.partner_2
+ cls.env.user.partner_id = cls.partner_2
+ cls.env.user.company_id.partner_id = cls.partner_2
- self.datetime = fields.Datetime.now()
- self.purchase = self.env["purchase.order"].create(
+ cls.datetime = fields.Datetime.now()
+ cls.purchase = cls.env["purchase.order"].create(
{
- "partner_id": self.partner_1.id,
- "date_order": self.datetime,
- "date_planned": self.datetime,
+ "partner_id": cls.partner_1.id,
+ "date_order": cls.datetime,
+ "date_planned": cls.datetime,
}
)
- self.po_line1 = self.purchase.order_line.create(
+ cls.po_line1 = cls.purchase.order_line.create(
{
- "order_id": self.purchase.id,
- "product_id": self.product_1.id,
- "name": self.product_1.name,
- "date_planned": self.datetime,
+ "order_id": cls.purchase.id,
+ "product_id": cls.product_1.id,
+ "name": cls.product_1.name,
+ "date_planned": cls.datetime,
"product_qty": 12,
- "product_uom": self.product_1.uom_id.id,
+ "product_uom": cls.product_1.uom_id.id,
"price_unit": 42.42,
}
)
- self.po_line2 = self.purchase.order_line.create(
+ cls.po_line2 = cls.purchase.order_line.create(
{
- "order_id": self.purchase.id,
- "product_id": self.product_2.id,
- "name": self.product_2.name,
- "date_planned": self.datetime,
+ "order_id": cls.purchase.id,
+ "product_id": cls.product_2.id,
+ "name": cls.product_2.name,
+ "date_planned": cls.datetime,
"product_qty": 2,
- "product_uom": self.product_2.uom_id.id,
+ "product_uom": cls.product_2.uom_id.id,
"price_unit": 12.34,
}
)
@@ -124,14 +135,11 @@ def test_edifact_purchase_get_address(self):
def test_action_confirm(self):
self.purchase.button_confirm()
- exchange_record = self.env["edi.exchange.record"].search(
- [
- ("model", "=", "purchase.order"),
- ("res_id", "=", self.purchase.id),
- ]
- )
- exchange_record.action_exchange_generate()
- self.assertNotEqual(exchange_record.exchange_file, False)
+ self.assertEqual(self.purchase.exchange_record_count, 1)
+ exchange_record = self.purchase.exchange_record_ids[0]
+ exchange_record.backend_id.exchange_generate(exchange_record)
+ file_content = exchange_record._get_file_content()
+ self.assertTrue(file_content, "The generated file content should not be empty.")
self.assertEqual(exchange_record.edi_exchange_state, "output_pending")
self.assertEqual(exchange_record.exchanged_on, False)
@@ -161,7 +169,7 @@ def test_edifact_purchase_wizard_import(self):
edifact_data = edifact_data.replace(
"'UNS+S'",
(
- "'LIN+3++:EN'PIA+1+FURN_667777:SA::91'PIA+1+FURN_667777:BP::92'"
+ "'LIN+3++:EN'PIA+1+FURN_9999:SA::91'PIA+1+FURN_9999:BP::92'"
"QTY+21:4.0:'QTY+52::'DTM+2:20250110:102'MOA+203:0.0'PRI+AAA:0.0'"
"PRI+AAB:0.0'RFF+PL:338'TAX+7+VAT+++:::0'UNS+S'"
),
@@ -182,11 +190,9 @@ def test_edifact_purchase_wizard_import(self):
)
self.assertTrue(new_order.order_id)
- self.assertEqual(new_order.move_ids.quantity_done, 4)
+ self.assertEqual(new_order.move_ids.quantity, 4)
- sum_quantity_done = sum(
- self.purchase.order_line.mapped("move_ids.quantity_done")
- )
+ sum_quantity_done = sum(self.purchase.order_line.mapped("move_ids.quantity"))
self.assertEqual(sum_quantity_done, 14.0)
def test_edifact_purchase_wizard_import_new_po(self):
@@ -210,8 +216,7 @@ def test_edifact_purchase_wizard_import_new_po(self):
action = wiz.import_order_button()
new_order = self.env["purchase.order"].search([("id", "=", action["res_id"])])
-
- sum_quantity_done = sum(new_order.order_line.mapped("move_ids.quantity_done"))
+ sum_quantity_done = sum(new_order.order_line.mapped("move_ids.quantity"))
self.assertNotEqual(self.purchase.id, new_order.id)
self.assertEqual(len(new_order.order_line), 2)
@@ -224,7 +229,7 @@ def test_edifact_purchase_wizard_import_ignore_unknown(self):
self.assertEqual(isinstance(edifact_data, str), True)
self.purchase.button_confirm()
edifact_data = edifact_data.replace("UNH++ORDERS", "UNH+1+DESADV").replace(
- "LIN+1++:EN'PIA+1+FURN_66668:SA::91", "LIN+1++:EN'PIA+1+FURN_666688:SA::91"
+ "LIN+1++:EN'PIA+1+FURN_7777:SA::91", "LIN+1++:EN'PIA+1+FURN_77778:SA::91"
)
wiz = self.env["purchase.order.import"].create(
@@ -238,9 +243,7 @@ def test_edifact_purchase_wizard_import_ignore_unknown(self):
# Import file to confirm purchase order
wiz.import_order_button()
- sum_quantity_done = sum(
- self.purchase.order_line.mapped("move_ids.quantity_done")
- )
+ sum_quantity_done = sum(self.purchase.order_line.mapped("move_ids.quantity"))
self.assertEqual(sum_quantity_done, 2.0)
def test_edifact_purchase_exchange_record_input(self):
@@ -251,6 +254,31 @@ def test_edifact_purchase_exchange_record_input(self):
self.purchase.button_confirm()
edifact_data = edifact_data.replace("UNH++ORDERS", "UNH+1+DESADV")
+ edifact_data = edifact_data.replace(
+ "'UNS+S'", # noqa: E501
+ (
+ "'LIN+3++:EN'PIA+1+FURN_8888:SA::91'PIA+1+FURN_8888:BP::92'"
+ "QTY+21:5.0:'QTY+52::'DTM+2:20250707:102'MOA+203:24.68'"
+ "PRI+AAA:12.34'PRI+AAB:12.34'RFF+PL:35'TAX+7+VAT+++:::0'UNS+S'"
+ ),
+ )
+
+ exchange_record_out = self.purchase.exchange_record_ids[0]
+ exchange_record_out.backend_id.exchange_generate(exchange_record_out)
+
+ exchange_record = self.env["edi.exchange.record"].create(
+ {
+ "backend_id": exchange_record_out.backend_id.id,
+ "type_id": self.exc_type_input.id,
+ "model": "purchase.order",
+ "res_id": self.purchase.id,
+ }
+ )
+ self.assertEqual(self.purchase.exchange_record_count, 2)
+
+ exchange_record._set_file_content(edifact_data)
+ exchange_record.write({"edi_exchange_state": "input_received"})
+ exchange_record.backend_id.exchange_process(exchange_record)
record = self.exc_type_input.backend_id.create_record(
self.exc_type_input.code,
@@ -270,7 +298,7 @@ def test_edifact_purchase_exchange_record_input(self):
self.assertEqual(record.res_id, self.purchase.id)
self.assertEqual(record.related_name, self.purchase.name)
- sum_quantity_done = sum(
- self.purchase.order_line.mapped("move_ids.quantity_done")
- )
- self.assertEqual(sum_quantity_done, 14.0)
+ sum_quantity_done = sum(self.purchase.order_line.mapped("move_ids.quantity"))
+ self.assertEqual(len(self.purchase.picking_ids), 1)
+ self.assertEqual(len(self.purchase.picking_ids[0].move_line_ids), 3)
+ self.assertEqual(sum_quantity_done, 19.0)
diff --git a/edi_purchase_edifact_oca/wizard/purchase_order_import.py b/edi_purchase_edifact_oca/wizard/purchase_order_import.py
index 0f2f74884..6f8a8ef0c 100644
--- a/edi_purchase_edifact_oca/wizard/purchase_order_import.py
+++ b/edi_purchase_edifact_oca/wizard/purchase_order_import.py
@@ -7,9 +7,11 @@
from base64 import b64decode, b64encode
from collections import defaultdict
-from odoo import _, api, fields, models
+from markupsafe import Markup
+
+from odoo import api, fields, models
from odoo.exceptions import UserError
-from odoo.tools import config, float_is_zero
+from odoo.tools import config, float_is_zero, html_escape
logger = logging.getLogger(__name__)
@@ -48,23 +50,26 @@ def _parse_file(self, filename, filecontent):
supported_types = self._get_supported_types()
# Check if the selected import type is supported
if self.import_type not in supported_types:
- raise UserError(_("Please select a valid import type before importing!"))
+ raise UserError(
+ self.env._("Please select a valid import type before importing!")
+ )
# Check if the detected MIME type is supported for the selected import type
if mimetype not in supported_types[self.import_type]:
raise UserError(
- _(
- "This file '%(filename)s' is not recognized as a %(type)s file. "
+ self.env._(
+ "This file '%(filename)s' is not recognized as "
+ "a %(import_type)s file. "
"Please check the file and its extension.",
filename=filename,
- type=self.import_type.upper(),
+ import_type=self.import_type.upper(),
)
)
- if hasattr(self, "parse_%s_order" % self.import_type):
- return getattr(self, "parse_%s_order" % self.import_type)(filecontent)
+ if hasattr(self, f"parse_{self.import_type}_order"):
+ return getattr(self, f"parse_{self.import_type}_order")(filecontent)
else:
raise UserError(
- _(
+ self.env._(
"This Import Type is not supported. Did you install "
"the module to support this type?"
)
@@ -73,7 +78,7 @@ def _parse_file(self, filename, filecontent):
@api.model
def parse_order(self, order_file, order_filename):
parsed_order = self._parse_file(order_filename, order_file)
- logger.debug("Result of order parsing: %s", parsed_order)
+ logger.debug(f"Result of order parsing: {parsed_order}")
defaults = (
("attachments", {}),
("chatter_msg", []),
@@ -98,7 +103,7 @@ def import_order_button(self):
parsed_order = self.parse_order(order_file_decoded, self.order_filename)
if not parsed_order.get("lines"):
- raise UserError(_("This order doesn't have any line !"))
+ raise UserError(self.env._("This order doesn't have any line !"))
order = parsed_order.get("order", False)
if order and not order.picking_ids.filtered(
lambda picking: picking.state not in ("done", "cancel")
@@ -121,8 +126,9 @@ def parse_edifact_order(self, filecontent):
supported = ["ORDERS", "DESADV"]
if msg_type not in supported:
raise UserError(
- _("{msg_type} document is not a Purchase Order document").format(
- msg_type=msg_type
+ self.env._(
+ "%(msg_type)s document is not a Purchase Order document",
+ msg_type=msg_type,
)
)
@@ -156,8 +162,9 @@ def parse_edifact_order(self, filecontent):
order_dict["order"] = existing_quotations[0]
else:
raise UserError(
- _("Purchase Order Id {id} is not found").format(
- id=order_dict["order_ref"]
+ self.env._(
+ "Purchase Order Id %(order_ref)s is not found",
+ order_ref=order_dict["order_ref"],
)
)
@@ -308,7 +315,6 @@ def _prepare_edifact_lines(self, interchange, order_dict):
imd_list.append(i)
for linseg in interchange.get_segments("LIN"):
-
piaseg = pia_list.pop(0) if pia_list else None
qtyseg = qty_list.pop(0) if qty_list else None
priseg = pri_list.pop(0) if pri_list else None
@@ -336,9 +342,9 @@ def _prepare_edifact_lines(self, interchange, order_dict):
except UserError as error:
if (
order
- and partner.edifact_despatch_advice_ignore_lines_with_unknown_products
+ and partner.edifact_despatch_advice_ignore_lines_with_unknown_products # noqa: E501
):
- order.message_post(body=_(error.name))
+ order.message_post(body=self.env._(str(error)))
order_dict["unknown_products"].append(line)
continue
else:
@@ -408,51 +414,63 @@ def _add_order_line_from_compare_res(self, order, compare_res, parsed_order):
line_vals["date_planned"] = parsed_order["delivery_detail"]["date_planned"]
new_line = polo.create(line_vals)
to_create_label.append(
- "%s %s x %s"
- % (
- new_line.product_uom_qty,
- new_line.product_uom.name,
- new_line.name,
- )
+ f"{new_line.product_uom_qty} {new_line.product_uom.name} "
+ f"x {new_line.name}"
)
chatter.append(
- _("%(orders)s new order line(s) created: %(label)s").format(
- orders=len(compare_res["to_add"]), label=", ".join(to_create_label)
+ self.env._(
+ "%(nbr_ol)s new order line(s) created: %(to_create_label)s",
+ nbr_ol=len(compare_res["to_add"]),
+ to_create_label=", ".join(to_create_label),
)
)
- # Update quantity_done with product_uom_qty with PO created based on
+ # Update quantity with product_uom_qty with PO created based on
# information from Despatch Advice
if order.state not in ["purchase", "done", "cancel"]:
order.with_context(skip_send_edifact=True).button_confirm()
for picking in order.picking_ids.filtered(
lambda p: p.state not in ["done", "cancel"]
):
- for move in picking.move_lines:
- move.quantity_done = move.product_uom_qty
- self._update_qty_done_package(picking.move_lines)
+ for move in picking.move_line_ids:
+ move.quantity = move.quantity_product_uom
+ self._update_qty_done_package(picking.move_line_ids)
def _remove_order_line_from_compare_res(self, compare_res, parsed_order):
chatter = parsed_order["chatter_msg"]
to_remove_label = [
- "%s %s x %s"
- % (line.product_uom_qty, line.product_uom.name, line.product_id.name)
+ f"{line.product_uom_qty} {line.product_uom.name} "
+ f"x {line.product_id.name}"
for line in compare_res["to_remove"]
]
chatter.append(
- _("{orders} order line(s) deleted: {label}").format(
- orders=len(compare_res["to_remove"]),
- label=", ".join(to_remove_label),
+ self.env._(
+ "%(nbr_ol)s order line(s) deleted: %(to_remove_label)s",
+ nbr_ol=len(compare_res["to_remove"]),
+ to_remove_label=", ".join(to_remove_label),
)
)
compare_res["to_remove"].unlink()
+ def _merge_import_lines(self, import_lines):
+ """Merge lines with the same product."""
+
+ merged_lines = {}
+ for line in import_lines:
+ product_key = tuple(sorted(line["product"].items()))
+ if product_key not in merged_lines:
+ merged_lines[product_key] = line.copy()
+ merged_lines[product_key]["qty"] = line["qty"]
+ else:
+ merged_lines[product_key]["qty"] += line["qty"]
+ return list(merged_lines.values())
+
@api.model
def update_order_lines(self, parsed_order, order):
chatter = parsed_order["chatter_msg"]
qty_diff_list = parsed_order["qty_diff"] = []
dpo = self.env["decimal.precision"]
bdio = self.env["business.document.import"]
- qty_prec = dpo.precision_get("Product UoS")
+ qty_prec = dpo.precision_get("Product Unit of Measure")
existing_lines = []
for oline in order.order_line:
# compute price unit without tax
@@ -471,86 +489,94 @@ def update_order_lines(self, parsed_order, order):
"price_unit": price_unit,
}
)
+ import_lines = self._merge_import_lines(parsed_order["lines"])
compare_res = bdio.compare_lines(
existing_lines,
- parsed_order["lines"],
+ import_lines,
chatter,
qty_precision=qty_prec,
seller=False,
)
+ # Display errors during the comparison between
+ # the Despatch Advice file and the order data.
+ if chatter:
+ order.message_post(body=Markup("
".join(chatter)))
+
# NOW, we start to write/delete/create the order lines
number_line_updated = 0
picking_dict = {}
- for oline, cdict in compare_res["to_update"].items():
- write_vals = {}
- if cdict.get("qty"):
- write_vals.update(self._prepare_update_order_line_vals(cdict))
- if oline.product_id.type == "product":
- delivery_qty = cdict["qty"][1]
- if oline.product_qty != delivery_qty + oline.qty_received:
- qty_diff_list.append(
- {
- "message": "Mismatch between ordered quantity"
- " ({}) and quantity being delivered ({})".format(
- oline.product_qty, delivery_qty
- ),
- "Order Line Info": {
- "id": oline.id,
- "product_id": oline.product_id.id,
- "barcode": oline.product_id.barcode,
- "default_code": oline.product_id.default_code,
- "description": oline.name,
- "product_qty": oline.product_qty,
- "qty_recieved": oline.qty_received,
- "incoming_qty": delivery_qty,
- },
- }
+ # An error may occur during the comparison between
+ # the imported data and the original data.
+ # In that case, compare_res will be set to False.
+ if compare_res:
+ for oline, cdict in compare_res["to_update"].items():
+ write_vals = {}
+ if cdict.get("qty"):
+ write_vals.update(self._prepare_update_order_line_vals(cdict))
+ if oline.product_id.type == "consu":
+ delivery_qty = cdict["qty"][1]
+ if oline.product_qty != delivery_qty + oline.qty_received:
+ qty_diff_list.append(
+ {
+ "message": "Mismatch between ordered quantity"
+ f" ({oline.product_qty}) and "
+ f"quantity being delivered ({delivery_qty})",
+ "Order Line Info": {
+ "id": oline.id,
+ "product_id": oline.product_id.id,
+ "barcode": oline.product_id.barcode,
+ "default_code": oline.product_id.default_code,
+ "description": oline.name,
+ "product_qty": oline.product_qty,
+ "qty_recieved": oline.qty_received,
+ "incoming_qty": delivery_qty,
+ },
+ }
+ )
+ updated_picking_dict = self._update_stock_moves(
+ oline, delivery_qty, picking_dict
)
- updated_picking_dict = self._update_stock_moves(
- oline, delivery_qty, picking_dict
- )
- if updated_picking_dict:
- picking_dict = updated_picking_dict
- number_line_updated += 1
-
- if write_vals:
- oline.write(write_vals)
- for picking, move_ids in picking_dict.items():
- message = (
- "Record has been updated automatically via the import Despatch Advice."
- f" Done quantities were updated on {len(move_ids)} lines out of "
- f"the {len(picking.move_line_ids)} Reception lines."
- )
- picking.message_post(body=_(message))
-
- if compare_res["to_remove"] and order.state not in ["purchase", "done"]:
- self._remove_order_line_from_compare_res(compare_res, parsed_order)
-
- if compare_res["to_add"]:
- if order.state in ["purchase", "done"]:
- sub_order = order.copy(default={"order_line": [(5, 0, 0)]})
- order.message_post(
- body=_(
- "Received some unexpected products. "
- "Created a new Purchase Order for it in "
- "%s."
- )
- % (sub_order.id, sub_order.name)
+ if updated_picking_dict:
+ picking_dict = updated_picking_dict
+ number_line_updated += 1
+
+ if write_vals:
+ oline.write(write_vals)
+ for picking, move_ids in picking_dict.items():
+ message = (
+ "Record has been updated automatically via the import Despatch Advice." # noqa: E501
+ f" Done quantities were updated on {len(move_ids)} lines out of "
+ f"the {len(picking.move_line_ids)} Reception lines."
)
- order = sub_order
- self._add_order_line_from_compare_res(order, compare_res, parsed_order)
+ picking.message_post(body=self.env._(message))
+
+ if compare_res["to_remove"] and order.state not in ["purchase", "done"]:
+ self._remove_order_line_from_compare_res(compare_res, parsed_order)
+
+ if compare_res["to_add"]:
+ if order.state in ["purchase", "done"]:
+ sub_order = order.copy(default={"order_line": [(5, 0, 0)]})
+ order.message_post(
+ body=Markup(
+ self.env._(
+ "Received some unexpected products. "
+ "Created a new Purchase Order for it in "
+ "%(so_name)s.", # noqa: E501
+ so_id=sub_order.id,
+ so_name=sub_order.name,
+ )
+ )
+ )
+ order = sub_order
+ self._add_order_line_from_compare_res(order, compare_res, parsed_order)
return number_line_updated
def _update_qty_done_package(self, moves):
if hasattr(moves, "qty_done_package"):
for move in moves:
- if (
- move.purchase_line_id
- and move.quantity_done > 0
- and move.package_qty
- ):
- move.qty_done_package = move.quantity_done / move.package_qty
+ if move.purchase_line_id and move.quantity > 0 and move.package_qty:
+ move.qty_done_package = move.quantity / move.package_qty
return moves
@api.model
@@ -561,32 +587,41 @@ def _update_stock_moves(self, order_line, new_qty, picking_dict=None):
lambda move: move.state not in ("done", "cancel", "draft")
)
if not moves:
- raise UserError(f"No valid moves to update for the line {order_line.name}")
+ raise UserError(
+ self.env._(
+ "No valid moves to update for the line %(order_name)s.",
+ order_name=order_line.name,
+ )
+ )
total_qty_update = (
- new_qty - order_line.qty_received - sum(moves.mapped("quantity_done"))
+ new_qty - order_line.qty_received - sum(moves.mapped("quantity"))
)
if total_qty_update <= 0:
order_line.order_id.message_post(
- body=_(
- "The quantity delivered for product "
- "%s "
- "is less than or equal to the quantity received."
+ body=Markup(
+ self.env._(
+ "The quantity delivered for product "
+ ""
+ "%(ol_product_name)s "
+ "is less than or equal to the quantity received.",
+ ol_product_id=order_line.product_id.id,
+ ol_product_name=order_line.product_id.name,
+ )
)
- % (order_line.product_id.id, order_line.product_id.name)
)
if total_qty_update > 0:
for move in moves:
if total_qty_update <= 0:
break
-
- available_qty = move.product_uom_qty - move.quantity_done
+ available_qty = move.product_uom_qty - move.quantity
if available_qty > 0:
if total_qty_update >= available_qty:
- move.quantity_done = move.product_uom_qty
+ move.quantity = move.product_uom_qty
total_qty_update -= available_qty
else:
- move.quantity_done += total_qty_update
+ move.quantity += total_qty_update
total_qty_update = 0
if not picking_dict.get(move.picking_id, False):
@@ -600,7 +635,7 @@ def _update_stock_moves(self, order_line, new_qty, picking_dict=None):
new_move = moves[-1].copy(
{
"product_uom_qty": total_qty_update,
- "quantity_done": total_qty_update,
+ "quantity": total_qty_update,
"state": "draft",
}
)
@@ -630,10 +665,9 @@ def update_purchase_order(self, order, parsed_order):
)
if currency != order.currency_id:
raise UserError(
- _(
- "The currency of the imported order {old} is different from "
- "the currency of the existing order {new}"
- ).format(
+ self.env._(
+ "The currency of the imported order %(old)s is different from "
+ "the currency of the existing order %(new)s",
old=currency.name,
new=order.currency_id.name,
)
@@ -641,6 +675,12 @@ def update_purchase_order(self, order, parsed_order):
vals = self._prepare_update_order_vals(parsed_order)
if vals:
order.write(vals)
+ for move in order.order_line.mapped("move_ids"):
+ if (
+ move.state not in ("done", "cancel", "draft")
+ and move.product_uom_qty == move.quantity
+ ):
+ move.quantity = 0
number_line_updated = self.update_order_lines(parsed_order, order)
bdio.post_create_or_update(parsed_order, order)
logger.info(
@@ -662,30 +702,36 @@ def update_purchase_order(self, order, parsed_order):
}
)
message = self._create_expected_reception_message(action)
- order.message_post(body=_(message))
+ order.message_post(body=self.env._(message))
return action
def _create_expected_reception_message(self, action):
- message = """
- \nThis order has been updated automatically via the import of file {}
- \nDone quantities were updated on {} lines out of the {} Reception lines
- """.format(
- self.order_filename,
+ message = (
+ "This order has been updated automatically via the import of file "
+ "{}
"
+ "Done quantities were updated on {} lines out of the {} Reception lines
"
+ ).format(
+ html_escape(self.order_filename),
action.get("number_line_updated", 0),
action.get("reception_lines", 0),
)
unknown_products = action.get("unknown_products", False)
if unknown_products:
- message += "\nUnknow Product: \n"
- message += " * " + "\n * ".join(
- json.dumps(rec, indent=4) for rec in unknown_products
+ message += "Unknown Product:
"
+ message += "
".join(
+ f" * {html_escape(json.dumps(rec, ensure_ascii=False))}"
+ for rec in unknown_products
)
+ message += "
"
+
qty_diff = action.get("qty_diff", False)
if qty_diff:
- message += "\nDifference of Qty: \n"
- message += " * " + "\n * ".join(
- json.dumps(rec, indent=4) for rec in qty_diff
+ message += "Difference of Qty:
"
+ message += "
".join(
+ f" * {html_escape(json.dumps(rec, ensure_ascii=False))}"
+ for rec in qty_diff
)
- return message
+ message += "
"
+ return Markup(message)
diff --git a/edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml b/edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml
index c7ec9be5c..561452649 100644
--- a/edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml
+++ b/edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml
@@ -15,7 +15,7 @@
diff --git a/test-requirements.txt b/test-requirements.txt
index 505024994..a8133e4b5 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,5 +1,2 @@
odoo-test-helper
xmlunittest
-odoo-addon-base_edifact @ git+https://github.com/OCA/edi.git@refs/pull/1185/head#subdirectory=base_edifact
-odoo-addon-edi_storage_oca @ git+https://github.com/OCA/edi-framework.git@refs/pull/135/head#subdirectory=edi_storage_oca
-odoo-addon-edi_purchase_oca @ git+https://github.com/OCA/edi-framework.git@refs/pull/180/head#subdirectory=edi_purchase_oca
From 6ee67dc81bec1311ffaafa1a6547b21c6c377448 Mon Sep 17 00:00:00 2001
From: phucph
Date: Mon, 13 Apr 2026 14:37:37 +0700
Subject: [PATCH 10/10] [DON'T MERGE] text-requirement.txt
---
test-requirements.txt | 2 ++
1 file changed, 2 insertions(+)
diff --git a/test-requirements.txt b/test-requirements.txt
index a8133e4b5..0ae76ba80 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,2 +1,4 @@
odoo-test-helper
xmlunittest
+odoo-addon-base_edifact @ git+https://github.com/OCA/edi.git@refs/pull/1185/head#subdirectory=base_edifact
+odoo-addon-edi_purchase_oca @ git+https://github.com/OCA/edi-framework.git@refs/pull/180/head#subdirectory=edi_purchase_oca