Skip to content

Commit aab3992

Browse files
committed
(experimental) add running totals functionality
1 parent f9e4b94 commit aab3992

File tree

4 files changed

+104
-3
lines changed

4 files changed

+104
-3
lines changed

hordak/admin.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,18 @@
1818

1919

2020
@admin.register(models.Account)
21-
class AccountAdmin(MPTTModelAdmin):
21+
class AccountAdmin(admin.ModelAdmin):
2222
list_display = (
2323
"name",
2424
"code_",
2525
"type_",
2626
"currencies",
2727
"balance_sum",
2828
"income",
29+
# "running_totals",
30+
# "running_incomes",
2931
)
30-
readonly_fields = ("balance",)
32+
readonly_fields = ("balance", "balance_sum", "income")
3133
raw_id_fields = ("parent",)
3234
search_fields = (
3335
"code",

hordak/defaults.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
DEFAULT_CURRENCY = getattr(settings, "DEFAULT_CURRENCY", "EUR")
77

8-
CURRENCIES = getattr(settings, "CURRENCIES", [])
8+
CURRENCIES = getattr(settings, "CURRENCIES", list)
99

1010
DECIMAL_PLACES = getattr(settings, "HORDAK_DECIMAL_PLACES", 2)
1111

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from django.core.management.base import BaseCommand
2+
from django.db.models import Q
3+
from hordak.models import Account
4+
from moneyed import Money
5+
6+
7+
try: # SubquerySum is quicker, but django-sql-utils can remain as optional dependency.
8+
from sql_util.utils import SubquerySum as Sum
9+
10+
legs_filter = Q(amount__lt=0)
11+
except ImportError:
12+
from django.db.models import Sum
13+
14+
legs_filter = Q(legs__amount__lt=0)
15+
16+
17+
class Command(BaseCommand):
18+
help = "Recalculate running totals for all accounts"
19+
20+
def handle(self, *args, **options):
21+
print("Recalculating running totals for all accounts")
22+
# queryset = Account.objects.filter(id__in=[1]).values("legs__amount_currency")
23+
# .annotate(
24+
# total=Sum("legs__amount"),
25+
# # income=Sum("legs__amount", filter=legs_filter),
26+
# )
27+
queryset = Account.objects.all()
28+
for account in queryset[:1000]:
29+
total = account.balance()
30+
print(total, account.name)
31+
account.running_totals = [total[c] for c in total.currencies()]
32+
print(account.running_totals)
33+
# account.running_totals = [Money(r["total"], r["amount_currency"]) for r in account]
34+
# account.running_incomes = [Money(r["income"], r["amount_currency"]) for r in account]
35+
account.save()
36+
if total:
37+
import pudb; pudb.set_trace()

hordak/models/core.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
from django.contrib.postgres.fields.array import ArrayField
2525
from django.db import models
26+
from django.db.models.signals import post_save, post_delete
27+
from django.dispatch import receiver
2628
from django.db import transaction as db_transaction
2729
from django.db.models import JSONField
2830
from django.utils import timezone
@@ -138,6 +140,27 @@ class Account(MPTTModel):
138140
default=defaults.CURRENCIES,
139141
verbose_name=_("currencies"),
140142
)
143+
running_totals = ArrayField(
144+
MoneyField(
145+
max_digits=MAX_DIGITS,
146+
decimal_places=DECIMAL_PLACES,
147+
default_currency=defaults.INTERNAL_CURRENCY,
148+
),
149+
default=list,
150+
help_text="Running totals for each currency. This field should be considered an estimated value calculated for performance reasons. It is not guaranteed to be accurate.",
151+
verbose_name=_("running totals"),
152+
)
153+
running_incomes = ArrayField(
154+
MoneyField(
155+
max_digits=MAX_DIGITS,
156+
decimal_places=DECIMAL_PLACES,
157+
help_text="Record debits as positive, credits as negative",
158+
default_currency=defaults.INTERNAL_CURRENCY,
159+
),
160+
default=list,
161+
help_text="Running incomes for each currency. This field should be considered an estimated value calculated for performance reasons. It is not guaranteed to be accurate.",
162+
verbose_name=_("running incomes"),
163+
)
141164

142165
objects = AccountManager.from_queryset(AccountQuerySet)()
143166

@@ -549,6 +572,45 @@ class Meta:
549572
verbose_name = _("Leg")
550573

551574

575+
@receiver(post_save, sender=Leg)
576+
def update_running_totals(sender, instance, created, **kwargs):
577+
"""Update the running total of the account associated with the leg"""
578+
if created:
579+
instance.account.running_totals[instance.amount.currency] += instance.amount
580+
else:
581+
# We are updating the leg, so we need to get the old amount
582+
old_amount = sender.objects.get(pk=instance.pk).amount
583+
instance.account.running_totals[instance.amount.currency] += instance.amount - old_amount
584+
585+
586+
@receiver(post_save, sender=Leg)
587+
def update_running_incomes(sender, instance, created, **kwargs):
588+
"""Update the running income of the account associated with the leg"""
589+
if created:
590+
if instance.is_credit():
591+
instance.account.running_income += instance.amount
592+
else:
593+
# We are updating the leg, so we need to get the old amount
594+
old_amount = sender.objects.get(pk=instance.pk).amount
595+
if instance.is_credit():
596+
instance.account.running_income += instance.amount - old_amount
597+
elif old_amount.amount < 0:
598+
instance.account.running_income += old_amount
599+
600+
601+
@receiver(post_delete, sender=Leg)
602+
def update_running_totals_on_delete(sender, instance, **kwargs):
603+
"""Update the running total of the account associated with the leg"""
604+
instance.account.running_totals[instance.amount.currency] -= instance.amount
605+
606+
607+
@receiver(post_delete, sender=Leg)
608+
def update_running_incomes_on_delete(sender, instance, **kwargs):
609+
"""Update the running income of the account associated with the leg"""
610+
if instance.is_credit():
611+
instance.account.running_income -= instance.amount
612+
613+
552614
class StatementImportManager(models.Manager):
553615
def get_by_natural_key(self, uuid):
554616
return self.get(uuid=uuid)

0 commit comments

Comments
 (0)