Skip to content

CRM Module

Lead pipeline management with swipeable stage navigation.

Overview

The CRM module provides:

  • Stage-based pipeline view with swipe navigation
  • Lead list with search and filtering
  • Lead detail with customer information
  • Quote creation from leads
  • Stage workflow management
  • Activity logging and comments
  • Mark as lost functionality

Routes

Pipeline View

GET /crm

Displays leads grouped by stage in a swipeable carousel interface.

Parameter Type Description
stage int Initial stage to display (stage_id)

Lead List

GET /crm/leads

Lists all active leads with search capability.

Parameter Type Description
q string Search query (name, phone, email)

Lead Detail

GET /crm/lead/{lead_id}

Displays full lead details including:

  • Contact information
  • Address
  • Stage and priority
  • Expected revenue
  • Linked quotations
  • Activity log
  • Tags

Create Quote from Lead

GET /crm/lead/{lead_id}/quote/new

Creates a new draft quote linked to the lead and redirects to quote editor.

API Endpoints

Stage Management

Get Stages

GET /api/crm/stages

Get all CRM stages ordered by sequence.

Response:

{
  "success": true,
  "stages": [
    {"id": 1, "name": "New", "sequence": 1, "is_won": false, "fold": false},
    {"id": 2, "name": "Qualified", "sequence": 2, "is_won": false, "fold": false},
    {"id": 3, "name": "Proposition", "sequence": 3, "is_won": false, "fold": false},
    {"id": 4, "name": "Won", "sequence": 10, "is_won": true, "fold": false}
  ]
}

Get Leads by Stage

GET /api/crm/stages/{stage_id}/leads

Get leads for a specific stage.

Response:

{
  "success": true,
  "leads": [
    {
      "id": 123,
      "name": "Window Blinds - John Doe",
      "partner_id": [45, "John Doe"],
      "contact_name": "John Doe",
      "phone": "+1 555-1234",
      "stage_id": [1, "New"],
      "expected_revenue": 5000.00,
      "quotation_count": 2
    }
  ],
  "count": 15
}

Change Stage

POST /api/crm/lead/{lead_id}/stage

Move lead to a different stage.

{
  "stage_id": 3
}

Response:

{
  "success": true,
  "stage_id": 3,
  "stage_name": "Proposition"
}

Lead Updates

Update Description

PUT /api/crm/lead/{lead_id}/description

{
  "description": "Customer interested in motorized blinds for living room"
}

Update Priority

POST /api/crm/lead/{lead_id}/priority

{
  "priority": 2
}

Priority values: 0-3 (0 = no stars, 3 = 3 stars)

Mark as Lost

POST /api/crm/lead/{lead_id}/lost

{
  "lost_reason_id": 5
}

This archives the lead (active=False) and sets the lost reason.

Get Lost Reasons

GET /api/crm/lost-reasons

Response:

{
  "success": true,
  "reasons": [
    {"id": 1, "name": "Too expensive"},
    {"id": 2, "name": "Went with competitor"},
    {"id": 3, "name": "Project cancelled"}
  ]
}

Comments

POST /api/crm/lead/{lead_id}/comment

{
  "comment": "Called customer, will follow up tomorrow"
}

Tags

GET /api/crm/tags

Get all available CRM tags.

Response:

{
  "success": true,
  "tags": [
    {"id": 1, "name": "Hot", "color": 1},
    {"id": 2, "name": "Referral", "color": 4}
  ]
}

Pipeline View

flowchart LR
    subgraph "Swipeable Pipeline"
        S1[New<br/>5 leads]
        S2[Qualified<br/>3 leads]
        S3[Proposition<br/>8 leads]
        S4[Won<br/>12 leads]
    end

    S1 <--> S2 <--> S3 <--> S4

Stage Navigation

The pipeline view uses a swipeable carousel:

  • Swipe left/right to navigate between stages
  • Stage pills show current position and counts
  • Tap a stage pill to jump to that stage
  • Pull down to refresh leads

Lead Card Display

Each lead card shows:

  • Lead name
  • Customer name (if linked)
  • Phone number (tap to call)
  • Expected revenue
  • Quote count badge
  • Priority stars
  • Tags

Quote Creation Flow

sequenceDiagram
    participant User
    participant PWA
    participant Odoo

    User->>PWA: Click "New Quote" on lead

    PWA->>Odoo: Get lead details
    Odoo-->>PWA: {partner_id, user_id, team_id}

    PWA->>Odoo: Get partner pricelist
    Odoo-->>PWA: {pricelist_id}

    PWA->>Odoo: Create sale.order
    Note over Odoo: partner_id from lead<br/>opportunity_id = lead_id

    Odoo-->>PWA: {new_order_id}

    PWA-->>User: Redirect to /quotes/{id}/edit

Stage Workflow

Default Stages

Stage Sequence is_won Description
New 1 false Fresh leads
Qualified 2 false Verified interest
Proposition 3 false Quote sent
Won 10 true Converted to sale

Lost Leads

Marking a lead as lost:

  1. Opens modal with lost reasons
  2. User selects reason
  3. Lead is archived (active=False)
  4. Lead disappears from pipeline

Activity Attribution

CRM actions are properly attributed to users:

partner_id = session.get('partner_id')
result = odoo_api.post_crm_lead_comment(lead_id, comment, author_id=partner_id)

Lead Fields

Field Type Description
name Char Lead/opportunity title
partner_id Many2one Linked customer
contact_name Char Contact person name
phone Char Phone number
mobile Char Mobile number
email_from Char Email address
street, city, state_id, zip Address fields
stage_id Many2one Pipeline stage
user_id Many2one Salesperson
team_id Many2one Sales team
expected_revenue Float Estimated value
probability Float Win probability (%)
priority Selection 0-3 stars
description Text Notes
tag_ids Many2many Tags
order_ids One2many Linked sales orders

The search function looks across multiple fields:

  • Lead name
  • Contact name
  • Phone number
  • Mobile number
  • Email
def search_crm_leads(query: str, limit: int = 50):
    query_lower = query.lower()
    filtered = [
        l for l in all_leads
        if (query_lower in (l.get('name') or '').lower() or
            query_lower in (l.get('contact_name') or '').lower() or
            query_lower in (l.get('phone') or '').lower() or
            query_lower in (l.get('mobile') or '').lower() or
            query_lower in (l.get('email_from') or '').lower())
    ]
    return filtered[:limit]

Linked Quotations

Quotations linked to a lead (via opportunity_id) are displayed:

def get_lead_quotations(lead_id):
    domain = [('opportunity_id', '=', lead_id)]
    return odoo_api.search_read('sale.order', domain, fields=[
        'id', 'name', 'state', 'amount_total', 'date_order'
    ])