Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions pos_next/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pos_next/api/invoices.py
Original file line number Diff line number Diff line change
Expand Up @@ -2745,6 +2745,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(),
Expand Down
21 changes: 21 additions & 0 deletions pos_next/api/promotions.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,27 @@ def get_promotions(pos_profile=None, company=None, include_disabled=False):
],
order_by="modified desc"
)
scheme_names = [s["name"] for s in schemes]

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 not pos_profiles or (pos_profile and pos_profile in pos_profiles):
filtered_schemes.append(scheme)

schemes = filtered_schemes

# Enrich with pricing rules count and details
today = getdate(nowdate())
Expand Down
95 changes: 94 additions & 1 deletion pos_next/overrides/pricing_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,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.
Expand All @@ -67,6 +67,8 @@ def patch_get_other_conditions(pr_utils):
def _patched_get_other_conditions(conditions, values, args):
conditions = _original_get_other_conditions(conditions, values, args)

conditions = _add_pos_only_condition(conditions, values, args)
conditions = _add_pos_profile_condition(conditions, values, args)
if not _has_pos_only_column():
return conditions

Expand All @@ -85,3 +87,94 @@ def _patched_get_other_conditions(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.

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
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
76 changes: 70 additions & 6 deletions pos_next/pos_next/custom/promotional_scheme.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -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": []
}
Original file line number Diff line number Diff line change
@@ -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
Loading