[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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user