diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index c0271d2a64c..dbe1d2b14b1 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -3,7 +3,7 @@ from odoo import api, fields, models, _, Command from odoo.exceptions import UserError, ValidationError -from odoo.osv.expression import AND +from odoo.osv.expression import AND, OR from odoo.tools import float_round from odoo.tools.misc import clean_context @@ -198,6 +198,26 @@ class MrpBom(models.Model): production._link_bom(res[0]) return res + def write(self, vals): + res = super().write(vals) + relevant_fields = ['bom_line_ids', 'byproduct_ids', 'product_tmpl_id', 'product_id', 'product_qty', 'operation_ids'] + if any(field_name in vals for field_name in relevant_fields): + # Searches for MOs using these BoMs to notify them that their BoM has been updated. + list_of_domain_by_bom = [] + for bom in self: + domain_by_products = [('product_id', 'in', bom.product_tmpl_id.product_variant_ids.ids)] + if bom.product_id: + domain_by_products = [('product_id', '=', bom.product_id.id)] + domain_for_confirmed_mo = AND([[('state', '=', 'confirmed')], domain_by_products]) + # Avoid confirmed MOs if the BoM's product was changed. + domain_by_states = OR([[('state', '=', 'draft')], domain_for_confirmed_mo]) + list_of_domain_by_bom.append(AND([[('bom_id', '=', bom.id)], domain_by_states])) + domain = OR(list_of_domain_by_bom) + productions = self.env['mrp.production'].search(domain) + if productions: + productions.is_outdated_bom = True + return res + def copy(self, default=None): res = super().copy(default) if self.operation_ids: diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index b5ea7eb4732..c56a4368f94 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -252,6 +252,7 @@ class MrpProduction(models.Model): allow_workorder_dependencies = fields.Boolean('Allow Work Order Dependencies') show_produce = fields.Boolean(compute='_compute_show_produce', help='Technical field to check if produce button can be shown') show_produce_all = fields.Boolean(compute='_compute_show_produce', help='Technical field to check if produce all button can be shown') + is_outdated_bom = fields.Boolean("Outdated BoM", help="The BoM has been updated since creation of the MO") _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'), @@ -956,6 +957,12 @@ class MrpProduction(models.Model): action['context']['warehouse'] = warehouse.id return action + def action_update_bom(self): + for production in self: + if production.bom_id: + production._link_bom(production.bom_id) + self.is_outdated_bom = False + def _get_bom_values(self, ratio=1): """ Returns the BoM lines, by-products and operations values needed to create a new BoM from this Manufacturing Order. @@ -963,19 +970,37 @@ class MrpProduction(models.Model): :rtype: tuple(dict, dict, dict) """ self.ensure_one() + + def get_uom_and_quantity(move): + # Use the BoM line/by-product's UoM if the move is linked to one of them. + target_uom = (move.bom_line_id or move.byproduct_id).product_uom_id or move.product_uom + # In order to be able to multiply the move quantity by the ratio, we + # have to be sure they both express in the same UoM. + qty = move.quantity_done or move.product_uom_qty + qty = move.product_uom._compute_quantity(qty * ratio, target_uom) + return (target_uom, qty) + # BoM lines values. - bom_lines_values = [Command.create({ - 'product_id': move_raw.product_id.id, - 'product_qty': (move_raw.quantity_done or move_raw.product_uom_qty) * ratio, - 'product_uom_id': move_raw.product_uom.id, - }) for move_raw in self.move_raw_ids] + bom_lines_values = [] + for move_raw in self.move_raw_ids: + uom, qty = get_uom_and_quantity(move_raw) + bom_line_vals = { + 'product_id': move_raw.product_id.id, + 'product_qty': qty, + 'product_uom_id': uom.id, + } + bom_lines_values.append(Command.create(bom_line_vals)) # By-Product lines values. - byproduct_values = [Command.create({ - 'cost_share': move_byproduct.cost_share, - 'product_id': move_byproduct.product_id.id, - 'product_qty': (move_byproduct.quantity_done or move_byproduct.product_uom_qty) * ratio, - 'product_uom_id': move_byproduct.product_uom.id, - }) for move_byproduct in self.move_byproduct_ids] + byproduct_values = [] + for move_byproduct in self.move_byproduct_ids: + uom, qty = get_uom_and_quantity(move_byproduct) + bom_byproduct_vals = { + 'cost_share': move_byproduct.cost_share, + 'product_id': move_byproduct.product_id.id, + 'product_qty': qty, + 'product_uom_id': uom.id, + } + byproduct_values.append(Command.create(bom_byproduct_vals)) # Operations values. operations_values = [Command.create(wo._get_operation_values()) for wo in self.workorder_ids] return (bom_lines_values, byproduct_values, operations_values) @@ -2145,7 +2170,6 @@ class MrpProduction(models.Model): def _link_bom(self, bom): """ Links the given BoM to the MO. Assigns BoM's lines, by-products and operations to the corresponding MO's components, by-products and workorders. - This method assumes the production isn't linked to any BoM yet. """ self.ensure_one() moves_to_unlink = self.env['stock.move'] @@ -2176,62 +2200,24 @@ class MrpProduction(models.Model): any(att_val.id in product_attribute_ids for att_val in record.bom_product_template_attribute_value_ids) ratio = self._get_ratio_between_mo_and_bom_quantities(bom) - bom_lines = list(bom.bom_line_ids.filtered(filter_by_attributes)) - bom_byproducts = list(bom.byproduct_ids.filtered(filter_by_attributes)) - operations = list(bom.operation_ids.filtered(filter_by_attributes)) - - # Compares the BoM's lines to the MO's components. - for move_raw in self.move_raw_ids: - for i, bom_line in enumerate(bom_lines): - if bom_line.product_id == move_raw.product_id: - move_raw.bom_line_id = bom_line - move_raw.product_uom_qty = bom_line.product_qty / ratio - move_raw.product_uom = bom_line.product_uom_id - del bom_lines[i] - break - # Creates a raw moves for each remaining BoM's lines. - raw_moves_values = [] - for bom_line in bom_lines: - raw_move_vals = self._get_move_raw_values( - bom_line.product_id, - bom_line.product_qty / ratio, - bom_line.product_uom_id, - bom_line=bom_line - ) - raw_moves_values.append(raw_move_vals) - self.move_raw_ids += self.env['stock.move'].create(raw_moves_values) - - # Compares the BoM's and the MO's by-products. - for move_byproduct in self.move_byproduct_ids: - for i, bom_byproduct in enumerate(bom_byproducts): - if bom_byproduct.product_id == move_byproduct.product_id: - move_byproduct.byproduct_id = bom_byproduct - move_byproduct.cost_share = bom_byproduct.cost_share - move_byproduct.product_uom_qty = bom_byproduct.product_qty / ratio - move_byproduct.product_uom = bom_byproduct.product_uom_id - del bom_byproducts[i] - break - # For each remaining BoM's by-product, creates a move finished. - byproduct_values = [] - for bom_byproduct in bom_byproducts: - qty = bom_byproduct.product_qty / ratio - move_byproduct_vals = self._get_move_finished_values( - bom_byproduct.product_id.id, qty, bom_byproduct.product_uom_id.id, - bom_byproduct.operation_id.id, bom_byproduct.id, bom_byproduct.cost_share - ) - byproduct_values.append(move_byproduct_vals) - self.move_finished_ids += self.env['stock.move'].create(byproduct_values) + bom_lines_by_id = {(bom_line.id, bom_line.product_id.id): bom_line for bom_line in bom.bom_line_ids.filtered(filter_by_attributes)} + bom_byproducts_by_id = {byproduct.id: byproduct for byproduct in bom.byproduct_ids.filtered(filter_by_attributes)} + operations_by_id = {operation.id: operation for operation in bom.operation_ids.filtered(filter_by_attributes)} # Compares the BoM's operations to the MO's workoders. for workorder in self.workorder_ids: - for i, operation in enumerate(operations): - if operation_key_values(operation) == operation_key_values(workorder): - workorder.operation_id = operation - del operations[i] - break + operation = operations_by_id.pop(workorder.operation_id.id, False) + if not operation: + for operation_id in operations_by_id: + _operation = operations_by_id[operation_id] + if operation_key_values(_operation) == operation_key_values(workorder): + operation = operations_by_id.pop(operation_id) + break + if operation and workorder.operation_id != operation: + workorder.operation_id = operation # Creates a workorder for each remaining operation. workorders_values = [] - for operation in operations: + for operation in operations_by_id.values(): workorder_vals = { 'name': operation.name, 'operation_id': operation.id, @@ -2243,6 +2229,81 @@ class MrpProduction(models.Model): workorders_values.append(workorder_vals) self.workorder_ids += self.env['mrp.workorder'].create(workorders_values) + # Compares the BoM's lines to the MO's components. + for move_raw in self.move_raw_ids: + bom_line = bom_lines_by_id.pop((move_raw.bom_line_id.id, move_raw.product_id.id), False) + # If the move isn't already linked to a BoM lines, search for a compatible line. + if not bom_line: + for _bom_line in bom_lines_by_id.values(): + if move_raw.product_id == _bom_line.product_id: + bom_line = bom_lines_by_id.pop((_bom_line.id, move_raw.product_id.id)) + if bom_line: + break + move_raw_qty = bom_line and move_raw.product_uom._compute_quantity( + move_raw.product_uom_qty * ratio, bom_line.product_uom_id + ) + if bom_line and ( + not move_raw.bom_line_id or + move_raw.bom_line_id.bom_id != bom or + move_raw.operation_id != bom_line.operation_id or + bom_line.product_qty != move_raw_qty + ): + move_raw.bom_line_id = bom_line + move_raw.product_id = bom_line.product_id + move_raw.product_uom_qty = bom_line.product_qty / ratio + move_raw.product_uom = bom_line.product_uom_id + if move_raw.operation_id != bom_line.operation_id: + move_raw.operation_id = bom_line.operation_id + move_raw.workorder_id = self.workorder_ids.filtered(lambda wo: wo.operation_id == move_raw.operation_id) + elif not bom_line: + moves_to_unlink |= move_raw + # Creates a raw moves for each remaining BoM's lines. + raw_moves_values = [] + for bom_line in bom_lines_by_id.values(): + raw_move_vals = self._get_move_raw_values( + bom_line.product_id, + bom_line.product_qty / ratio, + bom_line.product_uom_id, + bom_line=bom_line + ) + raw_moves_values.append(raw_move_vals) + self.env['stock.move'].create(raw_moves_values) + + # Compares the BoM's and the MO's by-products. + for move_byproduct in self.move_byproduct_ids: + bom_byproduct = bom_byproducts_by_id.pop(move_byproduct.byproduct_id.id, False) + if not bom_byproduct: + for _bom_byproduct in bom_byproducts_by_id.values(): + if move_byproduct.product_id == _bom_byproduct.product_id: + bom_byproduct = bom_byproducts_by_id.pop(_bom_byproduct.id) + break + move_byproduct_qty = bom_byproduct and move_byproduct.product_uom._compute_quantity( + move_byproduct.product_uom_qty * ratio, bom_byproduct.product_uom_id + ) + if bom_byproduct and ( + not move_byproduct.byproduct_id or + bom_byproduct.product_id != move_byproduct.product_id or + bom_byproduct.product_qty != move_byproduct_qty + ): + move_byproduct.byproduct_id = bom_byproduct + move_byproduct.cost_share = bom_byproduct.cost_share + move_byproduct.product_uom_qty = bom_byproduct.product_qty / ratio + move_byproduct.product_uom = bom_byproduct.product_uom_id + elif not bom_byproduct: + moves_to_unlink |= move_byproduct + # For each remaining BoM's by-product, creates a move finished. + byproduct_values = [] + for bom_byproduct in bom_byproducts_by_id.values(): + qty = bom_byproduct.product_qty * ratio + move_byproduct_vals = self._get_move_finished_values( + bom_byproduct.product_id.id, qty, bom_byproduct.product_uom_id.id, + bom_byproduct.operation_id.id, bom_byproduct.id, bom_byproduct.cost_share + ) + byproduct_values.append(move_byproduct_vals) + self.move_finished_ids += self.env['stock.move'].create(byproduct_values) + + moves_to_unlink._action_cancel() + moves_to_unlink.unlink() self.bom_id = bom @api.model diff --git a/addons/mrp/tests/test_bom.py b/addons/mrp/tests/test_bom.py index 725c581a5c8..57802339d5d 100644 --- a/addons/mrp/tests/test_bom.py +++ b/addons/mrp/tests/test_bom.py @@ -1342,3 +1342,285 @@ class TestBoM(TestMrpCommon): self.assertEqual(p1.qty_available, 5.0) self.assertEqual(p2.qty_available, 10.0) self.assertEqual(p3.qty_available, 10.0) + + def test_bom_updates_mo(self): + """ Creates a Manufacturing Order using a BoM, then modifies the BoM. + Checks the BoM will be marked as updated in the right situation, and checks the "Update BoM" + action update the MO accordingly to the changes done in the BoM. + """ + # Creates a BoM. + common_vals = {'type': "product"} + finished_product = self.env['product.product'].create(dict(common_vals, name="Monster in Jar")) + component_1 = self.env['product.product'].create(dict(common_vals, name="Monster")) + component_2 = self.env['product.product'].create(dict(common_vals, name="Jar")) + component_3 = self.env['product.product'].create(dict(common_vals, name="Bottle")) + by_product = self.env['product.product'].create(dict(common_vals, name="Monster's Tears")) + bom = self.env['mrp.bom'].create({ + 'product_tmpl_id': finished_product.product_tmpl_id.id, + 'product_qty': 1.0, + 'bom_line_ids': [Command.create({'product_id': p.id, 'product_qty': 1}) for p in [component_1, component_2]], + }) + + # Creates a MO. + mo_form = Form(self.env['mrp.production']) + mo_form.bom_id = bom + mo_1 = mo_form.save() + self.assertEqual(mo_1.move_raw_ids[0].product_uom_qty, 1) + self.assertEqual(mo_1.is_outdated_bom, False) + # Update MO's component quantity. + mo_form = Form(mo_1) + with mo_form.move_raw_ids.edit(0) as raw_move: + raw_move.product_uom_qty = 123 + mo_1 = mo_form.save() + self.assertEqual(mo_1.move_raw_ids[0].product_uom_qty, 123) + self.assertEqual(mo_1.is_outdated_bom, False, + "Making a modification in the MO shouldn't mark the BoM as updated") + + # Now, adds an operation and a by-product in the BoM. + bom.byproduct_ids = [Command.create({'product_id': by_product.id, 'product_qty': 2})] + bom_byproduct = bom.byproduct_ids + bom.operation_ids = [Command.create({ + 'name': "Gently insert the Monster in the Jar", + 'workcenter_id': self.workcenter_1.id, + })] + operation = bom.operation_ids + + self.assertEqual(mo_1.is_outdated_bom, True, + "By-Product and Operation were added to the BoM, it should be marked as updated") + # Call "Update BoM" action, it should reset the MO as defined by the BoM. + mo_1.action_update_bom() + self.assertEqual(mo_1.is_outdated_bom, False, + "After 'Update BoM' action, MO's BoM should no longer be marked as updated") + self.assertEqual(mo_1.workorder_ids.operation_id.id, operation.id) + self.assertEqual(mo_1.move_byproduct_ids.byproduct_id.id, bom_byproduct.id) + + # Now, checks the update works also with confirmed MO. + mo_1.action_confirm() + self.assertEqual(mo_1.is_outdated_bom, False, + "After 'Update BoM' action, MO's BoM should no longer be marked as updated") + # Updates the BoM again (increase first component quantity). + bom_form = Form(bom) + with bom_form.bom_line_ids.edit(0) as bom_line: + bom_line.product_qty += 1 + bom = bom_form.save() + self.assertEqual(mo_1.is_outdated_bom, True, + "BoM line's quantity was update, the BoM should be marked as updated") + mo_1.action_update_bom() + self.assertEqual(mo_1.is_outdated_bom, False, + "After 'Update BoM' action, MO's BoM should no longer be marked as updated") + self.assertRecordValues(mo_1.move_raw_ids, [ + {'bom_line_id': bom.bom_line_ids[0].id, 'product_uom_qty': bom.bom_line_ids[0].product_qty}, + {'bom_line_id': bom.bom_line_ids[1].id, 'product_uom_qty': bom.bom_line_ids[1].product_qty}, + ]) + # Updates the BoM again (replace a component by another product). + bom_form = Form(bom) + with bom_form.bom_line_ids.edit(1) as bom_line: + bom_line.product_id = component_3 + bom = bom_form.save() + self.assertEqual(mo_1.is_outdated_bom, True, + "A component was changed, BoM should be marked as updated") + mo_1.action_update_bom() + self.assertEqual(mo_1.is_outdated_bom, False, + "There should be no difference between the MO and BoM") + self.assertRecordValues(mo_1.move_raw_ids, [ + {'bom_line_id': bom.bom_line_ids[0].id, 'product_id': component_1.id}, + {'bom_line_id': bom.bom_line_ids[1].id, 'product_id': component_3.id}, + ]) + + # Updates the BoM again (delete a BoM line). + bom_form = Form(bom) + bom_form.bom_line_ids.remove(1) + bom = bom_form.save() + self.assertEqual(mo_1.is_outdated_bom, True, + "A component was changed, BoM should be marked as updated") + mo_1.action_update_bom() + self.assertEqual(mo_1.is_outdated_bom, False, + "There should be no difference between the MO and BoM") + self.assertEqual(len(mo_1.move_raw_ids), 1) + + # Updates the BoM by multiplying all its quantities by 3. + bom.product_qty *= 3 + bom.bom_line_ids[0].product_qty *= 3 + bom.byproduct_ids.product_qty *= 3 + self.assertEqual(mo_1.is_outdated_bom, True, + "Even if the BoM's changes don't imply actual changes for the MO, it should be marked as updated.") + + def test_bom_updates_mo_with_different_uom(self): + """ Creates a Manufacturing Order using a BoM and produces 1 dozen of the finished product, + then modifies the BoM's component's quantity and update the MO. + Checks the MO's raw moves' quantities are correctly updated. + """ + self.env.user.groups_id += self.env.ref('uom.group_uom') + uom_unit = self.env.ref('uom.product_uom_unit') + uom_dozen = self.env.ref('uom.product_uom_dozen') + # Creates a BoM. + common_vals = {'type': "product"} + finished_product = self.env['product.product'].create(dict(common_vals, name="Monster in Jar")) + component_1 = self.env['product.product'].create(dict(common_vals, name="Monster")) + component_2 = self.env['product.product'].create(dict(common_vals, name="Jar")) + bom = self.env['mrp.bom'].create({ + 'product_tmpl_id': finished_product.product_tmpl_id.id, + 'product_qty': 1.0, + 'bom_line_ids': [Command.create({'product_id': p.id, 'product_qty': 1}) for p in [component_1, component_2]], + }) + + # Creates a MO. + mo_form = Form(self.env['mrp.production']) + mo_form.bom_id = bom + mo_form.product_uom_id = uom_dozen + mo_1 = mo_form.save() + mo_1.action_confirm() + self.assertRecordValues(mo_1.move_raw_ids, [{ + 'product_id': component_1.id, 'product_uom_qty': 12, 'product_uom': uom_unit.id, + }, { + 'product_id': component_2.id, 'product_uom_qty': 12, 'product_uom': uom_unit.id, + }]) + + # Updates the BoM by set the first BoM line's quantity to 2. + bom_form = Form(bom) + with bom_form.bom_line_ids.edit(0) as bom_line: + bom_line.product_qty = 2 + bom = bom_form.save() + self.assertEqual(mo_1.is_outdated_bom, True) + + # Call "Update BoM" action, it should update the MO raw moves' quantity accordingly. + mo_1.action_update_bom() + self.assertEqual(mo_1.is_outdated_bom, False) + self.assertRecordValues(mo_1.move_raw_ids, [{ + 'product_id': component_1.id, 'product_uom_qty': 24, 'product_uom': uom_unit.id, + }, { + 'product_id': component_2.id, 'product_uom_qty': 12, 'product_uom': uom_unit.id, + }]) + + # Do the same but while changing the raw moves' UoM too. + mo_form = Form(self.env['mrp.production']) + mo_form.bom_id = bom + mo_form.product_uom_id = uom_dozen + with mo_form.move_raw_ids.edit(0) as move_raw: + move_raw.product_uom_qty = 2 + move_raw.product_uom = uom_dozen + with mo_form.move_raw_ids.edit(1) as move_raw: + move_raw.product_uom_qty = 1 + move_raw.product_uom = uom_dozen + mo_2 = mo_form.save() + mo_2.action_confirm() + self.assertRecordValues(mo_2.move_raw_ids, [ + {'product_id': component_1.id, 'product_uom_qty': 2, 'product_uom': uom_dozen.id}, + {'product_id': component_2.id, 'product_uom_qty': 1, 'product_uom': uom_dozen.id} + ]) + + # Updates the BoM by set the second BoM line's quantity to 2. + bom_form = Form(bom) + with bom_form.bom_line_ids.edit(1) as bom_line: + bom_line.product_qty = 2 + bom = bom_form.save() + self.assertEqual(mo_2.is_outdated_bom, True) + + # Call "Update BoM" action, it should update the MO raw moves' quantity accordingly. + # import pudb; pudb.set_trace() + mo_2.action_update_bom() + self.assertEqual(mo_2.is_outdated_bom, False) + # As there is a difference for the second component, the quantity should + # be updated (resets the UoM from the BoM line's one). + self.assertRecordValues(mo_2.move_raw_ids, [ + {'product_id': component_1.id, 'product_uom_qty': 2, 'product_uom': uom_dozen.id}, + {'product_id': component_2.id, 'product_uom_qty': 24, 'product_uom': uom_unit.id} + ]) + + def test_bom_updates_mo_after_updating_operations(self): + """ Creates a Manufacturing Order using a BoM with operations and checks the raw moves are + correctly linked to the right operation/workorder. Then modifies the BoM and updates the MO + and checks the moves' operation/workorder are correctly updated too. + """ + # Creates a BoM. + common_vals = {'type': "product"} + finished_product = self.env['product.product'].create(dict(common_vals, name="Monster in Jar")) + component_1 = self.env['product.product'].create(dict(common_vals, name="Monster")) + component_2 = self.env['product.product'].create(dict(common_vals, name="Jar")) + bom = self.env['mrp.bom'].create({ + 'product_tmpl_id': finished_product.product_tmpl_id.id, + 'product_qty': 1.0, + 'bom_line_ids': [Command.create({'product_id': p.id, 'product_qty': 1}) for p in [component_1, component_2]], + 'operation_ids': [ + Command.create({'name': 'OP1', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 10, 'sequence': 1}), + Command.create({'name': 'OP2', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 2}), + ], + }) + bom.bom_line_ids[0].operation_id = bom.operation_ids[0].id + # Creates a MO and confirms it. + mo_form = Form(self.env['mrp.production']) + mo_form.bom_id = bom + mo_1 = mo_form.save() + mo_1.action_confirm() + self.assertRecordValues(mo_1.move_raw_ids, [ + {'operation_id': bom.operation_ids[0].id, 'workorder_id': mo_1.workorder_ids[0].id}, + {'operation_id': False, 'workorder_id': mo_1.workorder_ids[1].id}, + ]) + + # Adds a new operation and links BoM's lines to other operations. + self.env['mrp.routing.workcenter'].create({ + 'name': 'OP3', 'bom_id': bom.id, 'workcenter_id': self.workcenter_1.id + }) + bom_form = Form(bom) + with bom_form.bom_line_ids.edit(0) as bom_line: + bom_line.operation_id = bom.operation_ids[2] + with bom_form.bom_line_ids.edit(1) as bom_line: + bom_line.operation_id = bom.operation_ids[0] + bom = bom_form.save() + self.assertEqual(mo_1.is_outdated_bom, True) + # Updates the MO's BoM, its raw moves' operations/workorders should be updated too. + mo_1.action_update_bom() + self.assertEqual(mo_1.is_outdated_bom, False) + self.assertRecordValues(mo_1.move_raw_ids, [ + {'operation_id': bom.operation_ids[2].id, 'workorder_id': mo_1.workorder_ids[2].id}, + {'operation_id': bom.operation_ids[0].id, 'workorder_id': mo_1.workorder_ids[0].id}, + ]) + + # Modifies the BoM's operations again. + bom_form = Form(bom) + with bom_form.bom_line_ids.edit(0) as bom_line: + bom_line.operation_id = self.env['mrp.routing.workcenter'] + with bom_form.bom_line_ids.edit(1) as bom_line: + bom_line.operation_id = self.env['mrp.routing.workcenter'] + bom = bom_form.save() + self.assertEqual(mo_1.is_outdated_bom, True) + # Updates the MO's BoM, its raw moves' operations/workorders should be correctly linked. + mo_1.action_update_bom() + self.assertEqual(mo_1.is_outdated_bom, False) + self.assertRecordValues(mo_1.move_raw_ids, [ + {'operation_id': False, 'workorder_id': mo_1.workorder_ids[2].id}, + {'operation_id': False, 'workorder_id': mo_1.workorder_ids[2].id}, + ]) + + def test_bom_updates_mo_with_pre_prod_picking(self): + """ With a 2-steps config, creates a MO, then updates its BoM by + replacing one of its BoM line's product. Updates the MO and checks a new + move for this product was created in the MO's picking. + """ + self.env.user.groups_id += self.env.ref('stock.group_adv_location') + self.warehouse_1.manufacture_steps = 'pbm' + + # Creates a MO. + mo_form = Form(self.env['mrp.production']) + mo_form.bom_id = self.bom_1 + mo_form.picking_type_id = self.warehouse_1.manu_type_id + mo_1 = mo_form.save() + mo_1.action_confirm() + picking = mo_1.picking_ids + self.assertRecordValues(picking.move_ids, [ + {'product_id': self.product_2.id, 'product_uom_qty': 2}, + {'product_id': self.product_1.id, 'product_uom_qty': 4}, + ]) + + # Updates the BoM, then updates the MO. + bom_form = Form(self.bom_1) + with bom_form.bom_line_ids.edit(0) as bom_line: + bom_line.product_id = self.product_3 + bom_form.save() + self.assertEqual(mo_1.is_outdated_bom, True) + mo_1.action_update_bom() + self.assertRecordValues(picking.move_ids, [ + {'product_id': self.product_2.id, 'product_uom_qty': 2}, # Ideally, this move should have been deleted but this isn't handled for now. + {'product_id': self.product_1.id, 'product_uom_qty': 4}, + {'product_id': self.product_3.id, 'product_uom_qty': 2}, + ]) diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 20eb49ad41b..edc05f46ae5 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -241,6 +241,7 @@