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_qty and max_qty: Define the quantity range that triggers a specific tier (e.g., 1-10 units vs. 11-20 units).
  • min_amt and max_amt: Alternative thresholds based on line-item value rather than pure quantity.
  • discount_percentage or discount_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

  1. Navigate to Accounts > Pricing Rule and click New.
  2. Configure the Apply On dimension (Item Code, Item Group, or Brand) and select your target items.
  3. 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
  4. Assign the same Priority value (e.g., 1) to all tiers to ensure the first matching range wins.
  5. Leave Apply Multiple Pricing Rules unchecked to prevent tier stacking.
  6. 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:

  1. Entry Point: apply_pricing_rule() in erpnext/accounts/doctype/pricing_rule/pricing_rule.py (lines 22-27) receives transaction data and normalizes arguments via set_transaction_type.

  2. Rule Retrieval: get_pricing_rules() in erpnext/accounts/doctype/pricing_rule/utils.py (lines 26-41) queries active rules matching the item, customer, and company context.

  3. Tier Filtering: filter_pricing_rules_for_qty_amount() in utils.py (lines 94-108) implements the core tier logic by checking if qty falls between min_qty and max_qty (or if amount falls between min_amt and max_amt).

  4. Priority Resolution: sorted_by_priority() in utils.py (lines 63-78) orders remaining candidates by the priority field and respects the apply_multiple_pricing_rules setting.

  5. Discount Application: apply_price_discount_rule() in pricing_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_qty ranges 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() in erpnext/accounts/doctype/pricing_rule/utils.py to 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_rules unchecked 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →