From 5cf8fb31bb96e8eadcd2e67f02cd570f1c299a35 Mon Sep 17 00:00:00 2001 From: "Tiffany Chang (tic)" Date: Mon, 9 Aug 2021 10:11:14 +0000 Subject: [PATCH] [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 Signed-off-by: Tiffany Chang --- addons/mrp/models/mrp_production.py | 14 ++++++++++++-- addons/mrp/tests/test_order.py | 12 ++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index fdaa3d5e912..ecd196cf532 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -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 diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 34343364000..46479ed66d6 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -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`. """