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

View file

@ -23,13 +23,13 @@ def validate_journal_entry(
entry_lines: List[Dict[str, Any]]
) -> None:
"""
Validate a journal entry and its lines.
Validate a journal entry and its lines (Beancount-style with single amount field).
Checks:
1. Entry must have at least 2 lines (double-entry requirement)
2. Entry must be balanced (sum of debits = sum of credits)
3. All lines must have valid amounts (non-negative)
4. All lines must have account_id
2. Entry must be balanced (sum of amounts = 0)
3. All lines must have account_id
4. No line should have amount = 0 (would serve no purpose)
Args:
entry: Journal entry dict with keys:
@ -38,8 +38,7 @@ def validate_journal_entry(
- entry_date: datetime
entry_lines: List of entry line dicts with keys:
- account_id: str
- debit: int
- credit: int
- amount: int (positive = debit, negative = credit)
Raises:
ValidationError: If validation fails
@ -66,64 +65,30 @@ def validate_journal_entry(
}
)
# Check amounts are non-negative
debit = line.get("debit", 0)
credit = line.get("credit", 0)
# Get amount (Beancount-style: positive = debit, negative = credit)
amount = line.get("amount", 0)
if debit < 0:
# Check that amount is non-zero (zero amounts serve no purpose)
if amount == 0:
raise ValidationError(
f"Entry line {i + 1} has negative debit: {debit}",
{
"entry_id": entry.get("id"),
"line_index": i,
"debit": debit,
}
)
if credit < 0:
raise ValidationError(
f"Entry line {i + 1} has negative credit: {credit}",
{
"entry_id": entry.get("id"),
"line_index": i,
"credit": credit,
}
)
# Check that a line doesn't have both debit and credit
if debit > 0 and credit > 0:
raise ValidationError(
f"Entry line {i + 1} has both debit and credit",
{
"entry_id": entry.get("id"),
"line_index": i,
"debit": debit,
"credit": credit,
}
)
# Check that a line has at least one non-zero amount
if debit == 0 and credit == 0:
raise ValidationError(
f"Entry line {i + 1} has both debit and credit as zero",
f"Entry line {i + 1} has amount = 0 (serves no purpose)",
{
"entry_id": entry.get("id"),
"line_index": i,
}
)
# Check entry is balanced
total_debits = sum(line.get("debit", 0) for line in entry_lines)
total_credits = sum(line.get("credit", 0) for line in entry_lines)
# Check entry is balanced (sum of amounts must equal 0)
# Beancount-style: positive amounts cancel out negative amounts
total_amount = sum(line.get("amount", 0) for line in entry_lines)
if total_debits != total_credits:
if total_amount != 0:
raise ValidationError(
"Journal entry is not balanced",
"Journal entry is not balanced (sum of amounts must equal 0)",
{
"entry_id": entry.get("id"),
"total_debits": total_debits,
"total_credits": total_credits,
"difference": total_debits - total_credits,
"total_amount": total_amount,
"line_count": len(entry_lines),
}
)