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
@@ -6,19 +6,12 @@ This module implements the BillModel, which represents an Invoice received from
6
6
  the Vendor states the amount owed by the recipient for the purposes of supplying goods and/or services.
7
7
  In addition to tracking the bill amount, it tracks the paid and due amount.
8
8
 
9
- Examples
10
- ________
11
- >>> user_model = request.user # django UserModel
12
- >>> entity_slug = kwargs['entity_slug'] # may come from view kwargs
13
- >>> bill_model = BillModel()
14
- >>> ledger_model, bill_model = bill_model.configure(entity_slug=entity_slug, user_model=user_model)
15
- >>> bill_model.save()
16
9
  """
17
-
10
+ import warnings
18
11
  from datetime import date, datetime
19
12
  from decimal import Decimal
20
13
  from typing import Union, Optional, Tuple, Dict, List
21
- from uuid import uuid4
14
+ from uuid import uuid4, UUID
22
15
 
23
16
  from django.contrib.auth import get_user_model
24
17
  from django.core.exceptions import ValidationError, ObjectDoesNotExist
@@ -31,6 +24,7 @@ from django.utils.translation import gettext_lazy as _
31
24
 
32
25
  from django_ledger.io import ASSET_CA_CASH, ASSET_CA_PREPAID, LIABILITY_CL_ACC_PAYABLE
33
26
  from django_ledger.io.io_core import get_localtime, get_localdate
27
+ from django_ledger.models.deprecations import deprecated_entity_slug_behavior
34
28
  from django_ledger.models.entity import EntityModel
35
29
  from django_ledger.models.items import ItemTransactionModelQuerySet, ItemTransactionModel, ItemModel, ItemModelQuerySet
36
30
  from django_ledger.models.mixins import (
@@ -49,7 +43,8 @@ from django_ledger.models.signals import (
49
43
  bill_status_void,
50
44
  )
51
45
  from django_ledger.models.utils import lazy_loader
52
- from django_ledger.settings import (DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_BILL_NUMBER_PREFIX)
46
+ from django_ledger.settings import (DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_BILL_NUMBER_PREFIX,
47
+ DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR)
53
48
 
54
49
  UserModel = get_user_model()
55
50
 
@@ -66,6 +61,32 @@ class BillModelQuerySet(QuerySet):
66
61
  building customized reports.
67
62
  """
68
63
 
64
+ def for_user(self, user_model) -> 'BillModelQuerySet':
65
+ """
66
+ Fetches a QuerySet of BillModels that the UserModel as access to.
67
+ May include BillModels from multiple Entities.
68
+
69
+ The user has access to bills if:
70
+ 1. Is listed as Manager of Entity.
71
+ 2. Is the Admin of the Entity.
72
+
73
+ Parameters
74
+ __________
75
+ user_model
76
+ Logged in and authenticated django UserModel instance.
77
+
78
+ Returns
79
+ _______
80
+ BillModelQuerySet
81
+ Returns a BillModelQuerySet with applied filters.
82
+ """
83
+ if user_model.is_superuser:
84
+ return self
85
+ return self.filter(
86
+ Q(ledger__entity__admin=user_model) |
87
+ Q(ledger__entity__managers__in=[user_model])
88
+ )
89
+
69
90
  def draft(self):
70
91
  """
71
92
  Default status of any bill that is created.
@@ -162,7 +183,7 @@ class BillModelQuerySet(QuerySet):
162
183
  """
163
184
  return self.filter(date_due__lt=get_localdate())
164
185
 
165
- def unpaid(self):
186
+ def unpaid(self) -> 'BillModelQuerySet':
166
187
  """
167
188
  Unpaid bills are those that are approved but have not received 100% of the amount due.
168
189
  Equivalent to approved().
@@ -177,82 +198,56 @@ class BillModelQuerySet(QuerySet):
177
198
 
178
199
  class BillModelManager(Manager):
179
200
  """
180
- A custom defined BillModelManager that will act as an interface to handling the initial DB queries
201
+ A custom-defined BillModelManager that will act as an interface to handling the initial DB queries
181
202
  to the BillModel. The default "get_queryset" has been overridden to refer the custom defined
182
203
  "BillModelQuerySet".
183
204
  """
184
205
 
185
- def get_queryset(self):
186
- qs = super().get_queryset()
206
+ def get_queryset(self) -> BillModelQuerySet:
207
+ qs = BillModelQuerySet(self.model, using=self._db)
187
208
  return qs.select_related(
188
209
  'ledger',
189
210
  'ledger__entity'
190
211
  )
191
212
 
192
- def for_user(self, user_model) -> BillModelQuerySet:
193
- """
194
- Fetches a QuerySet of BillModels that the UserModel as access to.
195
- May include BillModels from multiple Entities.
196
-
197
- The user has access to bills if:
198
- 1. Is listed as Manager of Entity.
199
- 2. Is the Admin of the Entity.
200
-
201
- Parameters
202
- __________
203
- user_model
204
- Logged in and authenticated django UserModel instance.
205
-
206
- Examples
207
- ________
208
- >>> request_user = request.user
209
- >>> bill_model_qs = BillModel.objects.for_user(user_model=request_user)
210
-
211
- Returns
212
- _______
213
- BillModelQuerySet
214
- Returns a BillModelQuerySet with applied filters.
215
- """
216
- qs = self.get_queryset()
217
- if user_model.is_superuser:
218
- return qs
219
- return qs.filter(
220
- Q(ledger__entity__admin=user_model) |
221
- Q(ledger__entity__managers__in=[user_model])
222
- )
223
-
224
- def for_entity(self, entity_slug, user_model) -> BillModelQuerySet:
213
+ @deprecated_entity_slug_behavior
214
+ def for_entity(self, entity_model: EntityModel | str | UUID = None, **kwargs) -> BillModelQuerySet:
225
215
  """
226
216
  Fetches a QuerySet of BillModels associated with a specific EntityModel & UserModel.
227
217
  May pass an instance of EntityModel or a String representing the EntityModel slug.
228
218
 
229
219
  Parameters
230
220
  __________
231
- entity_slug: str or EntityModel
221
+ entity_model: str or EntityModel
232
222
  The entity slug or EntityModel used for filtering the QuerySet.
233
- user_model
234
- Logged in and authenticated django UserModel instance.
235
-
236
- Examples
237
- ________
238
- >>> request_user = request.user
239
- >>> slug = kwargs['entity_slug'] # may come from request kwargs
240
- >>> bill_model_qs = BillModel.objects.for_entity(user_model=request_user, entity_slug=slug)
241
223
 
242
224
  Returns
243
225
  _______
244
226
  BillModelQuerySet
245
227
  Returns a BillModelQuerySet with applied filters.
246
228
  """
247
- qs = self.for_user(user_model)
248
- if isinstance(entity_slug, EntityModel):
249
- return qs.filter(
250
- Q(ledger__entity=entity_slug)
229
+ qs = self.get_queryset()
230
+ if 'user_model' in kwargs:
231
+ warnings.warn(
232
+ 'user_model parameter is deprecated and will be removed in a future release. '
233
+ 'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
234
+ DeprecationWarning,
235
+ stacklevel=2
251
236
  )
252
- elif isinstance(entity_slug, str):
253
- return qs.filter(
254
- Q(ledger__entity__slug__exact=entity_slug)
237
+ if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
238
+ qs = qs.for_user(kwargs['user_model'])
239
+
240
+ if isinstance(entity_model, EntityModel):
241
+ qs = qs.filter(ledger__entity=entity_model)
242
+ elif isinstance(entity_model, str):
243
+ qs = qs.filter(ledger__entity__slug__exact=entity_model)
244
+ elif isinstance(entity_model, UUID):
245
+ qs = qs.filter(ledger__entity_id=entity_model)
246
+ else:
247
+ raise BillModelValidationError(
248
+ 'Must pass EntityModel, slug or UUID'
255
249
  )
250
+ return qs
256
251
 
257
252
 
258
253
  class BillModelAbstract(
@@ -369,6 +364,26 @@ class BillModelAbstract(
369
364
  vendor = models.ForeignKey('django_ledger.VendorModel',
370
365
  on_delete=models.CASCADE,
371
366
  verbose_name=_('Vendor'))
367
+
368
+ cash_account = models.ForeignKey('django_ledger.AccountModel',
369
+ on_delete=models.RESTRICT,
370
+ null=True,
371
+ blank=True,
372
+ verbose_name=_('Cash Account'),
373
+ related_name=f'{REL_NAME_PREFIX}_cash_account')
374
+ prepaid_account = models.ForeignKey('django_ledger.AccountModel',
375
+ on_delete=models.RESTRICT,
376
+ null=True,
377
+ blank=True,
378
+ verbose_name=_('Prepaid Account'),
379
+ related_name=f'{REL_NAME_PREFIX}_prepaid_account')
380
+ unearned_account = models.ForeignKey('django_ledger.AccountModel',
381
+ on_delete=models.RESTRICT,
382
+ null=True,
383
+ blank=True,
384
+ verbose_name=_('Unearned Account'),
385
+ related_name=f'{REL_NAME_PREFIX}_unearned_account')
386
+
372
387
  additional_info = models.JSONField(blank=True,
373
388
  null=True,
374
389
  default=dict,
@@ -1180,6 +1195,7 @@ class BillModelAbstract(
1180
1195
 
1181
1196
  if not itemtxs_qs.count():
1182
1197
  raise BillModelValidationError(message=f'Cannot review a {self.__class__.__name__} without items...')
1198
+
1183
1199
  if not self.amount_due:
1184
1200
  raise BillModelValidationError(
1185
1201
  f'Bill {self.bill_number} cannot be marked as in review. Amount due must be greater than 0.'
@@ -39,28 +39,32 @@ roles to create comprehensive financial statements.
39
39
  This structure ensures a clear and organized approach to financial management within Django Ledger, facilitating
40
40
  accurate record-keeping and reporting.
41
41
  """
42
-
42
+ import warnings
43
43
  from random import choices
44
44
  from string import ascii_lowercase, digits
45
45
  from typing import Optional, Union, Dict
46
- from uuid import uuid4
46
+ from uuid import uuid4, UUID
47
47
 
48
48
  from django.apps import apps
49
49
  from django.contrib.auth import get_user_model
50
50
  from django.core.exceptions import ValidationError
51
51
  from django.db import models
52
- from django.db.models import Q, F, Count, Manager, QuerySet
52
+ from django.db.models import Q, F, Count, Manager, QuerySet, BooleanField, Value
53
53
  from django.db.models.signals import pre_save, post_save
54
54
  from django.dispatch import receiver
55
55
  from django.urls import reverse
56
56
  from django.utils.translation import gettext_lazy as _
57
57
 
58
- from django_ledger.io import (ROOT_COA, ROOT_GROUP_LEVEL_2, ROOT_GROUP_META, ROOT_ASSETS,
59
- ROOT_LIABILITIES, ROOT_CAPITAL,
60
- ROOT_INCOME, ROOT_COGS, ROOT_EXPENSES)
58
+ from django_ledger.io import (
59
+ ROOT_COA, ROOT_GROUP_LEVEL_2, ROOT_GROUP_META, ROOT_ASSETS,
60
+ ROOT_LIABILITIES, ROOT_CAPITAL,
61
+ ROOT_INCOME, ROOT_COGS, ROOT_EXPENSES, ROOT_GROUP
62
+ )
61
63
  from django_ledger.models import lazy_loader
62
64
  from django_ledger.models.accounts import AccountModel, AccountModelQuerySet
65
+ from django_ledger.models.deprecations import deprecated_entity_slug_behavior
63
66
  from django_ledger.models.mixins import CreateUpdateMixIn, SlugNameMixIn
67
+ from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
64
68
 
65
69
  UserModel = get_user_model()
66
70
 
@@ -75,21 +79,55 @@ class ChartOfAccountsModelValidationError(ValidationError):
75
79
 
76
80
  class ChartOfAccountModelQuerySet(QuerySet):
77
81
 
78
- def active(self):
82
+ def active(self) -> 'ChartOfAccountModelQuerySet':
79
83
  """
80
84
  QuerySet method to retrieve active items.
81
85
  """
82
86
  return self.filter(active=True)
83
87
 
88
+ def not_active(self) -> 'ChartOfAccountModelQuerySet':
89
+ """≤
90
+ QuerySet method to retrieve not active items.
91
+ """
92
+ return self.filter(active=False)
93
+
94
+ def for_user(self, user_model) -> 'ChartOfAccountModelQuerySet':
95
+ """
96
+ Fetches a QuerySet of ChartOfAccountModel that the UserModel as access to. May include ChartOfAccountModel from
97
+ multiple Entities. The user has access to bills if:
98
+ 1. Is listed as Manager of Entity.
99
+ 2. Is the Admin of the Entity.
100
+
101
+ Parameters
102
+ ----------
103
+ user_model
104
+ Logged in and authenticated django UserModel instance.
105
+
106
+ Returns
107
+ -------
108
+ ChartOfAccountQuerySet
109
+ Returns a ChartOfAccountQuerySet with applied filters.
110
+ """
111
+
112
+ if user_model.is_superuser:
113
+ return self
114
+
115
+ return self.filter(
116
+ (
117
+ Q(entity__admin=user_model) |
118
+ Q(entity__managers__in=[user_model])
119
+ )
120
+ )
121
+
84
122
 
85
123
  class ChartOfAccountModelManager(Manager):
86
124
  """
87
- A custom defined ChartOfAccountModelManager that will act as an interface to handling the initial DB queries
125
+ A custom-defined ChartOfAccountModelManager that will act as an interface to handling the initial DB queries
88
126
  to the ChartOfAccountModel.
89
127
  """
90
128
 
91
- def get_queryset(self):
92
- qs = super().get_queryset()
129
+ def get_queryset(self) -> ChartOfAccountModelQuerySet:
130
+ qs = ChartOfAccountModelQuerySet(self.model, using=self._db)
93
131
  return qs.annotate(
94
132
  _entity_slug=F('entity__slug'),
95
133
  accountmodel_total__count=Count(
@@ -107,34 +145,32 @@ class ChartOfAccountModelManager(Manager):
107
145
  # excludes coa root accounts...
108
146
  filter=Q(accountmodel__depth__gt=2) & Q(accountmodel__active=True)
109
147
  ),
110
- ).select_related('entity')
111
-
112
- def for_user(self, user_model) -> ChartOfAccountModelQuerySet:
113
- """
114
- Fetches a QuerySet of ChartOfAccountModel that the UserModel as access to. May include ChartOfAccountModel from
115
- multiple Entities. The user has access to bills if:
116
- 1. Is listed as Manager of Entity.
117
- 2. Is the Admin of the Entity.
118
-
119
- Parameters
120
- ----------
121
- user_model
122
- Logged in and authenticated django UserModel instance.
123
-
124
- Returns
125
- -------
126
- ChartOfAccountQuerySet
127
- Returns a ChartOfAccountQuerySet with applied filters.
128
- """
129
- qs = self.get_queryset()
130
- return qs.filter(
131
- (
132
- Q(entity__admin=user_model) |
133
- Q(entity__managers__in=[user_model])
148
+ # Root-group presence and uniqueness checks:
149
+ accountmodel_rootgroup__count=Count(
150
+ 'accountmodel',
151
+ filter=Q(accountmodel__role__in=ROOT_GROUP)
152
+ ),
153
+ accountmodel_rootgroup_roles__distinct_count=Count(
154
+ 'accountmodel__role',
155
+ filter=Q(accountmodel__role__in=ROOT_GROUP_META),
156
+ distinct=True
157
+ ),
158
+ ).annotate(
159
+ configured=models.Case(
160
+ models.When(
161
+ Q(accountmodel_rootgroup__count__gte=1) &
162
+ Q(accountmodel_rootgroup__count=F('accountmodel_rootgroup_roles__distinct_count')),
163
+ then=Value(True, output_field=BooleanField()),
164
+ ),
165
+ default=Value(False, output_field=BooleanField()),
166
+ output_field=BooleanField()
134
167
  )
135
- )
168
+ ).select_related('entity')
136
169
 
137
- def for_entity(self, entity_model, user_model) -> ChartOfAccountModelQuerySet:
170
+ @deprecated_entity_slug_behavior
171
+ def for_entity(self,
172
+ entity_model: Union['EntityModel | str | UUID'] = None,
173
+ **kwargs) -> ChartOfAccountModelQuerySet:
138
174
  """
139
175
  Fetches a QuerySet of ChartOfAccountsModel associated with a specific EntityModel & UserModel.
140
176
  May pass an instance of EntityModel or a String representing the EntityModel slug.
@@ -145,18 +181,36 @@ class ChartOfAccountModelManager(Manager):
145
181
  entity_slug: str or EntityModel
146
182
  The entity slug or EntityModel used for filtering the QuerySet.
147
183
 
148
- user_model
149
- Logged in and authenticated django UserModel instance.
150
-
151
184
  Returns
152
185
  -------
153
186
  ChartOfAccountQuerySet
154
187
  Returns a ChartOfAccountQuerySet with applied filters.
155
188
  """
156
- qs = self.for_user(user_model)
157
- if isinstance(entity_model, lazy_loader.get_entity_model()):
158
- return qs.filter(entity=entity_model)
159
- return qs.filter(entity__slug__iexact=entity_model)
189
+
190
+ EntityModel = lazy_loader.get_entity_model()
191
+
192
+ qs = self.get_queryset()
193
+ if 'user_model' in kwargs:
194
+ warnings.warn(
195
+ 'user_model parameter is deprecated and will be removed in a future release. '
196
+ 'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
197
+ DeprecationWarning,
198
+ stacklevel=2
199
+ )
200
+ if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
201
+ qs = qs.for_user(kwargs['user_model'])
202
+
203
+ if isinstance(entity_model, EntityModel):
204
+ qs = qs.filter(entity=entity_model)
205
+ elif isinstance(entity_model, str):
206
+ qs = qs.filter(entity__slug=entity_model)
207
+ elif isinstance(entity_model, UUID):
208
+ qs = qs.filter(entity_id=entity_model)
209
+ else:
210
+ raise ChartOfAccountsModelValidationError(
211
+ message='Must pass an instance of EntityModel, String or UUID for entity_slug.'
212
+ )
213
+ return qs
160
214
 
161
215
 
162
216
  class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
@@ -202,11 +256,80 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
202
256
  @property
203
257
  def entity_slug(self) -> str:
204
258
  try:
205
- # from QS annotation...
206
259
  return getattr(self, '_entity_slug')
207
260
  except AttributeError:
208
261
  return self.entity.slug
209
262
 
263
+ def is_configured(self) -> bool:
264
+ try:
265
+ return getattr(self, 'configured')
266
+ except AttributeError:
267
+ pass
268
+ account_qs = self.accountmodel_set.filter(role__in=[ROOT_GROUP])
269
+ self.configured = len(account_qs) == len(ROOT_GROUP)
270
+ return self.configured
271
+
272
+ def configure(self, raise_exception: bool = True):
273
+ """
274
+ A method that properly configures the ChartOfAccounts model and creates the appropriate hierarchy boilerplate
275
+ to support the insertion of new accounts into the chart of account model tree.
276
+ This method must be called every time the ChartOfAccounts model is created.
277
+
278
+ Parameters
279
+ ----------
280
+ raise_exception : bool, optional
281
+ Whether to raise an exception if root nodes already exist in the Chart of Accounts (default is True).
282
+ This indicates that the ChartOfAccountModel instance is already configured.
283
+ """
284
+ self.generate_slug(commit=False)
285
+
286
+ if not self.is_configured():
287
+ root_accounts_qs = self.get_coa_root_accounts_qs()
288
+ existing_root_roles = list(set(acc.role for acc in root_accounts_qs))
289
+
290
+ if len(existing_root_roles) > 0:
291
+ if raise_exception:
292
+ raise ChartOfAccountsModelValidationError(message=f'Root Nodes already Exist in CoA {self.uuid}...')
293
+ return
294
+
295
+ if ROOT_COA not in existing_root_roles:
296
+ # add coa root...
297
+ role_meta = ROOT_GROUP_META[ROOT_COA]
298
+ account_pk = uuid4()
299
+ root_account = AccountModel(
300
+ uuid=account_pk,
301
+ code=role_meta['code'],
302
+ name=role_meta['title'],
303
+ coa_model=self,
304
+ role=ROOT_COA,
305
+ role_default=True,
306
+ active=False,
307
+ locked=True,
308
+ balance_type=role_meta['balance_type']
309
+ )
310
+ AccountModel.add_root(instance=root_account)
311
+
312
+ # must retrieve root model after added pero django-treebeard documentation...
313
+ coa_root_account_model = AccountModel.objects.get(uuid__exact=account_pk)
314
+
315
+ for root_role in ROOT_GROUP_LEVEL_2:
316
+ if root_role not in existing_root_roles:
317
+ account_pk = uuid4()
318
+ role_meta = ROOT_GROUP_META[root_role]
319
+ coa_root_account_model.add_child(
320
+ instance=AccountModel(
321
+ uuid=account_pk,
322
+ code=role_meta['code'],
323
+ name=role_meta['title'],
324
+ coa_model=self,
325
+ role=root_role,
326
+ role_default=True,
327
+ active=False,
328
+ locked=True,
329
+ balance_type=role_meta['balance_type']
330
+ ))
331
+ self.configured = True
332
+
210
333
  def get_coa_root_accounts_qs(self) -> AccountModelQuerySet:
211
334
  """
212
335
  Retrieves the root accounts in the chart of accounts.
@@ -261,6 +384,10 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
261
384
  message=_(f'The account model {account_model} is not part of the chart of accounts {self.name}.'),
262
385
  )
263
386
 
387
+ # Chart of Accounts hasn't been configured...
388
+ if not self.is_configured():
389
+ self.configure(raise_exception=True)
390
+
264
391
  if not account_model.is_root_account():
265
392
 
266
393
  if not root_account_qs:
@@ -373,65 +500,6 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
373
500
  ]
374
501
  )
375
502
 
376
- def configure(self, raise_exception: bool = True):
377
- """
378
- A method that properly configures the ChartOfAccounts model and creates the appropriate hierarchy boilerplate
379
- to support the insertion of new accounts into the chart of account model tree.
380
- This method must be called every time the ChartOfAccounts model is created.
381
-
382
- Parameters
383
- ----------
384
- raise_exception : bool, optional
385
- Whether to raise an exception if root nodes already exist in the Chart of Accounts (default is True).
386
- This indicates that the ChartOfAccountModel instance is already configured.
387
- """
388
- self.generate_slug(commit=False)
389
-
390
- root_accounts_qs = self.get_coa_root_accounts_qs()
391
- existing_root_roles = list(set(acc.role for acc in root_accounts_qs))
392
-
393
- if len(existing_root_roles) > 0:
394
- if raise_exception:
395
- raise ChartOfAccountsModelValidationError(message=f'Root Nodes already Exist in CoA {self.uuid}...')
396
- return
397
-
398
- if ROOT_COA not in existing_root_roles:
399
- # add coa root...
400
- role_meta = ROOT_GROUP_META[ROOT_COA]
401
- account_pk = uuid4()
402
- root_account = AccountModel(
403
- uuid=account_pk,
404
- code=role_meta['code'],
405
- name=role_meta['title'],
406
- coa_model=self,
407
- role=ROOT_COA,
408
- role_default=True,
409
- active=False,
410
- locked=True,
411
- balance_type=role_meta['balance_type']
412
- )
413
- AccountModel.add_root(instance=root_account)
414
-
415
- # must retrieve root model after added pero django-treebeard documentation...
416
- coa_root_account_model = AccountModel.objects.get(uuid__exact=account_pk)
417
-
418
- for root_role in ROOT_GROUP_LEVEL_2:
419
- if root_role not in existing_root_roles:
420
- account_pk = uuid4()
421
- role_meta = ROOT_GROUP_META[root_role]
422
- coa_root_account_model.add_child(
423
- instance=AccountModel(
424
- uuid=account_pk,
425
- code=role_meta['code'],
426
- name=role_meta['title'],
427
- coa_model=self,
428
- role=root_role,
429
- role_default=True,
430
- active=False,
431
- locked=True,
432
- balance_type=role_meta['balance_type']
433
- ))
434
-
435
503
  def is_default(self) -> bool:
436
504
  """
437
505
  Check if the ChartOfAccountModel instance is set as the default for the EntityModel.
@@ -598,7 +666,6 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
598
666
  account_qs.update(locked=False)
599
667
  return account_qs
600
668
 
601
-
602
669
  def mark_as_default(self, commit: bool = False, raise_exception: bool = False, **kwargs):
603
670
  """
604
671
  Marks the current Chart of Accounts instances as default for the EntityModel.
@@ -839,6 +906,7 @@ class ChartOfAccountModel(ChartOfAccountModelAbstract):
839
906
  """
840
907
  Base ChartOfAccounts Model
841
908
  """
909
+
842
910
  class Meta(ChartOfAccountModelAbstract.Meta):
843
911
  abstract = False
844
912
 
@@ -854,5 +922,5 @@ def chartofaccountsmodel_presave(instance: ChartOfAccountModelAbstract, **kwargs
854
922
 
855
923
  @receiver(post_save, sender=ChartOfAccountModel)
856
924
  def chartofaccountsmodel_postsave(instance: ChartOfAccountModelAbstract, **kwargs):
857
- if instance._state.adding:
925
+ if not instance.is_configured():
858
926
  instance.configure()