function complete_approval
Completes an approval cycle by recording a user's approval decision (APPROVED, REJECTED, etc.) and managing the approval workflow, including sequential approver activation and final cycle completion.
/tf/active/vicechatdev/CDocs/controllers/approval_controller.py
792 - 988
complex
Purpose
This function handles the complete workflow for submitting an approval decision within a document approval cycle. It validates permissions, checks business rules, records the decision, manages sequential approval progression, determines overall approval outcomes when all approvers complete their tasks, sends notifications to relevant parties, and updates document permissions. It serves as the core controller for the approval decision-making process in a document management system.
Source Code
def complete_approval(
user: DocUser,
approval_uid: str,
decision: str,
comments: Optional[str] = None
) -> Dict[str, Any]:
"""
Complete a approval by submitting a decision.
Args:
user: User completing the approval
approval_uid: UID of approval cycle
decision: Decision (APPROVED, REJECTED, etc.)
comments: Optional comments about the decision
Returns:
Dictionary with approval completion details
Raises:
ResourceNotFoundError: If approval cycle not found
ValidationError: If validation fails
PermissionError: If user doesn't have permission
BusinessRuleError: If a business rule is violated
"""
try:
# Direct permission check at the beginning
logger.info(f"Checking if user {user.uid} has COMPLETE_APPROVAL permission")
if not permissions.user_has_permission(user, "COMPLETE_APPROVAL"):
logger.warning(f"Permission denied: User {user.uid} attempted to complete approval without COMPLETE_APPROVAL permission")
raise PermissionError("You do not have permission to complete approvals")
logger.info(f"User {user.uid} has permission to complete approval {approval_uid}")
# Get approval cycle instance
approval_cycle = ApprovalCycle(uid=approval_uid)
if not approval_cycle:
raise ResourceNotFoundError(f"Approval cycle not found: {approval_uid}")
# Check if approval status allows completion
if approval_cycle.status not in ["PENDING", "IN_PROGRESS"]:
raise BusinessRuleError(f"Cannot complete approval with status {approval_cycle.status}")
# Validate decision
if decision not in settings.APPROVAL_DECISIONS:
raise ValidationError(f"Invalid approval decision: {decision}")
# Check if user is a approver for this cycle
if not approval_cycle.is_approver(user.uid):
raise PermissionError("User is not a approver for this approval cycle")
# Get approver assignment
assignment = approval_cycle.get_approver_assignment(user.uid)
if not assignment:
raise ResourceNotFoundError(f"Approver assignment not found for user {user.uid}")
# Check if already completed
if assignment.status == "COMPLETED":
raise BusinessRuleError("This approver has already completed their approval")
# Update assignment
assignment.status = "COMPLETED"
assignment.decision = decision
assignment.decision_date = datetime.now()
assignment.decision_comments = comments
# Update first activity date if not already set
if not assignment.first_activity_date:
assignment.first_activity_date = datetime.now()
# Save assignment to database
assignment.save()
# Check for sequential approval progression
if approval_cycle.sequential:
# Find the next approver in sequence
next_approver = approval_cycle.get_next_approver(assignment.sequence_order)
if next_approver:
# Activate next approver
next_approver.status = "ACTIVE"
next_approver.save()
# Notify next approver
notifications.send_notification(
notification_type="APPROVE_ACTIVATED",
users=[next_approver.approver_uid],
resource_uid=approval_cycle.uid,
resource_type="ApprovalCycle",
message="Your turn to approval a document",
details={
"approval_uid": approval_uid,
"previous_approver": user.name
},
send_email=True,
email_template="approval_activated"
)
# Check if all approvers have completed their approval
assignments = approval_cycle.get_approver_assignments()
all_completed = all(a.status == "COMPLETED" for a in assignments)
# Get document
document = ControlledDocument(uid=approval_cycle.document_uid)
# If all completed, update approval cycle status
if all_completed:
# Calculate approval statistics
approved_count = sum(1 for a in assignments if a.status == "COMPLETED" and a.decision == "APPROVED")
total_count = len(assignments)
approval_percentage = (approved_count / total_count) * 100 if total_count > 0 else 0
# Determine overall result
overall_decision = "APPROVED" if approval_percentage >= approval_cycle.required_approval_percentage else "REJECTED"
# Update approval cycle
approval_cycle.status = "COMPLETED"
approval_cycle.completion_date = datetime.now()
approval_cycle.decision = overall_decision
approval_cycle.save()
# Log approval completion event
audit_trail.log_approval_event(
event_type="APPROVE_CYCLE_COMPLETED",
user=user,
approval_uid=approval_uid,
details={
"decision": overall_decision,
"approval_percentage": approval_percentage,
"approved_count": approved_count,
"total_approvers": total_count
}
)
# If document status is IN_APPROVE and approval was successful, update status
# Removed - this should be done by the document owner.
# if document and document.status.upper() == "IN_APPROVE" and overall_decision == "APPROVED":
# # Update to APPROVED status if approval was successful
# from CDocs.controllers.document_controller import update_document
# update_document(
# user=user,
# document_uid=document.uid,
# status="DRAFT"
# )
# Notify document owner
if document and document.owner_uid:
notifications.send_notification(
notification_type="APPROVE_COMPLETED",
users=[document.owner_uid],
resource_uid=approval_cycle.uid,
resource_type="ApprovalCycle",
message=f"Approval cycle for {document.doc_number} completed with result: {overall_decision}",
details={
"approval_uid": approval_uid,
"document_uid": document.uid,
"doc_number": document.doc_number,
"title": document.title,
"decision": overall_decision,
"approval_percentage": approval_percentage
},
send_email=True,
email_template="approval_completed"
)
else:
# Log individual approval decision
audit_trail.log_approval_event(
event_type="APPROVEER_DECISION_SUBMITTED",
user=user,
approval_uid=approval_uid,
details={
"decision": decision,
"approver_uid": user.uid,
"comments": comments
}
)
# Update sharing permissions for document based on new approval cycle
from CDocs.controllers.share_controller import manage_document_permissions
document = ControlledDocument(uid=document.uid)
permission_result = manage_document_permissions(document)
return {
"success": True,
"approval_uid": approval_uid,
"assignment": assignment.to_dict(),
"decision": decision,
"approval_completed": all_completed,
"message": "Approval decision submitted successfully"
}
except (ResourceNotFoundError, ValidationError, PermissionError, BusinessRuleError) as e:
# Re-raise known errors
raise
except Exception as e:
logger.error(f"Error completing approval: {e}")
raise BusinessRuleError(f"Failed to complete approval: {e}")
Parameters
| Name | Type | Default | Kind |
|---|---|---|---|
user |
DocUser | - | positional_or_keyword |
approval_uid |
str | - | positional_or_keyword |
decision |
str | - | positional_or_keyword |
comments |
Optional[str] | None | positional_or_keyword |
Parameter Details
user: DocUser object representing the user submitting the approval decision. Must have COMPLETE_APPROVAL permission and be an assigned approver for the specified approval cycle.
approval_uid: String containing the unique identifier (UID) of the approval cycle being completed. Used to retrieve the ApprovalCycle instance from the database.
decision: String representing the approval decision. Must be one of the valid values defined in settings.APPROVAL_DECISIONS (e.g., 'APPROVED', 'REJECTED'). This is the approver's final decision on the document.
comments: Optional string containing the approver's comments or justification for their decision. Can be None if no comments are provided. Stored in the assignment record for audit purposes.
Return Value
Type: Dict[str, Any]
Returns a dictionary with keys: 'success' (boolean, always True on successful completion), 'approval_uid' (string, the UID of the approval cycle), 'assignment' (dictionary representation of the updated ApproverAssignment), 'decision' (string, the submitted decision), 'approval_completed' (boolean, True if all approvers have completed their reviews), and 'message' (string, success confirmation message).
Dependencies
logginguuidostypingdatetimetracebackCDocs.dbCDocs.config.settingsCDocs.config.permissionsCDocs.models.documentCDocs.models.approvalCDocs.models.user_extensionsCDocs.utils.audit_trailCDocs.utils.notificationsCDocs.controllersCDocs.controllers.share_controllerCDocs.db.db_operations
Required Imports
from typing import Dict, Any, Optional
from datetime import datetime
from CDocs.models.user_extensions import DocUser
from CDocs.models.approval import ApprovalCycle, ApproverAssignment
from CDocs.models.document import ControlledDocument
from CDocs.config import settings, permissions
from CDocs.utils import audit_trail, notifications
from CDocs.controllers import PermissionError, ResourceNotFoundError, ValidationError, BusinessRuleError, log_controller_action
from CDocs.controllers.share_controller import manage_document_permissions
import logging
Conditional/Optional Imports
These imports are only needed under specific conditions:
from CDocs.controllers.document_controller import update_document
Condition: Only needed if uncommenting the code that automatically updates document status to DRAFT after approval
OptionalUsage Example
from CDocs.models.user_extensions import DocUser
from CDocs.controllers.approval_controller import complete_approval
# Get the user completing the approval
user = DocUser(uid='user123')
# Complete the approval with a decision
try:
result = complete_approval(
user=user,
approval_uid='approval-cycle-456',
decision='APPROVED',
comments='Document meets all quality standards and requirements.'
)
if result['success']:
print(f"Approval decision submitted: {result['decision']}")
if result['approval_completed']:
print("All approvers have completed their reviews.")
else:
print("Waiting for other approvers to complete.")
except PermissionError as e:
print(f"Permission denied: {e}")
except ValidationError as e:
print(f"Invalid input: {e}")
except BusinessRuleError as e:
print(f"Business rule violation: {e}")
Best Practices
- Always ensure the user has COMPLETE_APPROVAL permission before calling this function
- Validate that the decision value matches one of the allowed values in settings.APPROVAL_DECISIONS
- Handle all four exception types (ResourceNotFoundError, ValidationError, PermissionError, BusinessRuleError) when calling this function
- The function is decorated with @log_controller_action('complete_approval'), so all calls are automatically logged
- For sequential approvals, only the current active approver can submit a decision; the next approver is automatically activated
- The function automatically calculates approval percentages and determines overall approval outcomes based on required_approval_percentage
- Document permissions are automatically updated after approval completion via manage_document_permissions
- Email notifications are sent to next approvers (in sequential mode) and document owners (on completion)
- The function updates multiple database records (assignment, approval cycle, potentially next approver) in a single operation
- Comments are optional but recommended for audit trail purposes, especially for rejections
- Once an approver completes their assignment, they cannot submit another decision (checked via assignment.status == 'COMPLETED')
- The approval cycle must be in PENDING or IN_PROGRESS status to accept new decisions
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
function complete_approval_v1 90.5% similar
-
function complete_review 86.8% similar
-
function close_approval_cycle_v1 80.4% similar
-
function close_approval_cycle 77.0% similar
-
function create_approval_cycle 76.3% similar