From d446af605fd8143f3ab13beafe38491722cd490f Mon Sep 17 00:00:00 2001 From: DavidJForgeFlow Date: Thu, 11 Apr 2024 12:27:17 +0200 Subject: [PATCH] [IMP] stock_inventory: new small features and ux improvements Allow to create stock.quants making the quants smart button not invisible when quantity is equal to 0. (Removing one of the invisible conditions). [IMP] stock_inventory: adds exclude_sublocation flag [IMP] stock_inventory: Add flag to autocomplete adjustment when fully done. [IMP] stock_inventory Add company to adjustments to avoid multi-company errors. Adds information fields to tree view. Also adds 'Assign to' in stock inventory that propagates to quants. Also propagates the date field. --- stock_inventory/__manifest__.py | 2 + stock_inventory/models/__init__.py | 2 + stock_inventory/models/res_company.py | 15 ++ stock_inventory/models/res_config_settings.py | 12 ++ stock_inventory/models/stock_inventory.py | 143 +++++++++++++++--- stock_inventory/models/stock_quant.py | 32 +++- stock_inventory/security/security.xml | 11 ++ stock_inventory/static/description/index.html | 1 - stock_inventory/tests/test_stock_inventory.py | 95 ++++++++++++ .../views/res_config_settings_view.xml | 28 ++++ stock_inventory/views/stock_inventory.xml | 17 ++- 11 files changed, 328 insertions(+), 30 deletions(-) create mode 100644 stock_inventory/models/res_company.py create mode 100644 stock_inventory/models/res_config_settings.py create mode 100644 stock_inventory/security/security.xml create mode 100644 stock_inventory/views/res_config_settings_view.xml diff --git a/stock_inventory/__manifest__.py b/stock_inventory/__manifest__.py index 16fa4a782e90..2121d8d1bb7a 100644 --- a/stock_inventory/__manifest__.py +++ b/stock_inventory/__manifest__.py @@ -11,9 +11,11 @@ "depends": ["stock"], "data": [ "security/ir.model.access.csv", + "security/security.xml", "views/stock_inventory.xml", "views/stock_quant.xml", "views/stock_move_line.xml", + "views/res_config_settings_view.xml", ], "installable": True, "application": False, diff --git a/stock_inventory/models/__init__.py b/stock_inventory/models/__init__.py index 09732e334e94..b7114e34a3db 100644 --- a/stock_inventory/models/__init__.py +++ b/stock_inventory/models/__init__.py @@ -1,3 +1,5 @@ from . import stock_inventory from . import stock_quant from . import stock_move_line +from . import res_company +from . import res_config_settings diff --git a/stock_inventory/models/res_company.py b/stock_inventory/models/res_company.py new file mode 100644 index 000000000000..1181c7cc9990 --- /dev/null +++ b/stock_inventory/models/res_company.py @@ -0,0 +1,15 @@ +# Copyright 2024 ForgeFlow S.L. (http://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + stock_inventory_auto_complete = fields.Boolean( + help="If enabled, when all the quants prepared for the adjustment " + "are done, the adjustment is automatically set to done.", + default=False, + ) diff --git a/stock_inventory/models/res_config_settings.py b/stock_inventory/models/res_config_settings.py new file mode 100644 index 000000000000..3817c268b445 --- /dev/null +++ b/stock_inventory/models/res_config_settings.py @@ -0,0 +1,12 @@ +# Copyright 2024 ForgeFlow S.L. (http://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + stock_inventory_auto_complete = fields.Boolean( + related="company_id.stock_inventory_auto_complete", readonly=False + ) diff --git a/stock_inventory/models/stock_inventory.py b/stock_inventory/models/stock_inventory.py index 8e96d63fbc16..ec963ef6f227 100644 --- a/stock_inventory/models/stock_inventory.py +++ b/stock_inventory/models/stock_inventory.py @@ -8,9 +8,28 @@ 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", + states={"draft": [("readonly", False)]}, + readonly=True, + ) - date = fields.Datetime(default=lambda self: fields.Datetime.now()) + date = fields.Datetime( + default=lambda self: fields.Datetime.now(), + states={"draft": [("readonly", False)]}, + readonly=True, + ) + + company_id = fields.Many2one( + comodel_name="res.company", + readonly=True, + index=True, + states={"draft": [("readonly", False)]}, + default=lambda self: self.env.company, + required=True, + ) state = fields.Selection( [("draft", "Draft"), ("in_progress", "In Progress"), ("done", "Done")], @@ -22,7 +41,10 @@ class InventoryAdjustmentsGroup(models.Model): ) location_ids = fields.Many2many( - "stock.location", string="Locations", domain="[('usage', '=', 'internal')]" + "stock.location", + string="Locations", + domain="[('usage', '=', 'internal'), " + "'|', ('company_id', '=', company_id), ('company_id', '=', False)]", ) product_selection = fields.Selection( @@ -37,15 +59,24 @@ class InventoryAdjustmentsGroup(models.Model): required=True, ) - product_ids = fields.Many2many("product.product", string="Products") + product_ids = fields.Many2many( + "product.product", + string="Products", + domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]", + ) - stock_quant_ids = fields.Many2many("stock.quant", string="Inventory Adjustment") + stock_quant_ids = fields.Many2many( + "stock.quant", + string="Inventory Adjustment", + domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]", + ) category_id = fields.Many2one("product.category", string="Product Category") lot_ids = fields.Many2many( "stock.production.lot", string="Lot/Serial Numbers", + domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]", ) stock_move_ids = fields.One2many( @@ -66,14 +97,24 @@ class InventoryAdjustmentsGroup(models.Model): compute="_compute_count_stock_moves", string="Stock Moves Lines" ) + exclude_sublocation = fields.Boolean( + help="If enabled, it will only take into account " + "the locations selected, and not their children." + ) + + responsible_id = fields.Many2one( + comodel_name="res.users", + string="Assigned to", + states={"draft": [("readonly", False)]}, + readonly=True, + tracking=True, + help="Specific responsible of Inventory Adjustment.", + ) + @api.depends("stock_quant_ids") def _compute_count_stock_quants(self): self.count_stock_quants = len(self.stock_quant_ids) - count_todo = len( - self.stock_quant_ids.search( - [("id", "in", self.stock_quant_ids.ids), ("to_do", "=", "True")] - ) - ) + count_todo = len(self.stock_quant_ids.filtered(lambda sq: sq.to_do)) self.count_stock_quants_string = "{} / {}".format( count_todo, self.count_stock_quants ) @@ -99,11 +140,15 @@ def _get_quants(self, locations): 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), - ] + return ( + [ + ("location_id", "in", locations.mapped("id")), + ] + if self.exclude_sublocation + else [ + ("location_id", "child_of", locations.child_internal_location_ids.ids), + ] + ) def _get_domain_all_quants(self, base_domain): return base_domain @@ -146,13 +191,15 @@ def _get_domain_category_quants(self, base_domain): ] ) + def refresh_stock_quant_ids(self): + for rec in self: + rec.stock_quant_ids = rec._get_quants(rec.location_ids) + def action_state_to_in_progress(self): 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), + ("location_ids", "child_of", self.location_ids.ids), ], limit=1, ) @@ -164,25 +211,48 @@ def action_state_to_in_progress(self): % active_rec.name ) self.state = "in_progress" - self.stock_quant_ids = self._get_quants(self.location_ids) - self.stock_quant_ids.update({"to_do": True}) + self.refresh_stock_quant_ids() + self.stock_quant_ids.update( + { + "to_do": True, + "user_id": self.responsible_id, + "inventory_date": self.date, + } + ) return def action_state_to_done(self): self.state = "done" - self.stock_quant_ids.update({"to_do": True}) + self.stock_quant_ids.update( + { + "to_do": True, + "user_id": False, + "inventory_date": False, + } + ) + return + + def action_auto_state_to_done(self): + self.ensure_one() + if not any(self.stock_quant_ids.filtered(lambda sq: sq.to_do)): + self.action_state_to_done() return def action_state_to_draft(self): self.state = "draft" - self.stock_quant_ids.update({"to_do": True}) + self.stock_quant_ids.update( + { + "to_do": True, + "user_id": False, + "inventory_date": False, + } + ) self.stock_quant_ids = None return def action_view_inventory_adjustment(self): result = self.env["stock.quant"].action_view_inventory() - ia_ids = self.mapped("stock_quant_ids").ids - result["domain"] = [("id", "in", ia_ids)] + result["domain"] = [("id", "in", self.stock_quant_ids.ids)] result["search_view_id"] = self.env.ref("stock.quant_search_view").id result["context"]["search_default_to_do"] = 1 return result @@ -196,6 +266,31 @@ def action_view_stock_moves(self): result["context"] = [] return result + @api.constrains("state", "location_ids") + def _check_inventory_in_progress_not_override(self): + inventories = self.search([("state", "=", "in_progress")]) + for rec in inventories: + inventory = inventories.filtered( + lambda x: x.id != rec.id + and ( + any(i in x.location_ids for i in rec.location_ids) + or ( + any( + i in x.location_ids.child_internal_location_ids + for i in rec.location_ids + ) + and not x.exclude_sublocation + ) + ) + ) + if len(inventory) > 0: + raise ValidationError( + _( + "Cannot be more than one in progress inventory adjustment " + "affecting the same location at the same time." + ) + ) + @api.constrains("product_selection", "product_ids") def _check_one_product_in_product_selection(self): for rec in self: diff --git a/stock_inventory/models/stock_quant.py b/stock_inventory/models/stock_quant.py index bea534f330b6..bfd04a6c4b99 100644 --- a/stock_inventory/models/stock_quant.py +++ b/stock_inventory/models/stock_quant.py @@ -1,4 +1,4 @@ -from odoo import _, fields, models +from odoo import _, api, fields, models class StockQuant(models.Model): @@ -15,7 +15,10 @@ def _apply_inventory(self): .search([("state", "=", "in_progress")]) .filtered( lambda x: rec.location_id in x.location_ids - or rec.location_id in x.location_ids.child_ids + or ( + rec.location_id in x.location_ids.child_internal_location_ids + and not x.exclude_sublocation + ) ) ) moves = record_moves.search( @@ -36,9 +39,32 @@ def _apply_inventory(self): raise ValueError(_("No move lines have been created")) move = moves[len(moves) - 1] adjustment.stock_move_ids |= move - move.inventory_adjustment_id = adjustment + reference = move.reference + if adjustment.name and move.reference: + reference = adjustment.name + ": " + move.reference + elif adjustment.name: + reference = adjustment.name + move.write( + { + "inventory_adjustment_id": adjustment.id, + "reference": reference, + } + ) rec.to_do = False + if self.env.company.stock_inventory_auto_complete: + adjustment.action_auto_state_to_done() return res def _get_inventory_fields_write(self): return super()._get_inventory_fields_write() + ["to_do"] + + @api.model + def create(self, vals): + res = super().create(vals) + if self.env.context.get( + "active_model", False + ) == "stock.inventory" and self.env.context.get("active_id", False): + self.env["stock.inventory"].browse( + self.env.context.get("active_id") + ).refresh_stock_quant_ids() + return res diff --git a/stock_inventory/security/security.xml b/stock_inventory/security/security.xml new file mode 100644 index 000000000000..11aab9b291b1 --- /dev/null +++ b/stock_inventory/security/security.xml @@ -0,0 +1,11 @@ + + + + Stock Inventory multi-company + + + ['|',('company_id','=',False),('company_id', 'in', company_ids)] + + diff --git a/stock_inventory/static/description/index.html b/stock_inventory/static/description/index.html index 3f92e3c818ce..173226cfae56 100644 --- a/stock_inventory/static/description/index.html +++ b/stock_inventory/static/description/index.html @@ -1,4 +1,3 @@ - diff --git a/stock_inventory/tests/test_stock_inventory.py b/stock_inventory/tests/test_stock_inventory.py index 67b2a976cc51..fcf7680fdb52 100644 --- a/stock_inventory/tests/test_stock_inventory.py +++ b/stock_inventory/tests/test_stock_inventory.py @@ -8,6 +8,7 @@ class TestStockInventory(TransactionCase): def setUp(self): super(TestStockInventory, self).setUp() + self.env.company.stock_inventory_auto_complete = False self.quant_model = self.env["stock.quant"] self.move_model = self.env["stock.move.line"] self.inventory_model = self.env["stock.inventory"] @@ -314,3 +315,97 @@ def test_05_category_selection(self): 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() + + def test_06_exclude_sub_locations(self): + inventory1 = self.inventory_model.create( + { + "name": "Inventory_Test_1", + "product_selection": "all", + "location_ids": [self.location1.id], + "exclude_sublocation": True, + } + ) + inventory1.action_state_to_in_progress() + inventory2 = self.inventory_model.create( + { + "name": "Inventory_Test_2", + "product_selection": "all", + "location_ids": [self.location1.id], + "exclude_sublocation": True, + } + ) + 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], + ) + 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.quant1.inventory_quantity = 92 + self.quant1.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, 8) + self.assertEqual(inventory1.stock_move_ids.product_id.id, self.product.id) + self.assertEqual(inventory1.stock_move_ids.lot_id.id, self.lot_1.id) + self.assertEqual(inventory1.stock_move_ids.location_id.id, self.location1.id) + inventory1.action_state_to_done() + + def test_07_stock_inventory_auto_complete(self): + self.env.company.stock_inventory_auto_complete = True + 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.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() + self.assertEqual(inventory1.count_stock_moves, 2) + self.assertEqual(inventory1.count_stock_quants, 2) + self.assertEqual(inventory1.state, "done") diff --git a/stock_inventory/views/res_config_settings_view.xml b/stock_inventory/views/res_config_settings_view.xml new file mode 100644 index 000000000000..3ff8ec98778c --- /dev/null +++ b/stock_inventory/views/res_config_settings_view.xml @@ -0,0 +1,28 @@ + + + + + res_config_settings_view_form - stock_inventory + res.config.settings + + + +

Stock Inventory

+
+
+
+ +
+
+
+
+
+
+
+
+
diff --git a/stock_inventory/views/stock_inventory.xml b/stock_inventory/views/stock_inventory.xml index 8093595039a4..fed3e75b6602 100644 --- a/stock_inventory/views/stock_inventory.xml +++ b/stock_inventory/views/stock_inventory.xml @@ -41,7 +41,7 @@ name="action_view_inventory_adjustment" class="oe_stat_button" icon="fa-pencil-square-o" - attrs="{'invisible':['|', ('state', 'in', ['draft', 'done']), ('count_stock_quants', '=', 0)]}" + attrs="{'invisible':[('state', 'in', ['draft', 'done'])]}" > + + + + + + + @@ -150,5 +161,7 @@ sequence="30" action="action_view_inventory_group_form" /> - + + +