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:
parent
d0bec3ea5a
commit
4ae6a8f7d2
5 changed files with 35 additions and 45 deletions
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue