[FIX] stock: check SN uniqueness by location
To reproduce the issue: 1. In Settings, enable: - Storage Locations - Package 2. Create a product P - Type: Storable - Tracked by SN 3. Process a receipt with 1 x P, serial S 4. Return P in a package 5. Process a new receipt with 1 x P, still S as SN Error: An error is displayed "The serial number has already been assigned [...]". This is incorrect, the user should be able to receive that SN. After step 3, there exists a quant Q1: -1 x P at Supplier Location with S. Then, after step 4, a new quant Q2 is created: 1 x P at Supplier Location with S and the package. Because the field `package_id` is not the same on Q1 and Q2, both quants are not merged. As a result, step 5, we will update Q1 and have -2 x P at Supplier Location with S. This will trigger the constraint, hence the error message. OPW-3390615 closes odoo/odoo#134902 X-original-commit: 0459e42ac452996831b0443bce088b84735975f0 Signed-off-by: William Henrotin (whe) <whe@odoo.com> Signed-off-by: Adrien Widart (awt) <awt@odoo.com>
This commit is contained in:
@@ -560,10 +560,21 @@ class StockQuant(models.Model):
|
||||
|
||||
@api.constrains('quantity')
|
||||
def check_quantity(self):
|
||||
for quant in self:
|
||||
if quant.location_id.usage != 'inventory' and quant.lot_id and quant.product_id.tracking == 'serial' \
|
||||
and float_compare(abs(quant.quantity), 1, precision_rounding=quant.product_uom_id.rounding) > 0:
|
||||
raise ValidationError(_('The serial number has already been assigned: \n Product: %s, Serial Number: %s') % (quant.product_id.display_name, quant.lot_id.name))
|
||||
sn_quants = self.filtered(lambda q: q.product_id.tracking == 'serial' and q.location_id.usage != 'inventory' and q.lot_id)
|
||||
if not sn_quants:
|
||||
return
|
||||
domain = expression.OR([
|
||||
[('product_id', '=', q.product_id.id), ('location_id', '=', q.location_id.id), ('lot_id', '=', q.lot_id.id)]
|
||||
for q in sn_quants
|
||||
])
|
||||
groups = self._read_group(
|
||||
domain,
|
||||
['product_id', 'location_id', 'lot_id'],
|
||||
['quantity:sum'],
|
||||
)
|
||||
for product, _location, lot, qty in groups:
|
||||
if float_compare(abs(qty), 1, precision_rounding=product.uom_id.rounding) > 0:
|
||||
raise ValidationError(_('The serial number has already been assigned: \n Product: %s, Serial Number: %s') % (product.display_name, lot.name))
|
||||
|
||||
@api.constrains('location_id')
|
||||
def check_location_id(self):
|
||||
|
||||
@@ -7,7 +7,7 @@ from unittest.mock import patch
|
||||
from odoo import fields
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tests.common import Form, TransactionCase
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
|
||||
|
||||
@@ -819,6 +819,75 @@ class StockQuant(TransactionCase):
|
||||
else:
|
||||
self.assertEqual(q.display_name, '%s' % (q.location_id.display_name))
|
||||
|
||||
def test_serial_constraint_with_package_and_return(self):
|
||||
"""
|
||||
Receive product with serial S
|
||||
Return it in a package
|
||||
Confirm a new receipt with S
|
||||
"""
|
||||
stock_location = self.env['stock.warehouse'].search([], limit=1).lot_stock_id
|
||||
supplier_location = self.env.ref('stock.stock_location_suppliers')
|
||||
picking_type_in = self.env.ref('stock.picking_type_in')
|
||||
|
||||
receipt01 = self.env['stock.picking'].create({
|
||||
'picking_type_id': picking_type_in.id,
|
||||
'location_id': supplier_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'move_ids': [(0, 0, {
|
||||
'name': self.product_serial.name,
|
||||
'product_id': self.product_serial.id,
|
||||
'location_id': supplier_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.product_serial.uom_id.id,
|
||||
})],
|
||||
})
|
||||
receipt01.action_confirm()
|
||||
receipt01.move_line_ids.write({
|
||||
'lot_name': 'Michel',
|
||||
'qty_done': 1.0
|
||||
})
|
||||
receipt01.button_validate()
|
||||
|
||||
quant = self.env['stock.quant'].search([('product_id', '=', self.product_serial.id), ('location_id', '=', stock_location.id)])
|
||||
|
||||
wizard_form = Form(self.env['stock.return.picking'].with_context(active_ids=receipt01.ids, active_id=receipt01.ids[0], active_model='stock.picking'))
|
||||
wizard = wizard_form.save()
|
||||
wizard.product_return_moves.quantity = 1.0
|
||||
stock_return_picking_action = wizard.create_returns()
|
||||
|
||||
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||
return_pick.move_ids.move_line_ids.qty_done = 1.0
|
||||
return_pick.action_put_in_pack()
|
||||
return_pick._action_done()
|
||||
|
||||
self.assertEqual(return_pick.move_line_ids.lot_id, quant.lot_id)
|
||||
self.assertTrue(return_pick.move_line_ids.result_package_id, quant.lot_id)
|
||||
|
||||
receipt02 = self.env['stock.picking'].create({
|
||||
'picking_type_id': picking_type_in.id,
|
||||
'location_id': supplier_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'move_ids': [(0, 0, {
|
||||
'name': self.product_serial.name,
|
||||
'product_id': self.product_serial.id,
|
||||
'location_id': supplier_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.product_serial.uom_id.id,
|
||||
})],
|
||||
})
|
||||
receipt02.action_confirm()
|
||||
receipt02.move_line_ids.write({
|
||||
'lot_name': 'Michel',
|
||||
'qty_done': 1.0
|
||||
})
|
||||
receipt02.button_validate()
|
||||
|
||||
quant = self.env['stock.quant'].search([('product_id', '=', self.product_serial.id), ('location_id', '=', stock_location.id)])
|
||||
self.assertEqual(len(quant), 1)
|
||||
self.assertEqual(quant.lot_id.name, 'Michel')
|
||||
|
||||
class StockQuantRemovalStrategy(TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
Reference in New Issue
Block a user