feat(groups): group views with subtotals for budget and month
budget_group_views composes SectionViews into grouped dataclasses with a combined total and the default open flag. month_group_views does the same with planned and applied totals. Group order, labels, and section-to-group mapping all come from the groups module. Refs #11 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
032c35c75e
commit
0c533d62ed
2 changed files with 84 additions and 1 deletions
|
|
@ -9,7 +9,15 @@ from enum import Enum
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from quartermaster.groups import (
|
||||
GROUP_DEFAULT_OPEN,
|
||||
GROUP_LABELS,
|
||||
Group,
|
||||
group_order,
|
||||
sections_in_group,
|
||||
)
|
||||
from quartermaster.models import (
|
||||
SECTION_LABELS,
|
||||
DebtTarget,
|
||||
Entry,
|
||||
Month,
|
||||
|
|
@ -48,6 +56,16 @@ class ZeroAmounts:
|
|||
applied: Decimal
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MonthGroupView:
|
||||
group: Group
|
||||
label: str
|
||||
default_open: bool
|
||||
sections: list[MonthSectionView]
|
||||
total_planned: Decimal
|
||||
total_applied: Decimal
|
||||
|
||||
|
||||
def valid_year_month(year_month: str) -> bool:
|
||||
return bool(YEAR_MONTH_RE.match(year_month))
|
||||
|
||||
|
|
@ -148,6 +166,28 @@ def month_zero(month: Month) -> ZeroAmounts:
|
|||
)
|
||||
|
||||
|
||||
def month_group_views(month: Month) -> list[MonthGroupView]:
|
||||
views: list[MonthGroupView] = []
|
||||
for group in group_order():
|
||||
sections = [
|
||||
section_view(month, s, SECTION_LABELS[s])
|
||||
for s in sections_in_group(group)
|
||||
]
|
||||
planned = sum((sv.total_planned for sv in sections), Decimal("0"))
|
||||
applied = sum((sv.total_applied for sv in sections), Decimal("0"))
|
||||
views.append(
|
||||
MonthGroupView(
|
||||
group=group,
|
||||
label=GROUP_LABELS[group],
|
||||
default_open=GROUP_DEFAULT_OPEN[group],
|
||||
sections=sections,
|
||||
total_planned=planned,
|
||||
total_applied=applied,
|
||||
)
|
||||
)
|
||||
return views
|
||||
|
||||
|
||||
def section_view(month: Month, section: Section, label: str) -> MonthSectionView:
|
||||
entries = [e for e in month.entries if e.section == section]
|
||||
entries.sort(key=lambda e: e.id)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,14 @@ from decimal import Decimal
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from quartermaster.models import DebtTarget, Entry, Section
|
||||
from quartermaster.groups import (
|
||||
GROUP_DEFAULT_OPEN,
|
||||
GROUP_LABELS,
|
||||
Group,
|
||||
group_order,
|
||||
sections_in_group,
|
||||
)
|
||||
from quartermaster.models import SECTION_LABELS, DebtTarget, Entry, Section
|
||||
|
||||
|
||||
def zero_tone(value: Decimal) -> str:
|
||||
|
|
@ -23,6 +30,15 @@ class SectionView:
|
|||
total: Decimal
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BudgetGroupView:
|
||||
group: Group
|
||||
label: str
|
||||
default_open: bool
|
||||
sections: list[SectionView]
|
||||
total: Decimal
|
||||
|
||||
|
||||
def list_entries(db: Session, section: Section) -> list[Entry]:
|
||||
stmt = select(Entry).where(Entry.section == section).order_by(Entry.id)
|
||||
return list(db.scalars(stmt))
|
||||
|
|
@ -32,6 +48,33 @@ def section_total(entries: list[Entry]) -> Decimal:
|
|||
return sum((e.amount for e in entries), Decimal("0"))
|
||||
|
||||
|
||||
def section_view(db: Session, section: Section) -> SectionView:
|
||||
entries = list_entries(db, section)
|
||||
return SectionView(
|
||||
section=section,
|
||||
label=SECTION_LABELS[section],
|
||||
entries=entries,
|
||||
total=section_total(entries),
|
||||
)
|
||||
|
||||
|
||||
def budget_group_views(db: Session) -> list[BudgetGroupView]:
|
||||
views: list[BudgetGroupView] = []
|
||||
for group in group_order():
|
||||
sections = [section_view(db, s) for s in sections_in_group(group)]
|
||||
total = sum((sv.total for sv in sections), Decimal("0"))
|
||||
views.append(
|
||||
BudgetGroupView(
|
||||
group=group,
|
||||
label=GROUP_LABELS[group],
|
||||
default_open=GROUP_DEFAULT_OPEN[group],
|
||||
sections=sections,
|
||||
total=total,
|
||||
)
|
||||
)
|
||||
return views
|
||||
|
||||
|
||||
def budget_zero(db: Session) -> Decimal:
|
||||
stmt = select(Entry)
|
||||
total_income = Decimal("0")
|
||||
|
|
|
|||
Loading…
Reference in a new issue