diff --git a/edi_purchase_oca/README.rst b/edi_purchase_oca/README.rst new file mode 100644 index 000000000..f77e3e3da --- /dev/null +++ b/edi_purchase_oca/README.rst @@ -0,0 +1,93 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============ +EDI Purchase +============ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:8ab40ab13f4a26ea0ba04ff19e53af88e490d1b2e783699454ae6255422e00c3 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi--framework-lightgray.png?logo=github + :target: https://github.com/OCA/edi-framework/tree/19.0/edi_purchase_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-framework-19-0/edi-framework-19-0-edi_purchase_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-framework&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Handle purchase orders via EDI. + +This is a base module to plug purchase processes with the EDI framework. + +To handle inbound/outbound purchase orders, you need to create your own +integration modules on top of this base module. + +.. 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 +------- + +* ForgeFlow +* Camptocamp + +Contributors +------------ + +- Lois Rilo lois.rilo@forgeflow.com +- Simone Orsi simone.orsi@camptocamp.com +- Phan Hong Phuc + +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-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_oca/__init__.py b/edi_purchase_oca/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/edi_purchase_oca/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/edi_purchase_oca/__manifest__.py b/edi_purchase_oca/__manifest__.py new file mode 100644 index 000000000..91f3fcf70 --- /dev/null +++ b/edi_purchase_oca/__manifest__.py @@ -0,0 +1,32 @@ +# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "EDI Purchase", + "summary": """ + Define EDI Configuration for Purchase Orders""", + "version": "19.0.1.0.0", + "development_status": "Alpha", + "license": "LGPL-3", + "author": "ForgeFlow, Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/edi-framework", + "depends": [ + "purchase", + "edi_core_oca", + "edi_record_metadata_oca", + "edi_component_oca", + ], + "data": [ + # Data + "data/edi_configuration.xml", + # Views + "views/edi_exchange_record_views.xml", + "views/purchase_order_views.xml", + "views/res_partner_view.xml", + ], + "demo": [ + "demo/edi_backend.xml", + "demo/edi_exchange_type.xml", + "demo/edi_configuration.xml", + ], +} diff --git a/edi_purchase_oca/data/edi_configuration.xml b/edi_purchase_oca/data/edi_configuration.xml new file mode 100644 index 000000000..41417f92c --- /dev/null +++ b/edi_purchase_oca/data/edi_configuration.xml @@ -0,0 +1,13 @@ + + + + + On PO state change + on_edi_purchase_order_state_change + Trigger when a purchase order state changes + + + diff --git a/edi_purchase_oca/demo/edi_backend.xml b/edi_purchase_oca/demo/edi_backend.xml new file mode 100644 index 000000000..3410efd87 --- /dev/null +++ b/edi_purchase_oca/demo/edi_backend.xml @@ -0,0 +1,11 @@ + + + + Purchase DEMO + purchase_demo + + + purchase DEMO + + + diff --git a/edi_purchase_oca/demo/edi_configuration.xml b/edi_purchase_oca/demo/edi_configuration.xml new file mode 100644 index 000000000..6f614a31f --- /dev/null +++ b/edi_purchase_oca/demo/edi_configuration.xml @@ -0,0 +1,36 @@ + + + + Demo Purchase Order - order confirmed + Show case how you can send out an order automatically + + + + + +# ('draft', 'RFQ'), +# ('sent', 'RFQ Sent'), +# ('to approve', 'To Approve'), +# ('purchase', 'Purchase Order'), +# ('cancel', 'Cancelled') +if record.state == 'purchase': + record._edi_send_via_edi(conf.type_id) + + + + Demo Purchase Order - order cancelled + Show case how you can send out an order automatically + + + + + +if record.state == 'cancel': + record._edi_send_via_edi(conf.type_id) + + + diff --git a/edi_purchase_oca/demo/edi_exchange_type.xml b/edi_purchase_oca/demo/edi_exchange_type.xml new file mode 100644 index 000000000..161349aae --- /dev/null +++ b/edi_purchase_oca/demo/edi_exchange_type.xml @@ -0,0 +1,12 @@ + + + + + + Demo Purchase Order out + demo_PurchaseOrder_out + output + {record_name}-{type.code}-{dt} + xml + + diff --git a/edi_purchase_oca/i18n/edi_purchase_oca.pot b/edi_purchase_oca/i18n/edi_purchase_oca.pot new file mode 100644 index 000000000..be7f21321 --- /dev/null +++ b/edi_purchase_oca/i18n/edi_purchase_oca.pot @@ -0,0 +1,122 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * edi_purchase_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-12-31 04:47+0000\n" +"PO-Revision-Date: 2025-12-31 04:47+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: edi_purchase_oca +#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form +msgid "EDI" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.model,name:edi_purchase_oca.model_res_partner +msgid "Contact" +msgstr "" + +#. module: edi_purchase_oca +#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form +msgid "Disable" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_disable_auto +msgid "Disable auto" +msgstr "" + +#. module: edi_purchase_oca +#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form +msgid "EDI" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__origin_exchange_type_id +msgid "EDI origin exchange type" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__origin_exchange_record_id +msgid "EDI origin record" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_res_partner__edi_purchase_conf_ids +#: model:ir.model.fields,field_description:edi_purchase_oca.field_res_users__edi_purchase_conf_ids +msgid "EDI purchase configuration" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order__origin_exchange_record_id +msgid "EDI record that originated this document." +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_config +msgid "Edi Config" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_has_form_config +msgid "Edi Has Form Config" +msgstr "" + +#. module: edi_purchase_oca +#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form +msgid "Electronic Data Interchange" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_record_ids +msgid "Exchange Record" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_record_count +msgid "Exchange Record Count" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_related_record_ids +msgid "Exchange Related Record" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.ui.menu,name:edi_purchase_oca.menu_purchase_edi_root +msgid "Exchange records" +msgstr "" + +#. module: edi_purchase_oca +#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.view_partner_form +msgid "Purchase" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.model,name:edi_purchase_oca.model_purchase_order +msgid "Purchase Order" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.actions.act_window,name:edi_purchase_oca.act_open_edi_exchange_record_purchase_order_view +msgid "Purchase Order Exchange Record" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.ui.menu,name:edi_purchase_oca.menu_purchase_edi_exchange_record +msgid "Purchase Orders" +msgstr "" + +#. module: edi_purchase_oca +#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order__edi_disable_auto +msgid "When marked, EDI automatic processing will be avoided" +msgstr "" diff --git a/edi_purchase_oca/i18n/es.po b/edi_purchase_oca/i18n/es.po new file mode 100644 index 000000000..7d7a6b930 --- /dev/null +++ b/edi_purchase_oca/i18n/es.po @@ -0,0 +1,122 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * edi_purchase_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-12-31 04:48+0000\n" +"PO-Revision-Date: 2025-12-31 04:48+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: edi_purchase_oca +#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form +msgid "EDI" +msgstr "EDI" + +#. module: edi_purchase_oca +#: model:ir.model,name:edi_purchase_oca.model_res_partner +msgid "Contact" +msgstr "Contacto" + +#. module: edi_purchase_oca +#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form +msgid "Disable" +msgstr "Desactivar" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_disable_auto +msgid "Disable auto" +msgstr "Desactivar automático" + +#. module: edi_purchase_oca +#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form +msgid "EDI" +msgstr "EDI" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__origin_exchange_type_id +msgid "EDI origin exchange type" +msgstr "Tipo de intercambio de origen EDI" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__origin_exchange_record_id +msgid "EDI origin record" +msgstr "Registro de origen EDI" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_res_partner__edi_purchase_conf_ids +#: model:ir.model.fields,field_description:edi_purchase_oca.field_res_users__edi_purchase_conf_ids +msgid "EDI purchase configuration" +msgstr "Configuración de compra EDI" + +#. module: edi_purchase_oca +#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order__origin_exchange_record_id +msgid "EDI record that originated this document." +msgstr "Registro EDI que originó este documento" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_config +msgid "Edi Config" +msgstr "Configuración EDI" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__edi_has_form_config +msgid "Edi Has Form Config" +msgstr "EDI tiene configuración de formulario" + +#. module: edi_purchase_oca +#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.purchase_order_form +msgid "Electronic Data Interchange" +msgstr "Intercambio Electrónico de Datos" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_record_ids +msgid "Exchange Record" +msgstr "Registro de intercambio" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_record_count +msgid "Exchange Record Count" +msgstr "Conteo de registros de intercambio" + +#. module: edi_purchase_oca +#: model:ir.model.fields,field_description:edi_purchase_oca.field_purchase_order__exchange_related_record_ids +msgid "Exchange Related Record" +msgstr "Registro relacionado con el intercambio" + +#. module: edi_purchase_oca +#: model:ir.ui.menu,name:edi_purchase_oca.menu_purchase_edi_root +msgid "Exchange records" +msgstr "Registros de intercambio" + +#. module: edi_purchase_oca +#: model_terms:ir.ui.view,arch_db:edi_purchase_oca.view_partner_form +msgid "Purchase" +msgstr "Compra" + +#. module: edi_purchase_oca +#: model:ir.model,name:edi_purchase_oca.model_purchase_order +msgid "Purchase Order" +msgstr "Orden de compra" + +#. module: edi_purchase_oca +#: model:ir.actions.act_window,name:edi_purchase_oca.act_open_edi_exchange_record_purchase_order_view +msgid "Purchase Order Exchange Record" +msgstr "Registro de intercambio de la orden de compra" + +#. module: edi_purchase_oca +#: model:ir.ui.menu,name:edi_purchase_oca.menu_purchase_edi_exchange_record +msgid "Purchase Orders" +msgstr "Órdenes de compra" + +#. module: edi_purchase_oca +#: model:ir.model.fields,help:edi_purchase_oca.field_purchase_order__edi_disable_auto +msgid "When marked, EDI automatic processing will be avoided" +msgstr "Cuando se marque, se evitará el procesamiento automático de EDI" diff --git a/edi_purchase_oca/models/__init__.py b/edi_purchase_oca/models/__init__.py new file mode 100644 index 000000000..7b66e4fca --- /dev/null +++ b/edi_purchase_oca/models/__init__.py @@ -0,0 +1,3 @@ +from . import purchase_order_line +from . import purchase_order +from . import res_partner diff --git a/edi_purchase_oca/models/purchase_order.py b/edi_purchase_oca/models/purchase_order.py new file mode 100644 index 000000000..f066ce6c7 --- /dev/null +++ b/edi_purchase_oca/models/purchase_order.py @@ -0,0 +1,27 @@ +# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import models + + +class PurchaseOrder(models.Model): + _name = "purchase.order" + _inherit = [ + "purchase.order", + "edi.exchange.consumer.mixin", + ] + + def _edi_config_field_relation(self): + return self.partner_id.edi_purchase_conf_ids + + # edi_record_metadata api + def _edi_get_metadata_to_store(self, orig_vals): + data = super()._edi_get_metadata_to_store(orig_vals) + line_vals_by_edi_id = {} + for line_vals in orig_vals.get("order_line", []): + vals = line_vals[-1] + edi_id = vals.get("edi_id") + if edi_id: + line_vals_by_edi_id[edi_id] = vals + data.update({"orig_values": {"lines": line_vals_by_edi_id}}) + return data diff --git a/edi_purchase_oca/models/purchase_order_line.py b/edi_purchase_oca/models/purchase_order_line.py new file mode 100644 index 000000000..9cec4f939 --- /dev/null +++ b/edi_purchase_oca/models/purchase_order_line.py @@ -0,0 +1,36 @@ +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class PurchaseOrderLine(models.Model): + _name = "purchase.order.line" + _inherit = [ + "purchase.order.line", + "edi.exchange.consumer.mixin", + "edi.id.mixin", + ] + + edi_disable_auto = fields.Boolean(related="order_id.edi_disable_auto") + edi_exchange_ready = fields.Boolean(compute="_compute_edi_exchange_ready") + + @api.depends() + def _compute_edi_exchange_ready(self): + for rec in self: + rec.edi_exchange_ready = rec._edi_exchange_ready() + + def _edi_exchange_ready(self): + # Only product lines are eligible for EDI processing + # sections/notes and downpayment lines should be ignored + return not self.display_type and not self.is_downpayment + + @api.model_create_multi + def create(self, vals_list): + # Set default origin if not passed + for vals in vals_list: + orig_id = vals.get("origin_exchange_record_id") + if not orig_id and "order_id" in vals: + order = self.env["purchase.order"].browse(vals["order_id"]) + vals["origin_exchange_record_id"] = order.origin_exchange_record_id.id + return super().create(vals_list) diff --git a/edi_purchase_oca/models/res_partner.py b/edi_purchase_oca/models/res_partner.py new file mode 100644 index 000000000..815523731 --- /dev/null +++ b/edi_purchase_oca/models/res_partner.py @@ -0,0 +1,19 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models +from odoo.fields import Domain + + +class ResPartner(models.Model): + _inherit = "res.partner" + + edi_purchase_conf_ids = fields.Many2many( + string="EDI purchase configuration", + comodel_name="edi.configuration", + relation="res_partner_edi_purchase_configuration_rel", + column1="partner_id", + column2="conf_id", + domain=Domain("model_name", "=", "purchase.order"), + ) diff --git a/edi_purchase_oca/pyproject.toml b/edi_purchase_oca/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/edi_purchase_oca/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/edi_purchase_oca/readme/CONTRIBUTORS.md b/edi_purchase_oca/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..a18e9df0d --- /dev/null +++ b/edi_purchase_oca/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +* Lois Rilo +* Simone Orsi +* Phan Hong Phuc \<\> diff --git a/edi_purchase_oca/readme/DESCRIPTION.md b/edi_purchase_oca/readme/DESCRIPTION.md new file mode 100644 index 000000000..0ea465878 --- /dev/null +++ b/edi_purchase_oca/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +Handle purchase orders via EDI. + +This is a base module to plug purchase processes with the EDI framework. + +To handle inbound/outbound purchase orders, you need to create your own +integration modules on top of this base module. diff --git a/edi_purchase_oca/static/description/icon.png b/edi_purchase_oca/static/description/icon.png new file mode 100644 index 000000000..a79752645 Binary files /dev/null and b/edi_purchase_oca/static/description/icon.png differ diff --git a/edi_purchase_oca/static/description/index.html b/edi_purchase_oca/static/description/index.html new file mode 100644 index 000000000..69c5b549f --- /dev/null +++ b/edi_purchase_oca/static/description/index.html @@ -0,0 +1,441 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

EDI Purchase

+ +

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

+

Handle purchase orders via EDI.

+

This is a base module to plug purchase processes with the EDI framework.

+

To handle inbound/outbound purchase orders, you need to create your own +integration modules on top of this base module.

+
+

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

+
    +
  • ForgeFlow
  • +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

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-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_oca/tests/__init__.py b/edi_purchase_oca/tests/__init__.py new file mode 100644 index 000000000..6cb436569 --- /dev/null +++ b/edi_purchase_oca/tests/__init__.py @@ -0,0 +1,3 @@ +from . import common +from . import test_generate +from . import test_order diff --git a/edi_purchase_oca/tests/common.py b/edi_purchase_oca/tests/common.py new file mode 100644 index 000000000..1bd169cd4 --- /dev/null +++ b/edi_purchase_oca/tests/common.py @@ -0,0 +1,126 @@ +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import timedelta + +from odoo import fields +from odoo.fields import Command + +from odoo.addons.edi_core_oca.tests.common import EDIBackendTestMixin + + +class PurchaseEDIBackendTestMixin(EDIBackendTestMixin): + @classmethod + def _get_backend_type(cls): + backend_type = cls.env["edi.backend.type"].search( + [("code", "=", "purchase_demo")], limit=1 + ) + if backend_type: + return backend_type + return cls.env["edi.backend.type"].create( + { + "name": "Purchase DEMO", + "code": "purchase_demo", + } + ) + + @classmethod + def _get_backend(cls): + backend_type = cls._get_backend_type() + backend = cls.env["edi.backend"].search( + [("backend_type_id", "=", backend_type.id)], limit=1 + ) + if backend: + return backend + return cls.env["edi.backend"].create( + { + "name": "purchase DEMO", + "backend_type_id": backend_type.id, + } + ) + + @classmethod + def _create_exchange_type(cls, **kw): + model = cls.env["edi.exchange.type"] + code = kw.get("code") + if code: + exchange_type = model.search( + [("code", "=", code), ("backend_id", "=", cls.backend.id)], limit=1 + ) + if exchange_type: + return exchange_type + return super()._create_exchange_type(**kw) + + +class OrderMixin: + @classmethod + def _create_purchase_order(cls, **kw): + model = cls.env["purchase.order"] + vals = { + "partner_id": cls.vendor.id, + "user_id": cls.env.ref("base.user_admin").id, + "date_planned": fields.Datetime.now(), + } + vals.update(kw) + if hasattr(model, "play_onchanges"): + po_vals = model.play_onchanges(vals, []) + else: + po_vals = vals.copy() + if "order_line" in vals: + po_vals["order_line"] = [Command.create(x) for x in vals["order_line"]] + return model.create(po_vals) + + @classmethod + def _create_product(cls, name, uom): + return cls.env["product.product"].create( + { + "name": name, + "uom_id": uom.id, + "purchase_ok": True, + } + ) + + @classmethod + def _setup_order(cls, **kw): + cls.uom_unit = cls.env.ref("uom.product_uom_unit") + cls.vendor = cls.env["res.partner"].create( + {"name": "Azure Interior", "country_id": cls.env.company.country_id.id} + ) + cls.products = { + name: cls._create_product(name, uom) + for name, uom in { + "Delivery Grid": cls.uom_unit, + "Delivery Pallet": cls.uom_unit, + "Conference Room": cls.uom_unit, + }.items() + } + cls.product = cls.products["Delivery Grid"] + line_defaults = kw.pop("line_defaults", {}) + vals = { + "partner_id": cls.vendor.id, + "date_planned": fields.Datetime.now() + timedelta(days=7), + } + vals.update(kw) + if "partner_ref" not in vals: + vals["partner_ref"] = "ABC123" + vals["order_line"] = [ + { + "product_id": cls.products["Delivery Grid"].id, + "product_qty": 300, + "edi_id": 1000, + }, + { + "product_id": cls.products["Delivery Pallet"].id, + "product_qty": 200, + "edi_id": 2000, + }, + { + "product_id": cls.products["Conference Room"].id, + "product_qty": 100, + "edi_id": 3000, + }, + ] + if line_defaults: + for line in vals["order_line"]: + line.update(line_defaults) + return vals diff --git a/edi_purchase_oca/tests/test_generate.py b/edi_purchase_oca/tests/test_generate.py new file mode 100644 index 000000000..709ccea0e --- /dev/null +++ b/edi_purchase_oca/tests/test_generate.py @@ -0,0 +1,126 @@ +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.tests.common import TransactionComponentRegistryCase +from odoo.addons.edi_component_oca.tests.fake_components import ( + FakeOutputGenerator, + FakeOutputSender, +) + +from .common import OrderMixin, PurchaseEDIBackendTestMixin + + +class Generator(FakeOutputGenerator): + _backend_type = "purchase_demo" + _exchange_type = "demo_PurchaseOrder_out" + + +class Sender(FakeOutputSender): + _backend_type = "purchase_demo" + _exchange_type = "demo_PurchaseOrder_out" + + +class TestProcessComponent( + TransactionComponentRegistryCase, PurchaseEDIBackendTestMixin, OrderMixin +): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._setup_registry(cls) + cls._setup_env() + cls._setup_records() + cls.exc_type = cls._create_exchange_type( + name="Demo Purchase Order out", + code="demo_PurchaseOrder_out", + direction="output", + exchange_filename_pattern="{record_name}-{type.code}-{dt}", + exchange_file_ext="xml", + ) + model = cls.env.ref("edi_component_oca.model_edi_oca_component_handler") + cls.exc_type.generate_model_id = model + cls.exc_type.send_model_id = model + cls.exc_type.process_model_id = model + cls.exc_type.receive_model_id = model + cls.edi_conf_confirmed = cls.env["edi.configuration"].create( + { + "name": "Demo Purchase Order - order confirmed", + "type_id": cls.exc_type.id, + "backend_id": cls.backend.id, + "model_id": cls.env["ir.model"]._get_id("purchase.order"), + "trigger_id": cls.env.ref( + "edi_purchase_oca.edi_conf_trigger_purchase_order_state_change" + ).id, + "snippet_do": ( + "if record.state == 'purchase':\n" + " record._edi_send_via_edi(conf.type_id)" + ), + } + ) + cls.edi_conf_cancelled = cls.env["edi.configuration"].create( + { + "name": "Demo Purchase Order - order cancelled", + "type_id": cls.exc_type.id, + "backend_id": cls.backend.id, + "model_id": cls.env["ir.model"]._get_id("purchase.order"), + "trigger_id": cls.env.ref( + "edi_purchase_oca.edi_conf_trigger_purchase_order_state_change" + ).id, + "snippet_do": ( + "if record.state == 'cancel':\n" + " record._edi_send_via_edi(conf.type_id)" + ), + } + ) + cls._setup_order() + cls._load_module_components(cls, "edi_core_oca") + cls._load_module_components(cls, "edi_purchase_oca") + cls._build_components( + cls, + Generator, + Sender, + ) + + def setUp(self): + super().setUp() + Generator.reset_faked() + Sender.reset_faked() + + def test_lookup(self): + record = self.backend.create_record(self.exc_type.code, {}) + comp = self.backend._get_component(record, "generate") + self.assertEqual(comp._name, Generator._name) + comp = self.backend._get_component(record, "send") + self.assertEqual(comp._name, Sender._name) + + def test_new_order_no_conf_no_output(self): + order = self._create_purchase_order() + order.button_confirm() + self.assertFalse(order.exchange_record_ids) + + def test_new_order_1conf_output(self): + self.vendor.edi_purchase_conf_ids = self.edi_conf_confirmed + order = self._create_purchase_order() + self.assertFalse(order.exchange_record_ids) + order.with_context(fake_output="ORDER CONFIRM").button_confirm() + self.assertEqual(len(order.exchange_record_ids), 1) + record = order.exchange_record_ids[0] + self.assertEqual(record._get_file_content(), "ORDER CONFIRM") + self.assertEqual(record.type_id, self.exc_type) + + def test_new_order_2conf_output(self): + self.vendor.edi_purchase_conf_ids = ( + self.edi_conf_confirmed | self.edi_conf_cancelled + ) + order = self._create_purchase_order() + self.assertFalse(order.exchange_record_ids) + order.with_context(fake_output="ORDER CONFIRM").button_confirm() + self.assertEqual(len(order.exchange_record_ids), 1) + record = order.exchange_record_ids[0] + self.assertEqual(record._get_file_content(), "ORDER CONFIRM") + self.assertEqual(record.type_id, self.exc_type) + order.with_context(fake_output="ORDER CANCEL").button_cancel() + record1, record2 = order.exchange_record_ids + self.assertEqual(record1.type_id, self.exc_type) + self.assertEqual(record1._get_file_content(), "ORDER CONFIRM") + self.assertEqual(record2.type_id, self.exc_type) + self.assertEqual(record2._get_file_content(), "ORDER CANCEL") diff --git a/edi_purchase_oca/tests/test_order.py b/edi_purchase_oca/tests/test_order.py new file mode 100644 index 000000000..3285fa516 --- /dev/null +++ b/edi_purchase_oca/tests/test_order.py @@ -0,0 +1,61 @@ +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + +from .common import OrderMixin, PurchaseEDIBackendTestMixin + + +class TestOrder(TransactionCase, PurchaseEDIBackendTestMixin, OrderMixin): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, edi_framework_action=True)) + cls._setup_records() + cls.exchange_type_in.exchange_filename_pattern = "{record.id}-{type.code}-{dt}" + cls.exc_record_in = cls.backend.create_record( + cls.exchange_type_in.code, {"edi_exchange_state": "input_received"} + ) + order_vals = cls._setup_order() + cls.order = cls._create_purchase_order( + origin_exchange_record_id=cls.exc_record_in.id, + **order_vals, + ) + + def test_line_origin(self): + order = self.order + self.assertEqual(order.origin_exchange_record_id, self.exc_record_in) + lines = order.order_line + self.env["purchase.order.line"].create( + [ + { + "order_id": order.id, + "product_id": self.product.id, + "product_qty": 20, + "price_unit": 100.0, + "edi_id": 2000, + }, + { + "order_id": order.id, + "product_id": self.product.id, + "product_qty": 30, + "price_unit": 100.0, + "edi_id": 3000, + }, + ] + ) + order.invalidate_recordset() + new_line1, new_line2 = order.order_line - lines + self.assertEqual(new_line1.origin_exchange_record_id, self.exc_record_in) + self.assertEqual(new_line2.origin_exchange_record_id, self.exc_record_in) + + def test_line_exchange_ready(self): + line_model = self.env["purchase.order.line"] + + regular_line = line_model.new({"product_id": self.product.id}) + section_line = line_model.new({"display_type": "line_section"}) + downpayment_line = line_model.new({"is_downpayment": True}) + + self.assertTrue(regular_line.edi_exchange_ready) + self.assertFalse(section_line.edi_exchange_ready) + self.assertFalse(downpayment_line.edi_exchange_ready) diff --git a/edi_purchase_oca/views/edi_exchange_record_views.xml b/edi_purchase_oca/views/edi_exchange_record_views.xml new file mode 100644 index 000000000..5b3dbeb92 --- /dev/null +++ b/edi_purchase_oca/views/edi_exchange_record_views.xml @@ -0,0 +1,29 @@ + + + + + Purchase Order Exchange Records + ir.actions.act_window + edi.exchange.record + list,form + [('model', '=', 'purchase.order')] + {} + + + + diff --git a/edi_purchase_oca/views/purchase_order_views.xml b/edi_purchase_oca/views/purchase_order_views.xml new file mode 100644 index 000000000..4c97b26e7 --- /dev/null +++ b/edi_purchase_oca/views/purchase_order_views.xml @@ -0,0 +1,52 @@ + + + + + purchase.order + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_purchase_oca/views/res_partner_view.xml b/edi_purchase_oca/views/res_partner_view.xml new file mode 100644 index 000000000..4985d326e --- /dev/null +++ b/edi_purchase_oca/views/res_partner_view.xml @@ -0,0 +1,21 @@ + + + + res.partner + + + + + + + + + + + + + + + + +