REFACTOR Migrates to single 'amount' field for transactions

Refactors the data model to use a single 'amount' field for journal entry lines, aligning with the Beancount approach.
This simplifies the model, enhances compatibility, and eliminates invalid states.

Includes a database migration to convert existing debit/credit columns to the new 'amount' field.

Updates balance calculation logic to utilize the new amount field for improved accuracy and efficiency.
This commit is contained in:
padreug 2025-11-08 10:26:14 +01:00
parent 0b50ba0f82
commit 5cc2630777
7 changed files with 196 additions and 144 deletions

51
crud.py
View file

@ -142,13 +142,13 @@ async def create_journal_entry(
) -> JournalEntry:
entry_id = urlsafe_short_hash()
# Validate that debits equal credits
total_debits = sum(line.debit for line in data.lines)
total_credits = sum(line.credit for line in data.lines)
# Validate that entry balances (sum of all amounts = 0)
# Beancount-style: positive amounts cancel out negative amounts
total_amount = sum(line.amount for line in data.lines)
if total_debits != total_credits:
if total_amount != 0:
raise ValueError(
f"Journal entry must balance: debits={total_debits}, credits={total_credits}"
f"Journal entry must balance (sum of amounts = 0): sum={total_amount}"
)
entry_date = data.entry_date or datetime.now()
@ -191,23 +191,21 @@ async def create_journal_entry(
id=line_id,
journal_entry_id=entry_id,
account_id=line_data.account_id,
debit=line_data.debit,
credit=line_data.credit,
amount=line_data.amount,
description=line_data.description,
metadata=line_data.metadata,
)
# Insert with metadata as JSON string
await db.execute(
"""
INSERT INTO entry_lines (id, journal_entry_id, account_id, debit, credit, description, metadata)
VALUES (:id, :journal_entry_id, :account_id, :debit, :credit, :description, :metadata)
INSERT INTO entry_lines (id, journal_entry_id, account_id, amount, description, metadata)
VALUES (:id, :journal_entry_id, :account_id, :amount, :description, :metadata)
""",
{
"id": line.id,
"journal_entry_id": line.journal_entry_id,
"account_id": line.account_id,
"debit": line.debit,
"credit": line.credit,
"amount": line.amount,
"description": line.description,
"metadata": json.dumps(line.metadata),
},
@ -259,8 +257,7 @@ async def get_entry_lines(journal_entry_id: str) -> list[EntryLine]:
id=row.id,
journal_entry_id=row.journal_entry_id,
account_id=row.account_id,
debit=row.debit,
credit=row.credit,
amount=row.amount,
description=row.description,
metadata=metadata,
)
@ -364,13 +361,21 @@ async def get_journal_entries_by_user(
async def get_account_balance(account_id: str) -> int:
"""Calculate account balance (debits - credits for assets/expenses, credits - debits for liabilities/equity/revenue)
Only includes entries that are cleared (flag='*'), excludes pending/flagged/voided entries."""
"""
Calculate account balance using single amount field (Beancount-style).
Only includes entries that are cleared (flag='*'), excludes pending/flagged/voided entries.
For each account type:
- Assets/Expenses: balance = sum of amounts (positive amounts increase, negative decrease)
- Liabilities/Equity/Revenue: balance = -sum of amounts (negative amounts increase, positive decrease)
This works because we store amounts consistently:
- Debit (asset/expense increase) = positive amount
- Credit (liability/equity/revenue increase) = negative amount
"""
result = await db.fetchone(
"""
SELECT
COALESCE(SUM(el.debit), 0) as total_debit,
COALESCE(SUM(el.credit), 0) as total_credit
SELECT COALESCE(SUM(el.amount), 0) as total_amount
FROM entry_lines el
JOIN journal_entries je ON el.journal_entry_id = je.id
WHERE el.account_id = :id
@ -386,13 +391,12 @@ async def get_account_balance(account_id: str) -> int:
if not account:
return 0
total_debit = result["total_debit"]
total_credit = result["total_credit"]
total_amount = result["total_amount"]
# Use core BalanceCalculator for consistent logic
core_account_type = CoreAccountType(account.account_type.value)
return BalanceCalculator.calculate_account_balance(
total_debit, total_credit, core_account_type
return BalanceCalculator.calculate_account_balance_from_amount(
total_amount, core_account_type
)
@ -500,8 +504,7 @@ async def get_account_transactions(
id=row.id,
journal_entry_id=row.journal_entry_id,
account_id=row.account_id,
debit=row.debit,
credit=row.credit,
amount=row.amount,
description=row.description,
metadata=metadata,
)