pos_loyalty_auto_level/models/pos_order.py

123 lines
4.6 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import threading
from odoo import api, models, fields
_logger = logging.getLogger(__name__)
def _threaded_membership_update(registry, uid, context, order_id):
"""Background worker to update membership levels without blocking the payment transaction."""
try:
with registry.cursor() as new_cr:
new_env = api.Environment(new_cr, uid, context)
order = new_env['pos.order'].browse(order_id)
if not order.exists() or not order.partner_id:
return
order._update_customer_membership_level_logic()
new_cr.commit()
except Exception as e:
_logger.error("Background membership update failed: %s", e)
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:
# Dispatch to background thread
thread = threading.Thread(
target=_threaded_membership_update,
args=(self.env.registry, self.env.uid, self.env.context, order.id)
)
thread.daemon = True
thread.start()
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:
thread = threading.Thread(
target=_threaded_membership_update,
args=(self.env.registry, self.env.uid, self.env.context, order.id)
)
thread.daemon = True
thread.start()
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
# 1. Calculate total purchases (Optimized SQL SUM)
total_purchases = self._get_customer_total_purchases(partner)
# 2. Get multi-level programs
loyalty_programs = self.env['loyalty.program'].sudo().search(
[('multi_level_membership', '=', True)],
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
if not matched_program:
if partner.membership_level_id:
partner.sudo().write({'membership_level_id': False})
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:
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 = old_card.points
if abs(pts) > 0.0001:
new_card.points += pts
old_card.points = 0
@api.model
def _get_customer_total_purchases(self, partner):
"""SQL SUM aggregation for high performance."""
domain = [('partner_id', '=', partner.id), ('state', 'in', ('paid', 'done'))]
res = self.env['pos.order'].sudo()._read_group(domain, aggregates=['amount_total:sum'])
return res[0][0] if res else 0.0