gptdevelopers.io

About gptdevelopers.io/

Table of Contents:

Building GPT Systems & Software / gptdevelopers.io

Python CRM System – Build Your Own Customer Management Backend/

Michael

Michael

Michael is a software engineer and startup growth expert with 10+ years of software engineering and machine learning experience.

0 Min Read

Twitter LogoLinkedIn LogoFacebook Logo
Python CRM System – Build Your Own Customer Management Backend
Top No-Code and Low-Code Platforms for Rapid App Development
Top No-Code and Low-Code Platforms for Rapid App Development

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.