diff --git a/sale_order_split_strategy/README.rst b/sale_order_split_strategy/README.rst new file mode 100644 index 00000000000..36300ae8554 --- /dev/null +++ b/sale_order_split_strategy/README.rst @@ -0,0 +1,109 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +========================= +Sale Order Split Strategy +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:eae070c92d94657fec4e8c66ffa9654cd9e94c14538719432d4e840594c5e4dc + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-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%2Fsale--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/sale-workflow/tree/19.0/sale_order_split_strategy + :alt: OCA/sale-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sale-workflow-19-0/sale-workflow-19-0-sale_order_split_strategy + :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/sale-workflow&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides the possibility to define split strategies to be +applied on sales order for splitting. + +This is useful (but not only) when a sales order from external sources +(webshop, edi) includes products that need to be shipped by different +standards (temperature/environment) and so with different carriers. + +.. 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: + +Usage +===== + +To use this module you first need to create splitting strategies to +apply on sales order. + +:: + + Sales -> Configuration -> Split Startegies + +The splitting is based on an ir.filter applied on sale.order.line. + +Then on sales order, the Split Strategy field allows to select the +corresponding strategy. After which a button ``Split`` is available on +the form to split an order. + +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 + +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-grindtildeath| image:: https://github.com/grindtildeath.png?size=40px + :target: https://github.com/grindtildeath + :alt: grindtildeath + +Current `maintainer `__: + +|maintainer-grindtildeath| + +This module is part of the `OCA/sale-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_order_split_strategy/__init__.py b/sale_order_split_strategy/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/sale_order_split_strategy/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_order_split_strategy/__manifest__.py b/sale_order_split_strategy/__manifest__.py new file mode 100644 index 00000000000..304c601d682 --- /dev/null +++ b/sale_order_split_strategy/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Sale Order Split Strategy", + "summary": "Define strategies to split sales orders", + "version": "19.0.1.0.0", + "development_status": "Alpha", + "category": "Sales Management", + "website": "https://github.com/OCA/sale-workflow", + "author": "Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["grindtildeath"], + "license": "AGPL-3", + "depends": [ + "sale", + ], + "data": [ + "security/ir.model.access.csv", + "views/mail_message_template.xml", + "views/res_config.xml", + "views/sale_order_split_strategy.xml", + "views/sale_order.xml", + ], +} diff --git a/sale_order_split_strategy/i18n/it.po b/sale_order_split_strategy/i18n/it.po new file mode 100644 index 00000000000..5209c23c355 --- /dev/null +++ b/sale_order_split_strategy/i18n/it.po @@ -0,0 +1,200 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_order_split_strategy +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-01-21 08:49+0000\n" +"Last-Translator: mymage \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" +"X-Generator: Weblate 5.15.2\n" + +#. module: sale_order_split_strategy +#. odoo-python +#: code:addons/sale_order_split_strategy/models/sale_order.py:0 +msgid "" +"Cannot split order %(order_name)s according to its strategy because there " +"are no matching lines" +msgstr "" +"Non è possibile dividere l'ordine %(order_name)s in accordo alla sua " +"strategia perché non ci sono righe corrispondenti" + +#. module: sale_order_split_strategy +#. odoo-python +#: code:addons/sale_order_split_strategy/models/sale_order.py:0 +msgid "Cannot split orders %(order_names)s without any split strategy defined" +msgstr "" +"Non si può dividere l'ordine %(order_names)s senza una strategia di " +"suddivisione definita" + +#. module: sale_order_split_strategy +#: model:ir.model,name:sale_order_split_strategy.model_res_company +msgid "Companies" +msgstr "Aziende" + +#. module: sale_order_split_strategy +#: model:ir.model,name:sale_order_split_strategy.model_res_config_settings +msgid "Config Settings" +msgstr "Impostazioni configurazione" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__copy_notes +msgid "Copy Notes" +msgstr "Copia note" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__copy_sections +msgid "Copy Sections" +msgstr "Copia sezioni" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: sale_order_split_strategy +#: model:ir.model.fields.selection,name:sale_order_split_strategy.selection__res_company__sale_order_split_strategy_errors__do_nothing +msgid "Do nothing" +msgstr "Non fare nulla" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.res_config_settings_view_form_sale_exception +msgid "How split errors are to be reported" +msgstr "Come devono essere segnalati gli errori separati" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__id +msgid "ID" +msgstr "ID" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__line_filter_id +msgid "Line Filter" +msgstr "Filtro riga" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__name +msgid "Name" +msgstr "Nome" + +#. module: sale_order_split_strategy +#: model:ir.model,name:sale_order_split_strategy.model_sale_order_split_strategy +msgid "Order split strategy" +msgstr "Ordine strategia suddivisione" + +#. module: sale_order_split_strategy +#: model:ir.model.fields.selection,name:sale_order_split_strategy.selection__res_company__sale_order_split_strategy_errors__post_message +msgid "Post message" +msgstr "Invia messaggio" + +#. module: sale_order_split_strategy +#: model:ir.model.fields.selection,name:sale_order_split_strategy.selection__res_company__sale_order_split_strategy_errors__raise_errors +msgid "Raise errors" +msgstr "Segnala errori" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__remove_empty_sections_after_split +msgid "Remove Empty Sections After Split" +msgstr "Rimuovere sezioni vuote dopo la divisione" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_res_company__sale_order_split_strategy_errors +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_res_config_settings__sale_order_split_strategy_errors +msgid "Sale Order Split Strategy Errors" +msgstr "Errori strategia divisione ordine vendita" + +#. module: sale_order_split_strategy +#: model:ir.model,name:sale_order_split_strategy.model_sale_order +msgid "Sales Order" +msgstr "Ordine di vendita" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.view_order_form +msgid "Split" +msgstr "Dividi" + +#. module: sale_order_split_strategy +#: model:ir.actions.act_window,name:sale_order_split_strategy.sale_order_split_strategy_action +#: model:ir.ui.menu,name:sale_order_split_strategy.sale_order_split_strategy_menu +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.sale_order_split_strategy_view_tree +msgid "Split Strategies" +msgstr "Strategie suddivisione" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order__split_strategy_id +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.sale_order_split_strategy_view_form +msgid "Split Strategy" +msgstr "Strategia suddivisione" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,help:sale_order_split_strategy.field_sale_order__split_strategy_id +msgid "The strategy that will be used to split the sales order" +msgstr "La strategia che verrà usata per dividere l'ordine di vendita" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_to +msgid "This sale order had some of its lines split to" +msgstr "Questo ordine ha alcune righe suddivise in" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_from +msgid "This sale order was created after splitting lines from" +msgstr "" +"Questo ordine di vendita è stato creato dopo la suddivisione delle righe da" + +#. module: sale_order_split_strategy +#. odoo-python +#: code:addons/sale_order_split_strategy/models/sale_order.py:0 +msgid "" +"This sale order was not split using strategy %(strategy)s because there " +"would not be any lines left on this order." +msgstr "" +"Questo ordine di vendita non è stato suddiviso utilizzando la strategia %" +"(strategy)s perché non c'erano righe nell'ordine." + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_from +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_to +msgid "strategy." +msgstr "strategia." + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_from +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_to +msgid "using" +msgstr "usando" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" diff --git a/sale_order_split_strategy/i18n/pt.po b/sale_order_split_strategy/i18n/pt.po new file mode 100644 index 00000000000..68177d64fe0 --- /dev/null +++ b/sale_order_split_strategy/i18n/pt.po @@ -0,0 +1,188 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_order_split_strategy +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: pt\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: sale_order_split_strategy +#. odoo-python +#: code:addons/sale_order_split_strategy/models/sale_order.py:0 +msgid "" +"Cannot split order %(order_name)s according to its strategy because there " +"are no matching lines" +msgstr "" + +#. module: sale_order_split_strategy +#. odoo-python +#: code:addons/sale_order_split_strategy/models/sale_order.py:0 +msgid "Cannot split orders %(order_names)s without any split strategy defined" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model,name:sale_order_split_strategy.model_res_company +msgid "Companies" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model,name:sale_order_split_strategy.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__copy_notes +msgid "Copy Notes" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__copy_sections +msgid "Copy Sections" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__create_uid +msgid "Created by" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__create_date +msgid "Created on" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__display_name +msgid "Display Name" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields.selection,name:sale_order_split_strategy.selection__res_company__sale_order_split_strategy_errors__do_nothing +msgid "Do nothing" +msgstr "" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.res_config_settings_view_form_sale_exception +msgid "How split errors are to be reported" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__id +msgid "ID" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__write_date +msgid "Last Updated on" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__line_filter_id +msgid "Line Filter" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__name +msgid "Name" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model,name:sale_order_split_strategy.model_sale_order_split_strategy +msgid "Order split strategy" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields.selection,name:sale_order_split_strategy.selection__res_company__sale_order_split_strategy_errors__post_message +msgid "Post message" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields.selection,name:sale_order_split_strategy.selection__res_company__sale_order_split_strategy_errors__raise_errors +msgid "Raise errors" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__remove_empty_sections_after_split +msgid "Remove Empty Sections After Split" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_res_company__sale_order_split_strategy_errors +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_res_config_settings__sale_order_split_strategy_errors +msgid "Sale Order Split Strategy Errors" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model,name:sale_order_split_strategy.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__sequence +msgid "Sequence" +msgstr "" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.view_order_form +msgid "Split" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.actions.act_window,name:sale_order_split_strategy.sale_order_split_strategy_action +#: model:ir.ui.menu,name:sale_order_split_strategy.sale_order_split_strategy_menu +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.sale_order_split_strategy_view_tree +msgid "Split Strategies" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order__split_strategy_id +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.sale_order_split_strategy_view_form +msgid "Split Strategy" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,help:sale_order_split_strategy.field_sale_order__split_strategy_id +msgid "The strategy that will be used to split the sales order" +msgstr "" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_to +msgid "This sale order had some of its lines split to" +msgstr "" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_from +msgid "This sale order was created after splitting lines from" +msgstr "" + +#. module: sale_order_split_strategy +#. odoo-python +#: code:addons/sale_order_split_strategy/models/sale_order.py:0 +msgid "" +"This sale order was not split using strategy %(strategy)s because there " +"would not be any lines left on this order." +msgstr "" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_from +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_to +msgid "strategy." +msgstr "" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_from +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_to +msgid "using" +msgstr "" diff --git a/sale_order_split_strategy/i18n/sale_order_split_strategy.pot b/sale_order_split_strategy/i18n/sale_order_split_strategy.pot new file mode 100644 index 00000000000..68ee9c48a9b --- /dev/null +++ b/sale_order_split_strategy/i18n/sale_order_split_strategy.pot @@ -0,0 +1,187 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_order_split_strategy +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.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: sale_order_split_strategy +#. odoo-python +#: code:addons/sale_order_split_strategy/models/sale_order.py:0 +msgid "" +"Cannot split order %(order_name)s according to its strategy because there " +"are no matching lines" +msgstr "" + +#. module: sale_order_split_strategy +#. odoo-python +#: code:addons/sale_order_split_strategy/models/sale_order.py:0 +msgid "Cannot split orders %(order_names)s without any split strategy defined" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model,name:sale_order_split_strategy.model_res_company +msgid "Companies" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model,name:sale_order_split_strategy.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__copy_notes +msgid "Copy Notes" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__copy_sections +msgid "Copy Sections" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__create_uid +msgid "Created by" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__create_date +msgid "Created on" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__display_name +msgid "Display Name" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields.selection,name:sale_order_split_strategy.selection__res_company__sale_order_split_strategy_errors__do_nothing +msgid "Do nothing" +msgstr "" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.res_config_settings_view_form_sale_exception +msgid "How split errors are to be reported" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__id +msgid "ID" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__write_date +msgid "Last Updated on" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__line_filter_id +msgid "Line Filter" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__name +msgid "Name" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model,name:sale_order_split_strategy.model_sale_order_split_strategy +msgid "Order split strategy" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields.selection,name:sale_order_split_strategy.selection__res_company__sale_order_split_strategy_errors__post_message +msgid "Post message" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields.selection,name:sale_order_split_strategy.selection__res_company__sale_order_split_strategy_errors__raise_errors +msgid "Raise errors" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__remove_empty_sections_after_split +msgid "Remove Empty Sections After Split" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_res_company__sale_order_split_strategy_errors +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_res_config_settings__sale_order_split_strategy_errors +msgid "Sale Order Split Strategy Errors" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model,name:sale_order_split_strategy.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order_split_strategy__sequence +msgid "Sequence" +msgstr "" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.view_order_form +msgid "Split" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.actions.act_window,name:sale_order_split_strategy.sale_order_split_strategy_action +#: model:ir.ui.menu,name:sale_order_split_strategy.sale_order_split_strategy_menu +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.sale_order_split_strategy_view_tree +msgid "Split Strategies" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,field_description:sale_order_split_strategy.field_sale_order__split_strategy_id +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.sale_order_split_strategy_view_form +msgid "Split Strategy" +msgstr "" + +#. module: sale_order_split_strategy +#: model:ir.model.fields,help:sale_order_split_strategy.field_sale_order__split_strategy_id +msgid "The strategy that will be used to split the sales order" +msgstr "" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_to +msgid "This sale order had some of its lines split to" +msgstr "" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_from +msgid "This sale order was created after splitting lines from" +msgstr "" + +#. module: sale_order_split_strategy +#. odoo-python +#: code:addons/sale_order_split_strategy/models/sale_order.py:0 +msgid "" +"This sale order was not split using strategy %(strategy)s because there " +"would not be any lines left on this order." +msgstr "" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_from +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_to +msgid "strategy." +msgstr "" + +#. module: sale_order_split_strategy +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_from +#: model_terms:ir.ui.view,arch_db:sale_order_split_strategy.split_to +msgid "using" +msgstr "" diff --git a/sale_order_split_strategy/models/__init__.py b/sale_order_split_strategy/models/__init__.py new file mode 100644 index 00000000000..a427d2e6964 --- /dev/null +++ b/sale_order_split_strategy/models/__init__.py @@ -0,0 +1,4 @@ +from . import res_company +from . import res_config +from . import sale_order_split_strategy +from . import sale_order diff --git a/sale_order_split_strategy/models/res_company.py b/sale_order_split_strategy/models/res_company.py new file mode 100644 index 00000000000..b3b2e65c525 --- /dev/null +++ b/sale_order_split_strategy/models/res_company.py @@ -0,0 +1,17 @@ +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + sale_order_split_strategy_errors = fields.Selection( + [ + ("raise_errors", "Raise errors"), + ("post_message", "Post message"), + ("do_nothing", "Do nothing"), + ], + required=True, + default="raise_errors", + ) diff --git a/sale_order_split_strategy/models/res_config.py b/sale_order_split_strategy/models/res_config.py new file mode 100644 index 00000000000..fe8d3c5d4be --- /dev/null +++ b/sale_order_split_strategy/models/res_config.py @@ -0,0 +1,12 @@ +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + sale_order_split_strategy_errors = fields.Selection( + related="company_id.sale_order_split_strategy_errors", + readonly=False, + ) diff --git a/sale_order_split_strategy/models/sale_order.py b/sale_order_split_strategy/models/sale_order.py new file mode 100644 index 00000000000..e749591d693 --- /dev/null +++ b/sale_order_split_strategy/models/sale_order.py @@ -0,0 +1,175 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +import logging +from collections import defaultdict + +from odoo import fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + split_strategy_id = fields.Many2one( + comodel_name="sale.order.split.strategy", + help="The strategy that will be used to split the sales order", + ) + + def action_split(self, silent_errors=False): + # TODO: Remove silent_errors arg on next major migration and rely on company + # field + if silent_errors: + _logger.warning( + "sale.order.action_split argument silent_errors is deprecated." + " Please set error handling in split strategy at company level." + ) + silent_errors = ( + self.company_id.sale_order_split_strategy_errors != "raise_errors" + ) + orders_without_split = self.filtered(lambda o: not o.split_strategy_id) + if not silent_errors and orders_without_split: + raise UserError( + self.env._( + "Cannot split orders %(order_names)s " + "without any split strategy defined", + order_names=", ".join(orders_without_split.mapped("name")), + ) + ) + new_order_ids = [] + for order in self: + lines_to_split = order.split_strategy_id._select_lines_to_split(order) + if order._has_only_lines_to_split(lines_to_split): + order._handle_only_lines_to_split() + continue + if not lines_to_split: + order._handle_no_lines_to_split(silent_errors=silent_errors) + continue + new_order = order.copy(order._prepare_order_split_copy_defaults()) + sections_dict = order._get_lines_grouped_by_sections() + order._split_lines(sections_dict, lines_to_split, new_order) + order.message_post(body=order._prepare_message_split_to(new_order)) + new_order.message_post(body=new_order._prepare_message_split_from(order)) + new_order_ids.append(new_order.id) + order._postprocess_split_from() + new_order._postprocess_split_to(order) + return self.browse(new_order_ids) + + def _handle_no_lines_to_split(self, silent_errors=False): + self.ensure_one() + msg = self.env._( + "Cannot split order %(order_name)s according to its strategy" + " because there are no matching lines", + order_name=self.name, + ) + if not silent_errors: + raise UserError(msg) + elif self.company_id.sale_order_split_strategy_errors == "post_message": + self.message_post(body=msg) + + def _handle_only_lines_to_split(self): + self.ensure_one() + msg = self.env._( + "This sale order was not split using strategy %(strategy)s" + " because there would not be any lines left on this order.", + strategy=self.split_strategy_id.name, + ) + strategy_errors = self.company_id.sale_order_split_strategy_errors + if strategy_errors == "raise_errors": + raise UserError(msg) + elif strategy_errors == "post_message": + self.message_post(body=msg) + + def _has_only_lines_to_split(self, lines_to_split): + self.ensure_one() + return ( + self.order_line.filtered(lambda line: not line._is_delivery()) + == lines_to_split + ) + + def _split_lines(self, sections_dict, lines_to_split, target_order): + self.ensure_one() + copy_sections = self.split_strategy_id.copy_sections + copy_notes = self.split_strategy_id.copy_notes + for section_id, line_ids in sections_dict.items(): + section_line = self.env["sale.order.line"].browse(section_id) + section_lines = self.env["sale.order.line"].browse(line_ids) + for line in section_lines: + if line in lines_to_split: + line.write(target_order._prepare_order_split_line_move_defaults()) + # FIXME: We should consider if the line is part of the section + if copy_notes and line.display_type == "line_note": + line.copy(target_order._prepare_order_split_line_move_defaults()) + lines_in_section_to_split = [ + li in lines_to_split for li in section_lines if not li.display_type + ] + # If all lines in section are to split, move the section to split order + if copy_sections and section_line and all(lines_in_section_to_split): + section_line.write( + target_order._prepare_order_split_line_move_defaults() + ) + # If any line in section is to split, copy the section to split order + elif copy_sections and section_line and any(lines_in_section_to_split): + section_line.copy( + target_order._prepare_order_split_line_move_defaults() + ) + return True + + def _get_lines_grouped_by_sections(self): + # Prepare a dict from sale order lines and their sections + self.ensure_one() + sections_dict = defaultdict(list) + section_id = None + for line in self.order_line.sorted(): + if line.display_type == "line_section": + section_id = line.id + sections_dict[section_id].append(line.id) + return sections_dict + + def _prepare_message_split_from(self, order): + self.ensure_one() + return self.env["ir.ui.view"]._render_template( + "sale_order_split_strategy.split_from", values={"from_order": order} + ) + + def _prepare_message_split_to(self, order): + self.ensure_one() + return self.env["ir.ui.view"]._render_template( + "sale_order_split_strategy.split_to", + values={"from_order": self, "to_order": order}, + ) + + def _prepare_order_split_copy_defaults(self): + """Hook to customize values used on new sale order from split""" + self.ensure_one() + return { + "order_line": False, + "client_order_ref": self.client_order_ref, + "reference": self.reference, + "commitment_date": self.commitment_date, + } + + def _prepare_order_split_line_move_defaults(self): + """Hook to customize values used on new sale order line from split""" + self.ensure_one() + return {"order_id": self.id} + + def _postprocess_split_from(self): + self.ensure_one() + # Remove empty sections + if ( + not self.split_strategy_id.copy_sections + and self.split_strategy_id.remove_empty_sections_after_split + ): + sections_dict = self._get_lines_grouped_by_sections() + section_ids_to_unlink = [ + section_id + for section_id, line_ids in sections_dict.items() + if len(line_ids) == 1 and section_id == line_ids[0] + ] + section_lines = self.env["sale.order.line"].browse(section_ids_to_unlink) + section_lines.unlink() + + def _postprocess_split_to(self, origin_order): + self.ensure_one() diff --git a/sale_order_split_strategy/models/sale_order_split_strategy.py b/sale_order_split_strategy/models/sale_order_split_strategy.py new file mode 100644 index 00000000000..db2f83f3d05 --- /dev/null +++ b/sale_order_split_strategy/models/sale_order_split_strategy.py @@ -0,0 +1,35 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import fields, models +from odoo.fields import Domain +from odoo.tools.safe_eval import safe_eval + + +class SaleOrderSplitStrategy(models.Model): + _name = "sale.order.split.strategy" + _description = "Order split strategy" + _order = "sequence,name" + + name = fields.Char(required=True) + sequence = fields.Integer(default=10) + line_filter_id = fields.Many2one( + "ir.filters", + domain="[('model_id', '=', 'sale.order.line')]", + required=True, + ) + copy_sections = fields.Boolean() + remove_empty_sections_after_split = fields.Boolean() + copy_notes = fields.Boolean() + + def _select_lines_to_split(self, orders): + self.ensure_one() + domain = self._get_lines_to_split_domain(orders) + return ( + self.env["sale.order.line"] + .search(domain) + .filtered(lambda li: not li._is_delivery()) + ) + + def _get_lines_to_split_domain(self, orders): + domain = safe_eval(self.line_filter_id.domain) + return Domain.AND([domain, [("order_id", "in", orders.ids)]]) diff --git a/sale_order_split_strategy/pyproject.toml b/sale_order_split_strategy/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/sale_order_split_strategy/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/sale_order_split_strategy/readme/CONSTRIBUTORS.rst b/sale_order_split_strategy/readme/CONSTRIBUTORS.rst new file mode 100644 index 00000000000..e31e2f0c4fc --- /dev/null +++ b/sale_order_split_strategy/readme/CONSTRIBUTORS.rst @@ -0,0 +1 @@ +* Akim Juillerat diff --git a/sale_order_split_strategy/readme/DESCRIPTION.md b/sale_order_split_strategy/readme/DESCRIPTION.md new file mode 100644 index 00000000000..0fb31faa401 --- /dev/null +++ b/sale_order_split_strategy/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +This module provides the possibility to define split strategies to be +applied on sales order for splitting. + +This is useful (but not only) when a sales order from external sources (webshop, edi) includes +products that need to be shipped by different standards (temperature/environment) +and so with different carriers. diff --git a/sale_order_split_strategy/readme/USAGE.md b/sale_order_split_strategy/readme/USAGE.md new file mode 100644 index 00000000000..af7d9c7f186 --- /dev/null +++ b/sale_order_split_strategy/readme/USAGE.md @@ -0,0 +1,8 @@ +To use this module you first need to create splitting strategies to apply on sales order. + + Sales -> Configuration -> Split Startegies + +The splitting is based on an ir.filter applied on sale.order.line. + +Then on sales order, the Split Strategy field allows to select the corresponding strategy. +After which a button `Split` is available on the form to split an order. diff --git a/sale_order_split_strategy/security/ir.model.access.csv b/sale_order_split_strategy/security/ir.model.access.csv new file mode 100644 index 00000000000..ba94d65ba50 --- /dev/null +++ b/sale_order_split_strategy/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sale_order_split_strategy_user,access_sale_order_split_strategy_user,model_sale_order_split_strategy,sales_team.group_sale_salesman,1,0,0,0 +access_sale_order_split_strategy_manager,access_sale_order_split_strategy_manager,model_sale_order_split_strategy,sales_team.group_sale_manager,1,1,1,1 diff --git a/sale_order_split_strategy/static/description/icon.png b/sale_order_split_strategy/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/sale_order_split_strategy/static/description/icon.png differ diff --git a/sale_order_split_strategy/static/description/index.html b/sale_order_split_strategy/static/description/index.html new file mode 100644 index 00000000000..07bcc7280ae --- /dev/null +++ b/sale_order_split_strategy/static/description/index.html @@ -0,0 +1,447 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Sale Order Split Strategy

+ +

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

+

This module provides the possibility to define split strategies to be +applied on sales order for splitting.

+

This is useful (but not only) when a sales order from external sources +(webshop, edi) includes products that need to be shipped by different +standards (temperature/environment) and so with different carriers.

+
+

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

+ +
+

Usage

+

To use this module you first need to create splitting strategies to +apply on sales order.

+
+Sales -> Configuration -> Split Startegies
+
+

The splitting is based on an ir.filter applied on sale.order.line.

+

Then on sales order, the Split Strategy field allows to select the +corresponding strategy. After which a button Split is available on +the form to split an order.

+
+
+

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

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:

+

grindtildeath

+

This module is part of the OCA/sale-workflow project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/sale_order_split_strategy/tests/__init__.py b/sale_order_split_strategy/tests/__init__.py new file mode 100644 index 00000000000..606e13ffa46 --- /dev/null +++ b/sale_order_split_strategy/tests/__init__.py @@ -0,0 +1 @@ +from . import test_split_strategy diff --git a/sale_order_split_strategy/tests/test_split_strategy.py b/sale_order_split_strategy/tests/test_split_strategy.py new file mode 100644 index 00000000000..b788c0282f6 --- /dev/null +++ b/sale_order_split_strategy/tests/test_split_strategy.py @@ -0,0 +1,245 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo.exceptions import UserError +from odoo.tests import Form +from odoo.tests.common import RecordCapturer + +from odoo.addons.base.tests.common import BaseCommon + + +class TestSplitStrategy(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.product_consu_1 = cls.env["product.product"].create( + { + "name": "Consu 1", + "type": "consu", + "sale_ok": True, + "list_price": 10, + } + ) + cls.product_consu_2 = cls.env["product.product"].create( + { + "name": "Consu 2", + "type": "consu", + "sale_ok": True, + "list_price": 20, + } + ) + cls.product_service_1 = cls.env["product.product"].create( + { + "name": "Service 1", + "type": "service", + "sale_ok": True, + "list_price": 10, + } + ) + cls.product_service_2 = cls.env["product.product"].create( + { + "name": "Service 2", + "type": "service", + "sale_ok": True, + "list_price": 20, + } + ) + + cls.product_type_not_service_filter = cls.env["ir.filters"].create( + { + "name": "Product type not service", + "domain": "[('product_id.type', '!=', 'service')]", + "model_id": "sale.order.line", + } + ) + cls.product_type_not_service_strategy = cls.env[ + "sale.order.split.strategy" + ].create( + { + "name": "Product type", + "line_filter_id": cls.product_type_not_service_filter.id, + } + ) + + @classmethod + def _create_order(cls): + order_form = Form(cls.env["sale.order"]) + order_form.partner_id = cls.env["res.partner"].search([], limit=1) + for product in [ + cls.product_consu_1, + cls.product_consu_2, + cls.product_service_1, + cls.product_service_2, + ]: + with order_form.order_line.new() as line_form: + line_form.product_id = product + line_form.product_uom_qty = 1 + return order_form.save() + + def _set_sequences_and_sections(self, order): + for seq, line in enumerate(order.order_line, 1): + line.sequence = seq * 10 + # First section includes only consumable + first_section = order.order_line.create( + { + "order_id": order.id, + "display_type": "line_section", + "name": "First line", + "sequence": 5, + } + ) + # Middle section includes both consumable + service + middle_section = order.order_line.create( + { + "order_id": order.id, + "display_type": "line_section", + "name": "Middle lines", + "sequence": 15, + } + ) + # Last section includes only service + last_section = order.order_line.create( + { + "order_id": order.id, + "display_type": "line_section", + "name": "Last line", + "sequence": 35, + } + ) + order_lines = order.order_line.sorted() + self.assertEqual(order_lines[0], first_section) + self.assertEqual(order_lines[2], middle_section) + self.assertEqual(order_lines[5], last_section) + + def test_split_product_type(self): + order = self._create_order() + order.split_strategy_id = self.product_type_not_service_strategy + self.assertEqual(len(order.order_line), 4) + new_order = order.action_split() + self.assertEqual(len(order.order_line), 2) + self.assertNotIn(self.product_consu_1, order.order_line.mapped("product_id")) + self.assertNotIn(self.product_consu_2, order.order_line.mapped("product_id")) + self.assertIn(self.product_service_1, order.order_line.mapped("product_id")) + self.assertIn(self.product_service_2, order.order_line.mapped("product_id")) + self.assertEqual(len(new_order.order_line), 2) + self.assertIn(self.product_consu_1, new_order.order_line.mapped("product_id")) + self.assertIn(self.product_consu_2, new_order.order_line.mapped("product_id")) + self.assertNotIn( + self.product_service_1, new_order.order_line.mapped("product_id") + ) + self.assertNotIn( + self.product_service_2, new_order.order_line.mapped("product_id") + ) + + def test_split_product_type_copy_sections(self): + order = self._create_order() + self._set_sequences_and_sections(order) + order.order_line.sorted() + self.product_type_not_service_strategy.copy_sections = True + order.split_strategy_id = self.product_type_not_service_strategy + new_order = order.action_split() + # As new order includes only consu product, it must include + # first + middle sections + self.assertEqual(len(new_order.order_line), 4) + self.assertIn("First line", new_order.order_line.mapped("name")) + self.assertIn("Middle lines", new_order.order_line.mapped("name")) + self.assertNotIn("Last line", new_order.order_line.mapped("name")) + # As new order includes only service product, it must include + # middle + last sections + self.assertEqual(len(order.order_line), 4) + self.assertNotIn("First line", order.order_line.mapped("name")) + self.assertIn("Middle lines", order.order_line.mapped("name")) + self.assertIn("Last line", order.order_line.mapped("name")) + + def test_split_product_type_not_copy_sections(self): + order = self._create_order() + self._set_sequences_and_sections(order) + self.assertFalse(self.product_type_not_service_strategy.copy_sections) + order.split_strategy_id = self.product_type_not_service_strategy + new_order = order.action_split() + # New order does not have any section + self.assertEqual(len(new_order.order_line), 2) + self.assertNotIn("line_section", new_order.order_line.mapped("display_type")) + # Old order keeps all the sections + self.assertEqual(len(order.order_line), 5) + self.assertIn("First line", order.order_line.mapped("name")) + self.assertIn("Middle lines", order.order_line.mapped("name")) + self.assertIn("Last line", order.order_line.mapped("name")) + + def test_split_product_type_not_copy_sections_remove_empty(self): + order = self._create_order() + self._set_sequences_and_sections(order) + # Add extra empty section at the end + order.order_line.create( + { + "order_id": order.id, + "display_type": "line_section", + "name": "Empty section", + "sequence": 999, + } + ) + self.assertFalse(self.product_type_not_service_strategy.copy_sections) + self.product_type_not_service_strategy.remove_empty_sections_after_split = True + order.split_strategy_id = self.product_type_not_service_strategy + order.action_split() + # Old order has empty sections removed + self.assertEqual(len(order.order_line), 4) + self.assertNotIn("First line", order.order_line.mapped("name")) + self.assertIn("Middle lines", order.order_line.mapped("name")) + self.assertIn("Last line", order.order_line.mapped("name")) + self.assertNotIn("Empty section", order.order_line.mapped("name")) + + def test_error_no_strategy_on_split(self): + order = self._create_order() + with self.assertRaisesRegex(UserError, "without any split strategy defined"): + order.action_split() + + def test_error_no_lines_to_split(self): + order = self._create_order() + order.split_strategy_id = self.product_type_not_service_strategy + order.company_id.sale_order_split_strategy_errors = "do_nothing" + order.order_line.filtered( + lambda li: li.product_id in (self.product_consu_1, self.product_consu_2) + ).unlink() + with RecordCapturer(self.env["mail.message"].sudo(), []) as message_capture: + new_orders = order.action_split() + self.assertFalse(message_capture.records) + self.assertFalse(new_orders) + order.company_id.sale_order_split_strategy_errors = "post_message" + with RecordCapturer(self.env["mail.message"].sudo(), []) as message_capture: + new_orders = order.action_split() + self.assertTrue(message_capture.records) + self.assertFalse(new_orders) + order.company_id.sale_order_split_strategy_errors = "raise_errors" + with ( + RecordCapturer(self.env["mail.message"].sudo(), []) as message_capture, + self.assertRaisesRegex(UserError, "there are no matching lines"), + ): + new_orders = order.action_split() + self.assertFalse(message_capture.records) + + def test_error_only_lines_to_split(self): + order = self._create_order() + order.split_strategy_id = self.product_type_not_service_strategy + order.company_id.sale_order_split_strategy_errors = "do_nothing" + order.order_line.filtered( + lambda li: li.product_id not in (self.product_consu_1, self.product_consu_2) + ).unlink() + with RecordCapturer(self.env["mail.message"].sudo(), []) as message_capture: + new_orders = order.action_split() + self.assertFalse(message_capture.records) + self.assertFalse(new_orders) + order.company_id.sale_order_split_strategy_errors = "post_message" + with RecordCapturer(self.env["mail.message"].sudo(), []) as message_capture: + new_orders = order.action_split() + self.assertTrue(message_capture.records) + self.assertFalse(new_orders) + order.company_id.sale_order_split_strategy_errors = "raise_errors" + with ( + RecordCapturer(self.env["mail.message"].sudo(), []) as message_capture, + self.assertRaisesRegex( + UserError, "there would not be any lines left on this order" + ), + ): + new_orders = order.action_split() + self.assertFalse(message_capture.records) diff --git a/sale_order_split_strategy/views/mail_message_template.xml b/sale_order_split_strategy/views/mail_message_template.xml new file mode 100644 index 00000000000..8196838b1b8 --- /dev/null +++ b/sale_order_split_strategy/views/mail_message_template.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/sale_order_split_strategy/views/res_config.xml b/sale_order_split_strategy/views/res_config.xml new file mode 100644 index 00000000000..038d0e22cfe --- /dev/null +++ b/sale_order_split_strategy/views/res_config.xml @@ -0,0 +1,23 @@ + + + + res.config.settings + + + + + + + + + + diff --git a/sale_order_split_strategy/views/sale_order.xml b/sale_order_split_strategy/views/sale_order.xml new file mode 100644 index 00000000000..8e018dfcd4a --- /dev/null +++ b/sale_order_split_strategy/views/sale_order.xml @@ -0,0 +1,21 @@ + + + + sale.order.form + sale.order + + + + + + + + + diff --git a/sale_order_split_strategy/views/sale_order_split_strategy.xml b/sale_order_split_strategy/views/sale_order_split_strategy.xml new file mode 100644 index 00000000000..b9c7cfeb89e --- /dev/null +++ b/sale_order_split_strategy/views/sale_order_split_strategy.xml @@ -0,0 +1,57 @@ + + + + sale.order.split.strategy.view.form + sale.order.split.strategy + +
+ +
+
+
+

+ +

+
+ + + + + + + + +
+
+
+
+ + + sale.order.split.strategy.view.tree + sale.order.split.strategy + + + + + + + + + + Split Strategies + split-strategies + sale.order.split.strategy + list,form + + +