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:
parent
0b50ba0f82
commit
5cc2630777
7 changed files with 196 additions and 144 deletions
51
crud.py
51
crud.py
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue