Sales App¶
A CRM module for managing the full sales pipeline — from lead capture through account management and opportunity tracking — with built-in Metadata API integration for customizable list views and detail pages.
Key Features¶
- Lead Management — Capture and qualify leads with flexible metadata fields (phone, company, status, message)
- Accounts & Contacts — Organize customers into accounts with linked contacts and ownership tracking
- Opportunity Pipeline — Track deals with configurable stages, amounts, close dates, and win/loss status
- Metadata API Integration — All models are registered with the Metadata API for auto-generated list views, detail pages, and bulk actions
- Ownership & Scoping — Every record has an
ownerfield and ascopedmanager inherited fromBaseModel - Flexible Metadata — JSONField on every model allows storing custom fields without schema changes
Architecture¶
┌─────────────────────────────────────────────────────────────┐
│ Your Application │
│ (app/sales/metadata.py) │
└─────────────────────────────────────────────────────────────┘
│
Metadata API Registration
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Sales Models │
│ (grit/sales/models.py) │
│ │
│ ┌──────────┐ converts to ┌───────────────────┐ │
│ │ Lead │ ───────────────▶ │ Contact + Account │ │
│ └──────────┘ └───────────────────┘ │
│ │ │
│ has many │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ Opportunity │ │
│ │ (Deal/Pipeline)│ │
│ └───────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
inherits
│
▼
┌─────────────────────────────────────────────────────────────┐
│ BaseModel │
│ • UUID primary key • metadata (JSON) │
│ • created_at / updated_at • owner (FK to User) │
│ • scoped manager │
└─────────────────────────────────────────────────────────────┘
Quick Start¶
1. Register Models with the Metadata API¶
In your app/sales/metadata.py, register each model to enable auto-generated list and detail views:
from grit.core.metadata import metadata
from grit.sales.models import Lead, Account, Contact, Opportunity
@metadata.register(Lead)
class LeadMetadata(metadata.ModelMetadata):
list_display = ('name', 'email', 'message', 'created_at')
list_actions = [
[{'label': 'New Lead', 'action': 'new'}]
]
fieldsets = (
('Basic Information', {
'fields': ('name', 'email', 'message', 'created_at')
}),
)
2. Configure the Admin Panel¶
In your app/sales/admin.py, register models with the Django admin for back-office management:
from django.contrib import admin
from grit.sales.models import Lead
@admin.register(Lead)
class LeadAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'email', 'created_at')
search_fields = ('first_name', 'last_name', 'email')
ordering = ('-created_at',)
3. Create Leads Programmatically¶
Use the built-in LeadManager to create leads with metadata:
from grit.sales.models import Lead
lead = Lead.objects.create_with_metadata(
first_name="Jane",
last_name="Doe",
email="jane@example.com",
phone="+1-555-0123",
company="Acme Corp",
message="Interested in enterprise plan"
)
Core Models¶
Lead¶
Represents a potential customer who has expressed interest. Leads are the entry point of the sales pipeline.
| Field | Type | Description |
|---|---|---|
first_name |
CharField |
Lead's first name |
last_name |
CharField |
Lead's last name |
email |
EmailField |
Lead's email address |
Metadata Fields (stored in the metadata JSONField):
| Key | Type | Description |
|---|---|---|
phone |
string |
Phone number |
company |
string |
Company name |
status |
object |
Status with label and value (e.g., "New", "Unqualified") |
message |
string |
Message from the lead |
Custom Manager:
# LeadManager provides create_with_metadata() for structured lead creation
lead = Lead.objects.create_with_metadata(
first_name="Jane",
last_name="Doe",
email="jane@example.com",
phone="+1-555-0123",
company="Acme Corp"
)
Contact¶
Represents a qualified person associated with an account. Contacts can optionally be linked to a user in the system.
| Field | Type | Description |
|---|---|---|
first_name |
CharField |
Contact's first name |
last_name |
CharField |
Contact's last name |
title |
CharField |
Job title |
email |
EmailField |
Contact email |
user |
OneToOneField |
Optional link to a system user |
account |
ForeignKey |
Associated account |
Account¶
Represents a company or organization. Accounts group related contacts and opportunities together.
| Field | Type | Description |
|---|---|---|
name |
CharField |
Account/company name |
Related Objects:
- contacts — All contacts linked to this account
- opportunities — All opportunities linked to this account
Opportunity¶
Represents a potential deal in the sales pipeline. Opportunities track revenue, stage progression, and close dates.
| Field | Type | Description |
|---|---|---|
name |
CharField |
Opportunity name |
account |
ForeignKey |
Associated account |
amount |
DecimalField |
Deal value |
close_date |
DateField |
Expected close date |
stage |
CharField |
Pipeline stage (configurable via settings) |
is_closed |
BooleanField |
Whether the deal is closed |
is_won |
BooleanField |
Whether the deal was won |
Metadata API Configuration¶
List Views¶
Define filtered views for list pages using list_views. Each view specifies which fields to display and what filters to apply:
@metadata.register(Opportunity)
class OpportunityMetadata(metadata.ModelMetadata):
list_display = ('name', 'account', 'stage', 'amount', 'close_date')
list_views = {
'all': {
'label': 'All',
'fields': ('name', 'email', 'message', 'created_at'),
'filters': {}
},
'new': {
'label': 'New',
'fields': ('name', 'email', 'created_at'),
'filters': {
'stage': 'new'
}
}
}
Inlines¶
Display related records on detail pages using Django admin-style inlines:
from django.contrib import admin
class ContactInline(admin.TabularInline):
model = Contact
extra = 0
fields = ('first_name', 'last_name', 'email')
@metadata.register(Account)
class AccountMetadata(metadata.ModelMetadata):
list_display = ('name', 'owner')
inlines = [ContactInline, OpportunityInline]
Configuration Reference¶
Opportunity Stages¶
Opportunity stages are configurable via APP_METADATA_SETTINGS in your Django settings:
# settings.py
APP_METADATA_SETTINGS = {
'CHOICES': {
'opportunity_stages': {
'new': {'label': 'New'},
'qualification': {'label': 'Qualification'},
'proposal': {'label': 'Proposal'},
'negotiation': {'label': 'Negotiation'},
'closed_won': {'label': 'Closed Won'},
'closed_lost': {'label': 'Closed Lost'},
}
}
}
The first stage in the dictionary is used as the default value for new opportunities.
BaseModel Inherited Fields¶
All sales models inherit these fields from BaseModel:
| Field | Type | Description |
|---|---|---|
id |
UUIDField |
Auto-generated unique identifier |
metadata |
JSONField |
Flexible JSON storage for custom fields |
created_at |
DateTimeField |
Auto-set on creation |
updated_at |
DateTimeField |
Auto-set on save |
owner |
ForeignKey |
Record owner (links to user) |