diff --git a/base_substate/README.rst b/base_substate/README.rst new file mode 100644 index 0000000000..61612bf112 --- /dev/null +++ b/base_substate/README.rst @@ -0,0 +1,101 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============== +Base Sub State +============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:0bc86fb6e27937efbc054d578f4900621ecb441b6c7a1f121bc0d79925b7a1f1 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |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%2Fserver--ux-lightgray.png?logo=github + :target: https://github.com/OCA/server-ux/tree/19.0/base_substate + :alt: OCA/server-ux +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-ux-19-0/server-ux-19-0-base_substate + :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/server-ux&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provide abstract models to manage customizable substates to +be applied on different models (sale order, purchase, ...). + +example: +-------- + +- for the quotation state of a sale order we can define 3 substates "In + negotiation", "Won" and "Lost". +- We can also send mail when the substate is reached. + +It is not useful by itself. You can see an example of implementation in +the 'purchase_substate' module. (purchase-workflow repository). + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. You must install an application module depending this one (for + example purchase_substate) + +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 +------- + +* Akretion + +Contributors +------------ + +- Mourad EL HADJ MIMOUNE +- Kitti U. +- Alexei Rivera (migration to 15.0) +- Saran Lim. saranl@ecosoft.co.th +- Nishi Patel np@oerp.ca + +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/server-ux `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_substate/__init__.py b/base_substate/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/base_substate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/base_substate/__manifest__.py b/base_substate/__manifest__.py new file mode 100644 index 0000000000..66036e1a37 --- /dev/null +++ b/base_substate/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2020 Akretion () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# Copyright 2025 OERP Canada + +{ + "name": "Base Sub State", + "version": "19.0.1.0.0", + "category": "Tools", + "author": "Akretion, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/server-ux", + "license": "AGPL-3", + "depends": ["base", "mail"], + "data": [ + "security/base_substate_security.xml", + "security/ir.model.access.csv", + "views/base_substate_type_views.xml", + "views/base_substate_value_views.xml", + "views/base_substate_views.xml", + ], + "installable": True, +} diff --git a/base_substate/i18n/base_substate.pot b/base_substate/i18n/base_substate.pot new file mode 100644 index 0000000000..72cabd3b82 --- /dev/null +++ b/base_substate/i18n/base_substate.pot @@ -0,0 +1,205 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_substate +# +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: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__active +msgid "Active" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__model +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__model +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__model +msgid "Apply on" +msgstr "" + +#. module: base_substate +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_view_form +msgid "Archived" +msgstr "" + +#. module: base_substate +#: model:ir.actions.act_window,name:base_substate.act_open_base_substate_view +#: model:ir.model,name:base_substate.model_base_substate +#: model:ir.ui.menu,name:base_substate.menu_base_substate +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_view_form +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_view_search +msgid "Base Substate" +msgstr "" + +#. module: base_substate +#: model:ir.model,name:base_substate.model_base_substate_type +msgid "Base Substate Type" +msgstr "" + +#. module: base_substate +#: model:ir.model,name:base_substate.model_base_substate_mixin +msgid "BaseSubstate Mixin" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__create_uid +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__create_uid +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__create_uid +msgid "Created by" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__create_date +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__create_date +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__create_date +msgid "Created on" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__description +msgid "Description" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__display_name +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__display_name +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__display_name +msgid "Display Name" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__mail_template_id +msgid "Email Template" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate__sequence +msgid "Gives the sequence order when applying the default substate" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__id +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__id +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__id +msgid "ID" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate__mail_template_id +msgid "" +"If set, an email will be sent to the partner when the object reaches this " +"substate." +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__write_uid +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__write_uid +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__write_date +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__write_date +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__write_date +msgid "Last Updated on" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate__model +#: model:ir.model.fields,help:base_substate.field_target_state_value__model +msgid "Model for technical use" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__name +msgid "Name" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__sequence +msgid "Sequence" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate_mixin__substate_id +msgid "Sub State" +msgstr "" + +#. module: base_substate +#: model:ir.ui.menu,name:base_substate.menu_substate_config +msgid "Sub State Configuration" +msgstr "" + +#. module: base_substate +#: model:ir.actions.act_window,name:base_substate.act_open_base_substate_type_view +#: model:ir.ui.menu,name:base_substate.menu_base_substate_type +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_type_view_form +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_type_view_search +msgid "Sub State Type" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__name +msgid "Substate Name" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__base_substate_type_id +msgid "Substate Type" +msgstr "" + +#. module: base_substate +#: model:res.groups,name:base_substate.group_substate_manager +msgid "Substate manager" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__target_state_field +msgid "Target State Field" +msgstr "" + +#. module: base_substate +#: model:ir.actions.act_window,name:base_substate.act_open_target_state_value_view +#: model:ir.model,name:base_substate.model_target_state_value +#: model:ir.model.fields,field_description:base_substate.field_base_substate__target_state_value_id +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__target_state_value +#: model:ir.ui.menu,name:base_substate.menu_target_state_value +#: model_terms:ir.ui.view,arch_db:base_substate.target_state_value_view_form +#: model_terms:ir.ui.view,arch_db:base_substate.target_state_value_view_search +msgid "Target State Value" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__name +msgid "Target state Name" +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_target_state_value__name +msgid "" +"Target state translateble name.\n" +"Ex: for sale order \"Quotation\", \"Sale order\", \"Locked\"..." +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate_type__target_state_field +msgid "" +"Technical target state field name. Ex for sale order \"state\" for other " +"\"status\" ... " +msgstr "" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_target_state_value__target_state_value +msgid "" +"Technical target state value.\n" +"Ex: for sale order \"draft\", \"sale\", \"done\", ..." +msgstr "" diff --git a/base_substate/i18n/es.po b/base_substate/i18n/es.po new file mode 100644 index 0000000000..8ed151561a --- /dev/null +++ b/base_substate/i18n/es.po @@ -0,0 +1,237 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_substate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-11-11 15:17+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\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 4.17\n" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__active +msgid "Active" +msgstr "Activo" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__model +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__model +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__model +msgid "Apply on" +msgstr "Aplicar sobre" + +#. module: base_substate +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_view_form +msgid "Archived" +msgstr "Archivado" + +#. module: base_substate +#: model:ir.actions.act_window,name:base_substate.act_open_base_substate_view +#: model:ir.model,name:base_substate.model_base_substate +#: model:ir.ui.menu,name:base_substate.menu_base_substate +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_view_form +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_view_search +msgid "Base Substate" +msgstr "Subestado Base" + +#. module: base_substate +#: model:ir.model,name:base_substate.model_base_substate_type +msgid "Base Substate Type" +msgstr "Tipo de Subestado Base" + +#. module: base_substate +#: model:ir.model,name:base_substate.model_base_substate_mixin +msgid "BaseSubstate Mixin" +msgstr "Mezcla de subestado base" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__create_uid +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__create_uid +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__create_date +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__create_date +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__description +msgid "Description" +msgstr "Descripción" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__display_name +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__display_name +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__mail_template_id +msgid "Email Template" +msgstr "Plantilla de Correo Electrónico" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate__sequence +msgid "Gives the sequence order when applying the default substate" +msgstr "Indica el orden de secuencia al aplicar el subestado por defecto" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__id +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__id +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate__mail_template_id +msgid "" +"If set, an email will be sent to the partner when the object reaches this " +"substate." +msgstr "" +"Si se establece, se enviará un correo electrónico al socio cuando el objeto " +"alcance este subestado." + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__write_uid +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__write_uid +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__write_uid +msgid "Last Updated by" +msgstr "Actualizado por Última vez por" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__write_date +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__write_date +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate__model +#: model:ir.model.fields,help:base_substate.field_target_state_value__model +msgid "Model for technical use" +msgstr "Modelo para uso técnico" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__name +msgid "Name" +msgstr "Nombre" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate_mixin__substate_id +msgid "Sub State" +msgstr "Sub Estado" + +#. module: base_substate +#: model:ir.ui.menu,name:base_substate.menu_substate_config +msgid "Sub State Configuration" +msgstr "Configuración del Sub Estado" + +#. module: base_substate +#: model:ir.actions.act_window,name:base_substate.act_open_base_substate_type_view +#: model:ir.ui.menu,name:base_substate.menu_base_substate_type +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_type_view_form +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_type_view_search +msgid "Sub State Type" +msgstr "Tipo de Sub Estado" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__name +msgid "Substate Name" +msgstr "Nombre del Subestado" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__base_substate_type_id +msgid "Substate Type" +msgstr "Tipo de Subestado" + +#. module: base_substate +#: model:res.groups,name:base_substate.group_substate_manager +msgid "Substate manager" +msgstr "Gerente del Subestado" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__target_state_field +msgid "Target State Field" +msgstr "Objetivo Campo del Estado" + +#. module: base_substate +#: model:ir.actions.act_window,name:base_substate.act_open_target_state_value_view +#: model:ir.model,name:base_substate.model_target_state_value +#: model:ir.model.fields,field_description:base_substate.field_base_substate__target_state_value_id +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__target_state_value +#: model:ir.ui.menu,name:base_substate.menu_target_state_value +#: model_terms:ir.ui.view,arch_db:base_substate.target_state_value_view_form +#: model_terms:ir.ui.view,arch_db:base_substate.target_state_value_view_search +msgid "Target State Value" +msgstr "Valor Objetivo del Estado" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__name +msgid "Target state Name" +msgstr "Nombre del Estado objetivo" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_target_state_value__name +msgid "" +"Target state translateble name.\n" +"Ex: for sale order \"Quotation\", \"Sale order\", \"Locked\"..." +msgstr "" +"Nombre traducible del estado de destino.\n" +"Ej: para orden de venta \"Quotation\", \"Sale order\", \"Locked\"..." + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate_type__target_state_field +msgid "" +"Technical target state field name. Ex for sale order \"state\" for other " +"\"status\" ... " +msgstr "" +"Nombre de campo de estado de destino técnico. Ej para pedido de venta " +"\"state \" para otro \"status \" ... " + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_target_state_value__target_state_value +msgid "" +"Technical target state value.\n" +"Ex: for sale order \"draft\", \"sale\", \"done\", ..." +msgstr "" +"Valor de estado del objetivo técnico.\n" +"Ej: para pedido de venta \"draft \", \"sale \", \"done \", ..." + +#. module: base_substate +#. odoo-python +#: code:addons/base_substate/models/base_substate_mixin.py:0 +#, python-format +msgid "" +"The substate %(name)s is not defined for the state %(state)s but for " +"%(target_state)s " +msgstr "" +"El subestado %(name)s no está definido para el estado %(state)s sino para " +"%(target_state)s " + +#. module: base_substate +#. odoo-python +#: code:addons/base_substate/models/base_substate_mixin.py:0 +#, python-format +msgid "This substate is not define for this object but for %s" +msgstr "Este subestado no está definido para este objeto sino para %s" + +#~ msgid "Last Modified on" +#~ msgstr "Última Modificación el" diff --git a/base_substate/i18n/it.po b/base_substate/i18n/it.po new file mode 100644 index 0000000000..92b05253b7 --- /dev/null +++ b/base_substate/i18n/it.po @@ -0,0 +1,238 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_substate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-02-21 18:06+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.6.2\n" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__active +msgid "Active" +msgstr "Attivo" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__model +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__model +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__model +msgid "Apply on" +msgstr "Applica a" + +#. module: base_substate +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_view_form +msgid "Archived" +msgstr "In archivio" + +#. module: base_substate +#: model:ir.actions.act_window,name:base_substate.act_open_base_substate_view +#: model:ir.model,name:base_substate.model_base_substate +#: model:ir.ui.menu,name:base_substate.menu_base_substate +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_view_form +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_view_search +msgid "Base Substate" +msgstr "Substato base" + +#. module: base_substate +#: model:ir.model,name:base_substate.model_base_substate_type +msgid "Base Substate Type" +msgstr "Tipo substato base" + +#. module: base_substate +#: model:ir.model,name:base_substate.model_base_substate_mixin +msgid "BaseSubstate Mixin" +msgstr "Mixin substato base" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__create_uid +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__create_uid +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__create_date +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__create_date +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__description +msgid "Description" +msgstr "Descrizione" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__display_name +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__display_name +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__mail_template_id +msgid "Email Template" +msgstr "Modello e-mail" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate__sequence +msgid "Gives the sequence order when applying the default substate" +msgstr "Fornisce la sequenza quando si applica il substato predefinito" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__id +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__id +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__id +msgid "ID" +msgstr "ID" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate__mail_template_id +msgid "" +"If set, an email will be sent to the partner when the object reaches this " +"substate." +msgstr "" +"Se impostato, verrà mandata una e-mail al partner quando l'oggetto raggiunge " +"questo substato." + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__write_uid +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__write_uid +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__write_date +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__write_date +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate__model +#: model:ir.model.fields,help:base_substate.field_target_state_value__model +msgid "Model for technical use" +msgstr "Modello per uso tecnico" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__name +msgid "Name" +msgstr "Nome" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate_mixin__substate_id +msgid "Sub State" +msgstr "Substato" + +#. module: base_substate +#: model:ir.ui.menu,name:base_substate.menu_substate_config +msgid "Sub State Configuration" +msgstr "Configurazione substato" + +#. module: base_substate +#: model:ir.actions.act_window,name:base_substate.act_open_base_substate_type_view +#: model:ir.ui.menu,name:base_substate.menu_base_substate_type +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_type_view_form +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_type_view_search +msgid "Sub State Type" +msgstr "Tipo substato" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__name +msgid "Substate Name" +msgstr "Nome substato" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__base_substate_type_id +msgid "Substate Type" +msgstr "Tipo substato" + +#. module: base_substate +#: model:res.groups,name:base_substate.group_substate_manager +msgid "Substate manager" +msgstr "Responsabile substato" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__target_state_field +msgid "Target State Field" +msgstr "Campo stato obiettivo" + +#. module: base_substate +#: model:ir.actions.act_window,name:base_substate.act_open_target_state_value_view +#: model:ir.model,name:base_substate.model_target_state_value +#: model:ir.model.fields,field_description:base_substate.field_base_substate__target_state_value_id +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__target_state_value +#: model:ir.ui.menu,name:base_substate.menu_target_state_value +#: model_terms:ir.ui.view,arch_db:base_substate.target_state_value_view_form +#: model_terms:ir.ui.view,arch_db:base_substate.target_state_value_view_search +msgid "Target State Value" +msgstr "Valore stato obiettivo" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__name +msgid "Target state Name" +msgstr "Nome stato obiettivo" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_target_state_value__name +msgid "" +"Target state translateble name.\n" +"Ex: for sale order \"Quotation\", \"Sale order\", \"Locked\"..." +msgstr "" +"Nome traducibile dello stato obiettivo.\n" +"Es: per ordine di vendita \"Preventivo\", \"Ordine di vendita\", " +"\"Bloccato\"..." + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate_type__target_state_field +msgid "" +"Technical target state field name. Ex for sale order \"state\" for other " +"\"status\" ... " +msgstr "" +"Nome tecnico del campo obiettivo dello stato. Es: per ordine di vendita " +"\"state\", per altri \"status\" ... " + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_target_state_value__target_state_value +msgid "" +"Technical target state value.\n" +"Ex: for sale order \"draft\", \"sale\", \"done\", ..." +msgstr "" +"Valore tecnico dello stato obiettivo.\n" +"Es: per ordine di vendita \"draft\", \"sale\", \"done\", ..." + +#. module: base_substate +#. odoo-python +#: code:addons/base_substate/models/base_substate_mixin.py:0 +#, python-format +msgid "" +"The substate %(name)s is not defined for the state %(state)s but for " +"%(target_state)s " +msgstr "" +"Il substato %(name)s non è definito per lo stato %(state)s ma per " +"%(target_state)s " + +#. module: base_substate +#. odoo-python +#: code:addons/base_substate/models/base_substate_mixin.py:0 +#, python-format +msgid "This substate is not define for this object but for %s" +msgstr "Questo sottostato non è definito per questo oggetto ma per %s" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" diff --git a/base_substate/i18n/nl.po b/base_substate/i18n/nl.po new file mode 100644 index 0000000000..04338a2d03 --- /dev/null +++ b/base_substate/i18n/nl.po @@ -0,0 +1,216 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_substate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-06-19 20:25+0000\n" +"Last-Translator: Bosd \n" +"Language-Team: none\n" +"Language: nl\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.10.4\n" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__active +msgid "Active" +msgstr "Actief" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__model +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__model +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__model +msgid "Apply on" +msgstr "Toepassen op" + +#. module: base_substate +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_view_form +msgid "Archived" +msgstr "Gearchiveerd" + +#. module: base_substate +#: model:ir.actions.act_window,name:base_substate.act_open_base_substate_view +#: model:ir.model,name:base_substate.model_base_substate +#: model:ir.ui.menu,name:base_substate.menu_base_substate +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_view_form +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_view_search +msgid "Base Substate" +msgstr "Basis Substatus" + +#. module: base_substate +#: model:ir.model,name:base_substate.model_base_substate_type +msgid "Base Substate Type" +msgstr "Basis Substatus Type" + +#. module: base_substate +#: model:ir.model,name:base_substate.model_base_substate_mixin +msgid "BaseSubstate Mixin" +msgstr "BaseSubstate Mixin" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__create_uid +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__create_uid +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__create_uid +msgid "Created by" +msgstr "Aangemaakt door" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__create_date +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__create_date +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__create_date +msgid "Created on" +msgstr "Aangemaakt op" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__description +msgid "Description" +msgstr "Omschrijving" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__display_name +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__display_name +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__display_name +msgid "Display Name" +msgstr "Weergavenaam" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__mail_template_id +msgid "Email Template" +msgstr "E-mailsjabloon" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate__sequence +msgid "Gives the sequence order when applying the default substate" +msgstr "Geeft de volgorde aan bij het toepassen van de standaard substatus" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__id +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__id +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__id +msgid "ID" +msgstr "ID" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate__mail_template_id +msgid "" +"If set, an email will be sent to the partner when the object reaches this " +"substate." +msgstr "" +"Indien ingesteld, wordt er een e-mail naar de partner verzonden wanneer het " +"object deze substatus bereikt." + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__write_uid +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__write_uid +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__write_uid +msgid "Last Updated by" +msgstr "Laatst bijgewerkt door" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__write_date +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__write_date +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__write_date +msgid "Last Updated on" +msgstr "Laatst bijgewerkt op" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate__model +#: model:ir.model.fields,help:base_substate.field_target_state_value__model +msgid "Model for technical use" +msgstr "Model voor technisch gebruik" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__name +msgid "Name" +msgstr "Naam" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__sequence +msgid "Sequence" +msgstr "Volgorde" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate_mixin__substate_id +msgid "Sub State" +msgstr "Substatus" + +#. module: base_substate +#: model:ir.ui.menu,name:base_substate.menu_substate_config +msgid "Sub State Configuration" +msgstr "Substatus Configuratie" + +#. module: base_substate +#: model:ir.actions.act_window,name:base_substate.act_open_base_substate_type_view +#: model:ir.ui.menu,name:base_substate.menu_base_substate_type +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_type_view_form +#: model_terms:ir.ui.view,arch_db:base_substate.base_substate_type_view_search +msgid "Sub State Type" +msgstr "Substatus Type" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate__name +msgid "Substate Name" +msgstr "Naam Substatus" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__base_substate_type_id +msgid "Substate Type" +msgstr "Substatus Type" + +#. module: base_substate +#: model:res.groups,name:base_substate.group_substate_manager +msgid "Substate manager" +msgstr "Substatus beheerder" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_base_substate_type__target_state_field +msgid "Target State Field" +msgstr "Doelstatusveld" + +#. module: base_substate +#: model:ir.actions.act_window,name:base_substate.act_open_target_state_value_view +#: model:ir.model,name:base_substate.model_target_state_value +#: model:ir.model.fields,field_description:base_substate.field_base_substate__target_state_value_id +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__target_state_value +#: model:ir.ui.menu,name:base_substate.menu_target_state_value +#: model_terms:ir.ui.view,arch_db:base_substate.target_state_value_view_form +#: model_terms:ir.ui.view,arch_db:base_substate.target_state_value_view_search +msgid "Target State Value" +msgstr "Doelstatuswaarde" + +#. module: base_substate +#: model:ir.model.fields,field_description:base_substate.field_target_state_value__name +msgid "Target state Name" +msgstr "Naam doelstatus" + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_target_state_value__name +msgid "" +"Target state translateble name.\n" +"Ex: for sale order \"Quotation\", \"Sale order\", \"Locked\"..." +msgstr "" +"Vertaalbare naam van de doelstatus.\n" +"Bijv: voor verkooporder \"Offerte\", \"Verkooporder\", \"Vergrendeld\"..." + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_base_substate_type__target_state_field +msgid "" +"Technical target state field name. Ex for sale order \"state\" for other " +"\"status\" ... " +msgstr "" +"Technische naam van het doelstatusveld. Bijv. voor verkooporder \"state\" " +"voor andere \"status\"... " + +#. module: base_substate +#: model:ir.model.fields,help:base_substate.field_target_state_value__target_state_value +msgid "" +"Technical target state value.\n" +"Ex: for sale order \"draft\", \"sale\", \"done\", ..." +msgstr "" +"Technische doelstatuswaarde.\n" +"Bijv: voor verkooporder \"draft\", \"sale\", \"done\", ..." diff --git a/base_substate/models/__init__.py b/base_substate/models/__init__.py new file mode 100644 index 0000000000..6ac2cf86a3 --- /dev/null +++ b/base_substate/models/__init__.py @@ -0,0 +1,2 @@ +from . import base_substate +from . import base_substate_mixin diff --git a/base_substate/models/base_substate.py b/base_substate/models/base_substate.py new file mode 100644 index 0000000000..79706d1b6e --- /dev/null +++ b/base_substate/models/base_substate.py @@ -0,0 +1,103 @@ +# Copyright 2020 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class BaseSubstateType(models.Model): + """This model defines technical data which precises + for each target model concerned by substate, + the technical "state" field name. + Data in this model should be created by import as technical data + in the specific module. For example in sale_substate we can define: + base.substate.type: + - name: Sale order Substate + - model: sale.order + - target_state_field: state + """ + + _name = "base.substate.type" + _description = "Base Substate Type" + _order = "name asc, model asc" + + name = fields.Char(required=True, translate=True) + model = fields.Selection(selection=[], string="Apply on", required=True) + target_state_field = fields.Char( + required=True, + help="Technical target state field name." + ' Ex for sale order "state" for other "status" ... ', + ) + + +class TargetStateValue(models.Model): + """This model define technical data that precise the translatable name + of the target model state (ex:Quotation for 'draft' State) + Data in this model should be created by import as technical data + in specific module ex : sale_subsatate + """ + + _name = "target.state.value" + _description = "Target State Value" + _order = "name asc" + + name = fields.Char( + "Target state Name", + required=True, + translate=True, + help="Target state translateble name.\n" + 'Ex: for sale order "Quotation", "Sale order", "Locked"...', + ) + base_substate_type_id = fields.Many2one( + "base.substate.type", + string="Substate Type", + ondelete="restrict", + ) + target_state_value = fields.Char( + required=True, + help="Technical target state value.\n" + 'Ex: for sale order "draft", "sale", "done", ...', + ) + model = fields.Selection( + related="base_substate_type_id.model", + store=True, + readonly=True, + help="Model for technical use", + ) + + +class BaseSubstate(models.Model): + """This model define substates that will be applied on the target model. + for each state we can define one or more substate. + ex: + for the quotation state of a sale order we can define + 3 substates "In negotiation", + "Won" and "Lost". + We can also send mail when the susbstate is reached. + """ + + _name = "base.substate" + _description = "Base Substate" + _order = "active desc, sequence asc" + + name = fields.Char("Substate Name", required=True, translate=True) + description = fields.Text(translate=True) + sequence = fields.Integer( + index=True, + help="Gives the sequence order when applying the default substate", + ) + target_state_value_id = fields.Many2one( + "target.state.value", string="Target State Value", ondelete="restrict" + ) + active = fields.Boolean(default=True) + mail_template_id = fields.Many2one( + "mail.template", + string="Email Template", + help="If set, an email will be sent to the partner " + "when the object reaches this substate.", + ) + model = fields.Selection( + related="target_state_value_id.model", + store=True, + readonly=True, + help="Model for technical use", + ) diff --git a/base_substate/models/base_substate_mixin.py b/base_substate/models/base_substate_mixin.py new file mode 100644 index 0000000000..1b1745df1b --- /dev/null +++ b/base_substate/models/base_substate_mixin.py @@ -0,0 +1,118 @@ +# Copyright 2020 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# Copyright 2025 OERP Canada + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class BaseSubstateMixin(models.AbstractModel): + _name = "base.substate.mixin" + _description = "BaseSubstate Mixin" + _state_field = "state" + + @api.constrains("substate_id", _state_field) + def check_substate_id_value(self): + rec_states = dict(self._fields[self._state_field].selection) + for rec in self: + target_state = rec.substate_id.target_state_value_id.target_state_value + if rec.substate_id and rec.state != target_state: + error_msg = self.env._( + "The substate %(substate)s is not defined for " + "the state %(current_state)s but for " + "%(target_state)s", + substate=rec.substate_id.name, + current_state=rec_states[rec.state], + target_state=rec_states[target_state], + ) + raise ValidationError(error_msg) + + def _get_default_substate_id(self, state_val=False): + """Gives default substate_id""" + search_domain = self._get_default_substate_domain(state_val) + # perform search, return the first found + return ( + self.env["base.substate"] + .search(search_domain, order="sequence", limit=1) + .id + ) + + def _get_default_substate_domain(self, state_val=False): + """Override this method + to change domain values + """ + if not state_val: + state_val = self._get_default_state_value() + substate_type = self._get_substate_type() + state_field = substate_type.target_state_field + if self and not state_val and state_field in self._fields: + state_val = self[state_field] + + domain = [ + ("target_state_value_id.target_state_value", "=", state_val), + ("target_state_value_id.base_substate_type_id", "=", substate_type.id), + ] + return domain + + def _get_default_state_value(self): + """Override this method to change state_value""" + return "draft" + + def _get_substate_type(self): + """Override this method to change substate_type (get by xml id for example)""" + return self.env["base.substate.type"].search( + [("model", "=", self._name)], limit=1 + ) + + substate_id = fields.Many2one( + "base.substate", + string="Sub State", + ondelete="restrict", + default=lambda self: self._get_default_substate_id(), + index=True, + domain=lambda self: [("model", "=", self._name)], + copy=False, + tracking=True, + ) + + @api.constrains("substate_id") + def check_substate_id_consistency(self): + for mixin_obj in self: + if mixin_obj.substate_id and mixin_obj.substate_id.model != self._name: + error_msg = self.env._( + "This substate is not define for this object but for" + "%(substate_model)s", + substate_model=mixin_obj.substate_id.model, + ) + raise ValidationError(error_msg) + + def _update_before_write_create(self, values): + substate_type = self._get_substate_type() + state_field = substate_type.target_state_field + if values.get(state_field) and not values.get("substate_id"): + state_val = values.get(state_field) + values["substate_id"] = self._get_default_substate_id(state_val) + # Send mail if substate has mail template + if values.get("substate_id"): + substate = self.env["base.substate"].browse(values["substate_id"]) + if hasattr(self, "message_post_with_source") and substate.mail_template_id: + self.message_post_with_source( + substate.mail_template_id, + message_type="comment", + subtype_id=self.env["ir.model.data"]._xmlid_to_res_id( + "mail.mt_note" + ), + ) + return values + + def write(self, values): + values = self._update_before_write_create(values) + res = super().write(values) + return res + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + vals = self._update_before_write_create(vals) + res = super().create(vals_list) + return res diff --git a/base_substate/pyproject.toml b/base_substate/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/base_substate/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/base_substate/readme/CONTRIBUTORS.md b/base_substate/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..653093b81f --- /dev/null +++ b/base_substate/readme/CONTRIBUTORS.md @@ -0,0 +1,5 @@ +- Mourad EL HADJ MIMOUNE \<\> +- Kitti U. \<\> +- Alexei Rivera \<\> (migration to 15.0) +- Saran Lim. +- Nishi Patel diff --git a/base_substate/readme/DESCRIPTION.md b/base_substate/readme/DESCRIPTION.md new file mode 100644 index 0000000000..d00de1535b --- /dev/null +++ b/base_substate/readme/DESCRIPTION.md @@ -0,0 +1,11 @@ +This module provide abstract models to manage customizable substates to +be applied on different models (sale order, purchase, ...). + +## example: + +- for the quotation state of a sale order we can define 3 substates "In + negotiation", "Won" and "Lost". +- We can also send mail when the substate is reached. + +It is not useful by itself. You can see an example of implementation in +the 'purchase_substate' module. (purchase-workflow repository). diff --git a/base_substate/readme/USAGE.md b/base_substate/readme/USAGE.md new file mode 100644 index 0000000000..bc9abb0944 --- /dev/null +++ b/base_substate/readme/USAGE.md @@ -0,0 +1,2 @@ +1. You must install an application module depending this one (for + example purchase_substate) diff --git a/base_substate/security/base_substate_security.xml b/base_substate/security/base_substate_security.xml new file mode 100644 index 0000000000..4f98ee90ea --- /dev/null +++ b/base_substate/security/base_substate_security.xml @@ -0,0 +1,7 @@ + + + + Substate manager + + + diff --git a/base_substate/security/ir.model.access.csv b/base_substate/security/ir.model.access.csv new file mode 100644 index 0000000000..4cdb1a9ebd --- /dev/null +++ b/base_substate/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_base_substate_type,base.substate.type user,model_base_substate_type,base.group_user,1,0,0,0 +access_base_substate_type_manager,base.substate.type manager,model_base_substate_type,base_substate.group_substate_manager,1,1,1,1 +access_target_state_value,base.substate.value user,model_target_state_value,base.group_user,1,0,0,0 +access_target_state_value_manager,base.substate.value manager,model_target_state_value,base_substate.group_substate_manager,1,1,1,1 +access_base_substate,base.substate user,model_base_substate,base.group_user,1,0,0,0 +access_base_substate_manager,base.substate manager,model_base_substate,base_substate.group_substate_manager,1,1,1,1 diff --git a/base_substate/static/description/icon.png b/base_substate/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/base_substate/static/description/icon.png differ diff --git a/base_substate/static/description/index.html b/base_substate/static/description/index.html new file mode 100644 index 0000000000..f217f1a20e --- /dev/null +++ b/base_substate/static/description/index.html @@ -0,0 +1,447 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Base Sub State

+ +

Beta License: AGPL-3 OCA/server-ux Translate me on Weblate Try me on Runboat

+

This module provide abstract models to manage customizable substates to +be applied on different models (sale order, purchase, …).

+
+

example:

+
    +
  • for the quotation state of a sale order we can define 3 substates “In +negotiation”, “Won” and “Lost”.
  • +
  • We can also send mail when the substate is reached.
  • +
+

It is not useful by itself. You can see an example of implementation in +the ‘purchase_substate’ module. (purchase-workflow repository).

+

Table of contents

+ +
+

Usage

+
    +
  1. You must install an application module depending this one (for +example purchase_substate)
  2. +
+
+
+

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

+
    +
  • Akretion
  • +
+
+
+

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/server-ux project on GitHub.

+

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

+
+
+
+ + diff --git a/base_substate/tests/__init__.py b/base_substate/tests/__init__.py new file mode 100644 index 0000000000..6486f3f539 --- /dev/null +++ b/base_substate/tests/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import test_base_substate diff --git a/base_substate/tests/common.py b/base_substate/tests/common.py new file mode 100644 index 0000000000..cc2ccf12a1 --- /dev/null +++ b/base_substate/tests/common.py @@ -0,0 +1,59 @@ +# Copyright 2025 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +# Copyright 2025 OERP Canada + +from odoo.orm.model_classes import add_to_registry + +from odoo.addons.base.tests.common import BaseCommon + + +class CommonBaseSubstate(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + from .sale_test import ( + BaseSubstateType, + LineTest, + SaleTest, + ) + + for model_class in [ + SaleTest, + LineTest, + BaseSubstateType, + ]: + add_to_registry(cls.registry, model_class) + + test_models = [ + "base.substate.test.sale", + "base.substate.test.sale.line", + ] + cls.registry._setup_models__(cls.env.cr, test_models) + cls.registry.init_models(cls.env.cr, test_models, {"models_to_check": True}) + for model_name in test_models: + cls.addClassCleanup(cls.registry.__delitem__, model_name) + + cls.sale_test_model = cls.env[SaleTest._name] + cls.sale_line_test_model = cls.env[LineTest._name] + + models = cls.env["ir.model"].search( + [ + ( + "model", + "in", + ["base.substate.test.sale", "base.substate.test.sale.line"], + ) + ] + ) + for model in models: + # Access record: + cls.env["ir.model.access"].create( + { + "name": f"access {model.name}", + "model_id": model.id, + "perm_read": 1, + "perm_write": 1, + "perm_create": 1, + "perm_unlink": 1, + } + ) diff --git a/base_substate/tests/sale_test.py b/base_substate/tests/sale_test.py new file mode 100644 index 0000000000..e2385e176f --- /dev/null +++ b/base_substate/tests/sale_test.py @@ -0,0 +1,62 @@ +# Copyright 2020 Akretion Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models + + +class BaseSubstateType(models.Model): + _inherit = "base.substate.type" + + model = fields.Selection( + selection_add=[("base.substate.test.sale", "Sale Test")], + ondelete={"base.substate.test.sale": "cascade"}, + ) + + +class SaleTest(models.Model): + _name = "base.substate.test.sale" + _inherit = ["base.substate.mixin", "mail.thread"] + _description = "Base substate Test Model" + + name = fields.Char(required=True) + user_id = fields.Many2one("res.users", string="Responsible") + state = fields.Selection( + [("draft", "New"), ("cancel", "Cancelled"), ("sale", "Sale"), ("done", "Done")], + string="Status", + readonly=True, + default="draft", + ) + active = fields.Boolean(default=True) + partner_id = fields.Many2one("res.partner", string="Partner") + line_ids = fields.One2many( + comodel_name="base.substate.test.sale.line", + inverse_name="sale_id", + context={"active_test": False}, + ) + amount_total = fields.Float(compute="_compute_amount_total", store=True) + + @api.depends("line_ids") + def _compute_amount_total(self): + for record in self: + for line in record.line_ids: + record.amount_total += line.amount * line.qty + + def button_confirm(self): + self.write({"state": "sale"}) + return True + + def button_cancel(self): + self.write({"state": "cancel"}) + + +class LineTest(models.Model): + _name = "base.substate.test.sale.line" + _description = "Base substate Test Model Line" + + name = fields.Char() + sale_id = fields.Many2one( + comodel_name="base.substate.test.sale", + ondelete="cascade", + context={"active_test": False}, + ) + qty = fields.Float() + amount = fields.Float() diff --git a/base_substate/tests/test_base_substate.py b/base_substate/tests/test_base_substate.py new file mode 100644 index 0000000000..ea616ab7f3 --- /dev/null +++ b/base_substate/tests/test_base_substate.py @@ -0,0 +1,175 @@ +# Copyright 2020 Akretion Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import Command +from odoo.tests import tagged + +from .common import CommonBaseSubstate + + +@tagged("post_install", "-at_install", "mi_tag") +class TestBaseSubstate(CommonBaseSubstate): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.substate_type = cls.env["base.substate.type"] + cls.base_substate = cls.env["base.substate"] + + cls.mail_template = cls.env["mail.template"].create( + { + "name": "Waiting for legal documents", + "model_id": cls.env["ir.model"]._get(cls.sale_test_model._name).id, + "subject": "Test Email Substate", + } + ) + cls.sale_test_substate_type = cls.substate_type.create( + { + "name": "Sale", + "model": "base.substate.test.sale", + "target_state_field": "state", + } + ) + + cls.substate_val_quotation = cls.env["target.state.value"].create( + { + "name": "Quotation", + "base_substate_type_id": cls.sale_test_substate_type.id, + "target_state_value": "draft", + } + ) + + cls.substate_val_sale = cls.env["target.state.value"].create( + { + "name": "Sale order", + "base_substate_type_id": cls.sale_test_substate_type.id, + "target_state_value": "sale", + } + ) + cls.substate_under_negotiation = cls.base_substate.create( + { + "name": "Under negotiation", + "sequence": 1, + "target_state_value_id": cls.substate_val_quotation.id, + } + ) + + cls.substate_won = cls.base_substate.create( + { + "name": "Won", + "sequence": 1, + "target_state_value_id": cls.substate_val_quotation.id, + } + ) + + cls.substate_wait_docs = cls.base_substate.create( + { + "name": "Waiting for legal documents", + "sequence": 2, + "target_state_value_id": cls.substate_val_sale.id, + "mail_template_id": cls.mail_template.id, + } + ) + + cls.substate_valid_docs = cls.base_substate.create( + { + "name": "To validate legal documents", + "sequence": 3, + "target_state_value_id": cls.substate_val_sale.id, + } + ) + + cls.substate_in_delivering = cls.base_substate.create( + { + "name": "In delivering", + "sequence": 4, + "target_state_value_id": cls.substate_val_sale.id, + } + ) + cls.test_partner = cls.env["res.partner"].create( + { + "name": "Test Partner", + "email": "test@test.com", + } + ) + + def test_sale_order_substate(self): + so_test1 = self.sale_test_model.create( + { + "name": "Test base substate to basic sale", + "partner_id": self.test_partner.id, + "line_ids": [ + Command.create({"name": "line test", "amount": 120.0, "qty": 1.5}) + ], + } + ) + self.assertTrue(so_test1.state == "draft") + self.assertTrue(so_test1.substate_id == self.substate_under_negotiation) + self.assertNotIn( + self.mail_template.subject, so_test1.message_ids.mapped("subject") + ) + # Test that validation of sale order change substate_id + so_test1.button_confirm() + self.assertTrue(so_test1.state == "sale") + self.assertTrue(so_test1.substate_id == self.substate_wait_docs) + # Check some message_ids are created and sent email + self.assertIn( + self.mail_template.subject, so_test1.message_ids.mapped("subject") + ) + # Test that substate_id is set to false if + # there is not substate corresponding to state + so_test1.button_cancel() + self.assertTrue(so_test1.state == "cancel") + self.assertTrue(not so_test1.substate_id) + + def test_constrain_substate_mismatch(self): + """Test function when substate does not belong to current state.""" + so_test2 = self.sale_test_model.create( + { + "name": "Test base substate to basic sale", + "partner_id": self.test_partner.id, + "line_ids": [ + Command.create({"name": "line test 2", "amount": 120.0, "qty": 1.5}) + ], + } + ) + self.assertTrue(so_test2.state == "draft") + self.assertTrue(so_test2.substate_id == self.substate_under_negotiation) + + so_test2.write({"state": "sale"}) + so_test2.write({"substate_id": self.substate_wait_docs.id}) + self.assertTrue(so_test2.substate_id == self.substate_wait_docs) + + def test_write_triggers_default_substate(self): + """state field automatically sets the default substate.""" + so = self.sale_test_model.create( + { + "name": "Test Auto Substate on Write", + "partner_id": self.test_partner.id, + "line_ids": [ + Command.create({"name": "line test 3", "amount": 230.0, "qty": 3}) + ], + } + ) + self.assertTrue(so.state == "draft") + self.assertTrue(so.substate_id == self.substate_under_negotiation) + + so.write({"state": "sale"}) + + self.assertTrue(so.state == "sale") + self.assertTrue(so.substate_id == self.substate_wait_docs) + + def test_create_with_explicit_substate(self): + """Create with explicit substate.""" + so = self.sale_test_model.create( + { + "name": "Test Explicit Substate", + "partner_id": self.test_partner.id, + "line_ids": [ + Command.create({"name": "line test 4", "amount": 230.0, "qty": 2}) + ], + "substate_id": self.substate_won.id, + } + ) + self.assertTrue(so.state == "draft") + self.assertTrue(so.substate_id == self.substate_won) + self.assertNotIn(self.mail_template.subject, so.message_ids.mapped("subject")) diff --git a/base_substate/views/base_substate_type_views.xml b/base_substate/views/base_substate_type_views.xml new file mode 100644 index 0000000000..09ae34312a --- /dev/null +++ b/base_substate/views/base_substate_type_views.xml @@ -0,0 +1,85 @@ + + + + + base.substate.type + + + + + + + + + + base.substate.type + +
+ +
+
+ + + + + + +
+
+
+
+ + base.substate.type + + + + + + + + + + Sub State Type + ir.actions.act_window + base.substate.type + list,form + + [] + {} + + + + + form + + + + + + list + + + + +
diff --git a/base_substate/views/base_substate_value_views.xml b/base_substate/views/base_substate_value_views.xml new file mode 100644 index 0000000000..3864771831 --- /dev/null +++ b/base_substate/views/base_substate_value_views.xml @@ -0,0 +1,83 @@ + + + + + target.state.value + + + + + + + + + + target.state.value + +
+ +
+
+ + + + + + +
+
+
+
+ + target.state.value + + + + + + + + + + Target State Value + ir.actions.act_window + target.state.value + list,form + + [] + {} + + + + + form + + + + + + list + + + +
diff --git a/base_substate/views/base_substate_views.xml b/base_substate/views/base_substate_views.xml new file mode 100644 index 0000000000..1837321947 --- /dev/null +++ b/base_substate/views/base_substate_views.xml @@ -0,0 +1,88 @@ + + + + + base.substate + + + + + + + + + + + + base.substate + +
+ +
+ +
+
+
+ + + + + + + + + + +
+
+
+
+ + base.substate + + + + + + + + Base Substate + ir.actions.act_window + base.substate + list,form + + [] + {} + + + + + form + + + + + + list + + + +