From 073933ab01975ad84d8c3ff5e668da3eac2b7589 Mon Sep 17 00:00:00 2001 From: "Andrea Grazioso (agr-odoo)" Date: Fri, 2 Oct 2020 10:11:48 +0000 Subject: [PATCH] [FIX] mrp: merge SFP for 2 different MO Set the warehouse with manufacturing 3 steps Create a BoM with a Components that use a Sub BoM Set Both product MTO + Manufacture Create a MO You have only 2 picking for the first MO with the SFP that contains both the finished product and the intermediate component. However they are created and moved at two different times since you could not create the finished product withtout the component, so there is no meaning to put them both in the same picking. It happens because the _assign_picking will merge moves with the same procurement group. In our case, it's the wanted behavior to have the 2 differents SFP linked to the same MO. The idea between the fix is: - We want to have the pick components picking merged. - We want to have the store components split since they come from different MO So we create the different procurement groups during the _run_pull with the picking type of store finished product. This way the pickings won't be merged during _assign_picking We will also propagate those groups to the newly created MO in order to keep the picking type sequence and correct name. FW PORT of https://github.com/odoo/odoo/commit/bcfa03821b92bf52675db0e3b54d3cb19dfe3087 opw-2585525 closes odoo/odoo#85195 X-original-commit: a647e680a196f7e6d9147d51dd80f8c31e0e741b Signed-off-by: Arnold Moyaux Signed-off-by: Adrien Widart --- addons/mrp/models/mrp_production.py | 4 +- addons/mrp/models/stock_rule.py | 32 +++++- .../test_warehouse_multistep_manufacturing.py | 108 ++++++++++++++++++ 3 files changed, 141 insertions(+), 3 deletions(-) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 52254b25b82..2f8243b74bc 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -276,12 +276,12 @@ class MrpProduction(models.Model): @api.depends('procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids') def _compute_mrp_production_child_count(self): for production in self: - production.mrp_production_child_count = len(production.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids) + production.mrp_production_child_count = len(production.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids - production) @api.depends('move_dest_ids.group_id.mrp_production_ids') def _compute_mrp_production_source_count(self): for production in self: - production.mrp_production_source_count = len(production.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.mrp_production_ids) + production.mrp_production_source_count = len(production.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.mrp_production_ids - production) @api.depends('procurement_group_id.mrp_production_ids') def _compute_mrp_production_backorder(self): diff --git a/addons/mrp/models/stock_rule.py b/addons/mrp/models/stock_rule.py index 2998b8c15ec..f6aef3a9456 100644 --- a/addons/mrp/models/stock_rule.py +++ b/addons/mrp/models/stock_rule.py @@ -80,6 +80,26 @@ class StockRule(models.Model): subtype_id=self.env.ref('mail.mt_note').id) return True + @api.model + def _run_pull(self, procurements): + # Override to correctly assign the move generated from the pull + # in its production order (pbm_sam only) + for procurement, rule in procurements: + warehouse_id = rule.warehouse_id + if not warehouse_id: + warehouse_id = rule.location_dest_id.warehouse_id + if rule.picking_type_id == warehouse_id.sam_type_id: + manu_type_id = warehouse_id.manu_type_id + if manu_type_id: + name = manu_type_id.sequence_id.next_by_id() + else: + name = self.env['ir.sequence'].next_by_code('mrp.production') or _('New') + # Create now the procurement group that will be assigned to the new MO + # This ensure that the outgoing move PostProduction -> Stock is linked to its MO + # rather than the original record (MO or SO) + procurement.values['group_id'] = self.env["procurement.group"].create({'name': name}) + return super()._run_pull(procurements) + def _get_custom_move_fields(self): fields = super(StockRule, self)._get_custom_move_fields() fields += ['bom_line_id'] @@ -93,7 +113,7 @@ class StockRule(models.Model): def _prepare_mo_vals(self, product_id, product_qty, product_uom, location_dest_id, name, origin, company_id, values, bom): date_planned = self._get_date_planned(product_id, company_id, values) date_deadline = values.get('date_deadline') or date_planned + relativedelta(days=company_id.manufacturing_lead) + relativedelta(days=product_id.produce_delay) - return { + mo_values = { 'origin': origin, 'product_id': product_id.id, 'product_description_variants': values.get('product_description_variants'), @@ -113,6 +133,16 @@ class StockRule(models.Model): 'move_dest_ids': values.get('move_dest_ids') and [(4, x.id) for x in values['move_dest_ids']] or False, 'user_id': False, } + # Use the procurement group created in _run_pull mrp override + # Preserve the origin from the original stock move, if available + if location_dest_id.warehouse_id.manufacture_steps == 'pbm_sam' and values.get('move_dest_ids') and values.get('group_id') and values['move_dest_ids'][0].origin != values['group_id'].name: + origin = values['move_dest_ids'][0].origin + mo_values.update({ + 'name': values['group_id'].name, + 'procurement_group_id': values['group_id'].id, + 'origin': origin, + }) + return mo_values def _get_date_planned(self, product_id, company_id, values): format_date_planned = fields.Datetime.from_string(values['date_planned']) diff --git a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py index a7ea55dc1b8..af1d644bcfb 100644 --- a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py +++ b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py @@ -473,3 +473,111 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon): mo.action_confirm() self.assertEqual(self.bom.bom_line_ids.product_id + self.product_2, mo.picking_ids.move_ids.product_id) + + def test_manufacturing_complex_product_3_steps(self): + """ Test MO/picking after manufacturing a complex product which uses + manufactured components. Ensure that everything is created and picked + correctly. + """ + + self.warehouse.mto_pull_id.route_id.active = True + # Creating complex product which trigger another manifacture + + routes = self.warehouse.manufacture_pull_id.route_id + self.warehouse.mto_pull_id.route_id + self.complex_product = self.env['product.product'].create({ + 'name': 'Arrow', + 'type': 'product', + 'route_ids': [(6, 0, routes.ids)], + }) + + # Create raw product for manufactured product + self.raw_product_2 = self.env['product.product'].create({ + 'name': 'Raw Iron', + 'type': 'product', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + }) + + self.finished_product.route_ids = [(6, 0, routes.ids)] + + # Create bom for manufactured product + bom_product_form = Form(self.env['mrp.bom']) + bom_product_form.product_id = self.complex_product + bom_product_form.product_tmpl_id = self.complex_product.product_tmpl_id + with bom_product_form.bom_line_ids.new() as line: + line.product_id = self.finished_product + line.product_qty = 1.0 + with bom_product_form.bom_line_ids.new() as line: + line.product_id = self.raw_product_2 + line.product_qty = 1.0 + + self.complex_bom = bom_product_form.save() + + with Form(self.warehouse) as warehouse: + warehouse.manufacture_steps = 'pbm_sam' + + production_form = Form(self.env['mrp.production']) + production_form.product_id = self.complex_product + production_form.picking_type_id = self.warehouse.manu_type_id + production = production_form.save() + production.action_confirm() + + move_raw_ids = production.move_raw_ids + self.assertEqual(len(move_raw_ids), 2) + sfp_move_raw_id, raw_move_raw_id = move_raw_ids + self.assertEqual(sfp_move_raw_id.product_id, self.finished_product) + self.assertEqual(raw_move_raw_id.product_id, self.raw_product_2) + + for move_raw_id in move_raw_ids: + self.assertEqual(move_raw_id.picking_type_id, self.warehouse.manu_type_id) + + pbm_move = move_raw_id.move_orig_ids + self.assertEqual(len(pbm_move), 1) + self.assertEqual(pbm_move.location_id, self.warehouse.lot_stock_id) + self.assertEqual(pbm_move.location_dest_id, self.warehouse.pbm_loc_id) + self.assertEqual(pbm_move.picking_type_id, self.warehouse.pbm_type_id) + + # Check move locations + move_finished_ids = production.move_finished_ids + self.assertEqual(len(move_finished_ids), 1) + self.assertEqual(move_finished_ids.product_id, self.complex_product) + self.assertEqual(move_finished_ids.picking_type_id, self.warehouse.manu_type_id) + sam_move = move_finished_ids.move_dest_ids + self.assertEqual(len(sam_move), 1) + self.assertEqual(sam_move.location_id, self.warehouse.sam_loc_id) + self.assertEqual(sam_move.location_dest_id, self.warehouse.lot_stock_id) + self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id) + self.assertFalse(sam_move.move_dest_ids) + + subproduction = self.env['mrp.production'].browse(production.id+1) + sfp_pickings = subproduction.picking_ids.sorted('id') + + # SFP Production: 2 pickings, 1 group + self.assertEqual(len(sfp_pickings), 2) + self.assertEqual(sfp_pickings.mapped('group_id'), subproduction.procurement_group_id) + + # Move Raw Stick - Stock -> Preprocessing + picking = sfp_pickings[0] + self.assertEqual(len(picking.move_ids), 1) + picking.move_ids[0].product_id = self.raw_product + + # Move SFP - PostProcessing -> Stock + picking = sfp_pickings[1] + self.assertEqual(len(picking.move_ids), 1) + picking.move_ids[0].product_id = self.finished_product + + # Main production 2 pickings, 1 group + pickings = production.picking_ids.sorted('id') + self.assertEqual(len(pickings), 2) + self.assertEqual(pickings.mapped('group_id'), production.procurement_group_id) + + # Move 2 components Stock -> Preprocessing + picking = pickings[0] + self.assertEqual(len(picking.move_ids), 2) + picking.move_ids[0].product_id = self.finished_product + picking.move_ids[1].product_id = self.raw_product_2 + + # Move FP PostProcessing -> Stock + picking = pickings[1] + self.assertEqual(len(picking.move_ids), 1) + picking.product_id = self.complex_product