first ruggedy draft

This commit is contained in:
2026-06-28 13:48:15 +02:00
parent c916f64fcb
commit 1053f6a2a3
78 changed files with 20905 additions and 2 deletions

View File

@@ -0,0 +1,136 @@
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,
)