From 7a304ab67c0412bf14d4da31c7ab865b806d1b84 Mon Sep 17 00:00:00 2001 From: MostafaKadry Date: Thu, 26 Feb 2026 01:03:57 +0200 Subject: [PATCH 1/7] feat: Implement POS profile-specific promotion scheme assignment and filtering get_promotions by `Promotion Scheme POS Profile` --- pos_next/api/promotions.py | 18 +++ .../pos_next/custom/promotional_scheme.json | 137 ++++++++++++++++++ .../promotion_scheme_pos_profile/__init__.py | 0 .../promotion_scheme_pos_profile.json | 36 +++++ .../promotion_scheme_pos_profile.py | 9 ++ 5 files changed, 200 insertions(+) create mode 100644 pos_next/pos_next/custom/promotional_scheme.json create mode 100644 pos_next/pos_next/doctype/promotion_scheme_pos_profile/__init__.py create mode 100644 pos_next/pos_next/doctype/promotion_scheme_pos_profile/promotion_scheme_pos_profile.json create mode 100644 pos_next/pos_next/doctype/promotion_scheme_pos_profile/promotion_scheme_pos_profile.py diff --git a/pos_next/api/promotions.py b/pos_next/api/promotions.py index c160cfe1..011f3cbb 100644 --- a/pos_next/api/promotions.py +++ b/pos_next/api/promotions.py @@ -60,6 +60,24 @@ def get_promotions(pos_profile=None, company=None, include_disabled=False): ], order_by="modified desc" ) + filtered_schemes = [] + + for scheme in schemes: + pos_profiles = frappe.get_all( + "Promotion Scheme POS Profile", + filters={"parent": scheme["name"]}, + pluck="pos_profile" + ) + + scheme["pos_profiles"] = pos_profiles + + if pos_profiles: + if pos_profile and pos_profile in pos_profiles: + filtered_schemes.append(scheme) + else: + filtered_schemes.append(scheme) + + schemes = filtered_schemes # Enrich with pricing rules count and details today = getdate(nowdate()) diff --git a/pos_next/pos_next/custom/promotional_scheme.json b/pos_next/pos_next/custom/promotional_scheme.json new file mode 100644 index 00000000..8f4c5fea --- /dev/null +++ b/pos_next/pos_next/custom/promotional_scheme.json @@ -0,0 +1,137 @@ +{ + "custom_fields": [ + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2026-02-25 13:26:55.280290", + "default": "0", + "depends_on": null, + "description": "Applies only to POS Invoices when checked.", + "docstatus": 0, + "dt": "Promotional Scheme", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "apply_only_on_pos_next", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 2, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "disable", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Apply Only on POS Next", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-02-25 14:43:06.924204", + "modified_by": "Administrator", + "module": "POS Next", + "name": "Promotional Scheme-custom_apply_only_on_pos_next", + "no_copy": 0, + "non_negative": 0, + "options": null, + "owner": "Administrator", + "permlevel": 0, + "placeholder": "Apply Only on POS Next Inovices", + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2026-02-25 14:35:08.813548", + "default": null, + "depends_on": null, + "description": "Select the POS Profiles where this Promotion Scheme should apply; leave it blank to apply to all POS Profiles.", + "docstatus": 0, + "dt": "Promotional Scheme", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_pos_profiles", + "fieldtype": "Table", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 8, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "brands", + "is_system_generated": 0, + "is_virtual": 0, + "label": "POS Profiles", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-02-25 14:43:49.749317", + "modified_by": "Administrator", + "module": "POS Next", + "name": "Promotional Scheme-custom_pos_profiles", + "no_copy": 0, + "non_negative": 0, + "options": "Promotion Scheme POS Profile", + "owner": "Administrator", + "permlevel": 0, + "placeholder": "", + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + } + ], + "custom_perms": [], + "doctype": "Promotional Scheme", + "links": [], + "property_setters": [], + "sync_on_migrate": 1 +} \ No newline at end of file diff --git a/pos_next/pos_next/doctype/promotion_scheme_pos_profile/__init__.py b/pos_next/pos_next/doctype/promotion_scheme_pos_profile/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pos_next/pos_next/doctype/promotion_scheme_pos_profile/promotion_scheme_pos_profile.json b/pos_next/pos_next/doctype/promotion_scheme_pos_profile/promotion_scheme_pos_profile.json new file mode 100644 index 00000000..38ae053e --- /dev/null +++ b/pos_next/pos_next/doctype/promotion_scheme_pos_profile/promotion_scheme_pos_profile.json @@ -0,0 +1,36 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-02-25 13:44:49.802466", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "pos_profile" + ], + "fields": [ + { + "fieldname": "pos_profile", + "fieldtype": "Link", + "in_list_view": 1, + "label": "POS Profile", + "options": "POS Profile", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-02-25 14:34:02.956883", + "modified_by": "Administrator", + "module": "POS Next", + "name": "Promotion Scheme POS Profile", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/pos_next/pos_next/doctype/promotion_scheme_pos_profile/promotion_scheme_pos_profile.py b/pos_next/pos_next/doctype/promotion_scheme_pos_profile/promotion_scheme_pos_profile.py new file mode 100644 index 00000000..8c9c0e34 --- /dev/null +++ b/pos_next/pos_next/doctype/promotion_scheme_pos_profile/promotion_scheme_pos_profile.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, BrainWise and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class PromotionSchemePOSProfile(Document): + pass From fb0cfd071728a2904f4d3dcf83dea8ab3abf98b1 Mon Sep 17 00:00:00 2001 From: MostafaKadry Date: Mon, 2 Mar 2026 13:48:55 +0200 Subject: [PATCH 2/7] remove pos invice only check box field as it add in another branch --- .../pos_next/custom/promotional_scheme.json | 68 +------------------ 1 file changed, 2 insertions(+), 66 deletions(-) diff --git a/pos_next/pos_next/custom/promotional_scheme.json b/pos_next/pos_next/custom/promotional_scheme.json index 8f4c5fea..fb2e1ac9 100644 --- a/pos_next/pos_next/custom/promotional_scheme.json +++ b/pos_next/pos_next/custom/promotional_scheme.json @@ -1,69 +1,5 @@ { "custom_fields": [ - { - "_assign": null, - "_comments": null, - "_liked_by": null, - "_user_tags": null, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "creation": "2026-02-25 13:26:55.280290", - "default": "0", - "depends_on": null, - "description": "Applies only to POS Invoices when checked.", - "docstatus": 0, - "dt": "Promotional Scheme", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "apply_only_on_pos_next", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "idx": 2, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "disable", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Apply Only on POS Next", - "length": 0, - "link_filters": null, - "mandatory_depends_on": null, - "modified": "2026-02-25 14:43:06.924204", - "modified_by": "Administrator", - "module": "POS Next", - "name": "Promotional Scheme-custom_apply_only_on_pos_next", - "no_copy": 0, - "non_negative": 0, - "options": null, - "owner": "Administrator", - "permlevel": 0, - "placeholder": "Apply Only on POS Next Inovices", - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "show_dashboard": 0, - "sort_options": 0, - "translatable": 0, - "unique": 0, - "width": null - }, { "_assign": null, "_comments": null, @@ -83,7 +19,7 @@ "dt": "Promotional Scheme", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_pos_profiles", + "fieldname": "pos_profiles", "fieldtype": "Table", "hidden": 0, "hide_border": 0, @@ -103,7 +39,7 @@ "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2026-02-25 14:43:49.749317", + "modified": "2026-03-01 13:01:51.495686", "modified_by": "Administrator", "module": "POS Next", "name": "Promotional Scheme-custom_pos_profiles", From 249cc463d73072e30cd921979806421d33520867 Mon Sep 17 00:00:00 2001 From: MostafaKadry Date: Tue, 3 Mar 2026 12:57:44 +0200 Subject: [PATCH 3/7] feat: Implement POS profile filtering for promotional schemes in apply_offers funciton --- pos_next/api/invoices.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pos_next/api/invoices.py b/pos_next/api/invoices.py index 831ad797..943ee3fa 100644 --- a/pos_next/api/invoices.py +++ b/pos_next/api/invoices.py @@ -2642,6 +2642,32 @@ def apply_offers(invoice_data, selected_offers=None): # Include both promotional scheme rules and standalone pricing rules rule_map[record.name] = record + # Filter by Promotional Scheme POS Profile restriction + pos_profile_name = invoice.get("pos_profile") + if pos_profile_name and rule_map: + scheme_names = { + d.promotional_scheme for d in rule_map.values() if d.promotional_scheme + } + scheme_pos_profiles = {} + if scheme_names: + rows = frappe.get_all( + "Promotion Scheme POS Profile", + filters={"parent": ["in", list(scheme_names)]}, + fields=["parent", "pos_profile"], + ) + for row in rows: + scheme_pos_profiles.setdefault(row.parent, []).append(row.pos_profile) + + rule_map = { + name: details + for name, details in rule_map.items() + if ( + not details.promotional_scheme + or not scheme_pos_profiles.get(details.promotional_scheme) + or pos_profile_name in scheme_pos_profiles[details.promotional_scheme] + ) + } + if selected_offer_names: # Restrict available rules to the ones explicitly selected from the UI. rule_map = { From f1be6f9037735cd4437621e0f0fb7ebe67abb817 Mon Sep 17 00:00:00 2001 From: MostafaKadry Date: Tue, 3 Mar 2026 13:58:23 +0200 Subject: [PATCH 4/7] refactor: Move pricing rule promotional scheme filtering from the invoice API to the pricing rule override, now using the passed POS profile. --- pos_next/__init__.py | 3 -- pos_next/api/invoices.py | 27 +------------- pos_next/overrides/pricing_rule.py | 60 ++++++++++++++++++++++++------ 3 files changed, 49 insertions(+), 41 deletions(-) diff --git a/pos_next/__init__.py b/pos_next/__init__.py index 855eca29..478abc0c 100644 --- a/pos_next/__init__.py +++ b/pos_next/__init__.py @@ -16,9 +16,6 @@ def console(*data): # Patch get_other_conditions to exclude pos_only pricing rules from non-POS documents. -# No Frappe hook exists for non-whitelisted module-level functions (override_whitelisted_methods -# only works for @frappe.whitelist() HTTP endpoints, override_doctype_class only for DocType -# classes). This is the standard Python module init approach — runs once at import. try: from erpnext.accounts.doctype.pricing_rule import utils as pr_utils from pos_next.overrides.pricing_rule import patch_get_other_conditions diff --git a/pos_next/api/invoices.py b/pos_next/api/invoices.py index cf1e6c3c..560bd3c8 100644 --- a/pos_next/api/invoices.py +++ b/pos_next/api/invoices.py @@ -2595,6 +2595,7 @@ def apply_offers(invoice_data, selected_offers=None): "doctype": invoice.get("doctype") or "Sales Invoice", "name": invoice.get("name") or "POS-INVOICE", "is_pos": 1, + "pos_profile": profile.name, "company": profile.company, "transaction_date": invoice.get("posting_date") or nowdate(), "posting_date": invoice.get("posting_date") or nowdate(), @@ -2685,32 +2686,6 @@ def apply_offers(invoice_data, selected_offers=None): # Include both promotional scheme rules and standalone pricing rules rule_map[record.name] = record - # Filter by Promotional Scheme POS Profile restriction - pos_profile_name = invoice.get("pos_profile") - if pos_profile_name and rule_map: - scheme_names = { - d.promotional_scheme for d in rule_map.values() if d.promotional_scheme - } - scheme_pos_profiles = {} - if scheme_names: - rows = frappe.get_all( - "Promotion Scheme POS Profile", - filters={"parent": ["in", list(scheme_names)]}, - fields=["parent", "pos_profile"], - ) - for row in rows: - scheme_pos_profiles.setdefault(row.parent, []).append(row.pos_profile) - - rule_map = { - name: details - for name, details in rule_map.items() - if ( - not details.promotional_scheme - or not scheme_pos_profiles.get(details.promotional_scheme) - or pos_profile_name in scheme_pos_profiles[details.promotional_scheme] - ) - } - if selected_offer_names: # Restrict available rules to the ones explicitly selected from the UI. rule_map = { diff --git a/pos_next/overrides/pricing_rule.py b/pos_next/overrides/pricing_rule.py index 3b642700..9603149b 100644 --- a/pos_next/overrides/pricing_rule.py +++ b/pos_next/overrides/pricing_rule.py @@ -31,7 +31,7 @@ def sync_pos_only_to_pricing_rules(doc, method=None): def patch_get_other_conditions(pr_utils): - """Monkey-patch get_other_conditions to filter pos_only pricing rules. + """Monkey-patch get_other_conditions to filter pricing rules. No Frappe hook exists for non-whitelisted module-level functions, so monkey-patching is the only option for this SQL condition injection. @@ -41,18 +41,54 @@ def patch_get_other_conditions(pr_utils): def _patched_get_other_conditions(conditions, values, args): conditions = _original_get_other_conditions(conditions, values, args) - doctype = args.get("doctype", "") - # POS Invoice doctype — always POS, all rules apply - if doctype in ("POS Invoice", "POS Invoice Item"): - pass - # Sales Invoice — check is_pos flag - elif doctype in ("Sales Invoice", "Sales Invoice Item"): - if not args.get("is_pos"): - conditions += " and ifnull(`tabPricing Rule`.pos_only, 0) = 0" - # All other doctypes (Quotation, SO, DN, Purchase docs) — exclude POS-only - else: - conditions += " and ifnull(`tabPricing Rule`.pos_only, 0) = 0" + conditions = _add_pos_only_condition(conditions, values, args) + conditions = _add_pos_profile_condition(conditions, values, args) return conditions pr_utils.get_other_conditions = _patched_get_other_conditions + +def _add_pos_only_condition(conditions, values, args): + """Filter pos_only rules for non-POS documents.""" + doctype = args.get("doctype", "") + + # POS Invoice doctype — always POS, all rules apply + if doctype in ("POS Invoice", "POS Invoice Item"): + pass + # Sales Invoice — check is_pos flag + elif doctype in ("Sales Invoice", "Sales Invoice Item"): + if not args.get("is_pos"): + conditions += " and ifnull(`tabPricing Rule`.pos_only, 0) = 0" + # All other doctypes (Quotation, SO, DN, Purchase docs) — exclude POS-only + else: + conditions += " and ifnull(`tabPricing Rule`.pos_only, 0) = 0" + + return conditions + + +def _add_pos_profile_condition(conditions, values, args): + """Filter rules based on Promotion Scheme POS Profile restrictions.""" + pos_profile = args.get("pos_profile") + if not pos_profile: + return conditions + + # Include rule if: + # 1. Rule has no promotional_scheme (standalone rule), OR + # 2. Parent scheme has NO rows in Promotion Scheme POS Profile (apply to all), OR + # 3. Current pos_profile IS in the scheme's POS Profile list + conditions += """ AND ( + `tabPricing Rule`.promotional_scheme IS NULL + OR `tabPricing Rule`.promotional_scheme = '' + OR NOT EXISTS ( + SELECT 1 FROM `tabPromotion Scheme POS Profile` + WHERE parent = `tabPricing Rule`.promotional_scheme + ) + OR EXISTS ( + SELECT 1 FROM `tabPromotion Scheme POS Profile` + WHERE parent = `tabPricing Rule`.promotional_scheme + AND pos_profile = %(pos_profile)s + ) + )""" + values["pos_profile"] = pos_profile + + return conditions From 02718d7d321ec56a04efea1315685301495371e3 Mon Sep 17 00:00:00 2001 From: MostafaKadry Date: Tue, 3 Mar 2026 14:07:31 +0200 Subject: [PATCH 5/7] perf: Batch fetch promotion scheme POS profiles to eliminate queries and improve filtering efficiency. --- pos_next/api/promotions.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/pos_next/api/promotions.py b/pos_next/api/promotions.py index 011f3cbb..2ba7fff5 100644 --- a/pos_next/api/promotions.py +++ b/pos_next/api/promotions.py @@ -60,21 +60,24 @@ def get_promotions(pos_profile=None, company=None, include_disabled=False): ], order_by="modified desc" ) - filtered_schemes = [] + scheme_names = [s["name"] for s in schemes] - for scheme in schemes: - pos_profiles = frappe.get_all( - "Promotion Scheme POS Profile", - filters={"parent": scheme["name"]}, - pluck="pos_profile" - ) + rows = frappe.get_all( + "Promotion Scheme POS Profile", + filters={"parent": ["in", scheme_names]}, + fields=["parent", "pos_profile"] + ) + scheme_pos_profiles = {} + for row in rows: + scheme_pos_profiles.setdefault(row.parent, []).append(row.pos_profile) + + filtered_schemes = [] + for scheme in schemes: + pos_profiles = scheme_pos_profiles.get(scheme["name"], []) scheme["pos_profiles"] = pos_profiles - if pos_profiles: - if pos_profile and pos_profile in pos_profiles: - filtered_schemes.append(scheme) - else: + if not pos_profiles or (pos_profile and pos_profile in pos_profiles): filtered_schemes.append(scheme) schemes = filtered_schemes From bd9787e29eb30e12fff68f005620b1b916baa6d4 Mon Sep 17 00:00:00 2001 From: MostafaKadry Date: Wed, 11 Mar 2026 14:55:00 +0200 Subject: [PATCH 6/7] Add missed Table field in promotion scheme --- .../pos_next/custom/promotional_scheme.json | 76 +++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/pos_next/pos_next/custom/promotional_scheme.json b/pos_next/pos_next/custom/promotional_scheme.json index 038b9751..8f7e2af5 100644 --- a/pos_next/pos_next/custom/promotional_scheme.json +++ b/pos_next/pos_next/custom/promotional_scheme.json @@ -11,8 +11,72 @@ "collapsible": 0, "collapsible_depends_on": null, "columns": 0, - "creation": "2026-03-02 13:51:19.810524", + "creation": "2026-03-11 14:47:47.061432", "default": null, + "depends_on": "", + "description": "Select the POS Profiles where this Promotion Scheme should apply; leave it blank to apply to all POS Profiles.", + "docstatus": 0, + "dt": "Promotional Scheme", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "pos_profiles", + "fieldtype": "Table", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 8, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "brands", + "is_system_generated": 0, + "is_virtual": 0, + "label": "POS Profiles", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-03-11 14:50:13.710615", + "modified_by": "Administrator", + "module": null, + "name": "Promotional Scheme-custom_pos_profiles", + "no_copy": 0, + "non_negative": 0, + "options": "Promotion Scheme POS Profile", + "owner": "Administrator", + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2026-03-11 12:27:18.773235", + "default": "0", "depends_on": "eval:doc.selling", "description": "If checked, this scheme will only apply to POS transactions (Sales Invoices with is_pos=1 and POS Invoices)", "docstatus": 0, @@ -25,24 +89,24 @@ "hide_border": 0, "hide_days": 0, "hide_seconds": 0, - "idx": 3, + "idx": 19, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "disable", + "insert_after": "selling", "is_system_generated": 0, "is_virtual": 0, "label": "POS Only", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2026-03-02 13:54:14.946616", + "modified": "2026-03-02 12:00:00", "modified_by": "Administrator", - "module": null, - "name": "Promotional Scheme-custom_pos_only", + "module": "POS Next", + "name": "Promotional Scheme-pos_only", "no_copy": 0, "non_negative": 0, "options": null, From 3453216444d6d0a9eb15deb0d4ad9ef39eca55e0 Mon Sep 17 00:00:00 2001 From: MohamedAliSmk Date: Sun, 15 Mar 2026 13:02:36 +0200 Subject: [PATCH 7/7] perf: Optimize POS profile condition filtering in pricing rules by implementing request-level caching and using IN clauses to improve query efficiency. --- pos_next/overrides/pricing_rule.py | 76 ++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/pos_next/overrides/pricing_rule.py b/pos_next/overrides/pricing_rule.py index 9603149b..bb83726c 100644 --- a/pos_next/overrides/pricing_rule.py +++ b/pos_next/overrides/pricing_rule.py @@ -67,28 +67,74 @@ def _add_pos_only_condition(conditions, values, args): def _add_pos_profile_condition(conditions, values, args): - """Filter rules based on Promotion Scheme POS Profile restrictions.""" + """Filter rules based on Promotion Scheme POS Profile restrictions. + + Optimized to avoid correlated subqueries by pre-filtering promotional schemes + and using IN clauses. Uses request-level caching to avoid re-querying for + each item in the same request. + """ pos_profile = args.get("pos_profile") if not pos_profile: return conditions + # Cache key for request-level caching + cache_key = f"_pos_profile_schemes_{pos_profile}" + + # Check if we've already computed this for this request + if not hasattr(frappe.local, cache_key): + # Pre-query: Get all promotional schemes that have POS Profile restrictions + # (schemes with any rows in the child table) + restricted_schemes = frappe.db.sql_list(""" + SELECT DISTINCT parent + FROM `tabPromotion Scheme POS Profile` + """) + + # Pre-query: Get all promotional schemes that allow this specific pos_profile + allowed_schemes = frappe.db.sql_list(""" + SELECT DISTINCT parent + FROM `tabPromotion Scheme POS Profile` + WHERE pos_profile = %s + """, (pos_profile,)) + + # Store in request cache + frappe.local[cache_key] = { + "restricted": set(restricted_schemes) if restricted_schemes else set(), + "allowed": set(allowed_schemes) if allowed_schemes else set() + } + + scheme_data = frappe.local[cache_key] + restricted_schemes = scheme_data["restricted"] + allowed_schemes = scheme_data["allowed"] + + # Build condition using IN clauses instead of correlated subqueries # Include rule if: # 1. Rule has no promotional_scheme (standalone rule), OR # 2. Parent scheme has NO rows in Promotion Scheme POS Profile (apply to all), OR # 3. Current pos_profile IS in the scheme's POS Profile list - conditions += """ AND ( - `tabPricing Rule`.promotional_scheme IS NULL - OR `tabPricing Rule`.promotional_scheme = '' - OR NOT EXISTS ( - SELECT 1 FROM `tabPromotion Scheme POS Profile` - WHERE parent = `tabPricing Rule`.promotional_scheme - ) - OR EXISTS ( - SELECT 1 FROM `tabPromotion Scheme POS Profile` - WHERE parent = `tabPricing Rule`.promotional_scheme - AND pos_profile = %(pos_profile)s - ) - )""" - values["pos_profile"] = pos_profile + if not restricted_schemes: + # No schemes have restrictions, so all promotional schemes apply + # No additional filtering needed beyond NULL/empty check (which is already handled) + # This means all rules are included, so we don't need to add any condition + pass + else: + # Build IN clause for allowed schemes + if allowed_schemes: + allowed_list = list(allowed_schemes) + conditions += """ AND ( + `tabPricing Rule`.promotional_scheme IS NULL + OR `tabPricing Rule`.promotional_scheme = '' + OR `tabPricing Rule`.promotional_scheme NOT IN %(restricted_schemes)s + OR `tabPricing Rule`.promotional_scheme IN %(allowed_schemes)s + )""" + values["restricted_schemes"] = list(restricted_schemes) + values["allowed_schemes"] = allowed_list + else: + # No schemes allow this pos_profile, so exclude all restricted schemes + conditions += """ AND ( + `tabPricing Rule`.promotional_scheme IS NULL + OR `tabPricing Rule`.promotional_scheme = '' + OR `tabPricing Rule`.promotional_scheme NOT IN %(restricted_schemes)s + )""" + values["restricted_schemes"] = list(restricted_schemes) return conditions