django-dynamic-workflows 1.6.0
A powerful, configurable Django package for implementing dynamic multi-step workflow processes with database-stored actions
Requires Python: >=3.10

# Django Dynamic Workflows

A powerful, configurable Django package for implementing dynamic multi-step workflow processes with database-stored actions and approval flows.

## Features

- **Flexible Workflow Strategies** ⭐ NEW in v1.5.0: Choose from 3 hierarchy levels (Stage/Pipeline/Workflow-only) based on complexity
- **Generic Workflow Attachment**: Attach workflows to any Django model without hardcoded relationships
- **Database-Stored Actions**: Configure actions dynamically in the database with inheritance system
- **2-Tier Action Priority System**: Database → Settings action resolution (no conflicts)
- **Settings-Based Actions**: Configure project-wide actions via `WORKFLOW_ACTIONS_CONFIG`
- **Action Inheritance**: Stage → Pipeline → Workflow action hierarchy
- **Approval Flow Integration**: Built on top of django-approval-workflow package
- **Approval Type Support**: Control approval behavior with APPROVE, SUBMIT, CHECK_IN_VERIFY, and MOVE types
- **Complete Approval Actions**: Full support for approve, reject, delegate, and resubmission workflows
- **Resubmission & Delegation Logic**: Proper stage transitions and user assignments with workflow event triggers
- **Configurable Triggers**: Actions triggered on workflow events (approve, reject, delegate, etc.)
- **Opt-in Email Actions**: Configure email notifications using database actions or settings
- **Dynamic Function Execution**: Execute Python functions by database-stored paths
- **Workflow Cleanup & Management**: Built-in cleanup utilities to manage completed workflows and reduce database size
- **Admin Interface**: Rich Django admin for managing workflows, stages, and actions

## Installation

```bash
pip install django-dynamic-workflows
```

## Configuration

The Django Workflow Engine can be configured through your Django settings:

```python
# settings.py
DJANGO_WORKFLOW_ENGINE = {
    # Department Model Mapping (NEW)
    # Map the department GenericForeignKey to any model in your project
    'DEPARTMENT_MODEL': 'myapp.Department',  # Optional: specify your department model

    # Model Configuration
    'ENABLED_MODELS': [
        'myapp.PurchaseRequest',
        'crm.Opportunity',
        'support.Ticket',
    ],

    # Default field name for workflow status
    'DEFAULT_STATUS_FIELD': 'workflow_status',

    # Workflow Mappings
    'MODEL_WORKFLOW_MAPPINGS': {
        'myapp.PurchaseRequest': ['purchase_approval', 'emergency_approval'],
        'crm.Opportunity': ['sales_process'],
    },

    # Auto-start Configuration
    'AUTO_START_WORKFLOWS': {
        'myapp.PurchaseRequest': {
            'workflow_slug': 'purchase_approval',
            'conditions': {'amount__gte': 1000}  # Only for amounts >= 1000
        }
    },

    # Permissions
    'PERMISSIONS': {
        'REQUIRE_PERMISSION_TO_START': True,
        'REQUIRE_PERMISSION_TO_APPROVE': True,
    }
}
```
### Company Model Architecture

**Important**: The `company` field in workflow models uses Django's `AUTH_USER_MODEL` (User model) for maximum flexibility:

```python
# Workflow models use User as company for multi-tenant support:
class WorkFlow(models.Model):
    company = models.ForeignKey(
        settings.AUTH_USER_MODEL,  # Uses your User model
        on_delete=models.SET_NULL,
        related_name="workflow_company"
    )
```

#### Why User Model for Company?

This design supports various multi-tenant architectures:

```python
# Single Company per User
user.username = "acme_corp"
user.email = "admin@acmecorp.com"

# Multi-tenant SaaS where users represent companies
company_user = User.objects.create(
    username="company_123",
    email="admin@company123.com"
)

# Enterprise where User has company profile
user.profile.company_name = "Enterprise Corp"
```

#### Usage Examples

```python
# Create workflows for specific companies (users)
workflow = WorkFlow.objects.create(
    company=company_user,  # User representing the company
    name_en="Company Workflow"
)

# Filter workflows by company
company_workflows = WorkFlow.objects.filter(company=company_user)

# Multi-tenant isolation
user_workflows = WorkFlow.objects.filter(company=request.user)
```

This approach provides flexibility for:
- 🏢 **Multi-tenant SaaS**: Each tenant has a user representing their company
- 🏛️ **Enterprise**: Companies can be mapped through user profiles or groups
- 🔒 **Security**: Natural permission boundaries through Django's user system
- 📊 **Scalability**: Leverage Django's user management and authentication


## Quick Start

1. Add to INSTALLED_APPS:

```python
INSTALLED_APPS = [
    ...
    'approval_workflow',  # Required dependency
    'django_workflow_engine',
    ...
]
```

2. Run migrations:

```bash
python manage.py migrate
```

3. Register the built-in workflow handler:

```python
# settings.py
APPROVAL_HANDLERS = [
    "django_workflow_engine.handlers.WorkflowApprovalHandler",
]
```

This enables automatic workflow progression when approvals are completed.

4. Configure Approval Package Models:

Django Dynamic Workflows is built on top of the `django-approval-workflow` package. You need to configure two essential models for the approval system to work:

#### Required Settings

```python
# settings.py

# Role Model - for role-based approvals
# This model represents user roles (e.g., Manager, Director, CEO)
APPROVAL_ROLE_MODEL = 'myapp.Role'  # or 'auth.Group' to use Django's built-in Group model

# Dynamic Form Model - for approval forms (optional)
# This model represents dynamic forms that can be attached to approval steps
APPROVAL_DYNAMIC_FORM_MODEL = 'myapp.DynamicForm'  # Optional: for form-based approvals
```

#### Option 1: Use Django's Built-in Group Model (Simplest)

The easiest way is to use Django's built-in `Group` model for roles:

```python
# settings.py
APPROVAL_ROLE_MODEL = 'auth.Group'
```

Then create groups in Django admin or programmatically:

```python
from django.contrib.auth.models import Group

# Create roles as groups
finance_role = Group.objects.create(name='Finance Manager')
executive_role = Group.objects.create(name='Executive')

# Assign users to roles
user.groups.add(finance_role)
```

#### Option 2: Create a Custom Role Model

For more control, create a custom Role model:

```python
# myapp/models.py
from django.db import models
from django.contrib.auth import get_user_model

User = get_user_model()

class Role(models.Model):
    """Custom role model for approval workflows."""
    name = models.CharField(max_length=100, unique=True)
    description = models.TextField(blank=True)
    users = models.ManyToManyField(User, related_name='approval_roles')

    class Meta:
        db_table = 'approval_roles'

    def __str__(self):
        return self.name
```

Then configure it in settings:

```python
# settings.py
APPROVAL_ROLE_MODEL = 'myapp.Role'
```

#### Option 3: Dynamic Form Model (Optional)

If you want to attach forms to approval steps, create a DynamicForm model:

```python
# myapp/models.py
class DynamicForm(models.Model):
    """Dynamic form that can be attached to approval steps."""
    name = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    form_schema = models.JSONField()  # Store form fields as JSON

    def __str__(self):
        return self.name
```

Configure it:

```python
# settings.py
APPROVAL_DYNAMIC_FORM_MODEL = 'myapp.DynamicForm'
```

#### Complete Settings Example

```python
# settings.py
INSTALLED_APPS = [
    ...
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'approval_workflow',
    'django_workflow_engine',
    'myapp',
    ...
]

# Approval Package Configuration
APPROVAL_ROLE_MODEL = 'auth.Group'  # Using Django's built-in Group model
# APPROVAL_DYNAMIC_FORM_MODEL = 'myapp.DynamicForm'  # Optional

# Django Workflow Engine Configuration
DJANGO_WORKFLOW_ENGINE = {
    'DEPARTMENT_MODEL': 'myapp.Department',
}
```

4. Register a model for workflow support:

```python
from django_workflow_engine.services import register_model_for_workflow
from myapp.models import Ticket

register_model_for_workflow(
    Ticket,
    auto_start=True,
    status_field='workflow_status',
    stage_field='current_stage'
)
```

4. Attach and start a workflow:

```python
from django_workflow_engine.services import attach_workflow_to_object

attachment = attach_workflow_to_object(
    obj=my_ticket,
    workflow=my_workflow,
    user=request.user,
    auto_start=True
)
```

## 🔄 Workflow Cloning & Immutability

**IMPORTANT**: Django Dynamic Workflows automatically clones workflows when attaching them to objects to ensure **workflow immutability**. This prevents corruption of running workflows when the original workflow template is modified.

### How It Works

```python
# When you attach a workflow:
attachment = attach_workflow_to_object(obj=my_object, workflow=template_workflow)

# A clone is created automatically:
# - Original workflow: ID=1, name="Purchase Approval"
# - Cloned workflow: ID=2, name="Purchase Approval (Copy)", cloned_from=1

# Later modifications to the original won't affect running workflows:
template_workflow.name_en = "Updated Purchase Approval"
template_workflow.save()
# Running workflow (ID=2) remains unchanged - immutable! ✅
```

### Disable Cloning (Advanced)

If you need to use the original workflow without cloning (not recommended):

```python
attachment = attach_workflow_to_object(
    obj=my_object,
    workflow=template_workflow,
    disable_clone=True  # ⚠️ WARNING: May cause corruption
)
```

**⚠️ Warning**: Setting `disable_clone=True` may cause workflow corruption if the original workflow is modified after attachment. Only use this for special cases where you need direct workflow sharing.

### Benefits of Workflow Cloning

- ✅ **Data Integrity**: Running workflows remain stable even when templates change
- ✅ **Version Control**: Each workflow execution has its own immutable version
- ✅ **Audit Trail**: `cloned_from` field tracks the original template
- ✅ **Safe Updates**: Modify workflow templates without breaking active processes

## Core Concepts

### WorkFlow, Pipeline, Stage Hierarchy
- **WorkFlow**: Top-level workflow definition
- **Pipeline**: Departments or phases within a workflow
- **Stage**: Individual approval steps within a pipeline

## Workflow Strategy System

**New in v1.5.0**: The workflow engine now supports **3 flexible strategies** for structuring your approval workflows based on organizational complexity.

### Strategy Overview

Choose the right strategy based on your workflow complexity and organizational structure:

| Strategy | Value | Structure | Approvals Location | Use Case |
|----------|-------|-----------|-------------------|----------|
| **WORKFLOW_PIPELINE_STAGE** | 1 | Workflow → Pipeline → Stage | `stage_info` | Complex multi-department workflows with detailed stages |
| **WORKFLOW_PIPELINE** | 2 | Workflow → Pipeline | `pipeline_info` | Department-level approvals without stage granularity |
| **WORKFLOW_ONLY** | 3 | Workflow only | `workflow_info` | Simple single-step approval workflows |

### Strategy 1: WORKFLOW_PIPELINE_STAGE (Full Hierarchy)

**Best For**: Complex workflows with multiple departments and detailed approval stages

**Structure**:
```
Workflow
  ├── Pipeline 1 (Finance Department)
  │   ├── Stage 1 (Initial Review) ← Approvals here
  │   ├── Stage 2 (Budget Check) ← Approvals here
  │   └── Stage 3 (CFO Approval) ← Approvals here
  └── Pipeline 2 (Management)
      └── Stage 1 (Executive Approval) ← Approvals here
```

**Configuration Example**:
```python
from django_workflow_engine.models import WorkFlow, Pipeline, Stage
from django_workflow_engine.choices import WorkflowStrategy, ApprovalTypes
from approval_workflow.choices import RoleSelectionStrategy

# Create workflow with Strategy 1
workflow = WorkFlow.objects.create(
    name_en="Purchase Request Approval",
    strategy=WorkflowStrategy.WORKFLOW_PIPELINE_STAGE,
    company=user,
    is_active=True
)

# Create pipeline
finance_pipeline = Pipeline.objects.create(
    workflow=workflow,
    name_en="Finance Review",
    order=0
)

# Create stage with approvals in stage_info
initial_review = Stage.objects.create(
    pipeline=finance_pipeline,
    name_en="Initial Review",
    order=0,
    stage_info={
        "color": "#3498db",
        "approvals": [
            {
                "approval_type": ApprovalTypes.ROLE,
                "user_role": finance_role.id,
                "role_selection_strategy": RoleSelectionStrategy.ANYONE
            }
        ]
    }
)
```

**Use Cases**:
- Large purchase approvals with multiple review stages
- Complex HR workflows (recruitment → onboarding → training)
- Multi-department project approvals
- Detailed compliance workflows

### Strategy 2: WORKFLOW_PIPELINE (Two-Level)

**Best For**: Department-based workflows without stage-level detail

**Structure**:
```
Workflow
  ├── Pipeline 1 (HR Department) ← Approvals here
  ├── Pipeline 2 (Finance Department) ← Approvals here
  └── Pipeline 3 (Legal Department) ← Approvals here

Note: NO stages allowed in Strategy 2
```

**Configuration Example**:
```python
# Create workflow with Strategy 2
workflow = WorkFlow.objects.create(
    name_en="Employee Onboarding",
    strategy=WorkflowStrategy.WORKFLOW_PIPELINE,
    company=user,
    is_active=True
)

# Create pipeline with approvals in pipeline_info
hr_pipeline = Pipeline.objects.create(
    workflow=workflow,
    name_en="HR Processing",
    order=0,
    pipeline_info={
        "approvals": [
            {
                "approval_type": ApprovalTypes.ROLE,
                "user_role": hr_manager_role.id,
                "role_selection_strategy": RoleSelectionStrategy.ANYONE
            }
        ]
    }
)

# IMPORTANT: Do NOT create stages for Strategy 2 workflows
# Stages are not allowed and will be rejected by validation
```

**Use Cases**:
- Department-based approval workflows
- Sequential departmental reviews
- Cross-functional team approvals
- Simple multi-step processes

### Strategy 3: WORKFLOW_ONLY (Single-Level)

**Best For**: Simple, single-step approval workflows

**Structure**:
```
Workflow ← Approvals here

Note: NO pipelines or stages allowed in Strategy 3
```

**Configuration Example**:
```python
# Create workflow with Strategy 3
workflow = WorkFlow.objects.create(
    name_en="Time Off Request",
    strategy=WorkflowStrategy.WORKFLOW_ONLY,
    company=user,
    is_active=True,
    workflow_info={
        "approvals": [
            {
                "approval_type": ApprovalTypes.USER,
                "approval_user": manager.id
            }
        ]
    }
)

# IMPORTANT: Do NOT create pipelines or stages for Strategy 3 workflows
# They are not allowed and will be rejected by validation
```

**Use Cases**:
- Simple manager approval workflows
- Quick sign-off processes
- Single-step authorization
- Lightweight approval needs

### Strategy Selection Guide

**Choose Strategy 1** if you need:
- Multiple departments with detailed stages
- Complex approval chains with many steps
- Fine-grained control over each approval stage
- Different forms/requirements per stage

**Choose Strategy 2** if you need:
- Department-level approvals without stage detail
- Sequential departmental reviews
- Simpler structure than Strategy 1
- Each department approves as a unit

**Choose Strategy 3** if you need:
- Single approval step
- Minimal complexity
- Quick implementation
- One approver or approval group

### Strategy Validation

The system automatically validates structural constraints:

```python
# Strategy 1: Requires pipelines with stages
workflow = WorkFlow.objects.create(strategy=1, ...)
is_valid, msg = workflow.validate_completeness()
# Returns: False, "Strategy 1 workflow must have at least one pipeline"

# Strategy 2: Requires pipelines, NO stages allowed
workflow = WorkFlow.objects.create(strategy=2, ...)
pipeline = Pipeline.objects.create(workflow=workflow, ...)
stage = Stage.objects.create(pipeline=pipeline, ...)  # ❌ ValidationError!
# Error: "Strategy 2 (Workflow→Pipeline) cannot have stages."

# Strategy 3: NO pipelines or stages allowed
workflow = WorkFlow.objects.create(strategy=3, ...)
pipeline = Pipeline.objects.create(workflow=workflow, ...)  # ❌ ValidationError!
# Error: "Cannot create pipelines for Strategy 3 (Workflow Only) workflows."
```

### Using Serializers with Strategies

The serializers automatically validate strategy constraints:

```python
from django_workflow_engine.serializers import WorkFlowSerializer

# Strategy 1: Full hierarchy
workflow_data = {
    "name_en": "Complex Approval",
    "strategy": 1,  # WORKFLOW_PIPELINE_STAGE
    "pipelines": [
        {
            "name_en": "Finance",
            "stages": [
                {
                    "name_en": "Review",
                    "stage_info": {
                        "approvals": [{"approval_type": "role", "user_role": 1}]
                    }
                }
            ]
        }
    ]
}

# Strategy 2: Pipeline-level only
workflow_data = {
    "name_en": "Department Approval",
    "strategy": 2,  # WORKFLOW_PIPELINE
    "pipelines": [
        {
            "name_en": "HR",
            "pipeline_info": {
                "approvals": [{"approval_type": "role", "user_role": 2}]
            }
            # Note: No "stages" field - not allowed for Strategy 2
        }
    ]
}

# Strategy 3: Workflow-level only
workflow_data = {
    "name_en": "Simple Approval",
    "strategy": 3,  # WORKFLOW_ONLY
    "workflow_info": {
        "approvals": [{"approval_type": "user", "approval_user": 123}]
    }
    # Note: No "pipelines" field - not allowed for Strategy 3
}

serializer = WorkFlowSerializer(data=workflow_data)
if serializer.is_valid():
    workflow = serializer.save()
```

### Strategy Migration

If you're upgrading from a previous version, your existing workflows will continue to work. The default strategy is Strategy 1 (WORKFLOW_PIPELINE_STAGE).

To verify your workflows are using the correct strategy:

```python
from django_workflow_engine.models import WorkFlow

for workflow in WorkFlow.objects.all():
    is_valid, message = workflow.validate_completeness()
    print(f"{workflow.name_en} (Strategy {workflow.strategy}): {message}")
```

### Configurable Actions
- Database-stored function paths executed on workflow events
- Inheritance system: Stage overrides Pipeline overrides Workflow overrides Default
- Support for parameters and custom context

### Action Types
- `AFTER_APPROVE`: After approval step completion
- `AFTER_REJECT`: After workflow rejection
- `AFTER_RESUBMISSION`: After resubmission request
- `AFTER_DELEGATE`: After delegation to another user
- `AFTER_MOVE_STAGE`: After moving between stages
- `AFTER_MOVE_PIPELINE`: After moving between pipelines
- `ON_WORKFLOW_START`: When workflow begins
- `ON_WORKFLOW_COMPLETE`: When workflow completes

## Automatic Status Updates on Workflow Completion/Rejection

**New in v1.2.7**: Automatically update your model's status field when workflows complete or are rejected.

### Overview

The workflow engine can automatically update your model's status field based on workflow outcomes:
- When workflow **completes** → Update to completion status (e.g., "won", "closed", "completed")
- When workflow is **rejected** → Update to rejection status (e.g., "lost", "cancelled", "rejected")

This eliminates manual status management and ensures your models stay in sync with workflow states.

### Configuration

Configure status updates in your `WorkflowConfiguration`:

```python
from django_workflow_engine.models import WorkflowConfiguration
from django.contrib.contenttypes.models import ContentType

# Get or create configuration for your model
ct = ContentType.objects.get_for_model(YourModel)
config, created = WorkflowConfiguration.objects.get_or_create(
    content_type=ct,
    defaults={
        'status_field': 'status',  # Field name on your model
        'completion_status_value': 'completed',  # Value on workflow completion
        'rejection_status_value': 'rejected',    # Value on workflow rejection
    }
)
```

### Real-World Examples

#### Example 1: CRM Opportunity Workflow

```python
# models.py
class Opportunity(models.Model):
    name = models.CharField(max_length=200)
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    status = models.CharField(max_length=50, default='draft')
    # ... other fields

# Configure workflow
from django_workflow_engine.services import register_model_for_workflow

register_model_for_workflow(
    Opportunity,
    status_field='status',
    stage_field='current_stage'
)

# Set status values
ct = ContentType.objects.get_for_model(Opportunity)
config = WorkflowConfiguration.objects.get(content_type=ct)
config.completion_status_value = 'won'      # When deal closes
config.rejection_status_value = 'lost'      # When deal fails
config.save()

# Workflow behavior:
# - All stages approved → opportunity.status = 'won'
# - Any stage rejected → opportunity.status = 'lost'
```

#### Example 2: Support Ticket Workflow

```python
# models.py
class Ticket(models.Model):
    title = models.CharField(max_length=200)
    status = models.CharField(max_length=50, default='open')
    priority = models.CharField(max_length=20)
    # ... other fields

# Configure workflow
ct = ContentType.objects.get_for_model(Ticket)
config, created = WorkflowConfiguration.objects.get_or_create(
    content_type=ct,
    defaults={
        'status_field': 'status',
        'completion_status_value': 'closed',
        'rejection_status_value': 'cancelled',
    }
)

# Workflow behavior:
# - Resolution approved → ticket.status = 'closed'
# - Ticket rejected → ticket.status = 'cancelled'
```

#### Example 3: Purchase Request Workflow

```python
# models.py
class PurchaseRequest(models.Model):
    title = models.CharField(max_length=200)
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    status = models.CharField(max_length=50, default='pending')
    # ... other fields

# Configure workflow
ct = ContentType.objects.get_for_model(PurchaseRequest)
config = WorkflowConfiguration.objects.get(content_type=ct)
config.status_field = 'status'
config.completion_status_value = 'approved'
config.rejection_status_value = 'denied'
config.save()

# Workflow behavior:
# - All approvals completed → purchase_request.status = 'approved'
# - Finance rejects → purchase_request.status = 'denied'
```

### How It Works

When a workflow completes or is rejected, the system:

1. **Looks up the configuration** for your model's ContentType
2. **Checks if status_field is configured** (e.g., "status")
3. **Verifies the field exists** on your model
4. **Updates the field value**:
   - On completion: Sets `completion_status_value`
   - On rejection: Sets `rejection_status_value`
5. **Saves the change** using `update_fields` for efficiency
6. **Logs the update** for audit trail

### Optional Configuration

Status updates are completely optional and backward compatible:

```python
# No status field configured → No automatic updates
config.status_field = ''  # Not configured

# Status field configured but no completion value → No update on completion
config.status_field = 'status'
config.completion_status_value = ''  # Won't update on completion
config.rejection_status_value = 'rejected'  # Will update on rejection

# Full configuration → Updates on both events
config.status_field = 'status'
config.completion_status_value = 'completed'
config.rejection_status_value = 'rejected'
```

### Logging and Audit Trail

All status updates are logged:

```python
# Info log on successful update
logger.info(
    "Updated Opportunity(123).status from 'in_progress' to 'won' on workflow completion"
)

# Warning if field doesn't exist
logger.warning(
    "Model Opportunity does not have field 'status', cannot update status on completion"
)

# Debug if not configured
logger.debug(
    "No status_field configured for Opportunity, skipping status update"
)
```

### Django Admin Configuration

You can also configure status values via Django Admin:

1. Go to **Django Admin** → **Workflow Configurations**
2. Select your model's configuration
3. Set the fields:
   - **Status field**: The field name (e.g., "status")
   - **Completion status value**: Value on workflow completion (e.g., "completed")
   - **Rejection status value**: Value on workflow rejection (e.g., "rejected")
4. Save

### Migration

After upgrading to v1.2.7, run the migration:

```bash
python manage.py migrate django_workflow_engine
```

This adds the `completion_status_value` and `rejection_status_value` fields to `WorkflowConfiguration`.

### Best Practices

1. **Use consistent status values** across your application
2. **Define status choices** in your model for validation:
   ```python
   class Opportunity(models.Model):
       STATUS_CHOICES = [
           ('draft', 'Draft'),
           ('in_progress', 'In Progress'),
           ('won', 'Won'),
           ('lost', 'Lost'),
       ]
       status = models.CharField(max_length=50, choices=STATUS_CHOICES)
   ```

3. **Log status changes** for audit purposes (handled automatically)
4. **Test your workflow** to ensure status transitions work as expected
5. **Consider using signals** if you need additional logic on status change

## Custom Actions

The Django Workflow Engine supports powerful custom actions that execute automatically at key workflow events. Actions can send emails, update external systems, create tasks, log events, and more.

### Quick Example
```python
# myapp/workflow_actions.py
def send_approval_notification(context, parameters=None):
    """Send email when stage is approved"""
    attachment = context['attachment']
    user = context.get('user')

    recipients = parameters.get('recipients', [])

    send_mail(
        subject=f"Stage '{attachment.current_stage.name_en}' Approved",
        message=f"Approved by {user.get_full_name()}",
        from_email='noreply@company.com',
        recipient_list=recipients,
    )

    return {"email_sent": True}

# Register in Django Admin or code:
from django_workflow_engine.models import WorkflowAction
from django_workflow_engine.choices import ActionType

WorkflowAction.objects.create(
    stage_id=1,  # Specific stage
    action_type=ActionType.AFTER_APPROVE,
    function_path='myapp.workflow_actions.send_approval_notification',
    parameters={'recipients': ['manager@company.com']},
    order=1,
    is_active=True
)
```

### Action Types & Timing

| Action Type | When Triggered | Use For |
|-------------|----------------|---------|
| `AFTER_APPROVE` | After stage approval | Approval notifications, logging |
| `AFTER_MOVE_STAGE` | After moving to next stage | Status updates, task creation |
| `AFTER_MOVE_PIPELINE` | After moving to next pipeline | Role changes, permissions |
| `ON_WORKFLOW_START` | When workflow starts | Initial setup, notifications |
| `ON_WORKFLOW_COMPLETE` | When workflow finishes | Final actions, cleanup |
| `AFTER_REJECT` | After rejection | Rejection handling |
| `AFTER_RESUBMISSION` | After resubmission | Resubmission handling |

### Action Execution Order (Conflict Prevention)

The system prevents conflicts by executing actions in a specific order:

```
1. AFTER_APPROVE          ← Approval completed (sees current stage)
2. AFTER_MOVE_PIPELINE    ← Pipeline transition (if needed)
3. AFTER_MOVE_STAGE       ← Stage transition (sees new stage)
4. Start next stage approval flow
```

### Available Role Selection Strategies

When configuring role-based approvals:

```python
from approval_workflow.choices import RoleSelectionStrategy

# Available strategies:
'anyone'       # Any user with the role can approve
'consensus'    # ALL users with the role must approve
'round_robin'  # Rotate approval among role users
```

### 📚 Comprehensive Guides

- **Custom Actions**: For complete documentation including advanced examples, conflict resolution, and best practices, see: **[CUSTOM_ACTIONS_README.md](CUSTOM_ACTIONS_README.md)**
- **Approval Types**: For detailed information on approval behavior types (APPROVE, SUBMIT, CHECK_IN_VERIFY, MOVE), see: **[APPROVAL_TYPE_INTEGRATION_GUIDE.md](APPROVAL_TYPE_INTEGRATION_GUIDE.md)**

## Complete Example: Purchase Request Workflow

This example demonstrates a complete workflow from A to Z with 2 pipelines and multiple stages.

### Scenario: Purchase Request Process
- **Pipeline 1 (Finance Department)**: Initial Review → Budget Approval → Final Finance Sign-off
- **Pipeline 2 (Management)**: Executive Approval

### Step 1: Setup Models

```python
# models.py
from django.db import models
from django.contrib.auth.models import User

class PurchaseRequest(models.Model):
    title = models.CharField(max_length=200)
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    description = models.TextField()
    requester = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)

    # Workflow fields
    workflow_status = models.CharField(max_length=50, default='pending')
    current_stage = models.CharField(max_length=100, blank=True)

    def __str__(self):
        return f"Purchase Request: {self.title} - ${self.amount}"
```

### Step 2: Register Model for Workflow

```python
# apps.py or management command
from django_workflow_engine.services import register_model_for_workflow
from .models import PurchaseRequest

register_model_for_workflow(
    PurchaseRequest,
    auto_start=True,
    status_field='workflow_status'
    # Note: No stage_field needed - use get_current_stage(instance) helper instead
)
```

### Step 3: Create Workflow Structure Using Serializers

```python
# Create via API serializers (recommended) or Django Admin
from django_workflow_engine.serializers import WorkFlowSerializer, StageSerializer
from django_workflow_engine.models import WorkFlow, Pipeline, Stage
from rest_framework.request import Request

# 1. Create Workflow with Pipelines using WorkFlowSerializer
workflow_data = {
    'name_en': 'Purchase Request Approval',
    'name_ar': 'موافقة طلب الشراء',
    'company': 1,
    'is_active': True,
    'pipelines': [
        {
            'name_en': 'Finance Review',
            'name_ar': 'مراجعة مالية',
            'department_id': 1,  # Finance Department
            'order': 1,
            'number_of_stages': 3  # Will auto-create 3 stages
        },
        {
            'name_en': 'Executive Approval',
            'name_ar': 'موافقة تنفيذية',
            'department_id': 2,  # Management Department
            'order': 2,
            'number_of_stages': 1  # Will auto-create 1 stage
        }
    ]
}

# Create workflow with auto-generated stages
context = {'request': request, 'company_user': company_instance}
workflow_serializer = WorkFlowSerializer(data=workflow_data, context=context)
if workflow_serializer.is_valid():
    result = workflow_serializer.save()  # Returns workflow with pipelines and stages
    purchase_workflow = WorkFlow.objects.get(id=result['id'])

# 2. Configure Stage Approvals and Forms
# Now configure each stage with approval requirements, roles, and forms
from django_workflow_engine.serializers import StageSerializer
from django_workflow_engine.choices import ApprovalTypes
from approval_workflow.choices import RoleSelectionStrategy

# Get the auto-created stages
finance_pipeline = purchase_workflow.pipelines.get(name_en='Finance Review')
executive_pipeline = purchase_workflow.pipelines.get(name_en='Executive Approval')

# Configure Finance Stage 1: Initial Review
initial_review = finance_pipeline.stages.get(order=1)
stage_config = {
    'name_en': 'Initial Review',
    'name_ar': 'المراجعة الأولية',
    'pipeline_id': finance_pipeline.id,
    'stage_info': {
        'color': '#3498db',
        'approvals': [
            {
                'approval_type': ApprovalTypes.ROLE,
                'user_role': 1,  # Finance Reviewer Role ID
                'role_selection_strategy': RoleSelectionStrategy.ROUND_ROBIN,
                'required_form': 1  # Initial Review Form ID
            }
        ]
    }
}

stage_serializer = StageSerializer(initial_review, data=stage_config, partial=True)
if stage_serializer.is_valid():
    stage_serializer.save()

# Configure Finance Stage 2: Budget Approval
budget_approval = finance_pipeline.stages.get(order=2)
stage_config = {
    'name_en': 'Budget Approval',
    'name_ar': 'موافقة الميزانية',
    'pipeline_id': finance_pipeline.id,
    'stage_info': {
        'color': '#f39c12',
        'approvals': [
            {
                'approval_type': ApprovalTypes.ROLE,
                'user_role': 2,  # Budget Manager Role ID
                'role_selection_strategy': RoleSelectionStrategy.ANYONE,
                'required_form': 2  # Budget Approval Form ID
            }
        ]
    }
}

stage_serializer = StageSerializer(budget_approval, data=stage_config, partial=True)
if stage_serializer.is_valid():
    stage_serializer.save()

# Configure Finance Stage 3: Final Finance Sign-off
finance_signoff = finance_pipeline.stages.get(order=3)
stage_config = {
    'name_en': 'Final Finance Sign-off',
    'name_ar': 'الموافقة المالية النهائية',
    'pipeline_id': finance_pipeline.id,
    'stage_info': {
        'color': '#27ae60',
        'approvals': [
            {
                'approval_type': ApprovalTypes.USER,
                'approval_user': 123,  # CFO User ID
                'required_form': 3  # Final Approval Form ID
            }
        ]
    }
}

stage_serializer = StageSerializer(finance_signoff, data=stage_config, partial=True)
if stage_serializer.is_valid():
    stage_serializer.save()

# Configure Executive Stage: Executive Approval
executive_approval = executive_pipeline.stages.get(order=1)
stage_config = {
    'name_en': 'Executive Approval',
    'name_ar': 'موافقة تنفيذية',
    'pipeline_id': executive_pipeline.id,
    'stage_info': {
        'color': '#8e44ad',
        'approvals': [
            {
                'approval_type': ApprovalTypes.ROLE,
                'user_role': 3,  # Executive Role ID
                'role_selection_strategy': RoleSelectionStrategy.CONSENSUS
                # No required_form - executives can approve without additional forms
            }
        ]
    }
}

stage_serializer = StageSerializer(executive_approval, data=stage_config, partial=True)
if stage_serializer.is_valid():
    stage_serializer.save()
```

### Step 4: Start Workflow (A → Z Process)

```python
# views.py
from django_workflow_engine.services import attach_workflow_to_object

def create_purchase_request(request):
    # Create purchase request
    purchase_request = PurchaseRequest.objects.create(
        title=request.POST['title'],
        amount=request.POST['amount'],
        description=request.POST['description'],
        requester=request.user
    )

    # Attach and start workflow
    attachment = attach_workflow_to_object(
        obj=purchase_request,
        workflow=purchase_workflow,
        user=request.user,
        auto_start=True,
        metadata={
            'amount': float(purchase_request.amount),
            'priority': 'normal',
            'department': 'finance'
        }
    )

    # At this point:
    # - Purchase request is at "Initial Review" stage
    # - Current pipeline: Finance Review
    # - Status: "in_progress"

    return purchase_request
```

### Step 5: Progress Through Workflow

```python
# Helper function to get current stage (replaces stage_field dependency)
from django_workflow_engine.services import get_current_stage, get_workflow_attachment

def get_current_stage_info(purchase_request):
    """Get current stage information for purchase request"""
    attachment = get_workflow_attachment(purchase_request)
    if attachment:
        return {
            'current_stage': attachment.current_stage,
            'current_pipeline': attachment.current_pipeline,
            'stage_name': attachment.current_stage.name_en if attachment.current_stage else None,
            'pipeline_name': attachment.current_pipeline.name_en if attachment.current_pipeline else None
        }
    return None

# Use WorkflowApprovalSerializer (based on existing CRM implementation)
from django_workflow_engine.serializers import WorkflowApprovalSerializer

# FINANCE PIPELINE - STAGE 1: Initial Review
def approve_initial_review(request, purchase_request_id):
    purchase_request = PurchaseRequest.objects.get(id=purchase_request_id)

    # Check current stage
    stage_info = get_current_stage_info(purchase_request)
    print(f"Current stage: {stage_info['stage_name']} in {stage_info['pipeline_name']}")

    serializer = WorkflowApprovalSerializer(
        instance=purchase_request,  # Use instance, not object_instance
        data={
            'action': 'APPROVED',  # Use ApprovalStatus choices
            'form_data': {
                'reviewer_comment': 'Initial review passed - budget code verified',
                'budget_code': 'BDG-2024-001'
            }
        },
        context={'request': request}
    )

    if serializer.is_valid():
        serializer.save()
        # ✅ Automatically moves to: Finance Pipeline → Budget Approval stage

# FINANCE PIPELINE - STAGE 2: Budget Approval
def approve_budget(request, purchase_request_id):
    purchase_request = PurchaseRequest.objects.get(id=purchase_request_id)

    serializer = WorkflowApprovalSerializer(
        instance=purchase_request,
        data={
            'action': 'APPROVED',
            'form_data': {
                'budget_manager_comment': 'Budget approved - sufficient funds available',
                'allocated_budget': '50000.00'
            }
        },
        context={'request': request}
    )

    if serializer.is_valid():
        serializer.save()
        # ✅ Automatically moves to: Finance Pipeline → Final Finance Sign-off stage

# FINANCE PIPELINE - STAGE 3: Final Finance Sign-off
def final_finance_approval(request, purchase_request_id):
    purchase_request = PurchaseRequest.objects.get(id=purchase_request_id)

    serializer = WorkflowApprovalSerializer(
        instance=purchase_request,
        data={
            'action': 'APPROVED',
            'form_data': {
                'cfo_comment': 'Financially approved - ready for executive review',
                'finance_ref': 'FIN-2024-PR-001'
            }
        },
        context={'request': request}
    )

    if serializer.is_valid():
        serializer.save()
        # ✅ PIPELINE TRANSITION: Finance → Management Pipeline

# MANAGEMENT PIPELINE - STAGE 1: Executive Approval
def executive_approval(request, purchase_request_id):
    purchase_request = PurchaseRequest.objects.get(id=purchase_request_id)

    serializer = WorkflowApprovalSerializer(
        instance=purchase_request,
        data={
            'action': 'APPROVED',
            # No form_data required for executive approval (as configured)
        },
        context={'request': request}
    )

    if serializer.is_valid():
        serializer.save()
        # ✅ WORKFLOW COMPLETED!
        # Status automatically changes to: "completed"
```

### Step 6: Handle Rejections and Special Cases

```python
# Reject workflow
def reject_budget_approval(request, purchase_request_id):
    purchase_request = PurchaseRequest.objects.get(id=purchase_request_id)

    serializer = WorkflowApprovalSerializer(
        instance=purchase_request,
        data={
            'action': 'REJECTED',  # Use ApprovalStatus.REJECTED
            'reason': 'Insufficient budget allocation for this quarter'
        },
        context={'request': request}
    )

    if serializer.is_valid():