Refactors journal entry lines to use single amount

Simplifies the representation of journal entry lines by replacing separate debit and credit fields with a single 'amount' field.

Positive amounts represent debits, while negative amounts represent credits, aligning with Beancount's approach. This change improves code readability and simplifies calculations for balancing entries.
This commit is contained in:
padreug 2025-11-08 11:48:08 +01:00
parent d0bec3ea5a
commit 4ae6a8f7d2
5 changed files with 35 additions and 45 deletions

View file

@ -229,8 +229,8 @@ await create_account(CreateAccount(
await create_journal_entry(CreateJournalEntry(
description="Cash payment for groceries",
lines=[
CreateEntryLine(account_id=expense_account_id, debit=50000),
CreateEntryLine(account_id=cash_account_id, credit=50000)
CreateEntryLine(account_id=expense_account_id, amount=50000), # Positive = debit (expense increase)
CreateEntryLine(account_id=cash_account_id, amount=-50000) # Negative = credit (asset decrease)
],
flag=JournalEntryFlag.CLEARED,
meta={"source": "manual", "payment_method": "cash"}

View file

@ -61,8 +61,7 @@ class ImmutableEntryLine(NamedTuple):
id: str
journal_entry_id: str
account_id: str
debit: int
credit: int
amount: int # Beancount-style: positive = debit, negative = credit
description: Optional[str]
metadata: dict[str, Any]
flag: Optional[str] # Like Beancount: '!', '*', etc.
@ -145,15 +144,14 @@ class CastlePlugin(Protocol):
__plugins__ = ('check_all_balanced',)
def check_all_balanced(entries, settings, config):
"""Verify all journal entries have debits = credits"""
"""Verify all journal entries balance (sum of amounts = 0)"""
errors = []
for entry in entries:
total_debits = sum(line.debit for line in entry.lines)
total_credits = sum(line.credit for line in entry.lines)
if total_debits != total_credits:
total_amount = sum(line.amount for line in entry.lines)
if total_amount != 0:
errors.append({
'entry_id': entry.id,
'message': f'Unbalanced entry: debits={total_debits}, credits={total_credits}',
'message': f'Unbalanced entry: sum of amounts={total_amount} (must equal 0)',
'severity': 'error'
})
return entries, errors
@ -184,7 +182,7 @@ def check_receivable_limits(entries, settings, config):
for line in entry.lines:
if 'Accounts Receivable' in line.account_name:
user_id = extract_user_from_account(line.account_name)
receivables[user_id] = receivables.get(user_id, 0) + line.debit - line.credit
receivables[user_id] = receivables.get(user_id, 0) + line.amount
for user_id, amount in receivables.items():
if amount > max_per_user:
@ -367,22 +365,15 @@ async def get_user_inventory(user_id: str) -> CastleInventory:
# Add as position
metadata = json.loads(line.metadata) if line.metadata else {}
if line.debit > 0:
if line.amount != 0:
# Beancount-style: positive = debit, negative = credit
# Adjust sign for cost amount based on amount direction
cost_sign = 1 if line.amount > 0 else -1
inventory.add_position(CastlePosition(
currency="SATS",
amount=Decimal(line.debit),
amount=Decimal(line.amount),
cost_currency=metadata.get("fiat_currency"),
cost_amount=Decimal(metadata.get("fiat_amount", 0)),
date=line.created_at,
metadata=metadata
))
if line.credit > 0:
inventory.add_position(CastlePosition(
currency="SATS",
amount=-Decimal(line.credit),
cost_currency=metadata.get("fiat_currency"),
cost_amount=-Decimal(metadata.get("fiat_amount", 0)),
cost_amount=cost_sign * Decimal(metadata.get("fiat_amount", 0)),
date=line.created_at,
metadata=metadata
))
@ -840,17 +831,16 @@ class UnbalancedEntryError(NamedTuple):
async def validate_journal_entry(entry: CreateJournalEntry) -> list[CastleError]:
errors = []
total_debits = sum(line.debit for line in entry.lines)
total_credits = sum(line.credit for line in entry.lines)
# Beancount-style: sum of amounts must equal 0
total_amount = sum(line.amount for line in entry.lines)
if total_debits != total_credits:
if total_amount != 0:
errors.append(UnbalancedEntryError(
source={'created_by': entry.created_by},
message=f"Entry does not balance: debits={total_debits}, credits={total_credits}",
message=f"Entry does not balance: sum of amounts={total_amount} (must equal 0)",
entry=entry.dict(),
total_debits=total_debits,
total_credits=total_credits,
difference=total_debits - total_credits
total_amount=total_amount,
difference=total_amount
))
return errors

View file

@ -71,8 +71,7 @@ CREATE TABLE entry_lines (
id TEXT PRIMARY KEY,
journal_entry_id TEXT NOT NULL,
account_id TEXT NOT NULL,
debit INTEGER NOT NULL DEFAULT 0, -- Amount in satoshis
credit INTEGER NOT NULL DEFAULT 0, -- Amount in satoshis
amount INTEGER NOT NULL, -- Amount in satoshis (positive = debit, negative = credit)
description TEXT,
metadata TEXT DEFAULT '{}' -- JSON: {fiat_currency, fiat_amount, fiat_rate, btc_rate}
);
@ -314,17 +313,20 @@ for account in user_accounts:
total_balance -= account_balance # Positive asset = User owes Castle, so negative balance
# Calculate fiat balance from metadata
# Beancount-style: positive amount = debit, negative amount = credit
for line in account_entry_lines:
if line.metadata.fiat_currency and line.metadata.fiat_amount:
if account.account_type == AccountType.LIABILITY:
if line.credit > 0:
# For liabilities, negative amounts (credits) increase what castle owes
if line.amount < 0:
fiat_balances[currency] += fiat_amount # Castle owes more
elif line.debit > 0:
else:
fiat_balances[currency] -= fiat_amount # Castle owes less
elif account.account_type == AccountType.ASSET:
if line.debit > 0:
# For assets, positive amounts (debits) increase what user owes
if line.amount > 0:
fiat_balances[currency] -= fiat_amount # User owes more (negative balance)
elif line.credit > 0:
else:
fiat_balances[currency] += fiat_amount # User owes less
```
@ -767,10 +769,8 @@ async def export_beancount(
beancount_name = format_account_name(account.name, account.user_id)
beancount_type = map_account_type(account.account_type)
if line.debit > 0:
amount = line.debit
else:
amount = -line.credit
# Beancount-style: amount is already signed (positive = debit, negative = credit)
amount = line.amount
lines.append(f" {beancount_type}:{beancount_name} {amount} SATS")

View file

@ -41,7 +41,7 @@ Only entries with `flag='*'` (CLEARED) are included in balance calculations:
```sql
-- Balance query excludes pending/flagged/voided entries
SELECT SUM(debit), SUM(credit)
SELECT SUM(amount)
FROM entry_lines el
JOIN journal_entries je ON el.journal_entry_id = je.id
WHERE el.account_id = :account_id

View file

@ -276,8 +276,8 @@ balance = BalanceCalculator.calculate_account_balance(
# Build inventory from entry lines
entry_lines = [
{"debit": 100000, "credit": 0, "metadata": '{"fiat_currency": "EUR", "fiat_amount": "50.00"}'},
{"debit": 0, "credit": 50000, "metadata": "{}"}
{"amount": 100000, "metadata": '{"fiat_currency": "EUR", "fiat_amount": "50.00"}'}, # Positive = debit
{"amount": -50000, "metadata": "{}"} # Negative = credit
]
inventory = BalanceCalculator.build_inventory_from_entry_lines(
@ -306,8 +306,8 @@ entry = {
}
entry_lines = [
{"account_id": "acc1", "debit": 100000, "credit": 0},
{"account_id": "acc2", "debit": 0, "credit": 100000}
{"account_id": "acc1", "amount": 100000}, # Positive = debit
{"account_id": "acc2", "amount": -100000} # Negative = credit
]
try: