Skip to content

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 to open
  • If the stage status is closed_won, the deal status is set to closed_won
  • If the stage status is closed_lost, the deal status is set to closed_lost
  • When a deal transitions to a closed status, closed_date is 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:

  1. Loads the new stage and updates the deal (status auto-syncs via pre_update)
  2. Creates a Note activity recording the stage change for the audit trail
  3. 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',
)