pos_loyalty_auto_level/models/pos_order.py

154 lines
6.0 KiB
Python

# 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'))