diff --git a/edi_sale_oca/README.rst b/edi_sale_oca/README.rst new file mode 100644 index 0000000000..8368d389e4 --- /dev/null +++ b/edi_sale_oca/README.rst @@ -0,0 +1,122 @@ +========= +EDI Sales +========= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2b4f89b88e7d4e87c5a1249801ac1225c3c3e33b7c21ecd3f3eeb66e9e38f2da + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/14.0/edi_sale_oca + :alt: OCA/edi +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/edi-14-0/edi-14-0-edi_sale_oca + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/edi&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Inbound +~~~~~~~ +Receive sale orders from EDI channels. + +Control sale order confirmation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can decide if the order should be confirmed by exchange type. + +On your exchange type, go to advanced settings and add the following:: + + [...] + components: + process: + usage: input.process.sale.order + env_ctx: + # Values for the wizard + default_confirm_order: true + default_price_source: order + # Custom keys, whatever you need + random_one: true + +Note that `env_ctx` will propagate all keys to the whole env so you can use it +for any kind of context related configuration. In the case of the sale order import wizard +here we are just passing defaults as we could do in odoo standard. + +TODO: shall we add an exchange type example as demo? + +.. 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 +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Simone Orsi +* Duong (Tran Quoc) +* Thien (Vo Hong) + +Other credits +~~~~~~~~~~~~~ + +The migration of this module from 14.0 to 16.0 was financially supported by Camptocamp. + +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. + +.. |maintainer-simahawk| image:: https://github.com/simahawk.png?size=40px + :target: https://github.com/simahawk + :alt: simahawk + +Current `maintainer `__: + +|maintainer-simahawk| + +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_sale_oca/__init__.py b/edi_sale_oca/__init__.py new file mode 100644 index 0000000000..d2243add24 --- /dev/null +++ b/edi_sale_oca/__init__.py @@ -0,0 +1,3 @@ +from . import components +from . import models +from . import wizard diff --git a/edi_sale_oca/__manifest__.py b/edi_sale_oca/__manifest__.py new file mode 100644 index 0000000000..b469cdc1f8 --- /dev/null +++ b/edi_sale_oca/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "EDI Sales", + "summary": """ + Configuration and special behaviors for EDI on sales. + """, + "version": "14.0.1.0.0", + "development_status": "Alpha", + "license": "AGPL-3", + "author": "Camptocamp,Odoo Community Association (OCA)", + "maintainers": ["simahawk"], + "website": "https://github.com/OCA/edi", + "depends": [ + "edi_oca", + "edi_record_metadata_oca", + "sale_order_import", + ], + "data": [ + "data/job_function.xml", + "views/res_partner.xml", + "views/sale_order.xml", + "views/edi_exchange_record.xml", + "templates/exchange_chatter_msg.xml", + ], +} diff --git a/edi_sale_oca/components/__init__.py b/edi_sale_oca/components/__init__.py new file mode 100644 index 0000000000..bfb4ceb848 --- /dev/null +++ b/edi_sale_oca/components/__init__.py @@ -0,0 +1 @@ +from . import process diff --git a/edi_sale_oca/components/process.py b/edi_sale_oca/components/process.py new file mode 100644 index 0000000000..456c0a3957 --- /dev/null +++ b/edi_sale_oca/components/process.py @@ -0,0 +1,79 @@ +# Copyright 2021 Camptocamp SA +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import _ +from odoo.exceptions import UserError + +from odoo.addons.component.core import Component + + +class EDIExchangeSOInput(Component): + """Process sale orders.""" + + _name = "edi.input.sale.order.process" + _inherit = "edi.component.input.mixin" + _usage = "input.process.sale.order" + + def process(self): + wiz = self._setup_wizard() + res = wiz.import_order_button() + # TODO: log debug + if wiz.state == "update" and wiz.sale_id: + order = wiz.sale_id + msg = self.msg_order_existing_error + self._handle_existing_order(order, msg) + raise UserError(msg) + else: + order = self._handle_create_order(res["res_id"]) + return self.msg_order_created % order.name + + @property + def msg_order_existing_error(self): + return _("Sales order has already been imported before") + + @property + def msg_order_created(self): + return _("Sales order %s created") + + def _setup_wizard(self): + """Init a `sale.order.import` instance for current record.""" + # Set the right EDI origin on both order and lines + edi_defaults = {"origin_exchange_record_id": self.exchange_record.id} + addtional_ctx = dict( + sale_order_import__default_vals=dict(order=edi_defaults, lines=edi_defaults) + ) + wiz = ( + self.env["sale.order.import"] + .with_context(**addtional_ctx) + .sudo() + .create({}) + ) + wiz.order_file = self.exchange_record._get_file_content(binary=False) + wiz.order_filename = self.exchange_record.exchange_filename + wiz.order_file_change() + return wiz + + def _handle_create_order(self, order_id): + order = self.env["sale.order"].browse(order_id) + self.exchange_record._set_related_record(order) + return order + + def _handle_existing_order(self, order, message): + prev_record = self._get_previous_record(order) + self.exchange_record.message_post_with_view( + "edi_sale_oca.message_already_imported", + values={ + "order": order, + "prev_record": prev_record, + "message": message, + "level": "info", + }, + subtype_id=self.env.ref("mail.mt_note").id, + ) + + def _get_previous_record(self, order): + return self.env["edi.exchange.record"].search( + [("model", "=", "sale.order"), ("res_id", "=", order.id)], limit=1 + ) diff --git a/edi_sale_oca/data/job_function.xml b/edi_sale_oca/data/job_function.xml new file mode 100644 index 0000000000..94d62e089a --- /dev/null +++ b/edi_sale_oca/data/job_function.xml @@ -0,0 +1,7 @@ + + + + _edi_auto_handle_generate + + + diff --git a/edi_sale_oca/i18n/edi_sale_oca.pot b/edi_sale_oca/i18n/edi_sale_oca.pot new file mode 100644 index 0000000000..1e14c08c14 --- /dev/null +++ b/edi_sale_oca/i18n/edi_sale_oca.pot @@ -0,0 +1,187 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * edi_sale_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.view_order_form +msgid "EDI" +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.message_already_imported +msgid "Message:" +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.message_already_imported +msgid "Order:" +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.message_already_imported +msgid "Previous record:" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__edi_disable_auto +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__edi_disable_auto +msgid "Disable auto" +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.view_order_form +msgid "Disable automated actions" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.ui.menu,name:edi_sale_oca.menu_sale_edi_root +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.view_order_form +msgid "EDI" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__edi_id +msgid "EDI ID" +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.view_sales_order_filter +msgid "EDI exchange" +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.view_sales_order_filter +msgid "EDI exchange type" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__origin_edi_endpoint_id +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__origin_edi_endpoint_id +msgid "EDI origin endpoint" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__origin_exchange_type_id +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__origin_exchange_type_id +msgid "EDI origin exchange type" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__origin_exchange_record_id +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__origin_exchange_record_id +msgid "EDI origin record" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order__origin_exchange_record_id +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order_line__origin_exchange_record_id +msgid "EDI record that originated this document." +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__edi_config +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__edi_config +msgid "Edi Config" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__edi_exchange_ready +msgid "Edi Exchange Ready" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__edi_has_form_config +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__edi_has_form_config +msgid "Edi Has Form Config" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__exchange_record_ids +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__exchange_record_ids +msgid "Exchange Record" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__exchange_record_count +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__exchange_record_count +msgid "Exchange Record Count" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.ui.menu,name:edi_sale_oca.menu_sale_edi_records +msgid "Exchanges" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order_line__edi_id +msgid "Internal or external identifier for records." +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order__origin_edi_endpoint_id +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order_line__origin_edi_endpoint_id +msgid "Record generated via this endpoint" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.actions.act_window,name:edi_sale_oca.act_open_edi_exchange_record_sale_order_view +msgid "Sale Order Exchange Records" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model,name:edi_sale_oca.model_sale_order_import +msgid "Sale Order Import from Files" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model,name:edi_sale_oca.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model,name:edi_sale_oca.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: edi_sale_oca +#. odoo-python +#: code:addons/edi_sale_oca/components/process.py:0 +#, python-format +msgid "Sales order %s created" +msgstr "" + +#. module: edi_sale_oca +#. odoo-python +#: code:addons/edi_sale_oca/components/process.py:0 +#, python-format +msgid "Sales order has already been imported before" +msgstr "" + +#. module: edi_sale_oca +#. odoo-python +#: code:addons/edi_sale_oca/components/process.py:0 +#, python-format +msgid "Something went wrong with the importing wizard." +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.view_sales_order_filter +msgid "Source: EDI" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order__edi_disable_auto +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order_line__edi_disable_auto +msgid "When marked, EDI automatic processing will be avoided" +msgstr "" diff --git a/edi_sale_oca/i18n/it.po b/edi_sale_oca/i18n/it.po new file mode 100644 index 0000000000..7ca1d19c2d --- /dev/null +++ b/edi_sale_oca/i18n/it.po @@ -0,0 +1,188 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * edi_sale_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.view_order_form +msgid "EDI" +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.message_already_imported +msgid "Message:" +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.message_already_imported +msgid "Order:" +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.message_already_imported +msgid "Previous record:" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__edi_disable_auto +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__edi_disable_auto +msgid "Disable auto" +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.view_order_form +msgid "Disable automated actions" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.ui.menu,name:edi_sale_oca.menu_sale_edi_root +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.view_order_form +msgid "EDI" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__edi_id +msgid "EDI ID" +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.view_sales_order_filter +msgid "EDI exchange" +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.view_sales_order_filter +msgid "EDI exchange type" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__origin_edi_endpoint_id +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__origin_edi_endpoint_id +msgid "EDI origin endpoint" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__origin_exchange_type_id +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__origin_exchange_type_id +msgid "EDI origin exchange type" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__origin_exchange_record_id +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__origin_exchange_record_id +msgid "EDI origin record" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order__origin_exchange_record_id +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order_line__origin_exchange_record_id +msgid "EDI record that originated this document." +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__edi_config +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__edi_config +msgid "Edi Config" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__edi_exchange_ready +msgid "Edi Exchange Ready" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__edi_has_form_config +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__edi_has_form_config +msgid "Edi Has Form Config" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__exchange_record_ids +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__exchange_record_ids +msgid "Exchange Record" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order__exchange_record_count +#: model:ir.model.fields,field_description:edi_sale_oca.field_sale_order_line__exchange_record_count +msgid "Exchange Record Count" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.ui.menu,name:edi_sale_oca.menu_sale_edi_records +msgid "Exchanges" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order_line__edi_id +msgid "Internal or external identifier for records." +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order__origin_edi_endpoint_id +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order_line__origin_edi_endpoint_id +msgid "Record generated via this endpoint" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.actions.act_window,name:edi_sale_oca.act_open_edi_exchange_record_sale_order_view +msgid "Sale Order Exchange Records" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model,name:edi_sale_oca.model_sale_order_import +msgid "Sale Order Import from Files" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model,name:edi_sale_oca.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model,name:edi_sale_oca.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: edi_sale_oca +#. odoo-python +#: code:addons/edi_sale_oca/components/process.py:0 +#, python-format +msgid "Sales order %s created" +msgstr "" + +#. module: edi_sale_oca +#. odoo-python +#: code:addons/edi_sale_oca/components/process.py:0 +#, python-format +msgid "Sales order has already been imported before" +msgstr "" + +#. module: edi_sale_oca +#. odoo-python +#: code:addons/edi_sale_oca/components/process.py:0 +#, python-format +msgid "Something went wrong with the importing wizard." +msgstr "" + +#. module: edi_sale_oca +#: model_terms:ir.ui.view,arch_db:edi_sale_oca.view_sales_order_filter +msgid "Source: EDI" +msgstr "" + +#. module: edi_sale_oca +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order__edi_disable_auto +#: model:ir.model.fields,help:edi_sale_oca.field_sale_order_line__edi_disable_auto +msgid "When marked, EDI automatic processing will be avoided" +msgstr "" diff --git a/edi_sale_oca/models/__init__.py b/edi_sale_oca/models/__init__.py new file mode 100644 index 0000000000..6aacb75313 --- /dev/null +++ b/edi_sale_oca/models/__init__.py @@ -0,0 +1 @@ +from . import sale_order diff --git a/edi_sale_oca/models/sale_order.py b/edi_sale_oca/models/sale_order.py new file mode 100644 index 0000000000..7e3f23e29d --- /dev/null +++ b/edi_sale_oca/models/sale_order.py @@ -0,0 +1,73 @@ +# Copyright 2022 Camptocamp SA +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _name = "sale.order" + _inherit = [ + "sale.order", + "edi.exchange.consumer.mixin", + ] + # Receiver may send or not the response on create + # then for each update IF required. + # https://docs.oasis-open.org/ubl/os-UBL-2.3/UBL-2.3.html#S-ORDERING-POST-AWARD + # https://docs.peppol.eu/poacc/upgrade-3/profiles/28-ordering + # /#_response_code_on_header_level + + # TBD: implementing OrdResp for all modifications + # can be complex to manage (also for the 3rd party). + # Hence, we could block further modifications w/ sale exceptions + # and ask the sender to issue a new order request. + # This approach seems suitable only for orders that do not get processed immediately. + + disable_edi_auto = fields.Boolean( + states={"draft": [("readonly", False)]}, + ) + + # edi_record_metadata api + def _edi_get_metadata_to_store(self, orig_vals): + data = super()._edi_get_metadata_to_store(orig_vals) + # collect line values + line_vals_by_edi_id = {} + for line_vals in orig_vals.get("order_line", []): + # line_vals in the form `(0, 0, vals)` + vals = line_vals[-1] + line_vals_by_edi_id[vals["edi_id"]] = vals + + data.update({"orig_values": {"lines": line_vals_by_edi_id}}) + return data + + +class SaleOrderLine(models.Model): + _name = "sale.order.line" + _inherit = [ + "sale.order.line", + "edi.exchange.consumer.mixin", + "edi.id.mixin", + ] + + disable_edi_auto = fields.Boolean(related="order_id.disable_edi_auto") + + # TODO: add test + 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): + return not self._is_delivery() and not self.display_type + + @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["sale.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_sale_oca/readme/CONTRIBUTORS.rst b/edi_sale_oca/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..ab783cd7d3 --- /dev/null +++ b/edi_sale_oca/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Simone Orsi +* Duong (Tran Quoc) +* Thien (Vo Hong) diff --git a/edi_sale_oca/readme/CREDITS.rst b/edi_sale_oca/readme/CREDITS.rst new file mode 100644 index 0000000000..4c5b2fca2a --- /dev/null +++ b/edi_sale_oca/readme/CREDITS.rst @@ -0,0 +1 @@ +The migration of this module from 14.0 to 16.0 was financially supported by Camptocamp. diff --git a/edi_sale_oca/readme/DESCRIPTION.rst b/edi_sale_oca/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..55cdc88ede --- /dev/null +++ b/edi_sale_oca/readme/DESCRIPTION.rst @@ -0,0 +1,27 @@ +Inbound +~~~~~~~ +Receive sale orders from EDI channels. + +Control sale order confirmation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can decide if the order should be confirmed by exchange type. + +On your exchange type, go to advanced settings and add the following:: + + [...] + components: + process: + usage: input.process.sale.order + env_ctx: + # Values for the wizard + default_confirm_order: true + default_price_source: order + # Custom keys, whatever you need + random_one: true + +Note that `env_ctx` will propagate all keys to the whole env so you can use it +for any kind of context related configuration. In the case of the sale order import wizard +here we are just passing defaults as we could do in odoo standard. + +TODO: shall we add an exchange type example as demo? diff --git a/edi_sale_oca/static/description/icon.png b/edi_sale_oca/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/edi_sale_oca/static/description/icon.png differ diff --git a/edi_sale_oca/static/description/index.html b/edi_sale_oca/static/description/index.html new file mode 100644 index 0000000000..9f189b1bdf --- /dev/null +++ b/edi_sale_oca/static/description/index.html @@ -0,0 +1,453 @@ + + + + + +EDI Sales + + + +
+

EDI Sales

+ + +

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

+
+

Inbound

+

Receive sale orders from EDI channels.

+
+
+

Control sale order confirmation

+

You can decide if the order should be confirmed by exchange type.

+

On your exchange type, go to advanced settings and add the following:

+
+[...]
+components:
+    process:
+        usage: input.process.sale.order
+    env_ctx:
+            # Values for the wizard
+            default_confirm_order: true
+            default_price_source: order
+            # Custom keys, whatever you need
+            random_one: true
+
+

Note that env_ctx will propagate all keys to the whole env so you can use it +for any kind of context related configuration. In the case of the sale order import wizard +here we are just passing defaults as we could do in odoo standard.

+

TODO: shall we add an exchange type example as demo?

+
+

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.

+
+ +
+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The migration of this module from 14.0 to 16.0 was financially supported by Camptocamp.

+
+
+

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.

+

Current maintainer:

+

simahawk

+

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_sale_oca/templates/exchange_chatter_msg.xml b/edi_sale_oca/templates/exchange_chatter_msg.xml new file mode 100644 index 0000000000..b57444cc5a --- /dev/null +++ b/edi_sale_oca/templates/exchange_chatter_msg.xml @@ -0,0 +1,37 @@ + + + + diff --git a/edi_sale_oca/tests/__init__.py b/edi_sale_oca/tests/__init__.py new file mode 100644 index 0000000000..80e5225788 --- /dev/null +++ b/edi_sale_oca/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_order +from . import test_process diff --git a/edi_sale_oca/tests/common.py b/edi_sale_oca/tests/common.py new file mode 100644 index 0000000000..2fc1f15406 --- /dev/null +++ b/edi_sale_oca/tests/common.py @@ -0,0 +1,50 @@ +# Copyright 2022 Camptocamp SA +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields + + +class OrderMixin(object): + @classmethod + def _create_sale_order(cls, **kw): + """Create a sale order + + :return: sale order + """ + model = cls.env["sale.order"] + vals = dict(commitment_date=fields.Date.today()) + vals.update(kw) + so_vals = model.play_onchanges(vals, []) + if "order_line" in so_vals: + so_vals["order_line"] = [(0, 0, x) for x in vals["order_line"]] + return model.create(so_vals) + + @classmethod + def _setup_order(cls, **kw): + cls.product_a = cls.env.ref("product.product_product_4") + cls.product_a.barcode = "1" * 14 + cls.product_b = cls.env.ref("product.product_product_4b") + cls.product_b.barcode = "2" * 14 + cls.product_c = cls.env.ref("product.product_product_4c") + cls.product_c.barcode = "3" * 14 + cls.product_d = cls.env.ref("product.product_product_5") + cls.product_d.barcode = "4" * 14 + line_defaults = kw.pop("line_defaults", {}) + vals = { + "partner_id": cls.env.ref("base.res_partner_10").id, + "commitment_date": "2022-07-29", + } + vals.update(kw) + if "client_order_ref" not in vals: + vals["client_order_ref"] = "ABC123" + vals["order_line"] = [ + {"product_id": cls.product_a.id, "product_uom_qty": 300, "edi_id": 1000}, + {"product_id": cls.product_b.id, "product_uom_qty": 200, "edi_id": 2000}, + {"product_id": cls.product_c.id, "product_uom_qty": 100, "edi_id": 3000}, + ] + if line_defaults: + for line in vals["order_line"]: + line.update(line_defaults) + cls.sale = cls._create_sale_order(**vals) + cls.sale.action_confirm() diff --git a/edi_sale_oca/tests/test_order.py b/edi_sale_oca/tests/test_order.py new file mode 100644 index 0000000000..3f231c3808 --- /dev/null +++ b/edi_sale_oca/tests/test_order.py @@ -0,0 +1,50 @@ +# Copyright 2022 Camptocamp SA +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import SavepointCase + +from odoo.addons.edi_oca.tests.common import EDIBackendTestMixin + +from .common import OrderMixin + + +class TestOrder(SavepointCase, EDIBackendTestMixin, OrderMixin): + @classmethod + def setUpClass(cls): + super().setUpClass() + # force metadata storage w/ proper key + 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"} + ) + cls._setup_order( + origin_exchange_record_id=cls.exc_record_in.id, + ) + + def test_line_origin(self): + order = self.sale + self.assertEqual(order.origin_exchange_record_id, self.exc_record_in) + lines = order.order_line + self.env["sale.order.line"].create( + [ + { + "order_id": order.id, + "product_id": self.product_d.id, + "product_uom_qty": 300, + "edi_id": 4000, + }, + { + "order_id": order.id, + "product_id": self.product_d.id, + "product_uom_qty": 400, + "edi_id": 5000, + }, + ] + ) + order.invalidate_cache() + 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) diff --git a/edi_sale_oca/tests/test_process.py b/edi_sale_oca/tests/test_process.py new file mode 100644 index 0000000000..20b80296bd --- /dev/null +++ b/edi_sale_oca/tests/test_process.py @@ -0,0 +1,147 @@ +# Copyright 2022 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import base64 +import textwrap +from unittest import mock + +from odoo.addons.component.tests.common import SavepointComponentCase +from odoo.addons.edi_oca.tests.common import EDIBackendTestMixin + + +class TestProcessComponent(SavepointComponentCase, EDIBackendTestMixin): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._setup_env() + cls.backend = cls._get_backend() + cls.exc_type = cls._create_exchange_type( + name="Test SO import", + code="test_so_import", + direction="input", + exchange_file_ext="xml", + exchange_filename_pattern="{record.identifier}-{type.code}-{dt}", + backend_id=cls.backend.id, + # Bypass required fields with default_import_type = 'xml' in sale_order_import + advanced_settings_edit=textwrap.dedent( + """ + components: + process: + usage: input.process.sale.order + env_ctx: + default_price_source: 'pricelist' + default_import_type: 'xml' + random_key: custom + """ + ), + ) + cls.record = cls.backend.create_record( + "test_so_import", {"edi_exchange_state": "input_received"} + ) + cls.record._set_file_content(b"") + cls.wiz_model = cls.env["sale.order.import"] + + def test_lookup(self): + comp = self.backend._get_component(self.record, "process") + self.assertEqual(comp._name, "edi.input.sale.order.process") + + def test_wizard_setup(self): + comp = self.backend._get_component(self.record, "process") + with mock.patch.object( + type(self.wiz_model), "order_file_change" + ) as md_onchange: + wiz = comp._setup_wizard() + self.assertEqual(wiz._name, self.wiz_model._name) + self.assertEqual(wiz.env.context["random_key"], "custom") + self.assertEqual( + base64.b64decode(wiz.order_file), b"" + ) + self.assertEqual(wiz.order_filename, self.record.exchange_filename) + self.assertEqual(wiz.price_source, "pricelist") + md_onchange.assert_called() + + # In both tests here we don"t care about the specific format of the import. + # We only care that the wizard plugged with the component works as expected. + def test_existing_order(self): + order = self.env["sale.order"].create( + {"partner_id": self.env["res.partner"].search([], limit=1).id} + ) + m1 = mock.patch.object(type(self.wiz_model), "order_file_change") + m2 = mock.patch.object(type(self.wiz_model), "import_order_button") + m3 = mock.patch.object( + type(self.wiz_model), + "sale_id", + new_callable=mock.PropertyMock, + ) + m4 = mock.patch.object( + type(self.wiz_model), + "state", + new_callable=mock.PropertyMock, + ) + # Simulate the wizard detected an existing order state + err_msg = "Sales order has already been imported before" + with m1 as md_onchange, m2 as md_btn, m3 as md_sale_id, m4 as md_state: + md_sale_id.return_value = order + md_state.return_value = "update" + self.record.action_exchange_process() + md_onchange.assert_called() + md_btn.assert_called() + self.assertIn(err_msg, self.record.exchange_error) + + def test_new_order(self): + # Create the order manully and use it via the mock on md_btn + order = self.env["sale.order"].create( + {"partner_id": self.env["res.partner"].search([], limit=1).id} + ) + mock1 = mock.patch.object(type(self.wiz_model), "order_file_change") + mock2 = mock.patch.object(type(self.wiz_model), "import_order_button") + self.assertFalse(self.record.record) + # Simulate the wizard detected an existing order state + with mock1 as md_onchange, mock2 as md_btn: + md_btn.return_value = {"res_id": order.id} + self.record.action_exchange_process() + md_onchange.assert_called() + md_btn.assert_called() + + self.assertEqual(self.record.edi_exchange_state, "input_processed") + self.assertEqual(self.record.record, order) + self.assertIn( + "Exchange processed successfully", + "|".join(order.message_ids.mapped("body")), + ) + + def test_metadata(self): + parsed_order = { + "partner": {"email": "john.doe@example.com"}, + "date": "2023-05-18", + "order_ref": "EDISALE", + "lines": [ + { + "product": {"code": "FURN_8888"}, + "qty": 1, + "uom": {"unece_code": "C62"}, + "price_unit": 100, + "order_line_ref": "1111", + } + ], + "chatter_msg": [], + "doc_type": "rfq", + } + self.wiz_model.with_context( + edi_framework_action="process", + sale_order_import__default_vals=dict( + order=dict(origin_exchange_record_id=self.record.id) + ), + ).create_order(parsed_order, "pricelist") + metadata = self.record.get_metadata() + # Lines are mapped via `edi_id` (coming from `order_line_ref` by default) + line_metadata = metadata["orig_values"]["lines"]["1111"] + for k in ( + "product_id", + "product_uom_qty", + "product_uom", + "price_unit", + "edi_id", + ): + self.assertIn(k, line_metadata) diff --git a/edi_sale_oca/views/edi_exchange_record.xml b/edi_sale_oca/views/edi_exchange_record.xml new file mode 100644 index 0000000000..45f6475beb --- /dev/null +++ b/edi_sale_oca/views/edi_exchange_record.xml @@ -0,0 +1,30 @@ + + + + + + Sale Order Exchange Records + ir.actions.act_window + edi.exchange.record + tree,form + [('model', '=', 'sale.order')] + {} + + + + diff --git a/edi_sale_oca/views/res_partner.xml b/edi_sale_oca/views/res_partner.xml new file mode 100644 index 0000000000..c0afc4a76a --- /dev/null +++ b/edi_sale_oca/views/res_partner.xml @@ -0,0 +1,22 @@ + + + + + res.partner.view.form + res.partner + + + + + + + + + + + diff --git a/edi_sale_oca/views/sale_order.xml b/edi_sale_oca/views/sale_order.xml new file mode 100644 index 0000000000..a95bd268f3 --- /dev/null +++ b/edi_sale_oca/views/sale_order.xml @@ -0,0 +1,95 @@ + + + + + sale.order.form (in edi_sale) + sale.order + + + + + + + + + + + + + + + + + + + + + + + + + sale.order.tree (in edi_sale) + sale.order + + + + + + + + + + + sale.order.search (in edi_sale) + sale.order + + + + + + + + + + + + + + + + diff --git a/edi_sale_oca/wizard/__init__.py b/edi_sale_oca/wizard/__init__.py new file mode 100644 index 0000000000..e0ddf6156f --- /dev/null +++ b/edi_sale_oca/wizard/__init__.py @@ -0,0 +1 @@ +from . import sale_order_import diff --git a/edi_sale_oca/wizard/sale_order_import.py b/edi_sale_oca/wizard/sale_order_import.py new file mode 100644 index 0000000000..81a30e8f95 --- /dev/null +++ b/edi_sale_oca/wizard/sale_order_import.py @@ -0,0 +1,24 @@ +# Copyright 2022 Camptocamp SA +# @author: Simone Orsi +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import api, models + + +class SaleOrderImport(models.TransientModel): + _inherit = "sale.order.import" + + @api.model + def _prepare_create_order_line( + self, product, uom, order, import_line, price_source + ): + vals = super()._prepare_create_order_line( + product, uom, order, import_line, price_source + ) + # TODO: we should probably add an ext reference field to s.o.l. in sale_order_import + # and get rid of this override. + vals["edi_id"] = import_line.get("order_line_ref") or import_line.get( + "sequence" + ) + return vals diff --git a/setup/edi_sale_oca/odoo/addons/edi_sale_oca b/setup/edi_sale_oca/odoo/addons/edi_sale_oca new file mode 120000 index 0000000000..93db19f64c --- /dev/null +++ b/setup/edi_sale_oca/odoo/addons/edi_sale_oca @@ -0,0 +1 @@ +../../../../edi_sale_oca \ No newline at end of file diff --git a/setup/edi_sale_oca/setup.py b/setup/edi_sale_oca/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/edi_sale_oca/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)