[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) <sle@openerp.com>
This commit is contained in:
yhu-odoo
2020-03-17 12:40:26 +00:00
parent 41b83e425b
commit 86f091c0c6
5 changed files with 53 additions and 25 deletions
@@ -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):
@@ -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)
+26 -11
View File
@@ -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()
+16 -4
View File
@@ -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 = [
+1 -1
View File
@@ -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):