# -*- coding: utf-8 -*- import json from odoo import models, fields, api from odoo.fields import Domain class LoyaltyReward(models.Model): _inherit = 'loyalty.reward' reward_product_applicability = fields.Selection([ ('specific', 'Specific Product(s)'), ('cheapest', 'Cheapest Product on Order') ], string='Reward Product Applicability', default='specific') reward_product_category_id = fields.Many2one( 'product.category', string='Reward Product Category', help='If specified, only products under this category (including subcategories) will be considered for the cheapest product reward.' ) reward_product_ids_json = fields.Char( compute='_compute_reward_product_ids_json', string='Reward Product IDs JSON' ) @api.depends('reward_product_id', 'reward_product_tag_id', 'reward_type', 'reward_product_applicability', 'reward_product_category_id') def _compute_multi_product(self): # Override completely (no super()) to avoid recursive field invalidation. # In Odoo 19, reassigning a computed field from within its own compute method # (after calling super()) triggers cache invalidation and re-computation, # causing a RecursionError via WeakSet.__iter__ during environment cleanup. for reward in self: tag_products = reward.reward_product_tag_id.product_ids.filtered( lambda product: product.type != 'combo' ) if reward.reward_type == 'product' and reward.reward_product_applicability == 'cheapest': # For 'cheapest' applicability: include direct product + tag products, # but not category products (to avoid large sets and reference cycles). products = reward.reward_product_id + tag_products reward.multi_product = False reward.reward_product_ids = products else: # Default behaviour (mirrors the standard Odoo computation) products = reward.reward_product_id + tag_products reward.multi_product = reward.reward_type == 'product' and len(products) > 1 reward.reward_product_ids = ( products if reward.reward_type == 'product' else self.env['product.product'] ) @api.depends('reward_product_id', 'reward_product_tag_id', 'reward_type', 'reward_product_applicability', 'reward_product_category_id') def _compute_reward_product_ids_json(self): for reward in self: if reward.reward_type == 'product' and reward.reward_product_applicability == 'cheapest': products = reward.reward_product_id + reward.reward_product_tag_id.product_ids.filtered( lambda product: product.type != 'combo' ) if reward.reward_product_category_id: category_ids = self.env['product.category'].search([ ('id', 'child_of', reward.reward_product_category_id.ids) ]).ids cat_products = self.env['product.product'].search([ ('categ_id', 'in', category_ids), ('type', '!=', 'combo') ]) products |= cat_products reward.reward_product_ids_json = json.dumps(products.ids) else: reward.reward_product_ids_json = json.dumps([]) @api.model def _load_pos_data_fields(self, config): fields = super()._load_pos_data_fields(config) if 'reward_product_applicability' not in fields: fields.append('reward_product_applicability') if 'reward_product_category_id' not in fields: fields.append('reward_product_category_id') if 'reward_product_ids_json' not in fields: fields.append('reward_product_ids_json') return fields @api.model def _load_pos_data_domain(self, data, config): reward_product_tag_domain = [ ('reward_product_tag_id', '!=', False), '|', ('reward_product_tag_id.product_template_ids.active', '=', True), ('reward_product_tag_id.product_product_ids.active', '=', True), ] return Domain.AND([ [('program_id', 'in', config._get_program_ids().ids)], Domain.OR([ [('reward_type', '!=', 'product')], [('reward_product_id.active', '=', True)], [('reward_product_applicability', '=', 'cheapest')], reward_product_tag_domain, ]), ])