Skip to content

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

    new (Not Started)
    in_progress
    ┌───┴───┬──────────┐
    ↓       ↓          ↓
  done   waiting   deferred
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}")

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

# 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
})

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_id to sale.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 notif model
  • 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.