Skip to content

Commit 05e505d

Browse files
committed
Create base view for a BankAccount with transactions filtered by period
1 parent 5248e3d commit 05e505d

File tree

4 files changed

+360
-0
lines changed

4 files changed

+360
-0
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
{% extends "base/main.html" %}
2+
3+
{% load currency i18n %}
4+
5+
{% block content %}
6+
7+
<div class="row">
8+
<div class="col-xl-3 col-md-6 mb-4">
9+
<div class="card border-left-success shadow h-100 py-2">
10+
<div class="card-body">
11+
<div class="row no-gutters align-items-center">
12+
<div class="col mr-2">
13+
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
14+
{% translate "Deposits (selected period)" %}
15+
</div>
16+
<div class="h5 mb-0 font-weight-bold {% if bank_account.deposits < 0 %}text-danger{% else %}text-gray-800{% endif %}">
17+
{{ bank_account.deposits|money }}
18+
</div>
19+
</div>
20+
<div class="col-auto">
21+
<i class="fas fa-dollar-sign fa-2x text-gray-300"></i>
22+
</div>
23+
</div>
24+
</div>
25+
</div>
26+
</div>
27+
<div class="col-xl-3 col-md-6 mb-4">
28+
<div class="card border-left-danger shadow h-100 py-2">
29+
<div class="card-body">
30+
<div class="row no-gutters align-items-center">
31+
<div class="col mr-2">
32+
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">
33+
{% translate "Withdraws (selected period)" %}
34+
</div>
35+
<div class="h5 mb-0 font-weight-bold {% if bank_account.withdraws < 0 %}text-danger{% else %}text-gray-800{% endif %}">
36+
{{ bank_account.withdraws|money }}
37+
</div>
38+
</div>
39+
<div class="col-auto">
40+
<i class="fas fa-dollar-sign fa-2x text-gray-300"></i>
41+
</div>
42+
</div>
43+
</div>
44+
</div>
45+
</div>
46+
<div class="col-xl-3 col-md-6 mb-4">
47+
<div class="card border-left-primary shadow h-100 py-2">
48+
<div class="card-body">
49+
<div class="row no-gutters align-items-center">
50+
<div class="col mr-2">
51+
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
52+
{% translate "Balance (selected_period)" %}
53+
</div>
54+
<div class="h5 mb-0 font-weight-bold {% if bank_account.balance < 0 %}text-danger{% else %}text-gray-800{% endif %}">
55+
{{ bank_account.balance|money }}
56+
</div>
57+
</div>
58+
<div class="col-auto">
59+
<i class="fas fa-university fa-2x text-gray-300"></i>
60+
</div>
61+
</div>
62+
</div>
63+
</div>
64+
</div>
65+
<div class="col-xl-3 col-md-6 mb-4">
66+
<div class="card border-left-primary shadow h-100 py-2">
67+
<div class="card-body">
68+
<div class="row no-gutters align-items-center">
69+
<div class="col mr-2">
70+
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
71+
{% translate "Overall Balance" %}
72+
</div>
73+
<div class="h5 mb-0 font-weight-bold {% if bank_account.overall_balance < 0 %}text-danger{% else %}text-gray-800{% endif %}">
74+
{{ bank_account.overall_balance|money }}
75+
</div>
76+
</div>
77+
<div class="col-auto">
78+
<i class="fas fa-university fa-2x text-gray-300"></i>
79+
</div>
80+
</div>
81+
</div>
82+
</div>
83+
</div>
84+
</div>
85+
86+
87+
88+
{% endblock content %}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import datetime
2+
from http import HTTPStatus
3+
4+
import pytest
5+
from model_bakery import baker
6+
7+
from django.conf import settings
8+
from django.contrib.auth import get_user_model
9+
from django.test import RequestFactory
10+
from django.urls import reverse
11+
12+
from thebook.bookkeeping.models import BankAccount, Category, Document, Transaction
13+
from thebook.bookkeeping.views import (
14+
_get_bank_account_transactions_context,
15+
bank_account_transactions,
16+
)
17+
18+
19+
@pytest.fixture
20+
def bank_account(scope="module"):
21+
return BankAccount.objects.create(name="Test Bank Account")
22+
23+
24+
@pytest.fixture
25+
def user(scope="module"):
26+
return baker.make(get_user_model())
27+
28+
29+
def test_unauthenticated_access_to_bank_account_absolute_url_redirect_to_login_page(
30+
db, client, bank_account
31+
):
32+
bank_account_url = reverse("bookkeeping:bank-account", args=(bank_account.slug,))
33+
34+
response = client.get(bank_account_url)
35+
36+
assert response.status_code == HTTPStatus.FOUND
37+
assert response.url == f"{settings.LOGIN_URL}?next={bank_account_url}"
38+
39+
40+
def test_allowed_access_to_bank_account_details_authenticated(
41+
db, client, user, bank_account
42+
):
43+
client.force_login(user)
44+
45+
bank_account_url = reverse("bookkeeping:bank-account", args=(bank_account.slug,))
46+
47+
response = client.get(bank_account_url)
48+
49+
assert response.status_code == HTTPStatus.OK
50+
51+
52+
def test_not_found_with_invalid_bank_account_slug(db, client, user):
53+
client.force_login(user)
54+
55+
bank_account_url = reverse("bookkeeping:bank-account", args=("i-do-not-exist",))
56+
57+
response = client.get(bank_account_url)
58+
59+
assert response.status_code == HTTPStatus.NOT_FOUND
60+
61+
62+
def test_bank_account_returns_bank_account_instance_in_context(
63+
db, client, bank_account, user
64+
):
65+
client.force_login(user)
66+
67+
bank_account_url = reverse("bookkeeping:bank-account", args=(bank_account.slug,))
68+
69+
response = client.get(bank_account_url)
70+
71+
assert "bank_account" in response.context
72+
assert response.context["bank_account"] == bank_account
73+
74+
75+
def test_bank_account_returns_bank_account_instance_in_context(
76+
db, client, bank_account, user
77+
):
78+
client.force_login(user)
79+
80+
bank_account_url = reverse("bookkeeping:bank-account", args=(bank_account.slug,))
81+
82+
response = client.get(bank_account_url)
83+
84+
assert "bank_account" in response.context
85+
assert response.context["bank_account"] == bank_account
86+
87+
88+
def test_returns_bank_account_instance_in_context(db, client, bank_account, user):
89+
client.force_login(user)
90+
91+
bank_account_url = reverse("bookkeeping:bank-account", args=(bank_account.slug,))
92+
93+
response = client.get(bank_account_url)
94+
95+
assert "bank_account" in response.context
96+
assert response.context["bank_account"] == bank_account
97+
98+
99+
@pytest.mark.freeze_time("2026-02-03")
100+
def test_returns_bank_account_transactions_of_current_month_in_context(
101+
db, client, bank_account, user
102+
):
103+
# fmt: off
104+
t_1 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 1, 31))
105+
t_2 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 2, 1))
106+
t_3 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 2, 15))
107+
t_4 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 2, 28))
108+
t_5 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 3, 1))
109+
# fmt: on
110+
111+
client.force_login(user)
112+
113+
bank_account_url = reverse("bookkeeping:bank-account", args=(bank_account.slug,))
114+
115+
response = client.get(bank_account_url)
116+
117+
assert "transactions" in response.context
118+
assert t_1 not in response.context["transactions"]
119+
assert t_2 in response.context["transactions"]
120+
assert t_3 in response.context["transactions"]
121+
assert t_4 in response.context["transactions"]
122+
assert t_5 not in response.context["transactions"]
123+
124+
125+
@pytest.mark.freeze_time("2026-02-03")
126+
def test_returns_transactions_date_range_in_context(db, client, bank_account, user):
127+
client.force_login(user)
128+
129+
bank_account_url = reverse("bookkeeping:bank-account", args=(bank_account.slug,))
130+
131+
response = client.get(bank_account_url)
132+
133+
assert "start_date" in response.context
134+
assert response.context["start_date"] == datetime.date(2026, 2, 1)
135+
assert "end_date" in response.context
136+
assert response.context["end_date"] == datetime.date(2026, 3, 1)
137+
138+
139+
@pytest.mark.freeze_time("2026-02-03")
140+
def test_returns_bank_account_transactions_filtered_by_date_range(
141+
db, client, bank_account, user
142+
):
143+
# fmt: off
144+
t_1 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 1, 31))
145+
t_2 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 2, 1))
146+
t_3 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 2, 15))
147+
t_4 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 2, 28))
148+
t_5 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 3, 1))
149+
# fmt: on
150+
151+
client.force_login(user)
152+
153+
bank_account_url = reverse(
154+
"bookkeeping:bank-account",
155+
args=(bank_account.slug,),
156+
query={"start_date": "2026-02-10", "end_date": "2026-02-28"},
157+
)
158+
159+
response = client.get(bank_account_url)
160+
161+
assert "transactions" in response.context
162+
assert t_1 not in response.context["transactions"]
163+
assert t_2 not in response.context["transactions"]
164+
assert t_3 in response.context["transactions"]
165+
assert t_4 not in response.context["transactions"]
166+
assert t_5 not in response.context["transactions"]
167+
168+
169+
@pytest.mark.freeze_time("2026-02-03")
170+
def test_returns_bank_account_transactions_current_month_if_missing_start_date(
171+
db, client, bank_account, user
172+
):
173+
# fmt: off
174+
t_1 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 1, 31))
175+
t_2 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 2, 1))
176+
t_3 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 2, 15))
177+
t_4 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 2, 28))
178+
t_5 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 3, 1))
179+
# fmt: on
180+
181+
client.force_login(user)
182+
183+
bank_account_url = reverse(
184+
"bookkeeping:bank-account",
185+
args=(bank_account.slug,),
186+
query={
187+
"end_date": "2026-02-28",
188+
},
189+
)
190+
191+
response = client.get(bank_account_url)
192+
193+
assert "transactions" in response.context
194+
assert t_1 not in response.context["transactions"]
195+
assert t_2 in response.context["transactions"]
196+
assert t_3 in response.context["transactions"]
197+
assert t_4 in response.context["transactions"]
198+
assert t_5 not in response.context["transactions"]
199+
200+
201+
@pytest.mark.freeze_time("2026-02-03")
202+
def test_returns_bank_account_transactions_current_month_if_missing_end_date(
203+
db, client, bank_account, user
204+
):
205+
# fmt: off
206+
t_1 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 1, 31))
207+
t_2 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 2, 1))
208+
t_3 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 2, 15))
209+
t_4 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 2, 28))
210+
t_5 = baker.make(Transaction, bank_account=bank_account, date=datetime.date(2026, 3, 1))
211+
# fmt: on
212+
213+
client.force_login(user)
214+
215+
bank_account_url = reverse(
216+
"bookkeeping:bank-account",
217+
args=(bank_account.slug,),
218+
query={
219+
"start_date": "2026-02-10",
220+
},
221+
)
222+
223+
response = client.get(bank_account_url)
224+
225+
assert "transactions" in response.context
226+
assert t_1 not in response.context["transactions"]
227+
assert t_2 in response.context["transactions"]
228+
assert t_3 in response.context["transactions"]
229+
assert t_4 in response.context["transactions"]
230+
assert t_5 not in response.context["transactions"]

thebook/bookkeeping/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
app_name = "bookkeeping"
66

77
urlpatterns = [
8+
path(
9+
"bank_account/<slug:bank_account_slug>",
10+
views.BankAccountView.as_view(),
11+
name="bank-account",
12+
),
813
path(
914
"cb/<slug:bank_account_slug>/transactions",
1015
views.bank_account_transactions,

thebook/bookkeeping/views.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import calendar
12
import csv
23
import datetime
34

45
from django.contrib import messages
56
from django.http import HttpResponse, HttpResponseRedirect
67
from django.shortcuts import get_object_or_404, render
78
from django.utils.translation import gettext as _
9+
from django.views import View
810

911
from thebook.bookkeeping.importers import ImportTransactionsError, import_transactions
1012
from thebook.bookkeeping.models import BankAccount, Document, Transaction
@@ -205,3 +207,38 @@ def partial_bank_accounts_dashboard(request):
205207
"year": int(year),
206208
},
207209
)
210+
211+
212+
class BankAccountView(View):
213+
def get(self, request, bank_account_slug):
214+
bank_account = get_object_or_404(
215+
BankAccount,
216+
slug=bank_account_slug,
217+
)
218+
219+
start_date = request.GET.get("start_date")
220+
end_date = request.GET.get("end_date")
221+
if start_date is None or end_date is None:
222+
today = datetime.date.today()
223+
current_month = today.month
224+
current_year = today.year
225+
start_date = datetime.date(current_year, current_month, 1)
226+
_, last_day_of_month = calendar.monthrange(current_year, current_month)
227+
end_date = datetime.date(
228+
current_year, current_month, last_day_of_month
229+
) + datetime.timedelta(days=1)
230+
231+
transactions = bank_account.transactions.within_period(
232+
start_date=start_date, end_date=end_date
233+
)
234+
235+
return render(
236+
request,
237+
"bookkeeping/bank_account.html",
238+
context={
239+
"bank_account": bank_account,
240+
"start_date": start_date,
241+
"end_date": end_date,
242+
"transactions": transactions,
243+
},
244+
)

0 commit comments

Comments
 (0)