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..cf01a33aa037 100644 --- a/stock_inventory/models/stock_inventory.py +++ b/stock_inventory/models/stock_inventory.py @@ -1,5 +1,6 @@ from odoo import _, api, fields, models from odoo.exceptions import ValidationError +from odoo.osv import expression class InventoryAdjustmentsGroup(models.Model): @@ -7,9 +8,7 @@ class InventoryAdjustmentsGroup(models.Model): _description = "Inventory Adjustment Group" _order = "date desc, id desc" - name = fields.Char( - required=True, default="Inventory ", string="Inventory Reference" - ) + name = fields.Char(required=True, default="Inventory", string="Inventory Reference") date = fields.Datetime(default=lambda self: fields.Datetime.now()) @@ -18,12 +17,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 +41,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,44 +83,99 @@ def _compute_count_stock_moves(self): sm_ids = self.mapped("stock_move_ids").ids self.count_stock_moves = len(sm_ids) + def _get_quants(self, locations): + domain = [] + base_domain = self._get_base_domain(locations) + if self.product_selection == "all": + domain = self._get_domain_all_quants(base_domain) + elif self.product_selection == "manual": + domain = self._get_domain_manual_quants(base_domain) + elif self.product_selection == "one": + domain = self._get_domain_one_quant(base_domain) + elif self.product_selection == "lot": + domain = self._get_domain_lot_quants(base_domain) + elif self.product_selection == "category": + domain = self._get_domain_category_quants(base_domain) + return self.env["stock.quant"].search(domain) + + def _get_base_domain(self, locations): + return [ + "|", + ("location_id", "in", locations.mapped("id")), + ("location_id", "in", locations.child_ids.ids), + ] + + def _get_domain_all_quants(self, base_domain): + return base_domain + + def _get_domain_manual_quants(self, base_domain): + return expression.AND( + [base_domain, [("product_id", "in", self.product_ids.ids)]] + ) + + def _get_domain_one_quant(self, base_domain): + return expression.AND( + [ + base_domain, + [ + ("product_id", "in", self.product_ids.ids), + ], + ] + ) + + def _get_domain_lot_quants(self, base_domain): + return expression.AND( + [ + base_domain, + [ + ("product_id", "in", self.product_ids.ids), + ("lot_id", "in", self.lot_ids.ids), + ], + ] + ) + + def _get_domain_category_quants(self, base_domain): + return expression.AND( + [ + base_domain, + [ + "|", + ("product_id.categ_id", "=", self.category_id.id), + ("product_id.categ_id", "in", self.category_id.child_id.ids), + ], + ] + ) + def action_state_to_in_progress(self): - active_rec = self.env["stock.inventory"].search([("state", "=", "in_progress")]) + active_rec = self.env["stock.inventory"].search( + [ + ("state", "=", "in_progress"), + "|", + ("location_ids", "in", self.location_ids.mapped("id")), + ("location_ids", "in", self.location_ids.child_ids.ids), + ], + limit=1, + ) if active_rec: raise ValidationError( - _("There's already an Adjustment in Process: %s") % active_rec.name + _( + "There's already an Adjustment in Process using one requested Location: %s" + ) + % active_rec.name ) 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_quants(self.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 +195,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/models/stock_quant.py b/stock_inventory/models/stock_quant.py index 2b7b0980a8d4..3304b9a7ff3a 100644 --- a/stock_inventory/models/stock_quant.py +++ b/stock_inventory/models/stock_quant.py @@ -9,8 +9,15 @@ class StockQuant(models.Model): def _apply_inventory(self): res = super()._apply_inventory() record_moves = self.env["stock.move.line"] - adjustment = self.env["stock.inventory"].search([("state", "=", "in_progress")]) for rec in self: + adjustment = ( + self.env["stock.inventory"] + .search([("state", "=", "in_progress")]) + .filtered( + lambda x: rec.location_id in x.location_ids + or rec.location_id in x.location_ids.child_ids + ) + ) moves = record_moves.search( [ ("product_id", "=", rec.product_id.id), @@ -22,8 +29,7 @@ def _apply_inventory(self): ] ) move = moves[len(moves) - 1] - adjustment.stock_move_ids += move + adjustment.stock_move_ids |= move move.inventory_adjustment_id = adjustment rec.to_do = False - return res 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..a4bc2b0ca0ba 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( @@ -123,6 +125,7 @@ def test_01_all_locations(self): ) with self.assertRaises(ValidationError), self.cr.savepoint(): inventory2.action_state_to_in_progress() + self.assertEqual(inventory1.state, "in_progress") self.assertEqual( inventory1.stock_quant_ids.ids, [self.quant1.id, self.quant3.id, self.quant4.id], @@ -153,28 +156,74 @@ 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], } ) inventory1.action_state_to_in_progress() - inventory2 = self.inventory_model.create( + self.assertEqual(inventory1.state, "in_progress") + 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.state, "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_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_2", - "product_selection": "all", + "name": "Inventory_Test_5", + "product_selection": "one", "location_ids": [self.location1.id], + "product_ids": [self.product.id], } ) - with self.assertRaises(ValidationError), self.cr.savepoint(): - inventory2.action_state_to_in_progress() + 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.state, "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") @@ -197,3 +246,84 @@ 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_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.state, "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.state, "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" /> + + + + + + + - - -