[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:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user