Add AQMM dashboard for assurance engagement quality monitoring
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import and_, func, or_, select
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from app.modules.clients.models import Client
|
||||
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
||||
from app.modules.services.models import (
|
||||
ClientServiceSubscription,
|
||||
ClientServiceTaskInstance,
|
||||
EngagementClosureChecklist,
|
||||
ServiceCatalogue,
|
||||
)
|
||||
|
||||
|
||||
ASSURANCE_TYPE = "assurance"
|
||||
APPROVED_STATUSES = {"approved", "completed", "complete"}
|
||||
REVIEWED_STATUSES = {"reviewed", "approved", "completed", "complete", "not_required"}
|
||||
EVIDENCE_OK_STATUSES = {"accepted", "approved", "reviewed", "uploaded", "not_required"}
|
||||
CLOSURE_CLOSED_STATUSES = {"closed", "completed"}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AqmmScope:
|
||||
tenant_id: int | None
|
||||
branch_id: int | None
|
||||
own_user_id: int | None
|
||||
|
||||
|
||||
def _clean(value: Any) -> str:
|
||||
return str(value or "").strip()
|
||||
|
||||
|
||||
def _active_tenant_id(request, user) -> int | None:
|
||||
value = (
|
||||
request.session.get("active_tenant_id")
|
||||
or request.session.get("selected_tenant_id")
|
||||
or request.session.get("tenant_id")
|
||||
or getattr(user, "tenant_id", None)
|
||||
)
|
||||
return int(value) if value not in (None, "", 0, "0") else None
|
||||
|
||||
|
||||
def _active_branch_id(request, user, permissions: set[str]) -> int | None:
|
||||
value = request.session.get("active_branch_id")
|
||||
if value in (None, "", 0, "0"):
|
||||
if "clients.cross_branch" in permissions or "services.cross_branch" in permissions:
|
||||
return None
|
||||
branch_id = getattr(user, "branch_id", None)
|
||||
return int(branch_id) if branch_id not in (None, "", 0, "0") else None
|
||||
return int(value)
|
||||
|
||||
|
||||
def build_scope(db, request, user) -> AqmmScope:
|
||||
roles = set(get_user_roles(db, user.id))
|
||||
permissions = set(get_user_permissions(db, user.id))
|
||||
own_user_id = None
|
||||
if "clients.view.own_only" in permissions and "Firm Admin" not in roles:
|
||||
own_user_id = int(user.id)
|
||||
return AqmmScope(
|
||||
tenant_id=_active_tenant_id(request, user),
|
||||
branch_id=_active_branch_id(request, user, permissions),
|
||||
own_user_id=own_user_id,
|
||||
)
|
||||
|
||||
|
||||
def can_view_aqmm_dashboard(db, user) -> bool:
|
||||
roles = set(get_user_roles(db, user.id))
|
||||
permissions = set(get_user_permissions(db, user.id))
|
||||
if roles.intersection({"System Admin", "Firm Admin", "Partner", "Manager", "Branch Manager"}):
|
||||
return True
|
||||
return bool(permissions.intersection({"services.view", "clients.view", "audit.view"}))
|
||||
|
||||
|
||||
def _subscription_filters(scope: AqmmScope, financial_year: str = "", q: str = ""):
|
||||
filters = [ClientServiceSubscription.engagement_type == ASSURANCE_TYPE]
|
||||
if scope.tenant_id:
|
||||
filters.append(ClientServiceSubscription.tenant_id == scope.tenant_id)
|
||||
if scope.branch_id:
|
||||
filters.append(ClientServiceSubscription.branch_id == scope.branch_id)
|
||||
if financial_year:
|
||||
filters.append(ClientServiceSubscription.financial_year == financial_year)
|
||||
if scope.own_user_id:
|
||||
filters.append(
|
||||
or_(
|
||||
ClientServiceSubscription.assigned_partner_user_id == scope.own_user_id,
|
||||
ClientServiceSubscription.assigned_manager_user_id == scope.own_user_id,
|
||||
ClientServiceSubscription.assigned_staff_user_id == scope.own_user_id,
|
||||
ClientServiceSubscription.review_partner_user_id == scope.own_user_id,
|
||||
Client.partner_id == scope.own_user_id,
|
||||
)
|
||||
)
|
||||
if q:
|
||||
like = f"%{q.lower()}%"
|
||||
filters.append(
|
||||
or_(
|
||||
func.lower(Client.client_name).like(like),
|
||||
func.lower(Client.client_code).like(like),
|
||||
func.lower(ServiceCatalogue.service_name).like(like),
|
||||
func.lower(ServiceCatalogue.service_code).like(like),
|
||||
)
|
||||
)
|
||||
return filters
|
||||
|
||||
|
||||
def _count(db, stmt) -> int:
|
||||
return int(db.execute(stmt).scalar_one() or 0)
|
||||
|
||||
|
||||
def _subscription_id_subquery(scope: AqmmScope, financial_year: str = "", q: str = ""):
|
||||
return (
|
||||
select(ClientServiceSubscription.id)
|
||||
.join(Client, Client.id == ClientServiceSubscription.client_id)
|
||||
.join(ServiceCatalogue, ServiceCatalogue.id == ClientServiceSubscription.service_catalogue_id)
|
||||
.where(and_(*_subscription_filters(scope, financial_year, q)))
|
||||
.subquery()
|
||||
)
|
||||
|
||||
|
||||
def _task_count(db, subq, *extra_filters) -> int:
|
||||
return _count(
|
||||
db,
|
||||
select(func.count(ClientServiceTaskInstance.id)).where(
|
||||
ClientServiceTaskInstance.subscription_id.in_(select(subq.c.id)),
|
||||
ClientServiceTaskInstance.is_aqmm_task.is_(True),
|
||||
*extra_filters,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _subscription_task_counts(db, subscription_id: int) -> dict[str, int]:
|
||||
base = [ClientServiceTaskInstance.subscription_id == subscription_id, ClientServiceTaskInstance.is_aqmm_task.is_(True)]
|
||||
total = _count(db, select(func.count(ClientServiceTaskInstance.id)).where(*base))
|
||||
mandatory = _count(db, select(func.count(ClientServiceTaskInstance.id)).where(*base, ClientServiceTaskInstance.aqmm_mandatory.is_(True)))
|
||||
evidence_pending = _count(
|
||||
db,
|
||||
select(func.count(ClientServiceTaskInstance.id)).where(
|
||||
*base,
|
||||
ClientServiceTaskInstance.aqmm_evidence_required.is_(True),
|
||||
ClientServiceTaskInstance.evidence_status.notin_(list(EVIDENCE_OK_STATUSES)),
|
||||
),
|
||||
)
|
||||
manager_pending = _count(
|
||||
db,
|
||||
select(func.count(ClientServiceTaskInstance.id)).where(
|
||||
*base,
|
||||
ClientServiceTaskInstance.aqmm_manager_review_required.is_(True),
|
||||
ClientServiceTaskInstance.manager_review_status.notin_(list(REVIEWED_STATUSES)),
|
||||
),
|
||||
)
|
||||
partner_pending = _count(
|
||||
db,
|
||||
select(func.count(ClientServiceTaskInstance.id)).where(
|
||||
*base,
|
||||
ClientServiceTaskInstance.aqmm_partner_review_required.is_(True),
|
||||
ClientServiceTaskInstance.partner_review_status.notin_(list(REVIEWED_STATUSES)),
|
||||
),
|
||||
)
|
||||
review_partner_pending = _count(
|
||||
db,
|
||||
select(func.count(ClientServiceTaskInstance.id)).where(
|
||||
*base,
|
||||
ClientServiceTaskInstance.aqmm_review_partner_required.is_(True),
|
||||
ClientServiceTaskInstance.review_partner_review_status.notin_(list(REVIEWED_STATUSES)),
|
||||
),
|
||||
)
|
||||
completed = _count(
|
||||
db,
|
||||
select(func.count(ClientServiceTaskInstance.id)).where(
|
||||
*base,
|
||||
or_(
|
||||
ClientServiceTaskInstance.aqmm_status.in_(["completed", "complete", "approved"]),
|
||||
ClientServiceTaskInstance.status.in_(["completed", "complete", "done"]),
|
||||
),
|
||||
),
|
||||
)
|
||||
return {
|
||||
"total": total,
|
||||
"mandatory": mandatory,
|
||||
"completed": completed,
|
||||
"evidence_pending": evidence_pending,
|
||||
"manager_pending": manager_pending,
|
||||
"partner_pending": partner_pending,
|
||||
"review_partner_pending": review_partner_pending,
|
||||
}
|
||||
|
||||
|
||||
def build_aqmm_dashboard_payload(db, request, user, *, financial_year: str = "", q: str = "") -> dict[str, Any]:
|
||||
financial_year = _clean(financial_year)
|
||||
q = _clean(q)
|
||||
scope = build_scope(db, request, user)
|
||||
subq = _subscription_id_subquery(scope, financial_year, q)
|
||||
|
||||
total_assurance = _count(db, select(func.count()).select_from(subq))
|
||||
aqmm_approved = _count(
|
||||
db,
|
||||
select(func.count(ClientServiceSubscription.id)).where(
|
||||
ClientServiceSubscription.id.in_(select(subq.c.id)),
|
||||
ClientServiceSubscription.quality_workflow_status.in_(list(APPROVED_STATUSES)),
|
||||
),
|
||||
)
|
||||
aqmm_pending = max(total_assurance - aqmm_approved, 0)
|
||||
|
||||
kyc_pending = _count(
|
||||
db,
|
||||
select(func.count(ClientServiceSubscription.id)).where(
|
||||
ClientServiceSubscription.id.in_(select(subq.c.id)),
|
||||
ClientServiceSubscription.quality_kyc_status.notin_(list(APPROVED_STATUSES) + ["not_required"]),
|
||||
),
|
||||
)
|
||||
declaration_pending = _count(
|
||||
db,
|
||||
select(func.count(ClientServiceSubscription.id)).where(
|
||||
ClientServiceSubscription.id.in_(select(subq.c.id)),
|
||||
or_(
|
||||
ClientServiceSubscription.quality_independence_status.notin_(list(APPROVED_STATUSES) + ["not_required"]),
|
||||
ClientServiceSubscription.quality_conflict_status.notin_(list(APPROVED_STATUSES) + ["not_required"]),
|
||||
),
|
||||
),
|
||||
)
|
||||
letter_pending = _count(
|
||||
db,
|
||||
select(func.count(ClientServiceSubscription.id)).where(
|
||||
ClientServiceSubscription.id.in_(select(subq.c.id)),
|
||||
ClientServiceSubscription.quality_engagement_letter_status.notin_(list(APPROVED_STATUSES) + ["not_required"]),
|
||||
),
|
||||
)
|
||||
|
||||
aqmm_tasks_total = _task_count(db, subq)
|
||||
evidence_pending = _task_count(
|
||||
db,
|
||||
subq,
|
||||
ClientServiceTaskInstance.aqmm_evidence_required.is_(True),
|
||||
ClientServiceTaskInstance.evidence_status.notin_(list(EVIDENCE_OK_STATUSES)),
|
||||
)
|
||||
manager_review_pending = _task_count(
|
||||
db,
|
||||
subq,
|
||||
ClientServiceTaskInstance.aqmm_manager_review_required.is_(True),
|
||||
ClientServiceTaskInstance.manager_review_status.notin_(list(REVIEWED_STATUSES)),
|
||||
)
|
||||
partner_review_pending = _task_count(
|
||||
db,
|
||||
subq,
|
||||
ClientServiceTaskInstance.aqmm_partner_review_required.is_(True),
|
||||
ClientServiceTaskInstance.partner_review_status.notin_(list(REVIEWED_STATUSES)),
|
||||
)
|
||||
review_partner_pending = _task_count(
|
||||
db,
|
||||
subq,
|
||||
ClientServiceTaskInstance.aqmm_review_partner_required.is_(True),
|
||||
ClientServiceTaskInstance.review_partner_review_status.notin_(list(REVIEWED_STATUSES)),
|
||||
)
|
||||
rework_pending = _task_count(
|
||||
db,
|
||||
subq,
|
||||
ClientServiceTaskInstance.rework_status.in_(["requested", "pending", "rework_required"]),
|
||||
)
|
||||
|
||||
closure_closed = _count(
|
||||
db,
|
||||
select(func.count(EngagementClosureChecklist.id)).where(
|
||||
EngagementClosureChecklist.subscription_id.in_(select(subq.c.id)),
|
||||
EngagementClosureChecklist.closure_status.in_(list(CLOSURE_CLOSED_STATUSES)),
|
||||
),
|
||||
)
|
||||
closure_pending = max(total_assurance - closure_closed, 0)
|
||||
|
||||
engagement_rows = db.execute(
|
||||
select(ClientServiceSubscription, EngagementClosureChecklist)
|
||||
.join(Client, Client.id == ClientServiceSubscription.client_id)
|
||||
.join(ServiceCatalogue, ServiceCatalogue.id == ClientServiceSubscription.service_catalogue_id)
|
||||
.outerjoin(EngagementClosureChecklist, EngagementClosureChecklist.subscription_id == ClientServiceSubscription.id)
|
||||
.options(
|
||||
joinedload(ClientServiceSubscription.client),
|
||||
joinedload(ClientServiceSubscription.catalogue),
|
||||
joinedload(ClientServiceSubscription.assigned_partner),
|
||||
joinedload(ClientServiceSubscription.assigned_manager),
|
||||
joinedload(ClientServiceSubscription.assigned_staff),
|
||||
joinedload(ClientServiceSubscription.review_partner),
|
||||
)
|
||||
.where(and_(*_subscription_filters(scope, financial_year, q)))
|
||||
.order_by(ClientServiceSubscription.updated_at_utc.desc(), ClientServiceSubscription.id.desc())
|
||||
.limit(50)
|
||||
).all()
|
||||
|
||||
engagements = []
|
||||
for subscription, closure in engagement_rows:
|
||||
counts = _subscription_task_counts(db, subscription.id)
|
||||
blockers = []
|
||||
if subscription.quality_workflow_status not in APPROVED_STATUSES:
|
||||
blockers.append("AQMM acceptance pending")
|
||||
if counts["evidence_pending"]:
|
||||
blockers.append(f"Evidence pending: {counts['evidence_pending']}")
|
||||
if counts["manager_pending"]:
|
||||
blockers.append(f"Manager review pending: {counts['manager_pending']}")
|
||||
if counts["partner_pending"]:
|
||||
blockers.append(f"Partner review pending: {counts['partner_pending']}")
|
||||
if counts["review_partner_pending"]:
|
||||
blockers.append(f"Review partner review pending: {counts['review_partner_pending']}")
|
||||
if closure and closure.closure_status not in CLOSURE_CLOSED_STATUSES:
|
||||
blockers.append("Closure pending")
|
||||
engagements.append({
|
||||
"row": subscription,
|
||||
"closure": closure,
|
||||
"counts": counts,
|
||||
"blockers": blockers,
|
||||
})
|
||||
|
||||
pending_tasks = db.execute(
|
||||
select(ClientServiceTaskInstance)
|
||||
.options(joinedload(ClientServiceTaskInstance.subscription).joinedload(ClientServiceSubscription.client), joinedload(ClientServiceTaskInstance.subscription).joinedload(ClientServiceSubscription.catalogue))
|
||||
.where(
|
||||
ClientServiceTaskInstance.subscription_id.in_(select(subq.c.id)),
|
||||
ClientServiceTaskInstance.is_aqmm_task.is_(True),
|
||||
or_(
|
||||
and_(ClientServiceTaskInstance.aqmm_evidence_required.is_(True), ClientServiceTaskInstance.evidence_status.notin_(list(EVIDENCE_OK_STATUSES))),
|
||||
and_(ClientServiceTaskInstance.aqmm_manager_review_required.is_(True), ClientServiceTaskInstance.manager_review_status.notin_(list(REVIEWED_STATUSES))),
|
||||
and_(ClientServiceTaskInstance.aqmm_partner_review_required.is_(True), ClientServiceTaskInstance.partner_review_status.notin_(list(REVIEWED_STATUSES))),
|
||||
and_(ClientServiceTaskInstance.aqmm_review_partner_required.is_(True), ClientServiceTaskInstance.review_partner_review_status.notin_(list(REVIEWED_STATUSES))),
|
||||
ClientServiceTaskInstance.rework_status.in_(["requested", "pending", "rework_required"]),
|
||||
),
|
||||
)
|
||||
.order_by(ClientServiceTaskInstance.internal_target_date.asc().nulls_last(), ClientServiceTaskInstance.sequence_no.asc())
|
||||
.limit(50)
|
||||
).scalars().all()
|
||||
|
||||
return {
|
||||
"financial_year": financial_year,
|
||||
"q": q,
|
||||
"scope": scope,
|
||||
"kpis": {
|
||||
"total_assurance": total_assurance,
|
||||
"aqmm_approved": aqmm_approved,
|
||||
"aqmm_pending": aqmm_pending,
|
||||
"declaration_pending": declaration_pending,
|
||||
"kyc_pending": kyc_pending,
|
||||
"engagement_letter_pending": letter_pending,
|
||||
"aqmm_tasks_total": aqmm_tasks_total,
|
||||
"evidence_pending": evidence_pending,
|
||||
"manager_review_pending": manager_review_pending,
|
||||
"partner_review_pending": partner_review_pending,
|
||||
"review_partner_pending": review_partner_pending,
|
||||
"rework_pending": rework_pending,
|
||||
"closure_pending": closure_pending,
|
||||
"closure_closed": closure_closed,
|
||||
},
|
||||
"engagements": engagements,
|
||||
"pending_tasks": pending_tasks,
|
||||
}
|
||||
Reference in New Issue
Block a user