pos_loyalty_auto_level/models/pos_order.py

130 lines
5.5 KiB
Python

# 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 old cards (active or archived, with or without points) to clean them up.
# We use with_context(active_test=False) to include archived cards.
all_old_cards = self.env['loyalty.card'].sudo().with_context(active_test=False).search([
('partner_id', '=', partner.id),
('program_id', 'in', other_programs),
])
if all_old_cards:
# Find the target level card (even if archived) to avoid duplicates.
new_card = self.env['loyalty.card'].sudo().with_context(active_test=False).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,
})
elif not new_card.active:
# Reactivate the card if it was archived
new_card.active = True
# 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})