Before, when the user pastes a list of tracking numbers into the move
line's `lot_name` field (through the move's detailed view), a lot is
created for each line, creating a move line for each new lot, or
assigning each lot to an existing line with no lot.
With this commit, it will also take `qty_done` and `expiration_date` (if
`product_expiry` is installed) from the pasted list if it's relevant.
Each data should be separate either by a semicolon, either by a tab, and
the first data should always be the lot name.
Valid examples for lots and quantities:
lot-01;22
lot-02 12
Valid examples for lots, expiration date and quantities:
lot-03;8;01-02-2005
lot-04 2005/02/01 12-20
The only valid separation characters for the date are the space (" "),
the dash ("-") and the slash ("/"). Examples of valid dates:
20-02-24 20-Feb-2024
20 02 24 20 Feb 2024
20/02/24 20/Feb/2024
If any of the extra values can't be converted into either a quantity or
a date, the whole line will be used as the tracking number.
Also, if dates are pasted but the product doesn't use any expiration
date, the date's part will be ignored.
task-3171866
closes odoo/odoo#115401
Signed-off-by: Tiffany Chang <tic@odoo.com>
539 lines
23 KiB
Python
539 lines
23 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo import Command
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.tests.common import Form, TransactionCase
|
|
|
|
|
|
class StockGenerateCommon(TransactionCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
Product = cls.env['product.product']
|
|
cls.product_serial = Product.create({
|
|
'name': 'Tracked by SN',
|
|
'type': 'product',
|
|
'tracking': 'serial',
|
|
})
|
|
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
|
|
|
|
cls.warehouse = cls.env['stock.warehouse'].create({
|
|
'name': 'Base Warehouse',
|
|
'reception_steps': 'one_step',
|
|
'delivery_steps': 'ship_only',
|
|
'code': 'BWH'
|
|
})
|
|
cls.location = cls.env['stock.location'].create({
|
|
'name': 'Room A',
|
|
'location_id': cls.warehouse.lot_stock_id.id,
|
|
})
|
|
cls.location_dest = cls.env['stock.location'].create({
|
|
'name': 'Room B',
|
|
'location_id': cls.warehouse.lot_stock_id.id,
|
|
})
|
|
cls.receipt_picking_type = cls.warehouse.in_type_id
|
|
|
|
def get_new_move(self, nbre_of_lines=0, product=False):
|
|
product = product or self.product_serial
|
|
move_lines_vals = []
|
|
self.env['stock.quant']._update_available_quantity(product, self.location, nbre_of_lines)
|
|
for i in range(nbre_of_lines):
|
|
move_lines_vals.append(Command.create({
|
|
'product_id': product.id,
|
|
'product_uom_id': self.uom_unit.id,
|
|
'reserved_uom_qty': 1,
|
|
'location_id': self.location.id,
|
|
'location_dest_id': self.location_dest.id
|
|
}))
|
|
return self.env['stock.move'].create({
|
|
'name': 'Move Test',
|
|
'picking_type_id': self.receipt_picking_type.id,
|
|
'product_id': product.id,
|
|
'product_uom': self.uom_unit.id,
|
|
'location_id': self.location.id,
|
|
'location_dest_id': self.location_dest.id,
|
|
'move_line_ids': move_lines_vals
|
|
})
|
|
|
|
|
|
class StockGenerate(StockGenerateCommon):
|
|
def test_generate_01_sn(self):
|
|
""" Creates a move with 5 move lines, then asks for generates 5 Serial
|
|
Numbers. Checks move has 5 new move lines with each a SN, and the 5
|
|
original move lines are still unchanged.
|
|
"""
|
|
nbre_of_lines = 5
|
|
move = self.get_new_move(nbre_of_lines)
|
|
|
|
form_wizard = Form(self.env['stock.assign.serial'].with_context(
|
|
default_move_id=move.id,
|
|
default_next_serial_number='001',
|
|
default_next_serial_count=nbre_of_lines,
|
|
))
|
|
wiz = form_wizard.save()
|
|
self.assertEqual(len(move.move_line_ids), nbre_of_lines)
|
|
wiz.generate_serial_numbers()
|
|
# Checks new move lines have the right SN
|
|
generated_numbers = ['001', '002', '003', '004', '005']
|
|
self.assertEqual(len(move.move_line_ids), nbre_of_lines + len(generated_numbers))
|
|
for move_line in move.move_line_nosuggest_ids:
|
|
# For a product tracked by SN, the `qty_done` is set on 1 when
|
|
# `lot_name` is set.
|
|
self.assertEqual(move_line.qty_done, 1)
|
|
self.assertEqual(move_line.lot_name, generated_numbers.pop(0))
|
|
# Checks pre-generated move lines didn't change
|
|
for move_line in (move.move_line_ids - move.move_line_nosuggest_ids):
|
|
self.assertEqual(move_line.qty_done, 0)
|
|
self.assertEqual(move_line.lot_name, False)
|
|
|
|
def test_generate_02_prefix_suffix(self):
|
|
""" Generates some Serial Numbers and checks the prefix and/or suffix
|
|
are correctly used.
|
|
"""
|
|
nbre_of_lines = 10
|
|
# Case #1: Prefix, no suffix
|
|
move = self.get_new_move(nbre_of_lines)
|
|
form_wizard = Form(self.env['stock.assign.serial'].with_context(
|
|
default_move_id=move.id,
|
|
default_next_serial_number='bilou-87',
|
|
default_next_serial_count=nbre_of_lines,
|
|
))
|
|
wiz = form_wizard.save()
|
|
wiz.generate_serial_numbers()
|
|
# Checks all move lines have the right SN
|
|
generated_numbers = [
|
|
'bilou-87', 'bilou-88', 'bilou-89', 'bilou-90', 'bilou-91',
|
|
'bilou-92', 'bilou-93', 'bilou-94', 'bilou-95', 'bilou-96'
|
|
]
|
|
for move_line in move.move_line_nosuggest_ids:
|
|
# For a product tracked by SN, the `qty_done` is set on 1 when
|
|
# `lot_name` is set.
|
|
self.assertEqual(move_line.qty_done, 1)
|
|
self.assertEqual(
|
|
move_line.lot_name,
|
|
generated_numbers.pop(0)
|
|
)
|
|
|
|
# Case #2: No prefix, suffix
|
|
move = self.get_new_move(nbre_of_lines)
|
|
form_wizard = Form(self.env['stock.assign.serial'].with_context(
|
|
default_move_id=move.id,
|
|
default_next_serial_number='005-ccc',
|
|
default_next_serial_count=nbre_of_lines,
|
|
))
|
|
wiz = form_wizard.save()
|
|
wiz.generate_serial_numbers()
|
|
# Checks all move lines have the right SN
|
|
generated_numbers = [
|
|
'005-ccc', '006-ccc', '007-ccc', '008-ccc', '009-ccc',
|
|
'010-ccc', '011-ccc', '012-ccc', '013-ccc', '014-ccc'
|
|
]
|
|
for move_line in move.move_line_nosuggest_ids:
|
|
# For a product tracked by SN, the `qty_done` is set on 1 when
|
|
# `lot_name` is set.
|
|
self.assertEqual(move_line.qty_done, 1)
|
|
self.assertEqual(
|
|
move_line.lot_name,
|
|
generated_numbers.pop(0)
|
|
)
|
|
|
|
# Case #3: Prefix + suffix
|
|
move = self.get_new_move(nbre_of_lines)
|
|
form_wizard = Form(self.env['stock.assign.serial'].with_context(
|
|
default_move_id=move.id,
|
|
default_next_serial_number='alpha-012-345-beta',
|
|
default_next_serial_count=nbre_of_lines,
|
|
))
|
|
wiz = form_wizard.save()
|
|
wiz.generate_serial_numbers()
|
|
# Checks all move lines have the right SN
|
|
generated_numbers = [
|
|
'alpha-012-345-beta', 'alpha-012-346-beta', 'alpha-012-347-beta',
|
|
'alpha-012-348-beta', 'alpha-012-349-beta', 'alpha-012-350-beta',
|
|
'alpha-012-351-beta', 'alpha-012-352-beta', 'alpha-012-353-beta',
|
|
'alpha-012-354-beta'
|
|
]
|
|
for move_line in move.move_line_nosuggest_ids:
|
|
# For a product tracked by SN, the `qty_done` is set on 1 when
|
|
# `lot_name` is set.
|
|
self.assertEqual(move_line.qty_done, 1)
|
|
self.assertEqual(
|
|
move_line.lot_name,
|
|
generated_numbers.pop(0)
|
|
)
|
|
|
|
# Case #4: Prefix + suffix, identical number pattern
|
|
move = self.get_new_move(nbre_of_lines)
|
|
form_wizard = Form(self.env['stock.assign.serial'].with_context(
|
|
default_move_id=move.id,
|
|
default_next_serial_number='BAV023B00001S00001',
|
|
default_next_serial_count=nbre_of_lines,
|
|
))
|
|
wiz = form_wizard.save()
|
|
wiz.generate_serial_numbers()
|
|
# Checks all move lines have the right SN
|
|
generated_numbers = [
|
|
'BAV023B00001S00001', 'BAV023B00001S00002', 'BAV023B00001S00003',
|
|
'BAV023B00001S00004', 'BAV023B00001S00005', 'BAV023B00001S00006',
|
|
'BAV023B00001S00007', 'BAV023B00001S00008', 'BAV023B00001S00009',
|
|
'BAV023B00001S00010'
|
|
]
|
|
for move_line in move.move_line_nosuggest_ids:
|
|
# For a product tracked by SN, the `qty_done` is set on 1 when
|
|
# `lot_name` is set.
|
|
self.assertEqual(move_line.qty_done, 1)
|
|
self.assertEqual(
|
|
move_line.lot_name,
|
|
generated_numbers.pop(0)
|
|
)
|
|
|
|
def test_generate_03_raise_exception(self):
|
|
""" Tries to generate some SN but with invalid initial number.
|
|
"""
|
|
move = self.get_new_move(3)
|
|
form_wizard = Form(self.env['stock.assign.serial'].with_context(
|
|
default_move_id=move.id,
|
|
default_next_serial_number='code-xxx',
|
|
))
|
|
|
|
form_wizard.next_serial_count = 0
|
|
# Must raise an exception because `next_serial_count` must be greater than 0.
|
|
with self.assertRaises(ValidationError):
|
|
form_wizard.save()
|
|
|
|
form_wizard.next_serial_count = 3
|
|
wiz = form_wizard.save()
|
|
wiz.generate_serial_numbers()
|
|
self.assertEqual(move.move_line_nosuggest_ids.mapped('lot_name'), ["code-xxx0", "code-xxx1", "code-xxx2"])
|
|
|
|
def test_generate_04_generate_in_multiple_time(self):
|
|
""" Generates a Serial Number for each move lines (except the last one)
|
|
but with multiple assignments, and checks the generated Serial Numbers
|
|
are what we expect.
|
|
"""
|
|
nbre_of_lines = 10
|
|
move = self.get_new_move(nbre_of_lines)
|
|
|
|
form_wizard = Form(self.env['stock.assign.serial'].with_context(
|
|
default_move_id=move.id,
|
|
))
|
|
# First assignment
|
|
form_wizard.next_serial_count = 3
|
|
form_wizard.next_serial_number = '001'
|
|
wiz = form_wizard.save()
|
|
wiz.generate_serial_numbers()
|
|
# Second assignment
|
|
form_wizard.next_serial_count = 2
|
|
form_wizard.next_serial_number = 'bilou-64'
|
|
wiz = form_wizard.save()
|
|
wiz.generate_serial_numbers()
|
|
# Third assignment
|
|
form_wizard.next_serial_count = 4
|
|
form_wizard.next_serial_number = 'ro-1337-bot'
|
|
wiz = form_wizard.save()
|
|
wiz.generate_serial_numbers()
|
|
|
|
# Checks all move lines have the right SN
|
|
generated_numbers = [
|
|
# Correspond to the first assignment
|
|
'001', '002', '003',
|
|
# Correspond to the second assignment
|
|
'bilou-64', 'bilou-65',
|
|
# Correspond to the third assignment
|
|
'ro-1337-bot', 'ro-1338-bot', 'ro-1339-bot', 'ro-1340-bot',
|
|
]
|
|
self.assertEqual(len(move.move_line_ids), nbre_of_lines + len(generated_numbers))
|
|
self.assertEqual(len(move.move_line_nosuggest_ids), len(generated_numbers))
|
|
for move_line in move.move_line_nosuggest_ids:
|
|
self.assertEqual(move_line.qty_done, 1)
|
|
self.assertEqual(move_line.lot_name, generated_numbers.pop(0))
|
|
for move_line in (move.move_line_ids - move.move_line_nosuggest_ids):
|
|
self.assertEqual(move_line.qty_done, 0)
|
|
self.assertEqual(move_line.lot_name, False)
|
|
|
|
def test_generate_with_putaway(self):
|
|
""" Checks the `location_dest_id` of generated move lines is correclty
|
|
set in fonction of defined putaway rules.
|
|
"""
|
|
nbre_of_lines = 4
|
|
shelf_location = self.env['stock.location'].create({
|
|
'name': 'shelf1',
|
|
'usage': 'internal',
|
|
'location_id': self.location_dest.id,
|
|
})
|
|
|
|
# Checks a first time without putaway...
|
|
move = self.get_new_move(nbre_of_lines)
|
|
form_wizard = Form(self.env['stock.assign.serial'].with_context(
|
|
default_move_id=move.id,
|
|
))
|
|
form_wizard.next_serial_count = nbre_of_lines
|
|
form_wizard.next_serial_number = '001'
|
|
wiz = form_wizard.save()
|
|
wiz.generate_serial_numbers()
|
|
|
|
for move_line in move.move_line_nosuggest_ids:
|
|
self.assertEqual(move_line.qty_done, 1)
|
|
# The location dest must be the default one.
|
|
self.assertEqual(move_line.location_dest_id.id, self.location_dest.id)
|
|
|
|
# We need to activate multi-locations to use putaway rules.
|
|
grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
|
|
self.env.user.write({'groups_id': [(4, grp_multi_loc.id)]})
|
|
# Creates a putaway rule
|
|
self.env['stock.putaway.rule'].create({
|
|
'product_id': self.product_serial.id,
|
|
'location_in_id': self.location_dest.id,
|
|
'location_out_id': shelf_location.id,
|
|
})
|
|
|
|
# Checks now with putaway...
|
|
move = self.get_new_move(nbre_of_lines)
|
|
form_wizard = Form(self.env['stock.assign.serial'].with_context(
|
|
default_move_id=move.id,
|
|
))
|
|
form_wizard.next_serial_count = nbre_of_lines
|
|
form_wizard.next_serial_number = '001'
|
|
wiz = form_wizard.save()
|
|
wiz.generate_serial_numbers()
|
|
|
|
for move_line in move.move_line_nosuggest_ids:
|
|
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`.
|
|
"""
|
|
nbre_of_lines = 10
|
|
move = self.get_new_move(nbre_of_lines)
|
|
# We must begin with a move with 10 move lines.
|
|
self.assertEqual(len(move.move_line_ids), nbre_of_lines)
|
|
|
|
value_list = [
|
|
'abc-235',
|
|
'abc-237',
|
|
'abc-238',
|
|
'abc-282',
|
|
'abc-301',
|
|
]
|
|
values = '\n'.join(value_list)
|
|
|
|
move_form = Form(move, view='stock.view_stock_move_nosuggest_operations')
|
|
with move_form.move_line_nosuggest_ids.new() as line:
|
|
line.lot_name = values
|
|
move = move_form.save()
|
|
|
|
# After we set multiple SN, we must have now 15 move lines.
|
|
self.assertEqual(len(move.move_line_ids), nbre_of_lines + len(value_list))
|
|
# Then we look each SN name is correct.
|
|
for move_line in move.move_line_nosuggest_ids:
|
|
self.assertEqual(move_line.lot_name, value_list.pop(0))
|
|
for move_line in (move.move_line_ids - move.move_line_nosuggest_ids):
|
|
self.assertEqual(move_line.lot_name, False)
|
|
|
|
def test_set_multiple_lot_name_02_empty_values(self):
|
|
""" Sets multiple values with some empty lines in one time, then checks
|
|
we haven't create useless move line and all move line's `lot_name` have
|
|
been correctly set.
|
|
"""
|
|
nbre_of_lines = 5
|
|
move = self.get_new_move(nbre_of_lines)
|
|
# We must begin with a move with five move lines.
|
|
self.assertEqual(len(move.move_line_ids), nbre_of_lines)
|
|
|
|
value_list = [
|
|
'',
|
|
'abc-235',
|
|
'',
|
|
'abc-237',
|
|
'',
|
|
'',
|
|
'abc-238',
|
|
'abc-282',
|
|
'abc-301',
|
|
'',
|
|
]
|
|
values = '\n'.join(value_list)
|
|
|
|
# Checks we have more values than move lines.
|
|
self.assertTrue(len(move.move_line_ids) < len(value_list))
|
|
move_form = Form(move, view='stock.view_stock_move_nosuggest_operations')
|
|
with move_form.move_line_nosuggest_ids.new() as line:
|
|
line.lot_name = values
|
|
move = move_form.save()
|
|
|
|
filtered_value_list = list(filter(lambda line: len(line), value_list))
|
|
# After we set multiple SN, we must have a line for each value.
|
|
self.assertEqual(len(move.move_line_ids), nbre_of_lines + len(filtered_value_list))
|
|
# Then we look each SN name is correct.
|
|
for move_line in move.move_line_nosuggest_ids:
|
|
self.assertEqual(move_line.lot_name, filtered_value_list.pop(0))
|
|
for move_line in (move.move_line_ids - move.move_line_nosuggest_ids):
|
|
self.assertEqual(move_line.lot_name, False)
|
|
|
|
def test_set_multiple_lot_name_03_with_quantity(self):
|
|
""" In a move line's `lot_name` field, pastes a list of lots and quantities.
|
|
Checks the values are correctly interpreted when the format is respected,
|
|
and are used only as a lot name when they are erronous.
|
|
"""
|
|
product_lot = self.env['product.product'].create({
|
|
'name': 'Tracked by Lot Numbers',
|
|
'tracking': 'lot',
|
|
'type': 'product',
|
|
})
|
|
list_lot_and_qty = [
|
|
{'lot_name': "ln01", "qty": 2},
|
|
{'lot_name': "ln02", "qty": 3},
|
|
{'lot_name': "ln03", "qty": 6},
|
|
{'lot_name': "ln04", "qty": 4},
|
|
{'lot_name': "ln02", "qty": 5},
|
|
]
|
|
list_using_semicolon = '\n'.join([f'{line["lot_name"]};{line["qty"]}' for line in list_lot_and_qty])
|
|
list_using_tabs = '\n'.join([f'{line["lot_name"]}\t{line["qty"]}' for line in list_lot_and_qty])
|
|
# Runs the same test's asserts with two different lists.
|
|
for stringified_list in [list_using_semicolon, list_using_tabs]:
|
|
move = self.get_new_move(product=product_lot)
|
|
move_form = Form(move, view='stock.view_stock_move_nosuggest_operations')
|
|
with move_form.move_line_nosuggest_ids.new() as line:
|
|
line.lot_name = stringified_list
|
|
move = move_form.save()
|
|
self.assertEqual(len(move.move_line_ids), len(list_lot_and_qty))
|
|
for i, move_line in enumerate(move.move_line_ids):
|
|
self.assertEqual(move_line.lot_name, list_lot_and_qty[i]['lot_name'])
|
|
self.assertEqual(move_line.qty_done, list_lot_and_qty[i]['qty'])
|
|
|
|
# Checks with an erronous list, the move lines have the plain text as lot name.
|
|
erronous_list = [
|
|
"2;ln01",
|
|
"3;ln02",
|
|
"6;ln03",
|
|
"4;ln04",
|
|
"5;ln02",
|
|
]
|
|
move_1 = self.get_new_move(product=product_lot)
|
|
move_form = Form(move_1, view='stock.view_stock_move_nosuggest_operations')
|
|
with move_form.move_line_nosuggest_ids.new() as line:
|
|
line.lot_name = '\n'.join(erronous_list)
|
|
move_1 = move_form.save()
|
|
self.assertEqual(len(move_1.move_line_ids), len(list_lot_and_qty))
|
|
for i, move_line in enumerate(move_1.move_line_ids):
|
|
self.assertEqual(move_line.lot_name, erronous_list[i])
|
|
self.assertEqual(move_line.qty_done, 1)
|
|
|
|
# Does the same, but by giving a done quantity to the move line before pasting the list.
|
|
move_2 = self.get_new_move(product=product_lot)
|
|
move_form = Form(move_2, view='stock.view_stock_move_nosuggest_operations')
|
|
with move_form.move_line_nosuggest_ids.new() as line:
|
|
line.qty_done = 4
|
|
line.lot_name = '\n'.join(erronous_list)
|
|
move_2 = move_form.save()
|
|
self.assertEqual(len(move_2.move_line_ids), len(list_lot_and_qty))
|
|
for i, move_line in enumerate(move_2.move_line_ids):
|
|
self.assertEqual(move_line.lot_name, erronous_list[i])
|
|
self.assertEqual(move_line.qty_done, 4)
|
|
|
|
def test_set_multiple_lot_name_04_serial_with_quantity(self):
|
|
""" In a move line's `lot_name` field, pastes a list of lots and quantities.
|
|
Since the product is tracked by serial number, the quantity should be 1, even if the pasted
|
|
number is greater."""
|
|
product_serial = self.env['product.product'].create({
|
|
'name': 'Tracked by Lot Numbers',
|
|
'tracking': 'serial',
|
|
'type': 'product',
|
|
})
|
|
list_lot_and_qty = [
|
|
{'lot_name': "sn01", "qty": 2},
|
|
{'lot_name': "sn02", "qty": 1},
|
|
{'lot_name': "sn03", "qty": 0},
|
|
{'lot_name': "sn04", "qty": 12},
|
|
]
|
|
list_using_semicolon = '\n'.join([f'{line["lot_name"]};{line["qty"]}' for line in list_lot_and_qty])
|
|
list_using_tabs = '\n'.join([f'{line["lot_name"]}\t{line["qty"]}' for line in list_lot_and_qty])
|
|
# Runs the same test's asserts with two different lists.
|
|
for stringified_list in [list_using_semicolon, list_using_tabs]:
|
|
move = self.get_new_move(product=product_serial)
|
|
move_form = Form(move, view='stock.view_stock_move_nosuggest_operations')
|
|
with move_form.move_line_nosuggest_ids.new() as line:
|
|
line.lot_name = stringified_list
|
|
move = move_form.save()
|
|
self.assertEqual(len(move.move_line_ids), len(list_lot_and_qty))
|
|
for i, move_line in enumerate(move.move_line_ids):
|
|
self.assertEqual(move_line.lot_name, list_lot_and_qty[i]['lot_name'])
|
|
self.assertEqual(move_line.qty_done, 1, "qty can't be greater than 1 for serial")
|
|
|
|
def test_generate_with_putaway_02(self):
|
|
"""
|
|
Suppose a tracked-by-USN product P
|
|
Sub locations in WH/Stock + Storage Category
|
|
The Storage Category adds a capacity constraint (max 1 x P / Location)
|
|
- Plan a receipt with 2 x P
|
|
- Receive 4 x P
|
|
-> The test ensures that the destination locations are correct
|
|
"""
|
|
stock_location = self.warehouse.lot_stock_id
|
|
self.env.user.write({'groups_id': [(4, self.env.ref('stock.group_stock_storage_categories').id)]})
|
|
self.env.user.write({'groups_id': [(4, self.env.ref('stock.group_stock_multi_locations').id)]})
|
|
|
|
# max 1 x product_serial
|
|
stor_category = self.env['stock.storage.category'].create({
|
|
'name': 'Super Storage Category',
|
|
'product_capacity_ids': [(0, 0, {
|
|
'product_id': self.product_serial.id,
|
|
'quantity': 1,
|
|
})]
|
|
})
|
|
|
|
# 5 sub locations with the storage category
|
|
# (the last one should never be used)
|
|
sub_loc_01, sub_loc_02, sub_loc_03, sub_loc_04, dummy = self.env['stock.location'].create([{
|
|
'name': 'Sub Location %s' % i,
|
|
'usage': 'internal',
|
|
'location_id': stock_location.id,
|
|
'storage_category_id': stor_category.id,
|
|
} for i in [1, 2, 3, 4, 5]])
|
|
|
|
self.env['stock.putaway.rule'].create({
|
|
'location_in_id': stock_location.id,
|
|
'location_out_id': stock_location.id,
|
|
'product_id': self.product_serial.id,
|
|
'storage_category_id': stor_category.id,
|
|
})
|
|
|
|
# Receive 1 x P
|
|
receipt_picking = self.env['stock.picking'].create({
|
|
'picking_type_id': self.warehouse.in_type_id.id,
|
|
'location_id': self.env.ref('stock.stock_location_suppliers').id,
|
|
'location_dest_id': stock_location.id,
|
|
})
|
|
move = self.env['stock.move'].create({
|
|
'name': self.product_serial.name,
|
|
'product_id': self.product_serial.id,
|
|
'product_uom': self.product_serial.uom_id.id,
|
|
'product_uom_qty': 2.0,
|
|
'picking_id': receipt_picking.id,
|
|
'location_id': receipt_picking.location_id.id,
|
|
'location_dest_id': receipt_picking.location_dest_id.id,
|
|
})
|
|
receipt_picking.action_confirm()
|
|
|
|
self.assertEqual(move.move_line_ids[0].location_dest_id, sub_loc_01)
|
|
self.assertEqual(move.move_line_ids[1].location_dest_id, sub_loc_02)
|
|
|
|
form_wizard = Form(self.env['stock.assign.serial'].with_context(
|
|
default_move_id=move.id,
|
|
default_next_serial_number='001',
|
|
default_next_serial_count=4,
|
|
))
|
|
wiz = form_wizard.save()
|
|
wiz.generate_serial_numbers()
|
|
|
|
self.assertRecordValues(move.move_line_ids, [
|
|
{'qty_done': 1, 'lot_name': '001', 'location_dest_id': sub_loc_01.id},
|
|
{'qty_done': 1, 'lot_name': '002', 'location_dest_id': sub_loc_02.id},
|
|
{'qty_done': 1, 'lot_name': '003', 'location_dest_id': sub_loc_03.id},
|
|
{'qty_done': 1, 'lot_name': '004', 'location_dest_id': sub_loc_04.id},
|
|
])
|