From 5a65b4c524fb1e35568641da44448ba4a1e181b5 Mon Sep 17 00:00:00 2001 From: archeious Date: Fri, 17 Apr 2026 12:06:37 -0600 Subject: [PATCH] feat(zero): compute zero-amount for budget and month budget_zero sums income minus every non-income section on the budget config. month_zero returns both Planned and Applied versions over a month's entries. zero_tone classifies a value as zero / positive / negative so templates can pick a colour. Refs #7 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/quartermaster/month_service.py | 24 ++++++++++++++++++++++++ src/quartermaster/service.py | 18 ++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/quartermaster/month_service.py b/src/quartermaster/month_service.py index 7c96387..c55e5bd 100644 --- a/src/quartermaster/month_service.py +++ b/src/quartermaster/month_service.py @@ -42,6 +42,12 @@ class MonthSectionView: total_applied: Decimal +@dataclass(frozen=True) +class ZeroAmounts: + planned: Decimal + applied: Decimal + + def valid_year_month(year_month: str) -> bool: return bool(YEAR_MONTH_RE.match(year_month)) @@ -124,6 +130,24 @@ def _rows(entries: list[MonthEntry]) -> list[MonthRow]: return [MonthRow(entry=e, state=deviation_state(e)) for e in entries] +def month_zero(month: Month) -> ZeroAmounts: + income_planned = Decimal("0") + income_applied = Decimal("0") + other_planned = Decimal("0") + other_applied = Decimal("0") + for entry in month.entries: + if entry.section == Section.income: + income_planned += entry.planned + income_applied += entry.applied + else: + other_planned += entry.planned + other_applied += entry.applied + return ZeroAmounts( + planned=(income_planned - other_planned).quantize(Decimal("0.01")), + applied=(income_applied - other_applied).quantize(Decimal("0.01")), + ) + + 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) diff --git a/src/quartermaster/service.py b/src/quartermaster/service.py index e5fb3ca..e6a97cc 100644 --- a/src/quartermaster/service.py +++ b/src/quartermaster/service.py @@ -9,6 +9,12 @@ from sqlalchemy.orm import Session from quartermaster.models import DebtTarget, Entry, Section +def zero_tone(value: Decimal) -> str: + if value == 0: + return "zero" + return "positive" if value > 0 else "negative" + + @dataclass(frozen=True) class SectionView: section: Section @@ -26,6 +32,18 @@ def section_total(entries: list[Entry]) -> Decimal: return sum((e.amount for e in entries), Decimal("0")) +def budget_zero(db: Session) -> Decimal: + stmt = select(Entry) + total_income = Decimal("0") + total_non_income = Decimal("0") + for entry in db.scalars(stmt): + if entry.section == Section.income: + total_income += entry.amount + else: + total_non_income += entry.amount + return (total_income - total_non_income).quantize(Decimal("0.01")) + + def add_entry(db: Session, section: Section, name: str, amount: Decimal) -> Entry: entry = Entry(section=section, name=name.strip(), amount=amount) db.add(entry)