# Part of Odoo. See LICENSE file for full copyright and licensing details. import logging from odoo import api, models _logger = logging.getLogger(__name__) class PosOrder(models.Model): _inherit = 'pos.order' def action_pos_order_paid(self): """Override to update the customer's membership level after payment.""" res = super().action_pos_order_paid() for order in self: if order.partner_id: order._update_customer_membership_level() return res def confirm_coupon_programs(self, coupon_data): """Also update membership after the frontend confirms coupon programs to catch current order's points.""" res = super().confirm_coupon_programs(coupon_data) for order in self: if order.partner_id: order._update_customer_membership_level() return res def _update_customer_membership_level(self): """Determine and update the customer's loyalty membership level. 1. Sum all paid pos.order amount_total for this partner. 2. Fetch all loyalty.program where multi_level_membership = True, ordered by minimum_spend ascending. 3. Find the highest level the customer qualifies for (highest minimum_spend <= total purchases). 4. Update res.partner.membership_level_id if it differs. 5. Consolidate points from other tier cards. """ self.ensure_one() partner = self.partner_id if not partner: return # 1. Calculate total purchases for this customer (only paid/done orders) total_purchases = self._get_customer_total_purchases(partner) # 2. Get all multi-level loyalty programs sorted by minimum spend ASC loyalty_programs = self.env['loyalty.program'].sudo().search( [('multi_level_membership', '=', True)], order='minimum_spend asc', ) if not loyalty_programs: _logger.info( 'No multi-level loyalty programs found. ' 'Skipping membership level update for partner %s (ID: %s).', partner.name, partner.id, ) return # 3. Determine the appropriate level matched_program = None for program in loyalty_programs: min_spend = program.minimum_spend or 0.0 if total_purchases >= min_spend: matched_program = program else: break if not matched_program: _logger.info( 'Customer %s (ID: %s) total purchases %.2f do not meet ' 'the minimum spend of any multi-level loyalty program. ' 'Clearing membership level.', partner.name, partner.id, total_purchases, ) if partner.membership_level_id: partner.sudo().write({'membership_level_id': False}) return # 4. Compare with current level and update if different current_level_id = partner.membership_level_id.id if partner.membership_level_id else False if current_level_id != matched_program.id: _logger.info( 'Updating membership level for customer %s (ID: %s): ' '%s -> %s (total purchases: %.2f)', partner.name, partner.id, partner.membership_level_id.name if partner.membership_level_id else 'None', matched_program.name, total_purchases, ) partner.sudo().write({'membership_level_id': matched_program.id}) else: _logger.debug( 'Membership level unchanged for customer %s (ID: %s): ' '%s (total purchases: %.2f)', partner.name, partner.id, matched_program.name, total_purchases, ) # 5. Transfer points from old loyalty card(s) to new loyalty card all_multi_level_program_ids = loyalty_programs.mapped('id') other_programs = [pid for pid in all_multi_level_program_ids if pid != matched_program.id] if other_programs: old_cards = self.env['loyalty.card'].sudo().search([ ('partner_id', '=', partner.id), ('program_id', 'in', other_programs), ('points', '!=', 0), ]) if old_cards: new_card = self.env['loyalty.card'].sudo().search([ ('partner_id', '=', partner.id), ('program_id', '=', matched_program.id), ], limit=1) if not new_card: new_card = self.env['loyalty.card'].sudo().create({ 'partner_id': partner.id, 'program_id': matched_program.id, 'points': 0, }) for old_card in old_cards: pts_to_transfer = old_card.points if abs(pts_to_transfer) > 0.0001: # Simple check to avoid floating point noise transfers new_card.points += pts_to_transfer old_card.points = 0 _logger.info( 'Transferred %s points from loyalty program "%s" to "%s" for customer %s (ID: %s)', pts_to_transfer, old_card.program_id.name, new_card.program_id.name, partner.name, partner.id, ) @api.model def _get_customer_total_purchases(self, partner): """Calculate the total amount of paid/done POS orders for a given partner. :param partner: res.partner record :returns: float total of amount_total across all qualifying orders """ orders = self.sudo().search([ ('partner_id', '=', partner.id), ('state', 'in', ('paid', 'done')), ]) return sum(orders.mapped('amount_total'))