django-ledger 0.7.11__py3-none-any.whl → 0.8.1__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 (139) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/context.py +12 -0
  3. django_ledger/forms/account.py +45 -46
  4. django_ledger/forms/bill.py +0 -4
  5. django_ledger/forms/closing_entry.py +13 -1
  6. django_ledger/forms/data_import.py +182 -63
  7. django_ledger/forms/estimate.py +3 -6
  8. django_ledger/forms/invoice.py +3 -7
  9. django_ledger/forms/item.py +10 -18
  10. django_ledger/forms/purchase_order.py +2 -4
  11. django_ledger/io/io_core.py +515 -400
  12. django_ledger/io/io_generator.py +7 -6
  13. django_ledger/io/io_library.py +1 -2
  14. django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
  15. django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
  16. django_ledger/models/__init__.py +2 -1
  17. django_ledger/models/accounts.py +109 -69
  18. django_ledger/models/bank_account.py +40 -23
  19. django_ledger/models/bill.py +386 -333
  20. django_ledger/models/chart_of_accounts.py +173 -105
  21. django_ledger/models/closing_entry.py +99 -48
  22. django_ledger/models/customer.py +100 -66
  23. django_ledger/models/data_import.py +818 -323
  24. django_ledger/models/deprecations.py +61 -0
  25. django_ledger/models/entity.py +891 -644
  26. django_ledger/models/estimate.py +57 -28
  27. django_ledger/models/invoice.py +46 -26
  28. django_ledger/models/items.py +503 -142
  29. django_ledger/models/journal_entry.py +61 -47
  30. django_ledger/models/ledger.py +106 -42
  31. django_ledger/models/mixins.py +424 -281
  32. django_ledger/models/purchase_order.py +39 -17
  33. django_ledger/models/receipt.py +1083 -0
  34. django_ledger/models/transactions.py +242 -139
  35. django_ledger/models/unit.py +93 -54
  36. django_ledger/models/utils.py +12 -2
  37. django_ledger/models/vendor.py +121 -70
  38. django_ledger/report/core.py +2 -14
  39. django_ledger/settings.py +57 -71
  40. django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
  41. django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +25 -0
  42. django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
  43. django_ledger/static/django_ledger/css/djl_styles.css +273 -0
  44. django_ledger/templates/django_ledger/bills/includes/card_bill.html +2 -2
  45. django_ledger/templates/django_ledger/components/menu.html +41 -26
  46. django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
  47. django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
  48. django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
  49. django_ledger/templates/django_ledger/customer/tags/customer_table.html +8 -6
  50. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
  51. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
  52. django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
  53. django_ledger/templates/django_ledger/entity/includes/card_entity.html +12 -6
  54. django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -1
  55. django_ledger/templates/django_ledger/financial_statements/cash_flow.html +4 -1
  56. django_ledger/templates/django_ledger/financial_statements/income_statement.html +4 -1
  57. django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +27 -3
  58. django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +16 -4
  59. django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +73 -18
  60. django_ledger/templates/django_ledger/includes/widget_ratios.html +18 -24
  61. django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +3 -3
  62. django_ledger/templates/django_ledger/layouts/base.html +7 -2
  63. django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
  64. django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
  65. django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
  66. django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
  67. django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
  68. django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
  69. django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +12 -7
  70. django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
  71. django_ledger/templatetags/django_ledger.py +338 -191
  72. django_ledger/tests/test_accounts.py +1 -2
  73. django_ledger/tests/test_io.py +17 -0
  74. django_ledger/tests/test_purchase_order.py +3 -3
  75. django_ledger/tests/test_transactions.py +1 -2
  76. django_ledger/urls/__init__.py +1 -4
  77. django_ledger/urls/customer.py +3 -0
  78. django_ledger/urls/data_import.py +3 -0
  79. django_ledger/urls/receipt.py +102 -0
  80. django_ledger/urls/vendor.py +1 -0
  81. django_ledger/views/__init__.py +1 -0
  82. django_ledger/views/bill.py +8 -11
  83. django_ledger/views/chart_of_accounts.py +6 -4
  84. django_ledger/views/closing_entry.py +11 -7
  85. django_ledger/views/customer.py +68 -30
  86. django_ledger/views/data_import.py +120 -66
  87. django_ledger/views/djl_api.py +3 -5
  88. django_ledger/views/entity.py +2 -4
  89. django_ledger/views/estimate.py +3 -7
  90. django_ledger/views/inventory.py +3 -5
  91. django_ledger/views/invoice.py +4 -6
  92. django_ledger/views/item.py +7 -11
  93. django_ledger/views/journal_entry.py +1 -2
  94. django_ledger/views/mixins.py +125 -93
  95. django_ledger/views/purchase_order.py +24 -35
  96. django_ledger/views/receipt.py +294 -0
  97. django_ledger/views/unit.py +1 -2
  98. django_ledger/views/vendor.py +54 -16
  99. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/METADATA +43 -75
  100. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/RECORD +104 -122
  101. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +1 -1
  102. django_ledger-0.8.1.dist-info/top_level.txt +1 -0
  103. django_ledger/contrib/django_ledger_graphene/__init__.py +0 -0
  104. django_ledger/contrib/django_ledger_graphene/accounts/schema.py +0 -33
  105. django_ledger/contrib/django_ledger_graphene/api.py +0 -42
  106. django_ledger/contrib/django_ledger_graphene/apps.py +0 -6
  107. django_ledger/contrib/django_ledger_graphene/auth/mutations.py +0 -49
  108. django_ledger/contrib/django_ledger_graphene/auth/schema.py +0 -6
  109. django_ledger/contrib/django_ledger_graphene/bank_account/mutations.py +0 -61
  110. django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +0 -34
  111. django_ledger/contrib/django_ledger_graphene/bill/mutations.py +0 -0
  112. django_ledger/contrib/django_ledger_graphene/bill/schema.py +0 -34
  113. django_ledger/contrib/django_ledger_graphene/coa/mutations.py +0 -0
  114. django_ledger/contrib/django_ledger_graphene/coa/schema.py +0 -30
  115. django_ledger/contrib/django_ledger_graphene/customers/__init__.py +0 -0
  116. django_ledger/contrib/django_ledger_graphene/customers/mutations.py +0 -71
  117. django_ledger/contrib/django_ledger_graphene/customers/schema.py +0 -43
  118. django_ledger/contrib/django_ledger_graphene/data_import/mutations.py +0 -0
  119. django_ledger/contrib/django_ledger_graphene/data_import/schema.py +0 -0
  120. django_ledger/contrib/django_ledger_graphene/entity/mutations.py +0 -0
  121. django_ledger/contrib/django_ledger_graphene/entity/schema.py +0 -94
  122. django_ledger/contrib/django_ledger_graphene/item/mutations.py +0 -0
  123. django_ledger/contrib/django_ledger_graphene/item/schema.py +0 -31
  124. django_ledger/contrib/django_ledger_graphene/journal_entry/mutations.py +0 -0
  125. django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +0 -35
  126. django_ledger/contrib/django_ledger_graphene/ledger/mutations.py +0 -0
  127. django_ledger/contrib/django_ledger_graphene/ledger/schema.py +0 -32
  128. django_ledger/contrib/django_ledger_graphene/purchase_order/mutations.py +0 -0
  129. django_ledger/contrib/django_ledger_graphene/purchase_order/schema.py +0 -31
  130. django_ledger/contrib/django_ledger_graphene/transaction/mutations.py +0 -0
  131. django_ledger/contrib/django_ledger_graphene/transaction/schema.py +0 -36
  132. django_ledger/contrib/django_ledger_graphene/unit/mutations.py +0 -0
  133. django_ledger/contrib/django_ledger_graphene/unit/schema.py +0 -27
  134. django_ledger/contrib/django_ledger_graphene/vendor/mutations.py +0 -0
  135. django_ledger/contrib/django_ledger_graphene/vendor/schema.py +0 -37
  136. django_ledger/contrib/django_ledger_graphene/views.py +0 -12
  137. django_ledger-0.7.11.dist-info/top_level.txt +0 -4
  138. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/AUTHORS.md +0 -0
  139. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/LICENSE +0 -0
@@ -17,8 +17,9 @@ The TransactionModel, together with the IOMixIn, is essential for ensuring seaml
17
17
  financial statement production in the Django Ledger framework.
18
18
  """
19
19
 
20
+ import warnings
20
21
  from datetime import datetime, date
21
- from typing import List, Union, Optional, Set
22
+ from typing import List, Union, Set
22
23
  from uuid import uuid4, UUID
23
24
 
24
25
  from django.contrib.auth import get_user_model
@@ -27,13 +28,22 @@ from django.core.validators import MinValueValidator
27
28
  from django.db import models
28
29
  from django.db.models import Q, QuerySet, Manager, F
29
30
  from django.db.models.signals import pre_save
31
+ from django.urls import reverse
30
32
  from django.utils.translation import gettext_lazy as _
31
33
 
32
34
  from django_ledger.io.io_core import validate_io_timestamp
33
- from django_ledger.models import AccountModel, BillModel, EntityModel, InvoiceModel, LedgerModel
35
+ from django_ledger.models import (
36
+ AccountModel,
37
+ BillModel,
38
+ EntityModel,
39
+ InvoiceModel,
40
+ LedgerModel,
41
+ )
42
+ from django_ledger.models.deprecations import deprecated_entity_slug_behavior
34
43
  from django_ledger.models.mixins import CreateUpdateMixIn
35
44
  from django_ledger.models.unit import EntityUnitModel
36
45
  from django_ledger.models.utils import lazy_loader
46
+ from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
37
47
 
38
48
  UserModel = get_user_model()
39
49
 
@@ -49,7 +59,37 @@ class TransactionModelQuerySet(QuerySet):
49
59
  based on common use cases.
50
60
  """
51
61
 
52
- def posted(self) -> QuerySet:
62
+ def for_user(self, user_model) -> 'TransactionModelQuerySet':
63
+ """
64
+ Filters transactions accessible to a specific user based on their permissions.
65
+
66
+ Parameters
67
+ ----------
68
+ user_model : UserModel
69
+ The user object for which the transactions should be filtered.
70
+
71
+ Returns
72
+ -------
73
+ TransactionModelQuerySet
74
+ A queryset containing transactions filtered by the user's access level.
75
+
76
+ Description
77
+ -----------
78
+ - Returns all `TransactionModel` objects for superusers.
79
+ - For regular users, it filters transactions where:
80
+ - The user is an admin of the entity associated with the ledger in the transaction.
81
+ - The user is a manager of the entity associated with the ledger in the transaction.
82
+ """
83
+
84
+ if user_model.is_superuser:
85
+ return self
86
+
87
+ return self.filter(
88
+ Q(journal_entry__ledger__entity__admin=user_model)
89
+ | Q(journal_entry__ledger__entity__managers__in=[user_model])
90
+ )
91
+
92
+ def posted(self) -> 'TransactionModelQuerySet':
53
93
  """
54
94
  Retrieves transactions that are part of a posted journal entry and ledger.
55
95
 
@@ -63,11 +103,12 @@ class TransactionModelQuerySet(QuerySet):
63
103
  A QuerySet containing only transactions that meet the "posted" criteria.
64
104
  """
65
105
  return self.filter(
66
- Q(journal_entry__posted=True) &
67
- Q(journal_entry__ledger__posted=True)
106
+ Q(journal_entry__posted=True) & Q(journal_entry__ledger__posted=True)
68
107
  )
69
108
 
70
- def for_accounts(self, account_list: List[Union[AccountModel, str, UUID]]):
109
+ def for_accounts(
110
+ self, account_list: List[Union[AccountModel, str, UUID]]
111
+ ) -> 'TransactionModelQuerySet':
71
112
  """
72
113
  Filters transactions based on the accounts they are associated with.
73
114
 
@@ -85,7 +126,9 @@ class TransactionModelQuerySet(QuerySet):
85
126
 
86
127
  if not isinstance(account_list, list) or not len(account_list) > 0:
87
128
  raise TransactionModelValidationError(
88
- message=_('Account list must be a list of AccountModel, UUID or str objects (codes).')
129
+ message=_(
130
+ 'Account list must be a list of AccountModel, UUID or str objects (codes).'
131
+ )
89
132
  )
90
133
  if isinstance(account_list[0], str):
91
134
  return self.filter(account__code__in=account_list)
@@ -94,10 +137,14 @@ class TransactionModelQuerySet(QuerySet):
94
137
  elif isinstance(account_list[0], AccountModel):
95
138
  return self.filter(account__in=account_list)
96
139
  raise TransactionModelValidationError(
97
- message=_('Account list must be a list of AccountModel, UUID or str objects (codes).')
140
+ message=_(
141
+ 'Account list must be a list of AccountModel, UUID or str objects (codes).'
142
+ )
98
143
  )
99
144
 
100
- def for_roles(self, role_list: Union[str, List[str], Set[str]]):
145
+ def for_roles(
146
+ self, role_list: Union[str, List[str], Set[str]]
147
+ ) -> 'TransactionModelQuerySet':
101
148
  """
102
149
  Fetches a QuerySet of TransactionModels which AccountModel has a specific role.
103
150
 
@@ -115,7 +162,9 @@ class TransactionModelQuerySet(QuerySet):
115
162
  return self.filter(account__role__in=[role_list])
116
163
  return self.filter(account__role__in=role_list)
117
164
 
118
- def for_unit(self, unit_slug: Union[str, EntityUnitModel]):
165
+ def for_unit(
166
+ self, unit_slug: Union[str, EntityUnitModel]
167
+ ) -> 'TransactionModelQuerySet':
119
168
  """
120
169
  Filters transactions based on their associated entity unit.
121
170
 
@@ -133,7 +182,9 @@ class TransactionModelQuerySet(QuerySet):
133
182
  return self.filter(journal_entry__entity_unit=unit_slug)
134
183
  return self.filter(journal_entry__entity_unit__slug__exact=unit_slug)
135
184
 
136
- def for_activity(self, activity_list: Union[str, List[str], Set[str]]):
185
+ def for_activity(
186
+ self, activity_list: Union[str, List[str], Set[str]]
187
+ ) -> 'TransactionModelQuerySet':
137
188
  """
138
189
  Filters transactions based on their associated activity or activities.
139
190
 
@@ -151,7 +202,9 @@ class TransactionModelQuerySet(QuerySet):
151
202
  return self.filter(journal_entry__activity__in=[activity_list])
152
203
  return self.filter(journal_entry__activity__in=activity_list)
153
204
 
154
- def to_date(self, to_date: Union[str, date, datetime]):
205
+ def to_date(
206
+ self, to_date: Union[str, date, datetime]
207
+ ) -> 'TransactionModelQuerySet':
155
208
  """
156
209
  Filters transactions occurring on or before a specific date or timestamp.
157
210
 
@@ -177,7 +230,9 @@ class TransactionModelQuerySet(QuerySet):
177
230
  return self.filter(journal_entry__timestamp__date__lte=to_date)
178
231
  return self.filter(journal_entry__timestamp__lte=to_date)
179
232
 
180
- def from_date(self, from_date: Union[str, date, datetime]):
233
+ def from_date(
234
+ self, from_date: Union[str, date, datetime]
235
+ ) -> 'TransactionModelQuerySet':
181
236
  """
182
237
  Filters transactions occurring on or after a specific date or timestamp.
183
238
 
@@ -203,7 +258,7 @@ class TransactionModelQuerySet(QuerySet):
203
258
 
204
259
  return self.filter(journal_entry__timestamp__gte=from_date)
205
260
 
206
- def not_closing_entry(self):
261
+ def not_closing_entry(self) -> 'TransactionModelQuerySet':
207
262
  """
208
263
  Filters transactions that are *not* part of a closing journal entry.
209
264
 
@@ -214,7 +269,7 @@ class TransactionModelQuerySet(QuerySet):
214
269
  """
215
270
  return self.filter(journal_entry__is_closing_entry=False)
216
271
 
217
- def is_closing_entry(self):
272
+ def is_closing_entry(self) -> 'TransactionModelQuerySet':
218
273
  """
219
274
  Filters transactions that are part of a closing journal entry.
220
275
 
@@ -225,7 +280,9 @@ class TransactionModelQuerySet(QuerySet):
225
280
  """
226
281
  return self.filter(journal_entry__is_closing_entry=True)
227
282
 
228
- def for_ledger(self, ledger_model: Union[LedgerModel, UUID, str]):
283
+ def for_ledger(
284
+ self, ledger_model: Union[LedgerModel, UUID, str]
285
+ ) -> 'TransactionModelQuerySet':
229
286
  """
230
287
  Filters transactions for a specific ledger under a given entity.
231
288
 
@@ -243,7 +300,7 @@ class TransactionModelQuerySet(QuerySet):
243
300
  return self.filter(journal_entry__ledger__uuid__exact=ledger_model)
244
301
  return self.filter(journal_entry__ledger=ledger_model)
245
302
 
246
- def for_journal_entry(self, je_model):
303
+ def for_journal_entry(self, je_model) -> 'TransactionModelQuerySet':
247
304
  """
248
305
  Filters transactions for a specific journal entry under a given ledger and entity.
249
306
 
@@ -261,7 +318,9 @@ class TransactionModelQuerySet(QuerySet):
261
318
  return self.filter(journal_entry=je_model)
262
319
  return self.filter(journal_entry__uuid__exact=je_model)
263
320
 
264
- def for_bill(self, bill_model: Union[BillModel, str, UUID]):
321
+ def for_bill(
322
+ self, bill_model: Union[BillModel, str, UUID]
323
+ ) -> 'TransactionModelQuerySet':
265
324
  """
266
325
  Filters transactions for a specific bill under a given entity.
267
326
 
@@ -279,7 +338,9 @@ class TransactionModelQuerySet(QuerySet):
279
338
  return self.filter(journal_entry__ledger__billmodel=bill_model)
280
339
  return self.filter(journal_entry__ledger__billmodel__uuid__exact=bill_model)
281
340
 
282
- def for_invoice(self, invoice_model: Union[InvoiceModel, str, UUID]):
341
+ def for_invoice(
342
+ self, invoice_model: Union[InvoiceModel, str, UUID]
343
+ ) -> 'TransactionModelQuerySet':
283
344
  """
284
345
  Filters transactions for a specific invoice under a given entity.
285
346
 
@@ -295,9 +356,11 @@ class TransactionModelQuerySet(QuerySet):
295
356
  """
296
357
  if isinstance(invoice_model, InvoiceModel):
297
358
  return self.filter(journal_entry__ledger__invoicemodel=invoice_model)
298
- return self.filter(journal_entry__ledger__invoicemodel__uuid__exact=invoice_model)
359
+ return self.filter(
360
+ journal_entry__ledger__invoicemodel__uuid__exact=invoice_model
361
+ )
299
362
 
300
- def with_annotated_details(self):
363
+ def with_annotated_details(self) -> 'TransactionModelQuerySet':
301
364
  return self.annotate(
302
365
  entity_unit_name=F('journal_entry__entity_unit__name'),
303
366
  account_code=F('account__code'),
@@ -305,16 +368,16 @@ class TransactionModelQuerySet(QuerySet):
305
368
  timestamp=F('journal_entry__timestamp'),
306
369
  )
307
370
 
308
- def is_cleared(self):
371
+ def is_cleared(self) -> 'TransactionModelQuerySet':
309
372
  return self.filter(cleared=True)
310
373
 
311
- def not_cleared(self):
374
+ def not_cleared(self) -> 'TransactionModelQuerySet':
312
375
  return self.filter(cleared=False)
313
376
 
314
- def is_reconciled(self):
377
+ def is_reconciled(self) -> 'TransactionModelQuerySet':
315
378
  return self.filter(reconciled=True)
316
379
 
317
- def not_reconciled(self):
380
+ def not_reconciled(self) -> 'TransactionModelQuerySet':
318
381
  return self.filter(reconciled=False)
319
382
 
320
383
 
@@ -340,54 +403,27 @@ class TransactionModelManager(Manager):
340
403
  """
341
404
  qs = TransactionModelQuerySet(self.model, using=self._db)
342
405
  return qs.annotate(
406
+ _entity_slug=F('journal_entry__ledger__entity__slug'),
407
+ _ledger_uuid=F('journal_entry__ledger_id'),
343
408
  timestamp=F('journal_entry__timestamp'),
344
- _coa_id=F('account__coa_model_id') # Annotates the `coa_model_id` from the related `account`.
409
+ _coa_id=F('account__coa_model_id'),
345
410
  ).select_related(
346
- 'journal_entry', # Pre-loads the related Journal Entry.
347
- 'account', # Pre-loads the Account associated with the Transaction.
348
- 'account__coa_model', # Pre-loads the Chart of Accounts related to the Account.
349
- )
350
-
351
- def for_user(self, user_model) -> TransactionModelQuerySet:
352
- """
353
- Filters transactions accessible to a specific user based on their permissions.
354
-
355
- Parameters
356
- ----------
357
- user_model : UserModel
358
- The user object for which the transactions should be filtered.
359
-
360
- Returns
361
- -------
362
- TransactionModelQuerySet
363
- A queryset containing transactions filtered by the user's access level.
364
-
365
- Description
366
- -----------
367
- - Returns all `TransactionModel` objects for superusers.
368
- - For regular users, it filters transactions where:
369
- - The user is an admin of the entity associated with the ledger in the transaction.
370
- - The user is a manager of the entity associated with the ledger in the transaction.
371
- """
372
- qs = self.get_queryset()
373
- return qs.filter(
374
- Q(journal_entry__ledger__entity__admin=user_model) |
375
- Q(journal_entry__ledger__entity__managers__in=[user_model])
411
+ 'journal_entry',
412
+ 'account',
413
+ 'account__coa_model',
376
414
  )
377
415
 
378
- def for_entity(self,
379
- entity_slug: Union[EntityModel, str, UUID],
380
- user_model: Optional[UserModel] = None) -> TransactionModelQuerySet:
416
+ @deprecated_entity_slug_behavior
417
+ def for_entity(
418
+ self, entity_model: EntityModel | str | UUID = None, **kwargs
419
+ ) -> TransactionModelQuerySet:
381
420
  """
382
421
  Filters transactions for a specific entity, optionally scoped to a specific user.
383
422
 
384
423
  Parameters
385
424
  ----------
386
- entity_slug : Union[EntityModel, str, UUID]
425
+ entity_model : Union[EntityModel, str, UUID]
387
426
  Identifier for the entity. This can be an `EntityModel` object, a slug (str), or a UUID.
388
- user_model : Optional[UserModel], optional
389
- The user for whom transactions should be filtered. If provided, applies user-specific
390
- filtering. Defaults to None.
391
427
 
392
428
  Returns
393
429
  -------
@@ -399,70 +435,85 @@ class TransactionModelManager(Manager):
399
435
  - If `user_model` is provided, only transactions accessible by the user are included.
400
436
  - Supports flexible filtering by accepting different forms of `entity_slug`.
401
437
  """
402
- if user_model:
403
- qs = self.for_user(user_model=user_model)
404
- else:
405
- qs = self.get_queryset()
406
438
 
407
- if isinstance(entity_slug, EntityModel):
408
- return qs.filter(journal_entry__ledger__entity=entity_slug)
409
- elif isinstance(entity_slug, UUID):
410
- return qs.filter(journal_entry__ledger__entity_id=entity_slug)
411
- return qs.filter(journal_entry__ledger__entity__slug__exact=entity_slug)
439
+ qs = self.get_queryset()
440
+ if 'user_model' in kwargs:
441
+ warnings.warn(
442
+ 'user_model parameter is deprecated and will be removed in a future release. '
443
+ 'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
444
+ DeprecationWarning,
445
+ stacklevel=2,
446
+ )
447
+ if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
448
+ qs = qs.for_user(kwargs['user_model'])
449
+
450
+ if isinstance(entity_model, EntityModel):
451
+ qs = qs.filter(journal_entry__ledger__entity=entity_model)
452
+ elif isinstance(entity_model, UUID):
453
+ qs = qs.filter(journal_entry__ledger__entity_id=entity_model)
454
+ elif isinstance(entity_model, str):
455
+ qs = qs.filter(journal_entry__ledger__entity__slug__exact=entity_model)
456
+ else:
457
+ raise TransactionModelValidationError(
458
+ message='entity_model parameter must be either an EntityModel, String or a UUID.'
459
+ )
460
+ return qs
412
461
 
413
462
 
414
463
  class TransactionModelAbstract(CreateUpdateMixIn):
415
464
  """
416
- Abstract model for representing a financial transaction in the ledger system.
417
-
418
- This model defines the core structure and behavior that every transaction record is
419
- expected to have, including fields like transaction type, associated account, amount,
420
- and additional metadata used for validation and functionality.
421
-
422
- Attributes:
423
- -----------
424
- Constants:
425
- - CREDIT: Constant representing a credit transaction.
426
- - DEBIT: Constant representing a debit transaction.
427
- - TX_TYPE: A list of choices providing options for transaction types, including CREDIT and DEBIT.
428
-
429
- Fields:
430
- - uuid (UUIDField): The unique identifier for the transaction. Automatically generated, non-editable, and primary key.
431
- - tx_type (CharField): Specifies the transaction type (CREDIT or DEBIT). Choices are based on the TX_TYPE constant. Maximum length is 10 characters.
432
- - journal_entry (ForeignKey): References the related journal entry from the `django_ledger.JournalEntryModel`.
433
- This field is not editable and is essential for linking transactions to journal entries.
434
- - account (ForeignKey): References the associated account from `django_ledger.AccountModel`. Protected from being deleted.
435
- - amount (DecimalField): Represents the transaction amount, up to 20 digits and 2 decimal places.
436
- The default value is 0.00, and it enforces a minimum value of 0.
437
- - description (CharField): Optional field for a brief description of the transaction.
438
- The maximum length is 100 characters.
439
- - cleared (BooleanField): Indicates whether the transaction has been cleared. Defaults to False.
440
- - reconciled (BooleanField): Indicates whether the transaction has been reconciled. Defaults to False.
441
- - objects (TransactionModelManager): Custom model manager providing advanced helper methods for querying and filtering transactions.
465
+ Abstract base model class for representing financial transactions in a ledger system.
466
+
467
+ This class defines the structure and behavior of transactions in a generic ledger system,
468
+ including attributes such as transaction type, associated account, amount, description,
469
+ and status flags for cleared and reconciled state. It also includes methods and properties
470
+ to facilitate queries and determine transaction type.
471
+
472
+ Attributes
473
+ ----------
474
+ uuid : UUID
475
+ Unique identifier of the transaction.
476
+ tx_type : str
477
+ Type of the transaction, either 'credit' or 'debit'.
478
+ journal_entry : ForeignKey
479
+ Reference to the associated JournalEntryModel instance for this transaction.
480
+ account : ForeignKey
481
+ Reference to the associated account in the Chart of Accounts.
482
+ amount : Decimal
483
+ Monetary amount for the transaction. Must be a non-negative value.
484
+ description : str, optional
485
+ Description of the transaction.
486
+ cleared : bool
487
+ Indicates if the transaction has been cleared. Defaults to False.
488
+ reconciled : bool
489
+ Indicates if the transaction has been reconciled. Defaults to False.
490
+ objects : TransactionModelManager
491
+ Custom manager for managing transaction models.
442
492
  """
443
493
 
444
494
  CREDIT = 'credit'
445
495
  DEBIT = 'debit'
446
- TX_TYPE = [
447
- (CREDIT, _('Credit')),
448
- (DEBIT, _('Debit'))
449
- ]
496
+ TX_TYPE = [(CREDIT, _('Credit')), (DEBIT, _('Debit'))]
450
497
 
451
498
  uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
452
- tx_type = models.CharField(max_length=10, choices=TX_TYPE, verbose_name=_('Transaction Type'))
499
+ tx_type = models.CharField(
500
+ max_length=10, choices=TX_TYPE, verbose_name=_('Transaction Type')
501
+ )
453
502
 
454
503
  journal_entry = models.ForeignKey(
455
504
  'django_ledger.JournalEntryModel',
456
505
  editable=False,
457
506
  verbose_name=_('Journal Entry'),
458
507
  help_text=_('Journal Entry to be associated with this transaction.'),
459
- on_delete=models.CASCADE
508
+ on_delete=models.CASCADE,
460
509
  )
461
510
  account = models.ForeignKey(
462
511
  'django_ledger.AccountModel',
463
512
  verbose_name=_('Account'),
464
- help_text=_('Account from Chart of Accounts to be associated with this transaction.'),
465
- on_delete=models.PROTECT
513
+ help_text=_(
514
+ 'Account from Chart of Accounts to be associated with this transaction.'
515
+ ),
516
+ on_delete=models.PROTECT,
466
517
  )
467
518
  amount = models.DecimalField(
468
519
  decimal_places=2,
@@ -470,18 +521,18 @@ class TransactionModelAbstract(CreateUpdateMixIn):
470
521
  default=0.00,
471
522
  verbose_name=_('Amount'),
472
523
  help_text=_('Amount of the transaction.'),
473
- validators=[MinValueValidator(0)]
524
+ validators=[MinValueValidator(0)],
474
525
  )
475
526
  description = models.CharField(
476
527
  max_length=100,
477
528
  null=True,
478
529
  blank=True,
479
530
  verbose_name=_('Transaction Description'),
480
- help_text=_('A description to be included with this individual transaction.')
531
+ help_text=_('A description to be included with this individual transaction.'),
481
532
  )
482
533
  cleared = models.BooleanField(default=False, verbose_name=_('Cleared'))
483
534
  reconciled = models.BooleanField(default=False, verbose_name=_('Reconciled'))
484
- objects = TransactionModelManager()
535
+ objects = TransactionModelManager.from_queryset(TransactionModelQuerySet)()
485
536
 
486
537
  class Meta:
487
538
  abstract = True
@@ -504,14 +555,39 @@ class TransactionModelAbstract(CreateUpdateMixIn):
504
555
  name=self.account.name,
505
556
  balance_type=self.account.balance_type,
506
557
  amount=self.amount,
507
- tx_type=self.tx_type
558
+ tx_type=self.tx_type,
508
559
  )
509
560
 
561
+ @property
562
+ def entity_slug(self) -> str:
563
+ try:
564
+ return getattr(self, '_entity_slug')
565
+ except AttributeError:
566
+ pass
567
+ return self.journal_entry.ledger.entity.slug
568
+
569
+ @property
570
+ def ledger_uuid(self) -> UUID:
571
+ try:
572
+ return getattr(self, '_ledger_uuid')
573
+ except AttributeError:
574
+ pass
575
+ return self.journal_entry.ledger.uuid
576
+
510
577
  @property
511
578
  def coa_id(self):
512
579
  """
513
- Fetch the Chart of Accounts (CoA) ID associated with the transaction's account.
514
- Returns `None` if the account is not set.
580
+ Returns the Chart of Accounts (COA) ID associated with the current object.
581
+
582
+ The property attempts to retrieve the COA ID value stored internally, if it exists.
583
+ If the internal value is not set and the `account` attribute is not `None`,
584
+ the COA ID is obtained from the `account.coa_model_id` attribute. Otherwise,
585
+ it will return `None`.
586
+
587
+ Returns
588
+ -------
589
+ Any or None
590
+ The COA ID if it exists, otherwise `None`.
515
591
  """
516
592
  try:
517
593
  return getattr(self, '_coa_id')
@@ -520,12 +596,53 @@ class TransactionModelAbstract(CreateUpdateMixIn):
520
596
  return None
521
597
  return self.account.coa_model_id
522
598
 
523
- def is_debit(self):
599
+ def is_debit(self) -> bool:
600
+ """
601
+ Determines if the transaction type is a debit.
602
+
603
+ This method checks whether the transaction type of the current instance is
604
+ set to a debit type.
605
+
606
+ Returns
607
+ -------
608
+ bool
609
+ True if the transaction type is debit, otherwise False.
610
+ """
524
611
  return self.tx_type == self.DEBIT
525
612
 
526
- def is_credit(self):
613
+ def is_credit(self) -> bool:
614
+ """
615
+ Check if the transaction is of type 'credit'.
616
+
617
+ This method evaluates whether the transaction type of the current instance
618
+ matches the 'credit' transaction type.
619
+
620
+ Returns
621
+ -------
622
+ bool
623
+ True if the transaction type is 'credit', False otherwise.
624
+ """
527
625
  return self.tx_type == self.CREDIT
528
626
 
627
+ def get_ledger_detailr_url(self) -> str:
628
+ return reverse(
629
+ viewname='django_ledger:ledger-detail',
630
+ kwargs={
631
+ 'ledger_pk': self.ledger_uuid,
632
+ 'entity_slug': self.entity_slug,
633
+ },
634
+ )
635
+
636
+ def get_journal_entry_detail_url(self) -> str:
637
+ return reverse(
638
+ viewname='django_ledger:je-detail',
639
+ kwargs={
640
+ 'je_pk': self.journal_entry_id,
641
+ 'entity_slug': self.entity_slug,
642
+ 'ledger_pk': self.ledger_uuid,
643
+ },
644
+ )
645
+
529
646
 
530
647
  class TransactionModel(TransactionModelAbstract):
531
648
  """
@@ -543,15 +660,6 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs):
543
660
  This function is executed before saving a `TransactionModel` instance,
544
661
  ensuring that certain conditions are met to maintain data integrity.
545
662
 
546
- Parameters
547
- ----------
548
- instance : TransactionModel
549
- The `TransactionModel` instance that is about to be saved.
550
- kwargs : dict
551
- Additional keyword arguments, such as the optional `bypass_account_state`.
552
-
553
- Validations
554
- -----------
555
663
  The function performs the following validations:
556
664
  1. **Account Transactionality**:
557
665
  If the `bypass_account_state` flag is not provided or set to `False`,
@@ -565,6 +673,13 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs):
565
673
  the transaction cannot be modified. The save process is halted if the
566
674
  journal entry is marked as locked.
567
675
 
676
+ Parameters
677
+ ----------
678
+ instance : TransactionModel
679
+ The `TransactionModel` instance that is about to be saved.
680
+ kwargs : dict
681
+ Additional keyword arguments, such as the optional `bypass_account_state`.
682
+
568
683
  Raises
569
684
  ------
570
685
  TransactionModelValidationError
@@ -578,17 +693,6 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs):
578
693
  When the associated journal entry (`instance.journal_entry`) is locked,
579
694
  preventing modification of any related transactions. The error message
580
695
  describes the locked journal entry constraint.
581
-
582
- Example
583
- -------
584
- ```python
585
- instance = TransactionModel(...)
586
- try:
587
- transactionmodel_presave(instance)
588
- instance.save() # Save proceeds if no validation error occurs
589
- except TransactionModelValidationError as e:
590
- handle_error(str(e)) # Handle validation exception
591
- ```
592
696
  """
593
697
  bypass_account_state = kwargs.get('bypass_account_state', False)
594
698
 
@@ -597,12 +701,11 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs):
597
701
  message=_('Transactions cannot be linked to root accounts.')
598
702
  )
599
703
 
600
- if all([
601
- not bypass_account_state,
602
- not instance.account.can_transact()
603
- ]):
704
+ if all([not bypass_account_state, not instance.account.can_transact()]):
604
705
  raise TransactionModelValidationError(
605
- message=_(f'Cannot create or modify transactions on account model {instance.account}.')
706
+ message=_(
707
+ f'Cannot create or modify transactions on account model {instance.account}.'
708
+ )
606
709
  )
607
710
  if instance.journal_entry.is_locked():
608
711
  raise TransactionModelValidationError(