summaryrefslogtreecommitdiff
path: root/addons/stock/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/stock/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/stock/tests')
-rw-r--r--addons/stock/tests/__init__.py21
-rw-r--r--addons/stock/tests/common.py98
-rw-r--r--addons/stock/tests/common2.py57
-rw-r--r--addons/stock/tests/test_generate_serial_numbers.py377
-rw-r--r--addons/stock/tests/test_inventory.py804
-rw-r--r--addons/stock/tests/test_move.py4574
-rw-r--r--addons/stock/tests/test_move2.py3146
-rw-r--r--addons/stock/tests/test_multicompany.py598
-rw-r--r--addons/stock/tests/test_packing.py801
-rw-r--r--addons/stock/tests/test_packing_neg.py183
-rw-r--r--addons/stock/tests/test_proc_rule.py410
-rw-r--r--addons/stock/tests/test_product.py117
-rw-r--r--addons/stock/tests/test_quant.py662
-rw-r--r--addons/stock/tests/test_quant_inventory_mode.py222
-rw-r--r--addons/stock/tests/test_report.py1043
-rw-r--r--addons/stock/tests/test_report_stock_quantity.py146
-rw-r--r--addons/stock/tests/test_report_tours.py19
-rw-r--r--addons/stock/tests/test_robustness.py223
-rw-r--r--addons/stock/tests/test_stock_flow.py1978
-rw-r--r--addons/stock/tests/test_stock_location_search.py36
-rw-r--r--addons/stock/tests/test_warehouse.py633
-rw-r--r--addons/stock/tests/test_wise_operator.py190
22 files changed, 16338 insertions, 0 deletions
diff --git a/addons/stock/tests/__init__.py b/addons/stock/tests/__init__.py
new file mode 100644
index 00000000..cc3d2dfa
--- /dev/null
+++ b/addons/stock/tests/__init__.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+
+from . import test_stock_flow
+from . import test_product
+from . import test_warehouse
+from . import test_stock_location_search
+from . import test_quant
+from . import test_quant_inventory_mode
+from . import test_generate_serial_numbers
+from . import test_inventory
+from . import test_move
+from . import test_move2
+from . import test_multicompany
+from . import test_robustness
+from . import test_packing
+from . import test_packing_neg
+from . import test_proc_rule
+from . import test_wise_operator
+from . import test_report
+from . import test_report_stock_quantity
+from . import test_report_tours
diff --git a/addons/stock/tests/common.py b/addons/stock/tests/common.py
new file mode 100644
index 00000000..e05394c3
--- /dev/null
+++ b/addons/stock/tests/common.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+
+from odoo.tests import common
+
+
+class TestStockCommon(common.SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestStockCommon, cls).setUpClass()
+
+ cls.ProductObj = cls.env['product.product']
+ cls.UomObj = cls.env['uom.uom']
+ cls.PartnerObj = cls.env['res.partner']
+ cls.ModelDataObj = cls.env['ir.model.data']
+ cls.StockPackObj = cls.env['stock.move.line']
+ cls.StockQuantObj = cls.env['stock.quant']
+ cls.PickingObj = cls.env['stock.picking']
+ cls.MoveObj = cls.env['stock.move']
+ cls.InvObj = cls.env['stock.inventory']
+ cls.InvLineObj = cls.env['stock.inventory.line']
+ cls.LotObj = cls.env['stock.production.lot']
+
+ # Model Data
+ cls.picking_type_in = cls.ModelDataObj.xmlid_to_res_id('stock.picking_type_in')
+ cls.picking_type_out = cls.ModelDataObj.xmlid_to_res_id('stock.picking_type_out')
+ cls.supplier_location = cls.ModelDataObj.xmlid_to_res_id('stock.stock_location_suppliers')
+ cls.stock_location = cls.ModelDataObj.xmlid_to_res_id('stock.stock_location_stock')
+ pack_location = cls.env.ref('stock.location_pack_zone')
+ pack_location.active = True
+ cls.pack_location = pack_location.id
+ output_location = cls.env.ref('stock.stock_location_output')
+ output_location.active = True
+ cls.output_location = output_location.id
+ cls.customer_location = cls.ModelDataObj.xmlid_to_res_id('stock.stock_location_customers')
+ cls.categ_unit = cls.ModelDataObj.xmlid_to_res_id('uom.product_uom_categ_unit')
+ cls.categ_kgm = cls.ModelDataObj.xmlid_to_res_id('uom.product_uom_categ_kgm')
+
+ # Product Created A, B, C, D
+ cls.productA = cls.ProductObj.create({'name': 'Product A', 'type': 'product'})
+ cls.productB = cls.ProductObj.create({'name': 'Product B', 'type': 'product'})
+ cls.productC = cls.ProductObj.create({'name': 'Product C', 'type': 'product'})
+ cls.productD = cls.ProductObj.create({'name': 'Product D', 'type': 'product'})
+ cls.productE = cls.ProductObj.create({'name': 'Product E', 'type': 'product'})
+
+ # Configure unit of measure.
+ cls.uom_kg = cls.env['uom.uom'].search([('category_id', '=', cls.categ_kgm), ('uom_type', '=', 'reference')], limit=1)
+ cls.uom_kg.write({
+ 'name': 'Test-KG',
+ 'rounding': 0.000001})
+ cls.uom_tone = cls.UomObj.create({
+ 'name': 'Test-Tone',
+ 'category_id': cls.categ_kgm,
+ 'uom_type': 'bigger',
+ 'factor_inv': 1000.0,
+ 'rounding': 0.001})
+ cls.uom_gm = cls.UomObj.create({
+ 'name': 'Test-G',
+ 'category_id': cls.categ_kgm,
+ 'uom_type': 'smaller',
+ 'factor': 1000.0,
+ 'rounding': 0.001})
+ cls.uom_mg = cls.UomObj.create({
+ 'name': 'Test-MG',
+ 'category_id': cls.categ_kgm,
+ 'uom_type': 'smaller',
+ 'factor': 100000.0,
+ 'rounding': 0.001})
+ # Check Unit
+ cls.uom_unit = cls.env['uom.uom'].search([('category_id', '=', cls.categ_unit), ('uom_type', '=', 'reference')], limit=1)
+ cls.uom_unit.write({
+ 'name': 'Test-Unit',
+ 'rounding': 1.0})
+ cls.uom_dozen = cls.UomObj.create({
+ 'name': 'Test-DozenA',
+ 'category_id': cls.categ_unit,
+ 'factor_inv': 12,
+ 'uom_type': 'bigger',
+ 'rounding': 0.001})
+ cls.uom_sdozen = cls.UomObj.create({
+ 'name': 'Test-SDozenA',
+ 'category_id': cls.categ_unit,
+ 'factor_inv': 144,
+ 'uom_type': 'bigger',
+ 'rounding': 0.001})
+ cls.uom_sdozen_round = cls.UomObj.create({
+ 'name': 'Test-SDozenA Round',
+ 'category_id': cls.categ_unit,
+ 'factor_inv': 144,
+ 'uom_type': 'bigger',
+ 'rounding': 1.0})
+
+ # Product for different unit of measure.
+ cls.DozA = cls.ProductObj.create({'name': 'Dozon-A', 'type': 'product', 'uom_id': cls.uom_dozen.id, 'uom_po_id': cls.uom_dozen.id})
+ cls.SDozA = cls.ProductObj.create({'name': 'SuperDozon-A', 'type': 'product', 'uom_id': cls.uom_sdozen.id, 'uom_po_id': cls.uom_sdozen.id})
+ cls.SDozARound = cls.ProductObj.create({'name': 'SuperDozenRound-A', 'type': 'product', 'uom_id': cls.uom_sdozen_round.id, 'uom_po_id': cls.uom_sdozen_round.id})
+ cls.UnitA = cls.ProductObj.create({'name': 'Unit-A', 'type': 'product'})
+ cls.kgB = cls.ProductObj.create({'name': 'kg-B', 'type': 'product', 'uom_id': cls.uom_kg.id, 'uom_po_id': cls.uom_kg.id})
+ cls.gB = cls.ProductObj.create({'name': 'g-B', 'type': 'product', 'uom_id': cls.uom_gm.id, 'uom_po_id': cls.uom_gm.id})
diff --git a/addons/stock/tests/common2.py b/addons/stock/tests/common2.py
new file mode 100644
index 00000000..3f3c9db2
--- /dev/null
+++ b/addons/stock/tests/common2.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+
+from odoo.addons.mail.tests.common import mail_new_test_user
+from odoo.addons.product.tests import common
+
+
+class TestStockCommon(common.TestProductCommon):
+
+ def _create_move(self, product, src_location, dst_location, **values):
+ # TDE FIXME: user as parameter
+ Move = self.env['stock.move'].with_user(self.user_stock_manager)
+ # simulate create + onchange
+ move = Move.new({'product_id': product.id, 'location_id': src_location.id, 'location_dest_id': dst_location.id})
+ move.onchange_product_id()
+ move_values = move._convert_to_write(move._cache)
+ move_values.update(**values)
+ return Move.create(move_values)
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestStockCommon, cls).setUpClass()
+
+ # User Data: stock user and stock manager
+ cls.user_stock_user = mail_new_test_user(
+ cls.env,
+ name='Pauline Poivraisselle',
+ login='pauline',
+ email='p.p@example.com',
+ notification_type='inbox',
+ groups='stock.group_stock_user',
+ )
+ cls.user_stock_manager = mail_new_test_user(
+ cls.env,
+ name='Julie Tablier',
+ login='julie',
+ email='j.j@example.com',
+ notification_type='inbox',
+ groups='stock.group_stock_manager',
+ )
+
+ # Warehouses
+ cls.warehouse_1 = cls.env['stock.warehouse'].create({
+ 'name': 'Base Warehouse',
+ 'reception_steps': 'one_step',
+ 'delivery_steps': 'ship_only',
+ 'code': 'BWH'})
+
+ # Locations
+ cls.location_1 = cls.env['stock.location'].create({
+ 'name': 'TestLocation1',
+ 'posx': 3,
+ 'location_id': cls.warehouse_1.lot_stock_id.id,
+ })
+
+ # Existing data
+ cls.existing_inventories = cls.env['stock.inventory'].search([])
+ cls.existing_quants = cls.env['stock.quant'].search([])
diff --git a/addons/stock/tests/test_generate_serial_numbers.py b/addons/stock/tests/test_generate_serial_numbers.py
new file mode 100644
index 00000000..42e678a3
--- /dev/null
+++ b/addons/stock/tests/test_generate_serial_numbers.py
@@ -0,0 +1,377 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.exceptions import UserError, ValidationError
+from odoo.tests.common import Form, SavepointCase
+
+
+class StockGenerate(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(StockGenerate, cls).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.Wizard = cls.env['stock.assign.serial']
+
+ def get_new_move(self, nbre_of_lines):
+ move_lines_val = []
+ for i in range(nbre_of_lines):
+ move_lines_val.append({
+ 'product_id': self.product_serial.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_uom_qty': 1,
+ 'location_id': self.location.id,
+ 'location_dest_id': self.location_dest.id
+ })
+ return self.env['stock.move'].create({
+ 'name': 'Move Test',
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'location_id': self.location.id,
+ 'location_dest_id': self.location_dest.id,
+ 'move_line_ids': [(0, 0, line_vals) for line_vals in move_lines_val]
+ })
+
+ 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',
+ ))
+ wiz = form_wizard.save()
+ with self.assertRaises(UserError):
+ wiz.generate_serial_numbers()
+
+ 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()
+
+ 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
+ putaway_product = 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
+ picking_type = self.env['stock.picking.type'].search([
+ ('use_create_lots', '=', True),
+ ('warehouse_id', '=', self.warehouse.id)
+ ])
+ move = self.get_new_move(nbre_of_lines)
+ move.picking_type_id = picking_type
+ # 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
+ picking_type = self.env['stock.picking.type'].search([
+ ('use_create_lots', '=', True),
+ ('warehouse_id', '=', self.warehouse.id)
+ ])
+ move = self.get_new_move(nbre_of_lines)
+ move.picking_type_id = picking_type
+ # 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)
diff --git a/addons/stock/tests/test_inventory.py b/addons/stock/tests/test_inventory.py
new file mode 100644
index 00000000..8b7c6d0a
--- /dev/null
+++ b/addons/stock/tests/test_inventory.py
@@ -0,0 +1,804 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.exceptions import ValidationError
+from odoo.tests.common import Form, SavepointCase
+
+
+class TestInventory(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestInventory, cls).setUpClass()
+ cls.stock_location = cls.env.ref('stock.stock_location_stock')
+ cls.pack_location = cls.env.ref('stock.location_pack_zone')
+ cls.pack_location.active = True
+ cls.customer_location = cls.env.ref('stock.stock_location_customers')
+ cls.uom_unit = cls.env.ref('uom.product_uom_unit')
+ cls.product1 = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.product2 = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'tracking': 'serial',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+
+ def test_inventory_1(self):
+ """ Check that making an inventory adjustment to remove all products from stock is working
+ as expected.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 100)
+ self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 100.0)
+
+ # remove them with an inventory adjustment
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'remove product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ self.assertEqual(len(inventory.line_ids), 1)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 100)
+ inventory.line_ids.product_qty = 0 # Put the quantity back to 0
+ inventory.action_validate()
+
+ # check
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0)
+ self.assertEqual(sum(self.env['stock.quant']._gather(self.product1, self.stock_location).mapped('quantity')), 0.0)
+
+ def test_inventory_2(self):
+ """ Check that adding a tracked product through an inventory adjustment work as expected.
+ """
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'remove product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product2.id)]
+ })
+ inventory.action_start()
+ self.assertEqual(len(inventory.line_ids), 0)
+
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'sn2',
+ 'product_id': self.product2.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'location_id': self.stock_location.id,
+ 'product_id': self.product2.id,
+ 'prod_lot_id': lot1.id,
+ 'product_qty': 1
+ })
+ self.assertEqual(len(inventory.line_ids), 1)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 0)
+
+ inventory.action_validate()
+
+ # check
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot1), 1.0)
+ self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location, lot_id=lot1)), 1.0)
+ self.assertEqual(lot1.product_qty, 1.0)
+
+ def test_inventory_3(self):
+ """ Check that it's not posisble to have multiple products with a serial number through an
+ inventory adjustment
+ """
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'remove product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product2.id)]
+ })
+ inventory.action_start()
+ self.assertEqual(len(inventory.line_ids), 0)
+
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'sn2',
+ 'product_id': self.product2.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'location_id': self.stock_location.id,
+ 'product_id': self.product2.id,
+ 'prod_lot_id': lot1.id,
+ 'product_qty': 2
+ })
+ self.assertEqual(len(inventory.line_ids), 1)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 0)
+
+ with self.assertRaises(ValidationError):
+ inventory.action_validate()
+
+ def test_inventory_4(self):
+ """ Check that even if a product is tracked by serial number, it's possible to add
+ untracked one in an inventory adjustment.
+ """
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'remove product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product2.id)]
+ })
+ inventory.action_start()
+ self.assertEqual(len(inventory.line_ids), 0)
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'sn2',
+ 'product_id': self.product2.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'product_id': self.product2.id,
+ 'prod_lot_id': lot1.id,
+ 'product_qty': 1,
+ 'location_id': self.stock_location.id,
+ })
+ self.assertEqual(len(inventory.line_ids), 1)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 0)
+
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'product_id': self.product2.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 10,
+ 'location_id': self.stock_location.id,
+ })
+ self.assertEqual(len(inventory.line_ids), 2)
+ res_dict_for_warning_lot = inventory.action_validate()
+ wizard_warning_lot = self.env[(res_dict_for_warning_lot.get('res_model'))].browse(res_dict_for_warning_lot.get('res_id'))
+ wizard_warning_lot.action_confirm()
+
+ # check
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot1, strict=True), 11.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, strict=True), 10.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location), 11.0)
+ self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location, lot_id=lot1, strict=True).filtered(lambda q: q.lot_id)), 1.0)
+ self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location, strict=True)), 1.0)
+ self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location)), 2.0)
+
+ def test_inventory_5(self):
+ """ Check that assigning an owner does work.
+ """
+ owner1 = self.env['res.partner'].create({'name': 'test_inventory_5'})
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'remove product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)]
+ })
+ inventory.action_start()
+ self.assertEqual(len(inventory.line_ids), 0)
+
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'product_id': self.product1.id,
+ 'partner_id': owner1.id,
+ 'product_qty': 5,
+ 'location_id': self.stock_location.id,
+ })
+ self.assertEqual(len(inventory.line_ids), 1)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 0)
+ inventory.action_validate()
+
+ quant = self.env['stock.quant']._gather(self.product1, self.stock_location)
+ self.assertEqual(len(quant), 1)
+ self.assertEqual(quant.quantity, 5)
+ self.assertEqual(quant.owner_id.id, owner1.id)
+
+ def test_inventory_6(self):
+ """ Test that for chained moves, making an inventory adjustment to reduce a quantity that
+ has been reserved correctly free the reservation. After that, add products in stock and check
+ that they're used if the user encodes more than what's available through the chain
+ """
+ # add 10 products in stock
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'add 10 products 1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)]
+ })
+ inventory.action_start()
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'product_id': self.product1.id,
+ 'product_qty': 10,
+ 'location_id': self.stock_location.id
+ })
+ inventory.action_validate()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 10.0)
+
+ # Make a chain of two moves, validate the first and check that 10 products are reserved
+ # in the second one.
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_2_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_2_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+ (move_stock_pack + move_pack_cust)._action_confirm()
+ move_stock_pack._action_assign()
+ self.assertEqual(move_stock_pack.state, 'assigned')
+ move_stock_pack.move_line_ids.qty_done = 10
+ move_stock_pack._action_done()
+ self.assertEqual(move_stock_pack.state, 'done')
+ self.assertEqual(move_pack_cust.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._gather(self.product1, self.pack_location).quantity, 10.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.pack_location), 0.0)
+
+ # Make and inventory adjustment and remove two products from the pack location. This should
+ # free the reservation of the second move.
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'remove 2 products 1',
+ 'location_ids': [(4, self.pack_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ inventory.line_ids.product_qty = 8
+ inventory.action_validate()
+ self.assertEqual(self.env['stock.quant']._gather(self.product1, self.pack_location).quantity, 8.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.pack_location), 0)
+ self.assertEqual(move_pack_cust.state, 'partially_available')
+ self.assertEqual(move_pack_cust.reserved_availability, 8)
+
+ # If the user tries to assign again, only 8 products are available and thus the reservation
+ # state should not change.
+ move_pack_cust._action_assign()
+ self.assertEqual(move_pack_cust.state, 'partially_available')
+ self.assertEqual(move_pack_cust.reserved_availability, 8)
+
+ # Make a new inventory adjustment and bring two now products.
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'remove 2 products 1',
+ 'location_ids': [(4, self.pack_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ inventory.line_ids.product_qty = 10
+ inventory.action_validate()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.pack_location), 2)
+
+ # Nothing should have changed for our pack move
+ self.assertEqual(move_pack_cust.state, 'partially_available')
+ self.assertEqual(move_pack_cust.reserved_availability, 8)
+
+ # Running _action_assign will now find the new available quantity. Indeed, as the products
+ # are not discernabl (not lot/pack/owner), even if the new available quantity is not directly
+ # brought by the chain, the system fill take them into account.
+ move_pack_cust._action_assign()
+ self.assertEqual(move_pack_cust.state, 'assigned')
+
+ # move all the things
+ move_pack_cust.move_line_ids.qty_done = 10
+ move_stock_pack._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.pack_location), 0)
+
+ def test_inventory_7(self):
+ """ Check that duplicated quants create a single inventory line.
+ """
+ owner1 = self.env['res.partner'].create({'name': 'test_inventory_7'})
+ vals = {
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'owner_id': owner1.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1,
+ 'reserved_quantity': 0,
+ }
+ self.env['stock.quant'].create(vals)
+ self.env['stock.quant'].create(vals)
+ self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 2.0)
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ self.assertEqual(len(inventory.line_ids), 1)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 2)
+
+ def test_inventory_8(self):
+ """ Check inventory lines product quantity is 0 when inventory is
+ started with `prefill_counted_quantity` disable.
+ """
+ self.env['stock.quant'].create({
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ })
+ inventory_form = Form(self.env['stock.inventory'].with_context(
+ default_prefill_counted_quantity='zero',
+ ), view='stock.view_inventory_form')
+ inventory = inventory_form.save()
+ inventory.action_start()
+ self.assertNotEqual(len(inventory.line_ids), 0)
+ # Checks all inventory lines quantities are correctly set.
+ for line in inventory.line_ids:
+ self.assertEqual(line.product_qty, 0)
+ self.assertNotEqual(line.theoretical_qty, 0)
+
+ def test_inventory_9_cancel_then_start(self):
+ """ Checks when we cancel an inventory, then change its locations and/or
+ products setup and restart it, it will remove all its lines and restart
+ like a new inventory.
+ """
+ # Creates some records needed for the test...
+ product2 = self.env['product.product'].create({
+ 'name': 'Product B',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ loc1 = self.env['stock.location'].create({
+ 'name': 'SafeRoom A',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ # Adds some quants.
+ self.env['stock.quant'].create({
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': loc1.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ })
+ self.env['stock.quant'].create({
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ })
+ self.env['stock.quant'].create({
+ 'product_id': product2.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': loc1.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ })
+ self.env['stock.quant'].create({
+ 'product_id': product2.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ })
+ # Creates the inventory and configures if for product1
+ inventory_form = Form(self.env['stock.inventory'], view='stock.view_inventory_form')
+ inventory_form.product_ids.add(self.product1)
+ inventory = inventory_form.save()
+ inventory.action_start()
+ # Must have two inventory lines about product1.
+ self.assertEqual(len(inventory.line_ids), 2)
+ for line in inventory.line_ids:
+ self.assertEqual(line.product_id.id, self.product1.id)
+
+ # Cancels the inventory and changes for product2 in its setup.
+ inventory.action_cancel_draft()
+ inventory_form = Form(inventory)
+ inventory_form.product_ids.remove(self.product1.id)
+ inventory_form.product_ids.add(product2)
+ inventory = inventory_form.save()
+ inventory.action_start()
+ # Must have two inventory lines about product2.
+ self.assertEqual(len(inventory.line_ids), 2)
+ self.assertEqual(inventory.line_ids.product_id.id, product2.id)
+
+ def test_inventory_prefill_counted_quantity(self):
+ """ Checks that inventory lines have a `product_qty` set on zero or
+ equals to quantity on hand, depending of the `prefill_counted_quantity`.
+ """
+ # Set product quantity to 42.
+ vals = {
+ 'product_id': self.product1.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 42,
+ }
+ self.env['stock.quant'].create(vals)
+ # Generate new inventory, its line must have a theoretical
+ # quantity to 42 and a counted quantity to 42.
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'Default Qty',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ 'prefill_counted_quantity': 'counted',
+ })
+ inventory.action_start()
+ self.assertEqual(len(inventory.line_ids), 1)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 42)
+ self.assertEqual(inventory.line_ids.product_qty, 42)
+
+ # Generate new inventory, its line must have a theoretical
+ # quantity to 42 and a counted quantity to 0.
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'Default Qty',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ 'prefill_counted_quantity': 'zero',
+ })
+ inventory.action_start()
+ self.assertEqual(len(inventory.line_ids), 1)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 42)
+ self.assertEqual(inventory.line_ids.product_qty, 0)
+
+ def test_inventory_outdate_1(self):
+ """ Checks that inventory adjustment line is marked as outdated after
+ its corresponding quant is modify and its value was correctly updated
+ after user refreshed it.
+ """
+ # Set initial quantity to 7
+ vals = {
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ }
+ self.env['stock.quant'].create(vals)
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ # When a inventory line is created, it must not be marked as outdated
+ # and its `theoretical_qty` must be equals to quant quantity.
+ self.assertEqual(inventory.line_ids.outdated, False)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 7)
+
+ # Creates a new quant who'll update the existing one and so set product
+ # quantity to 5. Then expects inventory line has been marked as outdated.
+ vals = {
+ 'product_id': self.product1.id,
+ 'location_id': self.stock_location.id,
+ 'inventory_quantity': 5,
+ }
+ self.env['stock.quant'].with_context(inventory_mode=True).create(vals)
+ self.assertEqual(inventory.line_ids.outdated, True)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 7)
+ # Refreshes inventory line and expects quantity was recomputed to 5
+ inventory.line_ids.action_refresh_quantity()
+ self.assertEqual(inventory.line_ids.outdated, False)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 5)
+
+ def test_inventory_outdate_2(self):
+ """ Checks that inventory adjustment line is marked as outdated when a
+ quant is manually updated and its value is correctly updated when action
+ to refresh is called.
+ """
+ # Set initial quantity to 7
+ vals = {
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ }
+ quant = self.env['stock.quant'].create(vals)
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ self.assertEqual(inventory.line_ids.outdated, False)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 7)
+
+ # Decreases quant to 3 and expects inventory line is now outdated
+ quant.with_context(inventory_mode=True).write({'inventory_quantity': 3})
+ self.assertEqual(inventory.line_ids.outdated, True)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 7)
+ # Refreshes inventory line and expects quantity was recomputed to 3
+ inventory.line_ids.action_refresh_quantity()
+ self.assertEqual(inventory.line_ids.outdated, False)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 3)
+
+ def test_inventory_outdate_3(self):
+ """ Checks that outdated inventory adjustment line without difference
+ doesn't change quant when validated.
+ """
+ # Set initial quantity to 10
+ vals = {
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 10,
+ 'reserved_quantity': 0,
+ }
+ quant = self.env['stock.quant'].create(vals)
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ self.assertEqual(inventory.line_ids.outdated, False)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 10)
+
+ # increases quant to 15 and expects inventory line is now outdated
+ quant.with_context(inventory_mode=True).write({'inventory_quantity': 15})
+ self.assertEqual(inventory.line_ids.outdated, True)
+ # Don't refresh inventory line but valid it, and expect quantity is
+ # still equal to 15
+ inventory.action_validate()
+ self.assertEqual(inventory.line_ids.theoretical_qty, 10)
+ self.assertEqual(quant.quantity, 15)
+
+ def test_inventory_outdate_4(self):
+ """ Checks that outdated inventory adjustment line with difference
+ changes quant when validated.
+ """
+ # Set initial quantity to 10
+ vals = {
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 10,
+ 'reserved_quantity': 0,
+ }
+ quant = self.env['stock.quant'].create(vals)
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ self.assertEqual(inventory.line_ids.outdated, False)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 10)
+
+ # increases quant to 15 and expects inventory line is now outdated
+ quant.with_context(inventory_mode=True).write({'inventory_quantity': 15})
+ self.assertEqual(inventory.line_ids.outdated, True)
+ # Don't refresh inventory line but changes its value and valid it, and
+ # expects quantity is correctly adapted (15 + inventory line diff)
+ inventory.line_ids.product_qty = 12
+ inventory.action_validate()
+ self.assertEqual(inventory.line_ids.theoretical_qty, 10)
+ self.assertEqual(quant.quantity, 17)
+
+ def test_inventory_outdate_5(self):
+ """ Checks that inventory adjustment line is marked as outdated when an
+ another inventory adjustment line with common product/location is
+ validated and its value is updated when action to refresh is called.
+ """
+ # Set initial quantity to 7
+ vals = {
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ }
+ self.env['stock.quant'].create(vals)
+
+ inventory_1 = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory_1.action_start()
+ inventory_2 = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory_2.action_start()
+ self.assertEqual(inventory_1.line_ids.outdated, False)
+ self.assertEqual(inventory_1.line_ids.theoretical_qty, inventory_2.line_ids.theoretical_qty)
+
+ # Set product quantity to 8 in inventory 2 then validates it
+ inventory_2.line_ids.product_qty = 8
+ inventory_2.action_validate()
+ # Expects line of inventory 1 is now marked as outdated
+ self.assertEqual(inventory_1.line_ids.outdated, True)
+ self.assertEqual(inventory_1.line_ids.theoretical_qty, 7)
+ # Refreshes inventory line and expects quantity was recomputed to 8
+ inventory_1.line_ids.action_refresh_quantity()
+ self.assertEqual(inventory_1.line_ids.theoretical_qty, 8)
+
+ def test_inventory_dont_outdate_1(self):
+ """ Checks that inventory adjustment line isn't marked as outdated when
+ a not corresponding quant is created.
+ """
+ # Set initial quantity to 7 and create inventory adjustment for product1
+ vals = {
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ }
+ self.env['stock.quant'].create(vals)
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+ self.assertEqual(inventory.line_ids.outdated, False)
+
+ # Create quant for product3
+ product3 = self.env['product.product'].create({
+ 'name': 'Product C',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ vals = {
+ 'product_id': product3.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'inventory_quantity': 22,
+ 'reserved_quantity': 0,
+ }
+ self.env['stock.quant'].create(vals)
+ # Expect inventory line is still up to date
+ self.assertEqual(inventory.line_ids.outdated, False)
+
+ def test_inventory_dont_outdate_2(self):
+ """ Checks that inventory adjustment line isn't marked as outdated when
+ an another inventory adjustment line without common product/location is
+ validated.
+ """
+ # Set initial quantity for product1 and product3
+ self.env['stock.quant'].create({
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 7,
+ 'reserved_quantity': 0,
+ })
+ product3 = self.env['product.product'].create({
+ 'name': 'Product C',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ self.env['stock.quant'].create({
+ 'product_id': product3.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 10,
+ 'reserved_quantity': 0,
+ })
+
+ inventory_1 = self.env['stock.inventory'].create({
+ 'name': 'product1',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory_1.action_start()
+ inventory_2 = self.env['stock.inventory'].create({
+ 'name': 'product3',
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, product3.id)],
+ })
+ inventory_2.action_start()
+ self.assertEqual(inventory_1.line_ids.outdated, False)
+
+ # Set product3 quantity to 16 in inventory 2 then validates it
+ inventory_2.line_ids.product_qty = 16
+ inventory_2.action_validate()
+ # Expect line of inventory 1 is still up to date
+ self.assertEqual(inventory_1.line_ids.outdated, False)
+
+ def test_inventory_include_exhausted_product(self):
+ """ Checks that exhausted product (quant not set or == 0) is added
+ to inventory line
+ (only for location_ids selected or, if not set, for each main location
+ (linked directly to the warehouse) of the current company)
+ when the option is active """
+
+ # location_ids SET + product_ids SET
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'loc SET - pro SET',
+ 'exhausted': True,
+ 'location_ids': [(4, self.stock_location.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+
+ self.assertEqual(len(inventory.line_ids), 1)
+ self.assertEqual(inventory.line_ids.product_id.id, self.product1.id)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 0)
+ self.assertEqual(inventory.line_ids.location_id.id, self.stock_location.id)
+
+ # location_ids SET + product_ids UNSET
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'loc SET - pro UNSET',
+ 'exhausted': True,
+ 'location_ids': [(4, self.stock_location.id)]
+ })
+ inventory.action_start()
+ line_ids_p1 = [l for l in inventory.line_ids if l['product_id']['id'] == self.product1.id]
+ line_ids_p2 = [l for l in inventory.line_ids if l['product_id']['id'] == self.product2.id]
+ self.assertEqual(len(line_ids_p1), 1)
+ self.assertEqual(len(line_ids_p2), 1)
+ self.assertEqual(line_ids_p1[0].location_id.id, self.stock_location.id)
+ self.assertEqual(line_ids_p2[0].location_id.id, self.stock_location.id)
+
+ # location_ids UNSET + product_ids SET
+ warehouse = self.env['stock.warehouse'].create({
+ 'name': 'Warhouse',
+ 'code': 'WAR'
+ })
+ child_loc = self.env['stock.location'].create({
+ 'name': "ChildLOC",
+ 'usage': 'internal',
+ 'location_id': warehouse.lot_stock_id.id
+ })
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'loc UNSET - pro SET',
+ 'exhausted': True,
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+
+ line_ids = [l for l in inventory.line_ids if l['location_id']['id'] == warehouse.lot_stock_id.id]
+ self.assertEqual(len(line_ids), 1)
+ self.assertEqual(line_ids[0].theoretical_qty, 0)
+ self.assertEqual(line_ids[0].product_id.id, self.product1.id)
+
+ # Only the main location have a exhausted line
+ line_ids = [l for l in inventory.line_ids if l['location_id']['id'] == child_loc.id]
+ self.assertEqual(len(line_ids), 0)
+
+ # location_ids UNSET + product_ids UNSET
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'loc UNSET - pro UNSET',
+ 'exhausted': True
+ })
+ inventory.action_start()
+
+ # Product1 & Product2 line with warehouse location
+ line_ids_p1 = [l for l in inventory.line_ids if l['product_id']['id'] == self.product1.id and l['location_id']['id'] == warehouse.lot_stock_id.id]
+ line_ids_p2 = [l for l in inventory.line_ids if l['product_id']['id'] == self.product2.id and l['location_id']['id'] == warehouse.lot_stock_id.id]
+ self.assertEqual(len(line_ids_p1), 1)
+ self.assertEqual(len(line_ids_p2), 1)
+ self.assertEqual(line_ids_p1[0].theoretical_qty, 0)
+ self.assertEqual(line_ids_p2[0].theoretical_qty, 0)
+
+ # location_ids SET + product_ids SET but when product in one locations but no the other
+ self.env['stock.quant'].create({
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 10,
+ 'reserved_quantity': 0,
+ })
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'loc SET - pro SET',
+ 'exhausted': True,
+ 'location_ids': [(4, self.stock_location.id), (4, warehouse.lot_stock_id.id)],
+ 'product_ids': [(4, self.product1.id)],
+ })
+ inventory.action_start()
+
+ # need to have line for product1 for both location, one with quant the other not
+ line_ids_loc1 = [l for l in inventory.line_ids if l['location_id']['id'] == self.stock_location.id]
+ line_ids_loc2 = [l for l in inventory.line_ids if l['location_id']['id'] == warehouse.lot_stock_id.id]
+ self.assertEqual(len(line_ids_loc1), 1)
+ self.assertEqual(len(line_ids_loc2), 1)
+ self.assertEqual(line_ids_loc1[0].theoretical_qty, 10)
+ self.assertEqual(line_ids_loc2[0].theoretical_qty, 0)
diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py
new file mode 100644
index 00000000..eb52068e
--- /dev/null
+++ b/addons/stock/tests/test_move.py
@@ -0,0 +1,4574 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.exceptions import UserError
+from odoo.tests import Form
+from odoo.tests.common import SavepointCase
+
+
+class StockMove(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(StockMove, cls).setUpClass()
+ cls.stock_location = cls.env.ref('stock.stock_location_stock')
+ cls.customer_location = cls.env.ref('stock.stock_location_customers')
+ cls.supplier_location = cls.env.ref('stock.stock_location_suppliers')
+ cls.pack_location = cls.env.ref('stock.location_pack_zone')
+ cls.pack_location.active = True
+ cls.transit_location = cls.env['stock.location'].search([
+ ('company_id', '=', cls.env.company.id),
+ ('usage', '=', 'transit'),
+ ('active', '=', False)
+ ], limit=1)
+ cls.transit_location.active = True
+ cls.uom_unit = cls.env.ref('uom.product_uom_unit')
+ cls.uom_dozen = cls.env.ref('uom.product_uom_dozen')
+ cls.product = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.product_serial = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'tracking': 'serial',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.product_lot = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'tracking': 'lot',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.product_consu = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'consu',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+
+ def gather_relevant(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False):
+ quants = self.env['stock.quant']._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
+ return quants.filtered(lambda q: not (q.quantity == 0 and q.reserved_quantity == 0))
+
+ def test_in_1(self):
+ """ Receive products from a supplier. Check that a move line is created and that the
+ reception correctly increase a single quant in stock.
+ """
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # fill the move line
+ move_line = move1.move_line_ids[0]
+ self.assertEqual(move_line.product_qty, 100.0)
+ self.assertEqual(move_line.qty_done, 0.0)
+ move_line.qty_done = 100.0
+
+ # validation
+ move1._action_done()
+ self.assertEqual(move1.state, 'done')
+ # no quants are created in the supplier location
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.supplier_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.supplier_location, allow_negative=True), -100.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 100.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.supplier_location)), 1.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+
+ def test_in_2(self):
+ """ Receive 5 tracked products from a supplier. The create move line should have 5
+ reserved. If i assign the 5 items to lot1, the reservation should not change. Once
+ i validate, the reception correctly increase a single quant in stock.
+ """
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ move_line = move1.move_line_ids[0]
+ self.assertEqual(move_line.product_qty, 5)
+ move_line.lot_name = 'lot1'
+ move_line.qty_done = 5.0
+ self.assertEqual(move_line.product_qty, 5) # don't change reservation
+
+ move1._action_done()
+ self.assertEqual(move_line.product_qty, 0) # change reservation to 0 for done move
+ self.assertEqual(move1.state, 'done')
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.supplier_location), 0.0)
+ supplier_quants = self.gather_relevant(self.product_lot, self.supplier_location)
+ self.assertEqual(sum(supplier_quants.mapped('quantity')), -5.0)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location), 5.0)
+ self.assertEqual(len(self.gather_relevant(self.product_lot, self.supplier_location)), 1.0)
+ quants = self.gather_relevant(self.product_lot, self.stock_location)
+ self.assertEqual(len(quants), 1.0)
+ for quant in quants:
+ self.assertNotEqual(quant.in_date, False)
+
+ def test_in_3(self):
+ """ Receive 5 serial-tracked products from a supplier. The system should create 5 different
+ move line.
+ """
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 5)
+ move_line = move1.move_line_ids[0]
+ self.assertEqual(move1.reserved_availability, 5)
+
+ i = 0
+ for move_line in move1.move_line_ids:
+ move_line.lot_name = 'sn%s' % i
+ move_line.qty_done = 1
+ i += 1
+ self.assertEqual(move1.quantity_done, 5.0)
+ self.assertEqual(move1.product_qty, 5) # don't change reservation
+
+ move1._action_done()
+
+ self.assertEqual(move1.quantity_done, 5.0)
+ self.assertEqual(move1.product_qty, 5) # don't change reservation
+ self.assertEqual(move1.state, 'done')
+
+ # Quant balance should result with 5 quant in supplier and stock
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.supplier_location), 0.0)
+ supplier_quants = self.gather_relevant(self.product_serial, self.supplier_location)
+ self.assertEqual(sum(supplier_quants.mapped('quantity')), -5.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 5.0)
+
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.supplier_location)), 5.0)
+ quants = self.gather_relevant(self.product_serial, self.stock_location)
+ self.assertEqual(len(quants), 5.0)
+ for quant in quants:
+ self.assertNotEqual(quant.in_date, False)
+
+ def test_out_1(self):
+ """ Send products to a client. Check that a move line is created reserving products in
+ stock and that the delivery correctly remove the single quant in stock.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 100)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 100.0)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_out_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ # Should be a reserved quantity and thus a quant.
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+
+ # fill the move line
+ move_line = move1.move_line_ids[0]
+ self.assertEqual(move_line.product_qty, 100.0)
+ self.assertEqual(move_line.qty_done, 0.0)
+ move_line.qty_done = 100.0
+
+ # validation
+ move1._action_done()
+ self.assertEqual(move1.state, 'done')
+ # Check there is one quant in customer location
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 100.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.customer_location)), 1.0)
+ # there should be no quant amymore in the stock location
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 0.0)
+
+ def test_out_2(self):
+ """ Send a consumable product to a client. Check that a move line is created but
+ quants are not impacted.
+ """
+ # make some stock
+
+ self.product.type = 'consu'
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_out_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ # Should be a reserved quantity and thus a quant.
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 0.0)
+
+ # fill the move line
+ move_line = move1.move_line_ids[0]
+ self.assertEqual(move_line.product_qty, 100.0)
+ self.assertEqual(move_line.qty_done, 0.0)
+ move_line.qty_done = 100.0
+
+ # validation
+ move1._action_done()
+ self.assertEqual(move1.state, 'done')
+ # no quants are created in the customer location since it's a consumable
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.customer_location)), 0.0)
+ # there should be no quant amymore in the stock location
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 0.0)
+
+ def test_mixed_tracking_reservation_1(self):
+ """ Send products tracked by lot to a customer. In your stock, there are tracked and
+ untracked quants. Two moves lines should be created: one for the tracked ones, another
+ for the untracked ones.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 2)
+ self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 3, lot_id=lot1)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location), 5.0)
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(len(move1.move_line_ids), 2)
+
+ def test_mixed_tracking_reservation_2(self):
+ """ Send products tracked by lot to a customer. In your stock, there are two tracked and
+ mulitple untracked quants. There should be as many move lines as there are quants
+ reserved. Edit the reserve move lines to set them to new serial numbers, the reservation
+ should stay. Validate and the final quantity in stock should be 0, not negative.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 2)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 4.0)
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 4.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(len(move1.move_line_ids), 4)
+ for ml in move1.move_line_ids:
+ self.assertEqual(ml.product_qty, 1.0)
+
+ # assign lot3 and lot 4 to both untracked move lines
+ lot3 = self.env['stock.production.lot'].create({
+ 'name': 'lot3',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot4 = self.env['stock.production.lot'].create({
+ 'name': 'lot4',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ untracked_move_line = move1.move_line_ids.filtered(lambda ml: not ml.lot_id)
+ untracked_move_line[0].lot_id = lot3
+ untracked_move_line[1].lot_id = lot4
+ for ml in move1.move_line_ids:
+ self.assertEqual(ml.product_qty, 1.0)
+
+ # no changes on quants, even if i made some move lines with a lot id whom reserved on untracked quants
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, strict=True)), 1.0) # with a qty of 2
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot1, strict=True).filtered(lambda q: q.lot_id)), 1.0)
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot2, strict=True).filtered(lambda q: q.lot_id)), 1.0)
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot3, strict=True).filtered(lambda q: q.lot_id)), 0)
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location, lot_id=lot4, strict=True).filtered(lambda q: q.lot_id)), 0)
+
+ move1.move_line_ids.write({'qty_done': 1.0})
+
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot2, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot3, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot4, strict=True), 0.0)
+
+ def test_mixed_tracking_reservation_3(self):
+ """ Send two products tracked by lot to a customer. In your stock, there two tracked quants
+ and two untracked. Once the move is validated, add move lines to also move the two untracked
+ ones and assign them serial numbers on the fly. The final quantity in stock should be 0, not
+ negative.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot2)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0)
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.write({'qty_done': 1.0})
+ move1._action_done()
+
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 2)
+ lot3 = self.env['stock.production.lot'].create({
+ 'name': 'lot3',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot4 = self.env['stock.production.lot'].create({
+ 'name': 'lot4',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 1,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ 'lot_id': lot3.id,
+ })
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 1,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ 'lot_id': lot4.id
+ })
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot2, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot3, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot4, strict=True), 0.0)
+
+ def test_mixed_tracking_reservation_4(self):
+ """ Send two products tracked by lot to a customer. In your stock, there two tracked quants
+ and on untracked. Once the move is validated, edit one of the done move line to change the
+ serial number to one that is not in stock. The original serial should go back to stock and
+ the untracked quant should be tracked on the fly and sent instead.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot2)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0)
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.write({'qty_done': 1.0})
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot2, strict=True), 0.0)
+
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1)
+ lot3 = self.env['stock.production.lot'].create({
+ 'name': 'lot3',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+
+ move1.move_line_ids[1].lot_id = lot3
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot2, strict=True), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot3, strict=True), 0.0)
+
+ def test_mixed_tracking_reservation_5(self):
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_jenaimarre_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # create an untracked quant
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0)
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+
+ # create a new move line with a lot not assigned to any quant
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 1,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ 'lot_id': lot1.id
+ })
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(move1.reserved_availability, 0)
+
+ # validating the move line should move the lot, not create a negative quant in stock
+ move1._action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location)), 0.0)
+
+ def test_mixed_tracking_reservation_6(self):
+ # create an untracked quant
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_jenaimarre_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+
+ move_line = move1.move_line_ids
+ move_line.lot_id = lot1
+ self.assertEqual(move_line.product_qty, 1.0)
+ move_line.lot_id = lot2
+ self.assertEqual(move_line.product_qty, 1.0)
+ move_line.qty_done = 1
+
+ # validating the move line should move the lot, not create a negative quant in stock
+ move1._action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.stock_location)), 0.0)
+
+ def test_mixed_tracking_reservation_7(self):
+ """ Similar test_mixed_tracking_reservation_2 but creates first the tracked quant, then the
+ untracked ones. When adding a lot to the untracked move line, it should not decrease the
+ untracked quant then increase a non-existing tracked one that will fallback on the
+ untracked quant.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0)
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(len(move1.move_line_ids), 2)
+ for ml in move1.move_line_ids:
+ self.assertEqual(ml.product_qty, 1.0)
+
+ untracked_move_line = move1.move_line_ids.filtered(lambda ml: not ml.lot_id).lot_id = lot2
+ for ml in move1.move_line_ids:
+ self.assertEqual(ml.product_qty, 1.0)
+
+ move1.move_line_ids.write({'qty_done': 1.0})
+
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot2, strict=True), 0.0)
+ quants = self.gather_relevant(self.product_serial, self.stock_location)
+ self.assertEqual(len(quants), 0)
+
+ def test_mixed_tracking_reservation_8(self):
+ """ Send one product tracked by lot to a customer. In your stock, there are one tracked and
+ one untracked quant. Reserve the move, then edit the lot to one not present in stock. The
+ system will update the reservation and use the untracked quant. Now unreserve, no error
+ should happen
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+
+ # at first, we only make the tracked quant available in stock to make sure this one is selected
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1, lot_id=lot1)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_mixed_tracking_reservation_7',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(move1.move_line_ids.lot_id.id, lot1.id)
+
+ # change the lot_id to one not available in stock while an untracked quant is available
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1)
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ move1.move_line_ids.lot_id = lot2
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(move1.move_line_ids.lot_id.id, lot2.id)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 1.0)
+
+ # unreserve
+ move1._do_unreserve()
+
+ self.assertEqual(move1.reserved_availability, 0.0)
+ self.assertEqual(len(move1.move_line_ids), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1, strict=True), 2.0)
+
+ def test_putaway_1(self):
+ """ Receive products from a supplier. Check that putaway rules are rightly applied on
+ the receipt move line.
+ """
+ # This test will apply a putaway strategy on the stock location to put everything
+ # incoming in the sublocation shelf1.
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ # putaway from stock to shelf1
+ putaway = self.env['stock.putaway.rule'].create({
+ 'category_id': self.env.ref('product.product_category_all').id,
+ 'location_in_id': self.stock_location.id,
+ 'location_out_id': shelf1_location.id,
+ })
+ self.stock_location.write({
+ 'putaway_rule_ids': [(4, putaway.id, 0)]
+ })
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf1_location.id)
+
+ def test_putaway_2(self):
+ """ Receive products from a supplier. Check that putaway rules are rightly applied on
+ the receipt move line.
+ """
+ # This test will apply a putaway strategy by product on the stock location to put everything
+ # incoming in the sublocation shelf1.
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ # putaway from stock to shelf1
+ putaway = self.env['stock.putaway.rule'].create({
+ 'product_id': self.product.id,
+ 'location_in_id': self.stock_location.id,
+ 'location_out_id': shelf1_location.id,
+ })
+ self.stock_location.write({
+ 'putaway_rule_ids': [(4, putaway.id, 0)],
+ })
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_2',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf1_location.id)
+
+ def test_putaway_3(self):
+ """ Receive products from a supplier. Check that putaway rules are rightly applied on
+ the receipt move line.
+ """
+ # This test will apply both the putaway strategy by product and category. We check here
+ # that the putaway by product takes precedence.
+
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf2',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ putaway_category = self.env['stock.putaway.rule'].create({
+ 'category_id': self.env.ref('product.product_category_all').id,
+ 'location_in_id': self.supplier_location.id,
+ 'location_out_id': shelf1_location.id,
+ })
+ putaway_product = self.env['stock.putaway.rule'].create({
+ 'product_id': self.product.id,
+ 'location_in_id': self.supplier_location.id,
+ 'location_out_id': shelf2_location.id,
+ })
+ self.stock_location.write({
+ 'putaway_rule_ids': [(6, 0, [
+ putaway_category.id,
+ putaway_product.id
+ ])],
+ })
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_3',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf2_location.id)
+
+ def test_putaway_4(self):
+ """ Receive products from a supplier. Check that putaway rules are rightly applied on
+ the receipt move line.
+ """
+ # This test will apply both the putaway strategy by product and category. We check here
+ # that if a putaway by product is not matched, the fallback to the category is correctly
+ # done.
+
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf2',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ # putaway from stock to shelf1
+ putaway_category = self.env['stock.putaway.rule'].create({
+ 'category_id': self.env.ref('product.product_category_all').id,
+ 'location_in_id': self.stock_location.id,
+ 'location_out_id': shelf1_location.id,
+ })
+ putaway_product = self.env['stock.putaway.rule'].create({
+ 'product_id': self.product_consu.id,
+ 'location_in_id': self.stock_location.id,
+ 'location_out_id': shelf2_location.id,
+ })
+ self.stock_location.write({
+ 'putaway_rule_ids': [(6, 0, [
+ putaway_category.id,
+ putaway_product.id,
+ ])],
+ })
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_4',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf1_location.id)
+
+ def test_putaway_5(self):
+ """ Receive products from a supplier. Check that putaway rules are rightly applied on
+ the receipt move line.
+ """
+ # This test will apply putaway strategy by category.
+ # We check here that the putaway by category works when the category is
+ # set on parent category of the product.
+
+ shelf_location = self.env['stock.location'].create({
+ 'name': 'shelf',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ putaway = self.env['stock.putaway.rule'].create({
+ 'category_id': self.env.ref('product.product_category_all').id,
+ 'location_in_id': self.supplier_location.id,
+ 'location_out_id': shelf_location.id,
+ })
+ self.stock_location.write({
+ 'putaway_rule_ids': [(6, 0, [
+ putaway.id,
+ ])],
+ })
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_5',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf_location.id)
+
+ def test_putaway_6(self):
+ """ Receive products from a supplier. Check that putaway rules are rightly applied on
+ the receipt move line.
+ """
+ # This test will apply two putaway strategies by category. We check here
+ # that the most specific putaway takes precedence.
+
+ child_category = self.env['product.category'].create({
+ 'name': 'child_category',
+ 'parent_id': self.ref('product.product_category_all'),
+ })
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf2',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ putaway_category_all = self.env['stock.putaway.rule'].create({
+ 'category_id': self.env.ref('product.product_category_all').id,
+ 'location_in_id': self.supplier_location.id,
+ 'location_out_id': shelf1_location.id,
+ })
+ putaway_category_office_furn = self.env['stock.putaway.rule'].create({
+ 'category_id': child_category.id,
+ 'location_in_id': self.supplier_location.id,
+ 'location_out_id': shelf2_location.id,
+ })
+ self.stock_location.write({
+ 'putaway_rule_ids': [(6, 0, [
+ putaway_category_all.id,
+ putaway_category_office_furn.id,
+ ])],
+ })
+ self.product.categ_id = child_category
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_6',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf2_location.id)
+
+ def test_putaway_7(self):
+ """ Checks parents locations are also browsed when looking for putaways.
+
+ WH/Stock > WH/Stock/Floor1> WH/Stock/Floor1/Rack1 > WH/Stock/Floor1/Rack1/Shelf2
+ The putaway is on Floor1 to send to Shelf2
+ A move from supplier to Rack1 should send to shelf2
+ """
+ floor1 = self.env['stock.location'].create({
+ 'name': 'floor1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ rack1 = self.env['stock.location'].create({
+ 'name': 'rack1',
+ 'usage': 'internal',
+ 'location_id': floor1.id,
+ })
+ shelf2 = self.env['stock.location'].create({
+ 'name': 'shelf2',
+ 'usage': 'internal',
+ 'location_id': rack1.id,
+ })
+
+ # putaway floor1 -> shelf2
+ putaway = self.env['stock.putaway.rule'].create({
+ 'product_id': self.product.id,
+ 'location_in_id': floor1.id,
+ 'location_out_id': shelf2.id,
+ })
+ floor1.write({
+ 'putaway_rule_ids': [(4, putaway.id, 0)],
+ })
+
+ # stock move supplier -> rack1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_6',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': rack1.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ # check if the putaway was rightly applied
+ self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf2.id)
+
+ def test_availability_1(self):
+ """ Check that the `availability` field on a move is correctly computed when there is
+ more than enough products in stock.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 150.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.supplier_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 150.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+ self.assertEqual(move1.availability, 100.0)
+
+ def test_availability_2(self):
+ """ Check that the `availability` field on a move is correctly computed when there is
+ not enough products in stock.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 50.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.supplier_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 50.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+ self.assertEqual(move1.availability, 50.0)
+
+ def test_availability_3(self):
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, -1.0, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot2)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_availability_3',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(move1.reserved_availability, 1.0)
+
+ def test_availability_4(self):
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 30.0)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_availability_4',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 15.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_availability_4',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 15.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+
+ # set 15 as quantity done for the first and 30 as the second
+ move1.move_line_ids.qty_done = 15
+ move2.move_line_ids.qty_done = 30
+
+ # validate the second, the first should be unreserved
+ move2._action_done()
+
+ self.assertEqual(move1.state, 'confirmed')
+ self.assertEqual(move1.move_line_ids.qty_done, 15)
+ self.assertEqual(move2.state, 'done')
+
+ stock_quants = self.gather_relevant(self.product, self.stock_location)
+ self.assertEqual(len(stock_quants), 0)
+ customer_quants = self.gather_relevant(self.product, self.customer_location)
+ self.assertEqual(customer_quants.quantity, 30)
+ self.assertEqual(customer_quants.reserved_quantity, 0)
+
+ def test_availability_5(self):
+ """ Check that rerun action assign only create new stock move
+ lines instead of adding quantity in existing one.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 2.0)
+ # move from shelf1
+ move = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 4.0,
+ })
+ move._action_confirm()
+ move._action_assign()
+
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 4.0)
+ move._action_assign()
+
+ self.assertEqual(len(move.move_line_ids), 4.0)
+
+ def test_availability_6(self):
+ """ Check that, in the scenario where a move is in a bigger uom than the uom of the quants
+ and this uom only allows entire numbers, we don't make a partial reservation when the
+ quantity available is not enough to reserve the move. Check also that it is not possible
+ to set `quantity_done` with a value not honouring the UOM's rounding.
+ """
+ # on the dozen uom, set the rounding set 1.0
+ self.uom_dozen.rounding = 1
+
+ # 6 units are available in stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 6.0)
+
+ # the move should not be reserved
+ move = self.env['stock.move'].create({
+ 'name': 'test_availability_6',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1,
+ })
+ move._action_confirm()
+ move._action_assign()
+ self.assertEqual(move.state, 'confirmed')
+
+ # the quants should be left untouched
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 6.0)
+
+ # make 8 units available, the move should again not be reservabale
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0)
+ move._action_assign()
+ self.assertEqual(move.state, 'confirmed')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 8.0)
+
+ # make 12 units available, this time the move should be reservable
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 4.0)
+ move._action_assign()
+ self.assertEqual(move.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ # Check it isn't possible to set any value to quantity_done
+ with self.assertRaises(UserError):
+ move.quantity_done = 0.1
+ move._action_done()
+
+ with self.assertRaises(UserError):
+ move.quantity_done = 1.1
+ move._action_done()
+
+ with self.assertRaises(UserError):
+ move.quantity_done = 0.9
+ move._action_done()
+
+ move.quantity_done = 1
+ move._action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 12.0)
+
+ def test_availability_7(self):
+ """ Check that, in the scenario where a move is in a bigger uom than the uom of the quants
+ and this uom only allows entire numbers, we only reserve quantity honouring the uom's
+ rounding even if the quantity is set across multiple quants.
+ """
+ # on the dozen uom, set the rounding set 1.0
+ self.uom_dozen.rounding = 1
+
+ # make 12 quants of 1
+ for i in range(1, 13):
+ lot_id = self.env['stock.production.lot'].create({
+ 'name': 'lot%s' % str(i),
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot_id)
+
+ # the move should be reserved
+ move = self.env['stock.move'].create({
+ 'name': 'test_availability_7',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1,
+ })
+ move._action_confirm()
+ move._action_assign()
+ self.assertEqual(move.state, 'assigned')
+ self.assertEqual(len(move.move_line_ids.mapped('product_uom_id')), 1)
+ self.assertEqual(move.move_line_ids.mapped('product_uom_id'), self.uom_unit)
+
+ for move_line in move.move_line_ids:
+ move_line.qty_done = 1
+ move._action_done()
+
+ self.assertEqual(move.product_uom_qty, 1)
+ self.assertEqual(move.product_uom.id, self.uom_dozen.id)
+ self.assertEqual(move.state, 'done')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.customer_location), 12.0)
+ self.assertEqual(len(self.gather_relevant(self.product_serial, self.customer_location)), 12)
+
+ def test_availability_8(self):
+ """ Test the assignment mechanism when the product quantity is decreased on a partially
+ reserved stock move.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 3.0)
+ self.assertAlmostEqual(self.product.qty_available, 3.0)
+
+ move_partial = self.env['stock.move'].create({
+ 'name': 'test_partial',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+
+ move_partial._action_confirm()
+ move_partial._action_assign()
+ self.assertAlmostEqual(self.product.virtual_available, -2.0)
+ self.assertEqual(move_partial.state, 'partially_available')
+ move_partial.product_uom_qty = 3.0
+ move_partial._action_assign()
+ self.assertEqual(move_partial.state, 'assigned')
+
+ def test_availability_9(self):
+ """ Test the assignment mechanism when the product quantity is increase
+ on a receipt move.
+ """
+ move_receipt = self.env['stock.move'].create({
+ 'name': 'test_receipt_edit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ })
+
+ move_receipt._action_confirm()
+ move_receipt._action_assign()
+ self.assertEqual(move_receipt.state, 'assigned')
+ move_receipt.product_uom_qty = 3.0
+ move_receipt._action_assign()
+ self.assertEqual(move_receipt.state, 'assigned')
+ self.assertEqual(move_receipt.move_line_ids.product_uom_qty, 3)
+
+ def test_unreserve_1(self):
+ """ Check that unreserving a stock move sets the products reserved as available and
+ set the state back to confirmed.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 150.0)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.supplier_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 150.0)
+ self.assertEqual(move1.availability, 100.0)
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 50.0)
+
+ # unreserve
+ move1._do_unreserve()
+ self.assertEqual(len(move1.move_line_ids), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 150.0)
+ self.assertEqual(move1.state, 'confirmed')
+
+ def test_unreserve_2(self):
+ """ Check that unreserving a stock move sets the products reserved as available and
+ set the state back to confirmed even if they are in a pack.
+ """
+ package1 = self.env['stock.quant.package'].create({'name': 'test_unreserve_2_pack'})
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 150.0, package_id=package1)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_putaway_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.supplier_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ })
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 150.0)
+ self.assertEqual(move1.availability, 100.0)
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 50.0)
+
+ # unreserve
+ move1._do_unreserve()
+ self.assertEqual(len(move1.move_line_ids), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 150.0)
+ self.assertEqual(move1.state, 'confirmed')
+
+ def test_unreserve_3(self):
+ """ Similar to `test_unreserve_1` but checking the quants more in details.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_out_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ quants = self.gather_relevant(self.product, self.stock_location)
+ self.assertEqual(len(quants), 1.0)
+ self.assertEqual(quants.quantity, 2.0)
+ self.assertEqual(quants.reserved_quantity, 2.0)
+
+ move1._do_unreserve()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(len(quants), 1.0)
+ self.assertEqual(quants.quantity, 2.0)
+ self.assertEqual(quants.reserved_quantity, 0.0)
+ self.assertEqual(len(move1.move_line_ids), 0.0)
+
+ def test_unreserve_4(self):
+ """ Check the unreservation of a partially available stock move.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_out_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 3.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'partially_available')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ quants = self.gather_relevant(self.product, self.stock_location)
+ self.assertEqual(len(quants), 1.0)
+ self.assertEqual(quants.quantity, 2.0)
+ self.assertEqual(quants.reserved_quantity, 2.0)
+
+ move1._do_unreserve()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(len(quants), 1.0)
+ self.assertEqual(quants.quantity, 2.0)
+ self.assertEqual(quants.reserved_quantity, 0.0)
+ self.assertEqual(len(move1.move_line_ids), 0.0)
+
+ def test_unreserve_5(self):
+ """ Check the unreservation of a stock move reserved on multiple quants.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 3)
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 2,
+ })
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 5)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_unreserve_5',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ move1._do_unreserve()
+
+ quants = self.gather_relevant(self.product, self.stock_location)
+ self.assertEqual(len(quants), 2.0)
+ for quant in quants:
+ self.assertEqual(quant.reserved_quantity, 0)
+
+ def test_unreserve_6(self):
+ """ In a situation with a negative and a positive quant, reserve and unreserve.
+ """
+ q1 = self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': -10,
+ 'reserved_quantity': 0,
+ })
+
+ q2 = self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 30.0,
+ 'reserved_quantity': 10.0,
+ })
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_unreserve_6',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(move1.move_line_ids.product_qty, 10)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(q2.reserved_quantity, 20)
+
+ move1._do_unreserve()
+ self.assertEqual(move1.state, 'confirmed')
+ self.assertEqual(len(move1.move_line_ids), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0)
+ self.assertEqual(q2.reserved_quantity, 10)
+
+ def test_unreserve_7(self):
+ """ Check the unreservation of a stock move delete only stock move lines
+ without quantity done.
+ """
+ product = self.env['product.product'].create({
+ 'name': 'product',
+ 'tracking': 'serial',
+ 'type': 'product',
+ })
+
+ serial_numbers = self.env['stock.production.lot'].create([{
+ 'name': str(x),
+ 'product_id': product.id,
+ 'company_id': self.env.company.id,
+ } for x in range(5)])
+
+ for serial in serial_numbers:
+ self.env['stock.quant'].create({
+ 'product_id': product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ 'lot_id': serial.id,
+ 'reserved_quantity': 0.0,
+ })
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_unreserve_7',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 5.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 5)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product, self.stock_location), 0.0)
+
+ # Check state is changed even with 0 move lines unlinked
+ move1.move_line_ids.write({'qty_done': 1})
+ move1._do_unreserve()
+ self.assertEqual(len(move1.move_line_ids), 5)
+ self.assertEqual(move1.state, 'confirmed')
+ move1._action_assign()
+ # set a quantity done on the two first move lines
+ move1.move_line_ids.write({'qty_done': 0})
+ move1.move_line_ids[0].qty_done = 1
+ move1.move_line_ids[1].qty_done = 1
+
+ move1._do_unreserve()
+ self.assertEqual(move1.state, 'confirmed')
+ self.assertEqual(len(move1.move_line_ids), 2)
+ self.assertEqual(move1.move_line_ids.mapped('qty_done'), [1, 1])
+ self.assertEqual(move1.move_line_ids.mapped('product_uom_qty'), [0, 0])
+
+ def test_link_assign_1(self):
+ """ Test the assignment mechanism when two chained stock moves try to move one unit of an
+ untracked product.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+
+ (move_stock_pack + move_pack_cust)._action_confirm()
+ move_stock_pack._action_assign()
+ move_stock_pack.move_line_ids[0].qty_done = 1.0
+ move_stock_pack._action_done()
+ self.assertEqual(len(move_pack_cust.move_line_ids), 1)
+ move_line = move_pack_cust.move_line_ids[0]
+ self.assertEqual(move_line.location_id.id, self.pack_location.id)
+ self.assertEqual(move_line.location_dest_id.id, self.customer_location.id)
+ self.assertEqual(move_pack_cust.state, 'assigned')
+
+ def test_link_assign_2(self):
+ """ Test the assignment mechanism when two chained stock moves try to move one unit of a
+ tracked product.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot1)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location, lot1)), 1.0)
+
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_2_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_2_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+
+ (move_stock_pack + move_pack_cust)._action_confirm()
+ move_stock_pack._action_assign()
+
+ move_line_stock_pack = move_stock_pack.move_line_ids[0]
+ self.assertEqual(move_line_stock_pack.lot_id.id, lot1.id)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location, lot1)), 1.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.pack_location, lot1)), 0.0)
+
+ move_line_stock_pack.qty_done = 1.0
+ move_stock_pack._action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location, lot1)), 0.0)
+
+ move_line_pack_cust = move_pack_cust.move_line_ids[0]
+ self.assertEqual(move_line_pack_cust.lot_id.id, lot1.id)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.pack_location, lot_id=lot1), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.pack_location, lot1)), 1.0)
+
+ def test_link_assign_3(self):
+ """ Test the assignment mechanism when three chained stock moves (2 sources, 1 dest) try to
+ move multiple units of an untracked product.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+
+ move_stock_pack_1 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_stock_pack_2 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move_stock_pack_1.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_stock_pack_2.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack_1.id, 0), (4, move_stock_pack_2.id, 0)]})
+
+ (move_stock_pack_1 + move_stock_pack_2 + move_pack_cust)._action_confirm()
+
+ # assign and fulfill the first move
+ move_stock_pack_1._action_assign()
+ self.assertEqual(move_stock_pack_1.state, 'assigned')
+ self.assertEqual(len(move_stock_pack_1.move_line_ids), 1)
+ move_stock_pack_1.move_line_ids[0].qty_done = 1.0
+ move_stock_pack_1._action_done()
+ self.assertEqual(move_stock_pack_1.state, 'done')
+
+ # the destination move should be partially available and have one move line
+ self.assertEqual(move_pack_cust.state, 'partially_available')
+ self.assertEqual(len(move_pack_cust.move_line_ids), 1)
+ # Should have 1 quant in stock_location and another in pack_location
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.pack_location)), 1.0)
+
+ move_stock_pack_2._action_assign()
+ self.assertEqual(move_stock_pack_2.state, 'assigned')
+ self.assertEqual(len(move_stock_pack_2.move_line_ids), 1)
+ move_stock_pack_2.move_line_ids[0].qty_done = 1.0
+ move_stock_pack_2._action_done()
+ self.assertEqual(move_stock_pack_2.state, 'done')
+
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.pack_location)), 1.0)
+
+ self.assertEqual(move_pack_cust.state, 'assigned')
+ self.assertEqual(len(move_pack_cust.move_line_ids), 1)
+ move_line_1 = move_pack_cust.move_line_ids[0]
+ self.assertEqual(move_line_1.location_id.id, self.pack_location.id)
+ self.assertEqual(move_line_1.location_dest_id.id, self.customer_location.id)
+ self.assertEqual(move_line_1.product_qty, 2.0)
+ self.assertEqual(move_pack_cust.state, 'assigned')
+
+ def test_link_assign_4(self):
+ """ Test the assignment mechanism when three chained stock moves (2 sources, 1 dest) try to
+ move multiple units of a tracked by lot product.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0, lot_id=lot1)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location, lot1)), 1.0)
+
+ move_stock_pack_1 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_stock_pack_2 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move_stock_pack_1.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_stock_pack_2.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack_1.id, 0), (4, move_stock_pack_2.id, 0)]})
+
+ (move_stock_pack_1 + move_stock_pack_2 + move_pack_cust)._action_confirm()
+
+ # assign and fulfill the first move
+ move_stock_pack_1._action_assign()
+ self.assertEqual(len(move_stock_pack_1.move_line_ids), 1)
+ self.assertEqual(move_stock_pack_1.move_line_ids[0].lot_id.id, lot1.id)
+ move_stock_pack_1.move_line_ids[0].qty_done = 1.0
+ move_stock_pack_1._action_done()
+
+ # the destination move should be partially available and have one move line
+ self.assertEqual(len(move_pack_cust.move_line_ids), 1)
+
+ move_stock_pack_2._action_assign()
+ self.assertEqual(len(move_stock_pack_2.move_line_ids), 1)
+ self.assertEqual(move_stock_pack_2.move_line_ids[0].lot_id.id, lot1.id)
+ move_stock_pack_2.move_line_ids[0].qty_done = 1.0
+ move_stock_pack_2._action_done()
+
+ self.assertEqual(len(move_pack_cust.move_line_ids), 1)
+ move_line_1 = move_pack_cust.move_line_ids[0]
+ self.assertEqual(move_line_1.location_id.id, self.pack_location.id)
+ self.assertEqual(move_line_1.location_dest_id.id, self.customer_location.id)
+ self.assertEqual(move_line_1.product_qty, 2.0)
+ self.assertEqual(move_line_1.lot_id.id, lot1.id)
+ self.assertEqual(move_pack_cust.state, 'assigned')
+
+ def test_link_assign_5(self):
+ """ Test the assignment mechanism when three chained stock moves (1 sources, 2 dest) try to
+ move multiple units of an untracked product.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0)
+
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move_pack_cust_1 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_pack_cust_2 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust_1.id, 0), (4, move_pack_cust_2.id, 0)]})
+ move_pack_cust_1.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+ move_pack_cust_2.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+
+ (move_stock_pack + move_pack_cust_1 + move_pack_cust_2)._action_confirm()
+
+ # assign and fulfill the first move
+ move_stock_pack._action_assign()
+ self.assertEqual(len(move_stock_pack.move_line_ids), 1)
+ move_stock_pack.move_line_ids[0].qty_done = 2.0
+ move_stock_pack._action_done()
+
+ # the destination moves should be available and have one move line
+ self.assertEqual(len(move_pack_cust_1.move_line_ids), 1)
+ self.assertEqual(len(move_pack_cust_2.move_line_ids), 1)
+
+ move_pack_cust_1.move_line_ids[0].qty_done = 1.0
+ move_pack_cust_2.move_line_ids[0].qty_done = 1.0
+ (move_pack_cust_1 + move_pack_cust_2)._action_done()
+
+ def test_link_assign_6(self):
+ """ Test the assignment mechanism when four chained stock moves (2 sources, 2 dest) try to
+ move multiple units of an untracked by lot product. This particular test case simulates a two
+ step receipts with backorder.
+ """
+ move_supp_stock_1 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_6_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 3.0,
+ })
+ move_supp_stock_2 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_6_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move_stock_stock_1 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_6_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 3.0,
+ })
+ move_stock_stock_1.write({'move_orig_ids': [(4, move_supp_stock_1.id, 0), (4, move_supp_stock_2.id, 0)]})
+ move_stock_stock_2 = self.env['stock.move'].create({
+ 'name': 'test_link_assign_6_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 3.0,
+ })
+ move_stock_stock_2.write({'move_orig_ids': [(4, move_supp_stock_1.id, 0), (4, move_supp_stock_2.id, 0)]})
+
+ (move_supp_stock_1 + move_supp_stock_2 + move_stock_stock_1 + move_stock_stock_2)._action_confirm()
+ move_supp_stock_1._action_assign()
+ self.assertEqual(move_supp_stock_1.state, 'assigned')
+ self.assertEqual(move_supp_stock_2.state, 'assigned')
+ self.assertEqual(move_stock_stock_1.state, 'waiting')
+ self.assertEqual(move_stock_stock_2.state, 'waiting')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ # do the fist move, it'll bring 3 units in stock location so only `move_stock_stock_1`
+ # should be assigned
+ move_supp_stock_1.move_line_ids.qty_done = 3.0
+ move_supp_stock_1._action_done()
+ self.assertEqual(move_supp_stock_1.state, 'done')
+ self.assertEqual(move_supp_stock_2.state, 'assigned')
+ self.assertEqual(move_stock_stock_1.state, 'assigned')
+ self.assertEqual(move_stock_stock_2.state, 'waiting')
+
+ def test_link_assign_7(self):
+ # on the dozen uom, set the rounding set 1.0
+ self.uom_dozen.rounding = 1
+
+ # 6 units are available in stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 6.0)
+
+ # create pickings and moves for a pick -> pack mto scenario
+ picking_stock_pack = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_internal').id,
+ })
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_assign_7',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking_stock_pack.id,
+ })
+ picking_pack_cust = self.env['stock.picking'].create({
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_7',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking_pack_cust.id,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+ (move_stock_pack + move_pack_cust)._action_confirm()
+
+ # the pick should not be reservable because of the rounding of the dozen
+ move_stock_pack._action_assign()
+ self.assertEqual(move_stock_pack.state, 'confirmed')
+ move_pack_cust._action_assign()
+ self.assertEqual(move_pack_cust.state, 'waiting')
+
+ # move the 6 units by adding an unreserved move line
+ move_stock_pack.write({'move_line_ids': [(0, 0, {
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 6,
+ 'product_uom_qty': 0,
+ 'lot_id': False,
+ 'package_id': False,
+ 'result_package_id': False,
+ 'location_id': move_stock_pack.location_id.id,
+ 'location_dest_id': move_stock_pack.location_dest_id.id,
+ 'picking_id': picking_stock_pack.id,
+ })]})
+
+ # the quantity done on the move should not respect the rounding of the move line
+ self.assertEqual(move_stock_pack.quantity_done, 0.5)
+
+ # create the backorder in the uom of the quants
+ backorder_wizard_dict = picking_stock_pack.button_validate()
+ backorder_wizard = Form(self.env[backorder_wizard_dict['res_model']].with_context(backorder_wizard_dict['context'])).save()
+ backorder_wizard.process()
+ self.assertEqual(move_stock_pack.state, 'done')
+ self.assertEqual(move_stock_pack.quantity_done, 0.5)
+ self.assertEqual(move_stock_pack.product_uom_qty, 0.5)
+
+ # the second move should not be reservable because of the rounding on the dozen
+ move_pack_cust._action_assign()
+ self.assertEqual(move_pack_cust.state, 'partially_available')
+ move_line_pack_cust = move_pack_cust.move_line_ids
+ self.assertEqual(move_line_pack_cust.product_uom_qty, 6)
+ self.assertEqual(move_line_pack_cust.product_uom_id.id, self.uom_unit.id)
+
+ # move a dozen on the backorder to see how we handle the extra move
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_stock_pack.id)])
+ backorder.move_lines.write({'move_line_ids': [(0, 0, {
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_dozen.id,
+ 'qty_done': 1,
+ 'product_uom_qty': 0,
+ 'lot_id': False,
+ 'package_id': False,
+ 'result_package_id': False,
+ 'location_id': backorder.location_id.id,
+ 'location_dest_id': backorder.location_dest_id.id,
+ 'picking_id': backorder.id,
+ })]})
+ backorder.button_validate()
+ backorder_move = backorder.move_lines
+ self.assertEqual(backorder_move.state, 'done')
+ self.assertEqual(backorder_move.quantity_done, 12.0)
+ self.assertEqual(backorder_move.product_uom_qty, 12.0)
+ self.assertEqual(backorder_move.product_uom, self.uom_unit)
+
+ # the second move should now be reservable
+ move_pack_cust._action_assign()
+ self.assertEqual(move_pack_cust.state, 'assigned')
+ self.assertEqual(move_line_pack_cust.product_uom_qty, 12)
+ self.assertEqual(move_line_pack_cust.product_uom_id.id, self.uom_unit.id)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, move_stock_pack.location_dest_id), 6)
+
+ def test_link_assign_8(self):
+ """ Set the rounding of the dozen to 1.0, create a chain of two move for a dozen, the product
+ concerned is tracked by serial number. Check that the flow is ok.
+ """
+ # on the dozen uom, set the rounding set 1.0
+ self.uom_dozen.rounding = 1
+
+ # 6 units are available in stock
+ for i in range(1, 13):
+ lot_id = self.env['stock.production.lot'].create({
+ 'name': 'lot%s' % str(i),
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot_id)
+
+ # create pickings and moves for a pick -> pack mto scenario
+ picking_stock_pack = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_internal').id,
+ })
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_assign_7',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking_stock_pack.id,
+ })
+ picking_pack_cust = self.env['stock.picking'].create({
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_7',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking_pack_cust.id,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+ (move_stock_pack + move_pack_cust)._action_confirm()
+
+ move_stock_pack._action_assign()
+ self.assertEqual(move_stock_pack.state, 'assigned')
+ move_pack_cust._action_assign()
+ self.assertEqual(move_pack_cust.state, 'waiting')
+
+ for ml in move_stock_pack.move_line_ids:
+ ml.qty_done = 1
+ picking_stock_pack.button_validate()
+ self.assertEqual(move_pack_cust.state, 'assigned')
+ for ml in move_pack_cust.move_line_ids:
+ self.assertEqual(ml.product_uom_qty, 1)
+ self.assertEqual(ml.product_uom_id.id, self.uom_unit.id)
+ self.assertTrue(bool(ml.lot_id.id))
+
+ def test_link_assign_9(self):
+ """ Create an uom "3 units" which is 3 times the units but without rounding. Create 3
+ quants in stock and two chained moves. The first move will bring the 3 quants but the
+ second only validate 2 and create a backorder for the last one. Check that the reservation
+ is correctly cleared up for the last one.
+ """
+ uom_3units = self.env['uom.uom'].create({
+ 'name': '3 units',
+ 'category_id': self.uom_unit.category_id.id,
+ 'factor_inv': 3,
+ 'rounding': 1,
+ 'uom_type': 'bigger',
+ })
+ for i in range(1, 4):
+ lot_id = self.env['stock.production.lot'].create({
+ 'name': 'lot%s' % str(i),
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot_id)
+
+ picking_stock_pack = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_internal').id,
+ })
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_assign_9',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': uom_3units.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking_stock_pack.id,
+ })
+ picking_pack_cust = self.env['stock.picking'].create({
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_0',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': uom_3units.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking_pack_cust.id,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+ (move_stock_pack + move_pack_cust)._action_confirm()
+
+ picking_stock_pack.action_assign()
+ for ml in picking_stock_pack.move_lines.move_line_ids:
+ ml.qty_done = 1
+ picking_stock_pack.button_validate()
+ self.assertEqual(picking_pack_cust.state, 'assigned')
+ for ml in picking_pack_cust.move_lines.move_line_ids:
+ if ml.lot_id.name != 'lot3':
+ ml.qty_done = 1
+ res_dict_for_back_order = picking_pack_cust.button_validate()
+ backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
+ backorder_wizard.process()
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_pack_cust.id)])
+ backordered_move = backorder.move_lines
+
+ # due to the rounding, the backordered quantity is 0.999 ; we shoudln't be able to reserve
+ # 0.999 on a tracked by serial number quant
+ backordered_move._action_assign()
+ self.assertEqual(backordered_move.reserved_availability, 0)
+
+ # force the serial number and validate
+ lot3 = self.env['stock.production.lot'].search([('name', '=', "lot3")])
+ backorder.write({'move_line_ids': [(0, 0, {
+ 'product_id': self.product_serial.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 1,
+ 'product_uom_qty': 0,
+ 'lot_id': lot3.id,
+ 'package_id': False,
+ 'result_package_id': False,
+ 'location_id': backordered_move.location_id.id,
+ 'location_dest_id': backordered_move.location_dest_id.id,
+ 'move_id': backordered_move.id,
+ })]})
+
+ backorder.button_validate()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.customer_location), 3)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.pack_location), 0)
+
+ def test_link_assign_10(self):
+ """ Test the assignment mechanism with partial availability.
+ """
+ # make some stock:
+ # stock location: 2.0
+ # pack location: -1.0
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1.0)
+
+ move_out = self.env['stock.move'].create({
+ 'name': 'test_link_assign_out',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move_out._action_confirm()
+ move_out._action_assign()
+ move_out.quantity_done = 1.0
+ move_out._action_done()
+ self.assertEqual(len(self.gather_relevant(self.product, self.pack_location)), 1.0)
+
+ move_stock_pack = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move_pack_cust = self.env['stock.move'].create({
+ 'name': 'test_link_assign_1_2',
+ 'location_id': self.pack_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
+ move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
+
+ (move_stock_pack + move_pack_cust)._action_confirm()
+ move_stock_pack._action_assign()
+ move_stock_pack.quantity_done = 2.0
+ move_stock_pack._action_done()
+ self.assertEqual(len(move_pack_cust.move_line_ids), 1)
+
+ self.assertAlmostEqual(move_pack_cust.reserved_availability, 1.0)
+ self.assertEqual(move_pack_cust.state, 'partially_available')
+
+ def test_use_reserved_move_line_1(self):
+ """ Test that _free_reservation work when quantity is only available on
+ reserved move lines.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 10.0)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move2._action_confirm()
+ move2._action_assign()
+ move3 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 0.0,
+ 'quantity_done': 1.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3._action_done()
+ self.assertEqual(move3.state, 'done')
+ quant = self.env['stock.quant']._gather(self.product, self.stock_location)
+ self.assertEqual(quant.quantity, 9.0)
+ self.assertEqual(quant.reserved_quantity, 9.0)
+
+ def test_use_reserved_move_line_2(self):
+ # make 12 units available in stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 12.0)
+
+ # reserve 12 units
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_use_reserved_move_line_2_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 12,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ quant = self.env['stock.quant']._gather(self.product, self.stock_location)
+ self.assertEqual(quant.quantity, 12)
+ self.assertEqual(quant.reserved_quantity, 12)
+
+ # force a move of 1 dozen
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_use_reserved_move_line_2_2',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ self.assertEqual(move2.state, 'confirmed')
+ move2._set_quantity_done(1)
+ move2._action_done()
+
+ # mov1 should be unreserved and the quant should be unlinked
+ self.assertEqual(move1.state, 'confirmed')
+ quant = self.env['stock.quant']._gather(self.product, self.stock_location)
+ self.assertEqual(quant.quantity, 0)
+ self.assertEqual(quant.reserved_quantity, 0)
+
+ def test_use_unreserved_move_line_1(self):
+ """ Test that validating a stock move linked to an untracked product reserved by another one
+ correctly unreserves the other one.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0)
+
+ # prepare the conflicting move
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+
+ # reserve those move
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ move2._action_confirm()
+ move2._action_assign()
+ self.assertEqual(move2.state, 'confirmed')
+
+ # use the product from the first one
+ move2.write({'move_line_ids': [(0, 0, {
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 1,
+ 'product_uom_qty': 0,
+ 'lot_id': False,
+ 'package_id': False,
+ 'result_package_id': False,
+ 'location_id': move2.location_id.id,
+ 'location_dest_id': move2.location_dest_id.id,
+ })]})
+ move2._action_done()
+
+ # the first move should go back to confirmed
+ self.assertEqual(move1.state, 'confirmed')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ def test_use_unreserved_move_line_2(self):
+ """ Test that validating a stock move linked to a tracked product reserved by another one
+ correctly unreserves the other one.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot1)
+
+ # prepare the conflicting move
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+
+ # reserve those move
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 1.0)
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ move2._action_confirm()
+ move2._action_assign()
+ self.assertEqual(move2.state, 'confirmed')
+
+ # use the product from the first one
+ move2.write({'move_line_ids': [(0, 0, {
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 1,
+ 'product_uom_qty': 0,
+ 'lot_id': lot1.id,
+ 'package_id': False,
+ 'result_package_id': False,
+ 'location_id': move2.location_id.id,
+ 'location_dest_id': move2.location_dest_id.id,
+ })]})
+ move2._action_done()
+
+ # the first move should go back to confirmed
+ self.assertEqual(move1.state, 'confirmed')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 0.0)
+
+ def test_use_unreserved_move_line_3(self):
+ """ Test the behavior of `_free_reservation` when ran on a recordset of move lines where
+ some are assigned and some are force assigned. `_free_reservation` should not use an
+ already processed move line when looking for a move line candidate to unreserve.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_3',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 3.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.quantity_done = 1
+
+ # add a forced move line in `move1`
+ move1.write({'move_line_ids': [(0, 0, {
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 2,
+ 'product_uom_qty': 0,
+ 'lot_id': False,
+ 'package_id': False,
+ 'result_package_id': False,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ })]})
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 3.0)
+
+ def test_use_unreserved_move_line_4(self):
+ product_01 = self.env['product.product'].create({
+ 'name': 'Product 01',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ product_02 = self.env['product.product'].create({
+ 'name': 'Product 02',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ self.env['stock.quant']._update_available_quantity(product_01, self.stock_location, 1)
+ self.env['stock.quant']._update_available_quantity(product_02, self.stock_location, 1)
+
+ customer = self.env['res.partner'].create({'name': 'SuperPartner'})
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': customer.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+
+ p01_move = self.env['stock.move'].create({
+ 'name': 'SuperMove01',
+ 'location_id': picking.location_id.id,
+ 'location_dest_id': picking.location_dest_id.id,
+ 'picking_id': picking.id,
+ 'product_id': product_01.id,
+ 'product_uom_qty': 1,
+ 'product_uom': product_01.uom_id.id,
+ })
+ p02_move = self.env['stock.move'].create({
+ 'name': 'SuperMove02',
+ 'location_id': picking.location_id.id,
+ 'location_dest_id': picking.location_dest_id.id,
+ 'picking_id': picking.id,
+ 'product_id': product_02.id,
+ 'product_uom_qty': 1,
+ 'product_uom': product_02.uom_id.id,
+ })
+
+ picking.action_confirm()
+ picking.action_assign()
+ p01_move.product_uom_qty = 0
+ picking.do_unreserve()
+ picking.action_assign()
+ p01_move.product_uom_qty = 1
+ self.assertEqual(p01_move.state, 'confirmed')
+
+ def test_edit_reserved_move_line_1(self):
+ """ Test that editing a stock move line linked to an untracked product correctly and
+ directly adapts the reservation. In this case, we edit the sublocation where we take the
+ product to another sublocation where a product is available.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.env['stock.quant']._update_available_quantity(self.product, shelf2_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ move1.move_line_ids.location_id = shelf2_location.id
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ def test_edit_reserved_move_line_2(self):
+ """ Test that editing a stock move line linked to a tracked product correctly and directly
+ adapts the reservation. In this case, we edit the lot to another available one.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+
+ move1.move_line_ids.lot_id = lot2.id
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 0.0)
+
+ def test_edit_reserved_move_line_3(self):
+ """ Test that editing a stock move line linked to a packed product correctly and directly
+ adapts the reservation. In this case, we edit the package to another available one.
+ """
+ package1 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_3'})
+ package2 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_3'})
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, package_id=package1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, package_id=package2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package2), 1.0)
+
+ move1.move_line_ids.package_id = package2.id
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package2), 0.0)
+
+ def test_edit_reserved_move_line_4(self):
+ """ Test that editing a stock move line linked to an owned product correctly and directly
+ adapts the reservation. In this case, we edit the owner to another available one.
+ """
+ owner1 = self.env['res.partner'].create({'name': 'test_edit_reserved_move_line_4_1'})
+ owner2 = self.env['res.partner'].create({'name': 'test_edit_reserved_move_line_4_2'})
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, owner_id=owner1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, owner_id=owner2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner2), 1.0)
+
+ move1.move_line_ids.owner_id = owner2.id
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner2), 0.0)
+
+ def test_edit_reserved_move_line_5(self):
+ """ Test that editing a stock move line linked to a packed and tracked product correctly
+ and directly adapts the reservation. In this case, we edit the lot to another available one
+ that is not in a pack.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ package1 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_5'})
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot1, package_id=package1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1, package_id=package1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+ move_line = move1.move_line_ids[0]
+ move_line.write({'package_id': False, 'lot_id': lot2.id})
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 0.0)
+
+ def test_edit_reserved_move_line_6(self):
+ """ Test that editing a stock move line linked to an untracked product correctly and
+ directly adapts the reservation. In this case, we edit the sublocation where we take the
+ product to another sublocation where a product is NOT available.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(move1.move_line_ids.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ move1.move_line_ids.location_id = shelf2_location.id
+
+ self.assertEqual(move1.move_line_ids.state, 'confirmed')
+ self.assertEqual(move1.reserved_availability, 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+
+ def test_edit_reserved_move_line_7(self):
+ """ Send 5 tracked products to a client, but these products do not have any lot set in our
+ inventory yet: we only set them at delivery time. The created move line should have 5 items
+ without any lot set, if we edit to set them to lot1, the reservation should not change.
+ Validating the stock move should should not create a negative quant for this lot in stock
+ location.
+ # """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ # make some stock without assigning a lot id
+ self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 5)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+ move_line = move1.move_line_ids[0]
+ self.assertEqual(move_line.product_qty, 5)
+ move_line.qty_done = 5.0
+ self.assertEqual(move_line.product_qty, 5) # don't change reservation
+ move_line.lot_id = lot1
+ self.assertEqual(move_line.product_qty, 5) # don't change reservation when assgning a lot now
+
+ move1._action_done()
+ self.assertEqual(move_line.product_qty, 0) # change reservation to 0 for done move
+ self.assertEqual(move1.state, 'done')
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_lot, self.stock_location)), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_lot, self.stock_location, lot_id=lot1, strict=True)), 0.0)
+
+ def test_edit_reserved_move_line_8(self):
+ """ Send 5 tracked products to a client, but some of these products do not have any lot set
+ in our inventory yet: we only set them at delivery time. Adding a lot_id on the move line
+ that does not have any should not change its reservation, and validating should not create
+ a negative quant for this lot in stock.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ # make some stock without assigning a lot id
+ self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 3)
+ self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 2, lot_id=lot1)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ self.assertEqual(move1.state, 'draft')
+
+ # confirmation
+ move1._action_confirm()
+ self.assertEqual(move1.state, 'confirmed')
+
+ # assignment
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 2)
+
+ tracked_move_line = None
+ untracked_move_line = None
+ for move_line in move1.move_line_ids:
+ if move_line.lot_id:
+ tracked_move_line = move_line
+ else:
+ untracked_move_line = move_line
+
+ self.assertEqual(tracked_move_line.product_qty, 2)
+ tracked_move_line.qty_done = 2
+
+ self.assertEqual(untracked_move_line.product_qty, 3)
+ untracked_move_line.lot_id = lot2
+ self.assertEqual(untracked_move_line.product_qty, 3) # don't change reservation
+ untracked_move_line.qty_done = 3
+ self.assertEqual(untracked_move_line.product_qty, 3) # don't change reservation
+
+ move1._action_done()
+ self.assertEqual(untracked_move_line.product_qty, 0) # change reservation to 0 for done move
+ self.assertEqual(tracked_move_line.product_qty, 0) # change reservation to 0 for done move
+ self.assertEqual(move1.state, 'done')
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot2, strict=True), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_lot, self.stock_location)), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_lot, self.stock_location, lot_id=lot1, strict=True)), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_lot, self.stock_location, lot_id=lot2, strict=True)), 0.0)
+
+ def test_edit_done_move_line_1(self):
+ """ Test that editing a done stock move line linked to an untracked product correctly and
+ directly adapts the transfer. In this case, we edit the sublocation where we take the
+ product to another sublocation where a product is available.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.env['stock.quant']._update_available_quantity(self.product, shelf2_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+
+ # move from shelf1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ # edit once done, we actually moved from shelf2
+ move1.move_line_ids.location_id = shelf2_location.id
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ def test_edit_done_move_line_2(self):
+ """ Test that editing a done stock move line linked to a tracked product correctly and directly
+ adapts the transfer. In this case, we edit the lot to another available one.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+
+ move1.move_line_ids.lot_id = lot2.id
+
+ # reserved_availability should always been 0 for done move.
+ self.assertEqual(move1.reserved_availability, 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 0.0)
+
+ def test_edit_done_move_line_3(self):
+ """ Test that editing a done stock move line linked to a packed product correctly and directly
+ adapts the transfer. In this case, we edit the package to another available one.
+ """
+ package1 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_3'})
+ package2 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_3'})
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, package_id=package1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, package_id=package2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package2), 1.0)
+
+ move1.move_line_ids.package_id = package2.id
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, package_id=package2), 0.0)
+
+ def test_edit_done_move_line_4(self):
+ """ Test that editing a done stock move line linked to an owned product correctly and directly
+ adapts the transfer. In this case, we edit the owner to another available one.
+ """
+ owner1 = self.env['res.partner'].create({'name': 'test_edit_reserved_move_line_4_1'})
+ owner2 = self.env['res.partner'].create({'name': 'test_edit_reserved_move_line_4_2'})
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, owner_id=owner1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, owner_id=owner2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner2), 1.0)
+
+ move1.move_line_ids.owner_id = owner2.id
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, owner_id=owner2), 0.0)
+
+ def test_edit_done_move_line_5(self):
+ """ Test that editing a done stock move line linked to a packed and tracked product correctly
+ and directly adapts the transfer. In this case, we edit the lot to another available one
+ that is not in a pack.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product.id,
+ 'company_id': self.env.company.id,
+ })
+ package1 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_5'})
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot1, package_id=package1)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0, lot_id=lot2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1, package_id=package1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 1.0)
+ move_line = move1.move_line_ids[0]
+ move_line.write({'package_id': False, 'lot_id': lot2.id})
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot1, package_id=package1), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, lot_id=lot2), 0.0)
+
+ def test_edit_done_move_line_6(self):
+ """ Test that editing a done stock move line linked to an untracked product correctly and
+ directly adapts the transfer. In this case, we edit the sublocation where we take the
+ product to another sublocation where a product is NOT available.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ move1.move_line_ids.location_id = shelf2_location.id
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location, allow_negative=True), -1.0)
+
+ def test_edit_done_move_line_7(self):
+ """ Test that editing a done stock move line linked to an untracked product correctly and
+ directly adapts the transfer. In this case, we edit the sublocation where we take the
+ product to another sublocation where a product is NOT available because it has been reserved
+ by another move.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.env['stock.quant']._update_available_quantity(self.product, shelf2_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 1.0)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+
+ self.assertEqual(move2.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ move1.move_line_ids.location_id = shelf2_location.id
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf2_location), 0.0)
+ self.assertEqual(move2.state, 'confirmed')
+
+ def test_edit_done_move_line_8(self):
+ """ Test that editing a done stock move line linked to an untracked product correctly and
+ directly adapts the transfer. In this case, we increment the quantity done (and we do not
+ have more in stock.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ # move from shelf1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(move1.product_uom_qty, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ # edit once done, we actually moved 2 products
+ move1.move_line_ids.qty_done = 2
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location, allow_negative=True), -1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, allow_negative=True), -1.0)
+ self.assertEqual(move1.product_uom_qty, 2.0)
+
+ def test_edit_done_move_line_9(self):
+ """ Test that editing a done stock move line linked to an untracked product correctly and
+ directly adapts the transfer. In this case, we "cancel" the move by zeroing the qty done.
+ """
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, shelf1_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ # move from shelf1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ self.assertEqual(move1.product_uom_qty, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ # edit once done, we actually moved 2 products
+ move1.move_line_ids.qty_done = 0
+
+ self.assertEqual(move1.product_uom_qty, 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, shelf1_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ def test_edit_done_move_line_10(self):
+ """ Edit the quantity done for an incoming move shoudld also remove the quant if there
+ are no product in stock.
+ """
+ # move from shelf1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10
+ move1._action_done()
+
+ quant = self.gather_relevant(self.product, self.stock_location)
+ self.assertEqual(len(quant), 1.0)
+
+ # edit once done, we actually moved 2 products
+ move1.move_line_ids.qty_done = 0
+
+ quant = self.gather_relevant(self.product, self.stock_location)
+ self.assertEqual(len(quant), 0.0)
+ self.assertEqual(move1.product_uom_qty, 0.0)
+
+ def test_edit_done_move_line_11(self):
+ """ Add a move line and check if the quant is updated
+ """
+ owner = self.env['res.partner'].create({'name': 'Jean'})
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'partner_id': owner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ # move from shelf1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ move1.move_line_ids.qty_done = 10
+ picking._action_done()
+ self.assertEqual(move1.product_uom_qty, 10.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0)
+ self.env['stock.move.line'].create({
+ 'picking_id': move1.move_line_ids.picking_id.id,
+ 'move_id': move1.move_line_ids.move_id.id,
+ 'product_id': move1.move_line_ids.product_id.id,
+ 'qty_done': move1.move_line_ids.qty_done,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.move_line_ids.location_id.id,
+ 'location_dest_id': move1.move_line_ids.location_dest_id.id,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 20.0)
+ move1.move_line_ids[1].qty_done = 5
+ self.assertEqual(move1.product_uom_qty, 15.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 15.0)
+
+ def test_edit_done_move_line_12(self):
+ """ Test that editing a done stock move line linked a tracked product correctly and directly
+ adapts the transfer. In this case, we edit the lot to another one, but the original move line
+ is not in the default product's UOM.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ package1 = self.env['stock.quant.package'].create({'name': 'test_edit_done_move_line_12'})
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1.move_line_ids.lot_id = lot1.id
+ move1._action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1), 12.0)
+
+ # Change the done quantity from 1 dozen to two dozen
+ move1.move_line_ids.qty_done = 2
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1), 24.0)
+
+ def test_edit_done_move_line_13(self):
+ """ Test that editing a done stock move line linked to a packed and tracked product correctly
+ and directly adapts the transfer. In this case, we edit the lot to another available one
+ that we put in the same pack.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ package1 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_5'})
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1
+ move1.move_line_ids.lot_id = lot1.id
+ move1.move_line_ids.result_package_id = package1.id
+ move1._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1, package_id=package1), 1.0)
+
+ move1.move_line_ids.write({'lot_id': lot2.id})
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1, package_id=package1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot2, package_id=package1), 1.0)
+
+ def test_immediate_validate_1(self):
+ """ In a picking with a single available move, clicking on validate without filling any
+ quantities should open a wizard asking to process all the reservation (so, the whole move).
+ """
+ partner = self.env['res.partner'].create({'name': 'Jean'})
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ res_dict = picking.button_validate()
+ self.assertEqual(res_dict.get('res_model'), 'stock.immediate.transfer')
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ wizard.process()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0)
+
+ def test_immediate_validate_2(self):
+ """ In a picking with a single partially available move, clicking on validate without
+ filling any quantities should open a wizard asking to process all the reservation (so, only
+ a part of the initial demand). Validating this wizard should open another one asking for
+ the creation of a backorder. If the backorder is created, it should contain the quantities
+ not processed.
+ """
+ partner = self.env['res.partner'].create({'name': 'Jean'})
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 5.0)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_2',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ # Only 5 products are reserved on the move of 10, click on `button_validate`.
+ res_dict = picking.button_validate()
+ self.assertEqual(res_dict.get('res_model'), 'stock.immediate.transfer')
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ res_dict_for_back_order = wizard.process()
+ self.assertEqual(res_dict_for_back_order.get('res_model'), 'stock.backorder.confirmation')
+ backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
+ # Chose to create a backorder.
+ backorder_wizard.process()
+
+ # Only 5 products should be processed on the initial move.
+ self.assertEqual(picking.move_lines.state, 'done')
+ self.assertEqual(picking.move_lines.quantity_done, 5.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 0.0)
+
+ # The backoder should contain a move for the other 5 produts.
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', picking.id)])
+ self.assertEqual(len(backorder), 1.0)
+ self.assertEqual(backorder.move_lines.product_uom_qty, 5.0)
+
+ def test_immediate_validate_3(self):
+ """ In a picking with two moves, one partially available and one unavailable, clicking
+ on validate without filling any quantities should open a wizard asking to process all the
+ reservation (so, only a part of one of the moves). Validating this wizard should open
+ another one asking for the creation of a backorder. If the backorder is created, it should
+ contain the quantities not processed.
+ """
+ product5 = self.env['product.product'].create({
+ 'name': 'Product 5',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1)
+
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_internal').id,
+ })
+ product1_move = self.env['stock.move'].create({
+ 'name': 'product1_move',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100,
+ })
+ product5_move = self.env['stock.move'].create({
+ 'name': 'product3_move',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'picking_id': picking.id,
+ 'product_id': product5.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+
+ # product1_move should be partially available (1/100), product5_move should be totally
+ # unavailable (0/100)
+ self.assertEqual(product1_move.state, 'partially_available')
+ self.assertEqual(product5_move.state, 'confirmed')
+
+ action = picking.button_validate()
+ self.assertEqual(action.get('res_model'), 'stock.immediate.transfer')
+ wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
+ action = wizard.process()
+ self.assertTrue(isinstance(action, dict), 'Should open backorder wizard')
+ self.assertEqual(action.get('res_model'), 'stock.backorder.confirmation')
+ wizard = self.env[(action.get('res_model'))].browse(action.get('res_id')).with_context(action.get('context'))
+ wizard.process()
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', picking.id)])
+ self.assertEqual(len(backorder), 1.0)
+
+ # The backorder should contain 99 product1 and 100 product5.
+ for backorder_move in backorder.move_lines:
+ if backorder_move.product_id.id == self.product.id:
+ self.assertEqual(backorder_move.product_qty, 99)
+ elif backorder_move.product_id.id == product5.id:
+ self.assertEqual(backorder_move.product_qty, 100)
+
+ def test_immediate_validate_4(self):
+ """ In a picking with a single available tracked by lot move, clicking on validate without
+ filling any quantities should pop up the immediate transfer wizard.
+ """
+ partner = self.env['res.partner'].create({'name': 'Jean'})
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 5.0, lot_id=lot1)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ # move from shelf1
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_4',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ # No quantities filled, immediate transfer wizard should pop up.
+ immediate_trans_wiz_dict = picking.button_validate()
+ self.assertEqual(immediate_trans_wiz_dict.get('res_model'), 'stock.immediate.transfer')
+ immediate_trans_wiz = Form(self.env[immediate_trans_wiz_dict['res_model']].with_context(immediate_trans_wiz_dict['context'])).save()
+ immediate_trans_wiz.process()
+
+ self.assertEqual(picking.move_lines.quantity_done, 5.0)
+ # Check move_lines data
+ self.assertEqual(len(picking.move_lines.move_line_ids), 1)
+ self.assertEqual(picking.move_lines.move_line_ids.lot_id, lot1)
+ self.assertEqual(picking.move_lines.move_line_ids.qty_done, 5.0)
+ # Check quants data
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 0.0)
+
+ def _create_picking_test_immediate_validate_5(self, picking_type_id, product_id):
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_type_id': picking_type_id.id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'move1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_id': picking.id,
+ 'picking_type_id': picking_type_id.id,
+ 'product_id': product_id.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+
+ picking.action_confirm()
+
+ for line in picking.move_line_ids:
+ line.qty_done = line.product_uom_qty
+
+ return picking
+
+ def test_immediate_validate_5(self):
+ """ In a receipt with a single tracked by serial numbers move, clicking on validate without
+ filling any quantities nor lot should open an UserError except if the picking type is
+ configured to allow otherwise.
+ """
+ picking_type_id = self.env.ref('stock.picking_type_in')
+ product_id = self.product_serial
+ self.assertTrue(picking_type_id.use_create_lots or picking_type_id.use_existing_lots)
+ self.assertEqual(product_id.tracking, 'serial')
+
+ picking = self._create_picking_test_immediate_validate_5(picking_type_id, product_id)
+ # should raise because no serial numbers were specified
+ self.assertRaises(UserError, picking.button_validate)
+
+ picking_type_id.use_create_lots = False
+ picking_type_id.use_existing_lots = False
+ picking = self._create_picking_test_immediate_validate_5(picking_type_id, product_id)
+ picking.button_validate()
+ self.assertEqual(picking.state, 'done')
+
+ def test_immediate_validate_6(self):
+ """ In a receipt picking with two moves, one tracked and one untracked, clicking on
+ validate without filling any quantities should displays an UserError as long as no quantity
+ done and lot_name is set on the tracked move. Now if the user validates the picking, the
+ wizard telling the user all reserved quantities will be processed will NOT be opened. This
+ wizard is only opene if no quantities were filled. So validating the picking at this state
+ will open another wizard asking for the creation of a backorder. Now, if the user processed
+ on the second move more than the reservation, a wizard will ask him to confirm.
+ """
+ picking_type = self.env.ref('stock.picking_type_in')
+ picking_type.use_create_lots = True
+ picking_type.use_existing_lots = False
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_type_id': picking_type.id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'product1_move',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1,
+ })
+ product3_move = self.env['stock.move'].create({
+ 'name': 'product3_move',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+
+ with self.assertRaises(UserError):
+ picking.button_validate()
+ product3_move.move_line_ids[0].qty_done = 1
+ with self.assertRaises(UserError):
+ picking.button_validate()
+ product3_move.move_line_ids[0].lot_name = '271828'
+ action = picking.button_validate() # should open backorder wizard
+
+ self.assertTrue(isinstance(action, dict), 'Should open backorder wizard')
+ self.assertEqual(action.get('res_model'), 'stock.backorder.confirmation')
+
+ def test_immediate_validate_7(self):
+ """ In a picking with a single unavailable move, clicking on validate without filling any
+ quantities should display an UserError telling the user he cannot process a picking without
+ any processed quantity.
+ """
+ partner = self.env['res.partner'].create({'name': 'Jean'})
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_2',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+
+ scrap = self.env['stock.scrap'].create({
+ 'picking_id': picking.id,
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'scrap_qty': 5.0,
+ })
+ scrap.do_scrap()
+
+ # No products are reserved on the move of 10, click on `button_validate`.
+ with self.assertRaises(UserError):
+ picking.button_validate()
+
+ def test_immediate_validate_8(self):
+ """Validate three receipts at once."""
+ partner = self.env['res.partner'].create({'name': 'Pierre'})
+ receipt1 = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_8_1',
+ 'location_id': receipt1.location_id.id,
+ 'location_dest_id': receipt1.location_dest_id.id,
+ 'picking_id': receipt1.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ receipt1.action_confirm()
+ receipt2 = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_8_2',
+ 'location_id': receipt2.location_id.id,
+ 'location_dest_id': receipt2.location_dest_id.id,
+ 'picking_id': receipt2.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ receipt2.action_confirm()
+ receipt3 = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ self.env['stock.move'].create({
+ 'name': 'test_immediate_validate_8_3',
+ 'location_id': receipt3.location_id.id,
+ 'location_dest_id': receipt3.location_dest_id.id,
+ 'picking_id': receipt3.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ receipt3.action_confirm()
+
+ immediate_trans_wiz_dict = (receipt1 + receipt2).button_validate()
+ immediate_trans_wiz = Form(self.env[immediate_trans_wiz_dict['res_model']].with_context(immediate_trans_wiz_dict['context'])).save()
+ # The different transfers are displayed to the users.
+ self.assertTrue(immediate_trans_wiz.show_transfers)
+ # All transfers are processed by default
+ self.assertEqual(immediate_trans_wiz.immediate_transfer_line_ids.mapped('to_immediate'), [True, True])
+ # Only transfer receipt1
+ immediate_trans_wiz.immediate_transfer_line_ids.filtered(lambda line: line.picking_id == receipt2).to_immediate = False
+ immediate_trans_wiz.process()
+ self.assertEqual(receipt1.state, 'done')
+ self.assertEqual(receipt2.state, 'assigned')
+ # Transfer receipt2 and receipt3.
+ immediate_trans_wiz_dict = (receipt3 + receipt2).button_validate()
+ immediate_trans_wiz = Form(self.env[immediate_trans_wiz_dict['res_model']].with_context(immediate_trans_wiz_dict['context'])).save()
+ immediate_trans_wiz.process()
+ self.assertEqual(receipt2.state, 'done')
+ self.assertEqual(receipt3.state, 'done')
+
+ def test_set_quantity_done_1(self):
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_set_quantity_done_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_set_quantity_done_2',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ (move1 + move2)._action_confirm()
+ (move1 + move2).write({'quantity_done': 1})
+ self.assertEqual(move1.quantity_done, 1)
+ self.assertEqual(move2.quantity_done, 1)
+
+ def test_initial_demand_1(self):
+ """ Check that the initial demand is set to 0 when creating a move by hand, and
+ that changing the product on the move do not reset the initial demand.
+ """
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ })
+ self.assertEqual(move1.state, 'draft')
+ self.assertEqual(move1.product_uom_qty, 0)
+ move1.product_uom_qty = 100
+ move1.product_id = self.product_serial
+ move1.onchange_product_id()
+ self.assertEqual(move1.product_uom_qty, 100)
+
+ def test_scrap_1(self):
+ """ Check the created stock move and the impact on quants when we scrap a
+ storable product.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1)
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product.id,
+ 'product_uom_id':self.product.uom_id.id,
+ 'scrap_qty': 1,
+ })
+ scrap.do_scrap()
+ self.assertEqual(scrap.state, 'done')
+ move = scrap.move_id
+ self.assertEqual(move.state, 'done')
+ self.assertEqual(move.quantity_done, 1)
+ self.assertEqual(move.scrapped, True)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+
+ def test_scrap_2(self):
+ """ Check the created stock move and the impact on quants when we scrap a
+ consumable product.
+ """
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product_consu.id,
+ 'product_uom_id':self.product_consu.uom_id.id,
+ 'scrap_qty': 1,
+ })
+ self.assertEqual(scrap.name, 'New', 'Name should be New in draft state')
+ scrap.do_scrap()
+ self.assertTrue(scrap.name.startswith('SP/'), 'Sequence should be Changed after do_scrap')
+ self.assertEqual(scrap.state, 'done')
+ move = scrap.move_id
+ self.assertEqual(move.state, 'done')
+ self.assertEqual(move.quantity_done, 1)
+ self.assertEqual(move.scrapped, True)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_consu, self.stock_location), 0)
+
+ def test_scrap_3(self):
+ """ Scrap the product of a reserved move line. Check that the move line is
+ correctly deleted and that the associated stock move is not assigned anymore.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_scrap_3',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product.id,
+ 'product_uom_id':self.product.uom_id.id,
+ 'scrap_qty': 1,
+ })
+ scrap.do_scrap()
+ self.assertEqual(move1.state, 'confirmed')
+ self.assertEqual(len(move1.move_line_ids), 0)
+
+ def test_scrap_4(self):
+ """ Scrap the product of a picking. Then modify the
+ done linked stock move and ensure the scrap quantity is also
+ updated.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 10)
+ partner = self.env['res.partner'].create({'name': 'Kimberley'})
+ picking = self.env['stock.picking'].create({
+ 'name': 'A single picking with one move to scrap',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'A move to confirm and scrap its product',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking.id,
+ })
+ move1._action_confirm()
+
+ self.assertEqual(move1.state, 'confirmed')
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product.id,
+ 'product_uom_id': self.product.uom_id.id,
+ 'scrap_qty': 5,
+ 'picking_id': picking.id,
+ })
+
+ scrap.action_validate()
+ self.assertEqual(len(picking.move_lines), 2)
+ scrapped_move = picking.move_lines.filtered(lambda m: m.state == 'done')
+ self.assertTrue(scrapped_move, 'No scrapped move created.')
+ self.assertEqual(scrapped_move.scrap_ids.ids, [scrap.id], 'Wrong scrap linked to the move.')
+ self.assertEqual(scrap.scrap_qty, 5, 'Scrap quantity has been modified and is not correct anymore.')
+
+ scrapped_move.quantity_done = 8
+ self.assertEqual(scrap.scrap_qty, 8, 'Scrap quantity is not updated.')
+
+ def test_scrap_5(self):
+ """ Scrap the product of a reserved move line where the product is reserved in another
+ unit of measure. Check that the move line is correctly updated after the scrap.
+ """
+ # 4 units are available in stock
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 4)
+
+ # try to reserve a dozen
+ partner = self.env['res.partner'].create({'name': 'Kimberley'})
+ picking = self.env['stock.picking'].create({
+ 'name': 'A single picking with one move to scrap',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'A move to confirm and scrap its product',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking.id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.reserved_availability, 0.33)
+
+ # scrap a unit
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product.id,
+ 'product_uom_id': self.product.uom_id.id,
+ 'scrap_qty': 1,
+ 'picking_id': picking.id,
+ })
+ scrap.action_validate()
+
+ self.assertEqual(scrap.state, 'done')
+ self.assertEqual(move1.reserved_availability, 0.25)
+
+ def test_scrap_6(self):
+ """ Check that scrap correctly handle UoM. """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1)
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_dozen.id,
+ 'scrap_qty': 1,
+ })
+ warning_message = scrap.action_validate()
+ self.assertEqual(warning_message.get('res_model', 'Wrong Model'), 'stock.warn.insufficient.qty.scrap')
+ insufficient_qty_wizard = self.env['stock.warn.insufficient.qty.scrap'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'scrap_id': scrap.id,
+ 'quantity': 1,
+ 'product_uom_name': self.product.uom_id.name
+ })
+ insufficient_qty_wizard.action_done()
+ self.assertEqual(self.env['stock.quant']._gather(self.product, self.stock_location).quantity, -11)
+
+ def test_in_date_1(self):
+ """ Check that moving a tracked quant keeps the incoming date.
+ """
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.lot_name = 'lot1'
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ quant = self.gather_relevant(self.product_lot, self.stock_location)
+ self.assertEqual(len(quant), 1.0)
+ self.assertNotEqual(quant.in_date, False)
+
+ # Keep a reference to the initial incoming date in order to compare it later.
+ initial_incoming_date = quant.in_date
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 1
+ move2._action_done()
+
+ quant = self.gather_relevant(self.product_lot, self.pack_location)
+ self.assertEqual(len(quant), 1.0)
+ self.assertEqual(quant.in_date, initial_incoming_date)
+
+ def test_in_date_2(self):
+ """ Check that editing a done move line for a tracked product and changing its lot
+ correctly restores the original lot with its incoming date and remove the new lot
+ with its incoming date.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ # receive lot1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.lot_id = lot1
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ # receive lot2
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.lot_id = lot2
+ move2.move_line_ids.qty_done = 1
+ move2._action_done()
+
+ initial_in_date_lot2 = self.env['stock.quant'].search([
+ ('location_id', '=', self.stock_location.id),
+ ('product_id', '=', self.product_lot.id),
+ ('lot_id', '=', lot2.id),
+ ]).in_date
+
+ # Edit lot1's incoming date.
+ quant_lot1 = self.env['stock.quant'].search([
+ ('location_id', '=', self.stock_location.id),
+ ('product_id', '=', self.product_lot.id),
+ ('lot_id', '=', lot1.id),
+ ])
+ from odoo.fields import Datetime
+ from datetime import timedelta
+ initial_in_date_lot1 = Datetime.now() - timedelta(days=5)
+ quant_lot1.in_date = initial_in_date_lot1
+
+ # Move one quant to pack location
+ move3 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 1
+ move3._action_done()
+ quant_in_pack = self.env['stock.quant'].search([
+ ('product_id', '=', self.product_lot.id),
+ ('location_id', '=', self.pack_location.id),
+ ])
+ # As lot1 has an older date and FIFO is set by default, it's the one that should be
+ # in pack.
+ self.assertEqual(len(quant_in_pack), 1)
+ self.assertAlmostEqual(quant_in_pack.in_date, initial_in_date_lot1, delta=timedelta(seconds=1))
+ self.assertEqual(quant_in_pack.lot_id, lot1)
+
+ # Now, edit the move line and actually move the other lot
+ move3.move_line_ids.lot_id = lot2
+
+ # Check that lot1 correctly is back to stock with its right in_date
+ quant_lot1 = self.env['stock.quant'].search([
+ ('location_id.usage', '=', 'internal'),
+ ('product_id', '=', self.product_lot.id),
+ ('lot_id', '=', lot1.id),
+ ('quantity', '!=', 0),
+ ])
+ self.assertEqual(quant_lot1.location_id, self.stock_location)
+ self.assertAlmostEqual(quant_lot1.in_date, initial_in_date_lot1, delta=timedelta(seconds=1))
+
+ # Check that lo2 is in pack with is right in_date
+ quant_lot2 = self.env['stock.quant'].search([
+ ('location_id.usage', '=', 'internal'),
+ ('product_id', '=', self.product_lot.id),
+ ('lot_id', '=', lot2.id),
+ ('quantity', '!=', 0),
+ ])
+ self.assertEqual(quant_lot2.location_id, self.pack_location)
+ self.assertAlmostEqual(quant_lot2.in_date, initial_in_date_lot2, delta=timedelta(seconds=1))
+
+ def test_in_date_3(self):
+ """ Check that, when creating a move line on a done stock move, the lot and its incoming
+ date are correctly moved to the destination location.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ # receive lot1
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.lot_id = lot1
+ move1.move_line_ids.qty_done = 1
+ move1._action_done()
+
+ # receive lot2
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.lot_id = lot2
+ move2.move_line_ids.qty_done = 1
+ move2._action_done()
+
+ initial_in_date_lot2 = self.env['stock.quant'].search([
+ ('location_id', '=', self.stock_location.id),
+ ('product_id', '=', self.product_lot.id),
+ ('lot_id', '=', lot2.id),
+ ('quantity', '!=', 0),
+ ]).in_date
+
+ # Edit lot1's incoming date.
+ quant_lot1 = self.env['stock.quant'].search([
+ ('location_id.usage', '=', 'internal'),
+ ('product_id', '=', self.product_lot.id),
+ ('lot_id', '=', lot1.id),
+ ('quantity', '!=', 0),
+ ])
+ from odoo.fields import Datetime
+ from datetime import timedelta
+ initial_in_date_lot1 = Datetime.now() - timedelta(days=5)
+ quant_lot1.in_date = initial_in_date_lot1
+
+ # Move one quant to pack location
+ move3 = self.env['stock.move'].create({
+ 'name': 'test_in_date_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.pack_location.id,
+ 'product_id': self.product_lot.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 1
+ move3._action_done()
+
+ # Now, also move lot2
+ self.env['stock.move.line'].create({
+ 'move_id': move3.id,
+ 'product_id': move3.product_id.id,
+ 'qty_done': 1,
+ 'product_uom_id': move3.product_uom.id,
+ 'location_id': move3.location_id.id,
+ 'location_dest_id': move3.location_dest_id.id,
+ 'lot_id': lot2.id,
+ })
+
+ quants = self.env['stock.quant'].search([
+ ('location_id.usage', '=', 'internal'),
+ ('product_id', '=', self.product_lot.id),
+ ('quantity', '!=', 0),
+ ])
+ self.assertEqual(len(quants), 2)
+ for quant in quants:
+ if quant.lot_id == lot1:
+ self.assertAlmostEqual(quant.in_date, initial_in_date_lot1, delta=timedelta(seconds=1))
+ elif quant.lot_id == lot2:
+ self.assertAlmostEqual(quant.in_date, initial_in_date_lot2, delta=timedelta(seconds=1))
+
+ def test_edit_initial_demand_1(self):
+ """ Increase initial demand once everything is reserved and check if
+ the existing move_line is updated.
+ """
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.product_uom_qty = 15
+ # _action_assign is automatically called
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(move1.product_uom_qty, 15)
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ def test_edit_initial_demand_2(self):
+ """ Decrease initial demand once everything is reserved and check if
+ the existing move_line has been dropped after the updated and another
+ is created once the move is reserved.
+ """
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ move1.product_uom_qty = 5
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(move1.product_uom_qty, 5)
+ self.assertEqual(len(move1.move_line_ids), 1)
+
+ def test_initial_demand_3(self):
+ """ Increase the initial demand on a receipt picking, the system should automatically
+ reserve the new quantity.
+ """
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ 'immediate_transfer': True,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'quantity_done': 10.0,
+ 'picking_id': picking.id,
+ })
+ picking._autoconfirm_picking()
+ self.assertEqual(picking.state, 'assigned')
+ move1.quantity_done = 12
+ self.assertEqual(picking.state, 'assigned')
+
+ def test_initial_demand_4(self):
+ """ Increase the initial demand on a delivery picking, the system should not automatically
+ reserve the new quantity.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 12)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'picking_id': picking.id,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ self.assertEqual(picking.state, 'assigned')
+ move1.product_uom_qty = 12
+ self.assertEqual(picking.state, 'assigned') # actually, partially available
+ self.assertEqual(move1.state, 'partially_available')
+ picking.action_assign()
+ self.assertEqual(move1.state, 'assigned')
+
+ def test_change_product_type(self):
+ """ Changing type of an existing product will raise a user error if
+ - some move are reserved
+ - switching from a stockable product when qty_available is not zero
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 10)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_customer',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ with self.assertRaises(UserError):
+ self.product.type = 'consu'
+ move1._action_cancel()
+
+ with self.assertRaises(UserError):
+ self.product.type = 'consu'
+
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, -self.product.qty_available)
+ self.product.type = 'consu'
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_customer',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+
+ move2._action_confirm()
+ move2._action_assign()
+
+ with self.assertRaises(UserError):
+ self.product.type = 'product'
+ move2._action_cancel()
+ self.product.type = 'product'
+
+ def test_edit_done_picking_1(self):
+ """ Add a new move line in a done picking should generate an
+ associated move.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 12)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'picking_id': picking.id,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ move1.quantity_done = 10
+ picking._action_done()
+
+ self.assertEqual(len(picking.move_lines), 1, 'One move should exist for the picking.')
+ self.assertEqual(len(picking.move_line_ids), 1, 'One move line should exist for the picking.')
+
+ ml = self.env['stock.move.line'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 2.0,
+ 'picking_id': picking.id,
+ })
+
+ self.assertEqual(len(picking.move_lines), 2, 'The new move associated to the move line does not exist.')
+ self.assertEqual(len(picking.move_line_ids), 2, 'It should be 2 move lines for the picking.')
+ self.assertTrue(ml.move_id in picking.move_lines, 'Links are not correct between picking, moves and move lines.')
+ self.assertEqual(picking.state, 'done', 'Picking should still done after adding a new move line.')
+ self.assertTrue(all(move.state == 'done' for move in picking.move_lines), 'Wrong state for move.')
+
+ def test_put_in_pack_1(self):
+ """ Check that reserving a move and adding its move lines to
+ different packages work as expected.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ 'picking_id': picking.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+ move1.quantity_done = 1
+ picking.action_put_in_pack()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+ self.assertEqual(len(picking.move_line_ids), 2)
+ unpacked_ml = picking.move_line_ids.filtered(lambda ml: not ml.result_package_id)
+ self.assertEqual(unpacked_ml.product_qty, 1)
+ unpacked_ml.qty_done = 1
+ picking.action_put_in_pack()
+ self.assertEqual(len(picking.move_line_ids), 2)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+ picking.button_validate()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 2)
+
+ def test_put_in_pack_2(self):
+ """Check that reserving moves without done quantity
+ adding in same package.
+ """
+ product1 = self.env['product.product'].create({
+ 'name': 'Product B',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1)
+ self.env['stock.quant']._update_available_quantity(product1, self.stock_location, 2)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking.id,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_transit_2',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ 'picking_id': picking.id,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, self.stock_location), 0)
+ picking.action_put_in_pack()
+ self.assertEqual(len(picking.move_line_ids), 2)
+ self.assertEqual(picking.move_line_ids[0].qty_done, 1, "Stock move line should have 1 quantity as a done quantity.")
+ self.assertEqual(picking.move_line_ids[1].qty_done, 2, "Stock move line should have 2 quantity as a done quantity.")
+ line1_result_package = picking.move_line_ids[0].result_package_id
+ line2_result_package = picking.move_line_ids[1].result_package_id
+ self.assertEqual(line1_result_package, line2_result_package, "Product and Product1 should be in a same package.")
+
+ def test_put_in_pack_3(self):
+ """Check that one reserving move without done quantity and
+ another reserving move with done quantity adding in different
+ package.
+ """
+ product1 = self.env['product.product'].create({
+ 'name': 'Product B',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1)
+ self.env['stock.quant']._update_available_quantity(product1, self.stock_location, 2)
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_transit_2',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ 'picking_id': picking.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, self.stock_location), 0)
+ move1.quantity_done = 1
+ picking.action_put_in_pack()
+ move2.quantity_done = 2
+ picking.action_put_in_pack()
+ self.assertEqual(len(picking.move_line_ids), 2)
+ line1_result_package = picking.move_line_ids[0].result_package_id
+ line2_result_package = picking.move_line_ids[1].result_package_id
+ self.assertNotEqual(line1_result_package, line2_result_package, "Product and Product1 should be in a different package.")
+
+ def test_forecast_availability(self):
+ """ Make an outgoing picking in dozens for a product stored in units.
+ Check that reserved_availabity is expressed in move uom and forecast_availability is in product base uom
+ """
+ # create product
+ product = self.env['product.product'].create({
+ 'name': 'Product In Units',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(product, self.stock_location, 36.0)
+ # create picking
+ picking_out = self.env['stock.picking'].create({
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id})
+ move = self.env['stock.move'].create({
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 2.0,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id})
+ # confirm
+ picking_out.action_confirm()
+ # check availability
+ picking_out.action_assign()
+ # check reserved_availabity expressed in move uom
+ self.assertEqual(move.reserved_availability, 2)
+ # check forecast_availability expressed in product base uom
+ self.assertEqual(move.forecast_availability, 24)
diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py
new file mode 100644
index 00000000..32c8f094
--- /dev/null
+++ b/addons/stock/tests/test_move2.py
@@ -0,0 +1,3146 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import timedelta
+
+from odoo.addons.stock.tests.common import TestStockCommon
+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):
+ picking_client = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+
+ dest = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_client.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'state': 'waiting',
+ 'procure_method': 'make_to_order',
+ })
+
+ picking_pick = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.pack_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_pick.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.pack_location,
+ 'move_dest_ids': [(4, dest.id)],
+ 'state': 'confirmed',
+ })
+ return picking_pick, picking_client
+
+ def create_pick_pack_ship(self):
+ picking_ship = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+
+ ship = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 1,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_ship.id,
+ 'location_id': self.output_location,
+ 'location_dest_id': self.customer_location,
+ })
+
+ picking_pack = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.pack_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+
+ pack = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 1,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_pack.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.output_location,
+ 'move_dest_ids': [(4, ship.id)],
+ })
+
+ picking_pick = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.pack_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 1,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_pick.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.pack_location,
+ 'move_dest_ids': [(4, pack.id)],
+ 'state': 'confirmed',
+ })
+ return picking_pick, picking_pack, picking_ship
+
+ def test_mto_moves(self):
+ """
+ 10 in stock, do pick->ship and check ship is assigned when pick is done, then backorder of ship
+ """
+ picking_pick, picking_client = self.create_pick_ship()
+ location = self.env['stock.location'].browse(self.stock_location)
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.productA, location, 10.0)
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_pick._action_done()
+
+ self.assertEqual(picking_client.state, 'assigned', 'The state of the client should be assigned')
+
+ # Now partially transfer the ship
+ picking_client.move_lines[0].move_line_ids[0].qty_done = 5
+ picking_client._action_done() # no new in order to create backorder
+
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_client.id)])
+ self.assertEqual(backorder.state, 'waiting', 'Backorder should be waiting for reservation')
+
+ def test_mto_moves_transfer(self):
+ """
+ 10 in stock, 5 in pack. Make sure it does not assign the 5 pieces in pack
+ """
+ picking_pick, picking_client = self.create_pick_ship()
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
+ pack_location = self.env['stock.location'].browse(self.pack_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 5.0)
+
+ self.assertEqual(len(self.env['stock.quant']._gather(self.productA, stock_location)), 1.0)
+ self.assertEqual(len(self.env['stock.quant']._gather(self.productA, pack_location)), 1.0)
+
+ (picking_pick + picking_client).action_assign()
+
+ move_pick = picking_pick.move_lines
+ move_cust = picking_client.move_lines
+ self.assertEqual(move_pick.state, 'assigned')
+ self.assertEqual(picking_pick.state, 'assigned')
+ self.assertEqual(move_cust.state, 'waiting')
+ self.assertEqual(picking_client.state, 'waiting', 'The picking should not assign what it does not have')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 5.0)
+
+ move_pick.move_line_ids[0].qty_done = 10.0
+ picking_pick._action_done()
+
+ self.assertEqual(move_pick.state, 'done')
+ self.assertEqual(picking_pick.state, 'done')
+ self.assertEqual(move_cust.state, 'assigned')
+ self.assertEqual(picking_client.state, 'assigned', 'The picking should not assign what it does not have')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 5.0)
+ self.assertEqual(sum(self.env['stock.quant']._gather(self.productA, stock_location).mapped('quantity')), 0.0)
+ self.assertEqual(len(self.env['stock.quant']._gather(self.productA, pack_location)), 1.0)
+
+ def test_mto_moves_return(self):
+ picking_pick, picking_client = self.create_pick_ship()
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
+
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_pick._action_done()
+ self.assertEqual(picking_pick.state, 'done')
+ self.assertEqual(picking_client.state, 'assigned')
+
+ # return a part of what we've done
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0],
+ active_model='stock.picking'))
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 2.0 # Return only 2
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+ return_pick.move_lines[0].move_line_ids[0].qty_done = 2.0
+ return_pick._action_done()
+ # the client picking should not be assigned anymore, as we returned partially what we took
+ self.assertEqual(picking_client.state, 'confirmed')
+
+ def test_mto_moves_extra_qty(self):
+ """ Ensure that a move in MTO will support an extra quantity. The extra
+ move should be created in MTS and should not be merged in the initial
+ move if it's in MTO. It should also avoid to trigger the rules.
+ """
+ picking_pick, picking_client = self.create_pick_ship()
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ self.productA.write({'route_ids': [(4, self.env.ref('stock.route_warehouse0_mto').id)]})
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 15.0
+ picking_pick._action_done()
+ self.assertEqual(picking_pick.state, 'done')
+ self.assertEqual(picking_client.state, 'assigned')
+
+ picking_client.move_lines[0].move_line_ids[0].qty_done = 15.0
+ picking_client.move_lines._action_done()
+ self.assertEqual(len(picking_client.move_lines), 2)
+ move_lines = picking_client.move_lines.sorted()
+ self.assertEqual(move_lines.mapped('procure_method'), ['make_to_order', 'make_to_stock'])
+ self.assertEqual(move_lines.mapped('product_uom_qty'), [10.0, 5.0])
+
+ def test_mto_moves_return_extra(self):
+ picking_pick, picking_client = self.create_pick_ship()
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
+
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_pick._action_done()
+ self.assertEqual(picking_pick.state, 'done')
+ self.assertEqual(picking_client.state, 'assigned')
+
+ # return more than we've done
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0],
+ active_model='stock.picking'))
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 12.0 # Return 2 extra
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+
+ # Verify the extra move has been merged with the original move
+ self.assertAlmostEqual(return_pick.move_lines.product_uom_qty, 12.0)
+ self.assertAlmostEqual(return_pick.move_lines.quantity_done, 0.0)
+ self.assertAlmostEqual(return_pick.move_lines.reserved_availability, 10.0)
+
+ def test_mto_resupply_cancel_ship(self):
+ """ This test simulates a pick pack ship with a resupply route
+ set. Pick and pack are validated, ship is cancelled. This test
+ ensure that new picking are not created from the cancelled
+ ship after the scheduler task. The supply route is only set in
+ order to make the scheduler run without mistakes (no next
+ activity).
+ """
+ picking_pick, picking_pack, picking_ship = self.create_pick_pack_ship()
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ warehouse_1.write({'delivery_steps': 'pick_pack_ship'})
+ warehouse_2 = self.env['stock.warehouse'].create({
+ 'name': 'Small Warehouse',
+ 'code': 'SWH'
+ })
+ warehouse_1.write({
+ 'resupply_wh_ids': [(6, 0, [warehouse_2.id])]
+ })
+ resupply_route = self.env['stock.location.route'].search([('supplier_wh_id', '=', warehouse_2.id), ('supplied_wh_id', '=', warehouse_1.id)])
+ self.assertTrue(resupply_route)
+ self.productA.write({'route_ids': [(4, resupply_route.id), (4, self.env.ref('stock.route_warehouse0_mto').id)]})
+
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
+
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_pick._action_done()
+
+ picking_pack.action_assign()
+ picking_pack.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_pack._action_done()
+
+ picking_ship.action_cancel()
+ picking_ship.move_lines.write({'procure_method': 'make_to_order'})
+
+ self.env['procurement.group'].run_scheduler()
+ next_activity = self.env['mail.activity'].search([('res_model', '=', 'product.template'), ('res_id', '=', self.productA.product_tmpl_id.id)])
+ self.assertEqual(picking_ship.state, 'cancel')
+ self.assertFalse(next_activity, 'If a next activity has been created if means that scheduler failed\
+ and the end of this test do not have sense.')
+ self.assertEqual(len(picking_ship.move_lines.mapped('move_orig_ids')), 0,
+ 'Scheduler should not create picking pack and pick since ship has been manually cancelled.')
+
+ def test_no_backorder_1(self):
+ """ Check the behavior of doing less than asked in the picking pick and chosing not to
+ create a backorder. In this behavior, the second picking should obviously only be able to
+ reserve what was brought, but its initial demand should stay the same and the system will
+ ask the user will have to consider again if he wants to create a backorder or not.
+ """
+ picking_pick, picking_client = self.create_pick_ship()
+ location = self.env['stock.location'].browse(self.stock_location)
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.productA, location, 10.0)
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 5.0
+
+ # create a backorder
+ picking_pick._action_done()
+ picking_pick_backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_pick.id)])
+ self.assertEqual(picking_pick_backorder.state, 'confirmed')
+ self.assertEqual(picking_pick_backorder.move_lines.product_qty, 5.0)
+
+ self.assertEqual(picking_client.state, 'assigned')
+
+ # cancel the backorder
+ picking_pick_backorder.action_cancel()
+ self.assertEqual(picking_client.state, 'assigned')
+
+ def test_edit_done_chained_move(self):
+ """ Let’s say two moves are chained: the first is done and the second is assigned.
+ Editing the move line of the first move should impact the reservation of the second one.
+ """
+ picking_pick, picking_client = self.create_pick_ship()
+ location = self.env['stock.location'].browse(self.stock_location)
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.productA, location, 10.0)
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_pick._action_done()
+
+ self.assertEqual(picking_pick.state, 'done', 'The state of the pick should be done')
+ self.assertEqual(picking_client.state, 'assigned', 'The state of the client should be assigned')
+ self.assertEqual(picking_pick.move_lines.quantity_done, 10.0, 'Wrong quantity_done for pick move')
+ self.assertEqual(picking_client.move_lines.product_qty, 10.0, 'Wrong initial demand for client move')
+ self.assertEqual(picking_client.move_lines.reserved_availability, 10.0, 'Wrong quantity already reserved for client move')
+
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 5.0
+ self.assertEqual(picking_pick.state, 'done', 'The state of the pick should be done')
+ self.assertEqual(picking_client.state, 'assigned', 'The state of the client should be partially available')
+ self.assertEqual(picking_pick.move_lines.quantity_done, 5.0, 'Wrong quantity_done for pick move')
+ self.assertEqual(picking_client.move_lines.product_qty, 10.0, 'Wrong initial demand for client move')
+ self.assertEqual(picking_client.move_lines.reserved_availability, 5.0, 'Wrong quantity already reserved for client move')
+
+ # Check if run action_assign does not crash
+ picking_client.action_assign()
+
+ def test_edit_done_chained_move_with_lot(self):
+ """ Let’s say two moves are chained: the first is done and the second is assigned.
+ Editing the lot on the move line of the first move should impact the reservation of the second one.
+ """
+ self.productA.tracking = 'lot'
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.productA.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.productA.id,
+ 'company_id': self.env.company.id,
+ })
+ picking_pick, picking_client = self.create_pick_ship()
+ location = self.env['stock.location'].browse(self.stock_location)
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.productA, location, 10.0)
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].write({
+ 'qty_done': 10.0,
+ 'lot_id': lot1.id,
+ })
+ picking_pick._action_done()
+
+ self.assertEqual(picking_pick.state, 'done', 'The state of the pick should be done')
+ self.assertEqual(picking_client.state, 'assigned', 'The state of the client should be assigned')
+ self.assertEqual(picking_pick.move_lines.quantity_done, 10.0, 'Wrong quantity_done for pick move')
+ self.assertEqual(picking_client.move_lines.product_qty, 10.0, 'Wrong initial demand for client move')
+ self.assertEqual(picking_client.move_lines.move_line_ids.lot_id, lot1, 'Wrong lot for client move line')
+ self.assertEqual(picking_client.move_lines.reserved_availability, 10.0, 'Wrong quantity already reserved for client move')
+
+ picking_pick.move_lines[0].move_line_ids[0].lot_id = lot2.id
+ self.assertEqual(picking_pick.state, 'done', 'The state of the pick should be done')
+ self.assertEqual(picking_client.state, 'assigned', 'The state of the client should be partially available')
+ self.assertEqual(picking_pick.move_lines.quantity_done, 10.0, 'Wrong quantity_done for pick move')
+ self.assertEqual(picking_client.move_lines.product_qty, 10.0, 'Wrong initial demand for client move')
+ self.assertEqual(picking_client.move_lines.move_line_ids.lot_id, lot2, 'Wrong lot for client move line')
+ self.assertEqual(picking_client.move_lines.reserved_availability, 10.0, 'Wrong quantity already reserved for client move')
+
+ # Check if run action_assign does not crash
+ picking_client.action_assign()
+
+ def test_chained_move_with_uom(self):
+ """ Create pick ship with a different uom than the once used for quant.
+ Check that reserved quantity and flow work correctly.
+ """
+ picking_client = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ dest = self.MoveObj.create({
+ 'name': self.gB.name,
+ 'product_id': self.gB.id,
+ 'product_uom_qty': 5,
+ 'product_uom': self.uom_kg.id,
+ 'picking_id': picking_client.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'state': 'waiting',
+ })
+
+ picking_pick = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.pack_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+
+ self.MoveObj.create({
+ 'name': self.gB.name,
+ 'product_id': self.gB.id,
+ 'product_uom_qty': 5,
+ 'product_uom': self.uom_kg.id,
+ 'picking_id': picking_pick.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.pack_location,
+ 'move_dest_ids': [(4, dest.id)],
+ 'state': 'confirmed',
+ })
+ location = self.env['stock.location'].browse(self.stock_location)
+ pack_location = self.env['stock.location'].browse(self.pack_location)
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.gB, location, 10000.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.gB, pack_location), 0.0)
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 5.0
+ picking_pick._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.gB, location), 5000.0)
+ self.assertEqual(self.env['stock.quant']._gather(self.gB, pack_location).quantity, 5000.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.gB, pack_location), 0.0)
+ self.assertEqual(picking_client.state, 'assigned')
+ self.assertEqual(picking_client.move_lines.reserved_availability, 5.0)
+
+ def test_pick_ship_return(self):
+ """ Create pick and ship. Bring it ot the customer and then return
+ it to stock. This test check the state and the quantity after each move in
+ order to ensure that it is correct.
+ """
+ picking_pick, picking_ship = self.create_pick_ship()
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ pack_location = self.env['stock.location'].browse(self.pack_location)
+ customer_location = self.env['stock.location'].browse(self.customer_location)
+ self.productA.tracking = 'lot'
+ lot = self.env['stock.production.lot'].create({
+ 'product_id': self.productA.id,
+ 'name': '123456789',
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0, lot_id=lot)
+
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_pick._action_done()
+ self.assertEqual(picking_pick.state, 'done')
+ self.assertEqual(picking_ship.state, 'assigned')
+
+ picking_ship.action_assign()
+ picking_ship.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_ship._action_done()
+
+ customer_quantity = self.env['stock.quant']._get_available_quantity(self.productA, customer_location, lot_id=lot)
+ self.assertEqual(customer_quantity, 10, 'It should be one product in customer')
+
+ """ First we create the return picking for pick pinking.
+ Since we do not have created the return between customer and
+ output. This return should not be available and should only have
+ picking pick as origin move.
+ """
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0],
+ active_model='stock.picking'))
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 10.0
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+
+ self.assertEqual(return_pick_picking.state, 'waiting')
+
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking_ship.ids, active_id=picking_ship.ids[0],
+ active_model='stock.picking'))
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 10.0
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_ship_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+
+ self.assertEqual(return_ship_picking.state, 'assigned', 'Return ship picking should automatically be assigned')
+ """ We created the return for ship picking. The origin/destination
+ link between return moves should have been created during return creation.
+ """
+ self.assertTrue(return_ship_picking.move_lines in return_pick_picking.move_lines.mapped('move_orig_ids'),
+ 'The pick return picking\'s moves should have the ship return picking\'s moves as origin')
+
+ self.assertTrue(return_pick_picking.move_lines in return_ship_picking.move_lines.mapped('move_dest_ids'),
+ 'The ship return picking\'s moves should have the pick return picking\'s moves as destination')
+
+ return_ship_picking.move_lines[0].move_line_ids[0].write({
+ 'qty_done': 10.0,
+ 'lot_id': lot.id,
+ })
+ return_ship_picking._action_done()
+ self.assertEqual(return_ship_picking.state, 'done')
+ self.assertEqual(return_pick_picking.state, 'assigned')
+
+ customer_quantity = self.env['stock.quant']._get_available_quantity(self.productA, customer_location, lot_id=lot)
+ self.assertEqual(customer_quantity, 0, 'It should be one product in customer')
+
+ pack_quantity = self.env['stock.quant']._get_available_quantity(self.productA, pack_location, lot_id=lot)
+ self.assertEqual(pack_quantity, 0, 'It should be one product in pack location but is reserved')
+
+ # Should use previous move lot.
+ return_pick_picking.move_lines[0].move_line_ids[0].qty_done = 10.0
+ return_pick_picking._action_done()
+ self.assertEqual(return_pick_picking.state, 'done')
+
+ stock_quantity = self.env['stock.quant']._get_available_quantity(self.productA, stock_location, lot_id=lot)
+ self.assertEqual(stock_quantity, 10, 'The product is not back in stock')
+
+ def test_pick_pack_ship_return(self):
+ """ This test do a pick pack ship delivery to customer and then
+ return it to stock. Once everything is done, this test will check
+ if all the link orgini/destination between moves are correct.
+ """
+ picking_pick, picking_pack, picking_ship = self.create_pick_pack_ship()
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ self.productA.tracking = 'serial'
+ lot = self.env['stock.production.lot'].create({
+ 'product_id': self.productA.id,
+ 'name': '123456789',
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 1.0, lot_id=lot)
+
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 1.0
+ picking_pick._action_done()
+
+ picking_pack.action_assign()
+ picking_pack.move_lines[0].move_line_ids[0].qty_done = 1.0
+ picking_pack._action_done()
+
+ picking_ship.action_assign()
+ picking_ship.move_lines[0].move_line_ids[0].qty_done = 1.0
+ picking_ship._action_done()
+
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking_ship.ids, active_id=picking_ship.ids[0],
+ active_model='stock.picking'))
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 1.0
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_ship_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+
+ return_ship_picking.move_lines[0].move_line_ids[0].write({
+ 'qty_done': 1.0,
+ 'lot_id': lot.id,
+ })
+ return_ship_picking._action_done()
+
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking_pack.ids, active_id=picking_pack.ids[0],
+ active_model='stock.picking'))
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 1.0
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pack_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+
+ return_pack_picking.move_lines[0].move_line_ids[0].qty_done = 1.0
+ return_pack_picking._action_done()
+
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0],
+ active_model='stock.picking'))
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 1.0
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+
+ return_pick_picking.move_lines[0].move_line_ids[0].qty_done = 1.0
+ return_pick_picking._action_done()
+
+ # Now that everything is returned we will check if the return moves are correctly linked between them.
+ # +--------------------------------------------------------------------------------------------------------+
+ # | -- picking_pick(1) --> -- picking_pack(2) --> -- picking_ship(3) -->
+ # | Stock Pack Output Customer
+ # | <--- return pick(6) -- <--- return pack(5) -- <--- return ship(4) --
+ # +--------------------------------------------------------------------------------------------------------+
+ # Recaps of final link (MO = move_orig_ids, MD = move_dest_ids)
+ # picking_pick(1) : MO = (), MD = (2,6)
+ # picking_pack(2) : MO = (1), MD = (3,5)
+ # picking ship(3) : MO = (2), MD = (4)
+ # return ship(4) : MO = (3), MD = (5)
+ # return pack(5) : MO = (2, 4), MD = (6)
+ # return pick(6) : MO = (1, 5), MD = ()
+
+ self.assertEqual(len(picking_pick.move_lines.move_orig_ids), 0, 'Picking pick should not have origin moves')
+ self.assertEqual(set(picking_pick.move_lines.move_dest_ids.ids), set((picking_pack.move_lines | return_pick_picking.move_lines).ids))
+
+ self.assertEqual(set(picking_pack.move_lines.move_orig_ids.ids), set(picking_pick.move_lines.ids))
+ self.assertEqual(set(picking_pack.move_lines.move_dest_ids.ids), set((picking_ship.move_lines | return_pack_picking.move_lines).ids))
+
+ self.assertEqual(set(picking_ship.move_lines.move_orig_ids.ids), set(picking_pack.move_lines.ids))
+ self.assertEqual(set(picking_ship.move_lines.move_dest_ids.ids), set(return_ship_picking.move_lines.ids))
+
+ self.assertEqual(set(return_ship_picking.move_lines.move_orig_ids.ids), set(picking_ship.move_lines.ids))
+ self.assertEqual(set(return_ship_picking.move_lines.move_dest_ids.ids), set(return_pack_picking.move_lines.ids))
+
+ self.assertEqual(set(return_pack_picking.move_lines.move_orig_ids.ids), set((picking_pack.move_lines | return_ship_picking.move_lines).ids))
+ self.assertEqual(set(return_pack_picking.move_lines.move_dest_ids.ids), set(return_pick_picking.move_lines.ids))
+
+ self.assertEqual(set(return_pick_picking.move_lines.move_orig_ids.ids), set((picking_pick.move_lines | return_pack_picking.move_lines).ids))
+ self.assertEqual(len(return_pick_picking.move_lines.move_dest_ids), 0)
+
+ def test_merge_move_mto_mts(self):
+ """ Create 2 moves of the same product in the same picking with
+ one in 'MTO' and the other one in 'MTS'. The moves shouldn't be merged
+ """
+ picking_pick, picking_client = self.create_pick_ship()
+
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 3,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_client.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'origin': 'MPS',
+ 'procure_method': 'make_to_stock',
+ })
+ picking_client.action_confirm()
+ self.assertEqual(len(picking_client.move_lines), 2, 'Moves should not be merged')
+
+ def test_mto_cancel_move_line(self):
+ """ Create a pick ship situation. Then process the pick picking
+ with a backorder. Then try to unlink the move line created on
+ the ship and check if the picking and move state are updated.
+ Then validate the backorder and unlink the ship move lines in
+ order to check again if the picking and state are updated.
+ """
+ picking_pick, picking_client = self.create_pick_ship()
+ location = self.env['stock.location'].browse(self.stock_location)
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.productA, location, 10.0)
+ picking_pick.move_lines.quantity_done = 5.0
+ backorder_wizard_values = picking_pick.button_validate()
+ backorder_wizard = self.env[(backorder_wizard_values.get('res_model'))].browse(backorder_wizard_values.get('res_id')).with_context(backorder_wizard_values['context'])
+ backorder_wizard.process()
+
+ self.assertTrue(picking_client.move_line_ids, 'A move line should be created.')
+ self.assertEqual(picking_client.move_line_ids.product_uom_qty, 5, 'The move line should have 5 unit reserved.')
+
+ # Directly delete the move lines on the picking. (Use show detail operation on picking type)
+ # Should do the same behavior than unreserve
+ picking_client.move_line_ids.unlink()
+
+ self.assertEqual(picking_client.move_lines.state, 'waiting', 'The move state should be waiting since nothing is reserved and another origin move still in progess.')
+ self.assertEqual(picking_client.state, 'waiting', 'The picking state should not be ready anymore.')
+
+ picking_client.action_assign()
+
+ back_order = self.env['stock.picking'].search([('backorder_id', '=', picking_pick.id)])
+ back_order.move_lines.quantity_done = 5
+ back_order.button_validate()
+
+ self.assertEqual(picking_client.move_lines.reserved_availability, 10, 'The total quantity should be reserved since everything is available.')
+ picking_client.move_line_ids.unlink()
+
+ self.assertEqual(picking_client.move_lines.state, 'confirmed', 'The move should be confirmed since all the origin moves are processed.')
+ self.assertEqual(picking_client.state, 'confirmed', 'The picking should be confirmed since all the moves are confirmed.')
+
+ def test_unreserve(self):
+ picking_pick, picking_client = self.create_pick_ship()
+
+ self.assertEqual(picking_pick.state, 'confirmed')
+ picking_pick.do_unreserve()
+ self.assertEqual(picking_pick.state, 'confirmed')
+ location = self.env['stock.location'].browse(self.stock_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, location, 10.0)
+ picking_pick.action_assign()
+ self.assertEqual(picking_pick.state, 'assigned')
+ picking_pick.do_unreserve()
+ self.assertEqual(picking_pick.state, 'confirmed')
+
+ self.assertEqual(picking_client.state, 'waiting')
+ picking_client.do_unreserve()
+ self.assertEqual(picking_client.state, 'waiting')
+
+ def test_return_location(self):
+ """ In a pick ship scenario, send two items to the customer, then return one in the ship
+ location and one in a return location that is located in another warehouse.
+ """
+ pick_location = self.env['stock.location'].browse(self.stock_location)
+ pick_location.return_location = True
+
+ return_warehouse = self.env['stock.warehouse'].create({'name': 'return warehouse', 'code': 'rw'})
+ return_location = self.env['stock.location'].create({
+ 'name': 'return internal',
+ 'usage': 'internal',
+ 'location_id': return_warehouse.view_location_id.id
+ })
+
+ self.env['stock.quant']._update_available_quantity(self.productA, pick_location, 10.0)
+ picking_pick, picking_client = self.create_pick_ship()
+
+ # send the items to the customer
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_pick._action_done()
+ picking_client.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_client._action_done()
+
+ # return half in the pick location
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking_client.ids, active_id=picking_client.ids[0],
+ active_model='stock.picking'))
+ return1 = stock_return_picking_form.save()
+ return1.product_return_moves.quantity = 5.0
+ return1.location_id = pick_location.id
+ return_to_pick_picking_action = return1.create_returns()
+
+ return_to_pick_picking = self.env['stock.picking'].browse(return_to_pick_picking_action['res_id'])
+ return_to_pick_picking.move_lines[0].move_line_ids[0].qty_done = 5.0
+ return_to_pick_picking._action_done()
+
+ # return the remainig products in the return warehouse
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking_client.ids, active_id=picking_client.ids[0],
+ active_model='stock.picking'))
+ return2 = stock_return_picking_form.save()
+ return2.product_return_moves.quantity = 5.0
+ return2.location_id = return_location.id
+ return_to_return_picking_action = return2.create_returns()
+
+ return_to_return_picking = self.env['stock.picking'].browse(return_to_return_picking_action['res_id'])
+ return_to_return_picking.move_lines[0].move_line_ids[0].qty_done = 5.0
+ return_to_return_picking._action_done()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pick_location), 5.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, return_location), 5.0)
+ self.assertEqual(len(self.env['stock.quant'].search([('product_id', '=', self.productA.id), ('quantity', '!=', 0)])), 2)
+
+
+class TestSinglePicking(TestStockCommon):
+ def test_backorder_1(self):
+ """ Check the good behavior of creating a backorder for an available stock move.
+ """
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+
+ # make some stock
+ pack_location = self.env['stock.location'].browse(self.pack_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2)
+
+ # assign
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
+
+ # valid with backorder creation
+ delivery_order.move_lines[0].move_line_ids[0].qty_done = 1
+ delivery_order._action_done()
+ self.assertNotEqual(delivery_order.date_done, False)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 1.0)
+
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
+ self.assertEqual(backorder.state, 'confirmed')
+ backorder.action_assign()
+ self.assertEqual(backorder.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
+
+ def test_backorder_2(self):
+ """ Check the good behavior of creating a backorder for a partially available stock move.
+ """
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+
+ # make some stock
+ pack_location = self.env['stock.location'].browse(self.pack_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 1)
+
+ # assign to partially available
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
+
+ # valid with backorder creation
+ delivery_order.move_lines[0].move_line_ids[0].qty_done = 1
+ delivery_order._action_done()
+ self.assertNotEqual(delivery_order.date_done, False)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
+
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
+ self.assertEqual(backorder.state, 'confirmed')
+
+ def test_backorder_3(self):
+ """ Check the good behavior of creating a backorder for an available move on a picking with
+ two available moves.
+ """
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productB.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+
+ # make some stock
+ pack_location = self.env['stock.location'].browse(self.pack_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2)
+ self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2)
+
+ # assign to partially available
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ self.assertEqual(delivery_order.state, 'assigned')
+
+ delivery_order.move_lines[0].move_line_ids[0].qty_done = 2
+ delivery_order._action_done()
+
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
+ self.assertEqual(backorder.state, 'confirmed')
+
+ def test_backorder_4(self):
+ """ Check the good behavior if no backorder are created
+ for a picking with a missing product.
+ """
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productB.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+
+ # Update available quantities for each products
+ pack_location = self.env['stock.location'].browse(self.pack_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2)
+ self.env['stock.quant']._update_available_quantity(self.productB, pack_location, 2)
+
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ self.assertEqual(delivery_order.state, 'assigned')
+
+ # Process only one product without creating a backorder
+ delivery_order.move_lines[0].move_line_ids[0].qty_done = 2
+ res_dict = delivery_order.button_validate()
+ backorder_wizard = Form(self.env['stock.backorder.confirmation'].with_context(res_dict['context'])).save()
+ backorder_wizard.process_cancel_backorder()
+
+ # No backorder should be created and the move corresponding to the missing product should be cancelled
+ backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
+ self.assertFalse(backorder)
+ self.assertEqual(delivery_order.state, 'done')
+ self.assertEqual(delivery_order.move_lines[1].state, 'cancel')
+
+ def test_extra_move_1(self):
+ """ Check the good behavior of creating an extra move in a delivery order. This usecase
+ simulates the delivery of 2 item while the initial stock move had to move 1 and there's
+ only 1 in stock.
+ """
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ move1 = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 1,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+
+ # make some stock
+ pack_location = self.env['stock.location'].browse(self.pack_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 1)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 1.0)
+
+ # assign to available
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
+
+ # valid with backorder creation
+ delivery_order.move_lines[0].move_line_ids[0].qty_done = 2
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
+ delivery_order._action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location, allow_negative=True), -1.0)
+
+ self.assertEqual(move1.product_qty, 2.0)
+ self.assertEqual(move1.quantity_done, 2.0)
+ self.assertEqual(move1.reserved_availability, 0.0)
+ self.assertEqual(move1.move_line_ids.product_qty, 0.0) # change reservation to 0 for done move
+ self.assertEqual(sum(move1.move_line_ids.mapped('qty_done')), 2.0)
+ self.assertEqual(move1.state, 'done')
+
+ def test_extra_move_2(self):
+ """ Check the good behavior of creating an extra move in a delivery order. This usecase
+ simulates the delivery of 3 item while the initial stock move had to move 1 and there's
+ only 1 in stock.
+ """
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ move1 = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 1,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+
+ # make some stock
+ pack_location = self.env['stock.location'].browse(self.pack_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 1)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 1.0)
+
+ # assign to available
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
+
+ # valid with backorder creation
+ delivery_order.move_lines[0].move_line_ids[0].qty_done = 3
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
+ delivery_order._action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location, allow_negative=True), -2.0)
+
+ self.assertEqual(move1.product_qty, 3.0)
+ self.assertEqual(move1.quantity_done, 3.0)
+ self.assertEqual(move1.reserved_availability, 0.0)
+ self.assertEqual(move1.move_line_ids.product_qty, 0.0) # change reservation to 0 for done move
+ self.assertEqual(sum(move1.move_line_ids.mapped('qty_done')), 3.0)
+ self.assertEqual(move1.state, 'done')
+
+ def test_extra_move_3(self):
+ """ Check the good behavior of creating an extra move in a receipt. This usecase simulates
+ the receipt of 2 item while the initial stock move had to move 1.
+ """
+ receipt = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'picking_type_id': self.picking_type_in,
+ })
+ move1 = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 1,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': receipt.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ })
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+
+ # assign to available
+ receipt.action_confirm()
+ receipt.action_assign()
+ self.assertEqual(receipt.state, 'assigned')
+
+ # valid with backorder creation
+ receipt.move_lines[0].move_line_ids[0].qty_done = 2
+ receipt._action_done()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 2.0)
+
+ self.assertEqual(move1.product_qty, 2.0)
+ self.assertEqual(move1.quantity_done, 2.0)
+ self.assertEqual(move1.reserved_availability, 0.0)
+ self.assertEqual(move1.move_line_ids.product_qty, 0.0) # change reservation to 0 for done move
+ self.assertEqual(sum(move1.move_line_ids.mapped('qty_done')), 2.0)
+ self.assertEqual(move1.state, 'done')
+
+ def test_extra_move_4(self):
+ """ Create a picking with similar moves (created after
+ confirmation). Action done should propagate all the extra
+ quantity and only merge extra moves in their original moves.
+ """
+ delivery = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 5,
+ 'quantity_done': 10,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ })
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 5)
+ delivery.action_confirm()
+ delivery.action_assign()
+
+ delivery.write({
+ 'move_lines': [(0, 0, {
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 0,
+ 'quantity_done': 10,
+ 'state': 'assigned',
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ })]
+ })
+ delivery._action_done()
+ self.assertEqual(len(delivery.move_lines), 2, 'Move should not be merged together')
+ for move in delivery.move_lines:
+ self.assertEqual(move.quantity_done, move.product_uom_qty, 'Initial demand should be equals to quantity done')
+
+ def test_extra_move_5(self):
+ """ Create a picking a move that is problematic with
+ rounding (5.95 - 5.5 = 0.4500000000000002). Ensure that
+ initial demand is corrct afer action_done and backoder
+ are not created.
+ """
+ delivery = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ product = self.kgB
+ self.MoveObj.create({
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': 5.5,
+ 'quantity_done': 5.95,
+ 'product_uom': product.uom_id.id,
+ 'picking_id': delivery.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ })
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ self.env['stock.quant']._update_available_quantity(product, stock_location, 5.5)
+ delivery.action_confirm()
+ delivery.action_assign()
+ delivery._action_done()
+ self.assertEqual(delivery.move_lines.product_uom_qty, 5.95, 'Move initial demand should be 5.95')
+
+ back_order = self.env['stock.picking'].search([('backorder_id', '=', delivery.id)])
+ self.assertFalse(back_order, 'There should be no back order')
+
+ def test_recheck_availability_1(self):
+ """ Check the good behavior of check availability. I create a DO for 2 unit with
+ only one in stock. After the first check availability, I should have 1 reserved
+ product with one move line. After adding a second unit in stock and recheck availability.
+ The DO should have 2 reserved unit, be in available state and have only one move line.
+ """
+ self.env['stock.quant']._update_available_quantity(self.productA, self.env['stock.location'].browse(self.stock_location), 1.0)
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ move1 = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ })
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ # Check State
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(move1.state, 'partially_available')
+
+ # Check reserved quantity
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(move1.move_line_ids.product_qty, 1)
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'remove product1',
+ 'location_ids': [(4, self.stock_location)],
+ 'product_ids': [(4, self.productA.id)],
+ })
+ inventory.action_start()
+ inventory.line_ids.product_qty = 2
+ inventory.action_validate()
+ delivery_order.action_assign()
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(move1.state, 'assigned')
+
+ # Check reserved quantity
+ self.assertEqual(move1.reserved_availability, 2.0)
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(move1.move_line_ids.product_qty, 2)
+
+ def test_recheck_availability_2(self):
+ """ Same check than test_recheck_availability_1 but with lot this time.
+ If the new product has the same lot that already reserved one, the move lines
+ reserved quantity should be updated.
+ Otherwise a new move lines with the new lot should be added.
+ """
+ self.productA.tracking = 'lot'
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.productA.id,
+ 'company_id': self.env.company.id,
+ })
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 1.0, lot_id=lot1)
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ move1 = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ })
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ # Check State
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(move1.state, 'partially_available')
+
+ # Check reserved quantity
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(move1.move_line_ids.product_qty, 1)
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'remove product1',
+ 'location_ids': [(4, self.stock_location)],
+ 'product_ids': [(4, self.productA.id)],
+ })
+ inventory.action_start()
+ inventory.line_ids.prod_lot_id = lot1
+ inventory.line_ids.product_qty = 2
+ inventory.action_validate()
+ delivery_order.action_assign()
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(move1.state, 'assigned')
+
+ # Check reserved quantity
+ self.assertEqual(move1.reserved_availability, 2.0)
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(move1.move_line_ids.lot_id.id, lot1.id)
+ self.assertEqual(move1.move_line_ids.product_qty, 2)
+
+ def test_recheck_availability_3(self):
+ """ Same check than test_recheck_availability_2 but with different lots.
+ """
+ self.productA.tracking = 'lot'
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.productA.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.productA.id,
+ 'company_id': self.env.company.id,
+ })
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 1.0, lot_id=lot1)
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ move1 = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ })
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ # Check State
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(move1.state, 'partially_available')
+
+ # Check reserved quantity
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(move1.move_line_ids.product_qty, 1)
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'remove product1',
+ 'location_ids': [(4, self.stock_location)],
+ 'product_ids': [(4, self.productA.id)],
+ })
+ inventory.action_start()
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'location_id': inventory.location_ids[0].id,
+ 'prod_lot_id': lot2.id,
+ 'product_id': self.productA.id,
+ 'product_qty': 1,
+ })
+ inventory.action_validate()
+ delivery_order.action_assign()
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(move1.state, 'assigned')
+
+ # Check reserved quantity
+ self.assertEqual(move1.reserved_availability, 2.0)
+ self.assertEqual(len(move1.move_line_ids), 2)
+ move_lines = move1.move_line_ids.sorted()
+ self.assertEqual(move_lines[0].lot_id.id, lot1.id)
+ self.assertEqual(move_lines[1].lot_id.id, lot2.id)
+
+ def test_recheck_availability_4(self):
+ """ Same check than test_recheck_availability_2 but with serial number this time.
+ Serial number reservation should always create a new move line.
+ """
+ self.productA.tracking = 'serial'
+ serial1 = self.env['stock.production.lot'].create({
+ 'name': 'serial1',
+ 'product_id': self.productA.id,
+ 'company_id': self.env.company.id,
+ })
+ serial2 = self.env['stock.production.lot'].create({
+ 'name': 'serial2',
+ 'product_id': self.productA.id,
+ 'company_id': self.env.company.id,
+ })
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 1.0, lot_id=serial1)
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ move1 = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ })
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ # Check State
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(move1.state, 'partially_available')
+
+ # Check reserved quantity
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.assertEqual(move1.move_line_ids.product_qty, 1)
+
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'remove product1',
+ 'location_ids': [(4, self.stock_location)],
+ 'product_ids': [(4, self.productA.id)],
+ })
+ inventory.action_start()
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'location_id': inventory.location_ids[0].id,
+ 'prod_lot_id': serial2.id,
+ 'product_id': self.productA.id,
+ 'product_qty': 1,
+ })
+ inventory.action_validate()
+ delivery_order.action_assign()
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(move1.state, 'assigned')
+
+ # Check reserved quantity
+ self.assertEqual(move1.reserved_availability, 2.0)
+ self.assertEqual(len(move1.move_line_ids), 2)
+ move_lines = move1.move_line_ids.sorted()
+ self.assertEqual(move_lines[0].lot_id.id, serial1.id)
+ self.assertEqual(move_lines[1].lot_id.id, serial2.id)
+
+ def test_use_create_lot_use_existing_lot_1(self):
+ """ Check the behavior of a picking when `use_create_lot` and `use_existing_lot` are
+ set to False and there's a move for a tracked product.
+ """
+ self.env['stock.picking.type']\
+ .browse(self.picking_type_out)\
+ .write({
+ 'use_create_lots': False,
+ 'use_existing_lots': False,
+ })
+ self.productA.tracking = 'lot'
+
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+
+ delivery_order.action_confirm()
+ delivery_order.move_lines.quantity_done = 2
+ # do not set a lot_id or lot_name, it should work
+ delivery_order._action_done()
+
+ def test_use_create_lot_use_existing_lot_2(self):
+ """ Check the behavior of a picking when `use_create_lot` and `use_existing_lot` are
+ set to True and there's a move for a tracked product.
+ """
+ self.env['stock.picking.type']\
+ .browse(self.picking_type_out)\
+ .write({
+ 'use_create_lots': True,
+ 'use_existing_lots': True,
+ })
+ self.productA.tracking = 'lot'
+
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+
+ delivery_order.action_confirm()
+ delivery_order.move_lines.quantity_done = 2
+ move_line = delivery_order.move_lines.move_line_ids
+
+ # not lot_name set, should raise
+ with self.assertRaises(UserError):
+ delivery_order._action_done()
+
+ # enter a new lot name, should work
+ move_line.lot_name = 'newlot'
+ delivery_order._action_done()
+
+ def test_use_create_lot_use_existing_lot_3(self):
+ """ Check the behavior of a picking when `use_create_lot` is set to True and
+ `use_existing_lot` is set to False and there's a move for a tracked product.
+ """
+ self.env['stock.picking.type']\
+ .browse(self.picking_type_out)\
+ .write({
+ 'use_create_lots': True,
+ 'use_existing_lots': False,
+ })
+ self.productA.tracking = 'lot'
+
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+
+ delivery_order.action_confirm()
+ delivery_order.move_lines.quantity_done = 2
+ move_line = delivery_order.move_lines.move_line_ids
+
+ # not lot_name set, should raise
+ with self.assertRaises(UserError):
+ delivery_order._action_done()
+
+ # enter a new lot name, should work
+ move_line.lot_name = 'newlot'
+ delivery_order._action_done()
+
+ def test_use_create_lot_use_existing_lot_4(self):
+ """ Check the behavior of a picking when `use_create_lot` is set to False and
+ `use_existing_lot` is set to True and there's a move for a tracked product.
+ """
+ self.env['stock.picking.type']\
+ .browse(self.picking_type_out)\
+ .write({
+ 'use_create_lots': False,
+ 'use_existing_lots': True,
+ })
+ self.productA.tracking = 'lot'
+
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })
+
+ delivery_order.action_confirm()
+ delivery_order.move_lines.quantity_done = 2
+ move_line = delivery_order.move_lines.move_line_ids
+
+ # not lot_name set, should raise
+ with self.assertRaises(UserError):
+ delivery_order._action_done()
+
+ # creating a lot from the view should raise
+ with self.assertRaises(UserError):
+ self.env['stock.production.lot']\
+ .with_context(active_picking_id=delivery_order.id)\
+ .create({
+ 'name': 'lot1',
+ 'product_id': self.productA.id,
+ 'company_id': self.env.company.id,
+ })
+
+ # enter an existing lot_id, should work
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.productA.id,
+ 'company_id': self.env.company.id,
+ })
+ move_line.lot_id = lot1
+ delivery_order._action_done()
+
+ def test_merge_moves_1(self):
+ receipt = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'picking_type_id': self.picking_type_in,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 3,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': receipt.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 5,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': receipt.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 1,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': receipt.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ })
+ self.MoveObj.create({
+ 'name': self.productB.name,
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 5,
+ 'product_uom': self.productB.uom_id.id,
+ 'picking_id': receipt.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ })
+ receipt.action_confirm()
+ self.assertEqual(len(receipt.move_lines), 2, 'Moves were not merged')
+ self.assertEqual(receipt.move_lines.filtered(lambda m: m.product_id == self.productA).product_uom_qty, 9, 'Merged quantity is not correct')
+ self.assertEqual(receipt.move_lines.filtered(lambda m: m.product_id == self.productB).product_uom_qty, 5, 'Merge should not impact product B reserved quantity')
+
+ def test_merge_moves_2(self):
+ receipt = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'picking_type_id': self.picking_type_in,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 3,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': receipt.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'origin': 'MPS'
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 5,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': receipt.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'origin': 'PO0001'
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 3,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': receipt.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'origin': 'MPS'
+ })
+ receipt.action_confirm()
+ self.assertEqual(len(receipt.move_lines), 1, 'Moves were not merged')
+ self.assertEqual(receipt.move_lines.origin.count('MPS'), 1, 'Origin not merged together or duplicated')
+ self.assertEqual(receipt.move_lines.origin.count('PO0001'), 1, 'Origin not merged together or duplicated')
+
+ def test_merge_moves_3(self):
+ """ Create 2 moves without initial_demand and already a
+ quantity done. Check that we still have only 2 moves after
+ validation.
+ """
+ receipt = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'picking_type_id': self.picking_type_in,
+ })
+ move_1 = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 0,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': receipt.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'origin': 'MPS'
+ })
+ move_2 = self.MoveObj.create({
+ 'name': self.productB.name,
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 0,
+ 'product_uom': self.productB.uom_id.id,
+ 'picking_id': receipt.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'origin': 'PO0001'
+ })
+ move_1.quantity_done = 5
+ move_2.quantity_done = 5
+ receipt.button_validate()
+ self.assertEqual(len(receipt.move_lines), 2, 'Moves were not merged')
+
+ def test_merge_chained_moves(self):
+ """ Imagine multiple step delivery. Two different receipt picking for the same product should only generate
+ 1 picking from input to QC and another from QC to stock. The link at the end should follow this scheme.
+ Move receipt 1 \
+ Move Input-> QC - Move QC -> Stock
+ Move receipt 2 /
+ """
+ warehouse = self.env['stock.warehouse'].create({
+ 'name': 'TEST WAREHOUSE',
+ 'code': 'TEST1',
+ 'reception_steps': 'three_steps',
+ })
+ receipt1 = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location,
+ 'location_dest_id': warehouse.wh_input_stock_loc_id.id,
+ 'picking_type_id': warehouse.in_type_id.id,
+ })
+ move_receipt_1 = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 5,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': receipt1.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': warehouse.wh_input_stock_loc_id.id,
+ })
+ receipt2 = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location,
+ 'location_dest_id': warehouse.wh_input_stock_loc_id.id,
+ 'picking_type_id': warehouse.in_type_id.id,
+ })
+ move_receipt_2 = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 3,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': receipt2.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': warehouse.wh_input_stock_loc_id.id,
+ })
+ receipt1.action_confirm()
+ receipt2.action_confirm()
+
+ # Check following move has been created and grouped in one picking.
+ self.assertTrue(move_receipt_1.move_dest_ids, 'No move created from push rules')
+ self.assertTrue(move_receipt_2.move_dest_ids, 'No move created from push rules')
+ self.assertEqual(move_receipt_1.move_dest_ids.picking_id, move_receipt_2.move_dest_ids.picking_id, 'Destination moves should be in the same picking')
+
+ # Check link for input move are correct.
+ input_move = move_receipt_2.move_dest_ids
+ self.assertEqual(len(input_move.move_dest_ids), 1)
+ self.assertEqual(set(input_move.move_orig_ids.ids), set((move_receipt_2 | move_receipt_1).ids),
+ 'Move from input to QC should be merged and have the two receipt moves as origin.')
+ self.assertEqual(move_receipt_1.move_dest_ids, input_move)
+ self.assertEqual(move_receipt_2.move_dest_ids, input_move)
+
+ # Check link for quality check move are also correct.
+ qc_move = input_move.move_dest_ids
+ self.assertEqual(len(qc_move), 1)
+ self.assertTrue(qc_move.move_orig_ids == input_move, 'Move between QC and stock should only have the input move as origin')
+
+ def test_empty_moves_validation_1(self):
+ """ Use button validate on a picking that contains only moves
+ without initial demand and without quantity done should be
+ impossible and raise a usererror.
+ """
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 0,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ })
+ self.MoveObj.create({
+ 'name': self.productB.name,
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 0,
+ 'product_uom': self.productB.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ })
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ with self.assertRaises(UserError):
+ delivery_order.button_validate()
+
+ def test_empty_moves_validation_2(self):
+ """ Use button validate on a picking that contains only moves
+ without initial demand but at least one with a quantity done
+ should process the move with quantity done and cancel the
+ other.
+ """
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ move_a = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 0,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ })
+ move_b = self.MoveObj.create({
+ 'name': self.productB.name,
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 0,
+ 'product_uom': self.productB.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ })
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ move_a.quantity_done = 1
+ delivery_order.button_validate()
+
+ self.assertEqual(move_a.state, 'done')
+ self.assertEqual(move_b.state, 'cancel')
+ back_order = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
+ self.assertFalse(back_order, 'There should be no back order')
+
+ def test_unlink_move_1(self):
+ picking = Form(self.env['stock.picking'])
+ ptout = self.env['stock.picking.type'].browse(self.picking_type_out)
+ picking.picking_type_id = ptout
+ with picking.move_ids_without_package.new() as move:
+ move.product_id = self.productA
+ move.product_uom_qty = 10
+ picking = picking.save()
+ self.assertEqual(picking.immediate_transfer, False)
+ self.assertEqual(picking.state, 'draft')
+
+ picking = Form(picking)
+ picking.move_ids_without_package.remove(0)
+ picking = picking.save()
+ self.assertEqual(len(picking.move_ids_without_package), 0)
+
+ def test_additional_move_1(self):
+ """ On a planned trasfer, add a stock move when the picking is already ready. Check that
+ the check availability button appears and work.
+ """
+ # Make some stock for productA and productB.
+ receipt = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'picking_type_id': self.picking_type_in,
+ })
+ move_1 = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': receipt.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ })
+ move_2 = self.MoveObj.create({
+ 'name': self.productB.name,
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productB.uom_id.id,
+ 'picking_id': receipt.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ })
+ receipt.action_confirm()
+ move_1.quantity_done = 10
+ move_2.quantity_done = 10
+ receipt.button_validate()
+ self.assertEqual(self.productA.qty_available, 10)
+ self.assertEqual(self.productB.qty_available, 10)
+
+ # Create a delivery for 1 productA, reserve, check the picking is ready
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ 'move_type': 'one',
+ })
+ move_3 = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': delivery_order.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ })
+ delivery_order.action_confirm()
+ delivery_order.action_assign()
+ self.assertEqual(delivery_order.state, 'assigned')
+
+ # Add a unit of productB, the check_availability button should appear.
+ delivery_order = Form(delivery_order)
+ with delivery_order.move_ids_without_package.new() as move:
+ move.product_id = self.productB
+ move.product_uom_qty = 10
+ delivery_order = delivery_order.save()
+
+ # The autocoform ran, the picking shoud be confirmed and reservable.
+ self.assertEqual(delivery_order.state, 'confirmed')
+ self.assertEqual(delivery_order.show_mark_as_todo, False)
+ self.assertEqual(delivery_order.show_check_availability, True)
+
+ delivery_order.action_assign()
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(delivery_order.show_check_availability, False)
+ self.assertEqual(delivery_order.show_mark_as_todo, False)
+
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ self.assertEqual(self.env['stock.quant']._gather(self.productA, stock_location).reserved_quantity, 10.0)
+ self.assertEqual(self.env['stock.quant']._gather(self.productB, stock_location).reserved_quantity, 10.0)
+
+ def test_additional_move_2(self):
+ """ On an immediate trasfer, add a stock move when the picking is already ready. Check that
+ the check availability button doest not appear.
+ """
+ # Create a delivery for 1 productA, check the picking is ready
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ 'immediate_transfer': True,
+ 'move_ids_without_package': [(0, 0, {
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom': self.productA.uom_id.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'quantity_done': 5,
+ })],
+ })
+ self.assertEqual(delivery_order.state, 'assigned')
+
+ # Add a unit of productB, the check_availability button should not appear.
+ delivery_order = Form(delivery_order)
+ with delivery_order.move_ids_without_package.new() as move:
+ move.product_id = self.productB
+ delivery_order = delivery_order.save()
+
+ self.assertEqual(delivery_order.state, 'assigned')
+ self.assertEqual(delivery_order.show_check_availability, False)
+ self.assertEqual(delivery_order.show_mark_as_todo, False)
+
+ def test_owner_1(self):
+ """Make a receipt, set an owner and validate"""
+ owner1 = self.env['res.partner'].create({'name': 'owner'})
+ receipt = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'picking_type_id': self.picking_type_in,
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 1,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': receipt.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ })
+ receipt.action_confirm()
+ receipt = Form(receipt)
+ receipt.owner_id = owner1
+ receipt = receipt.save()
+ wiz = receipt.button_validate()
+ wiz = Form(self.env['stock.immediate.transfer'].with_context(wiz['context'])).save()
+ wiz.process()
+
+ supplier_location = self.env['stock.location'].browse(self.supplier_location)
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ supplier_quant = self.env['stock.quant']._gather(self.productA, supplier_location)
+ stock_quant = self.env['stock.quant']._gather(self.productA, stock_location)
+
+ self.assertEqual(supplier_quant.owner_id, owner1)
+ self.assertEqual(supplier_quant.quantity, -1)
+ self.assertEqual(stock_quant.owner_id, owner1)
+ self.assertEqual(stock_quant.quantity, 1)
+
+ def test_putaway_for_picking_sml(self):
+ """ Checks picking's move lines will take in account the putaway rules
+ to define the `location_dest_id`.
+ """
+ partner = self.env['res.partner'].create({'name': 'Partner'})
+ supplier_location = self.env['stock.location'].browse(self.supplier_location)
+ stock_location = self.env['stock.location'].create({
+ 'name': 'test-stock',
+ 'usage': 'internal',
+ })
+ shelf_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': stock_location.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)]})
+ putaway_product = self.env['stock.putaway.rule'].create({
+ 'product_id': self.productA.id,
+ 'location_in_id': stock_location.id,
+ 'location_out_id': shelf_location.id,
+ })
+ # Changes config of receipt type to allow to edit move lines directly.
+ picking_type = self.env['stock.picking.type'].browse(self.picking_type_in)
+ picking_type.show_operations = True
+
+ receipt_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ receipt_form.partner_id = partner
+ receipt_form.picking_type_id = picking_type
+ receipt_form.location_id = supplier_location
+ receipt_form.location_dest_id = stock_location
+ receipt = receipt_form.save()
+ with receipt_form.move_line_nosuggest_ids.new() as move_line:
+ move_line.product_id = self.productA
+
+ receipt = receipt_form.save()
+ # Checks receipt has still its destination location and checks its move
+ # line took the one from the putaway rule.
+ self.assertEqual(receipt.location_dest_id.id, stock_location.id)
+ self.assertEqual(receipt.move_line_ids.location_dest_id.id, shelf_location.id)
+
+ def test_cancel_plan_transfer(self):
+ """ Test canceling plan transfer """
+ # Create picking with stock move.
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ 'move_lines': [(0, 0, {
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productA.uom_id.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })]
+ })
+ # Confirm the outgoing picking, state should be changed.
+ picking.action_confirm()
+ self.assertEqual(picking.state, 'confirmed', "Picking should be in a confirmed state.")
+
+ # Picking in a confirmed state and try to cancel it.
+ picking.action_cancel()
+ self.assertEqual(picking.state, 'cancel', "Picking should be in a cancel state.")
+
+ def test_immediate_transfer(self):
+ """ Test picking should be in ready state if immediate transfer and SML is created via view +
+ Test picking cancelation with immediate transfer and done quantity"""
+ # create picking with stock move line
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ 'immediate_transfer': True,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.productA.id,
+ 'qty_done': 10,
+ 'product_uom_id': self.productA.uom_id.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ })]
+ })
+
+ self.assertEqual(picking.state, 'assigned', "Picking should not be in a draft state.")
+ self.assertEqual(len(picking.move_lines), 1, "Picking should have stock move.")
+ picking.action_cancel()
+ self.assertEqual(picking.move_lines.state, 'cancel', "Stock move should be in a cancel state.")
+ self.assertEqual(picking.state, 'cancel', "Picking should be in a cancel state.")
+
+
+class TestStockUOM(TestStockCommon):
+ def setUp(self):
+ super(TestStockUOM, self).setUp()
+ dp = self.env.ref('product.decimal_product_uom')
+ dp.digits = 7
+
+ def test_pickings_transfer_with_different_uom_and_back_orders(self):
+ """ Picking transfer with diffrent unit of meassure. """
+ # weight category
+ categ_test = self.env['uom.category'].create({'name': 'Bigger than tons'})
+
+ T_LBS = self.env['uom.uom'].create({
+ 'name': 'T-LBS',
+ 'category_id': categ_test.id,
+ 'uom_type': 'reference',
+ 'rounding': 0.01
+ })
+ T_GT = self.env['uom.uom'].create({
+ 'name': 'T-GT',
+ 'category_id': categ_test.id,
+ 'uom_type': 'bigger',
+ 'rounding': 0.0000001,
+ 'factor_inv': 2240.00,
+ })
+ T_TEST = self.env['product.product'].create({
+ 'name': 'T_TEST',
+ 'type': 'product',
+ 'uom_id': T_LBS.id,
+ 'uom_po_id': T_LBS.id,
+ 'tracking': 'lot',
+ })
+
+ picking_in = self.env['stock.picking'].create({
+ 'picking_type_id': self.picking_type_in,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location
+ })
+ move = self.env['stock.move'].create({
+ 'name': 'First move with 60 GT',
+ 'product_id': T_TEST.id,
+ 'product_uom_qty': 60,
+ 'product_uom': T_GT.id,
+ 'picking_id': picking_in.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location
+ })
+ picking_in.action_confirm()
+
+ self.assertEqual(move.product_uom_qty, 60.00, 'Wrong T_GT quantity')
+ self.assertEqual(move.product_qty, 134400.00, 'Wrong T_LBS quantity')
+
+ lot = self.env['stock.production.lot'].create({'name': 'Lot TEST', 'product_id': T_TEST.id, 'company_id': self.env.company.id, })
+ self.env['stock.move.line'].create({
+ 'move_id': move.id,
+ 'product_id': T_TEST.id,
+ 'product_uom_id': T_LBS.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'qty_done': 42760.00,
+ 'lot_id': lot.id,
+ })
+
+ picking_in._action_done()
+ back_order_in = self.env['stock.picking'].search([('backorder_id', '=', picking_in.id)])
+
+ self.assertEqual(len(back_order_in), 1.00, 'There should be one back order created')
+ self.assertEqual(back_order_in.move_lines.product_qty, 91640.00, 'There should be one back order created')
+
+ def test_move_product_with_different_uom(self):
+ """ Product defined in g with 0.01 rounding
+ Decimal Accuracy (DA) 3 digits.
+ Quantity on hand: 149.88g
+ Picking of 1kg
+ kg has 0.0001 rounding
+ Due to conversions, we may end up reserving 150g
+ (more than the quantity in stock), we check that
+ we reserve less than the quantity in stock
+ """
+ precision = self.env.ref('product.decimal_product_uom')
+ precision.digits = 3
+ precision_digits = precision.digits
+
+ self.uom_kg.rounding = 0.0001
+ self.uom_gm.rounding = 0.01
+
+ product_G = self.env['product.product'].create({
+ 'name': 'Product G',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'uom_id': self.uom_gm.id,
+ 'uom_po_id': self.uom_gm.id,
+ })
+
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ self.env['stock.quant']._update_available_quantity(product_G, stock_location, 149.88)
+ self.assertEqual(len(product_G.stock_quant_ids), 1, 'One quant should exist for the product.')
+ quant = product_G.stock_quant_ids
+
+ # transfer 1kg of product_G
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+
+ move = self.env['stock.move'].create({
+ 'name': 'test_reserve_product_G',
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_id': picking.id,
+ 'product_id': product_G.id,
+ 'product_uom': self.uom_kg.id,
+ 'product_uom_qty': 1,
+ })
+
+ self.assertEqual(move.product_uom.id, self.uom_kg.id)
+ self.assertEqual(move.product_uom_qty, 1.0)
+
+ picking.action_confirm()
+ picking.action_assign()
+
+ self.assertEqual(product_G.uom_id.rounding, 0.01)
+ self.assertEqual(move.product_uom.rounding, 0.0001)
+
+ self.assertEqual(len(picking.move_line_ids), 1, 'One move line should exist for the picking.')
+ move_line = picking.move_line_ids
+ # check that we do not reserve more (in the same UOM) than the quantity in stock
+ self.assertEqual(float_compare(move_line.product_qty, quant.quantity, precision_digits=precision_digits), -1, "We do not reserve more (in the same UOM) than the quantity in stock")
+ # check that we reserve the same quantity in the ml and the quant
+ self.assertTrue(float_is_zero(move_line.product_qty - quant.reserved_quantity, precision_digits=precision_digits))
+
+ def test_update_product_move_line_with_different_uom(self):
+ """ Check that when the move line and corresponding
+ product have different UOM with possibly conflicting
+ precisions, we do not reserve more than the quantity
+ in stock. Similar initial configuration as
+ test_move_product_with_different_uom.
+ """
+ precision = self.env.ref('product.decimal_product_uom')
+ precision.digits = 3
+ precision_digits = precision.digits
+
+ self.uom_kg.rounding = 0.0001
+ self.uom_gm.rounding = 0.01
+
+ product_LtDA = self.env['product.product'].create({
+ 'name': 'Product Less than DA',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'uom_id': self.uom_gm.id,
+ 'uom_po_id': self.uom_gm.id,
+ })
+
+ product_GtDA = self.env['product.product'].create({
+ 'name': 'Product Greater than DA',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'uom_id': self.uom_gm.id,
+ 'uom_po_id': self.uom_gm.id,
+ })
+
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+
+ # quantity in hand converted to kg is not more precise than the DA
+ self.env['stock.quant']._update_available_quantity(product_LtDA, stock_location, 149)
+ # quantity in hand converted to kg is more precise than the DA
+ self.env['stock.quant']._update_available_quantity(product_GtDA, stock_location, 149.88)
+
+ self.assertEqual(len(product_LtDA.stock_quant_ids), 1, 'One quant should exist for the product.')
+ self.assertEqual(len(product_GtDA.stock_quant_ids), 1, 'One quant should exist for the product.')
+ quant_LtDA = product_LtDA.stock_quant_ids
+ quant_GtDA = product_GtDA.stock_quant_ids
+
+ # create 2 moves of 1kg
+ move_LtDA = self.env['stock.move'].create({
+ 'name': 'test_reserve_product_LtDA',
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'product_id': product_LtDA.id,
+ 'product_uom': self.uom_kg.id,
+ 'product_uom_qty': 1,
+ })
+
+ move_GtDA = self.env['stock.move'].create({
+ 'name': 'test_reserve_product_GtDA',
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'product_id': product_GtDA.id,
+ 'product_uom': self.uom_kg.id,
+ 'product_uom_qty': 1,
+ })
+
+ self.assertEqual(move_LtDA.state, 'draft')
+ self.assertEqual(move_GtDA.state, 'draft')
+ move_LtDA._action_confirm()
+ move_GtDA._action_confirm()
+ self.assertEqual(move_LtDA.state, 'confirmed')
+ self.assertEqual(move_GtDA.state, 'confirmed')
+ # check availability, less than initial demand
+ move_LtDA._action_assign()
+ move_GtDA._action_assign()
+ self.assertEqual(move_LtDA.state, 'partially_available')
+ self.assertEqual(move_GtDA.state, 'partially_available')
+ # the initial demand is 1kg
+ self.assertEqual(move_LtDA.product_uom.id, self.uom_kg.id)
+ self.assertEqual(move_GtDA.product_uom.id, self.uom_kg.id)
+ self.assertEqual(move_LtDA.product_uom_qty, 1.0)
+ self.assertEqual(move_GtDA.product_uom_qty, 1.0)
+ # one move line is created
+ self.assertEqual(len(move_LtDA.move_line_ids), 1)
+ self.assertEqual(len(move_GtDA.move_line_ids), 1)
+
+ # increase quantity by 0.14988 kg (more precise than DA)
+ self.env['stock.quant']._update_available_quantity(product_LtDA, stock_location, 149.88)
+ self.env['stock.quant']._update_available_quantity(product_GtDA, stock_location, 149.88)
+
+ # _update_reserved_quantity is called on a move only in _action_assign
+ move_LtDA._action_assign()
+ move_GtDA._action_assign()
+
+ # as the move line for LtDA and its corresponding quant can be
+ # in different UOMs, a new move line can be created
+ # from _update_reserved_quantity
+ move_lines_LtDA = self.env["stock.move.line"].search([
+ ('product_id', '=', quant_LtDA.product_id.id),
+ ('location_id', '=', quant_LtDA.location_id.id),
+ ('lot_id', '=', quant_LtDA.lot_id.id),
+ ('package_id', '=', quant_LtDA.package_id.id),
+ ('owner_id', '=', quant_LtDA.owner_id.id),
+ ('product_qty', '!=', 0)
+ ])
+ reserved_on_move_lines_LtDA = sum(move_lines_LtDA.mapped('product_qty'))
+
+ move_lines_GtDA = self.env["stock.move.line"].search([
+ ('product_id', '=', quant_GtDA.product_id.id),
+ ('location_id', '=', quant_GtDA.location_id.id),
+ ('lot_id', '=', quant_GtDA.lot_id.id),
+ ('package_id', '=', quant_GtDA.package_id.id),
+ ('owner_id', '=', quant_GtDA.owner_id.id),
+ ('product_qty', '!=', 0)
+ ])
+ reserved_on_move_lines_GtDA = sum(move_lines_GtDA.mapped('product_qty'))
+
+ # check that we do not reserve more (in the same UOM) than the quantity in stock
+ self.assertEqual(float_compare(reserved_on_move_lines_LtDA, quant_LtDA.quantity, precision_digits=precision_digits), -1, "We do not reserve more (in the same UOM) than the quantity in stock")
+ self.assertEqual(float_compare(reserved_on_move_lines_GtDA, quant_GtDA.quantity, precision_digits=precision_digits), -1, "We do not reserve more (in the same UOM) than the quantity in stock")
+
+ # check that we reserve the same quantity in the ml and the quant
+ self.assertTrue(float_is_zero(reserved_on_move_lines_LtDA - quant_LtDA.reserved_quantity, precision_digits=precision_digits))
+ self.assertTrue(float_is_zero(reserved_on_move_lines_GtDA - quant_GtDA.reserved_quantity, precision_digits=precision_digits))
+
+
+class TestRoutes(TestStockCommon):
+ def setUp(self):
+ super(TestRoutes, self).setUp()
+ self.product1 = self.env['product.product'].create({
+ 'name': 'product a',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ self.uom_unit = self.env.ref('uom.product_uom_unit')
+ self.partner = self.env['res.partner'].create({'name': 'Partner'})
+
+ def _enable_pick_ship(self):
+ self.wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+
+ # create and get back the pick ship route
+ self.wh.write({'delivery_steps': 'pick_ship'})
+ self.pick_ship_route = self.wh.route_ids.filtered(lambda r: '(pick + ship)' in r.name)
+
+ def test_pick_ship_1(self):
+ """ Enable the pick ship route, force a procurement group on the
+ pick. When a second move is added, make sure the `partner_id` and
+ `origin` fields are erased.
+ """
+ self._enable_pick_ship()
+
+ # create a procurement group and set in on the pick stock rule
+ procurement_group0 = self.env['procurement.group'].create({})
+ pick_rule = self.pick_ship_route.rule_ids.filtered(lambda rule: 'Stock → Output' in rule.name)
+ push_rule = self.pick_ship_route.rule_ids - pick_rule
+ pick_rule.write({
+ 'group_propagation_option': 'fixed',
+ 'group_id': procurement_group0.id,
+ })
+
+ ship_location = pick_rule.location_id
+ customer_location = push_rule.location_id
+ partners = self.env['res.partner'].search([], limit=2)
+ partner0 = partners[0]
+ partner1 = partners[1]
+ procurement_group1 = self.env['procurement.group'].create({'partner_id': partner0.id})
+ procurement_group2 = self.env['procurement.group'].create({'partner_id': partner1.id})
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'first out move',
+ 'procure_method': 'make_to_order',
+ 'location_id': ship_location.id,
+ 'location_dest_id': customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'warehouse_id': self.wh.id,
+ 'group_id': procurement_group1.id,
+ 'origin': 'origin1',
+ })
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'second out move',
+ 'procure_method': 'make_to_order',
+ 'location_id': ship_location.id,
+ 'location_dest_id': customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'warehouse_id': self.wh.id,
+ 'group_id': procurement_group2.id,
+ 'origin': 'origin2',
+ })
+
+ # first out move, the "pick" picking should have a partner and an origin
+ move1._action_confirm()
+ picking_pick = move1.move_orig_ids.picking_id
+ self.assertEqual(picking_pick.partner_id.id, procurement_group1.partner_id.id)
+ self.assertEqual(picking_pick.origin, move1.group_id.name)
+
+ # second out move, the "pick" picking should have lost its partner and origin
+ move2._action_confirm()
+ self.assertEqual(picking_pick.partner_id.id, False)
+ self.assertEqual(picking_pick.origin, False)
+
+ def test_replenish_pick_ship_1(self):
+ """ Creates 2 warehouses and make a replenish using one warehouse
+ to ressuply the other one, Then check if the quantity and the product are matching
+ """
+ self.product_uom_qty = 42
+
+ warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ warehouse_2 = self.env['stock.warehouse'].create({
+ 'name': 'Small Warehouse',
+ 'code': 'SWH'
+ })
+ warehouse_1.write({
+ 'resupply_wh_ids': [(6, 0, [warehouse_2.id])]
+ })
+ resupply_route = self.env['stock.location.route'].search([('supplier_wh_id', '=', warehouse_2.id), ('supplied_wh_id', '=', warehouse_1.id)])
+ self.assertTrue(resupply_route, "Ressuply route not found")
+ self.product1.write({'route_ids': [(4, resupply_route.id), (4, self.env.ref('stock.route_warehouse0_mto').id)]})
+ self.wh = warehouse_1
+
+ replenish_wizard = self.env['product.replenish'].create({
+ 'product_id': self.product1.id,
+ 'product_tmpl_id': self.product1.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'quantity': self.product_uom_qty,
+ 'warehouse_id': self.wh.id,
+ })
+
+ replenish_wizard.launch_replenishment()
+ last_picking_id = self.env['stock.picking'].search([('origin', '=', 'Manual Replenishment')])[-1]
+ self.assertTrue(last_picking_id, 'Picking not found')
+ move_line = last_picking_id.move_lines.search([('product_id','=', self.product1.id)])
+ self.assertTrue(move_line,'The product is not in the picking')
+ self.assertEqual(move_line[0].product_uom_qty, self.product_uom_qty, 'Quantities does not match')
+ self.assertEqual(move_line[1].product_uom_qty, self.product_uom_qty, 'Quantities does not match')
+
+ def test_push_rule_on_move_1(self):
+ """ Create a route with a push rule, force it on a move, check that it is applied.
+ """
+ self._enable_pick_ship()
+ stock_location = self.env.ref('stock.stock_location_stock')
+
+ push_location = self.env['stock.location'].create({
+ 'location_id': stock_location.location_id.id,
+ 'name': 'push location',
+ })
+
+ # TODO: maybe add a new type on the "applicable on" fields?
+ route = self.env['stock.location.route'].create({
+ 'name': 'new route',
+ 'rule_ids': [(0, False, {
+ 'name': 'create a move to push location',
+ 'location_src_id': stock_location.id,
+ 'location_id': push_location.id,
+ 'company_id': self.env.company.id,
+ 'action': 'push',
+ 'auto': 'manual',
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })],
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'move with a route',
+ 'location_id': stock_location.id,
+ 'location_dest_id': stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'route_ids': [(4, route.id)]
+ })
+ move1._action_confirm()
+
+ pushed_move = move1.move_dest_ids
+ self.assertEqual(pushed_move.location_dest_id.id, push_location.id)
+
+ def test_location_dest_update(self):
+ """ Check the location dest of a stock move changed by a push rule
+ with auto field set to transparent is done correctly. The stock_move
+ is create with the move line directly to pass into action_confirm() via
+ action_done(). """
+ self.wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ new_loc = self.env['stock.location'].create({
+ 'name': 'New_location',
+ 'usage': 'internal',
+ 'location_id': self.env.ref('stock.stock_location_locations').id,
+ })
+ picking_type = self.env['stock.picking.type'].create({
+ 'name': 'new_picking_type',
+ 'code': 'internal',
+ 'sequence_code': 'NPT',
+ 'default_location_src_id': self.env.ref('stock.stock_location_stock').id,
+ 'default_location_dest_id': new_loc.id,
+ 'warehouse_id': self.wh.id,
+ })
+ route = self.env['stock.location.route'].create({
+ 'name': 'new route',
+ 'rule_ids': [(0, False, {
+ 'name': 'create a move to push location',
+ 'location_src_id': self.env.ref('stock.stock_location_stock').id,
+ 'location_id': new_loc.id,
+ 'company_id': self.env.company.id,
+ 'action': 'push',
+ 'auto': 'transparent',
+ 'picking_type_id': picking_type.id,
+ })],
+ })
+ product = self.env['product.product'].create({
+ 'name': 'new_product',
+ 'type': 'product',
+ 'route_ids': [(4, route.id)]
+ })
+ move1 = self.env['stock.move'].create({
+ 'name': 'move with a route',
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.env.ref('stock.stock_location_stock').id,
+ 'product_id': product.id,
+ 'product_uom_qty': 1.0,
+ 'product_uom': self.uom_unit.id,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': product.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.env.ref('stock.stock_location_stock').id,
+ 'qty_done': 1.00,
+ })],
+ })
+ move1._action_done()
+ self.assertEqual(move1.location_dest_id, new_loc)
+ positive_quant = product.stock_quant_ids.filtered(lambda q: q.quantity > 0)
+ self.assertEqual(positive_quant.location_id, new_loc)
+
+ def test_mtso_mto(self):
+ """ Run a procurement for 5 products when there are only 4 in stock then
+ check that MTO is applied on the moves when the rule is set to 'mts_else_mto'
+ """
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ warehouse.delivery_steps = 'pick_pack_ship'
+ partner_demo_customer = self.partner
+ final_location = partner_demo_customer.property_stock_customer
+ product_a = self.env['product.product'].create({
+ 'name': 'ProductA',
+ 'type': 'product',
+ })
+
+ self.env['stock.quant']._update_available_quantity(product_a, warehouse.wh_output_stock_loc_id, 4.0)
+
+ # We set quantities in the stock location to avoid warnings
+ # triggered by '_onchange_product_id_check_availability'
+ self.env['stock.quant']._update_available_quantity(product_a, warehouse.lot_stock_id, 4.0)
+
+ # We alter one rule and we set it to 'mts_else_mto'
+ values = {'warehouse_id': warehouse}
+ rule = self.env['procurement.group']._get_rule(product_a, final_location, values)
+ rule.procure_method = 'mts_else_mto'
+
+ pg = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mto'})
+
+ self.env['procurement.group'].run([
+ pg.Procurement(
+ product_a,
+ 5.0,
+ product_a.uom_id,
+ final_location,
+ 'test_mtso_mto',
+ 'test_mtso_mto',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg
+ }
+ )
+ ])
+
+ qty_available = self.env['stock.quant']._get_available_quantity(product_a, warehouse.wh_output_stock_loc_id)
+
+ # 3 pickings should be created.
+ picking_ids = self.env['stock.picking'].search([('group_id', '=', pg.id)])
+ self.assertEqual(len(picking_ids), 3)
+ for picking in picking_ids:
+ # Only the picking from Stock to Pack should be MTS
+ if picking.location_id == warehouse.lot_stock_id:
+ self.assertEqual(picking.move_lines.procure_method, 'make_to_stock')
+ else:
+ self.assertEqual(picking.move_lines.procure_method, 'make_to_order')
+
+ self.assertEqual(len(picking.move_lines), 1)
+ self.assertEqual(picking.move_lines.product_uom_qty, 5, 'The quantity of the move should be the same as on the SO')
+ self.assertEqual(qty_available, 4, 'The 4 products should still be available')
+
+ def test_mtso_mts(self):
+ """ Run a procurement for 4 products when there are 4 in stock then
+ check that MTS is applied on the moves when the rule is set to 'mts_else_mto'
+ """
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ warehouse.delivery_steps = 'pick_pack_ship'
+ partner_demo_customer = self.partner
+ final_location = partner_demo_customer.property_stock_customer
+ product_a = self.env['product.product'].create({
+ 'name': 'ProductA',
+ 'type': 'product',
+ })
+
+ self.env['stock.quant']._update_available_quantity(product_a, warehouse.wh_output_stock_loc_id, 4.0)
+
+ # We alter one rule and we set it to 'mts_else_mto'
+ values = {'warehouse_id': warehouse}
+ rule = self.env['procurement.group']._get_rule(product_a, final_location, values)
+ rule.procure_method = 'mts_else_mto'
+
+ pg = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mts'})
+
+ self.env['procurement.group'].run([
+ pg.Procurement(
+ product_a,
+ 4.0,
+ product_a.uom_id,
+ final_location,
+ 'test_mtso_mts',
+ 'test_mtso_mts',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg
+ }
+ )
+ ])
+
+ # A picking should be created with its move having MTS as procure method.
+ picking_ids = self.env['stock.picking'].search([('group_id', '=', pg.id)])
+ self.assertEqual(len(picking_ids), 1)
+ picking = picking_ids
+ self.assertEqual(picking.move_lines.procure_method, 'make_to_stock')
+ self.assertEqual(len(picking.move_lines), 1)
+ self.assertEqual(picking.move_lines.product_uom_qty, 4)
+
+ def test_mtso_multi_pg(self):
+ """ Run 3 procurements for 2 products at the same times when there are 4 in stock then
+ check that MTS is applied on the moves when the rule is set to 'mts_else_mto'
+ """
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ warehouse.delivery_steps = 'pick_pack_ship'
+ partner_demo_customer = self.partner
+ final_location = partner_demo_customer.property_stock_customer
+ product_a = self.env['product.product'].create({
+ 'name': 'ProductA',
+ 'type': 'product',
+ })
+
+ self.env['stock.quant']._update_available_quantity(product_a, warehouse.wh_output_stock_loc_id, 4.0)
+
+ # We alter one rule and we set it to 'mts_else_mto'
+ values = {'warehouse_id': warehouse}
+ rule = self.env['procurement.group']._get_rule(product_a, final_location, values)
+ rule.procure_method = 'mts_else_mto'
+
+ pg1 = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mts-1'})
+ pg2 = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mts-2'})
+ pg3 = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mts-3'})
+
+ self.env['procurement.group'].run([
+ pg1.Procurement(
+ product_a,
+ 2.0,
+ product_a.uom_id,
+ final_location,
+ 'test_mtso_mts_1',
+ 'test_mtso_mts_1',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg1
+ }
+ ),
+ pg2.Procurement(
+ product_a,
+ 2.0,
+ product_a.uom_id,
+ final_location,
+ 'test_mtso_mts_2',
+ 'test_mtso_mts_2',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg2
+ }
+ ),
+ pg3.Procurement(
+ product_a,
+ 2.0,
+ product_a.uom_id,
+ final_location,
+ 'test_mtso_mts_3',
+ 'test_mtso_mts_3',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg3
+ }
+ )
+ ])
+
+ pickings_pg1 = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
+ pickings_pg2 = self.env['stock.picking'].search([('group_id', '=', pg2.id)])
+ pickings_pg3 = self.env['stock.picking'].search([('group_id', '=', pg3.id)])
+
+ # The 2 first procurements should have create only 1 picking since enough quantities
+ # are left in the delivery location
+ self.assertEqual(len(pickings_pg1), 1)
+ self.assertEqual(len(pickings_pg2), 1)
+ self.assertEqual(pickings_pg1.move_lines.procure_method, 'make_to_stock')
+ self.assertEqual(pickings_pg2.move_lines.procure_method, 'make_to_stock')
+
+ # The last one should have 3 pickings as there's nothing left in the delivery location
+ self.assertEqual(len(pickings_pg3), 3)
+ for picking in pickings_pg3:
+ # Only the picking from Stock to Pack should be MTS
+ if picking.location_id == warehouse.lot_stock_id:
+ self.assertEqual(picking.move_lines.procure_method, 'make_to_stock')
+ else:
+ self.assertEqual(picking.move_lines.procure_method, 'make_to_order')
+
+ # All the moves should be should have the same quantity as it is on each procurements
+ self.assertEqual(len(picking.move_lines), 1)
+ self.assertEqual(picking.move_lines.product_uom_qty, 2)
+
+ def test_mtso_mto_adjust_01(self):
+ """ Run '_adjust_procure_method' for products A & B:
+ - Product A has 5.0 available
+ - Product B has 3.0 available
+ Stock moves (SM) are created for 4.0 units
+ After '_adjust_procure_method':
+ - SM for A is 'make_to_stock'
+ - SM for B is 'make_to_order'
+ """
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ final_location = self.partner.property_stock_customer
+ product_A = self.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ })
+ product_B = self.env['product.product'].create({
+ 'name': 'Product B',
+ 'type': 'product',
+ })
+
+ # We alter one rule and we set it to 'mts_else_mto'
+ rule = self.env['procurement.group']._get_rule(product_A, final_location, {'warehouse_id': warehouse})
+ rule.procure_method = 'mts_else_mto'
+
+ self.env['stock.quant']._update_available_quantity(product_A, warehouse.lot_stock_id, 5.0)
+ self.env['stock.quant']._update_available_quantity(product_B, warehouse.lot_stock_id, 3.0)
+
+ move_tmpl = {
+ 'name': 'Product',
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 4.0,
+ 'location_id': warehouse.lot_stock_id.id,
+ 'location_dest_id': self.partner.property_stock_customer.id,
+ 'warehouse_id': warehouse.id,
+ }
+ move_A_vals = dict(move_tmpl)
+ move_A_vals.update({
+ 'product_id': product_A.id,
+ })
+ move_A = self.env['stock.move'].create(move_A_vals)
+ move_B_vals = dict(move_tmpl)
+ move_B_vals.update({
+ 'product_id': product_B.id,
+ })
+ move_B = self.env['stock.move'].create(move_B_vals)
+ moves = move_A + move_B
+
+ self.assertEqual(move_A.procure_method, 'make_to_stock', 'Move A should be "make_to_stock"')
+ self.assertEqual(move_B.procure_method, 'make_to_stock', 'Move A should be "make_to_order"')
+ moves._adjust_procure_method()
+ self.assertEqual(move_A.procure_method, 'make_to_stock', 'Move A should be "make_to_stock"')
+ self.assertEqual(move_B.procure_method, 'make_to_order', 'Move A should be "make_to_order"')
+
+ def test_mtso_mto_adjust_02(self):
+ """ Run '_adjust_procure_method' for products A & B:
+ - Product A has 5.0 available
+ - Product B has 3.0 available
+ Stock moves (SM) are created for 2.0 + 2.0 units
+ After '_adjust_procure_method':
+ - SM for A is 'make_to_stock'
+ - SM for B is 'make_to_stock' and 'make_to_order'
+ """
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ final_location = self.partner.property_stock_customer
+ product_A = self.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ })
+ product_B = self.env['product.product'].create({
+ 'name': 'Product B',
+ 'type': 'product',
+ })
+
+ # We alter one rule and we set it to 'mts_else_mto'
+ rule = self.env['procurement.group']._get_rule(product_A, final_location, {'warehouse_id': warehouse})
+ rule.procure_method = 'mts_else_mto'
+
+ self.env['stock.quant']._update_available_quantity(product_A, warehouse.lot_stock_id, 5.0)
+ self.env['stock.quant']._update_available_quantity(product_B, warehouse.lot_stock_id, 3.0)
+
+ move_tmpl = {
+ 'name': 'Product',
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ 'location_id': warehouse.lot_stock_id.id,
+ 'location_dest_id': self.partner.property_stock_customer.id,
+ 'warehouse_id': warehouse.id,
+ }
+ move_A1_vals = dict(move_tmpl)
+ move_A1_vals.update({
+ 'product_id': product_A.id,
+ })
+ move_A1 = self.env['stock.move'].create(move_A1_vals)
+ move_A2_vals = dict(move_tmpl)
+ move_A2_vals.update({
+ 'product_id': product_A.id,
+ })
+ move_A2 = self.env['stock.move'].create(move_A2_vals)
+ move_B1_vals = dict(move_tmpl)
+ move_B1_vals.update({
+ 'product_id': product_B.id,
+ })
+ move_B1 = self.env['stock.move'].create(move_B1_vals)
+ move_B2_vals = dict(move_tmpl)
+ move_B2_vals.update({
+ 'product_id': product_B.id,
+ })
+ move_B2 = self.env['stock.move'].create(move_B2_vals)
+ moves = move_A1 + move_A2 + move_B1 + move_B2
+
+ self.assertEqual(move_A1.procure_method, 'make_to_stock', 'Move A1 should be "make_to_stock"')
+ self.assertEqual(move_A2.procure_method, 'make_to_stock', 'Move A2 should be "make_to_stock"')
+ self.assertEqual(move_B1.procure_method, 'make_to_stock', 'Move B1 should be "make_to_stock"')
+ self.assertEqual(move_B2.procure_method, 'make_to_stock', 'Move B2 should be "make_to_stock"')
+ moves._adjust_procure_method()
+ self.assertEqual(move_A1.procure_method, 'make_to_stock', 'Move A1 should be "make_to_stock"')
+ self.assertEqual(move_A2.procure_method, 'make_to_stock', 'Move A2 should be "make_to_stock"')
+ self.assertEqual(move_B1.procure_method, 'make_to_stock', 'Move B1 should be "make_to_stock"')
+ self.assertEqual(move_B2.procure_method, 'make_to_order', 'Move B2 should be "make_to_order"')
+
+ def test_mtso_mto_adjust_03(self):
+ """ Run '_adjust_procure_method' for products A with 4.0 available
+ 2 Stock moves (SM) are created:
+ - SM1 for 5.0 Units
+ - SM2 for 3.0 Units
+ SM1 is confirmed, so 'virtual_available' is -1.0.
+ SM1 should become 'make_to_order'
+ SM2 should remain 'make_to_stock'
+ """
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ final_location = self.partner.property_stock_customer
+ product_A = self.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ })
+
+ # We alter one rule and we set it to 'mts_else_mto'
+ rule = self.env['procurement.group']._get_rule(product_A, final_location, {'warehouse_id': warehouse})
+ rule.procure_method = 'mts_else_mto'
+
+ self.env['stock.quant']._update_available_quantity(product_A, warehouse.lot_stock_id, 4.0)
+
+ move_tmpl = {
+ 'name': 'Product',
+ 'product_id': product_A.id,
+ 'product_uom': self.uom_unit.id,
+ 'location_id': warehouse.lot_stock_id.id,
+ 'location_dest_id': self.partner.property_stock_customer.id,
+ 'warehouse_id': warehouse.id,
+ }
+ move_A1_vals = dict(move_tmpl)
+ move_A1_vals.update({
+ 'product_uom_qty': 5.0,
+ })
+ move_A1 = self.env['stock.move'].create(move_A1_vals)
+ move_A2_vals = dict(move_tmpl)
+ move_A2_vals.update({
+ 'product_uom_qty': 3.0,
+ })
+ move_A2 = self.env['stock.move'].create(move_A2_vals)
+ moves = move_A1 + move_A2
+
+ self.assertEqual(move_A1.procure_method, 'make_to_stock', 'Move A1 should be "make_to_stock"')
+ self.assertEqual(move_A2.procure_method, 'make_to_stock', 'Move A2 should be "make_to_stock"')
+ move_A1._action_confirm()
+ moves._adjust_procure_method()
+ self.assertEqual(move_A1.procure_method, 'make_to_order', 'Move A should be "make_to_stock"')
+ self.assertEqual(move_A2.procure_method, 'make_to_stock', 'Move A should be "make_to_order"')
+
+ def test_delay_alert_3(self):
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
+ warehouse.delivery_steps = 'pick_pack_ship'
+ partner_demo_customer = self.partner
+ final_location = partner_demo_customer.property_stock_customer
+ product_a = self.env['product.product'].create({
+ 'name': 'ProductA',
+ 'type': 'product',
+ })
+ pg = self.env['procurement.group'].create({'name': 'Test-delay_alert_3'})
+ self.env['procurement.group'].run([
+ pg.Procurement(
+ product_a,
+ 4.0,
+ product_a.uom_id,
+ final_location,
+ 'delay',
+ 'delay',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg
+ }
+ ),
+ ])
+ ship, pack, pick = self.env['stock.move'].search([('product_id', '=', product_a.id)])
+
+ # by default they all the same `date`
+ self.assertEqual(set((ship + pack + pick).mapped('date')), {pick.date})
+
+ # pick - pack - ship
+ ship.date += timedelta(days=2)
+ pack.date += timedelta(days=1)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertFalse(pack.delay_alert_date)
+ self.assertFalse(ship.delay_alert_date)
+
+ # move the pack after the ship
+ # pick - ship - pack
+ pack.date += timedelta(days=2)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertFalse(pack.delay_alert_date)
+ self.assertTrue(ship.delay_alert_date)
+ self.assertAlmostEqual(ship.delay_alert_date, pack.date)
+
+ # restore the pack before the ship
+ # pick - pack - ship
+ pack.date -= timedelta(days=2)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertFalse(pack.delay_alert_date)
+ self.assertFalse(ship.delay_alert_date)
+
+ # move the pick after the pack
+ # pack - ship - pick
+ pick.date += timedelta(days=3)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertTrue(pack.delay_alert_date)
+ self.assertFalse(ship.delay_alert_date)
+ self.assertAlmostEqual(pack.delay_alert_date, pick.date)
+
+ # move the ship before the pack
+ # ship - pack - pick
+ ship.date -= timedelta(days=2)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertTrue(pack.delay_alert_date)
+ self.assertTrue(ship.delay_alert_date)
+ self.assertAlmostEqual(pack.delay_alert_date, pick.date)
+ self.assertAlmostEqual(ship.delay_alert_date, pack.date)
+
+ # move the pack at the end
+ # ship - pick - pack
+ pack.date = pick.date + timedelta(days=2)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertFalse(pack.delay_alert_date)
+ self.assertTrue(ship.delay_alert_date)
+ self.assertAlmostEqual(ship.delay_alert_date, pack.date)
+
+ # fix the ship
+ ship.date = pack.date + timedelta(days=2)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertFalse(pack.delay_alert_date)
+ self.assertFalse(ship.delay_alert_date)
+
+
+class TestAutoAssign(TestStockCommon):
+ def create_pick_ship(self):
+ picking_client = self.env['stock.picking'].create({
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+
+ dest = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_client.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location,
+ 'state': 'waiting',
+ 'procure_method': 'make_to_order',
+ })
+
+ picking_pick = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.pack_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_pick.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.pack_location,
+ 'move_dest_ids': [(4, dest.id)],
+ 'state': 'confirmed',
+ })
+ return picking_pick, picking_client
+
+ def test_auto_assign_0(self):
+ """Create a outgoing MTS move without enough products in stock, then
+ validate a incoming move to check if the outgoing move is automatically
+ assigned.
+ """
+ pack_location = self.env['stock.location'].browse(self.pack_location)
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+
+ # create customer picking and move
+ customer_picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ customer_move = self.env['stock.move'].create({
+ 'name': 'customer move',
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'product_id': self.productA.id,
+ 'product_uom': self.productA.uom_id.id,
+ 'product_uom_qty': 10.0,
+ 'picking_id': customer_picking.id,
+ 'picking_type_id': self.picking_type_out,
+ })
+ customer_picking.action_confirm()
+ customer_picking.action_assign()
+ self.assertEqual(customer_move.state, 'confirmed')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 0)
+
+ # create supplier picking and move
+ supplier_picking = self.env['stock.picking'].create({
+ 'location_id': self.customer_location,
+ 'location_dest_id': self.stock_location,
+ 'picking_type_id': self.picking_type_in,
+ })
+ supplier_move = self.env['stock.move'].create({
+ 'name': 'test_transit_1',
+ 'location_id': self.customer_location,
+ 'location_dest_id': self.stock_location,
+ 'product_id': self.productA.id,
+ 'product_uom': self.productA.uom_id.id,
+ 'product_uom_qty': 10.0,
+ 'picking_id': supplier_picking.id,
+ })
+ customer_picking.action_confirm()
+ customer_picking.action_assign()
+ supplier_move.quantity_done = 10
+ supplier_picking._action_done()
+
+ # customer move should be automatically assigned and no more available product in stock
+ self.assertEqual(customer_move.state, 'assigned')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 0)
+
+ def test_auto_assign_1(self):
+ """Create a outgoing MTO move without enough products, then validate a
+ move to make it available to check if the outgoing move is not
+ automatically assigned.
+ """
+ picking_pick, picking_client = self.create_pick_ship()
+ pack_location = self.env['stock.location'].browse(self.pack_location)
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
+
+ # create another move to make product available in pack_location
+ picking_pick_2 = self.env['stock.picking'].create({
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.pack_location,
+ 'picking_type_id': self.picking_type_out,
+ })
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_pick_2.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.pack_location,
+ 'state': 'confirmed',
+ })
+ picking_pick_2.action_assign()
+ picking_pick_2.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_pick_2._action_done()
+
+ self.assertEqual(picking_client.state, 'waiting', "MTO moves can't be automatically assigned.")
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 10.0)
+
+ def test_serial_lot_ids(self):
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ self.customer_location = self.env.ref('stock.stock_location_customers')
+ self.supplier_location = self.env.ref('stock.stock_location_suppliers')
+ self.uom_unit = self.env.ref('uom.product_uom_unit')
+ self.product_serial = self.env['product.product'].create({
+ 'name': 'PSerial',
+ 'type': 'product',
+ 'tracking': 'serial',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+
+ move = self.env['stock.move'].create({
+ 'name': 'TestReceive',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_unit.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ self.assertEqual(move.state, 'draft')
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'serial1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'serial2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot3 = self.env['stock.production.lot'].create({
+ 'name': 'serial3',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ move.lot_ids = [(4, lot1.id)]
+ move.lot_ids = [(4, lot2.id)]
+ move.lot_ids = [(4, lot3.id)]
+ self.assertEqual(move.quantity_done, 3.0)
+ move.lot_ids = [(3, lot2.id)]
+ self.assertEqual(move.quantity_done, 2.0)
+
+ self.uom_dozen = self.env.ref('uom.product_uom_dozen')
+ move = self.env['stock.move'].create({
+ 'name': 'TestReceiveDozen',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product_serial.id,
+ 'product_uom': self.uom_dozen.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+ move.lot_ids = [(4, lot1.id)]
+ move.lot_ids = [(4, lot2.id)]
+ move.lot_ids = [(4, lot3.id)]
+ self.assertEqual(move.quantity_done, 3.0/12.0)
+
+ def test_update_description(self):
+ """ Create an empty picking. Adds a move on product1, select the picking type, add
+ again a move on product1. Confirm the picking. The two stock moves should be merged. """
+ product1 = self.env['product.product'].create({
+ 'name': 'product',
+ 'type':'product',
+ })
+ picking_form = Form(self.env['stock.picking'])
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = product1
+ move.product_uom_qty = 10
+ move.location_id = self.env.ref('stock.stock_location_suppliers')
+ move.location_dest_id = self.env.ref('stock.stock_location_stock')
+ picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = product1
+ move.product_uom_qty = 15
+
+ picking = picking_form.save()
+ picking.action_confirm()
+
+ self.assertEqual(len(picking.move_lines), 1)
+ self.assertEqual(picking.move_lines.product_uom_qty, 25)
diff --git a/addons/stock/tests/test_multicompany.py b/addons/stock/tests/test_multicompany.py
new file mode 100644
index 00000000..44f981f2
--- /dev/null
+++ b/addons/stock/tests/test_multicompany.py
@@ -0,0 +1,598 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.exceptions import UserError
+from odoo.tests.common import SavepointCase, Form
+
+
+class TestMultiCompany(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestMultiCompany, cls).setUpClass()
+ group_user = cls.env.ref('base.group_user')
+ group_stock_manager = cls.env.ref('stock.group_stock_manager')
+
+ cls.company_a = cls.env['res.company'].create({'name': 'Company A'})
+ cls.company_b = cls.env['res.company'].create({'name': 'Company B'})
+ cls.warehouse_a = cls.env['stock.warehouse'].search([('company_id', '=', cls.company_a.id)], limit=1)
+ cls.warehouse_b = cls.env['stock.warehouse'].search([('company_id', '=', cls.company_b.id)], limit=1)
+ cls.stock_location_a = cls.warehouse_a.lot_stock_id
+ cls.stock_location_b = cls.warehouse_b.lot_stock_id
+
+ cls.user_a = cls.env['res.users'].create({
+ 'name': 'user company a with access to company b',
+ 'login': 'user a',
+ 'groups_id': [(6, 0, [group_user.id, group_stock_manager.id])],
+ 'company_id': cls.company_a.id,
+ 'company_ids': [(6, 0, [cls.company_a.id, cls.company_b.id])]
+ })
+ cls.user_b = cls.env['res.users'].create({
+ 'name': 'user company a with access to company b',
+ 'login': 'user b',
+ 'groups_id': [(6, 0, [group_user.id, group_stock_manager.id])],
+ 'company_id': cls.company_b.id,
+ 'company_ids': [(6, 0, [cls.company_a.id, cls.company_b.id])]
+ })
+
+ def test_picking_type_1(self):
+ """As a user of Company A, check it is not possible to use a warehouse of Company B in a
+ picking type of Company A.
+ """
+ picking_type_company_a = self.env['stock.picking.type'].search([
+ ('company_id', '=', self.company_a.id)
+ ], limit=1)
+ with self.assertRaises(UserError):
+ picking_type_company_a.warehouse_id = self.warehouse_b
+
+ def test_picking_type_2(self):
+ """As a user of Company A, check it is not possible to change the company on an existing
+ picking type of Company A to Company B.
+ """
+ picking_type_company_a = self.env['stock.picking.type'].search([
+ ('company_id', '=', self.company_a.id)
+ ], limit=1)
+ with self.assertRaises(UserError):
+ picking_type_company_a.with_user(self.user_a).company_id = self.company_b
+
+ def test_putaway_1(self):
+ """As a user of Company A, create a putaway rule with locations of Company A and set the
+ company to Company B before saving. Check it is not possible.
+ """
+ stock_location_a_1 = self.env['stock.location'].with_user(self.user_a).create({
+ 'location_id': self.stock_location_a.id,
+ 'usage': 'internal',
+ 'name': 'A_1',
+ })
+ putaway_form = Form(self.env['stock.putaway.rule'])
+ putaway_form.location_in_id = self.stock_location_a
+ putaway_form.location_out_id = stock_location_a_1
+ putaway_form.company_id = self.company_b
+ with self.assertRaises(UserError):
+ putaway_form.save()
+
+ def test_putaway_2(self):
+ """As a user of Company A, check it is not possible to change the company on an existing
+ putaway rule to Company B.
+ """
+ stock_location_a_1 = self.env['stock.location'].with_user(self.user_a).create({
+ 'name': 'A_1',
+ 'location_id': self.stock_location_a.id,
+ 'usage': 'internal',
+ })
+ putaway_rule = self.env['stock.putaway.rule'].with_user(self.user_a).create({
+ 'location_in_id': self.stock_location_a.id,
+ 'location_out_id': stock_location_a_1.id
+ })
+ with self.assertRaises(UserError):
+ putaway_rule.company_id = self.company_b
+
+ def test_company_1(self):
+ """Check it is not possible to use the internal transit location of Company B on Company A."""
+ with self.assertRaises(UserError):
+ self.company_a.internal_transit_location_id = self.company_b.internal_transit_location_id
+
+ def test_partner_1(self):
+ """On a partner without company, as a user of Company B, check it is not possible to use a
+ location limited to Company A as `property_stock_supplier` or `property_stock_customer`.
+ """
+ shared_partner = self.env['res.partner'].create({
+ 'name': 'Shared Partner',
+ 'company_id': False,
+ })
+ with self.assertRaises(UserError):
+ shared_partner.with_user(self.user_b).property_stock_customer = self.stock_location_a
+
+ def test_inventory_1(self):
+ """Create an inventory in Company A for a product limited to Company A and, as a user of company
+ B, start the inventory and set its counted quantity to 10 before validating. The inventory
+ lines and stock moves should belong to Company A. The inventory loss location used should be
+ the one of Company A.
+ """
+ product = self.env['product.product'].create({
+ 'type': 'product',
+ 'company_id': self.company_a.id,
+ 'name': 'Product limited to company A',
+ })
+ inventory = self.env['stock.inventory'].with_user(self.user_a).create({})
+ self.assertEqual(inventory.company_id, self.company_a)
+ inventory.with_user(self.user_b).action_start()
+ inventory.with_user(self.user_b).line_ids = [(0, 0, {
+ 'product_qty': 10,
+ 'product_id': product.id,
+ 'location_id': self.stock_location_a.id,
+ })]
+ inventory.with_user(self.user_b).action_validate()
+ self.assertEqual(inventory.line_ids.company_id, self.company_a)
+ self.assertEqual(inventory.move_ids.company_id, self.company_a)
+ self.assertEqual(inventory.move_ids.location_id.company_id, self.company_a)
+
+ def test_inventory_2(self):
+ """Create an empty inventory in Company A and check it is not possible to use products limited
+ to Company B in it.
+ """
+ product = self.env['product.product'].create({
+ 'name': 'product limited to company b',
+ 'company_id': self.company_b.id,
+ 'type': 'product'
+ })
+ inventory = self.env['stock.inventory'].with_user(self.user_a).create({})
+ inventory.with_user(self.user_a).action_start()
+ inventory.with_user(self.user_a).line_ids = [(0, 0, {
+ 'product_id': product.id,
+ 'product_qty': 10,
+ 'location_id': self.stock_location_a.id,
+ })]
+ with self.assertRaises(UserError):
+ inventory.with_user(self.user_a).action_validate()
+
+ def test_inventory_3(self):
+ """As a user of Company A, check it is not possible to start an inventory adjustment for
+ a product limited to Company B.
+ """
+ product = self.env['product.product'].create({
+ 'name': 'product limited to company b',
+ 'company_id': self.company_b.id,
+ 'type': 'product'
+ })
+ inventory = self.env['stock.inventory'].with_user(self.user_a).create({'product_ids': [(4, product.id)]})
+ with self.assertRaises(UserError):
+ inventory.with_user(self.user_a).action_start()
+
+ def test_picking_1(self):
+ """As a user of Company A, create a picking and use a picking type of Company B, check the
+ create picking belongs to Company B.
+ """
+ picking_type_company_b = self.env['stock.picking.type'].search([('company_id', '=', self.company_b.id)], limit=1)
+ picking_form = Form(self.env['stock.picking'].with_user(self.user_a))
+ picking_form.picking_type_id = picking_type_company_b
+ picking = picking_form.save()
+ self.assertEqual(picking.company_id, self.company_b)
+
+ def test_location_1(self):
+ """Check it is not possible to set a location of Company B under a location of Company A."""
+ with self.assertRaises(UserError):
+ self.stock_location_b.location_id = self.stock_location_a
+
+ def test_lot_1(self):
+ """Check it is possible to create a stock.production.lot with the same name in Company A and
+ Company B"""
+ product_lot = self.env['product.product'].create({
+ 'type': 'product',
+ 'tracking': 'lot',
+ 'name': 'product lot',
+ })
+ self.env['stock.production.lot'].create({
+ 'name': 'lotA',
+ 'company_id': self.company_a.id,
+ 'product_id': product_lot.id,
+ })
+ self.env['stock.production.lot'].create({
+ 'name': 'lotA',
+ 'company_id': self.company_b.id,
+ 'product_id': product_lot.id,
+ })
+
+ def test_lot_2(self):
+ """Validate a picking of Company A receiving lot1 while being logged into Company B. Check
+ the lot is created in Company A.
+ """
+ product = self.env['product.product'].create({
+ 'type': 'product',
+ 'tracking': 'serial',
+ 'name': 'product',
+ })
+ picking = self.env['stock.picking'].with_user(self.user_a).create({
+ 'picking_type_id': self.warehouse_a.in_type_id.id,
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': self.stock_location_a.id,
+ })
+ self.assertEqual(picking.company_id, self.company_a)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_lot_2',
+ 'picking_type_id': picking.picking_type_id.id,
+ 'location_id': picking.location_id.id,
+ 'location_dest_id': picking.location_dest_id.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 1.0,
+ 'picking_id': picking.id,
+ 'company_id': picking.company_id.id,
+ })
+ picking.with_user(self.user_b).action_confirm()
+ self.assertEqual(picking.state, 'assigned')
+ move1.with_user(self.user_b).move_line_ids[0].qty_done = 1
+ move1.with_user(self.user_b).move_line_ids[0].lot_name = 'receipt_serial'
+ self.assertEqual(move1.move_line_ids[0].company_id, self.company_a)
+ picking.with_user(self.user_b).button_validate()
+ self.assertEqual(picking.state, 'done')
+ created_serial = self.env['stock.production.lot'].search([
+ ('name', '=', 'receipt_serial')
+ ])
+ self.assertEqual(created_serial.company_id, self.company_a)
+
+ def test_orderpoint_1(self):
+ """As a user of company A, create an orderpoint for company B. Check itsn't possible to
+ use a warehouse of companny A"""
+ product = self.env['product.product'].create({
+ 'type': 'product',
+ 'name': 'shared product',
+ })
+ orderpoint = Form(self.env['stock.warehouse.orderpoint'].with_user(self.user_a))
+ orderpoint.company_id = self.company_b
+ orderpoint.warehouse_id = self.warehouse_b
+ orderpoint.location_id = self.stock_location_a
+ orderpoint.product_id = product
+ with self.assertRaises(UserError):
+ orderpoint.save()
+ orderpoint.location_id = self.stock_location_b
+ orderpoint = orderpoint.save()
+ self.assertEqual(orderpoint.company_id, self.company_b)
+
+ def test_orderpoint_2(self):
+ """As a user of Company A, check it is not possible to change the company on an existing
+ orderpoint to Company B.
+ """
+ product = self.env['product.product'].create({
+ 'type': 'product',
+ 'name': 'shared product',
+ })
+ orderpoint = Form(self.env['stock.warehouse.orderpoint'].with_user(self.user_a))
+ orderpoint.company_id = self.company_a
+ orderpoint.warehouse_id = self.warehouse_a
+ orderpoint.location_id = self.stock_location_a
+ orderpoint.product_id = product
+ orderpoint = orderpoint.save()
+ self.assertEqual(orderpoint.company_id, self.company_a)
+ with self.assertRaises(UserError):
+ orderpoint.company_id = self.company_b.id
+
+ def test_product_1(self):
+ """ As an user of Company A, checks we can or cannot create new product
+ depending of its `company_id`."""
+ # Creates a new product with no company_id and set a responsible.
+ # The product must be created as there is no company on the product.
+ product_form = Form(self.env['product.template'].with_user(self.user_a))
+ product_form.name = 'Paramite Pie'
+ product_form.responsible_id = self.user_b
+ product = product_form.save()
+
+ self.assertEqual(product.company_id.id, False)
+ self.assertEqual(product.responsible_id.id, self.user_b.id)
+
+ # Creates a new product belong to Company A and set a responsible belong
+ # to Company B. The product mustn't be created as the product and the
+ # user don't belong of the same company.
+ self.user_b.company_ids = [(6, 0, [self.company_b.id])]
+ product_form = Form(self.env['product.template'].with_user(self.user_a))
+ product_form.name = 'Meech Munchy'
+ product_form.company_id = self.company_a
+ product_form.responsible_id = self.user_b
+
+ with self.assertRaises(UserError):
+ # Raises an UserError for company incompatibility.
+ product = product_form.save()
+
+ # Creates a new product belong to Company A and set a responsible belong
+ # to Company A & B (default B). The product must be created as the user
+ # belongs to product's company.
+ self.user_b.company_ids = [(6, 0, [self.company_a.id, self.company_b.id])]
+ product_form = Form(self.env['product.template'].with_user(self.user_a))
+ product_form.name = 'Scrab Cake'
+ product_form.company_id = self.company_a
+ product_form.responsible_id = self.user_b
+ product = product_form.save()
+
+ self.assertEqual(product.company_id.id, self.company_a.id)
+ self.assertEqual(product.responsible_id.id, self.user_b.id)
+
+ def test_warehouse_1(self):
+ """As a user of Company A, on its main warehouse, see it is impossible to change the
+ company_id, to use a view location of another company, to set a picking type to one
+ of another company
+ """
+ with self.assertRaises(UserError):
+ self.warehouse_a.company_id = self.company_b.id
+ with self.assertRaises(UserError):
+ self.warehouse_a.view_location_id = self.warehouse_b.view_location_id
+ with self.assertRaises(UserError):
+ self.warehouse_a.pick_type_id = self.warehouse_b.pick_type_id
+
+ def test_move_1(self):
+ """See it is not possible to confirm a stock move of Company A with a picking type of
+ Company B.
+ """
+ product = self.env['product.product'].create({
+ 'name': 'p1',
+ 'type': 'product'
+ })
+ picking_type_b = self.env['stock.picking.type'].search([
+ ('company_id', '=', self.company_b.id),
+ ], limit=1)
+ move = self.env['stock.move'].create({
+ 'company_id': self.company_a.id,
+ 'picking_type_id': picking_type_b.id,
+ 'location_id': self.stock_location_a.id,
+ 'location_dest_id': self.stock_location_a.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'name': 'stock_move',
+ })
+ with self.assertRaises(UserError):
+ move._action_confirm()
+
+ def test_move_2(self):
+ """See it is not possible to confirm a stock move of Company A with a destination location
+ of Company B.
+ """
+ product = self.env['product.product'].create({
+ 'name': 'p1',
+ 'type': 'product'
+ })
+ picking_type_b = self.env['stock.picking.type'].search([
+ ('company_id', '=', self.company_b.id),
+ ], limit=1)
+ move = self.env['stock.move'].create({
+ 'company_id': self.company_a.id,
+ 'picking_type_id': picking_type_b.id,
+ 'location_id': self.stock_location_a.id,
+ 'location_dest_id': self.stock_location_b.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'name': 'stock_move',
+ })
+ with self.assertRaises(UserError):
+ move._action_confirm()
+
+ def test_move_3(self):
+ """See it is not possible to confirm a stock move of Company A with a product restricted to
+ Company B.
+ """
+ product = self.env['product.product'].create({
+ 'name': 'p1',
+ 'type': 'product',
+ 'company_id': self.company_b.id,
+ })
+ picking_type_b = self.env['stock.picking.type'].search([
+ ('company_id', '=', self.company_b.id),
+ ], limit=1)
+ move = self.env['stock.move'].create({
+ 'company_id': self.company_a.id,
+ 'picking_type_id': picking_type_b.id,
+ 'location_id': self.stock_location_a.id,
+ 'location_dest_id': self.stock_location_a.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'name': 'stock_move',
+ })
+ with self.assertRaises(UserError):
+ move._action_confirm()
+
+ def test_intercom_lot_push(self):
+ """ Create a push rule to transfer products received in inter company
+ transit location to company b. Move a lot product from company a to the
+ transit location. Check the move created by the push rule is not chained
+ with previous move, and no product are reserved from inter-company
+ transit. """
+ supplier_location = self.env.ref('stock.stock_location_suppliers')
+ intercom_location = self.env.ref('stock.stock_location_inter_wh')
+ intercom_location.write({'active': True})
+
+ product_lot = self.env['product.product'].create({
+ 'type': 'product',
+ 'tracking': 'lot',
+ 'name': 'product lot',
+ })
+
+ picking_type_to_transit = self.env['stock.picking.type'].create({
+ 'name': 'To Transit',
+ 'sequence_code': 'TRANSIT',
+ 'code': 'outgoing',
+ 'company_id': self.company_a.id,
+ 'warehouse_id': False,
+ 'default_location_src_id': self.stock_location_a.id,
+ 'default_location_dest_id': intercom_location.id,
+ 'sequence_id': self.env['ir.sequence'].create({
+ 'code': 'transit',
+ 'name': 'transit sequence',
+ 'company_id': self.company_a.id,
+ }).id,
+ })
+
+ route = self.env['stock.location.route'].create({
+ 'name': 'Push',
+ 'company_id': False,
+ 'rule_ids': [(0, False, {
+ 'name': 'create a move to company b',
+ 'company_id': self.company_b.id,
+ 'location_src_id': intercom_location.id,
+ 'location_id': self.stock_location_b.id,
+ 'action': 'push',
+ 'auto': 'manual',
+ 'picking_type_id': self.warehouse_b.in_type_id.id,
+ })],
+ })
+
+ move_from_supplier = self.env['stock.move'].create({
+ 'company_id': self.company_a.id,
+ 'name': 'test_from_supplier',
+ 'location_id': supplier_location.id,
+ 'location_dest_id': self.stock_location_a.id,
+ 'product_id': product_lot.id,
+ 'product_uom': product_lot.uom_id.id,
+ 'product_uom_qty': 1.0,
+ 'picking_type_id': self.warehouse_a.in_type_id.id,
+ })
+ move_from_supplier._action_confirm()
+ move_line_1 = move_from_supplier.move_line_ids[0]
+ move_line_1.lot_name = 'lot 1'
+ move_line_1.qty_done = 1.0
+ move_from_supplier._action_done()
+ lot_1 = move_line_1.lot_id
+
+ move_to_transit = self.env['stock.move'].create({
+ 'company_id': self.company_a.id,
+ 'name': 'test_to_transit',
+ 'location_id': self.stock_location_a.id,
+ 'location_dest_id': intercom_location.id,
+ 'product_id': product_lot.id,
+ 'product_uom': product_lot.uom_id.id,
+ 'product_uom_qty': 1.0,
+ 'picking_type_id': picking_type_to_transit.id,
+ 'route_ids': [(4, route.id)],
+ })
+ move_to_transit._action_confirm()
+ move_to_transit._action_assign()
+ move_line_2 = move_to_transit.move_line_ids[0]
+ self.assertTrue(move_line_2.lot_id, move_line_1.lot_id)
+ move_line_2.qty_done = 1.0
+ move_to_transit._action_done()
+
+ move_push = self.env['stock.move'].search([('location_id', '=', intercom_location.id),
+ ('product_id', '=', product_lot.id)])
+ self.assertTrue(move_push, 'No move created from push rules')
+ self.assertEqual(move_push.state, "assigned")
+ self.assertTrue(move_push.move_line_ids, "No move line created for the move")
+ self.assertFalse(move_push in move_to_transit.move_dest_ids,
+ "Chained move created in transit location")
+ self.assertNotEqual(move_push.move_line_ids.lot_id, move_line_2.lot_id,
+ "Reserved from transit location")
+ picking_receipt = move_push.picking_id
+ with self.assertRaises(UserError):
+ picking_receipt.button_validate()
+
+ move_line_3 = move_push.move_line_ids[0]
+ move_line_3.lot_name = 'lot 2'
+ move_line_3.qty_done = 1.0
+ picking_receipt.button_validate()
+ lot_2 = move_line_3.lot_id
+ self.assertEqual(lot_1.company_id, self.company_a)
+ self.assertEqual(lot_1.name, 'lot 1')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product_lot, intercom_location, lot_1), 1.0)
+ self.assertEqual(lot_2.company_id, self.company_b)
+ self.assertEqual(lot_2.name, 'lot 2')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product_lot, self.stock_location_b, lot_2), 1.0)
+
+ def test_intercom_lot_pull(self):
+ """Use warehouse of comany a to resupply warehouse of company b. Check
+ pull rule works correctly in two companies and moves are unchained from
+ inter-company transit location."""
+ customer_location = self.env.ref('stock.stock_location_customers')
+ supplier_location = self.env.ref('stock.stock_location_suppliers')
+ intercom_location = self.env.ref('stock.stock_location_inter_wh')
+ intercom_location.write({'active': True})
+ partner = self.env['res.partner'].create({'name': 'Deco Addict'})
+ self.warehouse_a.resupply_wh_ids = [(6, 0, [self.warehouse_b.id])]
+ resupply_route = self.env['stock.location.route'].search([('supplier_wh_id', '=', self.warehouse_b.id),
+ ('supplied_wh_id', '=', self.warehouse_a.id)])
+ self.assertTrue(resupply_route, "Resupply route not found")
+
+ product_lot = self.env['product.product'].create({
+ 'type': 'product',
+ 'tracking': 'lot',
+ 'name': 'product lot',
+ 'route_ids': [(4, resupply_route.id), (4, self.env.ref('stock.route_warehouse0_mto').id)],
+ })
+
+ move_sup_to_whb = self.env['stock.move'].create({
+ 'company_id': self.company_b.id,
+ 'name': 'from_supplier_to_whb',
+ 'location_id': supplier_location.id,
+ 'location_dest_id': self.warehouse_b.lot_stock_id.id,
+ 'product_id': product_lot.id,
+ 'product_uom': product_lot.uom_id.id,
+ 'product_uom_qty': 1.0,
+ 'picking_type_id': self.warehouse_b.in_type_id.id,
+ })
+ move_sup_to_whb._action_confirm()
+ move_line_1 = move_sup_to_whb.move_line_ids[0]
+ move_line_1.lot_name = 'lot b'
+ move_line_1.qty_done = 1.0
+ move_sup_to_whb._action_done()
+ lot_b = move_line_1.lot_id
+
+ picking_out = self.env['stock.picking'].create({
+ 'company_id': self.company_a.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': self.warehouse_a.out_type_id.id,
+ 'location_id': self.stock_location_a.id,
+ 'location_dest_id': customer_location.id,
+ })
+ move_wha_to_cus = self.env['stock.move'].create({
+ 'name': "WH_A to Customer",
+ 'product_id': product_lot.id,
+ 'product_uom_qty': 1,
+ 'product_uom': product_lot.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location_a.id,
+ 'location_dest_id': customer_location.id,
+ 'warehouse_id': self.warehouse_a.id,
+ 'procure_method': 'make_to_order',
+ 'company_id': self.company_a.id,
+ })
+ picking_out.action_confirm()
+
+ move_whb_to_transit = self.env['stock.move'].search([('location_id', '=', self.stock_location_b.id),
+ ('product_id', '=', product_lot.id)])
+ move_transit_to_wha = self.env['stock.move'].search([('location_id', '=', intercom_location.id),
+ ('product_id', '=', product_lot.id)])
+ self.assertTrue(move_whb_to_transit, "No move created by pull rule")
+ self.assertTrue(move_transit_to_wha, "No move created by pull rule")
+ self.assertTrue(move_wha_to_cus in move_transit_to_wha.move_dest_ids,
+ "Moves are not chained")
+ self.assertFalse(move_transit_to_wha in move_whb_to_transit.move_dest_ids,
+ "Chained move created in transit location")
+ self.assertEqual(move_wha_to_cus.state, "waiting")
+ self.assertEqual(move_transit_to_wha.state, "waiting")
+ self.assertEqual(move_whb_to_transit.state, "confirmed")
+
+ (move_wha_to_cus + move_whb_to_transit + move_transit_to_wha).picking_id.action_assign()
+ self.assertEqual(move_wha_to_cus.state, "waiting")
+ self.assertEqual(move_transit_to_wha.state, "assigned")
+ self.assertEqual(move_whb_to_transit.state, "assigned")
+
+ res_dict = move_whb_to_transit.picking_id.button_validate()
+ self.assertEqual(res_dict.get('res_model'), 'stock.immediate.transfer')
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ wizard.process()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product_lot, intercom_location, lot_b), 1.0)
+ with self.assertRaises(UserError):
+ move_transit_to_wha.picking_id.button_validate()
+
+ move_line_2 = move_transit_to_wha.move_line_ids[0]
+ move_line_2.lot_name = 'lot a'
+ move_line_2.qty_done = 1.0
+ move_transit_to_wha._action_done()
+ lot_a = move_line_2.lot_id
+
+ move_wha_to_cus._action_assign()
+ self.assertEqual(move_wha_to_cus.state, "assigned")
+ res_dict = move_wha_to_cus.picking_id.button_validate()
+ self.assertEqual(res_dict.get('res_model'), 'stock.immediate.transfer')
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ wizard.process()
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product_lot, customer_location, lot_a), 1.0)
+
+ self.assertEqual(lot_a.company_id, self.company_a)
+ self.assertEqual(lot_a.name, 'lot a')
+ self.assertEqual(lot_b.company_id, self.company_b)
+ self.assertEqual(lot_b.name, 'lot b')
diff --git a/addons/stock/tests/test_packing.py b/addons/stock/tests/test_packing.py
new file mode 100644
index 00000000..24c80e0a
--- /dev/null
+++ b/addons/stock/tests/test_packing.py
@@ -0,0 +1,801 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import Form
+from odoo.tests.common import SavepointCase
+from odoo.tools import float_round
+from odoo.exceptions import UserError
+
+
+class TestPackingCommon(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestPackingCommon, cls).setUpClass()
+ cls.stock_location = cls.env.ref('stock.stock_location_stock')
+ cls.warehouse = cls.env['stock.warehouse'].search([('lot_stock_id', '=', cls.stock_location.id)], limit=1)
+ cls.warehouse.write({'delivery_steps': 'pick_pack_ship'})
+ cls.pack_location = cls.warehouse.wh_pack_stock_loc_id
+ cls.ship_location = cls.warehouse.wh_output_stock_loc_id
+ cls.customer_location = cls.env.ref('stock.stock_location_customers')
+
+ cls.productA = cls.env['product.product'].create({'name': 'Product A', 'type': 'product'})
+ cls.productB = cls.env['product.product'].create({'name': 'Product B', 'type': 'product'})
+
+
+class TestPacking(TestPackingCommon):
+
+ def test_put_in_pack(self):
+ """ In a pick pack ship scenario, create two packs in pick and check that
+ they are correctly recognised and handled by the pack and ship picking.
+ Along this test, we'll use action_toggle_processed to process a pack
+ from the entire_package_ids one2many and we'll directly fill the move
+ lines, the latter is the behavior when the user did not enable the display
+ of entire packs on the picking type.
+ """
+ self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 20.0)
+ self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 20.0)
+ ship_move_a = self.env['stock.move'].create({
+ 'name': 'The ship move',
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 5.0,
+ 'product_uom': self.productA.uom_id.id,
+ 'location_id': self.ship_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'warehouse_id': self.warehouse.id,
+ 'picking_type_id': self.warehouse.out_type_id.id,
+ 'procure_method': 'make_to_order',
+ 'state': 'draft',
+ })
+ ship_move_b = self.env['stock.move'].create({
+ 'name': 'The ship move',
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 5.0,
+ 'product_uom': self.productB.uom_id.id,
+ 'location_id': self.ship_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'warehouse_id': self.warehouse.id,
+ 'picking_type_id': self.warehouse.out_type_id.id,
+ 'procure_method': 'make_to_order',
+ 'state': 'draft',
+ })
+ ship_move_a._assign_picking()
+ ship_move_b._assign_picking()
+ ship_move_a._action_confirm()
+ ship_move_b._action_confirm()
+ pack_move_a = ship_move_a.move_orig_ids[0]
+ pick_move_a = pack_move_a.move_orig_ids[0]
+
+ pick_picking = pick_move_a.picking_id
+ packing_picking = pack_move_a.picking_id
+ shipping_picking = ship_move_a.picking_id
+
+ pick_picking.picking_type_id.show_entire_packs = True
+ packing_picking.picking_type_id.show_entire_packs = True
+ shipping_picking.picking_type_id.show_entire_packs = True
+
+ pick_picking.action_assign()
+ self.assertEqual(len(pick_picking.move_ids_without_package), 2)
+ pick_picking.move_line_ids.filtered(lambda ml: ml.product_id == self.productA).qty_done = 1.0
+ pick_picking.move_line_ids.filtered(lambda ml: ml.product_id == self.productB).qty_done = 2.0
+
+ first_pack = pick_picking.action_put_in_pack()
+ self.assertEqual(len(pick_picking.package_level_ids), 1, 'Put some products in pack should create a package_level')
+ self.assertEqual(pick_picking.package_level_ids[0].state, 'new', 'A new pack should be in state "new"')
+ pick_picking.move_line_ids.filtered(lambda ml: ml.product_id == self.productA and ml.qty_done == 0.0).qty_done = 4.0
+ pick_picking.move_line_ids.filtered(lambda ml: ml.product_id == self.productB and ml.qty_done == 0.0).qty_done = 3.0
+ second_pack = pick_picking.action_put_in_pack()
+ self.assertEqual(len(pick_picking.move_ids_without_package), 0)
+ self.assertEqual(len(packing_picking.move_ids_without_package), 2)
+ pick_picking.button_validate()
+ self.assertEqual(len(packing_picking.move_ids_without_package), 0)
+ self.assertEqual(len(first_pack.quant_ids), 2)
+ self.assertEqual(len(second_pack.quant_ids), 2)
+ packing_picking.action_assign()
+ self.assertEqual(len(packing_picking.package_level_ids), 2, 'Two package levels must be created after assigning picking')
+ packing_picking.package_level_ids.write({'is_done': True})
+ packing_picking._action_done()
+
+ def test_pick_a_pack_confirm(self):
+ pack = self.env['stock.quant.package'].create({'name': 'The pack to pick'})
+ self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 20.0, package_id=pack)
+ picking = self.env['stock.picking'].create({
+ 'picking_type_id': self.warehouse.int_type_id.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'state': 'draft',
+ })
+ picking.picking_type_id.show_entire_packs = True
+ package_level = self.env['stock.package_level'].create({
+ 'package_id': pack.id,
+ 'picking_id': picking.id,
+ 'location_dest_id': self.stock_location.id,
+ 'company_id': picking.company_id.id,
+ })
+ self.assertEqual(package_level.state, 'draft',
+ 'The package_level should be in draft as it has no moves, move lines and is not confirmed')
+ picking.action_confirm()
+ self.assertEqual(len(picking.move_ids_without_package), 0)
+ self.assertEqual(len(picking.move_lines), 1,
+ 'One move should be created when the package_level has been confirmed')
+ self.assertEqual(len(package_level.move_ids), 1,
+ 'The move should be in the package level')
+ self.assertEqual(package_level.state, 'confirmed',
+ 'The package level must be state confirmed when picking is confirmed')
+ picking.action_assign()
+ self.assertEqual(len(picking.move_lines), 1,
+ 'You still have only one move when the picking is assigned')
+ self.assertEqual(len(picking.move_lines.move_line_ids), 1,
+ 'The move should have one move line which is the reservation')
+ self.assertEqual(picking.move_line_ids.package_level_id.id, package_level.id,
+ 'The move line created should be linked to the package level')
+ self.assertEqual(picking.move_line_ids.package_id.id, pack.id,
+ 'The move line must have been reserved on the package of the package_level')
+ self.assertEqual(picking.move_line_ids.result_package_id.id, pack.id,
+ 'The move line must have the same package as result package')
+ self.assertEqual(package_level.state, 'assigned', 'The package level must be in state assigned')
+ package_level.write({'is_done': True})
+ self.assertEqual(len(package_level.move_line_ids), 1,
+ 'The package level should still keep one move line after have been set to "done"')
+ self.assertEqual(package_level.move_line_ids[0].qty_done, 20.0,
+ 'All quantity in package must be procesed in move line')
+ picking.button_validate()
+ self.assertEqual(len(picking.move_lines), 1,
+ 'You still have only one move when the picking is assigned')
+ self.assertEqual(len(picking.move_lines.move_line_ids), 1,
+ 'The move should have one move line which is the reservation')
+ self.assertEqual(package_level.state, 'done', 'The package level must be in state done')
+ self.assertEqual(pack.location_id.id, picking.location_dest_id.id,
+ 'The quant package must be in the destination location')
+ self.assertEqual(pack.quant_ids[0].location_id.id, picking.location_dest_id.id,
+ 'The quant must be in the destination location')
+
+ def test_multi_pack_reservation(self):
+ """ When we move entire packages, it is possible to have a multiple times
+ the same package in package level list, we make sure that only one is reserved,
+ and that the location_id of the package is the one where the package is once it
+ is reserved.
+ """
+ pack = self.env['stock.quant.package'].create({'name': 'The pack to pick'})
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env['stock.quant']._update_available_quantity(self.productA, shelf1_location, 20.0, package_id=pack)
+ picking = self.env['stock.picking'].create({
+ 'picking_type_id': self.warehouse.int_type_id.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'state': 'draft',
+ })
+ package_level = self.env['stock.package_level'].create({
+ 'package_id': pack.id,
+ 'picking_id': picking.id,
+ 'location_dest_id': self.stock_location.id,
+ 'company_id': picking.company_id.id,
+ })
+ package_level = self.env['stock.package_level'].create({
+ 'package_id': pack.id,
+ 'picking_id': picking.id,
+ 'location_dest_id': self.stock_location.id,
+ 'company_id': picking.company_id.id,
+ })
+ picking.action_confirm()
+ self.assertEqual(picking.package_level_ids.mapped('location_id.id'), [shelf1_location.id],
+ 'The package levels should still in the same location after confirmation.')
+ picking.action_assign()
+ package_level_reserved = picking.package_level_ids.filtered(lambda pl: pl.state == 'assigned')
+ package_level_confirmed = picking.package_level_ids.filtered(lambda pl: pl.state == 'confirmed')
+ self.assertEqual(package_level_reserved.location_id.id, shelf1_location.id, 'The reserved package level must be reserved in shelf1')
+ self.assertEqual(package_level_confirmed.location_id.id, shelf1_location.id, 'The not reserved package should keep its location')
+ picking.do_unreserve()
+ self.assertEqual(picking.package_level_ids.mapped('location_id.id'), [shelf1_location.id],
+ 'The package levels should have back the original location.')
+ picking.package_level_ids.write({'is_done': True})
+ picking.action_assign()
+ package_level_reserved = picking.package_level_ids.filtered(lambda pl: pl.state == 'assigned')
+ package_level_confirmed = picking.package_level_ids.filtered(lambda pl: pl.state == 'confirmed')
+ self.assertEqual(package_level_reserved.location_id.id, shelf1_location.id, 'The reserved package level must be reserved in shelf1')
+ self.assertEqual(package_level_confirmed.location_id.id, shelf1_location.id, 'The not reserved package should keep its location')
+ self.assertEqual(picking.package_level_ids.mapped('is_done'), [True, True], 'Both package should still done')
+
+ def test_put_in_pack_to_different_location(self):
+ """ Hitting 'Put in pack' button while some move lines go to different
+ location should trigger a wizard. This wizard applies the same destination
+ location to all the move lines
+ """
+ self.warehouse.in_type_id.show_reserved = True
+ shelf1_location = self.env['stock.location'].create({
+ 'name': 'shelf1',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ shelf2_location = self.env['stock.location'].create({
+ 'name': 'shelf2',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ picking = self.env['stock.picking'].create({
+ 'picking_type_id': self.warehouse.in_type_id.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'state': 'draft',
+ })
+ ship_move_a = self.env['stock.move'].create({
+ 'name': 'move 1',
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 5.0,
+ 'product_uom': self.productA.uom_id.id,
+ 'location_id': self.customer_location.id,
+ 'location_dest_id': shelf1_location.id,
+ 'picking_id': picking.id,
+ 'state': 'draft',
+ })
+ picking.action_confirm()
+ picking.action_assign()
+ picking.move_line_ids.filtered(lambda ml: ml.product_id == self.productA).qty_done = 5.0
+ picking.action_put_in_pack()
+ pack1 = self.env['stock.quant.package'].search([])[-1]
+ picking.write({
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 7.0,
+ 'qty_done': 7.0,
+ 'product_uom_id': self.productB.uom_id.id,
+ 'location_id': self.customer_location.id,
+ 'location_dest_id': shelf2_location.id,
+ 'picking_id': picking.id,
+ 'state': 'confirmed',
+ })]
+ })
+ picking.write({
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 5.0,
+ 'qty_done': 5.0,
+ 'product_uom_id': self.productA.uom_id.id,
+ 'location_id': self.customer_location.id,
+ 'location_dest_id': shelf1_location.id,
+ 'picking_id': picking.id,
+ 'state': 'confirmed',
+ })]
+ })
+ wizard_values = picking.action_put_in_pack()
+ wizard = self.env[(wizard_values.get('res_model'))].browse(wizard_values.get('res_id'))
+ wizard.location_dest_id = shelf2_location.id
+ wizard.action_done()
+ picking._action_done()
+ pack2 = self.env['stock.quant.package'].search([])[-1]
+ self.assertEqual(pack2.location_id.id, shelf2_location.id, 'The package must be stored in shelf2')
+ self.assertEqual(pack1.location_id.id, shelf1_location.id, 'The package must be stored in shelf1')
+ qp1 = pack2.quant_ids[0]
+ qp2 = pack2.quant_ids[1]
+ self.assertEqual(qp1.quantity + qp2.quantity, 12, 'The quant has not the good quantity')
+
+ def test_move_picking_with_package(self):
+ """
+ 355.4 rounded with 0.01 precision is 355.40000000000003.
+ check that nonetheless, moving a picking is accepted
+ """
+ self.assertEqual(self.productA.uom_id.rounding, 0.01)
+ self.assertEqual(
+ float_round(355.4, precision_rounding=self.productA.uom_id.rounding),
+ 355.40000000000003,
+ )
+ location_dict = {
+ 'location_id': self.stock_location.id,
+ }
+ quant = self.env['stock.quant'].create({
+ **location_dict,
+ **{'product_id': self.productA.id, 'quantity': 355.4}, # important number
+ })
+ package = self.env['stock.quant.package'].create({
+ **location_dict, **{'quant_ids': [(6, 0, [quant.id])]},
+ })
+ location_dict.update({
+ 'state': 'draft',
+ 'location_dest_id': self.ship_location.id,
+ })
+ move = self.env['stock.move'].create({
+ **location_dict,
+ **{
+ 'name': "XXX",
+ 'product_id': self.productA.id,
+ 'product_uom': self.productA.uom_id.id,
+ 'product_uom_qty': 355.40000000000003, # other number
+ }})
+ picking = self.env['stock.picking'].create({
+ **location_dict,
+ **{
+ 'picking_type_id': self.warehouse.in_type_id.id,
+ 'move_lines': [(6, 0, [move.id])],
+ }})
+
+ picking.action_confirm()
+ picking.action_assign()
+ move.quantity_done = move.reserved_availability
+ picking._action_done()
+ # if we managed to get there, there was not any exception
+ # complaining that 355.4 is not 355.40000000000003. Good job!
+
+ def test_move_picking_with_package_2(self):
+ """ Generate two move lines going to different location in the same
+ package.
+ """
+ shelf1 = self.env['stock.location'].create({
+ 'location_id': self.stock_location.id,
+ 'name': 'Shelf 1',
+ })
+ shelf2 = self.env['stock.location'].create({
+ 'location_id': self.stock_location.id,
+ 'name': 'Shelf 2',
+ })
+ package = self.env['stock.quant.package'].create({})
+
+ picking = self.env['stock.picking'].create({
+ 'picking_type_id': self.warehouse.in_type_id.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'state': 'draft',
+ })
+ self.env['stock.move.line'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': shelf1.id,
+ 'product_id': self.productA.id,
+ 'product_uom_id': self.productA.uom_id.id,
+ 'qty_done': 5.0,
+ 'picking_id': picking.id,
+ 'result_package_id': package.id,
+ })
+ self.env['stock.move.line'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': shelf2.id,
+ 'product_id': self.productA.id,
+ 'product_uom_id': self.productA.uom_id.id,
+ 'qty_done': 5.0,
+ 'picking_id': picking.id,
+ 'result_package_id': package.id,
+ })
+ picking.action_confirm()
+ with self.assertRaises(UserError):
+ picking._action_done()
+
+ def test_pack_in_receipt_two_step_single_putway(self):
+ """ Checks all works right in the following specific corner case:
+
+ * For a two-step receipt, receives two products using the same putaway
+ * Puts these products in a package then valid the receipt.
+ * Cancels the automatically generated internal transfer then create a new one.
+ * In this internal transfer, adds the package then valid it.
+ """
+ grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
+ grp_multi_step_rule = self.env.ref('stock.group_adv_location')
+ grp_pack = self.env.ref('stock.group_tracking_lot')
+ self.env.user.write({'groups_id': [(3, grp_multi_loc.id)]})
+ self.env.user.write({'groups_id': [(3, grp_multi_step_rule.id)]})
+ self.env.user.write({'groups_id': [(3, grp_pack.id)]})
+ self.warehouse.reception_steps = 'two_steps'
+ # Settings of receipt.
+ self.warehouse.in_type_id.show_operations = True
+ self.warehouse.in_type_id.show_entire_packs = True
+ self.warehouse.in_type_id.show_reserved = True
+ # Settings of internal transfer.
+ self.warehouse.int_type_id.show_operations = True
+ self.warehouse.int_type_id.show_entire_packs = True
+ self.warehouse.int_type_id.show_reserved = True
+
+ # Creates two new locations for putaway.
+ location_form = Form(self.env['stock.location'])
+ location_form.name = 'Shelf A'
+ location_form.location_id = self.stock_location
+ loc_shelf_A = location_form.save()
+
+ # Creates a new putaway rule for productA and productB.
+ putaway_A = self.env['stock.putaway.rule'].create({
+ 'product_id': self.productA.id,
+ 'location_in_id': self.stock_location.id,
+ 'location_out_id': loc_shelf_A.id,
+ })
+ putaway_B = self.env['stock.putaway.rule'].create({
+ 'product_id': self.productB.id,
+ 'location_in_id': self.stock_location.id,
+ 'location_out_id': loc_shelf_A.id,
+ })
+ self.stock_location.putaway_rule_ids = [(4, putaway_A.id, 0), (4, putaway_B.id, 0)]
+
+ # Create a new receipt with the two products.
+ receipt_form = Form(self.env['stock.picking'])
+ receipt_form.picking_type_id = self.warehouse.in_type_id
+ # Add 2 lines
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.productA
+ move_line.product_uom_qty = 1
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.productB
+ move_line.product_uom_qty = 1
+ receipt = receipt_form.save()
+ receipt.action_confirm()
+
+ # Adds quantities then packs them and valids the receipt.
+ receipt_form = Form(receipt)
+ with receipt_form.move_line_ids_without_package.edit(0) as move_line:
+ move_line.qty_done = 1
+ with receipt_form.move_line_ids_without_package.edit(1) as move_line:
+ move_line.qty_done = 1
+ receipt = receipt_form.save()
+ receipt.action_put_in_pack()
+ receipt.button_validate()
+
+ receipt_package = receipt.package_level_ids_details[0]
+ self.assertEqual(receipt_package.location_dest_id.id, receipt.location_dest_id.id)
+ self.assertEqual(
+ receipt_package.move_line_ids[0].location_dest_id.id,
+ receipt.location_dest_id.id)
+ self.assertEqual(
+ receipt_package.move_line_ids[1].location_dest_id.id,
+ receipt.location_dest_id.id)
+
+ # Checks an internal transfer was created following the validation of the receipt.
+ internal_transfer = self.env['stock.picking'].search([
+ ('picking_type_id', '=', self.warehouse.int_type_id.id)
+ ], order='id desc', limit=1)
+ self.assertEqual(internal_transfer.origin, receipt.name)
+ self.assertEqual(
+ len(internal_transfer.package_level_ids_details), 1)
+ internal_package = internal_transfer.package_level_ids_details[0]
+ self.assertNotEqual(
+ internal_package.location_dest_id.id,
+ internal_transfer.location_dest_id.id)
+ self.assertEqual(
+ internal_package.location_dest_id.id,
+ putaway_A.location_out_id.id,
+ "The package destination location must be the one from the putaway.")
+ self.assertEqual(
+ internal_package.move_line_ids[0].location_dest_id.id,
+ putaway_A.location_out_id.id,
+ "The move line destination location must be the one from the putaway.")
+ self.assertEqual(
+ internal_package.move_line_ids[1].location_dest_id.id,
+ putaway_A.location_out_id.id,
+ "The move line destination location must be the one from the putaway.")
+
+ # Cancels the internal transfer and creates a new one.
+ internal_transfer.action_cancel()
+ internal_form = Form(self.env['stock.picking'])
+ internal_form.picking_type_id = self.warehouse.int_type_id
+ internal_form.location_id = self.warehouse.wh_input_stock_loc_id
+ with internal_form.package_level_ids_details.new() as pack_line:
+ pack_line.package_id = receipt_package.package_id
+ internal_transfer = internal_form.save()
+
+ # Checks the package fields have been correctly set.
+ internal_package = internal_transfer.package_level_ids_details[0]
+ self.assertEqual(
+ internal_package.location_dest_id.id,
+ internal_transfer.location_dest_id.id)
+ internal_transfer.action_assign()
+ self.assertNotEqual(
+ internal_package.location_dest_id.id,
+ internal_transfer.location_dest_id.id)
+ self.assertEqual(
+ internal_package.location_dest_id.id,
+ putaway_A.location_out_id.id,
+ "The package destination location must be the one from the putaway.")
+ self.assertEqual(
+ internal_package.move_line_ids[0].location_dest_id.id,
+ putaway_A.location_out_id.id,
+ "The move line destination location must be the one from the putaway.")
+ self.assertEqual(
+ internal_package.move_line_ids[1].location_dest_id.id,
+ putaway_A.location_out_id.id,
+ "The move line destination location must be the one from the putaway.")
+ internal_transfer.button_validate()
+
+ def test_pack_in_receipt_two_step_multi_putaway(self):
+ """ Checks all works right in the following specific corner case:
+
+ * For a two-step receipt, receives two products using two putaways
+ targeting different locations.
+ * Puts these products in a package then valid the receipt.
+ * Cancels the automatically generated internal transfer then create a new one.
+ * In this internal transfer, adds the package then valid it.
+ """
+ grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
+ grp_multi_step_rule = self.env.ref('stock.group_adv_location')
+ grp_pack = self.env.ref('stock.group_tracking_lot')
+ self.env.user.write({'groups_id': [(3, grp_multi_loc.id)]})
+ self.env.user.write({'groups_id': [(3, grp_multi_step_rule.id)]})
+ self.env.user.write({'groups_id': [(3, grp_pack.id)]})
+ self.warehouse.reception_steps = 'two_steps'
+ # Settings of receipt.
+ self.warehouse.in_type_id.show_operations = True
+ self.warehouse.in_type_id.show_entire_packs = True
+ self.warehouse.in_type_id.show_reserved = True
+ # Settings of internal transfer.
+ self.warehouse.int_type_id.show_operations = True
+ self.warehouse.int_type_id.show_entire_packs = True
+ self.warehouse.int_type_id.show_reserved = True
+
+ # Creates two new locations for putaway.
+ location_form = Form(self.env['stock.location'])
+ location_form.name = 'Shelf A'
+ location_form.location_id = self.stock_location
+ loc_shelf_A = location_form.save()
+ location_form = Form(self.env['stock.location'])
+ location_form.name = 'Shelf B'
+ location_form.location_id = self.stock_location
+ loc_shelf_B = location_form.save()
+
+ # Creates a new putaway rule for productA and productB.
+ putaway_A = self.env['stock.putaway.rule'].create({
+ 'product_id': self.productA.id,
+ 'location_in_id': self.stock_location.id,
+ 'location_out_id': loc_shelf_A.id,
+ })
+ putaway_B = self.env['stock.putaway.rule'].create({
+ 'product_id': self.productB.id,
+ 'location_in_id': self.stock_location.id,
+ 'location_out_id': loc_shelf_B.id,
+ })
+ self.stock_location.putaway_rule_ids = [(4, putaway_A.id, 0), (4, putaway_B.id, 0)]
+ # location_form = Form(self.stock_location)
+ # location_form.putaway_rule_ids = [(4, putaway_A.id, 0), (4, putaway_B.id, 0), ],
+ # self.stock_location = location_form.save()
+
+ # Create a new receipt with the two products.
+ receipt_form = Form(self.env['stock.picking'])
+ receipt_form.picking_type_id = self.warehouse.in_type_id
+ # Add 2 lines
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.productA
+ move_line.product_uom_qty = 1
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.productB
+ move_line.product_uom_qty = 1
+ receipt = receipt_form.save()
+ receipt.action_confirm()
+
+ # Adds quantities then packs them and valids the receipt.
+ receipt_form = Form(receipt)
+ with receipt_form.move_line_ids_without_package.edit(0) as move_line:
+ move_line.qty_done = 1
+ with receipt_form.move_line_ids_without_package.edit(1) as move_line:
+ move_line.qty_done = 1
+ receipt = receipt_form.save()
+ receipt.action_put_in_pack()
+ receipt.button_validate()
+
+ receipt_package = receipt.package_level_ids_details[0]
+ self.assertEqual(receipt_package.location_dest_id.id, receipt.location_dest_id.id)
+ self.assertEqual(
+ receipt_package.move_line_ids[0].location_dest_id.id,
+ receipt.location_dest_id.id)
+ self.assertEqual(
+ receipt_package.move_line_ids[1].location_dest_id.id,
+ receipt.location_dest_id.id)
+
+ # Checks an internal transfer was created following the validation of the receipt.
+ internal_transfer = self.env['stock.picking'].search([
+ ('picking_type_id', '=', self.warehouse.int_type_id.id)
+ ], order='id desc', limit=1)
+ self.assertEqual(internal_transfer.origin, receipt.name)
+ self.assertEqual(
+ len(internal_transfer.package_level_ids_details), 1)
+ internal_package = internal_transfer.package_level_ids_details[0]
+ self.assertEqual(
+ internal_package.location_dest_id.id,
+ internal_transfer.location_dest_id.id)
+ self.assertNotEqual(
+ internal_package.location_dest_id.id,
+ putaway_A.location_out_id.id,
+ "The package destination location must be the one from the picking.")
+ self.assertNotEqual(
+ internal_package.move_line_ids[0].location_dest_id.id,
+ putaway_A.location_out_id.id,
+ "The move line destination location must be the one from the picking.")
+ self.assertNotEqual(
+ internal_package.move_line_ids[1].location_dest_id.id,
+ putaway_A.location_out_id.id,
+ "The move line destination location must be the one from the picking.")
+
+ # Cancels the internal transfer and creates a new one.
+ internal_transfer.action_cancel()
+ internal_form = Form(self.env['stock.picking'])
+ internal_form.picking_type_id = self.warehouse.int_type_id
+ internal_form.location_id = self.warehouse.wh_input_stock_loc_id
+ with internal_form.package_level_ids_details.new() as pack_line:
+ pack_line.package_id = receipt_package.package_id
+ internal_transfer = internal_form.save()
+
+ # Checks the package fields have been correctly set.
+ internal_package = internal_transfer.package_level_ids_details[0]
+ self.assertEqual(
+ internal_package.location_dest_id.id,
+ internal_transfer.location_dest_id.id)
+ internal_transfer.action_assign()
+ self.assertEqual(
+ internal_package.location_dest_id.id,
+ internal_transfer.location_dest_id.id)
+ self.assertNotEqual(
+ internal_package.location_dest_id.id,
+ putaway_A.location_out_id.id,
+ "The package destination location must be the one from the picking.")
+ self.assertNotEqual(
+ internal_package.move_line_ids[0].location_dest_id.id,
+ putaway_A.location_out_id.id,
+ "The move line destination location must be the one from the picking.")
+ self.assertNotEqual(
+ internal_package.move_line_ids[1].location_dest_id.id,
+ putaway_A.location_out_id.id,
+ "The move line destination location must be the one from the picking.")
+ internal_transfer.button_validate()
+
+ def test_partial_put_in_pack(self):
+ """ Create a simple move in a delivery. Reserve the quantity but set as quantity done only a part.
+ Call Put In Pack button. """
+ self.productA.tracking = 'lot'
+ lot1 = self.env['stock.production.lot'].create({
+ 'product_id': self.productA.id,
+ 'name': '00001',
+ 'company_id': self.warehouse.company_id.id
+ })
+ self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 20.0, lot_id=lot1)
+ ship_move_a = self.env['stock.move'].create({
+ 'name': 'The ship move',
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 5.0,
+ 'product_uom': self.productA.uom_id.id,
+ 'location_id': self.ship_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'warehouse_id': self.warehouse.id,
+ 'picking_type_id': self.warehouse.out_type_id.id,
+ 'procure_method': 'make_to_order',
+ 'state': 'draft',
+ })
+ ship_move_a._assign_picking()
+ ship_move_a._action_confirm()
+ pack_move_a = ship_move_a.move_orig_ids[0]
+ pick_move_a = pack_move_a.move_orig_ids[0]
+
+ pick_picking = pick_move_a.picking_id
+
+ pick_picking.picking_type_id.show_entire_packs = True
+
+ pick_picking.action_assign()
+
+ pick_picking.move_line_ids.qty_done = 3
+ first_pack = pick_picking.action_put_in_pack()
+
+ def test_action_assign_package_level(self):
+ """calling _action_assign on move does not erase lines' "result_package_id"
+ At the end of the method ``StockMove._action_assign()``, the method
+ ``StockPicking._check_entire_pack()`` is called. This method compares
+ the move lines with the quants of their source package, and if the entire
+ package is moved at once in the same transfer, a ``stock.package_level`` is
+ created. On creation of a ``stock.package_level``, the result package of
+ the move lines is directly updated with the entire package.
+ This is good on the first assign of the move, but when we call assign for
+ the second time on a move, for instance because it was made partially available
+ and we want to assign the remaining, it can override the result package we
+ selected before.
+ An override of ``StockPicking._check_move_lines_map_quant_package()`` ensures
+ that we ignore:
+ * picked lines (qty_done > 0)
+ * lines with a different result package already
+ """
+ package = self.env["stock.quant.package"].create({"name": "Src Pack"})
+ dest_package1 = self.env["stock.quant.package"].create({"name": "Dest Pack1"})
+
+ # Create new picking: 120 productA
+ picking_form = Form(self.env['stock.picking'])
+ picking_form.picking_type_id = self.warehouse.pick_type_id
+ with picking_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.productA
+ move_line.product_uom_qty = 120
+ picking = picking_form.save()
+
+ # mark as TO-DO
+ picking.action_confirm()
+
+ # Update quantity on hand: 100 units in package
+ self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 100, package_id=package)
+
+ # Check Availability
+ picking.action_assign()
+
+ self.assertEqual(picking.state, "assigned")
+ self.assertEqual(picking.package_level_ids.package_id, package)
+
+ move = picking.move_lines
+ line = move.move_line_ids
+
+ # change the result package and set a qty_done
+ line.qty_done = 100
+ line.result_package_id = dest_package1
+
+ # Update quantity on hand: 20 units in new_package
+ new_package = self.env["stock.quant.package"].create({"name": "New Pack"})
+ self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 20, package_id=new_package)
+
+ # Check Availability
+ picking.action_assign()
+
+ # Check that result package is not changed on first line
+ new_line = move.move_line_ids - line
+ self.assertRecordValues(
+ line + new_line,
+ [
+ {"qty_done": 100, "result_package_id": dest_package1.id},
+ {"qty_done": 0, "result_package_id": new_package.id},
+ ],
+ )
+
+ def test_entire_pack_overship(self):
+ """
+ Test the scenario of overshipping: we send the customer an entire package, even though it might be more than
+ what they initially ordered, and update the quantity on the sales order to reflect what was actually sent.
+ """
+ self.warehouse.delivery_steps = 'ship_only'
+ package = self.env["stock.quant.package"].create({"name": "Src Pack"})
+ self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 100, package_id=package)
+ self.warehouse.out_type_id.show_entire_packs = True
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.warehouse.out_type_id.id,
+ })
+ with Form(picking) as picking_form:
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = self.productA
+ move.product_uom_qty = 75
+ picking.action_confirm()
+ picking.action_assign()
+ with Form(picking) as picking_form:
+ with picking_form.package_level_ids_details.new() as package_level:
+ package_level.package_id = package
+ self.assertEqual(len(picking.move_lines), 1, 'Should have only 1 stock move')
+ self.assertEqual(len(picking.move_lines), 1, 'Should have only 1 stock move')
+ with Form(picking) as picking_form:
+ with picking_form.package_level_ids_details.edit(0) as package_level:
+ package_level.is_done = True
+ action = picking.button_validate()
+
+ self.assertEqual(action, True, 'Should not open wizard')
+
+ for ml in picking.move_line_ids:
+ self.assertEqual(ml.package_id, package, 'move_line.package')
+ self.assertEqual(ml.result_package_id, package, 'move_line.result_package')
+ self.assertEqual(ml.state, 'done', 'move_line.state')
+ quant = package.quant_ids.filtered(lambda q: q.location_id == self.customer_location)
+ self.assertEqual(len(quant), 1, 'Should have quant at customer location')
+ self.assertEqual(quant.reserved_quantity, 0, 'quant.reserved_quantity should = 0')
+ self.assertEqual(quant.quantity, 100.0, 'quant.quantity should = 100')
+ self.assertEqual(sum(ml.qty_done for ml in picking.move_line_ids), 100.0, 'total move_line.qty_done should = 100')
+ backorders = self.env['stock.picking'].search([('backorder_id', '=', picking.id)])
+ self.assertEqual(len(backorders), 0, 'Should not create a backorder')
+
+ def test_remove_package(self):
+ """
+ In the overshipping scenario, if I remove the package after adding it, we should not remove the associated
+ stock move.
+ """
+ self.warehouse.delivery_steps = 'ship_only'
+ package = self.env["stock.quant.package"].create({"name": "Src Pack"})
+ self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 100, package_id=package)
+ self.warehouse.out_type_id.show_entire_packs = True
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'picking_type_id': self.warehouse.out_type_id.id,
+ })
+ with Form(picking) as picking_form:
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = self.productA
+ move.product_uom_qty = 75
+ picking.action_assign()
+ with Form(picking) as picking_form:
+ with picking_form.package_level_ids_details.new() as package_level:
+ package_level.package_id = package
+ with Form(picking) as picking_form:
+ picking_form.package_level_ids.remove(0)
+ self.assertEqual(len(picking.move_lines), 1, 'Should have only 1 stock move')
diff --git a/addons/stock/tests/test_packing_neg.py b/addons/stock/tests/test_packing_neg.py
new file mode 100644
index 00000000..a42d33ed
--- /dev/null
+++ b/addons/stock/tests/test_packing_neg.py
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests.common import TransactionCase
+
+
+class TestPackingNeg(TransactionCase):
+
+ def test_packing_neg(self):
+ res_partner_2 = self.env['res.partner'].create({
+ 'name': 'Deco Addict',
+ 'email': 'deco.addict82@example.com',
+ })
+
+ res_partner_4 = self.env['res.partner'].create({
+ 'name': 'Ready Mat',
+ 'email': 'ready.mat28@example.com',
+ })
+
+ # Create a new "negative" storable product
+ product_neg = self.env['product.product'].create({
+ 'name': 'Negative product',
+ 'type': 'product',
+ 'categ_id': self.ref('product.product_category_1'),
+ 'list_price': 100.0,
+ 'standard_price': 70.0,
+ 'seller_ids': [(0, 0, {
+ 'delay': 1,
+ 'name': res_partner_2.id,
+ 'min_qty': 2.0,})],
+ 'uom_id': self.ref('uom.product_uom_unit'),
+ 'uom_po_id': self.ref('uom.product_uom_unit'),
+ })
+
+ # Create an incoming picking for this product of 300 PCE from suppliers to stock
+ vals = {
+ 'name': 'Incoming picking (negative product)',
+ 'partner_id': res_partner_2.id,
+ 'picking_type_id': self.ref('stock.picking_type_in'),
+ 'location_id': self.ref('stock.stock_location_suppliers'),
+ 'location_dest_id': self.ref('stock.stock_location_stock'),
+ 'move_lines': [(0, 0, {
+ 'name': 'NEG',
+ 'product_id': product_neg.id,
+ 'product_uom': product_neg.uom_id.id,
+ 'product_uom_qty': 300.00,
+ 'location_id': self.ref('stock.stock_location_suppliers'),
+ 'location_dest_id': self.ref('stock.stock_location_stock'),
+ })],
+ }
+ pick_neg = self.env['stock.picking'].create(vals)
+ pick_neg.onchange_picking_type()
+ pick_neg.move_lines.onchange_product_id()
+
+ # Confirm and assign picking
+ pick_neg.action_confirm()
+ pick_neg.action_assign()
+
+ # Put 120 pieces on Palneg 1 (package), 120 pieces on Palneg 2 with lot A and 60 pieces on Palneg 3
+ # create lot A
+ lot_a = self.env['stock.production.lot'].create({'name': 'Lot neg', 'product_id': product_neg.id, 'company_id': self.env.company.id})
+ # create package
+ package1 = self.env['stock.quant.package'].create({'name': 'Palneg 1'})
+ package2 = self.env['stock.quant.package'].create({'name': 'Palneg 2'})
+ package3 = self.env['stock.quant.package'].create({'name': 'Palneg 3'})
+ # Create package for each line and assign it as result_package_id
+ # create pack operation
+ pick_neg.move_line_ids[0].write({'result_package_id': package1.id, 'qty_done': 120})
+ new_pack1 = self.env['stock.move.line'].create({
+ 'product_id': product_neg.id,
+ 'product_uom_id': self.ref('uom.product_uom_unit'),
+ 'picking_id': pick_neg.id,
+ 'lot_id': lot_a.id,
+ 'qty_done': 120,
+ 'result_package_id': package2.id,
+ 'location_id': self.ref('stock.stock_location_suppliers'),
+ 'location_dest_id': self.ref('stock.stock_location_stock')
+ })
+ new_pack2 = self.env['stock.move.line'].create({
+ 'product_id': product_neg.id,
+ 'product_uom_id': self.ref('uom.product_uom_unit'),
+ 'picking_id': pick_neg.id,
+ 'result_package_id': package3.id,
+ 'qty_done': 60,
+ 'location_id': self.ref('stock.stock_location_suppliers'),
+ 'location_dest_id': self.ref('stock.stock_location_stock')
+ })
+
+ # Transfer the receipt
+ pick_neg._action_done()
+
+ # Make a delivery order of 300 pieces to the customer
+ vals = {
+ 'name': 'outgoing picking (negative product)',
+ 'partner_id': res_partner_4.id,
+ 'picking_type_id': self.ref('stock.picking_type_out'),
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'move_lines': [(0, 0, {
+ 'name': 'NEG',
+ 'product_id': product_neg.id,
+ 'product_uom': product_neg.uom_id.id,
+ 'product_uom_qty': 300.00,
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ })],
+ }
+ delivery_order_neg = self.env['stock.picking'].create(vals)
+ delivery_order_neg.onchange_picking_type()
+ delivery_order_neg.move_lines.onchange_product_id()
+
+ # Assign and confirm
+ delivery_order_neg.action_confirm()
+ delivery_order_neg.action_assign()
+
+ # Instead of doing the 300 pieces, you decide to take pallet 1 (do not mention
+ # product in operation here) and 140 pieces from lot A/pallet 2 and 10 pieces from pallet 3
+
+ for rec in delivery_order_neg.move_line_ids:
+ if rec.package_id.name == 'Palneg 1':
+ rec.qty_done = rec.product_qty
+ rec.result_package_id = False
+ elif rec.package_id.name == 'Palneg 2' and rec.lot_id.name == 'Lot neg':
+ rec.write({
+ 'qty_done': 140,
+ 'result_package_id': False,
+ })
+ elif rec.package_id.name == 'Palneg 3':
+ rec.qty_done = 10
+ rec.result_package_id = False
+
+ # Process this picking
+ delivery_order_neg._action_done()
+
+ # Check the quants that you have -20 pieces pallet 2 in stock, and a total quantity
+ # of 50 in stock from pallet 3 (should be 20+30, as it has been split by reservation)
+ records = self.env['stock.quant'].search([('product_id', '=', product_neg.id), ('quantity', '!=', '0')])
+ pallet_3_stock_qty = 0
+ for rec in records:
+ if rec.package_id.name == 'Palneg 2' and rec.location_id.id == self.ref('stock.stock_location_stock'):
+ self.assertTrue(rec.quantity == -20, "Should have -20 pieces in stock on pallet 2. Got " + str(rec.quantity))
+ self.assertTrue(rec.lot_id.name == 'Lot neg', "It should have kept its Lot")
+ elif rec.package_id.name == 'Palneg 3' and rec.location_id.id == self.ref('stock.stock_location_stock'):
+ pallet_3_stock_qty += rec.quantity
+ else:
+ self.assertTrue(rec.location_id.id != self.ref('stock.stock_location_stock'), "Unrecognized quant in stock")
+ self.assertEqual(pallet_3_stock_qty, 50, "Should have 50 pieces in stock on pallet 3")
+
+ # Create a picking for reconciling the negative quant
+ vals = {
+ 'name': 'reconciling_delivery',
+ 'partner_id': res_partner_4.id,
+ 'picking_type_id': self.ref('stock.picking_type_in'),
+ 'location_id': self.ref('stock.stock_location_suppliers'),
+ 'location_dest_id': self.ref('stock.stock_location_stock'),
+ 'move_lines': [(0, 0, {
+ 'name': 'NEG',
+ 'product_id': product_neg.id,
+ 'product_uom': product_neg.uom_id.id,
+ 'product_uom_qty': 20.0,
+ 'location_id': self.ref('stock.stock_location_suppliers'),
+ 'location_dest_id': self.ref('stock.stock_location_stock'),
+ })],
+ }
+ delivery_reconcile = self.env['stock.picking'].create(vals)
+ delivery_reconcile.onchange_picking_type()
+ delivery_reconcile.move_lines.onchange_product_id()
+
+ # Receive 20 products with lot neg in stock with a new incoming shipment that should be on pallet 2
+ delivery_reconcile.action_confirm()
+ lot = self.env["stock.production.lot"].search([
+ ('product_id', '=', product_neg.id),
+ ('name', '=', 'Lot neg')], limit=1)
+ pack = self.env["stock.quant.package"].search([('name', '=', 'Palneg 2')], limit=1)
+ delivery_reconcile.move_line_ids[0].write({'lot_id': lot.id, 'qty_done': 20.0, 'result_package_id': pack.id})
+ delivery_reconcile._action_done()
+
+ # Check the negative quant was reconciled
+ neg_quants = self.env['stock.quant'].search([
+ ('product_id', '=', product_neg.id),
+ ('quantity', '<', 0),
+ ('location_id.id', '!=', self.ref('stock.stock_location_suppliers'))])
+ self.assertTrue(len(neg_quants) == 0, "Negative quants should have been reconciled")
diff --git a/addons/stock/tests/test_proc_rule.py b/addons/stock/tests/test_proc_rule.py
new file mode 100644
index 00000000..85d52d2e
--- /dev/null
+++ b/addons/stock/tests/test_proc_rule.py
@@ -0,0 +1,410 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import date, datetime, timedelta
+
+from odoo.tests.common import Form, TransactionCase
+from odoo.tools import mute_logger
+
+
+class TestProcRule(TransactionCase):
+
+ def setUp(self):
+ super(TestProcRule, self).setUp()
+
+ self.uom_unit = self.env.ref('uom.product_uom_unit')
+ self.product = self.env['product.product'].create({
+ 'name': 'Desk Combination',
+ 'type': 'consu',
+ })
+ self.partner = self.env['res.partner'].create({'name': 'Partner'})
+
+ def test_proc_rule(self):
+ # Create a product route containing a stock rule that will
+ # generate a move from Stock for every procurement created in Output
+ product_route = self.env['stock.location.route'].create({
+ 'name': 'Stock -> output route',
+ 'product_selectable': True,
+ 'rule_ids': [(0, 0, {
+ 'name': 'Stock -> output rule',
+ 'action': 'pull',
+ 'picking_type_id': self.ref('stock.picking_type_internal'),
+ 'location_src_id': self.ref('stock.stock_location_stock'),
+ 'location_id': self.ref('stock.stock_location_output'),
+ })],
+ })
+
+ # Set this route on `product.product_product_3`
+ self.product.write({
+ 'route_ids': [(4, product_route.id)]})
+
+ # Create Delivery Order of 10 `product.product_product_3` from Output -> Customer
+ product = self.product
+ vals = {
+ 'name': 'Delivery order for procurement',
+ 'partner_id': self.partner.id,
+ 'picking_type_id': self.ref('stock.picking_type_out'),
+ 'location_id': self.ref('stock.stock_location_output'),
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'move_lines': [(0, 0, {
+ 'name': '/',
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 10.00,
+ 'procure_method': 'make_to_order',
+ })],
+ }
+ pick_output = self.env['stock.picking'].create(vals)
+ pick_output.move_lines.onchange_product_id()
+
+ # Confirm delivery order.
+ pick_output.action_confirm()
+
+ # I run the scheduler.
+ # Note: If purchase if already installed, the method _run_buy will be called due
+ # to the purchase demo data. As we update the stock module to run this test, the
+ # method won't be an attribute of stock.procurement at this moment. For that reason
+ # we mute the logger when running the scheduler.
+ with mute_logger('odoo.addons.stock.models.procurement'):
+ self.env['procurement.group'].run_scheduler()
+
+ # Check that a picking was created from stock to output.
+ moves = self.env['stock.move'].search([
+ ('product_id', '=', self.product.id),
+ ('location_id', '=', self.ref('stock.stock_location_stock')),
+ ('location_dest_id', '=', self.ref('stock.stock_location_output')),
+ ('move_dest_ids', 'in', [pick_output.move_lines[0].id])
+ ])
+ self.assertEqual(len(moves.ids), 1, "It should have created a picking from Stock to Output with the original picking as destination")
+
+ def test_propagate_deadline_move(self):
+ deadline = datetime.now()
+ move_dest = self.env['stock.move'].create({
+ 'name': 'move_dest',
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'date_deadline': deadline,
+ 'location_id': self.ref('stock.stock_location_output'),
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ })
+
+ move_orig = self.env['stock.move'].create({
+ 'name': 'move_orig',
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'date_deadline': deadline,
+ 'move_dest_ids': [(4, move_dest.id)],
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_output'),
+ 'quantity_done': 10,
+ })
+ new_deadline = move_orig.date_deadline - timedelta(days=6)
+ move_orig.date_deadline = new_deadline
+ self.assertEqual(move_dest.date_deadline, new_deadline, msg='deadline date should be propagated')
+ move_orig._action_done()
+ self.assertAlmostEqual(move_orig.date, datetime.now(), delta=timedelta(seconds=10), msg='date should be now')
+ self.assertEqual(move_orig.date_deadline, new_deadline, msg='deadline date should be unchanged')
+ self.assertEqual(move_dest.date_deadline, new_deadline, msg='deadline date should be unchanged')
+
+ def test_reordering_rule_1(self):
+ warehouse = self.env['stock.warehouse'].search([], limit=1)
+ orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
+ orderpoint_form.product_id = self.product
+ orderpoint_form.location_id = warehouse.lot_stock_id
+ orderpoint_form.product_min_qty = 0.0
+ orderpoint_form.product_max_qty = 5.0
+ orderpoint = orderpoint_form.save()
+
+ # get auto-created pull rule from when warehouse is created
+ rule = self.env['stock.rule'].search([
+ ('route_id', '=', warehouse.reception_route_id.id),
+ ('location_id', '=', warehouse.lot_stock_id.id),
+ ('location_src_id', '=', self.env.ref('stock.stock_location_suppliers').id),
+ ('action', '=', 'pull'),
+ ('procure_method', '=', 'make_to_stock'),
+ ('picking_type_id', '=', warehouse.in_type_id.id)])
+
+ # add a delay [i.e. lead days] so procurement will be triggered based on forecasted stock
+ rule.delay = 9.0
+
+ delivery_move = self.env['stock.move'].create({
+ 'name': 'Delivery',
+ 'date': datetime.today() + timedelta(days=5),
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 12.0,
+ 'location_id': warehouse.lot_stock_id.id,
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ })
+ delivery_move._action_confirm()
+ orderpoint._compute_qty()
+ self.env['procurement.group'].run_scheduler()
+
+ receipt_move = self.env['stock.move'].search([
+ ('product_id', '=', self.product.id),
+ ('location_id', '=', self.env.ref('stock.stock_location_suppliers').id)
+ ])
+ self.assertTrue(receipt_move)
+ self.assertEqual(receipt_move.date.date(), date.today())
+ self.assertEqual(receipt_move.product_uom_qty, 17.0)
+
+ def test_reordering_rule_2(self):
+ """Test when there is not enough product to assign a picking => automatically run
+ reordering rule (RR). Add extra product to already confirmed picking => automatically
+ run another RR
+ """
+ self.productA = self.env['product.product'].create({
+ 'name': 'Desk Combination',
+ 'type': 'product',
+ })
+
+ self.productB = self.env['product.product'].create({
+ 'name': 'Desk Decoration',
+ 'type': 'product',
+ })
+
+ warehouse = self.env['stock.warehouse'].search([], limit=1)
+ orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
+ orderpoint_form.product_id = self.productA
+ orderpoint_form.location_id = warehouse.lot_stock_id
+ orderpoint_form.product_min_qty = 0.0
+ orderpoint_form.product_max_qty = 5.0
+ orderpoint = orderpoint_form.save()
+
+ self.env['stock.warehouse.orderpoint'].create({
+ 'name': 'ProductB RR',
+ 'location_id': warehouse.lot_stock_id.id,
+ 'product_id': self.productB.id,
+ 'product_min_qty': 0,
+ 'product_max_qty': 5,
+ })
+
+ self.env['stock.rule'].create({
+ 'name': 'Rule Supplier',
+ 'route_id': warehouse.reception_route_id.id,
+ 'location_id': warehouse.lot_stock_id.id,
+ 'location_src_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'action': 'pull',
+ 'delay': 9.0,
+ 'procure_method': 'make_to_stock',
+ 'picking_type_id': warehouse.in_type_id.id,
+ })
+
+ delivery_picking = self.env['stock.picking'].create({
+ 'location_id': warehouse.lot_stock_id.id,
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'picking_type_id': self.ref('stock.picking_type_out'),
+ })
+ delivery_move = self.env['stock.move'].create({
+ 'name': 'Delivery',
+ 'product_id': self.productA.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 12.0,
+ 'location_id': warehouse.lot_stock_id.id,
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'picking_id': delivery_picking.id,
+ })
+ delivery_picking.action_confirm()
+ delivery_picking.action_assign()
+
+ receipt_move = self.env['stock.move'].search([
+ ('product_id', '=', self.productA.id),
+ ('location_id', '=', self.env.ref('stock.stock_location_suppliers').id)
+ ])
+
+ self.assertTrue(receipt_move)
+ self.assertEqual(receipt_move.date.date(), date.today())
+ self.assertEqual(receipt_move.product_uom_qty, 17.0)
+
+ delivery_picking.write({'move_lines': [(0, 0, {
+ 'name': 'Extra Move',
+ 'product_id': self.productB.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ 'location_id': warehouse.lot_stock_id.id,
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'picking_id': delivery_picking.id,
+ 'additional': True
+ })]})
+
+ receipt_move2 = self.env['stock.move'].search([
+ ('product_id', '=', self.productB.id),
+ ('location_id', '=', self.env.ref('stock.stock_location_suppliers').id)
+ ])
+
+ self.assertTrue(receipt_move2)
+ self.assertEqual(receipt_move2.date.date(), date.today())
+ self.assertEqual(receipt_move2.product_uom_qty, 10.0)
+
+ def test_fixed_procurement_01(self):
+ """ Run a procurement for 5 products when there are only 4 in stock then
+ check that MTO is applied on the moves when the rule is set to 'mts_else_mto'
+ """
+ self.partner = self.env['res.partner'].create({'name': 'Partner'})
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ warehouse.delivery_steps = 'pick_ship'
+ final_location = self.partner.property_stock_customer
+
+ # Create a product and add 10 units in stock
+ product_a = self.env['product.product'].create({
+ 'name': 'ProductA',
+ 'type': 'product',
+ })
+ self.env['stock.quant']._update_available_quantity(product_a, warehouse.lot_stock_id, 10.0)
+
+ # Create a route which will allows 'wave picking'
+ wave_pg = self.env['procurement.group'].create({'name': 'Wave PG'})
+ wave_route = self.env['stock.location.route'].create({
+ 'name': 'Wave for ProductA',
+ 'product_selectable': True,
+ 'sequence': 1,
+ 'rule_ids': [(0, 0, {
+ 'name': 'Stock -> output rule',
+ 'action': 'pull',
+ 'picking_type_id': self.ref('stock.picking_type_internal'),
+ 'location_src_id': self.ref('stock.stock_location_stock'),
+ 'location_id': self.ref('stock.stock_location_output'),
+ 'group_propagation_option': 'fixed',
+ 'group_id': wave_pg.id,
+ })],
+ })
+
+ # Set this route on `product_a`
+ product_a.write({
+ 'route_ids': [(4, wave_route.id)]
+ })
+
+ # Create a procurement for 2 units
+ pg = self.env['procurement.group'].create({'name': 'Wave 1'})
+ self.env['procurement.group'].run([
+ pg.Procurement(
+ product_a,
+ 2.0,
+ product_a.uom_id,
+ final_location,
+ 'wave_part_1',
+ 'wave_part_1',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg
+ }
+ )
+ ])
+
+ # 2 pickings should be created: 1 for pick, 1 for ship
+ picking_pick = self.env['stock.picking'].search([('group_id', '=', wave_pg.id)])
+ picking_ship = self.env['stock.picking'].search([('group_id', '=', pg.id)])
+ self.assertAlmostEqual(picking_pick.move_lines.product_uom_qty, 2.0)
+ self.assertAlmostEqual(picking_ship.move_lines.product_uom_qty, 2.0)
+
+ # Create a procurement for 3 units
+ pg = self.env['procurement.group'].create({'name': 'Wave 2'})
+ self.env['procurement.group'].run([
+ pg.Procurement(
+ product_a,
+ 3.0,
+ product_a.uom_id,
+ final_location,
+ 'wave_part_2',
+ 'wave_part_2',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg
+ }
+ )
+ ])
+
+ # The picking for the pick operation should be reused and the lines merged.
+ picking_ship = self.env['stock.picking'].search([('group_id', '=', pg.id)])
+ self.assertAlmostEqual(picking_pick.move_lines.product_uom_qty, 5.0)
+ self.assertAlmostEqual(picking_ship.move_lines.product_uom_qty, 3.0)
+
+
+class TestProcRuleLoad(TransactionCase):
+ def setUp(cls):
+ super(TestProcRuleLoad, cls).setUp()
+ cls.skipTest("Performance test, too heavy to run.")
+
+ def test_orderpoint_1(self):
+ """ Try 500 products with a 1000 RR(stock -> shelf1 and stock -> shelf2)
+ Also randomly include 4 miss configuration.
+ """
+ warehouse = self.env['stock.warehouse'].create({
+ 'name': 'Test Warehouse',
+ 'code': 'TWH'
+ })
+ warehouse.reception_steps = 'three_steps'
+ supplier_loc = self.env.ref('stock.stock_location_suppliers')
+ stock_loc = warehouse.lot_stock_id
+ shelf1 = self.env['stock.location'].create({
+ 'location_id': stock_loc.id,
+ 'usage': 'internal',
+ 'name': 'shelf1'
+ })
+ shelf2 = self.env['stock.location'].create({
+ 'location_id': stock_loc.id,
+ 'usage': 'internal',
+ 'name': 'shelf2'
+ })
+
+ products = self.env['product.product'].create([{'name': i, 'type': 'product'} for i in range(500)])
+ self.env['stock.warehouse.orderpoint'].create([{
+ 'product_id': products[i // 2].id,
+ 'location_id': (i % 2 == 0) and shelf1.id or shelf2.id,
+ 'warehouse_id': warehouse.id,
+ 'product_min_qty': 5,
+ 'product_max_qty': 10,
+ } for i in range(1000)])
+
+ self.env['stock.rule'].create({
+ 'name': 'Rule Shelf1',
+ 'route_id': warehouse.reception_route_id.id,
+ 'location_id': shelf1.id,
+ 'location_src_id': stock_loc.id,
+ 'action': 'pull',
+ 'procure_method': 'make_to_order',
+ 'picking_type_id': warehouse.int_type_id.id,
+ })
+ self.env['stock.rule'].create({
+ 'name': 'Rule Shelf2',
+ 'route_id': warehouse.reception_route_id.id,
+ 'location_id': shelf2.id,
+ 'location_src_id': stock_loc.id,
+ 'action': 'pull',
+ 'procure_method': 'make_to_order',
+ 'picking_type_id': warehouse.int_type_id.id,
+ })
+ self.env['stock.rule'].create({
+ 'name': 'Rule Supplier',
+ 'route_id': warehouse.reception_route_id.id,
+ 'location_id': warehouse.wh_input_stock_loc_id.id,
+ 'location_src_id': supplier_loc.id,
+ 'action': 'pull',
+ 'procure_method': 'make_to_stock',
+ 'picking_type_id': warehouse.in_type_id.id,
+ })
+
+ wrong_route = self.env['stock.location.route'].create({
+ 'name': 'Wrong Route',
+ })
+ self.env['stock.rule'].create({
+ 'name': 'Trap Rule',
+ 'route_id': wrong_route.id,
+ 'location_id': warehouse.wh_input_stock_loc_id.id,
+ 'location_src_id': supplier_loc.id,
+ 'action': 'pull',
+ 'procure_method': 'make_to_order',
+ 'picking_type_id': warehouse.in_type_id.id,
+ })
+ (products[50] | products[99] | products[150] | products[199]).write({
+ 'route_ids': [(4, wrong_route.id)]
+ })
+ self.env['procurement.group'].run_scheduler()
+ self.assertTrue(self.env['stock.move'].search([('product_id', 'in', products.ids)]))
+ for index in [50, 99, 150, 199]:
+ self.assertTrue(self.env['mail.activity'].search([
+ ('res_id', '=', products[index].product_tmpl_id.id),
+ ('res_model_id', '=', self.env.ref('product.model_product_template').id)
+ ]))
diff --git a/addons/stock/tests/test_product.py b/addons/stock/tests/test_product.py
new file mode 100644
index 00000000..4836513c
--- /dev/null
+++ b/addons/stock/tests/test_product.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+# Author: Leonardo Pistone
+# Copyright 2015 Camptocamp SA
+
+from odoo.addons.stock.tests.common2 import TestStockCommon
+from odoo.tests.common import Form
+
+
+class TestVirtualAvailable(TestStockCommon):
+ def setUp(self):
+ super(TestVirtualAvailable, self).setUp()
+
+ # Make `product3` a storable product for this test. Indeed, creating quants
+ # and playing with owners is not possible for consumables.
+ self.product_3.type = 'product'
+
+ self.env['stock.quant'].create({
+ 'product_id': self.product_3.id,
+ 'location_id': self.env.ref('stock.stock_location_stock').id,
+ 'quantity': 30.0})
+
+ self.env['stock.quant'].create({
+ 'product_id': self.product_3.id,
+ 'location_id': self.env.ref('stock.stock_location_stock').id,
+ 'quantity': 10.0,
+ 'owner_id': self.user_stock_user.partner_id.id})
+
+ self.picking_out = self.env['stock.picking'].create({
+ 'picking_type_id': self.ref('stock.picking_type_out'),
+ 'location_id': self.env.ref('stock.stock_location_stock').id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id})
+ self.env['stock.move'].create({
+ 'name': 'a move',
+ 'product_id': self.product_3.id,
+ 'product_uom_qty': 3.0,
+ 'product_uom': self.product_3.uom_id.id,
+ 'picking_id': self.picking_out.id,
+ 'location_id': self.env.ref('stock.stock_location_stock').id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id})
+
+ self.picking_out_2 = self.env['stock.picking'].create({
+ 'picking_type_id': self.ref('stock.picking_type_out'),
+ 'location_id': self.env.ref('stock.stock_location_stock').id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id})
+ self.env['stock.move'].create({
+ 'restrict_partner_id': self.user_stock_user.partner_id.id,
+ 'name': 'another move',
+ 'product_id': self.product_3.id,
+ 'product_uom_qty': 5.0,
+ 'product_uom': self.product_3.uom_id.id,
+ 'picking_id': self.picking_out_2.id,
+ 'location_id': self.env.ref('stock.stock_location_stock').id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id})
+
+ def test_without_owner(self):
+ self.assertAlmostEqual(40.0, self.product_3.virtual_available)
+ self.picking_out.action_assign()
+ self.picking_out_2.action_assign()
+ self.assertAlmostEqual(32.0, self.product_3.virtual_available)
+
+ def test_with_owner(self):
+ prod_context = self.product_3.with_context(owner_id=self.user_stock_user.partner_id.id)
+ self.assertAlmostEqual(10.0, prod_context.virtual_available)
+ self.picking_out.action_assign()
+ self.picking_out_2.action_assign()
+ self.assertAlmostEqual(5.0, prod_context.virtual_available)
+
+ def test_free_quantity(self):
+ """ Test the value of product.free_qty. Free_qty = qty_on_hand - qty_reserved"""
+ self.assertAlmostEqual(40.0, self.product_3.free_qty)
+ self.picking_out.action_confirm()
+ self.picking_out_2.action_confirm()
+ # No reservation so free_qty is unchanged
+ self.assertAlmostEqual(40.0, self.product_3.free_qty)
+ self.picking_out.action_assign()
+ self.picking_out_2.action_assign()
+ # 8 units are now reserved
+ self.assertAlmostEqual(32.0, self.product_3.free_qty)
+ self.picking_out.do_unreserve()
+ self.picking_out_2.do_unreserve()
+ # 8 units are available again
+ self.assertAlmostEqual(40.0, self.product_3.free_qty)
+
+ def test_archive_product_1(self):
+ """`qty_available` and `virtual_available` are computed on archived products"""
+ self.assertTrue(self.product_3.active)
+ self.assertAlmostEqual(40.0, self.product_3.qty_available)
+ self.assertAlmostEqual(40.0, self.product_3.virtual_available)
+ self.product_3.active = False
+ self.assertAlmostEqual(40.0, self.product_3.qty_available)
+ self.assertAlmostEqual(40.0, self.product_3.virtual_available)
+
+ def test_archive_product_2(self):
+ """Archiving a product should archive its reordering rules"""
+ self.assertTrue(self.product_3.active)
+ orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
+ orderpoint_form.product_id = self.product_3
+ orderpoint_form.location_id = self.env.ref('stock.stock_location_stock')
+ orderpoint_form.product_min_qty = 0.0
+ orderpoint_form.product_max_qty = 5.0
+ orderpoint = orderpoint_form.save()
+ self.assertTrue(orderpoint.active)
+ self.product_3.active = False
+ self.assertFalse(orderpoint.active)
+
+ def test_search_qty_available(self):
+ product = self.env['product.product'].create({
+ 'name': 'Brand new product',
+ 'type': 'product',
+ })
+ result = self.env['product.product'].search([
+ ('qty_available', '=', 0),
+ ('id', 'in', product.ids),
+ ])
+ self.assertEqual(product, result)
diff --git a/addons/stock/tests/test_quant.py b/addons/stock/tests/test_quant.py
new file mode 100644
index 00000000..cfcab420
--- /dev/null
+++ b/addons/stock/tests/test_quant.py
@@ -0,0 +1,662 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from contextlib import closing
+from datetime import datetime, timedelta
+
+from odoo.addons.mail.tests.common import mail_new_test_user
+from odoo.exceptions import ValidationError
+from odoo.tests.common import SavepointCase
+from odoo.exceptions import AccessError, UserError
+
+
+class StockQuant(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(StockQuant, cls).setUpClass()
+ cls.demo_user = mail_new_test_user(
+ cls.env,
+ name='Pauline Poivraisselle',
+ login='pauline',
+ email='p.p@example.com',
+ notification_type='inbox',
+ groups='base.group_user',
+ )
+ cls.stock_user = mail_new_test_user(
+ cls.env,
+ name='Pauline Poivraisselle',
+ login='pauline2',
+ email='p.p@example.com',
+ notification_type='inbox',
+ groups='stock.group_stock_user',
+ )
+
+ cls.product = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ })
+ cls.product_lot = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'tracking': 'lot',
+ })
+ cls.product_consu = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'consu',
+ })
+ cls.product_serial = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'tracking': 'serial',
+ })
+ cls.stock_location = cls.env['stock.location'].create({
+ 'name': 'stock_location',
+ 'usage': 'internal',
+ })
+ cls.stock_subloc2 = cls.env['stock.location'].create({
+ 'name': 'subloc2',
+ 'usage': 'internal',
+ 'location_id': cls.stock_location.id,
+ })
+
+ def gather_relevant(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False):
+ quants = self.env['stock.quant']._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
+ return quants.filtered(lambda q: not (q.quantity == 0 and q.reserved_quantity == 0))
+
+ def test_get_available_quantity_1(self):
+ """ Quantity availability with only one quant in a location.
+ """
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ def test_get_available_quantity_2(self):
+ """ Quantity availability with multiple quants in a location.
+ """
+ for i in range(3):
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 3.0)
+
+ def test_get_available_quantity_3(self):
+ """ Quantity availability with multiple quants (including negatives ones) in a location.
+ """
+ for i in range(3):
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ })
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': -3.0,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ def test_get_available_quantity_4(self):
+ """ Quantity availability with no quants in a location.
+ """
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ def test_get_available_quantity_5(self):
+ """ Quantity availability with multiple partially reserved quants in a location.
+ """
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 10.0,
+ 'reserved_quantity': 9.0,
+ })
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ 'reserved_quantity': 1.0,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ def test_get_available_quantity_6(self):
+ """ Quantity availability with multiple partially reserved quants in a location.
+ """
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 10.0,
+ 'reserved_quantity': 20.0,
+ })
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 5.0,
+ 'reserved_quantity': 0.0,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, allow_negative=True), -5.0)
+
+ def test_get_available_quantity_7(self):
+ """ Quantity availability with only one tracked quant in a location.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant'].create({
+ 'product_id': self.product_lot.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 10.0,
+ 'reserved_quantity': 20.0,
+ 'lot_id': lot1.id,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_lot, self.stock_location, lot_id=lot1, allow_negative=True), -10.0)
+
+ def test_get_available_quantity_8(self):
+ """ Quantity availability with a consumable product.
+ """
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_consu, self.stock_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product_consu, self.stock_location)), 0)
+ with self.assertRaises(ValidationError):
+ self.env['stock.quant']._update_available_quantity(self.product_consu, self.stock_location, 1.0)
+
+ def test_get_available_quantity_9(self):
+ """ Quantity availability by a demo user with access rights/rules.
+ """
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ })
+ self.env = self.env(user=self.demo_user)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ def test_increase_available_quantity_1(self):
+ """ Increase the available quantity when no quants are already in a location.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+
+ def test_increase_available_quantity_2(self):
+ """ Increase the available quantity when multiple quants are already in a location.
+ """
+ for i in range(2):
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 3.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 2)
+
+ def test_increase_available_quantity_3(self):
+ """ Increase the available quantity when a concurrent transaction is already increasing
+ the reserved quanntity for the same product.
+ """
+ quant = self.env['stock.quant'].search([('location_id', '=', self.stock_location.id)], limit=1)
+ if not quant:
+ self.skipTest('Cannot test concurrent transactions without demo data.')
+ product = quant.product_id
+ available_quantity = self.env['stock.quant']._get_available_quantity(product, self.stock_location, allow_negative=True)
+ # opens a new cursor and SELECT FOR UPDATE the quant, to simulate another concurrent reserved
+ # quantity increase
+ with closing(self.registry.cursor()) as cr:
+ cr.execute("SELECT id FROM stock_quant WHERE product_id=%s AND location_id=%s", (product.id, self.stock_location.id))
+ quant_id = cr.fetchone()
+ cr.execute("SELECT 1 FROM stock_quant WHERE id=%s FOR UPDATE", quant_id)
+ self.env['stock.quant']._update_available_quantity(product, self.stock_location, 1.0)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product, self.stock_location, allow_negative=True), available_quantity + 1)
+ self.assertEqual(len(self.gather_relevant(product, self.stock_location, strict=True)), 2)
+
+ def test_increase_available_quantity_4(self):
+ """ Increase the available quantity when no quants are already in a location with a user without access right.
+ """
+ self.env = self.env(user=self.demo_user)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0)
+
+ def test_increase_available_quantity_5(self):
+ """ Increase the available quantity when no quants are already in stock.
+ Increase a subLocation and check that quants are in this location. Also test inverse.
+ """
+ stock_sub_location = self.stock_location.child_ids[0]
+ product2 = self.env['product.product'].create({
+ 'name': 'Product B',
+ 'type': 'product',
+ })
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0)
+ self.env['stock.quant']._update_available_quantity(self.product, stock_sub_location, 1.0)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, stock_sub_location), 1.0)
+
+ self.env['stock.quant']._update_available_quantity(product2, stock_sub_location, 1.0)
+ self.env['stock.quant']._update_available_quantity(product2, self.stock_location, 1.0)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product2, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product2, stock_sub_location), 1.0)
+
+ def test_increase_available_quantity_6(self):
+ """ Increasing the available quantity in a view location should be forbidden.
+ """
+ location1 = self.env['stock.location'].create({
+ 'name': 'viewloc1',
+ 'usage': 'view',
+ 'location_id': self.stock_location.id,
+ })
+ with self.assertRaises(ValidationError):
+ self.env['stock.quant']._update_available_quantity(self.product, location1, 1.0)
+
+ def test_increase_available_quantity_7(self):
+ """ Setting a location's usage as "view" should be forbidden if it already
+ contains quant.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0)
+ self.assertTrue(len(self.stock_location.quant_ids.ids) > 0)
+ with self.assertRaises(UserError):
+ self.stock_location.usage = 'view'
+
+ def test_decrease_available_quantity_1(self):
+ """ Decrease the available quantity when no quants are already in a location.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, -1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location, allow_negative=True), -1.0)
+
+ def test_decrease_available_quantity_2(self):
+ """ Decrease the available quantity when multiple quants are already in a location.
+ """
+ for i in range(2):
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 2)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, -1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 1.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1)
+
+ def test_decrease_available_quantity_3(self):
+ """ Decrease the available quantity when a concurrent transaction is already increasing
+ the reserved quanntity for the same product.
+ """
+ quant = self.env['stock.quant'].search([('location_id', '=', self.stock_location.id)], limit=1)
+ if not quant:
+ self.skipTest('Cannot test concurrent transactions without demo data.')
+ product = quant.product_id
+ available_quantity = self.env['stock.quant']._get_available_quantity(product, self.stock_location, allow_negative=True)
+
+ # opens a new cursor and SELECT FOR UPDATE the quant, to simulate another concurrent reserved
+ # quantity increase
+ with closing(self.registry.cursor()) as cr:
+ cr.execute("SELECT 1 FROM stock_quant WHERE id = %s FOR UPDATE", quant.ids)
+ self.env['stock.quant']._update_available_quantity(product, self.stock_location, -1.0)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product, self.stock_location, allow_negative=True), available_quantity - 1)
+ self.assertEqual(len(self.gather_relevant(product, self.stock_location, strict=True)), 2)
+
+ def test_decrease_available_quantity_4(self):
+ """ Decrease the available quantity that delete the quant. The active user should have
+ read,write and unlink rights
+ """
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ })
+ self.env = self.env(user=self.demo_user)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, -1.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 0)
+
+ def test_increase_reserved_quantity_1(self):
+ """ Increase the reserved quantity of quantity x when there's a single quant in a given
+ location which has an available quantity of x.
+ """
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 10.0,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1)
+ self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 10.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1)
+
+ def test_increase_reserved_quantity_2(self):
+ """ Increase the reserved quantity of quantity x when there's two quants in a given
+ location which have an available quantity of x together.
+ """
+ for i in range(2):
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 5.0,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 2)
+ self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 10.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 2)
+
+ def test_increase_reserved_quantity_3(self):
+ """ Increase the reserved quantity of quantity x when there's multiple quants in a given
+ location which have an available quantity of x together.
+ """
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 5.0,
+ 'reserved_quantity': 2.0,
+ })
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 10.0,
+ 'reserved_quantity': 12.0,
+ })
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 8.0,
+ 'reserved_quantity': 3.0,
+ })
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 35.0,
+ 'reserved_quantity': 12.0,
+ })
+ # total quantity: 58
+ # total reserved quantity: 29
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 29.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 4)
+ self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 10.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 19.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 4)
+
+ def test_increase_reserved_quantity_4(self):
+ """ Increase the reserved quantity of quantity x when there's multiple quants in a given
+ location which have an available quantity of x together.
+ """
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 5.0,
+ 'reserved_quantity': 7.0,
+ })
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 12.0,
+ 'reserved_quantity': 10.0,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 2)
+ with self.assertRaises(UserError):
+ self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 10.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ def test_increase_reserved_quantity_5(self):
+ """ Decrease the available quantity when no quant are in a location.
+ """
+ with self.assertRaises(UserError):
+ self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ def test_decrease_reserved_quantity_1(self):
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 10.0,
+ 'reserved_quantity': 10.0,
+ })
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1)
+ self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, -10.0, strict=True)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10.0)
+ self.assertEqual(len(self.gather_relevant(self.product, self.stock_location)), 1)
+
+ def test_increase_decrease_reserved_quantity_1(self):
+ """ Decrease then increase reserved quantity when no quant are in a location.
+ """
+ with self.assertRaises(UserError):
+ self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ with self.assertRaises(UserError):
+ self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, -1.0, strict=True)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+
+ def test_action_done_1(self):
+ pack_location = self.env.ref('stock.location_pack_zone')
+ pack_location.active = True
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.env['stock.quant']._update_reserved_quantity(self.product, self.stock_location, -2.0, strict=True)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 2.0)
+ self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, -2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 0.0)
+ self.env['stock.quant']._update_available_quantity(self.product, pack_location, 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, pack_location), 2.0)
+
+ def test_mix_tracked_untracked_1(self):
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+
+ # add one tracked, one untracked
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot1)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 2.0)
+
+ self.env['stock.quant']._update_reserved_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot1, strict=True)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 1.0)
+
+ self.env['stock.quant']._update_reserved_quantity(self.product_serial, self.stock_location, -1.0, lot_id=lot1, strict=True)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 2.0)
+
+ with self.assertRaises(UserError):
+ self.env['stock.quant']._update_reserved_quantity(self.product_serial, self.stock_location, -1.0, strict=True)
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location), 2.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, strict=True), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product_serial, self.stock_location, lot_id=lot1), 2.0)
+
+ def test_access_rights_1(self):
+ """ Directly update the quant with a user with or without stock access rights sould raise
+ an AccessError.
+ """
+ quant = self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ })
+ self.env = self.env(user=self.demo_user)
+ with self.assertRaises(AccessError):
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ })
+ with self.assertRaises(AccessError):
+ quant.with_user(self.demo_user).write({'quantity': 2.0})
+ with self.assertRaises(AccessError):
+ quant.with_user(self.demo_user).unlink()
+
+ self.env = self.env(user=self.stock_user)
+ with self.assertRaises(AccessError):
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ })
+ with self.assertRaises(AccessError):
+ quant.with_user(self.demo_user).write({'quantity': 2.0})
+ with self.assertRaises(AccessError):
+ quant.with_user(self.demo_user).unlink()
+
+ def test_in_date_1(self):
+ """ Check that no incoming date is set when updating the quantity of an untracked quant.
+ """
+ quantity, in_date = self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0)
+ self.assertEqual(quantity, 1)
+ self.assertNotEqual(in_date, None)
+
+ def test_in_date_1b(self):
+ self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 1.0,
+ })
+ quantity, in_date = self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 2.0)
+ self.assertEqual(quantity, 3)
+ self.assertNotEqual(in_date, None)
+
+ def test_in_date_2(self):
+ """ Check that an incoming date is correctly set when updating the quantity of a tracked
+ quant.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ quantity, in_date = self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot1)
+ self.assertEqual(quantity, 1)
+ self.assertNotEqual(in_date, None)
+
+ def test_in_date_3(self):
+ """ Check that the FIFO strategies correctly applies when you have multiple lot received
+ at different times for a tracked product.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ in_date_lot1 = datetime.now()
+ in_date_lot2 = datetime.now() - timedelta(days=5)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot1, in_date=in_date_lot1)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot2, in_date=in_date_lot2)
+
+ quants = self.env['stock.quant']._update_reserved_quantity(self.product_serial, self.stock_location, 1)
+
+ # Default removal strategy is FIFO, so lot2 should be received as it was received earlier.
+ self.assertEqual(quants[0][0].lot_id.id, lot2.id)
+
+ def test_in_date_4(self):
+ """ Check that the LIFO strategies correctly applies when you have multiple lot received
+ at different times for a tracked product.
+ """
+ lifo_strategy = self.env['product.removal'].search([('method', '=', 'lifo')])
+ self.stock_location.removal_strategy_id = lifo_strategy
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product_serial.id,
+ 'company_id': self.env.company.id,
+ })
+ in_date_lot1 = datetime.now()
+ in_date_lot2 = datetime.now() - timedelta(days=5)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot1, in_date=in_date_lot1)
+ self.env['stock.quant']._update_available_quantity(self.product_serial, self.stock_location, 1.0, lot_id=lot2, in_date=in_date_lot2)
+
+ quants = self.env['stock.quant']._update_reserved_quantity(self.product_serial, self.stock_location, 1)
+
+ # Removal strategy is LIFO, so lot1 should be received as it was received later.
+ self.assertEqual(quants[0][0].lot_id.id, lot1.id)
+
+ def test_in_date_4b(self):
+ """ Check for LIFO and max with/without in_date that it handles the LIFO NULLS LAST well
+ """
+ stock_location1 = self.env['stock.location'].create({
+ 'name': 'Shelf 1',
+ 'location_id': self.stock_location.id
+ })
+ stock_location2 = self.env['stock.location'].create({
+ 'name': 'Shelf 2',
+ 'location_id': self.stock_location.id
+ })
+ lifo_strategy = self.env['product.removal'].search([('method', '=', 'lifo')])
+ self.stock_location.removal_strategy_id = lifo_strategy
+
+ self.env['stock.quant'].create({
+ 'product_id': self.product_serial.id,
+ 'location_id': stock_location1.id,
+ 'quantity': 1.0,
+ })
+
+ in_date_location2 = datetime.now()
+ self.env['stock.quant']._update_available_quantity(self.product_serial, stock_location2, 1.0, in_date=in_date_location2)
+
+ quants = self.env['stock.quant']._update_reserved_quantity(self.product_serial, self.stock_location, 1)
+
+ # Removal strategy is LIFO, so the one with date is the most recent one and should be selected
+ self.assertEqual(quants[0][0].location_id.id, stock_location2.id)
+
+ def test_in_date_5(self):
+ """ Receive the same lot at different times, once they're in the same location, the quants
+ are merged and only the earliest incoming date is kept.
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product_lot.id,
+ 'company_id': self.env.company.id,
+ })
+
+ from odoo.fields import Datetime
+ in_date1 = Datetime.now()
+ self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 1.0, lot_id=lot1, in_date=in_date1)
+
+ quant = self.env['stock.quant'].search([
+ ('product_id', '=', self.product_lot.id),
+ ('location_id', '=', self.stock_location.id),
+ ])
+ self.assertEqual(len(quant), 1)
+ self.assertEqual(quant.quantity, 1)
+ self.assertEqual(quant.lot_id.id, lot1.id)
+ self.assertEqual(quant.in_date, in_date1)
+
+ in_date2 = Datetime.now() - timedelta(days=5)
+ self.env['stock.quant']._update_available_quantity(self.product_lot, self.stock_location, 1.0, lot_id=lot1, in_date=in_date2)
+
+ quant = self.env['stock.quant'].search([
+ ('product_id', '=', self.product_lot.id),
+ ('location_id', '=', self.stock_location.id),
+ ])
+ self.assertEqual(len(quant), 1)
+ self.assertEqual(quant.quantity, 2)
+ self.assertEqual(quant.lot_id.id, lot1.id)
+ self.assertEqual(quant.in_date, in_date2)
diff --git a/addons/stock/tests/test_quant_inventory_mode.py b/addons/stock/tests/test_quant_inventory_mode.py
new file mode 100644
index 00000000..32e93e56
--- /dev/null
+++ b/addons/stock/tests/test_quant_inventory_mode.py
@@ -0,0 +1,222 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.mail.tests.common import mail_new_test_user
+from odoo.tests.common import SavepointCase
+from odoo.exceptions import AccessError, UserError
+
+
+class TestEditableQuant(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestEditableQuant, cls).setUpClass()
+
+ # Shortcut to call `stock.quant` with `inventory mode` set in the context
+ cls.Quant = cls.env['stock.quant'].with_context(inventory_mode=True)
+
+ Product = cls.env['product.product']
+ Location = cls.env['stock.location']
+ cls.product = Product.create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.product2 = Product.create({
+ 'name': 'Product B',
+ 'type': 'product',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.product_tracked_sn = Product.create({
+ 'name': 'Product tracked by SN',
+ 'type': 'product',
+ 'tracking': 'serial',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.warehouse = Location.create({
+ 'name': 'Warehouse',
+ 'usage': 'internal',
+ })
+ cls.stock = Location.create({
+ 'name': 'Stock',
+ 'usage': 'internal',
+ 'location_id': cls.warehouse.id,
+ })
+ cls.room1 = Location.create({
+ 'name': 'Room A',
+ 'usage': 'internal',
+ 'location_id': cls.stock.id,
+ })
+ cls.room2 = Location.create({
+ 'name': 'Room B',
+ 'usage': 'internal',
+ 'location_id': cls.stock.id,
+ })
+ cls.inventory_loss = cls.product.property_stock_inventory
+
+ def test_create_quant_1(self):
+ """ Create a new quant who don't exist yet.
+ """
+ # Checks we don't have any quant for this product.
+ quants = self.env['stock.quant'].search([('product_id', '=', self.product.id)])
+ self.assertEqual(len(quants), 0)
+ self.Quant.create({
+ 'product_id': self.product.id,
+ 'location_id': self.stock.id,
+ 'inventory_quantity': 24
+ })
+ quants = self.env['stock.quant'].search([
+ ('product_id', '=', self.product.id),
+ ('quantity', '>', 0),
+ ])
+ # Checks we have now a quant, and also checks the quantity is equals to
+ # what we set in `inventory_quantity` field.
+ self.assertEqual(len(quants), 1)
+ self.assertEqual(quants.quantity, 24)
+
+ stock_move = self.env['stock.move'].search([
+ ('product_id', '=', self.product.id),
+ ])
+ self.assertEqual(stock_move.location_id.id, self.inventory_loss.id)
+ self.assertEqual(stock_move.location_dest_id.id, self.stock.id)
+
+ def test_create_quant_2(self):
+ """ Try to create a quant who already exist.
+ Must update the existing quant instead of creating a new one.
+ """
+ # Creates a quants...
+ first_quant = self.Quant.create({
+ 'product_id': self.product.id,
+ 'location_id': self.room1.id,
+ 'quantity': 12,
+ })
+ quants = self.env['stock.quant'].search([
+ ('product_id', '=', self.product.id),
+ ('quantity', '>', 0),
+ ])
+ self.assertEqual(len(quants), 1)
+ # ... then try to create an another quant for the same product/location.
+ second_quant = self.Quant.create({
+ 'product_id': self.product.id,
+ 'location_id': self.room1.id,
+ 'inventory_quantity': 24,
+ })
+ quants = self.env['stock.quant'].search([
+ ('product_id', '=', self.product.id),
+ ('quantity', '>', 0),
+ ])
+ # Checks we still have only one quant, and first quant quantity was
+ # updated, and second quant had the same ID than the first quant.
+ self.assertEqual(len(quants), 1)
+ self.assertEqual(first_quant.quantity, 24)
+ self.assertEqual(first_quant.id, second_quant.id)
+ stock_move = self.env['stock.move'].search([
+ ('product_id', '=', self.product.id),
+ ])
+ self.assertEqual(len(stock_move), 1)
+
+ def test_create_quant_3(self):
+ """ Try to create a quant with `inventory_quantity` but not in inventory mode.
+ Creates two quants not in inventory mode:
+ - One with `quantity` (this one must be OK)
+ - One with `inventory_quantity` (this one must be null)
+ """
+ valid_quant = self.env['stock.quant'].create({
+ 'product_id': self.product.id,
+ 'location_id': self.room1.id,
+ 'quantity': 10,
+ })
+ invalid_quant = self.env['stock.quant'].create({
+ 'product_id': self.product2.id,
+ 'location_id': self.room1.id,
+ 'inventory_quantity': 20,
+ })
+ self.assertEqual(valid_quant.quantity, 10)
+ self.assertEqual(invalid_quant.quantity, 0)
+
+ def test_create_quant_4(self):
+ """ Try to create tree quants in inventory mode with `quantity` and/or `inventory_quantity`.
+ Creates two quants not in inventory mode:
+ - One with `quantity` (this one must be OK, but `inventory_mode` is useless here as it
+ doesn't enter in the inventory mode case and create quant as usual)
+ - One with `inventory_quantity` (this one must be OK)
+ - One with the two values (this one must raises an error as it enters in the inventory
+ mode but user can't edit directly `quantity` in inventory mode)
+ """
+ valid_quant = self.env['stock.quant'].with_context(inventory_mode=True).create({
+ 'product_id': self.product.id,
+ 'location_id': self.room1.id,
+ 'quantity': 10,
+ })
+ inventoried_quant = self.env['stock.quant'].with_context(inventory_mode=True).create({
+ 'product_id': self.product2.id,
+ 'location_id': self.room1.id,
+ 'inventory_quantity': 20,
+ })
+ with self.assertRaises(UserError):
+ invalid_quant = self.env['stock.quant'].with_context(inventory_mode=True).create({
+ 'product_id': self.product.id,
+ 'location_id': self.room2.id,
+ 'quantity': 10,
+ 'inventory_quantity': 20,
+ })
+ self.assertEqual(valid_quant.quantity, 10)
+ self.assertEqual(inventoried_quant.quantity, 20)
+
+ def test_edit_quant_1(self):
+ """ Increases manually quantity of a quant.
+ """
+ quant = self.Quant.create({
+ 'product_id': self.product.id,
+ 'location_id': self.room1.id,
+ 'quantity': 12,
+ })
+ quant.inventory_quantity = 24
+ self.assertEqual(quant.quantity, 24)
+ stock_move = self.env['stock.move'].search([
+ ('product_id', '=', self.product.id),
+ ])
+ self.assertEqual(stock_move.location_id.id, self.inventory_loss.id)
+ self.assertEqual(stock_move.location_dest_id.id, self.room1.id)
+
+ def test_edit_quant_2(self):
+ """ Decreases manually quantity of a quant.
+ """
+ quant = self.Quant.create({
+ 'product_id': self.product.id,
+ 'location_id': self.room1.id,
+ 'quantity': 12,
+ })
+ quant.inventory_quantity = 8
+ self.assertEqual(quant.quantity, 8)
+ stock_move = self.env['stock.move'].search([
+ ('product_id', '=', self.product.id),
+ ])
+ self.assertEqual(stock_move.location_id.id, self.room1.id)
+ self.assertEqual(stock_move.location_dest_id.id, self.inventory_loss.id)
+
+ def test_edit_quant_3(self):
+ """ Try to edit a record without the inventory mode.
+ Must raise an error.
+ """
+ self.demo_user = mail_new_test_user(
+ self.env,
+ name='Pauline Poivraisselle',
+ login='pauline',
+ email='p.p@example.com',
+ groups='base.group_user',
+ )
+ user_admin = self.env.ref('base.user_admin')
+ quant = self.Quant.create({
+ 'product_id': self.product.id,
+ 'location_id': self.room1.id,
+ 'quantity': 12
+ })
+ self.assertEqual(quant.quantity, 12)
+ # Try to write on quant without permission
+ with self.assertRaises(AccessError):
+ quant.with_user(self.demo_user).write({'inventory_quantity': 8})
+ self.assertEqual(quant.quantity, 12)
+
+ # Try to write on quant with permission
+ quant.with_user(user_admin).write({'inventory_quantity': 8})
+ self.assertEqual(quant.quantity, 8)
diff --git a/addons/stock/tests/test_report.py b/addons/stock/tests/test_report.py
new file mode 100644
index 00000000..91a3b83c
--- /dev/null
+++ b/addons/stock/tests/test_report.py
@@ -0,0 +1,1043 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import date, datetime, timedelta
+
+from odoo.tests.common import Form, SavepointCase
+
+
+class TestReportsCommon(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.partner = cls.env['res.partner'].create({'name': 'Partner'})
+ cls.ModelDataObj = cls.env['ir.model.data']
+ cls.picking_type_in = cls.env['stock.picking.type'].browse(cls.ModelDataObj.xmlid_to_res_id('stock.picking_type_in'))
+ cls.picking_type_out = cls.env['stock.picking.type'].browse(cls.ModelDataObj.xmlid_to_res_id('stock.picking_type_out'))
+ cls.supplier_location = cls.env['stock.location'].browse(cls.ModelDataObj.xmlid_to_res_id('stock.stock_location_suppliers'))
+ cls.stock_location = cls.env['stock.location'].browse(cls.ModelDataObj.xmlid_to_res_id('stock.stock_location_stock'))
+
+ product_form = Form(cls.env['product.product'])
+ product_form.type = 'product'
+ product_form.name = 'Product'
+ cls.product = product_form.save()
+ cls.product_template = cls.product.product_tmpl_id
+
+ def get_report_forecast(self, product_template_ids=False, product_variant_ids=False, context=False):
+ if product_template_ids:
+ report = self.env['report.stock.report_product_template_replenishment']
+ product_ids = product_template_ids
+ elif product_variant_ids:
+ report = self.env['report.stock.report_product_product_replenishment']
+ product_ids = product_template_ids
+ if context:
+ report = report.with_context(context)
+ report_values = report._get_report_values(docids=product_ids)
+ docs = report_values['docs']
+ lines = docs['lines']
+ return report_values, docs, lines
+
+
+class TestReports(TestReportsCommon):
+ def test_reports(self):
+ product1 = self.env['product.product'].create({
+ 'name': 'Mellohi',
+ 'default_code': 'C418',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'tracking': 'lot',
+ 'barcode': 'scan_me'
+ })
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'Volume-Beta',
+ 'product_id': product1.id,
+ 'company_id': self.env.company.id,
+ })
+ report = self.env.ref('stock.label_lot_template')
+ target = b'\n\n\n^XA\n^FO100,50\n^A0N,44,33^FD[C418]Mellohi^FS\n^FO100,100\n^A0N,44,33^FDLN/SN:Volume-Beta^FS\n^FO100,150^BY3\n^BCN,100,Y,N,N\n^FDVolume-Beta^FS\n^XZ\n\n\n'
+
+ rendering, qweb_type = report._render_qweb_text(lot1.id)
+ self.assertEqual(target, rendering.replace(b' ', b''), 'The rendering is not good')
+ self.assertEqual(qweb_type, 'text', 'the report type is not good')
+
+ def test_report_quantity_1(self):
+ product_form = Form(self.env['product.product'])
+ product_form.type = 'product'
+ product_form.name = 'Product'
+ product = product_form.save()
+
+ warehouse = self.env['stock.warehouse'].search([], limit=1)
+ stock = self.env['stock.location'].create({
+ 'name': 'New Stock',
+ 'usage': 'internal',
+ 'location_id': warehouse.view_location_id.id,
+ })
+
+ # Inventory Adjustement of 50.0 today.
+ self.env['stock.quant'].with_context(inventory_mode=True).create({
+ 'product_id': product.id,
+ 'location_id': stock.id,
+ 'inventory_quantity': 50
+ })
+ self.env['stock.move'].flush()
+ report_records_today = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today())],
+ ['product_qty'], [], lazy=False)
+ report_records_tomorrow = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today() + timedelta(days=1))],
+ ['product_qty'], [])
+ report_records_yesterday = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today() - timedelta(days=1))],
+ ['product_qty'], [])
+ self.assertEqual(sum([r['product_qty'] for r in report_records_today]), 50.0)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow]), 50.0)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_yesterday]), 0.0)
+
+ # Delivery of 20.0 units tomorrow
+ move_out = self.env['stock.move'].create({
+ 'name': 'Move Out 20',
+ 'date': datetime.now() + timedelta(days=1),
+ 'location_id': stock.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 20.0,
+ })
+ self.env['stock.move'].flush()
+ report_records_tomorrow = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today() + timedelta(days=1))],
+ ['product_qty'], [])
+ self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow]), 50.0)
+ move_out._action_confirm()
+ self.env['stock.move'].flush()
+ report_records_tomorrow = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today() + timedelta(days=1))],
+ ['product_qty', 'state'], ['state'], lazy=False)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'forecast']), 30.0)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'out']), -20.0)
+ report_records_today = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today())],
+ ['product_qty', 'state'], ['state'], lazy=False)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_today if r['state'] == 'forecast']), 50.0)
+
+ # Receipt of 10.0 units tomorrow
+ move_in = self.env['stock.move'].create({
+ 'name': 'Move In 10',
+ 'date': datetime.now() + timedelta(days=1),
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': stock.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 10.0,
+ })
+ move_in._action_confirm()
+ self.env['stock.move'].flush()
+ report_records_tomorrow = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today() + timedelta(days=1))],
+ ['product_qty', 'state'], ['state'], lazy=False)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'forecast']), 40.0)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'out']), -20.0)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'in']), 10.0)
+ report_records_today = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today())],
+ ['product_qty', 'state'], ['state'], lazy=False)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_today if r['state'] == 'forecast']), 50.0)
+
+ # Delivery of 20.0 units tomorrow
+ move_out = self.env['stock.move'].create({
+ 'name': 'Move Out 30 - Day-1',
+ 'date': datetime.now() - timedelta(days=1),
+ 'location_id': stock.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 30.0,
+ })
+ move_out._action_confirm()
+ self.env['stock.move'].flush()
+ report_records_today = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today())],
+ ['product_qty', 'state'], ['state'], lazy=False)
+ report_records_tomorrow = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today() + timedelta(days=1))],
+ ['product_qty', 'state'], ['state'], lazy=False)
+ report_records_yesterday = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today() - timedelta(days=1))],
+ ['product_qty', 'state'], ['state'], lazy=False)
+
+ self.assertEqual(sum([r['product_qty'] for r in report_records_yesterday if r['state'] == 'forecast']), -30.0)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_yesterday if r['state'] == 'out']), -30.0)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_yesterday if r['state'] == 'in']), 0.0)
+
+ self.assertEqual(sum([r['product_qty'] for r in report_records_today if r['state'] == 'forecast']), 20.0)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_today if r['state'] == 'out']), 0.0)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_today if r['state'] == 'in']), 0.0)
+
+ self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'forecast']), 10.0)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'out']), -20.0)
+ self.assertEqual(sum([r['product_qty'] for r in report_records_tomorrow if r['state'] == 'in']), 10.0)
+
+ def test_report_quantity_2(self):
+ """ Not supported case.
+ """
+ product_form = Form(self.env['product.product'])
+ product_form.type = 'product'
+ product_form.name = 'Product'
+ product = product_form.save()
+
+ warehouse = self.env['stock.warehouse'].search([], limit=1)
+ stock = self.env['stock.location'].create({
+ 'name': 'Stock Under Warehouse',
+ 'usage': 'internal',
+ 'location_id': warehouse.view_location_id.id,
+ })
+ stock_without_wh = self.env['stock.location'].create({
+ 'name': 'Stock Outside Warehouse',
+ 'usage': 'internal',
+ 'location_id': self.env.ref('stock.stock_location_locations').id,
+ })
+ self.env['stock.quant'].with_context(inventory_mode=True).create({
+ 'product_id': product.id,
+ 'location_id': stock.id,
+ 'inventory_quantity': 50
+ })
+ self.env['stock.quant'].with_context(inventory_mode=True).create({
+ 'product_id': product.id,
+ 'location_id': stock_without_wh.id,
+ 'inventory_quantity': 50
+ })
+ move = self.env['stock.move'].create({
+ 'name': 'Move outside warehouse',
+ 'location_id': stock.id,
+ 'location_dest_id': stock_without_wh.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 10.0,
+ })
+ move._action_confirm()
+ self.env['stock.move'].flush()
+ report_records = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today()), ('warehouse_id', '!=', False)],
+ ['product_qty', 'state'], ['state'], lazy=False)
+ self.assertEqual(sum([r['product_qty'] for r in report_records if r['state'] == 'forecast']), 40.0)
+ report_records = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today())],
+ ['product_qty', 'state'], ['state'], lazy=False)
+ self.assertEqual(sum([r['product_qty'] for r in report_records if r['state'] == 'forecast']), 40.0)
+ move = self.env['stock.move'].create({
+ 'name': 'Move outside warehouse',
+ 'location_id': stock_without_wh.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 10.0,
+ })
+ move._action_confirm()
+ self.env['stock.move'].flush()
+ report_records = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today())],
+ ['product_qty', 'state'], ['state'], lazy=False)
+ self.assertEqual(sum([r['product_qty'] for r in report_records if r['state'] == 'forecast']), 40.0)
+
+ def test_report_quantity_3(self):
+ product_form = Form(self.env['product.product'])
+ product_form.type = 'product'
+ product_form.name = 'Product'
+ product = product_form.save()
+
+ warehouse = self.env['stock.warehouse'].search([], limit=1)
+ stock = self.env['stock.location'].create({
+ 'name': 'Rack',
+ 'usage': 'view',
+ 'location_id': warehouse.view_location_id.id,
+ })
+ stock_real_loc = self.env['stock.location'].create({
+ 'name': 'Drawer',
+ 'usage': 'internal',
+ 'location_id': stock.id,
+ })
+
+ self.env['stock.move'].flush()
+ report_records = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today())],
+ ['product_qty'], [], lazy=False)
+ self.assertEqual(sum([r['product_qty'] for r in report_records if r['product_qty']]), 0.0)
+
+ # Receipt of 20.0 units tomorrow
+ move_in = self.env['stock.move'].create({
+ 'name': 'Move In 20',
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': stock.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 20.0,
+ })
+ move_in._action_confirm()
+ move_in.move_line_ids.location_dest_id = stock_real_loc.id
+ move_in.move_line_ids.qty_done = 20.0
+ move_in._action_done()
+ self.env['stock.move'].flush()
+ report_records = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today())],
+ ['product_qty'], [], lazy=False)
+ self.assertEqual(sum([r['product_qty'] for r in report_records]), 20.0)
+
+ # Delivery of 10.0 units tomorrow
+ move_out = self.env['stock.move'].create({
+ 'name': 'Move Out 10',
+ 'location_id': stock.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 10.0,
+ })
+ move_out._action_confirm()
+ move_out._action_assign()
+ move_out.move_line_ids.qty_done = 10.0
+ move_out._action_done()
+ self.env['stock.move'].flush()
+ report_records = self.env['report.stock.quantity'].read_group(
+ [('product_id', '=', product.id), ('date', '=', date.today())],
+ ['product_qty'], [], lazy=False)
+ self.assertEqual(sum([r['product_qty'] for r in report_records]), 10.0)
+
+ def test_report_forecast_1(self):
+ """ Checks report data for product is empty. Then creates and process
+ some operations and checks the report data accords rigthly these operations.
+ """
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 0, "Must have 0 line.")
+ self.assertEqual(draft_picking_qty['in'], 0)
+ self.assertEqual(draft_picking_qty['out'], 0)
+
+ # Creates a receipt then checks draft picking quantities.
+ receipt_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ receipt_form.partner_id = self.partner
+ receipt_form.picking_type_id = self.picking_type_in
+ receipt = receipt_form.save()
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 2
+ receipt = receipt_form.save()
+
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 0, "Must have 0 line.")
+ self.assertEqual(draft_picking_qty['in'], 2)
+ self.assertEqual(draft_picking_qty['out'], 0)
+
+ # Creates a delivery then checks draft picking quantities.
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ delivery = delivery_form.save()
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 5
+ delivery = delivery_form.save()
+
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 0, "Must have 0 line.")
+ self.assertEqual(draft_picking_qty['in'], 2)
+ self.assertEqual(draft_picking_qty['out'], 5)
+
+ # Confirms the delivery: must have one report line and no more pending qty out now.
+ delivery.action_confirm()
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 1, "Must have 1 line.")
+ self.assertEqual(draft_picking_qty['in'], 2)
+ self.assertEqual(draft_picking_qty['out'], 0)
+ delivery_line = lines[0]
+ self.assertEqual(delivery_line['quantity'], 5)
+ self.assertEqual(delivery_line['replenishment_filled'], False)
+ self.assertEqual(delivery_line['document_out'].id, delivery.id)
+
+ # Confirms the receipt, must have two report lines now:
+ # - line with 2 qty (from the receipt to the delivery)
+ # - line with 3 qty (delivery, unavailable)
+ receipt.action_confirm()
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 2, "Must have 2 line.")
+ self.assertEqual(draft_picking_qty['in'], 0)
+ self.assertEqual(draft_picking_qty['out'], 0)
+ fulfilled_line = lines[0]
+ unavailable_line = lines[1]
+ self.assertEqual(fulfilled_line['replenishment_filled'], True)
+ self.assertEqual(fulfilled_line['quantity'], 2)
+ self.assertEqual(fulfilled_line['document_in'].id, receipt.id)
+ self.assertEqual(fulfilled_line['document_out'].id, delivery.id)
+ self.assertEqual(unavailable_line['replenishment_filled'], False)
+ self.assertEqual(unavailable_line['quantity'], 3)
+ self.assertEqual(unavailable_line['document_out'].id, delivery.id)
+
+ # Creates a new receipt for the remaining quantity, confirm it...
+ receipt_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ receipt_form.partner_id = self.partner
+ receipt_form.picking_type_id = self.picking_type_in
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 3
+ receipt2 = receipt_form.save()
+ receipt2.action_confirm()
+
+ # ... and valid the first one.
+ receipt_form = Form(receipt)
+ with receipt_form.move_ids_without_package.edit(0) as move_line:
+ move_line.quantity_done = 2
+ receipt = receipt_form.save()
+ receipt.button_validate()
+
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 2, "Still must have 2 line.")
+ self.assertEqual(draft_picking_qty['in'], 0)
+ self.assertEqual(draft_picking_qty['out'], 0)
+ line1 = lines[0]
+ line2 = lines[1]
+ # First line must be fulfilled thanks to the stock on hand.
+ self.assertEqual(line1['quantity'], 2)
+ self.assertEqual(line1['replenishment_filled'], True)
+ self.assertEqual(line1['document_in'], False)
+ self.assertEqual(line1['document_out'].id, delivery.id)
+ # Second line must be linked to the second receipt.
+ self.assertEqual(line2['quantity'], 3)
+ self.assertEqual(line2['replenishment_filled'], True)
+ self.assertEqual(line2['document_in'].id, receipt2.id)
+ self.assertEqual(line2['document_out'].id, delivery.id)
+
+ def test_report_forecast_2_replenishments_order(self):
+ """ Creates a receipt then creates a delivery using half of the receipt quantity.
+ Checks replenishment lines are correctly sorted (assigned first, unassigned at the end).
+ """
+ # Creates a receipt then checks draft picking quantities.
+ receipt_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ receipt_form.partner_id = self.partner
+ receipt_form.picking_type_id = self.picking_type_in
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 6
+ receipt = receipt_form.save()
+ receipt.action_confirm()
+
+ # Creates a delivery then checks draft picking quantities.
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 3
+ delivery = delivery_form.save()
+ delivery.action_confirm()
+
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ self.assertEqual(len(lines), 2, "Must have 2 line.")
+ line_1 = lines[0]
+ line_2 = lines[1]
+ self.assertEqual(line_1['document_in'].id, receipt.id)
+ self.assertEqual(line_1['document_out'].id, delivery.id)
+ self.assertEqual(line_2['document_in'].id, receipt.id)
+ self.assertEqual(line_2['document_out'], False)
+
+ def test_report_forecast_3_sort_by_date(self):
+ """ Creates some deliveries with different dates and checks the report
+ lines are correctly sorted by date. Then, creates some receipts and
+ check their are correctly linked according to their date.
+ """
+ today = datetime.today()
+ one_hours = timedelta(hours=1)
+ one_day = timedelta(days=1)
+ one_month = timedelta(days=30)
+ # Creates a bunch of deliveries with different date.
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ delivery_form.scheduled_date = today
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 5
+ delivery_1 = delivery_form.save()
+ delivery_1.action_confirm()
+
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ delivery_form.scheduled_date = today + one_hours
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 5
+ delivery_2 = delivery_form.save()
+ delivery_2.action_confirm()
+
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ delivery_form.scheduled_date = today - one_hours
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 5
+ delivery_3 = delivery_form.save()
+ delivery_3.action_confirm()
+
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ delivery_form.scheduled_date = today + one_day
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 5
+ delivery_4 = delivery_form.save()
+ delivery_4.action_confirm()
+
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ delivery_form.scheduled_date = today - one_day
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 5
+ delivery_5 = delivery_form.save()
+ delivery_5.action_confirm()
+
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ delivery_form.scheduled_date = today + one_month
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 5
+ delivery_6 = delivery_form.save()
+ delivery_6.action_confirm()
+
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ delivery_form.scheduled_date = today - one_month
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 5
+ delivery_7 = delivery_form.save()
+ delivery_7.action_confirm()
+
+ # Order must be: 7, 5, 3, 1, 2, 4, 6
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 7, "The report must have 7 line.")
+ self.assertEqual(draft_picking_qty['in'], 0)
+ self.assertEqual(draft_picking_qty['out'], 0)
+ self.assertEqual(lines[0]['document_out'].id, delivery_7.id)
+ self.assertEqual(lines[1]['document_out'].id, delivery_5.id)
+ self.assertEqual(lines[2]['document_out'].id, delivery_3.id)
+ self.assertEqual(lines[3]['document_out'].id, delivery_1.id)
+ self.assertEqual(lines[4]['document_out'].id, delivery_2.id)
+ self.assertEqual(lines[5]['document_out'].id, delivery_4.id)
+ self.assertEqual(lines[6]['document_out'].id, delivery_6.id)
+
+ # Creates 3 receipts for 20 units.
+ receipt_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ receipt_form.partner_id = self.partner
+ receipt_form.picking_type_id = self.picking_type_in
+ receipt_form.scheduled_date = today + one_month
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 5
+ receipt_1 = receipt_form.save()
+ receipt_1.action_confirm()
+
+ receipt_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ receipt_form.partner_id = self.partner
+ receipt_form.picking_type_id = self.picking_type_in
+ receipt_form.scheduled_date = today - one_month
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 5
+ receipt_2 = receipt_form.save()
+ receipt_2.action_confirm()
+
+ receipt_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ receipt_form.partner_id = self.partner
+ receipt_form.picking_type_id = self.picking_type_in
+ receipt_form.scheduled_date = today - one_hours
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 10
+ receipt_3 = receipt_form.save()
+ receipt_3.action_confirm()
+
+ # Check report lines (link and order).
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 7, "The report must have 7 line.")
+ self.assertEqual(draft_picking_qty['in'], 0)
+ self.assertEqual(draft_picking_qty['out'], 0)
+ self.assertEqual(lines[0]['document_out'].id, delivery_7.id)
+ self.assertEqual(lines[0]['document_in'].id, receipt_2.id)
+ self.assertEqual(lines[0]['is_late'], False)
+ self.assertEqual(lines[1]['document_out'].id, delivery_5.id)
+ self.assertEqual(lines[1]['document_in'].id, receipt_3.id)
+ self.assertEqual(lines[1]['is_late'], True)
+ self.assertEqual(lines[2]['document_out'].id, delivery_3.id)
+ self.assertEqual(lines[2]['document_in'].id, receipt_3.id)
+ self.assertEqual(lines[2]['is_late'], False)
+ self.assertEqual(lines[3]['document_out'].id, delivery_1.id)
+ self.assertEqual(lines[3]['document_in'].id, receipt_1.id)
+ self.assertEqual(lines[3]['is_late'], True)
+ self.assertEqual(lines[4]['document_out'].id, delivery_2.id)
+ self.assertEqual(lines[4]['document_in'], False)
+ self.assertEqual(lines[5]['document_out'].id, delivery_4.id)
+ self.assertEqual(lines[5]['document_in'], False)
+ self.assertEqual(lines[6]['document_out'].id, delivery_6.id)
+ self.assertEqual(lines[6]['document_in'], False)
+
+ def test_report_forecast_4_intermediate_transfers(self):
+ """ Create a receipt in 3 steps and check the report line.
+ """
+ grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
+ grp_multi_routes = self.env.ref('stock.group_adv_location')
+ self.env.user.write({'groups_id': [(4, grp_multi_loc.id)]})
+ self.env.user.write({'groups_id': [(4, grp_multi_routes.id)]})
+ # Warehouse config.
+ warehouse = self.env.ref('stock.warehouse0')
+ warehouse.reception_steps = 'three_steps'
+ # Product config.
+ self.product.write({'route_ids': [(4, self.env.ref('stock.route_warehouse0_mto').id)]})
+ # Create a RR
+ pg1 = self.env['procurement.group'].create({})
+ reordering_rule = self.env['stock.warehouse.orderpoint'].create({
+ 'name': 'Product RR',
+ 'location_id': warehouse.lot_stock_id.id,
+ 'product_id': self.product.id,
+ 'product_min_qty': 5,
+ 'product_max_qty': 10,
+ 'group_id': pg1.id,
+ })
+ reordering_rule.action_replenish()
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ pickings = self.env['stock.picking'].search([('product_id', '=', self.product.id)])
+ receipt = pickings.filtered(lambda p: p.picking_type_id.id == self.picking_type_in.id)
+
+ # The Forecasted Report don't show intermediate moves, it must display only ingoing/outgoing documents.
+ self.assertEqual(len(lines), 1, "The report must have only 1 line.")
+ self.assertEqual(lines[0]['document_in'].id, receipt.id, "The report must only show the receipt.")
+ self.assertEqual(lines[0]['document_out'], False)
+ self.assertEqual(lines[0]['quantity'], reordering_rule.product_max_qty)
+
+ def test_report_forecast_5_multi_warehouse(self):
+ """ Create some transfer for two different warehouses and check the
+ report display the good moves according to the selected warehouse.
+ """
+ # Warehouse config.
+ wh_2 = self.env['stock.warehouse'].create({
+ 'name': 'Evil Twin Warehouse',
+ 'code': 'ETWH',
+ })
+ picking_type_out_2 = self.env['stock.picking.type'].search([
+ ('code', '=', 'outgoing'),
+ ('warehouse_id', '=', wh_2.id),
+ ])
+
+ # Creates a delivery then checks draft picking quantities.
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ delivery = delivery_form.save()
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 5
+ delivery = delivery_form.save()
+
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 0, "Must have 0 line.")
+ self.assertEqual(draft_picking_qty['out'], 5)
+
+ report_values, docs, lines = self.get_report_forecast(
+ product_template_ids=self.product_template.ids,
+ context={'warehouse': wh_2.id},
+ )
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 0)
+ self.assertEqual(draft_picking_qty['out'], 0)
+
+ # Confirm the delivery -> The report must now have 1 line.
+ delivery.action_confirm()
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 1)
+ self.assertEqual(draft_picking_qty['out'], 0)
+ self.assertEqual(lines[0]['document_out'].id, delivery.id)
+ self.assertEqual(lines[0]['quantity'], 5)
+
+ report_values, docs, lines = self.get_report_forecast(
+ product_template_ids=self.product_template.ids,
+ context={'warehouse': wh_2.id},
+ )
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 0)
+ self.assertEqual(draft_picking_qty['out'], 0)
+
+ # Creates a delivery for the second warehouse.
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = picking_type_out_2
+ delivery_2 = delivery_form.save()
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 8
+ delivery_2 = delivery_form.save()
+
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 1)
+ self.assertEqual(draft_picking_qty['out'], 0)
+ self.assertEqual(lines[0]['document_out'].id, delivery.id)
+ self.assertEqual(lines[0]['quantity'], 5)
+
+ report_values, docs, lines = self.get_report_forecast(
+ product_template_ids=self.product_template.ids,
+ context={'warehouse': wh_2.id},
+ )
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 0)
+ self.assertEqual(draft_picking_qty['out'], 8)
+ # Confirm the second delivery -> The report must now have 1 line.
+ delivery_2.action_confirm()
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 1)
+ self.assertEqual(draft_picking_qty['out'], 0)
+ self.assertEqual(lines[0]['document_out'].id, delivery.id)
+ self.assertEqual(lines[0]['quantity'], 5)
+
+ report_values, docs, lines = self.get_report_forecast(
+ product_template_ids=self.product_template.ids,
+ context={'warehouse': wh_2.id},
+ )
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 1)
+ self.assertEqual(draft_picking_qty['out'], 0)
+ self.assertEqual(lines[0]['document_out'].id, delivery_2.id)
+ self.assertEqual(lines[0]['quantity'], 8)
+
+ def test_report_forecast_6_multi_company(self):
+ """ Create transfers for two different companies and check report
+ display the right transfers.
+ """
+ # Configure second warehouse.
+ company_2 = self.env['res.company'].create({'name': 'Aperture Science'})
+ wh_2 = self.env['stock.warehouse'].search([('company_id', '=', company_2.id)])
+ wh_2_picking_type_in = wh_2.in_type_id
+
+ # Creates a receipt then checks draft picking quantities.
+ receipt_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ receipt_form.partner_id = self.partner
+ receipt_form.picking_type_id = self.picking_type_in
+ wh_1_receipt = receipt_form.save()
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 2
+ wh_1_receipt = receipt_form.save()
+
+ # Creates a receipt then checks draft picking quantities.
+ receipt_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ receipt_form.partner_id = self.partner
+ receipt_form.picking_type_id = wh_2_picking_type_in
+ wh_2_receipt = receipt_form.save()
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 5
+ wh_2_receipt = receipt_form.save()
+
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 0, "Must have 0 line.")
+ self.assertEqual(draft_picking_qty['in'], 2)
+ self.assertEqual(draft_picking_qty['out'], 0)
+
+ report_values, docs, lines = self.get_report_forecast(
+ product_template_ids=self.product_template.ids,
+ context={'warehouse': wh_2.id},
+ )
+ draft_picking_qty = docs['draft_picking_qty']
+ self.assertEqual(len(lines), 0, "Must have 0 line.")
+ self.assertEqual(draft_picking_qty['in'], 5)
+ self.assertEqual(draft_picking_qty['out'], 0)
+
+ # Confirm the receipts -> The report must now have one line for each company.
+ wh_1_receipt.action_confirm()
+ wh_2_receipt.action_confirm()
+
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ self.assertEqual(len(lines), 1, "Must have 1 line.")
+ self.assertEqual(lines[0]['document_in'].id, wh_1_receipt.id)
+ self.assertEqual(lines[0]['quantity'], 2)
+
+ report_values, docs, lines = self.get_report_forecast(
+ product_template_ids=self.product_template.ids,
+ context={'warehouse': wh_2.id},
+ )
+ self.assertEqual(len(lines), 1, "Must have 1 line.")
+ self.assertEqual(lines[0]['document_in'].id, wh_2_receipt.id)
+ self.assertEqual(lines[0]['quantity'], 5)
+
+ def test_report_forecast_7_multiple_variants(self):
+ """ Create receipts for different variant products and check the report
+ work well with them.Also, check the receipt/delivery lines are correctly
+ linked depending of their product variant.
+ """
+ # Create some variant's attributes.
+ product_attr_color = self.env['product.attribute'].create({'name': 'Color'})
+ color_gray = self.env['product.attribute.value'].create({
+ 'name': 'Old Fashioned Gray',
+ 'attribute_id': product_attr_color.id,
+ })
+ color_blue = self.env['product.attribute.value'].create({
+ 'name': 'Electric Blue',
+ 'attribute_id': product_attr_color.id,
+ })
+ product_attr_size = self.env['product.attribute'].create({'name': 'size'})
+ size_pocket = self.env['product.attribute.value'].create({
+ 'name': 'Pocket',
+ 'attribute_id': product_attr_size.id,
+ })
+ size_xl = self.env['product.attribute.value'].create({
+ 'name': 'XL',
+ 'attribute_id': product_attr_size.id,
+ })
+
+ # Create a new product and set some variants on the product.
+ product_template = self.env['product.template'].create({
+ 'name': 'Game Joy',
+ 'type': 'product',
+ 'attribute_line_ids': [
+ (0, 0, {
+ 'attribute_id': product_attr_color.id,
+ 'value_ids': [(6, 0, [color_gray.id, color_blue.id])]
+ }),
+ (0, 0, {
+ 'attribute_id': product_attr_size.id,
+ 'value_ids': [(6, 0, [size_pocket.id, size_xl.id])]
+ }),
+ ],
+ })
+ gamejoy_pocket_gray = product_template.product_variant_ids[0]
+ gamejoy_xl_gray = product_template.product_variant_ids[1]
+ gamejoy_pocket_blue = product_template.product_variant_ids[2]
+ gamejoy_xl_blue = product_template.product_variant_ids[3]
+
+ # Create two receipts.
+ receipt_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ receipt_form.partner_id = self.partner
+ receipt_form.picking_type_id = self.picking_type_in
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = gamejoy_pocket_gray
+ move_line.product_uom_qty = 8
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = gamejoy_pocket_blue
+ move_line.product_uom_qty = 4
+ receipt_1 = receipt_form.save()
+ receipt_1.action_confirm()
+
+ receipt_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ receipt_form.partner_id = self.partner
+ receipt_form.picking_type_id = self.picking_type_in
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = gamejoy_pocket_gray
+ move_line.product_uom_qty = 2
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = gamejoy_xl_gray
+ move_line.product_uom_qty = 10
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = gamejoy_xl_blue
+ move_line.product_uom_qty = 12
+ receipt_2 = receipt_form.save()
+ receipt_2.action_confirm()
+
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=product_template.ids)
+ self.assertEqual(len(lines), 5, "Must have 5 lines.")
+ self.assertEqual(docs['product_variants'].ids, product_template.product_variant_ids.ids)
+
+ # Create a delivery for one of these products and check the report lines
+ # are correctly linked to the good receipts.
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = gamejoy_pocket_gray
+ move_line.product_uom_qty = 10
+ delivery = delivery_form.save()
+ delivery.action_confirm()
+
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=product_template.ids)
+ self.assertEqual(len(lines), 5, "Still must have 5 lines.")
+ self.assertEqual(docs['product_variants'].ids, product_template.product_variant_ids.ids)
+ # First and second lines should be about the "Game Joy Pocket (gray)"
+ # and must link the delivery with the two receipt lines.
+ line_1 = lines[0]
+ line_2 = lines[1]
+ self.assertEqual(line_1['product']['id'], gamejoy_pocket_gray.id)
+ self.assertEqual(line_1['quantity'], 8)
+ self.assertTrue(line_1['replenishment_filled'])
+ self.assertEqual(line_1['document_in'].id, receipt_1.id)
+ self.assertEqual(line_1['document_out'].id, delivery.id)
+ self.assertEqual(line_2['product']['id'], gamejoy_pocket_gray.id)
+ self.assertEqual(line_2['quantity'], 2)
+ self.assertTrue(line_2['replenishment_filled'])
+ self.assertEqual(line_2['document_in'].id, receipt_2.id)
+ self.assertEqual(line_2['document_out'].id, delivery.id)
+
+ def test_report_forecast_8_delivery_to_receipt_link(self):
+ """
+ Create 2 deliveries, and 1 receipt tied to the second delivery.
+ The report should show the source document as the 2nd delivery, and show the first
+ delivery completely unfilled.
+ """
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 100
+ delivery = delivery_form.save()
+ delivery.action_confirm()
+
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 200
+ delivery2 = delivery_form.save()
+ delivery2.action_confirm()
+
+ receipt_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ receipt_form.partner_id = self.partner
+ receipt_form.picking_type_id = self.picking_type_in
+ receipt = receipt_form.save()
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 200
+ receipt = receipt_form.save()
+ receipt.move_lines[0].write({
+ 'move_dest_ids': [(4, delivery2.move_lines[0].id)],
+ })
+ receipt.action_confirm()
+ self.env['base'].flush()
+
+ _, _, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+
+ self.assertEqual(len(lines), 2, 'Only 2 lines')
+ delivery_line = [l for l in lines if l['document_out'].id == delivery.id][0]
+ self.assertTrue(delivery_line, 'No line for delivery 1')
+ self.assertFalse(delivery_line['replenishment_filled'])
+ delivery2_line = [l for l in lines if l['document_out'].id == delivery2.id][0]
+ self.assertTrue(delivery2_line, 'No line for delivery 2')
+ self.assertTrue(delivery2_line['replenishment_filled'])
+
+ def test_report_forecast_9_delivery_to_receipt_link_over_received(self):
+ """
+ Create 2 deliveries, and 1 receipt tied to the second delivery.
+ Set the quantity on the receipt to be enough for BOTH deliveries.
+ For example, this can happen if they have manually increased the quantity on the generated PO.
+ The report should show both deliveries fulfilled.
+ """
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 100
+ delivery = delivery_form.save()
+ delivery.action_confirm()
+
+ delivery_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ delivery_form.partner_id = self.partner
+ delivery_form.picking_type_id = self.picking_type_out
+ with delivery_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 200
+ delivery2 = delivery_form.save()
+ delivery2.action_confirm()
+
+ receipt_form = Form(self.env['stock.picking'].with_context(
+ force_detailed_view=True
+ ), view='stock.view_picking_form')
+ receipt_form.partner_id = self.partner
+ receipt_form.picking_type_id = self.picking_type_in
+ receipt = receipt_form.save()
+ with receipt_form.move_ids_without_package.new() as move_line:
+ move_line.product_id = self.product
+ move_line.product_uom_qty = 300
+ receipt = receipt_form.save()
+ receipt.move_lines[0].write({
+ 'move_dest_ids': [(4, delivery2.move_lines[0].id)],
+ })
+ receipt.action_confirm()
+ self.env['base'].flush()
+
+ _, _, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+
+ self.assertEqual(len(lines), 2, 'Only 2 lines')
+ delivery_line = [l for l in lines if l['document_out'].id == delivery.id][0]
+ self.assertTrue(delivery_line, 'No line for delivery 1')
+ self.assertTrue(delivery_line['replenishment_filled'])
+ delivery2_line = [l for l in lines if l['document_out'].id == delivery2.id][0]
+ self.assertTrue(delivery2_line, 'No line for delivery 2')
+ self.assertTrue(delivery2_line['replenishment_filled'])
diff --git a/addons/stock/tests/test_report_stock_quantity.py b/addons/stock/tests/test_report_stock_quantity.py
new file mode 100644
index 00000000..cdf0e8d5
--- /dev/null
+++ b/addons/stock/tests/test_report_stock_quantity.py
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import timedelta
+
+from odoo import fields, tests
+from odoo.tests.common import Form
+
+
+class TestReportStockQuantity(tests.TransactionCase):
+ def setUp(self):
+ super().setUp()
+ self.product1 = self.env['product.product'].create({
+ 'name': 'Mellohi',
+ 'default_code': 'C418',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'tracking': 'lot',
+ 'barcode': 'scan_me'
+ })
+ self.wh = self.env['stock.warehouse'].create({
+ 'name': 'Base Warehouse',
+ 'code': 'TESTWH'
+ })
+ self.categ_unit = self.env.ref('uom.product_uom_categ_unit')
+ self.uom_unit = self.env['uom.uom'].search([('category_id', '=', self.categ_unit.id), ('uom_type', '=', 'reference')], limit=1)
+ self.customer_location = self.env.ref('stock.stock_location_customers')
+ self.supplier_location = self.env.ref('stock.stock_location_suppliers')
+ # replenish
+ self.move1 = self.env['stock.move'].create({
+ 'name': 'test_in_1',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.wh.lot_stock_id.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100.0,
+ 'state': 'done',
+ 'date': fields.Datetime.now(),
+ })
+ self.quant1 = self.env['stock.quant'].create({
+ 'product_id': self.product1.id,
+ 'location_id': self.wh.lot_stock_id.id,
+ 'quantity': 100.0,
+ })
+ # ship
+ self.move2 = self.env['stock.move'].create({
+ 'name': 'test_out_1',
+ 'location_id': self.wh.lot_stock_id.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 120.0,
+ 'state': 'partially_available',
+ 'date': fields.Datetime.add(fields.Datetime.now(), days=3),
+ 'date_deadline': fields.Datetime.add(fields.Datetime.now(), days=3),
+ })
+ self.env['base'].flush()
+
+ def test_report_stock_quantity(self):
+ from_date = fields.Date.to_string(fields.Date.add(fields.Date.today(), days=-1))
+ to_date = fields.Date.to_string(fields.Date.add(fields.Date.today(), days=4))
+ report = self.env['report.stock.quantity'].read_group(
+ [('date', '>=', from_date), ('date', '<=', to_date), ('product_id', '=', self.product1.id)],
+ ['product_qty', 'date', 'product_id', 'state'],
+ ['date:day', 'product_id', 'state'],
+ lazy=False)
+ forecast_report = [x['product_qty'] for x in report if x['state'] == 'forecast']
+ self.assertEqual(forecast_report, [0, 100, 100, 100, -20, -20])
+
+ def test_report_stock_quantity_with_product_qty_filter(self):
+ from_date = fields.Date.to_string(fields.Date.add(fields.Date.today(), days=-1))
+ to_date = fields.Date.to_string(fields.Date.add(fields.Date.today(), days=4))
+ report = self.env['report.stock.quantity'].read_group(
+ [('product_qty', '<', 0), ('date', '>=', from_date), ('date', '<=', to_date), ('product_id', '=', self.product1.id)],
+ ['product_qty', 'date', 'product_id', 'state'],
+ ['date:day', 'product_id', 'state'],
+ lazy=False)
+ forecast_report = [x['product_qty'] for x in report if x['state'] == 'forecast']
+ self.assertEqual(forecast_report, [-20, -20])
+
+ def test_replenishment_report_1(self):
+ self.product_replenished = self.env['product.product'].create({
+ 'name': 'Security razor',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ # get auto-created pull rule from when warehouse is created
+ self.wh.reception_route_id.rule_ids.unlink()
+ self.env['stock.rule'].create({
+ 'name': 'Rule Supplier',
+ 'route_id': self.wh.reception_route_id.id,
+ 'location_id': self.wh.lot_stock_id.id,
+ 'location_src_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'action': 'pull',
+ 'delay': 1.0,
+ 'procure_method': 'make_to_stock',
+ 'picking_type_id': self.wh.in_type_id.id,
+ })
+ delivery_picking = self.env['stock.picking'].create({
+ 'location_id': self.wh.lot_stock_id.id,
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'picking_type_id': self.ref('stock.picking_type_out'),
+ })
+ self.env['stock.move'].create({
+ 'name': 'Delivery',
+ 'product_id': self.product_replenished.id,
+ 'product_uom_qty': 500.0,
+ 'product_uom': self.uom_unit.id,
+ 'location_id': self.wh.lot_stock_id.id,
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'picking_id': delivery_picking.id,
+ })
+ delivery_picking.action_confirm()
+
+ # Trigger the manual orderpoint creation for missing product
+ self.env['stock.move'].flush()
+ self.env['stock.warehouse.orderpoint'].action_open_orderpoints()
+
+ orderpoint = self.env['stock.warehouse.orderpoint'].search([
+ ('product_id', '=', self.product_replenished.id)
+ ])
+ self.assertTrue(orderpoint)
+ self.assertEqual(orderpoint.location_id, self.wh.lot_stock_id)
+ self.assertEqual(orderpoint.qty_to_order, 500.0)
+ orderpoint.action_replenish()
+ self.env['stock.warehouse.orderpoint'].action_open_orderpoints()
+
+ move = self.env['stock.move'].search([
+ ('product_id', '=', self.product_replenished.id),
+ ('location_dest_id', '=', self.wh.lot_stock_id.id)
+ ])
+ # Simulate a supplier delay
+ move.date = fields.datetime.now() + timedelta(days=1)
+ orderpoint = self.env['stock.warehouse.orderpoint'].search([
+ ('product_id', '=', self.product_replenished.id)
+ ])
+ self.assertFalse(orderpoint)
+
+ orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
+ orderpoint_form.product_id = self.product_replenished
+ orderpoint_form.location_id = self.wh.lot_stock_id
+ orderpoint = orderpoint_form.save()
+
+ self.assertEqual(orderpoint.qty_to_order, 0.0)
+ self.env['stock.warehouse.orderpoint'].action_open_orderpoints()
+ self.assertEqual(orderpoint.qty_to_order, 0.0)
diff --git a/addons/stock/tests/test_report_tours.py b/addons/stock/tests/test_report_tours.py
new file mode 100644
index 00000000..0958ed55
--- /dev/null
+++ b/addons/stock/tests/test_report_tours.py
@@ -0,0 +1,19 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import odoo
+from odoo.tests import Form, HttpCase, tagged
+
+
+@tagged('-at_install', 'post_install')
+class TestStockReportTour(HttpCase):
+ def setUp(self):
+ super().setUp()
+
+ def _get_report_url(self):
+ return '/web#&model=product.template&action=stock.product_template_action_product'
+
+
+ def test_stock_route_diagram_report(self):
+ """ Open the route diagram report."""
+ url = self._get_report_url()
+
+ self.start_tour(url, 'test_stock_route_diagram_report', login='admin', timeout=180)
diff --git a/addons/stock/tests/test_robustness.py b/addons/stock/tests/test_robustness.py
new file mode 100644
index 00000000..ef039188
--- /dev/null
+++ b/addons/stock/tests/test_robustness.py
@@ -0,0 +1,223 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.exceptions import UserError, ValidationError
+from odoo.tests.common import SavepointCase
+
+
+class TestRobustness(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestRobustness, cls).setUpClass()
+ cls.stock_location = cls.env.ref('stock.stock_location_stock')
+ cls.customer_location = cls.env.ref('stock.stock_location_customers')
+ cls.uom_unit = cls.env.ref('uom.product_uom_unit')
+ cls.uom_dozen = cls.env.ref('uom.product_uom_dozen')
+ cls.product1 = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+
+ def test_uom_factor(self):
+ """ Changing the factor of a unit of measure shouldn't be allowed while
+ quantities are reserved, else the existing move lines won't be consistent
+ with the `reserved_quantity` on quants.
+ """
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(
+ self.product1,
+ self.stock_location,
+ 12,
+ )
+
+ # reserve a dozen
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_uom_rounding',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_dozen.id,
+ 'product_uom_qty': 1,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ quant = self.env['stock.quant']._gather(
+ self.product1,
+ self.stock_location,
+ )
+
+ # assert the reservation
+ self.assertEqual(quant.reserved_quantity, 12)
+ self.assertEqual(move1.product_qty, 12)
+
+ # change the factor
+ with self.assertRaises(UserError):
+ with self.cr.savepoint():
+ move1.product_uom.factor = 0.05
+
+ # assert the reservation
+ self.assertEqual(quant.reserved_quantity, 12)
+ self.assertEqual(move1.state, 'assigned')
+ self.assertEqual(move1.product_qty, 12)
+
+ # unreserve
+ move1._do_unreserve()
+
+ def test_location_usage(self):
+ """ Changing the usage of a location shouldn't be allowed while
+ quantities are reserved, else the existing move lines won't be
+ consistent with the `reserved_quantity` on the quants.
+ """
+ # change stock usage
+ test_stock_location = self.env['stock.location'].create({
+ 'name': "Test Location",
+ 'location_id': self.stock_location.id,
+ })
+ test_stock_location.scrap_location = True
+
+ # make some stock
+ self.env['stock.quant']._update_available_quantity(
+ self.product1,
+ test_stock_location,
+ 1,
+ )
+
+ # reserve a unit
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_location_archive',
+ 'location_id': test_stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ self.assertEqual(move1.state, 'assigned')
+ quant = self.env['stock.quant']._gather(
+ self.product1,
+ test_stock_location,
+ )
+
+ # assert the reservation
+ self.assertEqual(quant.reserved_quantity, 0) # reservation is bypassed in scrap location
+ self.assertEqual(move1.product_qty, 1)
+
+ # change the stock usage
+ with self.assertRaises(UserError):
+ with self.cr.savepoint():
+ test_stock_location.scrap_location = False
+
+ # unreserve
+ move1._do_unreserve()
+
+ def test_package_unpack(self):
+ """ Unpack a package that contains quants with a reservation
+ should also remove the package on the reserved move lines.
+ """
+ package = self.env['stock.quant.package'].create({
+ 'name': 'Shell Helix HX7 10W30',
+ })
+
+ self.env['stock.quant']._update_available_quantity(
+ self.product1,
+ self.stock_location,
+ 10,
+ package_id=package
+ )
+
+ # reserve a dozen
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_uom_rounding',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(move1.move_line_ids.package_id, package)
+ package.unpack()
+ self.assertEqual(move1.move_line_ids.package_id, self.env['stock.quant.package'])
+
+ # unreserve
+ move1._do_unreserve()
+ self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1)
+ self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location, package_id=package)), 0)
+
+ self.assertEqual(self.env['stock.quant']._gather(self.product1, self.stock_location).reserved_quantity, 0)
+
+ def test_lot_id_product_id_mix(self):
+ """ Make sure it isn't possible to create a move line with a lot incompatible with its
+ product.
+ """
+ product1 = self.env['product.product'].create({
+ 'name': 'Product 1',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'tracking': 'lot',
+ })
+ product2 = self.env['product.product'].create({
+ 'name': 'Product 2',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'tracking': 'lot',
+ })
+
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': product1.id,
+ 'company_id': self.env.company.id,
+
+ })
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': product2.id,
+ 'company_id': self.env.company.id,
+ })
+
+ self.env['stock.quant']._update_available_quantity(product1, self.stock_location, 1, lot_id=lot1)
+ self.env['stock.quant']._update_available_quantity(product2, self.stock_location, 1, lot_id=lot2)
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_lot_id_product_id_mix_move_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_lot_id_product_id_mix_move_2',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': product2.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ (move1 + move2)._action_confirm()
+
+ with self.assertRaises(ValidationError):
+ move1.write({'move_line_ids': [(0, 0, {
+ 'product_id': product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 1,
+ 'lot_id': lot2.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ })]})
+
+ with self.assertRaises(ValidationError):
+ move2.write({'move_line_ids': [(0, 0, {
+ 'product_id': product2.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 1,
+ 'lot_id': lot1.id,
+ 'location_id': move2.location_id.id,
+ 'location_dest_id': move2.location_dest_id.id,
+ })]})
+
diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py
new file mode 100644
index 00000000..bf9782f4
--- /dev/null
+++ b/addons/stock/tests/test_stock_flow.py
@@ -0,0 +1,1978 @@
+# -*- coding: utf-8 -*-
+
+from odoo.addons.stock.tests.common import TestStockCommon
+from odoo.tests import Form
+from odoo.tools import mute_logger, float_round
+from odoo.exceptions import UserError
+from odoo import fields
+
+class TestStockFlow(TestStockCommon):
+ def setUp(cls):
+ super(TestStockFlow, cls).setUp()
+ decimal_product_uom = cls.env.ref('product.decimal_product_uom')
+ decimal_product_uom.digits = 3
+ cls.partner_company2 = cls.env['res.partner'].create({
+ '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):
+ """ Basic stock operation on incoming and outgoing shipment. """
+ LotObj = self.env['stock.production.lot']
+ # ----------------------------------------------------------------------
+ # Create incoming shipment of product A, B, C, D
+ # ----------------------------------------------------------------------
+ # Product A ( 1 Unit ) , Product C ( 10 Unit )
+ # Product B ( 1 Unit ) , Product D ( 10 Unit )
+ # Product D ( 5 Unit )
+ # ----------------------------------------------------------------------
+
+ picking_in = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_in,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ move_a = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 1,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_in.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ move_b = self.MoveObj.create({
+ 'name': self.productB.name,
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 1,
+ 'product_uom': self.productB.uom_id.id,
+ 'picking_id': picking_in.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ move_c = self.MoveObj.create({
+ 'name': self.productC.name,
+ 'product_id': self.productC.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productC.uom_id.id,
+ 'picking_id': picking_in.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ move_d = self.MoveObj.create({
+ 'name': self.productD.name,
+ 'product_id': self.productD.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productD.uom_id.id,
+ 'picking_id': picking_in.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': self.productD.name,
+ 'product_id': self.productD.id,
+ 'product_uom_qty': 5,
+ 'product_uom': self.productD.uom_id.id,
+ 'picking_id': picking_in.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+
+ # Check incoming shipment move lines state.
+ for move in picking_in.move_lines:
+ self.assertEqual(move.state, 'draft', 'Wrong state of move line.')
+ # Confirm incoming shipment.
+ picking_in.action_confirm()
+ # Check incoming shipment move lines state.
+ for move in picking_in.move_lines:
+ self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
+
+ # ----------------------------------------------------------------------
+ # Replace pack operation of incoming shipments.
+ # ----------------------------------------------------------------------
+ picking_in.action_assign()
+ move_a.move_line_ids.qty_done = 4
+ move_b.move_line_ids.qty_done = 5
+ move_c.move_line_ids.qty_done = 5
+ move_d.move_line_ids.qty_done = 5
+ lot2_productC = LotObj.create({'name': 'C Lot 2', 'product_id': self.productC.id, 'company_id': self.env.company.id})
+ self.StockPackObj.create({
+ 'product_id': self.productC.id,
+ 'qty_done': 2,
+ 'product_uom_id': self.productC.uom_id.id,
+ 'location_id': self.supplier_location,
+ '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,
+ 'product_uom_id': self.productD.uom_id.id,
+ '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'))
+ self.assertEqual(total_qty, 23, 'Wrong quantity in pack operation')
+
+ # Transfer Incoming Shipment.
+ picking_in._action_done()
+
+ # ----------------------------------------------------------------------
+ # Check state, quantity and total moves of incoming shipment.
+ # ----------------------------------------------------------------------
+
+ # Check total no of move lines of incoming shipment. move line e disappear from original picking to go in backorder.
+ self.assertEqual(len(picking_in.move_lines), 4, 'Wrong number of move lines.')
+ # Check incoming shipment state.
+ self.assertEqual(picking_in.state, 'done', 'Incoming shipment state should be done.')
+ # Check incoming shipment move lines state.
+ for move in picking_in.move_lines:
+ self.assertEqual(move.state, 'done', 'Wrong state of move line.')
+ # Check product A done quantity must be 3 and 1
+ moves = self.MoveObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', picking_in.id)])
+ self.assertEqual(moves.product_uom_qty, 4.0, 'Wrong move quantity for product A.')
+ # Check product B done quantity must be 4 and 1
+ moves = self.MoveObj.search([('product_id', '=', self.productB.id), ('picking_id', '=', picking_in.id)])
+ self.assertEqual(moves.product_uom_qty, 5.0, 'Wrong move quantity for product B.')
+ # Check product C done quantity must be 7
+ c_done_qty = self.MoveObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', picking_in.id)], limit=1).product_uom_qty
+ self.assertEqual(c_done_qty, 7.0, 'Wrong move quantity of product C (%s found instead of 7)' % (c_done_qty))
+ # Check product D done quantity must be 7
+ d_done_qty = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', picking_in.id)], limit=1).product_uom_qty
+ self.assertEqual(d_done_qty, 7.0, 'Wrong move quantity of product D (%s found instead of 7)' % (d_done_qty))
+
+ # ----------------------------------------------------------------------
+ # Check Back order of Incoming shipment.
+ # ----------------------------------------------------------------------
+
+ # Check back order created or not.
+ back_order_in = self.PickingObj.search([('backorder_id', '=', picking_in.id)])
+ self.assertEqual(len(back_order_in), 1, 'Back order should be created.')
+ # Check total move lines of back order.
+ self.assertEqual(len(back_order_in.move_lines), 2, 'Wrong number of move lines.')
+ # Check back order should be created with 3 quantity of product C.
+ moves = self.MoveObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', back_order_in.id)])
+ product_c_qty = [move.product_uom_qty for move in moves]
+ self.assertEqual(sum(product_c_qty), 3.0, 'Wrong move quantity of product C (%s found instead of 3)' % (product_c_qty))
+ # Check back order should be created with 8 quantity of product D.
+ moves = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', back_order_in.id)])
+ product_d_qty = [move.product_uom_qty for move in moves]
+ self.assertEqual(sum(product_d_qty), 8.0, 'Wrong move quantity of product D (%s found instead of 8)' % (product_d_qty))
+
+ # ======================================================================
+ # Create Outgoing shipment with ...
+ # product A ( 10 Unit ) , product B ( 5 Unit )
+ # product C ( 3 unit ) , product D ( 10 Unit )
+ # ======================================================================
+
+ picking_out = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ move_cust_a = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ move_cust_b = self.MoveObj.create({
+ 'name': self.productB.name,
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 5,
+ 'product_uom': self.productB.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ move_cust_c = self.MoveObj.create({
+ 'name': self.productC.name,
+ 'product_id': self.productC.id,
+ 'product_uom_qty': 3,
+ 'product_uom': self.productC.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ move_cust_d = self.MoveObj.create({
+ 'name': self.productD.name,
+ 'product_id': self.productD.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productD.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ # Confirm outgoing shipment.
+ picking_out.action_confirm()
+ for move in picking_out.move_lines:
+ self.assertEqual(move.state, 'confirmed', 'Wrong state of move line.')
+ # Product assign to outgoing shipments
+ picking_out.action_assign()
+ self.assertEqual(move_cust_a.state, 'partially_available', 'Wrong state of move line.')
+ self.assertEqual(move_cust_b.state, 'assigned', 'Wrong state of move line.')
+ self.assertEqual(move_cust_c.state, 'assigned', 'Wrong state of move line.')
+ self.assertEqual(move_cust_d.state, 'partially_available', 'Wrong state of move line.')
+ # Check availability for product A
+ aval_a_qty = self.MoveObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability
+ self.assertEqual(aval_a_qty, 4.0, 'Wrong move quantity availability of product A (%s found instead of 4)' % (aval_a_qty))
+ # Check availability for product B
+ aval_b_qty = self.MoveObj.search([('product_id', '=', self.productB.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability
+ self.assertEqual(aval_b_qty, 5.0, 'Wrong move quantity availability of product B (%s found instead of 5)' % (aval_b_qty))
+ # Check availability for product C
+ aval_c_qty = self.MoveObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability
+ self.assertEqual(aval_c_qty, 3.0, 'Wrong move quantity availability of product C (%s found instead of 3)' % (aval_c_qty))
+ # Check availability for product D
+ aval_d_qty = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability
+ self.assertEqual(aval_d_qty, 7.0, 'Wrong move quantity availability of product D (%s found instead of 7)' % (aval_d_qty))
+
+ # ----------------------------------------------------------------------
+ # Replace pack operation of outgoing shipment.
+ # ----------------------------------------------------------------------
+
+ move_cust_a.move_line_ids.qty_done = 2.0
+ move_cust_b.move_line_ids.qty_done = 3.0
+ self.StockPackObj.create({
+ 'product_id': self.productB.id,
+ 'qty_done': 2,
+ 'product_uom_id': self.productB.uom_id.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'move_id': move_cust_b.id})
+ # TODO care if product_qty and lot_id are set at the same times the system do 2 unreserve.
+ move_cust_c.move_line_ids[0].write({
+ 'qty_done': 2.0,
+ 'lot_id': lot2_productC.id,
+ })
+ self.StockPackObj.create({
+ 'product_id': self.productC.id,
+ 'qty_done': 3.0,
+ 'product_uom_id': self.productC.uom_id.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'move_id': move_cust_c.id})
+ move_cust_d.move_line_ids.qty_done = 6.0
+
+ # Transfer picking.
+ picking_out._action_done()
+
+ # ----------------------------------------------------------------------
+ # Check state, quantity and total moves of outgoing shipment.
+ # ----------------------------------------------------------------------
+
+ # check outgoing shipment status.
+ self.assertEqual(picking_out.state, 'done', 'Wrong state of outgoing shipment.')
+ # check outgoing shipment total moves and and its state.
+ self.assertEqual(len(picking_out.move_lines), 4, 'Wrong number of move lines')
+ for move in picking_out.move_lines:
+ self.assertEqual(move.state, 'done', 'Wrong state of move line.')
+ back_order_out = self.PickingObj.search([('backorder_id', '=', picking_out.id)])
+
+ # ------------------
+ # Check back order.
+ # -----------------
+
+ self.assertEqual(len(back_order_out), 1, 'Back order should be created.')
+ # Check total move lines of back order.
+ self.assertEqual(len(back_order_out.move_lines), 2, 'Wrong number of move lines')
+ # Check back order should be created with 8 quantity of product A.
+ product_a_qty = self.MoveObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', back_order_out.id)], limit=1).product_uom_qty
+ self.assertEqual(product_a_qty, 8.0, 'Wrong move quantity of product A (%s found instead of 8)' % (product_a_qty))
+ # Check back order should be created with 4 quantity of product D.
+ product_d_qty = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', back_order_out.id)], limit=1).product_uom_qty
+ self.assertEqual(product_d_qty, 4.0, 'Wrong move quantity of product D (%s found instead of 4)' % (product_d_qty))
+
+ # -----------------------------------------------------------------------
+ # Check stock location quant quantity and quantity available
+ # of product A, B, C, D
+ # -----------------------------------------------------------------------
+
+ # Check quants and available quantity for product A
+ quants = self.StockQuantObj.search([('product_id', '=', self.productA.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+
+ self.assertEqual(sum(total_qty), 2.0, 'Expecting 2.0 Unit , got %.4f Unit on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.productA.qty_available, 2.0, 'Wrong quantity available (%s found instead of 2.0)' % (self.productA.qty_available))
+ # Check quants and available quantity for product B
+ quants = self.StockQuantObj.search([('product_id', '=', self.productB.id), ('location_id', '=', self.stock_location), ('quantity', '!=', 0)])
+ self.assertFalse(quants, 'No quant should found as outgoing shipment took everything out of stock.')
+ self.assertEqual(self.productB.qty_available, 0.0, 'Product B should have zero quantity available.')
+ # Check quants and available quantity for product C
+ quants = self.StockQuantObj.search([('product_id', '=', self.productC.id), ('location_id', '=', self.stock_location), ('quantity', '!=', 0)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 2.0, 'Expecting 2.0 Unit, got %.4f Unit on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.productC.qty_available, 2.0, 'Wrong quantity available (%s found instead of 2.0)' % (self.productC.qty_available))
+ # Check quants and available quantity for product D
+ quant = self.StockQuantObj.search([('product_id', '=', self.productD.id), ('location_id', '=', self.stock_location), ('quantity', '!=', 0)], limit=1)
+ self.assertEqual(quant.quantity, 1.0, 'Expecting 1.0 Unit , got %.4f Unit on location stock!' % (quant.quantity))
+ self.assertEqual(self.productD.qty_available, 1.0, 'Wrong quantity available (%s found instead of 1.0)' % (self.productD.qty_available))
+
+ # -----------------------------------------------------------------------
+ # Back Order of Incoming shipment
+ # -----------------------------------------------------------------------
+
+ lot3_productC = LotObj.create({'name': 'Lot 3', 'product_id': self.productC.id, 'company_id': self.env.company.id})
+ lot4_productC = LotObj.create({'name': 'Lot 4', 'product_id': self.productC.id, 'company_id': self.env.company.id})
+ lot5_productC = LotObj.create({'name': 'Lot 5', 'product_id': self.productC.id, 'company_id': self.env.company.id})
+ lot6_productC = LotObj.create({'name': 'Lot 6', 'product_id': self.productC.id, 'company_id': self.env.company.id})
+ lot1_productD = LotObj.create({'name': 'Lot 1', 'product_id': self.productD.id, 'company_id': self.env.company.id})
+ LotObj.create({'name': 'Lot 2', 'product_id': self.productD.id, 'company_id': self.env.company.id})
+
+ # Confirm back order of incoming shipment.
+ back_order_in.action_confirm()
+ self.assertEqual(back_order_in.state, 'assigned', 'Wrong state of incoming shipment back order: %s instead of %s' % (back_order_in.state, 'assigned'))
+ for move in back_order_in.move_lines:
+ self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
+
+ # ----------------------------------------------------------------------
+ # Replace pack operation (Back order of Incoming shipment)
+ # ----------------------------------------------------------------------
+
+ packD = self.StockPackObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', back_order_in.id)], order='product_qty')
+ self.assertEqual(len(packD), 1, 'Wrong number of pack operation.')
+ packD[0].write({
+ 'qty_done': 8,
+ 'lot_id': lot1_productD.id,
+ })
+ packCs = self.StockPackObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', back_order_in.id)], limit=1)
+ packCs.write({
+ 'qty_done': 1,
+ 'lot_id': lot3_productC.id,
+ })
+ self.StockPackObj.create({
+ 'product_id': self.productC.id,
+ 'qty_done': 1,
+ 'product_uom_id': self.productC.uom_id.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'picking_id': back_order_in.id,
+ 'lot_id': lot4_productC.id,
+ })
+ self.StockPackObj.create({
+ 'product_id': self.productC.id,
+ 'qty_done': 2,
+ 'product_uom_id': self.productC.uom_id.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'picking_id': back_order_in.id,
+ 'lot_id': lot5_productC.id,
+ })
+ self.StockPackObj.create({
+ 'product_id': self.productC.id,
+ 'qty_done': 2,
+ 'product_uom_id': self.productC.uom_id.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'picking_id': back_order_in.id,
+ 'lot_id': lot6_productC.id,
+ })
+ self.StockPackObj.create({
+ 'product_id': self.productA.id,
+ 'qty_done': 10,
+ 'product_uom_id': self.productA.uom_id.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'picking_id': back_order_in.id
+ })
+ back_order_in._action_done()
+
+ # ----------------------------------------------------------------------
+ # Check state, quantity and total moves (Back order of Incoming shipment).
+ # ----------------------------------------------------------------------
+
+ # Check total no of move lines.
+ self.assertEqual(len(back_order_in.move_lines), 3, 'Wrong number of move lines')
+ # Check incoming shipment state must be 'Done'.
+ self.assertEqual(back_order_in.state, 'done', 'Wrong state of picking.')
+ # Check incoming shipment move lines state must be 'Done'.
+ for move in back_order_in.move_lines:
+ self.assertEqual(move.state, 'done', 'Wrong state of move lines.')
+ # Check product A done quantity must be 10
+ movesA = self.MoveObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', back_order_in.id)])
+ self.assertEqual(movesA.product_uom_qty, 10, "Wrong move quantity of product A (%s found instead of 10)" % (movesA.product_uom_qty))
+ # Check product C done quantity must be 3.0, 1.0, 2.0
+ movesC = self.MoveObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', back_order_in.id)])
+ self.assertEqual(movesC.product_uom_qty, 6.0, 'Wrong quantity of moves product C.')
+ # Check product D done quantity must be 5.0 and 3.0
+ movesD = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', back_order_in.id)])
+ d_done_qty = [move.product_uom_qty for move in movesD]
+ self.assertEqual(set(d_done_qty), set([8.0]), 'Wrong quantity of moves product D.')
+ # Check no back order is created.
+ self.assertFalse(self.PickingObj.search([('backorder_id', '=', back_order_in.id)]), "Should not create any back order.")
+
+ # -----------------------------------------------------------------------
+ # Check stock location quant quantity and quantity available
+ # of product A, B, C, D
+ # -----------------------------------------------------------------------
+
+ # Check quants and available quantity for product A.
+ quants = self.StockQuantObj.search([('product_id', '=', self.productA.id), ('location_id', '=', self.stock_location), ('quantity', '!=', 0)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 12.0, 'Wrong total stock location quantity (%s found instead of 12)' % (sum(total_qty)))
+ self.assertEqual(self.productA.qty_available, 12.0, 'Wrong quantity available (%s found instead of 12)' % (self.productA.qty_available))
+ # Check quants and available quantity for product B.
+ quants = self.StockQuantObj.search([('product_id', '=', self.productB.id), ('location_id', '=', self.stock_location), ('quantity', '!=', 0)])
+ self.assertFalse(quants, 'No quant should found as outgoing shipment took everything out of stock')
+ self.assertEqual(self.productB.qty_available, 0.0, 'Total quantity in stock should be 0 as the backorder took everything out of stock')
+ # Check quants and available quantity for product C.
+ quants = self.StockQuantObj.search([('product_id', '=', self.productC.id), ('location_id', '=', self.stock_location), ('quantity', '!=', 0)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 8.0, 'Wrong total stock location quantity (%s found instead of 8)' % (sum(total_qty)))
+ self.assertEqual(self.productC.qty_available, 8.0, 'Wrong quantity available (%s found instead of 8)' % (self.productC.qty_available))
+ # Check quants and available quantity for product D.
+ quants = self.StockQuantObj.search([('product_id', '=', self.productD.id), ('location_id', '=', self.stock_location), ('quantity', '!=', 0)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 9.0, 'Wrong total stock location quantity (%s found instead of 9)' % (sum(total_qty)))
+ self.assertEqual(self.productD.qty_available, 9.0, 'Wrong quantity available (%s found instead of 9)' % (self.productD.qty_available))
+
+ # -----------------------------------------------------------------------
+ # Back order of Outgoing shipment
+ # ----------------------------------------------------------------------
+
+ back_order_out._action_done()
+
+ # Check stock location quants and available quantity for product A.
+ quants = self.StockQuantObj.search([('product_id', '=', self.productA.id), ('location_id', '=', self.stock_location), ('quantity', '!=', 0)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertGreaterEqual(float_round(sum(total_qty), precision_rounding=0.0001), 1, 'Total stock location quantity for product A should not be nagative.')
+
+ def test_10_pickings_transfer_with_different_uom(self):
+ """ Picking transfer with diffrent unit of meassure. """
+
+ # ----------------------------------------------------------------------
+ # Create incoming shipment of products DozA, SDozA, SDozARound, kgB, gB
+ # ----------------------------------------------------------------------
+ # DozA ( 10 Dozen ) , SDozA ( 10.5 SuperDozen )
+ # SDozARound ( 10.5 10.5 SuperDozenRound ) , kgB ( 0.020 kg )
+ # gB ( 525.3 g )
+ # ----------------------------------------------------------------------
+
+ picking_in_A = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_in,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': self.DozA.name,
+ 'product_id': self.DozA.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.DozA.uom_id.id,
+ 'picking_id': picking_in_A.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': self.SDozA.name,
+ 'product_id': self.SDozA.id,
+ 'product_uom_qty': 10.5,
+ 'product_uom': self.SDozA.uom_id.id,
+ 'picking_id': picking_in_A.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': self.SDozARound.name,
+ 'product_id': self.SDozARound.id,
+ 'product_uom_qty': 10.5,
+ 'product_uom': self.SDozARound.uom_id.id,
+ 'picking_id': picking_in_A.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': self.kgB.name,
+ 'product_id': self.kgB.id,
+ 'product_uom_qty': 0.020,
+ 'product_uom': self.kgB.uom_id.id,
+ 'picking_id': picking_in_A.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': self.gB.name,
+ 'product_id': self.gB.id,
+ 'product_uom_qty': 525.3,
+ 'product_uom': self.gB.uom_id.id,
+ 'picking_id': picking_in_A.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+
+ # Check incoming shipment move lines state.
+ for move in picking_in_A.move_lines:
+ self.assertEqual(move.state, 'draft', 'Move state must be draft.')
+ # Confirm incoming shipment.
+ picking_in_A.action_confirm()
+ # Check incoming shipment move lines state.
+ for move in picking_in_A.move_lines:
+ self.assertEqual(move.state, 'assigned', 'Move state must be draft.')
+
+ # ----------------------------------------------------
+ # Check pack operation quantity of incoming shipments.
+ # ----------------------------------------------------
+
+ PackSdozAround = self.StockPackObj.search([('product_id', '=', self.SDozARound.id), ('picking_id', '=', picking_in_A.id)], limit=1)
+ self.assertEqual(PackSdozAround.product_qty, 11, 'Wrong quantity in pack operation (%s found instead of 11)' % (PackSdozAround.product_qty))
+ res_dict = picking_in_A.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ wizard.process()
+
+ # -----------------------------------------------------------------------
+ # Check stock location quant quantity and quantity available
+ # -----------------------------------------------------------------------
+
+ # Check quants and available quantity for product DozA
+ quants = self.StockQuantObj.search([('product_id', '=', self.DozA.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 10, 'Expecting 10 Dozen , got %.4f Dozen on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.DozA.qty_available, 10, 'Wrong quantity available (%s found instead of 10)' % (self.DozA.qty_available))
+ # Check quants and available quantity for product SDozA
+ quants = self.StockQuantObj.search([('product_id', '=', self.SDozA.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 10.5, 'Expecting 10.5 SDozen , got %.4f SDozen on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.SDozA.qty_available, 10.5, 'Wrong quantity available (%s found instead of 10.5)' % (self.SDozA.qty_available))
+ # Check quants and available quantity for product SDozARound
+ quants = self.StockQuantObj.search([('product_id', '=', self.SDozARound.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 11, 'Expecting 11 SDozenRound , got %.4f SDozenRound on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.SDozARound.qty_available, 11, 'Wrong quantity available (%s found instead of 11)' % (self.SDozARound.qty_available))
+ # Check quants and available quantity for product gB
+ quants = self.StockQuantObj.search([('product_id', '=', self.gB.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 525.3, 'Expecting 525.3 gram , got %.4f gram on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.gB.qty_available, 525.3, 'Wrong quantity available (%s found instead of 525.3' % (self.gB.qty_available))
+ # Check quants and available quantity for product kgB
+ quants = self.StockQuantObj.search([('product_id', '=', self.kgB.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 0.020, 'Expecting 0.020 kg , got %.4f kg on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.kgB.qty_available, 0.020, 'Wrong quantity available (%s found instead of 0.020)' % (self.kgB.qty_available))
+
+ # ----------------------------------------------------------------------
+ # Create Incoming Shipment B
+ # ----------------------------------------------------------------------
+
+ picking_in_B = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_in,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ move_in_a = self.MoveObj.create({
+ 'name': self.DozA.name,
+ 'product_id': self.DozA.id,
+ 'product_uom_qty': 120,
+ 'product_uom': self.uom_unit.id,
+ 'picking_id': picking_in_B.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': self.SDozA.name,
+ 'product_id': self.SDozA.id,
+ 'product_uom_qty': 1512,
+ 'product_uom': self.uom_unit.id,
+ 'picking_id': picking_in_B.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': self.SDozARound.name,
+ 'product_id': self.SDozARound.id,
+ 'product_uom_qty': 1584,
+ 'product_uom': self.uom_unit.id,
+ 'picking_id': picking_in_B.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': self.kgB.name,
+ 'product_id': self.kgB.id,
+ 'product_uom_qty': 20.0,
+ 'product_uom': self.uom_gm.id,
+ 'picking_id': picking_in_B.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': self.gB.name,
+ 'product_id': self.gB.id,
+ 'product_uom_qty': 0.525,
+ 'product_uom': self.uom_kg.id,
+ 'picking_id': picking_in_B.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+
+ # Check incoming shipment move lines state.
+ for move in picking_in_B.move_lines:
+ self.assertEqual(move.state, 'draft', 'Wrong state of move line.')
+ # Confirm incoming shipment.
+ picking_in_B.action_confirm()
+ # Check incoming shipment move lines state.
+ for move in picking_in_B.move_lines:
+ self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
+
+ # ----------------------------------------------------------------------
+ # Check product quantity and unit of measure of pack operaation.
+ # ----------------------------------------------------------------------
+
+ # Check pack operation quantity and unit of measure for product DozA.
+ PackdozA = self.StockPackObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', picking_in_B.id)], limit=1)
+ self.assertEqual(PackdozA.product_uom_qty, 120, 'Wrong quantity in pack operation (%s found instead of 120)' % (PackdozA.product_uom_qty))
+ self.assertEqual(PackdozA.product_qty, 10, 'Wrong real quantity in pack operation (%s found instead of 10)' % (PackdozA.product_qty))
+ self.assertEqual(PackdozA.product_uom_id.id, self.uom_unit.id, 'Wrong uom in pack operation for product DozA.')
+ # Check pack operation quantity and unit of measure for product SDozA.
+ PackSdozA = self.StockPackObj.search([('product_id', '=', self.SDozA.id), ('picking_id', '=', picking_in_B.id)], limit=1)
+ self.assertEqual(PackSdozA.product_uom_qty, 1512, 'Wrong quantity in pack operation (%s found instead of 1512)' % (PackSdozA.product_uom_qty))
+ self.assertEqual(PackSdozA.product_uom_id.id, self.uom_unit.id, 'Wrong uom in pack operation for product SDozA.')
+ # Check pack operation quantity and unit of measure for product SDozARound.
+ PackSdozAround = self.StockPackObj.search([('product_id', '=', self.SDozARound.id), ('picking_id', '=', picking_in_B.id)], limit=1)
+ self.assertEqual(PackSdozAround.product_uom_qty, 1584, 'Wrong quantity in pack operation (%s found instead of 1584)' % (PackSdozAround.product_uom_qty))
+ self.assertEqual(PackSdozAround.product_uom_id.id, self.uom_unit.id, 'Wrong uom in pack operation for product SDozARound.')
+ # Check pack operation quantity and unit of measure for product gB.
+ packgB = self.StockPackObj.search([('product_id', '=', self.gB.id), ('picking_id', '=', picking_in_B.id)], limit=1)
+ self.assertEqual(packgB.product_uom_qty, 0.525, 'Wrong quantity in pack operation (%s found instead of 0.525)' % (packgB.product_uom_qty))
+ self.assertEqual(packgB.product_qty, 525, 'Wrong real quantity in pack operation (%s found instead of 525)' % (packgB.product_qty))
+ self.assertEqual(packgB.product_uom_id.id, packgB.move_id.product_uom.id, 'Wrong uom in pack operation for product kgB.')
+ # Check pack operation quantity and unit of measure for product kgB.
+ packkgB = self.StockPackObj.search([('product_id', '=', self.kgB.id), ('picking_id', '=', picking_in_B.id)], limit=1)
+ self.assertEqual(packkgB.product_uom_qty, 20.0, 'Wrong quantity in pack operation (%s found instead of 20)' % (packkgB.product_uom_qty))
+ self.assertEqual(packkgB.product_uom_id.id, self.uom_gm.id, 'Wrong uom in pack operation for product kgB')
+
+ # ----------------------------------------------------------------------
+ # Replace pack operation of incoming shipment.
+ # ----------------------------------------------------------------------
+
+ self.StockPackObj.search([('product_id', '=', self.kgB.id), ('picking_id', '=', picking_in_B.id)]).write({
+ 'product_uom_qty': 0.020, 'product_uom_id': self.uom_kg.id})
+ self.StockPackObj.search([('product_id', '=', self.gB.id), ('picking_id', '=', picking_in_B.id)]).write({
+ 'product_uom_qty': 526, 'product_uom_id': self.uom_gm.id})
+ self.StockPackObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', picking_in_B.id)]).write({
+ 'product_uom_qty': 4, 'product_uom_id': self.uom_dozen.id})
+ self.StockPackObj.create({
+ 'product_id': self.DozA.id,
+ 'product_uom_qty': 48,
+ 'product_uom_id': self.uom_unit.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'move_id': move_in_a.id
+ })
+
+ # -----------------
+ # Transfer product.
+ # -----------------
+
+ res_dict = picking_in_B.button_validate()
+ wizard = Form(self.env[res_dict.get('res_model')].with_context(res_dict['context'])).save()
+ res_dict_for_back_order = wizard.process()
+ backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
+ backorder_wizard.process()
+
+ # -----------------------------------------------------------------------
+ # Check incoming shipment
+ # -----------------------------------------------------------------------
+ # Check incoming shipment state.
+ self.assertEqual(picking_in_B.state, 'done', 'Incoming shipment state should be done.')
+ # Check incoming shipment move lines state.
+ for move in picking_in_B.move_lines:
+ self.assertEqual(move.state, 'done', 'Wrong state of move line.')
+ # Check total done move lines for incoming shipment.
+ self.assertEqual(len(picking_in_B.move_lines), 5, 'Wrong number of move lines')
+ # Check product DozA done quantity.
+ moves_DozA = self.MoveObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', picking_in_B.id)], limit=1)
+ self.assertEqual(moves_DozA.product_uom_qty, 96, 'Wrong move quantity (%s found instead of 96)' % (moves_DozA.product_uom_qty))
+ self.assertEqual(moves_DozA.product_uom.id, self.uom_unit.id, 'Wrong uom in move for product DozA.')
+ # Check product SDozA done quantity.
+ moves_SDozA = self.MoveObj.search([('product_id', '=', self.SDozA.id), ('picking_id', '=', picking_in_B.id)], limit=1)
+ self.assertEqual(moves_SDozA.product_uom_qty, 1512, 'Wrong move quantity (%s found instead of 1512)' % (moves_SDozA.product_uom_qty))
+ self.assertEqual(moves_SDozA.product_uom.id, self.uom_unit.id, 'Wrong uom in move for product SDozA.')
+ # Check product SDozARound done quantity.
+ moves_SDozARound = self.MoveObj.search([('product_id', '=', self.SDozARound.id), ('picking_id', '=', picking_in_B.id)], limit=1)
+ self.assertEqual(moves_SDozARound.product_uom_qty, 1584, 'Wrong move quantity (%s found instead of 1584)' % (moves_SDozARound.product_uom_qty))
+ self.assertEqual(moves_SDozARound.product_uom.id, self.uom_unit.id, 'Wrong uom in move for product SDozARound.')
+ # Check product kgB done quantity.
+ moves_kgB = self.MoveObj.search([('product_id', '=', self.kgB.id), ('picking_id', '=', picking_in_B.id)], limit=1)
+ self.assertEqual(moves_kgB.product_uom_qty, 20, 'Wrong quantity in move (%s found instead of 20)' % (moves_kgB.product_uom_qty))
+ self.assertEqual(moves_kgB.product_uom.id, self.uom_gm.id, 'Wrong uom in move for product kgB.')
+ # Check two moves created for product gB with quantity (0.525 kg and 0.3 g)
+ moves_gB_kg = self.MoveObj.search([('product_id', '=', self.gB.id), ('picking_id', '=', picking_in_B.id), ('product_uom', '=', self.uom_kg.id)], limit=1)
+ self.assertEqual(moves_gB_kg.product_uom_qty, 0.526, 'Wrong move quantity (%s found instead of 0.526)' % (moves_gB_kg.product_uom_qty))
+ self.assertEqual(moves_gB_kg.product_uom.id, self.uom_kg.id, 'Wrong uom in move for product gB.')
+
+ # TODO Test extra move once the uom is editable in the move_lines
+
+ # ----------------------------------------------------------------------
+ # Check Back order of Incoming shipment.
+ # ----------------------------------------------------------------------
+
+ # Check back order created or not.
+ bo_in_B = self.PickingObj.search([('backorder_id', '=', picking_in_B.id)])
+ self.assertEqual(len(bo_in_B), 1, 'Back order should be created.')
+ # Check total move lines of back order.
+ self.assertEqual(len(bo_in_B.move_lines), 1, 'Wrong number of move lines')
+ # Check back order created with correct quantity and uom or not.
+ moves_DozA = self.MoveObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', bo_in_B.id)], limit=1)
+ self.assertEqual(moves_DozA.product_uom_qty, 24.0, 'Wrong move quantity (%s found instead of 0.525)' % (moves_DozA.product_uom_qty))
+ self.assertEqual(moves_DozA.product_uom.id, self.uom_unit.id, 'Wrong uom in move for product DozA.')
+
+ # ----------------------------------------------------------------------
+ # Check product stock location quantity and quantity available.
+ # ----------------------------------------------------------------------
+
+ # Check quants and available quantity for product DozA
+ quants = self.StockQuantObj.search([('product_id', '=', self.DozA.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 18, 'Expecting 18 Dozen , got %.4f Dozen on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.DozA.qty_available, 18, 'Wrong quantity available (%s found instead of 18)' % (self.DozA.qty_available))
+ # Check quants and available quantity for product SDozA
+ quants = self.StockQuantObj.search([('product_id', '=', self.SDozA.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 21, 'Expecting 21 SDozen , got %.4f SDozen on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.SDozA.qty_available, 21, 'Wrong quantity available (%s found instead of 21)' % (self.SDozA.qty_available))
+ # Check quants and available quantity for product SDozARound
+ quants = self.StockQuantObj.search([('product_id', '=', self.SDozARound.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 22, 'Expecting 22 SDozenRound , got %.4f SDozenRound on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.SDozARound.qty_available, 22, 'Wrong quantity available (%s found instead of 22)' % (self.SDozARound.qty_available))
+ # Check quants and available quantity for product gB.
+ quants = self.StockQuantObj.search([('product_id', '=', self.gB.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(round(sum(total_qty), 1), 1051.3, 'Expecting 1051 Gram , got %.4f Gram on location stock!' % (sum(total_qty)))
+ self.assertEqual(round(self.gB.qty_available, 1), 1051.3, 'Wrong quantity available (%s found instead of 1051)' % (self.gB.qty_available))
+ # Check quants and available quantity for product kgB.
+ quants = self.StockQuantObj.search([('product_id', '=', self.kgB.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 0.040, 'Expecting 0.040 kg , got %.4f kg on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.kgB.qty_available, 0.040, 'Wrong quantity available (%s found instead of 0.040)' % (self.kgB.qty_available))
+
+ # ----------------------------------------------------------------------
+ # Create outgoing shipment.
+ # ----------------------------------------------------------------------
+
+ before_out_quantity = self.kgB.qty_available
+ picking_out = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ self.MoveObj.create({
+ 'name': self.kgB.name,
+ 'product_id': self.kgB.id,
+ 'product_uom_qty': 0.966,
+ 'product_uom': self.uom_gm.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ self.MoveObj.create({
+ 'name': self.kgB.name,
+ 'product_id': self.kgB.id,
+ 'product_uom_qty': 0.034,
+ 'product_uom': self.uom_gm.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ picking_out.action_confirm()
+ picking_out.action_assign()
+ res_dict = picking_out.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ wizard.process()
+
+ # Check quantity difference after stock transfer.
+ quantity_diff = before_out_quantity - self.kgB.qty_available
+ self.assertEqual(float_round(quantity_diff, precision_rounding=0.0001), 0.001, 'Wrong quantity difference.')
+ self.assertEqual(self.kgB.qty_available, 0.039, 'Wrong quantity available (%s found instead of 0.039)' % (self.kgB.qty_available))
+
+ # ======================================================================
+ # Outgoing shipments.
+ # ======================================================================
+
+ # Create Outgoing shipment with ...
+ # product DozA ( 54 Unit ) , SDozA ( 288 Unit )
+ # product SDozRound ( 360 unit ) , product gB ( 0.503 kg )
+ # product kgB ( 19 g )
+ # ======================================================================
+
+ picking_out = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ self.MoveObj.create({
+ 'name': self.DozA.name,
+ 'product_id': self.DozA.id,
+ 'product_uom_qty': 54,
+ 'product_uom': self.uom_unit.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ self.MoveObj.create({
+ 'name': self.SDozA.name,
+ 'product_id': self.SDozA.id,
+ 'product_uom_qty': 288,
+ 'product_uom': self.uom_unit.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ self.MoveObj.create({
+ 'name': self.SDozARound.name,
+ 'product_id': self.SDozARound.id,
+ 'product_uom_qty': 361,
+ 'product_uom': self.uom_unit.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ self.MoveObj.create({
+ 'name': self.gB.name,
+ 'product_id': self.gB.id,
+ 'product_uom_qty': 0.503,
+ 'product_uom': self.uom_kg.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ self.MoveObj.create({
+ 'name': self.kgB.name,
+ 'product_id': self.kgB.id,
+ 'product_uom_qty': 20,
+ 'product_uom': self.uom_gm.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ # Confirm outgoing shipment.
+ picking_out.action_confirm()
+ for move in picking_out.move_lines:
+ self.assertEqual(move.state, 'confirmed', 'Wrong state of move line.')
+ # Assing product to outgoing shipments
+ picking_out.action_assign()
+ for move in picking_out.move_lines:
+ self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
+ # Check product A available quantity
+ DozA_qty = self.MoveObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', picking_out.id)], limit=1).product_qty
+ self.assertEqual(DozA_qty, 4.5, 'Wrong move quantity availability (%s found instead of 4.5)' % (DozA_qty))
+ # Check product B available quantity
+ SDozA_qty = self.MoveObj.search([('product_id', '=', self.SDozA.id), ('picking_id', '=', picking_out.id)], limit=1).product_qty
+ self.assertEqual(SDozA_qty, 2, 'Wrong move quantity availability (%s found instead of 2)' % (SDozA_qty))
+ # Check product C available quantity
+ SDozARound_qty = self.MoveObj.search([('product_id', '=', self.SDozARound.id), ('picking_id', '=', picking_out.id)], limit=1).product_qty
+ self.assertEqual(SDozARound_qty, 3, 'Wrong move quantity availability (%s found instead of 3)' % (SDozARound_qty))
+ # Check product D available quantity
+ gB_qty = self.MoveObj.search([('product_id', '=', self.gB.id), ('picking_id', '=', picking_out.id)], limit=1).product_qty
+ self.assertEqual(gB_qty, 503, 'Wrong move quantity availability (%s found instead of 503)' % (gB_qty))
+ # Check product D available quantity
+ kgB_qty = self.MoveObj.search([('product_id', '=', self.kgB.id), ('picking_id', '=', picking_out.id)], limit=1).product_qty
+ self.assertEqual(kgB_qty, 0.020, 'Wrong move quantity availability (%s found instead of 0.020)' % (kgB_qty))
+
+ res_dict = picking_out.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ wizard.process()
+
+ # ----------------------------------------------------------------------
+ # Check product stock location quantity and quantity available.
+ # ----------------------------------------------------------------------
+
+ # Check quants and available quantity for product DozA
+ quants = self.StockQuantObj.search([('product_id', '=', self.DozA.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 13.5, 'Expecting 13.5 Dozen , got %.4f Dozen on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.DozA.qty_available, 13.5, 'Wrong quantity available (%s found instead of 13.5)' % (self.DozA.qty_available))
+ # Check quants and available quantity for product SDozA
+ quants = self.StockQuantObj.search([('product_id', '=', self.SDozA.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 19, 'Expecting 19 SDozen , got %.4f SDozen on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.SDozA.qty_available, 19, 'Wrong quantity available (%s found instead of 19)' % (self.SDozA.qty_available))
+ # Check quants and available quantity for product SDozARound
+ quants = self.StockQuantObj.search([('product_id', '=', self.SDozARound.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 19, 'Expecting 19 SDozRound , got %.4f SDozRound on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.SDozARound.qty_available, 19, 'Wrong quantity available (%s found instead of 19)' % (self.SDozARound.qty_available))
+ # Check quants and available quantity for product gB.
+ quants = self.StockQuantObj.search([('product_id', '=', self.gB.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(round(sum(total_qty), 1), 548.3, 'Expecting 547.6 g , got %.4f g on location stock!' % (sum(total_qty)))
+ self.assertEqual(round(self.gB.qty_available, 1), 548.3, 'Wrong quantity available (%s found instead of 547.6)' % (self.gB.qty_available))
+ # Check quants and available quantity for product kgB.
+ quants = self.StockQuantObj.search([('product_id', '=', self.kgB.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 0.019, 'Expecting 0.019 kg , got %.4f kg on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.kgB.qty_available, 0.019, 'Wrong quantity available (%s found instead of 0.019)' % (self.kgB.qty_available))
+
+ # ----------------------------------------------------------------------
+ # Receipt back order of incoming shipment.
+ # ----------------------------------------------------------------------
+
+ res_dict = bo_in_B.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ wizard.process()
+ # Check quants and available quantity for product kgB.
+ quants = self.StockQuantObj.search([('product_id', '=', self.DozA.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 15.5, 'Expecting 15.5 Dozen , got %.4f Dozen on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.DozA.qty_available, 15.5, 'Wrong quantity available (%s found instead of 15.5)' % (self.DozA.qty_available))
+
+ # -----------------------------------------
+ # Create product in kg and receive in ton.
+ # -----------------------------------------
+
+ productKG = self.ProductObj.create({'name': 'Product KG', 'uom_id': self.uom_kg.id, 'uom_po_id': self.uom_kg.id, 'type': 'product'})
+ picking_in = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_in,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': productKG.name,
+ 'product_id': productKG.id,
+ 'product_uom_qty': 1.0,
+ 'product_uom': self.uom_tone.id,
+ 'picking_id': picking_in.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ # Check incoming shipment state.
+ self.assertEqual(picking_in.state, 'draft', 'Incoming shipment state should be draft.')
+ # Check incoming shipment move lines state.
+ for move in picking_in.move_lines:
+ self.assertEqual(move.state, 'draft', 'Wrong state of move line.')
+ # Confirm incoming shipment.
+ picking_in.action_confirm()
+ # Check incoming shipment move lines state.
+ for move in picking_in.move_lines:
+ self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
+ # Check pack operation quantity.
+ packKG = self.StockPackObj.search([('product_id', '=', productKG.id), ('picking_id', '=', picking_in.id)], limit=1)
+ self.assertEqual(packKG.product_qty, 1000, 'Wrong product real quantity in pack operation (%s found instead of 1000)' % (packKG.product_qty))
+ self.assertEqual(packKG.product_uom_qty, 1, 'Wrong product quantity in pack operation (%s found instead of 1)' % (packKG.product_uom_qty))
+ self.assertEqual(packKG.product_uom_id.id, self.uom_tone.id, 'Wrong product uom in pack operation.')
+ # Transfer Incoming shipment.
+ res_dict = picking_in.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ wizard.process()
+
+ # -----------------------------------------------------------------------
+ # Check incoming shipment after transfer.
+ # -----------------------------------------------------------------------
+
+ # Check incoming shipment state.
+ self.assertEqual(picking_in.state, 'done', 'Incoming shipment state: %s instead of %s' % (picking_in.state, 'done'))
+ # Check incoming shipment move lines state.
+ for move in picking_in.move_lines:
+ self.assertEqual(move.state, 'done', 'Wrong state of move lines.')
+ # Check total done move lines for incoming shipment.
+ self.assertEqual(len(picking_in.move_lines), 1, 'Wrong number of move lines')
+ # Check product DozA done quantity.
+ move = self.MoveObj.search([('product_id', '=', productKG.id), ('picking_id', '=', picking_in.id)], limit=1)
+ self.assertEqual(move.product_uom_qty, 1, 'Wrong product quantity in done move.')
+ self.assertEqual(move.product_uom.id, self.uom_tone.id, 'Wrong unit of measure in done move.')
+ self.assertEqual(productKG.qty_available, 1000, 'Wrong quantity available of product (%s found instead of 1000)' % (productKG.qty_available))
+ picking_out = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ self.MoveObj.create({
+ 'name': productKG.name,
+ 'product_id': productKG.id,
+ 'product_uom_qty': 2.5,
+ 'product_uom': self.uom_gm.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ picking_out.action_confirm()
+ picking_out.action_assign()
+ pack_opt = self.StockPackObj.search([('product_id', '=', productKG.id), ('picking_id', '=', picking_out.id)], limit=1)
+ pack_opt.write({'product_uom_qty': 0.5})
+ res_dict = picking_out.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ res_dict_for_back_order = wizard.process()
+ backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
+ backorder_wizard.process()
+ quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ # Check total quantity stock location.
+ self.assertEqual(sum(total_qty), 999.9995, 'Expecting 999.9995 kg , got %.4f kg on location stock!' % (sum(total_qty)))
+
+ # ---------------------------------
+ # Check Back order created or not.
+ # ---------------------------------
+ bo_out_1 = self.PickingObj.search([('backorder_id', '=', picking_out.id)])
+ self.assertEqual(len(bo_out_1), 1, 'Back order should be created.')
+ # Check total move lines of back order.
+ self.assertEqual(len(bo_out_1.move_lines), 1, 'Wrong number of move lines')
+ moves_KG = self.MoveObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_1.id)], limit=1)
+ # Check back order created with correct quantity and uom or not.
+ self.assertEqual(moves_KG.product_uom_qty, 2.0, 'Wrong move quantity (%s found instead of 2.0)' % (moves_KG.product_uom_qty))
+ self.assertEqual(moves_KG.product_uom.id, self.uom_gm.id, 'Wrong uom in move for product KG.')
+ bo_out_1.action_assign()
+ pack_opt = self.StockPackObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_1.id)], limit=1)
+ pack_opt.write({'product_uom_qty': 0.5})
+ res_dict = bo_out_1.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ res_dict_for_back_order = wizard.process()
+ backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
+ backorder_wizard.process()
+ quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+
+ # Check total quantity stock location.
+ self.assertEqual(sum(total_qty), 999.9990, 'Expecting 999.9990 kg , got %.4f kg on location stock!' % (sum(total_qty)))
+
+ # Check Back order created or not.
+ # ---------------------------------
+ bo_out_2 = self.PickingObj.search([('backorder_id', '=', bo_out_1.id)])
+ self.assertEqual(len(bo_out_2), 1, 'Back order should be created.')
+ # Check total move lines of back order.
+ self.assertEqual(len(bo_out_2.move_lines), 1, 'Wrong number of move lines')
+ # Check back order created with correct move quantity and uom or not.
+ moves_KG = self.MoveObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_2.id)], limit=1)
+ self.assertEqual(moves_KG.product_uom_qty, 1.5, 'Wrong move quantity (%s found instead of 1.5)' % (moves_KG.product_uom_qty))
+ self.assertEqual(moves_KG.product_uom.id, self.uom_gm.id, 'Wrong uom in move for product KG.')
+ bo_out_2.action_assign()
+ pack_opt = self.StockPackObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_2.id)], limit=1)
+ pack_opt.write({'product_uom_qty': 0.5})
+ res_dict = bo_out_2.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ res_dict_for_back_order = wizard.process()
+ backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
+ backorder_wizard.process()
+ # Check total quantity stock location of product KG.
+ quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 999.9985, 'Expecting 999.9985 kg , got %.4f kg on location stock!' % (sum(total_qty)))
+
+ # Check Back order created or not.
+ # ---------------------------------
+ bo_out_3 = self.PickingObj.search([('backorder_id', '=', bo_out_2.id)])
+ self.assertEqual(len(bo_out_3), 1, 'Back order should be created.')
+ # Check total move lines of back order.
+ self.assertEqual(len(bo_out_3.move_lines), 1, 'Wrong number of move lines')
+ # Check back order created with correct quantity and uom or not.
+ moves_KG = self.MoveObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_3.id)], limit=1)
+ self.assertEqual(moves_KG.product_uom_qty, 1, 'Wrong move quantity (%s found instead of 1.0)' % (moves_KG.product_uom_qty))
+ self.assertEqual(moves_KG.product_uom.id, self.uom_gm.id, 'Wrong uom in move for product KG.')
+ bo_out_3.action_assign()
+ pack_opt = self.StockPackObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_3.id)], limit=1)
+ pack_opt.write({'product_uom_qty': 0.5})
+ res_dict = bo_out_3.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ res_dict_for_back_order = wizard.process()
+ quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 999.9980, 'Expecting 999.9980 kg , got %.4f kg on location stock!' % (sum(total_qty)))
+
+ # Check Back order created or not.
+ # ---------------------------------
+ bo_out_4 = self.PickingObj.search([('backorder_id', '=', bo_out_3.id)])
+
+ self.assertEqual(len(bo_out_4), 1, 'Back order should be created.')
+ # Check total move lines of back order.
+ self.assertEqual(len(bo_out_4.move_lines), 1, 'Wrong number of move lines')
+ # Check back order created with correct quantity and uom or not.
+ moves_KG = self.MoveObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_4.id)], limit=1)
+ self.assertEqual(moves_KG.product_uom_qty, 0.5, 'Wrong move quantity (%s found instead of 0.5)' % (moves_KG.product_uom_qty))
+ self.assertEqual(moves_KG.product_uom.id, self.uom_gm.id, 'Wrong uom in move for product KG.')
+ bo_out_4.action_assign()
+ pack_opt = self.StockPackObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_4.id)], limit=1)
+ pack_opt.write({'product_uom_qty': 0.5})
+ res_dict = bo_out_4.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ wizard.process()
+ quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertAlmostEqual(sum(total_qty), 999.9975, msg='Expecting 999.9975 kg , got %.4f kg on location stock!' % (sum(total_qty)))
+
+ def test_20_create_inventory_with_different_uom(self):
+ """Create inventory with different unit of measure."""
+
+ # ------------------------------------------------
+ # Test inventory with product A(Unit).
+ # ------------------------------------------------
+
+ inventory = self.InvObj.create({'name': 'Test',
+ 'product_ids': [(4, self.UnitA.id)]})
+ inventory.action_start()
+ self.assertFalse(inventory.line_ids, "Inventory line should not created.")
+ inventory_line = self.InvLineObj.create({
+ 'inventory_id': inventory.id,
+ 'product_id': self.UnitA.id,
+ 'product_uom_id': self.uom_dozen.id,
+ 'product_qty': 10,
+ 'location_id': self.stock_location})
+ inventory.action_validate()
+ # Check quantity available of product UnitA.
+ quants = self.StockQuantObj.search([('product_id', '=', self.UnitA.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 120, 'Expecting 120 Units , got %.4f Units on location stock!' % (sum(total_qty)))
+ self.assertEqual(self.UnitA.qty_available, 120, 'Expecting 120 Units , got %.4f Units of quantity available!' % (self.UnitA.qty_available))
+ # Create Inventory again for product UnitA.
+ inventory = self.InvObj.create({'name': 'Test',
+ 'product_ids': [(4, self.UnitA.id)]})
+ inventory.action_start()
+ self.assertEqual(len(inventory.line_ids), 1, "One inventory line should be created.")
+ inventory_line = self.InvLineObj.search([('product_id', '=', self.UnitA.id), ('inventory_id', '=', inventory.id)], limit=1)
+ self.assertEqual(inventory_line.product_qty, 120, "Wrong product quantity in inventory line.")
+ # Modify the inventory line and set the quantity to 144 product on this new inventory.
+ inventory_line.write({'product_qty': 144})
+ inventory.action_validate()
+ move = self.MoveObj.search([('product_id', '=', self.UnitA.id), ('inventory_id', '=', inventory.id)], limit=1)
+ self.assertEqual(move.product_uom_qty, 24, "Wrong move quantity of product UnitA.")
+ # Check quantity available of product UnitA.
+ quants = self.StockQuantObj.search([('product_id', '=', self.UnitA.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 144, 'Expecting 144 Units , got %.4f Units on location stock!' % (sum(total_qty)))
+ self.UnitA._compute_quantities()
+ self.assertEqual(self.UnitA.qty_available, 144, 'Expecting 144 Units , got %.4f Units of quantity available!' % (self.UnitA.qty_available))
+
+ # ------------------------------------------------
+ # Test inventory with product KG.
+ # ------------------------------------------------
+
+ productKG = self.ProductObj.create({'name': 'Product KG', 'uom_id': self.uom_kg.id, 'uom_po_id': self.uom_kg.id, 'type': 'product'})
+ inventory = self.InvObj.create({'name': 'Inventory Product KG',
+ 'product_ids': [(4, productKG.id)]})
+ inventory.action_start()
+ self.assertFalse(inventory.line_ids, "Inventory line should not created.")
+ inventory_line = self.InvLineObj.create({
+ 'inventory_id': inventory.id,
+ 'product_id': productKG.id,
+ 'product_uom_id': self.uom_tone.id,
+ 'product_qty': 5,
+ 'location_id': self.stock_location})
+ inventory.action_validate()
+ quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 5000, 'Expecting 5000 kg , got %.4f kg on location stock!' % (sum(total_qty)))
+ self.assertEqual(productKG.qty_available, 5000, 'Expecting 5000 kg , got %.4f kg of quantity available!' % (productKG.qty_available))
+ # Create Inventory again.
+ inventory = self.InvObj.create({'name': 'Test',
+ 'product_ids': [(4, productKG.id)]})
+ inventory.action_start()
+ self.assertEqual(len(inventory.line_ids), 1, "One inventory line should be created.")
+ inventory_line = self.InvLineObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1)
+ self.assertEqual(inventory_line.product_qty, 5000, "Wrong product quantity in inventory line.")
+ # Modify the inventory line and set the quantity to 4000 product on this new inventory.
+ inventory_line.write({'product_qty': 4000})
+ inventory.action_validate()
+ # Check inventory move quantity of product KG.
+ move = self.MoveObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1)
+ self.assertEqual(move.product_uom_qty, 1000, "Wrong move quantity of product KG.")
+ # Check quantity available of product KG.
+ quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)])
+ total_qty = [quant.quantity for quant in quants]
+ self.assertEqual(sum(total_qty), 4000, 'Expecting 4000 kg , got %.4f on location stock!' % (sum(total_qty)))
+ productKG._compute_quantities()
+ self.assertEqual(productKG.qty_available, 4000, 'Expecting 4000 kg , got %.4f of quantity available!' % (productKG.qty_available))
+
+ # --------------------------------------------------------
+ # TEST EMPTY INVENTORY WITH PACKS and LOTS
+ # ---------------------------------------------------------
+
+ packproduct = self.ProductObj.create({'name': 'Pack Product', 'uom_id': self.uom_unit.id, 'uom_po_id': self.uom_unit.id, 'type': 'product'})
+ lotproduct = self.ProductObj.create({'name': 'Lot Product', 'uom_id': self.uom_unit.id, 'uom_po_id': self.uom_unit.id, 'type': 'product'})
+ inventory = self.InvObj.create({'name': 'Test Partial and Pack',
+ 'start_empty': True,
+ 'location_ids': [(4, self.stock_location)]})
+ inventory.action_start()
+ pack_obj = self.env['stock.quant.package']
+ lot_obj = self.env['stock.production.lot']
+ pack1 = pack_obj.create({'name': 'PACK00TEST1'})
+ pack_obj.create({'name': 'PACK00TEST2'})
+ lot1 = lot_obj.create({'name': 'Lot001', 'product_id': lotproduct.id, 'company_id': self.env.company.id})
+ move = self.MoveObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1)
+ self.assertEqual(len(move), 0, "Partial filter should not create a lines upon prepare")
+
+ line_vals = []
+ line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 10, 'product_uom_id': packproduct.uom_id.id}]
+ line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 20, 'product_uom_id': packproduct.uom_id.id, 'package_id': pack1.id}]
+ line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 30, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': lot1.id}]
+ line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 25, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': False}]
+ inventory.write({'line_ids': [(0, 0, x) for x in line_vals]})
+ inventory.action_validate()
+ self.assertEqual(packproduct.qty_available, 30, "Wrong qty available for packproduct")
+ self.assertEqual(lotproduct.qty_available, 55, "Wrong qty available for lotproduct")
+ quants = self.StockQuantObj.search([('product_id', '=', packproduct.id), ('location_id', '=', self.stock_location), ('package_id', '=', pack1.id)])
+ total_qty = sum([quant.quantity for quant in quants])
+ self.assertEqual(total_qty, 20, 'Expecting 20 units on package 1 of packproduct, but we got %.4f on location stock!' % (total_qty))
+
+ # Create an inventory that will put the lots without lot to 0 and check that taking without pack will not take it from the pack
+ inventory2 = self.InvObj.create({'name': 'Test Partial Lot and Pack2',
+ 'start_empty': True,
+ 'location_ids': [(4, self.stock_location)]})
+ inventory2.action_start()
+ line_vals = []
+ line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 20, 'product_uom_id': packproduct.uom_id.id}]
+ line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 0, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': False}]
+ line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 10, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': lot1.id}]
+ inventory2.write({'line_ids': [(0, 0, x) for x in line_vals]})
+ inventory2.action_validate()
+ self.assertEqual(packproduct.qty_available, 40, "Wrong qty available for packproduct")
+ self.assertEqual(lotproduct.qty_available, 10, "Wrong qty available for lotproduct")
+ quants = self.StockQuantObj.search([('product_id', '=', lotproduct.id), ('location_id', '=', self.stock_location), ('lot_id', '=', lot1.id)])
+ total_qty = sum([quant.quantity for quant in quants])
+ self.assertEqual(total_qty, 10, 'Expecting 0 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty))
+ quants = self.StockQuantObj.search([('product_id', '=', lotproduct.id), ('location_id', '=', self.stock_location), ('lot_id', '=', False)])
+ total_qty = sum([quant.quantity for quant in quants])
+ self.assertEqual(total_qty, 0, 'Expecting 0 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty))
+
+ def test_30_check_with_no_incoming_lot(self):
+ """ Picking in without lots and picking out with"""
+ # Change basic operation type not to get lots
+ # Create product with lot tracking
+ picking_in = self.env['stock.picking.type'].browse(self.picking_type_in)
+ picking_in.use_create_lots = False
+ self.productA.tracking = 'lot'
+ picking_in = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_in,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 4,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_in.id,
+ 'picking_type_id': self.picking_type_in,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+
+ # Check incoming shipment move lines state.
+ for move in picking_in.move_lines:
+ self.assertEqual(move.state, 'draft', 'Wrong state of move line.')
+ # Confirm incoming shipment.
+ picking_in.action_confirm()
+ # Check incoming shipment move lines state.
+ for move in picking_in.move_lines:
+ self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
+
+ res_dict = picking_in.button_validate()
+ wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
+ wizard.process()
+ picking_out = self.PickingObj.create({
+ 'name': 'testpicking',
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ move_out = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 3,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ picking_out.action_confirm()
+ picking_out.action_assign()
+ pack_opt = self.StockPackObj.search([('picking_id', '=', picking_out.id)], limit=1)
+ lot1 = self.LotObj.create({'product_id': self.productA.id, 'name': 'LOT1', 'company_id': self.env.company.id})
+ lot2 = self.LotObj.create({'product_id': self.productA.id, 'name': 'LOT2', 'company_id': self.env.company.id})
+ lot3 = self.LotObj.create({'product_id': self.productA.id, 'name': 'LOT3', 'company_id': self.env.company.id})
+
+ pack_opt.write({'lot_id': lot1.id, 'qty_done': 1.0})
+ self.StockPackObj.create({'product_id': self.productA.id, 'move_id': move_out.id, 'product_uom_id': move_out.product_uom.id, 'lot_id': lot2.id, 'qty_done': 1.0, 'location_id': self.stock_location, 'location_dest_id': self.customer_location})
+ self.StockPackObj.create({'product_id': self.productA.id, 'move_id': move_out.id, 'product_uom_id': move_out.product_uom.id, 'lot_id': lot3.id, 'qty_done': 2.0, 'location_id': self.stock_location, 'location_dest_id': self.customer_location})
+ picking_out._action_done()
+ quants = self.StockQuantObj.search([('product_id', '=', self.productA.id), ('location_id', '=', self.stock_location)])
+ # TODO wait sle fix
+ # self.assertFalse(quants, 'Should not have any quants in stock anymore')
+
+ def test_40_pack_in_pack(self):
+ """ Put a pack in pack"""
+ self.env['stock.picking.type'].browse(self.picking_type_in).show_reserved = True
+ picking_out = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location})
+ move_out = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 3,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.pack_location,
+ 'location_dest_id': self.customer_location})
+ picking_pack = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.pack_location})
+ move_pack = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 3,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_pack.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.pack_location,
+ 'move_dest_ids': [(4, move_out.id, 0)]})
+ picking_in = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_in,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ move_in = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 3,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_in.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location,
+ 'move_dest_ids': [(4, move_pack.id, 0)]})
+
+ # Check incoming shipment move lines state.
+ for move in picking_in.move_lines:
+ self.assertEqual(move.state, 'draft', 'Wrong state of move line.')
+ # Confirm incoming shipment.
+ picking_in.action_confirm()
+ # Check incoming shipment move lines state.
+ for move in picking_in.move_lines:
+ self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
+
+ # Check incoming shipment move lines state.
+ for move in picking_pack.move_lines:
+ self.assertEqual(move.state, 'draft', 'Wrong state of move line.')
+ # Confirm incoming shipment.
+ picking_pack.action_confirm()
+ # Check incoming shipment move lines state.
+ for move in picking_pack.move_lines:
+ self.assertEqual(move.state, 'waiting', 'Wrong state of move line.')
+
+ # Check incoming shipment move lines state.
+ for move in picking_out.move_lines:
+ self.assertEqual(move.state, 'draft', 'Wrong state of move line.')
+ # Confirm incoming shipment.
+ picking_out.action_confirm()
+ # Check incoming shipment move lines state.
+ for move in picking_out.move_lines:
+ self.assertEqual(move.state, 'waiting', 'Wrong state of move line.')
+
+ # Set the quantity done on the pack operation
+ move_in.move_line_ids.qty_done = 3.0
+ # Put in a pack
+ picking_in.action_put_in_pack()
+ # Get the new package
+ picking_in_package = move_in.move_line_ids.result_package_id
+ # Validate picking
+ picking_in._action_done()
+
+ # Check first picking state changed to done
+ for move in picking_in.move_lines:
+ self.assertEqual(move.state, 'done', 'Wrong state of move line.')
+ # Check next picking state changed to 'assigned'
+ for move in picking_pack.move_lines:
+ self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
+
+ # Set the quantity done on the pack operation
+ move_pack.move_line_ids.qty_done = 3.0
+ # Get the new package
+ picking_pack_package = move_pack.move_line_ids.result_package_id
+ # Validate picking
+ picking_pack._action_done()
+
+ # Check second picking state changed to done
+ for move in picking_pack.move_lines:
+ self.assertEqual(move.state, 'done', 'Wrong state of move line.')
+ # Check next picking state changed to 'assigned'
+ for move in picking_out.move_lines:
+ self.assertEqual(move.state, 'assigned', 'Wrong state of move line.')
+
+ # Validate picking
+ picking_out.move_line_ids.qty_done = 3.0
+ picking_out_package = move_out.move_line_ids.result_package_id
+ picking_out._action_done()
+
+ # check all pickings are done
+ for move in picking_in.move_lines:
+ self.assertEqual(move.state, 'done', 'Wrong state of move line.')
+ for move in picking_pack.move_lines:
+ self.assertEqual(move.state, 'done', 'Wrong state of move line.')
+ for move in picking_out.move_lines:
+ self.assertEqual(move.state, 'done', 'Wrong state of move line.')
+
+ # Check picking_in_package is in picking_pack_package
+ self.assertEqual(picking_in_package.id, picking_pack_package.id, 'The package created in the picking in is not in the one created in picking pack')
+ self.assertEqual(picking_pack_package.id, picking_out_package.id, 'The package created in the picking in is not in the one created in picking pack')
+ # Check that we have one quant in customer location.
+ quant = self.StockQuantObj.search([('product_id', '=', self.productA.id), ('location_id', '=', self.customer_location)])
+ self.assertEqual(len(quant), 1, 'There should be one quant with package for customer location')
+ # Check that the parent package of the quant is the picking_in_package
+
+ def test_50_create_in_out_with_product_pack_lines(self):
+ picking_in = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_in,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': self.productE.name,
+ 'product_id': self.productE.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productE.uom_id.id,
+ 'picking_id': picking_in.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ picking_in.action_confirm()
+ pack_obj = self.env['stock.quant.package']
+ pack1 = pack_obj.create({'name': 'PACKINOUTTEST1'})
+ pack2 = pack_obj.create({'name': 'PACKINOUTTEST2'})
+ picking_in.move_line_ids[0].result_package_id = pack1
+ picking_in.move_line_ids[0].qty_done = 4
+ packop2 = picking_in.move_line_ids[0].with_context(bypass_reservation_update=True).copy({'product_uom_qty': 0})
+ packop2.qty_done = 6
+ packop2.result_package_id = pack2
+ picking_in._action_done()
+ quants = self.env['stock.quant']._gather(self.productE, self.env['stock.location'].browse(self.stock_location))
+ self.assertEqual(sum([x.quantity for x in quants]), 10.0, 'Expecting 10 pieces in stock')
+ # Check the quants are in the package
+ self.assertEqual(sum(x.quantity for x in pack1.quant_ids), 4.0, 'Pack 1 should have 4 pieces')
+ self.assertEqual(sum(x.quantity for x in pack2.quant_ids), 6.0, 'Pack 2 should have 6 pieces')
+ picking_out = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ self.MoveObj.create({
+ 'name': self.productE.name,
+ 'product_id': self.productE.id,
+ 'product_uom_qty': 3,
+ 'product_uom': self.productE.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ picking_out.action_confirm()
+ picking_out.action_assign()
+ packout1 = picking_out.move_line_ids[0]
+ packout2 = picking_out.move_line_ids[0].with_context(bypass_reservation_update=True).copy({'product_uom_qty': 0})
+ packout1.qty_done = 2
+ packout1.package_id = pack1
+ packout2.package_id = pack2
+ packout2.qty_done = 1
+ picking_out._action_done()
+ # Should be only 1 negative quant in supplier location
+ neg_quants = self.env['stock.quant'].search([('product_id', '=', self.productE.id), ('quantity', '<', 0.0)])
+ self.assertEqual(len(neg_quants), 1, 'There should be 1 negative quants for supplier!')
+ self.assertEqual(neg_quants.location_id.id, self.supplier_location, 'There shoud be 1 negative quants for supplier!')
+
+ quants = self.env['stock.quant']._gather(self.productE, self.env['stock.location'].browse(self.stock_location))
+ self.assertEqual(len(quants), 2, 'We should have exactly 2 quants in the end')
+
+ def test_60_create_in_out_with_product_pack_lines(self):
+ picking_in = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_in,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ self.MoveObj.create({
+ 'name': self.productE.name,
+ 'product_id': self.productE.id,
+ 'product_uom_qty': 200,
+ 'product_uom': self.productE.uom_id.id,
+ 'picking_id': picking_in.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+
+ picking_in.action_confirm()
+ pack_obj = self.env['stock.quant.package']
+ pack1 = pack_obj.create({'name': 'PACKINOUTTEST1'})
+ pack2 = pack_obj.create({'name': 'PACKINOUTTEST2'})
+ picking_in.move_line_ids[0].result_package_id = pack1
+ picking_in.move_line_ids[0].qty_done = 120
+ packop2 = picking_in.move_line_ids[0].with_context(bypass_reservation_update=True).copy({'product_uom_qty': 0})
+ packop2.qty_done = 80
+ packop2.result_package_id = pack2
+ picking_in._action_done()
+ quants = self.env['stock.quant']._gather(self.productE, self.env['stock.location'].browse(self.stock_location))
+ self.assertEqual(sum([x.quantity for x in quants]), 200.0, 'Expecting 200 pieces in stock')
+ # Check the quants are in the package
+ self.assertEqual(sum(x.quantity for x in pack1.quant_ids), 120, 'Pack 1 should have 120 pieces')
+ self.assertEqual(sum(x.quantity for x in pack2.quant_ids), 80, 'Pack 2 should have 80 pieces')
+ picking_out = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ self.MoveObj.create({
+ 'name': self.productE.name,
+ 'product_id': self.productE.id,
+ 'product_uom_qty': 200,
+ 'product_uom': self.productE.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ picking_out.action_confirm()
+ picking_out.action_assign()
+ # Convert entire packs into taking out of packs
+ packout0 = picking_out.move_line_ids[0]
+ packout1 = picking_out.move_line_ids[1]
+ packout0.write({
+ 'package_id': pack1.id,
+ 'product_id': self.productE.id,
+ 'qty_done': 120.0,
+ 'product_uom_id': self.productE.uom_id.id,
+ })
+ packout1.write({
+ 'package_id': pack2.id,
+ 'product_id': self.productE.id,
+ 'qty_done': 80.0,
+ 'product_uom_id': self.productE.uom_id.id,
+ })
+ picking_out._action_done()
+ # Should be only 1 negative quant in supplier location
+ neg_quants = self.env['stock.quant'].search([('product_id', '=', self.productE.id), ('quantity', '<', 0.0)])
+ self.assertEqual(len(neg_quants), 1, 'There should be 1 negative quants for supplier!')
+ self.assertEqual(neg_quants.location_id.id, self.supplier_location, 'There shoud be 1 negative quants for supplier!')
+ # We should also make sure that when matching stock moves with pack operations, it takes the correct
+ quants = self.env['stock.quant']._gather(self.productE, self.env['stock.location'].browse(self.stock_location))
+ self.assertEqual(sum(quants.mapped('quantity')), 0, 'We should have no quants in the end')
+
+ def test_70_picking_state_all_at_once_reserve(self):
+ """ This test will check that the state of the picking is correctly computed according
+ to the state of its move lines and its move type.
+ """
+ # move_type: direct == partial, one == all at once
+ # picking: confirmed == waiting availability
+
+ # -----------------------------------------------------------
+ # "all at once" and "reserve" scenario
+ # -----------------------------------------------------------
+ # get one product in stock
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'Inventory Product Table',
+ 'line_ids': [(0, 0, {
+ 'product_id': self.productA.id,
+ 'product_uom_id': self.productA.uom_id.id,
+ 'product_qty': 1,
+ 'location_id': self.stock_location
+ })]
+ })
+ inventory.action_start()
+ inventory.action_validate()
+
+ # create a "all at once" delivery order for two products
+ picking_out = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ picking_out.move_type = 'one'
+
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ # validate this delivery order, it should be in the waiting state
+ picking_out.action_assign()
+ self.assertEqual(picking_out.state, "confirmed")
+
+ # receive one product in stock
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'Inventory Product Table',
+ 'line_ids': [(0, 0, {
+ 'product_id': self.productA.id,
+ 'product_uom_id': self.productA.uom_id.id,
+ 'product_qty': 2,
+ 'location_id': self.stock_location
+ })]
+ })
+ inventory.action_start()
+ inventory.action_validate()
+ # recheck availability of the delivery order, it should be assigned
+ picking_out.action_assign()
+ self.assertEqual(len(picking_out.move_lines), 1.0)
+ self.assertEqual(picking_out.move_lines.product_qty, 2.0)
+ self.assertEqual(picking_out.state, "assigned")
+
+ def test_71_picking_state_all_at_once_force_assign(self):
+ """ This test will check that the state of the picking is correctly computed according
+ to the state of its move lines and its move type.
+ """
+ # move_type: direct == partial, one == all at once
+ # picking: confirmed == waiting availability, partially_available = partially available
+
+ # -----------------------------------------------------------
+ # "all at once" and "force assign" scenario
+ # -----------------------------------------------------------
+ # create a "all at once" delivery order for two products
+ picking_out = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ picking_out.move_type = 'direct'
+
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+
+ # validate this delivery order, it should be in the waiting state
+ picking_out.action_assign()
+ self.assertEqual(picking_out.state, "confirmed")
+
+ def test_72_picking_state_partial_reserve(self):
+ """ This test will check that the state of the picking is correctly computed according
+ to the state of its move lines and its move type.
+ """
+ # move_type: direct == partial, one == all at once
+ # picking: confirmed == waiting availability, partially_available = partially available
+
+ # -----------------------------------------------------------
+ # "partial" and "reserve" scenario
+ # -----------------------------------------------------------
+ # get one product in stock
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'Inventory Product Table',
+ 'line_ids': [(0, 0, {
+ 'product_id': self.productA.id,
+ 'product_uom_id': self.productA.uom_id.id,
+ 'product_qty': 1,
+ 'location_id': self.stock_location
+ })]
+ })
+ inventory.action_start()
+ inventory.action_validate()
+
+ # create a "partial" delivery order for two products
+ picking_out = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ picking_out.move_type = 'direct'
+
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+
+ # validate this delivery order, it should be in partially available
+ picking_out.action_assign()
+ self.assertEqual(picking_out.state, "assigned")
+
+ # receive one product in stock
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'Inventory Product Table',
+ 'line_ids': [(0, 0, {
+ 'product_id': self.productA.id,
+ 'product_uom_id': self.productA.uom_id.id,
+ 'product_qty': 2,
+ 'location_id': self.stock_location
+ })]
+ })
+ inventory.action_start()
+ inventory.action_validate()
+
+ # recheck availability of the delivery order, it should be assigned
+ picking_out.action_assign()
+ self.assertEqual(picking_out.state, "assigned")
+
+ def test_73_picking_state_partial_force_assign(self):
+ """ This test will check that the state of the picking is correctly computed according
+ to the state of its move lines and its move type.
+ """
+ # move_type: direct == partial, one == all at once
+ # picking: confirmed == waiting availability, partially_available = partially available
+
+ # -----------------------------------------------------------
+ # "partial" and "force assign" scenario
+ # -----------------------------------------------------------
+ picking_out = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ picking_out.move_type = 'direct'
+
+ self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+
+ # validate this delivery order, it should be in the waiting state
+ picking_out.action_assign()
+ self.assertEqual(picking_out.state, "confirmed")
+
+ def test_74_move_state_waiting_mto(self):
+ """ This test will check that when a move is unreserved, its state changes to 'waiting' if
+ it has ancestors or if it has a 'procure_method' equal to 'make_to_order' else the state
+ changes to 'confirmed'.
+ """
+ picking_out = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_out,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ move_mto_alone = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'procure_method': 'make_to_order'})
+ move_with_ancestors = self.MoveObj.create({
+ 'name': self.productB.name,
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productB.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+ self.MoveObj.create({
+ 'name': self.productB.name,
+ 'product_id': self.productB.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productB.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location,
+ 'move_dest_ids': [(4, move_with_ancestors.id, 0)]})
+ other_move = self.MoveObj.create({
+ 'name': self.productC.name,
+ 'product_id': self.productC.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.productC.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.stock_location,
+ 'location_dest_id': self.customer_location})
+
+ move_mto_alone._action_confirm()
+ move_with_ancestors._action_confirm()
+ other_move._action_confirm()
+
+ move_mto_alone._do_unreserve()
+ move_with_ancestors._do_unreserve()
+ other_move._do_unreserve()
+
+ self.assertEqual(move_mto_alone.state, "waiting")
+ self.assertEqual(move_with_ancestors.state, "waiting")
+ self.assertEqual(other_move.state, "confirmed")
+
+ def test_80_partial_picking_without_backorder(self):
+ """ This test will create a picking with an initial demand for a product
+ then process a lesser quantity than the expected quantity to be processed.
+ When the wizard ask for a backorder, the 'NO BACKORDER' option will be selected
+ and no backorder should be created afterwards
+ """
+
+ picking = self.PickingObj.create({
+ 'picking_type_id': self.picking_type_in,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+ move_a = self.MoveObj.create({
+ 'name': self.productA.name,
+ 'product_id': self.productA.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.productA.uom_id.id,
+ 'picking_id': picking.id,
+ 'location_id': self.supplier_location,
+ 'location_dest_id': self.stock_location})
+
+ picking.action_confirm()
+
+ # Only 4 items are processed
+ move_a.move_line_ids.qty_done = 4
+ res_dict = picking.button_validate()
+ backorder_wizard = Form(self.env['stock.backorder.confirmation'].with_context(res_dict['context'])).save()
+ backorder_wizard.process_cancel_backorder()
+
+ # Checking that no backorders were attached to the picking
+ self.assertFalse(picking.backorder_id)
+
+ # Checking that the original move is still in the same picking
+ self.assertEqual(move_a.picking_id.id, picking.id)
+
+ move_lines = picking.move_lines
+ move_done = move_lines.browse(move_a.id)
+ move_canceled = move_lines - move_done
+
+ # Checking that the original move was set to done
+ self.assertEqual(move_done.product_uom_qty, 4)
+ self.assertEqual(move_done.state, 'done')
+
+ # Checking that the new move created was canceled
+ self.assertEqual(move_canceled.product_uom_qty, 6)
+ self.assertEqual(move_canceled.state, 'cancel')
+
+ # Checking that the canceled move is in the original picking
+ self.assertIn(move_canceled.id, picking.move_lines.mapped('id'))
+
+ def test_transit_multi_companies(self):
+ """ Ensure that inter company rules set the correct company on picking
+ and their moves.
+ """
+ grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
+ grp_multi_routes = self.env.ref('stock.group_adv_location')
+ grp_multi_companies = self.env.ref('base.group_multi_company')
+ self.env.user.write({'groups_id': [(4, grp_multi_loc.id)]})
+ self.env.user.write({'groups_id': [(4, grp_multi_routes.id)]})
+ self.env.user.write({'groups_id': [(4, grp_multi_companies.id)]})
+
+ company_2 = self.company
+ # Need to add a new company on user.
+ self.env.user.write({'company_ids': [(4, company_2.id)]})
+
+ warehouse_company_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
+
+ f = Form(self.env['stock.location.route'])
+ f.name = 'From Company 1 to InterCompany'
+ f.company_id = self.env.company
+ with f.rule_ids.new() as rule:
+ rule.name = 'From Company 1 to InterCompany'
+ rule.action = 'pull'
+ rule.picking_type_id = warehouse_company_1.in_type_id
+ rule.location_src_id = self.env.ref('stock.stock_location_inter_wh')
+ rule.procure_method = 'make_to_order'
+ route_a = f.save()
+ warehouse_company_2 = self.env['stock.warehouse'].search([('company_id', '=', company_2.id)], limit=1)
+ f = Form(self.env['stock.location.route'])
+ f.name = 'From InterCompany to Company 2'
+ f.company_id = company_2
+ with f.rule_ids.new() as rule:
+ rule.name = 'From InterCompany to Company 2'
+ rule.action = 'pull'
+ rule.picking_type_id = warehouse_company_2.out_type_id
+ rule.location_id = self.env.ref('stock.stock_location_inter_wh')
+ rule.procure_method = 'make_to_stock'
+ route_b = f.save()
+
+ product = self.env['product.product'].create({
+ 'name': 'The product from the other company that I absolutely want',
+ 'type': 'product',
+ 'route_ids': [(4, route_a.id), (4, route_b.id)]
+ })
+
+ replenish_wizard = self.env['product.replenish'].create({
+ 'product_id': product.id,
+ 'product_tmpl_id': product.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'quantity': '5',
+ 'warehouse_id': warehouse_company_1.id,
+ })
+ replenish_wizard.launch_replenishment()
+ incoming_picking = self.env['stock.picking'].search([('product_id', '=', product.id), ('picking_type_id', '=', warehouse_company_1.in_type_id.id)])
+ outgoing_picking = self.env['stock.picking'].search([('product_id', '=', product.id), ('picking_type_id', '=', warehouse_company_2.out_type_id.id)])
+
+ self.assertEqual(incoming_picking.company_id, self.env.company)
+ self.assertEqual(incoming_picking.move_lines.company_id, self.env.company)
+ self.assertEqual(outgoing_picking.company_id, company_2)
+ self.assertEqual(outgoing_picking.move_lines.company_id, company_2)
+
+ def test_transit_multi_companies_ultimate(self):
+ """ Ensure that inter company rules set the correct company on picking
+ and their moves. This test validate a picking with make_to_order moves.
+ Moves are created in batch with a company-focused environment. This test
+ should create moves for company_2 and company_3 at the same time.
+ Ensure they are not create in the same batch.
+ """
+ grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
+ grp_multi_routes = self.env.ref('stock.group_adv_location')
+ grp_multi_companies = self.env.ref('base.group_multi_company')
+ self.env.user.write({'groups_id': [(4, grp_multi_loc.id)]})
+ self.env.user.write({'groups_id': [(4, grp_multi_routes.id)]})
+ self.env.user.write({'groups_id': [(4, grp_multi_companies.id)]})
+
+ company_2 = self.company
+ # Need to add a new company on user.
+ self.env.user.write({'company_ids': [(4, company_2.id)]})
+
+ warehouse_company_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
+
+ f = Form(self.env['stock.location.route'])
+ f.name = 'From Company 1 to InterCompany'
+ f.company_id = self.env.company
+ with f.rule_ids.new() as rule:
+ rule.name = 'From Company 1 to InterCompany'
+ rule.action = 'pull'
+ rule.picking_type_id = warehouse_company_1.in_type_id
+ rule.location_src_id = self.env.ref('stock.stock_location_inter_wh')
+ rule.procure_method = 'make_to_order'
+ route_a = f.save()
+
+ warehouse_company_2 = self.env['stock.warehouse'].search([('company_id', '=', company_2.id)], limit=1)
+ f = Form(self.env['stock.location.route'])
+ f.name = 'From InterCompany to Company 2'
+ f.company_id = company_2
+ with f.rule_ids.new() as rule:
+ rule.name = 'From InterCompany to Company 2'
+ rule.action = 'pull'
+ rule.picking_type_id = warehouse_company_2.out_type_id
+ rule.location_id = self.env.ref('stock.stock_location_inter_wh')
+ rule.procure_method = 'make_to_stock'
+ route_b = f.save()
+
+ company_3 = self.env['res.company'].create({
+ 'name': 'Alaska Company'
+ })
+
+ warehouse_company_3 = self.env['stock.warehouse'].search([('company_id', '=', company_3.id)], limit=1)
+ f = Form(self.env['stock.location.route'])
+ f.name = 'From InterCompany to Company 3'
+ f.company_id = company_3
+ with f.rule_ids.new() as rule:
+ rule.name = 'From InterCompany to Company 3'
+ rule.action = 'pull'
+ rule.picking_type_id = warehouse_company_3.out_type_id
+ rule.location_id = self.env.ref('stock.stock_location_inter_wh')
+ rule.procure_method = 'make_to_stock'
+ route_c = f.save()
+
+ product_from_company_2 = self.env['product.product'].create({
+ 'name': 'The product from the other company that I absolutely want',
+ 'type': 'product',
+ 'route_ids': [(4, route_a.id), (4, route_b.id)]
+ })
+
+ product_from_company_3 = self.env['product.product'].create({
+ 'name': 'Ice',
+ 'type': 'product',
+ 'route_ids': [(4, route_a.id), (4, route_c.id)]
+ })
+
+ f = Form(self.env['stock.picking'], view='stock.view_picking_form')
+ f.picking_type_id = warehouse_company_1.out_type_id
+ with f.move_ids_without_package.new() as move:
+ move.product_id = product_from_company_2
+ move.product_uom_qty = 5
+ with f.move_ids_without_package.new() as move:
+ move.product_id = product_from_company_3
+ move.product_uom_qty = 5
+ picking = f.save()
+
+ picking.move_ids_without_package.write({'procure_method': 'make_to_order'})
+ picking.action_confirm()
+
+ incoming_picking = self.env['stock.picking'].search([('product_id', '=', product_from_company_2.id), ('picking_type_id', '=', warehouse_company_1.in_type_id.id)])
+ outgoing_picking = self.env['stock.picking'].search([('product_id', '=', product_from_company_2.id), ('picking_type_id', '=', warehouse_company_2.out_type_id.id)])
+
+ self.assertEqual(incoming_picking.company_id, self.env.company)
+ self.assertEqual(incoming_picking.move_lines.mapped('company_id'), self.env.company)
+ self.assertEqual(outgoing_picking.company_id, company_2)
+ self.assertEqual(outgoing_picking.move_lines.company_id, company_2)
+
+ incoming_picking = self.env['stock.picking'].search([('product_id', '=', product_from_company_3.id), ('picking_type_id', '=', warehouse_company_1.in_type_id.id)])
+ outgoing_picking = self.env['stock.picking'].search([('product_id', '=', product_from_company_3.id), ('picking_type_id', '=', warehouse_company_3.out_type_id.id)])
+
+ self.assertEqual(incoming_picking.company_id, self.env.company)
+ self.assertEqual(incoming_picking.move_lines.mapped('company_id'), self.env.company)
+ self.assertEqual(outgoing_picking.company_id, company_3)
+ self.assertEqual(outgoing_picking.move_lines.company_id, company_3)
+
+ def test_picking_scheduled_date_readonlyness(self):
+ """ As it seems we keep breaking this thing over and over this small
+ test ensure the scheduled_date is writable on a picking in state 'draft' or 'confirmed'
+ """
+ partner = self.env['res.partner'].create({'name': 'Hubert Bonisseur de la Bath'})
+ product = self.env['product.product'].create({'name': 'Un petit coup de polish', 'type': 'product'})
+ wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
+
+ f = Form(self.env['stock.picking'], view='stock.view_picking_form')
+ f.partner_id = partner
+ f.picking_type_id = wh.out_type_id
+ with f.move_ids_without_package.new() as move:
+ move.product_id = product
+ move.product_uom_qty = 5
+ f.scheduled_date = fields.Datetime.now()
+ picking = f.save()
+
+ f = Form(picking, view='stock.view_picking_form')
+ f.scheduled_date = fields.Datetime.now()
+ picking = f.save()
+
+ self.assertEqual(f.state, 'draft')
+ picking.action_confirm()
+
+ f = Form(picking, view='stock.view_picking_form')
+ f.scheduled_date = fields.Datetime.now()
+ picking = f.save()
+
+ self.assertEqual(f.state, 'confirmed')
diff --git a/addons/stock/tests/test_stock_location_search.py b/addons/stock/tests/test_stock_location_search.py
new file mode 100644
index 00000000..a7e27f54
--- /dev/null
+++ b/addons/stock/tests/test_stock_location_search.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+
+from odoo.tests import common
+
+
+class TestStockLocationSearch(common.TransactionCase):
+ def setUp(self):
+ super(TestStockLocationSearch, self).setUp()
+ self.location = self.env['stock.location']
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ self.sublocation = self.env['stock.location'].create({
+ 'name': 'Shelf 2',
+ 'barcode': 1201985,
+ 'location_id': self.stock_location.id
+ })
+ self.location_barcode_id = self.sublocation.id
+ self.barcode = self.sublocation.barcode
+ self.name = self.sublocation.name
+
+ def test_10_location_search_by_barcode(self):
+ """Search stock location by barcode"""
+ location_names = self.location.name_search(name=self.barcode)
+ self.assertEqual(len(location_names), 1)
+ location_id_found = location_names[0][0]
+ self.assertEqual(self.location_barcode_id, location_id_found)
+
+ def test_20_location_search_by_name(self):
+ """Search stock location by name"""
+ location_names = self.location.name_search(name=self.name)
+ location_ids_found = [location_name[0] for location_name in location_names]
+ self.assertTrue(self.location_barcode_id in location_ids_found)
+
+ def test_30_location_search_wo_results(self):
+ """Search stock location without results"""
+ location_names = self.location.name_search(name='nonexistent')
+ self.assertFalse(location_names)
diff --git a/addons/stock/tests/test_warehouse.py b/addons/stock/tests/test_warehouse.py
new file mode 100644
index 00000000..b5c2e16c
--- /dev/null
+++ b/addons/stock/tests/test_warehouse.py
@@ -0,0 +1,633 @@
+# -*- coding: utf-8 -*-
+
+from odoo.addons.stock.tests.common2 import TestStockCommon
+from odoo.tests import Form
+from odoo.exceptions import AccessError
+from odoo.tools import mute_logger
+
+
+class TestWarehouse(TestStockCommon):
+
+ def setUp(self):
+ super(TestWarehouse, self).setUp()
+ self.partner = self.env['res.partner'].create({'name': 'Deco Addict'})
+
+ def test_inventory_product(self):
+ self.product_1.type = 'product'
+ product_1_quant = self.env['stock.quant'].with_context(inventory_mode=True).create({
+ 'product_id': self.product_1.id,
+ 'inventory_quantity': 50.0,
+ 'location_id': self.warehouse_1.lot_stock_id.id,
+ })
+ inventory = self.env['stock.inventory'].with_user(self.user_stock_manager).create({
+ 'name': 'Starting for product_1',
+ 'location_ids': [(4, self.warehouse_1.lot_stock_id.id)],
+ 'product_ids': [(4, self.product_1.id)],
+ })
+ inventory.action_start()
+ # As done in common.py, there is already an inventory line existing
+ self.assertEqual(len(inventory.line_ids), 1)
+ self.assertEqual(inventory.line_ids.theoretical_qty, 50.0)
+ self.assertEqual(inventory.line_ids.product_id, self.product_1)
+ self.assertEqual(inventory.line_ids.product_uom_id, self.product_1.uom_id)
+
+ # Update the line, set to 35
+ inventory.line_ids.write({'product_qty': 35.0})
+ inventory.action_validate()
+
+ # Check related move and quants
+ self.assertIn(inventory.name, inventory.move_ids.name)
+ self.assertEqual(inventory.move_ids.product_qty, 15.0)
+ self.assertEqual(inventory.move_ids.location_id, self.warehouse_1.lot_stock_id)
+ self.assertEqual(inventory.move_ids.location_dest_id, self.product_1.property_stock_inventory) # Inventory loss
+ self.assertEqual(inventory.move_ids.state, 'done')
+ quants = self.env['stock.quant']._gather(self.product_1, self.product_1.property_stock_inventory)
+ self.assertEqual(len(quants), 1) # One quant created for inventory loss
+
+ # Check quantity of product in various locations: current, its parent, brother and other
+ self.assertEqual(self.env['stock.quant']._gather(self.product_1, self.warehouse_1.lot_stock_id).quantity, 35.0)
+ self.assertEqual(self.env['stock.quant']._gather(self.product_1, self.warehouse_1.lot_stock_id.location_id).quantity, 35.0)
+ self.assertEqual(self.env['stock.quant']._gather(self.product_1, self.warehouse_1.view_location_id).quantity, 35.0)
+
+ self.assertEqual(self.env['stock.quant']._gather(self.product_1, self.warehouse_1.wh_input_stock_loc_id).quantity, 0.0)
+ self.assertEqual(self.env['stock.quant']._gather(self.product_1, self.env.ref('stock.stock_location_stock')).quantity, 0.0)
+
+ def test_inventory_wizard_as_manager(self):
+ """ Using the "Update Quantity" wizard as stock manager.
+ """
+ self.product_1.type = 'product'
+ InventoryWizard = self.env['stock.change.product.qty'].with_user(self.user_stock_manager)
+ inventory_wizard = InventoryWizard.create({
+ 'product_id': self.product_1.id,
+ 'product_tmpl_id': self.product_1.product_tmpl_id.id,
+ 'new_quantity': 50.0,
+ })
+ inventory_wizard.change_product_qty()
+ # Check quantity was updated
+ self.assertEqual(self.product_1.virtual_available, 50.0)
+ self.assertEqual(self.product_1.qty_available, 50.0)
+
+ # Check associated quants: 2 quants for the product and the quantity (1 in stock, 1 in inventory adjustment)
+ quant = self.env['stock.quant'].search([('id', 'not in', self.existing_quants.ids)])
+ self.assertEqual(len(quant), 2)
+
+ def test_inventory_wizard_as_user(self):
+ """ Using the "Update Quantity" wizard as stock user.
+ """
+ self.product_1.type = 'product'
+ InventoryWizard = self.env['stock.change.product.qty'].with_user(self.user_stock_user)
+ inventory_wizard = InventoryWizard.create({
+ 'product_id': self.product_1.id,
+ 'product_tmpl_id': self.product_1.product_tmpl_id.id,
+ 'new_quantity': 50.0,
+ })
+ # User has no right on quant, must raise an AccessError
+ with self.assertRaises(AccessError):
+ inventory_wizard.change_product_qty()
+ # Check quantity wasn't updated
+ self.assertEqual(self.product_1.virtual_available, 0.0)
+ self.assertEqual(self.product_1.qty_available, 0.0)
+
+ # Check associated quants: 0 quant expected
+ quant = self.env['stock.quant'].search([('id', 'not in', self.existing_quants.ids)])
+ self.assertEqual(len(quant), 0)
+
+ def test_basic_move(self):
+ product = self.product_3.with_user(self.user_stock_manager)
+ product.type = 'product'
+ picking_out = self.env['stock.picking'].create({
+ 'partner_id': self.partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ 'location_id': self.warehouse_1.lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ })
+ customer_move = self.env['stock.move'].create({
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': 5,
+ 'product_uom': product.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': self.warehouse_1.lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ })
+ # simulate create + onchange
+ # test move values
+ self.assertEqual(customer_move.product_uom, product.uom_id)
+ self.assertEqual(customer_move.location_id, self.warehouse_1.lot_stock_id)
+ self.assertEqual(customer_move.location_dest_id, self.env.ref('stock.stock_location_customers'))
+
+ # confirm move, check quantity on hand and virtually available, without location context
+ customer_move._action_confirm()
+ self.assertEqual(product.qty_available, 0.0)
+ self.assertEqual(product.virtual_available, -5.0)
+
+ customer_move.quantity_done = 5
+ customer_move._action_done()
+ self.assertEqual(product.qty_available, -5.0)
+
+ # compensate negative quants by receiving products from supplier
+ receive_move = self._create_move(product, self.env.ref('stock.stock_location_suppliers'), self.warehouse_1.lot_stock_id, product_uom_qty=15)
+
+ receive_move._action_confirm()
+ receive_move.quantity_done = 15
+ receive_move._action_done()
+
+ product._compute_quantities()
+ self.assertEqual(product.qty_available, 10.0)
+ self.assertEqual(product.virtual_available, 10.0)
+
+ # new move towards customer
+ customer_move_2 = self._create_move(product, self.warehouse_1.lot_stock_id, self.env.ref('stock.stock_location_customers'), product_uom_qty=2)
+
+ customer_move_2._action_confirm()
+ product._compute_quantities()
+ self.assertEqual(product.qty_available, 10.0)
+ self.assertEqual(product.virtual_available, 8.0)
+
+ customer_move_2.quantity_done = 2.0
+ customer_move_2._action_done()
+ product._compute_quantities()
+ self.assertEqual(product.qty_available, 8.0)
+
+ def test_inventory_adjustment_and_negative_quants_1(self):
+ """Make sure negative quants from returns get wiped out with an inventory adjustment"""
+ productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'})
+ stock_location = self.env.ref('stock.stock_location_stock')
+ customer_location = self.env.ref('stock.stock_location_customers')
+
+ # Create a picking out and force availability
+ picking_out = self.env['stock.picking'].create({
+ 'partner_id': self.partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ })
+ self.env['stock.move'].create({
+ 'name': productA.name,
+ 'product_id': productA.id,
+ 'product_uom_qty': 1,
+ 'product_uom': productA.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ })
+ picking_out.action_confirm()
+ picking_out.move_lines.quantity_done = 1
+ picking_out._action_done()
+
+ quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)])
+ self.assertEqual(len(quant), 1)
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking_out.ids, active_id=picking_out.ids[0],
+ active_model='stock.picking'))
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 1.0
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+ return_pick.action_assign()
+ return_pick.move_lines.quantity_done = 1
+ return_pick._action_done()
+
+ quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)])
+ self.assertEqual(sum(quant.mapped('quantity')), 0)
+
+ def test_inventory_adjustment_and_negative_quants_2(self):
+ """Make sure negative quants get wiped out with an inventory adjustment"""
+ productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'})
+ stock_location = self.env.ref('stock.stock_location_stock')
+ customer_location = self.env.ref('stock.stock_location_customers')
+ location_loss = productA.property_stock_inventory
+
+ # Create a picking out and force availability
+ picking_out = self.env['stock.picking'].create({
+ 'partner_id': self.partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ })
+ self.env['stock.move'].create({
+ 'name': productA.name,
+ 'product_id': productA.id,
+ 'product_uom_qty': 1,
+ 'product_uom': productA.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ })
+ picking_out.action_confirm()
+ picking_out.move_lines.quantity_done = 1
+ picking_out._action_done()
+
+ # Make an inventory adjustment to set the quantity to 0
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'Starting for product_1',
+ 'location_ids': [(4, stock_location.id)],
+ 'product_ids': [(4, productA.id)],
+ })
+ inventory.action_start()
+ self.assertEqual(len(inventory.line_ids), 1, "Wrong inventory lines generated.")
+ self.assertEqual(inventory.line_ids.theoretical_qty, -1, "Theoretical quantity should be -1.")
+ inventory.line_ids.product_qty = 0 # Put the quantity back to 0
+ inventory.action_validate()
+
+ # The inventory adjustment should have created one
+ self.assertEqual(len(inventory.move_ids), 1)
+ quantity = inventory.move_ids.mapped('product_qty')
+ self.assertEqual(quantity, [1], "Moves created with wrong quantity.")
+ location_ids = inventory.move_ids.mapped('location_id').ids
+ self.assertEqual(set(location_ids), {location_loss.id})
+
+ # There should be no quant in the stock location
+ quants = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)])
+ self.assertEqual(sum(quants.mapped('quantity')), 0)
+
+ # There should be one quant in the inventory loss location
+ quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)])
+ self.assertEqual(len(quant), 1)
+
+ def test_resupply_route(self):
+ """ Simulate a resupply chain between warehouses.
+ Stock -> transit -> Dist. -> transit -> Shop -> Customer
+ Create the move from Shop to Customer and ensure that all the pull
+ rules are triggered in order to complete the move chain to Stock.
+ """
+ warehouse_stock = self.env['stock.warehouse'].create({
+ 'name': 'Stock.',
+ 'code': 'STK',
+ })
+
+ warehouse_distribution = self.env['stock.warehouse'].create({
+ 'name': 'Dist.',
+ 'code': 'DIST',
+ 'resupply_wh_ids': [(6, 0, [warehouse_stock.id])]
+ })
+
+ warehouse_shop = self.env['stock.warehouse'].create({
+ 'name': 'Shop',
+ 'code': 'SHOP',
+ 'resupply_wh_ids': [(6, 0, [warehouse_distribution.id])]
+ })
+
+ route_stock_to_dist = warehouse_distribution.resupply_route_ids
+ route_dist_to_shop = warehouse_shop.resupply_route_ids
+
+ # Change the procure_method on the pull rules between dist and shop
+ # warehouses. Since mto and resupply routes are both on product it will
+ # select one randomly between them and if it select the resupply it is
+ # 'make to stock' and it will not create the picking between stock and
+ # dist warehouses.
+ route_dist_to_shop.rule_ids.write({'procure_method': 'make_to_order'})
+
+ product = self.env['product.product'].create({
+ 'name': 'Fakir',
+ 'type': 'product',
+ 'route_ids': [(4, route_id) for route_id in [route_stock_to_dist.id, route_dist_to_shop.id, self.env.ref('stock.route_warehouse0_mto').id]],
+ })
+
+ picking_out = self.env['stock.picking'].create({
+ 'partner_id': self.partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ 'location_id': warehouse_shop.lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ })
+ self.env['stock.move'].create({
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': 1,
+ 'product_uom': product.uom_id.id,
+ 'picking_id': picking_out.id,
+ 'location_id': warehouse_shop.lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'warehouse_id': warehouse_shop.id,
+ 'procure_method': 'make_to_order',
+ })
+ picking_out.action_confirm()
+
+ moves = self.env['stock.move'].search([('product_id', '=', product.id)])
+ # Shop/Stock -> Customer
+ # Transit -> Shop/Stock
+ # Dist/Stock -> Transit
+ # Transit -> Dist/Stock
+ # Stock/Stock -> Transit
+ self.assertEqual(len(moves), 5, 'Invalid moves number.')
+ self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_stock.lot_stock_id.id)]))
+ self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_distribution.lot_stock_id.id)]))
+ self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_distribution.lot_stock_id.id)]))
+ self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_shop.lot_stock_id.id)]))
+ self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_shop.lot_stock_id.id)]))
+
+ def test_mutiple_resupply_warehouse(self):
+ """ Simulate the following situation:
+ - 2 shops with stock are resupply by 2 distinct warehouses
+ - Shop Namur is resupply by the warehouse stock Namur
+ - Shop Wavre is resupply by the warehouse stock Wavre
+ - Simulate 2 moves for the same product but in different shop.
+ This test ensure that the move are supplied by the correct distribution
+ warehouse.
+ """
+ customer_location = self.env.ref('stock.stock_location_customers')
+
+ warehouse_distribution_wavre = self.env['stock.warehouse'].create({
+ 'name': 'Stock Wavre.',
+ 'code': 'WV',
+ })
+
+ warehouse_shop_wavre = self.env['stock.warehouse'].create({
+ 'name': 'Shop Wavre',
+ 'code': 'SHWV',
+ 'resupply_wh_ids': [(6, 0, [warehouse_distribution_wavre.id])]
+ })
+
+ warehouse_distribution_namur = self.env['stock.warehouse'].create({
+ 'name': 'Stock Namur.',
+ 'code': 'NM',
+ })
+
+ warehouse_shop_namur = self.env['stock.warehouse'].create({
+ 'name': 'Shop Namur',
+ 'code': 'SHNM',
+ 'resupply_wh_ids': [(6, 0, [warehouse_distribution_namur.id])]
+ })
+
+ route_shop_namur = warehouse_shop_namur.resupply_route_ids
+ route_shop_wavre = warehouse_shop_wavre.resupply_route_ids
+ # The product contains the 2 resupply routes.
+ product = self.env['product.product'].create({
+ 'name': 'Fakir',
+ 'type': 'product',
+ 'route_ids': [(4, route_id) for route_id in [route_shop_namur.id, route_shop_wavre.id, self.env.ref('stock.route_warehouse0_mto').id]],
+ })
+
+ # Add 1 quant in each distribution warehouse.
+ self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_wavre.lot_stock_id, 1.0)
+ self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_namur.lot_stock_id, 1.0)
+
+ # Create the move for the shop Namur. Should create a resupply from
+ # distribution warehouse Namur.
+ picking_out_namur = self.env['stock.picking'].create({
+ 'partner_id': self.partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ 'location_id': warehouse_shop_namur.lot_stock_id.id,
+ 'location_dest_id': customer_location.id,
+ })
+ self.env['stock.move'].create({
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': 1,
+ 'product_uom': product.uom_id.id,
+ 'picking_id': picking_out_namur.id,
+ 'location_id': warehouse_shop_namur.lot_stock_id.id,
+ 'location_dest_id': customer_location.id,
+ 'warehouse_id': warehouse_shop_namur.id,
+ 'procure_method': 'make_to_order',
+ })
+ picking_out_namur.action_confirm()
+
+ # Validate the picking
+ # Dist. warehouse Namur -> transit Location -> Shop Namur
+ picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_namur.lot_stock_id.id)])
+ self.assertTrue(picking_stock_transit)
+ picking_stock_transit.action_assign()
+ picking_stock_transit.move_lines[0].quantity_done = 1.0
+ picking_stock_transit._action_done()
+
+ picking_transit_shop_namur = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_namur.lot_stock_id.id)])
+ self.assertTrue(picking_transit_shop_namur)
+ picking_transit_shop_namur.action_assign()
+ picking_transit_shop_namur.move_lines[0].quantity_done = 1.0
+ picking_transit_shop_namur._action_done()
+
+ picking_out_namur.action_assign()
+ picking_out_namur.move_lines[0].quantity_done = 1.0
+ picking_out_namur._action_done()
+
+ # Check that the correct quantity has been provided to customer
+ self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 1)
+ # Ensure there still no quants in distribution warehouse
+ self.assertEqual(sum(self.env['stock.quant']._gather(product, warehouse_distribution_namur.lot_stock_id).mapped('quantity')), 0)
+
+ # Create the move for the shop Wavre. Should create a resupply from
+ # distribution warehouse Wavre.
+ picking_out_wavre = self.env['stock.picking'].create({
+ 'partner_id': self.partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ 'location_id': warehouse_shop_wavre.lot_stock_id.id,
+ 'location_dest_id': customer_location.id,
+ })
+ self.env['stock.move'].create({
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': 1,
+ 'product_uom': product.uom_id.id,
+ 'picking_id': picking_out_wavre.id,
+ 'location_id': warehouse_shop_wavre.lot_stock_id.id,
+ 'location_dest_id': customer_location.id,
+ 'warehouse_id': warehouse_shop_wavre.id,
+ 'procure_method': 'make_to_order',
+ })
+ picking_out_wavre.action_confirm()
+
+ # Validate the picking
+ # Dist. warehouse Wavre -> transit Location -> Shop Wavre
+ picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_wavre.lot_stock_id.id)])
+ self.assertTrue(picking_stock_transit)
+ picking_stock_transit.action_assign()
+ picking_stock_transit.move_lines[0].quantity_done = 1.0
+ picking_stock_transit._action_done()
+
+ picking_transit_shop_wavre = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_wavre.lot_stock_id.id)])
+ self.assertTrue(picking_transit_shop_wavre)
+ picking_transit_shop_wavre.action_assign()
+ picking_transit_shop_wavre.move_lines[0].quantity_done = 1.0
+ picking_transit_shop_wavre._action_done()
+
+ picking_out_wavre.action_assign()
+ picking_out_wavre.move_lines[0].quantity_done = 1.0
+ picking_out_wavre._action_done()
+
+ # Check that the correct quantity has been provided to customer
+ self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 2)
+ # Ensure there still no quants in distribution warehouse
+ self.assertEqual(sum(self.env['stock.quant']._gather(product, warehouse_distribution_wavre.lot_stock_id).mapped('quantity')), 0)
+
+ def test_noleak(self):
+ # non-regression test to avoid company_id leaking to other warehouses (see blame)
+ partner = self.env['res.partner'].create({'name': 'Chicago partner'})
+ company = self.env['res.company'].create({
+ 'name': 'My Company (Chicago)1',
+ 'currency_id': self.ref('base.USD')
+ })
+ self.env['stock.warehouse'].create({
+ 'name': 'Chicago Warehouse2',
+ 'company_id': company.id,
+ 'code': 'Chic2',
+ 'partner_id': partner.id
+ })
+ wh = self.env["stock.warehouse"].search([])
+
+ assert len(set(wh.mapped("company_id.id"))) > 1
+
+ companies_before = wh.mapped(lambda w: (w.id, w.company_id))
+ # writing on any field should change the company of warehouses
+ wh.write({"name": "whatever"})
+ companies_after = wh.mapped(lambda w: (w.id, w.company_id))
+
+ self.assertEqual(companies_after, companies_before)
+
+ def test_toggle_active_warehouse_1(self):
+ """ Basic test that create a warehouse with classic configuration.
+ Archive it and check that locations, picking types, routes, rules are
+ correclty active or archive.
+ """
+ wh = Form(self.env['stock.warehouse'])
+ wh.name = "The attic of Willy"
+ wh.code = "WIL"
+ warehouse = wh.save()
+
+ custom_location = Form(self.env['stock.location'])
+ custom_location.name = "A Trunk"
+ custom_location.location_id = warehouse.lot_stock_id
+ custom_location = custom_location.save()
+
+ # Archive warehouse
+ warehouse.toggle_active()
+ # Global rule
+ self.assertFalse(warehouse.mto_pull_id.active)
+
+ # Route
+ self.assertFalse(warehouse.reception_route_id.active)
+ self.assertFalse(warehouse.delivery_route_id.active)
+
+ # Location
+ self.assertFalse(warehouse.lot_stock_id.active)
+ self.assertFalse(warehouse.wh_input_stock_loc_id.active)
+ self.assertFalse(warehouse.wh_qc_stock_loc_id.active)
+ self.assertFalse(warehouse.wh_output_stock_loc_id.active)
+ self.assertFalse(warehouse.wh_pack_stock_loc_id.active)
+ self.assertFalse(custom_location.active)
+
+ # Picking Type
+ self.assertFalse(warehouse.in_type_id.active)
+ self.assertFalse(warehouse.in_type_id.show_operations)
+ self.assertFalse(warehouse.out_type_id.active)
+ self.assertFalse(warehouse.int_type_id.active)
+ self.assertFalse(warehouse.pick_type_id.active)
+ self.assertFalse(warehouse.pack_type_id.active)
+
+ # Active warehouse
+ warehouse.toggle_active()
+ # Global rule
+ self.assertTrue(warehouse.mto_pull_id.active)
+
+ # Route
+ self.assertTrue(warehouse.reception_route_id.active)
+ self.assertTrue(warehouse.delivery_route_id.active)
+
+ # Location
+ self.assertTrue(warehouse.lot_stock_id.active)
+ self.assertFalse(warehouse.wh_input_stock_loc_id.active)
+ self.assertFalse(warehouse.wh_qc_stock_loc_id.active)
+ self.assertFalse(warehouse.wh_output_stock_loc_id.active)
+ self.assertFalse(warehouse.wh_pack_stock_loc_id.active)
+ self.assertTrue(custom_location.active)
+
+ # Picking Type
+ self.assertTrue(warehouse.in_type_id.active)
+ self.assertFalse(warehouse.in_type_id.show_operations)
+ self.assertTrue(warehouse.out_type_id.active)
+ self.assertTrue(warehouse.int_type_id.active)
+ self.assertFalse(warehouse.pick_type_id.active)
+ self.assertFalse(warehouse.pack_type_id.active)
+
+ def test_toggle_active_warehouse_2(self):
+ wh = Form(self.env['stock.warehouse'])
+ wh.name = "The attic of Willy"
+ wh.code = "WIL"
+ wh.reception_steps = "two_steps"
+ wh.delivery_steps = "pick_pack_ship"
+ warehouse = wh.save()
+
+ warehouse.resupply_wh_ids = [(6, 0, [self.warehouse_1.id])]
+
+ custom_location = Form(self.env['stock.location'])
+ custom_location.name = "A Trunk"
+ custom_location.location_id = warehouse.lot_stock_id
+ custom_location = custom_location.save()
+
+ # Add a warehouse on the route.
+ warehouse.reception_route_id.write({
+ 'warehouse_ids': [(4, self.warehouse_1.id)]
+ })
+
+ route = Form(self.env['stock.location.route'])
+ route.name = "Stair"
+ route = route.save()
+
+ route.warehouse_ids = [(6, 0, [warehouse.id, self.warehouse_1.id])]
+
+ # Pre archive a location and a route
+ warehouse.delivery_route_id.toggle_active()
+ warehouse.wh_pack_stock_loc_id.toggle_active()
+
+ # Archive warehouse
+ warehouse.toggle_active()
+ # Global rule
+ self.assertFalse(warehouse.mto_pull_id.active)
+
+ # Route
+ self.assertTrue(warehouse.reception_route_id.active)
+ self.assertFalse(warehouse.delivery_route_id.active)
+ self.assertTrue(route.active)
+
+ # Location
+ self.assertFalse(warehouse.lot_stock_id.active)
+ self.assertFalse(warehouse.wh_input_stock_loc_id.active)
+ self.assertFalse(warehouse.wh_qc_stock_loc_id.active)
+ self.assertFalse(warehouse.wh_output_stock_loc_id.active)
+ self.assertFalse(warehouse.wh_pack_stock_loc_id.active)
+ self.assertFalse(custom_location.active)
+
+ # Picking Type
+ self.assertFalse(warehouse.in_type_id.active)
+ self.assertFalse(warehouse.out_type_id.active)
+ self.assertFalse(warehouse.int_type_id.active)
+ self.assertFalse(warehouse.pick_type_id.active)
+ self.assertFalse(warehouse.pack_type_id.active)
+
+ # Active warehouse
+ warehouse.toggle_active()
+ # Global rule
+ self.assertTrue(warehouse.mto_pull_id.active)
+
+ # Route
+ self.assertTrue(warehouse.reception_route_id.active)
+ self.assertTrue(warehouse.delivery_route_id.active)
+
+ # Location
+ self.assertTrue(warehouse.lot_stock_id.active)
+ self.assertTrue(warehouse.wh_input_stock_loc_id.active)
+ self.assertFalse(warehouse.wh_qc_stock_loc_id.active)
+ self.assertTrue(warehouse.wh_output_stock_loc_id.active)
+ self.assertTrue(warehouse.wh_pack_stock_loc_id.active)
+ self.assertTrue(custom_location.active)
+
+ # Picking Type
+ self.assertTrue(warehouse.in_type_id.active)
+ self.assertTrue(warehouse.out_type_id.active)
+ self.assertTrue(warehouse.int_type_id.active)
+ self.assertTrue(warehouse.pick_type_id.active)
+ self.assertTrue(warehouse.pack_type_id.active)
+
+ def test_edit_warehouse_1(self):
+ wh = Form(self.env['stock.warehouse'])
+ wh.name = "Chicago"
+ wh.code = "chic"
+ warehouse = wh.save()
+ self.assertEqual(warehouse.int_type_id.barcode, 'CHIC-INTERNAL')
+ self.assertEqual(warehouse.int_type_id.sequence_id.prefix, 'chic/INT/')
+
+ wh = Form(warehouse)
+ wh.code = 'CH'
+ wh.save()
+ self.assertEqual(warehouse.int_type_id.barcode, 'CH-INTERNAL')
+ self.assertEqual(warehouse.int_type_id.sequence_id.prefix, 'CH/INT/')
diff --git a/addons/stock/tests/test_wise_operator.py b/addons/stock/tests/test_wise_operator.py
new file mode 100644
index 00000000..73868f4c
--- /dev/null
+++ b/addons/stock/tests/test_wise_operator.py
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests.common import TransactionCase
+
+
+class TestWiseOperator(TransactionCase):
+
+ def test_wise_operator(self):
+
+ # Create a new storable product
+ product_wise = self.env['product.product'].create({
+ 'name': 'Wise Unit',
+ 'type': 'product',
+ 'categ_id': self.ref('product.product_category_1'),
+ 'uom_id': self.ref('uom.product_uom_unit'),
+ 'uom_po_id': self.ref('uom.product_uom_unit'),
+ })
+
+ self.partner = self.env['res.partner'].create({'name': 'Deco Addict'})
+
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
+ self.shelf2 = self.env['stock.location'].create({
+ 'name': 'Shelf 2',
+ 'barcode': 1231985,
+ 'location_id': warehouse.lot_stock_id.id
+ })
+ self.shelf1 = self.env['stock.location'].create({
+ 'name': 'Shelf 1',
+ 'barcode': 1231892,
+ 'location_id': warehouse.lot_stock_id.id
+ })
+
+ self.partner2 = self.env['res.partner'].create({'name': 'Ready Mat'})
+
+ # Create an incoming picking for this product of 10 PCE from suppliers to stock
+ vals = {
+ 'name': 'Incoming picking (wise unit)',
+ 'partner_id': self.partner.id,
+ 'picking_type_id': self.ref('stock.picking_type_in'),
+ 'location_id': self.ref('stock.stock_location_suppliers'),
+ 'location_dest_id': self.ref('stock.stock_location_stock'),
+ 'move_lines': [(0, 0, {
+ 'name': '/',
+ 'product_id': product_wise.id,
+ 'product_uom': product_wise.uom_id.id,
+ 'product_uom_qty': 10.00,
+ 'location_id': self.ref('stock.stock_location_suppliers'),
+ 'location_dest_id': self.ref('stock.stock_location_stock'),
+ })],
+ }
+ pick1_wise = self.env['stock.picking'].create(vals)
+ pick1_wise.onchange_picking_type()
+ pick1_wise.move_lines.onchange_product_id()
+
+ # Confirm and assign picking and prepare partial
+ pick1_wise.action_confirm()
+ pick1_wise.action_assign()
+
+ # Put 4 pieces in shelf1 and 6 pieces in shelf2
+ package1 = self.env['stock.quant.package'].create({'name': 'Pack 1'})
+ pick1_wise.move_line_ids[0].write({
+ 'result_package_id': package1.id,
+ 'qty_done': 4,
+ 'location_dest_id': self.shelf1.id
+ })
+ new_pack1 = self.env['stock.move.line'].create({
+ 'product_id': product_wise.id,
+ 'product_uom_id': self.ref('uom.product_uom_unit'),
+ 'picking_id': pick1_wise.id,
+ 'qty_done': 6.0,
+ 'location_id': self.ref('stock.stock_location_suppliers'),
+ 'location_dest_id': self.shelf2.id
+ })
+
+ # Transfer the receipt
+ pick1_wise._action_done()
+
+ # Check the system created 3 quants
+ records = self.env['stock.quant'].search([('product_id', '=', product_wise.id)])
+ self.assertEqual(len(records.ids), 3, "The number of quants created is not correct")
+
+ # Make a delivery order of 5 pieces to the customer
+ vals = {
+ 'name': 'outgoing picking 1 (wise unit)',
+ 'partner_id': self.partner2.id,
+ 'picking_type_id': self.ref('stock.picking_type_out'),
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'move_lines': [(0, 0, {
+ 'name': '/',
+ 'product_id': product_wise.id,
+ 'product_uom': product_wise.uom_id.id,
+ 'product_uom_qty': 5.0,
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ })],
+ }
+ delivery_order_wise1 = self.env['stock.picking'].create(vals)
+ delivery_order_wise1.onchange_picking_type()
+ delivery_order_wise1.move_lines.onchange_product_id()
+
+ # Assign and confirm
+ delivery_order_wise1.action_confirm()
+ delivery_order_wise1.action_assign()
+ self.assertEqual(delivery_order_wise1.state, 'assigned')
+
+ # Make a delivery order of 5 pieces to the customer
+ vals = {
+ 'name': 'outgoing picking 2 (wise unit)',
+ 'partner_id': self.partner2.id,
+ 'picking_type_id': self.ref('stock.picking_type_out'),
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'move_lines': [(0, 0, {
+ 'name': '/',
+ 'product_id': product_wise.id,
+ 'product_uom': product_wise.uom_id.id,
+ 'product_uom_qty': 5.0,
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ })],
+ }
+ delivery_order_wise2 = self.env['stock.picking'].create(vals)
+ delivery_order_wise2.onchange_picking_type()
+ delivery_order_wise2.move_lines.onchange_product_id()
+
+ # Assign and confirm
+ delivery_order_wise2.action_confirm()
+ delivery_order_wise2.action_assign()
+ self.assertEqual(delivery_order_wise2.state, 'assigned')
+
+ # The operator is a wise guy and decides to do the opposite of what Odoo proposes.
+ # He uses the products reserved on picking 1 on picking 2 and vice versa
+ move1 = delivery_order_wise1.move_lines[0]
+ move2 = delivery_order_wise2.move_lines[0]
+ pack_ids1 = delivery_order_wise1.move_line_ids
+ pack_ids2 = delivery_order_wise2.move_line_ids
+
+ self.assertEqual(pack_ids1.location_id.id, self.shelf2.id)
+ self.assertEqual(set(pack_ids2.mapped('location_id.id')), set([
+ self.shelf1.id,
+ self.shelf2.id]))
+
+ # put the move lines from delivery_order_wise2 into delivery_order_wise1
+ for pack_id2 in pack_ids2:
+ new_pack_id1 = pack_id2.copy(default={'picking_id': delivery_order_wise1.id, 'move_id': move1.id})
+ new_pack_id1.qty_done = pack_id2.product_qty
+
+ new_move_lines = delivery_order_wise1.move_line_ids.filtered(lambda p: p.qty_done)
+ self.assertEqual(sum(new_move_lines.mapped('product_qty')), 0)
+ self.assertEqual(sum(new_move_lines.mapped('qty_done')), 5)
+ self.assertEqual(set(new_move_lines.mapped('location_id.id')), set([
+ self.shelf1.id,
+ self.shelf2.id]))
+
+ # put the move line from delivery_order_wise1 into delivery_order_wise2
+ new_pack_id2 = pack_ids1.copy(default={'picking_id': delivery_order_wise2.id, 'move_id': move2.id})
+ new_pack_id2.qty_done = pack_ids1.product_qty
+
+ new_move_lines = delivery_order_wise2.move_line_ids.filtered(lambda p: p.qty_done)
+ self.assertEqual(len(new_move_lines), 1)
+ self.assertEqual(sum(new_move_lines.mapped('product_qty')), 0)
+ self.assertEqual(sum(new_move_lines.mapped('qty_done')), 5)
+ self.assertEqual(new_move_lines.location_id.id, self.shelf2.id)
+
+ # Process this picking
+ delivery_order_wise1._action_done()
+
+ # Check there was no negative quant created by this picking
+
+ records = self.env['stock.quant'].search([
+ ('product_id', '=', product_wise.id),
+ ('quantity', '<', 0.0),
+ ('location_id.id', '=', self.ref('stock.stock_location_stock'))])
+ self.assertEqual(len(records.ids), 0, 'This should not have created a negative quant')
+
+ # Check the other delivery order has changed its state back to ready
+ self.assertEqual(delivery_order_wise2.state, 'assigned', "Delivery order 2 should be back in ready state")
+
+ # Process the second picking
+ delivery_order_wise2._action_done()
+
+ # Check all quants are in Customers and there are no negative quants anymore
+ records = self.env['stock.quant'].search([
+ ('product_id', '=', product_wise.id),
+ ('location_id', '!=', self.ref('stock.stock_location_suppliers'))])
+ self.assertTrue(all([x.location_id.id == self.ref('stock.stock_location_customers') and x.quantity > 0.0 or
+ x.location_id.id != self.ref('stock.stock_location_customers') and x.quantity == 0.0 for x in records]),
+ "Negative quant or wrong location detected")