Servicios de Software
Para Empresas
Productos
Crear Agentes IA
Seguridad
Portafolio
Contratar Desarrolladores
Contratar Desarrolladores
Get Senior Engineers Straight To Your Inbox

Every month we send out our top new engineers in our network who are looking for work, be the first to get informed when top engineers become available

At Slashdev, we connect top-tier software engineers with innovative companies. Our network includes the most talented developers worldwide, carefully vetted to ensure exceptional quality and reliability.
Build With Us
Python CRM App: Build Your Own Customer Management System/


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:
- System architecture – How Django models create a real client database
- Pipeline management – Moving deals through stages automatically
- Contact tracking – Building relationships that don’t slip through the cracks
- Task automation – Setting reminders that actually work
- Dashboard design – Seeing everything that matters at a glance
- Real code you can deploy – Two production-ready implementations
- Running your CRM locally – Step-by-step setup that won’t waste your afternoon
Overview:
Most CRMs feel like they were built for someone else’s business. They’re either too bloated with features you’ll never touch, or too simple to handle the chaos of real client work. This guide shows you how to build something different – a system that knows your workflow because you’re the one defining it.
The problem we’re solving: You’ve got clients in your email, deals in a spreadsheet, follow-ups in your head, and that one important contact buried in a text message from three months ago. Nothing talks to each other. Nothing reminds you when things are about to fall apart.
What this CRM does differently:
It treats your business like the living thing it is. Every client becomes a record with history. Every deal gets tracked from first contact to closed-won. Every task sits in a queue until you mark it done. The system doesn’t just store data – it shows you what’s overdue, what’s slipping, and where your attention should be right now.
The Django advantage:
Django gives you an admin panel immediately. No front-end work required to start managing clients. You get automatic form validation, built-in user authentication, and a database that scales when you need it to. The ORM (Object-Relational Mapping) means you write Python, not SQL – your models become your database schema automatically.
Core components you’re building:
Contact Management – Each client is more than a name. You’re storing communication history, preferred contact methods, and notes that matter when you’re trying to remember why they called last month. The system timestamps everything, so you can reconstruct conversations even when your memory fails.
Deal Pipeline – Deals flow through stages: Lead, Qualified, Proposal, Negotiation, Closed. Each stage has a probability of closing and an expected value. You’re not guessing about your month anymore – you can see exactly what’s likely to convert and what needs help.
Task System – Follow-ups don’t live in your head anymore. The CRM assigns tasks to deals and contacts, sets due dates, and flags what’s overdue. You open the dashboard and immediately see what you should be working on today.
Activity Logging – Every email, call, meeting – it’s all recorded. Not for surveillance, but because context matters. When a client calls after three months, you’re not scrambling to remember where you left off.
Why this beats subscription CRMs:
You control the data. You decide what fields matter. You add features when you need them, not when some product manager thinks you should. And you’re not paying $50/month per user for software you already know how to build.
What makes this production-ready:
We’re using proper model relationships (ForeignKeys and ManyToMany fields). We’re adding indexes where queries will slow down. We’re implementing soft deletes so you never lose client history. We’re thinking about what happens when five people are using this at once.
This isn’t academic. It’s the same structure that companies use when they outgrow off-the-shelf solutions and hire developers to build something custom. You’re just cutting out the middleman.
Practical Codes
Code 1: Complete CRM Models with Relationships
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from datetime import timedelta
class Contact(models.Model):
"""Core contact model - every client starts here"""
CONTACT_TYPE_CHOICES = [
('lead', 'Lead'),
('customer', 'Customer'),
('partner', 'Partner'),
('vendor', 'Vendor'),
]
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
phone = models.CharField(max_length=20, blank=True)
company = models.CharField(max_length=200, blank=True)
job_title = models.CharField(max_length=100, blank=True)
contact_type = models.CharField(max_length=20, choices=CONTACT_TYPE_CHOICES, default='lead')
# Relationship tracking
owner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='contacts')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
last_contacted = models.DateTimeField(null=True, blank=True)
# Additional context
notes = models.TextField(blank=True)
tags = models.CharField(max_length=500, blank=True, help_text="Comma-separated tags")
is_active = models.BooleanField(default=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['email']),
models.Index(fields=['last_contacted']),
models.Index(fields=['owner', 'is_active']),
]
def __str__(self):
return f"{self.first_name} {self.last_name} - {self.company}"
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
@property
def needs_followup(self):
"""Flag contacts not reached in 30 days"""
if not self.last_contacted:
return True
return timezone.now() - self.last_contacted > timedelta(days=30)
class Deal(models.Model):
"""Pipeline management - tracks opportunities through stages"""
STAGE_CHOICES = [
('lead', 'Lead'),
('qualified', 'Qualified'),
('proposal', 'Proposal Sent'),
('negotiation', 'Negotiation'),
('closed_won', 'Closed Won'),
('closed_lost', 'Closed Lost'),
]
STAGE_PROBABILITY = {
'lead': 10,
'qualified': 25,
'proposal': 50,
'negotiation': 75,
'closed_won': 100,
'closed_lost': 0,
}
title = models.CharField(max_length=200)
contact = models.ForeignKey(Contact, on_delete=models.CASCADE, related_name='deals')
value = models.DecimalField(max_digits=10, decimal_places=2)
stage = models.CharField(max_length=20, choices=STAGE_CHOICES, default='lead')
expected_close_date = models.DateField()
owner = 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)
closed_at = models.DateTimeField(null=True, blank=True)
notes = models.TextField(blank=True)
is_active = models.BooleanField(default=True)
class Meta:
ordering = ['-expected_close_date']
indexes = [
models.Index(fields=['stage', 'is_active']),
models.Index(fields=['owner', 'stage']),
models.Index(fields=['expected_close_date']),
]
def __str__(self):
return f"{self.title} - {self.get_stage_display()} (${self.value})"
@property
def win_probability(self):
"""Calculate weighted value based on stage"""
return self.STAGE_PROBABILITY.get(self.stage, 0)
@property
def weighted_value(self):
"""Expected value = deal value × probability"""
return float(self.value) * (self.win_probability / 100)
@property
def is_overdue(self):
"""Check if expected close date has passed"""
if self.stage in ['closed_won', 'closed_lost']:
return False
return timezone.now().date() > self.expected_close_date
def move_to_stage(self, new_stage):
"""Update stage and log the transition"""
old_stage = self.stage
self.stage = new_stage
if new_stage in ['closed_won', 'closed_lost']:
self.closed_at = timezone.now()
self.is_active = False
self.save()
# Create activity log
Activity.objects.create(
deal=self,
contact=self.contact,
activity_type='stage_change',
description=f"Stage changed from {old_stage} to {new_stage}",
owner=self.owner
)
class Task(models.Model):
"""Follow-up system - what needs to be done and when"""
PRIORITY_CHOICES = [
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
('urgent', 'Urgent'),
]
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
due_date = models.DateTimeField()
priority = models.CharField(max_length=10, choices=PRIORITY_CHOICES, default='medium')
# Relationships - tasks can be linked to deals or contacts
contact = models.ForeignKey(Contact, on_delete=models.CASCADE, related_name='tasks', null=True, blank=True)
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, related_name='tasks', null=True, blank=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tasks')
completed = models.BooleanField(default=False)
completed_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['completed', 'due_date']
indexes = [
models.Index(fields=['owner', 'completed', 'due_date']),
models.Index(fields=['due_date']),
]
def __str__(self):
status = "✓" if self.completed else "○"
return f"{status} {self.title} - Due: {self.due_date.strftime('%Y-%m-%d')}"
@property
def is_overdue(self):
"""Check if task is past due and not completed"""
if self.completed:
return False
return timezone.now() > self.due_date
def mark_complete(self):
"""Complete task and timestamp it"""
self.completed = True
self.completed_at = timezone.now()
self.save()
class Activity(models.Model):
"""Activity log - maintains history of all interactions"""
ACTIVITY_TYPES = [
('call', 'Phone Call'),
('email', 'Email'),
('meeting', 'Meeting'),
('note', 'Note'),
('stage_change', 'Stage Change'),
('task_completed', 'Task Completed'),
]
activity_type = models.CharField(max_length=20, choices=ACTIVITY_TYPES)
description = models.TextField()
contact = models.ForeignKey(Contact, on_delete=models.CASCADE, related_name='activities')
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, related_name='activities', null=True, blank=True)
owner = 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'
indexes = [
models.Index(fields=['contact', '-created_at']),
models.Index(fields=['deal', '-created_at']),
]
def __str__(self):
return f"{self.get_activity_type_display()} - {self.contact.full_name} - {self.created_at.strftime('%Y-%m-%d')}"
def save(self, *args, **kwargs):
"""Update last_contacted on the contact when activity is logged"""
super().save(*args, **kwargs)
if self.activity_type in ['call', 'email', 'meeting']:
self.contact.last_contacted = self.created_at
self.contact.save()
# Custom manager for dashboard queries
class DealManager(models.Manager):
def pipeline_summary(self, user=None):
"""Get deal counts and values by stage"""
queryset = self.filter(is_active=True)
if user:
queryset = queryset.filter(owner=user)
summary = {}
for stage, _ in Deal.STAGE_CHOICES:
deals = queryset.filter(stage=stage)
summary[stage] = {
'count': deals.count(),
'total_value': sum(d.value for d in deals),
'weighted_value': sum(d.weighted_value for d in deals),
}
return summary
# Add custom manager to Deal model
Deal.objects = DealManager()
Code 2: Dashboard Views and Admin Configuration
# views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.db.models import Sum, Count, Q
from django.utils import timezone
from datetime import timedelta
from .models import Contact, Deal, Task, Activity
@login_required
def dashboard(request):
"""Main CRM dashboard - shows everything that matters right now"""
user = request.user
today = timezone.now()
# Overdue tasks - highest priority
overdue_tasks = Task.objects.filter(
owner=user,
completed=False,
due_date__lt=today
).select_related('contact', 'deal')
# Today's tasks
today_tasks = Task.objects.filter(
owner=user,
completed=False,
due_date__date=today.date()
).select_related('contact', 'deal')
# Upcoming tasks (next 7 days)
upcoming_tasks = Task.objects.filter(
owner=user,
completed=False,
due_date__gt=today,
due_date__lte=today + timedelta(days=7)
).select_related('contact', 'deal')
# Active deals by stage
active_deals = Deal.objects.filter(owner=user, is_active=True)
pipeline_summary = Deal.objects.pipeline_summary(user)
# Calculate total pipeline value and weighted forecast
total_pipeline = active_deals.aggregate(total=Sum('value'))['total'] or 0
weighted_forecast = sum(d.weighted_value for d in active_deals)
# Deals closing this month
deals_closing_soon = active_deals.filter(
expected_close_date__month=today.month,
expected_close_date__year=today.year
).exclude(stage__in=['closed_won', 'closed_lost'])
# Contacts needing follow-up (not contacted in 30+ days)
stale_contacts = Contact.objects.filter(
Q(owner=user) & Q(is_active=True) &
(Q(last_contacted__lt=today - timedelta(days=30)) | Q(last_contacted__isnull=True))
)
# Recent activity feed
recent_activities = Activity.objects.filter(
owner=user
).select_related('contact', 'deal')[:10]
# Quick stats
stats = {
'total_active_contacts': Contact.objects.filter(owner=user, is_active=True).count(),
'total_active_deals': active_deals.count(),
'overdue_tasks_count': overdue_tasks.count(),
'deals_closing_this_month': deals_closing_soon.count(),
}
context = {
'overdue_tasks': overdue_tasks,
'today_tasks': today_tasks,
'upcoming_tasks': upcoming_tasks,
'pipeline_summary': pipeline_summary,
'total_pipeline': total_pipeline,
'weighted_forecast': weighted_forecast,
'deals_closing_soon': deals_closing_soon,
'stale_contacts': stale_contacts,
'recent_activities': recent_activities,
'stats': stats,
}
return render(request, 'crm/dashboard.html', context)
@login_required
def contact_detail(request, contact_id):
"""Full contact view with history and related deals"""
contact = get_object_or_404(Contact, id=contact_id, owner=request.user)
# Get all related data
deals = contact.deals.all()
tasks = contact.tasks.filter(completed=False)
activities = contact.activities.all()[:20]
# Calculate metrics
total_deal_value = deals.aggregate(total=Sum('value'))['total'] or 0
won_deals = deals.filter(stage='closed_won')
won_value = won_deals.aggregate(total=Sum('value'))['total'] or 0
context = {
'contact': contact,
'deals': deals,
'tasks': tasks,
'activities': activities,
'total_deal_value': total_deal_value,
'won_value': won_value,
'win_rate': (won_deals.count() / deals.count() * 100) if deals.count() > 0 else 0,
}
return render(request, 'crm/contact_detail.html', context)
@login_required
def deal_detail(request, deal_id):
"""Deal view with stage progression and task management"""
deal = get_object_or_404(Deal, id=deal_id, owner=request.user)
tasks = deal.tasks.all()
activities = deal.activities.all()
context = {
'deal': deal,
'tasks': tasks,
'activities': activities,
'stage_choices': Deal.STAGE_CHOICES,
}
return render(request, 'crm/deal_detail.html', context)
@login_required
def move_deal_stage(request, deal_id):
"""AJAX endpoint to move deals through pipeline"""
if request.method == 'POST':
deal = get_object_or_404(Deal, id=deal_id, owner=request.user)
new_stage = request.POST.get('stage')
if new_stage in dict(Deal.STAGE_CHOICES):
deal.move_to_stage(new_stage)
return redirect('deal_detail', deal_id=deal.id)
return redirect('dashboard')
@login_required
def complete_task(request, task_id):
"""Mark task as complete"""
task = get_object_or_404(Task, id=task_id, owner=request.user)
task.mark_complete()
# Log activity
if task.contact:
Activity.objects.create(
contact=task.contact,
deal=task.deal,
activity_type='task_completed',
description=f"Completed task: {task.title}",
owner=request.user
)
return redirect('dashboard')
# admin.py
from django.contrib import admin
from .models import Contact, Deal, Task, Activity
@admin.register(Contact)
class ContactAdmin(admin.ModelAdmin):
list_display = ['full_name', 'company', 'email', 'contact_type', 'owner', 'last_contacted', 'needs_followup']
list_filter = ['contact_type', 'is_active', 'created_at']
search_fields = ['first_name', 'last_name', 'email', 'company']
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Basic Information', {
'fields': ('first_name', 'last_name', 'email', 'phone', 'company', 'job_title')
}),
('Classification', {
'fields': ('contact_type', 'owner', 'tags', 'is_active')
}),
('Timeline', {
'fields': ('last_contacted', 'created_at', 'updated_at'),
}),
('Notes', {
'fields': ('notes',),
'classes': ('collapse',)
}),
)
@admin.register(Deal)
class DealAdmin(admin.ModelAdmin):
list_display = ['title', 'contact', 'value', 'stage', 'win_probability', 'expected_close_date', 'is_overdue', 'owner']
list_filter = ['stage', 'is_active', 'created_at']
search_fields = ['title', 'contact__first_name', 'contact__last_name']
readonly_fields = ['created_at', 'updated_at', 'closed_at', 'weighted_value']
fieldsets = (
('Deal Information', {
'fields': ('title', 'contact', 'value', 'stage')
}),
('Timeline', {
'fields': ('expected_close_date', 'closed_at', 'created_at', 'updated_at')
}),
('Assignment', {
'fields': ('owner', 'is_active')
}),
('Calculated Fields', {
'fields': ('weighted_value',),
'classes': ('collapse',)
}),
('Notes', {
'fields': ('notes',),
'classes': ('collapse',)
}),
)
@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
list_display = ['title', 'contact', 'deal', 'due_date', 'priority', 'completed', 'is_overdue', 'owner']
list_filter = ['completed', 'priority', 'due_date']
search_fields = ['title', 'description']
readonly_fields = ['completed_at', 'created_at']
actions = ['mark_completed']
def mark_completed(self, request, queryset):
for task in queryset:
task.mark_complete()
mark_completed.short_description = "Mark selected tasks as completed"
@admin.register(Activity)
class ActivityAdmin(admin.ModelAdmin):
list_display = ['activity_type', 'contact', 'deal', 'owner', 'created_at']
list_filter = ['activity_type', 'created_at']
search_fields = ['description', 'contact__first_name', 'contact__last_name']
readonly_fields = ['created_at']
4. How to Run the Code
Step 1: Set up your Django project
Open your terminal and create a new directory:
mkdir crm_project
cd crm_project
Step 2: Create a virtual environment
python -m venv venv
Activate it:
- Windows:
venv\Scripts\activate - Mac/Linux:
source venv/bin/activate
Step 3: Install Django
pip install django
Step 4: Start your Django project
django-admin startproject crm_config .
python manage.py startapp crm
Step 5: Add the app to your project
Open crm_config/settings.py and add 'crm' to your INSTALLED_APPS list:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'crm', # Add this line
]
Step 6: Add the models
Copy the entire first code block (models.py) into crm/models.py. Replace everything in that file.
Step 7: Add the views and admin configuration
Copy the second code block into two files:
- The views section goes into
crm/views.py - The admin section goes into
crm/admin.py
Step 8: Create the database
Run these commands:
python manage.py makemigrations
python manage.py migrate
You should see output showing tables being created for Contact, Deal, Task, and Activity.
Step 9: Create a superuser
python manage.py createsuperuser
Enter a username, email, and password when prompted. Remember these – you’ll need them to log in.
Step 10: Set up URLs
Create a file crm/urls.py and add:
from django.urls import path
from . import views
urlpatterns = [
path('', views.dashboard, name='dashboard'),
path('contact/<int:contact_id>/', views.contact_detail, name='contact_detail'),
path('deal/<int:deal_id>/', views.deal_detail, name='deal_detail'),
path('deal/<int:deal_id>/move/', views.move_deal_stage, name='move_deal_stage'),
path('task/<int:task_id>/complete/', views.complete_task, name='complete_task'),
]
Then edit crm_config/urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('crm/', include('crm.urls')),
]
Step 11: Run the server
python manage.py runserver
Step 12: Access your CRM
Open your browser and go to http://127.0.0.1:8000/admin/
Log in with the superuser credentials you created. You’ll see the Django admin interface with your CRM models ready to use.
Step 13: Add sample data
Click on “Contacts” and add a few test contacts. Then create deals associated with those contacts. Add tasks with different due dates to see how the dashboard prioritizes them.
Step 14: View your dashboard
Go to http://127.0.0.1:8000/crm/ to see your dashboard (you’ll need to create basic templates for the views to display properly, or just use the admin interface to manage everything).
That’s it. Your CRM is running locally. Every contact you add, every deal you track, every task you create – it’s all stored in your database and ready to scale.
Key Concepts
You just built something most businesses pay thousands for. Not a toy version – a real system with proper relationships, cascade logic, and tracking that doesn’t forget.
What makes this work:
Django models aren’t just database tables – they’re classes with behavior. The move_to_stage method doesn’t just update a field, it logs the change and handles what “closing a deal” actually means. ForeignKeys connect everything automatically. When you delete a contact, Django knows what to do with related records.
Properties turn data into decisions. needs_followup calculates itself based on last_contacted. weighted_value multiplies deal value by stage probability. You’re not storing these – they compute on demand, turning your database into a decision-making tool.
Indexes matter at scale. We indexed owner and is_active together because the dashboard filters on both constantly. Strategic indexing is what separates a prototype from something that stays fast when you hit 10,000 contacts.
What this replaces:
Spreadsheets that break when two people edit them. Email threads that hide follow-ups. Those $99/month subscriptions that force you into their workflow instead of adapting to yours. You control the data, you decide what fields matter, you add features when you need them.
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.
