refactor: move membership level updates to background threads and optimize purchase calculation with SQL aggregation
This commit is contained in:
parent
aa41dc5840
commit
b56303083a
@ -1,107 +1,90 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from odoo import api, models
|
import threading
|
||||||
|
from odoo import api, models, fields
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_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):
|
class PosOrder(models.Model):
|
||||||
_inherit = 'pos.order'
|
_inherit = 'pos.order'
|
||||||
|
|
||||||
def action_pos_order_paid(self):
|
def action_pos_order_paid(self):
|
||||||
"""Override to update the customer's membership level after payment."""
|
"""Override to update the customer's membership level in the background."""
|
||||||
res = super().action_pos_order_paid()
|
res = super().action_pos_order_paid()
|
||||||
for order in self:
|
for order in self:
|
||||||
if order.partner_id:
|
if order.partner_id:
|
||||||
order._update_customer_membership_level()
|
# 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
|
return res
|
||||||
|
|
||||||
def confirm_coupon_programs(self, coupon_data):
|
def confirm_coupon_programs(self, coupon_data):
|
||||||
"""Also update membership after the frontend confirms coupon programs to catch current order's points."""
|
"""Also update membership in background after coupon confirmation."""
|
||||||
res = super().confirm_coupon_programs(coupon_data)
|
res = super().confirm_coupon_programs(coupon_data)
|
||||||
for order in self:
|
for order in self:
|
||||||
if order.partner_id:
|
if order.partner_id:
|
||||||
order._update_customer_membership_level()
|
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
|
return res
|
||||||
|
|
||||||
def _update_customer_membership_level(self):
|
def _update_customer_membership_level_logic(self):
|
||||||
"""Determine and update the customer's loyalty membership level.
|
"""Internal logic for membership determination, called by background worker."""
|
||||||
|
|
||||||
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()
|
self.ensure_one()
|
||||||
partner = self.partner_id
|
partner = self.partner_id
|
||||||
if not partner:
|
|
||||||
return
|
# 1. Calculate total purchases (Optimized SQL SUM)
|
||||||
|
|
||||||
# 1. Calculate total purchases for this customer (only paid/done orders)
|
|
||||||
total_purchases = self._get_customer_total_purchases(partner)
|
total_purchases = self._get_customer_total_purchases(partner)
|
||||||
|
|
||||||
# 2. Get all multi-level loyalty programs sorted by minimum spend ASC
|
# 2. Get multi-level programs
|
||||||
loyalty_programs = self.env['loyalty.program'].sudo().search(
|
loyalty_programs = self.env['loyalty.program'].sudo().search(
|
||||||
[('multi_level_membership', '=', True)],
|
[('multi_level_membership', '=', True)],
|
||||||
order='minimum_spend asc',
|
order='minimum_spend asc',
|
||||||
)
|
)
|
||||||
|
|
||||||
if not loyalty_programs:
|
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
|
return
|
||||||
|
|
||||||
# 3. Determine the appropriate level
|
# 3. Determine level
|
||||||
matched_program = None
|
matched_program = None
|
||||||
for program in loyalty_programs:
|
for program in loyalty_programs:
|
||||||
min_spend = program.minimum_spend or 0.0
|
if total_purchases >= (program.minimum_spend or 0.0):
|
||||||
if total_purchases >= min_spend:
|
|
||||||
matched_program = program
|
matched_program = program
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
if not matched_program:
|
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:
|
if partner.membership_level_id:
|
||||||
partner.sudo().write({'membership_level_id': False})
|
partner.sudo().write({'membership_level_id': False})
|
||||||
return
|
return
|
||||||
|
|
||||||
# 4. Compare with current level and update if different
|
# 4. Update level
|
||||||
current_level_id = partner.membership_level_id.id if partner.membership_level_id else False
|
if partner.membership_level_id.id != matched_program.id:
|
||||||
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})
|
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
|
# 5. Transfer points
|
||||||
all_multi_level_program_ids = loyalty_programs.mapped('id')
|
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]
|
other_programs = [pid for pid in all_multi_level_program_ids if pid != matched_program.id]
|
||||||
|
|
||||||
@ -126,28 +109,14 @@ class PosOrder(models.Model):
|
|||||||
})
|
})
|
||||||
|
|
||||||
for old_card in old_cards:
|
for old_card in old_cards:
|
||||||
pts_to_transfer = old_card.points
|
pts = old_card.points
|
||||||
if abs(pts_to_transfer) > 0.0001: # Simple check to avoid floating point noise transfers
|
if abs(pts) > 0.0001:
|
||||||
new_card.points += pts_to_transfer
|
new_card.points += pts
|
||||||
old_card.points = 0
|
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
|
@api.model
|
||||||
def _get_customer_total_purchases(self, partner):
|
def _get_customer_total_purchases(self, partner):
|
||||||
"""Calculate the total amount of paid/done POS orders for a given partner.
|
"""SQL SUM aggregation for high performance."""
|
||||||
|
domain = [('partner_id', '=', partner.id), ('state', 'in', ('paid', 'done'))]
|
||||||
:param partner: res.partner record
|
res = self.env['pos.order'].sudo()._read_group(domain, aggregates=['amount_total:sum'])
|
||||||
:returns: float total of amount_total across all qualifying orders
|
return res[0][0] if res else 0.0
|
||||||
"""
|
|
||||||
orders = self.sudo().search([
|
|
||||||
('partner_id', '=', partner.id),
|
|
||||||
('state', 'in', ('paid', 'done')),
|
|
||||||
])
|
|
||||||
return sum(orders.mapped('amount_total'))
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user