Sales Activity Tracking Documentation¶
Overview¶
The Sales Activity module (sale.activ) provides comprehensive activity and task management for CRM processes. It tracks interactions, to-do items, and scheduled events related to sales opportunities, quotations, orders, and contacts. The model supports polymorphic relationships, allowing activities to be linked to various CRM entities, and includes reminder notifications to keep sales teams on schedule.
Model Information¶
Model Name: sale.activ
Display Name: Activity
Name Field: subject (used for display)
Features¶
- No audit logging
- No multi-company support
- Polymorphic relationships via
related_id - Reminder notification system
- Activity type categorization
- Priority and state tracking
Understanding Activity Management¶
What are Sales Activities?¶
Sales Activities are scheduled tasks, events, and interaction records that help manage the sales process:
Activity Types: - Email: Planned or completed email communications - WhatsApp: WhatsApp message interactions - Call: Phone call activities - Meeting: Scheduled meetings or appointments - Notes: Simple notes or memos - Event: Calendar events - Task: To-do items and action items
Polymorphic Relationships¶
The related_id field uses a Reference field type, allowing activities to be linked to multiple entity types:
related_id: Reference([
["sale.opportunity", "Opportunity"],
["sale.quot", "Quotation"],
["sale.order", "Sales Order"]
])
This means a single activity model serves all CRM entities, providing a unified activity stream.
Activity Lifecycle¶
States¶
| State | Description |
|---|---|
new |
Not Started - activity created but not begun |
in_progress |
In Progress - actively working on activity |
done |
Completed - activity finished |
waiting |
Waiting on someone else - blocked by external dependency |
deferred |
Deferred - postponed to later date |
Priority Levels¶
| Priority | Use Case |
|---|---|
high |
Urgent activities requiring immediate attention |
normal |
Standard priority (default) |
low |
Nice-to-have activities, no urgency |
Key Fields Reference¶
Core Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
type |
Selection | Yes | Activity type (email, call, meeting, task, etc.) |
subject |
Char(128) | Yes | Activity title/subject |
user_id |
Many2One | Yes | User assigned to activity |
date |
Date | Yes | Activity date (defaults to today) |
state |
Selection | Yes | Activity status (defaults to "new") |
Relationship Fields¶
| Field | Type | Description |
|---|---|---|
related_id |
Reference | Polymorphic link to opportunity/quotation/order |
contact_id |
Many2One | Associated contact (auto-filled from related record) |
Content Fields¶
| Field | Type | Description |
|---|---|---|
description |
Text | Detailed activity description |
body |
Text | Activity body/content |
notes |
Text | Additional notes |
Schedule Fields¶
| Field | Type | Description |
|---|---|---|
due_date |
Date | Due date for completion |
start_time |
Time | Start time (for meetings/events) |
end_time |
Time | End time (for meetings/events) |
location |
Char | Meeting location |
Communication Fields¶
| Field | Type | Description |
|---|---|---|
phone |
Char | Phone number for calls |
email |
Char | Email address |
Reminder Fields¶
| Field | Type | Description |
|---|---|---|
send_reminder |
Boolean | Enable reminder notification |
notif_time |
DateTime | When to send notification |
notif_email |
Char | Email address for notification |
other_users |
Many2Many | Additional users to notify |
Computed Fields¶
| Field | Type | Description |
|---|---|---|
priority |
Selection | Activity priority (high/normal/low) |
notifs |
One2Many | Reminder notifications created |
API Methods¶
1. Create Activity¶
Method: create(vals, context)
Creates a new activity and automatically sets up reminders if enabled.
Parameters:
vals = {
"type": "meeting", # Required
"subject": "Demo presentation", # Required
"user_id": 5, # Required (or defaults to current user)
"date": "2026-02-15", # Required (or defaults to today)
"related_id": "sale.opportunity,123", # Link to opportunity
"contact_id": 456, # Associated contact
"description": "Product demo for enterprise client",
"start_time": "14:00:00",
"end_time": "15:30:00",
"location": "Customer office - Conference Room A",
"priority": "high",
"state": "new",
"send_reminder": True, # Enable reminder
"notif_time": "2026-02-15 13:30:00", # Remind 30 min before
"other_users": [("set", [6, 7])] # Notify other team members
}
Returns: int - New activity ID
Example:
# Create call activity for opportunity follow-up
activity_id = get_model("sale.activ").create({
"type": "call",
"subject": "Follow up on quotation",
"user_id": user_id,
"date": "2026-01-20",
"due_date": "2026-01-20",
"related_id": f"sale.opportunity,{opport_id}",
"contact_id": contact_id,
"phone": "+1-555-1234",
"description": "Discuss pricing and timeline concerns",
"priority": "high",
"send_reminder": True,
"notif_time": "2026-01-20 09:00:00"
})
# Reminder notification created automatically via create() override
Auto-Generated Fields:
- state: Defaults to "new"
- date: Defaults to today
- user_id: Defaults to current user
- contact_id: Defaults from related record if available
2. Update Activity¶
Method: write(ids, vals, context)
Updates activity and refreshes reminders if reminder settings change.
Example:
# Update activity and reschedule reminder
get_model("sale.activ").write([activity_id], {
"state": "in_progress",
"notif_time": "2026-01-20 08:00:00", # Change reminder time
"notes": "Customer requested earlier call time"
})
# Reminder notifications automatically updated via write() override
Auto-Triggered: When send_reminder or notif_time changes, update_reminders() is called automatically.
3. Update Reminders¶
Method: update_reminders(ids, context)
Manages reminder notifications for activities.
Behavior:
1. Deletes existing notifications for this activity
2. If send_reminder is True:
- Creates notification for assigned user
- Creates notifications for all other_users
- Sets notification time (defaults to activity date at 06:00 if not specified)
- Schedules email notification using configured template
Example:
# Manually trigger reminder update
get_model("sale.activ").update_reminders([activity_id])
# Creates notification:
# - Title: "Reminder: [activity subject]"
# - Time: notif_time or date + 06:00:00
# - Users: user_id + other_users
# - Email: sent via template from settings
Settings Required:
- settings.sale_activity_email_template_id: Email template for activity reminders
4. Onchange: Send Reminder¶
Method: onchange_send_reminder(context)
UI helper that auto-fills notification time when reminder is enabled.
Behavior:
When user checks "send_reminder" checkbox:
- Sets notif_time = date + (start_time or "06:00:00")
Example:
# In UI, when user enables reminder:
data = {
"date": "2026-01-25",
"start_time": "14:00:00",
"send_reminder": True
}
result = get_model("sale.activ").onchange_send_reminder(
context={"data": data}
)
# Result: data["notif_time"] = "2026-01-25 14:00:00"
5. Default Contact Selection¶
Method: _get_contact(context) (internal default method)
Automatically selects contact from related record when creating activity.
Behavior:
- Extracts related_id from context defaults
- Looks up related record (opportunity, quotation, order)
- Returns related record's contact_id if exists
Example:
# When creating activity from opportunity form:
context = {
"defaults": {
"related_id": "sale.opportunity,123"
}
}
# System automatically sets:
# contact_id = opportunity[123].contact_id
Activity Types¶
Email Activities¶
get_model("sale.activ").create({
"type": "email",
"subject": "Send pricing proposal",
"user_id": user_id,
"date": "2026-01-18",
"related_id": f"sale.opportunity,{opport_id}",
"email": "customer@company.com",
"description": "Send updated pricing based on volume discounts discussed",
"priority": "high"
})
Call Activities¶
get_model("sale.activ").create({
"type": "call",
"subject": "Discovery call with CTO",
"user_id": user_id,
"date": "2026-01-19",
"start_time": "10:00:00",
"end_time": "11:00:00",
"related_id": f"sale.opportunity,{opport_id}",
"phone": "+1-555-9876",
"description": "Discuss technical requirements and integration needs",
"send_reminder": True
})
Meeting Activities¶
get_model("sale.activ").create({
"type": "meeting",
"subject": "Product demonstration",
"user_id": user_id,
"date": "2026-01-22",
"start_time": "14:00:00",
"end_time": "16:00:00",
"location": "Customer HQ - Board Room",
"related_id": f"sale.opportunity,{opport_id}",
"description": "Full product demo with decision-makers",
"other_users": [("set", [sales_manager_id, sales_engineer_id])],
"send_reminder": True,
"notif_time": "2026-01-22 13:30:00", # Remind 30 min before
"priority": "high"
})
Task Activities¶
get_model("sale.activ").create({
"type": "task",
"subject": "Prepare ROI analysis",
"user_id": user_id,
"date": "2026-01-20",
"due_date": "2026-01-23",
"related_id": f"sale.opportunity,{opport_id}",
"description": "Create custom ROI model showing 3-year savings",
"state": "new",
"priority": "high"
})
Search Functions¶
Find Activities by Type and State¶
# Open calls for today
today = datetime.today().strftime("%Y-%m-%d")
open_calls = get_model("sale.activ").search_browse([
["type", "=", "call"],
["date", "=", today],
["state", "in", ["new", "in_progress"]]
])
for call in open_calls:
print(f"{call.start_time}: Call {call.contact_id.name} - {call.subject}")
Find Overdue Activities¶
# Tasks past due date that aren't completed
today = datetime.today().strftime("%Y-%m-%d")
overdue = get_model("sale.activ").search_browse([
["due_date", "<", today],
["state", "!=", "done"]
])
for task in overdue:
print(f"OVERDUE: {task.subject} (assigned to {task.user_id.name})")
print(f" Due: {task.due_date}, Related: {task.related_id._model}")
Find Activities for Specific Opportunity¶
# All activities for an opportunity
activities = get_model("sale.activ").search_browse([
["related_id", "=", f"sale.opportunity,{opport_id}"]
])
# Group by state
by_state = {}
for activity in activities:
state = activity.state
if state not in by_state:
by_state[state] = []
by_state[state].append(activity)
print(f"Completed: {len(by_state.get('done', []))}")
print(f"Pending: {len(by_state.get('new', []))}")
Find User's Activities for the Week¶
from datetime import datetime, timedelta
week_start = datetime.today()
week_end = week_start + timedelta(days=7)
activities = get_model("sale.activ").search_browse([
["user_id", "=", user_id],
["date", ">=", week_start.strftime("%Y-%m-%d")],
["date", "<=", week_end.strftime("%Y-%m-%d")],
["state", "!=", "done"]
])
# Sort by date and time
activities = sorted(activities, key=lambda a: (a.date, a.start_time or "00:00:00"))
print(f"This week's agenda ({len(activities)} activities):")
for act in activities:
print(f"{act.date} {act.start_time or ''}: {act.type.upper()} - {act.subject}")
Related Models¶
| Model | Relationship | Description |
|---|---|---|
sale.opportunity |
Reference (polymorphic) | Link to opportunity |
sale.quot |
Reference (polymorphic) | Link to quotation |
sale.order |
Reference (polymorphic) | Link to sales order |
contact |
Many2One | Associated contact/customer |
base.user |
Many2One | Assigned user |
base.user |
Many2Many | Other involved users |
notif |
One2Many | Reminder notifications |
Common Use Cases¶
Use Case 1: Complete Sales Call Workflow¶
# 1. Schedule call activity
call_id = get_model("sale.activ").create({
"type": "call",
"subject": "Discuss contract terms",
"user_id": user_id,
"date": "2026-01-25",
"start_time": "10:00:00",
"end_time": "10:30:00",
"related_id": f"sale.opportunity,{opport_id}",
"contact_id": contact_id,
"phone": "+1-555-1234",
"priority": "high",
"state": "new",
"send_reminder": True,
"notif_time": "2026-01-25 09:45:00" # 15 min reminder
})
# 2. User receives reminder notification at 09:45
# 3. User starts call, updates state
get_model("sale.activ").write([call_id], {
"state": "in_progress"
})
# 4. After call, mark complete and add notes
get_model("sale.activ").write([call_id], {
"state": "done",
"notes": """
Call completed successfully.
- Customer agreed to pricing
- Needs legal review (2 weeks)
- Will provide references by EOW
- Follow-up meeting scheduled for 2/8
"""
})
# 5. Create follow-up activity
get_model("sale.activ").create({
"type": "meeting",
"subject": "Contract review meeting",
"user_id": user_id,
"date": "2026-02-08",
"start_time": "14:00:00",
"related_id": f"sale.opportunity,{opport_id}",
"description": "Review contract after legal approval",
"send_reminder": True
})
Use Case 2: Team Meeting Coordination¶
# Schedule meeting with multiple team members
meeting_id = get_model("sale.activ").create({
"type": "meeting",
"subject": "Quarterly Business Review with ACME Corp",
"user_id": account_manager_id, # Primary owner
"date": "2026-02-10",
"start_time": "09:00:00",
"end_time": "11:00:00",
"location": "ACME Corp HQ - Executive Conference Room",
"related_id": f"sale.order,{order_id}",
"contact_id": customer_contact_id,
"description": """
Agenda:
1. Review Q4 results
2. Discuss expansion opportunities
3. Address open support tickets
4. Plan Q1 initiatives
""",
"other_users": [("set", [
sales_manager_id,
customer_success_id,
technical_lead_id
])],
"send_reminder": True,
"notif_time": "2026-02-09 16:00:00", # Day before reminder
"priority": "high"
})
# All four users receive notification
# Each can see meeting on their calendar
# Meeting linked to sales order for context
Use Case 3: Activity Dashboard¶
# Build user activity dashboard
user_id = get_active_user()
today = datetime.today().strftime("%Y-%m-%d")
# Today's activities
todays_activities = get_model("sale.activ").search_browse([
["user_id", "=", user_id],
["date", "=", today],
["state", "!=", "done"]
])
# Overdue activities
overdue = get_model("sale.activ").search_browse([
["user_id", "=", user_id],
["due_date", "<", today],
["state", "!=", "done"]
])
# This week's activities
week_end = (datetime.today() + timedelta(days=7)).strftime("%Y-%m-%d")
this_week = get_model("sale.activ").search_browse([
["user_id", "=", user_id],
["date", ">=", today],
["date", "<=", week_end],
["state", "!=", "done"]
])
# High priority activities
high_priority = get_model("sale.activ").search_browse([
["user_id", "=", user_id],
["priority", "=", "high"],
["state", "!=", "done"]
])
dashboard = {
"today": {
"count": len(todays_activities),
"activities": [
{
"time": a.start_time or "All day",
"type": a.type,
"subject": a.subject,
"related": a.related_id._model if a.related_id else None
}
for a in sorted(todays_activities, key=lambda x: x.start_time or "00:00")
]
},
"overdue": {
"count": len(overdue),
"activities": [
{
"due": a.due_date,
"subject": a.subject,
"days_overdue": (datetime.today() - datetime.strptime(a.due_date, "%Y-%m-%d")).days
}
for a in overdue
]
},
"this_week": len(this_week),
"high_priority": len(high_priority)
}
print(f"Today: {dashboard['today']['count']} activities")
print(f"Overdue: {dashboard['overdue']['count']} activities")
print(f"This week: {dashboard['this_week']} activities")
print(f"High priority: {dashboard['high_priority']} activities")
Use Case 4: Opportunity Activity Timeline¶
# Show complete activity history for opportunity
opport_id = 123
activities = get_model("sale.activ").search_browse([
["related_id", "=", f"sale.opportunity,{opport_id}"]
])
# Sort by date, then time
activities = sorted(activities,
key=lambda a: (a.date, a.start_time or "00:00:00"))
print(f"Activity Timeline for Opportunity {opport_id}")
print("="*60)
for act in activities:
status_icon = "✅" if act.state == "done" else "⏳"
priority_icon = "🔥" if act.priority == "high" else ""
print(f"\n{status_icon} {priority_icon} {act.date} - {act.type.upper()}")
print(f" {act.subject}")
print(f" Assigned: {act.user_id.name}")
print(f" Status: {act.state}")
if act.notes:
print(f" Notes: {act.notes[:100]}...")
# Shows complete engagement history
# Helps with opportunity review and handoffs
Use Case 5: Automated Activity Creation¶
# Workflow: Create follow-up activities when quotation sent
def create_followup_activities(quot_id):
"""
Auto-create follow-up activity schedule when quotation is sent
"""
quot = get_model("sale.quot").browse(quot_id)
# 3-day follow-up call
follow_date_1 = (datetime.today() + timedelta(days=3)).strftime("%Y-%m-%d")
get_model("sale.activ").create({
"type": "call",
"subject": f"Follow up on quotation {quot.number}",
"user_id": quot.user_id.id,
"date": follow_date_1,
"related_id": f"sale.quot,{quot_id}",
"contact_id": quot.contact_id.id,
"description": "Check if customer has questions about proposal",
"priority": "normal",
"send_reminder": True
})
# 7-day email reminder
follow_date_2 = (datetime.today() + timedelta(days=7)).strftime("%Y-%m-%d")
get_model("sale.activ").create({
"type": "email",
"subject": f"Send reminder email for {quot.number}",
"user_id": quot.user_id.id,
"date": follow_date_2,
"related_id": f"sale.quot,{quot_id}",
"description": "Send gentle reminder if no response received",
"priority": "normal",
"send_reminder": True
})
# 14-day check-in task
follow_date_3 = (datetime.today() + timedelta(days=14)).strftime("%Y-%m-%d")
get_model("sale.activ").create({
"type": "task",
"subject": f"Review status of {quot.number}",
"user_id": quot.user_id.id,
"date": follow_date_3,
"due_date": follow_date_3,
"related_id": f"sale.quot,{quot_id}",
"description": "If no response, determine next steps or close",
"priority": "normal",
"send_reminder": True
})
return "Created 3 follow-up activities"
# Use in quotation send workflow
result = create_followup_activities(quot_id)
Best Practices¶
1. Always Link Activities to CRM Records¶
# Bad: Standalone activity with no context
get_model("sale.activ").create({
"type": "call",
"subject": "Call John about pricing",
"user_id": user_id,
"date": "2026-01-20"
})
# Good: Linked to opportunity for full context
get_model("sale.activ").create({
"type": "call",
"subject": "Discuss pricing concerns",
"user_id": user_id,
"date": "2026-01-20",
"related_id": f"sale.opportunity,{opport_id}", # ✅ Context
"contact_id": contact_id, # ✅ Who
"description": "Address pricing questions raised in last meeting",
"priority": "high"
})
# Benefits:
# - Complete activity history on opportunity
# - Easy handoffs to other team members
# - Better reporting and analytics
2. Use Descriptive Subjects¶
# Bad: Vague subjects
"Call customer"
"Follow up"
"Meeting"
# Good: Clear, actionable subjects
"Discovery call - Technical requirements discussion"
"Follow up on quotation QUOT-0123 pricing questions"
"Demo meeting - Enterprise features walkthrough"
# Benefits:
# - Understand priority at a glance
# - Better calendar readability
# - Easier to search and filter
3. Set Reminders for Important Activities¶
# Enable reminders for time-sensitive activities
get_model("sale.activ").create({
"type": "meeting",
"subject": "Executive presentation",
"user_id": user_id,
"date": "2026-01-30",
"start_time": "15:00:00",
"related_id": f"sale.opportunity,{opport_id}",
"send_reminder": True,
"notif_time": "2026-01-30 14:30:00", # 30 min warning
"other_users": [("set", [manager_id])], # Notify manager too
"priority": "high"
})
# Ensures:
# - Don't miss important meetings
# - Time to prepare beforehand
# - Team is coordinated
4. Update Activity State Throughout Lifecycle¶
# Track progress through states
# 1. Created
activity_id = get_model("sale.activ").create({
"type": "task",
"subject": "Prepare proposal",
"state": "new" # Initial state
})
# 2. Started working
get_model("sale.activ").write([activity_id], {
"state": "in_progress"
})
# 3. Blocked by customer
get_model("sale.activ").write([activity_id], {
"state": "waiting",
"notes": "Waiting for customer to provide technical specs"
})
# 4. Customer responds, resume work
get_model("sale.activ").write([activity_id], {
"state": "in_progress"
})
# 5. Complete
get_model("sale.activ").write([activity_id], {
"state": "done",
"notes": "Proposal completed and sent to customer"
})
# Benefits:
# - Accurate status tracking
# - Identify bottlenecks
# - Better team visibility
Performance Tips¶
1. Use search_browse for Iteration¶
# Bad: Two database calls
activity_ids = get_model("sale.activ").search([["user_id", "=", user_id]])
activities = get_model("sale.activ").browse(activity_ids)
# Good: Single query
activities = get_model("sale.activ").search_browse([["user_id", "=", user_id]])
2. Filter at Database Level¶
# Bad: Load all, filter in Python
all_activities = get_model("sale.activ").search_browse([])
my_calls = [a for a in all_activities if a.type == "call" and a.user_id.id == user_id]
# Good: Filter in database query
my_calls = get_model("sale.activ").search_browse([
["type", "=", "call"],
["user_id", "=", user_id]
])
3. Batch Create Activities¶
# When creating multiple activities
activities_to_create = [
{"type": "call", "subject": "Call 1", "date": "2026-01-20"},
{"type": "call", "subject": "Call 2", "date": "2026-01-21"},
{"type": "call", "subject": "Call 3", "date": "2026-01-22"}
]
for activity_data in activities_to_create:
activity_data.update({
"user_id": user_id,
"related_id": f"sale.opportunity,{opport_id}"
})
get_model("sale.activ").create(activity_data)
Troubleshooting¶
Reminder Not Sending¶
Cause: Missing email template in settings or send_reminder not enabled
Solution: Configure email template and enable reminder
# 1. Check template is configured
settings = get_model("settings").browse(1)
if not settings.sale_activity_email_template_id:
print("ERROR: No activity reminder email template configured")
# 2. Ensure send_reminder is True
activity = get_model("sale.activ").browse(activity_id)
if not activity.send_reminder:
activity.write({"send_reminder": True})
# 3. Verify notification was created
if not activity.notifs:
activity.update_reminders([activity_id])
Contact Not Auto-Filling¶
Cause: Related record doesn't have contact_id or context not passed Solution: Manually set contact or ensure proper context
# Check related record has contact
opport = get_model("sale.opportunity").browse(opport_id)
if not opport.contact_id:
print("Opportunity has no contact - cannot auto-fill")
# Manually set contact when creating activity
get_model("sale.activ").create({
"type": "call",
"subject": "Follow up",
"related_id": f"sale.opportunity,{opport_id}",
"contact_id": contact_id # Explicit
})
Activity Not Showing in Related Record¶
Cause: Incorrect related_id format Solution: Ensure proper Reference field format
# Wrong format
"related_id": opport_id # ❌ Just the ID
# Correct format
"related_id": f"sale.opportunity,{opport_id}" # ✅ Model,ID
Configuration Settings¶
Required Settings¶
| Setting | Location | Description |
|---|---|---|
| Activity Email Template | settings.sale_activity_email_template_id |
Email template for activity reminders |
Optional Settings¶
| Setting | Default | Description |
|---|---|---|
| Default Reminder Time | 06:00:00 | Time of day for reminders if not specified |
| Notification Lead Time | User configured | How early to send reminders |
Integration Points¶
Internal Modules¶
- Opportunity Management: Activities linked via
related_idtosale.opportunity - Quotation Management: Activities linked to
sale.quot - Sales Order Management: Activities linked to
sale.order - Contact Management: All activities associated with contacts
- Notification System: Reminder notifications via
notifmodel - Email System: Reminder emails sent via email templates
- Calendar: Activities displayed on user calendars
- User Management: Multi-user coordination via
other_users
Version History¶
Last Updated: 2026-01-05 Model Version: sale_activ.py Framework: Netforce
Additional Resources¶
- Opportunity Documentation:
sale.opportunity - Quotation Documentation:
sale.quot - Notification System:
notif - Email Templates:
email.template
Support & Feedback¶
For issues or questions about this module: 1. Verify email template is configured for reminders 2. Check related_id format is correct (model,id) 3. Ensure contact exists on related records for auto-fill 4. Review notification records for reminder delivery 5. Test different activity types and reminder scenarios
This documentation is generated for developer onboarding and reference purposes.