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¶
| 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¶
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")
Related Models¶
| 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_idcheck - 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.