DEV Community

Brad
Brad

Posted on

Build a CRM with Python: Track Leads and Deals Without Paying for Salesforce

Managing leads and clients in spreadsheets is a recipe for lost deals. Here's how to build a lightweight CRM in Python that tracks everything — for free.

Why Not Just Use Salesforce?

Salesforce costs $25-300/user/month. For freelancers and small businesses managing under 500 contacts, you're paying for complexity you don't need. A Python-based CRM gives you exactly what you need.

The Core Data Structure

import sqlite3
import datetime

def init_db():
    conn = sqlite3.connect('crm.db')
    c = conn.cursor()

    c.execute('''CREATE TABLE IF NOT EXISTS contacts (
        id INTEGER PRIMARY KEY,
        name TEXT,
        email TEXT UNIQUE,
        phone TEXT,
        company TEXT,
        status TEXT DEFAULT 'lead',
        created_at TEXT,
        last_contact TEXT,
        notes TEXT
    )''')

    c.execute('''CREATE TABLE IF NOT EXISTS deals (
        id INTEGER PRIMARY KEY,
        contact_id INTEGER,
        title TEXT,
        value REAL,
        stage TEXT DEFAULT 'prospect',
        expected_close TEXT,
        notes TEXT,
        FOREIGN KEY (contact_id) REFERENCES contacts (id)
    )''')

    c.execute('''CREATE TABLE IF NOT EXISTS interactions (
        id INTEGER PRIMARY KEY,
        contact_id INTEGER,
        type TEXT,
        notes TEXT,
        created_at TEXT,
        FOREIGN KEY (contact_id) REFERENCES contacts (id)
    )''')

    conn.commit()
    return conn
Enter fullscreen mode Exit fullscreen mode

Adding and Updating Contacts

def add_contact(conn, name, email, company=None, phone=None, status='lead'):
    c = conn.cursor()
    now = datetime.datetime.now().isoformat()
    c.execute('''INSERT OR REPLACE INTO contacts
                 (name, email, phone, company, status, created_at, last_contact)
                 VALUES (?, ?, ?, ?, ?, ?, ?)''',
              (name, email, phone, company, status, now, now))
    conn.commit()
    return c.lastrowid

def update_contact_status(conn, email, new_status):
    """Move contact through pipeline: lead -> qualified -> proposal -> won/lost"""
    valid_statuses = ['lead', 'qualified', 'proposal', 'negotiation', 'won', 'lost']
    if new_status not in valid_statuses:
        raise ValueError(f"Invalid status. Choose from: {valid_statuses}")

    c = conn.cursor()
    c.execute('UPDATE contacts SET status = ?, last_contact = ? WHERE email = ?',
              (new_status, datetime.datetime.now().isoformat(), email))
    conn.commit()
Enter fullscreen mode Exit fullscreen mode

Auto-Follow-Up Reminders

This is the killer feature — automatic reminders for contacts you haven't touched:

import smtplib
from email.mime.text import MIMEText

def check_follow_ups(conn, days_threshold=7):
    """Find contacts not touched in X days."""
    c = conn.cursor()
    cutoff = (datetime.datetime.now() - datetime.timedelta(days=days_threshold)).isoformat()

    c.execute('''SELECT name, email, company, status, last_contact
                 FROM contacts
                 WHERE last_contact < ? AND status NOT IN ('won', 'lost')
                 ORDER BY last_contact ASC''', (cutoff,))

    return c.fetchall()

def send_follow_up_digest(overdue_contacts, your_email, smtp_password):
    """Email yourself a daily digest of overdue follow-ups."""
    if not overdue_contacts:
        return

    body = "Daily CRM Follow-Up Digest\n\n"
    body += f"Contacts needing attention ({len(overdue_contacts)}):\n\n"

    for contact in overdue_contacts:
        name, email, company, status, last_contact = contact
        days_ago = (datetime.datetime.now() - datetime.datetime.fromisoformat(last_contact)).days
        body += f"- {name} ({company or 'No company'}): {status} pipeline, last contacted {days_ago} days ago\n"
        body += f"  Email: {email}\n\n"

    msg = MIMEText(body)
    msg['Subject'] = f"CRM Alert: {len(overdue_contacts)} contacts need follow-up"
    msg['From'] = your_email
    msg['To'] = your_email

    with smtplib.SMTP_SSL('smtp.gmail.com', 465) as server:
        server.login(your_email, smtp_password)
        server.send_message(msg)
Enter fullscreen mode Exit fullscreen mode

Deal Pipeline Tracking

def add_deal(conn, contact_email, title, value, expected_close_days=30):
    c = conn.cursor()
    contact = c.execute('SELECT id FROM contacts WHERE email = ?', (contact_email,)).fetchone()
    if not contact:
        raise ValueError(f"Contact not found: {contact_email}")

    expected_close = (datetime.datetime.now() + datetime.timedelta(days=expected_close_days)).date().isoformat()

    c.execute('''INSERT INTO deals (contact_id, title, value, stage, expected_close)
                 VALUES (?, ?, ?, 'prospect', ?)''',
              (contact[0], title, value, expected_close))
    conn.commit()
    return c.lastrowid

def get_pipeline_summary(conn):
    """Get pipeline value by stage."""
    c = conn.cursor()
    c.execute('''SELECT stage, COUNT(*), SUM(value)
                 FROM deals
                 WHERE stage NOT IN ('won', 'lost')
                 GROUP BY stage''')

    results = c.fetchall()
    print("\n=== Pipeline Summary ===")
    total_value = 0
    for stage, count, value in results:
        print(f"{stage.capitalize()}: {count} deals worth ${value:,.0f}")
        total_value += value
    print(f"Total Pipeline: ${total_value:,.0f}")
    return results
Enter fullscreen mode Exit fullscreen mode

Putting It Together

def run_daily_crm():
    conn = init_db()

    # Check for overdue follow-ups
    overdue = check_follow_ups(conn, days_threshold=5)

    if overdue:
        send_follow_up_digest(overdue, "you@gmail.com", "your-app-password")
        print(f"WARNING: {len(overdue)} contacts need follow-up")
    else:
        print("All contacts up to date")

    # Show pipeline
    get_pipeline_summary(conn)
    conn.close()

if __name__ == '__main__':
    run_daily_crm()
Enter fullscreen mode Exit fullscreen mode

Running It Daily

0 9 * * 1-5 /usr/bin/python3 /path/to/crm.py >> /var/log/crm.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Extending the CRM

Once you have the basics working:

  • Web interface: Add Flask for a browser UI
  • Import from CSV: Parse your existing spreadsheet data
  • Email templates: Store follow-up templates, auto-personalize
  • Revenue forecasting: Weight deals by stage probability

This entire CRM system — and 20 more Python business automation scripts — are available as a ready-to-run toolkit at https://lukassbrad.gumroad.com/l/ugeka.


What CRM features matter most to your business? Drop a comment below.

Top comments (0)