Sale Coupon Documentation¶
Overview¶
The Sale Coupon module (sale.coupon) manages individual coupon instances that are distributed to customers. Each coupon has a unique code, tracks its usage state, and enforces time-based validity rules. Coupons are generated from sale.coupon.master templates and can be activated by customers to receive promotional discounts.
Model Information¶
Model Name: sale.coupon
Display Name: Coupon
Key Fields: code (unique coupon code)
Features¶
- Unique coupon code generation (auto-generated)
- State-based lifecycle (available/in_use/used/expired)
- Time-based activation and expiration
- Customer assignment and tracking
- Master template integration
- Automated state transitions via scheduled tasks
- Email-based customer search
Understanding Key Fields¶
What are Key Fields?¶
In Netforce models, Key Fields are a combination of fields that together create a composite unique identifier for a record.
For the sale.coupon model, the key field is:
This means the code field must be unique:
- code - The unique coupon code (e.g., "123-456-7890")
Why Key Fields Matter¶
Uniqueness Guarantee - Key fields prevent duplicate records by ensuring unique codes:
# Examples of valid codes:
Coupon 1: code = "123-456-7890" # Valid
Coupon 2: code = "234-567-8901" # Valid
Coupon 3: code = "345-678-9012" # Valid
# This would fail - duplicate key:
Coupon 4: code = "123-456-7890" # ERROR: Key already exists!
Database Implementation¶
The key field is enforced at the database level using a unique constraint:
This translates to:
State Workflow¶
| State | Description |
|---|---|
available |
Coupon is available for customer to use (initial state) |
in_use |
Customer has activated the coupon, time limit started |
used |
Coupon has been redeemed/used for a purchase |
expired |
Coupon has expired (either before or during use) |
State Transitions:
- available → in_use: Customer activates coupon (calls use_coupon())
- in_use → used: Coupon usage completes successfully
- available → expired: Expiry date passes while coupon is available
- in_use → expired: Use duration time limit expires
Key Fields Reference¶
Identification Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
code |
Char | Yes | Unique coupon code (auto-generated) |
master_id |
Many2One | Yes | Link to coupon master template |
contact_id |
Many2One | Yes | Customer who owns this coupon |
State and Status Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
state |
Selection | Yes | Current state (available/in_use/used/expired) |
active |
Boolean | Yes | Whether coupon is visible to customer |
Time Management Fields¶
| Field | Type | Description |
|---|---|---|
use_date |
DateTime | When customer activated the coupon (read-only) |
expiry_date |
DateTime | When coupon expires |
use_duration |
Integer | Minutes customer has to use after activation |
hide_date |
DateTime | When to hide coupon from customer view |
Computed Search Fields¶
| Field | Type | Description |
|---|---|---|
contact_email |
Char | Customer's email address (function field for searching) |
Code Generation¶
The model automatically generates unique coupon codes using the _get_code() method:
# Generated code format: XXX-YYY-ZZZZ
# Example: "123-456-7890"
# Pattern:
# - 3 random digits (000-999)
# - Hyphen
# - 3 random digits (000-999)
# - Hyphen
# - 4 random digits (0000-9999)
# Example codes:
"042-789-3456"
"987-123-0001"
"555-888-9999"
Uniqueness Check: The generation loops until a unique code is found that doesn't exist in the database.
API Methods¶
1. Create Coupon¶
Method: create(vals, context)
Creates a new coupon instance. Typically called by sale.coupon.master.create_coupons() rather than directly.
Parameters:
vals = {
"master_id": 123, # Required: coupon master template
"contact_id": 456, # Required: customer
"code": "auto-generated", # Optional: auto-generated if omitted
"state": "available", # Optional: defaults to "available"
"active": True, # Optional: defaults to True
"expiry_date": "2026-12-31 23:59:59", # Optional: from master
"use_duration": 120, # Optional: from master
"hide_date": "2027-01-01 00:00:00" # Optional: from master
}
Returns: int - New coupon ID
Example:
# Typically created via master, but can be created directly
coupon_id = get_model("sale.coupon").create({
"master_id": master_id,
"contact_id": customer_id,
"expiry_date": "2026-06-30 23:59:59",
"use_duration": 60 # 1 hour after activation
})
# Code is auto-generated: e.g., "789-012-3456"
2. Use Coupon (Activate)¶
Method: use_coupon(ids, context)
Customer activates the coupon, starting the usage time limit.
Parameters:
- ids (list): Coupon IDs to activate (typically one)
Behavior:
- Validates coupon state is "available"
- Checks expiry date hasn't passed
- Sets state to "in_use"
- Records current time as use_date
- If use_duration is set, calculates new expiry_date = now + use_duration minutes
- Raises exception if coupon is invalid or expired
Returns: None (raises exception on error)
Example:
# Customer activates their coupon
try:
get_model("sale.coupon").use_coupon([coupon_id])
print("Coupon activated successfully!")
# Customer now has 'use_duration' minutes to complete purchase
except Exception as e:
print(f"Error: {e}")
# Possible errors:
# - "Invalid coupon status" (already used/expired)
# - "Coupon is expired"
Effect on Coupon:
# Before activation:
{
"state": "available",
"use_date": None,
"expiry_date": "2026-12-31 23:59:59",
"use_duration": 120 # 2 hours
}
# After activation:
{
"state": "in_use",
"use_date": "2026-06-15 10:30:00",
"expiry_date": "2026-06-15 12:30:00", # 2 hours from now
"use_duration": 120
}
3. Update Coupon States (Scheduled Task)¶
Method: update_coupons(context)
Automated task that transitions coupons between states based on dates. Should be run periodically (e.g., every 5 minutes).
Behavior:
- Expire available coupons: Finds coupons in "available" state where expiry_date has passed → sets state to "expired"
- Complete in-use coupons: Finds coupons in "in_use" state where expiry_date has passed → sets state to "used" (time limit expired)
- Hide expired coupons: Finds active coupons where hide_date has passed → sets active to False
Example:
# Run as scheduled task (cron job every 5 minutes)
get_model("sale.coupon").update_coupons()
# This will:
# 1. Expire any available coupons past their expiry_date
# 2. Mark in-use coupons as used if use_duration has elapsed
# 3. Hide coupons past their hide_date
Typical Cron Configuration:
# In your scheduler configuration
{
"model": "sale.coupon",
"method": "update_coupons",
"schedule": "*/5 * * * *", # Every 5 minutes
"args": [],
"kwargs": {}
}
4. Create Individual Coupon (Event-Driven)¶
Method: create_individual_coupon(master_ids, context)
Creates coupons for specific customers, typically triggered by workflow events.
Context Options:
Behavior:
- For each customer in trigger_ids
- For each master in master_ids
- Creates one coupon with values from master
Example:
# When new customer registers, give them welcome coupon
def on_customer_signup(customer_id):
get_model("sale.coupon").create_individual_coupon(
[welcome_coupon_master_id],
context={"trigger_ids": [customer_id]}
)
Search Functions¶
Search by Customer¶
# Find all coupons for a specific customer
customer_coupons = get_model("sale.coupon").search([
["contact_id", "=", customer_id],
["active", "=", True]
])
Search by State¶
# Find available coupons for a customer
available_coupons = get_model("sale.coupon").search([
["contact_id", "=", customer_id],
["state", "=", "available"],
["active", "=", True]
])
Search by Email¶
# Find coupons by customer email
email_coupons = get_model("sale.coupon").search([
["contact_email", "=", "customer@example.com"],
["state", "!=", "used"]
])
Search by Code¶
# Look up a coupon by its code
coupon_ids = get_model("sale.coupon").search([
["code", "=", "123-456-7890"]
])
Computed Fields Functions¶
contact_email (function field)¶
Returns the customer's email address for this coupon. Used to enable searching coupons by customer email without needing to join tables.
Implementation:
# Defined as:
"contact_email": fields.Char(
"Contact Email",
function="_get_related",
function_search="_search_related",
function_context={"path": "contact_id.email"},
search=True
)
Related Models¶
| Model | Relationship | Description |
|---|---|---|
sale.coupon.master |
Many2One | Template this coupon was generated from |
contact |
Many2One | Customer who owns this coupon |
Common Use Cases¶
Use Case 1: Customer Activates and Uses Coupon¶
# Full coupon lifecycle from customer perspective
# 1. Customer views their available coupons
customer_id = 456
available = get_model("sale.coupon").search_browse([
["contact_id", "=", customer_id],
["state", "=", "available"],
["active", "=", True]
])
for coupon in available:
print(f"Code: {coupon.code}, Expires: {coupon.expiry_date}")
# 2. Customer activates a coupon
coupon_id = available[0].id
try:
get_model("sale.coupon").use_coupon([coupon_id])
print("Coupon activated! You have 2 hours to complete your purchase.")
except Exception as e:
print(f"Cannot activate: {e}")
# 3. Customer completes purchase using the coupon
# (Application logic validates coupon and applies discount)
# 4. Mark coupon as used
get_model("sale.coupon").write([coupon_id], {"state": "used"})
Use Case 2: Flash Sale with Time Limit¶
# Coupon that must be used within 30 minutes of activation
# 1. Coupon is created with use_duration
coupon = get_model("sale.coupon").browse(coupon_id)
# use_duration = 30 minutes
# state = "available"
# 2. Customer activates coupon at 10:00 AM
get_model("sale.coupon").use_coupon([coupon_id])
# state = "in_use"
# use_date = "2026-06-15 10:00:00"
# expiry_date = "2026-06-15 10:30:00" # 30 minutes later
# 3. Customer completes purchase at 10:15 AM (within time)
get_model("sale.coupon").write([coupon_id], {"state": "used"})
# 4. Alternative: Customer doesn't complete purchase in time
# Scheduled task runs at 10:35 AM:
get_model("sale.coupon").update_coupons()
# Finds coupon with state="in_use" and expiry_date < now
# Sets state to "used" (time expired)
Use Case 3: Birthday Coupon Distribution¶
# Monthly job: Create birthday coupons
import datetime
from datetime import timedelta
# 1. Find customers with birthdays this month
today = datetime.date.today()
month_start = today.replace(day=1)
month_end = (month_start + timedelta(days=32)).replace(day=1) - timedelta(days=1)
birthday_customers = get_model("contact").search([
["birthdate", ">=", month_start.strftime("%Y-%m-%d")],
["birthdate", "<=", month_end.strftime("%Y-%m-%d")]
])
# 2. Create individual coupons (valid for 30 days from birthday)
birthday_master_id = 100 # Predefined birthday coupon master
for customer_id in birthday_customers:
customer = get_model("contact").browse(customer_id)
expiry = customer.birthdate + timedelta(days=30)
coupon_id = get_model("sale.coupon").create({
"master_id": birthday_master_id,
"contact_id": customer_id,
"expiry_date": expiry.strftime("%Y-%m-%d 23:59:59"),
"use_duration": None # No time limit after activation
})
# Send email notification
send_email(customer.email, f"Your birthday coupon: {coupon.code}")
Use Case 4: Coupon Code Lookup and Validation¶
# Customer enters coupon code at checkout
def validate_coupon_code(code, customer_id):
# 1. Find coupon by code
coupon_ids = get_model("sale.coupon").search([
["code", "=", code]
])
if not coupon_ids:
return {"valid": False, "error": "Invalid coupon code"}
coupon = get_model("sale.coupon").browse(coupon_ids[0])
# 2. Check ownership
if coupon.contact_id.id != customer_id:
return {"valid": False, "error": "This coupon belongs to another customer"}
# 3. Check state
if coupon.state != "available":
return {"valid": False, "error": f"Coupon is {coupon.state}"}
# 4. Check expiry
from datetime import datetime
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if coupon.expiry_date and now > coupon.expiry_date:
return {"valid": False, "error": "Coupon has expired"}
# 5. Check active
if not coupon.active:
return {"valid": False, "error": "Coupon is no longer available"}
return {"valid": True, "coupon_id": coupon.id, "master": coupon.master_id.name}
# Usage at checkout
result = validate_coupon_code("123-456-7890", current_customer_id)
if result["valid"]:
# Activate and apply discount
get_model("sale.coupon").use_coupon([result["coupon_id"]])
else:
print(f"Error: {result['error']}")
Use Case 5: Expired Coupon Cleanup¶
# Scheduled cleanup task (runs daily)
from datetime import datetime, timedelta
def cleanup_expired_coupons():
# 1. Update all coupon states (handles expiration)
get_model("sale.coupon").update_coupons()
# 2. Find coupons that should be hidden (past hide_date)
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
to_hide = get_model("sale.coupon").search([
["active", "=", True],
["hide_date", "<=", now]
])
if to_hide:
get_model("sale.coupon").write(to_hide, {"active": False})
print(f"Hidden {len(to_hide)} expired coupons")
# 3. Optional: Archive very old coupons (e.g., > 1 year expired)
one_year_ago = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")
old_coupons = get_model("sale.coupon").search([
["state", "in", ["expired", "used"]],
["expiry_date", "<", one_year_ago]
])
print(f"Found {len(old_coupons)} coupons older than 1 year")
# Could move to archive table or mark for deletion
Use Case 6: Coupon Usage Analytics¶
# Report: Coupon master performance
def analyze_coupon_master_performance(master_id):
# Get all coupons from this master
all_coupons = get_model("sale.coupon").search([
["master_id", "=", master_id]
])
coupons = get_model("sale.coupon").browse(all_coupons)
stats = {
"total_issued": len(all_coupons),
"available": 0,
"in_use": 0,
"used": 0,
"expired": 0,
"redemption_rate": 0
}
for coupon in coupons:
stats[coupon.state] += 1
if stats["total_issued"] > 0:
stats["redemption_rate"] = (stats["used"] / stats["total_issued"]) * 100
return stats
# Example usage
master_id = 123
stats = analyze_coupon_master_performance(master_id)
print(f"Coupon Campaign Performance:")
print(f"Total Issued: {stats['total_issued']}")
print(f"Used: {stats['used']}")
print(f"Redemption Rate: {stats['redemption_rate']:.2f}%")
print(f"Still Available: {stats['available']}")
print(f"Expired Unused: {stats['expired']}")
Best Practices¶
1. Always Use update_coupons() Scheduled Task¶
# Good: Run update_coupons regularly
# Cron job every 5 minutes
{
"model": "sale.coupon",
"method": "update_coupons",
"schedule": "*/5 * * * *"
}
# Bad: Manual state management
# Don't manually track expiration - let the system do it
2. Validate Before Activation¶
# Good: Check all conditions before use_coupon()
coupon = get_model("sale.coupon").browse(coupon_id)
if coupon.state != "available":
print("Coupon is not available")
elif coupon.contact_id.id != current_customer:
print("This coupon belongs to another customer")
else:
try:
get_model("sale.coupon").use_coupon([coupon_id])
except Exception as e:
print(f"Activation failed: {e}")
# Bad: Just call use_coupon without checks
get_model("sale.coupon").use_coupon([coupon_id]) # May raise unexpected errors
3. Don't Reuse Coupon Codes¶
# The _key = ["code"] constraint ensures uniqueness
# Each coupon gets a unique code
# Good: Let the system generate codes
coupon_id = get_model("sale.coupon").create({
"master_id": master_id,
"contact_id": customer_id
# code is auto-generated
})
# Bad: Don't try to manually set codes
coupon_id = get_model("sale.coupon").create({
"master_id": master_id,
"contact_id": customer_id,
"code": "MY-CODE" # Risk of duplicate error
})
4. Set Appropriate use_duration¶
# Good: Reasonable time limits based on campaign type
# Flash sale - short duration
{
"use_duration": 30 # 30 minutes
}
# Standard coupon - longer duration
{
"use_duration": 1440 # 24 hours
}
# No rush - no duration limit
{
"use_duration": None # Use until expiry_date
}
Performance Tips¶
1. Index Critical Fields¶
The model marks key fields as searchable:
- master_id - searchable
- contact_id - searchable
- state - searchable
- code - unique key (automatically indexed)
2. Batch State Updates¶
The update_coupons() method uses efficient bulk updates:
# Efficient: Bulk update
ids = self.search([["state", "=", "available"], ["expiry_date", "<=", t]])
if ids:
self.write(ids, {"state": "expired"})
# Inefficient: Loop and update one by one
for id in ids:
self.write([id], {"state": "expired"}) # Many DB calls
3. Limit Active Coupon Queries¶
# Good: Filter for active coupons
coupons = get_model("sale.coupon").search([
["contact_id", "=", customer_id],
["active", "=", True],
["state", "=", "available"]
])
# Less efficient: Get all and filter in Python
all_coupons = get_model("sale.coupon").search([["contact_id", "=", customer_id]])
active = [c for c in get_model("sale.coupon").browse(all_coupons) if c.active and c.state == "available"]
Troubleshooting¶
"Invalid coupon status" error when activating¶
Cause: Coupon state is not "available"
Solution:
- Check current state: could be "in_use", "used", or "expired"
- Verify customer hasn't already activated it
- Run update_coupons() to sync states
"Coupon is expired" error¶
Cause: Expiry date has passed
Solution:
- Check expiry_date field
- Verify system time is correct
- Consider extending expiry for specific customers if needed
"Duplicate key error on code"¶
Cause: Trying to create coupon with existing code
Solution:
- Don't manually set code - let auto-generation handle it
- Code generation loops until unique, but ensure random is working
- Check database for orphaned codes
Coupons not transitioning states automatically¶
Cause: update_coupons() scheduled task not running
Solution:
- Verify cron job is configured
- Check system scheduler is active
- Manually run update_coupons() to test
- Check for errors in scheduler logs
Security Considerations¶
Permission Model¶
- Create coupons: Marketing/admin only (via master.create_coupons)
- View own coupons: Customers can see only their coupons
- Activate coupons: Customers can activate only their own coupons
Data Access¶
- Coupon ownership validated in
use_coupon() - Codes are unpredictable (random generation)
- States prevent reuse of coupons
Database Constraints¶
Unique Code Constraint¶
-- The _key = ["code"] creates a unique constraint
CREATE UNIQUE INDEX sale_coupon_code_unique
ON sale_coupon (code);
This ensures no two coupons can have the same code.
Integration Points¶
Internal Modules¶
- sale.coupon.master: Template source for coupons
- sale.promotion: Promotions may require specific coupons
- contact: Customer ownership and email lookup
External Integration¶
- Email Systems: Send coupon codes to customers
- Mobile Apps: Display customer's available coupons
- POS Systems: Validate and redeem coupons in-store
Version History¶
Last Updated: 2026-01-05 Model Version: sale_coupon.py (95 lines) Framework: Netforce
Additional Resources¶
- Sale Coupon Master Documentation:
sale.coupon.master - Sale Promotion Documentation:
sale.promotion - Contact Documentation:
contact
Support & Feedback¶
For issues or questions about this module: 1. Check sale.coupon.master documentation for campaign setup 2. Verify scheduled task is running for state transitions 3. Review customer coupon ownership and permissions 4. Test state lifecycle: available → in_use → used
This documentation is generated for developer onboarding and reference purposes.