🔍 Code Extractor

function complete_approval

Maturity: 77

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.

File:
/tf/active/vicechatdev/CDocs/controllers/approval_controller.py
Lines:
792 - 988
Complexity:
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

  • logging
  • uuid
  • os
  • typing
  • datetime
  • traceback
  • CDocs.db
  • CDocs.config.settings
  • CDocs.config.permissions
  • CDocs.models.document
  • CDocs.models.approval
  • CDocs.models.user_extensions
  • CDocs.utils.audit_trail
  • CDocs.utils.notifications
  • CDocs.controllers
  • CDocs.controllers.share_controller
  • CDocs.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

Optional

Usage 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

Similar Components

AI-powered semantic similarity - components with related functionality:

  • function complete_approval_v1 90.5% similar

    Records a user's approval decision (APPROVED or REJECTED) for a document in an approval cycle, updating the approval status and document state accordingly.

    From: /tf/active/vicechatdev/CDocs/controllers/approval_controller_bis.py
  • function complete_review 86.8% similar

    Completes a document review cycle by submitting a reviewer's decision (APPROVED/REJECTED), updating review status, managing sequential review workflows, and triggering notifications.

    From: /tf/active/vicechatdev/CDocs/controllers/review_controller.py
  • function close_approval_cycle_v1 80.4% similar

    Administratively closes an approval cycle by setting a final decision (APPROVED or REJECTED), updating the associated document status, and notifying relevant stakeholders.

    From: /tf/active/vicechatdev/CDocs/controllers/approval_controller_bis.py
  • function close_approval_cycle 77.0% similar

    Closes a completed approval cycle and optionally updates the associated document's status, with permission checks, audit logging, and notifications.

    From: /tf/active/vicechatdev/CDocs/controllers/approval_controller.py
  • function create_approval_cycle 76.3% similar

    Creates a new approval cycle for a document, assigning approvers with configurable workflow options (sequential/parallel), instructions, and due dates.

    From: /tf/active/vicechatdev/CDocs/controllers/approval_controller_bis.py
← Back to Browse