137 lines
4.2 KiB
Python
137 lines
4.2 KiB
Python
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,
|
|
)
|