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