diff --git a/addons/mrp/data/mrp_demo.xml b/addons/mrp/data/mrp_demo.xml index 68e28abe950c5..c168e4902eaed 100644 --- a/addons/mrp/data/mrp_demo.xml +++ b/addons/mrp/data/mrp_demo.xml @@ -696,14 +696,17 @@ - - + + diff --git a/addons/mrp/models/__init__.py b/addons/mrp/models/__init__.py index e9fc93bef6ae6..db69451b2fece 100644 --- a/addons/mrp/models/__init__.py +++ b/addons/mrp/models/__init__.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import mrp_document +from . import mrp_abstract_workorder from . import res_config_settings from . import mrp_bom from . import mrp_routing diff --git a/addons/mrp/models/mrp_abstract_workorder.py b/addons/mrp/models/mrp_abstract_workorder.py new file mode 100644 index 0000000000000..44fb34fdf1ee9 --- /dev/null +++ b/addons/mrp/models/mrp_abstract_workorder.py @@ -0,0 +1,354 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.addons import decimal_precision as dp +from odoo.exceptions import UserError +from odoo.tools import float_compare, float_round, float_is_zero + + +class MrpAbstractWorkorder(models.AbstractModel): + _name = "mrp.abstract.workorder" + _description = "Common code between produce wizards and workorders." + + production_id = fields.Many2one('mrp.production', 'Manufacturing Order', required=True) + product_id = fields.Many2one(related='production_id.product_id', readonly=True, store=True) + qty_producing = fields.Float(string='Currently Produced Quantity', digits=dp.get_precision('Product Unit of Measure')) + product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True, readonly=True) + final_lot_id = fields.Many2one('stock.production.lot', string='Lot/Serial Number', domain="[('product_id', '=', product_id)]") + product_tracking = fields.Selection(related="product_id.tracking") + + @api.onchange('qty_producing') + def _onchange_qty_producing(self): + line_values = self._update_workorder_lines() + for vals in line_values['to_create']: + self.workorder_line_ids |= self.workorder_line_ids.new(vals) + if line_values['to_delete']: + self.workorder_line_ids -= line_values['to_delete'] + for line, vals in line_values['to_update'].items(): + line.update(vals) + + def _update_workorder_lines(self): + """ Update workorder lines, according to the new qty currently + produced. It returns a dict with line to create, update or delete. + It do not directly write or unlink the line because this function is + used in onchange and request that write on db (e.g. workorder creation). + """ + line_values = {'to_create': [], 'to_delete': [], 'to_update': {}} + for move_raw in self.move_raw_ids.filtered(lambda move: move.state not in ('done', 'cancel')): + move_workorder_lines = self.workorder_line_ids.filtered(lambda w: w.move_id == move_raw) + + # Compute the new quantity for the current component + rounding = move_raw.product_uom.rounding + new_qty = self.product_uom_id._compute_quantity( + self.qty_producing * move_raw.unit_factor, + self.production_id.product_uom_id, + round=False + ) + qty_todo = float_round(new_qty - sum(move_workorder_lines.mapped('qty_done')), precision_rounding=rounding) + + # 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: + if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: + break + 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 + else: + new_val = workorder_line.qty_to_consume - qty_todo + line_values['to_update'][workorder_line] = { + 'qty_to_consume': new_val, + 'qty_done': new_val, + 'qty_reserved': new_val, + } + qty_todo = 0 + else: + # Search among wo lines which one could be updated + for move_line in move_raw.move_line_ids: + # Get workorder lines that match reservation. + candidates = move_workorder_lines._find_candidate(move_line) + while candidates: + if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: + break + candidate = candidates.pop() + qty_to_add = move_line.product_uom_qty - candidate.qty_done + line_values['to_update'][candidate] = { + 'qty_done': candidate.qty_done + qty_to_add, + 'qty_to_consume': candidate.qty_to_consume + qty_to_add, + 'qty_reserved': candidate.qty_reserved + qty_to_add, + } + qty_todo -= qty_to_add + + # if there are still qty_todo, create new wo lines + if float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: + for vals in self._generate_lines_values(move_raw, qty_todo): + line_values['to_create'].append(vals) + return line_values + + @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. + """ + lines = [] + is_tracked = move.product_id.tracking != 'none' + for move_line in move.move_line_ids: + if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0: + break + # move line already 'used' in workorder (from its lot for instance) + if move_line.lot_produced_id or float_compare(move_line.product_uom_qty, move_line.qty_done, precision_rounding=move.product_uom.rounding) <= 0: + continue + # search wo line on which the lot is not fully consumed or other reserved lot + linked_wo_line = self.workorder_line_ids.filtered( + lambda line: line.product_id == move_line.product_id and + line.lot_id == move_line.lot_id + ) + if linked_wo_line: + if float_compare(sum(linked_wo_line.mapped('qty_to_consume')), move_line.product_uom_qty, precision_rounding=move.product_uom.rounding) < 0: + to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty - sum(linked_wo_line.mapped('qty_to_consume'))) + else: + continue + else: + to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty) + line = { + 'move_id': move.id, + 'product_id': move.product_id.id, + 'product_uom_id': is_tracked and move.product_id.uom_id.id or move.product_uom.id, + 'qty_to_consume': to_consume_in_line, + 'qty_reserved': move_line.product_uom_qty, + 'lot_id': move_line.lot_id.id, + 'qty_done': is_tracked and 0 or to_consume_in_line + } + lines.append(line) + qty_to_consume -= to_consume_in_line + # The move has not reserved the whole quantity so we create new wo lines + if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: + if move.product_id.tracking == 'serial': + while float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: + line = { + 'move_id': move.id, + 'product_id': move.product_id.id, + 'product_uom_id': move.product_id.uom_id.id, + 'qty_to_consume': 1, + 'qty_done': 1, + } + lines.append(line) + qty_to_consume -= 1 + else: + line = { + 'move_id': move.id, + 'product_id': move.product_id.id, + 'product_uom_id': move.product_uom.id, + 'qty_to_consume': qty_to_consume, + 'qty_done': is_tracked and 0 or qty_to_consume + } + lines.append(line) + return lines + + def _update_finished_move(self): + """ Update the finished move & move lines in order to set the finished + product lot on it as well as the produced quantity. This method get the + information either from the last workorder or from the Produce wizard.""" + production_move = self.production_id.move_finished_ids.filtered( + lambda move: move.product_id == self.product_id and + move.state not in ('done', 'cancel') + ) + if production_move and production_move.product_id.tracking != 'none': + if not self.final_lot_id: + raise UserError(_('You need to provide a lot for the finished product.')) + move_line = production_move.move_line_ids.filtered( + lambda line: line.lot_id.id == self.final_lot_id.id + ) + if move_line: + if self.product_id.tracking == 'serial': + raise UserError(_('You cannot produce the same serial number twice.')) + move_line.product_uom_qty += self.qty_producing + move_line.qty_done += self.qty_producing + else: + move_line.create({ + 'move_id': production_move.id, + 'product_id': production_move.product_id.id, + 'lot_id': self.final_lot_id.id, + 'product_uom_qty': self.qty_producing, + 'product_uom_id': self.product_uom_id.id, + 'qty_done': self.qty_producing, + 'location_id': production_move.location_id.id, + 'location_dest_id': production_move.location_dest_id.id, + }) + else: + rounding = production_move.product_uom.rounding + production_move._set_quantity_done( + production_move.quantity_done + float_round(self.qty_producing, precision_rounding=rounding) + ) + + def _update_raw_moves(self): + """ Once the production is done, the lots written on workorder lines + are saved on stock move lines""" + vals_list = [] + workorder_lines_to_process = self.workorder_line_ids.filtered(lambda line: line.qty_done > 0) + for line in workorder_lines_to_process: + line._update_move_lines() + if float_compare(line.qty_done, 0, precision_rounding=line.product_uom_id.rounding) > 0: + vals_list += line._create_extra_move_lines() + + self.workorder_line_ids.unlink() + self.env['stock.move.line'].create(vals_list) + + +class MrpAbstractWorkorderLine(models.AbstractModel): + _name = "mrp.abstract.workorder.line" + _description = "Abstract model to implement product_produce_line as well as\ + workorder_line" + + move_id = fields.Many2one('stock.move') + product_id = fields.Many2one('product.product', 'Product', required=True) + product_tracking = fields.Selection(related="product_id.tracking") + lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial Number') + qty_to_consume = fields.Float('To Consume', digits=dp.get_precision('Product Unit of Measure')) + product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure') + qty_done = fields.Float('Consumed') + qty_reserved = fields.Float('Reserved', digits=dp.get_precision('Product Unit of Measure')) + + @api.onchange('lot_id') + def _onchange_lot_id(self): + """ When the user is encoding a produce line for a tracked product, we apply some logic to + help him. This onchange will automatically switch `qty_done` to 1.0. + """ + if self.product_id.tracking == 'serial': + self.qty_done = 1 + + @api.onchange('qty_done') + def _onchange_qty_done(self): + """ When the user is encoding a produce line for a tracked product, we apply some logic to + help him. This onchange will warn him if he set `qty_done` to a non-supported value. + """ + res = {} + if self.product_id.tracking == 'serial' and not float_is_zero(self.qty_done, self.product_uom_id.rounding): + if float_compare(self.qty_done, 1.0, precision_rounding=self.product_uom_id.rounding) != 0: + message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name + res['warning'] = {'title': _('Warning'), 'message': message} + return res + + def _update_move_lines(self): + """ update a move line to save the workorder line data""" + self.ensure_one() + if self.lot_id: + move_lines = self.move_id.move_line_ids.filtered(lambda ml: ml.lot_id == self.lot_id and not ml.lot_produced_id) + else: + move_lines = self.move_id.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_produced_id) + + # Sanity check: if the product is a serial number and `lot` is already present in the other + # consumed move lines, raise. + if self.product_id.tracking != 'none' and not self.lot_id: + raise UserError(_('Please enter a lot or serial number for %s !' % self.product_id.display_name)) + + if self.lot_id and self.product_id.tracking == 'serial' and self.lot_id in self.move_id.move_line_ids.filtered(lambda ml: ml.qty_done).mapped('lot_id'): + raise UserError(_('You cannot consume the same serial number twice. Please correct the serial numbers encoded.')) + + # Update reservation and quantity done + for ml in move_lines: + rounding = ml.product_uom_id.rounding + if float_compare(self.qty_done, 0, precision_rounding=rounding) <= 0: + break + quantity_to_process = min(self.qty_done, ml.product_uom_qty - ml.qty_done) + self.qty_done -= quantity_to_process + + new_quantity_done = (ml.qty_done + quantity_to_process) + # if we produce less than the reserved quantity to produce the finished products + # in different lots, + # we create different component_move_lines to record which one was used + # on which lot of finished product + if float_compare(new_quantity_done, ml.product_uom_qty, precision_rounding=rounding) >= 0: + ml.write({ + 'qty_done': new_quantity_done, + 'lot_produced_id': self._get_final_lot().id + }) + else: + new_qty_reserved = ml.product_uom_qty - new_quantity_done + default = { + 'product_uom_qty': new_quantity_done, + 'qty_done': new_quantity_done, + 'lot_produced_id': self._get_final_lot().id + } + ml.copy(default=default) + ml.with_context(bypass_reservation_update=True).write({ + 'product_uom_qty': new_qty_reserved, + 'qty_done': 0 + }) + + def _create_extra_move_lines(self): + """Create new sml if quantity produced is bigger than the reserved one""" + vals_list = [] + quants = self.env['stock.quant']._gather(self.product_id, self.move_id.location_id, lot_id=self.lot_id, strict=False) + # Search for a sub-locations where the product is available. + # Loop on the quants to get the locations. If there is not enough + # quantity into stock, we take the move location. Anyway, no + # reservation is made, so it is still possible to change it afterwards. + for quant in quants: + quantity = quant.reserved_quantity - quant.quantity + rounding = quant.product_uom_id.rounding + if (float_compare(quant.quantity, 0, precision_rounding=rounding) <= 0 or + float_compare(quantity, 0, precision_rounding=rounding) <= 0): + continue + vals = { + 'move_id': self.move_id.id, + 'product_id': self.product_id.id, + 'location_id': quant.location_id.id, + 'location_dest_id': self.move_id.location_dest_id.id, + 'product_uom_qty': 0, + 'product_uom_id': quant.product_uom_id.id, + 'qty_done': min(quantity, self.qty_done), + 'lot_produced_id': self._get_final_lot().id, + } + if self.lot_id: + vals.update({'lot_id': self.lot_id.id}) + + vals_list.append(vals) + self.qty_done -= vals['qty_done'] + # If all the qty_done is distributed, we can close the loop + if float_compare(self.qty_done, 0, precision_rounding=self.product_uom_id.rounding) <= 0: + break + + if float_compare(self.qty_done, 0, precision_rounding=self.product_uom_id.rounding) > 0: + vals = { + 'move_id': self.move_id.id, + 'product_id': self.product_id.id, + 'location_id': self.move_id.location_id.id, + 'location_dest_id': self.move_id.location_dest_id.id, + 'product_uom_qty': 0, + 'product_uom_id': self.product_uom_id.id, + 'qty_done': self.qty_done, + 'lot_produced_id': self._get_final_lot().id, + } + if self.lot_id: + vals.update({'lot_id': self.lot_id.id}) + + vals_list.append(vals) + + return vals_list + + def _find_candidate(self, move_line): + """ Method used in order to return move lines that match reservation. + The purpose is to update exisiting workorder line regarding the + reservation of the raw moves. + """ + rounding = move_line.product_uom_id.rounding + return self.filtered(lambda line: + line.lot_id == move_line.lot_id and + float_compare(line.qty_done, move_line.product_uom_qty, precision_rounding=rounding) < 0 and + line.product_id == move_line.product_id) + + # To be implemented in specific model + def _get_final_lot(self): + raise NotImplementedError('Method _get_final_lot() undefined on %s' % self) + + def _get_production(self): + raise NotImplementedError('Method _get_production() undefined on %s' % self) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 4adb325c4d371..b497dca8e6af5 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -509,6 +509,7 @@ def _get_move_raw_values(self, bom_line, line_data): data = { 'sequence': bom_line.sequence, 'name': self.name, + 'reference': self.name, 'date': self.date_planned_start, 'date_expected': self.date_planned_start, 'bom_line_id': bom_line.id, @@ -715,6 +716,7 @@ def _workorders_create(self, bom, bom_data): 'name': operation.name, 'production_id': self.id, 'workcenter_id': operation.workcenter_id.id, + 'product_uom_id': self.product_uom_id.id, 'operation_id': operation.id, 'duration_expected': duration_expected, 'state': len(workorders) == 0 and 'ready' or 'pending', @@ -733,7 +735,7 @@ def _workorders_create(self, bom, bom_data): moves_raw.mapped('move_line_ids').write({'workorder_id': workorder.id}) (moves_finished + moves_raw).write({'workorder_id': workorder.id}) - workorder._generate_lot_ids() + 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 cc1be6ab7a83f..a892db6e098ad 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -13,31 +13,17 @@ class MrpWorkorder(models.Model): _name = 'mrp.workorder' _description = 'Work Order' - _inherit = ['mail.thread', 'mail.activity.mixin'] + _inherit = ['mail.thread', 'mail.activity.mixin', 'mrp.abstract.workorder'] name = fields.Char( 'Work Order', required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) - workcenter_id = fields.Many2one( 'mrp.workcenter', 'Work Center', required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) working_state = fields.Selection( 'Workcenter Status', related='workcenter_id.working_state', readonly=False, help='Technical: used in views only') - - production_id = fields.Many2one( - 'mrp.production', 'Manufacturing Order', - index=True, ondelete='cascade', required=True, tracking=True, - states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) - product_id = fields.Many2one( - 'product.product', 'Product', - related='production_id.product_id', readonly=True, - help='Technical: used in views only.', store=True) - product_uom_id = fields.Many2one( - 'uom.uom', 'Unit of Measure', - related='production_id.product_uom_id', readonly=True, - help='Technical: used in views only.') production_availability = fields.Selection( 'Stock Availability', readonly=True, related='production_id.reservation_state', store=True, @@ -46,9 +32,6 @@ class MrpWorkorder(models.Model): 'Production State', readonly=True, related='production_id.state', help='Technical: used in views only.') - product_tracking = fields.Selection( - 'Product Tracking', related='production_id.product_id.tracking', readonly=False, - help='Technical: used in views only.') qty_production = fields.Float('Original Production Quantity', readonly=True, related='production_id.product_qty') qty_remaining = fields.Float('Quantity To Be Produced', compute='_compute_qty_remaining', digits=dp.get_precision('Product Unit of Measure')) qty_produced = fields.Float( @@ -56,13 +39,8 @@ class MrpWorkorder(models.Model): readonly=True, digits=dp.get_precision('Product Unit of Measure'), help="The number of products already handled by this work order") - qty_producing = fields.Float( - 'Currently Produced Quantity', default=1.0, - digits=dp.get_precision('Product Unit of Measure'), - states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) is_produced = fields.Boolean(string="Has Been Produced", compute='_compute_is_produced') - state = fields.Selection([ ('pending', 'Waiting for another WO'), ('ready', 'Ready'), @@ -105,11 +83,7 @@ class MrpWorkorder(models.Model): 'stock.move', 'workorder_id', 'Moves') move_line_ids = fields.One2many( 'stock.move.line', 'workorder_id', 'Moves to Track', - domain=[('done_wo', '=', True)], help="Inventory moves for which you must scan a lot number at this work order") - active_move_line_ids = fields.One2many( - 'stock.move.line', 'workorder_id', - domain=[('done_wo', '=', False)]) final_lot_id = fields.Many2one( 'stock.production.lot', 'Lot/Serial Number', domain="[('product_id', '=', product_id)]", states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) @@ -129,6 +103,7 @@ 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') @api.multi def name_get(self): @@ -179,55 +154,6 @@ def _compute_color(self): for order in (self - late_orders): order.color = 2 - @api.onchange('qty_producing') - def _onchange_qty_producing(self): - """ Update stock.move.lot records, according to the new qty currently - produced. """ - moves = self.move_raw_ids.filtered(lambda move: move.state not in ('done', 'cancel') and move.product_id.tracking != 'none' and move.product_id.id != self.production_id.product_id.id) - for move in moves: - move_lots = self.active_move_line_ids.filtered(lambda move_lot: move_lot.move_id == move) - if not move_lots: - continue - rounding = move.product_uom.rounding - new_qty = float_round(move.unit_factor * self.qty_producing, precision_rounding=rounding) - if move.product_id.tracking == 'lot': - move_lots[0].product_qty = new_qty - move_lots[0].qty_done = new_qty - elif move.product_id.tracking == 'serial': - # Create extra pseudo record - qty_todo = float_round(new_qty - sum(move_lots.mapped('qty_done')), precision_rounding=rounding) - if float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: - while float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: - self.active_move_line_ids += self.env['stock.move.line'].new({ - 'move_id': move.id, - 'product_id': move.product_id.id, - 'lot_id': False, - 'product_uom_qty': 0.0, - 'product_uom_id': move.product_uom.id, - 'qty_done': min(1.0, qty_todo), - 'workorder_id': self.id, - 'done_wo': False, - 'location_id': move.location_id.id, - 'location_dest_id': move.location_dest_id.id, - 'date': move.date, - }) - qty_todo -= 1 - elif float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0: - qty_todo = abs(qty_todo) - for move_lot in move_lots: - if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: - break - if not move_lot.lot_id and float_compare(qty_todo, move_lot.qty_done, precision_rounding=rounding) >= 0: - qty_todo = float_round(qty_todo - move_lot.qty_done, precision_rounding=rounding) - self.active_move_line_ids -= move_lot # Difference operator - else: - #move_lot.product_qty = move_lot.product_qty - qty_todo - if float_compare(move_lot.qty_done - qty_todo, 0, precision_rounding=rounding) == 1: - move_lot.qty_done = move_lot.qty_done - qty_todo - else: - move_lot.qty_done = 0 - qty_todo = 0 - @api.multi def write(self, values): if list(values.keys()) != ['time_ids'] and any(workorder.state == 'done' for workorder in self): @@ -240,42 +166,20 @@ def write(self, values): 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_lot_ids(self): - """ Generate stock move lines """ + def generate_wo_lines(self): + """ Generate workorder line """ self.ensure_one() - MoveLine = self.env['stock.move.line'] - tracked_moves = self.move_raw_ids.filtered( - lambda move: move.state not in ('done', 'cancel') and move.product_id.tracking != 'none' and move.product_id != self.production_id.product_id) - for move in tracked_moves: - qty = move.unit_factor * self.qty_producing - if move.product_id.tracking == 'serial': - while float_compare(qty, 0.0, precision_rounding=move.product_uom.rounding) > 0: - MoveLine.create({ - 'move_id': move.id, - 'product_uom_qty': 0, - 'product_uom_id': move.product_uom.id, - 'qty_done': min(1, qty), - 'production_id': self.production_id.id, - 'workorder_id': self.id, - 'product_id': move.product_id.id, - 'done_wo': False, - 'location_id': move.location_id.id, - 'location_dest_id': move.location_dest_id.id, - }) - qty -= 1 - else: - MoveLine.create({ - 'move_id': move.id, - 'product_uom_qty': 0, - 'product_uom_id': move.product_uom.id, - 'qty_done': qty, - 'product_id': move.product_id.id, - 'production_id': self.production_id.id, - 'workorder_id': self.id, - 'done_wo': False, - 'location_id': move.location_id.id, - 'location_dest_id': move.location_dest_id.id, - }) + raw_moves = self.move_raw_ids.filtered( + lambda move: move.state not in ('done', 'cancel') + ) + for move in raw_moves: + qty_to_consume = move.product_uom._compute_quantity( + self.qty_producing * move.unit_factor, + move.product_id.uom_id, + round=False + ) + 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)], @@ -288,7 +192,6 @@ def _get_byproduct_move_line(self, by_product_move, quantity): 'product_uom_qty': quantity, 'product_uom_id': by_product_move.product_uom.id, 'qty_done': quantity, - 'workorder_id': self.id, 'location_id': by_product_move.location_id.id, 'location_dest_id': by_product_move.location_dest_id.id, } @@ -305,89 +208,22 @@ def record_production(self): if self.qty_producing <= 0: raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.')) - if (self.production_id.product_id.tracking != 'none') and not self.final_lot_id and self.move_raw_ids: - raise UserError(_('You should provide a lot/serial number for the final product.')) - - # Update quantities done on each raw material line - # For each untracked component without any 'temporary' move lines, - # (the new workorder tablet view allows registering consumed quantities for untracked components) - # we assume that only the theoretical quantity was used - for move in self.move_raw_ids: - if move.has_tracking == 'none' and (move.state not in ('done', 'cancel'))\ - and move.unit_factor and not move.move_line_ids.filtered(lambda ml: not ml.done_wo): - rounding = move.product_uom.rounding - if self.product_id.tracking != 'none': - qty_to_add = float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) - move._generate_consumed_move_line(qty_to_add, self.final_lot_id) - elif len(move._get_move_lines()) < 2: - move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) - else: - move._set_quantity_done(move.quantity_done + float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)) - - # Transfer quantities from temporary to final move lots or make them final - for move_line in self.active_move_line_ids: - # Check if move_line already exists - if move_line.qty_done <= 0: # rounding... - move_line.sudo().unlink() - continue - if move_line.product_id.tracking != 'none' and not move_line.lot_id: - raise UserError(_('You should provide a lot/serial number for a component.')) - # Search other move_line where it could be added: - lots = self.move_line_ids.filtered(lambda x: (x.lot_id.id == move_line.lot_id.id) and (not x.lot_produced_id) and (not x.done_move) and (x.product_id == move_line.product_id)) - if lots: - lots[0].qty_done += move_line.qty_done - lots[0].lot_produced_id = self.final_lot_id.id - self._link_to_quality_check(move_line, lots[0]) - move_line.sudo().unlink() - else: - move_line.lot_produced_id = self.final_lot_id.id - move_line.done_wo = True - # One a piece is produced, you can launch the next work order if self.next_work_order_id.state == 'pending': self.next_work_order_id.state = 'ready' - self.move_line_ids.filtered( - lambda move_line: not move_line.done_move and not move_line.lot_produced_id and move_line.qty_done > 0 - ).write({ - 'lot_produced_id': self.final_lot_id.id, - 'lot_produced_qty': self.qty_producing - }) - # 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: - production_move = self.production_id.move_finished_ids.filtered( - lambda x: (x.product_id.id == self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))) - if production_move.product_id.tracking != 'none': - move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id) - if move_line: - move_line.product_uom_qty += self.qty_producing - move_line.qty_done += self.qty_producing - else: - move_line.create({'move_id': production_move.id, - 'product_id': production_move.product_id.id, - 'lot_id': self.final_lot_id.id, - 'product_uom_qty': self.qty_producing, - 'product_uom_id': production_move.product_uom.id, - 'qty_done': self.qty_producing, - 'workorder_id': self.id, - 'location_id': production_move.location_id.id, - 'location_dest_id': production_move.location_dest_id.id, - }) - else: - production_move.quantity_done += self.qty_producing + uom_id = self.production_id.product_uom_id + self._update_finished_move() + self.production_id.move_finished_ids.filtered( + lambda move: move.product_id == self.product_id and + move.state not in ('done', 'cancel') + ).workorder_id = self.id - if not self.next_work_order_id: - for by_product_move in self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))): - if by_product_move.has_tracking != 'serial': - values = self._get_byproduct_move_line(by_product_move, self.qty_producing * by_product_move.unit_factor) - self.env['stock.move.line'].create(values) - elif by_product_move.has_tracking == 'serial': - qty_todo = by_product_move.product_uom._compute_quantity(self.qty_producing * by_product_move.unit_factor, by_product_move.product_id.uom_id) - for i in range(0, int(float_round(qty_todo, precision_digits=0))): - values = self._get_byproduct_move_line(by_product_move, 1) - self.env['stock.move.line'].create(values) + # Transfer quantities from temporary to final move line or make them final + self._update_raw_moves() # Update workorder quantity produced self.qty_produced += self.qty_producing @@ -403,10 +239,10 @@ def record_production(self): elif self.production_id.product_id.tracking == 'serial': self._assign_default_final_lot_id() self.qty_producing = 1.0 - self._generate_lot_ids() + self.generate_wo_lines() else: self.qty_producing = float_round(self.production_id.product_qty - self.qty_produced, precision_rounding=rounding) - self._generate_lot_ids() + 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() @@ -541,3 +377,17 @@ def action_see_move_scrap(self): def _compute_qty_remaining(self): for wo in self: wo.qty_remaining = float_round(wo.qty_production - wo.qty_produced, precision_rounding=wo.production_id.product_uom_id.rounding) + + +class MrpWorkorderLine(models.Model): + _name = 'mrp.workorder.line' + _inherit = ["mrp.abstract.workorder.line"] + _description = "Workorder move line" + + workorder_id = fields.Many2one('mrp.workorder', 'Workorder') + + def _get_final_lot(self): + return self.workorder_id.final_lot_id + + def _get_production(self): + return self.workorder_id.production_id diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 7d079afe6fd55..34c3269e7c76f 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -225,65 +225,6 @@ def _generate_move_phantom(self, bom_line, product_qty, quantity_done): return move return self.env['stock.move'] - def _generate_consumed_move_line(self, qty_to_add, final_lot, lot=False): - if lot: - move_lines = self.move_line_ids.filtered(lambda ml: ml.lot_id == lot and not ml.lot_produced_id) - else: - move_lines = self.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_produced_id) - - # Sanity check: if the product is a serial number and `lot` is already present in the other - # consumed move lines, raise. - if lot and self.product_id.tracking == 'serial' and lot in self.move_line_ids.filtered(lambda ml: ml.qty_done).mapped('lot_id'): - raise UserError(_('You cannot consume the same serial number twice. Please correct the serial numbers encoded.')) - - for ml in move_lines: - rounding = ml.product_uom_id.rounding - if float_compare(qty_to_add, 0, precision_rounding=rounding) <= 0: - break - quantity_to_process = min(qty_to_add, ml.product_uom_qty - ml.qty_done) - qty_to_add -= quantity_to_process - - new_quantity_done = (ml.qty_done + quantity_to_process) - if float_compare(new_quantity_done, ml.product_uom_qty, precision_rounding=rounding) >= 0: - ml.write({'qty_done': new_quantity_done, 'lot_produced_id': final_lot.id}) - else: - new_qty_reserved = ml.product_uom_qty - new_quantity_done - default = {'product_uom_qty': new_quantity_done, - 'qty_done': new_quantity_done, - 'lot_produced_id': final_lot.id} - ml.copy(default=default) - ml.with_context(bypass_reservation_update=True).write({'product_uom_qty': new_qty_reserved, 'qty_done': 0}) - - if float_compare(qty_to_add, 0, precision_rounding=self.product_uom.rounding) > 0: - # Search for a sub-location where the product is available. This might not be perfectly - # correct if the quantity available is spread in several sub-locations, but at least - # we should be closer to the reality. Anyway, no reservation is made, so it is still - # possible to change it afterwards. - quants = self.env['stock.quant']._gather(self.product_id, self.location_id, lot_id=lot, strict=False) - available_quantity = self.product_id.uom_id._compute_quantity( - self.env['stock.quant']._get_available_quantity( - self.product_id, self.location_id, lot_id=lot, strict=False - ), self.product_uom - ) - location_id = False - if float_compare(qty_to_add, available_quantity, precision_rounding=self.product_uom.rounding) < 0: - location_id = quants.filtered(lambda r: r.quantity > 0)[-1:].location_id - - vals = { - 'move_id': self.id, - 'product_id': self.product_id.id, - 'location_id': location_id.id if location_id else self.location_id.id, - 'production_id': self.raw_material_production_id.id, - 'location_dest_id': self.location_dest_id.id, - 'product_uom_qty': 0, - 'product_uom_id': self.product_uom.id, - 'qty_done': qty_to_add, - 'lot_produced_id': final_lot.id, - } - if lot: - vals.update({'lot_id': lot.id}) - self.env['stock.move.line'].create(vals) - def _get_upstream_documents_and_responsibles(self, visited): if self.created_production_id and self.created_production_id.state not in ('done', 'cancel'): return [(self.created_production_id, self.created_production_id.user_id, visited)] diff --git a/addons/mrp/security/ir.model.access.csv b/addons/mrp/security/ir.model.access.csv index 0f48edfa18d13..23b010a67b8c4 100644 --- a/addons/mrp/security/ir.model.access.csv +++ b/addons/mrp/security/ir.model.access.csv @@ -28,6 +28,8 @@ access_product_supplierinfo_user,product.supplierinfo user,product.model_product access_res_partner,res.partner,base.model_res_partner,mrp.group_mrp_user,1,0,0,0 access_mrp_workorder_mrp_user,mrp.workorder.user,model_mrp_workorder,mrp.group_mrp_user,1,1,1,1 access_mrp_workorder_mrp_manager,mrp.workorder,model_mrp_workorder,mrp.group_mrp_manager,1,1,1,1 +access_mrp_workorder_line_mrp_user,mrp.workorder.user,model_mrp_workorder_line,mrp.group_mrp_user,1,1,1,1 +access_mrp_workorder_line_mrp_manager,mrp.workorder,model_mrp_workorder_line,mrp.group_mrp_manager,1,1,1,1 access_resource_calendar_leaves_user,mrp.resource.calendar.leaves.user,resource.model_resource_calendar_leaves,mrp.group_mrp_user,1,1,1,1 access_resource_calendar_leaves_manager,mrp.resource.calendar.leaves.manager,resource.model_resource_calendar_leaves,mrp.group_mrp_manager,1,0,0,0 access_resource_calendar_attendance_mrp_user,mrp.resource.calendar.attendance.mrp.user,resource.model_resource_calendar_attendance,mrp.group_mrp_user,1,1,1,1 diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index ac4ec19b748a1..2935b6b47f2b5 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -143,7 +143,7 @@ def test_basic(self): 'active_id': man_order.id, 'active_ids': [man_order.id], })) - produce_form.product_qty = 1.0 + produce_form.qty_producing = 1.0 produce_wizard = produce_form.save() produce_wizard.do_produce() @@ -412,7 +412,7 @@ def test_multiple_post_inventory(self): # produce one item, call `post_inventory` context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id} produce_form = Form(self.env['mrp.product.produce'].with_context(context)) - produce_form.product_qty = 1.00 + produce_form.qty_producing = 1.00 custom_laptop_produce = produce_form.save() custom_laptop_produce.do_produce() mo_custom_laptop.post_inventory() @@ -425,7 +425,7 @@ def test_multiple_post_inventory(self): # produce the second item, call `post_inventory` context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id} produce_form = Form(self.env['mrp.product.produce'].with_context(context)) - produce_form.product_qty = 1.00 + produce_form.qty_producing = 1.00 custom_laptop_produce = produce_form.save() custom_laptop_produce.do_produce() mo_custom_laptop.post_inventory() @@ -458,7 +458,7 @@ def test_rounding(self): 'active_id': production.id, 'active_ids': [production.id], })) - produce_form.product_qty = 8 + produce_form.qty_producing = 8 produce_wizard = produce_form.save() produce_wizard.do_produce() self.assertEqual(production.move_raw_ids[0].quantity_done, 16, 'Should use half-up rounding when producing') @@ -480,10 +480,9 @@ def test_product_produce_1(self): 'active_ids': [mo.id], })) product_produce = produce_form.save() + self.assertEqual(len(product_produce.workorder_line_ids), 2, 'You should have produce lines even the consumed products are not tracked.') product_produce.do_produce() - self.assertEqual(len(product_produce.produce_line_ids), 2, 'You should have produce lines even the consumed products are not tracked.') - def test_product_produce_2(self): """ Check that line are created when the consumed products are tracked by serial and the lot proposed are correct. """ @@ -511,9 +510,9 @@ def test_product_produce_2(self): })) product_produce = produce_form.save() - self.assertEqual(len(product_produce.produce_line_ids), 3, 'You should have 3 produce lines. One for each serial to consume') - product_produce.product_qty = 1 - produce_line_1 = product_produce.produce_line_ids[0] + self.assertEqual(len(product_produce.workorder_line_ids), 3, 'You should have 3 produce lines. One for each serial to consume') + product_produce.qty_producing = 1 + produce_line_1 = product_produce.workorder_line_ids[0] produce_line_1.qty_done = 1 remaining_lot = (lot_p1_1 | lot_p1_2) - produce_line_1.lot_id product_produce.do_produce() @@ -523,8 +522,8 @@ def test_product_produce_2(self): 'active_ids': [mo.id], })) product_produce = produce_form.save() - self.assertEqual(len(product_produce.produce_line_ids), 2, 'You should have 2 produce lines since one has already be consumed.') - for line in product_produce.produce_line_ids.filtered(lambda x: x.lot_id): + self.assertEqual(len(product_produce.workorder_line_ids), 2, 'You should have 2 produce lines since one has already be consumed.') + for line in product_produce.workorder_line_ids.filtered(lambda x: x.lot_id): self.assertEqual(line.lot_id, remaining_lot, 'Wrong lot proposed.') def test_product_produce_3(self): @@ -560,15 +559,15 @@ def test_product_produce_3(self): 'active_id': mo.id, 'active_ids': [mo.id], })) - produce_form.product_qty = 1.0 + produce_form.qty_producing = 1.0 product_produce = produce_form.save() - product_produce.lot_id = final_product_lot.id + product_produce.final_lot_id = final_product_lot.id # product 1 lot 1 shelf1 # product 1 lot 1 shelf2 # product 1 lot 2 - self.assertEqual(len(product_produce.produce_line_ids), 4, 'You should have 4 produce lines. lot 1 shelf_1, lot 1 shelf_2, lot2 and for product which have tracking None') + self.assertEqual(len(product_produce.workorder_line_ids), 4, 'You should have 4 produce lines. lot 1 shelf_1, lot 1 shelf_2, lot2 and for product which have tracking None') - for produce_line in product_produce.produce_line_ids: + for produce_line in product_produce.workorder_line_ids: produce_line.qty_done = produce_line.qty_to_consume + 1 product_produce.do_produce() @@ -614,9 +613,10 @@ def test_product_produce_4(self): 'active_id': mo.id, 'active_ids': [mo.id], }).create({ - 'product_qty': 1.0, + 'qty_producing': 1.0, }) - product_produce._onchange_product_qty() + line_values = product_produce._update_workorder_lines() + product_produce.workorder_line_ids |= product_produce.workorder_line_ids.create(line_values['to_create']) product_produce.do_produce() ml_p1 = mo.move_raw_ids.filtered(lambda x: x.product_id == p1).mapped('move_line_ids') @@ -658,11 +658,11 @@ def test_product_produce_5(self): 'active_id': mo.id, 'active_ids': [mo.id], }).create({ - 'product_qty': 5.0, + 'qty_producing': 5.0, }) - produce_wizard._onchange_product_qty() + produce_wizard._onchange_qty_producing() - for produce_line in produce_wizard.produce_line_ids: + for produce_line in produce_wizard.workorder_line_ids: produce_line.qty_done = produce_line.qty_to_consume produce_wizard.do_produce() @@ -705,11 +705,11 @@ def test_product_produce_uom(self): 'active_id': mo.id, 'active_ids': [mo.id], })) - produce_form.lot_id = final_product_lot + produce_form.final_lot_id = final_product_lot product_produce = produce_form.save() - self.assertEqual(product_produce.product_qty, 1) + self.assertEqual(product_produce.qty_producing, 1) self.assertEqual(product_produce.product_uom_id, unit, 'Should be 1 unit since the tracking is serial.') - product_produce.lot_id = final_product_lot.id + product_produce.final_lot_id = final_product_lot.id product_produce.do_produce() move_line_raw = mo.move_raw_ids.mapped('move_line_ids').filtered(lambda m: m.qty_done) diff --git a/addons/mrp/tests/test_procurement.py b/addons/mrp/tests/test_procurement.py index 522ef94342915..a6a8319d4ea0a 100644 --- a/addons/mrp/tests/test_procurement.py +++ b/addons/mrp/tests/test_procurement.py @@ -70,7 +70,7 @@ def test_procurement(self): 'active_id': produce_product_4.id, 'active_ids': [produce_product_4.id], })) - produce_form.product_qty = produce_product_4.product_qty + produce_form.qty_producing = produce_product_4.product_qty product_produce = produce_form.save() product_produce.do_produce() produce_product_4.post_inventory() @@ -96,7 +96,7 @@ def test_procurement(self): 'active_id': production_product_6.id, 'active_ids': [production_product_6.id], })) - produce_form.product_qty = production_product_6.product_qty + produce_form.qty_producing = production_product_6.product_qty product_produce = produce_form.save() product_produce.do_produce() production_product_6.post_inventory() diff --git a/addons/mrp/tests/test_traceability.py b/addons/mrp/tests/test_traceability.py index 4b7918bb0ca32..7f94f17ba8c15 100644 --- a/addons/mrp/tests/test_traceability.py +++ b/addons/mrp/tests/test_traceability.py @@ -70,10 +70,10 @@ def test_tracking_types_on_mo(self): })) if finished_product.tracking != 'serial': - produce_form.product_qty = 1 + produce_form.qty_producing = 1 if finished_product.tracking != 'none': - produce_form.lot_id = self.env['stock.production.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.id}) + produce_form.final_lot_id = self.env['stock.production.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.id}) produce_wizard = produce_form.save() produce_wizard.do_produce() @@ -113,4 +113,3 @@ def test_tracking_types_on_mo(self): unfoldable, 'Parts with tracking type "%s", should have be unfoldable : %s' % (tracking, unfoldable) ) - diff --git a/addons/mrp/tests/test_unbuild.py b/addons/mrp/tests/test_unbuild.py index 19dde75622b91..28a436c9869c1 100644 --- a/addons/mrp/tests/test_unbuild.py +++ b/addons/mrp/tests/test_unbuild.py @@ -28,7 +28,7 @@ def test_unbuild_standart(self): 'active_id': mo.id, 'active_ids': [mo.id], })) - produce_form.product_qty = 5.0 + produce_form.qty_producing = 5.0 produce_wizard = produce_form.save() produce_wizard.do_produce() @@ -100,8 +100,8 @@ def test_unbuild_with_final_lot(self): 'active_id': mo.id, 'active_ids': [mo.id], })) - produce_form.product_qty = 5.0 - produce_form.lot_id = lot + produce_form.qty_producing = 5.0 + produce_form.final_lot_id = lot produce_wizard = produce_form.save() produce_wizard.do_produce() @@ -185,7 +185,7 @@ def test_unbuild_with_comnsumed_lot(self): 'active_id': mo.id, 'active_ids': [mo.id], })) - produce_form.product_qty = 5.0 + produce_form.qty_producing = 5.0 produce_wizard = produce_form.save() produce_wizard.do_produce() @@ -275,8 +275,8 @@ def test_unbuild_with_everything_tracked(self): 'active_id': mo.id, 'active_ids': [mo.id], })) - produce_form.product_qty = 5.0 - produce_form.lot_id = lot_final + produce_form.qty_producing = 5.0 + produce_form.final_lot_id = lot_final produce_wizard = produce_form.save() produce_wizard.do_produce() @@ -372,7 +372,7 @@ def test_unbuild_with_duplicate_move(self): 'active_id': mo.id, 'active_ids': [mo.id], })) - produce_form.product_qty = 5.0 + produce_form.qty_producing = 5.0 produce_wizard = produce_form.save() produce_wizard.do_produce() @@ -398,8 +398,8 @@ def test_unbuild_with_duplicate_move(self): self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_1), 1, 'You should have get your product with lot 1 in stock') self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have the 3 basic product for lot 2 in stock') self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_3), 2, 'You should have get one product back for lot 3') - - + + def test_production_links_with_non_tracked_lots(self): """ This test produces an MO in two times and checks that the move lines are linked in a correct way """ @@ -408,43 +408,43 @@ def test_production_links_with_non_tracked_lots(self): 'name': 'lot_1', 'product_id': p2.id, }) - + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot_1) lot_finished_1 = self.env['stock.production.lot'].create({ 'name': 'lot_finished_1', 'product_id': p_final.id, }) - + produce_form = Form(self.env['mrp.product.produce'].with_context({ 'active_id': mo.id, 'active_ids': [mo.id], })) - produce_form.product_qty = 3.0 - produce_form.lot_id = lot_finished_1 + produce_form.qty_producing = 3.0 + produce_form.final_lot_id = lot_finished_1 produce_wizard = produce_form.save() - produce_wizard.produce_line_ids[0].lot_id = lot_1 + produce_wizard.workorder_line_ids[0].lot_id = lot_1 produce_wizard.do_produce() - + lot_2 = self.env['stock.production.lot'].create({ 'name': 'lot_2', 'product_id': p2.id, }) - + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 4, lot_id=lot_2) lot_finished_2 = self.env['stock.production.lot'].create({ 'name': 'lot_finished_2', 'product_id': p_final.id, }) - + produce_form = Form(self.env['mrp.product.produce'].with_context({ 'active_id': mo.id, 'active_ids': [mo.id], })) - produce_form.product_qty = 2.0 - produce_form.lot_id = lot_finished_2 + produce_form.qty_producing = 2.0 + produce_form.final_lot_id = lot_finished_2 produce_wizard = produce_form.save() - produce_wizard.produce_line_ids[0].lot_id = lot_2 + produce_wizard.workorder_line_ids[0].lot_id = lot_2 produce_wizard.do_produce() mo.button_mark_done() ml = mo.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and m.lot_produced_id == lot_finished_1) @@ -453,9 +453,9 @@ def test_production_links_with_non_tracked_lots(self): self.assertEqual(ml[0].qty_done, 8.0, 'Should have consumed 8 for the second lot') def test_unbuild_with_routes(self): - """ This test creates a MO of a stockable product (Table). A new route for rule QC/Unbuild -> Stock + """ This test creates a MO of a stockable product (Table). A new route for rule QC/Unbuild -> Stock is created with Warehouse -> True. - The unbuild order should revert the consumed components into QC/Unbuild location for quality check + The unbuild order should revert the consumed components into QC/Unbuild location for quality check and then a picking should be generated for transferring components from QC/Unbuild location to stock. """ StockQuant = self.env['stock.quant'] @@ -528,7 +528,7 @@ def test_unbuild_with_routes(self): 'active_id': mo.id, 'active_ids': [mo.id], })) - produce_form.product_qty = 1.0 + produce_form.qty_producing = 1.0 produce_wizard = produce_form.save() produce_wizard.do_produce() diff --git a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py index f02d9bba70f45..a0d78d3cc78d5 100644 --- a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py +++ b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py @@ -187,7 +187,7 @@ def test_manufacturing_flow(self): 'active_id': production_order.id, 'active_ids': [production_order.id], })) - produce_form.product_qty = production_order.product_qty + produce_form.qty_producing = production_order.product_qty product_produce = produce_form.save() product_produce.do_produce() production_order.button_mark_done() diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py index fcfce68733da5..b182ff72c032a 100644 --- a/addons/mrp/tests/test_workorder_operation.py +++ b/addons/mrp/tests/test_workorder_operation.py @@ -94,13 +94,13 @@ def test_00_workorder_process(self): finished_lot =self.env['stock.production.lot'].create({'product_id': production_table.product_id.id}) workorder.write({'final_lot_id': finished_lot.id}) workorder.button_start() - for active_move_line_id in workorder.active_move_line_ids: - if active_move_line_id.product_id.id == product_bolt.id: - active_move_line_id.write({'lot_id': lot_bolt.id, 'qty_done': 1}) - if active_move_line_id.product_id.id == product_table_sheet.id: - active_move_line_id.write({'lot_id': lot_sheet.id, 'qty_done': 1}) - if active_move_line_id.product_id.id == product_table_leg.id: - active_move_line_id.write({'lot_id': lot_leg.id, 'qty_done': 1}) + for workorder_line_id in workorder.workorder_line_ids: + if workorder_line_id.product_id.id == product_bolt.id: + workorder_line_id.write({'lot_id': lot_bolt.id, 'qty_done': 1}) + if workorder_line_id.product_id.id == product_table_sheet.id: + workorder_line_id.write({'lot_id': lot_sheet.id, 'qty_done': 1}) + if workorder_line_id.product_id.id == product_table_leg.id: + workorder_line_id.write({'lot_id': lot_leg.id, 'qty_done': 1}) self.assertEqual(workorder.state, 'progress') workorder.record_production() self.assertEqual(workorder.state, 'done') @@ -197,7 +197,7 @@ def test_00b_workorder_process(self): finished_lot = self.env['stock.production.lot'].create({'product_id': production_table.product_id.id}) workorders[0].write({'final_lot_id': finished_lot.id, 'qty_producing': 1.0}) workorders[0].button_start() - workorders[0].active_move_line_ids[0].write({'lot_id': lot_sheet.id, 'qty_done': 1}) + workorders[0].workorder_line_ids[0].write({'lot_id': lot_sheet.id, 'qty_done': 1}) self.assertEqual(workorders[0].state, 'progress') workorders[0].record_production() @@ -209,7 +209,7 @@ def test_00b_workorder_process(self): # --------------------------------------------------------- workorders[1].button_start() workorders[1].qty_producing = 1.0 - workorders[1].active_move_line_ids[0].write({'lot_id': lot_leg.id, 'qty_done': 4}) + workorders[1].workorder_line_ids[0].write({'lot_id': lot_leg.id, 'qty_done': 4}) workorders[1].record_production() move_leg = production_table.move_raw_ids.filtered(lambda p: p.product_id == product_table_leg) #self.assertEqual(workorders[1].state, 'done') @@ -220,7 +220,7 @@ def test_00b_workorder_process(self): # --------------------------------------------------------- workorders[2].button_start() workorders[2].qty_producing = 1.0 - move_lot = workorders[2].active_move_line_ids[0] + move_lot = workorders[2].workorder_line_ids[0] move_lot.write({'lot_id': lot_bolt.id, 'qty_done': 4}) move_table_bolt = production_table.move_raw_ids.filtered(lambda p: p.product_id.id == product_bolt.id) workorders[2].record_production() @@ -338,11 +338,11 @@ def test_01_without_workorder(self): # Produce 6 Unit of custom laptop will consume ( 12 Unit of keybord and 12 Unit of charger) context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id} product_form = Form(self.env['mrp.product.produce'].with_context(context)) - product_form.product_qty = 6.00 + product_form.qty_producing = 6.00 laptop_lot_001 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id}) - product_form.lot_id = laptop_lot_001 + product_form.final_lot_id = laptop_lot_001 product_consume = product_form.save() - product_consume.produce_line_ids[0].qty_done = 12 + product_consume.workorder_line_ids[0].qty_done = 12 product_consume.do_produce() # Check consumed move after produce 6 quantity of customized laptop. @@ -365,12 +365,12 @@ def test_01_without_workorder(self): # Produce 4 Unit of custom laptop will consume ( 8 Unit of keybord and 8 Unit of charger). context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id} produce_form = Form(self.env['mrp.product.produce'].with_context(context)) - produce_form.product_qty = 4.00 + produce_form.qty_producing = 4.00 laptop_lot_002 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id}) - produce_form.lot_id = laptop_lot_002 + produce_form.final_lot_id = laptop_lot_002 product_consume = produce_form.save() - self.assertEquals(len(product_consume.produce_line_ids), 2) - product_consume.produce_line_ids[0].qty_done = 8 + self.assertEquals(len(product_consume.workorder_line_ids), 2) + product_consume.workorder_line_ids[0].qty_done = 8 product_consume.do_produce() charger_move = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_charger.id and x.state != 'done') keybord_move = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_keybord.id and x.state !='done') @@ -504,13 +504,13 @@ def test_02_different_uom_on_bomlines(self): mo_custom_product.action_assign() context = {"active_ids": [mo_custom_product.id], "active_id": mo_custom_product.id} produce_form = Form(self.env['mrp.product.produce'].with_context(context)) - produce_form.product_qty = 10.00 - produce_form.lot_id = lot_a + produce_form.qty_producing = 10.00 + produce_form.final_lot_id = lot_a product_consume = produce_form.save() # laptop_lot_002 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id}) - self.assertEquals(len(product_consume.produce_line_ids), 2) - product_consume.produce_line_ids.filtered(lambda x : x.product_id == product_C).write({'qty_done': 3000}) - product_consume.produce_line_ids.filtered(lambda x : x.product_id == product_B).write({'qty_done': 20}) + self.assertEquals(len(product_consume.workorder_line_ids), 2) + product_consume.workorder_line_ids.filtered(lambda x: x.product_id == product_C).write({'qty_done': 3000}) + product_consume.workorder_line_ids.filtered(lambda x: x.product_id == product_B).write({'qty_done': 20}) product_consume.do_produce() mo_custom_product.post_inventory() diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml index 570972977656a..be3e4aba4459b 100644 --- a/addons/mrp/views/mrp_workorder_views.xml +++ b/addons/mrp/views/mrp_workorder_views.xml @@ -153,17 +153,14 @@ - + - - - + + + - - - diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py index b92616c9a5052..a701ddb01b0a3 100644 --- a/addons/mrp/wizard/change_production_qty.py +++ b/addons/mrp/wizard/change_production_qty.py @@ -86,7 +86,7 @@ def change_prod_qty(self): quantity = quantity if (quantity > 0) else 0 if float_is_zero(quantity, precision_digits=precision): wo.final_lot_id = False - wo.active_move_line_ids.unlink() + wo.workorder_line_ids.unlink() wo.qty_producing = quantity if wo.qty_produced < wo.qty_production and wo.state == 'done': wo.state = 'progress' @@ -101,6 +101,11 @@ def change_prod_qty(self): moves_finished = production.move_finished_ids.filtered(lambda move: move.operation_id == operation) #TODO: code does nothing, unless maybe by_products? moves_raw.mapped('move_line_ids').write({'workorder_id': wo.id}) (moves_finished + moves_raw).write({'workorder_id': wo.id}) - if quantity > 0 and wo.move_raw_ids.filtered(lambda x: x.product_id.tracking != 'none') and not wo.active_move_line_ids: - wo._generate_lot_ids() + if wo.state not in ('done', 'cancel'): + line_values = wo._update_workorder_lines() + wo.workorder_line_ids |= wo.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) return {} diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py index 309f87abc9a68..b1db108d38060 100644 --- a/addons/mrp/wizard/mrp_product_produce.py +++ b/addons/mrp/wizard/mrp_product_produce.py @@ -4,13 +4,14 @@ from datetime import datetime from odoo import api, fields, models, _ -from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError -from odoo.tools import float_compare, float_round +from odoo.tools import float_compare + class MrpProductProduce(models.TransientModel): _name = "mrp.product.produce" _description = "Record Production" + _inherit = ["mrp.abstract.workorder"] @api.model def default_get(self, fields): @@ -32,21 +33,17 @@ def default_get(self, fields): res['product_uom_id'] = todo_uom if 'serial' in fields: res['serial'] = bool(serial_finished) - if 'product_qty' in fields: - res['product_qty'] = todo_quantity + if 'qty_producing' in fields: + res['qty_producing'] = todo_quantity return res serial = fields.Boolean('Requires Serial') - production_id = fields.Many2one('mrp.production', 'Production') - product_id = fields.Many2one('product.product', 'Product') - product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True) - product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure') - lot_id = fields.Many2one('stock.production.lot', string='Lot/Serial Number') - produce_line_ids = fields.One2many('mrp.product.produce.line', 'product_produce_id', string='Product to Track') - product_tracking = fields.Selection(related="product_id.tracking", readonly=True) + product_tracking = fields.Selection(related="product_id.tracking") is_pending_production = fields.Boolean(compute='_compute_pending_production') + workorder_line_ids = fields.One2many('mrp.product.produce.line', 'product_produce_id') + move_raw_ids = fields.One2many(related='production_id.move_raw_ids') - @api.depends('product_qty') + @api.depends('qty_producing') def _compute_pending_production(self): """ Compute if it exits remaining quantity once the quantity on the current wizard will be processed. The purpose is to display or not @@ -54,7 +51,7 @@ def _compute_pending_production(self): """ for product_produce in self: remaining_qty = product_produce._get_todo(product_produce.production_id) - product_produce.is_pending_production = remaining_qty - product_produce.product_qty > 0.0 + product_produce.is_pending_production = remaining_qty - product_produce.qty_producing > 0.0 def continue_production(self): """ Save current wizard and directly opens a new. """ @@ -67,7 +64,7 @@ def continue_production(self): def action_generate_serial(self): self.ensure_one() product_produce_wiz = self.env.ref('mrp.view_mrp_product_produce_wizard', False) - self.lot_id = self.env['stock.production.lot'].create({ + self.final_lot_id = self.env['stock.production.lot'].create({ 'product_id': self.product_id.id }) return { @@ -88,58 +85,6 @@ def do_produce(self): self._record_production() return {'type': 'ir.actions.act_window_close'} - @api.multi - def check_finished_move_lots(self): - produce_move = self.production_id.move_finished_ids.filtered(lambda x: x.product_id == self.product_id and x.state not in ('done', 'cancel')) - if produce_move and produce_move.product_id.tracking != 'none': - if not self.lot_id: - raise UserError(_('You need to provide a lot for the finished product.')) - existing_move_line = produce_move.move_line_ids.filtered(lambda x: x.lot_id == self.lot_id) - if existing_move_line: - if self.product_id.tracking == 'serial': - raise UserError(_('You cannot produce the same serial number twice.')) - produced_qty = self.product_uom_id._compute_quantity(self.product_qty, existing_move_line.product_uom_id) - existing_move_line.product_uom_qty += produced_qty - existing_move_line.qty_done += produced_qty - else: - vals = { - 'move_id': produce_move.id, - 'product_id': produce_move.product_id.id, - 'production_id': self.production_id.id, - 'product_uom_qty': self.product_qty, - 'product_uom_id': self.product_uom_id.id, - 'qty_done': self.product_qty, - 'lot_id': self.lot_id.id, - 'location_id': produce_move.location_id.id, - 'location_dest_id': produce_move.location_dest_id.id, - } - self.env['stock.move.line'].create(vals) - - for pl in self.produce_line_ids: - if pl.qty_done: - if pl.product_id.tracking != 'none' and not pl.lot_id: - raise UserError(_('Please enter a lot or serial number for %s !' % pl.product_id.display_name)) - if not pl.move_id: - # Find move_id that would match - move_id = self.production_id.move_raw_ids.filtered(lambda m: m.product_id == pl.product_id and m.state not in ('done', 'cancel')) - if move_id: - pl.move_id = move_id - else: - # create a move and put it in there - order = self.production_id - pl.move_id = self.env['stock.move'].create({ - 'name': order.name, - 'product_id': pl.product_id.id, - 'product_uom': pl.product_uom_id.id, - 'location_id': order.location_src_id.id, - 'location_dest_id': self.product_id.property_stock_production.id, - 'raw_material_production_id': order.id, - 'group_id': order.procurement_group_id.id, - 'origin': order.name, - 'state': 'confirmed'}) - pl.move_id._generate_consumed_move_line(pl.qty_done, self.lot_id, lot=pl.lot_id) - return True - def _get_todo(self, production): """ This method will return remaining todo quantity of production. """ main_product_moves = production.move_finished_ids.filtered(lambda x: x.product_id.id == production.product_id.id) @@ -147,105 +92,54 @@ def _get_todo(self, production): todo_quantity = todo_quantity if (todo_quantity > 0) else 0 return todo_quantity + @api.multi def _record_production(self): - # Nothing to do for lots since values are created using default data (stock.move.lots) - quantity = self.product_qty + # Check all the product_produce line have a move id (the user can add product + # to consume directly in the wizard) + for line in self.workorder_line_ids: + if not line.move_id: + order = self.production_id + # Find move_id that would match + move_id = order.move_raw_ids.filtered( + lambda m: m.product_id == line.product_id and m.state not in ('done', 'cancel') + ) + if not move_id: + # create a move to assign it to the line + move_id = self.env['stock.move'].create({ + 'name': order.name, + 'reference': order.name, + 'product_id': line.product_id.id, + 'product_uom': line.product_uom_id.id, + 'location_id': order.location_src_id.id, + 'location_dest_id': line.product_id.property_stock_production.id, + 'raw_material_production_id': order.id, + 'group_id': order.procurement_group_id.id, + 'origin': order.name, + 'state': 'confirmed' + }) + line.move_id = move_id.id + + # Save product produce lines data into stock moves/move lines + quantity = self.qty_producing if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0: raise UserError(_("The production order for '%s' has no quantity specified.") % self.product_id.display_name) - for move in self.production_id.move_finished_ids: - if move.product_id.tracking == 'none' and move.state not in ('done', 'cancel'): - rounding = move.product_uom.rounding - if move.product_id.id == self.production_id.product_id.id: - move.quantity_done += float_round(quantity, precision_rounding=rounding) - elif move.unit_factor: - # byproducts handling - move.quantity_done += float_round(quantity * move.unit_factor, precision_rounding=rounding) - self.check_finished_move_lots() + self._update_finished_move() + self._update_raw_moves() if self.production_id.state == 'confirmed': self.production_id.write({ 'date_start': datetime.now(), }) - @api.onchange('product_qty') - def _onchange_product_qty(self): - lines = [] - qty_todo = self.product_uom_id._compute_quantity(self.product_qty, self.production_id.product_uom_id, round=False) - for move in self.production_id.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel') and (m.bom_line_id or m.product_uom_qty)): - qty_to_consume = float_round(qty_todo * move.unit_factor, precision_rounding=move.product_uom.rounding) - for move_line in move.move_line_ids: - if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0: - break - if move_line.lot_produced_id or float_compare(move_line.product_uom_qty, move_line.qty_done, precision_rounding=move.product_uom.rounding) <= 0: - continue - to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty) - lines.append({ - 'move_id': move.id, - 'qty_to_consume': to_consume_in_line, - 'qty_done': to_consume_in_line, - 'lot_id': move_line.lot_id.id, - 'product_uom_id': move.product_uom.id, - 'product_id': move.product_id.id, - 'qty_reserved': min(to_consume_in_line, move_line.product_uom_qty), - }) - qty_to_consume -= to_consume_in_line - if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: - if move.product_id.tracking == 'serial': - while float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: - lines.append({ - 'move_id': move.id, - 'qty_to_consume': 1, - 'qty_done': 1, - 'product_uom_id': move.product_uom.id, - 'product_id': move.product_id.id, - }) - qty_to_consume -= 1 - else: - lines.append({ - 'move_id': move.id, - 'qty_to_consume': qty_to_consume, - 'qty_done': qty_to_consume, - 'product_uom_id': move.product_uom.id, - 'product_id': move.product_id.id, - }) - - self.produce_line_ids = [(0, 0, x) for x in lines] class MrpProductProduceLine(models.TransientModel): - _name = "mrp.product.produce.line" - _description = "Record Production Line" - - product_produce_id = fields.Many2one('mrp.product.produce') - product_id = fields.Many2one('product.product', 'Product') - product_tracking = fields.Selection(related="product_id.tracking", readonly=False) - lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial Number') - qty_to_consume = fields.Float('To Consume', digits=dp.get_precision('Product Unit of Measure')) - product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure') - qty_done = fields.Float('Consumed') - move_id = fields.Many2one('stock.move') - qty_reserved = fields.Float('Reserved') - - @api.onchange('lot_id') - def _onchange_lot_id(self): - """ When the user is encoding a produce line for a tracked product, we apply some logic to - help him. This onchange will automatically switch `qty_done` to 1.0. - """ - res = {} - if self.product_id.tracking == 'serial': - self.qty_done = 1 - return res + _name = 'mrp.product.produce.line' + _inherit = ["mrp.abstract.workorder.line"] + _description = "Record production line" - @api.onchange('qty_done') - def _onchange_qty_done(self): - """ When the user is encoding a produce line for a tracked product, we apply some logic to - help him. This onchange will warn him if he set `qty_done` to a non-supported value. - """ - res = {} - if self.product_id.tracking == 'serial' and self.qty_done: - if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0: - message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name - res['warning'] = {'title': _('Warning'), 'message': message} - return res + product_produce_id = fields.Many2one('mrp.product.produce', 'Produce wizard') + + def _get_final_lot(self): + return self.product_produce_id.final_lot_id - @api.onchange('product_id') - def _onchange_product_id(self): - self.product_uom_id = self.product_id.uom_id.id + def _get_production(self): + return self.product_produce_id.production_id diff --git a/addons/mrp/wizard/mrp_product_produce_views.xml b/addons/mrp/wizard/mrp_product_produce_views.xml index 964967ad2a0af..2c7954e1a6ecb 100644 --- a/addons/mrp/wizard/mrp_product_produce_views.xml +++ b/addons/mrp/wizard/mrp_product_produce_views.xml @@ -12,22 +12,22 @@ -