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 ```
271 lines
10 KiB
Python
271 lines
10 KiB
Python
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,
|
|
),
|
|
}
|