How to Implement Custom Pricing Rules with Tiered Pricing in ERPNext
Create multiple Pricing Rule documents with sequential min_qty and max_qty thresholds to build automatic volume-based discount tiers that the ERPNext engine selects via filter_pricing_rules_for_qty_amount() in utils.py.
ERPNext's Pricing Rule framework enables dynamic discount calculations based on transaction context, but implementing true tiered pricing requires configuring quantity threshold fields to create distinct discount bands. This guide explains how to leverage the min_qty, max_qty, and priority fields in the frappe/erpnext repository to automatically apply volume-based discounts during sales and purchase transactions.
How Tiered Pricing Works in ERPNext
The Pricing Rule Doctype serves as the central engine for price adjustments in ERPNext. Tiered pricing relies on specific threshold fields to create quantity or value-based discount bands:
min_qtyandmax_qty: Define the quantity range that triggers a specific tier (e.g., 1-10 units vs. 11-20 units).min_amtandmax_amt: Alternative thresholds based on line-item value rather than pure quantity.discount_percentageordiscount_amount: The actual price reduction applied when the tier matches.priority: Determines rule precedence when multiple tiers are candidates; use identical priorities for mutually exclusive tiers.apply_multiple_pricing_rules: When unchecked, ensures only one tier applies per line item, preventing discount stacking.
When processing transactions, ERPNext calls apply_pricing_rule() in erpnext/accounts/doctype/pricing_rule/pricing_rule.py. The engine evaluates active rules against the current transaction context, then filters candidates through filter_pricing_rules_for_qty_amount() in erpnext/accounts/doctype/pricing_rule/utils.py (lines 94-108) to identify which tier's quantity or amount range contains the current line-item values.
Step-by-Step Implementation
You can implement tiered pricing through the ERPNext user interface or programmatically via the Frappe framework.
Creating Rules via the User Interface
- Navigate to Accounts > Pricing Rule and click New.
- Configure the Apply On dimension (Item Code, Item Group, or Brand) and select your target items.
- Set the Tier Thresholds:
- For Tier 1 (1-10 units):
Min Qty= 1,Max Qty= 10,Discount Percentage= 5 - For Tier 2 (11-20 units):
Min Qty= 11,Max Qty= 20,Discount Percentage= 10
- For Tier 1 (1-10 units):
- Assign the same Priority value (e.g., 1) to all tiers to ensure the first matching range wins.
- Leave Apply Multiple Pricing Rules unchecked to prevent tier stacking.
- Save and create additional Pricing Rule documents for each tier.
Creating Rules via Python Script
For batch creation or automated deployment, use the Frappe ORM to generate tiered rules programmatically:
import frappe
def create_volume_tiers(item_code, company):
tiers = [
{"min_qty": 1, "max_qty": 10, "discount": 5},
{"min_qty": 11, "max_qty": 20, "discount": 10},
{"min_qty": 21, "max_qty": 0, "discount": 15} # 0 = unlimited
]
for i, tier in enumerate(tiers, 1):
rule = frappe.get_doc({
"doctype": "Pricing Rule",
"title": f"Volume Tier {i} - {tier['discount']}%",
"apply_on": "Item Code",
"items": [{"item_code": item_code}],
"price_or_product_discount": "Price",
"rate_or_discount": "Discount Percentage",
"discount_percentage": tier["discount"],
"min_qty": tier["min_qty"],
"max_qty": tier["max_qty"],
"priority": 1,
"selling": 1,
"company": company
})
rule.insert(ignore_permissions=True)
# Execute via bench console
create_volume_tiers("ITEM-001", "My Company")
This script creates three distinct pricing tiers that apply automatically when quantities fall within the specified ranges.
Architectural Deep Dive
Understanding the code execution flow helps debug complex tiered pricing scenarios. The process flows through several key functions in the ERPNext source:
-
Entry Point:
apply_pricing_rule()inerpnext/accounts/doctype/pricing_rule/pricing_rule.py(lines 22-27) receives transaction data and normalizes arguments viaset_transaction_type. -
Rule Retrieval:
get_pricing_rules()inerpnext/accounts/doctype/pricing_rule/utils.py(lines 26-41) queries active rules matching the item, customer, and company context. -
Tier Filtering:
filter_pricing_rules_for_qty_amount()inutils.py(lines 94-108) implements the core tier logic by checking ifqtyfalls betweenmin_qtyandmax_qty(or ifamountfalls betweenmin_amtandmax_amt). -
Priority Resolution:
sorted_by_priority()inutils.py(lines 63-78) orders remaining candidates by thepriorityfield and respects theapply_multiple_pricing_rulessetting. -
Discount Application:
apply_price_discount_rule()inpricing_rule.py(lines 56-73) updates the transaction line-item fields (discount_percentage,price_list_rate) with the selected tier's values.
The tier determination occurs server-side; no additional client-side code is required for standard implementations.
Practical Code Examples
Full Transaction Workflow
This example demonstrates creating tiers and observing automatic application in a Sales Order:
import frappe
# Step 1: Create three volume tiers
def setup_tiered_pricing():
item_code = "PROD-001"
company = frappe.defaults.get_user_default("company")
for min_q, max_q, disc in [(1, 10, 5), (11, 20, 10), (21, 0, 15)]:
frappe.get_doc({
"doctype": "Pricing Rule",
"title": f"Tier {min_q}-{max_q or 'unlimited'}",
"apply_on": "Item Code",
"items": [{"item_code": item_code}],
"rate_or_discount": "Discount Percentage",
"discount_percentage": disc,
"min_qty": min_q,
"max_qty": max_q or 0,
"priority": 1,
"selling": 1,
"company": company
}).insert(ignore_permissions=True)
# Step 2: Create Sales Order with quantity in second tier
so = frappe.get_doc({
"doctype": "Sales Order",
"customer": "CUST-001",
"company": frappe.defaults.get_user_default("company"),
"items": [{"item_code": "PROD-001", "qty": 15, "rate": 100}]
})
so.insert()
so.save()
print(f"Applied discount: {so.items[0].discount_percentage}%") # Output: 10%
Direct API Testing
Test your tier logic without creating full transactions by calling the core function directly:
from erpnext.accounts.doctype.pricing_rule.pricing_rule import apply_pricing_rule
result = apply_pricing_rule({
"items": [{
"item_code": "PROD-001",
"qty": 23,
"price_list_rate": 120
}],
"price_list": "Standard Selling",
"company": "My Company",
"transaction_type": "selling"
})
print(result[0]["discount_percentage"]) # Returns 15 for the third tier
Summary
- Create multiple Pricing Rules with sequential
min_qty/max_qtyranges to establish distinct discount tiers. - Match priorities across tiers to ensure the engine selects the first valid range rather than applying multiple discounts.
- Leverage
filter_pricing_rules_for_qty_amount()inerpnext/accounts/doctype/pricing_rule/utils.pyto handle automatic tier selection based on transaction quantity. - Use
apply_pricing_rule()as the entry point for programmatic rule application or API integration. - Leave
apply_multiple_pricing_rulesunchecked for standard tiered pricing to prevent discount stacking across tiers.
Frequently Asked Questions
How do I handle unlimited maximum quantities in the top tier?
Set max_qty to 0 in your Pricing Rule configuration. According to the validation logic in pricing_rule.py, a value of 0 indicates no upper limit, allowing the top tier to capture all quantities above your min_qty threshold.
Can I tier pricing based on total order value instead of individual line quantities?
Yes. Use the min_amt and max_amt fields instead of quantity fields. The filter_pricing_rules_for_qty_amount() function in utils.py evaluates both quantity and amount thresholds, calculating the line value as qty * price_list_rate during the filtering process.
Why are multiple tiered rules applying instead of just the correct one?
Check that all tiers share the identical priority value and that Apply Multiple Pricing Rules is unchecked. If priorities differ or the multiple rules flag is enabled, the engine may stack discounts from different tiers or apply incorrect precedence, bypassing the mutual exclusivity intended for tiered pricing.
What happens if a transaction quantity falls between defined tiers?
If no rule matches the specific quantity range (e.g., a gap between max_qty of tier 1 and min_qty of tier 2), the filter_pricing_rules_for_qty_amount() function returns an empty set, resulting in no pricing rule being applied. Ensure your min_qty and max_qty values create contiguous ranges without gaps.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →