Skip to content
Merged
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
8 changes: 5 additions & 3 deletions POS/src/stores/posCart.js
Original file line number Diff line number Diff line change
Expand Up @@ -953,9 +953,11 @@ export const usePOSCartStore = defineStore("posCart", () => {
);
} else if (offer.apply_on === "Item Group") {
const eligibleGroups = offer.eligible_item_groups || [];
eligibleItems = invoiceItems.value.filter((item) =>
eligibleGroups.includes(item.item_group)
);
eligibleItems = eligibleGroups.includes("All Item Groups")
? invoiceItems.value
: invoiceItems.value.filter((item) =>
eligibleGroups.includes(item.item_group)
);
} else if (offer.apply_on === "Brand") {
const eligibleBrands = offer.eligible_brands || [];
eligibleItems = invoiceItems.value.filter((item) =>
Expand Down
9 changes: 6 additions & 3 deletions POS/src/stores/posOffers.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ export const usePOSOffersStore = defineStore("posOffers", () => {
} else if (offer?.apply_on === "Item Group") {
const eligibleGroups = offer.eligible_item_groups || [];
if (eligibleGroups.length > 0) {
if (eligibleGroups.includes("All Item Groups")) {
return cartSnapshot.value.itemCount || 0;
}
// Sum quantities of all items in eligible groups
return eligibleGroups.reduce((sum, group) => {
return sum + (itemGroupQuantities[group] || 0);
Expand Down Expand Up @@ -259,9 +262,9 @@ export const usePOSOffersStore = defineStore("posOffers", () => {
} else if (offer?.apply_on === "Item Group") {
const eligibleGroups = offer.eligible_item_groups || [];
if (eligibleGroups.length > 0) {
const hasEligibleGroup = eligibleGroups.some((group) =>
cartItemGroups.includes(group)
);
const hasEligibleGroup =
eligibleGroups.includes("All Item Groups") ||
eligibleGroups.some((group) => cartItemGroups.includes(group));
if (!hasEligibleGroup) {
return {
eligible: false,
Expand Down
24 changes: 24 additions & 0 deletions pos_next/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,30 @@ def console(*data):
except Exception:
pass


try:
from erpnext.accounts.doctype.pricing_rule import pricing_rule as _erpnext_pricing_rule

from pos_next.overrides.pricing_rule import (
apply_price_discount_rule as _pos_next_apply_price_discount_rule,
)

_erpnext_pricing_rule.apply_price_discount_rule = _pos_next_apply_price_discount_rule
except Exception:
if frappe:
frappe.log_error(frappe.get_traceback(), "Pricing Rule Override Error")


try:
from erpnext.accounts.doctype.promotional_scheme import promotional_scheme as _promotional_scheme

for _min_max_field in ("apply_discount_on_price", "min_or_max_discount_qty_limit"):
if _min_max_field not in _promotional_scheme.price_discount_fields:
_promotional_scheme.price_discount_fields.append(_min_max_field)
except Exception:
if frappe:
frappe.log_error(frappe.get_traceback(), "Promotional Scheme Field Patch Error")

# Frappe/ERPNext compatibility shim:
# ERPNext may pass do_not_round_fields to round_floats_in, but older Frappe
# versions don't accept that kwarg.
Expand Down
35 changes: 35 additions & 0 deletions pos_next/api/invoices.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@
from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules as erpnext_get_applied_pricing_rules,
)
from pos_next.overrides.pricing_rule import apply_min_max_price_discounts
except Exception: # pragma: no cover - ERPNext not installed in some environments
erpnext_apply_pricing_rule = None
erpnext_get_applied_pricing_rules = None
erpnext_apply_pricing_rule_on_transaction = None
apply_min_max_price_discounts = None


# ==========================================
Expand Down Expand Up @@ -3205,6 +3207,12 @@ def apply_offers(invoice_data, selected_offers=None):
# Fetch full pricing rule to get discount values
full_rule = frappe.get_cached_doc("Pricing Rule", rule_name)

# Min/Max rules are deferred to apply_min_max_price_discounts
# (cross-item ranking). Applying them here would discount every
# matching item, defeating the "cheapest/most-expensive" logic.
if full_rule.get("apply_discount_on_price") in ("Min", "Max"):
continue

if full_rule.rate_or_discount == "Discount Percentage" and full_rule.discount_percentage:
discount_percentage += flt(full_rule.discount_percentage)
elif full_rule.rate_or_discount == "Discount Amount" and full_rule.discount_amount:
Expand Down Expand Up @@ -3272,6 +3280,33 @@ def apply_offers(invoice_data, selected_offers=None):
free_items_map.setdefault(key, free_item_doc)
applied_rules.update(txn_result.get("applied_rules", set()))

# Apply Min/Max ("cheapest/most-expensive item") price rules. These were
# deferred by the per-item engine (see pos_next.overrides.pricing_rule) and
# need a cross-item ranking pass over the whole cart. The mock doc has no
# calculate_taxes_and_totals(); the post-processor materialises rate/amount
# on each discounted item directly.
if apply_min_max_price_discounts:
mock_doc = frappe._dict(
{
"doctype": invoice.get("doctype") or "Sales Invoice",
"items": prepared_items,
"selling_price_list": pricing_args.price_list,
"company": pricing_args.company,
"customer": pricing_args.customer,
}
)
min_max_allowed = set(rule_map) if selected_offer_names else None
apply_min_max_price_discounts(mock_doc, allowed_rules=min_max_allowed)

# Surface Min/Max rules in the response so the frontend tracks them as applied.
if erpnext_get_applied_pricing_rules:
for prepared_item in prepared_items:
if not prepared_item.get("pricing_rules"):
continue
for pr_name in erpnext_get_applied_pricing_rules(prepared_item.get("pricing_rules")):
if pr_name in rule_map:
applied_rules.add(pr_name)

return {
"items": [dict(item) for item in prepared_items],
"free_items": [dict(item) for item in free_items_map.values()],
Expand Down
12 changes: 11 additions & 1 deletion pos_next/api/offers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import frappe
from frappe import _
from frappe.utils import flt, getdate, nowdate
from frappe.utils import cint, flt, getdate, nowdate

# ============================================================================
# Constants
Expand Down Expand Up @@ -76,6 +76,8 @@ class Offer:
rate: float
discount_amount: float
discount_percentage: float
apply_discount_on_price: str | None
min_or_max_discount_qty_limit: int
valid_from: str | None
valid_upto: str | None
source: str
Expand Down Expand Up @@ -242,6 +244,7 @@ def fetch_price_slabs(scheme_names: list[str]) -> dict[str, dict]:
SELECT
parent, min_qty, max_qty, min_amount, max_amount,
rate_or_discount, rate, discount_amount, discount_percentage,
apply_discount_on_price, min_or_max_discount_qty_limit,
apply_multiple_pricing_rules
FROM `tabPromotional Scheme Price Discount`
WHERE parent IN %s AND disable = 0
Expand Down Expand Up @@ -338,6 +341,10 @@ def build_from_scheme_rule(rule: dict, slab: dict, eligibility: OfferEligibility
rate=flt(slab.get("rate", 0)) if is_price_discount else 0,
discount_amount=flt(slab.get("discount_amount", 0)) if is_price_discount else 0,
discount_percentage=flt(slab.get("discount_percentage", 0)) if is_price_discount else 0,
apply_discount_on_price=(slab.get("apply_discount_on_price") if is_price_discount else None),
min_or_max_discount_qty_limit=(
cint(slab.get("min_or_max_discount_qty_limit", 0)) if is_price_discount else 0
),
valid_from=rule.get("valid_from"),
valid_upto=rule.get("valid_upto"),
source=OfferSource.PROMOTIONAL_SCHEME,
Expand Down Expand Up @@ -392,6 +399,8 @@ def build_from_standalone_rule(rule: dict, eligibility: OfferEligibility) -> Off
rate=flt(rule.get("rate", 0)),
discount_amount=flt(rule.get("discount_amount", 0)),
discount_percentage=flt(rule.get("discount_percentage", 0)),
apply_discount_on_price=rule.get("apply_discount_on_price"),
min_or_max_discount_qty_limit=cint(rule.get("min_or_max_discount_qty_limit", 0)),
valid_from=rule.get("valid_from"),
valid_upto=rule.get("valid_upto"),
source=OfferSource.PRICING_RULE,
Expand Down Expand Up @@ -530,6 +539,7 @@ def _get_standalone_pricing_rule_offers(company: str, date: str) -> list[Offer]:
name, title, apply_on, selling,
coupon_code_based, one_time_per_customer, price_or_product_discount,
rate_or_discount, rate, discount_amount, discount_percentage,
apply_discount_on_price, min_or_max_discount_qty_limit,
min_qty, max_qty, min_amt, max_amt,
priority, valid_from, valid_upto
FROM `tabPricing Rule`
Expand Down
17 changes: 15 additions & 2 deletions pos_next/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@
# page_js = {"page" : "public/js/file.js"}

# include js in doctype views
doctype_js = {"Customer": "public/js/customer.js"}
doctype_js = {
"Customer": "public/js/customer.js",
"Pricing Rule": "public/js/pricing_rule.js",
"Promotional Scheme": "public/js/promotional_scheme.js",
}
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
Expand Down Expand Up @@ -151,6 +155,7 @@
"validate": [
"pos_next.api.sales_invoice_hooks.validate",
"pos_next.api.wallet.validate_wallet_payment",
"pos_next.overrides.pricing_rule.apply_min_max_price_discounts",
],
"before_cancel": "pos_next.api.sales_invoice_hooks.before_cancel",
"on_submit": [
Expand All @@ -165,7 +170,15 @@
"after_insert": "pos_next.realtime_events.emit_invoice_created_event",
},
"POS Profile": {"on_update": "pos_next.realtime_events.emit_pos_profile_updated_event"},
"Promotional Scheme": {"on_update": "pos_next.overrides.pricing_rule.sync_pos_only_to_pricing_rules"},
"Promotional Scheme": {
"validate": "pos_next.overrides.pricing_rule.enforce_min_max_pricing_config",
"on_update": "pos_next.overrides.pricing_rule.sync_pos_only_to_pricing_rules",
},
"Pricing Rule": {"validate": "pos_next.overrides.pricing_rule.enforce_min_max_pricing_config"},
"Sales Order": {"validate": "pos_next.overrides.pricing_rule.apply_min_max_price_discounts"},
"Quotation": {"validate": "pos_next.overrides.pricing_rule.apply_min_max_price_discounts"},
"Delivery Note": {"validate": "pos_next.overrides.pricing_rule.apply_min_max_price_discounts"},
"POS Invoice": {"validate": "pos_next.overrides.pricing_rule.apply_min_max_price_discounts"},
}

# Scheduled Tasks
Expand Down
Loading
Loading