Implement viewing bank balances
This commit is contained in:
parent
1ee3d04ea6
commit
35ffa036ae
|
@ -105,4 +105,10 @@ urlpatterns = [
|
||||||
banks.BanksCurrencies.as_view(),
|
banks.BanksCurrencies.as_view(),
|
||||||
name="currencies",
|
name="currencies",
|
||||||
),
|
),
|
||||||
|
# Bank balances
|
||||||
|
path(
|
||||||
|
"banks/<str:type>/balances/",
|
||||||
|
banks.BanksBalances.as_view(),
|
||||||
|
name="balances",
|
||||||
|
),
|
||||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
|
|
@ -3,7 +3,6 @@ from abc import ABC
|
||||||
|
|
||||||
class AggregatorClient(ABC):
|
class AggregatorClient(ABC):
|
||||||
def store_account_info(self, account_infos):
|
def store_account_info(self, account_infos):
|
||||||
print("STORE ACCOUNT INFO CALLED")
|
|
||||||
# account_infos = {
|
# account_infos = {
|
||||||
# bank: accounts
|
# bank: accounts
|
||||||
# for bank, accounts in account_info.items()
|
# for bank, accounts in account_info.items()
|
||||||
|
@ -41,6 +40,3 @@ class AggregatorClient(ABC):
|
||||||
|
|
||||||
self.instance.currencies = currencies
|
self.instance.currencies = currencies
|
||||||
self.instance.save()
|
self.instance.save()
|
||||||
|
|
||||||
print("CURRENCIES", self.instance.currencies)
|
|
||||||
print("ACCOUNT INFO", self.instance.account_info)
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ class NordigenClient(BaseClient, AggregatorClient):
|
||||||
"""
|
"""
|
||||||
# This function is a stub.
|
# This function is a stub.
|
||||||
|
|
||||||
return ["GB", "SE", "BG", "UA"]
|
return ["GB", "SE", "BG"]
|
||||||
|
|
||||||
async def get_banks(self, country):
|
async def get_banks(self, country):
|
||||||
"""
|
"""
|
||||||
|
@ -129,7 +129,6 @@ class NordigenClient(BaseClient, AggregatorClient):
|
||||||
|
|
||||||
path = f"accounts/{account_id}/details"
|
path = f"accounts/{account_id}/details"
|
||||||
response = await self.call(path, schema="AccountDetails")
|
response = await self.call(path, schema="AccountDetails")
|
||||||
print("RESPONSE", response)
|
|
||||||
if "account" not in response:
|
if "account" not in response:
|
||||||
return False
|
return False
|
||||||
parsed = response["account"]
|
parsed = response["account"]
|
||||||
|
@ -151,9 +150,7 @@ class NordigenClient(BaseClient, AggregatorClient):
|
||||||
async def get_all_account_info(self, requisition=None, store=False):
|
async def get_all_account_info(self, requisition=None, store=False):
|
||||||
to_return = {}
|
to_return = {}
|
||||||
if not requisition:
|
if not requisition:
|
||||||
print("NOT REQUISITION")
|
|
||||||
requisitions = await self.get_requisitions()
|
requisitions = await self.get_requisitions()
|
||||||
print("GOT REQS", requisitions)
|
|
||||||
else:
|
else:
|
||||||
requisitions = [await self.get_requisition(requisition)]
|
requisitions = [await self.get_requisition(requisition)]
|
||||||
|
|
||||||
|
@ -169,6 +166,78 @@ class NordigenClient(BaseClient, AggregatorClient):
|
||||||
to_return[req["institution_id"]] = [account_info]
|
to_return[req["institution_id"]] = [account_info]
|
||||||
|
|
||||||
if store:
|
if store:
|
||||||
print("TO STORE IS TRUE")
|
if requisition is not None:
|
||||||
|
raise Exception("Cannot store partial data")
|
||||||
self.store_account_info(to_return)
|
self.store_account_info(to_return)
|
||||||
return to_return
|
return to_return
|
||||||
|
|
||||||
|
async def get_balance(self, account_id):
|
||||||
|
"""
|
||||||
|
Get the balance and currency of an account.
|
||||||
|
:param account_id: the account ID
|
||||||
|
:return: tuple of (currency, amount)
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
|
|
||||||
|
path = f"accounts/{account_id}/balances"
|
||||||
|
response = await self.call(path, schema="AccountBalance")
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
currency = None
|
||||||
|
if "balances" not in response:
|
||||||
|
return (False, False)
|
||||||
|
for entry in response["balances"]:
|
||||||
|
if currency:
|
||||||
|
if not currency == entry["balanceAmount"]["currency"]:
|
||||||
|
return (False, False)
|
||||||
|
total += float(entry["balanceAmount"]["amount"])
|
||||||
|
currency = entry["balanceAmount"]["currency"]
|
||||||
|
return (currency, total)
|
||||||
|
|
||||||
|
async def get_all_balances(self):
|
||||||
|
"""
|
||||||
|
Get all balances.
|
||||||
|
Keyed by bank.
|
||||||
|
"""
|
||||||
|
if self.instance.account_info is None:
|
||||||
|
await self.get_all_account_info(store=True)
|
||||||
|
|
||||||
|
account_balances = {}
|
||||||
|
for bank, accounts in self.instance.account_info.items():
|
||||||
|
if bank not in account_balances:
|
||||||
|
account_balances[bank] = []
|
||||||
|
for account in accounts:
|
||||||
|
account_id = account["account_id"]
|
||||||
|
currency, amount = await self.get_balance(account_id)
|
||||||
|
account_balances[bank].append(
|
||||||
|
{
|
||||||
|
"currency": currency,
|
||||||
|
"balance": amount,
|
||||||
|
"account_id": account_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return account_balances
|
||||||
|
|
||||||
|
async def get_total_map(self):
|
||||||
|
"""
|
||||||
|
Return a dictionary keyed by currencies with the amounts as values.
|
||||||
|
:return: dict keyed by currency, values are amounts
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
if self.instance.account_info is None:
|
||||||
|
await self.get_all_account_info(store=True)
|
||||||
|
|
||||||
|
totals = {}
|
||||||
|
for bank, accounts in self.instance.account_info.items():
|
||||||
|
for account in accounts:
|
||||||
|
account_id = account["account_id"]
|
||||||
|
currency, amount = await self.get_balance(account_id)
|
||||||
|
if not amount:
|
||||||
|
continue
|
||||||
|
if not currency:
|
||||||
|
continue
|
||||||
|
if currency in totals:
|
||||||
|
totals[currency] += amount
|
||||||
|
else:
|
||||||
|
totals[currency] = amount
|
||||||
|
return totals
|
||||||
|
|
|
@ -18,9 +18,7 @@ async def job():
|
||||||
for aggregator in aggregators:
|
for aggregator in aggregators:
|
||||||
if aggregator.service == "nordigen":
|
if aggregator.service == "nordigen":
|
||||||
instance = await NordigenClient(aggregator)
|
instance = await NordigenClient(aggregator)
|
||||||
print("RUNNING GET ALL ACCOUNT INFO")
|
|
||||||
await instance.get_all_account_info(store=True)
|
await instance.get_all_account_info(store=True)
|
||||||
print("FINISHED RUNNING")
|
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"No such client library: {aggregator.service}")
|
raise NotImplementedError(f"No such client library: {aggregator.service}")
|
||||||
aggregator.fetch_accounts = False
|
aggregator.fetch_accounts = False
|
||||||
|
|
|
@ -231,7 +231,7 @@
|
||||||
<a class="navbar-item" href="{% url 'currencies' type='page' %}">
|
<a class="navbar-item" href="{% url 'currencies' type='page' %}">
|
||||||
Currencies
|
Currencies
|
||||||
</a>
|
</a>
|
||||||
<a class="navbar-item" href="#">
|
<a class="navbar-item" href="{% url 'balances' type='page' %}">
|
||||||
Balances
|
Balances
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
{% load cache %}
|
||||||
|
{% load cachalot cache %}
|
||||||
|
{% load nsep %}
|
||||||
|
|
||||||
|
{% get_last_invalidation 'core.Aggregator' as last %}
|
||||||
|
{% include 'mixins/partials/notify.html' %}
|
||||||
|
{# cache 600 objects_banks_balances request.user.id object_list type last #}
|
||||||
|
|
||||||
|
{% for bank, accounts in object_list.items %}
|
||||||
|
<h1 class="title is-4">{{ bank }}</h1>
|
||||||
|
<table
|
||||||
|
class="table is-fullwidth is-hoverable"
|
||||||
|
hx-target="#{{ bank }}-table"
|
||||||
|
id="{{ bank }}-table"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||||
|
hx-get="{{ list_url }}">
|
||||||
|
<thead>
|
||||||
|
<th>currency</th>
|
||||||
|
<th>balance</th>
|
||||||
|
<th>id</th>
|
||||||
|
|
||||||
|
</thead>
|
||||||
|
{% for account in accounts %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ account.currency }}</td>
|
||||||
|
<td>{{ account.balance }}</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
class="has-text-grey"
|
||||||
|
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ account.account_id }}');">
|
||||||
|
<span class="icon" data-tooltip="Copy to clipboard">
|
||||||
|
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</table>
|
||||||
|
{% endfor %}
|
||||||
|
{# endcache #}
|
|
@ -1,16 +1,18 @@
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from mixins.views import ObjectList, ObjectRead
|
from mixins.views import ObjectList
|
||||||
from two_factor.views.mixins import OTPRequiredMixin
|
from two_factor.views.mixins import OTPRequiredMixin
|
||||||
|
|
||||||
|
from core.clients.aggregators.nordigen import NordigenClient
|
||||||
from core.models import Aggregator
|
from core.models import Aggregator
|
||||||
from core.util import logs
|
from core.util import logs
|
||||||
|
from core.views.helpers import synchronize_async_helper
|
||||||
|
|
||||||
log = logs.get_logger(__name__)
|
log = logs.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BanksCurrencies(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
class BanksCurrencies(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
||||||
"""
|
"""
|
||||||
Get a list of configured currencies from the banks we use.
|
Get a list of bank accounts with their details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
list_template = "partials/banks-currencies-list.html"
|
list_template = "partials/banks-currencies-list.html"
|
||||||
|
@ -36,7 +38,30 @@ class BanksCurrencies(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
||||||
return account_info
|
return account_info
|
||||||
|
|
||||||
|
|
||||||
class BankCurrencyDetails(LoginRequiredMixin, OTPRequiredMixin, ObjectRead):
|
class BanksBalances(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
||||||
"""
|
"""
|
||||||
Get the bank details for the selected currency.
|
Get the bank balances.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
list_template = "partials/banks-balances-list.html"
|
||||||
|
page_title = "Bank Balances"
|
||||||
|
|
||||||
|
context_object_name_singular = "balance"
|
||||||
|
context_object_name = "balances"
|
||||||
|
|
||||||
|
list_url_name = "balances"
|
||||||
|
list_url_args = ["type"]
|
||||||
|
|
||||||
|
def get_queryset(self, **kwargs):
|
||||||
|
aggregators = Aggregator.objects.filter(user=self.request.user, enabled=True)
|
||||||
|
account_balances = {}
|
||||||
|
for aggregator in aggregators:
|
||||||
|
run = synchronize_async_helper(NordigenClient(aggregator))
|
||||||
|
balance_map = synchronize_async_helper(run.get_all_balances())
|
||||||
|
for k, v in balance_map.items():
|
||||||
|
if k not in account_balances:
|
||||||
|
account_balances[k] = []
|
||||||
|
for item in v:
|
||||||
|
account_balances[k].append(item)
|
||||||
|
|
||||||
|
return account_balances
|
||||||
|
|
Loading…
Reference in New Issue