from __future__ import annotations from decimal import Decimal from mousehold_domain import ( FinancialEventStatus, PaymentLineStatus, PaymentStatus, PlanningStatus, ReconciliationStatus, SettlementStatus, ) from sqlalchemy import select from sqlalchemy.orm import Session from .. import models from ..schemas import ProjectSummary def _money(value: Decimal | int | str = "0.00") -> Decimal: return Decimal(value).quantize(Decimal("0.01")) def calculate_project_summary(session: Session, project_id: str) -> ProjectSummary: project = session.get(models.Project, project_id) if not project: raise ValueError("Project not found") budget_lines = list( session.scalars( select(models.ProjectBudgetLine).where( models.ProjectBudgetLine.project_id == project_id ) ).all() ) planned_items = list( session.scalars( select(models.ProjectPlannedItem).where( models.ProjectPlannedItem.project_id == project_id ) ).all() ) payment_lines = list( session.scalars( select(models.PaymentLine) .join( models.ProjectPlannedItem, models.PaymentLine.planned_item_id == models.ProjectPlannedItem.id, ) .where(models.ProjectPlannedItem.project_id == project_id) ).all() ) linked_events = list( session.scalars( select(models.FinancialEvent).where(models.FinancialEvent.project_id == project_id) ).all() ) budgeted_total = _money(sum((line.budgeted_amount for line in budget_lines), Decimal("0.00"))) committed_total = _money( sum( ( item.committed_amount if item.committed_amount is not None else item.estimated_amount if item.planning_status == PlanningStatus.COMMITTED else Decimal("0.00") ) for item in planned_items ) ) paid_total = _money( sum( ( line.amount for line in payment_lines if line.status in {PaymentLineStatus.PAID, PaymentLineStatus.RECONCILED} ), Decimal("0.00"), ) + sum( ( event.amount for event in linked_events if event.status in { FinancialEventStatus.CONFIRMED, FinancialEventStatus.BOOKED, FinancialEventStatus.SETTLED, } ), Decimal("0.00"), ) ) reconciled_total = _money( sum( (line.amount for line in payment_lines if line.status == PaymentLineStatus.RECONCILED), Decimal("0.00"), ) ) settled_total = _money( sum( ( item.committed_amount or item.estimated_amount for item in planned_items if item.settlement_status == SettlementStatus.SETTLED ), Decimal("0.00"), ) ) due_later_total = _money(max(Decimal("0.00"), committed_total - paid_total)) remaining_uncommitted_budget = _money(budgeted_total - committed_total) open_actions: list[str] = [] for item in planned_items: expected = item.committed_amount or item.estimated_amount if item.planning_status == PlanningStatus.COMMITTED and item.payment_status in { PaymentStatus.UNPAID, PaymentStatus.PARTIALLY_PAID, }: open_actions.append(f"{item.description} still has payment due.") if item.reconciliation_status == ReconciliationStatus.NO_EVIDENCE and expected > 0: open_actions.append(f"{item.description} needs evidence.") return ProjectSummary( project_id=project.id, currency=project.currency, budgeted_total=budgeted_total, committed_total=committed_total, paid_total=paid_total, reconciled_total=reconciled_total, settled_total=settled_total, due_later_total=due_later_total, remaining_uncommitted_budget=remaining_uncommitted_budget, open_actions=open_actions, )