From abc97d5bfbd767483e9f6abe5bfbc06e657517fc Mon Sep 17 00:00:00 2001 From: Bernat Puig Font Date: Mon, 5 Sep 2022 17:47:59 +0200 Subject: [PATCH] [FIX] mrp_multi_level: Manage Kits in MRP Multi Level --- mrp_multi_level/demo/mrp_bom_demo.xml | 44 +++++++++++++++++ .../demo/product_mrp_area_demo.xml | 16 ++++++ mrp_multi_level/demo/product_product_demo.xml | 37 ++++++++++++++ .../demo/product_supplierinfo_demo.xml | 16 ++++++ mrp_multi_level/models/product_mrp_area.py | 10 +++- mrp_multi_level/tests/common.py | 14 ++++++ mrp_multi_level/tests/test_mrp_multi_level.py | 39 +++++++++++++++ mrp_multi_level/wizards/mrp_multi_level.py | 49 ++++++++++--------- 8 files changed, 200 insertions(+), 25 deletions(-) diff --git a/mrp_multi_level/demo/mrp_bom_demo.xml b/mrp_multi_level/demo/mrp_bom_demo.xml index b71257be6..d2a83a6d3 100644 --- a/mrp_multi_level/demo/mrp_bom_demo.xml +++ b/mrp_multi_level/demo/mrp_bom_demo.xml @@ -44,6 +44,28 @@ + + + + + 5 + + + + 2 + + 5 + + + + + 2 + + 5 + + + 5 + + + + + phantom + 5 + + + + 1 + + 5 + + + + + 3 + + 5 + + diff --git a/mrp_multi_level/demo/product_mrp_area_demo.xml b/mrp_multi_level/demo/product_mrp_area_demo.xml index 64f129f2f..9f6b71efa 100644 --- a/mrp_multi_level/demo/product_mrp_area_demo.xml +++ b/mrp_multi_level/demo/product_mrp_area_demo.xml @@ -8,6 +8,10 @@ + + + + @@ -32,6 +36,10 @@ + + + + @@ -40,6 +48,14 @@ + + + + + + + + diff --git a/mrp_multi_level/demo/product_product_demo.xml b/mrp_multi_level/demo/product_product_demo.xml index 8c0b77248..fdfb3d97c 100644 --- a/mrp_multi_level/demo/product_product_demo.xml +++ b/mrp_multi_level/demo/product_product_demo.xml @@ -20,6 +20,16 @@ + + FP-3 + + product + + + 3 + + + @@ -43,6 +53,15 @@ 3 + + SF-3 + + product + + + 3 + + PP-1 @@ -62,6 +81,24 @@ + + PP-3 + + product + + + + + + + PP-4 + + product + + + + + AV-11 steel diff --git a/mrp_multi_level/demo/product_supplierinfo_demo.xml b/mrp_multi_level/demo/product_supplierinfo_demo.xml index 2a6921240..4821b5464 100644 --- a/mrp_multi_level/demo/product_supplierinfo_demo.xml +++ b/mrp_multi_level/demo/product_supplierinfo_demo.xml @@ -44,4 +44,20 @@ 0 100 + + + + + 2 + 0 + 10 + + + + + + 3 + 0 + 80 + diff --git a/mrp_multi_level/models/product_mrp_area.py b/mrp_multi_level/models/product_mrp_area.py index 02c0c41c6..ec2aaf157 100644 --- a/mrp_multi_level/models/product_mrp_area.py +++ b/mrp_multi_level/models/product_mrp_area.py @@ -87,6 +87,7 @@ class ProductMRPArea(models.Model): selection=[('buy', 'Buy'), ('none', 'Undefined'), ('manufacture', 'Produce'), + ('phantom', 'Kit'), ('pull', 'Pull From'), ('push', 'Push To'), ('pull_push', 'Pull & Push')], @@ -177,7 +178,12 @@ class ProductMRPArea(models.Model): # TODO: better way to get company } rule = group_obj._get_rule(rec.product_id, proc_loc, values) - rec.supply_method = rule.action if rule else 'none' + if rule.action == 'manufacture' and \ + rec.product_id.product_tmpl_id.bom_ids and \ + rec.product_id.product_tmpl_id.bom_ids[0].type == 'phantom': + rec.supply_method = 'phantom' + else: + rec.supply_method = rule.action if rule else 'none' @api.multi @api.depends('supply_method', 'product_id.route_ids', @@ -224,4 +230,4 @@ class ProductMRPArea(models.Model): @api.multi def _to_be_exploded(self): self.ensure_one() - return self.supply_method == 'manufacture' + return self.supply_method in ['manufacture', 'phantom'] diff --git a/mrp_multi_level/tests/common.py b/mrp_multi_level/tests/common.py index 237f67907..98c85e868 100644 --- a/mrp_multi_level/tests/common.py +++ b/mrp_multi_level/tests/common.py @@ -29,10 +29,14 @@ class TestMrpMultiLevelCommon(SavepointCase): cls.fp_1 = cls.env.ref('mrp_multi_level.product_product_fp_1') cls.fp_2 = cls.env.ref('mrp_multi_level.product_product_fp_2') + cls.fp_3 = cls.env.ref('mrp_multi_level.product_product_fp_3') cls.sf_1 = cls.env.ref('mrp_multi_level.product_product_sf_1') cls.sf_2 = cls.env.ref('mrp_multi_level.product_product_sf_2') + cls.sf_3 = cls.env.ref('mrp_multi_level.product_product_sf_3') cls.pp_1 = cls.env.ref('mrp_multi_level.product_product_pp_1') cls.pp_2 = cls.env.ref('mrp_multi_level.product_product_pp_2') + cls.pp_3 = cls.env.ref('mrp_multi_level.product_product_pp_3') + cls.pp_4 = cls.env.ref('mrp_multi_level.product_product_pp_4') cls.product_4b = cls.env.ref('product.product_product_4b') cls.av_11 = cls.env.ref('mrp_multi_level.product_product_av_11') cls.av_12 = cls.env.ref('mrp_multi_level.product_product_av_12') @@ -222,6 +226,16 @@ class TestMrpMultiLevelCommon(SavepointCase): 'location_id': cls.stock_location.id, 'location_dest_id': cls.customer_location.id }), + (0, 0, { + 'name': 'Test move fp-3', + 'product_id': cls.fp_3.id, + 'date_expected': date_move, + 'date': date_move, + 'product_uom': cls.fp_3.uom_id.id, + 'product_uom_qty': 5, + 'location_id': cls.stock_location.id, + 'location_dest_id': cls.customer_location.id + }), (0, 0, { 'name': 'Test move product-4b', 'product_id': cls.product_4b.id, diff --git a/mrp_multi_level/tests/test_mrp_multi_level.py b/mrp_multi_level/tests/test_mrp_multi_level.py index eee8de3a3..543e66d6b 100644 --- a/mrp_multi_level/tests/test_mrp_multi_level.py +++ b/mrp_multi_level/tests/test_mrp_multi_level.py @@ -328,3 +328,42 @@ class TestMrpMultiLevel(TestMrpMultiLevelCommon): ) self.assertEqual(len(inventory), 1) self.assertEqual(inventory.date, date_move.date()) + + def test_15_phantom_comp_planning(self): + """ + Phantom components will not appear in MRP Inventory or Planned Orders. + MRP Parameter will have 'phantom' supply method. + """ + # SF-3 + sf_3_line_1 = self.mrp_inventory_obj.search([ + ('product_mrp_area_id.product_id', '=', self.sf_3.id) + ]) + self.assertEqual(len(sf_3_line_1), 0) + sf_3_planned_order_1 = self.planned_order_obj.search([ + ('product_mrp_area_id.product_id', '=', self.sf_3.id) + ]) + self.assertEqual(len(sf_3_planned_order_1), 0) + sf_3_mrp_parameter = self.product_mrp_area_obj.search([ + ('product_id', '=', self.sf_3.id) + ]) + self.assertEqual(sf_3_mrp_parameter.supply_method, 'phantom') + # PP-3 + pp_3_line_1 = self.mrp_inventory_obj.search([ + ('product_mrp_area_id.product_id', '=', self.pp_3.id), + ]) + self.assertEqual(len(pp_3_line_1), 1) + self.assertEqual(pp_3_line_1.demand_qty, 20.0) + pp_3_planned_orders = self.planned_order_obj.search([ + ('product_mrp_area_id.product_id', '=', self.pp_3.id), + ]) + self.assertEqual(len(pp_3_planned_orders), 2) + # PP-4 + pp_4_line_1 = self.mrp_inventory_obj.search([ + ('product_mrp_area_id.product_id', '=', self.pp_4.id), + ]) + self.assertEqual(len(pp_4_line_1), 1) + self.assertEqual(pp_4_line_1.demand_qty, 30.0) + pp_4_planned_orders = self.planned_order_obj.search([ + ('product_mrp_area_id.product_id', '=', self.pp_4.id), + ]) + self.assertEqual(len(pp_4_planned_orders), 1) diff --git a/mrp_multi_level/wizards/mrp_multi_level.py b/mrp_multi_level/wizards/mrp_multi_level.py index 485e26bf5..03b4a03bd 100644 --- a/mrp_multi_level/wizards/mrp_multi_level.py +++ b/mrp_multi_level/wizards/mrp_multi_level.py @@ -7,6 +7,7 @@ from odoo import api, fields, models, exceptions, _ from datetime import date, timedelta import logging + logger = logging.getLogger(__name__) @@ -97,8 +98,8 @@ class MultiLevelMrp(models.TransientModel): @api.model def _prepare_planned_order_data( - self, product_mrp_area, qty, mrp_date_supply, - mrp_action_date, name + self, product_mrp_area, qty, mrp_date_supply, + mrp_action_date, name ): return { 'product_mrp_area_id': product_mrp_area.id, @@ -169,7 +170,7 @@ class MultiLevelMrp(models.TransientModel): @api.model def explode_action( - self, product_mrp_area_id, mrp_action_date, name, qty, action + self, product_mrp_area_id, mrp_action_date, name, qty, action ): """Explode requirements.""" mrp_date_demand = mrp_action_date @@ -189,7 +190,7 @@ class MultiLevelMrp(models.TransientModel): bomline.product_id.type != 'product': continue if self.with_context(mrp_explosion=True)._exclude_from_mrp( - bomline.product_id, + bomline.product_id, product_mrp_area_id.mrp_area_id): # Stop explosion. continue @@ -211,7 +212,7 @@ class MultiLevelMrp(models.TransientModel): @api.model def create_action( - self, product_mrp_area_id, mrp_date, mrp_qty, name, values=None, + self, product_mrp_area_id, mrp_date, mrp_qty, name, values=None, ): if not values: values = {} @@ -225,12 +226,12 @@ class MultiLevelMrp(models.TransientModel): @api.model def create_planned_order( - self, product_mrp_area_id, mrp_qty, - name, mrp_date_supply, mrp_action_date, values=None, + self, product_mrp_area_id, mrp_qty, + name, mrp_date_supply, mrp_action_date, values=None, ): self = self.with_context(auditlog_disabled=True) if self._exclude_from_mrp( - product_mrp_area_id.product_id, + product_mrp_area_id.product_id, product_mrp_area_id.mrp_area_id): values['qty_ordered'] = 0.0 return values @@ -243,7 +244,10 @@ class MultiLevelMrp(models.TransientModel): order_data = self._prepare_planned_order_data( product_mrp_area_id, qty, mrp_date_supply, mrp_action_date, name) - planned_order = self.env['mrp.planned.order'].create(order_data) + # Do not create planned order for products that are Kits + planned_order = False + if not product_mrp_area_id.supply_method == 'phantom': + planned_order = self.env['mrp.planned.order'].create(order_data) qty_ordered = qty_ordered + qty if product_mrp_area_id._to_be_exploded(): @@ -297,7 +301,7 @@ class MultiLevelMrp(models.TransientModel): products = bom_lines.mapped('product_id') products.write({'llc': llc}) counter = self.env['product.product'].search_count([('llc', '=', - llc)]) + llc)]) log_msg = 'Low level code %s finished - Nbr. products: %s' % ( llc, counter) logger.info(log_msg) @@ -383,8 +387,7 @@ class MultiLevelMrp(models.TransientModel): return True @api.model - def _prepare_mrp_move_data_from_purchase_order( - self, poline, product_mrp_area): + def _prepare_mrp_move_data_from_purchase_order(self, poline, product_mrp_area): mrp_date = date.today() if fields.Date.from_string(poline.date_planned) > date.today(): mrp_date = fields.Date.from_string(poline.date_planned) @@ -484,12 +487,12 @@ class MultiLevelMrp(models.TransientModel): if self._exclude_move(move): continue if last_date and ( - fields.Date.from_string(move.mrp_date) - >= last_date + timedelta(days=grouping_delta)) and ( - (onhand + last_qty + move.mrp_qty) - < product_mrp_area.mrp_minimum_stock - or (onhand + last_qty) - < product_mrp_area.mrp_minimum_stock): + fields.Date.from_string(move.mrp_date) + >= last_date + timedelta(days=grouping_delta)) and ( + (onhand + last_qty + move.mrp_qty) + < product_mrp_area.mrp_minimum_stock + or (onhand + last_qty) + < product_mrp_area.mrp_minimum_stock): name = 'Grouped Demand for %d Days' % grouping_delta qtytoorder = product_mrp_area.mrp_minimum_stock - \ onhand - last_qty @@ -504,8 +507,8 @@ class MultiLevelMrp(models.TransientModel): last_qty = 0.00 nbr_create += 1 if (onhand + last_qty + move.mrp_qty) < \ - product_mrp_area.mrp_minimum_stock or \ - (onhand + last_qty) < \ + product_mrp_area.mrp_minimum_stock or \ + (onhand + last_qty) < \ product_mrp_area.mrp_minimum_stock: if not last_date or last_qty == 0.0: last_date = fields.Date.from_string(move.mrp_date) @@ -720,9 +723,9 @@ class MultiLevelMrp(models.TransientModel): for product_mrp_area in product_mrp_area_ids: # Build the time-phased inventory - if self._exclude_from_mrp( - product_mrp_area.product_id, - product_mrp_area.mrp_area_id): + if self._exclude_from_mrp(product_mrp_area.product_id, + product_mrp_area.mrp_area_id) or \ + product_mrp_area.supply_method == 'phantom': continue self._init_mrp_inventory(product_mrp_area) logger.info('End MRP final process')