diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/stock/tests | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/stock/tests')
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") |
