from datetime import date from typing import Any from app.modules.business.constants import ( DONE_STATUSES, PROJECT_CLOSED_STATUSES, ) from app.modules.business.models import ( Project, WorkTask, ) from app.modules.business.service import serialize_model from app.modules.reports.constants import ( ATTENTION_SCORE_THRESHOLD, HEALTH_PENALTY_WEIGHTS, HEALTH_SCORE_MAX, HEALTH_SCORE_MIN, HEALTHY_SCORE_THRESHOLD, HealthLevel, LifecycleAttentionKey, MetricKey, ReportText, ) class ReportLifecycleHealthMixin: def _lifecycle_health( self, projects: dict[str, Any], tasks: dict[str, Any], risks: dict[str, Any], suppliers: dict[str, Any], include_global_risk: bool, ) -> dict[str, Any]: penalty = ( risks[MetricKey.OVERDUE_TASKS] * HEALTH_PENALTY_WEIGHTS[MetricKey.OVERDUE_TASKS] + risks[MetricKey.DELAYED_PROJECTS] * HEALTH_PENALTY_WEIGHTS[MetricKey.DELAYED_PROJECTS] + risks[MetricKey.OVER_BUDGET_PROJECTS] * HEALTH_PENALTY_WEIGHTS[MetricKey.OVER_BUDGET_PROJECTS] + risks[MetricKey.EXTERNAL_HIGH_EVENTS] * HEALTH_PENALTY_WEIGHTS[MetricKey.EXTERNAL_HIGH_EVENTS] + max( HEALTH_SCORE_MIN, projects[MetricKey.BUDGET_USAGE_RATE] - HEALTH_SCORE_MAX, ) * HEALTH_PENALTY_WEIGHTS[MetricKey.BUDGET_USAGE_RATE] + (HEALTH_SCORE_MAX - tasks[MetricKey.COMPLETION_RATE]) * HEALTH_PENALTY_WEIGHTS[MetricKey.COMPLETION_RATE] ) if include_global_risk: penalty += suppliers[MetricKey.BLACKLISTED] * HEALTH_PENALTY_WEIGHTS[ MetricKey.BLACKLISTED ] score = max( HEALTH_SCORE_MIN, min(HEALTH_SCORE_MAX, round(HEALTH_SCORE_MAX - penalty, 2)), ) if score >= HEALTHY_SCORE_THRESHOLD: level = HealthLevel.HEALTHY elif score >= ATTENTION_SCORE_THRESHOLD: level = HealthLevel.ATTENTION else: level = HealthLevel.CRITICAL return {MetricKey.SCORE: score, MetricKey.LEVEL: level} def _lifecycle_attention( self, project_conditions: list[Any], task_conditions: list[Any], ) -> dict[str, Any]: delayed = self._records( Project, Project.due_date.is_not(None), Project.due_date < date.today(), Project.status.notin_(PROJECT_CLOSED_STATUSES), *project_conditions, limit=10, order_by=Project.due_date.asc(), ) over_budget = self._records( Project, Project.budget_amount > 0, Project.actual_amount > Project.budget_amount, *project_conditions, limit=10, order_by=Project.id.desc(), ) overdue_tasks = self._records( WorkTask, WorkTask.due_date.is_not(None), WorkTask.due_date < date.today(), WorkTask.status.notin_(DONE_STATUSES), *task_conditions, limit=10, order_by=WorkTask.due_date.asc(), ) return { LifecycleAttentionKey.DELAYED_PROJECTS: [serialize_model(item) for item in delayed], LifecycleAttentionKey.OVER_BUDGET_PROJECTS: [ serialize_model(item) for item in over_budget ], LifecycleAttentionKey.OVERDUE_TASKS: [ serialize_model(item) for item in overdue_tasks ], } def _lifecycle_recommendations( self, projects: dict[str, Any], tasks: dict[str, Any], procurements: dict[str, Any], expenses: dict[str, Any], funds: dict[str, Any], suppliers: dict[str, Any], risks: dict[str, Any], include_global_risk: bool, ) -> list[str]: recommendations: list[str] = [] if risks[MetricKey.DELAYED_PROJECTS]: recommendations.append(ReportText.RECOMMEND_DELAYED) if risks[MetricKey.OVER_BUDGET_PROJECTS]: recommendations.append(ReportText.RECOMMEND_OVER_BUDGET) if tasks[MetricKey.OVERDUE]: recommendations.append(ReportText.RECOMMEND_OVERDUE_TASKS) if ( procurements[MetricKey.PENDING_APPROVAL] or expenses[MetricKey.PENDING_APPROVAL] ): recommendations.append(ReportText.RECOMMEND_APPROVALS) if include_global_risk and funds[MetricKey.RISK_ACCOUNTS]: recommendations.append(ReportText.RECOMMEND_FUNDS) if include_global_risk and suppliers[MetricKey.RISKY]: recommendations.append(ReportText.RECOMMEND_SUPPLIERS) if not recommendations: recommendations.append(ReportText.RECOMMEND_STABLE) return [str(item) for item in recommendations]