# 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' @api.model def sync_from_ui(self, orders): import time start_time = time.time() _logger.info("POS_PERF: === GLOBAL START sync_from_ui (%s orders) ===", len(orders)) res = super().sync_from_ui(orders) _logger.info("POS_PERF: === GLOBAL FINISH sync_from_ui took %.4f seconds ===", time.time() - start_time) return res @api.model_create_multi def create(self, vals_list): import time start_time = time.time() _logger.info("POS_PERF: Starting pos.order create for %s records", len(vals_list)) res = super().create(vals_list) _logger.info("POS_PERF: pos.order create finished in %.4f seconds", time.time() - start_time) return res def _process_saved_order(self, draft): import time start_time = time.time() _logger.info("POS_PERF: Starting _process_saved_order") res = super()._process_saved_order(draft) _logger.info("POS_PERF: _process_saved_order finished in %.4f seconds", time.time() - start_time) return res def _create_order_picking(self): import time start_time = time.time() _logger.info("POS_PERF: Starting _create_order_picking") res = super()._create_order_picking() _logger.info("POS_PERF: _create_order_picking finished in %.4f seconds", time.time() - start_time) return res def action_pos_order_paid(self): """Override to update the customer's membership level in the background.""" import time start_time = time.time() _logger.info("POS_PERF: Starting action_pos_order_paid for %s orders", len(self)) res = super().action_pos_order_paid() mid_time = time.time() _logger.info("POS_PERF: Super() action_pos_order_paid took %.4f seconds", mid_time - start_time) 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