Skip to content

Approval Workflows

Netforce provides a powerful and flexible approval workflow system that allows you to create multi-step approval processes for any business document. The system supports complex routing based on amount thresholds, user roles, and custom criteria.

Overview

The approval workflow system consists of three main models:

  • approve.wkf - Workflow definitions with routing rules
  • approve.wkf.step - Individual workflow steps with assigned approvers
  • approval - Approval requests and their status

Core Components

1. Workflow Definition (approve.wkf)

The workflow model defines the approval process and routing criteria:

class ApprovalWorkflow(Model):
    _name = "approve.wkf"
    _fields = {
        "name": fields.Char("Workflow Name"),
        "model_id": fields.Many2One("model", "Target Model"),
        "user_id": fields.Many2One("base.user", "Specific User"),
        "group_id": fields.Many2One("user.group", "User Group"),
        "min_amount": fields.Decimal("Minimum Amount"),
        "max_amount": fields.Decimal("Maximum Amount"),
        "priority": fields.Integer("Priority"),
        "steps": fields.One2Many("approve.wkf.step", "wkf_id", "Steps"),
        "company_id": fields.Many2One("company", "Company"),
    }

2. Workflow Steps (approve.wkf.step)

Individual steps define the approval sequence:

class ApprovalWorkflowStep(Model):
    _name = "approve.wkf.step"
    _fields = {
        "wkf_id": fields.Many2One("approve.wkf", "Workflow"),
        "sequence": fields.Integer("Step Sequence"),
        "approve_user_id": fields.Many2One("base.user", "Approver"),
        "company_id": fields.Many2One("company", "Company"),
    }
    _order = "sequence, id"

3. Approval Requests (approval)

Active approval requests track the approval status:

class Approval(Model):
    _name = "approval"
    _fields = {
        "date": fields.DateTime("Request Date"),
        "approve_date": fields.DateTime("Approve Date"),
        "user_id": fields.Many2One("base.user", "Approver"),
        "related_id": fields.Reference([
            ["purchase.order", "Purchase Order"],
            ["expense.application", "Expense Application"]
        ], "Related Document"),
        "state": fields.Selection([
            ["pending", "Awaiting Approval"],
            ["approved", "Approved"],
            ["rejected", "Rejected"],
            ["cancelled", "Cancelled"]
        ], "Status"),
        "wkf_step_id": fields.Many2One("approve.wkf.step", "Step"),
        "company_id": fields.Many2One("company", "Company"),
    }

Implementing Approval Workflows

1. Add Approval Support to Your Model

First, add approval-related fields to your model:

class PurchaseOrder(Model):
    _name = "purchase.order"
    _fields = {
        # ... existing fields ...
        "state": fields.Selection([
            ["draft", "Draft"],
            ["wait_approve", "Awaiting Approval"],
            ["approved", "Approved"],
            ["done", "Completed"]
        ], "Status"),
        "approvals": fields.One2Many("approval", "related_id", "Approvals"),
        "date_approve": fields.DateTime("Approved Date"),
    }

2. Implement Approval Methods

Add methods to handle the approval process:

def submit_for_approval(self, ids, context={}):
    """Submit document for approval"""
    for obj in self.browse(ids):
        if obj.state != "draft":
            raise Exception("Can only submit draft documents")

        # Update state
        obj.write({"state": "wait_approve"})

        # Find applicable workflow
        wkf_id = get_model("approve.wkf").find_wkf(
            model_name="purchase.order",
            user_id=obj.user_id.id,
            company_id=obj.company_id.id,
            amount=obj.amount_total
        )

        if not wkf_id:
            # No workflow found, auto-approve
            self.auto_approve([obj.id])
            return

        # Create approval requests for first step
        wkf = get_model("approve.wkf").browse(wkf_id)
        first_steps = [s for s in wkf.steps if s.sequence == 1]

        for step in first_steps:
            get_model("approval").create({
                "related_id": f"{self._name},{obj.id}",
                "user_id": step.approve_user_id.id,
                "state": "pending",
                "wkf_step_id": step.id,
            })

def approve(self, ids, context={}):
    """Approve document"""
    user_id = get_active_user()

    for obj in self.browse(ids):
        # Find pending approval for current user
        pending_approval = None
        for approval in obj.approvals:
            if (approval.state == "pending" and 
                approval.user_id.id == user_id):
                pending_approval = approval
                break

        if not pending_approval:
            raise Exception("No pending approval found for current user")

        # Mark approval as approved
        pending_approval.write({
            "state": "approved",
            "approve_date": time.strftime("%Y-%m-%d %H:%M:%S")
        })

        # Check if all approvals are complete
        self._check_approval_complete([obj.id])

def _check_approval_complete(self, ids):
    """Check if approval process is complete"""
    for obj in self.browse(ids):
        current_step = self._get_current_approval_step(obj)

        if not current_step:
            # All approvals complete
            obj.write({
                "state": "approved",
                "date_approve": time.strftime("%Y-%m-%d %H:%M:%S")
            })
            continue

        # Check if current step is complete
        pending_count = 0
        for approval in obj.approvals:
            if (approval.wkf_step_id.sequence == current_step and
                approval.state == "pending"):
                pending_count += 1

        if pending_count == 0:
            # Current step complete, create next step approvals
            self._create_next_step_approvals(obj, current_step)

def reject(self, ids, context={}):
    """Reject document"""
    user_id = get_active_user()

    for obj in self.browse(ids):
        # Find and reject user's approval
        for approval in obj.approvals:
            if (approval.state == "pending" and 
                approval.user_id.id == user_id):
                approval.write({
                    "state": "rejected",
                    "approve_date": time.strftime("%Y-%m-%d %H:%M:%S")
                })

        # Cancel all other pending approvals
        for approval in obj.approvals:
            if approval.state == "pending":
                approval.write({"state": "cancelled"})

        obj.write({"state": "draft"})

Workflow Configuration

1. Setting up Workflows

Create workflows through the UI or programmatically:

# Create workflow for purchase orders > $10,000
workflow_id = get_model("approve.wkf").create({
    "name": "PO High Value Approval",
    "model_id": get_model("model").search([["name", "=", "purchase.order"]])[0],
    "min_amount": 10000,
    "company_id": 1,
    "priority": 1
})

# Add approval steps
get_model("approve.wkf.step").create({
    "wkf_id": workflow_id,
    "sequence": 1,
    "approve_user_id": manager_user_id,
})

get_model("approve.wkf.step").create({
    "wkf_id": workflow_id,
    "sequence": 2,
    "approve_user_id": director_user_id,
})

2. Workflow Selection Logic

The system selects workflows based on multiple criteria:

def find_workflow(model_name, user_id, company_id, amount=0):
    """Find the most appropriate workflow"""
    workflows = get_model("approve.wkf").search_browse([
        ["model_id.name", "=", model_name]
    ])

    for wkf in workflows:
        # Check company
        if wkf.company_id.id != company_id:
            continue

        # Check user restrictions
        if wkf.user_id and wkf.user_id.id != user_id:
            continue

        # Check group restrictions
        if wkf.group_id:
            user_in_group = user_id in [u.id for u in wkf.group_id.users]
            if not user_in_group:
                continue

        # Check amount thresholds
        if wkf.min_amount and amount < wkf.min_amount:
            continue
        if wkf.max_amount and amount > wkf.max_amount:
            continue

        return wkf

    return None

Advanced Patterns

1. Conditional Approval Steps

Create dynamic approval paths based on document properties:

def get_approval_steps(self, obj):
    """Get approval steps based on document conditions"""
    steps = []

    # Always require manager approval
    steps.append({"user_id": obj.department_id.manager_id.id, "sequence": 1})

    # High-value items need director approval
    if obj.amount_total > 50000:
        steps.append({"user_id": get_director_user(), "sequence": 2})

    # IT purchases need IT manager approval
    if obj.category == "IT":
        steps.append({"user_id": get_it_manager(), "sequence": 2})

    return steps

2. Parallel vs Sequential Approval

# Sequential: Each step waits for previous to complete
steps = [
    {"sequence": 1, "user_id": manager_id},      # Step 1: Manager
    {"sequence": 2, "user_id": director_id},     # Step 2: Director (after manager)
]

# Parallel: Multiple approvers in same step
steps = [
    {"sequence": 1, "user_id": manager_id},      # Step 1: Both approvers
    {"sequence": 1, "user_id": finance_mgr_id},  # Step 1: must approve
]

3. Auto-Approval Rules

Implement automatic approval for certain conditions:

def check_auto_approval(self, obj):
    """Check if document can be auto-approved"""
    # Auto-approve small amounts
    if obj.amount_total < 1000:
        return True

    # Auto-approve for certain suppliers
    trusted_suppliers = ["SUPP001", "SUPP002"]
    if obj.supplier_id.code in trusted_suppliers:
        return True

    # Auto-approve recurring orders
    if obj.is_recurring:
        return True

    return False

UI Integration

1. Form Buttons

Add approval buttons to your form layouts:

<form model="purchase.order">
    <top>
        <button string="Submit for Approval" 
                method="submit_for_approval" 
                states="draft" 
                icon="upload"/>
        <button string="Approve" 
                method="approve" 
                states="wait_approve" 
                icon="check"
                attrs='{"invisible":[["can_approve","=",false]]}'/>
        <button string="Reject" 
                method="reject" 
                states="wait_approve" 
                icon="times" 
                confirm="Reject this document?"/>
    </top>

    <field name="approvals" nolabel="1">
        <list>
            <field name="user_id"/>
            <field name="date"/>
            <field name="state"/>
            <field name="approve_date"/>
        </list>
    </field>
</form>

2. Approval Dashboard

Create dashboard widgets for pending approvals:

def get_pending_approvals(self, user_id):
    """Get pending approvals for user"""
    approvals = get_model("approval").search_browse([
        ["user_id", "=", user_id],
        ["state", "=", "pending"]
    ])

    return [{
        "id": app.id,
        "document": app.related_id._string,
        "reference": app.related_id.number if hasattr(app.related_id, 'number') else '',
        "date": app.date,
        "amount": getattr(app.related_id, 'amount_total', 0)
    } for app in approvals]

Email Notifications

Integrate with email system to notify approvers:

def send_approval_notification(self, approval_id):
    """Send email notification to approver"""
    approval = get_model("approval").browse(approval_id)

    if not approval.user_id.email:
        return

    # Get document details
    doc = approval.related_id

    get_model("email.message").create({
        "to": approval.user_id.email,
        "subject": f"Approval Required: {doc._string} {doc.number}",
        "body": f"""
        <p>You have a document pending approval:</p>
        <ul>
            <li><strong>Document:</strong> {doc._string}</li>
            <li><strong>Number:</strong> {doc.number}</li>
            <li><strong>Amount:</strong> {getattr(doc, 'amount_total', 'N/A')}</li>
            <li><strong>Requested By:</strong> {doc.user_id.name}</li>
        </ul>
        <p>Please log in to review and approve.</p>
        """,
        "type": "out"
    })

Best Practices

1. Error Handling

def approve(self, ids, context={}):
    try:
        # Approval logic
        pass
    except Exception as e:
        # Log error
        get_model("log").create({
            "level": "error",
            "message": f"Approval failed: {str(e)}",
            "model": self._name,
            "record_id": ids[0] if ids else None
        })
        raise

2. Audit Trail

Keep detailed approval history:

def create_audit_log(self, action, approval_id, notes=None):
    """Create audit log entry"""
    approval = get_model("approval").browse(approval_id)

    get_model("audit.log").create({
        "action": action,
        "user_id": get_active_user(),
        "date": time.strftime("%Y-%m-%d %H:%M:%S"),
        "model": "approval",
        "record_id": approval_id,
        "related_model": approval.related_id._model,
        "related_id": approval.related_id.id,
        "notes": notes
    })

3. Performance Optimization

Use indexes and efficient queries:

# Add database indexes for common queries
_sql_constraints = [
    ("approval_user_state_idx", "index (user_id, state)", ""),
    ("approval_related_idx", "index (related_id)", ""),
]

# Use efficient queries
def get_user_pending_count(self, user_id):
    """Get count of pending approvals efficiently"""
    res = database.get("SELECT COUNT(*) FROM approval WHERE user_id=%s AND state='pending'", [user_id])
    return res[0][0] if res else 0

Next Steps