diff --git a/models/__init__.py b/models/__init__.py index e6d2229..0a17b64 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,2 +1,3 @@ from . import res_partner from . import pos_order +from . import loyalty_card diff --git a/models/loyalty_card.py b/models/loyalty_card.py new file mode 100644 index 0000000..06cf710 --- /dev/null +++ b/models/loyalty_card.py @@ -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 diff --git a/static/src/app/screens/partner_list_patch.js b/static/src/app/screens/partner_list_patch.js index 89aa1c9..fabdb1f 100644 --- a/static/src/app/screens/partner_list_patch.js +++ b/static/src/app/screens/partner_list_patch.js @@ -5,14 +5,73 @@ import { patch } from "@web/core/utils/patch"; import { normalize } from "@web/core/l10n/utils"; 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) { - // Filter the partner list to strictly only display partners who are loyalty members - const filteredPartners = partners.filter((p) => p.is_loyalty_member); - + if (!this._filteredLoyaltyCache) { + 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() ?? ""); + + // 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) { // When query is empty, slice and sort following standard POS behavior - return filteredPartners + computedResult = filteredPartners .slice(0, 1000) .toSorted((a, b) => this.props.partner?.id === a.id @@ -21,36 +80,56 @@ patch(PartnerList.prototype, { ? 1 : (a.name || "").localeCompare(b.name || "") ); - } + } else { + // When query is present, optimize search filtering + const exactMatches = filteredPartners.filter((partner) => partner.exactMatch(searchWord)); + if (exactMatches.length > 0) { + computedResult = exactMatches; + } else { + const numberString = searchWord.replace(/[+\s()-]/g, ""); + const isSearchWordNumber = /^[0-9]+$/.test(numberString); + const patternBase = isSearchWordNumber ? numberString : searchWord; - // When query is present, optimize search filtering - const exactMatches = filteredPartners.filter((partner) => partner.exactMatch(searchWord)); - if (exactMatches.length > 0) { - return exactMatches; - } + const hasWildcard = patternBase.includes("%"); + const matches = []; - const numberString = searchWord.replace(/[+\s()-]/g, ""); - const isSearchWordNumber = /^[0-9]+$/.test(numberString); - const patternBase = isSearchWordNumber ? numberString : searchWord; - const regex = new RegExp( - patternBase - .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) { - if (!p._normalizedSearchString) { - p._normalizedSearchString = normalize(p.searchString || ""); - } - if (regex.test(p._normalizedSearchString)) { - matches.push(p); - if (matches.length >= 100) { - break; + 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( + patternBase + .replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + .replace(/%/g, ".*") + ); + for (const p of filteredPartners) { + if (!p._normalizedSearchString) { + p._normalizedSearchString = normalize(p.searchString || ""); + } + if (regex.test(p._normalizedSearchString)) { + matches.push(p); + if (matches.length >= 100) { + break; + } + } + } } + computedResult = matches; } } - return matches; + + queryCache.queries.set(searchWord, computedResult); + return computedResult; } });