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