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) <noreply@anthropic.com>
This commit is contained in:
parent
38c8921885
commit
5a65b4c524
2 changed files with 42 additions and 0 deletions
|
|
@ -42,6 +42,12 @@ class MonthSectionView:
|
||||||
total_applied: Decimal
|
total_applied: Decimal
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ZeroAmounts:
|
||||||
|
planned: Decimal
|
||||||
|
applied: Decimal
|
||||||
|
|
||||||
|
|
||||||
def valid_year_month(year_month: str) -> bool:
|
def valid_year_month(year_month: str) -> bool:
|
||||||
return bool(YEAR_MONTH_RE.match(year_month))
|
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]
|
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:
|
def section_view(month: Month, section: Section, label: str) -> MonthSectionView:
|
||||||
entries = [e for e in month.entries if e.section == section]
|
entries = [e for e in month.entries if e.section == section]
|
||||||
entries.sort(key=lambda e: e.id)
|
entries.sort(key=lambda e: e.id)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,12 @@ from sqlalchemy.orm import Session
|
||||||
from quartermaster.models import DebtTarget, Entry, Section
|
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)
|
@dataclass(frozen=True)
|
||||||
class SectionView:
|
class SectionView:
|
||||||
section: Section
|
section: Section
|
||||||
|
|
@ -26,6 +32,18 @@ def section_total(entries: list[Entry]) -> Decimal:
|
||||||
return sum((e.amount for e in entries), Decimal("0"))
|
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:
|
def add_entry(db: Session, section: Section, name: str, amount: Decimal) -> Entry:
|
||||||
entry = Entry(section=section, name=name.strip(), amount=amount)
|
entry = Entry(section=section, name=name.strip(), amount=amount)
|
||||||
db.add(entry)
|
db.add(entry)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue