From 31e650e4d3a410c43105ba1bf837ff3c8673522d Mon Sep 17 00:00:00 2001 From: DavidJForgeFlow Date: Tue, 8 Nov 2022 12:14:51 +0100 Subject: [PATCH] [15.0][IMP] stock_inventory: add product selection and fixup --- stock_inventory/__manifest__.py | 2 + stock_inventory/models/stock_inventory.py | 128 ++++++++++++---- stock_inventory/readme/CONTRIBUTORS.rst | 2 +- stock_inventory/readme/USAGE.rst | 5 +- stock_inventory/tests/test_stock_inventory.py | 137 +++++++++++++++++- stock_inventory/views/stock_inventory.xml | 28 +++- 6 files changed, 269 insertions(+), 33 deletions(-) diff --git a/stock_inventory/__manifest__.py b/stock_inventory/__manifest__.py index 18f7c836545c..fc7c70b8abae 100644 --- a/stock_inventory/__manifest__.py +++ b/stock_inventory/__manifest__.py @@ -2,6 +2,8 @@ "name": "Stock Inventory Adjustment", "version": "15.0.1.0.0", "license": "LGPL-3", + "maintainer": ["DavidJForgeFlow"], + "development_status": "Beta", "category": "Inventory/Inventory", "summary": "Allows to do an easier follow up of the Inventory Adjustments", "author": "ForgeFlow, Odoo Community Association (OCA)", diff --git a/stock_inventory/models/stock_inventory.py b/stock_inventory/models/stock_inventory.py index cbd9c33c9be0..3922304c9de3 100644 --- a/stock_inventory/models/stock_inventory.py +++ b/stock_inventory/models/stock_inventory.py @@ -8,7 +8,7 @@ class InventoryAdjustmentsGroup(models.Model): _order = "date desc, id desc" name = fields.Char( - required=True, default="Inventory ", string="Inventory Reference" + required=True, default="Inventory", string="Inventory Reference" ) date = fields.Datetime(default=lambda self: fields.Datetime.now()) @@ -18,12 +18,22 @@ class InventoryAdjustmentsGroup(models.Model): default="draft", ) + owner_id = fields.Many2one( + "res.partner", "Owner", help="This is the owner of the inventory adjustment" + ) + location_ids = fields.Many2many( - "stock.location", string="Location", domain="[('usage', '=', 'internal')]" + "stock.location", string="Locations", domain="[('usage', '=', 'internal')]" ) product_selection = fields.Selection( - [("all", "All Products"), ("manual", "Manual Selection")], + [ + ("all", "All Products"), + ("manual", "Manual Selection"), + ("category", "Product Category"), + ("one", "One Product"), + ("lot", "Lot/Serial Number"), + ], default="all", required=True, ) @@ -32,6 +42,13 @@ class InventoryAdjustmentsGroup(models.Model): stock_quant_ids = fields.Many2many("stock.quant", string="Inventory Adjustment") + category_id = fields.Many2one("product.category", string="Product Category") + + lot_ids = fields.Many2many( + "stock.production.lot", + string="Lot/Serial Numbers", + ) + stock_move_ids = fields.One2many( "stock.move.line", "inventory_adjustment_id", @@ -67,6 +84,58 @@ def _compute_count_stock_moves(self): sm_ids = self.mapped("stock_move_ids").ids self.count_stock_moves = len(sm_ids) + def _get_all_quants(self, locations): + return self.env["stock.quant"].search( + [ + "|", + ("location_id", "in", locations.mapped("id")), + ("location_id", "in", locations.child_ids.ids), + ] + ) + + def _get_manual_quants(self, locations): + return self.env["stock.quant"].search( + [ + ("product_id", "in", self.product_ids.ids), + "|", + ("location_id", "=", locations.mapped("id")), + ("location_id", "in", locations.child_ids.ids), + ] + ) + + def _get_one_quant(self, locations): + return self.env["stock.quant"].search( + [ + ("product_id", "in", self.product_ids.ids), + "|", + ("location_id", "=", locations.mapped("id")), + ("location_id", "in", locations.child_ids.ids), + ] + ) + + def _get_lot_quants(self, locations): + return self.env["stock.quant"].search( + [ + ("product_id", "in", self.product_ids.ids), + ("lot_id", "in", self.lot_ids.ids), + "|", + ("location_id", "=", locations.mapped("id")), + ("location_id", "in", locations.child_ids.ids), + ] + ) + + def _get_category_quants(self, locations): + return self.env["stock.quant"].search( + [ + "|", + ("product_id.categ_id", "=", self.category_id.id), + ("product_id.categ_id", "in", self.category_id.child_id.ids), + "|", + ("location_id", "=", locations.mapped("id")), + ("location_id", "in", locations.child_ids.ids), + ] + ) + def action_state_to_in_progress(self): active_rec = self.env["stock.inventory"].search([("state", "=", "in_progress")]) if active_rec: @@ -75,36 +144,26 @@ def action_state_to_in_progress(self): ) self.state = "in_progress" if self.product_selection == "all": - for location in self._origin.location_ids: - self.stock_quant_ids = self.env["stock.quant"].search( - [ - "|", - ("location_id", "=", location.id), - ("location_id", "in", location.child_ids.ids), - ] - ) - else: - for location in self._origin.location_ids: - self.stock_quant_ids = self.env["stock.quant"].search( - [ - ("product_id", "in", self.product_ids.ids), - "|", - ("location_id", "=", location.id), - ("location_id", "in", location.child_ids.ids), - ] - ) + self.stock_quant_ids = self._get_all_quants(self._origin.location_ids) + elif self.product_selection == "manual": + self.stock_quant_ids = self._get_manual_quants(self._origin.location_ids) + elif self.product_selection == "one": + self.stock_quant_ids = self._get_one_quant(self._origin.location_ids) + elif self.product_selection == "lot": + self.stock_quant_ids = self._get_lot_quants(self._origin.location_ids) + elif self.product_selection == "category": + self.stock_quant_ids = self._get_category_quants(self._origin.location_ids) + self.stock_quant_ids.update({"to_do": True}) return def action_state_to_done(self): self.state = "done" - for quant in self.stock_quant_ids: - quant.to_do = True + self.stock_quant_ids.update({"to_do": True}) return def action_state_to_draft(self): self.state = "draft" - for quant in self.stock_quant_ids: - quant.to_do = True + self.stock_quant_ids.update({"to_do": True}) self.stock_quant_ids = None return @@ -124,3 +183,22 @@ def action_view_stock_moves(self): result["domain"] = [("id", "in", sm_ids)] result["context"] = [] return result + + @api.constrains("product_selection", "product_ids") + def _check_one_product_in_product_selection(self): + for rec in self: + if len(rec.product_ids) > 1: + if rec.product_selection == "one": + raise ValidationError( + _( + "When 'Product Selection: One Product' is selected" + " you are only able to add one product." + ) + ) + elif rec.product_selection == "lot": + raise ValidationError( + _( + "When 'Product Selection: Lot Serial Number' is selected" + " you are only able to add one product." + ) + ) diff --git a/stock_inventory/readme/CONTRIBUTORS.rst b/stock_inventory/readme/CONTRIBUTORS.rst index 95cc88de027e..cc0127024465 100644 --- a/stock_inventory/readme/CONTRIBUTORS.rst +++ b/stock_inventory/readme/CONTRIBUTORS.rst @@ -1,3 +1,3 @@ * `ForgeFlow `_: - * David Jiménez + * David Jiménez diff --git a/stock_inventory/readme/USAGE.rst b/stock_inventory/readme/USAGE.rst index 556dca26538b..fc0e36c07ff9 100644 --- a/stock_inventory/readme/USAGE.rst +++ b/stock_inventory/readme/USAGE.rst @@ -1,6 +1,9 @@ Go to Inventory / Operations / Inventory Adjustments. Here you can see the list of Adjustment Grouped. If you create a new Group, you can choose 2 types of product selection: -- All Products (all products from theselected locations) +- All Products (all products from theselected locations). - Manual Selection (choose manually each product in location). +- One Product (choose only one product in locations). +- Lot Serial Number (choose one product, any lots and locations). +- Product Category (choose one product category [childs also taken into account]). When you start the adjustment (only one at a time) clicking on adjustments gets you to the view where adjustments are made. From the group view, if you click on Stock Moves you can see the movements done (includes the 0 qty moves). diff --git a/stock_inventory/tests/test_stock_inventory.py b/stock_inventory/tests/test_stock_inventory.py index 44e2e43e7b2c..b76703535fbe 100644 --- a/stock_inventory/tests/test_stock_inventory.py +++ b/stock_inventory/tests/test_stock_inventory.py @@ -12,6 +12,7 @@ def setUp(self): self.move_model = self.env["stock.move.line"] self.inventory_model = self.env["stock.inventory"] self.location_model = self.env["stock.location"] + self.product_categ = self.env["product.category"].create({"name": "Test Categ"}) self.product = self.env["product.product"].create( { "name": "Product 1 test", @@ -23,6 +24,7 @@ def setUp(self): { "name": "Product 1 test", "type": "product", + "categ_id": self.product_categ.id, } ) self.lot_1 = self.env["stock.production.lot"].create( @@ -153,7 +155,7 @@ def test_02_manual_selection(self): x.action_state_to_done() inventory1 = self.inventory_model.create( { - "name": "Inventory_Test_1", + "name": "Inventory_Test_3", "product_selection": "manual", "location_ids": [self.location1.id], "product_ids": [self.product.id], @@ -162,7 +164,7 @@ def test_02_manual_selection(self): inventory1.action_state_to_in_progress() inventory2 = self.inventory_model.create( { - "name": "Inventory_Test_2", + "name": "Inventory_Test_4", "product_selection": "all", "location_ids": [self.location1.id], } @@ -197,3 +199,134 @@ def test_02_manual_selection(self): self.assertEqual(inventory1.count_stock_quants, 2) self.assertEqual(inventory1.count_stock_quants_string, "0 / 2") inventory1.action_state_to_done() + + def test_03_one_selection(self): + x = self.inventory_model.search([("state", "=", "in_progress")]) + if x: + x.action_state_to_done() + with self.assertRaises(ValidationError), self.cr.savepoint(): + inventory1 = self.inventory_model.create( + { + "name": "Inventory_Test_5", + "product_selection": "one", + "location_ids": [self.location1.id], + "product_ids": [self.product.id, self.product2.id], + } + ) + inventory1 = self.inventory_model.create( + { + "name": "Inventory_Test_5", + "product_selection": "one", + "location_ids": [self.location1.id], + "product_ids": [self.product.id], + } + ) + inventory1.action_state_to_in_progress() + inventory1.product_ids = [self.product.id] + self.assertEqual( + inventory1.stock_quant_ids.ids, [self.quant1.id, self.quant3.id] + ) + inventory1.action_state_to_draft() + self.assertEqual(inventory1.stock_quant_ids.ids, []) + inventory1.action_state_to_in_progress() + self.assertEqual(inventory1.count_stock_moves, 0) + self.assertEqual(inventory1.count_stock_quants, 2) + self.assertEqual(inventory1.count_stock_quants_string, "2 / 2") + inventory1.action_view_inventory_adjustment() + self.quant3.inventory_quantity = 74 + self.quant3.action_apply_inventory() + inventory1._compute_count_stock_quants() + inventory1.action_view_stock_moves() + self.assertEqual(inventory1.count_stock_moves, 1) + self.assertEqual(inventory1.count_stock_quants, 2) + self.assertEqual(inventory1.count_stock_quants_string, "1 / 2") + self.assertEqual(inventory1.stock_move_ids.qty_done, 26) + self.assertEqual(inventory1.stock_move_ids.product_id.id, self.product.id) + self.assertEqual(inventory1.stock_move_ids.lot_id.id, self.lot_3.id) + self.assertEqual(inventory1.stock_move_ids.location_id.id, self.location3.id) + self.quant1.inventory_quantity = 65 + self.quant1.action_apply_inventory() + inventory1._compute_count_stock_quants() + self.assertEqual(inventory1.count_stock_moves, 2) + self.assertEqual(inventory1.count_stock_quants, 2) + self.assertEqual(inventory1.count_stock_quants_string, "0 / 2") + inventory1.action_state_to_done() + + def test_04_lot_selection(self): + x = self.inventory_model.search([("state", "=", "in_progress")]) + if x: + x.action_state_to_done() + with self.assertRaises(ValidationError), self.cr.savepoint(): + inventory1 = self.inventory_model.create( + { + "name": "Inventory_Test_6", + "product_selection": "lot", + "location_ids": [self.location1.id], + "lot_ids": [self.lot_3.id], + "product_ids": [self.product.id, self.product2.id], + } + ) + inventory1 = self.inventory_model.create( + { + "name": "Inventory_Test_6", + "product_selection": "lot", + "location_ids": [self.location1.id], + "lot_ids": [self.lot_3.id], + "product_ids": [self.product.id], + } + ) + inventory1.product_ids = [self.product.id] + inventory1.action_state_to_in_progress() + self.assertEqual(inventory1.stock_quant_ids.ids, [self.quant3.id]) + inventory1.action_state_to_draft() + self.assertEqual(inventory1.stock_quant_ids.ids, []) + inventory1.action_state_to_in_progress() + self.assertEqual(inventory1.count_stock_moves, 0) + self.assertEqual(inventory1.count_stock_quants, 1) + self.assertEqual(inventory1.count_stock_quants_string, "1 / 1") + inventory1.action_view_inventory_adjustment() + self.quant3.inventory_quantity = 74 + self.quant3.action_apply_inventory() + inventory1._compute_count_stock_quants() + inventory1.action_view_stock_moves() + self.assertEqual(inventory1.count_stock_moves, 1) + self.assertEqual(inventory1.count_stock_quants, 1) + self.assertEqual(inventory1.count_stock_quants_string, "0 / 1") + self.assertEqual(inventory1.stock_move_ids.qty_done, 26) + self.assertEqual(inventory1.stock_move_ids.product_id.id, self.product.id) + self.assertEqual(inventory1.stock_move_ids.lot_id.id, self.lot_3.id) + self.assertEqual(inventory1.stock_move_ids.location_id.id, self.location3.id) + inventory1.action_state_to_done() + + def test_05_category_selection(self): + x = self.inventory_model.search([("state", "=", "in_progress")]) + if x: + x.action_state_to_done() + inventory1 = self.inventory_model.create( + { + "name": "Inventory_Test_7", + "product_selection": "category", + "location_ids": [self.location3.id], + "category_id": self.product_categ.id, + } + ) + inventory1.action_state_to_in_progress() + self.assertEqual(inventory1.stock_quant_ids.ids, [self.quant4.id]) + inventory1.action_state_to_draft() + self.assertEqual(inventory1.stock_quant_ids.ids, []) + inventory1.action_state_to_in_progress() + self.assertEqual(inventory1.count_stock_moves, 0) + self.assertEqual(inventory1.count_stock_quants, 1) + self.assertEqual(inventory1.count_stock_quants_string, "1 / 1") + inventory1.action_view_inventory_adjustment() + self.quant4.inventory_quantity = 74 + self.quant4.action_apply_inventory() + inventory1._compute_count_stock_quants() + inventory1.action_view_stock_moves() + self.assertEqual(inventory1.count_stock_moves, 1) + self.assertEqual(inventory1.count_stock_quants, 1) + self.assertEqual(inventory1.count_stock_quants_string, "0 / 1") + self.assertEqual(inventory1.stock_move_ids.qty_done, 26) + self.assertEqual(inventory1.stock_move_ids.product_id.id, self.product2.id) + self.assertEqual(inventory1.stock_move_ids.location_id.id, self.location3.id) + inventory1.action_state_to_done() diff --git a/stock_inventory/views/stock_inventory.xml b/stock_inventory/views/stock_inventory.xml index 20f469b0c0a1..8093595039a4 100644 --- a/stock_inventory/views/stock_inventory.xml +++ b/stock_inventory/views/stock_inventory.xml @@ -82,14 +82,34 @@ attrs="{'readonly':[('state', 'in', ['in_progress', 'done'])]}" required="1" /> + + + + + + + - - -