From 313531a20c31bc218daac33d6b18c515897ddc3c Mon Sep 17 00:00:00 2001 From: Benjamin Willig Date: Wed, 7 Feb 2024 16:57:50 +0100 Subject: [PATCH] Various fixes and improvements for stock_inventory - fixed the way to link the move to the inventory, loading all move lines linked to a product or a lot for a given location was not right - fixed computed methods - do not use ValidationError if not inside a python constrains - use states field attribute to avoid duplicating visibility condition - missing ensure_one - added cancel state to match with old odoo inventory --- stock_inventory/models/stock_inventory.py | 143 +++++++++++++++--- stock_inventory/models/stock_quant.py | 6 +- stock_inventory/tests/test_stock_inventory.py | 29 ++-- stock_inventory/views/stock_inventory.xml | 34 +++-- 4 files changed, 161 insertions(+), 51 deletions(-) diff --git a/stock_inventory/models/stock_inventory.py b/stock_inventory/models/stock_inventory.py index 8f5903e86784..a5d657561b75 100644 --- a/stock_inventory/models/stock_inventory.py +++ b/stock_inventory/models/stock_inventory.py @@ -1,7 +1,11 @@ from odoo import _, api, fields, models -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError from odoo.osv import expression +READONLY_STATES = { + "draft": [("readonly", False)], +} + class InventoryAdjustmentsGroup(models.Model): _name = "stock.inventory" @@ -15,14 +19,14 @@ class InventoryAdjustmentsGroup(models.Model): required=True, default="Inventory", string="Inventory Reference", - states={"draft": [("readonly", False)]}, readonly=True, + states=READONLY_STATES, ) date = fields.Datetime( default=lambda self: fields.Datetime.now(), - states={"draft": [("readonly", False)]}, readonly=True, + states=READONLY_STATES, ) company_id = fields.Many2one( @@ -35,13 +39,22 @@ class InventoryAdjustmentsGroup(models.Model): ) state = fields.Selection( - [("draft", "Draft"), ("in_progress", "In Progress"), ("done", "Done")], + [ + ("draft", "Draft"), + ("in_progress", "In Progress"), + ("done", "Done"), + ("cancel", "Cancelled"), + ], default="draft", tracking=True, ) owner_id = fields.Many2one( - "res.partner", "Owner", help="This is the owner of the inventory adjustment" + "res.partner", + "Owner", + help="This is the owner of the inventory adjustment", + readonly=True, + states=READONLY_STATES, ) location_ids = fields.Many2many( @@ -49,6 +62,8 @@ class InventoryAdjustmentsGroup(models.Model): string="Locations", domain="[('usage', '=', 'internal'), " "'|', ('company_id', '=', company_id), ('company_id', '=', False)]", + readonly=True, + states=READONLY_STATES, ) product_selection = fields.Selection( @@ -61,32 +76,47 @@ class InventoryAdjustmentsGroup(models.Model): ], default="all", required=True, + readonly=True, + states=READONLY_STATES, ) product_ids = fields.Many2many( "product.product", string="Products", domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]", + readonly=True, + states=READONLY_STATES, ) stock_quant_ids = fields.Many2many( "stock.quant", string="Inventory Adjustment", domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]", + readonly=True, + states=READONLY_STATES, ) - category_id = fields.Many2one("product.category", string="Product Category") + category_id = fields.Many2one( + "product.category", + string="Product Category", + readonly=True, + states=READONLY_STATES, + ) lot_ids = fields.Many2many( "stock.lot", string="Lot/Serial Numbers", domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]", + readonly=True, + states=READONLY_STATES, ) stock_move_ids = fields.One2many( "stock.move.line", "inventory_adjustment_id", string="Inventory Adjustments Done", + readonly=True, + states=READONLY_STATES, ) count_stock_quants = fields.Integer( @@ -100,6 +130,9 @@ class InventoryAdjustmentsGroup(models.Model): count_stock_moves = fields.Integer( compute="_compute_count_stock_moves", string="Stock Moves Lines" ) + action_state_to_cancel_allowed = fields.Boolean( + compute="_compute_action_state_to_cancel_allowed" + ) exclude_sublocation = fields.Boolean( help="If enabled, it will only take into account " @@ -117,18 +150,38 @@ class InventoryAdjustmentsGroup(models.Model): @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.filtered(lambda sq: sq.to_do)) - self.count_stock_quants_string = "{} / {}".format( - count_todo, self.count_stock_quants - ) + for rec in self: + quants = rec.stock_quant_ids + quants_to_do = quants.filtered(lambda q: q.to_do) + count_todo = len(quants_to_do) + rec.count_stock_quants = len(quants) + rec.count_stock_quants_string = "{} / {}".format( + count_todo, rec.count_stock_quants + ) @api.depends("stock_move_ids") def _compute_count_stock_moves(self): - sm_ids = self.mapped("stock_move_ids").ids - self.count_stock_moves = len(sm_ids) + group_fname = "inventory_adjustment_id" + group_data = self.env["stock.move.line"].read_group( + [ + (group_fname, "in", self.ids), + ], + [group_fname], + [group_fname], + ) + data_by_adj_id = { + row[group_fname][0]: row.get(f"{group_fname}_count", 0) + for row in group_data + } + for rec in self: + rec.count_stock_moves = data_by_adj_id.get(rec.id, 0) + + def _compute_action_state_to_cancel_allowed(self): + for rec in self: + rec.action_state_to_cancel_allowed = rec.state == "draft" def _get_quants(self, locations): + self.ensure_one() domain = [] base_domain = self._get_base_domain(locations) if self.product_selection == "all": @@ -158,11 +211,13 @@ def _get_domain_all_quants(self, base_domain): return base_domain def _get_domain_manual_quants(self, base_domain): + self.ensure_one() return expression.AND( [base_domain, [("product_id", "in", self.product_ids.ids)]] ) def _get_domain_one_quant(self, base_domain): + self.ensure_one() return expression.AND( [ base_domain, @@ -173,6 +228,7 @@ def _get_domain_one_quant(self, base_domain): ) def _get_domain_lot_quants(self, base_domain): + self.ensure_one() return expression.AND( [ base_domain, @@ -184,6 +240,7 @@ def _get_domain_lot_quants(self, base_domain): ) def _get_domain_category_quants(self, base_domain): + self.ensure_one() return expression.AND( [ base_domain, @@ -200,6 +257,7 @@ def refresh_stock_quant_ids(self): rec.stock_quant_ids = rec._get_quants(rec.location_ids) def action_state_to_in_progress(self): + self.ensure_one() active_rec = self.env["stock.inventory"].search( [ ("state", "=", "in_progress"), @@ -208,15 +266,20 @@ def action_state_to_in_progress(self): limit=1, ) if active_rec: - raise ValidationError( + raise UserError( _( "There's already an Adjustment in Process using one requested Location: %s" ) % active_rec.name ) - self.state = "in_progress" - self.refresh_stock_quant_ids() - self.stock_quant_ids.update( + quants = self._get_quants(self.location_ids) + self.write( + { + "state": "in_progress", + "stock_quant_ids": [(6, 0, quants.ids)], + } + ) + quants.write( { "to_do": True, "user_id": self.responsible_id, @@ -226,6 +289,7 @@ def action_state_to_in_progress(self): return def action_state_to_done(self): + self.ensure_one() self.state = "done" self.stock_quant_ids.update( { @@ -243,6 +307,7 @@ def action_auto_state_to_done(self): return def action_state_to_draft(self): + self.ensure_one() self.state = "draft" self.stock_quant_ids.update( { @@ -254,20 +319,52 @@ def action_state_to_draft(self): self.stock_quant_ids = None return + def action_state_to_cancel(self): + self.ensure_one() + self._check_action_state_to_cancel() + self.write( + { + "state": "cancel", + } + ) + + def _check_action_state_to_cancel(self): + for rec in self: + if not rec.action_state_to_cancel_allowed: + raise UserError( + _( + "You can't cancel this inventory %(display_name)s.", + display_name=rec.display_name, + ) + ) + def action_view_inventory_adjustment(self): + self.ensure_one() result = self.env["stock.quant"].action_view_inventory() - 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 + ia_ids = self.mapped("stock_quant_ids").ids + context = result.get("context", {}) + context.update( + { + "search_default_to_do": 1, + "inventory_id": self.id, + } + ) + result.update( + { + "domain": [("id", "in", ia_ids)], + "search_view_id": self.env.ref("stock.quant_search_view").id, + "context": context, + } + ) return result def action_view_stock_moves(self): + self.ensure_one() result = self.env["ir.actions.act_window"]._for_xml_id( "stock_inventory.action_view_stock_move_line_inventory_tree" ) - sm_ids = self.mapped("stock_move_ids").ids - result["domain"] = [("id", "in", sm_ids)] - result["context"] = [] + result["domain"] = [("inventory_adjustment_id", "=", self.id)] + result["context"] = {} return result @api.constrains("state", "location_ids") diff --git a/stock_inventory/models/stock_quant.py b/stock_inventory/models/stock_quant.py index 81ea00132297..cf0246c631ca 100644 --- a/stock_inventory/models/stock_quant.py +++ b/stock_inventory/models/stock_quant.py @@ -58,9 +58,9 @@ def _apply_inventory(self): def _get_inventory_fields_write(self): return super()._get_inventory_fields_write() + ["to_do"] - @api.model - def create(self, vals): - res = super().create(vals) + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) if self.env.context.get( "active_model", False ) == "stock.inventory" and self.env.context.get("active_id", False): diff --git a/stock_inventory/tests/test_stock_inventory.py b/stock_inventory/tests/test_stock_inventory.py index 24ce73aa25f0..3219b2ea2de7 100644 --- a/stock_inventory/tests/test_stock_inventory.py +++ b/stock_inventory/tests/test_stock_inventory.py @@ -1,7 +1,7 @@ # Copyright 2022 ForgeFlow S.L # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError from odoo.tests.common import TransactionCase @@ -121,7 +121,7 @@ def test_01_all_locations(self): "location_ids": [self.location1.id], } ) - with self.assertRaises(ValidationError), self.cr.savepoint(): + with self.assertRaises(UserError), self.cr.savepoint(): inventory2.action_state_to_in_progress() self.assertEqual(inventory1.state, "in_progress") self.assertEqual( @@ -137,7 +137,7 @@ def test_01_all_locations(self): inventory1.action_view_inventory_adjustment() self.quant1.inventory_quantity = 92 self.quant1.action_apply_inventory() - inventory1._compute_count_stock_quants() + inventory1.invalidate_recordset() inventory1.action_view_stock_moves() self.assertEqual(inventory1.count_stock_moves, 1) self.assertEqual(inventory1.count_stock_quants, 3) @@ -172,8 +172,7 @@ def test_02_manual_selection(self): 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() + inventory1.invalidate_recordset() self.assertEqual(inventory1.count_stock_moves, 1) self.assertEqual(inventory1.count_stock_quants, 2) self.assertEqual(inventory1.count_stock_quants_string, "1 / 2") @@ -183,15 +182,15 @@ def test_02_manual_selection(self): 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() + inventory1.invalidate_recordset() 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): - with self.assertRaises(ValidationError), self.cr.savepoint(): - inventory1 = self.inventory_model.create( + with self.assertRaises(UserError), self.cr.savepoint(): + self.inventory_model.create( { "name": "Inventory_Test_5", "product_selection": "one", @@ -222,7 +221,7 @@ def test_03_one_selection(self): inventory1.action_view_inventory_adjustment() self.quant3.inventory_quantity = 74 self.quant3.action_apply_inventory() - inventory1._compute_count_stock_quants() + inventory1.invalidate_recordset() inventory1.action_view_stock_moves() self.assertEqual(inventory1.count_stock_moves, 1) self.assertEqual(inventory1.count_stock_quants, 2) @@ -233,15 +232,15 @@ def test_03_one_selection(self): 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() + inventory1.invalidate_recordset() 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): - with self.assertRaises(ValidationError), self.cr.savepoint(): - inventory1 = self.inventory_model.create( + with self.assertRaises(UserError), self.cr.savepoint(): + self.inventory_model.create( { "name": "Inventory_Test_6", "product_selection": "lot", @@ -272,7 +271,7 @@ def test_04_lot_selection(self): inventory1.action_view_inventory_adjustment() self.quant3.inventory_quantity = 74 self.quant3.action_apply_inventory() - inventory1._compute_count_stock_quants() + inventory1.invalidate_recordset() inventory1.action_view_stock_moves() self.assertEqual(inventory1.count_stock_moves, 1) self.assertEqual(inventory1.count_stock_quants, 1) @@ -306,7 +305,7 @@ def test_05_category_selection(self): inventory1.action_view_inventory_adjustment() self.quant4.inventory_quantity = 74 self.quant4.action_apply_inventory() - inventory1._compute_count_stock_quants() + inventory1.invalidate_recordset() inventory1.action_view_stock_moves() self.assertEqual(inventory1.count_stock_moves, 1) self.assertEqual(inventory1.count_stock_quants, 1) @@ -334,7 +333,7 @@ def test_06_exclude_sub_locations(self): "exclude_sublocation": True, } ) - with self.assertRaises(ValidationError), self.cr.savepoint(): + with self.assertRaises(UserError), self.cr.savepoint(): inventory2.action_state_to_in_progress() self.assertEqual(inventory1.state, "in_progress") self.assertEqual( diff --git a/stock_inventory/views/stock_inventory.xml b/stock_inventory/views/stock_inventory.xml index 44b81f8ba76a..a4394b587530 100644 --- a/stock_inventory/views/stock_inventory.xml +++ b/stock_inventory/views/stock_inventory.xml @@ -20,6 +20,13 @@ attrs="{'invisible':['|',('state', 'in', ['draft', 'done']), ('count_stock_moves', '!=', 0)]}" string="Back to Draft" /> + +