gptdevelopers.io
Hire GPT Developers
Table of Contents:
Python CRM System – Build Your Own Customer Management Backend/

Thanks For Commenting On Our Post!
We’re excited to share this comprehensive guide with you. This resource includes best practices, and real-world implementation strategies that we use at slashdev when building apps for clients worldwide.
What’s Inside This Guide:
- Why custom CRMs beat off-the-shelf solutions – and when to build your own
- Django architecture for business logic – models, pipelines, and automated workflows
- Production-ready Python code – client tracking, deal stages, and follow-up automation
- Deployment walkthrough – from local dev to live system your team can use
- Key learnings – building business tools that replace expensive SaaS subscriptions
Overview:
Every business tool you pay $50/month for started as someone realizing: “I could build this myself in a weekend.” CRMs are the perfect example. You’re paying HubSpot or Salesforce hundreds per user for features you’ll never touch, while the core functionality – tracking clients, managing deals, scheduling follow-ups – is surprisingly straightforward to build.
Here’s what most people don’t realize: a custom CRM isn’t about matching every enterprise feature. It’s about building exactly what you need, the way you work, without the bloat.
The Real Problem with Commercial CRMs
You’ve tried them. Salesforce has 47 tabs you’ll never click. HubSpot wants you to “complete your setup” for features you don’t need. Pipedrive is clean but charges per user and locks basic automations behind premium tiers.
The real issues:
- Over-engineering – Built for every possible use case, optimized for none
- Rigid workflows – Their pipeline stages don’t match how your business actually operates
- Pricing models – Pay per user, pay for storage, pay for automations, pay for integrations
- Data lock-in – Your customer data lives in their system, export is painful
- Complexity overhead – Training new team members takes days because the UI does too much
Meanwhile, what you actually need:
- A database of clients with contact info and interaction history
- A visual pipeline showing where each deal stands
- Automated reminders so nothing falls through the cracks
- Simple reporting on deal values and conversion rates
- Fast search and filtering
That’s it. And you can build all of this in Django with less code than you think.
What We’re Building
This isn’t a toy project. It’s a production-ready CRM backend with:
Core Features:
- Client Management – Full contact database with custom fields, notes, and activity timelines
- Deal Pipeline – Visual stages (Lead → Qualified → Proposal → Negotiation → Closed)
- Automated Follow-ups – System tracks when deals go stale and reminds you
- Task Management – Assign follow-up actions to specific deals and clients
- Activity Logging – Every email, call, and meeting gets timestamped
- Search & Filters – Find any client or deal in seconds
- Dashboard Analytics – Revenue in pipeline, conversion rates, deals closing this month
Why Django?
Django gives you:
- Built-in admin panel (instant UI for managing data)
- ORM for database operations (no raw SQL required)
- Authentication and permissions out of the box
- Easy REST API creation with Django REST Framework
- Solid documentation and massive ecosystem
You’re not building from scratch – you’re assembling proven components.
The Architecture
Models (Database Layer)
├── Client: stores customer info
├── Deal: tracks opportunities through pipeline
├── Activity: logs interactions (calls, emails, meetings)
├── Task: manages follow-ups and reminders
└── User: team members who own accounts
Business Logic (Service Layer)
├── Pipeline automation (move deals based on time/actions)
├── Notification system (remind about stale deals)
├── Activity tracking (log every interaction automatically)
└── Reporting (calculate metrics and forecasts)
Interface
├── Django Admin (for internal team use)
├── REST API (for custom frontends or integrations)
└── Simple dashboard (HTML/CSS for quick overview)
Why This Matters
Building your own CRM isn’t about saving money (though you will). It’s about control.
You decide what fields matter. You decide what triggers a notification. You decide how the pipeline works. When your sales process changes, you update the code instead of arguing with support about feature requests.
Plus, you own the data. Export to CSV anytime. Backup to your own servers. Integrate with your existing tools without waiting for Zapier support.
This is what small teams and solo founders need: a system that works exactly how they think, without the enterprise bloat.
Practical Codes
1.Core Django Models
# crm/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from datetime import timedelta
class Client(models.Model):
"""Customer/Company record"""
LEAD_SOURCE_CHOICES = [
('website', 'Website'),
('referral', 'Referral'),
('cold_outreach', 'Cold Outreach'),
('event', 'Event'),
('other', 'Other'),
]
company_name = models.CharField(max_length=200)
contact_name = models.CharField(max_length=200)
email = models.EmailField(unique=True)
phone = models.CharField(max_length=20, blank=True)
website = models.URLField(blank=True)
lead_source = models.CharField(max_length=50, choices=LEAD_SOURCE_CHOICES)
industry = models.CharField(max_length=100, blank=True)
company_size = models.CharField(max_length=50, blank=True)
notes = models.TextField(blank=True)
assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True,
related_name='clients')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"{self.company_name} - {self.contact_name}"
@property
def active_deals_count(self):
return self.deals.filter(stage__in=['lead', 'qualified', 'proposal', 'negotiation']).count()
@property
def total_deal_value(self):
return self.deals.filter(stage__in=['lead', 'qualified', 'proposal', 'negotiation']).aggregate(
total=models.Sum('value')
)['total'] or 0
class Deal(models.Model):
"""Sales opportunity/pipeline item"""
STAGE_CHOICES = [
('lead', 'Lead'),
('qualified', 'Qualified'),
('proposal', 'Proposal Sent'),
('negotiation', 'Negotiation'),
('closed_won', 'Closed Won'),
('closed_lost', 'Closed Lost'),
]
title = models.CharField(max_length=200)
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='deals')
stage = models.CharField(max_length=20, choices=STAGE_CHOICES, default='lead')
value = models.DecimalField(max_digits=10, decimal_places=2)
expected_close_date = models.DateField()
probability = models.IntegerField(default=50, help_text="Probability of closing (0-100)")
description = models.TextField(blank=True)
lost_reason = models.TextField(blank=True)
assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True,
related_name='deals')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
stage_changed_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"{self.title} - {self.client.company_name} (${self.value})"
def save(self, *args, **kwargs):
# Track stage changes
if self.pk:
old_deal = Deal.objects.get(pk=self.pk)
if old_deal.stage != self.stage:
self.stage_changed_at = timezone.now()
super().save(*args, **kwargs)
@property
def is_stale(self):
"""Deal hasn't been updated in 7+ days"""
return (timezone.now() - self.updated_at).days > 7
@property
def days_in_current_stage(self):
return (timezone.now() - self.stage_changed_at).days
@property
def weighted_value(self):
"""Expected value based on probability"""
return float(self.value) * (self.probability / 100)
class Activity(models.Model):
"""Log of interactions with clients"""
ACTIVITY_TYPES = [
('call', 'Phone Call'),
('email', 'Email'),
('meeting', 'Meeting'),
('note', 'Note'),
]
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='activities')
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, null=True, blank=True,
related_name='activities')
activity_type = models.CharField(max_length=20, choices=ACTIVITY_TYPES)
subject = models.CharField(max_length=200)
description = models.TextField()
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
verbose_name_plural = 'Activities'
def __str__(self):
return f"{self.activity_type} - {self.client.company_name} - {self.created_at.strftime('%Y-%m-%d')}"
class Task(models.Model):
"""Follow-up tasks and reminders"""
PRIORITY_CHOICES = [
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
]
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='tasks')
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, null=True, blank=True,
related_name='tasks')
assigned_to = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tasks')
priority = models.CharField(max_length=10, choices=PRIORITY_CHOICES, default='medium')
due_date = models.DateTimeField()
completed = models.BooleanField(default=False)
completed_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['due_date']
def __str__(self):
return f"{self.title} - Due: {self.due_date.strftime('%Y-%m-%d')}"
@property
def is_overdue(self):
return not self.completed and timezone.now() > self.due_date
def mark_complete(self):
self.completed = True
self.completed_at = timezone.now()
self.save()
2. Automated Pipeline Management
# crm/services.py
from django.utils import timezone
from django.core.mail import send_mail
from datetime import timedelta
from .models import Deal, Task, Activity
class PipelineManager:
"""Automated deal pipeline logic"""
STAGE_TIMEOUTS = {
'lead': 14, # days before considered stale
'qualified': 10,
'proposal': 7,
'negotiation': 5,
}
@classmethod
def check_stale_deals(cls):
"""Find deals that need attention"""
stale_deals = []
for stage, max_days in cls.STAGE_TIMEOUTS.items():
cutoff_date = timezone.now() - timedelta(days=max_days)
deals = Deal.objects.filter(
stage=stage,
stage_changed_at__lt=cutoff_date
).exclude(stage__in=['closed_won', 'closed_lost'])
for deal in deals:
stale_deals.append({
'deal': deal,
'days_stale': (timezone.now() - deal.stage_changed_at).days,
'stage': stage
})
return stale_deals
@classmethod
def create_follow_up_tasks(cls, stale_deals):
"""Auto-create tasks for stale deals"""
tasks_created = []
for item in stale_deals:
deal = item['deal']
# Check if task already exists
existing = Task.objects.filter(
deal=deal,
completed=False,
title__icontains='Follow up'
).exists()
if not existing:
task = Task.objects.create(
title=f"Follow up on {deal.title}",
description=f"This deal has been in {deal.get_stage_display()} stage for {item['days_stale']} days.",
client=deal.client,
deal=deal,
assigned_to=deal.assigned_to,
priority='high',
due_date=timezone.now() + timedelta(days=1)
)
tasks_created.append(task)
return tasks_created
@classmethod
def send_daily_digest(cls, user):
"""Email summary of user's pipeline"""
today = timezone.now().date()
# Tasks due today
tasks_due = Task.objects.filter(
assigned_to=user,
due_date__date=today,
completed=False
)
# Deals closing soon
deals_closing = Deal.objects.filter(
assigned_to=user,
expected_close_date__lte=today + timedelta(days=7),
stage__in=['proposal', 'negotiation']
)
# Stale deals
stale = cls.check_stale_deals()
user_stale = [s for s in stale if s['deal'].assigned_to == user]
message = f"""
Good morning! Here's your CRM digest:
📋 Tasks Due Today: {tasks_due.count()}
{chr(10).join([f" - {t.title}" for t in tasks_due[:5]])}
💰 Deals Closing This Week: {deals_closing.count()}
{chr(10).join([f" - {d.title} (${d.value})" for d in deals_closing[:5]])}
⚠️ Deals Needing Attention: {len(user_stale)}
{chr(10).join([f" - {s['deal'].title} ({s['days_stale']} days in stage)" for s in user_stale[:5]])}
Login to your CRM to take action.
"""
send_mail(
subject=f'Your CRM Digest - {today.strftime("%b %d")}',
message=message,
from_email='crm@yourcompany.com',
recipient_list=[user.email],
fail_silently=True,
)
class ActivityTracker:
"""Helper for logging activities"""
@staticmethod
def log_activity(client, activity_type, subject, description, user, deal=None):
"""Create activity log entry"""
return Activity.objects.create(
client=client,
deal=deal,
activity_type=activity_type,
subject=subject,
description=description,
created_by=user
)
@staticmethod
def get_client_timeline(client):
"""Get full activity history for client"""
activities = client.activities.all()
tasks = client.tasks.all()
timeline = []
for activity in activities:
timeline.append({
'type': 'activity',
'date': activity.created_at,
'data': activity
})
for task in tasks:
timeline.append({
'type': 'task',
'date': task.due_date,
'data': task
})
return sorted(timeline, key=lambda x: x['date'], reverse=True)
3. Dashboard Analytics
# crm/analytics.py
from django.db.models import Sum, Count, Q, Avg
from django.utils import timezone
from datetime import timedelta
from .models import Deal, Client, Activity
class CRMAnalytics:
"""Generate dashboard metrics"""
@staticmethod
def get_pipeline_summary():
"""Overview of deals by stage"""
summary = Deal.objects.values('stage').annotate(
count=Count('id'),
total_value=Sum('value'),
weighted_value=Sum('value') * Avg('probability') / 100
)
return {
item['stage']: {
'count': item['count'],
'value': float(item['total_value'] or 0),
'weighted': float(item['weighted_value'] or 0),
}
for item in summary
}
@staticmethod
def get_conversion_rates():
"""Calculate stage-to-stage conversion"""
total_deals = Deal.objects.count()
if total_deals == 0:
return {}
stages = ['qualified', 'proposal', 'negotiation', 'closed_won']
conversions = {}
for stage in stages:
count = Deal.objects.filter(stage=stage).count()
conversions[stage] = round((count / total_deals) * 100, 1)
return conversions
@staticmethod
def get_revenue_forecast(months=3):
"""Predict revenue based on weighted pipeline"""
end_date = timezone.now().date() + timedelta(days=30 * months)
deals = Deal.objects.filter(
expected_close_date__lte=end_date,
stage__in=['qualified', 'proposal', 'negotiation']
)
forecast = sum(deal.weighted_value for deal in deals)
return {
'total_weighted_value': round(forecast, 2),
'deal_count': deals.count(),
'months': months
}
@staticmethod
def get_team_performance():
"""Stats per team member"""
from django.contrib.auth.models import User
performance = []
for user in User.objects.filter(is_active=True):
active_deals = Deal.objects.filter(
assigned_to=user,
stage__in=['lead', 'qualified', 'proposal', 'negotiation']
)
won_deals = Deal.objects.filter(
assigned_to=user,
stage='closed_won',
updated_at__gte=timezone.now() - timedelta(days=90)
)
performance.append({
'user': user.get_full_name() or user.username,
'active_deals': active_deals.count(),
'pipeline_value': float(active_deals.aggregate(Sum('value'))['value__sum'] or 0),
'deals_won_90d': won_deals.count(),
'revenue_won_90d': float(won_deals.aggregate(Sum('value'))['value__sum'] or 0),
})
return sorted(performance, key=lambda x: x['pipeline_value'], reverse=True)
@staticmethod
def get_activity_stats(days=30):
"""Team activity metrics"""
cutoff = timezone.now() - timedelta(days=days)
activities = Activity.objects.filter(created_at__gte=cutoff)
return {
'total_activities': activities.count(),
'by_type': dict(activities.values_list('activity_type').annotate(Count('id'))),
'new_clients': Client.objects.filter(created_at__gte=cutoff).count(),
'new_deals': Deal.objects.filter(created_at__gte=cutoff).count(),
}
4.Management Commands for Automation
# crm/management/commands/run_daily_crm_tasks.py
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from crm.services import PipelineManager
class Command(BaseCommand):
help = 'Run daily CRM automation tasks'
def handle(self, *args, **options):
self.stdout.write('Running daily CRM tasks...')
# Check for stale deals
stale_deals = PipelineManager.check_stale_deals()
self.stdout.write(f'Found {len(stale_deals)} stale deals')
# Create follow-up tasks
tasks = PipelineManager.create_follow_up_tasks(stale_deals)
self.stdout.write(f'Created {len(tasks)} follow-up tasks')
# Send daily digests
active_users = User.objects.filter(is_active=True)
for user in active_users:
PipelineManager.send_daily_digest(user)
self.stdout.write(self.style.SUCCESS(
f'✓ Sent digests to {active_users.count()} users'
))
How to Run:
Initial Setup (One Time)
1. Create Django Project
pip install django djangorestframework
django-admin startproject crm_project
cd crm_project
python manage.py startapp crm
2. Add Models Copy the models code into crm/models.py, then:
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
3. Register in Admin Create crm/admin.py:
from django.contrib import admin
from .models import Client, Deal, Activity, Task
admin.site.register(Client)
admin.site.register(Deal)
admin.site.register(Activity)
admin.site.register(Task)
Run Development Server
python manage.py runserver
Access admin at http://localhost:8000/admin
Using the System
Add Clients:
- Login to admin panel
- Navigate to Clients → Add Client
- Fill contact info, assign to team member
Create Deals:
- Navigate to Deals → Add Deal
- Select client, set value and expected close date
- Choose pipeline stage
Log Activities:
from crm.services import ActivityTracker
from crm.models import Client
from django.contrib.auth.models import User
client = Client.objects.get(id=1)
user = User.objects.get(id=1)
ActivityTracker.log_activity(
client=client,
activity_type='call',
subject='Discovery call',
description='Discussed their needs for Q1',
user=user
)
Run Daily Automation:
python manage.py run_daily_crm_tasks
Set Up Cron Job (for daily digests):
# Add to crontab
0 8 * * * cd /path/to/crm_project && python manage.py run_daily_crm_tasks
Viewing Analytics
from crm.analytics import CRMAnalytics
# Pipeline overview
print(CRMAnalytics.get_pipeline_summary())
# Revenue forecast
print(CRMAnalytics.get_revenue_forecast(months=3))
# Team performance
print(CRMAnalytics.get_team_performance())
Key Concepts
You’ve now built a full-featured CRM using Django’s ORM for data modeling, custom business logic for pipeline automation and stale deal detection, activity tracking for complete client interaction history, and analytics functions for revenue forecasting and team performance metrics. Building this system teaches you practical database design, workflow automation, and how to create internal business tools that replace expensive SaaS subscriptions – giving you skills that apply to any custom software project for real business operations. The key is not replicating every enterprise CRM feature, but understanding your specific workflow and building exactly what moves your business forward without the bloat.
Would you like to focus on the first detailed section, “Why custom CRMs beat off-the-shelf solutions?”
About slashdev.io
At slashdev.io, we’re a global software engineering company specializing in building production web and mobile applications. We combine cutting-edge LLM technologies (Claude Code, Gemini, Grok, ChatGPT) with traditional tech stacks like ReactJS, Laravel, iOS, and Flutter to deliver exceptional results.
What sets us apart:
- Expert developers at $50/hour
- AI-powered development workflows for enhanced productivity
- Full-service engineering support, not just code
- Experience building real production applications at scale
Whether you’re building your next app or need expert developers to join your team, we provide ongoing developer relationships that go beyond one-time assessments.
Need Development Support?
Building something ambitious? We’d love to help. Our team specializes in turning ideas into production-ready applications using the latest AI-powered development techniques combined with solid engineering fundamentals.
