From 3d34d58388ab362bf8e69cc2e75adbdfda3331b2 Mon Sep 17 00:00:00 2001 From: Adrien Widart <awt@odoo.com> Date: Mon, 1 Feb 2021 13:38:49 +0000 Subject: [PATCH] [FIX] mrp: force kit to be consumable product Kits must be consumable products, they might never be in stock. Setting kits as storable products can create some issues if the user creates some reordering rules. Because kits might never be in stock, it's impossible to fullfil the quantity of a reordering rule: asking a mini/maxi quantity on a kit makes no sense. Odoo will continuously propose to order more to reach the quantity necessary for the kit, but will never reach the asked qty, as it's a kit, not a manufactured product. OPW-2448878 closes odoo/odoo#65339 Signed-off-by: Steve Van Essche <svs-odoo@users.noreply.github.com> --- addons/mrp/data/mrp_demo.xml | 10 ++-------- addons/mrp/i18n/mrp.pot | 12 ++++++++++++ addons/mrp/models/mrp_bom.py | 7 +++++++ addons/mrp/models/product.py | 9 ++++++++- addons/mrp/tests/common.py | 5 ++++- addons/mrp_bom_cost/tests/test_bom_price.py | 1 + addons/sale_mrp/tests/test_sale_mrp_flow.py | 6 ++++-- 7 files changed, 38 insertions(+), 12 deletions(-) diff --git a/addons/mrp/data/mrp_demo.xml b/addons/mrp/data/mrp_demo.xml index 0c922662370a..3a7276be0ad2 100644 --- a/addons/mrp/data/mrp_demo.xml +++ b/addons/mrp/data/mrp_demo.xml @@ -398,7 +398,7 @@ <field name="categ_id" ref="product.product_category_5"/> <field name="standard_price">600.0</field> <field name="list_price">147.0</field> - <field name="type">product</field> + <field name="type">consu</field> <field name="uom_id" ref="uom.product_uom_unit"/> <field name="uom_po_id" ref="uom.product_uom_unit"/> <field name="description">Table kit</field> @@ -407,6 +407,7 @@ </record> <record id="product_product_table_kit_product_template" model="product.template"> + <field name="type">consu</field> <field name="route_ids" eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"/> </record> @@ -543,13 +544,6 @@ <field name="product_qty">30</field> <field name="location_id" ref="stock.stock_location_14"/> </record> - <record id="stock_inventory_line_product_table_kit" model="stock.inventory.line"> - <field name="product_id" ref="product_product_table_kit"/> - <field name="product_uom_id" ref="uom.product_uom_unit"/> - <field name="inventory_id" ref="stock_inventory_drawer"/> - <field name="product_qty">30</field> - <field name="location_id" ref="stock.stock_location_14"/> - </record> <function model="stock.inventory" name="_action_done"> <function eval="[[('state','=','draft'), ('id', '=', ref('stock_inventory_drawer'))]]" model="stock.inventory" name="search"/> diff --git a/addons/mrp/i18n/mrp.pot b/addons/mrp/i18n/mrp.pot index 8fb1dc5f2134..bd5a8eef3101 100644 --- a/addons/mrp/i18n/mrp.pot +++ b/addons/mrp/i18n/mrp.pot @@ -1242,6 +1242,12 @@ msgstr "" msgid "Followers (Partners)" msgstr "" +#. module: mrp +#: code:addons/mrp/models/mrp_bom.py:86 +#, python-format +msgid "For %s to be a kit, its product type must be 'Consumable'." +msgstr "" + #. module: mrp #: model_terms:ir.ui.view,arch_db:mrp.report_mrporder msgid "From" @@ -3374,6 +3380,12 @@ msgstr "" msgid "The operations for producing this BoM. When a routing is specified, the production orders will be executed through work orders, otherwise everything is processed in the production order itself. " msgstr "" +#. module: mrp +#: code:addons/mrp/models/product.py:30 +#, python-format +msgid "The product type of %s must be 'Consumable' because it has at least one kit-type bill of materials." +msgstr "" + #. module: mrp #: code:addons/mrp/wizard/mrp_product_produce.py:56 #, python-format diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index 43a178c479fd..4b4585b63bb5 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -79,6 +79,13 @@ class MrpBom(models.Model): if bom.bom_line_ids.filtered(lambda x: x.product_id.product_tmpl_id == bom.product_tmpl_id): raise ValidationError(_('BoM line product %s should not be same as BoM product.') % bom.display_name) + @api.constrains('product_tmpl_id', 'product_id', 'type') + def _check_kit_is_consumable(self): + for bom in self.filtered(lambda b: b.type == 'phantom'): + if (bom.product_id and bom.product_id.type or bom.product_tmpl_id.type) != "consu": + raise ValidationError(_("For %s to be a kit, its product type must be 'Consumable'." + % (bom.product_id and bom.product_id.display_name or bom.product_tmpl_id.display_name))) + @api.onchange('product_uom_id') def onchange_product_uom_id(self): res = {} diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py index 9d77fc44a78d..560ce82eeb74 100644 --- a/addons/mrp/models/product.py +++ b/addons/mrp/models/product.py @@ -2,8 +2,9 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import timedelta -from odoo import api, fields, models +from odoo import api, fields, models, _ from odoo.tools.float_utils import float_round +from odoo.exceptions import ValidationError class ProductTemplate(models.Model): @@ -22,6 +23,12 @@ class ProductTemplate(models.Model): for product in self: product.bom_count = self.env['mrp.bom'].search_count([('product_tmpl_id', '=', product.id)]) + @api.constrains('type') + def _check_phantom_bom_is_consumable_template(self): + for product_tmpl in self: + if product_tmpl.type != 'consu' and 'phantom' in product_tmpl.bom_ids.mapped('type'): + raise ValidationError(_("The product type of %s must be 'Consumable' because it has at least one kit-type bill of materials." % product_tmpl.display_name)) + @api.multi def _compute_used_in_bom_count(self): for template in self: diff --git a/addons/mrp/tests/common.py b/addons/mrp/tests/common.py index 47cb622de23d..8e3b8bb56c48 100644 --- a/addons/mrp/tests/common.py +++ b/addons/mrp/tests/common.py @@ -55,9 +55,12 @@ class TestMrpCommon(common2.TestStockCommon): user_group_mrp_manager = cls.env.ref('mrp.group_mrp_manager') # Update demo products - (cls.product_2 | cls.product_3 | cls.product_4 | cls.product_5 | cls.product_6 | cls.product_7 | cls.product_8).write({ + (cls.product_2 | cls.product_3 | cls.product_4 | cls.product_6 | cls.product_7 | cls.product_8).write({ 'type': 'product', }) + cls.product_5.write({ + 'type': 'consu', + }) # User Data: mrp user and mrp manager Users = cls.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nosubscribe': True}) diff --git a/addons/mrp_bom_cost/tests/test_bom_price.py b/addons/mrp_bom_cost/tests/test_bom_price.py index c91f6203bca5..5846b1461efb 100644 --- a/addons/mrp_bom_cost/tests/test_bom_price.py +++ b/addons/mrp_bom_cost/tests/test_bom_price.py @@ -25,6 +25,7 @@ class TestBom(common.TransactionCase): # Products. self.dining_table = self._create_product('Dining Table', 1000) self.table_head = self._create_product('Table Head', 300) + self.table_head.type = 'consu' self.screw = self._create_product('Screw', 10) self.leg = self._create_product('Leg', 25) self.glass = self._create_product('Glass', 100) diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py index 27eebfae4434..ced79634ab8b 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_flow.py +++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py @@ -66,6 +66,9 @@ class TestSaleMrpFlow(common.TransactionCase): product_a = create_product('Product A', self.uom_unit, routes=[route_manufacture, route_mto]) product_c = create_product('Product C', self.uom_kg) product_b = create_product('Product B', self.uom_dozen, routes=[route_manufacture, route_mto]) + product_b.write({ + 'type': 'consu' + }) product_d = create_product('Product D', self.uom_unit, routes=[route_manufacture, route_mto]) # ------------------------------------------------------------------------------------------ @@ -332,7 +335,6 @@ class TestSaleMrpFlow(common.TransactionCase): """ Test delivered quantity on SO based on delivered quantity in pickings.""" # intial so product = self.env.ref('mrp.product_product_table_kit') - self.env['stock.quant']._update_available_quantity(product, self.stock_location, -product.qty_available) product.type = 'consu' product.invoice_policy = 'delivery' # Remove the MTO route as purchase is not installed and since the procurement removal the exception is directly raised @@ -417,7 +419,7 @@ class TestSaleMrpFlow(common.TransactionCase): Product = self.env['product.product'] self.finished_product = Product.create({ 'name': 'Finished product', - 'type': 'product', + 'type': 'consu', 'uom_id': self.uom_unit.id, 'invoice_policy': 'delivery', 'categ_id': self.category.id}) -- GitLab