[FIX] mrp: correctly copy non-backordered MO

Steps to reproduce:
Step 1: make a MO and create less than the quantity to produce
Step 2: Mark As Done (with no backorder)
Step 3: duplicate the MO and try to change the quantity to produce

Expected result: qty to produce changes as expected
Actual result: server error

Issue is due to the copied MO's `move_finished_ids` including a copy
of the cancelled finished move (i.e. the qty not backordered) so there
were 2 `move_finished_ids` for the product to produce. This resulted in
an access error since the onchange to update the `move_finished_ids`
only expects 1 move for the product to produce and results in a
singleton error.

Note we copy cancelled move_raw_ids because otherwise we wouldn't be
able to duplicate cancelled MOs without losing all of its components.

Issue 2 of Task: 2618962

closes odoo/odoo#75073

X-original-commit: 456c337534427db1296030b004ae061b47d8db79
Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
Signed-off-by: Tiffany Chang <tic@odoo.com>
This commit is contained in:
Tiffany Chang (tic)
2021-08-13 10:04:21 +00:00
parent 94cb5b6e8f
commit 5cf8fb31bb
2 changed files with 22 additions and 4 deletions
+12 -2
View File
@@ -190,11 +190,11 @@ class MrpProduction(models.Model):
move_raw_ids = fields.One2many(
'stock.move', 'raw_material_production_id', 'Components',
copy=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
domain=[('scrapped', '=', False)])
move_finished_ids = fields.One2many(
'stock.move', 'production_id', 'Finished Products',
copy=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
domain=[('scrapped', '=', False)])
move_byproduct_ids = fields.One2many('stock.move', compute='_compute_move_byproduct_ids', inverse='_set_move_byproduct_ids')
finished_move_line_ids = fields.One2many(
@@ -773,6 +773,16 @@ class MrpProduction(models.Model):
workorders_to_delete.unlink()
return super(MrpProduction, self).unlink()
def copy_data(self, default=None):
default = dict(default or {})
# covers at least 2 cases: backorders generation (follow default logic for moves copying)
# and copying a done MO via the form (i.e. copy only the non-cancelled moves since no backorder = cancelled finished moves)
if not default or 'move_finished_ids' not in default:
default['move_finished_ids'] = [(0, 0, move.copy_data()[0]) for move in self.move_finished_ids.filtered(lambda m: m.state != 'cancel' and m.product_qty != 0.0)]
if not default or 'move_raw_ids' not in default:
default['move_raw_ids'] = [(0, 0, move.copy_data()[0]) for move in self.move_raw_ids.filtered(lambda m: m.product_qty != 0.0)]
return super(MrpProduction, self).copy_data(default=default)
def action_toggle_is_locked(self):
self.ensure_one()
self.is_locked = not self.is_locked
+10 -2
View File
@@ -55,7 +55,7 @@ class TestMrpOrder(TestMrpCommon):
}).action_apply_inventory()
test_date_planned = Dt.now() - timedelta(days=1)
test_quantity = 2.0
test_quantity = 3.0
man_order_form = Form(self.env['mrp.production'].with_user(self.user_mrp_user))
man_order_form.product_id = self.product_4
man_order_form.bom_id = self.bom_1
@@ -89,7 +89,7 @@ class TestMrpOrder(TestMrpCommon):
# produce product
mo_form = Form(man_order)
mo_form.qty_producing = 1.0
mo_form.qty_producing = 2.0
man_order = mo_form.save()
action = man_order.button_mark_done()
@@ -101,6 +101,14 @@ class TestMrpOrder(TestMrpCommon):
backorder.save().action_close_mo()
self.assertEqual(man_order.state, 'done', "Production order should be done.")
# check that copy handles moves correctly
mo_copy = man_order.copy()
self.assertEqual(mo_copy.state, 'draft', "Copied production order should be draft.")
self.assertEqual(len(mo_copy.move_raw_ids), 4,
"Incorrect number of component moves [i.e. all non-0 (even cancelled) moves should be copied].")
self.assertEqual(len(mo_copy.move_finished_ids), 1, "Incorrect number of moves for products to produce [i.e. cancelled moves should not be copied")
self.assertEqual(mo_copy.move_finished_ids.product_uom_qty, 2, "Incorrect qty of products to produce")
def test_production_availability(self):
""" Checks the availability of a production order through mutliple calls to `action_assign`.
"""