[FIX] stock: avoid attempt to create the same lot

How to reproduce:
  - Create a product tracked by lot;
  - Create two receipts for this product;
  - Use the same lot name in the two receipts;
  - Try to validate the two receipts in the same time
    => `ValidationError` will trigger in the `_check_unique_lot`
        constrain of the production lot.

task-2646107

X-original-commit: cf018e9f215336ef19e0a517d4e116f0da26850e
Part-of: odoo/odoo#78189
This commit is contained in:
svs-odoo
2021-10-14 08:12:43 +00:00
parent f0b3250945
commit 714f2b49ce
5 changed files with 142 additions and 18 deletions
@@ -33,4 +33,4 @@ class StockMoveLine(models.Model):
def _assign_production_lot(self, lot):
super()._assign_production_lot(lot)
self.lot_id._update_date_values(self.expiration_date)
self.lot_id._update_date_values(self[0].expiration_date)
+20 -12
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import Counter
from collections import Counter, defaultdict
from odoo import _, api, fields, tools, models
from odoo.exceptions import UserError, ValidationError
@@ -596,20 +596,28 @@ class StockMoveLine(models.Model):
def _create_and_assign_production_lot(self):
""" 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]
lot_vals = []
# It is possible to have multiple time the same lot to create & assign,
# so we handle the case with 2 dictionaries.
key_to_index = {} # key to index of the lot
key_to_mls = defaultdict(lambda: self.env['stock.move.line']) # key to all mls
for ml in self:
key = (ml.company_id.id, ml.product_id.id, ml.lot_name)
key_to_mls[key] |= ml
if ml.tracking != 'lot' or key not in key_to_index:
key_to_index[key] = len(lot_vals)
lot_vals.append({
'company_id': ml.company_id.id,
'name': ml.lot_name,
'product_id': ml.product_id.id
})
lots = self.env['stock.production.lot'].create(lot_vals)
for ml, lot in zip(self, lots):
ml._assign_production_lot(lot)
for key, mls in key_to_mls.items():
mls._assign_production_lot(lots[key_to_index[key]].with_prefetch(lots._ids)) # With prefetch to reconstruct the ones broke by accessing by index
def _assign_production_lot(self, lot):
self.ensure_one()
self.write({
'lot_id': lot.id
})
self.write({'lot_id': lot.id})
def _reservation_is_updatable(self, quantity, reserved_quant):
self.ensure_one()
@@ -295,6 +295,7 @@ class StockGenerate(TransactionCase):
self.assertEqual(move_line.qty_done, 1)
# The location dest must be now the one from the putaway.
self.assertEqual(move_line.location_dest_id.id, shelf_location.id)
def test_set_multiple_lot_name_01(self):
""" Sets five SN in one time in stock move view form, then checks move
has five new move lines with the right `lot_name`.
-1
View File
@@ -9,7 +9,6 @@ from odoo.exceptions import UserError
from odoo.tests import Form
from odoo.tools import float_is_zero, float_compare
from odoo.tests.common import Form
class TestPickShip(TestStockCommon):
def create_pick_ship(self):
+120 -4
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from odoo.addons.stock.tests.common import TestStockCommon
from odoo.exceptions import ValidationError
from odoo.tests import Form
from odoo.tools import mute_logger, float_round
from odoo import fields
@@ -16,12 +17,12 @@ class TestStockFlow(TestStockCommon):
'name': 'My Company (Chicago)-demo',
'email': 'chicago@yourcompany.com',
'company_id': False,
})
})
cls.company = cls.env['res.company'].create({
'currency_id': cls.env.ref('base.USD').id,
'partner_id': cls.partner_company2.id,
'name': 'My Company (Chicago)-demo',
})
})
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
def test_00_picking_create_and_transfer_quantity(self):
@@ -106,7 +107,7 @@ class TestStockFlow(TestStockCommon):
'location_dest_id': self.stock_location,
'move_id': move_c.id,
'lot_id': lot2_productC.id,
})
})
self.StockPackObj.create({
'product_id': self.productD.id,
'qty_done': 2,
@@ -114,7 +115,7 @@ class TestStockFlow(TestStockCommon):
'location_id': self.supplier_location,
'location_dest_id': self.stock_location,
'move_id': move_d.id
})
})
# Check incoming shipment total quantity of pack operation
total_qty = sum(self.StockPackObj.search([('move_id', 'in', picking_in.move_lines.ids)]).mapped('qty_done'))
@@ -1904,3 +1905,118 @@ class TestStockFlow(TestStockCommon):
picking = picking_form.save()
self.assertEqual(picking.state, 'draft')
def test_validate_multiple_pickings_with_same_lot_names(self):
""" Checks only one lot is created when the same lot name is used in
different pickings and those pickings are validated together.
"""
# Creates two tracked products (one by lots and one by SN).
product_lot = self.env['product.product'].create({
'name': 'Tracked by lot',
'type': 'product',
'tracking': 'lot',
})
product_serial = self.env['product.product'].create({
'name': 'Tracked by SN',
'type': 'product',
'tracking': 'serial',
})
# Creates two receipts using some lot names in common.
picking_type = self.env['stock.picking.type'].browse(self.picking_type_in)
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = picking_type
with picking_form.move_ids_without_package.new() as move:
move.product_id = product_lot
move.product_uom_qty = 8
receipt_1 = picking_form.save()
receipt_1.action_confirm()
move_form = Form(receipt_1.move_lines, view="stock.view_stock_move_operations")
with move_form.move_line_ids.edit(0) as line:
line.lot_name = 'lot-001'
line.qty_done = 3
with move_form.move_line_ids.new() as line:
line.lot_name = 'lot-002'
line.qty_done = 3
with move_form.move_line_ids.new() as line:
line.lot_name = 'lot-003'
line.qty_done = 2
move = move_form.save()
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = picking_type
with picking_form.move_ids_without_package.new() as move:
move.product_id = product_lot
move.product_uom_qty = 8
receipt_2 = picking_form.save()
receipt_2.action_confirm()
move_form = Form(receipt_2.move_lines, view="stock.view_stock_move_operations")
with move_form.move_line_ids.edit(0) as line:
line.lot_name = 'lot-003'
line.qty_done = 2
with move_form.move_line_ids.new() as line:
line.lot_name = 'lot-004'
line.qty_done = 4
with move_form.move_line_ids.new() as line:
line.lot_name = 'lot-001'
line.qty_done = 1
with move_form.move_line_ids.new() as line:
line.lot_name = 'lot-005'
line.qty_done = 1
move = move_form.save()
# Validates the two receipts and checks the move lines' lot.
(receipt_1 | receipt_2).button_validate()
lots = self.env['stock.production.lot'].search([('product_id', '=', product_lot.id)])
self.assertEqual(len(lots), 5)
lot1, lot2, lot3, lot4, lot5 = lots
self.assertEqual(lot1.name, 'lot-001')
self.assertEqual(lot2.name, 'lot-002')
self.assertEqual(lot3.name, 'lot-003')
self.assertEqual(lot4.name, 'lot-004')
self.assertEqual(lot5.name, 'lot-005')
self.assertEqual(receipt_1.move_line_ids[0].lot_id.id, lot1.id)
self.assertEqual(receipt_1.move_line_ids[1].lot_id.id, lot2.id)
self.assertEqual(receipt_1.move_line_ids[2].lot_id.id, lot3.id)
self.assertEqual(receipt_2.move_line_ids[0].lot_id.id, lot3.id)
self.assertEqual(receipt_2.move_line_ids[1].lot_id.id, lot4.id)
self.assertEqual(receipt_2.move_line_ids[2].lot_id.id, lot1.id)
self.assertEqual(receipt_2.move_line_ids[3].lot_id.id, lot5.id)
# Checks also it still raise an error when it tries to create multiple time
# the same serial numbers (same scenario but with SN instead of lots).
picking_type = self.env['stock.picking.type'].browse(self.picking_type_in)
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = picking_type
with picking_form.move_ids_without_package.new() as move:
move.product_id = product_serial
move.product_uom_qty = 2
receipt_1 = picking_form.save()
receipt_1.action_confirm()
move_form = Form(receipt_1.move_lines, view="stock.view_stock_move_operations")
with move_form.move_line_ids.edit(0) as line:
line.lot_name = 'sn-001'
with move_form.move_line_ids.new() as line:
line.lot_name = 'sn-002'
move = move_form.save()
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = picking_type
with picking_form.move_ids_without_package.new() as move:
move.product_id = product_serial
move.product_uom_qty = 2
receipt_2 = picking_form.save()
receipt_2.action_confirm()
move_form = Form(receipt_2.move_lines, view="stock.view_stock_move_operations")
with move_form.move_line_ids.edit(0) as line:
line.lot_name = 'sn-002'
with move_form.move_line_ids.new() as line:
line.lot_name = 'sn-001'
move = move_form.save()
# Validates the two receipts => It should raise an error as there is duplicate SN.
with self.assertRaises(ValidationError):
(receipt_1 | receipt_2).button_validate()