From af9967840645b4a00d396643d21284fe1cf4722a Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Wed, 3 Jun 2026 10:02:10 +0700 Subject: [PATCH] feat: patch PosOrder to implement multi-level membership program selection logic --- static/src/app/models/pos_order.js | 91 ++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/static/src/app/models/pos_order.js b/static/src/app/models/pos_order.js index e69de29..b27ccef 100644 --- a/static/src/app/models/pos_order.js +++ b/static/src/app/models/pos_order.js @@ -0,0 +1,91 @@ +/** @odoo-module **/ + +import { PosOrder } from "@point_of_sale/app/models/pos_order"; +import { patch } from "@web/core/utils/patch"; + +/** + * Safely extract a numeric ID from a Many2One field value. + * Handles three forms Odoo may return: + * - raw integer: 5 + * - [id, name] tuple: [5, "Gold"] + * - record object: { id: 5, name: "Gold", ... } + */ +function resolveManyToOneId(value) { + if (!value && value !== 0) { + return null; + } + if (Array.isArray(value)) { + return parseInt(value[0], 10); + } + if (typeof value === 'object') { + return parseInt(value.id, 10); + } + return parseInt(value, 10); +} + +// Track recursion depth to prevent infinite loops when filtering multiLevelPrograms +let _checkingMultiLevel = false; + +patch(PosOrder.prototype, { + _programIsApplicable(program) { + // Evaluate base program applicability first. + const isApplicable = super._programIsApplicable(...arguments); + if (!isApplicable) { + return false; + } + + // Custom Logic for Multi-Level Membership. + // Guard against recursive calls triggered when filtering all programs below. + if (program.multi_level_membership && !_checkingMultiLevel) { + // Retrieve all loyalty programs + const allPrograms = this.models['loyalty.program'].getAll(); + + // Set guard BEFORE filtering to prevent re-entry into this block + _checkingMultiLevel = true; + let multiLevelPrograms; + try { + // Filter programs that have the multi-level flag and pass the base applicability check. + // With the guard set, this._programIsApplicable(p) will skip the custom block, + // effectively calling only the base logic for each candidate program. + multiLevelPrograms = allPrograms.filter( + (p) => p.multi_level_membership && this._programIsApplicable(p) + ); + } finally { + _checkingMultiLevel = false; + } + + // If there are no applicable multi-level programs, block all of them. + if (multiLevelPrograms.length === 0) { + return false; + } + + let bestProgram = null; + const partner = this.getPartner(); + + // If the partner is set and has a membership level, try to match it. + if (partner && partner.membership_level_id) { + const membershipId = resolveManyToOneId(partner.membership_level_id); + if (membershipId) { + // Find the matching program among applicable multi-level programs + bestProgram = multiLevelPrograms.find((p) => p.id === membershipId) || null; + } + } + + // Fallback: pick the program with the smallest minimum_spend + if (!bestProgram) { + bestProgram = multiLevelPrograms.reduce((prev, curr) => { + const prevSpend = prev.minimum_spend || 0; + const currSpend = curr.minimum_spend || 0; + return currSpend < prevSpend ? curr : prev; + }, multiLevelPrograms[0]); + } + + // Only permit the chosen program to be active + if (program.id !== bestProgram.id) { + return false; + } + } + + return true; + }, +});