diff --git a/addons/stock/__manifest__.py b/addons/stock/__manifest__.py index eae8a525651..5d6bf5a63a8 100644 --- a/addons/stock/__manifest__.py +++ b/addons/stock/__manifest__.py @@ -19,6 +19,7 @@ 'data/stock_demo.yml', 'data/stock_location_demo_cpu1.xml', 'data/stock_location_demo_cpu3.yml', + 'data/stock_quant_demo.xml', ], 'data': [ 'security/stock_security.xml', diff --git a/addons/stock/data/stock_quant_demo.xml b/addons/stock/data/stock_quant_demo.xml new file mode 100644 index 00000000000..54693634dfd --- /dev/null +++ b/addons/stock/data/stock_quant_demo.xml @@ -0,0 +1,12 @@ + + + Whiteboard + product + + + + + + 10.0 + + diff --git a/addons/stock/tests/__init__.py b/addons/stock/tests/__init__.py index 92b1e412f45..a9a18e270e4 100644 --- a/addons/stock/tests/__init__.py +++ b/addons/stock/tests/__init__.py @@ -5,3 +5,7 @@ from . import test_product from . import test_warehouse from . import test_shipment from . import test_stock_location_search +from . import test_quant +from . import test_inventory +from . import test_move +from . import test_move2 diff --git a/addons/stock/tests/test_inventory.py b/addons/stock/tests/test_inventory.py new file mode 100644 index 00000000000..c9a40aa9038 --- /dev/null +++ b/addons/stock/tests/test_inventory.py @@ -0,0 +1,143 @@ +# -*- 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 TransactionCase + + +class TestInventory(TransactionCase): + def setUp(self): + super(TestInventory, self).setUp() + self.stock_location = self.env.ref('stock.stock_location_stock') + self.uom_unit = self.env.ref('product.product_uom_unit') + self.product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + 'categ_id': self.env.ref('product.product_category_all').id, + }) + self.product2 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + 'tracking': 'serial', + 'categ_id': self.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']._increase_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', + 'filter': 'product', + 'location_id': self.stock_location.id, + 'product_id': self.product1.id, + }) + inventory.prepare_inventory() + 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_done() + + # check + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 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', + 'filter': 'product', + 'location_id': self.stock_location.id, + 'product_id': self.product2.id, + 'exhausted': True, # should be set by an onchange + }) + inventory.prepare_inventory() + self.assertEqual(len(inventory.line_ids), 1) + self.assertEqual(inventory.line_ids.theoretical_qty, 0) + + lot1 = self.env['stock.production.lot'].create({ + 'name': 'sn2', + 'product_id': self.product2.id, + }) + + inventory.line_ids.prod_lot_id = lot1 + inventory.line_ids.product_qty = 1 + + inventory.action_done() + + # 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', + 'filter': 'product', + 'location_id': self.stock_location.id, + 'product_id': self.product2.id, + 'exhausted': True, # should be set by an onchange + }) + inventory.prepare_inventory() + self.assertEqual(len(inventory.line_ids), 1) + self.assertEqual(inventory.line_ids.theoretical_qty, 0) + + lot1 = self.env['stock.production.lot'].create({ + 'name': 'sn2', + 'product_id': self.product2.id, + }) + + inventory.line_ids.prod_lot_id = lot1 + inventory.line_ids.product_qty = 2 + + with self.assertRaises(ValidationError): + inventory.action_done() + + 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', + 'filter': 'product', + 'location_id': self.stock_location.id, + 'product_id': self.product2.id, + 'exhausted': True, # should be set by an onchange + }) + inventory.prepare_inventory() + self.assertEqual(len(inventory.line_ids), 1) + self.assertEqual(inventory.line_ids.theoretical_qty, 0) + + lot1 = self.env['stock.production.lot'].create({ + 'name': 'sn2', + 'product_id': self.product2.id, + }) + + inventory.line_ids.prod_lot_id = lot1 + inventory.line_ids.product_qty = 1 + + 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, + }) + inventory.action_done() + + # check + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot1, strict=True), 1.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)), 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) diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py new file mode 100644 index 00000000000..cde1160a82c --- /dev/null +++ b/addons/stock/tests/test_move.py @@ -0,0 +1,2204 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError + + +class StockMove(TransactionCase): + def setUp(self): + super(StockMove, self).setUp() + 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.pack_location = self.env.ref('stock.location_pack_zone') + self.uom_unit = self.env.ref('product.product_uom_unit') + self.product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + 'categ_id': self.env.ref('product.product_category_all').id, + }) + self.product2 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + 'tracking': 'serial', + 'categ_id': self.env.ref('product.product_category_all').id, + }) + self.product3 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + 'tracking': 'lot', + 'categ_id': self.env.ref('product.product_category_all').id, + }) + + 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.product1.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.pack_operation_ids), 1) + + # fill the move line + move_line = move1.pack_operation_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.product1, self.supplier_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 100.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.supplier_location)), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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.product3.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, 'confirmed') + + # assignment + move1.action_assign() + self.assertEqual(move1.state, 'assigned') + self.assertEqual(len(move1.pack_operation_ids), 1) + move_line = move1.pack_operation_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, 5) # don't change reservation + self.assertEqual(move1.state, 'done') + + # no quants are created in the supplier location + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.supplier_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.stock_location), 5.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product3, self.supplier_location)), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product3, self.stock_location)), 1.0) + + def test_in_3(self): + """ Receive 5 serial-tracked products from a supplier. The system should create 5 differents + 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.product2.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, 'confirmed') + + # assignment + move1.action_assign() + self.assertEqual(move1.state, 'assigned') + self.assertEqual(len(move1.pack_operation_ids), 5) + move_line = move1.pack_operation_ids[0] + self.assertEqual(move1.reserved_availability, 5) + + i = 0 + for move_line in move1.pack_operation_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') + + # no quants are created in the supplier location + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.supplier_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location), 5.0) + + self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.supplier_location)), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location)), 5.0) + + 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']._increase_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) + + # 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.product1.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.pack_operation_ids), 1) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + # Should be a reserved quantity and thus a quant. + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0) + + # fill the move line + move_line = move1.pack_operation_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 + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.customer_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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.product1, self.stock_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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.product1.type = 'consu' + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.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.pack_operation_ids), 1) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + # Should be a reserved quantity and thus a quant. + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 0.0) + + # fill the move line + move_line = move1.pack_operation_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 + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.customer_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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.product1, self.stock_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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.product3.id, + }) + self.env['stock.quant']._increase_available_quantity(self.product3, self.stock_location, 2) + self.env['stock.quant']._increase_available_quantity(self.product3, self.stock_location, 3, lot_id=lot1) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, 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.product3.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 5.0, + }) + move1.action_confirm() + move1.action_assign() + + self.assertEqual(len(move1.pack_operation_ids), 2) + + def test_mixed_tracking_reservation_2(self): + """ Send products tracked by lot to a customer. In your stock, there 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.product2.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': self.product2.id, + }) + self.env['stock.quant']._increase_available_quantity(self.product2, self.stock_location, 2) + self.env['stock.quant']._increase_available_quantity(self.product2, self.stock_location, 1, lot_id=lot1) + self.env['stock.quant']._increase_available_quantity(self.product2, self.stock_location, 1, lot_id=lot2) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, 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.product2.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 4.0, + }) + move1.action_confirm() + move1.action_assign() + self.assertEqual(len(move1.pack_operation_ids), 4) + for ml in move1.pack_operation_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.product2.id, + }) + lot4 = self.env['stock.production.lot'].create({ + 'name': 'lot4', + 'product_id': self.product2.id, + }) + untracked_move_line = move1.pack_operation_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.pack_operation_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.env['stock.quant']._gather(self.product2, self.stock_location, strict=True)), 1.0) # with a qty of 2 + self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location, lot_id=lot1, strict=True)), 1.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location, lot_id=lot2, strict=True)), 1.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location, lot_id=lot3, strict=True)), 0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location, lot_id=lot4, strict=True)), 0) + + move1.pack_operation_ids.write({'qty_done': 1.0}) + + move1.action_done() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot1, strict=True), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot2, strict=True), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot3, strict=True), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, 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.product2.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': self.product2.id, + }) + self.env['stock.quant']._increase_available_quantity(self.product2, self.stock_location, 1, lot_id=lot1) + self.env['stock.quant']._increase_available_quantity(self.product2, self.stock_location, 1, lot_id=lot2) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, 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.product2.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 2.0, + }) + move1.action_confirm() + move1.action_assign() + move1.pack_operation_ids.write({'qty_done': 1.0}) + move1.action_done() + + self.env['stock.quant']._increase_available_quantity(self.product2, self.stock_location, 2) + lot3 = self.env['stock.production.lot'].create({ + 'name': 'lot3', + 'product_id': self.product2.id, + }) + lot4 = self.env['stock.production.lot'].create({ + 'name': 'lot4', + 'product_id': self.product2.id, + }) + + self.env['stock.pack.operation'].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.pack.operation'].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.product2, self.stock_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot1, strict=True), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot2, strict=True), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot3, strict=True), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, 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.product2.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': self.product2.id, + }) + self.env['stock.quant']._increase_available_quantity(self.product2, self.stock_location, 1, lot_id=lot1) + self.env['stock.quant']._increase_available_quantity(self.product2, self.stock_location, 1, lot_id=lot2) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, 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.product2.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 2.0, + }) + move1.action_confirm() + move1.action_assign() + move1.pack_operation_ids.write({'qty_done': 1.0}) + move1.action_done() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot1, strict=True), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot2, strict=True), 0.0) + + self.env['stock.quant']._increase_available_quantity(self.product2, self.stock_location, 1) + lot3 = self.env['stock.production.lot'].create({ + 'name': 'lot3', + 'product_id': self.product2.id, + }) + + move1.with_context(debug=True).pack_operation_ids[1].lot_id = lot3 + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot1, strict=True), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot2, strict=True), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, 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.product2.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']._increase_available_quantity(self.product2, self.stock_location, 1.0) + lot1 = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': self.product2.id, + }) + + # create a new move line with a lot not assigned to any quant + self.env['stock.pack.operation'].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.pack_operation_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.product2, self.stock_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location)), 0.0) + + def test_mixed_tracking_reservation_6(self): + # create an untracked quant + self.env['stock.quant']._increase_available_quantity(self.product2, 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.product2.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.product2.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': self.product2.id, + }) + + move_line = move1.pack_operation_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.product2, self.stock_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location)), 0.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 = self.env['product.putaway'].create({ + 'name': 'putaway stock->shelf1', + 'fixed_location_ids': [(0, 0, { + 'category_id': self.env.ref('product.product_category_all').id, + 'fixed_location_id': shelf1_location.id, + })] + }) + self.stock_location.write({ + 'putaway_strategy_id': putaway.id, + }) + + # 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 100.0, + }) + move1.action_confirm() + self.assertEqual(move1.state, 'confirmed') + + # assignment + move1.action_assign() + self.assertEqual(move1.state, 'assigned') + self.assertEqual(len(move1.pack_operation_ids), 1) + + # check if the putaway was rightly applied + self.assertEqual(move1.pack_operation_ids.location_dest_id.id, shelf1_location.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']._increase_available_quantity(self.product1, self.stock_location, 150.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 100.0, + }) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 150.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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']._increase_available_quantity(self.product1, self.stock_location, 50.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 100.0, + }) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 50.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0) + self.assertEqual(move1.availability, 50.0) + + 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']._increase_available_quantity(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 100.0, + }) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.pack_operation_ids), 1) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 50.0) + + # unreserve + move1.do_unreserve() + self.assertEqual(len(move1.pack_operation_ids), 0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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']._increase_available_quantity(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 100.0, + }) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.pack_operation_ids), 1) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, package_id=package1), 50.0) + + # unreserve + move1.do_unreserve() + self.assertEqual(len(move1.pack_operation_ids), 0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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']._increase_available_quantity(self.product1, self.stock_location, 2) + 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), 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.product1.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.pack_operation_ids), 1) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + quants = self.env['stock.quant']._gather(self.product1, 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.product1, 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.pack_operation_ids), 0.0) + + def test_unreserve_4(self): + """ Check the unreservation of a partially available stock move. + """ + # make some stock + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 2) + 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), 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.product1.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.pack_operation_ids), 1) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + quants = self.env['stock.quant']._gather(self.product1, 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.product1, 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.pack_operation_ids), 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']._increase_available_quantity(self.product1, self.stock_location, 1.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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.product1.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.product1.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.pack_operation_ids[0].qty_done = 1.0 + move_stock_pack.action_done() + self.assertEqual(len(move_pack_cust.pack_operation_ids), 1) + move_line = move_pack_cust.pack_operation_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.product1.id, + }) + + # make some stock + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 1.0, lot_id=lot1) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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.product1.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.product1.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.pack_operation_ids[0] + self.assertEqual(move_line_stock_pack.lot_id.id, lot1.id) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location, lot1)), 1.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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.product1, self.stock_location, lot_id=lot1), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location, lot1)), 0.0) + + move_line_pack_cust = move_pack_cust.pack_operation_ids[0] + self.assertEqual(move_line_pack_cust.lot_id.id, lot1.id) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.pack_location, lot_id=lot1), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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']._increase_available_quantity(self.product1, self.stock_location, 2.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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.product1.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.product1.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.product1.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, 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.pack_operation_ids), 1) + move_stock_pack_1.pack_operation_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.pack_operation_ids), 1) + # Should have 1 quant in stock_location and another in pack_location + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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.pack_operation_ids), 1) + move_stock_pack_2.pack_operation_ids[0].qty_done = 1.0 + move_stock_pack_2.action_done() + self.assertEqual(move_stock_pack_2.state, 'done') + + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.pack_location)), 1.0) + + self.assertEqual(move_pack_cust.state, 'assigned') + self.assertEqual(len(move_pack_cust.pack_operation_ids), 1) + move_line_1 = move_pack_cust.pack_operation_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.product1.id, + }) + + # make some stock + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 2.0, lot_id=lot1) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, 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.product1.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.product1.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.product1.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, 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.pack_operation_ids), 1) + self.assertEqual(move_stock_pack_1.pack_operation_ids[0].lot_id.id, lot1.id) + move_stock_pack_1.pack_operation_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.pack_operation_ids), 1) + + move_stock_pack_2.action_assign() + self.assertEqual(len(move_stock_pack_2.pack_operation_ids), 1) + self.assertEqual(move_stock_pack_2.pack_operation_ids[0].lot_id.id, lot1.id) + move_stock_pack_2.pack_operation_ids[0].qty_done = 1.0 + move_stock_pack_2.action_done() + + self.assertEqual(len(move_pack_cust.pack_operation_ids), 1) + move_line_1 = move_pack_cust.pack_operation_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']._increase_available_quantity(self.product1, 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.product1.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.product1.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.product1.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, 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.pack_operation_ids), 1) + move_stock_pack.pack_operation_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.pack_operation_ids), 1) + self.assertEqual(len(move_pack_cust_2.pack_operation_ids), 1) + + move_pack_cust_1.pack_operation_ids[0].qty_done = 1.0 + move_pack_cust_2.pack_operation_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.product1.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.product1.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.product1.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, 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.product1.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, 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, 'confirmed') + 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.product1, 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.pack_operation_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, 'confirmed') + self.assertEqual(move_stock_stock_1.state, 'assigned') + self.assertEqual(move_stock_stock_2.state, 'waiting') + + 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']._increase_available_quantity(self.product1, 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.product1.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.product1.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.product1, 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') + + # force assign the second one + move2.force_assign() + self.assertEqual(move2.state, 'assigned') + + # use the product from the first one + move2.write({'pack_operation_ids': [(0, 0, { + 'product_id': self.product1.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.product1, 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.product1.id, + }) + + # make some stock + self.env['stock.quant']._increase_available_quantity(self.product1, 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.product1.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.product1.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.product1, 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') + + # force assign the second one + move2.force_assign() + self.assertEqual(move2.state, 'assigned') + + # use the product from the first one + move2.write({'pack_operation_ids': [(0, 0, { + 'product_id': self.product1.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.product1, self.stock_location, lot_id=lot1), 0.0) + + 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']._increase_available_quantity(self.product1, shelf1_location, 1.0) + self.env['stock.quant']._increase_available_quantity(self.product1, shelf2_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf2_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.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.product1, shelf1_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf2_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + + move1.pack_operation_ids.location_id = shelf2_location.id + + self.assertEqual(move1.reserved_availability, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf2_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': self.product1.id, + }) + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 1.0, lot_id=lot1) + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 1.0, lot_id=lot2) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.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.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot2), 1.0) + + move1.pack_operation_ids.lot_id = lot2.id + + self.assertEqual(move1.reserved_availability, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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']._increase_available_quantity(self.product1, self.stock_location, 1.0, package_id=package1) + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 1.0, package_id=package2) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, package_id=package1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.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.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, package_id=package1), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, package_id=package2), 1.0) + + move1.pack_operation_ids.package_id = package2.id + + self.assertEqual(move1.reserved_availability, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, package_id=package1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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']._increase_available_quantity(self.product1, self.stock_location, 1.0, owner_id=owner1) + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 1.0, owner_id=owner2) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, owner_id=owner1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.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.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, owner_id=owner1), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, owner_id=owner2), 1.0) + + move1.pack_operation_ids.owner_id = owner2.id + + self.assertEqual(move1.reserved_availability, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, owner_id=owner1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': self.product1.id, + }) + package1 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_5'}) + + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 1.0, lot_id=lot1, package_id=package1) + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 1.0, lot_id=lot2) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1, package_id=package1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.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.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1, package_id=package1), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot2), 1.0) + move_line = move1.pack_operation_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.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1, package_id=package1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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']._increase_available_quantity(self.product1, shelf1_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.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.product1, shelf1_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf2_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + + move1.pack_operation_ids.location_id = shelf2_location.id + + self.assertEqual(move1.reserved_availability, 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product3.id, + }) + # make some stock without assigning a lot id + self.env['stock.quant']._increase_available_quantity(self.product3, 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.product3.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.pack_operation_ids), 1) + move_line = move1.pack_operation_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.with_context(debug=True).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, 5) # don't change reservation + self.assertEqual(move1.state, 'done') + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.stock_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.stock_location, lot_id=lot1, strict=True), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product3, self.stock_location)), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product3, 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.product3.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': self.product3.id, + }) + # make some stock without assigning a lot id + self.env['stock.quant']._increase_available_quantity(self.product3, self.stock_location, 3) + self.env['stock.quant']._increase_available_quantity(self.product3, 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.product3.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.pack_operation_ids), 2) + + tracked_move_line = None + untracked_move_line = None + for move_line in move1.pack_operation_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, 3) # don't change reservation + self.assertEqual(tracked_move_line.product_qty, 2) # don't change reservation + self.assertEqual(move1.state, 'done') + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.stock_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.stock_location, lot_id=lot1, strict=True), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.stock_location, lot_id=lot2, strict=True), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product3, self.stock_location)), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product3, self.stock_location, lot_id=lot1, strict=True)), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product3, 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']._increase_available_quantity(self.product1, shelf1_location, 1.0) + self.env['stock.quant']._increase_available_quantity(self.product1, shelf2_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf2_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move1.action_confirm() + move1.action_assign() + move1.pack_operation_ids.qty_done = 1 + move1.action_done() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf2_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + + # edit once done, we actually moved from shelf2 + move1.pack_operation_ids.location_id = shelf2_location.id + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf2_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': self.product1.id, + }) + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 1.0, lot_id=lot1) + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 1.0, lot_id=lot2) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move1.action_confirm() + move1.action_assign() + move1.pack_operation_ids.qty_done = 1 + move1.action_done() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot2), 1.0) + + move1.pack_operation_ids.lot_id = lot2.id + + self.assertEqual(move1.reserved_availability, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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']._increase_available_quantity(self.product1, self.stock_location, 1.0, package_id=package1) + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 1.0, package_id=package2) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, package_id=package1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move1.action_confirm() + move1.action_assign() + move1.pack_operation_ids.qty_done = 1 + move1.action_done() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, package_id=package1), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, package_id=package2), 1.0) + + move1.pack_operation_ids.package_id = package2.id + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, package_id=package1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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']._increase_available_quantity(self.product1, self.stock_location, 1.0, owner_id=owner1) + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 1.0, owner_id=owner2) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, owner_id=owner1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move1.action_confirm() + move1.action_assign() + move1.pack_operation_ids.qty_done = 1 + move1.action_done() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, owner_id=owner1), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, owner_id=owner2), 1.0) + + move1.pack_operation_ids.owner_id = owner2.id + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, owner_id=owner1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': self.product1.id, + }) + package1 = self.env['stock.quant.package'].create({'name': 'test_edit_reserved_move_line_5'}) + + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 1.0, lot_id=lot1, package_id=package1) + self.env['stock.quant']._increase_available_quantity(self.product1, self.stock_location, 1.0, lot_id=lot2) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1, package_id=package1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move1.action_confirm() + move1.action_assign() + move1.pack_operation_ids.qty_done = 1 + move1.action_done() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1, package_id=package1), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot2), 1.0) + move_line = move1.pack_operation_ids[0] + move_line.write({'package_id': False, 'lot_id': lot2.id}) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1, package_id=package1), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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']._increase_available_quantity(self.product1, shelf1_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move1.action_confirm() + move1.action_assign() + move1.pack_operation_ids.qty_done = 1 + move1.action_done() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf2_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + + move1.pack_operation_ids.location_id = shelf2_location.id + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf2_location), -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']._increase_available_quantity(self.product1, shelf1_location, 1.0) + self.env['stock.quant']._increase_available_quantity(self.product1, shelf2_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move1.action_confirm() + move1.action_assign() + move1.pack_operation_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.product1.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.product1, shelf1_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf2_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + + move1.pack_operation_ids.location_id = shelf2_location.id + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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']._increase_available_quantity(self.product1, shelf1_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move1.action_confirm() + move1.action_assign() + move1.pack_operation_ids.qty_done = 1 + move1.action_done() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + + # edit once done, we actually moved 2 products + move1.pack_operation_ids.qty_done = 2 + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), -1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), -1.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']._increase_available_quantity(self.product1, shelf1_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move1.action_confirm() + move1.action_assign() + move1.pack_operation_ids.qty_done = 1 + move1.action_done() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 0.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + + # edit once done, we actually moved 2 products + move1.pack_operation_ids.qty_done = 0 + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 10.0, + }) + move1.action_confirm() + move1.action_assign() + move1.pack_operation_ids.qty_done = 10 + move1.action_done() + + quant = self.env['stock.quant']._gather(self.product1, self.stock_location) + self.assertEqual(len(quant), 1.0) + + # edit once done, we actually moved 2 products + move1.pack_operation_ids.qty_done = 0 + + quant = self.env['stock.quant']._gather(self.product1, self.stock_location) + self.assertEqual(len(quant), 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 10.0, + }) + picking.action_confirm() + picking.action_assign() + move1.pack_operation_ids.qty_done = 10 + picking.action_done() + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 10.0) + self.env['stock.pack.operation'].create({ + 'picking_id': move1.pack_operation_ids.picking_id.id, + 'move_id': move1.pack_operation_ids.move_id.id, + 'product_id': move1.pack_operation_ids.product_id.id, + 'qty_done': move1.pack_operation_ids.qty_done, + 'product_uom_id': move1.product_uom.id, + 'location_id': move1.pack_operation_ids.location_id.id, + 'location_dest_id': move1.pack_operation_ids.location_dest_id.id, + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 20.0) + move1.pack_operation_ids[1].qty_done = 5 + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 15.0) + + def test_immediate_validate_1(self): + """ Create a picking and simulates validate button effect. + Move line quantity should be set to their reservation quantity automatically + """ + 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 + 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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 10.0, + }) + picking.action_confirm() + picking.action_assign() + res_dict = picking.button_validate() + wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id')) + wizard.process() + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 10.0) + + def test_immediate_validate_2(self): + """ Create a picking and simulates validate button effect. + Move line quantity should be set to their reservation quantity automatically. + If the Initial demand quantity is greater than the available quantity. It should only + set the quantity done to the available(reserved) quantity and create a backorder with the + remaining quantity. + """ + owner = self.env['res.partner'].create({'name': 'Jean'}) + self.env['stock.quant']._increase_available_quantity(self.product1, 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': owner.id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + }) + # move from shelf1 + self.env['stock.move'].create({ + 'name': 'test_edit_moveline_1', + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'picking_id': picking.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 10.0, + }) + picking.action_confirm() + picking.action_assign() + res_dict = picking.button_validate() + wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id')) + wizard.process() + + self.assertEqual(len(picking.move_lines.pack_operation_ids), 1) + self.assertEqual(picking.move_lines.quantity_done, 5.0) + self.assertEqual(picking.move_lines.pack_operation_ids.qty_done, 5.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 0.0) + + 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): + """ Create a picking and simulates validate button effect. + In case of a force assign it should ignore the reserved quantity and set + the quantity done to the initial demand. It should also create the pack_operation line. + """ + owner = 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': owner.id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + }) + # move from shelf1 + self.env['stock.move'].create({ + 'name': 'test_edit_moveline_1', + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'picking_id': picking.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 10.0, + }) + picking.action_confirm() + picking.force_assign() + res_dict = picking.button_validate() + wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id')) + wizard.process() + + self.assertEqual(picking.move_lines.quantity_done, 10.0) + # Check move_lines data + self.assertEqual(len(picking.move_lines.pack_operation_ids), 1) + self.assertEqual(picking.move_lines.pack_operation_ids.qty_done, 10.0) + self.assertEqual(picking.move_lines.pack_operation_ids.product_qty, 0.0) + # Check quants data + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), -10.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0) + + def test_immediate_validate_4(self): + """ Create a picking and simulates validate button effect. + If the product is tracked by lot. The user should set the lot + and the quantity done himself before validate. Otherwise it should + return a UserError. + """ + owner = self.env['res.partner'].create({'name': 'Jean'}) + lot1 = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': self.product3.id, + }) + self.env['stock.quant']._increase_available_quantity(self.product3, 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': owner.id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + }) + # move from shelf1 + self.env['stock.move'].create({ + 'name': 'test_edit_moveline_1', + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'picking_id': picking.id, + 'product_id': self.product3.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 5.0, + }) + picking.action_confirm() + picking.action_assign() + with self.assertRaises(UserError): + picking.button_validate() + picking.move_lines.pack_operation_ids[0].qty_done = 5.0 + # We do not need to catch wizard since everything is already defined and the action_done will be processed immediately + picking.button_validate() + + self.assertEqual(picking.move_lines.quantity_done, 5.0) + # Check move_lines data + self.assertEqual(len(picking.move_lines.pack_operation_ids), 1) + self.assertEqual(picking.move_lines.pack_operation_ids.lot_id, lot1) + self.assertEqual(picking.move_lines.pack_operation_ids.qty_done, 5.0) + # Check quants data + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 0.0) + + 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.product1.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.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 2.0, + }) + (move1 + move2).action_confirm() + (move1 + move2).force_assign() + (move1 + move2).write({'quantity_done': 1}) + self.assertEqual(move1.quantity_done, 1) + self.assertEqual(move2.quantity_done, 1) diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py new file mode 100644 index 00000000000..e0e7c1204e6 --- /dev/null +++ b/addons/stock/tests/test_move2.py @@ -0,0 +1,701 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.stock.tests.common import TestStockCommon + + +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, + 'partner_id': self.partner_delta_id, + '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', + }) + + picking_pick = self.env['stock.picking'].create({ + 'location_id': self.stock_location, + 'location_dest_id': self.pack_location, + 'partner_id': self.partner_delta_id, + '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_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']._increase_available_quantity(self.productA, location, 10.0) + picking_pick.action_assign() + picking_pick.move_lines[0].pack_operation_ids[0].qty_done = 10.0 + picking_pick.do_transfer() + + self.assertEqual(picking_client.state, 'assigned', 'The state of the client should be assigned') + + # Now partially transfer the ship + picking_client.move_lines[0].pack_operation_ids[0].qty_done = 5 + picking_client.do_transfer() # no new in order to create backorder + + backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_client.id)]) + self.assertEqual(backorder.state, 'assigned', 'Backorder should be started') + + 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']._increase_available_quantity(self.productA, stock_location, 10.0) + pack_location = self.env['stock.location'].browse(self.pack_location) + self.env['stock.quant']._increase_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.pack_operation_ids[0].qty_done = 10.0 + picking_pick.do_transfer() + + 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(len(self.env['stock.quant']._gather(self.productA, stock_location)), 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']._increase_available_quantity(self.productA, stock_location, 10.0) + + picking_pick.action_assign() + picking_pick.move_lines[0].pack_operation_ids[0].qty_done = 10.0 + picking_pick.do_transfer() + self.assertEqual(picking_pick.state, 'done') + self.assertEqual(picking_client.state, 'assigned') + + # return a part of what we've done + stock_return_picking = self.env['stock.return.picking']\ + .with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0])\ + .create({}) + 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].pack_operation_ids[0].qty_done = 2.0 + return_pick.do_transfer() + # the client picking should not be assigned anymore, as we returned partially what we took + self.assertEqual(picking_client.state, 'confirmed') + + 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']._increase_available_quantity(self.productA, location, 10.0) + picking_pick.action_assign() + picking_pick.move_lines[0].pack_operation_ids[0].qty_done = 5.0 + + # create a backorder + picking_pick.do_transfer() + picking_pick_backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_pick.id)]) + self.assertEqual(picking_pick_backorder.state, 'assigned') + self.assertEqual(picking_pick_backorder.pack_operation_ids.product_qty, 5.0) + + self.assertEqual(picking_client.state, 'partially_available') + + # cancel the backorder + picking_pick_backorder.action_cancel() + self.assertEqual(picking_client.state, 'partially_available') + + 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']._increase_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, 'partially_available', '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, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': self.productA.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']._increase_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() + + +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, + 'partner_id': self.partner_delta_id, + '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']._increase_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].pack_operation_ids[0].qty_done = 1 + delivery_order.do_transfer() + 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, 'assigned') + + 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, + 'partner_id': self.partner_delta_id, + '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']._increase_available_quantity(self.productA, pack_location, 1) + + # assign to partially available + delivery_order.action_confirm() + delivery_order.action_assign() + self.assertEqual(delivery_order.state, 'partially_available') + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0) + + # valid with backorder creation + delivery_order.move_lines[0].pack_operation_ids[0].qty_done = 1 + delivery_order.do_transfer() + 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_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, + 'partner_id': self.partner_delta_id, + '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']._increase_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].pack_operation_ids[0].qty_done = 2 + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0) + delivery_order.with_context(debug=True).do_transfer() + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), -1.0) + + extra_move = delivery_order.move_lines - move1 + extra_move_line = extra_move.pack_operation_ids[0] + + # self.assertEqual(move1.product_qty, 1.0) + # self.assertEqual(move1.quantity_done, 1.0) + self.assertEqual(move1.reserved_availability, 1.0) + self.assertEqual(move1.pack_operation_ids.product_qty, 1.0) # should keep the reservation + self.assertEqual(move1.pack_operation_ids.qty_done, 1.0) + self.assertEqual(move1.state, 'done') + + self.assertEqual(extra_move.product_qty, 1.0) + self.assertEqual(extra_move.quantity_done, 1.0) + self.assertEqual(extra_move.reserved_availability, 0.0) + self.assertEqual(extra_move_line.product_qty, 0.0) # should not be able to reserve + self.assertEqual(extra_move_line.qty_done, 1.0) + self.assertEqual(extra_move.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, + 'partner_id': self.partner_delta_id, + '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']._increase_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].pack_operation_ids[0].qty_done = 3 + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0) + delivery_order.do_transfer() + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), -2.0) + + extra_move = delivery_order.move_lines - move1 + extra_move_line = extra_move.pack_operation_ids[0] + + self.assertEqual(move1.product_qty, 1.0) + self.assertEqual(move1.quantity_done, 1.0) + self.assertEqual(move1.reserved_availability, 1.0) + self.assertEqual(move1.pack_operation_ids.product_qty, 1.0) # should keep the reservation + self.assertEqual(move1.pack_operation_ids.qty_done, 1.0) + self.assertEqual(move1.state, 'done') + + self.assertEqual(extra_move.product_qty, 2.0) + self.assertEqual(extra_move.quantity_done, 2.0) + self.assertEqual(extra_move.reserved_availability, 0.0) + self.assertEqual(extra_move_line.product_qty, 0.0) # should not be able to reserve + self.assertEqual(extra_move_line.qty_done, 2.0) + self.assertEqual(extra_move.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, + 'partner_id': self.partner_delta_id, + '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].pack_operation_ids[0].qty_done = 2 + receipt.with_context(debug=True).do_transfer() + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 2.0) + + extra_move = receipt.move_lines - move1 + extra_move_line = extra_move.pack_operation_ids[0] + + self.assertEqual(move1.product_qty, 1.0) + self.assertEqual(move1.quantity_done, 1.0) + self.assertEqual(move1.reserved_availability, 1.0) + self.assertEqual(move1.pack_operation_ids.product_qty, 1.0) # should keep the reservation + self.assertEqual(move1.pack_operation_ids.qty_done, 1.0) + self.assertEqual(move1.state, 'done') + + self.assertEqual(extra_move.product_qty, 1.0) + self.assertEqual(extra_move.quantity_done, 1.0) + self.assertEqual(extra_move.reserved_availability, 0.0) + self.assertEqual(extra_move_line.product_qty, 0.0) # should not be able to reserve + self.assertEqual(extra_move_line.qty_done, 1.0) + self.assertEqual(extra_move.state, 'done') + + 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']._increase_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, + 'partner_id': self.partner_delta_id, + '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, 'partially_available') + self.assertEqual(move1.state, 'partially_available') + + # Check reserved quantity + self.assertEqual(move1.reserved_availability, 1.0) + self.assertEqual(len(move1.pack_operation_ids), 1) + self.assertEqual(move1.pack_operation_ids.product_qty, 1) + + inventory = self.env['stock.inventory'].create({ + 'name': 'remove product1', + 'filter': 'product', + 'location_id': self.stock_location, + 'product_id': self.productA.id, + }) + inventory.prepare_inventory() + inventory.line_ids.product_qty = 2 + inventory.action_done() + 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.pack_operation_ids), 1) + self.assertEqual(move1.pack_operation_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, + }) + stock_location = self.env['stock.location'].browse(self.stock_location) + self.env['stock.quant']._increase_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, + 'partner_id': self.partner_delta_id, + '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, 'partially_available') + self.assertEqual(move1.state, 'partially_available') + + # Check reserved quantity + self.assertEqual(move1.reserved_availability, 1.0) + self.assertEqual(len(move1.pack_operation_ids), 1) + self.assertEqual(move1.pack_operation_ids.product_qty, 1) + + inventory = self.env['stock.inventory'].create({ + 'name': 'remove product1', + 'filter': 'product', + 'location_id': self.stock_location, + 'product_id': self.productA.id, + }) + inventory.prepare_inventory() + inventory.line_ids.prod_lot_id = lot1 + inventory.line_ids.product_qty = 2 + inventory.action_done() + 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.pack_operation_ids), 1) + self.assertEqual(move1.pack_operation_ids.lot_id.id, lot1.id) + self.assertEqual(move1.pack_operation_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, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': self.productA.id, + }) + stock_location = self.env['stock.location'].browse(self.stock_location) + self.env['stock.quant']._increase_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, + 'partner_id': self.partner_delta_id, + '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, 'partially_available') + self.assertEqual(move1.state, 'partially_available') + + # Check reserved quantity + self.assertEqual(move1.reserved_availability, 1.0) + self.assertEqual(len(move1.pack_operation_ids), 1) + self.assertEqual(move1.pack_operation_ids.product_qty, 1) + + inventory = self.env['stock.inventory'].create({ + 'name': 'remove product1', + 'filter': 'product', + 'location_id': self.stock_location, + 'product_id': self.productA.id, + }) + inventory.prepare_inventory() + self.env['stock.inventory.line'].create({ + 'inventory_id': inventory.id, + 'location_id': inventory.location_id.id, + 'partner_id': inventory.partner_id.id, + 'prod_lot_id': lot2.id, + 'product_id': self.productA.id, + 'product_qty': 1, + }) + inventory.action_done() + 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.pack_operation_ids), 2) + self.assertEqual(move1.pack_operation_ids[0].lot_id.id, lot1.id) + self.assertEqual(move1.pack_operation_ids[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, + }) + serial2 = self.env['stock.production.lot'].create({ + 'name': 'serial2', + 'product_id': self.productA.id, + }) + stock_location = self.env['stock.location'].browse(self.stock_location) + self.env['stock.quant']._increase_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, + 'partner_id': self.partner_delta_id, + '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, 'partially_available') + self.assertEqual(move1.state, 'partially_available') + + # Check reserved quantity + self.assertEqual(move1.reserved_availability, 1.0) + self.assertEqual(len(move1.pack_operation_ids), 1) + self.assertEqual(move1.pack_operation_ids.product_qty, 1) + + inventory = self.env['stock.inventory'].create({ + 'name': 'remove product1', + 'filter': 'product', + 'location_id': self.stock_location, + 'product_id': self.productA.id, + }) + inventory.prepare_inventory() + self.env['stock.inventory.line'].create({ + 'inventory_id': inventory.id, + 'location_id': inventory.location_id.id, + 'partner_id': inventory.partner_id.id, + 'prod_lot_id': serial2.id, + 'product_id': self.productA.id, + 'product_qty': 1, + }) + inventory.action_done() + 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.pack_operation_ids), 2) + self.assertEqual(move1.pack_operation_ids[0].lot_id.id, serial1.id) + self.assertEqual(move1.pack_operation_ids[1].lot_id.id, serial2.id) diff --git a/addons/stock/tests/test_quant.py b/addons/stock/tests/test_quant.py new file mode 100644 index 00000000000..a9dae64d77f --- /dev/null +++ b/addons/stock/tests/test_quant.py @@ -0,0 +1,564 @@ +# -*- 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 TransactionCase +from odoo.exceptions import AccessError, UserError + + +class StockQuant(TransactionCase): + def setUp(self): + super(StockQuant, self).setUp() + Users = self.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nosubscribe': True}) + self.demo_user = Users.create({ + 'name': 'Pauline Poivraisselle', + 'login': 'pauline', + 'email': 'p.p@example.com', + 'notification_type': 'inbox', + 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])] + }) + self.stock_user = Users.create({ + 'name': 'Pauline Poivraisselle', + 'login': 'pauline2', + 'email': 'p.p@example.com', + 'notification_type': 'inbox', + 'groups_id': [(6, 0, [self.env.ref('stock.group_stock_user').id])] + }) + + def test_get_available_quantity_1(self): + """ Quantity availability with only one quant in a location. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 1.0, + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 1.0) + + def test_get_available_quantity_2(self): + """ Quantity availability with multiple quants in a location. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + for i in range(3): + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 1.0, + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 3.0) + + def test_get_available_quantity_3(self): + """ Quantity availability with multiple quants (including negatives ones) in a location. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + for i in range(3): + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 1.0, + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': -3.0, + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0) + + def test_get_available_quantity_4(self): + """ Quantity availability with no quants in a location. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0) + + def test_get_available_quantity_5(self): + """ Quantity availability with multiple partially reserved quants in a location. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 10.0, + 'reserved_quantity': 9.0, + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 1.0, + 'reserved_quantity': 1.0, + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 1.0) + + def test_get_available_quantity_6(self): + """ Quantity availability with multiple partially reserved quants in a location. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 10.0, + 'reserved_quantity': 20.0, + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 5.0, + 'reserved_quantity': 0.0, + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), -5.0) + + def test_get_available_quantity_7(self): + """ Quantity availability with only one tracked quant in a location. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + 'tracking': 'lot', + }) + lot1 = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': product1.id, + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 10.0, + 'reserved_quantity': 20.0, + 'lot_id': lot1.id, + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, lot_id=lot1), -10.0) + + def test_get_available_quantity_8(self): + """ Quantity availability with a consumable product. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'consu', + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, stock_location)), 0) + with self.assertRaises(ValidationError): + self.env['stock.quant']._increase_available_quantity(product1, stock_location, 1.0) + + def test_get_available_quantity_9(self): + """ Quantity availability by a demo user with access rights/rules. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 1.0, + }) + self.env = self.env(user=self.env.ref('base.user_demo')) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 1.0) + + def test_increase_available_quantity_1(self): + """ Increase the available quantity when no quants are already in a location. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.env['stock.quant']._increase_available_quantity(product1, stock_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 1.0) + + def test_increase_available_quantity_2(self): + """ Increase the available quantity when multiple quants are already in a location. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + for i in range(2): + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 1.0, + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 2.0) + self.env['stock.quant']._increase_available_quantity(product1, stock_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 3.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, 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. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product = self.env.ref('stock.test_quant_product') + product.type = 'product' # product 12 is a consumable by default + self.assertEqual(self.env['stock.quant']._get_available_quantity(product, stock_location), 10.0) + + # opens a new cursor and SELECT FOR UPDATE the quant, to simulate another concurrent reserved + # quantity increase + cr2 = self.registry.cursor() + cr2.execute("SELECT id FROM stock_quant WHERE product_id=%s AND location_id=%s", (product.id, stock_location.id)) + quant_id = cr2.fetchone() + cr2.execute("SELECT 1 FROM stock_quant WHERE id=%s FOR UPDATE", quant_id) + + self.env['stock.quant']._increase_available_quantity(product, stock_location, 1.0) + cr2.rollback() + cr2.close() + self.assertEqual(self.env['stock.quant']._get_available_quantity(product, stock_location), 11.0) + self.assertEqual(len(self.env['stock.quant']._gather(product, stock_location)), 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. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.env = self.env(user=self.env.ref('base.user_demo')) + self.env['stock.quant']._increase_available_quantity(product1, stock_location, 1.0) + + def test_decrease_available_quantity_1(self): + """ Decrease the available quantity when no quants are already in a location. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.env['stock.quant']._decrease_available_quantity(product1, stock_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), -1.0) + + def test_decrease_available_quantity_2(self): + """ Decrease the available quantity when multiple quants are already in a location. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + for i in range(2): + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 1.0, + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 2.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, stock_location)), 2) + self.env['stock.quant']._decrease_available_quantity(product1, stock_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 1.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, 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. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product = self.env.ref('stock.test_quant_product') + self.assertEqual(self.env['stock.quant']._get_available_quantity(product, stock_location), 10.0) + quants = self.env['stock.quant']._gather(product, stock_location) + self.assertEqual(len(quants), 1) + + # opens a new cursor and SELECT FOR UPDATE the quant, to simulate another concurrent reserved + # quantity increase + cr2 = self.registry.cursor() + cr2.execute("SELECT 1 FROM stock_quant WHERE id = %s FOR UPDATE", quants.ids) + self.env['stock.quant']._decrease_available_quantity(product, stock_location, 1.0) + cr2.rollback() + cr2.close() + self.assertEqual(self.env['stock.quant']._get_available_quantity(product, stock_location), 9.0) + self.assertEqual(len(self.env['stock.quant']._gather(product, stock_location)), 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 + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 1.0, + }) + self.env = self.env(user=self.demo_user) + self.env['stock.quant']._decrease_available_quantity(product1, stock_location, 1.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, 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. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 10.0, + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 10.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, stock_location)), 1) + self.env['stock.quant']._increase_reserved_quantity(product1, stock_location, 10.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, 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. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + for i in range(2): + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 5.0, + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 10.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, stock_location)), 2) + self.env['stock.quant']._increase_reserved_quantity(product1, stock_location, 10.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, 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. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 5.0, + 'reserved_quantity': 2.0, + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 10.0, + 'reserved_quantity': 12.0, + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 8.0, + 'reserved_quantity': 3.0, + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': 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(product1, stock_location), 29.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, stock_location)), 4) + self.env['stock.quant']._increase_reserved_quantity(product1, stock_location, 10.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 19.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, 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. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 5.0, + 'reserved_quantity': 7.0, + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 12.0, + 'reserved_quantity': 10.0, + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, stock_location)), 2) + with self.assertRaises(UserError): + self.env['stock.quant']._increase_reserved_quantity(product1, stock_location, 10.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0) + + def test_increase_reserved_quantity_5(self): + """ Decrease the available quantity when no quant are in a location. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + with self.assertRaises(UserError): + self.env['stock.quant']._increase_reserved_quantity(product1, stock_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0) + + def test_decrease_reserved_quantity_1(self): + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 10.0, + 'reserved_quantity': 10.0, + }) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, stock_location)), 1) + self.env['stock.quant']._decrease_reserved_quantity(product1, stock_location, 10.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 10.0) + self.assertEqual(len(self.env['stock.quant']._gather(product1, stock_location)), 1) + + def test_increase_decrease_reserved_quantity_1(self): + """ Decrease then increase reserved quantity when no quant are in a location. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + with self.assertRaises(UserError): + self.env['stock.quant']._increase_reserved_quantity(product1, stock_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0) + with self.assertRaises(UserError): + self.env['stock.quant']._decrease_reserved_quantity(product1, stock_location, 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0) + + def test_action_done_1(self): + stock_location = self.env.ref('stock.stock_location_stock') + pack_location = self.env.ref('stock.location_pack_zone') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + self.env['stock.quant']._increase_available_quantity(product1, stock_location, 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 2.0) + self.env['stock.quant']._increase_reserved_quantity(product1, stock_location, 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0) + self.env['stock.quant']._decrease_reserved_quantity(product1, stock_location, 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 2.0) + self.env['stock.quant']._decrease_available_quantity(product1, stock_location, 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0) + self.env['stock.quant']._increase_available_quantity(product1, pack_location, 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, pack_location), 2.0) + + def test_mix_tracked_untracked_1(self): + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + 'tracking': 'serial', + }) + lot1 = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': product1.id, + }) + + # add one tracked, one untracked + self.env['stock.quant']._increase_available_quantity(product1, stock_location, 1.0) + self.env['stock.quant']._increase_available_quantity(product1, stock_location, 1.0, lot_id=lot1) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, strict=True), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, lot_id=lot1), 1.0) + + self.env['stock.quant']._increase_reserved_quantity(product1, stock_location, 1.0, lot_id=lot1, strict=True) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, strict=True), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, lot_id=lot1), 0.0) + + self.env['stock.quant']._decrease_reserved_quantity(product1, stock_location, 1.0, lot_id=lot1, strict=True) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, strict=True), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, lot_id=lot1), 1.0) + + with self.assertRaises(UserError): + self.env['stock.quant']._decrease_reserved_quantity(product1, stock_location, 1.0) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 2.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, strict=True), 1.0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, lot_id=lot1), 1.0) + + def test_access_rights_1(self): + """ Directly update the quant with a user with or without stock access rights sould raise + an AccessError. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + quant = self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': 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': product1.id, + 'location_id': stock_location.id, + 'quantity': 1.0, + }) + with self.assertRaises(AccessError): + quant.sudo(self.demo_user).write({'quantity': 2.0}) + with self.assertRaises(AccessError): + quant.sudo(self.demo_user).unlink() + + self.env = self.env(user=self.stock_user) + with self.assertRaises(AccessError): + self.env['stock.quant'].create({ + 'product_id': product1.id, + 'location_id': stock_location.id, + 'quantity': 1.0, + }) + with self.assertRaises(AccessError): + quant.sudo(self.demo_user).write({'quantity': 2.0}) + with self.assertRaises(AccessError): + quant.sudo(self.demo_user).unlink()