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 rulesapprove.wkf.step- Individual workflow steps with assigned approversapproval- 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¶
- Learn about Security for permission-based approval routing
- Explore Background Jobs for automated approval reminders
- Check Multi-Company for company-specific workflows
- Review API Reference for workflow management endpoints