diff --git a/addons/mrp/models/mrp_abstract_workorder.py b/addons/mrp/models/mrp_abstract_workorder.py index a54691c6436..7e05d187f5a 100644 --- a/addons/mrp/models/mrp_abstract_workorder.py +++ b/addons/mrp/models/mrp_abstract_workorder.py @@ -28,6 +28,12 @@ class MrpAbstractWorkorder(models.AbstractModel): @api.onchange('qty_producing') def _onchange_qty_producing(self): + """ Modify the qty currently producing will modify the existing + workorder line in order to match the new quantity to consume for each + component and their reserved quantity. + """ + if self.qty_producing <= 0: + raise UserError(_('You have to produce at least one %s.') % self.product_uom_id.name) line_values = self._update_workorder_lines() for vals in line_values['to_create']: self.workorder_line_ids |= self.workorder_line_ids.new(vals) diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index 136537c13f7..4df8c4611e4 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -3,6 +3,7 @@ from datetime import datetime from dateutil.relativedelta import relativedelta +from collections import defaultdict from odoo import api, fields, models, _ from odoo.exceptions import UserError @@ -103,21 +104,60 @@ class MrpWorkorder(models.Model): capacity = fields.Float( 'Capacity', default=1.0, help="Number of pieces that can be produced in parallel.") - workorder_line_ids = fields.One2many('mrp.workorder.line', 'workorder_id', string='Workorder lines') - final_lot_domain = fields.One2many('stock.production.lot', compute="_compute_final_lot_domain") + workorder_line_ids = fields.One2many( + 'mrp.workorder.line', + 'workorder_id', + string='Workorder lines', + domain=[('is_finished', '=', False)] + ) + finished_workorder_line_ids = fields.One2many( + 'mrp.workorder.line', + 'workorder_id', + string='Final Lot lines', + domain=[('is_finished', '=', True)] + ) + allowed_lots_domain = fields.One2many(comodel_name='stock.production.lot', compute="_compute_allowed_lots_domain") - @api.depends('production_id') - def _compute_final_lot_domain(self): - for wo in self: - # check if self is not the first workorder in the list - if self.env['mrp.workorder'].search([('next_work_order_id', '=', wo.id)]): - wo.final_lot_domain = self.env['stock.production.lot'].search([ - ('use_next_on_work_order_id', '=', wo.id), - ]).ids + @api.onchange('final_lot_id') + def _onchange_final_lot_id(self): + """When the user changes the lot being currently produced, suggest + a quantity to produce consistent with the previous workorders. """ + previous_wo = self.env['mrp.workorder'].search([ + ('next_work_order_id', '=', self.id) + ]) + if previous_wo: + line = previous_wo.finished_workorder_line_ids.filtered(lambda line: line.lot_id == self.final_lot_id) + if line: + self.qty_producing = line.qty_done + + def _compute_allowed_lots_domain(self): + """ browse already created lot and respective quantities""" + productions = self.mapped('production_id') + for production in productions: + if production.product_id.tracking == 'none': + continue + + rounding = production.product_uom_id.rounding + finished_workorder_lines = production.workorder_ids.mapped('finished_workorder_line_ids') + qties_done_per_lot = defaultdict(list) + for finished_workorder_line in finished_workorder_lines: + qties_done_per_lot[finished_workorder_line.lot_id.id].append(finished_workorder_line.qty_done) + + qty_to_produce = production.product_qty + allowed_lot_ids = self.env['stock.production.lot'] + qty_produced = sum([max(qty_dones) for qty_dones in qties_done_per_lot.values()]) + if float_compare(qty_produced, qty_to_produce, precision_rounding=rounding) < 0: + # If we haven't produced enough, all lots are available + allowed_lot_ids = self.env['stock.production.lot'].search([('product_id', '=', production.product_id.id)]) else: - wo.final_lot_domain = self.env['stock.production.lot'].search([ - ('product_id', '=', wo.product_id.id), - ]).ids + # If we produced enough, only the already produced lots are available + allowed_lot_ids = self.env['stock.production.lot'].browse(qties_done_per_lot.keys()) + workorders = production.workorder_ids.filtered(lambda wo: wo.state not in ('done', 'cancel')) + for workorder in workorders: + if workorder.product_tracking == 'serial': + workorder.allowed_lots_domain = allowed_lot_ids - workorder.finished_workorder_line_ids.mapped('lot_id') + else: + workorder.allowed_lots_domain = allowed_lot_ids @api.multi def name_get(self): @@ -195,9 +235,38 @@ class MrpWorkorder(models.Model): line_values = self._generate_lines_values(move, qty_to_consume) self.workorder_line_ids |= self.env['mrp.workorder.line'].create(line_values) - def _assign_default_final_lot_id(self): - self.final_lot_id = self.env['stock.production.lot'].search([('use_next_on_work_order_id', '=', self.id)], - order='create_date, id', limit=1) + def _apply_update_workorder_lines(self): + """ update existing line on the workorder. It could be trigger manually + after a modification of qty_producing. + """ + self.ensure_one() + line_values = self._update_workorder_lines() + self.workorder_line_ids |= self.workorder_line_ids.create(line_values['to_create']) + if line_values['to_delete']: + line_values['to_delete'].unlink() + for line, vals in line_values['to_update'].items(): + line.write(vals) + + 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 + if not r_line.lot_id: + continue + candidates = self.finished_workorder_line_ids.filtered(lambda line: line.lot_id == r_line.lot_id) + rounding = self.product_uom_id.rounding + if not candidates: + self.write({ + 'final_lot_id': r_line.lot_id.id, + 'qty_producing': r_line.qty_done, + }) + return True + elif float_compare(candidates.qty_done, r_line.qty_done, precision_rounding=rounding) < 0: + self.write({ + 'final_lot_id': r_line.lot_id.id, + 'qty_producing': r_line.qty_done - candidates.qty_done, + }) + return True + return False def _get_byproduct_move_line(self, by_product_move, quantity): return { @@ -216,7 +285,7 @@ class MrpWorkorder(models.Model): return True self.ensure_one() - if self.qty_producing <= 0: + if float_compare(self.qty_producing, 0, precision_rounding=self.product_uom_id.rounding) <= 0: raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.')) # One a piece is produced, you can launch the next work order @@ -224,39 +293,93 @@ class MrpWorkorder(models.Model): self.next_work_order_id.state = 'ready' # If last work order, then post lots used - # TODO: should be same as checking if for every workorder something has been done? if not self.next_work_order_id: self._update_finished_move() # Transfer quantities from temporary to final move line or make them final self._update_raw_moves() + # Transfert lot and quantity produced to a finished workorder line + if self.product_tracking != 'none' and self.final_lot_id: + self._create_or_update_finished_line() + # Update workorder quantity produced self.qty_produced += self.qty_producing - if self.final_lot_id: - self.final_lot_id.use_next_on_work_order_id = self.next_work_order_id - self.final_lot_id = False + # Suggest a finished lot on the next workorder + if self.next_work_order_id and self.production_id.product_id.tracking != 'none' and not self.next_work_order_id.final_lot_id: + self.next_work_order_id._defaults_from_finished_workorder_line(self.finished_workorder_line_ids) + # As we may have changed the quantity to produce on the next workorder, + # make sure to update its wokorder lines + self.next_work_order_id._apply_update_workorder_lines() - # Set a qty producing + # Test if the production is done rounding = self.production_id.product_uom_id.rounding - if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0: - self.qty_producing = 0 - elif self.production_id.product_id.tracking == 'serial': - self._assign_default_final_lot_id() - self.qty_producing = 1.0 - self.generate_wo_lines() + if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) < 0: + previous_wo = self.env['mrp.workorder'] + if self.product_tracking != 'none': + previous_wo = self.env['mrp.workorder'].search([ + ('next_work_order_id', '=', self.id) + ]) + candidate_found_in_previous_wo = False + if previous_wo: + candidate_found_in_previous_wo = self._defaults_from_finished_workorder_line(previous_wo.finished_workorder_line_ids) + if not candidate_found_in_previous_wo: + # self is the first workorder + self.qty_producing = self.qty_remaining + self.final_lot_id = False + if self.product_tracking == 'serial': + self.qty_producing = 1 + + self._apply_update_workorder_lines() else: - self.qty_producing = float_round(self.production_id.product_qty - self.qty_produced, precision_rounding=rounding) - self.generate_wo_lines() - - if self.next_work_order_id and self.production_id.product_id.tracking != 'none': - self.next_work_order_id._assign_default_final_lot_id() - - if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0: + self.qty_producing = 0 self.button_finish() return True + def _create_or_update_finished_line(self): + """ + 1. Check that the final lot and the quantity producing is valid regarding + other workorders of this production + 2. Save final lot and quantity producing to suggest on next workorder + """ + self.ensure_one() + final_lot_quantity = self.qty_production + rounding = self.product_uom_id.rounding + # Get the max quantity possible for current lot in other workorders + for workorder in (self.production_id.workorder_ids - self): + # We add the remaining quantity to the produced quantity for the + # current lot. For 5 finished products: if in the first wo it + # creates 4 lot A and 1 lot B and in the second it create 3 lot A + # and it remains 2 units to product, it could produce 5 lot A. + # In this case we select 4 since it would conflict with the first + # workorder otherwise. + line = workorder.finished_workorder_line_ids.filtered(lambda line: line.lot_id == self.final_lot_id) + if line and float_compare(line.qty_done + workorder.qty_remaining, final_lot_quantity, precision_rounding=rounding) <= 0: + final_lot_quantity = line.qty_done + workorder.qty_remaining + elif float_compare(workorder.qty_remaining, final_lot_quantity, precision_rounding=rounding) < 0: + final_lot_quantity = workorder.qty_remaining + + # final lot line for this lot on this workorder. + current_lot_lines = self.finished_workorder_line_ids.filtered(lambda line: line.lot_id == self.final_lot_id) + + # this lot has already been produced + if float_compare(final_lot_quantity, current_lot_lines.qty_done + self.qty_producing, precision_rounding=rounding) < 0: + raise UserError(_('You have produced %s %s of lot %s in the previous workorder. You are trying to produce %s in this one') % + (final_lot_quantity, self.product_id.uom_id.name, self.final_lot_id.name, current_lot_lines.qty_done + self.qty_producing)) + + # Update workorder line that regiter final lot created + if not current_lot_lines: + current_lot_lines = self.env['mrp.workorder.line'].create({ + 'workorder_id': self.id, + 'is_finished': True, + 'product_id': self.product_id.id, + 'lot_id': self.final_lot_id.id, + 'qty_done': self.qty_producing, + }) + else: + current_lot_lines.qty_done += self.qty_producing + @api.multi def button_start(self): self.ensure_one() @@ -391,6 +514,7 @@ class MrpWorkorderLine(models.Model): _description = "Workorder move line" workorder_id = fields.Many2one('mrp.workorder', 'Workorder') + is_finished = fields.Boolean('Finished Lot Line', default=False) def _get_final_lot(self): return self.workorder_id.final_lot_id diff --git a/addons/mrp/models/stock_production_lot.py b/addons/mrp/models/stock_production_lot.py index 7a04e34f272..2df39cd7648 100644 --- a/addons/mrp/models/stock_production_lot.py +++ b/addons/mrp/models/stock_production_lot.py @@ -1,17 +1,13 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models, _ +from odoo import models, _ from odoo.exceptions import UserError class StockProductionLot(models.Model): _inherit = 'stock.production.lot' - use_next_on_work_order_id = fields.Many2one('mrp.workorder', - string="Next Work Order to Use", - help='Technical: used to figure out default serial number on work orders') - def _check_create(self): active_mo_id = self.env.context.get('active_mo_id') if active_mo_id: diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 93c0b6f474a..1d71880011b 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -691,6 +691,7 @@ class TestMrpOrder(TestMrpCommon): with self.assertRaises(UserError): # try adding another line for a bom product to increase the quantity + produce_form.qty_producing = 1 with produce_form.workorder_line_ids.new() as line: line.product_id = p1 line.qty_done = 1 @@ -709,6 +710,7 @@ class TestMrpOrder(TestMrpCommon): 'active_id': mo.id, 'active_ids': [mo.id], })) + produce_form.qty_producing = 1 with produce_form.workorder_line_ids.new() as line: line.product_id = self.product_4 line.qty_done = 1 diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml index f451bfd13bd..a3270e74811 100644 --- a/addons/mrp/views/mrp_workorder_views.xml +++ b/addons/mrp/views/mrp_workorder_views.xml @@ -105,7 +105,7 @@ - +