Deals & Pipelines¶
Deals represent sales opportunities that progress through pipeline stages. Each deal is linked to an entity and a stage within a pipeline.
Pipeline¶
A pipeline defines a sequence of stages that deals move through.
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
required | Pipeline name (unique) |
description |
str \| None |
None |
Pipeline description |
is_active |
bool |
True |
Whether the pipeline is active |
Pipelines are system-wide and not owned by individual users.
Stage¶
Stages represent steps within a pipeline.
| Field | Type | Default | Description |
|---|---|---|---|
pipeline |
Pipeline |
required | Parent pipeline (FK) |
name |
str |
required | Stage name |
description |
str \| None |
None |
Stage description |
order |
int |
required | Display order within the pipeline |
probability |
float |
0.0 |
Win probability percentage (0–100) |
status |
Literal['open', 'closed_won', 'closed_lost'] |
'open' |
Stage status |
The StageManager automatically calls select_related('pipeline') on all queries.
Deal¶
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
required | Deal name |
amount |
float \| None |
None |
Deal value (must be >= 0) |
currency |
str |
'USD' |
Currency code |
entity |
Entity |
required | Related entity (FK) |
stage |
Stage |
required | Current pipeline stage (FK) |
assigned_to |
User \| None |
None |
Deal owner (FK to User) |
expected_close_date |
datetime \| None |
None |
Expected close date |
closed_date |
datetime \| None |
None |
Actual close date (auto-set) |
status |
Literal['open', 'closed_won', 'closed_lost'] |
'open' |
Deal status (auto-synced with stage) |
custom_fields |
dict[str, Any] \| None |
None |
Custom field values |
created_at / updated_at |
datetime |
auto | Timestamps |
The DealManager automatically calls select_related('stage', 'assigned_to', 'entity') on all queries.
Automatic Status Sync¶
When a deal is updated, the pre_update hook automatically syncs the deal's status with its stage:
- If the stage status is
open, the deal status is set toopen - If the stage status is
closed_won, the deal status is set toclosed_won - If the stage status is
closed_lost, the deal status is set toclosed_lost - When a deal transitions to a closed status,
closed_dateis automatically set to the current UTC time
DealService¶
DealService provides transactional methods for deal stage management.
move_deal_to_stage¶
from amsdal_crm.services.deal_service import DealService
updated_deal = DealService.move_deal_to_stage(
deal=deal,
new_stage_id='<stage_object_id>',
note='Moving to negotiation phase',
user_email='user@example.com',
)
# Async version
updated_deal = await DealService.amove_deal_to_stage(
deal=deal,
new_stage_id='<stage_object_id>',
note='Moving to negotiation phase',
user_email='user@example.com',
)
This method:
- Loads the new stage and updates the deal (status auto-syncs via
pre_update) - Creates a
Noteactivity recording the stage change for the audit trail - Emits lifecycle events
Both sync and async versions run inside a transaction (@transaction / @async_transaction).
Lifecycle Events¶
The following lifecycle events are emitted by DealService:
| Event | When | Payload |
|---|---|---|
ON_DEAL_STAGE_CHANGE |
Any stage change | deal, old_stage, new_stage, user_email |
ON_DEAL_WON |
Stage status is closed_won |
deal, user_email |
ON_DEAL_LOST |
Stage status is closed_lost |
deal, user_email |
Events are published via LifecycleProducer and can be subscribed to for custom integrations:
from amsdal_crm.constants import CRMLifecycleEvent
Example¶
from amsdal_crm.models.pipeline import Pipeline
from amsdal_crm.models.stage import Stage
from amsdal_crm.models.deal import Deal
from amsdal_crm.services.deal_service import DealService
# Create a pipeline
pipeline = Pipeline(name='Enterprise Sales')
pipeline.save(force_insert=True)
# Create stages
prospecting = Stage(
pipeline=pipeline,
name='Prospecting',
order=1,
probability=10.0,
status='open',
)
prospecting.save(force_insert=True)
negotiation = Stage(
pipeline=pipeline,
name='Negotiation',
order=2,
probability=50.0,
status='open',
)
negotiation.save(force_insert=True)
closed_won = Stage(
pipeline=pipeline,
name='Closed Won',
order=3,
probability=100.0,
status='closed_won',
)
closed_won.save(force_insert=True)
# Create a deal
deal = Deal(
name='Acme Enterprise License',
amount=50000.0,
entity=entity,
stage=prospecting,
)
deal.save(force_insert=True)
# Move deal through stages
DealService.move_deal_to_stage(
deal=deal,
new_stage_id=negotiation._object_id,
note='Initial meeting went well',
user_email='sales@example.com',
)