diff --git a/models/pos_order.py b/models/pos_order.py index c256977..f258bcf 100644 --- a/models/pos_order.py +++ b/models/pos_order.py @@ -1,107 +1,90 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging -from odoo import api, models +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 after payment.""" + """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: - 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 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) for order in self: 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 - 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. - """ + def _update_customer_membership_level_logic(self): + """Internal logic for membership determination, called by background worker.""" self.ensure_one() partner = self.partner_id - if not partner: - return - - # 1. Calculate total purchases for this customer (only paid/done orders) + + # 1. Calculate total purchases (Optimized SQL SUM) 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( [('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 + # 3. Determine level matched_program = None for program in loyalty_programs: - min_spend = program.minimum_spend or 0.0 - if total_purchases >= min_spend: + if total_purchases >= (program.minimum_spend or 0.0): 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, - ) + # 4. Update level + if partner.membership_level_id.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') 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: - 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 + pts = old_card.points + if abs(pts) > 0.0001: + new_card.points += pts 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')) + """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