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.

Files changed (65) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/admin/coa.py +3 -3
  3. django_ledger/forms/account.py +2 -0
  4. django_ledger/forms/coa.py +1 -6
  5. django_ledger/forms/transactions.py +3 -1
  6. django_ledger/io/io_core.py +95 -79
  7. django_ledger/io/io_digest.py +4 -5
  8. django_ledger/io/io_generator.py +5 -4
  9. django_ledger/io/io_library.py +241 -16
  10. django_ledger/io/roles.py +32 -10
  11. django_ledger/migrations/0015_remove_chartofaccountmodel_locked_and_more.py +22 -0
  12. django_ledger/models/accounts.py +13 -9
  13. django_ledger/models/bill.py +3 -3
  14. django_ledger/models/closing_entry.py +39 -28
  15. django_ledger/models/coa.py +244 -35
  16. django_ledger/models/entity.py +119 -51
  17. django_ledger/models/invoice.py +3 -2
  18. django_ledger/models/journal_entry.py +8 -4
  19. django_ledger/models/ledger.py +63 -11
  20. django_ledger/models/mixins.py +2 -2
  21. django_ledger/models/transactions.py +20 -11
  22. django_ledger/report/balance_sheet.py +1 -1
  23. django_ledger/report/cash_flow_statement.py +5 -5
  24. django_ledger/report/core.py +2 -2
  25. django_ledger/report/income_statement.py +2 -2
  26. django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
  27. django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +10 -11
  28. django_ledger/templates/django_ledger/account/account_create.html +17 -11
  29. django_ledger/templates/django_ledger/account/account_list.html +12 -9
  30. django_ledger/templates/django_ledger/account/tags/account_txs_table.html +6 -1
  31. django_ledger/templates/django_ledger/account/tags/accounts_table.html +97 -93
  32. django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html +17 -0
  33. django_ledger/templates/django_ledger/{code_of_accounts → chart_of_accounts}/coa_update.html +1 -4
  34. django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html +74 -0
  35. django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +1 -1
  36. django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +6 -6
  37. django_ledger/templates/django_ledger/includes/widget_ic.html +1 -1
  38. django_ledger/templates/django_ledger/invoice/invoice_list.html +91 -94
  39. django_ledger/templates/django_ledger/journal_entry/includes/card_journal_entry.html +16 -7
  40. django_ledger/templates/django_ledger/journal_entry/je_detail.html +1 -1
  41. django_ledger/templates/django_ledger/ledger/tags/ledgers_table.html +10 -0
  42. django_ledger/templatetags/django_ledger.py +9 -8
  43. django_ledger/tests/base.py +134 -8
  44. django_ledger/tests/test_accounts.py +16 -0
  45. django_ledger/tests/test_auth.py +5 -7
  46. django_ledger/tests/test_bill.py +1 -1
  47. django_ledger/tests/test_chart_of_accounts.py +46 -0
  48. django_ledger/tests/test_closing_entry.py +16 -19
  49. django_ledger/tests/test_entity.py +3 -3
  50. django_ledger/tests/test_io.py +192 -2
  51. django_ledger/tests/test_transactions.py +290 -0
  52. django_ledger/urls/account.py +18 -3
  53. django_ledger/urls/chart_of_accounts.py +21 -1
  54. django_ledger/urls/ledger.py +7 -0
  55. django_ledger/views/account.py +29 -4
  56. django_ledger/views/coa.py +79 -4
  57. django_ledger/views/djl_api.py +4 -1
  58. django_ledger/views/journal_entry.py +1 -1
  59. django_ledger/views/mixins.py +3 -0
  60. {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/METADATA +1 -1
  61. {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/RECORD +65 -59
  62. {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/AUTHORS.md +0 -0
  63. {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/LICENSE +0 -0
  64. {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/WHEEL +0 -0
  65. {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
- for entity_model in self.ENTITY_MODEL_QUERYSET:
21
- for y in [self.START_DATE.year, self.START_DATE.year + 1]:
22
- for m in range(1, 13):
23
- ce_model, ce_txs_list = entity_model.close_books_for_month(
24
- year=y,
25
- month=m,
26
- force_update=True)
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(5):
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(7): # previously 10
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(7):
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(3):
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
@@ -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 test_this_runs(self):
7
- self.assertEqual(True, True)
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")
@@ -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
- path('<slug:entity_slug>/create/',
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>/<slug:coa_slug>/update/', views.ChartOfAccountsUpdateView.as_view(), name='coa-update'),
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
  ]
@@ -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
  ]