# How to Implement Custom Pricing Rules with Tiered Pricing in ERPNext

> Learn to implement custom tiered pricing rules in ERPNext. Create sequential pricing tiers for automatic volume-based discounts using min_qty and max_qty thresholds.

- Repository: [Frappe/erpnext](https://github.com/frappe/erpnext)
- Tags: how-to-guide
- Published: 2026-05-20

---

**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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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:

```python
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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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:

```python
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:

```python
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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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.