Skip to content

Sale Voucher Documentation

Overview

The Sale Voucher module (sale.voucher) provides a comprehensive discount voucher and gift certificate system. Vouchers can offer various benefit types including fixed discounts, percentage discounts, free products, or credits. The system includes extensive validation logic for minimum purchase amounts, customer restrictions, product criteria, and usage limits.


Model Information

Model Name: sale.voucher Display Name: Voucher Name Field: code (voucher code displayed as name)

Features

  • Multiple benefit types (discounts, free products, credits)
  • Flexible validation rules and criteria
  • Customer-specific and generic vouchers
  • Product and category restrictions
  • Minimum order amount and quantity requirements
  • Usage limits (per customer and total)
  • Expiration date management
  • Customizable error messages for each validation rule
  • Integration with e-commerce carts and sales orders

Benefit Types

The voucher system supports multiple benefit types for customers and referrers:

Customer Benefits

Type Code Description
Fixed Discount fixed_discount_order Fixed amount off the entire order
Free Product free_product Specific product given for free
Percent Discount percent_discount_product Percentage off specific products
Credits credit Store credits added to customer account

Referrer Benefits

Type Code Description
Credits credit Store credits for customer who referred this voucher

State Workflow

active ←→ inactive
State Description
active Voucher can be applied to orders
inactive Voucher is disabled and cannot be used

Key Fields Reference

Identification Fields

Field Type Required Description
code Char Yes Unique voucher code (displayed as name)
sequence Char No Sequence number for ordering
state Selection Yes Voucher status (active/inactive)
product_id Many2One Yes Configuration product for voucher

Benefit Configuration Fields

Field Type Description
benefit_type Selection Type of benefit for customer (fixed_discount_order/free_product/percent_discount_product/credit)
refer_benefit_type Selection Type of benefit for referrer (credit)
discount_amount Decimal Fixed discount amount
discount_percent Decimal Percentage discount value
discount_product_groups Many2Many Product groups eligible for discount
discount_product_id Many2One Specific product for discount/free product
discount_max_qty Integer Maximum quantity of discounted items
credit_amount Decimal Credit amount for customer
refer_credit_amount Decimal Credit amount for referrer

Validation Rules Fields

Field Type Description
min_order_amount Decimal Minimum order total required
min_order_amount_msg Text Custom error message
max_orders_per_customer Integer Maximum times one customer can use
max_orders_per_customer_msg Text Custom error message
max_orders Integer Maximum total uses across all customers
max_orders_msg Text Custom error message
new_customer Boolean Only allow new customers
new_customer_msg Text Custom error message

Product/Category Criteria Fields

Field Type Description
product_groups Many2Many Product groups that must be in cart
product_groups_msg Text Custom error message
cond_product_id Many2One Specific product required in cart
cond_product_msg Text Custom error message
cond_product_categ_id Many2One Product category required
cond_product_categ_msg Text Custom error message

Quantity Criteria Fields

Field Type Description
min_qty Decimal Minimum quantity required
min_qty_msg Text Custom error message
qty_multiple Decimal Quantity must be multiple of this
qty_multiple_msg Text Custom error message

Customer Restriction Fields

Field Type Description
customer_id Many2One Specific customer who can use (personalized voucher)
customer_msg Text Custom error message
contact_groups Many2Many Customer groups eligible
contact_groups_msg Text Custom error message

Time Management Fields

Field Type Description
expire_date Date Expiration date
expire_date_msg Text Custom error message
description Text Voucher description

Relationship Fields

Field Type Description
carts One2Many E-commerce carts using this voucher
sale_orders One2Many Sales orders using this voucher

API Methods

1. Create Voucher

Method: create(vals, context)

Creates a new voucher with validation rules and benefit configuration.

Parameters:

vals = {
    "code": "SAVE20",                      # Required: unique code
    "state": "active",                     # Required: active or inactive
    "product_id": 123,                     # Required: config product
    "benefit_type": "fixed_discount_order",# Required: benefit type
    "discount_amount": 20.00,              # Discount value
    "min_order_amount": 100.00,            # Minimum order required
    "min_order_amount_msg": "Order must be $100+",
    "max_orders_per_customer": 1,          # Usage limit per customer
    "expire_date": "2026-12-31",           # Expiration
    "description": "Get $20 off orders over $100"
}

Returns: int - New voucher ID

Example:

# Create a $20 off voucher with minimum purchase
voucher_id = get_model("sale.voucher").create({
    "code": "WELCOME20",
    "state": "active",
    "product_id": product_id,
    "benefit_type": "fixed_discount_order",
    "discount_amount": 20.00,
    "min_order_amount": 100.00,
    "min_order_amount_msg": "Spend at least $100 to use this voucher",
    "max_orders_per_customer": 1,
    "new_customer": True,
    "new_customer_msg": "This voucher is for new customers only",
    "expire_date": "2026-12-31",
    "description": "Welcome gift: $20 off your first order over $100"
})


2. Apply Voucher

Method: apply_voucher(ids, context)

Validates voucher eligibility and calculates discount amount. This is the core validation method.

Context Options:

context = {
    "contact_id": 456,                    # Customer ID
    "amount_total": 150.00,               # Order subtotal (before shipping)
    "amount_ship": 10.00,                 # Shipping amount
    "products": [                         # Products in order
        {"product_id": 101, "qty": 2, "amount": 100.00},
        {"product_id": 102, "qty": 1, "amount": 50.00}
    ],
    "order_type": "online"                # Order type (if applicable)
}

Behavior: - Validates expiration date - Checks customer eligibility - Validates minimum order amount - Checks new vs. existing customer status - Validates usage limits (per customer and total) - Checks product/category criteria - Validates minimum quantity requirements - Calculates discount amount based on benefit type - Returns discount amount or error message

Returns: dict with keys: - discount_amount (Decimal): Amount to discount - error_message (string): Error message if voucher cannot be applied (optional)

Example:

# Apply voucher at checkout
voucher = get_model("sale.voucher").browse(voucher_id)

result = voucher.apply_voucher(context={
    "contact_id": customer_id,
    "amount_total": 150.00,
    "amount_ship": 10.00,
    "products": [
        {"product_id": laptop_id, "qty": 1, "amount": 150.00}
    ]
})

if "error_message" in result:
    print(f"Cannot apply voucher: {result['error_message']}")
else:
    print(f"Discount: ${result['discount_amount']}")
    # Apply discount to order


Validation Logic

The apply_voucher method performs extensive validation. Here's the complete validation flow:

1. Expiration Date Check

if voucher.expire_date and current_date > voucher.expire_date:
    return {"discount_amount": 0, "error_message": voucher.expire_date_msg or "This voucher is expired."}

2. Customer-Specific Voucher

if voucher.customer_id and customer_id != voucher.customer_id.id:
    return {"discount_amount": 0, "error_message": voucher.customer_msg or "This voucher can not apply to this customer."}

3. Minimum Order Amount

amount_total_ship = (amount_total or 0) + (amount_ship or 0)
if voucher.min_order_amount and amount_total_ship < voucher.min_order_amount:
    return {"discount_amount": 0, "error_message": voucher.min_order_amount_msg or "Order total is insufficient to use this voucher."}

4. New Customer Only

if voucher.new_customer:
    previous_orders = get_model("sale.order").search([
        ["contact_id", "=", customer_id],
        ["state", "!=", "voided"]
    ])
    if previous_orders:
        return {"discount_amount": 0, "error_message": voucher.new_customer_msg or "This voucher can only be used by new customers."}

5. Usage Limits

# Per customer limit
if voucher.max_orders_per_customer:
    customer_uses = get_model("sale.order").search([
        ["contact_id", "=", customer_id],
        ["voucher_id", "=", voucher.id],
        ["state", "!=", "voided"]
    ])
    if len(customer_uses) >= voucher.max_orders_per_customer:
        return {"discount_amount": 0, "error_message": voucher.max_orders_per_customer_msg or "The maximum usage limit has been reached for this voucher"}

# Total usage limit
if voucher.max_orders:
    total_uses = get_model("sale.order").search([
        ["voucher_id", "=", voucher.id],
        ["state", "!=", "voided"]
    ])
    if len(total_uses) >= voucher.max_orders:
        return {"discount_amount": 0, "error_message": voucher.max_orders_msg or "The maximum usage limit has been reached for this voucher"}

6. Product Category Criteria

if voucher.cond_product_categ_id:
    product_ids = [line["product_id"] for line in products]
    matching_products = get_model("product").search([
        ["id", "in", product_ids],
        ["categ_id", "child_of", voucher.cond_product_categ_id.id]
    ])
    if not matching_products:
        return {"discount_amount": 0, "error_message": voucher.cond_product_categ_msg or "Wrong product category"}

7. Specific Product Criteria

if voucher.cond_product_id:
    product_ids = [line["product_id"] for line in products]
    if voucher.cond_product_id.id not in product_ids:
        return {"discount_amount": 0, "error_message": voucher.cond_product_msg or "Wrong product"}

8. Quantity Validation

qty_check = 0
for line in products:
    if voucher.cond_product_id and line.get("product_id") != voucher.cond_product_id.id:
        continue
    # Additional category checks...
    qty_check += line.get("qty", 1)

if voucher.min_qty and qty_check < voucher.min_qty:
    return {"discount_amount": 0, "error_message": voucher.min_qty_msg or f"Order qty is too low ({qty_check} < {voucher.min_qty})"}

if voucher.qty_multiple and qty_check % voucher.qty_multiple != 0:
    return {"discount_amount": 0, "error_message": voucher.qty_multiple_msg or f"Order qty is not a multiple of {voucher.qty_multiple}"}

Discount Calculation

After validation passes, the discount is calculated based on benefit_type:

Fixed Discount on Order

if benefit_type == "fixed_discount_order":
    discount_amount = min(voucher.discount_amount, amount_total_ship or 0)

Percentage Discount on Order

if benefit_type == "percent_discount_order":
    discount_amount = amount_total * (voucher.discount_percent or 0) / 100
    discount_amount = min(discount_amount, amount_total_ship or 0)

Percentage Discount on Product

if benefit_type == "percent_discount_product":
    discount_amount = 0
    for line in products:
        if line.get("product_id") == voucher.cond_product_id.id:
            discount_amount += (line.get("amount") or 0) * (voucher.discount_percent or 0) / 100
    discount_amount = min(discount_amount, amount_total_ship or 0)

Search Functions

Search by Code

# Find voucher by code
voucher_ids = get_model("sale.voucher").search([
    ["code", "=", "SAVE20"]
])

Search Active Vouchers

# Get all active vouchers
active_vouchers = get_model("sale.voucher").search([
    ["state", "=", "active"]
])

Search by Sequence

# Get vouchers in sequence order
vouchers = get_model("sale.voucher").search([], order="sequence,code")

Model Relationship Description
sale.order One2Many (reverse) Orders that used this voucher
ecom2.cart One2Many (reverse) Carts with this voucher applied
product Many2One Configuration product and discount product
product.categ Many2One Product category criteria
product.group Many2Many Product groups for criteria and discounts
contact Many2One Specific customer for personalized vouchers
contact.group Many2Many Customer groups eligible for voucher

Common Use Cases

Use Case 1: New Customer Welcome Voucher

# $15 off for new customers on orders over $50

voucher_id = get_model("sale.voucher").create({
    "code": "WELCOME15",
    "state": "active",
    "product_id": product_id,
    "benefit_type": "fixed_discount_order",
    "discount_amount": 15.00,
    "min_order_amount": 50.00,
    "min_order_amount_msg": "Spend at least $50 to use this voucher",
    "new_customer": True,
    "new_customer_msg": "This welcome voucher is for new customers only",
    "max_orders_per_customer": 1,
    "max_orders_per_customer_msg": "You can only use this voucher once",
    "expire_date": "2026-12-31",
    "description": "Welcome! Get $15 off your first order over $50"
})

# Customer applies at checkout
result = get_model("sale.voucher").browse(voucher_id).apply_voucher(context={
    "contact_id": new_customer_id,
    "amount_total": 75.00,
    "amount_ship": 5.00,
    "products": [{"product_id": 101, "qty": 1, "amount": 75.00}]
})
# Returns: {"discount_amount": 15.00}

Use Case 2: Category-Specific Percentage Discount

# 20% off electronics, minimum 2 items

electronics_categ_id = get_model("product.categ").search([["name", "=", "Electronics"]])[0]

voucher_id = get_model("sale.voucher").create({
    "code": "ELEC20",
    "state": "active",
    "product_id": product_id,
    "benefit_type": "percent_discount_product",
    "discount_percent": 20.00,
    "cond_product_categ_id": electronics_categ_id,
    "cond_product_categ_msg": "This voucher applies only to electronics",
    "min_qty": 2,
    "min_qty_msg": "Buy at least 2 electronics items to use this voucher",
    "expire_date": "2026-06-30",
    "description": "Get 20% off electronics when you buy 2 or more items"
})

Use Case 3: Limited Quantity Flash Sale

# First 100 customers get $50 off

voucher_id = get_model("sale.voucher").create({
    "code": "FLASH50",
    "state": "active",
    "product_id": product_id,
    "benefit_type": "fixed_discount_order",
    "discount_amount": 50.00,
    "min_order_amount": 200.00,
    "max_orders": 100,  # Only first 100 orders
    "max_orders_msg": "Sorry, this flash sale has ended (limit reached)",
    "max_orders_per_customer": 1,
    "expire_date": "2026-07-15",
    "description": "Flash Sale: $50 off orders over $200 - First 100 customers only!"
})

Use Case 4: Buy X Get Y Free

# Buy 3 items of product X, get 1 free

product_x_id = 789

voucher_id = get_model("sale.voucher").create({
    "code": "BUY3GET1",
    "state": "active",
    "product_id": product_id,
    "benefit_type": "free_product",
    "discount_product_id": product_x_id,
    "discount_max_qty": 1,  # Only 1 free
    "cond_product_id": product_x_id,
    "cond_product_msg": "This voucher applies only to Product X",
    "min_qty": 3,
    "min_qty_msg": "Buy 3 to get 1 free",
    "description": "Buy 3, get 1 free!"
})

Use Case 5: VIP Customer Exclusive

# 30% off for VIP members only

vip_group_id = get_model("contact.group").search([["name", "=", "VIP Members"]])[0]

voucher_id = get_model("sale.voucher").create({
    "code": "VIP30",
    "state": "active",
    "product_id": product_id,
    "benefit_type": "percent_discount_order",  # Assuming this exists
    "discount_percent": 30.00,
    "contact_groups": [("set", [vip_group_id])],
    "contact_groups_msg": "This voucher is exclusive to VIP members",
    "min_order_amount": 500.00,
    "expire_date": "2026-12-31",
    "description": "VIP Exclusive: 30% off orders over $500"
})

Use Case 6: Personalized Birthday Voucher

# Create personalized birthday voucher for specific customer

birthday_customer_id = 456

voucher_id = get_model("sale.voucher").create({
    "code": f"BDAY-{birthday_customer_id}",  # Unique code
    "state": "active",
    "product_id": product_id,
    "benefit_type": "fixed_discount_order",
    "discount_amount": 25.00,
    "customer_id": birthday_customer_id,  # Only this customer
    "customer_msg": "This is a personalized birthday gift voucher",
    "max_orders_per_customer": 1,
    "expire_date": "2026-06-30",  # Valid for 30 days
    "description": "Happy Birthday! Enjoy $25 off your next purchase"
})

# Send email to customer
customer = get_model("contact").browse(birthday_customer_id)
send_email(customer.email, f"Your birthday voucher code: {voucher.code}")

Best Practices

1. Always Provide Custom Error Messages

# Good: Clear, helpful error messages
voucher_id = get_model("sale.voucher").create({
    "code": "SUMMER20",
    "min_order_amount": 100.00,
    "min_order_amount_msg": "Add $X more to your cart to use this voucher (minimum $100)",
    "new_customer": True,
    "new_customer_msg": "This summer welcome voucher is for new customers only. Check out our other deals!",
    "expire_date": "2026-08-31",
    "expire_date_msg": "This summer voucher has expired. Browse our current offers!"
})

# Bad: Generic error messages
voucher_id = get_model("sale.voucher").create({
    "code": "SUMMER20",
    "min_order_amount": 100.00,
    # No custom messages - users see generic errors
})

2. Set Appropriate Usage Limits

# Good: Prevent abuse with limits
voucher_id = get_model("sale.voucher").create({
    "code": "SAVE20",
    "max_orders_per_customer": 1,      # Once per customer
    "max_orders": 500,                  # Total limit
    "expire_date": "2026-12-31"        # Time limit
})

# Risky: No limits
voucher_id = get_model("sale.voucher").create({
    "code": "SAVE20",
    # No limits - could be very expensive!
})

3. Test Voucher Validation

# Good: Test voucher before going live

# Create inactive voucher first
voucher_id = get_model("sale.voucher").create({
    "code": "TEST-BLACKFRIDAY50",
    "state": "inactive",  # Test while inactive
    "discount_amount": 50.00,
    "min_order_amount": 200.00
})

# Test with sample data
test_result = get_model("sale.voucher").browse(voucher_id).apply_voucher(context={
    "contact_id": test_customer_id,
    "amount_total": 250.00,
    "amount_ship": 10.00,
    "products": [{"product_id": 101, "qty": 1, "amount": 250.00}]
})

if "error_message" not in test_result:
    print(f"Test passed: ${test_result['discount_amount']} discount")
    # Activate voucher
    get_model("sale.voucher").write([voucher_id], {"state": "active", "code": "BLACKFRIDAY50"})
else:
    print(f"Test failed: {test_result['error_message']}")

4. Use Sequence for Organization

# Good: Use sequence to control voucher order/priority
vouchers = [
    {"code": "BEST-DEAL", "sequence": "001"},     # Highest priority
    {"code": "GOOD-DEAL", "sequence": "002"},
    {"code": "OK-DEAL", "sequence": "003"}
]

for voucher in vouchers:
    get_model("sale.voucher").create({
        **voucher,
        "state": "active",
        "product_id": product_id,
        "benefit_type": "fixed_discount_order",
        "discount_amount": 10.00
    })

Performance Tips

1. Optimize Product Criteria Checks

The model uses efficient database queries for product category checks:

# Efficient: Single query with child_of operator
matching_products = get_model("product").search([
    ["id", "in", product_ids],
    ["categ_id", "child_of", category_id]
])

# Avoid: Loading all products and checking in Python

2. Cache Active Vouchers

# Cache active vouchers to reduce database queries
def get_active_vouchers():
    # Cache for 5 minutes
    cache_key = "active_vouchers"
    cached = cache.get(cache_key)
    if cached:
        return cached

    vouchers = get_model("sale.voucher").search([["state", "=", "active"]])
    cache.set(cache_key, vouchers, timeout=300)
    return vouchers

3. Validate Early

# Good: Quick checks first (avoid expensive queries)
def validate_voucher(voucher, context):
    # Quick checks
    if voucher.state != "active":
        return False
    if voucher.expire_date and current_date > voucher.expire_date:
        return False

    # Then expensive checks (database queries)
    if voucher.new_customer:
        previous_orders = get_model("sale.order").search([...])
        # ...

Troubleshooting

"Order total is insufficient to use this voucher"

Cause: Cart total (including shipping) is below min_order_amount Solution: - Check that amount_total + amount_ship >= min_order_amount - Display clear message showing how much more to add - Consider whether shipping should be included in minimum

"This voucher can only be used by new customers"

Cause: Customer has previous non-voided orders Solution: - Verify customer is actually new - Check for voided orders (they don't count) - Consider using customer registration date instead

"The maximum usage limit has been reached"

Cause: max_orders or max_orders_per_customer limit reached Solution: - Check current usage count - Consider increasing limit or creating new voucher - Review non-voided orders (voided orders don't count toward limit)

Voucher not applying expected discount

Cause: Validation failing silently or discount calculation issue Solution: - Check apply_voucher() return value for error_message - Verify benefit_type matches expected discount type - Check product/category criteria are met - Review minimum quantity requirements


Security Considerations

Permission Model

  • Create vouchers: Admin/marketing staff only
  • Apply vouchers: Public (validation ensures eligibility)
  • View voucher codes: Customers can try any code, validation enforces rules

Data Access

  • Customer-specific vouchers validated by customer_id check
  • Usage limits prevent abuse
  • Expired vouchers automatically rejected

Integration Points

Internal Modules

  • sale.order: Orders that use vouchers
  • ecom2.cart: Shopping carts with vouchers applied
  • product: Products for criteria and discounts
  • product.categ: Product category criteria
  • product.group: Product group criteria
  • contact: Customer restrictions
  • contact.group: Customer group eligibility

External Integration

  • Email Marketing: Send voucher codes to customers
  • Mobile Apps: Voucher input and validation
  • POS Systems: In-store voucher redemption

Version History

Last Updated: 2026-01-05 Model Version: sale_voucher.py (199 lines) Framework: Netforce


Additional Resources

  • Sale Order Documentation: sale.order
  • E-commerce Cart Documentation: ecom2.cart
  • Product Documentation: product
  • Contact Documentation: contact

Support & Feedback

For issues or questions about this module: 1. Test vouchers with apply_voucher() method directly 2. Review all validation rules and custom error messages 3. Check usage limits and expiration dates 4. Verify product/category criteria match cart contents 5. Test with sample data before activating


This documentation is generated for developer onboarding and reference purposes.