refactor(core,ai): 调整模块导入路径并移除废弃文件

- 修复 scheduler.py 中的导入路径错误,将 reports.service
  改为 reports.services
- 移除废弃的 app/core/background/task_queue.py 文件
- 移除废弃的 app/modules/ai_agent/adapters.py 文件
- 修复 ai_memory/service.py 中的导入路径错误,将
  events.service 改为 events.services
```
This commit is contained in:
2026-07-09 18:50:52 +08:00
parent dc8605ce3f
commit bf309ecdf7
83 changed files with 4871 additions and 4153 deletions

View File

@@ -0,0 +1,15 @@
from app.modules.reports.services.lifecycle.filters import ReportLifecycleFilterMixin
from app.modules.reports.services.lifecycle.health import ReportLifecycleHealthMixin
from app.modules.reports.services.lifecycle.rendering import ReportLifecycleRenderingMixin
from app.modules.reports.services.lifecycle.report import ReportLifecycleReportMixin
from app.modules.reports.services.lifecycle.stats import ReportLifecycleStatsMixin
class ReportLifecycleMixin(
ReportLifecycleReportMixin,
ReportLifecycleRenderingMixin,
ReportLifecycleHealthMixin,
ReportLifecycleStatsMixin,
ReportLifecycleFilterMixin,
):
"""Project lifecycle report composition and supporting calculations."""

View File

@@ -0,0 +1,87 @@
from datetime import date, datetime
from typing import Any
from sqlalchemy import or_
from app.modules.business.models import (
AttendanceRecord,
Expense,
Procurement,
Project,
RiskEvent,
WorkTask,
)
from app.modules.reports.constants import (
LifecycleFilterKey,
LifecycleSection,
)
class ReportLifecycleFilterMixin:
def _lifecycle_filters(
self,
project_code: str | None,
owner: str | None,
period_start: date | None,
period_end: date | None,
) -> dict[str, Any]:
project_conditions: list[Any] = []
task_conditions: list[Any] = []
procurement_conditions: list[Any] = []
expense_conditions: list[Any] = []
attendance_conditions: list[Any] = []
risk_conditions: list[Any] = []
labels = {
LifecycleFilterKey.PROJECT_CODE: project_code,
LifecycleFilterKey.OWNER: owner,
LifecycleFilterKey.PERIOD_START: period_start.isoformat() if period_start else None,
LifecycleFilterKey.PERIOD_END: period_end.isoformat() if period_end else None,
}
if project_code:
project_conditions.append(Project.code == project_code)
task_conditions.append(WorkTask.project_code == project_code)
procurement_conditions.append(Procurement.project_code == project_code)
expense_conditions.append(Expense.project_code == project_code)
attendance_conditions.append(AttendanceRecord.project_code == project_code)
risk_conditions.append(RiskEvent.project_code == project_code)
if owner:
project_conditions.append(Project.owner == owner)
task_conditions.append(WorkTask.owner == owner)
risk_conditions.append(RiskEvent.owner == owner)
if period_start:
project_conditions.append(
or_(Project.due_date.is_(None), Project.due_date >= period_start)
)
task_conditions.append(WorkTask.due_date >= period_start)
attendance_conditions.append(AttendanceRecord.work_date >= period_start)
risk_conditions.append(
RiskEvent.detected_at >= datetime.combine(period_start, datetime.min.time())
)
if period_end:
project_conditions.append(
or_(Project.start_date.is_(None), Project.start_date <= period_end)
)
task_conditions.append(WorkTask.due_date <= period_end)
attendance_conditions.append(AttendanceRecord.work_date <= period_end)
risk_conditions.append(
RiskEvent.detected_at <= datetime.combine(period_end, datetime.max.time())
)
if period_start:
start_at = datetime.combine(period_start, datetime.min.time())
procurement_conditions.append(Procurement.created_at >= start_at)
expense_conditions.append(Expense.created_at >= start_at)
if period_end:
end_at = datetime.combine(period_end, datetime.max.time())
procurement_conditions.append(Procurement.created_at <= end_at)
expense_conditions.append(Expense.created_at <= end_at)
return {
LifecycleFilterKey.LABELS: labels,
LifecycleSection.PROJECTS: project_conditions,
LifecycleSection.TASKS: task_conditions,
LifecycleSection.PROCUREMENTS: procurement_conditions,
LifecycleSection.EXPENSES: expense_conditions,
LifecycleSection.ATTENDANCE: attendance_conditions,
LifecycleSection.RISKS: risk_conditions,
}

View File

@@ -0,0 +1,138 @@
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]

View File

@@ -0,0 +1,97 @@
from typing import Any
from app.modules.reports.constants import (
LifecycleResponseKey,
LifecycleSection,
MetricKey,
ReportText,
)
from app.modules.reports.services.common import _json_safe, _money
class ReportLifecycleRenderingMixin:
def _lifecycle_lines(
self,
filters: dict[str, Any],
metrics: dict[str, Any],
recommendations: list[str],
) -> list[str]:
scope = (
"".join(f"{key}={value}" for key, value in filters.items() if value)
or ReportText.DEFAULT_SCOPE
)
projects = metrics[LifecycleSection.PROJECTS]
tasks = metrics[LifecycleSection.TASKS]
procurements = metrics[LifecycleSection.PROCUREMENTS]
expenses = metrics[LifecycleSection.EXPENSES]
funds = metrics[LifecycleSection.FUNDS]
suppliers = metrics[LifecycleSection.SUPPLIERS]
attendance = metrics[LifecycleSection.ATTENDANCE]
risks = metrics[LifecycleSection.RISKS]
health = metrics[LifecycleSection.HEALTH]
lines = [
f"- 范围:{scope}",
f"- 生命周期健康分:{health[MetricKey.SCORE]}{health[MetricKey.LEVEL]}",
(
f"- 项目:总数 {projects[MetricKey.TOTAL]}"
f"活跃 {projects[MetricKey.ACTIVE]}"
f"平均进度 {projects[MetricKey.AVERAGE_PROGRESS_PERCENT]}%"
),
(
f"- 成本:预算 {_money(projects[MetricKey.BUDGET_TOTAL])}"
f"实际 {_money(projects[MetricKey.ACTUAL_TOTAL])}"
f"预算使用率 {projects[MetricKey.BUDGET_USAGE_RATE]}%"
),
(
f"- 任务:总数 {tasks[MetricKey.TOTAL]}"
f"完成 {tasks[MetricKey.COMPLETED]}"
f"完成率 {tasks[MetricKey.COMPLETION_RATE]}%"
f"逾期 {tasks[MetricKey.OVERDUE]}"
),
(
f"- 采购/费用:待批采购 {procurements[MetricKey.PENDING_APPROVAL]}"
f"待批费用 {expenses[MetricKey.PENDING_APPROVAL]}"
f"未付款采购 {procurements[MetricKey.UNPAID]}"
),
(
f"- 资金:余额 {_money(funds[MetricKey.CURRENT_BALANCE_TOTAL])}"
f"净头寸 {_money(funds[MetricKey.NET_POSITION])}"
f"风险账户 {funds[MetricKey.RISK_ACCOUNTS]}"
),
(
f"- 风险:等级 {risks[MetricKey.RISK_LEVEL]}"
f"风险分 {risks[MetricKey.RISK_SCORE]}"
f"延期项目 {risks[MetricKey.DELAYED_PROJECTS]}"
f"超预算项目 {risks[MetricKey.OVER_BUDGET_PROJECTS]}"
f"打开事件 {risks[MetricKey.OPEN_EVENTS]}"
),
(
f"- 供应商/考勤:风险供应商 {suppliers[MetricKey.RISKY]}"
f"异常打卡 {attendance[MetricKey.ABNORMAL]}"
f"异常率 {attendance[MetricKey.ABNORMAL_RATE]}%"
),
ReportText.ACTION_HEADER,
]
lines.extend(f" - {item}" for item in recommendations)
return lines
def _lifecycle_ai_analysis(self, report: dict[str, Any], actor: str) -> dict[str, Any]:
try:
from app.modules.ai_agent.constants import AIResponseKey
from app.modules.ai_agent.skills import AISkillId
from app.modules.ai_agent.service import AIService
report_snapshot = _json_safe(report)
result = AIService(self.db).run_skill(
AISkillId.PROJECT_LIFECYCLE_ANALYSIS,
context={LifecycleResponseKey.PROJECT_LIFECYCLE_REPORT: report_snapshot},
actor=actor,
)
except Exception as exc:
return {
AIResponseKey.OK: False,
AIResponseKey.ERROR: str(exc),
AIResponseKey.TYPE: type(exc).__name__,
}
return _json_safe({AIResponseKey.OK: True, **result})

View File

@@ -0,0 +1,94 @@
from datetime import date
from typing import Any
from app.core.constants import ActorValue
from app.modules.reports.constants import (
LifecycleFilterKey,
LifecycleResponseKey,
LifecycleSection,
ReportTitle,
)
from app.modules.reports.services.common import _json_safe
class ReportLifecycleReportMixin:
def project_lifecycle_report(
self,
project_code: str | None = None,
owner: str | None = None,
period_start: date | None = None,
period_end: date | None = None,
include_ai: bool = False,
actor: str = ActorValue.API,
) -> dict[str, Any]:
"""Build a full lifecycle report for project progress, delivery, cost, and risk."""
filters = self._lifecycle_filters(project_code, owner, period_start, period_end)
project_conditions = filters[LifecycleSection.PROJECTS]
task_conditions = filters[LifecycleSection.TASKS]
procurement_conditions = filters[LifecycleSection.PROCUREMENTS]
expense_conditions = filters[LifecycleSection.EXPENSES]
attendance_conditions = filters[LifecycleSection.ATTENDANCE]
risk_conditions = filters[LifecycleSection.RISKS]
project_stats = self._lifecycle_project_stats(project_conditions)
task_stats = self._lifecycle_task_stats(task_conditions)
procurement_stats = self._lifecycle_procurement_stats(procurement_conditions)
expense_stats = self._lifecycle_expense_stats(expense_conditions)
fund_stats = self._lifecycle_fund_stats()
supplier_stats = self._lifecycle_supplier_stats()
attendance_stats = self._lifecycle_attendance_stats(attendance_conditions)
risk_stats = self._lifecycle_risk_stats(
project_conditions,
task_conditions,
risk_conditions,
)
include_global_risk = not (project_code or owner)
health = self._lifecycle_health(
project_stats,
task_stats,
risk_stats,
supplier_stats,
include_global_risk,
)
attention = self._lifecycle_attention(project_conditions, task_conditions)
recommendations = self._lifecycle_recommendations(
project_stats,
task_stats,
procurement_stats,
expense_stats,
fund_stats,
supplier_stats,
risk_stats,
include_global_risk,
)
metrics = {
LifecycleSection.HEALTH: health,
LifecycleSection.PROJECTS: project_stats,
LifecycleSection.TASKS: task_stats,
LifecycleSection.PROCUREMENTS: procurement_stats,
LifecycleSection.EXPENSES: expense_stats,
LifecycleSection.FUNDS: fund_stats,
LifecycleSection.SUPPLIERS: supplier_stats,
LifecycleSection.ATTENDANCE: attendance_stats,
LifecycleSection.RISKS: risk_stats,
}
lines = self._lifecycle_lines(
filters[LifecycleFilterKey.LABELS],
metrics,
recommendations,
)
report = {
LifecycleResponseKey.TITLE: ReportTitle.PROJECT_LIFECYCLE,
LifecycleResponseKey.FILTERS: filters[LifecycleFilterKey.LABELS],
LifecycleResponseKey.METRICS: _json_safe(metrics),
LifecycleResponseKey.ATTENTION: _json_safe(attention),
LifecycleResponseKey.RECOMMENDATIONS: recommendations,
LifecycleResponseKey.LINES: lines,
LifecycleResponseKey.CONTENT: "\n".join(lines),
}
if include_ai:
report[LifecycleResponseKey.AI_ANALYSIS] = self._lifecycle_ai_analysis(report, actor)
return report

View File

@@ -0,0 +1,270 @@
from datetime import date
from typing import Any
from app.modules.business.constants import (
ATTENDANCE_ABNORMAL_STATUSES,
DONE_STATUSES,
GENERATED_RISK_EVENT_TYPES,
PROJECT_CLOSED_STATUSES,
PENDING_APPROVAL_STATUSES,
SUPPLIER_RISK_LEVELS,
RiskLevel,
StatusValue,
)
from app.modules.business.models import (
AttendanceRecord,
Expense,
FundAccount,
Procurement,
Project,
RiskEvent,
Supplier,
WorkTask,
)
from app.modules.reports.constants import (
LIFECYCLE_RISK_SCORE_WEIGHTS,
MetricKey,
)
from app.modules.reports.services.common import _rate
from app.modules.risk.constants import risk_level_for_score
class ReportLifecycleStatsMixin:
def _lifecycle_project_stats(self, conditions: list[Any]) -> dict[str, Any]:
total = self._count(Project, *conditions)
active = self._count(Project, Project.status.notin_(PROJECT_CLOSED_STATUSES), *conditions)
closed = total - active
budget_total = self._sum(Project.budget_amount, *conditions)
actual_total = self._sum(Project.actual_amount, *conditions)
delayed = self._count(
Project,
Project.due_date.is_not(None),
Project.due_date < date.today(),
Project.status.notin_(PROJECT_CLOSED_STATUSES),
*conditions,
)
over_budget = self._count(
Project,
Project.budget_amount > 0,
Project.actual_amount > Project.budget_amount,
*conditions,
)
return {
MetricKey.TOTAL: total,
MetricKey.ACTIVE: active,
MetricKey.CLOSED: closed,
MetricKey.AVERAGE_PROGRESS_PERCENT: self._avg(
Project.progress_percent,
*conditions,
),
MetricKey.BY_STATUS: self._group_counts(Project, Project.status, *conditions),
MetricKey.BY_RISK_LEVEL: self._group_counts(Project, Project.risk_level, *conditions),
MetricKey.BUDGET_TOTAL: budget_total,
MetricKey.ACTUAL_TOTAL: actual_total,
MetricKey.BUDGET_USAGE_RATE: _rate(actual_total, budget_total),
MetricKey.DELAYED: delayed,
MetricKey.OVER_BUDGET: over_budget,
}
def _lifecycle_task_stats(self, conditions: list[Any]) -> dict[str, Any]:
total = self._count(WorkTask, *conditions)
completed = self._count(WorkTask, WorkTask.status.in_(DONE_STATUSES), *conditions)
overdue = self._count(
WorkTask,
WorkTask.due_date.is_not(None),
WorkTask.due_date < date.today(),
WorkTask.status.notin_(DONE_STATUSES),
*conditions,
)
blocked = self._count(
WorkTask,
WorkTask.blocker.is_not(None),
WorkTask.status.notin_(DONE_STATUSES),
*conditions,
)
return {
MetricKey.TOTAL: total,
MetricKey.COMPLETED: completed,
MetricKey.OPEN: total - completed,
MetricKey.OVERDUE: overdue,
MetricKey.BLOCKED: blocked,
MetricKey.COMPLETION_RATE: _rate(completed, total),
MetricKey.BY_STATUS: self._group_counts(WorkTask, WorkTask.status, *conditions),
MetricKey.BY_PRIORITY: self._group_counts(WorkTask, WorkTask.priority, *conditions),
}
def _lifecycle_procurement_stats(self, conditions: list[Any]) -> dict[str, Any]:
total = self._count(Procurement, *conditions)
pending_approval = self._count(
Procurement,
Procurement.approval_status.in_(PENDING_APPROVAL_STATUSES),
*conditions,
)
pending_delivery = self._count(
Procurement,
Procurement.delivery_status.notin_(DONE_STATUSES),
*conditions,
)
unpaid = self._count(
Procurement,
Procurement.payment_status != StatusValue.PAID,
*conditions,
)
expected_total = self._sum(Procurement.expected_amount, *conditions)
actual_total = self._sum(Procurement.actual_amount, *conditions)
return {
MetricKey.TOTAL: total,
MetricKey.PENDING_APPROVAL: pending_approval,
MetricKey.PENDING_DELIVERY: pending_delivery,
MetricKey.UNPAID: unpaid,
MetricKey.EXPECTED_TOTAL: expected_total,
MetricKey.ACTUAL_TOTAL: actual_total,
MetricKey.ACTUAL_VS_EXPECTED_RATE: _rate(actual_total, expected_total),
}
def _lifecycle_expense_stats(self, conditions: list[Any]) -> dict[str, Any]:
total = self._count(Expense, *conditions)
pending_approval = self._count(
Expense,
Expense.approval_status.in_(PENDING_APPROVAL_STATUSES),
*conditions,
)
unpaid = self._count(Expense, Expense.payment_status != StatusValue.PAID, *conditions)
return {
MetricKey.TOTAL: total,
MetricKey.PENDING_APPROVAL: pending_approval,
MetricKey.UNPAID: unpaid,
MetricKey.AMOUNT_TOTAL: self._sum(Expense.amount, *conditions),
MetricKey.BY_TYPE: self._group_counts(Expense, Expense.expense_type, *conditions),
}
def _lifecycle_fund_stats(self) -> dict[str, Any]:
balance = self._sum(FundAccount.current_balance)
receivable = self._sum(FundAccount.expected_receivable)
payable = self._sum(FundAccount.expected_payable)
safety_line = self._sum(FundAccount.safety_line)
risk_accounts = self._count(
FundAccount,
FundAccount.current_balance < FundAccount.safety_line,
)
return {
MetricKey.ACCOUNTS_TOTAL: self._count(FundAccount),
MetricKey.CURRENT_BALANCE_TOTAL: balance,
MetricKey.EXPECTED_RECEIVABLE_TOTAL: receivable,
MetricKey.EXPECTED_PAYABLE_TOTAL: payable,
MetricKey.SAFETY_LINE_TOTAL: safety_line,
MetricKey.NET_POSITION: balance + receivable - payable,
MetricKey.RISK_ACCOUNTS: risk_accounts,
}
def _lifecycle_supplier_stats(self) -> dict[str, Any]:
risky = self._count(
Supplier,
(Supplier.blacklist_status != StatusValue.NORMAL)
| Supplier.risk_level.in_(SUPPLIER_RISK_LEVELS),
)
blacklisted = self._count(Supplier, Supplier.blacklist_status != StatusValue.NORMAL)
return {
MetricKey.TOTAL: self._count(Supplier),
MetricKey.RISKY: risky,
MetricKey.BLACKLISTED: blacklisted,
MetricKey.BY_RISK_LEVEL: self._group_counts(Supplier, Supplier.risk_level),
}
def _lifecycle_attendance_stats(self, conditions: list[Any]) -> dict[str, Any]:
total = self._count(AttendanceRecord, *conditions)
abnormal = self._count(
AttendanceRecord,
AttendanceRecord.status.in_(ATTENDANCE_ABNORMAL_STATUSES),
*conditions,
)
return {
MetricKey.TOTAL: total,
MetricKey.ABNORMAL: abnormal,
MetricKey.ABNORMAL_RATE: _rate(abnormal, total),
MetricKey.BY_STATUS: self._group_counts(
AttendanceRecord,
AttendanceRecord.status,
*conditions,
),
}
def _lifecycle_risk_stats(
self,
project_conditions: list[Any],
task_conditions: list[Any],
risk_conditions: list[Any],
) -> dict[str, Any]:
overdue_tasks = self._count(
WorkTask,
WorkTask.due_date.is_not(None),
WorkTask.due_date < date.today(),
WorkTask.status.notin_(DONE_STATUSES),
*task_conditions,
)
delayed_projects = self._count(
Project,
Project.due_date.is_not(None),
Project.due_date < date.today(),
Project.status.notin_(PROJECT_CLOSED_STATUSES),
*project_conditions,
)
over_budget_projects = self._count(
Project,
Project.budget_amount > 0,
Project.actual_amount > Project.budget_amount,
*project_conditions,
)
open_events = self._count(
RiskEvent,
RiskEvent.status == StatusValue.OPEN,
*risk_conditions,
)
high_events = self._count(
RiskEvent,
RiskEvent.status == StatusValue.OPEN,
RiskEvent.risk_level == RiskLevel.HIGH,
*risk_conditions,
)
external_open_events = self._count(
RiskEvent,
RiskEvent.status == StatusValue.OPEN,
RiskEvent.risk_type.notin_(GENERATED_RISK_EVENT_TYPES),
*risk_conditions,
)
external_high_events = self._count(
RiskEvent,
RiskEvent.status == StatusValue.OPEN,
RiskEvent.risk_level == RiskLevel.HIGH,
RiskEvent.risk_type.notin_(GENERATED_RISK_EVENT_TYPES),
*risk_conditions,
)
risk_score = (
overdue_tasks * LIFECYCLE_RISK_SCORE_WEIGHTS[MetricKey.OVERDUE_TASKS]
+ delayed_projects * LIFECYCLE_RISK_SCORE_WEIGHTS[MetricKey.DELAYED_PROJECTS]
+ over_budget_projects * LIFECYCLE_RISK_SCORE_WEIGHTS[MetricKey.OVER_BUDGET_PROJECTS]
+ external_open_events * LIFECYCLE_RISK_SCORE_WEIGHTS[MetricKey.EXTERNAL_OPEN_EVENTS]
+ external_high_events * LIFECYCLE_RISK_SCORE_WEIGHTS[MetricKey.EXTERNAL_HIGH_EVENTS]
)
return {
MetricKey.RISK_LEVEL: risk_level_for_score(risk_score),
MetricKey.RISK_SCORE: risk_score,
MetricKey.OVERDUE_TASKS: overdue_tasks,
MetricKey.DELAYED_PROJECTS: delayed_projects,
MetricKey.OVER_BUDGET_PROJECTS: over_budget_projects,
MetricKey.OPEN_EVENTS: open_events,
MetricKey.HIGH_EVENTS: high_events,
MetricKey.EXTERNAL_OPEN_EVENTS: external_open_events,
MetricKey.EXTERNAL_HIGH_EVENTS: external_high_events,
MetricKey.EVENTS_BY_TYPE: self._group_counts(
RiskEvent,
RiskEvent.risk_type,
*risk_conditions,
),
MetricKey.EVENTS_BY_LEVEL: self._group_counts(
RiskEvent,
RiskEvent.risk_level,
*risk_conditions,
),
}