django-ledger 0.5.6.2__py3-none-any.whl → 0.5.6.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-ledger might be problematic. Click here for more details.
- django_ledger/__init__.py +1 -1
- django_ledger/admin/coa.py +3 -3
- django_ledger/forms/account.py +2 -0
- django_ledger/forms/coa.py +1 -6
- django_ledger/forms/transactions.py +3 -1
- django_ledger/io/io_core.py +95 -79
- django_ledger/io/io_digest.py +4 -5
- django_ledger/io/io_generator.py +5 -4
- django_ledger/io/io_library.py +241 -16
- django_ledger/io/roles.py +32 -10
- django_ledger/migrations/0015_remove_chartofaccountmodel_locked_and_more.py +22 -0
- django_ledger/models/accounts.py +13 -9
- django_ledger/models/bill.py +3 -3
- django_ledger/models/closing_entry.py +39 -28
- django_ledger/models/coa.py +244 -35
- django_ledger/models/entity.py +119 -51
- django_ledger/models/invoice.py +3 -2
- django_ledger/models/journal_entry.py +8 -4
- django_ledger/models/ledger.py +63 -11
- django_ledger/models/mixins.py +2 -2
- django_ledger/models/transactions.py +20 -11
- django_ledger/report/balance_sheet.py +1 -1
- django_ledger/report/cash_flow_statement.py +5 -5
- django_ledger/report/core.py +2 -2
- django_ledger/report/income_statement.py +2 -2
- django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
- django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +10 -11
- django_ledger/templates/django_ledger/account/account_create.html +17 -11
- django_ledger/templates/django_ledger/account/account_list.html +12 -9
- django_ledger/templates/django_ledger/account/tags/account_txs_table.html +6 -1
- django_ledger/templates/django_ledger/account/tags/accounts_table.html +97 -93
- django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html +17 -0
- django_ledger/templates/django_ledger/{code_of_accounts → chart_of_accounts}/coa_update.html +1 -4
- django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html +74 -0
- django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +1 -1
- django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +6 -6
- django_ledger/templates/django_ledger/includes/widget_ic.html +1 -1
- django_ledger/templates/django_ledger/invoice/invoice_list.html +91 -94
- django_ledger/templates/django_ledger/journal_entry/includes/card_journal_entry.html +16 -7
- django_ledger/templates/django_ledger/journal_entry/je_detail.html +1 -1
- django_ledger/templates/django_ledger/ledger/tags/ledgers_table.html +10 -0
- django_ledger/templatetags/django_ledger.py +9 -8
- django_ledger/tests/base.py +134 -8
- django_ledger/tests/test_accounts.py +16 -0
- django_ledger/tests/test_auth.py +5 -7
- django_ledger/tests/test_bill.py +1 -1
- django_ledger/tests/test_chart_of_accounts.py +46 -0
- django_ledger/tests/test_closing_entry.py +16 -19
- django_ledger/tests/test_entity.py +3 -3
- django_ledger/tests/test_io.py +192 -2
- django_ledger/tests/test_transactions.py +290 -0
- django_ledger/urls/account.py +18 -3
- django_ledger/urls/chart_of_accounts.py +21 -1
- django_ledger/urls/ledger.py +7 -0
- django_ledger/views/account.py +29 -4
- django_ledger/views/coa.py +79 -4
- django_ledger/views/djl_api.py +4 -1
- django_ledger/views/journal_entry.py +1 -1
- django_ledger/views/mixins.py +3 -0
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/METADATA +1 -1
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/RECORD +65 -59
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/AUTHORS.md +0 -0
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/LICENSE +0 -0
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/WHEEL +0 -0
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/top_level.txt +0 -0
|
@@ -17,17 +17,25 @@ class ClosingEntryModelTests(DjangoLedgerBaseTest):
|
|
|
17
17
|
p.name: set(p.pattern.converters.keys()) for p in closing_entry_urls
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
self.MONTHS = list(range(1, 13))
|
|
21
|
+
self.YEARS = [self.START_DATE.year, self.START_DATE.year + 1]
|
|
22
|
+
|
|
23
|
+
def test_closing_entry_create(self):
|
|
24
|
+
entity_model = self.get_random_entity_model()
|
|
25
|
+
for y in self.YEARS:
|
|
26
|
+
for m in self.MONTHS:
|
|
27
|
+
entity_model.close_books_for_month(
|
|
28
|
+
year=y,
|
|
29
|
+
month=m,
|
|
30
|
+
post_closing_entry=True
|
|
31
|
+
)
|
|
27
32
|
|
|
28
33
|
def test_protected_views(self):
|
|
29
34
|
self.logout_client()
|
|
30
35
|
entity_model = self.get_random_entity_model()
|
|
36
|
+
entity_model.close_entity_books(
|
|
37
|
+
closing_date=self.get_random_date()
|
|
38
|
+
)
|
|
31
39
|
closing_entry_model = choice(entity_model.get_closing_entries())
|
|
32
40
|
|
|
33
41
|
for path, kwargs in self.URL_PATTERNS.items():
|
|
@@ -53,16 +61,5 @@ class ClosingEntryModelTests(DjangoLedgerBaseTest):
|
|
|
53
61
|
self.login_client()
|
|
54
62
|
entity_model = self.get_random_entity_model()
|
|
55
63
|
url = reverse('django_ledger:closing-entry-list', kwargs={'entity_slug': entity_model.slug})
|
|
56
|
-
with self.assertNumQueries(
|
|
64
|
+
with self.assertNumQueries(4):
|
|
57
65
|
response = self.CLIENT.get(path=url)
|
|
58
|
-
|
|
59
|
-
def test_closing_entry_create(self):
|
|
60
|
-
entity_model = self.get_random_entity_model()
|
|
61
|
-
|
|
62
|
-
with self.assertNumQueries(1):
|
|
63
|
-
|
|
64
|
-
# closing entry does not exist...
|
|
65
|
-
entity_model.close_books_for_month(
|
|
66
|
-
year=self.START_DATE.year + 2,
|
|
67
|
-
month=self.START_DATE.month
|
|
68
|
-
)
|
|
@@ -226,7 +226,7 @@ class EntityModelTests(DjangoLedgerBaseTest):
|
|
|
226
226
|
})
|
|
227
227
|
response = self.CLIENT.get(entity_detail_url)
|
|
228
228
|
|
|
229
|
-
with self.assertNumQueries(
|
|
229
|
+
with self.assertNumQueries(8): # previously 10
|
|
230
230
|
local_dt = get_localdate()
|
|
231
231
|
entity_month_detail_url = reverse('django_ledger:entity-dashboard-month',
|
|
232
232
|
kwargs={
|
|
@@ -236,7 +236,7 @@ class EntityModelTests(DjangoLedgerBaseTest):
|
|
|
236
236
|
})
|
|
237
237
|
self.assertRedirects(response, entity_month_detail_url)
|
|
238
238
|
|
|
239
|
-
with self.assertNumQueries(
|
|
239
|
+
with self.assertNumQueries(8):
|
|
240
240
|
# same as before, but this time the session must not be update because user has not suited entities...
|
|
241
241
|
response = self.CLIENT.get(entity_month_detail_url)
|
|
242
242
|
self.assertContains(response, text=entity_model.name)
|
|
@@ -249,7 +249,7 @@ class EntityModelTests(DjangoLedgerBaseTest):
|
|
|
249
249
|
# entity_models = self.create_entity_models(n=1)
|
|
250
250
|
entity_model = choice(self.ENTITY_MODEL_QUERYSET)
|
|
251
251
|
# ENTITY-DELETE VIEW...
|
|
252
|
-
with self.assertNumQueries(
|
|
252
|
+
with self.assertNumQueries(4):
|
|
253
253
|
entity_delete_url = reverse('django_ledger:entity-delete',
|
|
254
254
|
kwargs={
|
|
255
255
|
'entity_slug': entity_model.slug
|
django_ledger/tests/test_io.py
CHANGED
|
@@ -1,7 +1,197 @@
|
|
|
1
|
+
from datetime import timedelta, datetime
|
|
2
|
+
from random import randint
|
|
3
|
+
from zoneinfo import ZoneInfo
|
|
4
|
+
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
|
|
7
|
+
from django_ledger.io.io_core import IOValidationError
|
|
8
|
+
from django_ledger.models import EntityModel
|
|
1
9
|
from django_ledger.tests.base import DjangoLedgerBaseTest
|
|
2
10
|
|
|
3
11
|
|
|
4
12
|
class IOTest(DjangoLedgerBaseTest):
|
|
5
13
|
|
|
6
|
-
def
|
|
7
|
-
self.
|
|
14
|
+
def test_digest_dttm__dttm(self):
|
|
15
|
+
self.assertTrue(settings.USE_TZ, msg='Timezone not enabled.')
|
|
16
|
+
|
|
17
|
+
entity_model = self.get_random_entity_model()
|
|
18
|
+
from_datetime = self.START_DATE
|
|
19
|
+
to_datetime = self.START_DATE + timedelta(days=randint(10, 60))
|
|
20
|
+
|
|
21
|
+
io_digest = entity_model.digest(from_date=from_datetime, to_date=to_datetime)
|
|
22
|
+
self.assertTrue(isinstance(io_digest.get_to_datetime(), datetime))
|
|
23
|
+
self.assertTrue(isinstance(io_digest.get_from_datetime(), datetime))
|
|
24
|
+
self.assertEqual(io_digest.get_from_datetime(), from_datetime)
|
|
25
|
+
self.assertEqual(io_digest.get_to_datetime(), to_datetime)
|
|
26
|
+
|
|
27
|
+
def test_digest_dt__dttm(self):
|
|
28
|
+
self.assertTrue(settings.USE_TZ, msg='Timezone not enabled.')
|
|
29
|
+
|
|
30
|
+
entity_model = self.get_random_entity_model()
|
|
31
|
+
from_datetime = self.START_DATE
|
|
32
|
+
to_datetime = self.START_DATE + timedelta(days=randint(10, 60))
|
|
33
|
+
|
|
34
|
+
io_digest = entity_model.digest(from_date=from_datetime.date(), to_date=to_datetime)
|
|
35
|
+
self.assertTrue(isinstance(io_digest.get_to_datetime(), datetime))
|
|
36
|
+
self.assertTrue(isinstance(io_digest.get_from_datetime(), datetime))
|
|
37
|
+
|
|
38
|
+
self.assertEqual(
|
|
39
|
+
# the assumed datetime given a date...
|
|
40
|
+
io_digest.get_from_datetime(),
|
|
41
|
+
|
|
42
|
+
# equals the localized datetime @ 0:00
|
|
43
|
+
datetime.combine(
|
|
44
|
+
from_datetime.date(),
|
|
45
|
+
datetime.min.time(),
|
|
46
|
+
tzinfo=ZoneInfo(settings.TIME_ZONE)
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
self.assertEqual(io_digest.get_to_datetime(), to_datetime)
|
|
51
|
+
|
|
52
|
+
def test_digest_dttm__dt(self):
|
|
53
|
+
self.assertTrue(settings.USE_TZ, msg='Timezone not enabled.')
|
|
54
|
+
|
|
55
|
+
entity_model = self.get_random_entity_model()
|
|
56
|
+
from_datetime = self.START_DATE
|
|
57
|
+
to_datetime = self.START_DATE + timedelta(days=randint(10, 60))
|
|
58
|
+
|
|
59
|
+
io_digest = entity_model.digest(from_date=from_datetime, to_date=to_datetime.date())
|
|
60
|
+
|
|
61
|
+
self.assertTrue(isinstance(io_digest.get_to_datetime(), datetime))
|
|
62
|
+
self.assertTrue(isinstance(io_digest.get_from_datetime(), datetime))
|
|
63
|
+
|
|
64
|
+
self.assertEqual(io_digest.get_from_datetime(), from_datetime)
|
|
65
|
+
|
|
66
|
+
self.assertEqual(
|
|
67
|
+
# the assumed datetime given a date...
|
|
68
|
+
io_digest.get_to_datetime(),
|
|
69
|
+
|
|
70
|
+
# equals the localized datetime @ 0:00
|
|
71
|
+
datetime.combine(
|
|
72
|
+
to_datetime.date(),
|
|
73
|
+
datetime.min.time(),
|
|
74
|
+
tzinfo=ZoneInfo(settings.TIME_ZONE)
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def test_digest_dt__dt(self):
|
|
79
|
+
self.assertTrue(settings.USE_TZ, msg='Timezone not enabled.')
|
|
80
|
+
|
|
81
|
+
entity_model = self.get_random_entity_model()
|
|
82
|
+
from_datetime = self.START_DATE
|
|
83
|
+
to_datetime = self.START_DATE + timedelta(days=randint(10, 60))
|
|
84
|
+
|
|
85
|
+
io_digest = entity_model.digest(from_date=from_datetime.date(), to_date=to_datetime.date())
|
|
86
|
+
|
|
87
|
+
self.assertTrue(isinstance(io_digest.get_to_datetime(), datetime))
|
|
88
|
+
self.assertTrue(isinstance(io_digest.get_from_datetime(), datetime))
|
|
89
|
+
|
|
90
|
+
self.assertEqual(
|
|
91
|
+
# the assumed datetime given a date...
|
|
92
|
+
io_digest.get_from_datetime(),
|
|
93
|
+
|
|
94
|
+
# equals the localized datetime @ 0:00
|
|
95
|
+
datetime.combine(
|
|
96
|
+
from_datetime.date(),
|
|
97
|
+
datetime.min.time(),
|
|
98
|
+
tzinfo=ZoneInfo(settings.TIME_ZONE)
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
self.assertEqual(
|
|
103
|
+
# the assumed datetime given a date...
|
|
104
|
+
io_digest.get_to_datetime(),
|
|
105
|
+
|
|
106
|
+
# equals the localized datetime @ 0:00
|
|
107
|
+
datetime.combine(
|
|
108
|
+
to_datetime.date(),
|
|
109
|
+
datetime.min.time(),
|
|
110
|
+
tzinfo=ZoneInfo(settings.TIME_ZONE)
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def test_digest_none__dttm(self):
|
|
115
|
+
self.assertTrue(settings.USE_TZ, msg='Timezone not enabled.')
|
|
116
|
+
|
|
117
|
+
entity_model = self.get_random_entity_model()
|
|
118
|
+
to_datetime = self.START_DATE + timedelta(days=randint(10, 60))
|
|
119
|
+
|
|
120
|
+
io_digest = entity_model.digest(to_date=to_datetime)
|
|
121
|
+
|
|
122
|
+
self.assertTrue(io_digest.get_from_datetime() is None)
|
|
123
|
+
self.assertTrue(isinstance(io_digest.get_to_datetime(), datetime))
|
|
124
|
+
|
|
125
|
+
self.assertEqual(
|
|
126
|
+
# the assumed datetime given a date...
|
|
127
|
+
io_digest.get_from_datetime(),
|
|
128
|
+
|
|
129
|
+
# equals the localized datetime @ 0:00
|
|
130
|
+
None
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
self.assertEqual(io_digest.get_to_datetime(), to_datetime)
|
|
134
|
+
|
|
135
|
+
def test_digest_none__dt(self):
|
|
136
|
+
self.assertTrue(settings.USE_TZ, msg='Timezone not enabled.')
|
|
137
|
+
|
|
138
|
+
entity_model = self.get_random_entity_model()
|
|
139
|
+
to_datetime = self.START_DATE + timedelta(days=randint(10, 60))
|
|
140
|
+
|
|
141
|
+
io_digest = entity_model.digest(to_date=to_datetime.date())
|
|
142
|
+
|
|
143
|
+
self.assertTrue(io_digest.get_from_datetime() is None)
|
|
144
|
+
self.assertTrue(isinstance(io_digest.get_to_datetime(), datetime))
|
|
145
|
+
|
|
146
|
+
self.assertEqual(
|
|
147
|
+
# the assumed datetime given a date...
|
|
148
|
+
io_digest.get_from_datetime(),
|
|
149
|
+
|
|
150
|
+
# equals the localized datetime @ 0:00
|
|
151
|
+
None
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
self.assertEqual(
|
|
155
|
+
# the assumed datetime given a date...
|
|
156
|
+
io_digest.get_to_datetime(),
|
|
157
|
+
|
|
158
|
+
# equals the localized datetime @ 0:00
|
|
159
|
+
datetime.combine(
|
|
160
|
+
to_datetime.date(),
|
|
161
|
+
datetime.min.time(),
|
|
162
|
+
tzinfo=ZoneInfo(settings.TIME_ZONE)
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def test_digest_entity(self):
|
|
167
|
+
entity_model = self.get_random_entity_model()
|
|
168
|
+
from_datetime = self.START_DATE
|
|
169
|
+
to_datetime = self.START_DATE + timedelta(days=randint(10, 60))
|
|
170
|
+
|
|
171
|
+
with self.assertRaises(IOValidationError):
|
|
172
|
+
entity_model.digest(
|
|
173
|
+
entity_slug='1234',
|
|
174
|
+
from_date=from_datetime,
|
|
175
|
+
to_date=to_datetime
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
io_digest = entity_model.digest(
|
|
179
|
+
entity_slug=entity_model.slug,
|
|
180
|
+
from_date=from_datetime,
|
|
181
|
+
to_date=to_datetime
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
self.assertTrue(isinstance(io_digest.IO_MODEL, EntityModel))
|
|
185
|
+
self.assertTrue(io_digest.get_io_data(), io_digest.IO_DATA)
|
|
186
|
+
self.assertTrue(io_digest.IO_DATA['entity_slug'], entity_model.slug)
|
|
187
|
+
self.assertFalse(io_digest.IO_DATA['by_activity'])
|
|
188
|
+
self.assertFalse(io_digest.IO_DATA['by_unit'])
|
|
189
|
+
self.assertFalse(io_digest.IO_DATA['by_tx_type'])
|
|
190
|
+
|
|
191
|
+
# io_digest = entity_model.digest(
|
|
192
|
+
# unit_slug='3212',
|
|
193
|
+
# from_date=from_datetime,
|
|
194
|
+
# to_date=to_datetime
|
|
195
|
+
# )
|
|
196
|
+
#
|
|
197
|
+
# self.assertEqual(io_digest.get_io_txs_queryset().count(), 0)
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from random import choice, randint
|
|
3
|
+
|
|
4
|
+
from django.contrib.auth import get_user_model
|
|
5
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
6
|
+
from django.db.models import Count
|
|
7
|
+
from django.db.utils import IntegrityError
|
|
8
|
+
|
|
9
|
+
from django_ledger.forms.transactions import (
|
|
10
|
+
get_transactionmodel_formset_class,
|
|
11
|
+
TransactionModelForm,
|
|
12
|
+
TransactionModelFormSet
|
|
13
|
+
)
|
|
14
|
+
from django_ledger.io.io_core import get_localdate
|
|
15
|
+
from django_ledger.models import (
|
|
16
|
+
TransactionModel, EntityModel, AccountModel, LedgerModel, JournalEntryModel,
|
|
17
|
+
TransactionModelValidationError, JournalEntryValidationError
|
|
18
|
+
)
|
|
19
|
+
from django_ledger.tests.base import DjangoLedgerBaseTest
|
|
20
|
+
|
|
21
|
+
UserModel = get_user_model()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TransactionModelTest(DjangoLedgerBaseTest):
|
|
25
|
+
|
|
26
|
+
def test_invalid_balance(self):
|
|
27
|
+
entity_model = self.get_random_entity_model()
|
|
28
|
+
txs_model = self.get_random_transaction(entity_model=entity_model)
|
|
29
|
+
|
|
30
|
+
# transaction model does not allow for negative balances
|
|
31
|
+
txs_model.amount = Decimal('-100.00')
|
|
32
|
+
|
|
33
|
+
with self.assertRaises(ValidationError):
|
|
34
|
+
txs_model.full_clean()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TransactionModelFormTest(DjangoLedgerBaseTest):
|
|
38
|
+
|
|
39
|
+
def test_valid_data(self):
|
|
40
|
+
entity_model: EntityModel = self.get_random_entity_model()
|
|
41
|
+
|
|
42
|
+
account_model = str(self.get_random_account(entity_model=entity_model, balance_type='credit').uuid),
|
|
43
|
+
random_tx_type = choice([tx_type[0] for tx_type in TransactionModel.TX_TYPE])
|
|
44
|
+
|
|
45
|
+
form_data = {
|
|
46
|
+
'account': account_model[0],
|
|
47
|
+
'tx_type': random_tx_type,
|
|
48
|
+
'amount': Decimal(randint(10000, 99999)),
|
|
49
|
+
'description': 'Bought Something ...'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
form = TransactionModelForm(form_data)
|
|
53
|
+
|
|
54
|
+
self.assertTrue(form.is_valid(), msg=f'Form is invalid with error: {form.errors}')
|
|
55
|
+
with self.assertRaises(IntegrityError):
|
|
56
|
+
form.save()
|
|
57
|
+
|
|
58
|
+
def test_invalid_tx_type(self):
|
|
59
|
+
account_model = choice(AccountModel.objects.filter(balance_type='credit'))
|
|
60
|
+
form = TransactionModelForm({
|
|
61
|
+
'account': account_model,
|
|
62
|
+
'tx_type': 'crebit patty',
|
|
63
|
+
})
|
|
64
|
+
self.assertFalse(form.is_valid(), msg='tx_type other than credit / debit shouldn\'t be valid')
|
|
65
|
+
|
|
66
|
+
def test_blank_data(self):
|
|
67
|
+
form = TransactionModelForm()
|
|
68
|
+
self.assertFalse(form.is_valid(), msg='Form without data is supposed to be invalid')
|
|
69
|
+
|
|
70
|
+
def test_invalid_account(self):
|
|
71
|
+
with self.assertRaises(ObjectDoesNotExist):
|
|
72
|
+
form = TransactionModelForm({
|
|
73
|
+
'account': 'Asset',
|
|
74
|
+
})
|
|
75
|
+
form.is_valid()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TransactionModelFormSetTest(DjangoLedgerBaseTest):
|
|
79
|
+
|
|
80
|
+
def get_random_txs_formsets(self,
|
|
81
|
+
entity_model: EntityModel,
|
|
82
|
+
ledger_model: LedgerModel = None,
|
|
83
|
+
je_model: JournalEntryModel = None) -> TransactionModelFormSet:
|
|
84
|
+
"""
|
|
85
|
+
Returns a TransactionModelFormSet with prefilled form data.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
if ledger_model:
|
|
89
|
+
# if ledger model provided, get a je_model from provided ledger model...
|
|
90
|
+
je_model: JournalEntryModel = self.get_random_je(
|
|
91
|
+
entity_model=entity_model,
|
|
92
|
+
ledger_model=ledger_model
|
|
93
|
+
) if not je_model else je_model
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
|
|
97
|
+
# get a journal entry that has transactions...
|
|
98
|
+
je_model = JournalEntryModel.objects.for_entity(
|
|
99
|
+
entity_slug=entity_model,
|
|
100
|
+
user_model=self.user_model
|
|
101
|
+
).annotate(
|
|
102
|
+
txs_count=Count('transactionmodel')).filter(
|
|
103
|
+
txs_count__gt=0).order_by('-timestamp').first()
|
|
104
|
+
|
|
105
|
+
TransactionModelFormSet = get_transactionmodel_formset_class(journal_entry_model=je_model)
|
|
106
|
+
|
|
107
|
+
txs_formset = TransactionModelFormSet(
|
|
108
|
+
entity_slug=entity_model.slug,
|
|
109
|
+
user_model=self.user_model,
|
|
110
|
+
je_model=je_model,
|
|
111
|
+
ledger_pk=je_model.ledger_id,
|
|
112
|
+
)
|
|
113
|
+
return txs_formset
|
|
114
|
+
|
|
115
|
+
def test_valid_formset(self):
|
|
116
|
+
"""
|
|
117
|
+
Saved Transaction instances should have identical detail with initial formset.
|
|
118
|
+
"""
|
|
119
|
+
entity_model: EntityModel = self.get_random_entity_model()
|
|
120
|
+
ledger_model: LedgerModel = self.get_random_ledger(entity_model=entity_model)
|
|
121
|
+
je_model: JournalEntryModel = self.get_random_je(entity_model=entity_model, ledger_model=ledger_model)
|
|
122
|
+
credit_account: AccountModel = self.get_random_account(entity_model=entity_model, balance_type='credit')
|
|
123
|
+
debit_account: AccountModel = self.get_random_account(entity_model=entity_model, balance_type='debit')
|
|
124
|
+
transaction_amount = Decimal.from_float(randint(10000, 99999))
|
|
125
|
+
|
|
126
|
+
txs_formset = self.get_random_txs_formsets(
|
|
127
|
+
entity_model=entity_model,
|
|
128
|
+
je_model=je_model,
|
|
129
|
+
ledger_model=ledger_model
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
self.assertTrue(
|
|
133
|
+
txs_formset.is_valid(),
|
|
134
|
+
msg=f"Formset is not valid, error: {txs_formset.errors}")
|
|
135
|
+
|
|
136
|
+
txs_instances = txs_formset.save(commit=False)
|
|
137
|
+
for txs in txs_instances:
|
|
138
|
+
if not txs.journal_entry_id:
|
|
139
|
+
txs.journal_entry_id = je_model.uuid
|
|
140
|
+
|
|
141
|
+
txs_instances = txs_formset.save()
|
|
142
|
+
for txs in txs_instances:
|
|
143
|
+
if txs.tx_type == 'credit':
|
|
144
|
+
self.assertEqual(
|
|
145
|
+
txs.account, credit_account,
|
|
146
|
+
msg=f'Saved Transaction record has mismatched Credit Account from the submitted formset. Saved:{txs.account} | form:{credit_account}')
|
|
147
|
+
|
|
148
|
+
elif txs.tx_type == 'debit':
|
|
149
|
+
self.assertEqual(
|
|
150
|
+
txs.account, debit_account,
|
|
151
|
+
msg=f'Saved Transaction record has mismatched Debit Account from the submitted formset. Saved:{txs.account} | form:{debit_account}')
|
|
152
|
+
|
|
153
|
+
self.assertEqual(
|
|
154
|
+
txs.amount, Decimal(transaction_amount),
|
|
155
|
+
msg=f'Saved Transaction record has mismatched total amount from the submitted formset. Saved:{txs.amount} | form:{transaction_amount}')
|
|
156
|
+
|
|
157
|
+
def test_imbalance_transactions(self):
|
|
158
|
+
"""
|
|
159
|
+
Imbalanced Transactions should be invalid.
|
|
160
|
+
"""
|
|
161
|
+
entity_model: EntityModel = self.get_random_entity_model()
|
|
162
|
+
|
|
163
|
+
txs_formset = self.get_random_txs_formsets(entity_model=entity_model)
|
|
164
|
+
|
|
165
|
+
self.assertFalse(
|
|
166
|
+
txs_formset.is_valid(),
|
|
167
|
+
msg=f"Formset is supposed to be invalid because of imbalance transaction"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def test_je_locked(self):
|
|
171
|
+
"""
|
|
172
|
+
Transaction on locked a locked Journal Entry should fail.
|
|
173
|
+
"""
|
|
174
|
+
entity_model: EntityModel = self.get_random_entity_model()
|
|
175
|
+
ledger_model: LedgerModel = self.get_random_ledger(
|
|
176
|
+
entity_model=entity_model
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
je_model: JournalEntryModel = self.get_random_je(
|
|
180
|
+
entity_model=entity_model,
|
|
181
|
+
ledger_model=ledger_model
|
|
182
|
+
)
|
|
183
|
+
je_model.mark_as_locked(commit=True, raise_exception=False)
|
|
184
|
+
|
|
185
|
+
self.assertTrue(je_model.is_locked())
|
|
186
|
+
txs_model = je_model.transactionmodel_set.all().first()
|
|
187
|
+
txs_model.amount += Decimal.from_float(1.00)
|
|
188
|
+
|
|
189
|
+
with self.assertRaises(TransactionModelValidationError):
|
|
190
|
+
txs_model.save()
|
|
191
|
+
|
|
192
|
+
with self.assertRaises(
|
|
193
|
+
TransactionModelValidationError,
|
|
194
|
+
msg=f'Cannot create transaction on locked Journal Entry'
|
|
195
|
+
):
|
|
196
|
+
je_model.transactionmodel_set.create(
|
|
197
|
+
amount=Decimal.from_float(100.00),
|
|
198
|
+
account=self.get_random_account(entity_model=entity_model, balance_type='debit')
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def test_ledger_lock(self):
|
|
202
|
+
"""
|
|
203
|
+
Transaction on locked a locked Ledger should fail.
|
|
204
|
+
"""
|
|
205
|
+
entity_model: EntityModel = self.get_random_entity_model()
|
|
206
|
+
ledger_model = self.get_random_ledger(entity_model=entity_model)
|
|
207
|
+
ledger_model.post(commit=True, raise_exception=False)
|
|
208
|
+
self.assertTrue(ledger_model.is_posted())
|
|
209
|
+
ledger_model.lock(commit=True, raise_exception=False)
|
|
210
|
+
self.assertTrue(ledger_model.is_locked())
|
|
211
|
+
|
|
212
|
+
with self.assertRaises(
|
|
213
|
+
JournalEntryValidationError,
|
|
214
|
+
msg='Cannot create Journal Entries on locked ledgers.'
|
|
215
|
+
):
|
|
216
|
+
ledger_model.journal_entries.create(
|
|
217
|
+
timestamp=get_localdate(),
|
|
218
|
+
description='Test Journal Entry'
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
je_model = ledger_model.journal_entries.first()
|
|
222
|
+
|
|
223
|
+
with self.assertRaises(
|
|
224
|
+
JournalEntryValidationError,
|
|
225
|
+
msg='Cannot unpost journal entry on locked ledgers'
|
|
226
|
+
):
|
|
227
|
+
je_model.mark_as_unposted(commit=True, raise_exception=True)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class GetTransactionModelFormSetClassTest(DjangoLedgerBaseTest):
|
|
231
|
+
|
|
232
|
+
def test_unlocked_journal_entry_formset(self):
|
|
233
|
+
"""
|
|
234
|
+
The Formset will contain 6 extra forms & delete fields if Journal Entry is unlocked.
|
|
235
|
+
"""
|
|
236
|
+
entity_model: EntityModel = self.get_random_entity_model()
|
|
237
|
+
ledger_model: LedgerModel = self.get_random_ledger(entity_model=entity_model)
|
|
238
|
+
je_model: JournalEntryModel = self.get_random_je(entity_model=entity_model, ledger_model=ledger_model)
|
|
239
|
+
je_model.mark_as_unlocked(commit=True)
|
|
240
|
+
|
|
241
|
+
transaction_model_form_set = get_transactionmodel_formset_class(journal_entry_model=je_model)
|
|
242
|
+
txs_formset = transaction_model_form_set(
|
|
243
|
+
user_model=self.user_model,
|
|
244
|
+
je_model=je_model,
|
|
245
|
+
ledger_pk=ledger_model,
|
|
246
|
+
entity_slug=entity_model.slug,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
self.assertTrue(not je_model.is_locked(),
|
|
250
|
+
msg="At this point in this test case, Journal Entry should be unlocked.")
|
|
251
|
+
|
|
252
|
+
delete_field = '<input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE">'
|
|
253
|
+
self.assertInHTML(
|
|
254
|
+
delete_field,
|
|
255
|
+
txs_formset.as_table(),
|
|
256
|
+
msg_prefix='Transactions Formset with unlocked Journal Entry should have `can_delete` enabled'
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
self.assertEqual(len(txs_formset), 6,
|
|
260
|
+
msg='Transactions Formset with unlocked Journal Entry should have 6 extras')
|
|
261
|
+
|
|
262
|
+
def test_locked_journal_entry_formset(self):
|
|
263
|
+
"""
|
|
264
|
+
The Formset will contain no extra forms & only forms with Transaction if Journal Entry is locked.
|
|
265
|
+
"""
|
|
266
|
+
entity_model: EntityModel = self.get_random_entity_model()
|
|
267
|
+
ledger_model: LedgerModel = self.get_random_ledger(entity_model=entity_model)
|
|
268
|
+
je_model: JournalEntryModel = self.get_random_je(entity_model=entity_model, ledger_model=ledger_model)
|
|
269
|
+
# transaction_pairs = randint(1, 12)
|
|
270
|
+
# self.get_random_transactions(entity_model=entity_model, je_model=je_model,
|
|
271
|
+
# pairs=transaction_pairs) # Fill Journal Entry with Transactions
|
|
272
|
+
|
|
273
|
+
je_model.mark_as_locked(commit=True)
|
|
274
|
+
self.assertTrue(
|
|
275
|
+
je_model.is_locked(),
|
|
276
|
+
msg="Journal Entry should be locked in this test case")
|
|
277
|
+
|
|
278
|
+
transaction_model_form_set = get_transactionmodel_formset_class(journal_entry_model=je_model)
|
|
279
|
+
|
|
280
|
+
txs_formset = transaction_model_form_set(
|
|
281
|
+
user_model=self.user_model,
|
|
282
|
+
je_model=je_model,
|
|
283
|
+
ledger_pk=ledger_model,
|
|
284
|
+
entity_slug=entity_model.slug,
|
|
285
|
+
queryset=je_model.transactionmodel_set.all().order_by('account__code')
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
self.assertEqual(
|
|
289
|
+
len(txs_formset), (je_model.transactionmodel_set.count()), # Convert pairs to total count
|
|
290
|
+
msg="Transactions Formset with unlocked Journal Entry did not match the expected count")
|
django_ledger/urls/account.py
CHANGED
|
@@ -3,15 +3,30 @@ from django.urls import path
|
|
|
3
3
|
from django_ledger import views
|
|
4
4
|
|
|
5
5
|
urlpatterns = [
|
|
6
|
+
|
|
7
|
+
# NO COA SLUG USES DEFAULT COA....
|
|
8
|
+
path('<slug:entity_slug>/create/',
|
|
9
|
+
views.AccountModelCreateView.as_view(),
|
|
10
|
+
name='account-create'),
|
|
6
11
|
path('<slug:entity_slug>/list/',
|
|
7
12
|
views.AccountModelListView.as_view(),
|
|
8
13
|
name='account-list'),
|
|
9
14
|
path('<slug:entity_slug>/list/active/',
|
|
10
15
|
views.AccountModelListView.as_view(active_only=True),
|
|
11
16
|
name='account-list-active'),
|
|
12
|
-
|
|
17
|
+
|
|
18
|
+
# EXPLICIT COA...
|
|
19
|
+
path('<slug:entity_slug>/<slug:coa_slug>/create/',
|
|
13
20
|
views.AccountModelCreateView.as_view(),
|
|
14
|
-
name='account-create'),
|
|
21
|
+
name='account-create-coa'),
|
|
22
|
+
path('<slug:entity_slug>/<slug:coa_slug>/list/',
|
|
23
|
+
views.AccountModelListView.as_view(),
|
|
24
|
+
name='account-list-coa'),
|
|
25
|
+
path('<slug:entity_slug>/<slug:coa_slug>/list/active/',
|
|
26
|
+
views.AccountModelListView.as_view(active_only=True),
|
|
27
|
+
name='account-list-active-coa'),
|
|
28
|
+
|
|
29
|
+
# Account Transaction Detail....
|
|
15
30
|
path('<slug:entity_slug>/update/<uuid:account_pk>/',
|
|
16
31
|
views.AccountModelUpdateView.as_view(),
|
|
17
32
|
name='account-update'),
|
|
@@ -31,7 +46,7 @@ urlpatterns = [
|
|
|
31
46
|
views.AccountModelDateDetailView.as_view(),
|
|
32
47
|
name='account-detail-date'),
|
|
33
48
|
|
|
34
|
-
# Actions...
|
|
49
|
+
# Account Actions...
|
|
35
50
|
path('<slug:entity_slug>/action/<uuid:account_pk>/activate/',
|
|
36
51
|
views.AccountModelModelActionView.as_view(action_name='activate'),
|
|
37
52
|
name='account-action-activate'),
|
|
@@ -3,5 +3,25 @@ from django.urls import path
|
|
|
3
3
|
from django_ledger import views
|
|
4
4
|
|
|
5
5
|
urlpatterns = [
|
|
6
|
-
path('<slug:entity_slug
|
|
6
|
+
path('<slug:entity_slug>/list/',
|
|
7
|
+
views.ChartOfAccountsListView.as_view(),
|
|
8
|
+
name='coa-list'),
|
|
9
|
+
path('<slug:entity_slug>/detail/<slug:coa_slug>/',
|
|
10
|
+
views.ChartOfAccountsListView.as_view(),
|
|
11
|
+
name='coa-detail'),
|
|
12
|
+
path('<slug:entity_slug>/update/<slug:coa_slug>/',
|
|
13
|
+
views.ChartOfAccountsUpdateView.as_view(),
|
|
14
|
+
name='coa-update'),
|
|
15
|
+
|
|
16
|
+
# ACTIONS....
|
|
17
|
+
path('<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/',
|
|
18
|
+
views.CharOfAccountModelActionView.as_view(action_name='mark_as_default'),
|
|
19
|
+
name='coa-action-mark-as-default'),
|
|
20
|
+
path('<slug:entity_slug>/action/<slug:coa_slug>/mark-as-active/',
|
|
21
|
+
views.CharOfAccountModelActionView.as_view(action_name='mark_as_active'),
|
|
22
|
+
name='coa-action-mark-as-active'),
|
|
23
|
+
path('<slug:entity_slug>/action/<slug:coa_slug>/mark-as-inactive/',
|
|
24
|
+
views.CharOfAccountModelActionView.as_view(action_name='mark_as_inactive'),
|
|
25
|
+
name='coa-action-mark-as-inactive'),
|
|
26
|
+
|
|
7
27
|
]
|
django_ledger/urls/ledger.py
CHANGED
|
@@ -50,4 +50,11 @@ urlpatterns = [
|
|
|
50
50
|
views.LedgerModelModelActionView.as_view(action_name='unlock'),
|
|
51
51
|
name='ledger-action-unlock'),
|
|
52
52
|
|
|
53
|
+
path('<slug:entity_slug>/action/<uuid:ledger_pk>/hide/',
|
|
54
|
+
views.LedgerModelModelActionView.as_view(action_name='hide'),
|
|
55
|
+
name='ledger-action-hide'),
|
|
56
|
+
path('<slug:entity_slug>/action/<uuid:ledger_pk>/unhide/',
|
|
57
|
+
views.LedgerModelModelActionView.as_view(action_name='unhide'),
|
|
58
|
+
name='ledger-action-unhide'),
|
|
59
|
+
|
|
53
60
|
]
|