feat: implement loyalty card membership synchronization and optimize partner list search performance with caching
This commit is contained in:
parent
bbe7748aa1
commit
73c35159fb
@ -1,2 +1,3 @@
|
|||||||
from . import res_partner
|
from . import res_partner
|
||||||
from . import pos_order
|
from . import pos_order
|
||||||
|
from . import loyalty_card
|
||||||
|
|||||||
25
models/loyalty_card.py
Normal file
25
models/loyalty_card.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from odoo import models, api
|
||||||
|
|
||||||
|
class LoyaltyCard(models.Model):
|
||||||
|
_inherit = 'loyalty.card'
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
cards = super().create(vals_list)
|
||||||
|
for card in cards:
|
||||||
|
if card.partner_id and card.active and card.program_id.manual_membership and card.program_id.program_type == 'loyalty':
|
||||||
|
# Set partner's membership level to this program
|
||||||
|
card.partner_id.sudo().write({'membership_level_id': card.program_id.id})
|
||||||
|
return cards
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
res = super().write(vals)
|
||||||
|
if 'active' in vals or 'partner_id' in vals or 'program_id' in vals:
|
||||||
|
for card in self:
|
||||||
|
if card.partner_id and card.active and card.program_id.manual_membership and card.program_id.program_type == 'loyalty':
|
||||||
|
card.partner_id.sudo().write({'membership_level_id': card.program_id.id})
|
||||||
|
elif 'active' in vals and not vals['active']:
|
||||||
|
# Card was archived, if it matched the partner's current membership level, clear it or let auto-leveling recalculate
|
||||||
|
if card.partner_id and card.partner_id.membership_level_id == card.program_id:
|
||||||
|
card.partner_id.sudo().write({'membership_level_id': False})
|
||||||
|
return res
|
||||||
@ -5,14 +5,73 @@ import { patch } from "@web/core/utils/patch";
|
|||||||
import { normalize } from "@web/core/l10n/utils";
|
import { normalize } from "@web/core/l10n/utils";
|
||||||
|
|
||||||
patch(PartnerList.prototype, {
|
patch(PartnerList.prototype, {
|
||||||
|
setup() {
|
||||||
|
super.setup(...arguments);
|
||||||
|
this._filteredLoyaltyCache = new Map();
|
||||||
|
this._partnersSearchCache = new Map();
|
||||||
|
},
|
||||||
|
|
||||||
|
async editPartner() {
|
||||||
|
if (this._filteredLoyaltyCache) {
|
||||||
|
this._filteredLoyaltyCache.clear();
|
||||||
|
}
|
||||||
|
if (this._partnersSearchCache) {
|
||||||
|
this._partnersSearchCache.clear();
|
||||||
|
}
|
||||||
|
return super.editPartner(...arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getNewPartners() {
|
||||||
|
if (this._filteredLoyaltyCache) {
|
||||||
|
this._filteredLoyaltyCache.clear();
|
||||||
|
}
|
||||||
|
if (this._partnersSearchCache) {
|
||||||
|
this._partnersSearchCache.clear();
|
||||||
|
}
|
||||||
|
return super.getNewPartners(...arguments);
|
||||||
|
},
|
||||||
|
|
||||||
getPartners(partners) {
|
getPartners(partners) {
|
||||||
// Filter the partner list to strictly only display partners who are loyalty members
|
if (!this._filteredLoyaltyCache) {
|
||||||
const filteredPartners = partners.filter((p) => p.is_loyalty_member);
|
this._filteredLoyaltyCache = new Map();
|
||||||
|
}
|
||||||
|
if (!this._partnersSearchCache) {
|
||||||
|
this._partnersSearchCache = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Get or compute the filtered loyalty members list for the given partners array reference
|
||||||
|
let filteredInfo = this._filteredLoyaltyCache.get(partners);
|
||||||
|
if (!filteredInfo || filteredInfo.sourceLength !== partners.length) {
|
||||||
|
filteredInfo = {
|
||||||
|
sourceLength: partners.length,
|
||||||
|
data: partners.filter((p) => p.is_loyalty_member),
|
||||||
|
};
|
||||||
|
this._filteredLoyaltyCache.set(partners, filteredInfo);
|
||||||
|
}
|
||||||
|
const filteredPartners = filteredInfo.data;
|
||||||
|
|
||||||
const searchWord = normalize(this.state.query?.trim() ?? "");
|
const searchWord = normalize(this.state.query?.trim() ?? "");
|
||||||
|
|
||||||
|
// 2. Check if we have cached results for this specific query and partners array
|
||||||
|
let queryCache = this._partnersSearchCache.get(partners);
|
||||||
|
if (!queryCache || queryCache.sourceLength !== partners.length) {
|
||||||
|
queryCache = {
|
||||||
|
sourceLength: partners.length,
|
||||||
|
queries: new Map(),
|
||||||
|
};
|
||||||
|
this._partnersSearchCache.set(partners, queryCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedResult = queryCache.queries.get(searchWord);
|
||||||
|
if (cachedResult !== undefined) {
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
let computedResult;
|
||||||
|
|
||||||
if (!searchWord) {
|
if (!searchWord) {
|
||||||
// When query is empty, slice and sort following standard POS behavior
|
// When query is empty, slice and sort following standard POS behavior
|
||||||
return filteredPartners
|
computedResult = filteredPartners
|
||||||
.slice(0, 1000)
|
.slice(0, 1000)
|
||||||
.toSorted((a, b) =>
|
.toSorted((a, b) =>
|
||||||
this.props.partner?.id === a.id
|
this.props.partner?.id === a.id
|
||||||
@ -21,25 +80,39 @@ patch(PartnerList.prototype, {
|
|||||||
? 1
|
? 1
|
||||||
: (a.name || "").localeCompare(b.name || "")
|
: (a.name || "").localeCompare(b.name || "")
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
|
|
||||||
// When query is present, optimize search filtering
|
// When query is present, optimize search filtering
|
||||||
const exactMatches = filteredPartners.filter((partner) => partner.exactMatch(searchWord));
|
const exactMatches = filteredPartners.filter((partner) => partner.exactMatch(searchWord));
|
||||||
if (exactMatches.length > 0) {
|
if (exactMatches.length > 0) {
|
||||||
return exactMatches;
|
computedResult = exactMatches;
|
||||||
}
|
} else {
|
||||||
|
|
||||||
const numberString = searchWord.replace(/[+\s()-]/g, "");
|
const numberString = searchWord.replace(/[+\s()-]/g, "");
|
||||||
const isSearchWordNumber = /^[0-9]+$/.test(numberString);
|
const isSearchWordNumber = /^[0-9]+$/.test(numberString);
|
||||||
const patternBase = isSearchWordNumber ? numberString : searchWord;
|
const patternBase = isSearchWordNumber ? numberString : searchWord;
|
||||||
|
|
||||||
|
const hasWildcard = patternBase.includes("%");
|
||||||
|
const matches = [];
|
||||||
|
|
||||||
|
if (!hasWildcard) {
|
||||||
|
// Fast path: plain substring match
|
||||||
|
for (const p of filteredPartners) {
|
||||||
|
if (!p._normalizedSearchString) {
|
||||||
|
p._normalizedSearchString = normalize(p.searchString || "");
|
||||||
|
}
|
||||||
|
if (p._normalizedSearchString.includes(patternBase)) {
|
||||||
|
matches.push(p);
|
||||||
|
if (matches.length >= 100) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Regex path (fallback when user specifically types %)
|
||||||
const regex = new RegExp(
|
const regex = new RegExp(
|
||||||
patternBase
|
patternBase
|
||||||
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||||
.replace(/%/g, ".*")
|
.replace(/%/g, ".*")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pre-normalize and cache p.searchString, then filter and limit to 100 items to avoid rendering lag
|
|
||||||
const matches = [];
|
|
||||||
for (const p of filteredPartners) {
|
for (const p of filteredPartners) {
|
||||||
if (!p._normalizedSearchString) {
|
if (!p._normalizedSearchString) {
|
||||||
p._normalizedSearchString = normalize(p.searchString || "");
|
p._normalizedSearchString = normalize(p.searchString || "");
|
||||||
@ -51,6 +124,12 @@ patch(PartnerList.prototype, {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return matches;
|
}
|
||||||
|
computedResult = matches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queryCache.queries.set(searchWord, computedResult);
|
||||||
|
return computedResult;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user