Skip to content

Recurring Sales Documentation

Overview

The Recurring Sales module (sale.recurring) manages subscription-based and recurring sales orders. It automates the generation of periodic sales orders based on configurable schedules (daily, monthly, yearly) and tracks the next run date for each subscription. This module is ideal for businesses with subscription services, maintenance contracts, rental agreements, or any recurring revenue model.


Model Information

Model Name: sale.recurring Display Name: Recurring Sales Name Field: Not defined (uses default ID) Order: next_date (sorted by next scheduled date)

Features

  • ✅ Audit logging enabled (_audit_log = True)
  • ✅ Multi-company support (company_id)
  • ❌ Full-text content search
  • ❌ Unique key constraint

Subscription Intervals

The module supports flexible recurring intervals:

Interval Unit Code Description Example
Day day Daily recurring orders Every 1 day, Every 7 days
Month month Monthly recurring orders Every 1 month, Every 3 months (quarterly)
Year year Yearly recurring orders Every 1 year, Every 2 years

Key Fields Reference

Core Subscription Fields

Field Type Required Description
contact_id Many2One Customer for the recurring order
description Text Description of the subscription
product_id Many2One Product to sell (if single-product subscription)
qty Decimal Quantity per recurring order
unit_price Decimal Unit price for the product

Schedule Fields

Field Type Required Description
interval_num Integer Number of interval units (e.g., "3" for quarterly)
interval_unit Selection Unit of time: day/month/year
next_date Date Next scheduled order generation date
active Boolean Whether subscription is active (default: True)

Financial Fields

Field Type Description
amount Decimal Calculated amount (qty × unit_price)

Procurement Fields

Field Type Description
supplier_id Many2One Supplier for back-to-back procurement

Administrative Fields

Field Type Description
company_id Many2One Company that owns this subscription

API Methods

1. Create Subscription

Method: create(vals, context)

Creates a new recurring sales subscription.

Parameters:

vals = {
    "contact_id": 123,              # Required: Customer
    "description": "Monthly...",    # Required: Description
    "product_id": 789,              # Product for subscription
    "qty": 1,                       # Required: Quantity
    "unit_price": 99.00,            # Required: Price
    "interval_num": 1,              # Required: Every 1...
    "interval_unit": "month",       # Required: ...month
    "next_date": "2025-02-01",      # Required: First run date
    "active": True,                 # Active subscription
    "supplier_id": 456              # Optional: Supplier
}

Returns: int - New subscription ID

Example:

# Create monthly subscription
subscription_id = get_model("sale.recurring").create({
    "contact_id": 123,
    "description": "Monthly Software License - Premium Plan",
    "product_id": 789,
    "qty": 1,
    "unit_price": 99.00,
    "interval_num": 1,
    "interval_unit": "month",
    "next_date": "2025-02-01",
    "active": True
})


2. Send Reminder Emails

Method: send_reminders(days_before, from_addr, to_addrs, context)

Sends email reminders for expiring or expired subscriptions.

Parameters: - days_before (int): Number of days before expiry to send warning - from_addr (str): Email sender address - to_addrs (str): Comma-separated recipient addresses - context (dict): Optional context

Behavior: - Searches for subscriptions expiring within days_before days - Searches for subscriptions already expired - Sends two separate emails: - Warning: Subscriptions expiring soon - Urgent: Subscriptions already expired - Queues emails for asynchronous sending

Returns: str - Summary message (e.g., "2 expiring soon, 1 expired")

Example:

# Send reminders for subscriptions expiring in next 7 days
result = get_model("sale.recurring").send_reminders(
    days_before=7,
    from_addr="subscriptions@company.com",
    to_addrs="manager@company.com,sales@company.com"
)
# Returns: "5 expiring soon, 2 expired"

Email Format:

Warning Email (Expiring Soon):

Subject: Reminder: 5 subscriptions expiring soon

2025-02-01 ABC Corp Monthly Software License
================================================================================
2025-02-03 XYZ Inc Annual Maintenance Contract
================================================================================
...

Urgent Email (Already Expired):

Subject: URGENT Reminder: 2 subscriptions expired

2025-01-01 Old Customer Monthly Service
================================================================================
2025-01-15 Another Corp Quarterly Support
================================================================================
...


Computed Fields Functions

get_amount(ids, context)

Calculates the amount for each subscription.

Formula:

amount = (qty or 0) * (unit_price or 0)

Returns: Dictionary mapping subscription IDs to calculated amounts


Search Functions

Search by Customer

# Find all subscriptions for a customer
condition = [["contact_id", "=", customer_id]]
subscription_ids = get_model("sale.recurring").search(condition)

Search by Product

# Find all subscriptions for a product
condition = [["product_id", "=", product_id]]
subscription_ids = get_model("sale.recurring").search(condition)

Search by Interval

# Find all monthly subscriptions
condition = [["interval_unit", "=", "month"]]
subscription_ids = get_model("sale.recurring").search(condition)

Search by Active Status

# Find all active subscriptions
condition = [["active", "=", True]]
subscription_ids = get_model("sale.recurring").search(condition)

Search by Next Date Range

# Find subscriptions due in next 30 days
from datetime import datetime, timedelta

today = datetime.today().strftime("%Y-%m-%d")
future = (datetime.today() + timedelta(days=30)).strftime("%Y-%m-%d")

condition = [
    ["next_date", ">=", today],
    ["next_date", "<=", future],
    ["active", "=", True]
]
subscription_ids = get_model("sale.recurring").search(condition)

Common Use Cases

Use Case 1: Create Monthly Subscription Service

# Create a monthly SaaS subscription

subscription_id = get_model("sale.recurring").create({
    "contact_id": 123,
    "description": "Cloud Storage Premium - 1TB",
    "product_id": 789,  # Cloud Storage 1TB product
    "qty": 1,
    "unit_price": 29.99,
    "interval_num": 1,
    "interval_unit": "month",
    "next_date": "2025-02-01",  # First billing date
    "active": True
})

print(f"Created subscription {subscription_id}")

Use Case 2: Create Quarterly Maintenance Contract

# Create quarterly maintenance with 3-month interval

subscription_id = get_model("sale.recurring").create({
    "contact_id": 456,
    "description": "Equipment Maintenance Contract - Quarterly",
    "product_id": 999,  # Maintenance service product
    "qty": 1,
    "unit_price": 499.00,
    "interval_num": 3,  # Every 3 months
    "interval_unit": "month",
    "next_date": "2025-04-01",
    "active": True
})

Use Case 3: Create Annual Subscription

# Create annual software license

subscription_id = get_model("sale.recurring").create({
    "contact_id": 789,
    "description": "Enterprise Software License - Annual",
    "product_id": 1000,
    "qty": 50,  # 50 user licenses
    "unit_price": 199.00,  # Per user
    "interval_num": 1,
    "interval_unit": "year",
    "next_date": "2026-01-01",
    "active": True
})

Use Case 4: Automated Reminder System

# Set up daily cron job to check and send reminders

def check_subscription_reminders():
    """Run daily to send subscription reminders"""

    # Send warnings 14 days before expiry
    result = get_model("sale.recurring").send_reminders(
        days_before=14,
        from_addr="subscriptions@company.com",
        to_addrs="sales@company.com,manager@company.com"
    )

    print(f"Reminder check: {result}")

# Schedule this function in cron or task scheduler

Use Case 5: Generate Recurring Orders (Custom Implementation)

# Custom script to generate sales orders from subscriptions
# Note: This is example logic - actual implementation may vary

from datetime import datetime, timedelta

def generate_recurring_orders():
    """Generate sales orders for due subscriptions"""

    today = datetime.today().strftime("%Y-%m-%d")

    # Find subscriptions due today or overdue
    subscription_ids = get_model("sale.recurring").search([
        ["next_date", "<=", today],
        ["active", "=", True]
    ])

    for sub in get_model("sale.recurring").browse(subscription_ids):
        # Create sales order
        order_vals = {
            "contact_id": sub.contact_id.id,
            "date": today,
            "ref": f"Subscription: {sub.description}",
            "lines": [
                ("create", {
                    "product_id": sub.product_id.id,
                    "description": sub.description,
                    "qty": sub.qty,
                    "unit_price": sub.unit_price,
                    "uom_id": sub.product_id.uom_id.id
                })
            ]
        }

        order_id = get_model("sale.order").create(order_vals)
        print(f"Created order {order_id} for subscription {sub.id}")

        # Calculate next run date
        if sub.interval_unit == "day":
            next_date = datetime.strptime(sub.next_date, "%Y-%m-%d") + \
                       timedelta(days=sub.interval_num)
        elif sub.interval_unit == "month":
            # Add months (simplified - consider proper date library)
            current = datetime.strptime(sub.next_date, "%Y-%m-%d")
            month = current.month + sub.interval_num
            year = current.year + (month - 1) // 12
            month = ((month - 1) % 12) + 1
            next_date = current.replace(year=year, month=month)
        elif sub.interval_unit == "year":
            current = datetime.strptime(sub.next_date, "%Y-%m-%d")
            next_date = current.replace(year=current.year + sub.interval_num)

        # Update subscription next date
        sub.write({"next_date": next_date.strftime("%Y-%m-%d")})
        print(f"Updated subscription {sub.id} next date to {next_date}")

# Run this periodically (e.g., daily cron job)

Use Case 6: Deactivate Expired Subscriptions

# Find and deactivate subscriptions that should no longer run

def deactivate_expired_subscriptions(cutoff_date):
    """Deactivate subscriptions past their final date"""

    subscription_ids = get_model("sale.recurring").search([
        ["next_date", "<", cutoff_date],
        ["active", "=", True]
    ])

    if subscription_ids:
        get_model("sale.recurring").write(subscription_ids, {
            "active": False
        })

        print(f"Deactivated {len(subscription_ids)} expired subscriptions")

Use Case 7: Subscription Reporting

# Generate subscription revenue report

def get_subscription_metrics():
    """Calculate key subscription metrics"""

    # Get all active subscriptions
    subscriptions = get_model("sale.recurring").search_read(
        [["active", "=", True]],
        ["contact_id", "amount", "interval_unit", "interval_num"]
    )

    # Calculate Monthly Recurring Revenue (MRR)
    mrr = 0
    arr = 0

    for sub in subscriptions:
        monthly_value = 0

        if sub["interval_unit"] == "month":
            # Convert to monthly
            monthly_value = sub["amount"] / sub["interval_num"]
        elif sub["interval_unit"] == "year":
            # Convert annual to monthly
            monthly_value = sub["amount"] / (sub["interval_num"] * 12)
        elif sub["interval_unit"] == "day":
            # Convert daily to monthly (approximate)
            monthly_value = sub["amount"] * (30 / sub["interval_num"])

        mrr += monthly_value

    arr = mrr * 12

    return {
        "mrr": mrr,
        "arr": arr,
        "active_subscriptions": len(subscriptions)
    }

# Use the metrics
metrics = get_subscription_metrics()
print(f"MRR: ${metrics['mrr']:,.2f}")
print(f"ARR: ${metrics['arr']:,.2f}")
print(f"Active Subscriptions: {metrics['active_subscriptions']}")

Best Practices

1. Always Set Next Date Correctly

# Bad: Setting next_date in the past
{
    "next_date": "2024-01-01"  # Already passed!
}

# Good: Set to future date when subscription should start
{
    "next_date": "2025-02-01"  # Future date
}

2. Use Descriptive Subscription Names

# Bad: Vague description
{
    "description": "Monthly service"
}

# Good: Clear, detailed description
{
    "description": "Cloud Storage Premium Plan - 1TB - Monthly Billing"
}

3. Set Appropriate Interval for Business Model

# Quarterly subscription (every 3 months)
{
    "interval_num": 3,
    "interval_unit": "month"  # Not "quarter" - use month with num=3
}

# Bi-weekly subscription (every 14 days)
{
    "interval_num": 14,
    "interval_unit": "day"
}

# Semi-annual subscription (every 6 months)
{
    "interval_num": 6,
    "interval_unit": "month"
}

4. Implement Automated Processing

# Set up cron job or scheduler to:
# 1. Generate orders daily
# 2. Send reminders weekly
# 3. Update metrics monthly

# Example crontab entries:
# 0 2 * * * python /path/to/generate_recurring_orders.py
# 0 8 * * 1 python /path/to/send_reminders.py  # Monday 8 AM

Performance Tips

1. Index Next Date for Performance

The model orders by next_date by default, making searches efficient:

# Efficient - uses index
condition = [["next_date", "<=", today]]

2. Batch Process Subscriptions

# Good: Process in batches
batch_size = 100
offset = 0

while True:
    subscription_ids = get_model("sale.recurring").search(
        [["active", "=", True]],
        limit=batch_size,
        offset=offset
    )

    if not subscription_ids:
        break

    # Process batch
    process_subscriptions(subscription_ids)

    offset += batch_size

Model Relationship Description
contact Many2One Customer for the subscription
product Many2One Product being sold
company Many2One Company owning the subscription
sale.order Indirect Orders generated from subscription
email.message Indirect Reminder emails sent

Troubleshooting

Reminders Not Sending

Cause: Missing email configuration or invalid addresses Solution: Verify email settings and recipient addresses; check email queue

Subscriptions Not Generating Orders

Cause: No automated task configured Solution: The model doesn't auto-generate orders - implement custom logic or scheduled task

Incorrect Next Date Calculation

Cause: Manual date calculation errors Solution: Use proper date libraries (datetime, dateutil) for date arithmetic

Inactive Subscriptions Processing

Cause: Not filtering by active=True Solution: Always include ["active", "=", True] in search conditions


Configuration Settings

Required Settings

Setting Location Description
Email Server Settings > Email SMTP configuration for reminders
Company Settings Default company for subscriptions
Products Product Master Products for subscriptions

Optional Settings

Setting Default Description
Active True Default subscription status
Supplier None Default supplier for procurement

Integration Points

Sales

  • sale.order: Generate recurring orders from subscriptions
  • contact: Customer information and billing
  • product: Product details and pricing

Email

  • email.message: Send reminder emails
  • Automated email queue processing

Multi-Company

  • company: Multi-company subscription isolation

Version History

Last Updated: 2025-01-05 Model Version: sale_recurring.py (101 lines) Framework: Netforce


Additional Resources

  • Sales Order Documentation: sale.order
  • Contact Documentation: contact
  • Product Documentation: product
  • Email Message Documentation: email.message

Support & Feedback

For issues or questions about this module: 1. Verify email configuration for reminders 2. Implement order generation logic per business needs 3. Set up appropriate cron jobs/schedulers 4. Test subscription calculations thoroughly 5. Monitor next_date accuracy


This documentation is generated for developer onboarding and reference purposes.