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:
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:
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
Related Models¶
| 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.