[IMP] mrp: update BoM button

The "Update BoM" button action was moved from `mrp_plm` to `mrp` so it
can be displayed if the BoM was updated now. This action can be called
for confirmed MO now.
Each time a change is done on a BoM, all the draft and confirmed MOs
using this BoM will be notified the BoM changed, thanks to a new boolean
field, `bom_has_been_updated`.

An exception however: if the MO is confirmed and the BoM's product was
changed, the MO shouldn't have the "Update BoM" button displayed.
Otherwise, it would change the finished product of a confirmed MO.

Also, it's important to notice than due to a limitation, only changes
done at the BoM level will mark it as modified. That means that
modifying subrecords in their own views (for example, adding steps in a
BoM's operation) will not mark the BoM as updated !

See odoo/enterprise#35969
task-3089368

Part-of: odoo/odoo#110227
This commit is contained in:
svs-odoo
2023-06-19 16:39:34 +02:00
parent e831903612
commit d7392829c7
5 changed files with 433 additions and 64 deletions
+21 -1
View File
@@ -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:
+124 -63
View File
@@ -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
+282
View File
@@ -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},
])
@@ -241,6 +241,7 @@
</div>
<label for="bom_id" name="bom_label"/>
<div class='o_row' name="bom_div">
<field name='is_outdated_bom' invisible='1'/>
<field name="bom_id"
context="{'default_product_tmpl_id': product_tmpl_id}"
attrs="{'readonly': [('state', '!=', 'draft')], 'invisible': [('state', '!=', 'draft'), ('bom_id', '=', False)]}"/>
@@ -249,6 +250,9 @@
attrs="{'invisible': ['|', '|', ('bom_id', '!=', False), ('product_id', '=', False), ('move_raw_ids', '=', [])]}">
<span class="ms-2">Generate BOM</span>
</button>
<button name="action_update_bom" string="Update BoM" type="object"
title="Note that another version of this BOM is available." class="text-danger"
attrs="{'invisible': ['|', ('is_outdated_bom', '=', False), ('state', 'not in', ['draft', 'confirmed'])]}"/>
</div>
<label for="lot_producing_id" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('product_tracking', 'in', ('none', False))]}"/>
<div class="o_row" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('product_tracking', 'in', ('none', False))]}">
@@ -13,6 +13,8 @@
<xpath expr="//div[@name='button_box']" position="replace"/>
<xpath expr="//div[hasclass('oe_title')]" position="replace"/>
<xpath expr="//button[@name='action_generate_bom']" position="replace"/>
<xpath expr="//field[@name='is_outdated_bom']" position="replace"/>
<xpath expr="//button[@name='action_update_bom']" position="replace"/>
<xpath expr="//group[@name='group_extra_info']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>