[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:
Adrien Widart (awt)
2023-09-08 16:48:05 +00:00
parent a907a220fc
commit 02347e4620
2 changed files with 85 additions and 5 deletions
+15 -4
View File
@@ -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):
+70 -1
View File
@@ -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()