From e286b6c6e6b06aae72ac7e87c54a0a64bc6d967e Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Mon, 1 Jun 2026 23:51:59 +0700 Subject: [PATCH] feat: implement automated customer membership level adjustment and point consolidation in POS orders --- models/pos_order.py | 120 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/models/pos_order.py b/models/pos_order.py index e69de29..5793ac9 100644 --- a/models/pos_order.py +++ b/models/pos_order.py @@ -0,0 +1,120 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, fields + +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: + # Update total spend + order.partner_id.sudo().total_spend += order.amount_total + + # Update membership level synchronously + order._update_customer_membership_level_logic() + 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: + order._update_customer_membership_level_logic() + 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. Get total purchases from the new total_spend field + total_purchases = partner.total_spend + + # 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 + + # Fallback to the lowest membership level (Membership Silver) if no level matches the spend. + # This prevents auto-assignment from clearing or setting membership_level_id to False. + if not matched_program and loyalty_programs: + matched_program = loyalty_programs[0] + + if not matched_program: + 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: + # Find ALL old cards (with or without points) to clean them up + all_old_cards = self.env['loyalty.card'].sudo().search([ + ('partner_id', '=', partner.id), + ('program_id', 'in', other_programs), + ]) + + if all_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, + }) + + # Transfer non-zero points to the new card, then delete ALL old cards + for old_card in all_old_cards: + pts = old_card.points + if abs(pts) > 0.0001: + new_card.points += pts + + # Transfer loyalty history (point transactions) to the new card + old_histories = self.env['loyalty.history'].sudo().search([ + ('card_id', 'in', all_old_cards.ids) + ]) + if old_histories: + old_histories.write({'card_id': new_card.id}) + + # Transfer POS order line coupon references to the new card + old_pos_lines = self.env['pos.order.line'].sudo().search([ + ('coupon_id', 'in', all_old_cards.ids) + ]) + if old_pos_lines: + old_pos_lines.write({'coupon_id': new_card.id}) + + # Transfer Sales order line coupon references to the new card + old_sale_lines = self.env['sale.order.line'].sudo().search([ + ('coupon_id', 'in', all_old_cards.ids) + ]) + if old_sale_lines: + old_sale_lines.write({'coupon_id': new_card.id}) + + # Archive old-level cards (active=False) instead of deleting, + # because pos_order_line.coupon_id may reference them (FK constraint). + # Archiving hides them from the UI while preserving order history. + all_old_cards.write({'active': False})