feat(notes): service-layer support for notes on entry and month_entry

add_entry and add_month_entry accept an optional notes keyword. A
new set_entry_notes function updates a single budget entry's notes.
update_month_entry gains a notes parameter guarded by a sentinel so
callers can distinguish "do not touch notes" from "clear to NULL".
create_month copies entry.notes into each freshly snapshotted
month_entry. Blank / whitespace notes normalise to NULL.

Refs #13

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
archeious 2026-04-17 12:51:25 -06:00
parent 4d40843e24
commit 034a8d65f5
2 changed files with 48 additions and 2 deletions

View file

@ -112,6 +112,7 @@ def create_month(db: Session, year_month: str) -> Month:
name=e.name, name=e.name,
planned=e.amount, planned=e.amount,
applied=Decimal("0.00"), applied=Decimal("0.00"),
notes=e.notes,
origin_name=e.name, origin_name=e.name,
origin_planned=e.amount, origin_planned=e.amount,
source_entry_id=e.id, source_entry_id=e.id,
@ -203,12 +204,20 @@ def section_view(month: Month, section: Section, label: str) -> MonthSectionView
) )
def _clean_notes(raw: str | None) -> str | None:
if raw is None:
return None
stripped = raw.strip()
return stripped if stripped else None
def add_month_entry( def add_month_entry(
db: Session, db: Session,
month: Month, month: Month,
section: Section, section: Section,
name: str, name: str,
planned: Decimal, planned: Decimal,
notes: str | None = None,
) -> MonthEntry: ) -> MonthEntry:
entry = MonthEntry( entry = MonthEntry(
month_id=month.id, month_id=month.id,
@ -216,6 +225,7 @@ def add_month_entry(
name=name.strip(), name=name.strip(),
planned=planned, planned=planned,
applied=Decimal("0.00"), applied=Decimal("0.00"),
notes=_clean_notes(notes),
origin_name=None, origin_name=None,
origin_planned=None, origin_planned=None,
source_entry_id=None, source_entry_id=None,
@ -243,6 +253,9 @@ def delete_month_entry(db: Session, month: Month, entry_id: int) -> Section | No
return section return section
_NOTES_SENTINEL = object()
def update_month_entry( def update_month_entry(
db: Session, db: Session,
month: Month, month: Month,
@ -251,6 +264,7 @@ def update_month_entry(
name: str | None = None, name: str | None = None,
planned: Decimal | None = None, planned: Decimal | None = None,
applied: Decimal | None = None, applied: Decimal | None = None,
notes: str | None | object = _NOTES_SENTINEL,
) -> MonthEntry | None: ) -> MonthEntry | None:
entry = get_month_entry(db, month, entry_id) entry = get_month_entry(db, month, entry_id)
if entry is None: if entry is None:
@ -261,6 +275,8 @@ def update_month_entry(
entry.planned = planned entry.planned = planned
if applied is not None: if applied is not None:
entry.applied = applied entry.applied = applied
if notes is not _NOTES_SENTINEL:
entry.notes = _clean_notes(notes) # type: ignore[arg-type]
db.commit() db.commit()
db.refresh(entry) db.refresh(entry)
return entry return entry

View file

@ -87,14 +87,44 @@ def budget_zero(db: Session) -> Decimal:
return (total_income - total_non_income).quantize(Decimal("0.01")) 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(
entry = Entry(section=section, name=name.strip(), amount=amount) db: Session,
section: Section,
name: str,
amount: Decimal,
notes: str | None = None,
) -> Entry:
entry = Entry(
section=section,
name=name.strip(),
amount=amount,
notes=_clean_notes(notes),
)
db.add(entry) db.add(entry)
db.commit() db.commit()
db.refresh(entry) db.refresh(entry)
return entry return entry
def _clean_notes(raw: str | None) -> str | None:
if raw is None:
return None
stripped = raw.strip()
return stripped if stripped else None
def set_entry_notes(
db: Session, entry_id: int, notes: str | None
) -> Entry | None:
entry = db.get(Entry, entry_id)
if entry is None:
return None
entry.notes = _clean_notes(notes)
db.commit()
db.refresh(entry)
return entry
def delete_entry(db: Session, entry_id: int) -> Entry | None: def delete_entry(db: Session, entry_id: int) -> Entry | None:
entry = db.get(Entry, entry_id) entry = db.get(Entry, entry_id)
if entry is None: if entry is None: