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 Elec­tronic 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 Elec­tronic 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 + + + +
+

EDI PURCHASE EDIFACT OCA

+ + +

Alpha License: AGPL-3 OCA/edi Translate me on Weblate Try me on Runboat

+
+
UN/EDIFACT
+
United Nations rules for Elec­tronic 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

+ +
+

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

+ +
+
+

Other credits

+

The development of this module has been financially supported by:

+
    +
  • Trobz
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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/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 + +
+ +
+

Upload below the customer order as EDIFACT file.

+
+
+ + + + + +
+
+
+
+
+ + 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 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Alpha License: AGPL-3 OCA/edi Translate me on Weblate Try me on Runboat

+

Alpha License: AGPL-3 OCA/edi Translate me on Weblate Try me on Runboat

UN/EDIFACT
United Nations rules for Elec­tronic Data Interchange for Administration, Commerce and Transport
@@ -403,7 +403,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.

@@ -430,11 +430,13 @@

Other credits

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +Odoo Community Association +

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 @@
-

Upload below the customer order as EDIFACT file.

+

Upload below the customer order as EDIFACT file.

@@ -38,7 +36,7 @@ purchase.order.import form new - + Date: Mon, 14 Apr 2025 13:39:50 +0200 Subject: [PATCH 03/10] [MIG] edi_purchase_edifact_oca to 14.0 [FIX] edi_purchase_edifact_oca: Use company directly from purchase.order --- edi_purchase_edifact_oca/__manifest__.py | 2 +- edi_purchase_edifact_oca/models/purchase.py | 2 +- edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/edi_purchase_edifact_oca/__manifest__.py b/edi_purchase_edifact_oca/__manifest__.py index f45d271b2..1e0ef8541 100644 --- a/edi_purchase_edifact_oca/__manifest__.py +++ b/edi_purchase_edifact_oca/__manifest__.py @@ -3,7 +3,7 @@ { "name": "EDI PURCHASE EDIFACT OCA", "summary": "Create and send EDIFACT order files", - "version": "12.0.1.0.0", + "version": "14.0.1.0.0", "development_status": "Alpha", "website": "https://github.com/OCA/edi", "author": "Trobz, Odoo Community Association (OCA)", diff --git a/edi_purchase_edifact_oca/models/purchase.py b/edi_purchase_edifact_oca/models/purchase.py index 5e7a8d6c6..435d9d8c2 100644 --- a/edi_purchase_edifact_oca/models/purchase.py +++ b/edi_purchase_edifact_oca/models/purchase.py @@ -63,7 +63,7 @@ def _edifact_purchase_get_interchange(self): # 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 + [("partner_id", "=", self.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` 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 c9a24f5a7..c7ec9be5c 100644 --- a/edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml +++ b/edi_purchase_edifact_oca/wizard/purchase_order_import_view.xml @@ -26,7 +26,7 @@ class="oe_highlight" string="Import" /> -