From bdcb3d192be1e01e1141aa09c60027337009a67b Mon Sep 17 00:00:00 2001 From: Arnold Moyaux Date: Wed, 24 Feb 2021 09:30:10 +0000 Subject: [PATCH] [REF] stock: move stock.inventory feature into stock.quant This commit removes stock.inventory(.line) and moves its general functionality into stock.quant. Some features are lost during this switch as well. Feature Additions: - New single list view for inventory adjustments (no more multiple inventory adjustment records to keep track of!) - Improved cyclic counts (annual inventory day setting) + next inventory dates are immediately viewable in view (vs auto-generated inventories based only on location) - Specific quants (i.e. counts) can be assigned to users for more flexibility (vs only able to restrict by location + product combinations) - Counts can be requested (i.e. bulk assigned to user/for a specific inventory date) Feature Removals: - Can no longer look at previous inventory adjustments linked to a specific record. Each quant has a history button to show inventory related moves. [relevant account moves are also now harder to see via stock as well] Other changes: - Bulk of changes were for demo/test - Some changes were done to ensure "Update Quantity"/"Inventory Report" views still mainly function the same as before with the exception of a new column added to support updating quant quantities in these views. Task: 2440026 ENT PR: odoo/enterprise#17329 Upgrade PR: odoo/upgrade#2326 closes odoo/odoo#68409 Signed-off-by: Arnold Moyaux --- addons/mrp/data/mrp_demo.xml | 66 +- addons/mrp/tests/test_backorder.py | 2 +- addons/mrp/tests/test_order.py | 32 +- addons/mrp/tests/test_procurement.py | 4 +- addons/mrp/tests/test_stock.py | 45 +- addons/mrp/tests/test_traceability.py | 53 +- addons/mrp_account/tests/test_mrp_account.py | 47 +- .../tests/test_stock_landed_costs_mrp.py | 28 +- addons/point_of_sale/tests/common.py | 13 +- .../point_of_sale/tests/test_anglo_saxon.py | 4 +- .../tests/test_point_of_sale_flow.py | 31 +- addons/sale_mrp/tests/test_sale_mrp_flow.py | 38 +- addons/sale_stock/data/sale_order_demo.xml | 26 +- .../tests/test_anglo_saxon_valuation.py | 16 +- addons/stock/__manifest__.py | 6 +- addons/stock/data/stock_demo.xml | 148 ++- addons/stock/data/stock_demo2.xml | 27 +- addons/stock/models/__init__.py | 1 - addons/stock/models/product.py | 2 +- addons/stock/models/res_company.py | 21 +- addons/stock/models/res_config_settings.py | 5 +- addons/stock/models/stock_inventory.py | 767 --------------- addons/stock/models/stock_location.py | 38 +- addons/stock/models/stock_move.py | 6 +- addons/stock/models/stock_move_line.py | 5 +- addons/stock/models/stock_quant.py | 259 +++-- addons/stock/models/stock_rule.py | 6 - addons/stock/populate/stock.py | 133 --- addons/stock/report/report_stockinventory.xml | 91 +- addons/stock/report/stock_report_views.xml | 6 +- addons/stock/report/stock_traceability.py | 8 +- addons/stock/security/ir.model.access.csv | 6 +- addons/stock/security/stock_security.xml | 12 - .../js/inventory_report_list_controller.js | 11 +- .../js/inventory_singleton_list_controller.js | 140 ++- .../js/inventory_singleton_list_renderer.js | 34 + .../src/js/inventory_singleton_list_view.js | 2 + .../inventory_validate_button_controller.js | 89 -- .../src/js/inventory_validate_button_view.js | 16 - .../stock/static/src/xml/inventory_report.xml | 24 +- addons/stock/tests/common.py | 2 - addons/stock/tests/common2.py | 2 +- addons/stock/tests/test_inventory.py | 918 +++++------------- addons/stock/tests/test_move2.py | 60 +- addons/stock/tests/test_multicompany.py | 61 +- .../stock/tests/test_quant_inventory_mode.py | 17 +- addons/stock/tests/test_report.py | 6 +- addons/stock/tests/test_stock_flow.py | 198 +--- addons/stock/tests/test_warehouse.py | 65 +- .../stock/views/res_config_settings_views.xml | 12 + addons/stock/views/stock_inventory_views.xml | 271 ------ addons/stock/views/stock_move_line_views.xml | 8 +- addons/stock/views/stock_move_views.xml | 7 +- addons/stock/views/stock_quant_views.xml | 58 +- addons/stock/wizard/__init__.py | 2 + .../stock/wizard/stock_change_product_qty.py | 2 +- .../stock/wizard/stock_inventory_conflict.py | 28 + .../stock/wizard/stock_inventory_conflict.xml | 42 + addons/stock/wizard/stock_request_count.py | 29 + addons/stock/wizard/stock_request_count.xml | 30 + .../stock/wizard/stock_track_confirmation.py | 20 +- .../wizard/stock_track_confirmation_views.xml | 9 +- addons/stock_account/__manifest__.py | 1 + addons/stock_account/models/__init__.py | 1 - .../stock_account/models/stock_inventory.py | 42 - addons/stock_account/models/stock_quant.py | 21 + .../views/stock_account_views.xml | 27 +- .../stock_account/views/stock_quant_views.xml | 4 +- addons/stock_account/wizard/__init__.py | 1 + .../wizard/stock_request_count.py | 16 + .../wizard/stock_request_count.xml | 13 + .../data/stock_picking_batch_demo.xml | 41 +- ...st_website_sale_stock_product_warehouse.py | 5 +- 73 files changed, 1411 insertions(+), 2876 deletions(-) delete mode 100644 addons/stock/models/stock_inventory.py create mode 100644 addons/stock/static/src/js/inventory_singleton_list_renderer.js delete mode 100644 addons/stock/static/src/js/inventory_validate_button_controller.js delete mode 100644 addons/stock/static/src/js/inventory_validate_button_view.js delete mode 100644 addons/stock/views/stock_inventory_views.xml create mode 100644 addons/stock/wizard/stock_inventory_conflict.py create mode 100644 addons/stock/wizard/stock_inventory_conflict.xml create mode 100644 addons/stock/wizard/stock_request_count.py create mode 100644 addons/stock/wizard/stock_request_count.xml delete mode 100644 addons/stock_account/models/stock_inventory.py create mode 100644 addons/stock_account/wizard/stock_request_count.py create mode 100644 addons/stock_account/wizard/stock_request_count.xml diff --git a/addons/mrp/data/mrp_demo.xml b/addons/mrp/data/mrp_demo.xml index f96e8e7987f..8ebceb107d4 100644 --- a/addons/mrp/data/mrp_demo.xml +++ b/addons/mrp/data/mrp_demo.xml @@ -509,67 +509,55 @@ - - Inventory: Drawer - - - + - - - 50 - + 50.0 + - + - - - 40 - + 40.0 + - + - - - 50 - + 50.0 + - + - - - 50 - + 50.0 + - - + - - - 50 + 50.0 - + - - - 20 + 20.0 - + - - - 30 + 30.0 - - + + diff --git a/addons/mrp/tests/test_backorder.py b/addons/mrp/tests/test_backorder.py index 7d4a838cfbf..1d1d5f1040f 100644 --- a/addons/mrp/tests/test_backorder.py +++ b/addons/mrp/tests/test_backorder.py @@ -55,7 +55,7 @@ class TestMrpProductionBackorder(TestMrpCommon): 'product_id': product.id, 'inventory_quantity': 100, 'location_id': production.location_src_id.id, - }) + })._apply_inventory() production.action_assign() self.assertEqual(production.state, 'confirmed') self.assertEqual(production.reserve_visible, False) diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 7d25c768d7b..834d46db8a9 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -43,22 +43,16 @@ class TestMrpOrder(TestMrpCommon): consume strictly what's needed. """ self.product_1.type = 'product' self.product_2.type = 'product' - inventory = self.env['stock.inventory'].create({ - 'name': 'Initial inventory', - 'line_ids': [(0, 0, { - 'product_id': self.product_1.id, - 'product_uom_id': self.product_1.uom_id.id, - 'product_qty': 500, - 'location_id': self.warehouse_1.lot_stock_id.id - }), (0, 0, { - 'product_id': self.product_2.id, - 'product_uom_id': self.product_2.uom_id.id, - 'product_qty': 500, - 'location_id': self.warehouse_1.lot_stock_id.id - })] - }) - inventory.action_start() - inventory.action_validate() + self.env['stock.quant'].create({ + 'location_id': self.warehouse_1.lot_stock_id.id, + 'product_id': self.product_1.id, + 'inventory_quantity': 500 + }).action_apply_inventory() + self.env['stock.quant'].create({ + 'location_id': self.warehouse_1.lot_stock_id.id, + 'product_id': self.product_2.id, + 'inventory_quantity': 500 + }).action_apply_inventory() test_date_planned = Dt.now() - timedelta(days=1) test_quantity = 2.0 @@ -107,7 +101,7 @@ class TestMrpOrder(TestMrpCommon): backorder.save().action_close_mo() self.assertEqual(man_order.state, 'done', "Production order should be done.") - def test_production_avialability(self): + def test_production_availability(self): """ Checks the availability of a production order through mutliple calls to `action_assign`. """ self.bom_3.bom_line_ids.filtered(lambda x: x.product_id == self.product_5).unlink() @@ -132,7 +126,7 @@ class TestMrpOrder(TestMrpCommon): 'product_id': self.product_2.id, 'inventory_quantity': 2.0, 'location_id': self.stock_location_14.id - }) + }).action_apply_inventory() production_2.action_assign() # check sub product availability state is partially available @@ -143,7 +137,7 @@ class TestMrpOrder(TestMrpCommon): 'product_id': self.product_2.id, 'inventory_quantity': 5.0, 'location_id': self.stock_location_14.id - }) + }).action_apply_inventory() production_2.action_assign() # check sub product availability state is assigned diff --git a/addons/mrp/tests/test_procurement.py b/addons/mrp/tests/test_procurement.py index 0932da42624..db18c22097b 100644 --- a/addons/mrp/tests/test_procurement.py +++ b/addons/mrp/tests/test_procurement.py @@ -60,7 +60,7 @@ class TestProcurement(TestMrpCommon): 'product_id': self.product_2.id, 'inventory_quantity': 48, 'location_id': self.warehouse.lot_stock_id.id, - }) + }).action_apply_inventory() produce_product_4.action_assign() self.assertEqual(produce_product_4.product_qty, 8, "Wrong quantity of finish product.") self.assertEqual(produce_product_4.product_uom_id, self.uom_dozen, "Wrong quantity of finish product.") @@ -84,7 +84,7 @@ class TestProcurement(TestMrpCommon): 'product_id': self.product_2.id, 'inventory_quantity': 12, 'location_id': self.warehouse.lot_stock_id.id, - }) + }).action_apply_inventory() production_product_6.action_assign() # ------------------------------------ diff --git a/addons/mrp/tests/test_stock.py b/addons/mrp/tests/test_stock.py index 64587e1cde9..4da19ce09ab 100644 --- a/addons/mrp/tests/test_stock.py +++ b/addons/mrp/tests/test_stock.py @@ -26,17 +26,11 @@ class TestWarehouse(common.TestMrpCommon): 'name': 'Assembly Line 1', 'resource_calendar_id': self.env.ref('resource.resource_calendar_std').id, }) - inventory = self.env['stock.inventory'].create({ - 'name': 'Initial inventory', - 'line_ids': [(0, 0, { - 'product_id': self.graphics_card.id, - 'product_uom_id': self.graphics_card.uom_id.id, - 'product_qty': 16.0, - 'location_id': self.stock_location_14.id, - })] - }) - inventory.action_start() - inventory.action_validate() + self.env['stock.quant'].create({ + 'location_id': self.stock_location_14.id, + 'product_id': self.graphics_card.id, + 'inventory_quantity': 16.0 + }).action_apply_inventory() self.bom_laptop = self.env['mrp.bom'].create({ 'product_tmpl_id': self.laptop.product_tmpl_id.id, @@ -112,22 +106,21 @@ class TestWarehouse(common.TestMrpCommon): 'company_id': self.env.company.id, }) - stock_inv_product_4 = self.env['stock.inventory'].create({ - 'name': 'Stock Inventory for Stick', - 'product_ids': [(4, self.product_4.id)], - 'line_ids': [ - (0, 0, {'product_id': self.product_4.id, 'product_uom_id': self.product_4.uom_id.id, 'product_qty': 8, 'prod_lot_id': lot_product_4.id, 'location_id': self.stock_location_14.id}), - ]}) + # Inventory for Stick + self.env['stock.quant'].create({ + 'location_id': self.stock_location_14.id, + 'product_id': self.product_4.id, + 'inventory_quantity': 8, + 'lot_id': lot_product_4.id + }).action_apply_inventory() - stock_inv_product_2 = self.env['stock.inventory'].create({ - 'name': 'Stock Inventory for Stone Tools', - 'product_ids': [(4, self.product_2.id)], - 'line_ids': [ - (0, 0, {'product_id': self.product_2.id, 'product_uom_id': self.product_2.uom_id.id, 'product_qty': 12, 'prod_lot_id': lot_product_2.id, 'location_id': self.stock_location_14.id}) - ]}) - (stock_inv_product_4 | stock_inv_product_2)._action_start() - stock_inv_product_2.action_validate() - stock_inv_product_4.action_validate() + # Inventory for Stone Tools + self.env['stock.quant'].create({ + 'location_id': self.stock_location_14.id, + 'product_id': self.product_2.id, + 'inventory_quantity': 12, + 'lot_id': lot_product_2.id + }).action_apply_inventory() #Create Manufacturing order. production_form = Form(self.env['mrp.production']) diff --git a/addons/mrp/tests/test_traceability.py b/addons/mrp/tests/test_traceability.py index 502cac0519d..d878d1110df 100644 --- a/addons/mrp/tests/test_traceability.py +++ b/addons/mrp/tests/test_traceability.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tests import Form, tagged +from odoo.tests import Form from odoo.addons.mrp.tests.common import TestMrpCommon -import uuid + class TestTraceability(TestMrpCommon): TRACKING_TYPES = ['none', 'serial', 'lot'] @@ -24,21 +24,39 @@ class TestTraceability(TestMrpCommon): consumed_lot = self._create_product('lot') consumed_serial = self._create_product('serial') stock_id = self.env.ref('stock.stock_location_stock').id - inventory_adjustment = self.env['stock.inventory'].create({ - 'name': 'Initial Inventory', - 'location_ids': [(4, stock_id)], + Lot = self.env['stock.production.lot'] + # create inventory + quants = self.env['stock.quant'].create({ + 'location_id': stock_id, + 'product_id': consumed_no_track.id, + 'inventory_quantity': 3 }) - inventory_adjustment.action_start() - inventory_adjustment.write({ - 'line_ids': [ - (0,0, {'product_id': consumed_no_track.id, 'product_qty': 3, 'location_id': stock_id}), - (0,0, {'product_id': consumed_lot.id, 'product_qty': 3, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'L1', 'product_id': consumed_lot.id, 'company_id': self.env.company.id}).id, 'location_id': stock_id}), - (0,0, {'product_id': consumed_serial.id, 'product_qty': 1, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'S1', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id, 'location_id': stock_id}), - (0,0, {'product_id': consumed_serial.id, 'product_qty': 1, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'S2', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id, 'location_id': stock_id}), - (0,0, {'product_id': consumed_serial.id, 'product_qty': 1, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'S3', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id, 'location_id': stock_id}), - ] + quants |= self.env['stock.quant'].create({ + 'location_id': stock_id, + 'product_id': consumed_lot.id, + 'inventory_quantity': 3, + 'lot_id': Lot.create({'name': 'L1', 'product_id': consumed_lot.id, 'company_id': self.env.company.id}).id }) - inventory_adjustment.action_validate() + quants |= self.env['stock.quant'].create({ + 'location_id': stock_id, + 'product_id': consumed_serial.id, + 'inventory_quantity': 1, + 'lot_id': Lot.create({'name': 'S1', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id + }) + quants |= self.env['stock.quant'].create({ + 'location_id': stock_id, + 'product_id': consumed_serial.id, + 'inventory_quantity': 1, + 'lot_id': Lot.create({'name': 'S2', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id + }) + quants |= self.env['stock.quant'].create({ + 'location_id': stock_id, + 'product_id': consumed_serial.id, + 'inventory_quantity': 1, + 'lot_id': Lot.create({'name': 'S3', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id + }) + quants.action_apply_inventory() + for finished_product in [finished_no_track, finished_lot, finished_serial]: bom = self.env['mrp.bom'].create({ 'product_id': finished_product.id, @@ -78,9 +96,7 @@ class TestTraceability(TestMrpCommon): ml.qty_done = 1 details_operation_form.save() - mo.button_mark_done() - self.assertEqual(mo.state, 'done', "Production order should be in done state.") # Check results of traceability @@ -89,9 +105,7 @@ class TestTraceability(TestMrpCommon): 'model': 'mrp.production', }) lines = self.env['stock.traceability.report'].with_context(context).get_lines() - self.assertEqual(len(lines), 1, "Should always return 1 line : the final product") - final_product = lines[0] self.assertEqual(final_product['unfoldable'], True, "Final product should always be unfoldable") @@ -101,7 +115,6 @@ class TestTraceability(TestMrpCommon): 'model_id': final_product['model_id'], 'model_name': final_product['model'], }) - self.assertEqual(len(lines), 3, "There should be 3 lines. 1 for untracked, 1 for lot, and 1 for serial") for line in lines: diff --git a/addons/mrp_account/tests/test_mrp_account.py b/addons/mrp_account/tests/test_mrp_account.py index f827a0eb023..04d4a15c295 100644 --- a/addons/mrp_account/tests/test_mrp_account.py +++ b/addons/mrp_account/tests/test_mrp_account.py @@ -143,32 +143,29 @@ class TestMrpAccount(TestMrpCommon): self.product_screw.standard_price = 0.1 self.product_table_leg.tracking = 'none' self.product_table_sheet.tracking = 'none' - inventory = self.env['stock.inventory'].create({ - 'name': 'Inventory Product Table', - 'line_ids': [(0, 0, { - 'product_id': self.product_table_sheet.id, # tracking serial - 'product_uom_id': self.product_table_sheet.uom_id.id, - 'product_qty': 20, - 'location_id': self.source_location_id - }), (0, 0, { - 'product_id': self.product_table_leg.id, # tracking lot - 'product_uom_id': self.product_table_leg.uom_id.id, - 'product_qty': 20, - 'location_id': self.source_location_id - }), (0, 0, { - 'product_id': self.product_bolt.id, - 'product_uom_id': self.product_bolt.uom_id.id, - 'product_qty': 20, - 'location_id': self.source_location_id - }), (0, 0, { - 'product_id': self.product_screw.id, - 'product_uom_id': self.product_screw.uom_id.id, - 'product_qty': 200000, - 'location_id': self.source_location_id - }), - ] + # Inventory Product Table + quants = self.env['stock.quant'].with_context(inventory_mode=True).create({ + 'product_id': self.product_table_sheet.id, # tracking serial + 'inventory_quantity': 20, + 'location_id': self.source_location_id, }) - inventory.action_validate + quants |= self.env['stock.quant'].with_context(inventory_mode=True).create({ + 'product_id': self.product_table_leg.id, # tracking lot + 'inventory_quantity': 20, + 'location_id': self.source_location_id, + }) + quants |= self.env['stock.quant'].with_context(inventory_mode=True).create({ + 'product_id': self.product_bolt.id, + 'inventory_quantity': 20, + 'location_id': self.source_location_id, + }) + quants |= self.env['stock.quant'].create({ + 'product_id': self.product_screw.id, + 'inventory_quantity': 200000, + 'location_id': self.source_location_id, + }) + quants.action_apply_inventory() + bom = self.mrp_bom_desk.copy() bom.operation_ids = False production_table_form = Form(self.env['mrp.production']) diff --git a/addons/mrp_landed_costs/tests/test_stock_landed_costs_mrp.py b/addons/mrp_landed_costs/tests/test_stock_landed_costs_mrp.py index 686a35abd8d..74c13d229c0 100644 --- a/addons/mrp_landed_costs/tests/test_stock_landed_costs_mrp.py +++ b/addons/mrp_landed_costs/tests/test_stock_landed_costs_mrp.py @@ -80,22 +80,18 @@ class TestStockLandedCostsMrp(ValuationReconciliationTestCommon): }) def test_landed_cost_on_mrp(self): - inventory = self.env['stock.inventory'].create({ - 'name': 'Initial inventory', - 'line_ids': [(0, 0, { - 'product_id': self.product_component1.id, - 'product_uom_id': self.product_component1.uom_id.id, - 'product_qty': 500, - 'location_id': self.warehouse_1.lot_stock_id.id - }), (0, 0, { - 'product_id': self.product_component2.id, - 'product_uom_id': self.product_component2.uom_id.id, - 'product_qty': 500, - 'location_id': self.warehouse_1.lot_stock_id.id - })] + # Initial inventory + quants = self.env['stock.quant'].with_context(inventory_mode=True).create({ + 'product_id': self.product_component1.id, + 'inventory_quantity': 500, + 'location_id': self.warehouse_1.lot_stock_id.id, }) - inventory.action_start() - inventory.action_validate() + quants |= self.env['stock.quant'].with_context(inventory_mode=True).create({ + 'product_id': self.product_component2.id, + 'inventory_quantity': 500, + 'location_id': self.warehouse_1.lot_stock_id.id, + }) + quants.action_apply_inventory() man_order_form = Form(self.env['mrp.production'].with_user(self.allow_user)) man_order_form.product_id = self.product_refrigerator @@ -120,8 +116,6 @@ class TestStockLandedCostsMrp(ValuationReconciliationTestCommon): mo_form = Form(man_order.with_user(self.allow_user)) mo_form.qty_producing = 2 man_order = mo_form.save() - - man_order.button_mark_done() landed_cost = Form(self.env['stock.landed.cost'].with_user(self.allow_user)).save() diff --git a/addons/point_of_sale/tests/common.py b/addons/point_of_sale/tests/common.py index c2e11e55c4b..27dd35a462b 100644 --- a/addons/point_of_sale/tests/common.py +++ b/addons/point_of_sale/tests/common.py @@ -474,19 +474,12 @@ class TestPoSCommon(ValuationReconciliationTestCommon): def adjust_inventory(cls, products, quantities): """ Adjust inventory of the given products """ - inventory = cls.env['stock.inventory'].create({ - 'name': 'Inventory adjustment' - }) for product, qty in zip(products, quantities): - cls.env['stock.inventory.line'].create({ + cls.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': product.id, - 'product_uom_id': cls.env.ref('uom.product_uom_unit').id, - 'inventory_id': inventory.id, - 'product_qty': qty, + 'inventory_quantity': qty, 'location_id': cls.stock_location_components.id, - }) - inventory._action_start() - inventory.action_validate() + }).action_apply_inventory() def open_new_session(self): """ Used to open new pos session in each configuration. diff --git a/addons/point_of_sale/tests/test_anglo_saxon.py b/addons/point_of_sale/tests/test_anglo_saxon.py index aed16559189..468a62d77ed 100644 --- a/addons/point_of_sale/tests/test_anglo_saxon.py +++ b/addons/point_of_sale/tests/test_anglo_saxon.py @@ -136,13 +136,13 @@ class TestAngloSaxonFlow(TestAngloSaxonCommon): 'product_id': self.product.id, 'inventory_quantity': 5.0, 'location_id': self.warehouse.lot_stock_id.id, - }) + }).action_apply_inventory() self.product.standard_price = 1.0 self.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': self.product.id, 'inventory_quantity': 10.0, 'location_id': self.warehouse.lot_stock_id.id, - }) + }).action_apply_inventory() self.assertEqual(self.product.value_svl, 30, "Value should be (5*5 + 5*1) = 30") self.assertEqual(self.product.quantity_svl, 10) diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py index c99231c780a..f8cc739505c 100644 --- a/addons/point_of_sale/tests/test_point_of_sale_flow.py +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -99,13 +99,6 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'categ_id': self.env.ref('product.product_category_all').id, }) - inventory = self.env['stock.inventory'].create({ - 'name': 'add product2', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product2.id)], - }) - inventory.action_start() - lot1 = self.env['stock.production.lot'].create({ 'name': '1001', 'product_id': self.product2.id, @@ -117,24 +110,18 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'company_id': self.env.company.id, }) - self.env['stock.inventory.line'].create([ - { - 'inventory_id': inventory.id, - 'location_id': self.stock_location.id, + self.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': self.product2.id, - 'prod_lot_id': lot1.id, - 'product_qty': 1 - }, - { - 'inventory_id': inventory.id, + 'inventory_quantity': 1, 'location_id': self.stock_location.id, + 'lot_id': lot1.id + }).action_apply_inventory() + self.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': self.product2.id, - 'prod_lot_id': lot2.id, - 'product_qty': 1 - }, - ]) - - inventory.action_validate() + 'inventory_quantity': 1, + 'location_id': self.stock_location.id, + 'lot_id': lot2.id + }).action_apply_inventory() # create pos order with the two SN created before diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py index 20542210307..305692f05ac 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_flow.py +++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py @@ -20,8 +20,7 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon): cls.StockMove = cls.env['stock.move'] cls.UoM = cls.env['uom.uom'] cls.MrpProduction = cls.env['mrp.production'] - cls.Inventory = cls.env['stock.inventory'] - cls.InventoryLine = cls.env['stock.inventory.line'] + cls.Quant = cls.env['stock.quant'] cls.ProductCategory = cls.env['product.category'] cls.categ_unit = cls.env.ref('uom.product_uom_categ_unit') @@ -405,19 +404,11 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon): # Need 20 kg product c to produce 20 unit product D. # -------------------------------------------------- - inventory = self.Inventory.create({ - 'name': 'Inventory Product KG', - 'product_ids': [(4, product_c.id)]}) - - inventory.action_start() - self.assertFalse(inventory.line_ids, "Inventory line should not created.") - self.InventoryLine.create({ - 'inventory_id': inventory.id, - 'product_id': product_c.id, - 'product_uom_id': self.uom_kg.id, - 'product_qty': 20, - 'location_id': self.company_data['default_warehouse'].lot_stock_id.id}) - inventory.action_validate() + self.Quant.with_context(inventory_mode=True).create({ + 'product_id': product_c.id, # uom = uom_kg + 'inventory_quantity': 20, + 'location_id': self.company_data['default_warehouse'].lot_stock_id.id, + }).action_apply_inventory() # -------------------------------------------------- # Assign product c to manufacturing order of product D. @@ -453,19 +444,12 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon): # ------------------------------ # Need product C ( 20 kg + 6 kg + 1502.5 gm = 27.5025 kg) # ------------------------------------------------------- - inventory = self.Inventory.create({ - 'name': 'Inventory Product C KG', - 'product_ids': [(4, product_c.id)]}) - inventory.action_start() - self.assertFalse(inventory.line_ids, "Inventory line should not created.") - self.InventoryLine.create({ - 'inventory_id': inventory.id, - 'product_id': product_c.id, - 'product_uom_id': self.uom_kg.id, - 'product_qty': 27.5025, - 'location_id': self.company_data['default_warehouse'].lot_stock_id.id}) - inventory.action_validate() + self.Quant.with_context(inventory_mode=True).create({ + 'product_id': product_c.id, # uom = uom_kg + 'inventory_quantity': 27.51, # round up due to kg.rounding = 0.01 + 'location_id': self.company_data['default_warehouse'].lot_stock_id.id, + }).action_apply_inventory() # Assign product to manufacturing order of product A. # --------------------------------------------------- diff --git a/addons/sale_stock/data/sale_order_demo.xml b/addons/sale_stock/data/sale_order_demo.xml index 0e6f3ef66eb..fbebd9a9bff 100644 --- a/addons/sale_stock/data/sale_order_demo.xml +++ b/addons/sale_stock/data/sale_order_demo.xml @@ -134,30 +134,22 @@ - - Inventory for new Customizable Desks - - + + - - - 65.0 + 65.0 - + - - - 70.0 + 70.0 - - - - - - + + diff --git a/addons/sale_stock/tests/test_anglo_saxon_valuation.py b/addons/sale_stock/tests/test_anglo_saxon_valuation.py index e79c96ff87b..2d75d82b27e 100644 --- a/addons/sale_stock/tests/test_anglo_saxon_valuation.py +++ b/addons/sale_stock/tests/test_anglo_saxon_valuation.py @@ -22,19 +22,11 @@ class TestAngloSaxonValuation(ValuationReconciliationTestCommon): }) def _inv_adj_two_units(self): - inventory = self.env['stock.inventory'].create({ - 'name': 'test', - 'location_ids': [(4, self.company_data['default_warehouse'].lot_stock_id.id)], - 'product_ids': [(4, self.product.id)], - }) - inventory.action_start() - self.env['stock.inventory.line'].create({ - 'inventory_id': inventory.id, + self.env['stock.quant'].with_context(inventory_mode=True).create({ + 'product_id': self.product.id, # tracking serial + 'inventory_quantity': 2, 'location_id': self.company_data['default_warehouse'].lot_stock_id.id, - 'product_id': self.product.id, - 'product_qty': 2, - }) - inventory.action_validate() + }).action_apply_inventory() def _so_and_confirm_two_units(self): sale_order = self.env['sale.order'].create({ diff --git a/addons/stock/__manifest__.py b/addons/stock/__manifest__.py index 8509c651d04..9d9b848bf01 100644 --- a/addons/stock/__manifest__.py +++ b/addons/stock/__manifest__.py @@ -50,8 +50,10 @@ 'wizard/stock_picking_return_views.xml', 'wizard/stock_scheduler_compute_views.xml', 'wizard/stock_immediate_transfer_views.xml', + 'wizard/stock_inventory_conflict.xml', 'wizard/stock_backorder_confirmation_views.xml', 'wizard/stock_quantity_history.xml', + 'wizard/stock_request_count.xml', 'wizard/stock_rules_report_views.xml', 'wizard/stock_warn_insufficient_qty_views.xml', 'wizard/product_replenish_views.xml', @@ -65,7 +67,6 @@ 'views/stock_production_lot_views.xml', 'views/stock_picking_views.xml', 'views/stock_scrap_views.xml', - 'views/stock_inventory_views.xml', 'views/stock_quant_views.xml', 'views/stock_location_views.xml', 'views/stock_warehouse_views.xml', @@ -98,10 +99,9 @@ 'web.assets_backend': [ 'stock/static/src/js/inventory_report_list_controller.js', 'stock/static/src/js/inventory_report_list_view.js', + 'stock/static/src/js/inventory_singleton_list_renderer.js', 'stock/static/src/js/inventory_singleton_list_controller.js', 'stock/static/src/js/inventory_singleton_list_view.js', - 'stock/static/src/js/inventory_validate_button_controller.js', - 'stock/static/src/js/inventory_validate_button_view.js', 'stock/static/src/js/report_stock_forecasted.js', 'stock/static/src/js/stock_orderpoint_list_controller.js', 'stock/static/src/js/stock_orderpoint_list_model.js', diff --git a/addons/stock/data/stock_demo.xml b/addons/stock/data/stock_demo.xml index d63bad9c490..2454c3397b3 100644 --- a/addons/stock/data/stock_demo.xml +++ b/addons/stock/data/stock_demo.xml @@ -12,136 +12,109 @@ - + - - Starting Inventory - - - - - - + - - - 16.0 + 16.0 - + - - - 18.0 + 18.0 - + - - - 500.0 + 500.0 - + - - - 22.0 + 22.0 - + - - - 33.0 + 33.0 - + - - - 26.0 + 26.0 - + - - - 30.0 + 30.0 - + - - - 45.0 + 45.0 - + - - - 50.0 + 50.0 - + - - - 55.0 + 55.0 - + - - - 60.0 + 60.0 - + - - - 10.0 + 10.0 - + - - - 2.0 + 2.0 - + - - - 80.0 - + 80.0 + - + - - - 60.0 + 60.0 - + - - - 16.0 + 16.0 - - + + - - - - @@ -201,16 +174,10 @@ - - Starting Inventory - - - + - - - 200.0 + 200.0 - - - - + + diff --git a/addons/stock/data/stock_demo2.xml b/addons/stock/data/stock_demo2.xml index dd318a2aef2..94644edd72f 100644 --- a/addons/stock/data/stock_demo2.xml +++ b/addons/stock/data/stock_demo2.xml @@ -45,11 +45,6 @@ - - Inventory for Cable Management Box - - - CM-BOX-00001 @@ -62,24 +57,26 @@ - + - - - 50.0 + 50.0 - + - + - - - 40.0 + 40.0 - + + + + + diff --git a/addons/stock/models/__init__.py b/addons/stock/models/__init__.py index 214903e9f7d..03ac4fdf92a 100644 --- a/addons/stock/models/__init__.py +++ b/addons/stock/models/__init__.py @@ -6,7 +6,6 @@ from . import product_strategy from . import res_company from . import res_partner from . import res_config_settings -from . import stock_inventory from . import stock_location from . import stock_move from . import stock_move_line diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 0aa7c82c97b..5b3a424bf5d 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -849,7 +849,7 @@ class ProductTemplate(models.Model): 'stock.group_stock_multi_locations', 'stock.group_production_lot', 'stock.group_tracking_owner', - 'product.group_stock_packaging' + 'product.group_tracking_lot' ] if (self.env.user.user_has_groups(','.join(advanced_option_groups))): return self.action_open_quants() diff --git a/addons/stock/models/res_company.py b/addons/stock/models/res_company.py index 93a960edbf3..8ece71f4056 100644 --- a/addons/stock/models/res_company.py +++ b/addons/stock/models/res_company.py @@ -22,6 +22,26 @@ class Company(models.Model): domain="[('model', '=', 'stock.picking')]", default=_default_confirmation_mail_template, help="Email sent to the customer once the order is done.") + annual_inventory_month = fields.Selection([ + ('1', 'January'), + ('2', 'February'), + ('3', 'March'), + ('4', 'April'), + ('5', 'May'), + ('6', 'June'), + ('7', 'July'), + ('8', 'August'), + ('9', 'September'), + ('10', 'October'), + ('11', 'November'), + ('12', 'December'), + ], string='Annual Inventory Month', + default='12', + help="Annual inventory month for products not in a location with a cyclic inventory date. Set to no month if no automatic annual inventory.") + annual_inventory_day = fields.Integer( + string='Day of the month', default=31, + help="""Day of the month when the annual inventory should occur. If zero or negative, then the first day of the month will be selected instead. + If greater than the last day of a month, then the last day of the month will be selected instead.""") def _create_transit_location(self): '''Create a transit location with company_id being the given company_id. This is needed @@ -77,7 +97,6 @@ class Company(models.Model): company.id, ) - def _create_scrap_location(self): parent_location = self.env.ref('stock.stock_location_locations_virtual', raise_if_not_found=False) for company in self: diff --git a/addons/stock/models/res_config_settings.py b/addons/stock/models/res_config_settings.py index 014fe3875eb..11971f4f0b2 100644 --- a/addons/stock/models/res_config_settings.py +++ b/addons/stock/models/res_config_settings.py @@ -39,7 +39,10 @@ class ResConfigSettings(models.TransientModel): module_quality_control_worksheet = fields.Boolean("Quality Worksheet") group_stock_multi_locations = fields.Boolean('Storage Locations', implied_group='stock.group_stock_multi_locations', help="Store products in specific locations of your warehouse (e.g. bins, racks) and to track inventory accordingly.") - group_stock_storage_categories = fields.Boolean('Storage Categories', implied_group='stock.group_stock_storage_categories') + group_stock_storage_categories = fields.Boolean( + 'Storage Categories', implied_group='stock.group_stock_storage_categories') + annual_inventory_month = fields.Selection(related='company_id.annual_inventory_month', readonly=False) + annual_inventory_day = fields.Integer(related='company_id.annual_inventory_day', readonly=False) @api.onchange('group_stock_multi_locations') def _onchange_group_stock_multi_locations(self): diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py deleted file mode 100644 index 0081c357c20..00000000000 --- a/addons/stock/models/stock_inventory.py +++ /dev/null @@ -1,767 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from collections import defaultdict - -from odoo import _, api, fields, models -from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG -from odoo.exceptions import UserError, ValidationError -from odoo.osv import expression -from odoo.tools import float_compare, float_is_zero - - -class Inventory(models.Model): - _name = "stock.inventory" - _description = "Inventory" - _order = "date desc, id desc" - _inherit = ['mail.thread', 'mail.activity.mixin'] - - name = fields.Char( - 'Inventory Reference', default="Inventory", - readonly=True, required=True, - states={'draft': [('readonly', False)]}) - date = fields.Datetime( - 'Inventory Date', - readonly=True, required=True, - default=fields.Datetime.now, - help="If the inventory adjustment is not validated, date at which the theoritical quantities have been checked.\n" - "If the inventory adjustment is validated, date at which the inventory adjustment has been validated.") - line_ids = fields.One2many( - 'stock.inventory.line', 'inventory_id', string='Inventories', - copy=False, readonly=False, - states={'done': [('readonly', True)]}) - move_ids = fields.One2many( - 'stock.move', 'inventory_id', string='Created Moves', - states={'done': [('readonly', True)]}) - state = fields.Selection(string='Status', selection=[ - ('draft', 'Draft'), - ('cancel', 'Cancelled'), - ('confirm', 'In Progress'), - ('done', 'Validated')], - copy=False, index=True, readonly=True, tracking=True, - default='draft') - company_id = fields.Many2one( - 'res.company', 'Company', - readonly=True, index=True, required=True, - states={'draft': [('readonly', False)]}, - default=lambda self: self.env.company) - location_ids = fields.Many2many( - 'stock.location', string='Locations', - readonly=True, check_company=True, - states={'draft': [('readonly', False)]}, - domain="[('company_id', '=', company_id), ('usage', 'in', ['internal', 'transit'])]") - product_ids = fields.Many2many( - 'product.product', string='Products', check_company=True, - domain="[('type', '=', 'product'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", readonly=True, - states={'draft': [('readonly', False)]}, - help="Specify Products to focus your inventory on particular Products.") - start_empty = fields.Boolean('Empty Inventory', - help="Allows to start with an empty inventory.") - prefill_counted_quantity = fields.Selection(string='Counted Quantities', - help="Allows to start with a pre-filled counted quantity for each lines or " - "with all counted quantities set to zero.", default='counted', - selection=[('counted', 'Default to stock on hand'), ('zero', 'Default to zero')]) - exhausted = fields.Boolean( - 'Include Exhausted Products', readonly=True, - states={'draft': [('readonly', False)]}, - help="Include also products with quantity of 0") - is_conflict_inventory = fields.Boolean(string="Is Auto-generated From Conflict", readonly=True, - help="Technical flag to indicate this inventory was auto-generated due to a conflicting inventory. This allows us to auto-add/remove products when inventory is still in draft.") - lot_ids = fields.Many2many('stock.production.lot', string='Duplicate Serial Numbers', readonly=True, - help="Technical field to support auto-generated conflicting inventory from a duplicated SN. This value is expected to never be viewed/edited except by conflicting inventory checks.") - - @api.onchange('company_id') - def _onchange_company_id(self): - # If the multilocation group is not active, default the location to the one of the main - # warehouse. - if not self.user_has_groups('stock.group_stock_multi_locations'): - warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.company_id.id)], limit=1) - if warehouse: - self.location_ids = warehouse.lot_stock_id - - def copy_data(self, default=None): - name = _("%s (copy)") % (self.name) - default = dict(default or {}, name=name) - return super(Inventory, self).copy_data(default) - - @api.ondelete(at_uninstall=False) - def _unlink_if_draft_or_cancel(self): - for inventory in self: - if inventory.state not in ('draft', 'cancel'): - raise UserError(_('You can only delete a draft inventory adjustment. If the inventory adjustment is not done, you can cancel it.')) - - def action_validate(self): - if not self.exists(): - return - self.ensure_one() - if not self.user_has_groups('stock.group_stock_manager'): - raise UserError(_("Only a stock manager can validate an inventory adjustment.")) - if self.state != 'confirm': - raise UserError(_( - "You can't validate the inventory '%s', maybe this inventory " - "has been already validated or isn't ready.", self.name)) - inventory_lines = self.line_ids.filtered(lambda l: l.product_id.tracking in ['lot', 'serial'] and not l.prod_lot_id and l.theoretical_qty != l.product_qty) - lines = self.line_ids.filtered(lambda l: float_compare(l.product_qty, 1, precision_rounding=l.product_uom_id.rounding) > 0 and l.product_id.tracking == 'serial' and l.prod_lot_id) - if inventory_lines and not lines: - wiz_lines = [(0, 0, {'product_id': product.id, 'tracking': product.tracking}) for product in inventory_lines.mapped('product_id')] - wiz = self.env['stock.track.confirmation'].create({'inventory_id': self.id, 'tracking_line_ids': wiz_lines}) - return { - 'name': _('Tracked Products in Inventory Adjustment'), - 'type': 'ir.actions.act_window', - 'view_mode': 'form', - 'views': [(False, 'form')], - 'res_model': 'stock.track.confirmation', - 'target': 'new', - 'res_id': wiz.id, - } - self._action_done() - self.line_ids._check_company() - self._check_company() - return True - - def _action_done(self): - negative = next((line for line in self.mapped('line_ids') if line.product_qty < 0 and line.product_qty != line.theoretical_qty), False) - if negative: - raise UserError(_( - 'You cannot set a negative product quantity in an inventory line:\n\t%s - qty: %s', - negative.product_id.display_name, - negative.product_qty - )) - self.action_check() - self.write({'state': 'done', 'date': fields.Datetime.now()}) - self.post_inventory() - if self.user_has_groups('stock.group_stock_multi_locations') and not self.product_ids and self.location_ids: - locations = self.env['stock.location'].with_context(active_test=False).search([('id', 'child_of', self.location_ids.ids), ('usage', 'in', ['internal', 'transit'])]) - locations.last_inventory_date = fields.Datetime.now() - return True - - def post_inventory(self): - # The inventory is posted as a single step which means quants cannot be moved from an internal location to another using an inventory - # as they will be moved to inventory loss, and other quants will be created to the encoded quant location. This is a normal behavior - # as quants cannot be reuse from inventory location (users can still manually move the products before/after the inventory if they want). - self.mapped('move_ids').filtered(lambda move: move.state != 'done')._action_done() - return True - - def action_check(self): - """ Checks the inventory and computes the stock move to do """ - # tde todo: clean after _generate_moves - for inventory in self.filtered(lambda x: x.state not in ('done','cancel')): - # first remove the existing stock moves linked to this inventory - inventory.with_context(prefetch_fields=False).mapped('move_ids').unlink() - inventory.line_ids._generate_moves() - - def action_cancel_draft(self): - self.mapped('move_ids')._action_cancel() - self.line_ids.unlink() - self.write({'state': 'draft'}) - - def action_start(self): - self.ensure_one() - self._action_start() - self._check_company() - return self.action_open_inventory_lines() - - def _action_start(self): - """ Confirms the Inventory Adjustment and generates its inventory lines - if its state is draft and don't have already inventory lines (can happen - with demo data or tests). - """ - for inventory in self: - if inventory.state != 'draft': - continue - vals = { - 'state': 'confirm', - 'date': fields.Datetime.now() - } - if not inventory.line_ids and not inventory.start_empty: - self.env['stock.inventory.line'].create(inventory._get_inventory_lines_values()) - inventory.write(vals) - - def action_open_inventory_lines(self): - self.ensure_one() - action = { - 'type': 'ir.actions.act_window', - 'view_mode': 'tree', - 'name': _('Inventory Lines'), - 'res_model': 'stock.inventory.line', - } - context = { - 'default_is_editable': True, - 'default_inventory_id': self.id, - 'default_company_id': self.company_id.id, - } - # Define domains and context - domain = [ - ('inventory_id', '=', self.id), - ('location_id.usage', 'in', ['internal', 'transit']) - ] - if self.location_ids: - context['default_location_id'] = self.location_ids[0].id - if len(self.location_ids) == 1: - if not self.location_ids[0].child_ids: - context['readonly_location_id'] = True - - if self.product_ids: - # no_create on product_id field - action['view_id'] = self.env.ref('stock.stock_inventory_line_tree_no_product_create').id - if len(self.product_ids) == 1: - context['default_product_id'] = self.product_ids[0].id - else: - # no product_ids => we're allowed to create new products in tree - action['view_id'] = self.env.ref('stock.stock_inventory_line_tree').id - - action['context'] = context - action['domain'] = domain - return action - - def action_view_related_move_lines(self): - self.ensure_one() - domain = [('move_id', 'in', self.move_ids.ids)] - action = { - 'name': _('Product Moves'), - 'type': 'ir.actions.act_window', - 'res_model': 'stock.move.line', - 'view_type': 'list', - 'view_mode': 'list,form', - 'domain': domain, - } - return action - - def action_print(self): - return self.env.ref('stock.action_report_inventory').report_action(self) - - def _get_quantities(self): - """Return quantities group by product_id, location_id, lot_id, package_id and owner_id - - :return: a dict with keys as tuple of group by and quantity as value - :rtype: dict - """ - self.ensure_one() - if self.location_ids: - domain_loc = [('id', 'child_of', self.location_ids.ids)] - else: - domain_loc = [('company_id', '=', self.company_id.id), ('usage', 'in', ['internal', 'transit'])] - locations_ids = [l['id'] for l in self.env['stock.location'].search_read(domain_loc, ['id'])] - - domain = [('company_id', '=', self.company_id.id), - ('location_id', 'in', locations_ids)] - if self.prefill_counted_quantity == 'zero': - domain.append(('product_id.active', '=', True)) - if self.lot_ids: - domain = expression.AND([domain, [('lot_id', 'in', self.lot_ids.ids)]]) - if self.is_conflict_inventory and not self.lot_ids: - domain = expression.AND([domain, [('quantity', '<', 0)]]) - else: - domain.append(('quantity', '!=', 0)) - if self.product_ids: - domain = expression.AND([domain, [('product_id', 'in', self.product_ids.ids)]]) - - fields = ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id', 'quantity:sum'] - group_by = ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id'] - - quants = self.env['stock.quant'].read_group(domain, fields, group_by, lazy=False) - return {( - quant['product_id'] and quant['product_id'][0] or False, - quant['location_id'] and quant['location_id'][0] or False, - quant['lot_id'] and quant['lot_id'][0] or False, - quant['package_id'] and quant['package_id'][0] or False, - quant['owner_id'] and quant['owner_id'][0] or False): - quant['quantity'] for quant in quants - } - - def _get_exhausted_inventory_lines_vals(self, non_exhausted_set): - """Return the values of the inventory lines to create if the user - wants to include exhausted products. Exhausted products are products - without quantities or quantity equal to 0. - - :param non_exhausted_set: set of tuple (product_id, location_id) of non exhausted product-location - :return: a list containing the `stock.inventory.line` values to create - :rtype: list - """ - self.ensure_one() - if self.product_ids: - product_ids = self.product_ids.ids - else: - product_ids = self.env['product.product'].search_read([ - '|', ('company_id', '=', self.company_id.id), ('company_id', '=', False), - ('type', '=', 'product'), - ('active', '=', True)], ['id']) - product_ids = [p['id'] for p in product_ids] - - if self.location_ids: - location_ids = self.location_ids.ids - else: - location_ids = self.env['stock.warehouse'].search([('company_id', '=', self.company_id.id)]).lot_stock_id.ids - - vals = [] - for product_id in product_ids: - for location_id in location_ids: - if ((product_id, location_id) not in non_exhausted_set): - vals.append({ - 'inventory_id': self.id, - 'product_id': product_id, - 'location_id': location_id, - 'theoretical_qty': 0 - }) - return vals - - def _get_inventory_lines_values(self): - """Return the values of the inventory lines to create for this inventory. - - :return: a list containing the `stock.inventory.line` values to create - :rtype: list - """ - self.ensure_one() - quants_groups = self._get_quantities() - vals = [] - for (product_id, location_id, lot_id, package_id, owner_id), quantity in quants_groups.items(): - line_values = { - 'inventory_id': self.id, - 'product_qty': 0 if self.prefill_counted_quantity == "zero" else quantity, - 'theoretical_qty': quantity, - 'prod_lot_id': lot_id, - 'partner_id': owner_id, - 'product_id': product_id, - 'location_id': location_id, - 'package_id': package_id - } - line_values['product_uom_id'] = self.env['product.product'].browse(product_id).uom_id.id - vals.append(line_values) - if self.exhausted: - vals += self._get_exhausted_inventory_lines_vals({(l['product_id'], l['location_id']) for l in vals}) - return vals - - @api.model - def _run_inventory_tasks(self, company_id=False): - """ Run conflict inventory tasks and generate/clean up cyclic inventories for locations as follows: - - [to create]: - The location has a cyclic count set (i.e. inventory every XX days) and has a next_inventory_date of today or earlier - - The location is not a descendant of another location that is having a cyclic inventory created - - The location doesn't already have an in progress inventory specific to (i.e. only for) it - - [to delete]: - Existing draft cyclic inventory that was created before today (i.e. still draft) [new one created afterwards] - - Existing draft location inventory that is a descendant of a location that has a next_inventory_date of today or earlier - """ - # if cron triggered => apply to all companies - company_domain = [('company_id', '!=', False)] - if company_id: - # if manually triggered => only apply to user's companies - company_domain = [('company_id', '=', company_id)] - - # cyclic location inventories - domain = expression.AND([[('next_inventory_date', '<=', fields.Date.today())], company_domain]) - locations = self.env['stock.location'].search(domain) - if locations: - # ignore any locations that are children of other locations that should have an inventory done - locations = locations.filtered(lambda l: not any(int(location_id) in locations.ids for location_id in l.parent_path.split('/')[:-2])) - existing_loc_invs = self.search([('state', 'in', ['draft', 'confirm']), ('product_ids', '=', False), ('location_ids', '!=', False)]) - invs_to_unlink = self.env['stock.inventory'] - existing_cyclic_inv = self.env['stock.inventory'] - for inventory in existing_loc_invs: - # assume inventories with more than 1 location were manually created and should be untouched - if len(inventory.location_ids) == 1: - if inventory.state == 'draft' \ - and (any(int(location_id) in locations.ids for location_id in inventory.location_ids[0].parent_path.split('/')[:-2]) - or (inventory.location_ids[0] in locations and inventory.create_date < fields.Datetime.today())): - invs_to_unlink |= inventory - elif inventory.location_ids[0] in locations: - existing_cyclic_inv |= inventory - invs_to_unlink.unlink() - locations -= existing_cyclic_inv.mapped('location_ids') - location_vals = [] - for location in locations: - location_vals.append({'name': "Reoccuring Inventory for: " + str(location.name), - 'company_id': location.company_id.id, - 'location_ids': location}) - self.create(location_vals) - # conflict inventories handled in separate function so inventories can also be updated when user opens inventory menuitem - self._run_conflict_inventory_tasks(company_id=company_id) - - @api.model - def _run_conflict_inventory_tasks(self, company_id=False): - """ Updates/creates/deletes conflict inventories. Conflict inventories include: - - negative quantities values, created per warehouse - Note that an in progress inventory will prevent an update/unlink when its - corresponding values are out of date. - """ - company_domain = [('company_id', '!=', False)] - if company_id: - # if manually triggered => only apply to user's companies - company_domain = [('company_id', '=', company_id)] - # negative quantity check - inventory_vals_to_create = [] - updated_invs = self.env['stock.inventory'] - existing_conflict_invs = self.search(expression.AND([ - [ - ('state', 'in', ['draft', 'confirm']), - ('is_conflict_inventory', '=', True) - ], company_domain])) - neg_quants = self.env['stock.quant'].search(expression.AND([ - [ - ('quantity', '<', 0.0), - ('location_id.usage', 'in', ['internal', 'transit']) - ], company_domain])) - company_ids = neg_quants.mapped('company_id') - warehouse_ids = self.env['stock.warehouse'].search([('company_id', 'in', company_ids.ids)]) - warehouse_locations = self.env['stock.location'].search([('id', 'child_of', warehouse_ids.mapped('view_location_id').ids)]) - company_to_warehouses = defaultdict(lambda: self.env['stock.warehouse']) - warehouse_to_locations = defaultdict(lambda: self.env['stock.location']) - for warehouse in warehouse_ids: - company_to_warehouses[warehouse.company_id] |= warehouse - warehouse_to_locations[warehouse] = warehouse_locations.filtered(lambda l: any(int(location_id) == warehouse.view_location_id.id for location_id in l.parent_path.split('/')[:-2])) - for company_id in company_ids: - # separate auto-generated inventories by warehouse - for warehouse in company_to_warehouses[company_id]: - # avoid conflicting inventories! Wait until the next time this is run after the previous neg qty inventory is completed - if existing_conflict_invs.filtered(lambda i: i.state == 'confirm' and i.company_id == company_id and i.location_ids & warehouse_to_locations[warehouse]): - continue - warehouse_quants = neg_quants.filtered(lambda q: q.location_id in warehouse_to_locations[warehouse]) - if warehouse_quants: - draft_inv = existing_conflict_invs.filtered(lambda i: i.state == 'draft' and i.company_id == company_id and i.location_ids & warehouse_to_locations[warehouse]) - if draft_inv: - # only write in in first draft in case there are duplicates due to function being called while its already - draft_inv[0].write({'product_ids': warehouse_quants.mapped('product_id')}) - updated_invs |= draft_inv[0] - else: - inventory_vals_to_create.append({ - 'name': "Negative Quantity Inventory: " + warehouse.name, - 'company_id': company_id.id, - 'product_ids': warehouse_quants.mapped('product_id'), - 'is_conflict_inventory': True, - 'location_ids': warehouse.view_location_id.child_ids - }) - - # conflicting SN check - domain = expression.AND([[('location_id.usage', 'in', ['internal', 'transit']), - ('lot_id', '!=', False), - ('product_id.tracking', '=', 'serial'), - ('quantity', '!=', 0.0)], - company_domain]) - quants = self.env['stock.quant'].read_group(domain, ['lot_id', 'company_id', 'product_id'], ['lot_id', 'company_id', 'product_id'], lazy=False) - company_to_sn_conflicts = defaultdict(lambda: ([], [])) - for quant in quants: - if quant['__count'] > 1: - company_to_sn_conflicts[quant['company_id'][0]][0].append(quant['lot_id'][0]) - company_to_sn_conflicts[quant['company_id'][0]][1].append(quant['product_id'][0]) - - for company_id, (lot_ids, product_ids) in company_to_sn_conflicts.items(): - # avoid conflicting inventories! Wait until the next time this is run after the previous conflicting SN inventory is completed - if existing_conflict_invs.filtered(lambda i: i.state == 'confirm' and i.lot_ids and i.company_id.id == company_id): - continue - draft_inv = existing_conflict_invs.filtered(lambda i: i.state == 'draft' and i.lot_ids and i.company_id.id == company_id) - if draft_inv: - draft_inv.write({'lot_ids': lot_ids, - 'product_ids': product_ids}) - updated_invs |= draft_inv - else: - inventory_vals_to_create.append({ - 'name': "Duplicate SN Inventory", - 'company_id': company_id, - 'lot_ids': lot_ids, - 'product_ids': product_ids, - 'is_conflict_inventory': True - }) - self.create(inventory_vals_to_create) - # remove all obsolete conflict inventories - (existing_conflict_invs.filtered(lambda i: i.state == 'draft') - updated_invs).unlink() - - @api.model - def action_open_inventory_view(self): - self._run_conflict_inventory_tasks() - return self.env["ir.actions.actions"]._for_xml_id("stock.action_inventory_form") - - -class InventoryLine(models.Model): - _name = "stock.inventory.line" - _description = "Inventory Line" - _order = "product_id, inventory_id, location_id, prod_lot_id" - - @api.model - def _domain_location_id(self): - if self.env.context.get('active_model') == 'stock.inventory': - inventory = self.env['stock.inventory'].browse(self.env.context.get('active_id')) - if inventory.exists() and inventory.location_ids: - return "[('company_id', '=', company_id), ('usage', 'in', ['internal', 'transit']), ('id', 'child_of', %s)]" % inventory.location_ids.ids - return "[('company_id', '=', company_id), ('usage', 'in', ['internal', 'transit'])]" - - @api.model - def _domain_product_id(self): - if self.env.context.get('active_model') == 'stock.inventory': - inventory = self.env['stock.inventory'].browse(self.env.context.get('active_id')) - if inventory.exists() and len(inventory.product_ids) > 1: - return "[('type', '=', 'product'), '|', ('company_id', '=', False), ('company_id', '=', company_id), ('id', 'in', %s)]" % inventory.product_ids.ids - return "[('type', '=', 'product'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]" - - is_editable = fields.Boolean(help="Technical field to restrict editing.") - inventory_id = fields.Many2one( - 'stock.inventory', 'Inventory', check_company=True, - index=True, ondelete='cascade') - partner_id = fields.Many2one('res.partner', 'Owner', check_company=True) - product_id = fields.Many2one( - 'product.product', 'Product', check_company=True, - domain=lambda self: self._domain_product_id(), - index=True, required=True) - product_uom_id = fields.Many2one( - 'uom.uom', 'Product Unit of Measure', - required=True, readonly=True) - product_qty = fields.Float( - 'Counted Quantity', - readonly=True, states={'confirm': [('readonly', False)]}, - digits='Product Unit of Measure', default=0) - categ_id = fields.Many2one(related='product_id.categ_id', store=True) - location_id = fields.Many2one( - 'stock.location', 'Location', check_company=True, - domain=lambda self: self._domain_location_id(), - index=True, required=True) - package_id = fields.Many2one( - 'stock.quant.package', 'Pack', index=True, check_company=True, - domain="[('location_id', '=', location_id)]", - ) - prod_lot_id = fields.Many2one( - 'stock.production.lot', 'Lot/Serial Number', check_company=True, - domain="[('product_id','=',product_id), ('company_id', '=', company_id)]") - company_id = fields.Many2one( - 'res.company', 'Company', related='inventory_id.company_id', - index=True, readonly=True, store=True) - state = fields.Selection(string='Status', related='inventory_id.state') - theoretical_qty = fields.Float( - 'Theoretical Quantity', - digits='Product Unit of Measure', readonly=True) - difference_qty = fields.Float('Difference', compute='_compute_difference', - help="Indicates the gap between the product's theoretical quantity and its newest quantity.", - readonly=True, digits='Product Unit of Measure', search="_search_difference_qty") - inventory_date = fields.Datetime('Inventory Date', readonly=True, - default=fields.Datetime.now, - help="Last date at which the On Hand Quantity has been computed.") - outdated = fields.Boolean(string='Quantity outdated', - compute='_compute_outdated', search='_search_outdated') - product_tracking = fields.Selection(string='Tracking', related='product_id.tracking', readonly=True) - - @api.depends('product_qty', 'theoretical_qty') - def _compute_difference(self): - for line in self: - line.difference_qty = line.product_qty - line.theoretical_qty - - @api.depends('inventory_date', 'product_id.stock_move_ids', 'theoretical_qty', 'product_uom_id.rounding') - def _compute_outdated(self): - quants_by_inventory = {inventory: inventory._get_quantities() for inventory in self.inventory_id} - for line in self: - quants = quants_by_inventory[line.inventory_id] - if line.state == 'done' or not line.id: - line.outdated = False - continue - qty = quants.get(( - line.product_id.id, - line.location_id.id, - line.prod_lot_id.id, - line.package_id.id, - line.partner_id.id), 0 - ) - if float_compare(qty, line.theoretical_qty, precision_rounding=line.product_uom_id.rounding) != 0: - line.outdated = True - else: - line.outdated = False - - @api.onchange('product_id', 'location_id', 'product_uom_id', 'prod_lot_id', 'partner_id', 'package_id') - def _onchange_quantity_context(self): - if self.product_id: - self.product_uom_id = self.product_id.uom_id - if self.product_id and self.location_id and self.product_id.uom_id.category_id == self.product_uom_id.category_id: # TDE FIXME: last part added because crash - theoretical_qty = self.product_id.get_theoretical_quantity( - self.product_id.id, - self.location_id.id, - lot_id=self.prod_lot_id.id, - package_id=self.package_id.id, - owner_id=self.partner_id.id, - to_uom=self.product_uom_id.id, - ) - else: - theoretical_qty = 0 - # Sanity check on the lot. - if self.prod_lot_id: - if self.product_id.tracking == 'none' or self.product_id != self.prod_lot_id.product_id: - self.prod_lot_id = False - - if self.prod_lot_id and self.product_id.tracking == 'serial': - # We force `product_qty` to 1 for SN tracked product because it's - # the only relevant value aside 0 for this kind of product. - self.product_qty = 1 - elif self.product_id and float_compare(self.product_qty, self.theoretical_qty, precision_rounding=self.product_uom_id.rounding) == 0: - # We update `product_qty` only if it equals to `theoretical_qty` to - # avoid to reset quantity when user manually set it. - self.product_qty = theoretical_qty - self.theoretical_qty = theoretical_qty - - @api.onchange('product_qty', 'prod_lot_id') - def _onchange_serial_number(self): - if self.prod_lot_id and self.product_id.tracking == 'serial' and not float_is_zero(self.product_qty, self.product_id.uom_id.rounding): - dupe_sn_lines = self.env['stock.inventory.line'] - message = _('Is this expected? For example this can happen if pick-pack-ship was not completed in the right order. In this case the issue will be solved ' - 'automatically once all steps are completed. Otherwise, you can adjust quantities in these locations.') - # first check current inventory for non-zero dupe SNs - if self.inventory_id: - dupe_sn_lines |= self.inventory_id.line_ids.filtered( - lambda l: l.product_id == self.product_id - and l.prod_lot_id == self.prod_lot_id - and l.location_id.id is not self.location_id.id - and not float_is_zero(l.product_qty, l.product_id.uom_id.rounding)) - if len(dupe_sn_lines) > 0: - message = _('There is already one or more inventory adjustment line(s) counting the Serial Number (%s) at the location(s): %s.\n\n', - self.prod_lot_id.name, ', '.join(dupe_sn_lines.location_id.mapped('display_name'))) + message - return {'warning': {'title': _('Warning'), 'message': message}} - # then check locations not in inventory for dupe SNs - if self.inventory_id and self.inventory_id.location_ids: - quants = self.env['stock.quant'].search([('product_id', '=', self.product_id.id), - ('lot_id', '=', self.prod_lot_id.id), - ('quantity', '!=', 0), - ('location_id', 'not in', self.inventory_id.location_ids.ids), - '|', ('location_id.usage', '=', 'customer'), - '&', ('company_id', '=', self.company_id.id), - ('location_id.usage', 'in', ('internal', 'transit'))]) - if quants: - message = _('The Serial Number (%s) is already used in these location(s): %s.\n\n', - self.prod_lot_id.name, ', '.join(quants.location_id.mapped('display_name'))) + message - return {'warning': {'title': _('Warning'), 'message': message}} - - @api.model_create_multi - def create(self, vals_list): - """ Override to handle the case we create inventory line without - `theoretical_qty` because this field is usually computed, but in some - case (typicaly in tests), we create inventory line without trigger the - onchange, so in this case, we set `theoretical_qty` depending of the - product's theoretical quantity. - Handles the same problem with `product_uom_id` as this field is normally - set in an onchange of `product_id`. - Finally, this override checks we don't try to create a duplicated line. - """ - for values in vals_list: - if 'theoretical_qty' not in values: - theoretical_qty = self.env['product.product'].get_theoretical_quantity( - values['product_id'], - values['location_id'], - lot_id=values.get('prod_lot_id'), - package_id=values.get('package_id'), - owner_id=values.get('partner_id'), - to_uom=values.get('product_uom_id'), - ) - values['theoretical_qty'] = theoretical_qty - if 'product_id' in values and 'product_uom_id' not in values: - values['product_uom_id'] = self.env['product.product'].browse(values['product_id']).uom_id.id - res = super(InventoryLine, self).create(vals_list) - res._check_no_duplicate_line() - return res - - def write(self, vals): - res = super(InventoryLine, self).write(vals) - self._check_no_duplicate_line() - return res - - def _check_no_duplicate_line(self): - for line in self: - domain = [ - ('id', '!=', line.id), - ('product_id', '=', line.product_id.id), - ('location_id', '=', line.location_id.id), - ('partner_id', '=', line.partner_id.id), - ('package_id', '=', line.package_id.id), - ('prod_lot_id', '=', line.prod_lot_id.id), - ('inventory_id', '=', line.inventory_id.id)] - existings = self.search_count(domain) - if existings: - raise UserError(_("There is already one inventory adjustment line for this product," - " you should rather modify this one instead of creating a new one.")) - - @api.constrains('product_id') - def _check_product_id(self): - """ As no quants are created for consumable products, it should not be possible do adjust - their quantity. - """ - for line in self: - if line.product_id.type != 'product': - raise ValidationError(_("You can only adjust storable products.") + '\n\n%s -> %s' % (line.product_id.display_name, line.product_id.type)) - - def _get_move_values(self, qty, location_id, location_dest_id, out): - self.ensure_one() - return { - 'name': _('INV:') + (self.inventory_id.name or ''), - 'product_id': self.product_id.id, - 'product_uom': self.product_uom_id.id, - 'product_uom_qty': qty, - 'date': self.inventory_id.date, - 'company_id': self.inventory_id.company_id.id, - 'inventory_id': self.inventory_id.id, - 'state': 'confirmed', - 'restrict_partner_id': self.partner_id.id, - 'location_id': location_id, - 'location_dest_id': location_dest_id, - 'move_line_ids': [(0, 0, { - 'product_id': self.product_id.id, - 'lot_id': self.prod_lot_id.id, - 'product_uom_qty': 0, # bypass reservation here - 'product_uom_id': self.product_uom_id.id, - 'qty_done': qty, - 'package_id': out and self.package_id.id or False, - 'result_package_id': (not out) and self.package_id.id or False, - 'location_id': location_id, - 'location_dest_id': location_dest_id, - 'owner_id': self.partner_id.id, - })] - } - - def _get_virtual_location(self): - return self.product_id.with_company(self.company_id).property_stock_inventory - - def _generate_moves(self): - vals_list = [] - for line in self: - virtual_location = line._get_virtual_location() - rounding = line.product_id.uom_id.rounding - if float_is_zero(line.difference_qty, precision_rounding=rounding): - continue - if line.difference_qty > 0: # found more than expected - vals = line._get_move_values(line.difference_qty, virtual_location.id, line.location_id.id, False) - else: - vals = line._get_move_values(abs(line.difference_qty), line.location_id.id, virtual_location.id, True) - vals_list.append(vals) - return self.env['stock.move'].create(vals_list) - - def action_refresh_quantity(self): - filtered_lines = self.filtered(lambda l: l.state != 'done') - for line in filtered_lines: - if line.outdated: - quants = self.env['stock.quant']._gather(line.product_id, line.location_id, lot_id=line.prod_lot_id, package_id=line.package_id, owner_id=line.partner_id, strict=True) - if quants.exists(): - quantity = sum(quants.mapped('quantity')) - if line.theoretical_qty != quantity: - line.theoretical_qty = quantity - else: - line.theoretical_qty = 0 - line.inventory_date = fields.Datetime.now() - - def action_reset_product_qty(self): - """ Write `product_qty` to zero on the selected records. """ - impacted_lines = self.env['stock.inventory.line'] - for line in self: - if line.state == 'done': - continue - impacted_lines |= line - impacted_lines.write({'product_qty': 0}) - - def _search_difference_qty(self, operator, value): - if operator == '=': - result = True - elif operator == '!=': - result = False - else: - raise NotImplementedError() - lines = self.search([('inventory_id', '=', self.env.context.get('default_inventory_id'))]) - line_ids = lines.filtered(lambda line: float_is_zero(line.difference_qty, line.product_id.uom_id.rounding) == result).ids - return [('id', 'in', line_ids)] - - def _search_outdated(self, operator, value): - if operator != '=': - if operator == '!=' and isinstance(value, bool): - value = not value - else: - raise NotImplementedError() - lines = self.search([('inventory_id', '=', self.env.context.get('default_inventory_id'))]) - line_ids = lines.filtered(lambda line: line.outdated == value).ids - return [('id', 'in', line_ids)] diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py index 54a890b7946..fd6eda2e23c 100644 --- a/addons/stock/models/stock_location.py +++ b/addons/stock/models/stock_location.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + +import calendar + from collections import defaultdict, OrderedDict from datetime import timedelta @@ -63,8 +66,8 @@ class Location(models.Model): putaway_rule_ids = fields.One2many('stock.putaway.rule', 'location_in_id', 'Putaway Rules') barcode = fields.Char('Barcode', copy=False) quant_ids = fields.One2many('stock.quant', 'location_id') - cyclic_inventory_frequency = fields.Integer("Inventory Frequency (Days)", default=0, help=" When different than 0, inventory adjustments for products stored at this location will be created automatically at the defined frequency.") - last_inventory_date = fields.Datetime("Last Effective Inventory", readonly=True, help="Date of the last inventory at this location.") + cyclic_inventory_frequency = fields.Integer("Inventory Frequency (Days)", default=0, help=" When different than 0, inventory count date for products stored at this location will be automatically set at the defined frequency.") + last_inventory_date = fields.Date("Last Effective Inventory", readonly=True, help="Date of the last inventory at this location.") next_inventory_date = fields.Date("Next Expected Inventory", compute="_compute_next_inventory_date", store=True, help="Date for next planned inventory based on cyclic schedule.") warehouse_view_ids = fields.One2many('stock.warehouse', 'view_location_id', readonly=True) warehouse_id = fields.Many2one('stock.warehouse', compute='_compute_warehouse_id') @@ -109,7 +112,7 @@ class Location(models.Model): if location.company_id and location.usage in ['internal', 'transit'] and location.cyclic_inventory_frequency > 0: try: if location.last_inventory_date: - days_until_next_inventory = location.cyclic_inventory_frequency - (fields.Date.today() - location.last_inventory_date.date()).days + days_until_next_inventory = location.cyclic_inventory_frequency - (fields.Date.today() - location.last_inventory_date).days if days_until_next_inventory <= 0: location.next_inventory_date = fields.Date.today() + timedelta(days=1) else: @@ -232,6 +235,35 @@ class Location(models.Model): return putaway_rules._get_putaway_location(product, quantity, package, qty_by_location) or self + def _get_next_inventory_date(self): + """ Used to get the next inventory date for a quant located in this location. It is + based on: + 1. Does the location have a cyclic inventory set? + 2. If not 1, then is there an annual inventory date set (for its company)? + 3. If not 1 and 2, then quants have no next inventory date.""" + if self.usage not in ['internal', 'transit']: + return False + next_inventory_date = False + if self.next_inventory_date: + next_inventory_date = self.next_inventory_date + elif self.company_id.annual_inventory_month: + today = fields.Date.today() + annual_inventory_month = int(self.company_id.annual_inventory_month) + # Manage 0 and negative annual_inventory_day + annual_inventory_day = max(self.company_id.annual_inventory_day, 1) + max_day = calendar.monthrange(today.year, annual_inventory_month)[1] + # Manage annual_inventory_day bigger than last_day + annual_inventory_day = min(annual_inventory_day, max_day) + next_inventory_date = today.replace( + month=annual_inventory_month, day=annual_inventory_day) + if next_inventory_date <= today: + # Manage leap year with the february + max_day = calendar.monthrange(today.year + 1, annual_inventory_month)[1] + annual_inventory_day = min(annual_inventory_day, max_day) + next_inventory_date = next_inventory_date.replace( + day=annual_inventory_day, year=today.year + 1) + return next_inventory_date + def should_bypass_reservation(self): self.ensure_one() return self.usage in ('supplier', 'customer', 'inventory', 'production') or self.scrap_location or (self.usage == 'transit' and not self.company_id) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 577c64db47a..1fedcc7c1ba 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -130,7 +130,7 @@ class StockMove(models.Model): help='If checked, when this move is cancelled, cancel the linked move too') delay_alert_date = fields.Datetime('Delay Alert Date', help='Process at this date to be on time', compute="_compute_delay_alert_date", store=True) picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type', compute='_compute_picking_type_id', store=True, check_company=True) - inventory_id = fields.Many2one('stock.inventory', 'Inventory', check_company=True) + is_inventory = fields.Boolean('Inventory') move_line_ids = fields.One2many('stock.move.line', 'move_id') move_line_nosuggest_ids = fields.One2many('stock.move.line', 'move_id', domain=['|', ('product_qty', '=', 0.0), ('qty_done', '!=', 0.0)]) origin_returned_move_id = fields.Many2one( @@ -1507,13 +1507,13 @@ class StockMove(models.Model): # Cancel moves where necessary ; we should do it before creating the extra moves because # this operation could trigger a merge of moves. for move in moves: - if move.quantity_done <= 0: + if move.quantity_done <= 0 and not move.is_inventory: if float_compare(move.product_uom_qty, 0.0, precision_rounding=move.product_uom.rounding) == 0 or cancel_backorder: move._action_cancel() # Create extra moves where necessary for move in moves: - if move.state == 'cancel' or move.quantity_done <= 0: + if move.state == 'cancel' or (move.quantity_done <= 0 and not move.is_inventory): continue moves_ids_todo |= move._create_extra_move().ids diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index 89efc4582f8..dd7cbaf9ac1 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -62,6 +62,7 @@ class StockMoveLine(models.Model): picking_type_entire_packs = fields.Boolean(related='picking_id.picking_type_id.show_entire_packs', readonly=True) state = fields.Selection(related='move_id.state', store=True, related_sudo=False) is_initial_demand_editable = fields.Boolean(related='move_id.is_initial_demand_editable') + is_inventory = fields.Boolean(related='move_id.is_inventory') is_locked = fields.Boolean(related='move_id.is_locked', default=True, readonly=True) consume_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'consume_line_id', 'produce_line_id', help="Technical link to see who consumed what. ") produce_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'produce_line_id', 'consume_line_id', help="Technical link to see which line was produced with this. ") @@ -511,7 +512,7 @@ class StockMoveLine(models.Model): # checkboxes on the picking type, he's allowed to enter tracked # products without a `lot_id`. continue - elif ml.move_id.inventory_id: + elif ml.is_inventory: # If an inventory adjustment is linked, the user is allowed to enter # tracked products without a `lot_id`. continue @@ -520,7 +521,7 @@ class StockMoveLine(models.Model): ml_ids_tracked_without_lot.add(ml.id) elif qty_done_float_compared < 0: raise UserError(_('No negative quantities allowed')) - else: + elif not ml.is_inventory: ml_ids_to_delete.add(ml.id) if ml_ids_tracked_without_lot: diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index a3de8614b84..1f79c6ff7d1 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -8,7 +8,7 @@ from psycopg2 import Error, OperationalError from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError from odoo.osv import expression -from odoo.tools.float_utils import float_compare, float_is_zero, float_round +from odoo.tools.float_utils import float_compare, float_is_zero _logger = logging.getLogger(__name__) @@ -80,9 +80,6 @@ class StockQuant(models.Model): 'Quantity', help='Quantity of products in this quant, in the default unit of measure of the product', readonly=True) - inventory_quantity = fields.Float( - 'Inventoried Quantity', compute='_compute_inventory_quantity', - inverse='_set_inventory_quantity', groups='stock.group_stock_manager') reserved_quantity = fields.Float( 'Reserved Quantity', default=0.0, @@ -96,39 +93,36 @@ class StockQuant(models.Model): tracking = fields.Selection(related='product_id.tracking', readonly=True) on_hand = fields.Boolean('On Hand', store=False, search='_search_on_hand') + # Inventory Fields + inventory_quantity = fields.Float( + 'Counted Quantity', digits='Product Unit of Measure', groups='stock.group_stock_manager', + help="The product's counted quantity.") + inventory_diff_quantity = fields.Float( + 'Difference', compute='_compute_inventory_diff_quantity', store=True, + help="Indicates the gap between the product's theoretical quantity and its counted quantity.", + readonly=True, digits='Product Unit of Measure', groups='stock.group_stock_manager') + inventory_date = fields.Date( + 'Scheduled Date', compute='_compute_inventory_date', store=True, readonly=False, + help="Next date the On Hand Quantity should be counted.") + user_id = fields.Many2one( + 'res.users', 'Assigned To', groups='stock.group_stock_manager', help="User assigned to do product count.") + @api.depends('quantity', 'reserved_quantity') def _compute_available_quantity(self): for quant in self: quant.available_quantity = quant.quantity - quant.reserved_quantity - @api.depends('quantity') - def _compute_inventory_quantity(self): - if not self._is_inventory_mode(): - self.inventory_quantity = 0 - return - for quant in self: - quant.inventory_quantity = quant.quantity + @api.depends('location_id') + def _compute_inventory_date(self): + quants = self.filtered(lambda q: not q.inventory_date and q.location_id.usage in ['internal', 'transit']) + date_by_location = {loc: loc._get_next_inventory_date() for loc in quants.location_id} + for quant in quants: + quant.inventory_date = date_by_location[quant.location_id] - def _set_inventory_quantity(self): - """ Inverse method to create stock move when `inventory_quantity` is set - (`inventory_quantity` is only accessible in inventory mode). - """ - if not self._is_inventory_mode(): - return + @api.depends('inventory_quantity') + def _compute_inventory_diff_quantity(self): for quant in self: - # Get the quantity to create a move for. - rounding = quant.product_id.uom_id.rounding - diff = float_round(quant.inventory_quantity - quant.quantity, precision_rounding=rounding) - diff_float_compared = float_compare(diff, 0, precision_rounding=rounding) - # Create and vaidate a move so that the quant matches its `inventory_quantity`. - if diff_float_compared == 0: - continue - elif diff_float_compared > 0: - move_vals = quant._get_inventory_move_values(diff, quant.product_id.with_company(quant.company_id).property_stock_inventory, quant.location_id) - else: - move_vals = quant._get_inventory_move_values(-diff, quant.location_id, quant.product_id.with_company(quant.company_id).property_stock_inventory, out=True) - move = quant.env['stock.move'].with_context(inventory_mode=False).create(move_vals) - move._action_done() + quant.inventory_diff_quantity = quant.inventory_quantity - quant.quantity def _search_on_hand(self, operator, value): """Handle the "on_hand" filter, indirectly calling `_get_domain_locations`.""" @@ -166,6 +160,9 @@ class StockQuant(models.Model): quant = self.sudo().create(vals) # Set the `inventory_quantity` field to create the necessary move. quant.inventory_quantity = inventory_quantity + quant.user_id = vals.get('user_id', self.env.user.id) + quant.inventory_date = fields.Date.today() + return quant res = super(StockQuant, self).create(vals) if self._is_inventory_mode(): @@ -184,8 +181,8 @@ class StockQuant(models.Model): fields.append('reserved_quantity') result = super(StockQuant, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) for group in result: - if self._is_inventory_mode(): - group['inventory_quantity'] = group.get('quantity', 0) + if self.env.context.get('inventory_report_mode'): + group['inventory_quantity'] = False if 'available_quantity' in fields: group['available_quantity'] = group['quantity'] - group['reserved_quantity'] return result @@ -200,7 +197,11 @@ class StockQuant(models.Model): if any(field for field in vals.keys() if field not in allowed_fields): raise UserError(_("Quant's editing is restricted, you can't do this operation.")) self = self.sudo() - return super(StockQuant, self).write(vals) + res = super(StockQuant, self).write(vals) + if res and self.env.context.get('inventory_report_mode'): + # update context to prevent recursive write call + self.with_context({'inventory_report_mode': False}).action_apply_inventory() + return res return super(StockQuant, self).write(vals) def action_view_stock_moves(self): @@ -221,17 +222,116 @@ class StockQuant(models.Model): @api.model def action_view_quants(self): self = self.with_context(search_default_internal_loc=1) - if not self.user_has_groups('stock.group_stock_multi_locations'): - company_user = self.env.company - warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_user.id)], limit=1) - if warehouse: - self = self.with_context(default_location_id=warehouse.lot_stock_id.id) - - # If user have rights to write on quant, we set quants in inventory mode. - if self.user_has_groups('stock.group_stock_manager'): - self = self.with_context(inventory_mode=True) + self = self._set_view_context() return self._get_quants_action(extend=True) + @api.model + def action_view_inventory(self): + """ Similar to _get_quants_action except specific for inventory adjustments (i.e. inventory counts). """ + self = self._set_view_context() + self._quant_tasks() + + ctx = dict(self.env.context or {}) + ctx['no_at_date'] = True + action = { + 'name': _('Stock On Hand'), + 'view_mode': 'list', + 'view_id': self.env.ref('stock.view_stock_quant_tree_inventory_editable').id, + 'res_model': 'stock.quant', + 'type': 'ir.actions.act_window', + 'context': ctx, + 'domain': [('location_id.usage', 'in', ['internal', 'transit'])], + 'help': """ +

+ Want to speed up your inventory counts? Try our Barcode app +

+ Barcode scanner can be activated via inventory settings. + Manual inventory adjustments can also be performed and pre-filled with + suggested counted quantity. +

+ """ + } + return action + + def action_apply_inventory(self): + quants_outdated = [] + products_tracked_without_lot = [] + for quant in self: + rounding = quant.product_uom_id.rounding + if fields.Float.is_zero(quant.inventory_diff_quantity, precision_rounding=rounding)\ + and fields.Float.is_zero(quant.inventory_quantity, precision_rounding=rounding)\ + and fields.Float.is_zero(quant.quantity, precision_rounding=rounding): + continue + if fields.Float.compare(quant.inventory_quantity - quant.inventory_diff_quantity, quant.quantity, precision_rounding=rounding): + quants_outdated.append(quant.id) + if quant.product_id.tracking in ['lot', 'serial'] and\ + not quant.lot_id and quant.inventory_quantity != quant.quantity: + products_tracked_without_lot.append(quant.product_id.id) + # for some reason if multi-record, env.context doesn't pass to wizards... + ctx = dict(self.env.context or {}) + ctx['default_quant_ids'] = self.ids + if quants_outdated: + ctx['default_quant_to_fix_ids'] = quants_outdated + return { + 'name': _('Conflict in Inventory Adjustment'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'views': [(False, 'form')], + 'res_model': 'stock.inventory.conflict', + 'target': 'new', + 'context': ctx, + } + if products_tracked_without_lot: + ctx['default_product_ids'] = products_tracked_without_lot + return { + 'name': _('Tracked Products in Inventory Adjustment'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'views': [(False, 'form')], + 'res_model': 'stock.track.confirmation', + 'target': 'new', + 'context': ctx, + } + self._apply_inventory() + + def action_inventory_history(self): + self.ensure_one() + action = { + 'name': _('History'), + 'view_mode': 'list,form', + 'res_model': 'stock.move.line', + 'views': [(self.env.ref('stock.view_move_line_tree').id, 'list'), (False, 'form')], + 'type': 'ir.actions.act_window', + 'context': { + 'search_default_inventory': 1, + 'search_default_done': 1, + }, + 'domain': [ + ('product_id', '=', self.product_id.id), + ('company_id', '=', self.company_id.id), + '|', + ('location_id', '=', self.location_id.id), + ('location_dest_id', '=', self.location_id.id), + ], + } + if self.lot_id: + action['context']['search_default_lot_id'] = self.lot_id.id + if self.package_id: + action['context']['search_default_package_id'] = self.package_id.id + action['context']['search_default_result_package_id'] = self.package_id.id + if self.owner_id: + action['context']['search_default_owner_id'] = self.owner_id.id + return action + + def action_set_inventory_quantity(self): + for quant in self: + quant.inventory_quantity = quant.quantity + self.user_id = self.env.user.id + + def action_set_inventory_quantity_to_zero(self): + self.inventory_quantity = 0 + self.inventory_diff_quantity = 0 + @api.constrains('product_id') def check_product_id(self): if any(elem.product_id.type != 'product' for elem in self): @@ -341,18 +441,16 @@ class StockQuant(models.Model): if self.tracking == 'none' or self.product_id != self.lot_id.product_id: vals['lot_id'] = None - quants = self._gather(self.product_id, self.location_id, lot_id=self.lot_id, package_id=self.package_id, owner_id=self.owner_id, strict=True) - reserved_quantity = sum(quants.mapped('reserved_quantity')) - quantity = sum(quants.mapped('quantity')) + quant = self._gather( + self.product_id, self.location_id, lot_id=self.lot_id, + package_id=self.package_id, owner_id=self.owner_id, strict=True) + if quant: + self.quantity = quant.quantity - vals['reserved_quantity'] = reserved_quantity - # Update `quantity` only if the user manually updated `inventory_quantity`. - if float_compare(self.quantity, self.inventory_quantity, precision_rounding=self.product_uom_id.rounding) == 0: - vals['quantity'] = quantity # Special case: directly set the quantity to one for serial numbers, # it'll trigger `inventory_quantity` compute. if self.lot_id and self.tracking == 'serial': - vals['quantity'] = 1 + vals['inventory_quantity'] = 1 if vals: self.update(vals) @@ -378,6 +476,30 @@ class StockQuant(models.Model): if message: return {'warning': {'title': _('Warning'), 'message': message}} + def _apply_inventory(self): + move_vals = [] + for quant in self: + # Create and validate a move so that the quant matches its `inventory_quantity`. + if float_compare(quant.inventory_diff_quantity, 0, precision_rounding=quant.product_uom_id.rounding) > 0: + move_vals.append( + quant._get_inventory_move_values(quant.inventory_diff_quantity, + quant.product_id.with_company(quant.company_id).property_stock_inventory, + quant.location_id)) + else: + move_vals.append( + quant._get_inventory_move_values(-quant.inventory_diff_quantity, + quant.location_id, + quant.product_id.with_company(quant.company_id).property_stock_inventory, + out=True)) + moves = self.env['stock.move'].with_context(inventory_mode=False).create(move_vals) + moves._action_done() + self.location_id.write({'last_inventory_date': fields.Date.today()}) + date_by_location = {loc: loc._get_next_inventory_date() for loc in self.mapped('location_id')} + for quant in self: + quant.inventory_date = date_by_location[quant.location_id] + self.write({'inventory_quantity': 0, 'user_id': False}) + self.write({'inventory_diff_quantity': 0}) + @api.model def _update_available_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, in_date=None): """ Increase or decrease `reserved_quantity` of a set of quants for a given set of @@ -498,8 +620,10 @@ class StockQuant(models.Model): """ precision_digits = max(6, self.sudo().env.ref('product.decimal_product_uom').digits * 2) # Use a select instead of ORM search for UoM robustness. - query = """SELECT id FROM stock_quant WHERE (round(quantity::numeric, %s) = 0 OR quantity IS NULL) AND round(reserved_quantity::numeric, %s) = 0;""" - params = (precision_digits, precision_digits) + query = """SELECT id FROM stock_quant WHERE (round(quantity::numeric, %s) = 0 OR quantity IS NULL) + AND round(reserved_quantity::numeric, %s) = 0 + AND (round(inventory_quantity::numeric, %s) = 0 OR inventory_quantity IS NULL);""" + params = (precision_digits, precision_digits, precision_digits) self.env.cr.execute(query, params) quant_ids = self.env['stock.quant'].browse([quant['id'] for quant in self.env.cr.dictfetchall()]) quant_ids.sudo().unlink() @@ -516,6 +640,7 @@ class StockQuant(models.Model): SELECT min(id) as to_update_quant_id, (array_agg(id ORDER BY id))[2:array_length(array_agg(id), 1)] as to_delete_quant_ids, SUM(reserved_quantity) as reserved_quantity, + SUM(inventory_quantity) as inventory_quantity, SUM(quantity) as quantity, MIN(in_date) as in_date FROM stock_quant @@ -526,6 +651,7 @@ class StockQuant(models.Model): UPDATE stock_quant q SET quantity = d.quantity, reserved_quantity = d.reserved_quantity, + inventory_quantity = d.inventory_quantity, in_date = d.in_date FROM dupes d WHERE d.to_update_quant_id = q.id @@ -550,19 +676,19 @@ class StockQuant(models.Model): "inventory session", meaning a mode where we need to create the stock.move record necessary to be consistent with the `inventory_quantity` field. """ - return self.env.context.get('inventory_mode') is True and self.user_has_groups('stock.group_stock_manager') + return self.env.context.get('inventory_mode') and self.user_has_groups('stock.group_stock_manager') @api.model def _get_inventory_fields_create(self): """ Returns a list of fields user can edit when he want to create a quant in `inventory_mode`. """ - return ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id', 'inventory_quantity'] + return ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id'] + self._get_inventory_fields_write() @api.model def _get_inventory_fields_write(self): """ Returns a list of fields user can edit when he want to edit a quant in `inventory_mode`. """ - return ['inventory_quantity'] + return ['inventory_quantity', 'inventory_diff_quantity', 'inventory_date', 'user_id'] def _get_inventory_move_values(self, qty, location_id, location_dest_id, out=False): """ Called when user manually set a new quantity (via `inventory_quantity`) @@ -574,8 +700,12 @@ class StockQuant(models.Model): :return: dict with all values needed to create a new `stock.move` with its move line. """ self.ensure_one() + if fields.Float.is_zero(qty, 0, precision_rounding=self.product_uom_id.rounding): + name = _('Product Quantity Confirmed') + else: + name = _('Product Quantity Updated') return { - 'name': _('Product Quantity Updated'), + 'name': name, 'product_id': self.product_id.id, 'product_uom': self.product_uom_id.id, 'product_uom_qty': qty, @@ -583,6 +713,7 @@ class StockQuant(models.Model): 'state': 'confirmed', 'location_id': location_id.id, 'location_dest_id': location_dest_id.id, + 'is_inventory': True, 'move_line_ids': [(0, 0, { 'product_id': self.product_id.id, 'product_uom_id': self.product_uom_id.id, @@ -597,9 +728,22 @@ class StockQuant(models.Model): })] } + def _set_view_context(self): + """ Adds context when opening quants related views. """ + if not self.user_has_groups('stock.group_stock_multi_locations'): + company_user = self.env.company + warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_user.id)], limit=1) + if warehouse: + self = self.with_context(default_location_id=warehouse.lot_stock_id.id, hide_location=True) + + # If user have rights to write on quant, we set quants in inventory mode. + if self.user_has_groups('stock.group_stock_manager'): + self = self.with_context(inventory_mode=True) + return self + @api.model def _get_quants_action(self, domain=None, extend=False): - """ Returns an action to open quant view. + """ Returns an action to open (non-inventory adjustment) quant view. Depending of the context (user have right to be inventory mode or not), the list view will be editable or readonly. @@ -608,6 +752,7 @@ class StockQuant(models.Model): """ self._quant_tasks() ctx = dict(self.env.context or {}) + ctx['inventory_report_mode'] = True ctx.pop('group_by', None) action = { 'name': _('Stock On Hand'), diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py index 7227579915c..f25050e5d5b 100644 --- a/addons/stock/models/stock_rule.py +++ b/addons/stock/models/stock_rule.py @@ -517,12 +517,6 @@ class ProcurementGroup(models.Model): if use_new_cursor: self._cr.commit() - # Run cyclic inventories - self.env['stock.inventory']._run_inventory_tasks(company_id) - - if use_new_cursor: - self._cr.commit() - @api.model def run_scheduler(self, use_new_cursor=False, company_id=False): """ Call the scheduler in order to check the running procurements (super method), to check the minimum stock rules diff --git a/addons/stock/populate/stock.py b/addons/stock/populate/stock.py index 7527bf2904e..b97eac92922 100644 --- a/addons/stock/populate/stock.py +++ b/addons/stock/populate/stock.py @@ -303,139 +303,6 @@ class StockWarehouseOrderpoint(models.Model): ] -class Inventory(models.Model): - _inherit = 'stock.inventory' - - _populate_sizes = {'small': 5, 'medium': 10, 'large': 20} - _populate_dependencies = ['stock.location', 'product.product'] - - def _populate(self, size): - inventories = super()._populate(size) - - def start_inventory_sample(sample_ratio): - random = populate.Random('start_inventory_sample') - inventories_to_start = self.browse(random.sample(inventories.ids, int(len(inventories.ids) * sample_ratio))) - # Start empty to let the stock.inventory.line populate create lines - inventories_to_start.start_empty = True - for inventory in inventories_to_start: - inventory.action_start() - - # Start 80 % of adjustment inventory - start_inventory_sample(0.8) - - return inventories - - def _populate_factories(self): - - company_ids = self.env.registry.populated_models['res.company'][:COMPANY_NB_WITH_STOCK] - valid_locations = self.env['stock.location'].search([ - ('company_id', 'in', company_ids), - ('usage', 'in', ['internal', 'transit']) - ]) - locations_by_company = dict(groupby(valid_locations, key=lambda loc: loc.company_id.id)) - locations_by_company = {company_id: self.env['stock.location'].concat(*locations) for company_id, locations in locations_by_company.items()} - products = self.env['product.product'].browse(self.env.registry.populated_models['product.product']).filtered(lambda p: p.type == 'product') - - def get_locations_ids(values, counter, random): - location_ids_company = locations_by_company[values['company_id']] - # We set locations to ease the creation of stock.inventory.line - # Should be larger enough to avoid the emptiness of generator_product_loc_dict - return random.sample(location_ids_company.ids, int(len(location_ids_company.ids) * 0.5)) - - def get_product_ids(values, counter, random): - # We set products to ease the creation of stock.inventory.line - # Should be larger enough to avoid the emptiness of generator_product_loc_dict - return random.sample(products.ids, int(len(products.ids) * 0.5)) - - return [ - ('name', populate.constant("Inventory-Pop-{counter}")), - ('company_id', populate.iterate(company_ids)), - ('location_ids', populate.compute(get_locations_ids)), - ('product_ids', populate.compute(get_product_ids)), - ] - - -class InventoryLine(models.Model): - _inherit = 'stock.inventory.line' - - _populate_sizes = {'small': 500, 'medium': 5_000, 'large': 20_000} - _populate_dependencies = ['stock.inventory'] - - def _populate(self, size): - inventory_lines = super()._populate(size) - - def create_missing_lots(): - _logger.info("Create lot/serial for inventory line to be ready to validate") - lots_values = [] - for line in inventory_lines: - if line.product_tracking == 'none': - continue - lots_values.append({ - 'name': "InLine-%d" % line.id, - 'product_id': line.product_id.id, - 'company_id': line.inventory_id.company_id.id - }) - lots = self.env['stock.production.lot'].create(lots_values).ids[::-1] - for line in inventory_lines: - if line.product_tracking == 'none': - continue - line.prod_lot_id = lots.pop() - - def validate_inventory_sample(sample_ratio): - # Validate the inventory adjustment, useful to have a stock at beginning - inventories_ids = inventory_lines.inventory_id.ids - random = populate.Random('validate_inventory') - inventories_to_done = self.env['stock.inventory'].browse(random.sample(inventories_ids, int(len(inventories_ids) * sample_ratio))) - _logger.info("Validate %d inventory adjustment" % len(inventories_to_done)) - for inventory in inventories_to_done: - inventory.action_validate() - - # Create missing tracking lot/serial in batch - create_missing_lots() - - # (Un)comment to test a DB with a current stock. - # validate_inventory_sample(0.8) - - return inventory_lines - - def _populate_factories(self): - - inventories = self.env['stock.inventory'].browse(self.env.registry.populated_models['stock.inventory']) - inventories = inventories.filtered(lambda i: i.state == 'confirm') - - def compute_product_location(iterator, field_name, model_name): - random = populate.Random('compute_product_location') - - # To avoid create twice the same line inventory (avoid _check_no_duplicate_line) : product/location (limitation for lot) - # Use generator to avoid cartisian product in memory - generator_product_loc_dict = {} - for inventory in inventories: - # TODO: randomize cartesian product - generator_product_loc_dict[inventory.id] = cartesian_product( - random.sample(inventory.location_ids, len(inventory.location_ids)), - random.sample(inventory.product_ids, len(inventory.product_ids)) - ) - - for values in iterator: - loc_id, product = next(generator_product_loc_dict[values['inventory_id']]) - values['product_id'] = product.id - values['location_id'] = loc_id.id - yield values - - def get_product_qty(values, counter, random): - product = self.env['product.product'].browse(values['product_id']) - if product.tracking == 'serial': - return 1.0 - else: - return random.randint(5, 25) - - return [ - ('inventory_id', populate.iterate(inventories.ids)), - ('_compute_product_location', compute_product_location), - ('product_qty', populate.compute(get_product_qty)), - ] - - class PickingType(models.Model): _inherit = 'stock.picking.type' diff --git a/addons/stock/report/report_stockinventory.xml b/addons/stock/report/report_stockinventory.xml index 2aff98f17dd..9a1b7a4d929 100644 --- a/addons/stock/report/report_stockinventory.xml +++ b/addons/stock/report/report_stockinventory.xml @@ -3,54 +3,51 @@ diff --git a/addons/stock/report/stock_report_views.xml b/addons/stock/report/stock_report_views.xml index dea0838fc60..cff99953af5 100644 --- a/addons/stock/report/stock_report_views.xml +++ b/addons/stock/report/stock_report_views.xml @@ -23,12 +23,12 @@
Count Sheet - stock.inventory + stock.quant qweb-pdf stock.report_inventory stock.report_inventory - 'Inventory - %s' % (object.name) - + 'Count Sheet' + report diff --git a/addons/stock/report/stock_traceability.py b/addons/stock/report/stock_traceability.py index 42495ea5a32..28fbedb6829 100644 --- a/addons/stock/report/stock_traceability.py +++ b/addons/stock/report/stock_traceability.py @@ -89,10 +89,10 @@ class MrpStockReport(models.TransientModel): res_model = 'stock.picking' res_id = picking_id.id ref = picking_id.name - elif move_line.move_id.inventory_id: - res_model = 'stock.inventory' - res_id = move_line.move_id.inventory_id.id - ref = 'Inv. Adj.: ' + move_line.move_id.inventory_id.name + elif move_line.move_id.is_inventory: + res_model = 'stock.move' + res_id = move_line.move_id.id + ref = 'Inventory Adjustment' elif move_line.move_id.scrapped and move_line.move_id.scrap_ids: res_model = 'stock.scrap' res_id = move_line.move_id.scrap_ids[0].id diff --git a/addons/stock/security/ir.model.access.csv b/addons/stock/security/ir.model.access.csv index 1b840aa3705..40489e6a6ca 100644 --- a/addons/stock/security/ir.model.access.csv +++ b/addons/stock/security/ir.model.access.csv @@ -13,10 +13,6 @@ access_stock_picking_type_manager,stock.picking.type manager,model_stock_picking access_stock_production_lot_user,stock.production.lot user,model_stock_production_lot,stock.group_stock_user,1,1,1,1 access_stock_move_manager,stock.move manager,model_stock_move,stock.group_stock_manager,1,1,1,1 access_stock_move_user,stock.move user,model_stock_move,stock.group_stock_user,1,1,1,0 -access_stock_inventory_user,stock.inventory user,model_stock_inventory,stock.group_stock_user,1,1,1,0 -access_stock_inventory_manager,stock.inventory manager,model_stock_inventory,stock.group_stock_manager,1,1,1,1 -access_stock_inventory_line_user,stock.inventory.line user,model_stock_inventory_line,stock.group_stock_user,1,1,1,0 -access_stock_inventory_line_manager,stock.inventory.line manager,model_stock_inventory_line,stock.group_stock_manager,1,1,1,1 access_product_product_stock_user,product_product_stock_user,product.model_product_product,stock.group_stock_user,1,0,0,0 access_product_template_stock_user,product.template stock user,product.model_product_template,stock.group_stock_user,1,0,0,0 access_uom_category_stock_manager,uom.category stock_manager,uom.model_uom_category,stock.group_stock_manager,1,1,1,1 @@ -87,3 +83,5 @@ access_stock_storage_category_user,stock.storage.category.user,model_stock_stora access_stock_storage_category_manager,stock.storage.category.manager,model_stock_storage_category,stock.group_stock_manager,1,1,1,1 access_stock_storage_category_capacity_user,stock.storage.category.capacity.user,model_stock_storage_category_capacity,base.group_user,1,0,0,0 access_stock_storage_category_capacity_manager,stock.storage.category.capacity.manager,model_stock_storage_category_capacity,stock.group_stock_manager,1,1,1,1 +access_stock_inventory_conflict,stock.inventory.conflict,model_stock_inventory_conflict,stock.group_stock_manager,1,1,1,0 +access_stock_request_count,stock.request.count,model_stock_request_count,stock.group_stock_manager,1,1,1,0 diff --git a/addons/stock/security/stock_security.xml b/addons/stock/security/stock_security.xml index 171d0d6f397..ad72b4b74bb 100644 --- a/addons/stock/security/stock_security.xml +++ b/addons/stock/security/stock_security.xml @@ -129,18 +129,6 @@ ['|',('company_id','=',False),('company_id', 'in', company_ids)] - - Inventory Line multi-company - - [('company_id', 'in', company_ids)] - - - - Inventory multi-company - - [('company_id', 'in', company_ids)] - - stock_warehouse.orderpoint multi-company diff --git a/addons/stock/static/src/js/inventory_report_list_controller.js b/addons/stock/static/src/js/inventory_report_list_controller.js index eb6a3ed6f46..e2a086002b8 100644 --- a/addons/stock/static/src/js/inventory_report_list_controller.js +++ b/addons/stock/static/src/js/inventory_report_list_controller.js @@ -1,13 +1,10 @@ odoo.define('stock.InventoryReportListController', function (require) { "use strict"; -var core = require('web.core'); var ListController = require('web.ListController'); -var qweb = core.qweb; - - var InventoryReportListController = ListController.extend({ + buttons_template: 'StockInventoryReport.Buttons', // ------------------------------------------------------------------------- // Public @@ -24,11 +21,9 @@ var InventoryReportListController = ListController.extend({ renderButtons: function ($node) { this._super.apply(this, arguments); if (this.context.no_at_date) { - return; + this.$buttons.find('button.o_button_at_date').hide(); } - var $buttonToDate = $(qweb.render('InventoryReport.Buttons')); - $buttonToDate.on('click', this._onOpenWizard.bind(this)); - this.$buttons.prepend($buttonToDate); + this.$buttons.on('click', '.o_button_at_date', this._onOpenWizard.bind(this)); }, // ------------------------------------------------------------------------- diff --git a/addons/stock/static/src/js/inventory_singleton_list_controller.js b/addons/stock/static/src/js/inventory_singleton_list_controller.js index 9e5bd4db850..42912a42745 100644 --- a/addons/stock/static/src/js/inventory_singleton_list_controller.js +++ b/addons/stock/static/src/js/inventory_singleton_list_controller.js @@ -7,20 +7,43 @@ var InventoryReportListController = require('stock.InventoryReportListController var _t = core._t; /** - * The purpose of this override is to avoid to have two or more similar records - * in the list view. + * The purpose of this override has 2 purposes: * - * It's used in quant list view, a list editable where when you create a new - * line about a quant who already exists, we want to update the existing one - * instead of create a new one, and then we don't want to have two similar line - * in the list view, so we refresh it. + * 1. Avoid having two or more similar records in the list view. + * It is expected to be used in quant list views, when the list is editable + * and allows new line creation. Checks for whether newly created quant + * already exists and if it does => update the existing quant instead of the + * newly created one, and then we refresh the view to avoid having 2 similar lines. + * + * 2. Support Inventory Adjustments (i.e. inventory counts) view. + * Specifically adds necessary buttons (or hides them when appropriate) and + * autosaving logic to prevent button actions before edits are saved. */ var SingletonListController = InventoryReportListController.extend({ + buttons_template: 'StockQuant.Buttons', + + // ------------------------------------------------------------------------- + // Public + // ------------------------------------------------------------------------- /** * @override - * @return {Promise} rejected when update the list because we don't want - * anymore to select a cell who maybe doesn't exist anymore. + */ + renderButtons: function ($node) { + this._super.apply(this, arguments); + this.$buttons.on('click', '.o_button_apply_inventory', this._onApplyInventory.bind(this)); + this.$buttons.on('click', '.o_button_request_count_inventory', this._onRequestCountInventory.bind(this)); + this.$buttons.on('click', '.o_button_set_inventory', this._onSetInventory.bind(this)); + }, + + // ------------------------------------------------------------------------- + // Private + // ------------------------------------------------------------------------- + + /** + * @override + * @return {Promise} rejected when updating the list because we don't want + * to select a cell that might not exist anymore. */ _confirmSave: function (id) { var newRecord = this.model.localData[id]; @@ -51,8 +74,8 @@ var SingletonListController = InventoryReportListController.extend({ var similarRecords = findSimilarRecords(handle); if (similarRecords.length > 1) { - var notification = _t("You tried to create a record who already exists."+ - "
This last one has been modified instead."); + var notification = _t("You tried to create a record that already exists."+ + "
The existing record has been modified instead."); this.do_notify(_t("This record already exists."), notification); this.reload(); return Promise.reject(); @@ -61,6 +84,103 @@ var SingletonListController = InventoryReportListController.extend({ return this._super.apply(this, arguments); } }, + + /** + * Override to show/hide inventory adjustment specific buttons. + * Maybe a bit too hacky since it's not the intended purpose of this function, + * but for some reason `_updateControlPanel` isn't called during reload action + * after triggering any of these buttons. + * + * @override + */ + _renderHeaderButtons() { + this._super.apply(this, arguments); + + if (this.selectedRecords.length > 0 && !this.context.inventory_report_mode) { + $('.o_button_apply_inventory').removeClass('d-none'); + $('.o_button_request_count_inventory').removeClass('d-none'); + $('.o_button_set_inventory').removeClass('d-none'); + } else { + $('.o_button_apply_inventory').addClass('d-none'); + $('.o_button_request_count_inventory').addClass('d-none'); + $('.o_button_set_inventory').addClass('d-none'); + } + }, + + // ------------------------------------------------------------------------- + // Handlers + // ------------------------------------------------------------------------- + + _onApplyInventory: function (ev) { + var self = this; + var ids = this.getSelectedIds(); + if (!ids.length) { + var modified_records = this.initialState.data.filter(record => record.data.inventory_diff_quantity != 0) + ids = modified_records.map(record => record.data.id) + } + if (ids.length) { + return this._rpc({ + model: 'stock.quant', + method: 'action_apply_inventory', + args: [ids], + context: this.context, + }).then((result) => { + if (!result) { + return self.trigger_up('reload'); + } + return self.do_action(result, {on_close: () => this.reload(),}); + }) + } + }, + + /** + * + * @private + * @param {OdooEvent} ev + */ + _onButtonClicked: function (ev) { + ev.stopPropagation(); + var self = this; + return self.saveRecord(ev.data.record.id, { + stayInEdit: true, + }).then(function () { + // we need to re-get the record to make sure we have changes made + // by the basic model, such as the new res_id, if the record is + // new. + var record = self.model.get(ev.data.record.id); + return self._callButtonAction(ev.data.attrs, record); + }).then(function () { + self._enableButtons(); + }).guardedCatch(this._enableButtons.bind(this)); + }, + + _onRequestCountInventory: function (ev) { + var ids = this.getSelectedIds(); + if ( ids.length ) { + return this.do_action('stock.action_stock_request_count', + { + additional_context: { + default_quant_ids: ids, + }, + on_close: () => this.reload(), + }); + } + }, + + _onSetInventory: function (ev) { + var self = this; + var ids = this.getSelectedIds(); + if (ids.length) { + return this._rpc({ + model: 'stock.quant', + method: 'action_set_inventory_quantity', + args: [ids], + context: this.context, + }).then(() => { + return self.trigger_up('reload'); + }); + } + }, }); return SingletonListController; diff --git a/addons/stock/static/src/js/inventory_singleton_list_renderer.js b/addons/stock/static/src/js/inventory_singleton_list_renderer.js new file mode 100644 index 00000000000..b8eedd810da --- /dev/null +++ b/addons/stock/static/src/js/inventory_singleton_list_renderer.js @@ -0,0 +1,34 @@ +odoo.define('stock.SingletonListRenderer', function (require) { +"use strict"; + +var core = require('web.core'); +var ListRenderer = require('web.ListRenderer'); + +var _t = core._t; + +/** + * The purpose of this override is to disable line buttons when the row is edited. + * + */ + +var SingletonListRenderer = ListRenderer.extend({ + + _disableRecordSelectors: function () { + this._super.apply(this, arguments); + var row = this._getRow(this._getRecordID(this.currentRow))[0]; + var lineButtons = row.querySelectorAll('button[name=action_set_inventory_quantity]'); + lineButtons.forEach(elem => elem.setAttribute('disabled', true)); + }, + /** + * @private + */ + _enableRecordSelectors: function () { + this._super.apply(this, arguments); + var lineButtons = this.el.querySelectorAll('button.disabled'); + lineButtons.forEach(elem => elem.removeAttribute('disabled')); + }, +}); + +return SingletonListRenderer; + +}); diff --git a/addons/stock/static/src/js/inventory_singleton_list_view.js b/addons/stock/static/src/js/inventory_singleton_list_view.js index 53faf5b801e..4c4855d8f96 100644 --- a/addons/stock/static/src/js/inventory_singleton_list_view.js +++ b/addons/stock/static/src/js/inventory_singleton_list_view.js @@ -3,11 +3,13 @@ odoo.define('stock.SingletonListView', function (require) { var InventoryReportListView = require('stock.InventoryReportListView'); var SingletonListController = require('stock.SingletonListController'); +var SingletonListRenderer = require('stock.SingletonListRenderer'); var viewRegistry = require('web.view_registry'); var SingletonListView = InventoryReportListView.extend({ config: _.extend({}, InventoryReportListView.prototype.config, { Controller: SingletonListController, + Renderer: SingletonListRenderer, }), }); diff --git a/addons/stock/static/src/js/inventory_validate_button_controller.js b/addons/stock/static/src/js/inventory_validate_button_controller.js deleted file mode 100644 index cd554bd12e5..00000000000 --- a/addons/stock/static/src/js/inventory_validate_button_controller.js +++ /dev/null @@ -1,89 +0,0 @@ -odoo.define('stock.InventoryValidationController', function (require) { -"use strict"; - -var core = require('web.core'); -var ListController = require('web.ListController'); - -var _t = core._t; -var qweb = core.qweb; - -var InventoryValidationController = ListController.extend({ - events: _.extend({ - 'click .o_button_validate_inventory': '_onValidateInventory' - }, ListController.prototype.events), - /** - * @override - */ - init: function (parent, model, renderer, params) { - var context = renderer.state.getContext(); - this.inventory_id = context.active_id; - return this._super.apply(this, arguments); - }, - - // ------------------------------------------------------------------------- - // Public - // ------------------------------------------------------------------------- - - /** - * @override - */ - renderButtons: function () { - this._super.apply(this, arguments); - var $validationButton = $(qweb.render('InventoryLines.Buttons')); - this.$buttons.prepend($validationButton); - }, - - // ------------------------------------------------------------------------- - // Handlers - // ------------------------------------------------------------------------- - - /** - * Handler called when user click on validation button in inventory lines - * view. Makes an rpc to try to validate the inventory, then will go back on - * the inventory view form if it was validated. - * This method could also open a wizard in case something was missing. - * - * @private - */ - _onValidateInventory: function () { - var self = this; - var prom = Promise.resolve(); - var recordID = this.renderer.getEditableRecordID(); - if (recordID) { - // If user's editing a record, we wait to save it before to try to - // validate the inventory. - prom = this.saveRecord(recordID); - } - - prom.then(function () { - self._rpc({ - model: 'stock.inventory', - method: 'action_validate', - args: [self.inventory_id] - }).then(function (res) { - var exitCallback = function (infos) { - // In case we discarded a wizard, we do nothing to stay on - // the same view... - if (infos && infos.special) { - return; - } - // ... but in any other cases, we go back on the inventory form. - self.do_notify( - false, - _t("The inventory has been validated")); - self.trigger_up('history_back'); - }; - - if (_.isObject(res)) { - self.do_action(res, { on_close: exitCallback }); - } else { - return exitCallback(); - } - }); - }); - }, -}); - -return InventoryValidationController; - -}); diff --git a/addons/stock/static/src/js/inventory_validate_button_view.js b/addons/stock/static/src/js/inventory_validate_button_view.js deleted file mode 100644 index ed5a5f42335..00000000000 --- a/addons/stock/static/src/js/inventory_validate_button_view.js +++ /dev/null @@ -1,16 +0,0 @@ -odoo.define('stock.InventoryValidationView', function (require) { -"use strict"; - -var InventoryValidationController = require('stock.InventoryValidationController'); -var ListView = require('web.ListView'); -var viewRegistry = require('web.view_registry'); - -var InventoryValidationView = ListView.extend({ - config: _.extend({}, ListView.prototype.config, { - Controller: InventoryValidationController - }) -}); - -viewRegistry.add('inventory_validate_button', InventoryValidationView); - -}); diff --git a/addons/stock/static/src/xml/inventory_report.xml b/addons/stock/static/src/xml/inventory_report.xml index f33d72979ec..757d235ad18 100644 --- a/addons/stock/static/src/xml/inventory_report.xml +++ b/addons/stock/static/src/xml/inventory_report.xml @@ -1,8 +1,26 @@ - + + + + + + + + + + + + + diff --git a/addons/stock/tests/common.py b/addons/stock/tests/common.py index 9401283ea97..e853f6c3104 100644 --- a/addons/stock/tests/common.py +++ b/addons/stock/tests/common.py @@ -16,8 +16,6 @@ class TestStockCommon(common.TransactionCase): cls.StockQuantObj = cls.env['stock.quant'] cls.PickingObj = cls.env['stock.picking'] cls.MoveObj = cls.env['stock.move'] - cls.InvObj = cls.env['stock.inventory'] - cls.InvLineObj = cls.env['stock.inventory.line'] cls.LotObj = cls.env['stock.production.lot'] # Model Data diff --git a/addons/stock/tests/common2.py b/addons/stock/tests/common2.py index 0445e632398..d5c32f2a18b 100644 --- a/addons/stock/tests/common2.py +++ b/addons/stock/tests/common2.py @@ -53,5 +53,5 @@ class TestStockCommon(common.TestProductCommon): }) # Existing data - cls.existing_inventories = cls.env['stock.inventory'].search([]) + cls.existing_inventories = cls.env['stock.quant'].search([('inventory_quantity', '!=', 0.0)]) cls.existing_quants = cls.env['stock.quant'].search([]) diff --git a/addons/stock/tests/test_inventory.py b/addons/stock/tests/test_inventory.py index 4453f4555b0..814ce766660 100644 --- a/addons/stock/tests/test_inventory.py +++ b/addons/stock/tests/test_inventory.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from odoo.exceptions import ValidationError from odoo.tests.common import Form, TransactionCase @@ -37,48 +37,47 @@ class TestInventory(TransactionCase): self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 100.0) # remove them with an inventory adjustment - inventory = self.env['stock.inventory'].create({ - 'name': 'remove product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)], - }) - inventory.action_start() - self.assertEqual(len(inventory.line_ids), 1) - self.assertEqual(inventory.line_ids.theoretical_qty, 100) - inventory.line_ids.product_qty = 0 # Put the quantity back to 0 - inventory.action_validate() + inventory_quant = self.env['stock.quant'].search([ + ('location_id', '=', self.stock_location.id), + ('product_id', '=', self.product1.id), + ]) + + self.assertEqual(len(inventory_quant), 1) + self.assertEqual(inventory_quant.quantity, 100) + self.assertEqual(inventory_quant.inventory_quantity, 0) + + inventory_quant.action_apply_inventory() # check self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) self.assertEqual(sum(self.env['stock.quant']._gather(self.product1, self.stock_location).mapped('quantity')), 0.0) def test_inventory_2(self): - """ Check that adding a tracked product through an inventory adjustment work as expected. + """ Check that adding a tracked product through an inventory adjustment works as expected. """ - inventory = self.env['stock.inventory'].create({ - 'name': 'remove product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product2.id)] - }) - inventory.action_start() - self.assertEqual(len(inventory.line_ids), 0) + inventory_quant = self.env['stock.quant'].search([ + ('location_id', '=', self.stock_location.id), + ('product_id', '=', self.product2.id) + ]) + + self.assertEqual(len(inventory_quant), 0) lot1 = self.env['stock.production.lot'].create({ 'name': 'sn2', 'product_id': self.product2.id, 'company_id': self.env.company.id, }) - self.env['stock.inventory.line'].create({ - 'inventory_id': inventory.id, + inventory_quant = self.env['stock.quant'].create({ 'location_id': self.stock_location.id, 'product_id': self.product2.id, - 'prod_lot_id': lot1.id, - 'product_qty': 1 + 'lot_id': lot1.id, + 'inventory_quantity': 1 }) - self.assertEqual(len(inventory.line_ids), 1) - self.assertEqual(inventory.line_ids.theoretical_qty, 0) - inventory.action_validate() + self.assertEqual(inventory_quant.quantity, 0) + self.assertEqual(inventory_quant.inventory_diff_quantity, 1) + + inventory_quant.action_apply_inventory() # check self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot1), 1.0) @@ -86,72 +85,74 @@ class TestInventory(TransactionCase): 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 + """ Check that it's not possible to have multiple products with the same serial number through an inventory adjustment """ - inventory = self.env['stock.inventory'].create({ - 'name': 'remove product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product2.id)] - }) - inventory.action_start() - self.assertEqual(len(inventory.line_ids), 0) + inventory_quant = self.env['stock.quant'].search([ + ('location_id', '=', self.stock_location.id), + ('product_id', '=', self.product2.id) + ]) + self.assertEqual(len(inventory_quant), 0) lot1 = self.env['stock.production.lot'].create({ 'name': 'sn2', 'product_id': self.product2.id, 'company_id': self.env.company.id, }) - self.env['stock.inventory.line'].create({ - 'inventory_id': inventory.id, + inventory_quant = self.env['stock.quant'].create({ 'location_id': self.stock_location.id, 'product_id': self.product2.id, - 'prod_lot_id': lot1.id, - 'product_qty': 2 + 'lot_id': lot1.id, + 'inventory_quantity': 2 }) - self.assertEqual(len(inventory.line_ids), 1) - self.assertEqual(inventory.line_ids.theoretical_qty, 0) + + self.assertEqual(len(inventory_quant), 1) + self.assertEqual(inventory_quant.quantity, 0) with self.assertRaises(ValidationError): - inventory.action_validate() + inventory_quant.action_apply_inventory() def test_inventory_4(self): - """ Check that even if a product is tracked by serial number, it's possible to add + """ Check that even if a product is tracked by serial number, it's possible to add an untracked one in an inventory adjustment. """ - inventory = self.env['stock.inventory'].create({ - 'name': 'remove product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product2.id)] - }) - inventory.action_start() - self.assertEqual(len(inventory.line_ids), 0) + quant_domain = [ + ('location_id', '=', self.stock_location.id), + ('product_id', '=', self.product2.id) + ] + inventory_quants = self.env['stock.quant'].search(quant_domain) + self.assertEqual(len(inventory_quants), 0) lot1 = self.env['stock.production.lot'].create({ 'name': 'sn2', 'product_id': self.product2.id, 'company_id': self.env.company.id, }) - self.env['stock.inventory.line'].create({ - 'inventory_id': inventory.id, - 'product_id': self.product2.id, - 'prod_lot_id': lot1.id, - 'product_qty': 1, + self.env['stock.quant'].create({ 'location_id': self.stock_location.id, + 'product_id': self.product2.id, + 'lot_id': lot1.id, + 'inventory_quantity': 1 }) - self.assertEqual(len(inventory.line_ids), 1) - self.assertEqual(inventory.line_ids.theoretical_qty, 0) - self.env['stock.inventory.line'].create({ - 'inventory_id': inventory.id, - 'product_id': self.product2.id, - 'product_uom_id': self.uom_unit.id, - 'product_qty': 10, + inventory_quants = self.env['stock.quant'].search(quant_domain) + self.assertEqual(len(inventory_quants), 1) + self.assertEqual(inventory_quants.quantity, 0) + + self.env['stock.quant'].create({ 'location_id': self.stock_location.id, + 'product_id': self.product2.id, + 'inventory_quantity': 10 }) - self.assertEqual(len(inventory.line_ids), 2) - res_dict_for_warning_lot = inventory.action_validate() - wizard_warning_lot = self.env[(res_dict_for_warning_lot.get('res_model'))].browse(res_dict_for_warning_lot.get('res_id')) - wizard_warning_lot.action_confirm() + inventory_quants = self.env['stock.quant'].search(quant_domain) + self.assertEqual(len(inventory_quants), 2) + stock_confirmation_action = inventory_quants.action_apply_inventory() + stock_confirmation_wizard_form = Form( + self.env['stock.track.confirmation'].with_context( + **stock_confirmation_action['context']) + ) + + stock_confirmation_wizard = stock_confirmation_wizard_form.save() + stock_confirmation_wizard.action_confirm() # check self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot1, strict=True), 1.0) @@ -162,28 +163,19 @@ class TestInventory(TransactionCase): self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location)), 2.0) def test_inventory_5(self): - """ Check that assigning an owner does work. + """ Check that assigning an owner works. """ owner1 = self.env['res.partner'].create({'name': 'test_inventory_5'}) - inventory = self.env['stock.inventory'].create({ - 'name': 'remove product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)] - }) - inventory.action_start() - self.assertEqual(len(inventory.line_ids), 0) - - self.env['stock.inventory.line'].create({ - 'inventory_id': inventory.id, - 'product_id': self.product1.id, - 'partner_id': owner1.id, - 'product_qty': 5, + inventory_quant = self.env['stock.quant'].create({ 'location_id': self.stock_location.id, + 'product_id': self.product1.id, + 'inventory_quantity': 5, + 'owner_id': owner1.id, }) - self.assertEqual(len(inventory.line_ids), 1) - self.assertEqual(inventory.line_ids.theoretical_qty, 0) - inventory.action_validate() + + self.assertEqual(inventory_quant.quantity, 0) + inventory_quant.action_apply_inventory() quant = self.env['stock.quant']._gather(self.product1, self.stock_location) self.assertEqual(len(quant), 1) @@ -192,23 +184,16 @@ class TestInventory(TransactionCase): def test_inventory_6(self): """ Test that for chained moves, making an inventory adjustment to reduce a quantity that - has been reserved correctly free the reservation. After that, add products in stock and check + has been reserved correctly frees the reservation. After that, add products to stock and check that they're used if the user encodes more than what's available through the chain """ - # add 10 products in stock - inventory = self.env['stock.inventory'].create({ - 'name': 'add 10 products 1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)] - }) - inventory.action_start() - self.env['stock.inventory.line'].create({ - 'inventory_id': inventory.id, + # add 10 products to stock + inventory_quant = self.env['stock.quant'].create({ + 'location_id': self.stock_location.id, 'product_id': self.product1.id, - 'product_qty': 10, - 'location_id': self.stock_location.id + 'inventory_quantity': 10, }) - inventory.action_validate() + inventory_quant.action_apply_inventory() self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 10.0) # Make a chain of two moves, validate the first and check that 10 products are reserved @@ -241,16 +226,14 @@ class TestInventory(TransactionCase): self.assertEqual(self.env['stock.quant']._gather(self.product1, self.pack_location).quantity, 10.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.pack_location), 0.0) - # Make and inventory adjustment and remove two products from the pack location. This should + # Make an inventory adjustment and remove two products from the pack location. This should # free the reservation of the second move. - inventory = self.env['stock.inventory'].create({ - 'name': 'remove 2 products 1', - 'location_ids': [(4, self.pack_location.id)], - 'product_ids': [(4, self.product1.id)], - }) - inventory.action_start() - inventory.line_ids.product_qty = 8 - inventory.action_validate() + inventory_quant = self.env['stock.quant'].search([ + ('location_id', '=', self.pack_location.id), + ('product_id', '=', self.product1.id) + ]) + inventory_quant.inventory_quantity = 8 + inventory_quant.action_apply_inventory() self.assertEqual(self.env['stock.quant']._gather(self.product1, self.pack_location).quantity, 8.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.pack_location), 0) self.assertEqual(move_pack_cust.state, 'partially_available') @@ -262,15 +245,13 @@ class TestInventory(TransactionCase): self.assertEqual(move_pack_cust.state, 'partially_available') self.assertEqual(move_pack_cust.reserved_availability, 8) - # Make a new inventory adjustment and bring two now products. - inventory = self.env['stock.inventory'].create({ - 'name': 'remove 2 products 1', - 'location_ids': [(4, self.pack_location.id)], - 'product_ids': [(4, self.product1.id)], - }) - inventory.action_start() - inventory.line_ids.product_qty = 10 - inventory.action_validate() + # Make a new inventory adjustment and add two new products. + inventory_quant = self.env['stock.quant'].search([ + ('location_id', '=', self.pack_location.id), + ('product_id', '=', self.product1.id) + ]) + inventory_quant.inventory_quantity = 10 + inventory_quant.action_apply_inventory() self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.pack_location), 2) @@ -278,9 +259,9 @@ class TestInventory(TransactionCase): self.assertEqual(move_pack_cust.state, 'partially_available') self.assertEqual(move_pack_cust.reserved_availability, 8) - # Running _action_assign will now find the new available quantity. Indeed, as the products - # are not discernabl (not lot/pack/owner), even if the new available quantity is not directly - # brought by the chain, the system fill take them into account. + # Running _action_assign will now find the new available quantity. Since the products + # are not differentiated (no lot/pack/owner), even if the new available quantity is not directly + # brought by the chain, the system will take them into account. move_pack_cust._action_assign() self.assertEqual(move_pack_cust.state, 'assigned') @@ -300,192 +281,87 @@ class TestInventory(TransactionCase): 'owner_id': owner1.id, 'location_id': self.stock_location.id, 'quantity': 1, - 'reserved_quantity': 0, } self.env['stock.quant'].create(vals) - self.env['stock.quant'].create(vals) + self.env['stock.quant'].create(dict(**vals, inventory_quantity=1)) self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 2.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 2.0) + self.env['stock.quant']._quant_tasks() + inventory_quant = self.env['stock.quant'].search([ + ('location_id', '=', self.stock_location.id), + ('product_id', '=', self.product1.id) + ]) + self.assertEqual(len(inventory_quant), 1) + self.assertEqual(inventory_quant.inventory_quantity, 1) + self.assertEqual(inventory_quant.quantity, 2) - inventory = self.env['stock.inventory'].create({ - 'name': 'product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)], - }) - inventory.action_start() - self.assertEqual(len(inventory.line_ids), 1) - self.assertEqual(inventory.line_ids.theoretical_qty, 2) - - def test_inventory_8(self): - """ Check inventory lines product quantity is 0 when inventory is - started with `prefill_counted_quantity` disable. - """ - self.env['stock.quant'].create({ - 'product_id': self.product1.id, - 'product_uom_id': self.uom_unit.id, - 'location_id': self.stock_location.id, - 'quantity': 7, - 'reserved_quantity': 0, - }) - inventory_form = Form(self.env['stock.inventory'].with_context( - default_prefill_counted_quantity='zero', - ), view='stock.view_inventory_form') - inventory = inventory_form.save() - inventory.action_start() - self.assertNotEqual(len(inventory.line_ids), 0) - # Checks all inventory lines quantities are correctly set. - for line in inventory.line_ids: - self.assertEqual(line.product_qty, 0) - self.assertNotEqual(line.theoretical_qty, 0) - - def test_inventory_9_cancel_then_start(self): - """ Checks when we cancel an inventory, then change its locations and/or - products setup and restart it, it will remove all its lines and restart - like a new inventory. - """ - # Creates some records needed for the test... - product2 = self.env['product.product'].create({ - 'name': 'Product B', - 'type': 'product', - 'categ_id': self.env.ref('product.product_category_all').id, - }) - loc1 = self.env['stock.location'].create({ - 'name': 'SafeRoom A', - 'usage': 'internal', - 'location_id': self.stock_location.id, - }) - # Adds some quants. - self.env['stock.quant'].create({ - 'product_id': self.product1.id, - 'product_uom_id': self.uom_unit.id, - 'location_id': loc1.id, - 'quantity': 7, - 'reserved_quantity': 0, - }) - self.env['stock.quant'].create({ - 'product_id': self.product1.id, - 'product_uom_id': self.uom_unit.id, - 'location_id': self.stock_location.id, - 'quantity': 7, - 'reserved_quantity': 0, - }) - self.env['stock.quant'].create({ - 'product_id': product2.id, - 'product_uom_id': self.uom_unit.id, - 'location_id': loc1.id, - 'quantity': 7, - 'reserved_quantity': 0, - }) - self.env['stock.quant'].create({ - 'product_id': product2.id, - 'product_uom_id': self.uom_unit.id, - 'location_id': self.stock_location.id, - 'quantity': 7, - 'reserved_quantity': 0, - }) - # Creates the inventory and configures if for product1 - inventory_form = Form(self.env['stock.inventory'], view='stock.view_inventory_form') - inventory_form.product_ids.add(self.product1) - inventory = inventory_form.save() - inventory.action_start() - # Must have two inventory lines about product1. - self.assertEqual(len(inventory.line_ids), 2) - for line in inventory.line_ids: - self.assertEqual(line.product_id.id, self.product1.id) - - # Cancels the inventory and changes for product2 in its setup. - inventory.action_cancel_draft() - inventory_form = Form(inventory) - inventory_form.product_ids.remove(self.product1.id) - inventory_form.product_ids.add(product2) - inventory = inventory_form.save() - inventory.action_start() - # Must have two inventory lines about product2. - self.assertEqual(len(inventory.line_ids), 2) - self.assertEqual(inventory.line_ids.product_id.id, product2.id) - - def test_inventory_prefill_counted_quantity(self): - """ Checks that inventory lines have a `product_qty` set on zero or - equals to quantity on hand, depending of the `prefill_counted_quantity`. + def test_inventory_counted_quantity(self): + """ Checks that inventory quants have a `inventory quantity` set to zero + after an adjustment. """ # Set product quantity to 42. - vals = { + inventory_quant = self.env['stock.quant'].create(vals={ 'product_id': self.product1.id, 'location_id': self.stock_location.id, - 'quantity': 42, - } - self.env['stock.quant'].create(vals) - # Generate new inventory, its line must have a theoretical - # quantity to 42 and a counted quantity to 42. - inventory = self.env['stock.inventory'].create({ - 'name': 'Default Qty', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)], - 'prefill_counted_quantity': 'counted', + 'inventory_quantity': 42, }) - inventory.action_start() - self.assertEqual(len(inventory.line_ids), 1) - self.assertEqual(inventory.line_ids.theoretical_qty, 42) - self.assertEqual(inventory.line_ids.product_qty, 42) - # Generate new inventory, its line must have a theoretical - # quantity to 42 and a counted quantity to 0. - inventory = self.env['stock.inventory'].create({ - 'name': 'Default Qty', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)], - 'prefill_counted_quantity': 'zero', - }) - inventory.action_start() - self.assertEqual(len(inventory.line_ids), 1) - self.assertEqual(inventory.line_ids.theoretical_qty, 42) - self.assertEqual(inventory.line_ids.product_qty, 0) + # quantity of 42 and a counted quantity to 0. + inventory_quant.action_apply_inventory() + self.assertEqual(len(inventory_quant), 1) + self.assertEqual(inventory_quant.inventory_quantity, 0) + self.assertEqual(inventory_quant.quantity, 42) def test_inventory_outdate_1(self): - """ Checks that inventory adjustment line is marked as outdated after - its corresponding quant is modify and its value was correctly updated - after user refreshed it. + """ Checks that applying an inventory adjustment that is outdated due to + its corresponding quant being modified after its inventory quantity is set + opens a wizard. The wizard should warn about the conflict and its value should be + corrected after user confirms the inventory quantity. """ # Set initial quantity to 7 - vals = { - 'product_id': self.product1.id, - 'product_uom_id': self.uom_unit.id, - 'location_id': self.stock_location.id, - 'quantity': 7, - 'reserved_quantity': 0, - } - self.env['stock.quant'].create(vals) + self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 7) + inventory_quant = self.env['stock.quant'].search([ + ('location_id', '=', self.stock_location.id), + ('product_id', '=', self.product1.id) + ]) + # When a quant is created, it must not be marked as outdated + # and its `inventory_quantity` must be equal to zero. + self.assertEqual(inventory_quant.inventory_quantity, 0) - inventory = self.env['stock.inventory'].create({ - 'name': 'product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)], + inventory_quant.inventory_quantity = 5 + self.assertEqual(inventory_quant.inventory_diff_quantity, -2) + + # Deliver 3 units + move_out = self.env['stock.move'].create({ + 'name': 'Outgoing move of 3 units', + '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, }) - inventory.action_start() - # When a inventory line is created, it must not be marked as outdated - # and its `theoretical_qty` must be equals to quant quantity. - self.assertEqual(inventory.line_ids.outdated, False) - self.assertEqual(inventory.line_ids.theoretical_qty, 7) + move_out._action_confirm() + move_out._action_assign() + move_out.move_line_ids.qty_done = 3 + move_out._action_done() - # Creates a new quant who'll update the existing one and so set product - # quantity to 5. Then expects inventory line has been marked as outdated. - vals = { - 'product_id': self.product1.id, - 'location_id': self.stock_location.id, - 'inventory_quantity': 5, - } - self.env['stock.quant'].with_context(inventory_mode=True).create(vals) - self.assertEqual(inventory.line_ids.outdated, True) - self.assertEqual(inventory.line_ids.theoretical_qty, 7) - # Refreshes inventory line and expects quantity was recomputed to 5 - inventory.line_ids.action_refresh_quantity() - self.assertEqual(inventory.line_ids.outdated, False) - self.assertEqual(inventory.line_ids.theoretical_qty, 5) + # Ensure that diff didn't change. + self.assertEqual(inventory_quant.inventory_diff_quantity, -2) + self.assertEqual(inventory_quant.inventory_quantity, 5) + self.assertEqual(inventory_quant.quantity, 4) + + conflict_wizard_values = inventory_quant.action_apply_inventory() + conflict_wizard_form = Form(self.env['stock.inventory.conflict'].with_context(conflict_wizard_values['context'])) + conflict_wizard = conflict_wizard_form.save() + conflict_wizard.quant_to_fix_ids.inventory_quantity = 5 + conflict_wizard.action_validate() + self.assertEqual(inventory_quant.inventory_diff_quantity, 0) + self.assertEqual(inventory_quant.inventory_quantity, 0) + self.assertEqual(inventory_quant.quantity, 5) def test_inventory_outdate_2(self): - """ Checks that inventory adjustment line is marked as outdated when a - quant is manually updated and its value is correctly updated when action - to refresh is called. + """ Checks that an outdated inventory adjustment auto-corrects when + changing its inventory quantity after its corresponding quant has been modified. """ # Set initial quantity to 7 vals = { @@ -493,30 +369,33 @@ class TestInventory(TransactionCase): 'product_uom_id': self.uom_unit.id, 'location_id': self.stock_location.id, 'quantity': 7, - 'reserved_quantity': 0, + 'inventory_quantity': 7 } quant = self.env['stock.quant'].create(vals) - inventory = self.env['stock.inventory'].create({ - 'name': 'product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)], + # Decrease quant to 3 and inventory line is now outdated + move_out = self.env['stock.move'].create({ + 'name': 'Outgoing move of 3 units', + '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': 4.0, }) - inventory.action_start() - self.assertEqual(inventory.line_ids.outdated, False) - self.assertEqual(inventory.line_ids.theoretical_qty, 7) + move_out._action_confirm() + move_out._action_assign() + move_out.move_line_ids.qty_done = 4 + move_out._action_done() - # Decreases quant to 3 and expects inventory line is now outdated - quant.with_context(inventory_mode=True).write({'inventory_quantity': 3}) - self.assertEqual(inventory.line_ids.outdated, True) - self.assertEqual(inventory.line_ids.theoretical_qty, 7) - # Refreshes inventory line and expects quantity was recomputed to 3 - inventory.line_ids.action_refresh_quantity() - self.assertEqual(inventory.line_ids.outdated, False) - self.assertEqual(inventory.line_ids.theoretical_qty, 3) + self.assertEqual(quant.inventory_quantity, 7) + self.assertEqual(quant.inventory_diff_quantity, 0) + # Refresh inventory line and quantity will recompute to 3 + quant.inventory_quantity = 3 + self.assertEqual(quant.inventory_quantity, 3) + self.assertEqual(quant.inventory_diff_quantity, 0) def test_inventory_outdate_3(self): - """ Checks that outdated inventory adjustment line without difference + """ Checks that an inventory adjustment line without a difference doesn't change quant when validated. """ # Set initial quantity to 10 @@ -525,121 +404,26 @@ class TestInventory(TransactionCase): 'product_uom_id': self.uom_unit.id, 'location_id': self.stock_location.id, 'quantity': 10, - 'reserved_quantity': 0, } quant = self.env['stock.quant'].create(vals) - inventory = self.env['stock.inventory'].create({ - 'name': 'product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)], - }) - inventory.action_start() - self.assertEqual(inventory.line_ids.outdated, False) - self.assertEqual(inventory.line_ids.theoretical_qty, 10) - - # increases quant to 15 and expects inventory line is now outdated - quant.with_context(inventory_mode=True).write({'inventory_quantity': 15}) - self.assertEqual(inventory.line_ids.outdated, True) - # Don't refresh inventory line but valid it, and expect quantity is - # still equal to 15 - inventory.action_validate() - self.assertEqual(inventory.line_ids.theoretical_qty, 10) - self.assertEqual(quant.quantity, 15) - - def test_inventory_outdate_4(self): - """ Checks that outdated inventory adjustment line with difference - changes quant when validated. - """ - # Set initial quantity to 10 - vals = { - 'product_id': self.product1.id, - 'product_uom_id': self.uom_unit.id, - 'location_id': self.stock_location.id, - 'quantity': 10, - 'reserved_quantity': 0, - } - quant = self.env['stock.quant'].create(vals) - - inventory = self.env['stock.inventory'].create({ - 'name': 'product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)], - }) - inventory.action_start() - self.assertEqual(inventory.line_ids.outdated, False) - self.assertEqual(inventory.line_ids.theoretical_qty, 10) - - # increases quant to 15 and expects inventory line is now outdated - quant.with_context(inventory_mode=True).write({'inventory_quantity': 15}) - self.assertEqual(inventory.line_ids.outdated, True) - # Don't refresh inventory line but changes its value and valid it, and - # expects quantity is correctly adapted (15 + inventory line diff) - inventory.line_ids.product_qty = 12 - inventory.action_validate() - self.assertEqual(inventory.line_ids.theoretical_qty, 10) - self.assertEqual(quant.quantity, 17) - - def test_inventory_outdate_5(self): - """ Checks that inventory adjustment line is marked as outdated when an - another inventory adjustment line with common product/location is - validated and its value is updated when action to refresh is called. - """ - # Set initial quantity to 7 - vals = { - 'product_id': self.product1.id, - 'product_uom_id': self.uom_unit.id, - 'location_id': self.stock_location.id, - 'quantity': 7, - 'reserved_quantity': 0, - } - self.env['stock.quant'].create(vals) - - inventory_1 = self.env['stock.inventory'].create({ - 'name': 'product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)], - }) - inventory_1.action_start() - inventory_2 = self.env['stock.inventory'].create({ - 'name': 'product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)], - }) - inventory_2.action_start() - self.assertEqual(inventory_1.line_ids.outdated, False) - self.assertEqual(inventory_1.line_ids.theoretical_qty, inventory_2.line_ids.theoretical_qty) - - # Set product quantity to 8 in inventory 2 then validates it - inventory_2.line_ids.product_qty = 8 - inventory_2.action_validate() - # Expects line of inventory 1 is now marked as outdated - self.assertEqual(inventory_1.line_ids.outdated, True) - self.assertEqual(inventory_1.line_ids.theoretical_qty, 7) - # Refreshes inventory line and expects quantity was recomputed to 8 - inventory_1.line_ids.action_refresh_quantity() - self.assertEqual(inventory_1.line_ids.theoretical_qty, 8) + quant.inventory_quantity = 10 + quant.action_apply_inventory() + self.assertEqual(quant.quantity, 10) + self.assertEqual(quant.inventory_quantity, 0) def test_inventory_dont_outdate_1(self): """ Checks that inventory adjustment line isn't marked as outdated when - a not corresponding quant is created. + a non-corresponding quant is created. """ # Set initial quantity to 7 and create inventory adjustment for product1 - vals = { + inventory_quant = self.env['stock.quant'].create({ 'product_id': self.product1.id, 'product_uom_id': self.uom_unit.id, 'location_id': self.stock_location.id, 'quantity': 7, - 'reserved_quantity': 0, - } - self.env['stock.quant'].create(vals) - inventory = self.env['stock.inventory'].create({ - 'name': 'product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)], + 'inventory_quantity': 5 }) - inventory.action_start() - self.assertEqual(inventory.line_ids.outdated, False) # Create quant for product3 product3 = self.env['product.product'].create({ @@ -647,167 +431,20 @@ class TestInventory(TransactionCase): 'type': 'product', 'categ_id': self.env.ref('product.product_category_all').id, }) - vals = { + self.env['stock.quant'].create({ 'product_id': product3.id, 'product_uom_id': self.uom_unit.id, 'location_id': self.stock_location.id, 'inventory_quantity': 22, 'reserved_quantity': 0, - } - self.env['stock.quant'].create(vals) - # Expect inventory line is still up to date - self.assertEqual(inventory.line_ids.outdated, False) - - def test_inventory_dont_outdate_2(self): - """ Checks that inventory adjustment line isn't marked as outdated when - an another inventory adjustment line without common product/location is - validated. - """ - # Set initial quantity for product1 and product3 - self.env['stock.quant'].create({ - 'product_id': self.product1.id, - 'product_uom_id': self.uom_unit.id, - 'location_id': self.stock_location.id, - 'quantity': 7, - 'reserved_quantity': 0, }) - product3 = self.env['product.product'].create({ - 'name': 'Product C', - 'type': 'product', - 'categ_id': self.env.ref('product.product_category_all').id, - }) - self.env['stock.quant'].create({ - 'product_id': product3.id, - 'product_uom_id': self.uom_unit.id, - 'location_id': self.stock_location.id, - 'quantity': 10, - 'reserved_quantity': 0, - }) - - inventory_1 = self.env['stock.inventory'].create({ - 'name': 'product1', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)], - }) - inventory_1.action_start() - inventory_2 = self.env['stock.inventory'].create({ - 'name': 'product3', - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, product3.id)], - }) - inventory_2.action_start() - self.assertEqual(inventory_1.line_ids.outdated, False) - - # Set product3 quantity to 16 in inventory 2 then validates it - inventory_2.line_ids.product_qty = 16 - inventory_2.action_validate() - # Expect line of inventory 1 is still up to date - self.assertEqual(inventory_1.line_ids.outdated, False) - - def test_inventory_include_exhausted_product(self): - """ Checks that exhausted product (quant not set or == 0) is added - to inventory line - (only for location_ids selected or, if not set, for each main location - (linked directly to the warehouse) of the current company) - when the option is active """ - - # location_ids SET + product_ids SET - inventory = self.env['stock.inventory'].create({ - 'name': 'loc SET - pro SET', - 'exhausted': True, - 'location_ids': [(4, self.stock_location.id)], - 'product_ids': [(4, self.product1.id)], - }) - inventory.action_start() - - self.assertEqual(len(inventory.line_ids), 1) - self.assertEqual(inventory.line_ids.product_id.id, self.product1.id) - self.assertEqual(inventory.line_ids.theoretical_qty, 0) - self.assertEqual(inventory.line_ids.location_id.id, self.stock_location.id) - - # location_ids SET + product_ids UNSET - inventory = self.env['stock.inventory'].create({ - 'name': 'loc SET - pro UNSET', - 'exhausted': True, - 'location_ids': [(4, self.stock_location.id)] - }) - inventory.action_start() - line_ids_p1 = [l for l in inventory.line_ids if l['product_id']['id'] == self.product1.id] - line_ids_p2 = [l for l in inventory.line_ids if l['product_id']['id'] == self.product2.id] - self.assertEqual(len(line_ids_p1), 1) - self.assertEqual(len(line_ids_p2), 1) - self.assertEqual(line_ids_p1[0].location_id.id, self.stock_location.id) - self.assertEqual(line_ids_p2[0].location_id.id, self.stock_location.id) - - # location_ids UNSET + product_ids SET - warehouse = self.env['stock.warehouse'].create({ - 'name': 'Warhouse', - 'code': 'WAR' - }) - child_loc = self.env['stock.location'].create({ - 'name': "ChildLOC", - 'usage': 'internal', - 'location_id': warehouse.lot_stock_id.id - }) - - inventory = self.env['stock.inventory'].create({ - 'name': 'loc UNSET - pro SET', - 'exhausted': True, - 'product_ids': [(4, self.product1.id)], - }) - inventory.action_start() - - line_ids = [l for l in inventory.line_ids if l['location_id']['id'] == warehouse.lot_stock_id.id] - self.assertEqual(len(line_ids), 1) - self.assertEqual(line_ids[0].theoretical_qty, 0) - self.assertEqual(line_ids[0].product_id.id, self.product1.id) - - # Only the main location have a exhausted line - line_ids = [l for l in inventory.line_ids if l['location_id']['id'] == child_loc.id] - self.assertEqual(len(line_ids), 0) - - # location_ids UNSET + product_ids UNSET - inventory = self.env['stock.inventory'].create({ - 'name': 'loc UNSET - pro UNSET', - 'exhausted': True - }) - inventory.action_start() - - # Product1 & Product2 line with warehouse location - line_ids_p1 = [l for l in inventory.line_ids if l['product_id']['id'] == self.product1.id and l['location_id']['id'] == warehouse.lot_stock_id.id] - line_ids_p2 = [l for l in inventory.line_ids if l['product_id']['id'] == self.product2.id and l['location_id']['id'] == warehouse.lot_stock_id.id] - self.assertEqual(len(line_ids_p1), 1) - self.assertEqual(len(line_ids_p2), 1) - self.assertEqual(line_ids_p1[0].theoretical_qty, 0) - self.assertEqual(line_ids_p2[0].theoretical_qty, 0) - - # location_ids SET + product_ids SET but when product in one locations but no the other - self.env['stock.quant'].create({ - 'product_id': self.product1.id, - 'product_uom_id': self.uom_unit.id, - 'location_id': self.stock_location.id, - 'quantity': 10, - 'reserved_quantity': 0, - }) - inventory = self.env['stock.inventory'].create({ - 'name': 'loc SET - pro SET', - 'exhausted': True, - 'location_ids': [(4, self.stock_location.id), (4, warehouse.lot_stock_id.id)], - 'product_ids': [(4, self.product1.id)], - }) - inventory.action_start() - - # need to have line for product1 for both location, one with quant the other not - line_ids_loc1 = [l for l in inventory.line_ids if l['location_id']['id'] == self.stock_location.id] - line_ids_loc2 = [l for l in inventory.line_ids if l['location_id']['id'] == warehouse.lot_stock_id.id] - self.assertEqual(len(line_ids_loc1), 1) - self.assertEqual(len(line_ids_loc2), 1) - self.assertEqual(line_ids_loc1[0].theoretical_qty, 10) - self.assertEqual(line_ids_loc2[0].theoretical_qty, 0) + inventory_quant.action_apply_inventory() + # Expect action apply do not return a wizard + self.assertEqual(inventory_quant.quantity, 5) def test_cyclic_inventory(self): - """ Check that a location with a cyclic inventory set has its inventory - properly auto-generate and correctly record relevant dates. + """ Check that locations with and without cyclic inventory set has its inventory + dates auto-generate and apply relevant dates. """ grp_multi_loc = self.env.ref('stock.group_stock_multi_locations') self.env.user.write({'groups_id': [(4, grp_multi_loc.id)]}) @@ -827,6 +464,12 @@ class TestInventory(TransactionCase): 'last_inventory_date': now - timedelta(days=5), }) + no_cyclic_loc = self.env['stock.location'].create({ + 'name': 'No Cyclic Inv Location', + 'usage': 'internal', + 'location_id': self.stock_location.id, + }) + no_cyclic_loc.company_id.write({'annual_inventory_day': str(today.day), 'annual_inventory_month': str(today.month)}) new_loc_form = Form(new_loc) new_loc_form.cyclic_inventory_frequency = 2 new_loc = new_loc_form.save() @@ -840,149 +483,28 @@ class TestInventory(TransactionCase): self.assertEqual(new_loc.next_inventory_date, today + timedelta(days=2)) # previous inventory done + cyclic_inventory_frequency < today => next_inventory_date = tomorrow self.assertEqual(existing_loc2.next_inventory_date, today + timedelta(days=1)) - # check that cyclic inventories are correctly autogenerated - new_loc.next_inventory_date = today - timedelta(days=2) - Inventory = self.env['stock.inventory'] - Inventory._run_inventory_tasks() - cyclic_inventories = Inventory.search([('location_ids', 'in', [new_loc.id, existing_loc2.id])]) - # only new_loc should have triggered an inventory - self.assertEqual(len(cyclic_inventories), 1) - cyclic_inventories._action_done() - self.assertEqual(new_loc.next_inventory_date, today + timedelta(days=2)) - self.assertEqual(new_loc.last_inventory_date.date(), today) + self.env['stock.quant']._update_available_quantity(self.product1, new_loc, 5) + self.env['stock.quant']._update_available_quantity(self.product1, existing_loc2, 5) + self.env['stock.quant']._update_available_quantity(self.product1, no_cyclic_loc, 5) + # cyclic inventory locations should auto-assign their next inventory date to their quants + quant_new_loc = self.env['stock.quant'].search([('location_id', '=', new_loc.id)]) + quant_existing_loc = self.env['stock.quant'].search([('location_id', '=', existing_loc2.id)]) + self.assertEqual(quant_new_loc.inventory_date, new_loc.next_inventory_date) + self.assertEqual(quant_existing_loc.inventory_date, existing_loc2.next_inventory_date) + # quant without a cyclic inventory location should default to the company's annual inventory date + quant_non_cyclic_loc = self.env['stock.quant'].search([('location_id', '=', no_cyclic_loc.id)]) + self.assertEqual(quant_non_cyclic_loc.inventory_date.month, int(no_cyclic_loc.company_id.annual_inventory_month)) + self.assertEqual(quant_non_cyclic_loc.inventory_date.day, no_cyclic_loc.company_id.annual_inventory_day) - def test_neg_qty_conflict_inventory(self): - """ Check that auto-created negative quantity stock inventories work correctly.""" - - Inventory = self.env['stock.inventory'] - # check that neg qty will generate inventory - self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, -1) - Inventory._run_conflict_inventory_tasks() - neg_qty_inventories = Inventory.search([('is_conflict_inventory', '=', True)]) - self.assertEqual(len(neg_qty_inventories), 1) - self.assertEqual(neg_qty_inventories.product_ids[0], self.product1) - - # check that inventory auto-updates correctly when still in draft - self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, -1) - self.env['stock.quant']._update_available_quantity(self.product2, self.pack_location, -1) - Inventory._run_conflict_inventory_tasks() - self.assertEqual(len(neg_qty_inventories.product_ids), 2, "New negative quantity product should be auto-added to existing draft inventory") - self.env['stock.quant']._update_available_quantity(self.product2, self.pack_location, 1) - Inventory._run_conflict_inventory_tasks() - self.assertEqual(len(neg_qty_inventories.product_ids), 2, "Only remove 1 of 2 neg quants shouldn't remove product from inventory") - self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1) - Inventory._run_conflict_inventory_tasks() - self.assertEqual(len(neg_qty_inventories.product_ids), 1, "Previously neg quantity product should be auto-removed from existing draft inventory") - - # check that inventory does not auto-update/auto-generate when neg qty inventory is in progress - neg_qty_inventories.action_start() - self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, -1) - Inventory._run_conflict_inventory_tasks() - neg_qty_inventories = Inventory.search([('is_conflict_inventory', '=', True)]) - self.assertEqual(len(neg_qty_inventories), 1) - self.assertEqual(len(neg_qty_inventories.product_ids), 1) - - # check that inventory auto-deletes when no more neg qtys - neg_qty_inventories.action_cancel_draft() - self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 1) - self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1) - Inventory._run_conflict_inventory_tasks() - neg_qty_inventories = Inventory.search([('is_conflict_inventory', '=', True)]) - self.assertEqual(len(neg_qty_inventories), 0) - - def test_dupe_sn_conflict_inventory(self): - """ Check that auto-created duplicated serial number inventories work correctly. - """ - lot1 = self.env['stock.production.lot'].create({ - 'name': 'dupe_sn1', - 'product_id': self.product2.id, - 'company_id': self.env.company.id, - }) - lot2 = lot1.copy(default={'name': 'dupe_sn2'}) - - Inventory = self.env['stock.inventory'] - # check that duplicted SN will auto-generate inventory - self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1, lot_id=lot1) - self.env['stock.quant']._update_available_quantity(self.product2, self.pack_location, 1, lot_id=lot1) - Inventory._run_conflict_inventory_tasks() - dupe_sn_inventories = Inventory.search([('is_conflict_inventory', '=', True), ('lot_ids', "!=", False)]) - self.assertEqual(len(dupe_sn_inventories), 1) - self.assertEqual(len(dupe_sn_inventories.lot_ids), 1) - self.assertEqual(len(dupe_sn_inventories.product_ids), 1) - - # check that inventory auto-updates correctly when still in draft - self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1, lot_id=lot2) - self.env['stock.quant']._update_available_quantity(self.product2, self.pack_location, 1, lot_id=lot2) - Inventory._run_conflict_inventory_tasks() - self.assertEqual(len(dupe_sn_inventories.lot_ids), 2, "New duplicate SN should be auto-added to existing draft inventory") - self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, -1, lot_id=lot2) - Inventory._run_conflict_inventory_tasks() - self.assertEqual(len(dupe_sn_inventories.lot_ids), 1) - self.assertEqual(len(dupe_sn_inventories.product_ids), 1, "Only removing 1 of 3 duplicated SNs shouldn't remove product from inventory") - - # check that inventory does not auto-update/auto-generate when inventory is in progress - dupe_sn_inventories.action_start() - self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1, lot_id=lot2) - Inventory._run_conflict_inventory_tasks() - dupe_sn_inventories = Inventory.search([('is_conflict_inventory', '=', True)]) - self.assertEqual(len(dupe_sn_inventories), 1) - self.assertEqual(len(dupe_sn_inventories.lot_ids), 1) - - # check that inventory auto-deletes when no more dupe SNs - dupe_sn_inventories.action_cancel_draft() - self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, -1, lot_id=lot1) - self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, -1, lot_id=lot2) - Inventory._run_conflict_inventory_tasks() - neg_qty_inventories = Inventory.search([('is_conflict_inventory', '=', True)]) - self.assertEqual(len(neg_qty_inventories), 0) - - def test_inventory_sn_warning(self): - """ Check that warnings pop up when duplicate SNs added. - Two cases covered: - - Check for dupes within the inventory (i.e. inventory adjustment lines) - - Check for dupes not in inventory (i.e. existing quants w/ same SN name) - """ - # check duplicated SN within inventory adjustment - lot1 = self.env['stock.production.lot'].create({ - 'name': 'sn1', - 'product_id': self.product2.id, - 'company_id': self.env.company.id, - }) - self.env['stock.quant']._update_available_quantity(self.product2, self.pack_location, 1, lot_id=lot1) - - inventory = self.env['stock.inventory'].create({ - 'name': 'Serial Onchange Check', - 'location_ids': [self.stock_location.id, self.pack_location.id], - 'product_ids': [(4, self.product2.id)], - }) - - inventory.action_start() - self.assertEqual(len(inventory.line_ids), 1) - - inventory_line = self.env['stock.inventory.line'].create({ - 'inventory_id': inventory.id, - 'location_id': self.stock_location.id, - 'product_id': self.product2.id, - 'prod_lot_id': lot1.id, - 'product_qty': 1 - }) - warning = False - warning = inventory_line._onchange_serial_number() - self.assertTrue(warning, 'Multiple lines with same serial number not detected') - self.assertEqual(list(warning.keys())[0], 'warning', 'Warning message was not returned') - - # check duplicate SN outside of inventory adjustment - lot2 = self.env['stock.production.lot'].create({ - 'name': 'sn2', - 'product_id': self.product2.id, - 'company_id': self.env.company.id, - }) - self.env['stock.quant']._update_available_quantity(self.product2, self.customer_location, 1, lot_id=lot2) - inventory_line.write({ - 'prod_lot_id': lot2.id, - }) - warning = False - warning = inventory_line._onchange_serial_number() - self.assertTrue(warning, 'Reuse of existing serial number not detected') - self.assertEqual(list(warning.keys())[0], 'warning', 'Warning message was not returned') + quant_new_loc.inventory_quantity = 10 + (quant_new_loc | quant_existing_loc | quant_non_cyclic_loc).action_apply_inventory() + # check location's last inventory dates + their quants next inventory dates + self.assertEqual(new_loc.last_inventory_date, date.today()) + self.assertEqual(existing_loc2.last_inventory_date, date.today()) + self.assertEqual(no_cyclic_loc.last_inventory_date, date.today()) + self.assertEqual(new_loc.next_inventory_date, date.today() + timedelta(days=2)) + self.assertEqual(existing_loc2.next_inventory_date, date.today() + timedelta(days=2)) + self.assertEqual(quant_new_loc.inventory_date, date.today() + timedelta(days=2)) + self.assertEqual(quant_existing_loc.inventory_date, date.today() + timedelta(days=2)) + self.assertEqual(quant_non_cyclic_loc.inventory_date, date.today() + timedelta(days=365)) diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py index ed1cfec15d3..bac14f0abdd 100644 --- a/addons/stock/tests/test_move2.py +++ b/addons/stock/tests/test_move2.py @@ -1146,14 +1146,12 @@ class TestSinglePicking(TestStockCommon): self.assertEqual(len(move1.move_line_ids), 1) self.assertEqual(move1.move_line_ids.product_qty, 1) - inventory = self.env['stock.inventory'].create({ - 'name': 'remove product1', - 'location_ids': [(4, self.stock_location)], - 'product_ids': [(4, self.productA.id)], + inventory_quant = self.env['stock.quant'].create({ + 'location_id': self.stock_location, + 'product_id': self.productA.id, + 'inventory_quantity': 2 }) - inventory.action_start() - inventory.line_ids.product_qty = 2 - inventory.action_validate() + inventory_quant.action_apply_inventory() delivery_order.action_assign() self.assertEqual(delivery_order.state, 'assigned') self.assertEqual(move1.state, 'assigned') @@ -1202,15 +1200,13 @@ class TestSinglePicking(TestStockCommon): self.assertEqual(len(move1.move_line_ids), 1) self.assertEqual(move1.move_line_ids.product_qty, 1) - inventory = self.env['stock.inventory'].create({ - 'name': 'remove product1', - 'location_ids': [(4, self.stock_location)], - 'product_ids': [(4, self.productA.id)], + inventory_quant = self.env['stock.quant'].create({ + 'location_id': self.stock_location, + 'product_id': self.productA.id, + 'inventory_quantity': 2, + 'lot_id': lot1.id }) - inventory.action_start() - inventory.line_ids.prod_lot_id = lot1 - inventory.line_ids.product_qty = 2 - inventory.action_validate() + inventory_quant.action_apply_inventory() delivery_order.action_assign() self.assertEqual(delivery_order.state, 'assigned') self.assertEqual(move1.state, 'assigned') @@ -1262,20 +1258,13 @@ class TestSinglePicking(TestStockCommon): self.assertEqual(len(move1.move_line_ids), 1) self.assertEqual(move1.move_line_ids.product_qty, 1) - inventory = self.env['stock.inventory'].create({ - 'name': 'remove product1', - 'location_ids': [(4, self.stock_location)], - 'product_ids': [(4, self.productA.id)], - }) - inventory.action_start() - self.env['stock.inventory.line'].create({ - 'inventory_id': inventory.id, - 'location_id': inventory.location_ids[0].id, - 'prod_lot_id': lot2.id, + inventory_quant = self.env['stock.quant'].create({ + 'location_id': self.stock_location, 'product_id': self.productA.id, - 'product_qty': 1, + 'inventory_quantity': 1, + 'lot_id': lot2.id }) - inventory.action_validate() + inventory_quant.action_apply_inventory() delivery_order.action_assign() self.assertEqual(delivery_order.state, 'assigned') self.assertEqual(move1.state, 'assigned') @@ -1329,20 +1318,13 @@ class TestSinglePicking(TestStockCommon): self.assertEqual(len(move1.move_line_ids), 1) self.assertEqual(move1.move_line_ids.product_qty, 1) - inventory = self.env['stock.inventory'].create({ - 'name': 'remove product1', - 'location_ids': [(4, self.stock_location)], - 'product_ids': [(4, self.productA.id)], - }) - inventory.action_start() - self.env['stock.inventory.line'].create({ - 'inventory_id': inventory.id, - 'location_id': inventory.location_ids[0].id, - 'prod_lot_id': serial2.id, + inventory_quant = self.env['stock.quant'].create({ + 'location_id': self.stock_location, 'product_id': self.productA.id, - 'product_qty': 1, + 'inventory_quantity': 1, + 'lot_id': serial2.id }) - inventory.action_validate() + inventory_quant.action_apply_inventory() delivery_order.action_assign() self.assertEqual(delivery_order.state, 'assigned') self.assertEqual(move1.state, 'assigned') diff --git a/addons/stock/tests/test_multicompany.py b/addons/stock/tests/test_multicompany.py index 8609a5007f6..1740852afa2 100644 --- a/addons/stock/tests/test_multicompany.py +++ b/addons/stock/tests/test_multicompany.py @@ -27,7 +27,7 @@ class TestMultiCompany(TransactionCase): 'company_ids': [(6, 0, [cls.company_a.id, cls.company_b.id])] }) cls.user_b = cls.env['res.users'].create({ - 'name': 'user company a with access to company b', + 'name': 'user company b with access to company a', 'login': 'user b', 'groups_id': [(6, 0, [group_user.id, group_stock_manager.id])], 'company_id': cls.company_b.id, @@ -103,60 +103,45 @@ class TestMultiCompany(TransactionCase): shared_partner.with_user(self.user_b).property_stock_customer = self.stock_location_a def test_inventory_1(self): - """Create an inventory in Company A for a product limited to Company A and, as a user of company - B, start the inventory and set its counted quantity to 10 before validating. The inventory - lines and stock moves should belong to Company A. The inventory loss location used should be - the one of Company A. + """Create a quant (inventory adjustment) in Company A for a product limited to Company A and + as a user of company B, apply the inventory adjustment and set its counted quantity to 10 + before validating. The quant and stock moves should belong to Company A. """ product = self.env['product.product'].create({ 'type': 'product', 'company_id': self.company_a.id, 'name': 'Product limited to company A', }) - inventory = self.env['stock.inventory'].with_user(self.user_a).create({}) - self.assertEqual(inventory.company_id, self.company_a) - inventory.with_user(self.user_b).action_start() - inventory.with_user(self.user_b).line_ids = [(0, 0, { - 'product_qty': 10, - 'product_id': product.id, + inventory_quant = self.env['stock.quant'].with_user(self.user_a).with_context(inventory_mode=True).create({ 'location_id': self.stock_location_a.id, - })] - inventory.with_user(self.user_b).action_validate() - self.assertEqual(inventory.line_ids.company_id, self.company_a) - self.assertEqual(inventory.move_ids.company_id, self.company_a) - self.assertEqual(inventory.move_ids.location_id.company_id, self.company_a) + 'product_id': product.id, + 'inventory_quantity': 0 + }) + self.assertEqual(inventory_quant.company_id, self.company_a) + inventory_quant.with_user(self.user_b).inventory_quantity = 10 + inventory_quant.with_user(self.user_b).action_apply_inventory() + last_move_id = self.env['stock.move'].search([('is_inventory', '=', True)])[-1] + self.assertEqual(inventory_quant.company_id, self.company_a) + self.assertEqual(last_move_id.company_id, self.company_a) + self.assertEqual(last_move_id.quantity_done, 10) + self.assertEqual(last_move_id.location_id.company_id, self.company_a) def test_inventory_2(self): - """Create an empty inventory in Company A and check it is not possible to use products limited - to Company B in it. + """Try to create a quant (inventory adjustment) in Company A and check it is not possible to use + products limited to Company B in it. """ product = self.env['product.product'].create({ 'name': 'product limited to company b', 'company_id': self.company_b.id, 'type': 'product' }) - inventory = self.env['stock.inventory'].with_user(self.user_a).create({}) - inventory.with_user(self.user_a).action_start() - inventory.with_user(self.user_a).line_ids = [(0, 0, { - 'product_id': product.id, - 'product_qty': 10, - 'location_id': self.stock_location_a.id, - })] - with self.assertRaises(UserError): - inventory.with_user(self.user_a).action_validate() - def test_inventory_3(self): - """As a user of Company A, check it is not possible to start an inventory adjustment for - a product limited to Company B. - """ - product = self.env['product.product'].create({ - 'name': 'product limited to company b', - 'company_id': self.company_b.id, - 'type': 'product' - }) - inventory = self.env['stock.inventory'].with_user(self.user_a).create({'product_ids': [(4, product.id)]}) with self.assertRaises(UserError): - inventory.with_user(self.user_a).action_start() + self.env['stock.quant'].with_user(self.user_a).with_context(inventory_mode=True).create({ + 'location_id': self.stock_location_a.id, + 'product_id': product.id, + 'inventory_quantity': 10 + }) def test_picking_1(self): """As a user of Company A, create a picking and use a picking type of Company B, check the diff --git a/addons/stock/tests/test_quant_inventory_mode.py b/addons/stock/tests/test_quant_inventory_mode.py index 1d58b264d31..115a35803a4 100644 --- a/addons/stock/tests/test_quant_inventory_mode.py +++ b/addons/stock/tests/test_quant_inventory_mode.py @@ -63,7 +63,7 @@ class TestEditableQuant(TransactionCase): 'product_id': self.product.id, 'location_id': self.stock.id, 'inventory_quantity': 24 - }) + }).action_apply_inventory() quants = self.env['stock.quant'].search([ ('product_id', '=', self.product.id), ('quantity', '>', 0), @@ -100,6 +100,7 @@ class TestEditableQuant(TransactionCase): 'location_id': self.room1.id, 'inventory_quantity': 24, }) + second_quant.action_apply_inventory() quants = self.env['stock.quant'].search([ ('product_id', '=', self.product.id), ('quantity', '>', 0), @@ -115,10 +116,10 @@ class TestEditableQuant(TransactionCase): self.assertEqual(len(stock_move), 1) def test_create_quant_3(self): - """ Try to create a quant with `inventory_quantity` but not in inventory mode. - Creates two quants not in inventory mode: + """ Try to create a quant with `inventory_quantity` but without applying it. + Creates two quants: - One with `quantity` (this one must be OK) - - One with `inventory_quantity` (this one must be null) + - One with `inventory_quantity` (this one will have null quantity) """ valid_quant = self.env['stock.quant'].create({ 'product_id': self.product.id, @@ -152,6 +153,7 @@ class TestEditableQuant(TransactionCase): 'location_id': self.room1.id, 'inventory_quantity': 20, }) + inventoried_quant.action_apply_inventory() with self.assertRaises(UserError): invalid_quant = self.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': self.product.id, @@ -171,6 +173,7 @@ class TestEditableQuant(TransactionCase): 'quantity': 12, }) quant.inventory_quantity = 24 + quant.action_apply_inventory() self.assertEqual(quant.quantity, 24) stock_move = self.env['stock.move'].search([ ('product_id', '=', self.product.id), @@ -187,6 +190,7 @@ class TestEditableQuant(TransactionCase): 'quantity': 12, }) quant.inventory_quantity = 8 + quant.action_apply_inventory() self.assertEqual(quant.quantity, 8) stock_move = self.env['stock.move'].search([ ('product_id', '=', self.product.id), @@ -219,6 +223,7 @@ class TestEditableQuant(TransactionCase): # Try to write on quant with permission quant.with_user(user_admin).write({'inventory_quantity': 8}) + quant.action_apply_inventory() self.assertEqual(quant.quantity, 8) def test_sn_warning(self): @@ -237,7 +242,7 @@ class TestEditableQuant(TransactionCase): 'location_id': self.room1.id, 'inventory_quantity': 1, 'lot_id': sn1.id - }) + }).action_apply_inventory() dupe_sn = self.Quant.create({ 'product_id': self.product_tracked_sn.id, @@ -245,7 +250,7 @@ class TestEditableQuant(TransactionCase): 'inventory_quantity': 1, 'lot_id': sn1.id }) - + dupe_sn.action_apply_inventory() warning = False warning = dupe_sn._onchange_serial_number() self.assertTrue(warning, 'Reuse of existing serial number not detected') diff --git a/addons/stock/tests/test_report.py b/addons/stock/tests/test_report.py index b21677cd3a4..63f1f41c58e 100644 --- a/addons/stock/tests/test_report.py +++ b/addons/stock/tests/test_report.py @@ -78,7 +78,7 @@ class TestReports(TestReportsCommon): 'product_id': product.id, 'location_id': stock.id, 'inventory_quantity': 50 - }) + }).action_apply_inventory() self.env['stock.move'].flush() report_records_today = self.env['report.stock.quantity'].read_group( [('product_id', '=', product.id), ('date', '=', date.today())], @@ -200,12 +200,12 @@ class TestReports(TestReportsCommon): 'product_id': product.id, 'location_id': stock.id, 'inventory_quantity': 50 - }) + }).action_apply_inventory() self.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': product.id, 'location_id': stock_without_wh.id, 'inventory_quantity': 50 - }) + }).action_apply_inventory() move = self.env['stock.move'].create({ 'name': 'Move outside warehouse', 'location_id': stock.id, diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index bf9782f4199..1a0fcc899f8 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -3,9 +3,9 @@ from odoo.addons.stock.tests.common import TestStockCommon from odoo.tests import Form from odoo.tools import mute_logger, float_round -from odoo.exceptions import UserError from odoo import fields + class TestStockFlow(TestStockCommon): def setUp(cls): super(TestStockFlow, cls).setUp() @@ -117,7 +117,7 @@ class TestStockFlow(TestStockCommon): # Check incoming shipment total quantity of pack operation total_qty = sum(self.StockPackObj.search([('move_id', 'in', picking_in.move_lines.ids)]).mapped('qty_done')) - self.assertEqual(total_qty, 23, 'Wrong quantity in pack operation') + self.assertEqual(total_qty, 23, 'Wrong quantity in pack operation') # Transfer Incoming Shipment. picking_in._action_done() @@ -1064,113 +1064,44 @@ class TestStockFlow(TestStockCommon): total_qty = [quant.quantity for quant in quants] self.assertAlmostEqual(sum(total_qty), 999.9975, msg='Expecting 999.9975 kg , got %.4f kg on location stock!' % (sum(total_qty))) - def test_20_create_inventory_with_different_uom(self): - """Create inventory with different unit of measure.""" - - # ------------------------------------------------ - # Test inventory with product A(Unit). - # ------------------------------------------------ - - inventory = self.InvObj.create({'name': 'Test', - 'product_ids': [(4, self.UnitA.id)]}) - inventory.action_start() - self.assertFalse(inventory.line_ids, "Inventory line should not created.") - inventory_line = self.InvLineObj.create({ - 'inventory_id': inventory.id, - 'product_id': self.UnitA.id, - 'product_uom_id': self.uom_dozen.id, - 'product_qty': 10, - 'location_id': self.stock_location}) - inventory.action_validate() - # Check quantity available of product UnitA. - quants = self.StockQuantObj.search([('product_id', '=', self.UnitA.id), ('location_id', '=', self.stock_location)]) - total_qty = [quant.quantity for quant in quants] - self.assertEqual(sum(total_qty), 120, 'Expecting 120 Units , got %.4f Units on location stock!' % (sum(total_qty))) - self.assertEqual(self.UnitA.qty_available, 120, 'Expecting 120 Units , got %.4f Units of quantity available!' % (self.UnitA.qty_available)) - # Create Inventory again for product UnitA. - inventory = self.InvObj.create({'name': 'Test', - 'product_ids': [(4, self.UnitA.id)]}) - inventory.action_start() - self.assertEqual(len(inventory.line_ids), 1, "One inventory line should be created.") - inventory_line = self.InvLineObj.search([('product_id', '=', self.UnitA.id), ('inventory_id', '=', inventory.id)], limit=1) - self.assertEqual(inventory_line.product_qty, 120, "Wrong product quantity in inventory line.") - # Modify the inventory line and set the quantity to 144 product on this new inventory. - inventory_line.write({'product_qty': 144}) - inventory.action_validate() - move = self.MoveObj.search([('product_id', '=', self.UnitA.id), ('inventory_id', '=', inventory.id)], limit=1) - self.assertEqual(move.product_uom_qty, 24, "Wrong move quantity of product UnitA.") - # Check quantity available of product UnitA. - quants = self.StockQuantObj.search([('product_id', '=', self.UnitA.id), ('location_id', '=', self.stock_location)]) - total_qty = [quant.quantity for quant in quants] - self.assertEqual(sum(total_qty), 144, 'Expecting 144 Units , got %.4f Units on location stock!' % (sum(total_qty))) - self.UnitA._compute_quantities() - self.assertEqual(self.UnitA.qty_available, 144, 'Expecting 144 Units , got %.4f Units of quantity available!' % (self.UnitA.qty_available)) - - # ------------------------------------------------ - # Test inventory with product KG. - # ------------------------------------------------ - - productKG = self.ProductObj.create({'name': 'Product KG', 'uom_id': self.uom_kg.id, 'uom_po_id': self.uom_kg.id, 'type': 'product'}) - inventory = self.InvObj.create({'name': 'Inventory Product KG', - 'product_ids': [(4, productKG.id)]}) - inventory.action_start() - self.assertFalse(inventory.line_ids, "Inventory line should not created.") - inventory_line = self.InvLineObj.create({ - 'inventory_id': inventory.id, - 'product_id': productKG.id, - 'product_uom_id': self.uom_tone.id, - 'product_qty': 5, - 'location_id': self.stock_location}) - inventory.action_validate() - quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)]) - total_qty = [quant.quantity for quant in quants] - self.assertEqual(sum(total_qty), 5000, 'Expecting 5000 kg , got %.4f kg on location stock!' % (sum(total_qty))) - self.assertEqual(productKG.qty_available, 5000, 'Expecting 5000 kg , got %.4f kg of quantity available!' % (productKG.qty_available)) - # Create Inventory again. - inventory = self.InvObj.create({'name': 'Test', - 'product_ids': [(4, productKG.id)]}) - inventory.action_start() - self.assertEqual(len(inventory.line_ids), 1, "One inventory line should be created.") - inventory_line = self.InvLineObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1) - self.assertEqual(inventory_line.product_qty, 5000, "Wrong product quantity in inventory line.") - # Modify the inventory line and set the quantity to 4000 product on this new inventory. - inventory_line.write({'product_qty': 4000}) - inventory.action_validate() - # Check inventory move quantity of product KG. - move = self.MoveObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1) - self.assertEqual(move.product_uom_qty, 1000, "Wrong move quantity of product KG.") - # Check quantity available of product KG. - quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)]) - total_qty = [quant.quantity for quant in quants] - self.assertEqual(sum(total_qty), 4000, 'Expecting 4000 kg , got %.4f on location stock!' % (sum(total_qty))) - productKG._compute_quantities() - self.assertEqual(productKG.qty_available, 4000, 'Expecting 4000 kg , got %.4f of quantity available!' % (productKG.qty_available)) - + def test_20_create_inventory_with_packs_and_lots(self): # -------------------------------------------------------- # TEST EMPTY INVENTORY WITH PACKS and LOTS # --------------------------------------------------------- packproduct = self.ProductObj.create({'name': 'Pack Product', 'uom_id': self.uom_unit.id, 'uom_po_id': self.uom_unit.id, 'type': 'product'}) lotproduct = self.ProductObj.create({'name': 'Lot Product', 'uom_id': self.uom_unit.id, 'uom_po_id': self.uom_unit.id, 'type': 'product'}) - inventory = self.InvObj.create({'name': 'Test Partial and Pack', - 'start_empty': True, - 'location_ids': [(4, self.stock_location)]}) - inventory.action_start() + quant_obj = self.env['stock.quant'].with_context(inventory_mode=True) pack_obj = self.env['stock.quant.package'] lot_obj = self.env['stock.production.lot'] pack1 = pack_obj.create({'name': 'PACK00TEST1'}) pack_obj.create({'name': 'PACK00TEST2'}) lot1 = lot_obj.create({'name': 'Lot001', 'product_id': lotproduct.id, 'company_id': self.env.company.id}) - move = self.MoveObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1) - self.assertEqual(len(move), 0, "Partial filter should not create a lines upon prepare") - line_vals = [] - line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 10, 'product_uom_id': packproduct.uom_id.id}] - line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 20, 'product_uom_id': packproduct.uom_id.id, 'package_id': pack1.id}] - line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 30, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': lot1.id}] - line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 25, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': False}] - inventory.write({'line_ids': [(0, 0, x) for x in line_vals]}) - inventory.action_validate() + packproduct_no_pack_quant = quant_obj.create({ + 'product_id': packproduct.id, + 'inventory_quantity': 10.0, + 'location_id': self.stock_location + }) + packproduct_quant = quant_obj.create({ + 'product_id': packproduct.id, + 'inventory_quantity': 20.0, + 'location_id': self.stock_location, + 'package_id': pack1.id + }) + lotproduct_no_lot_quant = quant_obj.create({ + 'product_id': lotproduct.id, + 'inventory_quantity': 25.0, + 'location_id': self.stock_location + }) + lotproduct_quant = quant_obj.create({ + 'product_id': lotproduct.id, + 'inventory_quantity': 30.0, + 'location_id': self.stock_location, + 'lot_id': lot1.id + }) + (packproduct_no_pack_quant | packproduct_quant | lotproduct_no_lot_quant | lotproduct_quant).action_apply_inventory() + self.assertEqual(packproduct.qty_available, 30, "Wrong qty available for packproduct") self.assertEqual(lotproduct.qty_available, 55, "Wrong qty available for lotproduct") quants = self.StockQuantObj.search([('product_id', '=', packproduct.id), ('location_id', '=', self.stock_location), ('package_id', '=', pack1.id)]) @@ -1178,16 +1109,13 @@ class TestStockFlow(TestStockCommon): self.assertEqual(total_qty, 20, 'Expecting 20 units on package 1 of packproduct, but we got %.4f on location stock!' % (total_qty)) # Create an inventory that will put the lots without lot to 0 and check that taking without pack will not take it from the pack - inventory2 = self.InvObj.create({'name': 'Test Partial Lot and Pack2', - 'start_empty': True, - 'location_ids': [(4, self.stock_location)]}) - inventory2.action_start() - line_vals = [] - line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 20, 'product_uom_id': packproduct.uom_id.id}] - line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 0, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': False}] - line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 10, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': lot1.id}] - inventory2.write({'line_ids': [(0, 0, x) for x in line_vals]}) - inventory2.action_validate() + packproduct_no_pack_quant.inventory_quantity = 20 + lotproduct_no_lot_quant.inventory_quantity = 0 + lotproduct_quant.inventory_quantity = 10 + packproduct_no_pack_quant.action_apply_inventory() + lotproduct_no_lot_quant.action_apply_inventory() + lotproduct_quant.action_apply_inventory() + self.assertEqual(packproduct.qty_available, 40, "Wrong qty available for packproduct") self.assertEqual(lotproduct.qty_available, 10, "Wrong qty available for lotproduct") quants = self.StockQuantObj.search([('product_id', '=', lotproduct.id), ('location_id', '=', self.stock_location), ('lot_id', '=', lot1.id)]) @@ -1513,17 +1441,12 @@ class TestStockFlow(TestStockCommon): # "all at once" and "reserve" scenario # ----------------------------------------------------------- # get one product in stock - inventory = self.env['stock.inventory'].create({ - 'name': 'Inventory Product Table', - 'line_ids': [(0, 0, { - 'product_id': self.productA.id, - 'product_uom_id': self.productA.uom_id.id, - 'product_qty': 1, - 'location_id': self.stock_location - })] + inventory_quant = self.env['stock.quant'].create({ + 'location_id': self.stock_location, + 'product_id': self.productA.id, + 'inventory_quantity': 1, }) - inventory.action_start() - inventory.action_validate() + inventory_quant.action_apply_inventory() # create a "all at once" delivery order for two products picking_out = self.PickingObj.create({ @@ -1545,17 +1468,8 @@ class TestStockFlow(TestStockCommon): self.assertEqual(picking_out.state, "confirmed") # receive one product in stock - inventory = self.env['stock.inventory'].create({ - 'name': 'Inventory Product Table', - 'line_ids': [(0, 0, { - 'product_id': self.productA.id, - 'product_uom_id': self.productA.uom_id.id, - 'product_qty': 2, - 'location_id': self.stock_location - })] - }) - inventory.action_start() - inventory.action_validate() + inventory_quant.inventory_quantity = 2 + inventory_quant.action_apply_inventory() # recheck availability of the delivery order, it should be assigned picking_out.action_assign() self.assertEqual(len(picking_out.move_lines), 1.0) @@ -1603,17 +1517,12 @@ class TestStockFlow(TestStockCommon): # "partial" and "reserve" scenario # ----------------------------------------------------------- # get one product in stock - inventory = self.env['stock.inventory'].create({ - 'name': 'Inventory Product Table', - 'line_ids': [(0, 0, { - 'product_id': self.productA.id, - 'product_uom_id': self.productA.uom_id.id, - 'product_qty': 1, - 'location_id': self.stock_location - })] + inventory_quant = self.env['stock.quant'].create({ + 'location_id': self.stock_location, + 'product_id': self.productA.id, + 'inventory_quantity': 1, }) - inventory.action_start() - inventory.action_validate() + inventory_quant.action_apply_inventory() # create a "partial" delivery order for two products picking_out = self.PickingObj.create({ @@ -1636,17 +1545,8 @@ class TestStockFlow(TestStockCommon): self.assertEqual(picking_out.state, "assigned") # receive one product in stock - inventory = self.env['stock.inventory'].create({ - 'name': 'Inventory Product Table', - 'line_ids': [(0, 0, { - 'product_id': self.productA.id, - 'product_uom_id': self.productA.uom_id.id, - 'product_qty': 2, - 'location_id': self.stock_location - })] - }) - inventory.action_start() - inventory.action_validate() + inventory_quant.inventory_quantity = 2 + inventory_quant.action_apply_inventory() # recheck availability of the delivery order, it should be assigned picking_out.action_assign() diff --git a/addons/stock/tests/test_warehouse.py b/addons/stock/tests/test_warehouse.py index b5c2e16c044..e5f93105cc3 100644 --- a/addons/stock/tests/test_warehouse.py +++ b/addons/stock/tests/test_warehouse.py @@ -19,28 +19,29 @@ class TestWarehouse(TestStockCommon): 'inventory_quantity': 50.0, 'location_id': self.warehouse_1.lot_stock_id.id, }) - inventory = self.env['stock.inventory'].with_user(self.user_stock_manager).create({ - 'name': 'Starting for product_1', - 'location_ids': [(4, self.warehouse_1.lot_stock_id.id)], - 'product_ids': [(4, self.product_1.id)], - }) - inventory.action_start() - # As done in common.py, there is already an inventory line existing - self.assertEqual(len(inventory.line_ids), 1) - self.assertEqual(inventory.line_ids.theoretical_qty, 50.0) - self.assertEqual(inventory.line_ids.product_id, self.product_1) - self.assertEqual(inventory.line_ids.product_uom_id, self.product_1.uom_id) + product_1_quant.action_apply_inventory() - # Update the line, set to 35 - inventory.line_ids.write({'product_qty': 35.0}) - inventory.action_validate() + # Make sure the inventory was successful + move_in_id = self.env['stock.move'].search([('is_inventory', '=', True), ('product_id', '=', self.product_1.id)]) + self.assertEqual(len(move_in_id), 1) + self.assertEqual(move_in_id.product_qty, 50.0) + self.assertEqual(product_1_quant.quantity, 50.0) + self.assertEqual(move_in_id.product_uom, self.product_1.uom_id) + self.assertEqual(move_in_id.state, 'done') + + # Update the inventory, set to 35 + product_1_quant.inventory_quantity = 35.0 + product_1_quant.action_apply_inventory() # Check related move and quants - self.assertIn(inventory.name, inventory.move_ids.name) - self.assertEqual(inventory.move_ids.product_qty, 15.0) - self.assertEqual(inventory.move_ids.location_id, self.warehouse_1.lot_stock_id) - self.assertEqual(inventory.move_ids.location_dest_id, self.product_1.property_stock_inventory) # Inventory loss - self.assertEqual(inventory.move_ids.state, 'done') + move_ids = self.env['stock.move'].search([('is_inventory', '=', True), ('product_id', '=', self.product_1.id)]) + self.assertEqual(len(move_ids), 2) + move_out_id = move_ids[-1] + self.assertEqual(move_out_id.product_qty, 15.0) + self.assertEqual(move_out_id.location_id, self.warehouse_1.lot_stock_id) + self.assertEqual(move_out_id.location_dest_id, self.product_1.property_stock_inventory) # Inventory loss + self.assertEqual(move_out_id.state, 'done') + quants = self.env['stock.quant']._gather(self.product_1, self.product_1.property_stock_inventory) self.assertEqual(len(quants), 1) # One quant created for inventory loss @@ -219,25 +220,21 @@ class TestWarehouse(TestStockCommon): picking_out._action_done() # Make an inventory adjustment to set the quantity to 0 - inventory = self.env['stock.inventory'].create({ - 'name': 'Starting for product_1', - 'location_ids': [(4, stock_location.id)], - 'product_ids': [(4, productA.id)], - }) - inventory.action_start() - self.assertEqual(len(inventory.line_ids), 1, "Wrong inventory lines generated.") - self.assertEqual(inventory.line_ids.theoretical_qty, -1, "Theoretical quantity should be -1.") - inventory.line_ids.product_qty = 0 # Put the quantity back to 0 - inventory.action_validate() + quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)]) + self.assertEqual(len(quant), 1, "Wrong number of quants created.") + self.assertEqual(quant.quantity, -1, "Theoretical quantity should be -1.") + # Put the quantity back to 0 + quant.inventory_quantity = 0 + quant.action_apply_inventory() # The inventory adjustment should have created one - self.assertEqual(len(inventory.move_ids), 1) - quantity = inventory.move_ids.mapped('product_qty') - self.assertEqual(quantity, [1], "Moves created with wrong quantity.") - location_ids = inventory.move_ids.mapped('location_id').ids - self.assertEqual(set(location_ids), {location_loss.id}) + move = self.env['stock.move'].search([('product_id', '=', productA.id), ('is_inventory', '=', True)]) + self.assertEqual(len(move), 1) + self.assertEqual(move.product_qty, 1, "Moves created with wrong quantity.") + self.assertEqual(move.location_id.id, location_loss.id) # There should be no quant in the stock location + self.env['stock.quant']._quant_tasks() quants = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)]) self.assertEqual(sum(quants.mapped('quantity')), 0) diff --git a/addons/stock/views/res_config_settings_views.xml b/addons/stock/views/res_config_settings_views.xml index df5594494a8..b22a33a9563 100644 --- a/addons/stock/views/res_config_settings_views.xml +++ b/addons/stock/views/res_config_settings_views.xml @@ -67,6 +67,18 @@ +
+
+
+

Barcode

diff --git a/addons/stock/views/stock_inventory_views.xml b/addons/stock/views/stock_inventory_views.xml deleted file mode 100644 index 8f25fc77c25..00000000000 --- a/addons/stock/views/stock_inventory_views.xml +++ /dev/null @@ -1,271 +0,0 @@ - - - - - stock.inventory.line.tree - stock.inventory.line - - - - - - - - - - - - - -
-
-
- - - - - - - - - - - - -
- - - -
- -
-
- - - Inventory Adjustments - ir.actions.act_window - stock.inventory - tree,kanban,form - - - -

- Want to speed up your inventory counts? Try our Barcode app -

- Barcode scanner can be activated via inventory settings. - Manual inventory adjustments can also be performed and pre-filled with - suggested counted quantity. -

-
-
- - - Inventory Adjustments - - code - -action = model.action_open_inventory_view() - - - - - Set counted quantities to 0 - - - code - -if records: - action = records.action_reset_product_qty() - - - - - Recompute On Hand Quantity - - - code - -if records: - action = records.action_refresh_quantity() - - - - - diff --git a/addons/stock/views/stock_move_line_views.xml b/addons/stock/views/stock_move_line_views.xml index 662ac364164..9f08c3c5b4d 100644 --- a/addons/stock/views/stock_move_line_views.xml +++ b/addons/stock/views/stock_move_line_views.xml @@ -53,7 +53,7 @@ - + @@ -73,6 +73,10 @@ + + + + @@ -87,6 +91,8 @@ + + diff --git a/addons/stock/views/stock_move_views.xml b/addons/stock/views/stock_move_views.xml index b3d53c08c9a..138b7f3e913 100644 --- a/addons/stock/views/stock_move_views.xml +++ b/addons/stock/views/stock_move_views.xml @@ -90,7 +90,7 @@ - + @@ -108,11 +108,11 @@ attrs="{'invisible': [('show_details_visible', '=', False)]}" options='{"warn": true}'/>
-
+
Initial Demand
Quantity Done
-
+
Quantity Done /
@@ -346,6 +346,7 @@ + diff --git a/addons/stock/views/stock_quant_views.xml b/addons/stock/views/stock_quant_views.xml index 89ae0e7f995..f64059ca6a7 100644 --- a/addons/stock/views/stock_quant_views.xml +++ b/addons/stock/views/stock_quant_views.xml @@ -9,6 +9,8 @@ + + @@ -22,6 +24,9 @@ + + + @@ -109,6 +114,7 @@ + stock.quant.tree.editable stock.quant @@ -120,7 +126,6 @@ - - + @@ -163,7 +167,7 @@ - + @@ -330,6 +334,50 @@ + + + stock.quant.inventory.tree.editable + stock.quant + + + + + + + + + + + + + + + + + + +