# Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, models, fields class PosOrder(models.Model): _inherit = 'pos.order' def action_pos_order_paid(self): """Override to update the customer's membership level in the background.""" res = super().action_pos_order_paid() for order in self: if order.partner_id: # Update total spend order.partner_id.sudo().total_spend += order.amount_total # Update membership level synchronously order._update_customer_membership_level_logic() return res def confirm_coupon_programs(self, coupon_data): """Also update membership in background after coupon confirmation.""" res = super().confirm_coupon_programs(coupon_data) for order in self: if order.partner_id: order._update_customer_membership_level_logic() return res def _update_customer_membership_level_logic(self): """Internal logic for membership determination, called by background worker.""" self.ensure_one() partner = self.partner_id # If the customer has a manual membership level assigned, do not auto-change it. if partner.membership_level_id and partner.membership_level_id.manual_membership: return # 1. Get total purchases from the new total_spend field total_purchases = partner.total_spend # 2. Get multi-level programs that are not manual-only loyalty_programs = self.env['loyalty.program'].sudo().search( [('multi_level_membership', '=', True), ('manual_membership', '=', False)], order='minimum_spend asc', ) if not loyalty_programs: return # 3. Determine level matched_program = None for program in loyalty_programs: if total_purchases >= (program.minimum_spend or 0.0): matched_program = program else: break # Fallback to the lowest membership level (Membership Silver) if no level matches the spend. # This prevents auto-assignment from clearing or setting membership_level_id to False. if not matched_program and loyalty_programs: matched_program = loyalty_programs[0] if not matched_program: return # 4. Update level if partner.membership_level_id.id != matched_program.id: partner.sudo().write({'membership_level_id': matched_program.id}) # 5. Transfer points 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: # Find ALL active old cards (with or without points) to clean them up all_old_cards = self.env['loyalty.card'].sudo().search([ ('partner_id', '=', partner.id), ('program_id', 'in', other_programs), ('active', '=', True), ]) if all_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, }) # Transfer non-zero points to the new card, then archive ALL old cards for old_card in all_old_cards: pts = old_card.points if abs(pts) > 0.0001: new_card.points += pts old_card.points = 0.0 # Transfer loyalty history (point transactions) to the new card old_histories = self.env['loyalty.history'].sudo().search([ ('card_id', 'in', all_old_cards.ids) ]) if old_histories: old_histories.write({'card_id': new_card.id}) # Transfer POS order line coupon references to the new card old_pos_lines = self.env['pos.order.line'].sudo().search([ ('coupon_id', 'in', all_old_cards.ids) ]) if old_pos_lines: old_pos_lines.write({'coupon_id': new_card.id}) # Transfer Sales order line coupon references to the new card old_sale_lines = self.env['sale.order.line'].sudo().search([ ('coupon_id', 'in', all_old_cards.ids) ]) if old_sale_lines: old_sale_lines.write({'coupon_id': new_card.id}) # Archive old-level cards (active=False) instead of deleting, # because pos_order_line.coupon_id may reference them (FK constraint). # Archiving hides them from the UI while preserving order history. all_old_cards.write({'active': False, 'points': 0.0})