django-ledger 0.7.11__py3-none-any.whl → 0.8.0__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 (114) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/context.py +12 -0
  3. django_ledger/forms/bill.py +0 -4
  4. django_ledger/forms/closing_entry.py +13 -1
  5. django_ledger/forms/data_import.py +1 -1
  6. django_ledger/forms/estimate.py +3 -6
  7. django_ledger/forms/invoice.py +3 -7
  8. django_ledger/forms/item.py +10 -18
  9. django_ledger/forms/purchase_order.py +2 -4
  10. django_ledger/io/io_core.py +8 -26
  11. django_ledger/io/io_generator.py +7 -6
  12. django_ledger/io/io_library.py +1 -2
  13. django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
  14. django_ledger/models/accounts.py +109 -69
  15. django_ledger/models/bank_account.py +40 -23
  16. django_ledger/models/bill.py +79 -63
  17. django_ledger/models/chart_of_accounts.py +173 -105
  18. django_ledger/models/closing_entry.py +99 -48
  19. django_ledger/models/customer.py +60 -39
  20. django_ledger/models/data_import.py +55 -41
  21. django_ledger/models/deprecations.py +61 -0
  22. django_ledger/models/entity.py +18 -16
  23. django_ledger/models/estimate.py +57 -28
  24. django_ledger/models/invoice.py +46 -26
  25. django_ledger/models/items.py +503 -142
  26. django_ledger/models/journal_entry.py +61 -47
  27. django_ledger/models/ledger.py +106 -42
  28. django_ledger/models/mixins.py +5 -3
  29. django_ledger/models/purchase_order.py +39 -17
  30. django_ledger/models/transactions.py +152 -113
  31. django_ledger/models/unit.py +57 -30
  32. django_ledger/models/vendor.py +75 -43
  33. django_ledger/report/core.py +2 -14
  34. django_ledger/settings.py +56 -71
  35. django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
  36. django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +25 -0
  37. django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
  38. django_ledger/static/django_ledger/css/djl_styles.css +273 -0
  39. django_ledger/templates/django_ledger/bills/includes/card_bill.html +2 -2
  40. django_ledger/templates/django_ledger/components/menu.html +41 -26
  41. django_ledger/templates/django_ledger/customer/tags/customer_table.html +5 -5
  42. django_ledger/templates/django_ledger/entity/includes/card_entity.html +12 -6
  43. django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -1
  44. django_ledger/templates/django_ledger/financial_statements/cash_flow.html +4 -1
  45. django_ledger/templates/django_ledger/financial_statements/income_statement.html +4 -1
  46. django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +27 -3
  47. django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +16 -4
  48. django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +73 -18
  49. django_ledger/templates/django_ledger/includes/widget_ratios.html +18 -24
  50. django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +3 -3
  51. django_ledger/templates/django_ledger/layouts/base.html +6 -1
  52. django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +9 -5
  53. django_ledger/tests/test_accounts.py +1 -2
  54. django_ledger/tests/test_io.py +17 -0
  55. django_ledger/tests/test_purchase_order.py +3 -3
  56. django_ledger/tests/test_transactions.py +1 -2
  57. django_ledger/urls/__init__.py +0 -4
  58. django_ledger/views/bill.py +8 -11
  59. django_ledger/views/chart_of_accounts.py +6 -4
  60. django_ledger/views/closing_entry.py +11 -7
  61. django_ledger/views/customer.py +13 -17
  62. django_ledger/views/data_import.py +7 -6
  63. django_ledger/views/djl_api.py +3 -5
  64. django_ledger/views/entity.py +2 -4
  65. django_ledger/views/estimate.py +3 -7
  66. django_ledger/views/inventory.py +3 -5
  67. django_ledger/views/invoice.py +4 -6
  68. django_ledger/views/item.py +7 -11
  69. django_ledger/views/journal_entry.py +1 -2
  70. django_ledger/views/mixins.py +25 -19
  71. django_ledger/views/purchase_order.py +24 -35
  72. django_ledger/views/unit.py +1 -2
  73. django_ledger/views/vendor.py +1 -2
  74. {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info}/METADATA +43 -75
  75. {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info}/RECORD +79 -108
  76. {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info}/WHEEL +1 -1
  77. django_ledger-0.8.0.dist-info/top_level.txt +1 -0
  78. django_ledger/contrib/django_ledger_graphene/__init__.py +0 -0
  79. django_ledger/contrib/django_ledger_graphene/accounts/schema.py +0 -33
  80. django_ledger/contrib/django_ledger_graphene/api.py +0 -42
  81. django_ledger/contrib/django_ledger_graphene/apps.py +0 -6
  82. django_ledger/contrib/django_ledger_graphene/auth/mutations.py +0 -49
  83. django_ledger/contrib/django_ledger_graphene/auth/schema.py +0 -6
  84. django_ledger/contrib/django_ledger_graphene/bank_account/mutations.py +0 -61
  85. django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +0 -34
  86. django_ledger/contrib/django_ledger_graphene/bill/mutations.py +0 -0
  87. django_ledger/contrib/django_ledger_graphene/bill/schema.py +0 -34
  88. django_ledger/contrib/django_ledger_graphene/coa/mutations.py +0 -0
  89. django_ledger/contrib/django_ledger_graphene/coa/schema.py +0 -30
  90. django_ledger/contrib/django_ledger_graphene/customers/__init__.py +0 -0
  91. django_ledger/contrib/django_ledger_graphene/customers/mutations.py +0 -71
  92. django_ledger/contrib/django_ledger_graphene/customers/schema.py +0 -43
  93. django_ledger/contrib/django_ledger_graphene/data_import/mutations.py +0 -0
  94. django_ledger/contrib/django_ledger_graphene/data_import/schema.py +0 -0
  95. django_ledger/contrib/django_ledger_graphene/entity/mutations.py +0 -0
  96. django_ledger/contrib/django_ledger_graphene/entity/schema.py +0 -94
  97. django_ledger/contrib/django_ledger_graphene/item/mutations.py +0 -0
  98. django_ledger/contrib/django_ledger_graphene/item/schema.py +0 -31
  99. django_ledger/contrib/django_ledger_graphene/journal_entry/mutations.py +0 -0
  100. django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +0 -35
  101. django_ledger/contrib/django_ledger_graphene/ledger/mutations.py +0 -0
  102. django_ledger/contrib/django_ledger_graphene/ledger/schema.py +0 -32
  103. django_ledger/contrib/django_ledger_graphene/purchase_order/mutations.py +0 -0
  104. django_ledger/contrib/django_ledger_graphene/purchase_order/schema.py +0 -31
  105. django_ledger/contrib/django_ledger_graphene/transaction/mutations.py +0 -0
  106. django_ledger/contrib/django_ledger_graphene/transaction/schema.py +0 -36
  107. django_ledger/contrib/django_ledger_graphene/unit/mutations.py +0 -0
  108. django_ledger/contrib/django_ledger_graphene/unit/schema.py +0 -27
  109. django_ledger/contrib/django_ledger_graphene/vendor/mutations.py +0 -0
  110. django_ledger/contrib/django_ledger_graphene/vendor/schema.py +0 -37
  111. django_ledger/contrib/django_ledger_graphene/views.py +0 -12
  112. django_ledger-0.7.11.dist-info/top_level.txt +0 -4
  113. {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info/licenses}/AUTHORS.md +0 -0
  114. {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info/licenses}/LICENSE +0 -0
@@ -16,9 +16,9 @@ the process of generating accurate financial reports.
16
16
  The TransactionModel, together with the IOMixIn, is essential for ensuring seamless, efficient, and reliable
17
17
  financial statement production in the Django Ledger framework.
18
18
  """
19
-
19
+ import warnings
20
20
  from datetime import datetime, date
21
- from typing import List, Union, Optional, Set
21
+ from typing import List, Union, Set
22
22
  from uuid import uuid4, UUID
23
23
 
24
24
  from django.contrib.auth import get_user_model
@@ -30,10 +30,18 @@ from django.db.models.signals import pre_save
30
30
  from django.utils.translation import gettext_lazy as _
31
31
 
32
32
  from django_ledger.io.io_core import validate_io_timestamp
33
- from django_ledger.models import AccountModel, BillModel, EntityModel, InvoiceModel, LedgerModel
33
+ from django_ledger.models import (
34
+ AccountModel,
35
+ BillModel,
36
+ EntityModel,
37
+ InvoiceModel,
38
+ LedgerModel
39
+ )
40
+ from django_ledger.models.deprecations import deprecated_entity_slug_behavior
34
41
  from django_ledger.models.mixins import CreateUpdateMixIn
35
42
  from django_ledger.models.unit import EntityUnitModel
36
43
  from django_ledger.models.utils import lazy_loader
44
+ from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
37
45
 
38
46
  UserModel = get_user_model()
39
47
 
@@ -49,7 +57,37 @@ class TransactionModelQuerySet(QuerySet):
49
57
  based on common use cases.
50
58
  """
51
59
 
52
- def posted(self) -> QuerySet:
60
+ def for_user(self, user_model) -> 'TransactionModelQuerySet':
61
+ """
62
+ Filters transactions accessible to a specific user based on their permissions.
63
+
64
+ Parameters
65
+ ----------
66
+ user_model : UserModel
67
+ The user object for which the transactions should be filtered.
68
+
69
+ Returns
70
+ -------
71
+ TransactionModelQuerySet
72
+ A queryset containing transactions filtered by the user's access level.
73
+
74
+ Description
75
+ -----------
76
+ - Returns all `TransactionModel` objects for superusers.
77
+ - For regular users, it filters transactions where:
78
+ - The user is an admin of the entity associated with the ledger in the transaction.
79
+ - The user is a manager of the entity associated with the ledger in the transaction.
80
+ """
81
+
82
+ if user_model.is_superuser:
83
+ return self
84
+
85
+ return self.filter(
86
+ Q(journal_entry__ledger__entity__admin=user_model) |
87
+ Q(journal_entry__ledger__entity__managers__in=[user_model])
88
+ )
89
+
90
+ def posted(self) -> 'TransactionModelQuerySet':
53
91
  """
54
92
  Retrieves transactions that are part of a posted journal entry and ledger.
55
93
 
@@ -67,7 +105,7 @@ class TransactionModelQuerySet(QuerySet):
67
105
  Q(journal_entry__ledger__posted=True)
68
106
  )
69
107
 
70
- def for_accounts(self, account_list: List[Union[AccountModel, str, UUID]]):
108
+ def for_accounts(self, account_list: List[Union[AccountModel, str, UUID]]) -> 'TransactionModelQuerySet':
71
109
  """
72
110
  Filters transactions based on the accounts they are associated with.
73
111
 
@@ -97,7 +135,7 @@ class TransactionModelQuerySet(QuerySet):
97
135
  message=_('Account list must be a list of AccountModel, UUID or str objects (codes).')
98
136
  )
99
137
 
100
- def for_roles(self, role_list: Union[str, List[str], Set[str]]):
138
+ def for_roles(self, role_list: Union[str, List[str], Set[str]]) -> 'TransactionModelQuerySet':
101
139
  """
102
140
  Fetches a QuerySet of TransactionModels which AccountModel has a specific role.
103
141
 
@@ -115,7 +153,7 @@ class TransactionModelQuerySet(QuerySet):
115
153
  return self.filter(account__role__in=[role_list])
116
154
  return self.filter(account__role__in=role_list)
117
155
 
118
- def for_unit(self, unit_slug: Union[str, EntityUnitModel]):
156
+ def for_unit(self, unit_slug: Union[str, EntityUnitModel]) -> 'TransactionModelQuerySet':
119
157
  """
120
158
  Filters transactions based on their associated entity unit.
121
159
 
@@ -133,7 +171,7 @@ class TransactionModelQuerySet(QuerySet):
133
171
  return self.filter(journal_entry__entity_unit=unit_slug)
134
172
  return self.filter(journal_entry__entity_unit__slug__exact=unit_slug)
135
173
 
136
- def for_activity(self, activity_list: Union[str, List[str], Set[str]]):
174
+ def for_activity(self, activity_list: Union[str, List[str], Set[str]]) -> 'TransactionModelQuerySet':
137
175
  """
138
176
  Filters transactions based on their associated activity or activities.
139
177
 
@@ -151,7 +189,7 @@ class TransactionModelQuerySet(QuerySet):
151
189
  return self.filter(journal_entry__activity__in=[activity_list])
152
190
  return self.filter(journal_entry__activity__in=activity_list)
153
191
 
154
- def to_date(self, to_date: Union[str, date, datetime]):
192
+ def to_date(self, to_date: Union[str, date, datetime]) -> 'TransactionModelQuerySet':
155
193
  """
156
194
  Filters transactions occurring on or before a specific date or timestamp.
157
195
 
@@ -177,7 +215,7 @@ class TransactionModelQuerySet(QuerySet):
177
215
  return self.filter(journal_entry__timestamp__date__lte=to_date)
178
216
  return self.filter(journal_entry__timestamp__lte=to_date)
179
217
 
180
- def from_date(self, from_date: Union[str, date, datetime]):
218
+ def from_date(self, from_date: Union[str, date, datetime]) -> 'TransactionModelQuerySet':
181
219
  """
182
220
  Filters transactions occurring on or after a specific date or timestamp.
183
221
 
@@ -203,7 +241,7 @@ class TransactionModelQuerySet(QuerySet):
203
241
 
204
242
  return self.filter(journal_entry__timestamp__gte=from_date)
205
243
 
206
- def not_closing_entry(self):
244
+ def not_closing_entry(self) -> 'TransactionModelQuerySet':
207
245
  """
208
246
  Filters transactions that are *not* part of a closing journal entry.
209
247
 
@@ -214,7 +252,7 @@ class TransactionModelQuerySet(QuerySet):
214
252
  """
215
253
  return self.filter(journal_entry__is_closing_entry=False)
216
254
 
217
- def is_closing_entry(self):
255
+ def is_closing_entry(self) -> 'TransactionModelQuerySet':
218
256
  """
219
257
  Filters transactions that are part of a closing journal entry.
220
258
 
@@ -225,7 +263,7 @@ class TransactionModelQuerySet(QuerySet):
225
263
  """
226
264
  return self.filter(journal_entry__is_closing_entry=True)
227
265
 
228
- def for_ledger(self, ledger_model: Union[LedgerModel, UUID, str]):
266
+ def for_ledger(self, ledger_model: Union[LedgerModel, UUID, str]) -> 'TransactionModelQuerySet':
229
267
  """
230
268
  Filters transactions for a specific ledger under a given entity.
231
269
 
@@ -243,7 +281,7 @@ class TransactionModelQuerySet(QuerySet):
243
281
  return self.filter(journal_entry__ledger__uuid__exact=ledger_model)
244
282
  return self.filter(journal_entry__ledger=ledger_model)
245
283
 
246
- def for_journal_entry(self, je_model):
284
+ def for_journal_entry(self, je_model) -> 'TransactionModelQuerySet':
247
285
  """
248
286
  Filters transactions for a specific journal entry under a given ledger and entity.
249
287
 
@@ -261,7 +299,7 @@ class TransactionModelQuerySet(QuerySet):
261
299
  return self.filter(journal_entry=je_model)
262
300
  return self.filter(journal_entry__uuid__exact=je_model)
263
301
 
264
- def for_bill(self, bill_model: Union[BillModel, str, UUID]):
302
+ def for_bill(self, bill_model: Union[BillModel, str, UUID]) -> 'TransactionModelQuerySet':
265
303
  """
266
304
  Filters transactions for a specific bill under a given entity.
267
305
 
@@ -279,7 +317,7 @@ class TransactionModelQuerySet(QuerySet):
279
317
  return self.filter(journal_entry__ledger__billmodel=bill_model)
280
318
  return self.filter(journal_entry__ledger__billmodel__uuid__exact=bill_model)
281
319
 
282
- def for_invoice(self, invoice_model: Union[InvoiceModel, str, UUID]):
320
+ def for_invoice(self, invoice_model: Union[InvoiceModel, str, UUID]) -> 'TransactionModelQuerySet':
283
321
  """
284
322
  Filters transactions for a specific invoice under a given entity.
285
323
 
@@ -297,7 +335,7 @@ class TransactionModelQuerySet(QuerySet):
297
335
  return self.filter(journal_entry__ledger__invoicemodel=invoice_model)
298
336
  return self.filter(journal_entry__ledger__invoicemodel__uuid__exact=invoice_model)
299
337
 
300
- def with_annotated_details(self):
338
+ def with_annotated_details(self) -> 'TransactionModelQuerySet':
301
339
  return self.annotate(
302
340
  entity_unit_name=F('journal_entry__entity_unit__name'),
303
341
  account_code=F('account__code'),
@@ -305,16 +343,16 @@ class TransactionModelQuerySet(QuerySet):
305
343
  timestamp=F('journal_entry__timestamp'),
306
344
  )
307
345
 
308
- def is_cleared(self):
346
+ def is_cleared(self) -> 'TransactionModelQuerySet':
309
347
  return self.filter(cleared=True)
310
348
 
311
- def not_cleared(self):
349
+ def not_cleared(self) -> 'TransactionModelQuerySet':
312
350
  return self.filter(cleared=False)
313
351
 
314
- def is_reconciled(self):
352
+ def is_reconciled(self) -> 'TransactionModelQuerySet':
315
353
  return self.filter(reconciled=True)
316
354
 
317
- def not_reconciled(self):
355
+ def not_reconciled(self) -> 'TransactionModelQuerySet':
318
356
  return self.filter(reconciled=False)
319
357
 
320
358
 
@@ -348,46 +386,15 @@ class TransactionModelManager(Manager):
348
386
  'account__coa_model', # Pre-loads the Chart of Accounts related to the Account.
349
387
  )
350
388
 
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])
376
- )
377
-
378
- def for_entity(self,
379
- entity_slug: Union[EntityModel, str, UUID],
380
- user_model: Optional[UserModel] = None) -> TransactionModelQuerySet:
389
+ @deprecated_entity_slug_behavior
390
+ def for_entity(self, entity_model: EntityModel | str | UUID = None, **kwargs) -> TransactionModelQuerySet:
381
391
  """
382
392
  Filters transactions for a specific entity, optionally scoped to a specific user.
383
393
 
384
394
  Parameters
385
395
  ----------
386
- entity_slug : Union[EntityModel, str, UUID]
396
+ entity_model : Union[EntityModel, str, UUID]
387
397
  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
398
 
392
399
  Returns
393
400
  -------
@@ -399,46 +406,60 @@ class TransactionModelManager(Manager):
399
406
  - If `user_model` is provided, only transactions accessible by the user are included.
400
407
  - Supports flexible filtering by accepting different forms of `entity_slug`.
401
408
  """
402
- if user_model:
403
- qs = self.for_user(user_model=user_model)
404
- else:
405
- qs = self.get_queryset()
406
409
 
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)
410
+ qs = self.get_queryset()
411
+ if 'user_model' in kwargs:
412
+ warnings.warn(
413
+ 'user_model parameter is deprecated and will be removed in a future release. '
414
+ 'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
415
+ DeprecationWarning,
416
+ stacklevel=2
417
+ )
418
+ if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
419
+ qs = qs.for_user(kwargs['user_model'])
420
+
421
+ if isinstance(entity_model, EntityModel):
422
+ qs = qs.filter(journal_entry__ledger__entity=entity_model)
423
+ elif isinstance(entity_model, UUID):
424
+ qs = qs.filter(journal_entry__ledger__entity_id=entity_model)
425
+ elif isinstance(entity_model, str):
426
+ qs = qs.filter(journal_entry__ledger__entity__slug__exact=entity_model)
427
+ else:
428
+ raise TransactionModelValidationError(
429
+ message='entity_model parameter must be either an EntityModel, String or a UUID.'
430
+ )
431
+ return qs
412
432
 
413
433
 
414
434
  class TransactionModelAbstract(CreateUpdateMixIn):
415
435
  """
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.
436
+ Abstract base model class for representing financial transactions in a ledger system.
437
+
438
+ This class defines the structure and behavior of transactions in a generic ledger system,
439
+ including attributes such as transaction type, associated account, amount, description,
440
+ and status flags for cleared and reconciled state. It also includes methods and properties
441
+ to facilitate queries and determine transaction type.
442
+
443
+ Attributes
444
+ ----------
445
+ uuid : UUID
446
+ Unique identifier of the transaction.
447
+ tx_type : str
448
+ Type of the transaction, either 'credit' or 'debit'.
449
+ journal_entry : ForeignKey
450
+ Reference to the associated JournalEntryModel instance for this transaction.
451
+ account : ForeignKey
452
+ Reference to the associated account in the Chart of Accounts.
453
+ amount : Decimal
454
+ Monetary amount for the transaction. Must be a non-negative value.
455
+ description : str, optional
456
+ Description of the transaction.
457
+ cleared : bool
458
+ Indicates if the transaction has been cleared. Defaults to False.
459
+ reconciled : bool
460
+ Indicates if the transaction has been reconciled. Defaults to False.
461
+ objects : TransactionModelManager
462
+ Custom manager for managing transaction models.
442
463
  """
443
464
 
444
465
  CREDIT = 'credit'
@@ -481,7 +502,7 @@ class TransactionModelAbstract(CreateUpdateMixIn):
481
502
  )
482
503
  cleared = models.BooleanField(default=False, verbose_name=_('Cleared'))
483
504
  reconciled = models.BooleanField(default=False, verbose_name=_('Reconciled'))
484
- objects = TransactionModelManager()
505
+ objects = TransactionModelManager.from_queryset(TransactionModelQuerySet)()
485
506
 
486
507
  class Meta:
487
508
  abstract = True
@@ -510,8 +531,17 @@ class TransactionModelAbstract(CreateUpdateMixIn):
510
531
  @property
511
532
  def coa_id(self):
512
533
  """
513
- Fetch the Chart of Accounts (CoA) ID associated with the transaction's account.
514
- Returns `None` if the account is not set.
534
+ Returns the Chart of Accounts (COA) ID associated with the current object.
535
+
536
+ The property attempts to retrieve the COA ID value stored internally, if it exists.
537
+ If the internal value is not set and the `account` attribute is not `None`,
538
+ the COA ID is obtained from the `account.coa_model_id` attribute. Otherwise,
539
+ it will return `None`.
540
+
541
+ Returns
542
+ -------
543
+ Any or None
544
+ The COA ID if it exists, otherwise `None`.
515
545
  """
516
546
  try:
517
547
  return getattr(self, '_coa_id')
@@ -521,9 +551,31 @@ class TransactionModelAbstract(CreateUpdateMixIn):
521
551
  return self.account.coa_model_id
522
552
 
523
553
  def is_debit(self):
554
+ """
555
+ Determines if the transaction type is a debit.
556
+
557
+ This method checks whether the transaction type of the current instance is
558
+ set to a debit type.
559
+
560
+ Returns
561
+ -------
562
+ bool
563
+ True if the transaction type is debit, otherwise False.
564
+ """
524
565
  return self.tx_type == self.DEBIT
525
566
 
526
567
  def is_credit(self):
568
+ """
569
+ Check if the transaction is of type 'credit'.
570
+
571
+ This method evaluates whether the transaction type of the current instance
572
+ matches the 'credit' transaction type.
573
+
574
+ Returns
575
+ -------
576
+ bool
577
+ True if the transaction type is 'credit', False otherwise.
578
+ """
527
579
  return self.tx_type == self.CREDIT
528
580
 
529
581
 
@@ -543,15 +595,6 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs):
543
595
  This function is executed before saving a `TransactionModel` instance,
544
596
  ensuring that certain conditions are met to maintain data integrity.
545
597
 
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
598
  The function performs the following validations:
556
599
  1. **Account Transactionality**:
557
600
  If the `bypass_account_state` flag is not provided or set to `False`,
@@ -565,6 +608,13 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs):
565
608
  the transaction cannot be modified. The save process is halted if the
566
609
  journal entry is marked as locked.
567
610
 
611
+ Parameters
612
+ ----------
613
+ instance : TransactionModel
614
+ The `TransactionModel` instance that is about to be saved.
615
+ kwargs : dict
616
+ Additional keyword arguments, such as the optional `bypass_account_state`.
617
+
568
618
  Raises
569
619
  ------
570
620
  TransactionModelValidationError
@@ -578,17 +628,6 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs):
578
628
  When the associated journal entry (`instance.journal_entry`) is locked,
579
629
  preventing modification of any related transactions. The error message
580
630
  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
631
  """
593
632
  bypass_account_state = kwargs.get('bypass_account_state', False)
594
633
 
@@ -20,11 +20,11 @@ Key advantages of EntityUnits:
20
20
  flexibility to track inventory, expenses, or income associated with distinct
21
21
  business units.
22
22
  """
23
-
23
+ import warnings
24
24
  from random import choices
25
25
  from string import ascii_lowercase, digits, ascii_uppercase
26
26
  from typing import Optional
27
- from uuid import uuid4
27
+ from uuid import uuid4, UUID
28
28
 
29
29
  from django.core.exceptions import ValidationError
30
30
  from django.db import models
@@ -36,7 +36,9 @@ from treebeard.mp_tree import MP_Node, MP_NodeManager, MP_NodeQuerySet
36
36
 
37
37
  from django_ledger.io.io_core import IOMixIn
38
38
  from django_ledger.models import lazy_loader
39
+ from django_ledger.models.deprecations import deprecated_entity_slug_behavior
39
40
  from django_ledger.models.mixins import CreateUpdateMixIn, SlugNameMixIn
41
+ from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
40
42
 
41
43
  ENTITY_UNIT_RANDOM_SLUG_SUFFIX = ascii_lowercase + digits
42
44
 
@@ -46,9 +48,14 @@ class EntityUnitModelValidationError(ValidationError):
46
48
 
47
49
 
48
50
  class EntityUnitModelQuerySet(MP_NodeQuerySet):
49
- """
50
- A custom defined EntityUnitModel Queryset.
51
- """
51
+
52
+ def for_user(self, user_model) -> 'EntityUnitModelQuerySet':
53
+ if user_model.is_superuser:
54
+ return self
55
+ return self.filter(
56
+ Q(entity__admin=user_model) |
57
+ Q(entity__managers__in=[user_model])
58
+ )
52
59
 
53
60
 
54
61
  class EntityUnitModelManager(MP_NodeManager):
@@ -60,41 +67,61 @@ class EntityUnitModelManager(MP_NodeManager):
60
67
  _entity_name=F('entity__name'),
61
68
  )
62
69
 
63
- def for_user(self, user_model):
64
- qs = self.get_queryset()
65
- if user_model.is_superuser:
66
- return qs
67
- return qs.filter(
68
- Q(entity__admin=user_model) |
69
- Q(entity__managers__in=[user_model])
70
- )
71
-
72
- def for_entity(self, entity_slug: str, user_model):
70
+ @deprecated_entity_slug_behavior
71
+ def for_entity(self, entity_model: 'EntityModel | str | UUID' = None, **kwargs):
73
72
  """
74
- Fetches a QuerySet of EntityUnitModels associated with a specific EntityModel & UserModel.
75
- May pass an instance of EntityModel or a String representing the EntityModel slug.
73
+ Filter the queryset based on the provided entity model, its slug, or its UUID.
74
+
75
+ Provides functionality to filter entities within a queryset by comparing either
76
+ an EntityModel instance, its slug, or its UUID. This method also handles optional
77
+ deprecated behavior for filtering by the 'user_model' for backward compatibility.
76
78
 
77
79
  Parameters
78
80
  ----------
79
- entity_slug: str or EntityModel
80
- The entity slug or EntityModel used for filtering the QuerySet.
81
- user_model
82
- Logged in and authenticated django UserModel instance.
81
+ entity_model : EntityModel | str | UUID
82
+ An entity filter criterion that could be a specific EntityModel instance, a
83
+ string representing the entity slug, or a UUID corresponding to the entity.
84
+
85
+ **kwargs : dict, optional
86
+ Additional keyword arguments. If the 'user_model' parameter is provided,
87
+ a deprecation warning is issued, and the functionality temporarily delegates
88
+ to legacy behavior based on the value of 'DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR'.
83
89
 
84
90
  Returns
85
91
  -------
86
- EntityUnitModelQuerySet
87
- Returns a EntityUnitModelQuerySet with applied filters.
92
+ QuerySet
93
+ A queryset filtered based on the provided entity criteria.
94
+
95
+ Raises
96
+ ------
97
+ EntityUnitModelValidationError
98
+ Raised when the `entity_model` parameter does not match any of the accepted types
99
+ (EntityModel, str, or UUID) and fails validation.
88
100
  """
89
- qs = self.for_user(user_model)
90
- if isinstance(entity_slug, lazy_loader.get_entity_model()):
91
- return qs.filter(
92
- Q(entity=entity_slug)
101
+ EntityModel = lazy_loader.get_entity_model()
93
102
 
103
+ qs = self.get_queryset()
104
+ if 'user_model' in kwargs:
105
+ warnings.warn(
106
+ 'user_model parameter is deprecated and will be removed in a future release. '
107
+ 'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
108
+ DeprecationWarning,
109
+ stacklevel=2
94
110
  )
95
- return qs.filter(
96
- Q(entity__slug__exact=entity_slug)
97
- )
111
+ if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
112
+ qs = qs.for_user(kwargs['user_model'])
113
+
114
+ if isinstance(entity_model, EntityModel):
115
+ qs = qs.filter(entity=entity_model)
116
+ elif isinstance(entity_model, str):
117
+ qs = qs.filter(entity__slug__exact=entity_model)
118
+ elif isinstance(entity_model, UUID):
119
+ qs = qs.filter(entity_id=entity_model)
120
+ else:
121
+ raise EntityUnitModelValidationError(
122
+ message='Must pass EntityModel, slug or UUID'
123
+ )
124
+ return qs
98
125
 
99
126
 
100
127
  class EntityUnitModelAbstract(MP_Node,