feat: patch PosOrder to implement multi-level membership program selection logic

This commit is contained in:
Suherdy Yacob 2026-06-03 10:02:10 +07:00
parent 9768a3795c
commit af99678406

View File

@ -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;
},
});