From 2190568d9e6fda200db6e28bf13e24fa5d3bc634 Mon Sep 17 00:00:00 2001 From: William Henrotin Date: Thu, 4 Apr 2019 09:29:54 +0000 Subject: [PATCH] [IMP] mrp: suggest lot on workorder On a manufactoring order, 'checking availability' will make the reservation of the components to consume. If those components are tracked by lot or serial numbers, the reservation will also set a available lot/sn on the move lines and workorder lines. The reservation already works on product produce wizard lines and workorder line (form view). We need this commit as preliminary work to being able to suggest them on the workorders tablet view too. In case of requesting the reservation after planning the production (applying reservation on already created workorder lines and checks), we deleted all lines not yet consumed. They are then recreated with the reservation. The user can obiously still use the lot/sn he wants by changing it onto the workorder. Task : 1891864 closes odoo/odoo#28020 Signed-off-by: Simon Lejeune (sle) --- addons/mrp/models/mrp_abstract_workorder.py | 39 +++++++++++++++++---- addons/mrp/models/mrp_production.py | 3 +- addons/mrp/models/mrp_workorder.py | 28 ++++++++++++++- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/addons/mrp/models/mrp_abstract_workorder.py b/addons/mrp/models/mrp_abstract_workorder.py index 7e05d187f5a..c4e156ec4bb 100644 --- a/addons/mrp/models/mrp_abstract_workorder.py +++ b/addons/mrp/models/mrp_abstract_workorder.py @@ -75,18 +75,24 @@ class MrpAbstractWorkorder(models.AbstractModel): # Remove or lower quantity on exisiting workorder lines if float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0: qty_todo = abs(qty_todo) - for workorder_line in move_workorder_lines.sorted(key=lambda wl: wl.qty_reserved): + # Try to decrease or remove lines that are not reserved and + # partialy reserved first. A different decrease strategy could + # be define in _unreserve_order method. + for workorder_line in move_workorder_lines.sorted(key=lambda wl: wl._unreserve_order()): if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: break + # If the quantity to consume on the line is lower than the + # quantity to remove, the line could be remove. if float_compare(workorder_line.qty_to_consume, qty_todo, precision_rounding=rounding) <= 0: - # update qty_todo for next wo_line qty_todo = float_round(qty_todo - workorder_line.qty_to_consume, precision_rounding=rounding) if line_values['to_delete']: line_values['to_delete'] |= workorder_line else: line_values['to_delete'] = workorder_line + # decrease the quantity on the line else: new_val = workorder_line.qty_to_consume - qty_todo + # avoid to write a negative reserved quantity new_reserved = max(0, workorder_line.qty_reserved - qty_todo) line_values['to_update'][workorder_line] = { 'qty_to_consume': new_val, @@ -97,12 +103,17 @@ class MrpAbstractWorkorder(models.AbstractModel): else: # Search among wo lines which one could be updated qty_reserved_wl = defaultdict(float) + # Try to update the line with the greater reservation first in + # order to promote bigger batch. for workorder_line in move_workorder_lines.sorted(key=lambda wl: wl.qty_reserved, reverse=True): rounding = workorder_line.product_uom_id.rounding if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: break move_lines = workorder_line._get_move_lines() qty_reserved_wl[workorder_line.lot_id] += workorder_line.qty_reserved + # The reserved quantity according to exisiting move line + # already produced (with qty_done set) and other production + # lines with the same lot that are currently on production. qty_reserved_remaining = sum(move_lines.mapped('product_uom_qty')) - sum(move_lines.mapped('qty_done')) - qty_reserved_wl[workorder_line.lot_id] if float_compare(qty_reserved_remaining, 0, precision_rounding=rounding) > 0: qty_to_add = min(qty_reserved_remaining, qty_todo) @@ -114,6 +125,13 @@ class MrpAbstractWorkorder(models.AbstractModel): qty_todo -= qty_to_add qty_reserved_wl[workorder_line.lot_id] += qty_to_add + # If a line exists without reservation and without lot. It + # means that previous operations could not find any reserved + # quantity and created a line without lot prefilled. In this + # case, the system will not find an existing move line with + # available reservation anymore and will increase this line + # instead of creating a new line without lot and reserved + # quantities. if not workorder_line.qty_reserved and not workorder_line.lot_id and workorder_line.product_tracking != 'serial': line_values['to_update'][workorder_line] = { 'qty_done': workorder_line.qty_to_consume + qty_todo, @@ -130,9 +148,10 @@ class MrpAbstractWorkorder(models.AbstractModel): @api.model def _generate_lines_values(self, move, qty_to_consume): """ Create workorder line. First generate line based on the reservation, - in order to match the reservation. If the quantity to consume is greater - than the reservation quantity then create line with the correct quantity - to consume but without lot or serial number. + in order to prefill reserved quantity, lot and serial number. + If the quantity to consume is greater than the reservation quantity then + create line with the correct quantity to consume but without lot or + serial number. """ lines = [] is_tracked = move.product_id.tracking != 'none' @@ -227,8 +246,9 @@ class MrpAbstractWorkorder(models.AbstractModel): ) def _update_raw_moves(self): - """ Once the production is done, the lots written on workorder lines - are saved on stock move lines""" + """ Once the production is done. Modify the workorder lines into + stock move line with the registered lot and quantity done. + """ # Before writting produce quantities, we ensure they respect the bom strictness self._strict_consumption_check() vals_list = [] @@ -389,6 +409,11 @@ class MrpAbstractWorkorderLine(models.AbstractModel): return vals_list + def _unreserve_order(self): + """ Unreserve line with lower reserved quantity first """ + self.ensure_one() + return (self.qty_reserved,) + def _get_move_lines(self): return self.move_id.move_line_ids.filtered(lambda ml: ml.lot_id == self.lot_id and ml.product_id == self.product_id) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index de09754e2da..e50ba0a7e0e 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -592,6 +592,7 @@ class MrpProduction(models.Model): def action_assign(self): for production in self: production.move_raw_ids._action_assign() + production.workorder_ids._refresh_wo_lines() return True @api.multi @@ -729,7 +730,7 @@ class MrpProduction(models.Model): moves_raw.mapped('move_line_ids').write({'workorder_id': workorder.id}) (moves_finished + moves_raw).write({'workorder_id': workorder.id}) - workorder.generate_wo_lines() + workorder._generate_wo_lines() return workorders def _check_lots(self): diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index 4df8c4611e4..6384b5a78ff 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -220,7 +220,7 @@ class MrpWorkorder(models.Model): raise UserError(_('The planned end date of the work order cannot be prior to the planned start date, please correct this to save the work order.')) return super(MrpWorkorder, self).write(values) - def generate_wo_lines(self): + def _generate_wo_lines(self): """ Generate workorder line """ self.ensure_one() raw_moves = self.move_raw_ids.filtered( @@ -247,6 +247,32 @@ class MrpWorkorder(models.Model): for line, vals in line_values['to_update'].items(): line.write(vals) + def _refresh_wo_lines(self): + for workorder in self: + raw_moves = workorder.move_raw_ids.filtered( + lambda move: move.state not in ('done', 'cancel') + ) + wl_to_unlink = self.env['mrp.workorder.line'] + for move in raw_moves: + rounding = move.product_uom.rounding + qty_already_consumed = 0.0 + workorder_lines = workorder.workorder_line_ids.filtered(lambda w: w.move_id == move) + for wl in workorder_lines: + if not wl.qty_done: + wl_to_unlink |= wl + continue + + qty_already_consumed += wl.qty_done + qty_to_consume = move.product_uom._compute_quantity( + workorder.qty_producing * move.unit_factor, + move.product_id.uom_id, + round=False + ) + wl_to_unlink.unlink() + if float_compare(qty_to_consume, qty_already_consumed, precision_rounding=rounding) > 0: + line_values = workorder._generate_lines_values(move, qty_to_consume - qty_already_consumed) + workorder.workorder_line_ids |= self.env['mrp.workorder.line'].create(line_values) + def _defaults_from_finished_workorder_line(self, reference_lot_lines): for r_line in reference_lot_lines: # see which lot we could suggest and its related qty_producing