Skip to content

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:

_key = ["code"]

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:

_key = ["code"]

This translates to:

CREATE UNIQUE INDEX sale_coupon_code_unique
    ON sale_coupon (code);

State Workflow

available → in_use → used
    ↓          ↓
 expired ← expired
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: - availablein_use: Customer activates coupon (calls use_coupon()) - in_useused: Coupon usage completes successfully - availableexpired: Expiry date passes while coupon is available - in_useexpired: 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:

context = {
    "trigger_ids": [customer_id1, customer_id2],  # Customer IDs
}

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
)


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.