# Part of Odoo. See LICENSE file for full copyright and licensing details. import ast import json from odoo import api, fields, models from odoo.exceptions import ValidationError from odoo.fields import Domain class LoyaltyReward(models.Model): _name = "loyalty.reward" _description = "Loyalty Reward" _rec_name = "description" _order = "required_points asc" @api.model def default_get(self, fields): # Try to copy the values of the program types default's result = super().default_get(fields) if "program_type" in self.env.context: program_type = self.env.context["program_type"] program_default_values = self.env["loyalty.program"]._program_type_default_values() if ( program_type in program_default_values and len(program_default_values[program_type]["reward_ids"]) == 2 and isinstance(program_default_values[program_type]["reward_ids"][1][2], dict) ): result.update({ k: v for k, v in program_default_values[program_type]["reward_ids"][1][2].items() if k in fields }) return result def _get_discount_mode_select(self): # The value is provided in the loyalty program's view since we may not have a program_id yet # and makes sure to display the currency related to the program instead of the company's. symbol = self.env.context.get("currency_symbol", self.env.company.currency_id.symbol) return [ ("percent", "%"), ("per_order", symbol), ("per_point", self.env._("%s per point", symbol)), ] @api.depends("program_id", "description") def _compute_display_name(self): for reward in self: reward.display_name = f"{reward.program_id.name} - {reward.description}" active = fields.Boolean(default=True) program_id = fields.Many2one( comodel_name="loyalty.program", ondelete="cascade", required=True, index=True ) program_type = fields.Selection(related="program_id.program_type") # Stored for security rules company_id = fields.Many2one(related="program_id.company_id", store=True) currency_id = fields.Many2one(related="program_id.currency_id") description = fields.Char( translate=True, compute="_compute_description", precompute=True, store=True, readonly=False, required=True, ) reward_type = fields.Selection( selection=[("product", "Free Product"), ("discount", "Discount")], required=True, default="discount", ) user_has_debug = fields.Boolean(compute="_compute_user_has_debug") # Discount rewards discount = fields.Float(string="Discount", default=10) discount_mode = fields.Selection( selection=_get_discount_mode_select, required=True, default="percent" ) discount_applicability = fields.Selection( selection=[ ("order", "Order"), ("cheapest", "Cheapest Product"), ("specific", "Specific Products"), ], default="order", ) discount_product_domain = fields.Char(default="[]") discount_product_ids = fields.Many2many( string="Discounted Products", comodel_name="product.product" ) discount_product_category_id = fields.Many2one( string="Discounted Prod. Categories", comodel_name="product.category" ) discount_product_tag_id = fields.Many2one( string="Discounted Prod. Tag", comodel_name="product.tag" ) all_discount_product_ids = fields.Many2many( comodel_name="product.product", compute="_compute_all_discount_product_ids" ) reward_product_domain = fields.Char(compute="_compute_reward_product_domain", store=False) discount_max_amount = fields.Monetary( string="Max Discount", help="This is the max amount this reward may discount, leave to 0 for no limit.", ) discount_line_product_id = fields.Many2one( help="Product used in the sales order to apply the discount. Each reward has its own" " product for reporting purpose", comodel_name="product.product", ondelete="restrict", copy=False, ) is_global_discount = fields.Boolean(compute="_compute_is_global_discount") # Product rewards reward_product_id = fields.Many2one( string="Product", comodel_name="product.product", domain=[("type", "!=", "combo")] ) reward_product_tag_id = fields.Many2one(string="Product Tag", comodel_name="product.tag") multi_product = fields.Boolean(compute="_compute_multi_product") reward_product_ids = fields.Many2many( string="Reward Products", help="These are the products that can be claimed with this rule.", comodel_name="product.product", compute="_compute_multi_product", search="_search_reward_product_ids", ) reward_product_qty = fields.Integer(default=1) reward_product_uom_id = fields.Many2one( comodel_name="uom.uom", compute="_compute_reward_product_uom_id" ) required_points = fields.Float(string="Points needed", default=1) point_name = fields.Char(related="program_id.portal_point_name", readonly=True) clear_wallet = fields.Boolean(default=False) _required_points_positive = models.Constraint( "CHECK (required_points > 0)", "The required points for a reward must be strictly positive." ) _product_qty_positive = models.Constraint( "CHECK (reward_type != 'product' OR reward_product_qty > 0)", "The reward product quantity must be strictly positive.", ) _discount_positive = models.Constraint( "CHECK (reward_type != 'discount' OR discount > 0)", "The discount must be strictly positive.", ) @api.depends("reward_product_id.product_tmpl_id.uom_id", "reward_product_tag_id") def _compute_reward_product_uom_id(self): for reward in self: reward.reward_product_uom_id = reward.reward_product_ids.product_tmpl_id.uom_id[:1] def _find_all_category_children(self, category_id, child_ids): if len(category_id.child_id) > 0: for child_id in category_id.child_id: child_ids.append(child_id.id) self._find_all_category_children(child_id, child_ids) return child_ids def _get_discount_product_domain(self): self.ensure_one() constrains = [] if self.discount_product_ids: constrains.append([("id", "in", self.discount_product_ids.ids)]) if self.discount_product_category_id: product_category_ids = self._find_all_category_children( self.discount_product_category_id, [] ) product_category_ids.append(self.discount_product_category_id.id) constrains.append([("categ_id", "in", product_category_ids)]) if self.discount_product_tag_id: constrains.append([("all_product_tag_ids", "in", self.discount_product_tag_id.id)]) domain = Domain.OR(constrains) if constrains else Domain.TRUE if self.discount_product_domain and self.discount_product_domain != "[]": domain &= Domain(ast.literal_eval(self.discount_product_domain)) return domain @api.model def _get_active_products_domain(self): return [ "|", ("reward_type", "!=", "product"), "&", ("reward_type", "=", "product"), "|", "&", ("reward_product_tag_id", "=", False), ("reward_product_id.active", "=", True), "&", ("reward_product_tag_id", "!=", False), ("reward_product_ids.active", "=", True), ] @api.depends("discount_product_domain") def _compute_reward_product_domain(self): compute_all_discount_product = ( self .env["ir.config_parameter"] .sudo() .get_bool("loyalty.compute_all_discount_product_ids", True) ) for reward in self: if compute_all_discount_product: reward.reward_product_domain = "null" else: reward.reward_product_domain = json.dumps( list(reward._get_discount_product_domain()) ) @api.depends( "discount_product_ids", "discount_product_category_id", "discount_product_tag_id", "discount_product_domain", ) def _compute_all_discount_product_ids(self): compute_all_discount_product = ( self .env["ir.config_parameter"] .sudo() .get_bool("loyalty.compute_all_discount_product_ids", True) ) for reward in self: if compute_all_discount_product: reward.all_discount_product_ids = self.env["product.product"].search( reward._get_discount_product_domain() ) else: reward.all_discount_product_ids = self.env["product.product"] @api.depends("reward_product_id", "reward_product_tag_id", "reward_type") def _compute_multi_product(self): for reward in self: products = reward.reward_product_id + reward.reward_product_tag_id.product_ids.filtered( lambda product: product.type != "combo" ) reward.multi_product = reward.reward_type == "product" and len(products) > 1 reward.reward_product_ids = (reward.reward_type == "product" and products) or self.env[ "product.product" ] def _search_reward_product_ids(self, operator, value): if operator != "in": return NotImplemented return [ "&", ("reward_type", "=", "product"), "|", ("reward_product_id", operator, value), ("reward_product_tag_id.product_ids", operator, value), ] @api.depends( "reward_type", "reward_product_id", "discount_mode", "reward_product_tag_id", "discount", "currency_id", "discount_applicability", "all_discount_product_ids", ) def _compute_description(self): for reward in self: reward_string = "" if reward.program_type == "gift_card": reward_string = reward.env._("Gift Card") elif reward.program_type == "ewallet": reward_string = reward.env._("eWallet") elif reward.reward_type == "product": products = reward.reward_product_ids if len(products) == 0: reward_string = reward.env._("Free Product") elif len(products) == 1: reward_string = reward.env._( "Free Product - %s", reward.reward_product_id.with_context( display_default_code=False ).display_name, ) else: reward_string = reward.env._( "Free Product - [%s]", ", ".join( products.with_context(display_default_code=False).mapped("display_name") ), ) elif reward.reward_type == "discount": format_string = "%(amount)g %(symbol)s" if reward.currency_id.position == "before": format_string = "%(symbol)s %(amount)g" formatted_amount = format_string % { "amount": reward.discount, "symbol": reward.currency_id.symbol, } if reward.discount_mode == "percent": reward_string = reward.env._("%g%% on ", reward.discount) elif reward.discount_mode == "per_point": reward_string = reward.env._("%s per point on ", formatted_amount) elif reward.discount_mode == "per_order": reward_string = reward.env._("%s on ", formatted_amount) if reward.discount_applicability == "order": reward_string += reward.env._("your order") elif reward.discount_applicability == "cheapest": reward_string += reward.env._("the cheapest product") elif reward.discount_applicability == "specific": product_available = self.env["product.product"].search( reward._get_discount_product_domain(), limit=2 ) if len(product_available) == 1: reward_string += product_available.with_context( display_default_code=False ).display_name else: reward_string += reward.env._("specific products") if reward.discount_max_amount: format_string = "%(amount)g %(symbol)s" if reward.currency_id.position == "before": format_string = "%(symbol)s %(amount)g" formatted_amount = format_string % { "amount": reward.discount_max_amount, "symbol": reward.currency_id.symbol, } reward_string += reward.env._(" (Max %s)", formatted_amount) reward.description = reward_string @api.depends("reward_type", "discount_applicability", "discount_mode") def _compute_is_global_discount(self): for reward in self: reward.is_global_discount = ( reward.reward_type == "discount" and reward.discount_applicability == "order" and reward.discount_mode in ["per_order", "percent"] ) @api.depends_context("uid") @api.depends("reward_type") def _compute_user_has_debug(self): self.user_has_debug = self.env.user.has_group("base.group_no_one") @api.constrains("reward_product_id") def _check_reward_product_id_no_combo(self): if any(reward.reward_product_id.type == "combo" for reward in self): raise ValidationError(self.env._('A reward product can\'t be of type "combo".')) def _create_missing_discount_line_products(self): # Make sure we create the product that will be used for our discounts rewards = self.filtered(lambda r: not r.discount_line_product_id) products = self.env["product.product"].create(rewards._get_discount_product_values()) for reward, product in zip(rewards, products): reward.discount_line_product_id = product @api.model_create_multi def create(self, vals_list): res = super().create(vals_list) res._create_missing_discount_line_products() return res def write(self, vals): res = super().write(vals) if "description" in vals: self._create_missing_discount_line_products() # Keep the name of our discount product up to date for reward in self: reward.discount_line_product_id.write({"name": reward.description}) if "active" in vals: if vals["active"]: self.discount_line_product_id.action_unarchive() else: self.discount_line_product_id.action_archive() return res def update_field_translations(self, field_name, translations, source_lang=''): res = super().update_field_translations(field_name, translations, source_lang=source_lang) if field_name == 'description' and self.discount_line_product_id: self.discount_line_product_id.update_field_translations('name', translations, source_lang=source_lang) return res def unlink(self): programs = self.program_id res = super().unlink() # Not guaranteed to trigger the constraint programs._constrains_reward_ids() return res def _get_discount_product_values(self): return [ { "name": reward.description, "type": "service", "sale_ok": False, "purchase_ok": False, "lst_price": 0, "taxes_id": [], "supplier_taxes_id": [], } for reward in self ]