from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError from odoo.osv import expression READONLY_STATES = { "draft": [("readonly", False)], } class InventoryAdjustmentsGroup(models.Model): _name = "stock.inventory" _description = "Inventory Adjustment Group" _order = "date desc, id desc" _inherit = [ "mail.thread", ] name = fields.Char( required=True, default="Inventory", string="Inventory Reference", readonly=True, states=READONLY_STATES, ) date = fields.Datetime( default=lambda self: fields.Datetime.now(), readonly=True, states=READONLY_STATES, ) 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"), ("cancel", "Cancelled"), ], default="draft", tracking=True, ) owner_id = fields.Many2one( "res.partner", "Owner", help="This is the owner of the inventory adjustment", readonly=True, states=READONLY_STATES, ) location_ids = fields.Many2many( "stock.location", string="Locations", domain="[('usage', '=', 'internal'), " "'|', ('company_id', '=', company_id), ('company_id', '=', False)]", readonly=True, states=READONLY_STATES, ) product_selection = fields.Selection( [ ("all", "All Products"), ("manual", "Manual Selection"), ("category", "Product Category"), ("one", "One Product"), ("lot", "Lot/Serial Number"), ], 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", 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( compute="_compute_count_stock_quants", string="# Adjustments" ) count_stock_quants_string = fields.Char( compute="_compute_count_stock_quants", string="Adjustments" ) 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 " "the locations selected, and not their children." ) responsible_id = fields.Many2one( comodel_name="res.users", string="Assigned to", states={"draft": [("readonly", False)]}, readonly=True, help="Specific responsible of Inventory Adjustment.", ) products_under_review_ids = fields.Many2many( comodel_name="product.product", compute="_compute_products_under_review_ids", search="_search_products_under_review_ids", string="Products Under Review", relation="stock_inventory_product_review_rel", ) def _search_products_under_review_ids(self, operator, value): quants = self.env["stock.quant"].search( [("to_do", "=", True), ("product_id", operator, value)] ) inventories = quants.mapped("stock_inventory_ids") return [("id", "in", inventories.ids), ("state", "=", "in_progress")] @api.depends("stock_quant_ids", "stock_quant_ids.to_do", "state") def _compute_products_under_review_ids(self): for record in self: if record.state == "in_progress": products = record.stock_quant_ids.filtered( lambda quant: quant.to_do ).mapped("product_id") record.products_under_review_ids = ( [(6, 0, products.ids)] if products else [(5, 0, 0)] ) else: record.products_under_review_ids = [(5, 0, 0)] @api.depends("stock_quant_ids") def _compute_count_stock_quants(self): for rec in self: quants = rec.stock_quant_ids quants_to_do = quants.filtered(lambda q: q.to_do) quants_pending_to_review = quants_to_do.filtered( lambda q: q.current_inventory_id.id == rec.id ) count_pending_to_review = len(quants_pending_to_review) rec.count_stock_quants = len(quants) rec.count_stock_quants_string = "{} / {}".format( count_pending_to_review, rec.count_stock_quants ) @api.depends("stock_move_ids") def _compute_count_stock_moves(self): 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": 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")), ] 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 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, [ ("product_id", "in", self.product_ids.ids), ], ] ) def _get_domain_lot_quants(self, base_domain): self.ensure_one() 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): self.ensure_one() 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 refresh_stock_quant_ids(self): for rec in self: rec.stock_quant_ids = rec._get_quants(rec.location_ids) def _get_quant_joined_names(self, quants, field): return ", ".join(quants.mapped(f"{field}.display_name")) def action_state_to_in_progress(self): self.ensure_one() search_filter = [ ( "location_id", "child_of" if not self.exclude_sublocation else "in", self.location_ids.ids, ), ("to_do", "=", True), ] if self.product_ids: search_filter.append(("product_id", "in", self.product_ids.ids)) error_field = "product_id" error_message = _( "There are active adjustments for the requested products: %(names)s. " "Blocking adjustments: %(blocking_names)s" ) else: error_field = "location_id" error_message = _( "There's already an Adjustment in Process " "using one requested Location: %(names)s. " "Blocking adjustments: %(blocking_names)s" ) quants = self.env["stock.quant"].search(search_filter) if quants: inventory_ids = self.env["stock.inventory"].search( [("stock_quant_ids", "in", quants.ids), ("state", "=", "in_progress")] ) if inventory_ids: blocking_names = ", ".join(inventory_ids.mapped("name")) names = self._get_quant_joined_names(quants, error_field) raise ValidationError( error_message % {"names": names, "blocking_names": blocking_names} ) 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, "inventory_date": self.date, "current_inventory_id": self.id, } ) return def action_state_to_done(self): self.ensure_one() self.state = "done" self.stock_quant_ids.filtered( lambda q: q.current_inventory_id.id == self.id ).update( { "to_do": False, "user_id": False, "inventory_date": False, "current_inventory_id": 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.ensure_one() self.state = "draft" self.stock_quant_ids.filtered( lambda q: q.current_inventory_id.id == self.id ).update( { "to_do": False, "user_id": False, "inventory_date": False, "current_inventory_id": False, } ) 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() context = result.get("context", {}) context.update( { "search_default_to_do": 1, "inventory_id": self.id, "default_to_do": True, } ) result.update( { "domain": [ ("id", "in", self.stock_quant_ids.ids), ("current_inventory_id", "=", self.id), ], "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" ) result["domain"] = [("inventory_adjustment_id", "=", self.id)] result["context"] = {} return result def _check_inventory_in_progress_not_override(self): for rec in self: if rec.state == "in_progress": location_condition = [ ( "location_ids", "child_of" if not rec.exclude_sublocation else "in", rec.location_ids.ids, ) ] if rec.product_ids: product_condition = [ ("state", "=", "in_progress"), ("id", "!=", rec.id), ("product_ids", "in", rec.product_ids.ids), ] + location_condition inventories = self.search(product_condition) else: inventories = self.search( [("state", "=", "in_progress"), ("id", "!=", rec.id)] + location_condition ) for inventory in inventories: if any( i in inventory.location_ids.ids for i in rec.location_ids.ids ): raise ValidationError( _( "Cannot have more than one in-progress inventory adjustment " "affecting the same location or product at the same time." ) ) @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." ) )