diff --git a/mrp_lot_number_propagation/__init__.py b/mrp_lot_number_propagation/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/mrp_lot_number_propagation/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mrp_lot_number_propagation/__manifest__.py b/mrp_lot_number_propagation/__manifest__.py new file mode 100644 index 000000000..f439e3c63 --- /dev/null +++ b/mrp_lot_number_propagation/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +{ + "name": "MRP Serial Number Propagation", + "version": "14.0.1.0.0", + "development_status": "Beta", + "license": "AGPL-3", + "author": "Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["sebalix"], + "summary": "Propagate a serial number from a component to a finished product", + "website": "https://github.com/OCA/manufacture", + "category": "Manufacturing", + "depends": ["mrp"], + "data": [ + "views/mrp_bom.xml", + "views/mrp_production.xml", + ], + "installable": True, + "application": False, +} diff --git a/mrp_lot_number_propagation/i18n/es.po b/mrp_lot_number_propagation/i18n/es.po new file mode 100644 index 000000000..debb149ab --- /dev/null +++ b/mrp_lot_number_propagation/i18n/es.po @@ -0,0 +1,192 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mrp_lot_number_propagation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-10-29 08:29+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: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/product_template.py:0 +#, python-format +msgid "" +"A BoM propagating serial numbers requires this product to be tracked as " +"such." +msgstr "" +"Una BoM que propague números de serie requiere que este producto sea " +"rastreado como tal." + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,help:mrp_lot_number_propagation.field_mrp_bom__lot_number_propagation +msgid "" +"Allow to propagate the lot/serial number from a component to the finished " +"product." +msgstr "" +"Permite propagar el número de lote/serie de un componente al producto " +"acabado." + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_mrp_bom +msgid "Bill of Material" +msgstr "Lista de Material" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_mrp_bom_line +msgid "Bill of Material Line" +msgstr "Línea de la factura de materiales" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_production.py:0 +#, python-format +msgid "" +"Bill of material is marked for lot number propagation, but there are " +"multiple components propagating lot number. Please check BOM configuration." +msgstr "" +"La lista de materiales está marcada para la propagación del número de lote, " +"pero hay varios componentes que propagan el número de lote. Compruebe la " +"configuración de la lista de materiales." + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_production.py:0 +#, python-format +msgid "" +"Bill of material is marked for lot number propagation, but there are no " +"components propagating lot number. Please check BOM configuration." +msgstr "" +"La lista de materiales está marcada para la propagación del número de lote, " +"pero no hay componentes que propaguen el número de lote. Compruebe la " +"configuración de la lista de materiales." + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_bom__display_lot_number_propagation +msgid "Display Lot Number Propagation" +msgstr "Mostrar Número de Lote Propagación" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_bom_line__display_propagate_lot_number +msgid "Display Propagate Lot Number" +msgstr "Mostrar Propagación Número de Lote" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_production__is_lot_number_propagated +msgid "Is Lot Number Propagated" +msgstr "Es el Número de Lote Propagado" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_bom__lot_number_propagation +msgid "Lot Number Propagation" +msgstr "Propagación del Número de Lote" + +#. module: mrp_lot_number_propagation +#: model_terms:ir.ui.view,arch_db:mrp_lot_number_propagation.mrp_production_form_view +msgid "Lot/Serial Number" +msgstr "Lote/Número de serie" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_production.py:0 +#, python-format +msgid "" +"Lot/Serial number %s already exists and has been used. Unable to propagate " +"it." +msgstr "" +"El lote/número de serie %s ya existe y ha sido utilizado. No se ha podido " +"propagar." + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_production.py:0 +#, python-format +msgid "" +"Lot/Serial number is propagated from a component, you are not allowed to " +"change it." +msgstr "" +"El número de lote/serie se propaga desde un componente, usted no está " +"autorizado a modificarlo." + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,help:mrp_lot_number_propagation.field_mrp_production__is_lot_number_propagated +msgid "" +"Lot/serial number is propagated from a component to the finished product." +msgstr "" +"El número de lote/serie se propaga de un componente al producto acabado." + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_bom_line.py:0 +#, python-format +msgid "" +"Only components tracked by serial number can propagate its lot/serial number" +" to the finished product." +msgstr "" +"Sólo los componentes rastreados por número de serie pueden propagar su " +"número de lote/serie al producto acabado." + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_product_product +msgid "Product" +msgstr "Producto" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_product_template +msgid "Product Template" +msgstr "Plantilla del Producto" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_mrp_production +msgid "Production Order" +msgstr "Orden de Producción" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_bom_line__propagate_lot_number +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_stock_move__propagate_lot_number +msgid "Propagate Lot Number" +msgstr "Propagar el Número de Lote" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_production__propagated_lot_producing +msgid "Propagated Lot Producing" +msgstr "Producción de Lotes Propagados" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_stock_move +msgid "Stock Move" +msgstr "Movimiento de Existencias" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,help:mrp_lot_number_propagation.field_mrp_production__propagated_lot_producing +msgid "" +"The BoM used on this manufacturing order is set to propagate lot number from" +" one of its components. The value will be computed once the corresponding " +"component is selected." +msgstr "" +"La lista de materiales utilizada en esta orden de producción está " +"configurada para propagar el número de lote desde uno de sus componentes. El " +"valor se calculará una vez seleccionado el componente correspondiente." + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/product_template.py:0 +#, python-format +msgid "" +"This component is configured to propagate its serial number in the following" +" Bill of Materials:{boms}'" +msgstr "" +"Este componente está configurado para propagar su número de serie en la " +"siguiente lista de materiales:{boms}'" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_bom.py:0 +#, python-format +msgid "" +"With 'Lot Number Propagation' enabled, a line has to be configured with the " +"'Propagate Lot Number' option." +msgstr "" +"Con la \"Propagación del número de lote\" activada, debe configurarse una " +"línea con la opción \"Propagar número de lote\"." diff --git a/mrp_lot_number_propagation/i18n/it.po b/mrp_lot_number_propagation/i18n/it.po new file mode 100644 index 000000000..087c1ced9 --- /dev/null +++ b/mrp_lot_number_propagation/i18n/it.po @@ -0,0 +1,198 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mrp_lot_number_propagation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-11-21 16:35+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 4.17\n" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/product_template.py:0 +#, python-format +msgid "" +"A BoM propagating serial numbers requires this product to be tracked as such." +msgstr "" +"Una distinta base che propaga i numeri di serie richiede che questo prodotto " +"sia tracciato come tale." + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,help:mrp_lot_number_propagation.field_mrp_bom__lot_number_propagation +msgid "" +"Allow to propagate the lot/serial number from a component to the finished " +"product." +msgstr "" +"Consente di propagare il numero di lotto/matricola da un componente al " +"prodotto finito." + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_mrp_bom +msgid "Bill of Material" +msgstr "Distinta base" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_mrp_bom_line +msgid "Bill of Material Line" +msgstr "Riga distinta base" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_production.py:0 +#, python-format +msgid "" +"Bill of material is marked for lot number propagation, but there are " +"multiple components propagating lot number. Please check BOM configuration." +msgstr "" +"La distinta base è impostata per la propagazione del numero di lotto, ma ci " +"sono più componenti che propagano il numero di lotto. Verificare la " +"configurazione della DB." + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_production.py:0 +#, python-format +msgid "" +"Bill of material is marked for lot number propagation, but there are no " +"components propagating lot number. Please check BOM configuration." +msgstr "" +"La distinta base è impostata per la propagazione del numero di lotto, ma non " +"ci sono componenti che propagano il numero di lotto. Verificare la " +"configurazione della DB." + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_bom__display_lot_number_propagation +msgid "Display Lot Number Propagation" +msgstr "Visualizza propagazione numero di lotto" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_bom_line__display_propagate_lot_number +msgid "Display Propagate Lot Number" +msgstr "Visualizza il numero di lotto propagato" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_production__is_lot_number_propagated +msgid "Is Lot Number Propagated" +msgstr "Il numero di lotto è propagato" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_bom__lot_number_propagation +msgid "Lot Number Propagation" +msgstr "Propagazione numero di lotto" + +#. module: mrp_lot_number_propagation +#: model_terms:ir.ui.view,arch_db:mrp_lot_number_propagation.mrp_production_form_view +msgid "Lot/Serial Number" +msgstr "Numero di lotto/serie" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_production.py:0 +#, python-format +msgid "" +"Lot/Serial number %s already exists and has been used. Unable to propagate " +"it." +msgstr "" +"Il lotto/seriale %s esiste ed è stato usato. Non è possibile propagarlo." + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_production.py:0 +#, python-format +msgid "" +"Lot/Serial number is propagated from a component, you are not allowed to " +"change it." +msgstr "" +"Il numero di lotto/matricola è propagato da un componente, non è consentito " +"modificarlo." + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,help:mrp_lot_number_propagation.field_mrp_production__is_lot_number_propagated +msgid "" +"Lot/serial number is propagated from a component to the finished product." +msgstr "" +"Il numero di lotto/matricola è propagato da un componente al prodotto finito." + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_bom_line.py:0 +#, python-format +msgid "" +"Only components tracked by serial number can propagate its lot/serial number " +"to the finished product." +msgstr "" +"Solo i componenti tracciati da una matricola possono propagare il loro " +"numero di lotto/matricola al prodotto finito." + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_product_product +msgid "Product" +msgstr "Prodotto" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_product_template +msgid "Product Template" +msgstr "Modello prodotto" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_mrp_production +msgid "Production Order" +msgstr "Ordine di produzione" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_bom_line__propagate_lot_number +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_stock_move__propagate_lot_number +msgid "Propagate Lot Number" +msgstr "Propaga numero di lotto" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_production__propagated_lot_producing +msgid "Propagated Lot Producing" +msgstr "Produzione lotto propagato" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_stock_move +msgid "Stock Move" +msgstr "Movimento di magazzino" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,help:mrp_lot_number_propagation.field_mrp_production__propagated_lot_producing +msgid "" +"The BoM used on this manufacturing order is set to propagate lot number from " +"one of its components. The value will be computed once the corresponding " +"component is selected." +msgstr "" +"La DiBa utilizzata in questo ordine di produzione è impostata per propagare " +"il nomero di lotto da uno dei suoi componenti. Il valore verrà calcolato una " +"volta che il componente corrispondente sarà selezionato." + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/product_template.py:0 +#, python-format +msgid "" +"This component is configured to propagate its serial number in the following " +"Bill of Materials:{boms}'" +msgstr "" +"Questo componente è configurato per propagare la sua matricola nella " +"seguente DiBa: {boms}'" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_bom.py:0 +#, python-format +msgid "" +"With 'Lot Number Propagation' enabled, a line has to be configured with the " +"'Propagate Lot Number' option." +msgstr "" +"Con 'Propagazione numero di lotto' abilitata, una riga deve essere " +"configurata con l'opzione 'Propaga numero di lotto'." + +#, python-format +#~ msgid "" +#~ "Only one BoM line can propagate its lot/serial number to the finished " +#~ "product." +#~ msgstr "" +#~ "Solo una riga di DiBa può propagare il suo numero di lotto/matricola al " +#~ "prodotto finito." diff --git a/mrp_lot_number_propagation/i18n/mrp_lot_number_propagation.pot b/mrp_lot_number_propagation/i18n/mrp_lot_number_propagation.pot new file mode 100644 index 000000000..3f4b0d5e3 --- /dev/null +++ b/mrp_lot_number_propagation/i18n/mrp_lot_number_propagation.pot @@ -0,0 +1,165 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mrp_lot_number_propagation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.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: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/product_template.py:0 +#, python-format +msgid "" +"A BoM propagating serial numbers requires this product to be tracked as " +"such." +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,help:mrp_lot_number_propagation.field_mrp_bom__lot_number_propagation +msgid "" +"Allow to propagate the lot/serial number from a component to the finished " +"product." +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_mrp_bom +msgid "Bill of Material" +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_mrp_bom_line +msgid "Bill of Material Line" +msgstr "" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_production.py:0 +#, python-format +msgid "" +"Bill of material is marked for lot number propagation, but there are " +"multiple components propagating lot number. Please check BOM configuration." +msgstr "" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_production.py:0 +#, python-format +msgid "" +"Bill of material is marked for lot number propagation, but there are no " +"components propagating lot number. Please check BOM configuration." +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_bom__display_lot_number_propagation +msgid "Display Lot Number Propagation" +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_bom_line__display_propagate_lot_number +msgid "Display Propagate Lot Number" +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_production__is_lot_number_propagated +msgid "Is Lot Number Propagated" +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_bom__lot_number_propagation +msgid "Lot Number Propagation" +msgstr "" + +#. module: mrp_lot_number_propagation +#: model_terms:ir.ui.view,arch_db:mrp_lot_number_propagation.mrp_production_form_view +msgid "Lot/Serial Number" +msgstr "" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_production.py:0 +#, python-format +msgid "" +"Lot/Serial number %s already exists and has been used. Unable to propagate " +"it." +msgstr "" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_production.py:0 +#, python-format +msgid "" +"Lot/Serial number is propagated from a component, you are not allowed to " +"change it." +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,help:mrp_lot_number_propagation.field_mrp_production__is_lot_number_propagated +msgid "" +"Lot/serial number is propagated from a component to the finished product." +msgstr "" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_bom_line.py:0 +#, python-format +msgid "" +"Only components tracked by serial number can propagate its lot/serial number" +" to the finished product." +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_product_product +msgid "Product" +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_product_template +msgid "Product Template" +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_mrp_production +msgid "Production Order" +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_bom_line__propagate_lot_number +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_stock_move__propagate_lot_number +msgid "Propagate Lot Number" +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,field_description:mrp_lot_number_propagation.field_mrp_production__propagated_lot_producing +msgid "Propagated Lot Producing" +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model,name:mrp_lot_number_propagation.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: mrp_lot_number_propagation +#: model:ir.model.fields,help:mrp_lot_number_propagation.field_mrp_production__propagated_lot_producing +msgid "" +"The BoM used on this manufacturing order is set to propagate lot number from" +" one of its components. The value will be computed once the corresponding " +"component is selected." +msgstr "" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/product_template.py:0 +#, python-format +msgid "" +"This component is configured to propagate its serial number in the following" +" Bill of Materials:{boms}'" +msgstr "" + +#. module: mrp_lot_number_propagation +#: code:addons/mrp_lot_number_propagation/models/mrp_bom.py:0 +#, python-format +msgid "" +"With 'Lot Number Propagation' enabled, a line has to be configured with the " +"'Propagate Lot Number' option." +msgstr "" diff --git a/mrp_lot_number_propagation/models/__init__.py b/mrp_lot_number_propagation/models/__init__.py new file mode 100644 index 000000000..b92e6ec25 --- /dev/null +++ b/mrp_lot_number_propagation/models/__init__.py @@ -0,0 +1,6 @@ +from . import mrp_bom +from . import mrp_bom_line +from . import mrp_production +from . import product_product +from . import product_template +from . import stock_move diff --git a/mrp_lot_number_propagation/models/mrp_bom.py b/mrp_lot_number_propagation/models/mrp_bom.py new file mode 100644 index 000000000..0e6204175 --- /dev/null +++ b/mrp_lot_number_propagation/models/mrp_bom.py @@ -0,0 +1,88 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import _, api, fields, models, tools +from odoo.exceptions import ValidationError + + +class MrpBom(models.Model): + _inherit = "mrp.bom" + + lot_number_propagation = fields.Boolean( + default=False, + help=( + "Allow to propagate the lot/serial number " + "from a component to the finished product." + ), + ) + display_lot_number_propagation = fields.Boolean( + compute="_compute_display_lot_number_propagation" + ) + + @api.depends( + "type", + "product_tmpl_id.tracking", + "product_qty", + "product_uom_id", + "bom_line_ids.product_id.tracking", + "bom_line_ids.product_qty", + "bom_line_ids.product_uom_id", + ) + def _compute_display_lot_number_propagation(self): + """Check if a lot number can be propagated. + + A lot number can be propagated from a component to the finished product if: + - the type of the BoM is normal (Manufacture this product) + - the finished product is tracked by serial number + - the quantity of the finished product is 1 and its UoM is unit + - there is at least one bom line, with a component tracked by serial, + having a quantity of 1 and its UoM is unit + """ + uom_unit = self.env.ref("uom.product_uom_unit") + for bom in self: + bom.display_lot_number_propagation = ( + bom.type in self._get_lot_number_propagation_bom_types() + and bom.product_tmpl_id.tracking == "serial" + and tools.float_compare( + bom.product_qty, 1, precision_rounding=bom.product_uom_id.rounding + ) + == 0 + and bom.product_uom_id == uom_unit + and bom._has_tracked_product_to_propagate() + ) + + def _get_lot_number_propagation_bom_types(self): + return ["normal"] + + def _has_tracked_product_to_propagate(self): + self.ensure_one() + uom_unit = self.env.ref("uom.product_uom_unit") + for line in self.bom_line_ids: + if ( + line.product_id.tracking == "serial" + and line.product_uom_id == uom_unit + and tools.float_compare( + line.product_qty, 1, precision_rounding=line.product_uom_id.rounding + ) + == 0 + ): + return True + return False + + @api.onchange("display_lot_number_propagation") + def onchange_display_lot_number_propagation(self): + if not self.display_lot_number_propagation: + self.lot_number_propagation = False + + @api.constrains("lot_number_propagation") + def _check_propagate_lot_number(self): + for bom in self: + if not bom.lot_number_propagation: + continue + if not bom.bom_line_ids.filtered("propagate_lot_number"): + raise ValidationError( + _( + "With 'Lot Number Propagation' enabled, a line has " + "to be configured with the 'Propagate Lot Number' option." + ) + ) diff --git a/mrp_lot_number_propagation/models/mrp_bom_line.py b/mrp_lot_number_propagation/models/mrp_bom_line.py new file mode 100644 index 000000000..b0f8d97f0 --- /dev/null +++ b/mrp_lot_number_propagation/models/mrp_bom_line.py @@ -0,0 +1,48 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class MrpBomLine(models.Model): + _inherit = "mrp.bom.line" + + propagate_lot_number = fields.Boolean( + default=False, + ) + display_propagate_lot_number = fields.Boolean( + compute="_compute_display_propagate_lot_number" + ) + + @api.depends( + "bom_id.display_lot_number_propagation", + "bom_id.lot_number_propagation", + ) + def _compute_display_propagate_lot_number(self): + for line in self: + line.display_propagate_lot_number = ( + line.bom_id.display_lot_number_propagation + and line.bom_id.lot_number_propagation + ) + + @api.constrains("propagate_lot_number") + def _check_propagate_lot_number(self): + """ + This function should check: + + - if the bom has lot_number_propagation marked, there is one and + only one line of this bom with propagate_lot_number marked. + - the bom line being marked with lot_number_propagation is of the same + tracking type as the finished product + """ + for line in self: + if not line.bom_id.lot_number_propagation: + continue + if line.propagate_lot_number and line.product_id.tracking != "serial": + raise ValidationError( + _( + "Only components tracked by serial number can propagate " + "its lot/serial number to the finished product." + ) + ) diff --git a/mrp_lot_number_propagation/models/mrp_production.py b/mrp_lot_number_propagation/models/mrp_production.py new file mode 100644 index 000000000..8d8b090d4 --- /dev/null +++ b/mrp_lot_number_propagation/models/mrp_production.py @@ -0,0 +1,193 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from lxml import etree + +from odoo import _, api, fields, models, tools +from odoo.exceptions import UserError +from odoo.osv import expression +from odoo.tools.safe_eval import safe_eval + +from odoo.addons.base.models.ir_ui_view import ( + transfer_modifiers_to_node, + transfer_node_to_modifiers, +) + + +class MrpProduction(models.Model): + _inherit = "mrp.production" + + is_lot_number_propagated = fields.Boolean( + default=False, + readonly=True, + help=( + "Lot/serial number is propagated " + "from a component to the finished product." + ), + ) + propagated_lot_producing = fields.Char( + compute="_compute_propagated_lot_producing", + help=( + "The BoM used on this manufacturing order is set to propagate " + "lot number from one of its components. The value will be " + "computed once the corresponding component is selected." + ), + ) + + @api.depends( + "move_raw_ids.propagate_lot_number", + "move_raw_ids.move_line_ids.qty_done", + "move_raw_ids.move_line_ids.lot_id", + ) + def _compute_propagated_lot_producing(self): + for order in self: + order.propagated_lot_producing = False + move_with_lot = order._get_propagating_component_move() + line_with_sn = move_with_lot.move_line_ids.filtered( + lambda l: ( + l.lot_id + and l.product_id.tracking == "serial" + and tools.float_compare( + l.qty_done, 1, precision_rounding=l.product_uom_id.rounding + ) + == 0 + ) + ) + if len(line_with_sn) == 1: + order.propagated_lot_producing = line_with_sn.lot_id.name + + @api.onchange("bom_id") + def _onchange_bom_id_lot_number_propagation(self): + self.is_lot_number_propagated = self.bom_id.lot_number_propagation + + def action_confirm(self): + res = super().action_confirm() + self._set_lot_number_propagation_data_from_bom() + return res + + def _get_propagating_component_move(self): + self.ensure_one() + return self.move_raw_ids.filtered(lambda o: o.propagate_lot_number) + + def _set_lot_number_propagation_data_from_bom(self): + """Copy information from BoM to the manufacturing order.""" + for order in self: + propagate_lot = order.bom_id.lot_number_propagation + if not propagate_lot: + continue + order.is_lot_number_propagated = propagate_lot + propagate_move = order.move_raw_ids.filtered( + lambda m: m.bom_line_id.propagate_lot_number + ) + if not propagate_move: + raise UserError( + _( + "Bill of material is marked for lot number propagation, but " + "there are no components propagating lot number. " + "Please check BOM configuration." + ) + ) + elif len(propagate_move) > 1: + raise UserError( + _( + "Bill of material is marked for lot number propagation, but " + "there are multiple components propagating lot number. " + "Please check BOM configuration." + ) + ) + else: + propagate_move.propagate_lot_number = True + + def _post_inventory(self, cancel_backorder=False): + self._create_and_assign_propagated_lot_number() + return super()._post_inventory(cancel_backorder=cancel_backorder) + + def _create_and_assign_propagated_lot_number(self): + for order in self: + if not order.is_lot_number_propagated or order.lot_producing_id: + continue + finish_moves = order.move_finished_ids.filtered( + lambda m: m.product_id == order.product_id + and m.state not in ("done", "cancel") + ) + if finish_moves and not finish_moves.quantity_done: + lot_model = self.env["stock.production.lot"] + lot = lot_model.search( + [ + ("product_id", "=", order.product_id.id), + ("company_id", "=", order.company_id.id), + ("name", "=", order.propagated_lot_producing), + ], + limit=1, + ) + if lot.quant_ids: + raise UserError( + _( + "Lot/Serial number %s already exists and has been used. " + "Unable to propagate it." + ) + ) + if not lot: + lot = self.env["stock.production.lot"].create( + { + "product_id": order.product_id.id, + "company_id": order.company_id.id, + "name": order.propagated_lot_producing, + } + ) + order.with_context(lot_propagation=True).lot_producing_id = lot + + def write(self, vals): + for order in self: + if ( + order.is_lot_number_propagated + and "lot_producing_id" in vals + and not self.env.context.get("lot_propagation") + ): + raise UserError( + _( + "Lot/Serial number is propagated from a component, " + "you are not allowed to change it." + ) + ) + return super().write(vals) + + def fields_view_get( + self, view_id=None, view_type="form", toolbar=False, submenu=False + ): + # Override to hide the "lot_producing_id" field + "action_generate_serial" + # button if the MO is configured to propagate a serial number + result = super().fields_view_get( + view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu + ) + if result.get("name") in self._views_to_adapt(): + result["arch"] = self._fields_view_get_adapt_lot_tags_attrs(result) + return result + + def _views_to_adapt(self): + """Return the form view names bound to 'mrp.production' to adapt.""" + return ["mrp.production.form"] + + def _fields_view_get_adapt_lot_tags_attrs(self, view): + """Hide elements related to lot if it is automatically propagated.""" + doc = etree.XML(view["arch"]) + tags = ( + "//label[@for='lot_producing_id']", + "//field[@name='lot_producing_id']/..", # parent
+ ) + for xpath_expr in tags: + attrs_key = "invisible" + nodes = doc.xpath(xpath_expr) + for field in nodes: + attrs = safe_eval(field.attrib.get("attrs", "{}")) + if not attrs[attrs_key]: + continue + invisible_domain = expression.OR( + [attrs[attrs_key], [("is_lot_number_propagated", "=", True)]] + ) + attrs[attrs_key] = invisible_domain + field.set("attrs", str(attrs)) + modifiers = {} + transfer_node_to_modifiers(field, modifiers, self.env.context) + transfer_modifiers_to_node(modifiers, field) + return etree.tostring(doc, encoding="unicode") diff --git a/mrp_lot_number_propagation/models/product_product.py b/mrp_lot_number_propagation/models/product_product.py new file mode 100644 index 000000000..69c72b19d --- /dev/null +++ b/mrp_lot_number_propagation/models/product_product.py @@ -0,0 +1,13 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + @api.constrains("tracking") + def _check_bom_propagate_lot_number(self): + for product in self: + product.product_tmpl_id._check_bom_propagate_lot_number() diff --git a/mrp_lot_number_propagation/models/product_template.py b/mrp_lot_number_propagation/models/product_template.py new file mode 100644 index 000000000..7a42adbb5 --- /dev/null +++ b/mrp_lot_number_propagation/models/product_template.py @@ -0,0 +1,42 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import _, api, models +from odoo.exceptions import ValidationError + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + @api.constrains("tracking") + def _check_bom_propagate_lot_number(self): + """Block tracking type updates if the product is used by a BoM.""" + for product in self: + if product.tracking == "serial": + continue + # Check BoMs + for bom in product.bom_ids: + if bom.lot_number_propagation: + raise ValidationError( + _( + "A BoM propagating serial numbers requires " + "this product to be tracked as such." + ) + ) + # Check lines of BoMs + bom_lines = self.env["mrp.bom.line"].search( + [ + ("product_id", "in", product.product_variant_ids.ids), + ("propagate_lot_number", "=", True), + ("bom_id.lot_number_propagation", "=", True), + ] + ) + if bom_lines: + boms = "\n- ".join(bom_lines.mapped("bom_id.display_name")) + boms = "\n- " + boms + raise ValidationError( + _( + "This component is configured to propagate its " + "serial number in the following Bill of Materials:{boms}'" + ).format(boms=boms) + ) diff --git a/mrp_lot_number_propagation/models/stock_move.py b/mrp_lot_number_propagation/models/stock_move.py new file mode 100644 index 000000000..cda1fba97 --- /dev/null +++ b/mrp_lot_number_propagation/models/stock_move.py @@ -0,0 +1,13 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class StockMove(models.Model): + _inherit = "stock.move" + + propagate_lot_number = fields.Boolean( + default=False, + readonly=True, + ) diff --git a/mrp_lot_number_propagation/readme/CONTRIBUTORS.rst b/mrp_lot_number_propagation/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..313df49ab --- /dev/null +++ b/mrp_lot_number_propagation/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Akim Juillerat +* Sébastien Alix diff --git a/mrp_lot_number_propagation/readme/DESCRIPTION.rst b/mrp_lot_number_propagation/readme/DESCRIPTION.rst new file mode 100644 index 000000000..d2563bc4a --- /dev/null +++ b/mrp_lot_number_propagation/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Allow to propagate a lot number from a component to a finished product. diff --git a/mrp_lot_number_propagation/readme/ROADMAP.rst b/mrp_lot_number_propagation/readme/ROADMAP.rst new file mode 100644 index 000000000..38015d2db --- /dev/null +++ b/mrp_lot_number_propagation/readme/ROADMAP.rst @@ -0,0 +1 @@ +* Add compatibility with lot number (in addition to serial number) diff --git a/mrp_lot_number_propagation/tests/__init__.py b/mrp_lot_number_propagation/tests/__init__.py new file mode 100644 index 000000000..a16beef0b --- /dev/null +++ b/mrp_lot_number_propagation/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mrp_bom diff --git a/mrp_lot_number_propagation/tests/common.py b/mrp_lot_number_propagation/tests/common.py new file mode 100644 index 000000000..72cb3cf84 --- /dev/null +++ b/mrp_lot_number_propagation/tests/common.py @@ -0,0 +1,159 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import random +import string + +from odoo import fields +from odoo.tests import Form, common + + +class Common(common.SavepointCase): + + LOT_NAME = "PROPAGATED-LOT" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.bom = cls.env.ref("mrp.mrp_bom_desk") + cls.bom_product_template = cls.env.ref( + "mrp.product_product_computer_desk_product_template" + ) + cls.bom_product_product = cls.env.ref("mrp.product_product_computer_desk") + cls.product_tracked_by_lot = cls.env.ref( + "mrp.product_product_computer_desk_leg" + ) + cls.product_tracked_by_sn = cls.env.ref( + "mrp.product_product_computer_desk_head" + ) + cls.product_template_tracked_by_sn = cls.env.ref( + "mrp.product_product_computer_desk_head_product_template" + ) + cls.line_tracked_by_lot = cls.bom.bom_line_ids.filtered( + lambda o: o.product_id == cls.product_tracked_by_lot + ) + cls.line_tracked_by_sn = cls.bom.bom_line_ids.filtered( + lambda o: o.product_id == cls.product_tracked_by_sn + ) + cls.line_no_tracking = fields.first( + cls.bom.bom_line_ids.filtered(lambda o: o.product_id.tracking == "none") + ) + + @classmethod + def _update_qty_in_location( + cls, location, product, quantity, package=None, lot=None, in_date=None + ): + quants = cls.env["stock.quant"]._gather( + product, location, lot_id=lot, package_id=package, strict=True + ) + # this method adds the quantity to the current quantity, so remove it + quantity -= sum(quants.mapped("quantity")) + cls.env["stock.quant"]._update_available_quantity( + product, + location, + quantity, + package_id=package, + lot_id=lot, + in_date=in_date, + ) + + @classmethod + def _update_stock_component_qty(cls, order=None, bom=None, location=None): + if not order and not bom: + return + if order: + bom = order.bom_id + if not location: + location = cls.env.ref("stock.stock_location_stock") + for line in bom.bom_line_ids: + if line.product_id.type != "product": + continue + lot = None + if line.product_id.tracking != "none": + lot_name = "".join( + random.choice(string.ascii_lowercase) for i in range(10) + ) + if line.propagate_lot_number: + lot_name = cls.LOT_NAME + vals = { + "product_id": line.product_id.id, + "company_id": line.company_id.id, + "name": lot_name, + } + lot = cls.env["stock.production.lot"].create(vals) + cls._update_qty_in_location( + location, + line.product_id, + line.product_qty, + lot=lot, + ) + + @classmethod + def _get_lot_quants(cls, lot, location=None): + quants = lot.quant_ids.filtered(lambda q: q.quantity > 0) + if location: + quants = quants.filtered( + lambda q: q.location_id.parent_path in location.parent_path + ) + return quants + + @classmethod + def _add_color_and_legs_variants(cls, product_template): + color_attribute = cls.env.ref("product.product_attribute_2") + color_att_value_white = cls.env.ref("product.product_attribute_value_3") + color_att_value_black = cls.env.ref("product.product_attribute_value_4") + legs_attribute = cls.env.ref("product.product_attribute_1") + legs_att_value_steel = cls.env.ref("product.product_attribute_value_1") + legs_att_value_alu = cls.env.ref("product.product_attribute_value_2") + cls._add_variants( + product_template, + { + color_attribute: [color_att_value_white, color_att_value_black], + legs_attribute: [legs_att_value_steel, legs_att_value_alu], + }, + ) + + @classmethod + def _add_variants(cls, product_template, attribute_values_dict): + for attribute, att_values_list in attribute_values_dict.items(): + cls.env["product.template.attribute.line"].create( + { + "product_tmpl_id": product_template.id, + "attribute_id": attribute.id, + "value_ids": [(6, 0, [att_val.id for att_val in att_values_list])], + } + ) + + @classmethod + def _create_bom_with_variants(cls): + attribute_values_dict = { + att_val.product_attribute_value_id.name: att_val.id + for att_val in cls.env["product.template.attribute.value"].search( + [("product_tmpl_id", "=", cls.bom_product_template.id)] + ) + } + new_bom_form = Form(cls.env["mrp.bom"]) + new_bom_form.product_tmpl_id = cls.bom_product_template + new_bom = new_bom_form.save() + bom_line_create_values = [] + for product in cls.product_template_tracked_by_sn.product_variant_ids: + create_values = {"bom_id": new_bom.id} + create_values["product_id"] = product.id + att_values_commands = [] + for att_value in product.product_template_attribute_value_ids: + att_values_commands.append( + (4, attribute_values_dict[att_value.name], 0) + ) + create_values[ + "bom_product_template_attribute_value_ids" + ] = att_values_commands + bom_line_create_values.append(create_values) + cls.env["mrp.bom.line"].create(bom_line_create_values) + new_bom_form = Form(new_bom) + new_bom_form.lot_number_propagation = True + for line_position, _bom_line in enumerate(new_bom.bom_line_ids): + new_bom_line_form = new_bom_form.bom_line_ids.edit(line_position) + new_bom_line_form.propagate_lot_number = True + new_bom_line_form.save() + return new_bom_form.save() diff --git a/mrp_lot_number_propagation/tests/test_mrp_bom.py b/mrp_lot_number_propagation/tests/test_mrp_bom.py new file mode 100644 index 000000000..6b740ab19 --- /dev/null +++ b/mrp_lot_number_propagation/tests/test_mrp_bom.py @@ -0,0 +1,72 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.exceptions import ValidationError +from odoo.tests.common import Form + +from .common import Common + + +class TestMrpBom(Common): + def test_bom_display_lot_number_propagation(self): + self.assertTrue(self.bom.display_lot_number_propagation) + self.bom.product_tmpl_id.tracking = "none" + self.assertFalse(self.bom.display_lot_number_propagation) + + def test_bom_line_check_propagate_lot_number_not_tracked(self): + form = Form(self.bom) + form.lot_number_propagation = True + # Flag a line that can't be propagated + line_form = form.bom_line_ids.edit(2) # line without tracking + line_form.propagate_lot_number = True + line_form.save() + with self.assertRaisesRegex(ValidationError, "Only components tracked"): + form.save() + + def test_bom_line_check_propagate_lot_number_tracked_by_lot(self): + form = Form(self.bom) + form.lot_number_propagation = True + # Flag a line tracked by lot (not SN) which is not supported + line_form = form.bom_line_ids.edit(1) + line_form.propagate_lot_number = True + line_form.save() + with self.assertRaisesRegex(ValidationError, "Only components tracked"): + form.save() + + def test_bom_line_check_propagate_lot_number_same_tracking(self): + form = Form(self.bom) + form.lot_number_propagation = True + # Flag a line whose tracking type is the same than the finished product + line_form = form.bom_line_ids.edit(0) + line_form.propagate_lot_number = True + line_form.save() + form.save() + + def test_bom_check_propagate_lot_number(self): + # Configure the BoM to propagate the lot/SN without enabling any line + with self.assertRaisesRegex(ValidationError, "a line has to be configured"): + self.bom.lot_number_propagation = True + + def test_reset_tracking_on_bom_product(self): + # Configure the BoM to propagate the lot/SN + with Form(self.bom) as form: + form.lot_number_propagation = True + line_form = form.bom_line_ids.edit(0) # Line tracked by SN + line_form.propagate_lot_number = True + line_form.save() + form.save() + # Reset the tracking on the finished product + with self.assertRaisesRegex(ValidationError, "A BoM propagating"): + self.bom.product_tmpl_id.tracking = "none" + + def test_reset_tracking_on_bom_component(self): + # Configure the BoM to propagate the lot/SN + with Form(self.bom) as form: + form.lot_number_propagation = True + line_form = form.bom_line_ids.edit(0) # Line tracked by SN + line_form.propagate_lot_number = True + line_form.save() + form.save() + # Reset the tracking on the component which propagates the SN + with self.assertRaisesRegex(ValidationError, "This component is"): + self.line_tracked_by_sn.product_id.tracking = "none" diff --git a/mrp_lot_number_propagation/tests/test_mrp_production.py b/mrp_lot_number_propagation/tests/test_mrp_production.py new file mode 100644 index 000000000..c4030e134 --- /dev/null +++ b/mrp_lot_number_propagation/tests/test_mrp_production.py @@ -0,0 +1,147 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.exceptions import UserError +from odoo.tests.common import Form + +from .common import Common + + +class TestMrpProduction(Common): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Configure the BoM to propagate lot number + cls._configure_bom() + cls.order = cls._create_order(cls.bom_product_product, cls.bom) + + @classmethod + def _configure_bom(cls): + with Form(cls.bom) as form: + form.lot_number_propagation = True + line_form = form.bom_line_ids.edit(0) # Line tracked by SN + line_form.propagate_lot_number = True + line_form.save() + form.save() + + @classmethod + def _create_order(cls, product, bom): + with Form(cls.env["mrp.production"]) as form: + form.product_id = product + form.bom_id = bom + return form.save() + + def _set_qty_done(self, order): + for line in order.move_raw_ids.move_line_ids: + line.qty_done = line.product_uom_qty + order.qty_producing = order.product_qty + + def test_order_propagated_lot_producing(self): + self.assertTrue(self.order.is_lot_number_propagated) # set by onchange + self._update_stock_component_qty(self.order) + self.order.action_confirm() + self.order.action_assign() + self.assertTrue(self.order.is_lot_number_propagated) # set by action_confirm + self.assertTrue(any(self.order.move_raw_ids.mapped("propagate_lot_number"))) + self._set_qty_done(self.order) + self.assertEqual(self.order.propagated_lot_producing, self.LOT_NAME) + + def test_order_write_lot_producing_id_not_allowed(self): + with self.assertRaisesRegex(UserError, "not allowed"): + self.order.write({"lot_producing_id": False}) + + def test_order_post_inventory(self): + self._update_stock_component_qty(self.order) + self.order.action_confirm() + self.order.action_assign() + self._set_qty_done(self.order) + self.assertTrue(self.order.is_lot_number_propagated) # set by action_confirm + self.order.button_mark_done() + self.assertEqual(self.order.lot_producing_id.name, self.LOT_NAME) + + def test_order_post_inventory_lot_already_exists_but_not_used(self): + self._update_stock_component_qty(self.order) + self.order.action_confirm() + self.order.action_assign() + self._set_qty_done(self.order) + self.assertEqual(self.order.propagated_lot_producing, self.LOT_NAME) + # Create a lot with the same number for the finished product + # without any stock/quants (so not used at all) before validating the MO + existing_lot = self.env["stock.production.lot"].create( + { + "product_id": self.order.product_id.id, + "company_id": self.order.company_id.id, + "name": self.order.propagated_lot_producing, + } + ) + self.order.button_mark_done() + self.assertEqual(self.order.lot_producing_id, existing_lot) + + def test_order_post_inventory_lot_already_exists_and_used(self): + self._update_stock_component_qty(self.order) + self.order.action_confirm() + self.order.action_assign() + self._set_qty_done(self.order) + self.assertEqual(self.order.propagated_lot_producing, self.LOT_NAME) + # Create a lot with the same number for the finished product + # with some stock/quants (so it is considered as used) before + # validating the MO + existing_lot = self.env["stock.production.lot"].create( + { + "product_id": self.order.product_id.id, + "company_id": self.order.company_id.id, + "name": self.order.propagated_lot_producing, + } + ) + self._update_qty_in_location( + self.env.ref("stock.stock_location_stock"), + self.order.product_id, + 1, + lot=existing_lot, + ) + with self.assertRaisesRegex(UserError, "already exists and has been used"): + self.order.button_mark_done() + + def test_confirm_with_variant_ok(self): + self._add_color_and_legs_variants(self.bom_product_template) + self._add_color_and_legs_variants(self.product_template_tracked_by_sn) + new_bom = self._create_bom_with_variants() + self.assertTrue(new_bom.lot_number_propagation) + # As all variants must have a single component + # where lot must be propagated, there should not be any error + for product in self.bom_product_template.product_variant_ids: + new_order = self._create_order(product, new_bom) + new_order.action_confirm() + + def test_confirm_with_variant_multiple(self): + self._add_color_and_legs_variants(self.bom_product_template) + self._add_color_and_legs_variants(self.product_template_tracked_by_sn) + new_bom = self._create_bom_with_variants() + # Remove application on variant for first bom line + # with this only the first variant of the product template + # will have a single component where lot must be propagated + new_bom.bom_line_ids[0].bom_product_template_attribute_value_ids = [(5, 0, 0)] + for cnt, product in enumerate(self.bom_product_template.product_variant_ids): + new_order = self._create_order(product, new_bom) + if cnt == 0: + new_order.action_confirm() + else: + with self.assertRaisesRegex(UserError, "multiple components"): + new_order.action_confirm() + + def test_confirm_with_variant_no(self): + self._add_color_and_legs_variants(self.bom_product_template) + self._add_color_and_legs_variants(self.product_template_tracked_by_sn) + new_bom = self._create_bom_with_variants() + for cnt, product in enumerate(self.bom_product_template.product_variant_ids): + new_order = self._create_order(product, new_bom) + if cnt == 0: + # Fake the case with no component by removing the flag + new_order.move_raw_ids.bom_line_id.write( + {"propagate_lot_number": False} + ) + with self.assertRaisesRegex(UserError, "no component"): + new_order.action_confirm() + new_order.action_assign() + else: + new_order.action_confirm() diff --git a/mrp_lot_number_propagation/views/mrp_bom.xml b/mrp_lot_number_propagation/views/mrp_bom.xml new file mode 100644 index 000000000..5f3f6f6a9 --- /dev/null +++ b/mrp_lot_number_propagation/views/mrp_bom.xml @@ -0,0 +1,28 @@ + + + + + + mrp.bom.form.inherit + mrp.bom + + + + + + + + + + + + + + diff --git a/mrp_lot_number_propagation/views/mrp_production.xml b/mrp_lot_number_propagation/views/mrp_production.xml new file mode 100644 index 000000000..62509a1dc --- /dev/null +++ b/mrp_lot_number_propagation/views/mrp_production.xml @@ -0,0 +1,25 @@ + + + + + + mrp.production.form.inherit + mrp.production + + + + + + + + + + + diff --git a/setup/mrp_lot_number_propagation/odoo/addons/mrp_lot_number_propagation b/setup/mrp_lot_number_propagation/odoo/addons/mrp_lot_number_propagation new file mode 120000 index 000000000..d7a58299b --- /dev/null +++ b/setup/mrp_lot_number_propagation/odoo/addons/mrp_lot_number_propagation @@ -0,0 +1 @@ +../../../../mrp_lot_number_propagation \ No newline at end of file diff --git a/setup/mrp_lot_number_propagation/setup.py b/setup/mrp_lot_number_propagation/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/mrp_lot_number_propagation/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)