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