From 86f091c0c6f617285b6464ddf4ffaeb614f9f204 Mon Sep 17 00:00:00 2001 From: yhu-odoo Date: Fri, 31 Jan 2020 10:40:09 +0000 Subject: [PATCH] [IMP]stock,product_expiry: improve error message for sn number check ======= PURPOSE ======= Improve duplicated serial number message to show more information. ======= SPEC ======= In this commit, we show full information about all invalide product name and lot/sn number when the user enters duplicated ones. - Change the sql constraint for checking duplicate sn/lot to python constraint to support customised error message. - Create lot in batch to show all invalide ones in error message. ======= LINKS ======= task 2083605 pr #44366 Related: odoo/upgrade#954 Signed-off-by: Simon Lejeune (sle) --- .../product_expiry/models/production_lot.py | 15 ++++---- .../product_expiry/models/stock_move_line.py | 4 +- addons/stock/models/stock_move_line.py | 37 +++++++++++++------ addons/stock/models/stock_production_lot.py | 20 ++++++++-- addons/stock/models/stock_quant.py | 2 +- 5 files changed, 53 insertions(+), 25 deletions(-) diff --git a/addons/product_expiry/models/production_lot.py b/addons/product_expiry/models/production_lot.py index 122c59bf7ba..c84d6171048 100644 --- a/addons/product_expiry/models/production_lot.py +++ b/addons/product_expiry/models/production_lot.py @@ -48,13 +48,14 @@ class StockProductionLot(models.Model): return res # Assign dates according to products data - @api.model - def create(self, vals): - dates = self._get_dates(vals.get('product_id') or self.env.context.get('default_product_id')) - for d in dates: - if not vals.get(d): - vals[d] = dates[d] - return super(StockProductionLot, self).create(vals) + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + dates = self._get_dates(vals.get('product_id') or self.env.context.get('default_product_id')) + for d in dates: + if not vals.get(d): + vals[d] = dates[d] + return super().create(vals_list) @api.onchange('expiration_date') def _onchange_expiration_date(self): diff --git a/addons/product_expiry/models/stock_move_line.py b/addons/product_expiry/models/stock_move_line.py index 49e9a6fc180..b71993d89a6 100644 --- a/addons/product_expiry/models/stock_move_line.py +++ b/addons/product_expiry/models/stock_move_line.py @@ -32,6 +32,6 @@ class StockMoveLine(models.Model): else: self.expiration_date = False - def _create_and_assign_production_lot(self): - super(StockMoveLine, self)._create_and_assign_production_lot() + def _assign_production_lot(self, lot): + super()._assign_production_lot(lot) self.lot_id._update_date_values(self.expiration_date) diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index c5365b87a1f..86e33b9cadc 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -396,6 +396,8 @@ class StockMoveLine(models.Model): # the line. It is mandatory in order to free the reservation and correctly apply # `action_done` on the next move lines. ml_to_delete = self.env['stock.move.line'] + ml_to_create_lot = self.env['stock.move.line'] + tracked_ml_without_lot = self.env['stock.move.line'] for ml in self: # Check here if `ml.qty_done` respects the rounding of `ml.product_uom_id`. uom_qty = float_round(ml.qty_done, precision_rounding=ml.product_uom_id.rounding, rounding_method='HALF-UP') @@ -415,7 +417,8 @@ class StockMoveLine(models.Model): # If a picking type is linked, we may have to create a production lot on # the fly before assigning it to the move line if the user checked both # `use_create_lots` and `use_existing_lots`. - ml._create_and_assign_production_lot() + if ml.lot_name: + ml_to_create_lot |= ml elif not picking_type_id.use_create_lots and not picking_type_id.use_existing_lots: # If the user disabled both `use_create_lots` and `use_existing_lots` # checkboxes on the picking type, he's allowed to enter tracked @@ -426,12 +429,18 @@ class StockMoveLine(models.Model): # tracked products without a `lot_id`. continue - if not ml.lot_id: - raise UserError(_('You need to supply a Lot/Serial number for product %s.') % ml.product_id.display_name) + if not ml.lot_id and ml not in ml_to_create_lot: + tracked_ml_without_lot |= ml elif qty_done_float_compared < 0: raise UserError(_('No negative quantities allowed')) else: ml_to_delete |= ml + + if tracked_ml_without_lot: + raise UserError(_('You need to supply a Lot/Serial Number for product: \n - ') + + '\n - '.join(tracked_ml_without_lot.mapped('product_id.display_name'))) + ml_to_create_lot._create_and_assign_production_lot() + ml_to_delete.unlink() (self - ml_to_delete)._check_company() @@ -481,15 +490,21 @@ class StockMoveLine(models.Model): return lines def _create_and_assign_production_lot(self): - """ Creates and assign a new production lot of the move line.""" + """ Creates and assign new production lots for move lines.""" + lot_vals = [{ + 'company_id': ml.move_id.company_id.id, + 'name': ml.lot_name, + 'product_id': ml.product_id.id, + } for ml in self] + lots = self.env['stock.production.lot'].create(lot_vals) + for ml, lot in zip(self, lots): + ml._assign_production_lot(lot) + + def _assign_production_lot(self, lot): self.ensure_one() - if self.lot_name and not self.lot_id: - lot = self.env['stock.production.lot'].create({ - 'company_id': self.move_id.company_id.id, - 'name': self.lot_name, - 'product_id': self.product_id.id, - }) - self.write({'lot_id': lot.id}) + self.write({ + 'lot_id': lot.id + }) def _reservation_is_updatable(self, quantity, reserved_quant): self.ensure_one() diff --git a/addons/stock/models/stock_production_lot.py b/addons/stock/models/stock_production_lot.py index 40df922447c..004a51b7c03 100644 --- a/addons/stock/models/stock_production_lot.py +++ b/addons/stock/models/stock_production_lot.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import _, api, fields, models -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError class ProductionLot(models.Model): @@ -27,9 +27,21 @@ class ProductionLot(models.Model): display_complete = fields.Boolean(compute='_compute_display_complete') company_id = fields.Many2one('res.company', 'Company', required=True, store=True, index=True) - _sql_constraints = [ - ('name_ref_uniq', 'unique (name, product_id, company_id)', 'The combination of serial number and product must be unique across a company !'), - ] + @api.constrains('name', 'product_id', 'company_id') + def _check_unique_lot(self): + domain = [('product_id', 'in', self.product_id.ids), + ('company_id', 'in', self.company_id.ids), + ('name', 'in', self.mapped('name'))] + fields = ['company_id', 'product_id', 'name'] + groupby = ['company_id', 'product_id', 'name'] + records = self.read_group(domain, fields, groupby, lazy=False) + error_message_lines = [] + for rec in records: + if rec['__count'] != 1: + product_name = self.env['product.product'].browse(rec['product_id'][0]).display_name + error_message_lines.append(_(" - Product: %s, Serial Number: %s") % (product_name, rec['name'])) + if error_message_lines: + raise ValidationError(_('The combination of serial number and product must be unique across a company.\nFollowing combination contains duplicates:\n') + '\n'.join(error_message_lines)) def _domain_product_id(self): domain = [ diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index 73b90d4d5b6..f819e85ad84 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -249,7 +249,7 @@ class StockQuant(models.Model): def check_quantity(self): for quant in self: if float_compare(quant.quantity, 1, precision_rounding=quant.product_uom_id.rounding) > 0 and quant.lot_id and quant.product_id.tracking == 'serial': - raise ValidationError(_('A serial number should only be linked to a single product.')) + raise ValidationError(_('The serial number has already been assigned: \n Product: %s, Serial Number: %s') % (quant.product_id.display_name, quant.lot_id.name)) @api.constrains('location_id') def check_location_id(self):