From 556a905166e794bc0956af84fcf660c548a724d6 Mon Sep 17 00:00:00 2001 From: Arnold Moyaux Date: Mon, 18 May 2020 17:25:19 +0200 Subject: [PATCH] [REF] mrp: byproduct onchange Create the byproduct moves at the onchange. task-2241471 --- addons/mrp/data/mrp_demo.xml | 4 + addons/mrp/models/mrp_abstract_workorder.py | 2 +- addons/mrp/models/mrp_production.py | 102 ++++++++++++------ addons/mrp/models/stock_rule.py | 1 + addons/mrp/tests/test_backorder.py | 3 +- addons/mrp/tests/test_order.py | 2 +- addons/mrp/views/mrp_production_views.xml | 45 ++++++++ .../models/stock_picking.py | 1 + 8 files changed, 123 insertions(+), 37 deletions(-) diff --git a/addons/mrp/data/mrp_demo.xml b/addons/mrp/data/mrp_demo.xml index 7295121a19d..60c1b1ad768 100644 --- a/addons/mrp/data/mrp_demo.xml +++ b/addons/mrp/data/mrp_demo.xml @@ -625,6 +625,10 @@ + + + + diff --git a/addons/mrp/models/mrp_abstract_workorder.py b/addons/mrp/models/mrp_abstract_workorder.py index 0dc6d8fae58..5cf3096e6e5 100644 --- a/addons/mrp/models/mrp_abstract_workorder.py +++ b/addons/mrp/models/mrp_abstract_workorder.py @@ -521,7 +521,7 @@ class MrpAbstractWorkorderLine(models.AbstractModel): if self[self._get_raw_workorder_inverse_name()]: values = mo._get_move_raw_values(self.product_id, self.qty_done, self.product_uom_id) elif self.product_id != mo.product_id: - values = mo._get_finished_move_value(self.product_id.id, self.qty_done, self.product_uom_id.id) + values = mo._get_move_finished_values(self.product_id.id, self.qty_done, self.product_uom_id.id) else: # The line is neither a component nor a byproduct return diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 44d3589b113..6c8024e1b14 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -494,7 +494,8 @@ class MrpProduction(models.Model): order.move_byproduct_ids = order.move_finished_ids.filtered(lambda m: m.product_id != order.product_id) def _set_move_byproduct_ids(self): - self.move_finished_ids |= self.move_byproduct_ids + move_finished_ids = self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id) + self.move_finished_ids = move_finished_ids | self.move_byproduct_ids @api.depends('state') def _compute_show_lock(self): @@ -558,8 +559,14 @@ class MrpProduction(models.Model): @api.onchange('date_planned_start') def _onchange_date_planned_start(self): - if not self.workorder_ids: - self.date_planned_finished = self.date_planned_start + datetime.timedelta(hours=1) + if not self.is_planned: + date_planned_finished = self.date_planned_start + relativedelta(days=self.product_id.produce_delay) + date_planned_finished = date_planned_finished + relativedelta(days=self.company_id.manufacturing_lead) + if date_planned_finished == self.date_planned_start: + date_planned_finished = date_planned_finished + relativedelta(hours=1) + self.date_planned_finished = date_planned_finished + self.move_raw_ids = [(1, m.id, {'date_expected': self.date_planned_start, 'date': self.date_planned_start}) for m in self.move_raw_ids] + self.move_finished_ids = [(1, m.id, {'date_expected': date_planned_finished, 'date': date_planned_finished}) for m in self.move_finished_ids] @api.onchange('bom_id', 'product_id', 'product_qty', 'product_uom_id') def _onchange_move_raw(self): @@ -579,6 +586,28 @@ class MrpProduction(models.Model): else: self.move_raw_ids = [(2, move.id) for move in self.move_raw_ids.filtered(lambda m: m.bom_line_id)] + @api.onchange('bom_id', 'product_id', 'product_qty', 'product_uom_id') + def _onchange_move_finished(self): + if self.product_id and self.product_qty > 0: + # keep manual entries + list_move_finished = [(4, move.id) for move in self.move_finished_ids.filtered( + lambda m: not m.byproduct_id and m.product_id != self.product_id)] + moves_finished_values = self._get_moves_finished_values() + moves_byproduct_dict = {move.byproduct_id.id: move for move in self.move_finished_ids.filtered(lambda m: m.byproduct_id)} + move_finished = self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id) + for move_finished_values in moves_finished_values: + if move_finished_values.get('byproduct_id') in moves_byproduct_dict: + # update existing entries + list_move_finished += [(1, moves_byproduct_dict[move_finished_values['byproduct_id']].id, move_finished_values)] + elif move_finished_values.get('product_id') == self.product_id.id and move_finished: + list_move_finished += [(1, move_finished.id, move_finished_values)] + else: + # add new entries + list_move_finished += [(0, 0, move_finished_values)] + self.move_finished_ids = list_move_finished + else: + self.move_finished_ids = [(2, move.id) for move in self.move_finished_ids.filtered(lambda m: m.bom_line_id)] + @api.onchange('location_src_id', 'move_raw_ids', 'bom_id') def _onchange_location(self): source_location = self.location_src_id @@ -587,6 +616,17 @@ class MrpProduction(models.Model): 'location_id': source_location.id, }) + @api.onchange('location_dest_id', 'move_finished_ids', 'bom_id') + def _onchange_location_dest(self): + destination_location = self.location_dest_id + update_value_list = [] + for move in self.move_finished_ids: + update_value_list += [(1, move.id, ({ + 'warehouse_id': destination_location.get_warehouse().id, + 'location_dest_id': destination_location.id, + }))] + self.move_finished_ids = update_value_list + @api.onchange('picking_type_id') def onchange_picking_type(self): location = self.env.ref('stock.stock_location_stock') @@ -595,6 +635,7 @@ class MrpProduction(models.Model): except (AttributeError, AccessError): location = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1).lot_stock_id self.move_raw_ids.update({'picking_type_id': self.picking_type_id}) + self.move_finished_ids.update({'picking_type_id': self.picking_type_id}) self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id @@ -608,6 +649,8 @@ class MrpProduction(models.Model): self._create_workorder() def write(self, vals): + if 'workorder_ids' in self: + production_to_replan = self.filtered(lambda p: p.is_planned) res = super(MrpProduction, self).write(vals) for production in self: @@ -616,7 +659,7 @@ class MrpProduction(models.Model): raise UserError(_('You cannot move a manufacturing order once it is cancelled or done.')) if production.is_partially_planned: raise UserError(_('You cannot move a manufacturing order once it has a planned workorder, move related workorder(s) instead.')) - if ('move_raw_ids' in vals or 'move_finished_ids' in vals) and production.state != 'draft': + if any(field in ['move_raw_ids', 'move_finished_ids', 'workorder_ids'] for field in vals) and production.state != 'draft': if production.state == 'done': # for some reason moves added after state = 'done' won't save group_id, reference if added in # "stock_move.default_get()" @@ -630,6 +673,8 @@ class MrpProduction(models.Model): 'date_expected': production.date_planned_finished, }) production._autoconfirm_production() + if production in production_to_replan: + production._plan_workorders(replan=True) if production.state == 'done' and ('lot_producing_id' in vals or 'qty_producing' in vals): finished_move_lines = production.move_finished_ids.filtered( lambda move: move.product_id == self.product_id and move.state == 'done').mapped('move_line_ids') @@ -637,6 +682,7 @@ class MrpProduction(models.Model): finished_move_lines.write({'lot_id': vals.get('lot_producing_id')}) if 'qty_producing' in vals: finished_move_lines.write({'qty_done': vals.get('qty_producing')}) + if not production.bom_id.operation_ids and vals.get('date_planned_start') and not vals.get('date_planned_finished'): new_date_planned_start = fields.Datetime.to_datetime(vals.get('date_planned_start')) if not production.date_planned_finished or new_date_planned_start >= production.date_planned_finished: @@ -656,12 +702,13 @@ class MrpProduction(models.Model): procurement_group_vals = self._prepare_procurement_group_vals(values) values['procurement_group_id'] = self.env["procurement.group"].create(procurement_group_vals).id production = super(MrpProduction, self).create(values) - production.move_raw_ids.write({ + (production.move_raw_ids | production.move_finished_ids).write({ 'group_id': production.procurement_group_id.id, }) # Trigger move_raw creation when importing a file if 'import_file' in self.env.context: production._onchange_move_raw() + production._onchange_move_finished() return production def unlink(self): @@ -710,7 +757,7 @@ class MrpProduction(models.Model): for workorder in production.workorder_ids: workorder.duration_expected = workorder._get_duration_expected() - def _get_finished_move_value(self, product_id, product_uom_qty, product_uom, operation_id=False, byproduct_id=False): + def _get_move_finished_values(self, product_id, product_uom_qty, product_uom, operation_id=False, byproduct_id=False): group_orders = self.procurement_group_id.mrp_production_ids move_dest_ids = self.move_dest_ids if len(group_orders) > 1: @@ -726,7 +773,7 @@ class MrpProduction(models.Model): 'operation_id': operation_id, 'byproduct_id': byproduct_id, 'name': self.name, - 'date': self.date_planned_start, + 'date': date_planned_finished, 'date_expected': date_planned_finished, 'picking_type_id': self.picking_type_id.id, 'location_id': self.product_id.with_company(self.company_id).property_stock_production.id, @@ -743,18 +790,18 @@ class MrpProduction(models.Model): 'move_dest_ids': [(4, x.id) for x in move_dest_ids], } - def _generate_finished_moves(self): - if self.product_id in self.bom_id.byproduct_ids.mapped('product_id'): - raise UserError(_("You cannot have %s as the finished product and in the Byproducts") % self.product_id.name) - moves_values = [self._get_finished_move_value(self.product_id.id, self.product_qty, self.product_uom_id.id)] - for byproduct in self.bom_id.byproduct_ids: - product_uom_factor = self.product_uom_id._compute_quantity(self.product_qty, self.bom_id.product_uom_id) - qty = byproduct.product_qty * (product_uom_factor / self.bom_id.product_qty) - move_values = self._get_finished_move_value(byproduct.product_id.id, - qty, byproduct.product_uom_id.id, byproduct.operation_id.id, - byproduct.id) - moves_values.append(move_values) - moves = self.env['stock.move'].create(moves_values) + def _get_moves_finished_values(self): + moves = [] + for production in self: + if production.product_id in production.bom_id.byproduct_ids.mapped('product_id'): + raise UserError(_("You cannot have %s as the finished product and in the Byproducts") % self.product_id.name) + moves = [production._get_move_finished_values(production.product_id.id, production.product_qty, production.product_uom_id.id)] + for byproduct in production.bom_id.byproduct_ids: + product_uom_factor = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id) + qty = byproduct.product_qty * (product_uom_factor / production.bom_id.product_qty) + moves.append(production._get_move_finished_values( + byproduct.product_id.id, qty, byproduct.product_uom_id.id, + byproduct.operation_id.id, byproduct.id)) return moves def _get_moves_raw_values(self): @@ -983,7 +1030,6 @@ class MrpProduction(models.Model): production.consumption = production.bom_id.consumption if not production.move_raw_ids: raise UserError(_("Add some materials to consume before marking this MO as to do.")) - production._generate_finished_moves() production.move_raw_ids._adjust_procure_method() (production.move_raw_ids | production.move_finished_ids)._action_confirm() production.workorder_ids._action_confirm() @@ -998,7 +1044,7 @@ class MrpProduction(models.Model): """ Create work orders. And probably do stuff, like things. """ orders_to_plan = self.filtered(lambda order: not order.is_planned) for order in orders_to_plan: - order.move_raw_ids.filtered(lambda m: m.state == 'draft')._action_confirm() + (order.move_raw_ids | order.move_finished_ids).filtered(lambda m: m.state == 'draft')._action_confirm() # `propagate_date` enables the automatic rescheduling which could lead to hard to # understand behavior if a manufacturing order is planned, i.e. if the work orders do # have their leaves booked in the workcenter calendar. We thus disable the @@ -1273,7 +1319,6 @@ class MrpProduction(models.Model): for production in self: if production.backorder_sequence == 0: # Activate backorder naming production.backorder_sequence = 1 - backorder_mo = production.copy(default=self._get_backorder_mo_vals(production)) for wo in backorder_mo.workorder_ids: wo.qty_produced = 0 @@ -1322,23 +1367,14 @@ class MrpProduction(models.Model): if self.env.context.get('mo_ids_to_backorder'): productions_to_backorder = self.browse(self.env.context['mo_ids_to_backorder']) productions_not_to_backorder = self - productions_to_backorder - # As a backorder will be created to produce the remaining quantity, we don't want - # action_done (in _post_inventory) create any new move for the finished one and the - # byproduct with the delta quantity (product_uom_qty - quantity_done) - for production in productions_to_backorder: - for move in production.move_finished_ids: - if move.product_id == production.product_id: - move.product_uom_qty = production.qty_producing - else: - move.product_uom_qty = move.quantity_done else: productions_not_to_backorder = self productions_to_backorder = self.env['mrp.production'] self.workorder_ids.button_finish() - productions_not_to_backorder._post_inventory(cancel_backorder=False) - productions_to_backorder._post_inventory(cancel_backorder=True) + productions_not_to_backorder._post_inventory(cancel_backorder=True) + productions_to_backorder._post_inventory(cancel_backorder=False) backorders = productions_to_backorder._generate_backorder_productions() # Moves without quantity done are not posted => set them as done instead of canceling. In diff --git a/addons/mrp/models/stock_rule.py b/addons/mrp/models/stock_rule.py index 64427e58d76..75d2df4a046 100644 --- a/addons/mrp/models/stock_rule.py +++ b/addons/mrp/models/stock_rule.py @@ -55,6 +55,7 @@ class StockRule(models.Model): # create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example) productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create(productions_values) self.env['stock.move'].sudo().create(productions._get_moves_raw_values()) + self.env['stock.move'].sudo().create(productions._get_moves_finished_values()) productions._create_workorder() productions.filtered(lambda p: p.move_raw_ids).action_confirm() diff --git a/addons/mrp/tests/test_backorder.py b/addons/mrp/tests/test_backorder.py index a55b957184e..ec2e27f1edd 100644 --- a/addons/mrp/tests/test_backorder.py +++ b/addons/mrp/tests/test_backorder.py @@ -132,7 +132,6 @@ class TestMrpProductionBackorder(TestMrpCommon): """ with Form(self.warehouse) as warehouse: warehouse.manufacture_steps = 'pbm_sam' - production, _, product_to_build, product_to_use_1, product_to_use_2 = self.generate_mo(qty_base_1=4, qty_final=4, picking_type_id=self.warehouse.manu_type_id) move_raw_ids = production.move_raw_ids @@ -221,7 +220,7 @@ class TestMrpProductionBackorder(TestMrpCommon): backorder.save().action_backorder() active_production = active_production.procurement_group_id.mrp_production_ids[-1] - self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), nb_product_todo, f'You should have the {nb_product_todo} final product in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), nb_product_todo, f'You should have the {nb_product_todo} final product in stock') self.assertEqual(len(production.procurement_group_id.mrp_production_ids), nb_product_todo) def test_tracking_backorder_series_serial_1(self): diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 9e844270b56..bae3b44428f 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -78,7 +78,7 @@ class TestMrpOrder(TestMrpCommon): # check production move production_move = man_order.move_finished_ids - self.assertEqual(production_move.date, test_date_planned) + self.assertAlmostEqual(production_move.date, test_date_planned + timedelta(hours=1), delta=timedelta(seconds=10)) self.assertEqual(production_move.product_id, self.product_4) self.assertEqual(production_move.product_uom, man_order.product_uom_id) self.assertEqual(production_move.product_qty, man_order.product_qty) diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 25a2c2f5dab..d5b5ecf3a72 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -171,6 +171,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/mrp_subcontracting/models/stock_picking.py b/addons/mrp_subcontracting/models/stock_picking.py index 137015dc846..3af0d45b1ff 100644 --- a/addons/mrp_subcontracting/models/stock_picking.py +++ b/addons/mrp_subcontracting/models/stock_picking.py @@ -138,6 +138,7 @@ class StockPicking(models.Model): for move, bom in subcontract_details: mo = self.env['mrp.production'].with_company(move.company_id).create(self._prepare_subcontract_mo_vals(move, bom)) self.env['stock.move'].create(mo._get_moves_raw_values()) + self.env['stock.move'].create(mo._get_moves_finished_values()) mo.action_confirm() # Link the finished to the receipt move.