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
@@ -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()
@@ -2,8 +2,8 @@
2
2
  Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
3
  Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
4
  """
5
-
6
- from datetime import datetime, time
5
+ import warnings
6
+ from datetime import datetime, time, date
7
7
  from decimal import Decimal
8
8
  from itertools import groupby, chain
9
9
  from typing import Optional
@@ -12,58 +12,75 @@ from uuid import uuid4, UUID
12
12
  from django.core.exceptions import ValidationError
13
13
  from django.core.validators import MinValueValidator
14
14
  from django.db import models
15
- from django.db.models import Q, Count
15
+ from django.db.models import Q, Manager, QuerySet, Count
16
16
  from django.db.models.signals import pre_save
17
17
  from django.urls import reverse
18
18
  from django.utils.timezone import make_aware
19
19
  from django.utils.translation import gettext_lazy as _
20
20
 
21
+ from django_ledger.models import EntityModel, deprecated_entity_slug_behavior, lazy_loader
21
22
  from django_ledger.models.journal_entry import JournalEntryModel
22
23
  from django_ledger.models.ledger import LedgerModel
23
24
  from django_ledger.models.mixins import CreateUpdateMixIn, MarkdownNotesMixIn
24
25
  from django_ledger.models.transactions import TransactionModel
25
- from django_ledger.models.utils import lazy_loader
26
+ from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
26
27
 
27
28
 
28
29
  class ClosingEntryValidationError(ValidationError):
29
30
  pass
30
31
 
31
32
 
32
- class ClosingEntryModelQuerySet(models.QuerySet):
33
+ class ClosingEntryModelQuerySet(QuerySet):
33
34
 
34
- def posted(self):
35
+ def for_user(self, user_model):
36
+ if user_model.is_superuser:
37
+ return self
38
+ return self.filter(
39
+ Q(entity_model__admin=user_model) |
40
+ Q(entity_model__managers__in=[user_model])
41
+ )
42
+
43
+ def posted(self) -> 'ClosingEntryModelQuerySet':
35
44
  return self.filter(posted=True)
36
45
 
37
- def not_posted(self):
46
+ def not_posted(self) -> 'ClosingEntryModelQuerySet':
38
47
  return self.filter(posted=False)
39
48
 
40
49
 
41
- class ClosingEntryModelManager(models.Manager):
50
+ class ClosingEntryModelManager(Manager):
42
51
 
43
- def get_queryset(self):
52
+ def get_queryset(self) -> ClosingEntryModelQuerySet:
44
53
  qs = ClosingEntryModelQuerySet(self.model, using=self._db)
45
54
  return qs.annotate(
46
55
  ce_txs_count=Count('closingentrytransactionmodel')
47
56
  )
48
57
 
49
- def for_user(self, user_model):
58
+ @deprecated_entity_slug_behavior
59
+ def for_entity(self, entity_model: EntityModel | str | UUID = None, **kwargs) -> ClosingEntryModelQuerySet:
60
+
50
61
  qs = self.get_queryset()
51
- if user_model.is_superuser:
52
- return qs
53
- return qs.filter(
54
- Q(entity_model__admin=user_model) |
55
- Q(entity_model__managers__in=[user_model])
56
- )
57
62
 
58
- def for_entity(self, entity_slug, user_model):
59
- qs = self.for_user(user_model)
60
- if isinstance(entity_slug, lazy_loader.get_entity_model()):
61
- return qs.filter(
62
- Q(entity_model=entity_slug)
63
+ if 'user_model' in kwargs:
64
+ warnings.warn(
65
+ 'user_model parameter is deprecated and will be removed in a future release. '
66
+ 'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
67
+ DeprecationWarning,
68
+ stacklevel=2
63
69
  )
64
- return qs.filter(
65
- Q(entity_model__slug__exact=entity_slug)
66
- )
70
+ if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
71
+ qs = qs.for_user(kwargs['user_model'])
72
+
73
+ if isinstance(entity_model, EntityModel):
74
+ qs = qs.filter(entity_model=entity_model)
75
+ elif isinstance(entity_model, str):
76
+ qs = qs.filter(entity_model__slug__exact=entity_model)
77
+ elif isinstance(entity_model, UUID):
78
+ qs = qs.filter(entity_model_id=entity_model)
79
+ else:
80
+ raise ClosingEntryValidationError(
81
+ message='Must pass EntityModel, slug or UUID'
82
+ )
83
+ return qs
67
84
 
68
85
 
69
86
  class ClosingEntryModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn):
@@ -247,7 +264,7 @@ class ClosingEntryModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn):
247
264
  self.posted = False
248
265
 
249
266
  TransactionModel.objects.for_entity(
250
- entity_slug=self.entity_model_id
267
+ entity_model=self.entity_model_id
251
268
  ).for_ledger(ledger_model=self.ledger_model).delete()
252
269
 
253
270
  self.ledger_model.journal_entries.all().delete()
@@ -321,7 +338,7 @@ class ClosingEntryModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn):
321
338
  self.ledger_model.unpost(commit=True, raise_exception=True)
322
339
 
323
340
  TransactionModel.objects.for_entity(
324
- entity_slug=self.entity_model_id
341
+ entity_model=self.entity_model_id
325
342
  ).for_ledger(ledger_model=self.ledger_model).delete()
326
343
 
327
344
  return self.ledger_model.delete()
@@ -361,19 +378,62 @@ class ClosingEntryModel(ClosingEntryModelAbstract):
361
378
  abstract = False
362
379
 
363
380
 
364
- class ClosingEntryTransactionModelQuerySet(models.QuerySet):
365
- pass
381
+ def closingentrymodel_presave(instance: ClosingEntryModel, **kwargs):
382
+ instance.create_entry_ledger(commit=False)
383
+
384
+
385
+ pre_save.connect(closingentrymodel_presave, sender=ClosingEntryModel)
386
+
387
+
388
+ class ClosingEntryTransactionModelQuerySet(QuerySet):
366
389
 
390
+ def for_user(self, user_model) -> 'ClosingEntryTransactionModelQuerySet':
391
+ if user_model.is_superuser:
392
+ return self
393
+ return self.filter(
394
+ Q(closing_entry_model__entity_model__admin=user_model) |
395
+ Q(closing_entry_model__entity_model__managers__in=[user_model])
396
+ )
367
397
 
368
- class ClosingEntryTransactionModelManager(models.Manager):
398
+ def for_closing_date(self, closing_date: date) -> 'ClosingEntryTransactionModelQuerySet':
399
+ return self.filter(
400
+ closing_entry_model__closing_date__exact=closing_date
401
+ )
402
+
403
+
404
+ class ClosingEntryTransactionModelManager(Manager):
405
+
406
+ def get_queryset(self) -> ClosingEntryTransactionModelQuerySet:
407
+ return ClosingEntryTransactionModelQuerySet(self.model, using=self._db)
408
+
409
+ @deprecated_entity_slug_behavior
410
+ def for_entity(self, entity_model: EntityModel | str | UUID = None,
411
+ **kwargs) -> ClosingEntryTransactionModelQuerySet:
412
+ EntityModel = lazy_loader.get_entity(entity_model)
369
413
 
370
- def for_entity(self, entity_slug):
371
414
  qs = self.get_queryset()
372
- if isinstance(entity_slug, lazy_loader.get_entity_model()):
373
- return qs.filter(closing_entry_model__entity_model=entity_slug)
374
- elif isinstance(entity_slug, UUID):
375
- return qs.filter(closing_entry_model__entity_model__uuid__exact=entity_slug)
376
- return qs.filter(closing_entry_model__entity_model__slug__exact=entity_slug)
415
+
416
+ if 'user_model' in kwargs:
417
+ warnings.warn(
418
+ 'user_model parameter is deprecated and will be removed in a future release. '
419
+ 'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
420
+ DeprecationWarning,
421
+ stacklevel=2
422
+ )
423
+ if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
424
+ qs = qs.for_user(kwargs['user_model'])
425
+
426
+ if isinstance(entity_model, EntityModel):
427
+ qs = qs.filter(closing_entry_model__entity_model=entity_model)
428
+ elif isinstance(entity_model, UUID):
429
+ qs = qs.filter(closing_entry_model__entity_model_id=entity_model)
430
+ elif isinstance(entity_model, str):
431
+ qs = qs.filter(closing_entry_model__slug__exact=entity_model)
432
+ else:
433
+ raise ClosingEntryValidationError(
434
+ message=_('Must pass EntityModel or UUID or str')
435
+ )
436
+ return qs
377
437
 
378
438
 
379
439
  class ClosingEntryTransactionModelAbstract(CreateUpdateMixIn):
@@ -453,13 +513,11 @@ class ClosingEntryTransactionModelAbstract(CreateUpdateMixIn):
453
513
  def __str__(self):
454
514
  return f'{self.__class__.__name__}: {self.closing_entry_model.closing_date.strftime("%D")} | {self.balance}'
455
515
 
456
- def is_debit(self) -> Optional[bool]:
457
- if self.tx_type is not None:
458
- return self.tx_type == TransactionModel.DEBIT
516
+ def is_debit(self) -> bool:
517
+ return self.tx_type == TransactionModel.DEBIT
459
518
 
460
- def is_credit(self) -> Optional[bool]:
461
- if self.tx_type is not None:
462
- return self.tx_type == TransactionModel.CREDIT
519
+ def is_credit(self) -> bool:
520
+ return self.tx_type == TransactionModel.CREDIT
463
521
 
464
522
  def adjust_tx_type_for_negative_balance(self):
465
523
  if self.balance < Decimal('0.00'):
@@ -485,13 +543,6 @@ class ClosingEntryTransactionModel(ClosingEntryTransactionModelAbstract):
485
543
  abstract = False
486
544
 
487
545
 
488
- def closingentrymodel_presave(instance: ClosingEntryModel, **kwargs):
489
- instance.create_entry_ledger(commit=False)
490
-
491
-
492
- pre_save.connect(closingentrymodel_presave, sender=ClosingEntryModel)
493
-
494
-
495
546
  def closingentrytransactionmodel_presave(instance: ClosingEntryTransactionModel, **kwargs):
496
547
  instance.adjust_tx_type_for_negative_balance()
497
548