diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index f9436f66870..73f4c861727 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -195,6 +195,12 @@ class StockMove(models.Model): 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: diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py index 33ffb3d48f9..15c1684561e 100644 --- a/addons/mrp/wizard/mrp_product_produce.py +++ b/addons/mrp/wizard/mrp_product_produce.py @@ -125,6 +125,8 @@ class MrpProductProduce(models.TransientModel): 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.')) existing_move_line.product_uom_qty += self.product_qty existing_move_line.qty_done += self.product_qty else: @@ -181,27 +183,25 @@ class MrpProductProduceLine(models.TransientModel): @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 - @api.constrains('lot_id') - def _check_lot_id(self): - for ml in self: - if ml.product_id.tracking == 'serial': - produce_lines_to_check = ml.product_produce_id.produce_line_ids.filtered(lambda l: l.product_id == ml.product_id and l.lot_id) - message = produce_lines_to_check._check_for_duplicated_serial_numbers() - if message: - raise ValidationError(message) - - def _check_for_duplicated_serial_numbers(self): - if self.mapped('lot_id'): - lots_map = [(ml.product_id.id, ml.lot_id.name) for ml in self] - recorded_serials_counter = Counter(lots_map) - for (product_id, lot_id), occurrences in recorded_serials_counter.items(): - if occurrences > 1 and lot_id is not False: - return _('You cannot consume the same serial number twice. Please correct the serial numbers encoded.') + @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': + 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 for products with unique serial number.') % self.product_id.uom_id.name + res['warning'] = {'title': _('Warning'), 'message': message} + return res @api.onchange('product_id') def _onchange_product_id(self): diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index 4582c907575..cb9244e4511 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -109,36 +109,49 @@ class StockMoveLine(models.Model): @api.onchange('lot_name', 'lot_id') def onchange_serial_number(self): + """ When the user is encoding a move line for a tracked product, we apply some logic to + help him. This includes: + - automatically switch `qty_done` to 1.0 + - warn if he has already encoded `lot_name` in another move line + """ res = {} if self.product_id.tracking == 'serial': - self.qty_done = 1 - move_lines_to_check = self._get_similar_move_lines() - self - message = move_lines_to_check._check_for_duplicated_serial_numbers() + if not self.qty_done: + self.qty_done = 1 + + message = None + if self.lot_name or self.lot_id: + move_lines_to_check = self._get_similar_move_lines() - self + if self.lot_name: + counter = Counter(move_lines_to_check.mapped('lot_name')) + if counter.get(self.lot_name) and counter[self.lot_name] > 1: + message = _('You cannot use the same serial number twice. Please correct the serial numbers encoded.') + elif self.lot_id: + counter = Counter(move_lines_to_check.mapped('lot_id.id')) + if counter.get(self.lot_id.id) and counter[self.lot_id.id] > 1: + message = _('You cannot use the same serial number twice. Please correct the serial numbers encoded.') + if message: res['warning'] = {'title': _('Warning'), 'message': message} return res + @api.onchange('qty_done') + def _onchange_qty_done(self): + """ When the user is encoding a move 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': + 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 for products with unique serial number.') % self.product_id.uom_id.name + res['warning'] = {'title': _('Warning'), 'message': message} + return res + @api.constrains('qty_done') def _check_positive_qty_done(self): if any([ml.qty_done < 0 for ml in self]): raise ValidationError(_('You can not enter negative quantities!')) - @api.constrains('lot_id', 'lot_name', 'qty_done') - def _check_unique_serial_number(self): - for ml in self.filtered(lambda ml: ml.move_id.product_id.tracking == 'serial' and (ml.lot_id or ml.lot_name)): - move_lines_to_check = ml._get_similar_move_lines() - message = move_lines_to_check._check_for_duplicated_serial_numbers() - if message: - raise ValidationError(message) - if float_compare(ml.qty_done, 1.0, precision_rounding=ml.move_id.product_id.uom_id.rounding) == 1: - raise UserError(_( - 'You can only process 1.0 %s for products with unique serial number.') % ml.product_id.uom_id.name) - if ml.lot_name: - already_exist = self.env['stock.production.lot'].search( - [('name', '=', ml.lot_name), ('product_id', '=', ml.product_id.id)]) - if already_exist: - return _('You have already assigned this serial number to this product. Please correct the serial numbers encoded.') - def _get_similar_move_lines(self): self.ensure_one() lines = self.env['stock.move.line'] @@ -147,25 +160,6 @@ class StockMoveLine(models.Model): lines |= picking_id.move_line_ids.filtered(lambda ml: ml.product_id == self.product_id and (ml.lot_id or ml.lot_name)) return lines - def _check_for_duplicated_serial_numbers(self): - """ This method is used in _check_unique_serial_number and in onchange_serial_number to check that a same serial number is not used twice amongst the recordset passed. - - :return: an error message directed to the user if needed else False - """ - if self.mapped('lot_id'): - lots_map = [(ml.product_id.id, ml.lot_id.name) for ml in self] - recorded_serials_counter = Counter(lots_map) - for (product_id, lot_id), occurrences in recorded_serials_counter.items(): - if occurrences > 1 and lot_id is not False: - return _('You cannot use the same serial number twice. Please correct the serial numbers encoded.') - elif self.mapped('lot_name'): - lots_map = [(ml.product_id.id, ml.lot_name) for ml in self] - recorded_serials_counter = Counter(lots_map) - for (product_id, lot_id), occurrences in recorded_serials_counter.items(): - if occurrences > 1 and lot_id is not False: - return _('You cannot use the same serial number twice. Please correct the serial numbers encoded.') - return False - @api.model def create(self, vals): vals['ordered_qty'] = vals.get('product_uom_qty')