3 changed files with 234 additions and 27 deletions
@ -0,0 +1,88 @@
|
||||
"""Read models and shared calculations for reconciliation checkpoints.""" |
||||
|
||||
from dataclasses import dataclass |
||||
from datetime import date |
||||
from decimal import Decimal |
||||
from uuid import UUID |
||||
|
||||
from django.db.models import DecimalField, Sum, Value |
||||
from django.db.models.functions import Coalesce |
||||
|
||||
from finance.models import ReconciliationCheckpoint, Transaction |
||||
|
||||
|
||||
_MONEY_FIELD = DecimalField(max_digits=24, decimal_places=2) |
||||
_ZERO = Decimal("0.00") |
||||
|
||||
|
||||
@dataclass(frozen=True) |
||||
class CheckpointStatus: |
||||
checkpoint_id: UUID |
||||
account_id: UUID |
||||
account_name: str |
||||
currency: str |
||||
as_of_date: date |
||||
institution_balance: Decimal |
||||
snapshot_ledger_balance: Decimal |
||||
snapshot_difference: Decimal |
||||
live_ledger_balance: Decimal |
||||
live_difference: Decimal |
||||
state: str |
||||
|
||||
|
||||
def ledger_balance_through(account_id: UUID, as_of_date: date) -> Decimal: |
||||
from finance.services.base import money |
||||
|
||||
total = Transaction.objects.filter( |
||||
account_id=account_id, |
||||
posting_date__lte=as_of_date, |
||||
).aggregate( |
||||
total=Coalesce( |
||||
Sum("amount"), |
||||
Value(_ZERO), |
||||
output_field=_MONEY_FIELD, |
||||
) |
||||
)["total"] |
||||
return money(total, allow_zero=True) |
||||
|
||||
|
||||
def checkpoint_status(checkpoint: ReconciliationCheckpoint) -> CheckpointStatus: |
||||
account = checkpoint.account |
||||
live_ledger_balance = ledger_balance_through(checkpoint.account_id, checkpoint.as_of_date) |
||||
live_difference = checkpoint.institution_balance - live_ledger_balance |
||||
if live_difference == _ZERO: |
||||
state = "reconciled" |
||||
elif checkpoint.difference_at_creation == _ZERO: |
||||
state = "changed_since_reconciliation" |
||||
else: |
||||
state = "difference" |
||||
return CheckpointStatus( |
||||
checkpoint_id=checkpoint.pk, |
||||
account_id=account.pk, |
||||
account_name=account.name, |
||||
currency=account.currency, |
||||
as_of_date=checkpoint.as_of_date, |
||||
institution_balance=checkpoint.institution_balance, |
||||
snapshot_ledger_balance=checkpoint.ledger_balance_at_creation, |
||||
snapshot_difference=checkpoint.difference_at_creation, |
||||
live_ledger_balance=live_ledger_balance, |
||||
live_difference=live_difference, |
||||
state=state, |
||||
) |
||||
|
||||
|
||||
def checkpoint_history(account_id: UUID) -> list[CheckpointStatus]: |
||||
checkpoints = ReconciliationCheckpoint.objects.filter( |
||||
account_id=account_id, |
||||
).select_related("account").order_by("-as_of_date", "-created_at", "-id") |
||||
return [checkpoint_status(checkpoint) for checkpoint in checkpoints] |
||||
|
||||
|
||||
def latest_checkpoint_statuses(currency: str) -> list[CheckpointStatus]: |
||||
checkpoints = ReconciliationCheckpoint.objects.filter( |
||||
account__archived=False, |
||||
account__currency=currency, |
||||
).select_related("account").order_by( |
||||
"account_id", "-as_of_date", "-created_at", "-id" |
||||
).distinct("account_id") |
||||
return [checkpoint_status(checkpoint) for checkpoint in checkpoints] |
||||
Loading…
Reference in new issue