django-ledger 0.6.3__py3-none-any.whl → 0.7.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 (115) hide show
  1. django_ledger/__init__.py +1 -4
  2. django_ledger/admin/coa.py +1 -2
  3. django_ledger/contrib/django_ledger_graphene/accounts/schema.py +1 -1
  4. django_ledger/forms/account.py +60 -44
  5. django_ledger/forms/bank_account.py +3 -2
  6. django_ledger/forms/bill.py +24 -36
  7. django_ledger/forms/customer.py +1 -1
  8. django_ledger/forms/data_import.py +3 -3
  9. django_ledger/forms/estimate.py +1 -1
  10. django_ledger/forms/invoice.py +5 -7
  11. django_ledger/forms/item.py +24 -15
  12. django_ledger/forms/transactions.py +3 -3
  13. django_ledger/io/io_core.py +4 -2
  14. django_ledger/io/io_middleware.py +5 -0
  15. django_ledger/io/roles.py +6 -0
  16. django_ledger/migrations/0017_alter_accountmodel_unique_together_and_more.py +31 -0
  17. django_ledger/models/accounts.py +629 -342
  18. django_ledger/models/bank_account.py +0 -4
  19. django_ledger/models/bill.py +0 -3
  20. django_ledger/models/closing_entry.py +0 -3
  21. django_ledger/models/coa.py +59 -48
  22. django_ledger/models/coa_default.py +9 -8
  23. django_ledger/models/customer.py +0 -4
  24. django_ledger/models/data_import.py +0 -3
  25. django_ledger/models/entity.py +80 -40
  26. django_ledger/models/estimate.py +0 -9
  27. django_ledger/models/invoice.py +0 -3
  28. django_ledger/models/items.py +4 -6
  29. django_ledger/models/journal_entry.py +2 -5
  30. django_ledger/models/ledger.py +0 -3
  31. django_ledger/models/mixins.py +0 -3
  32. django_ledger/models/purchase_order.py +0 -4
  33. django_ledger/models/signals.py +0 -3
  34. django_ledger/models/transactions.py +2 -5
  35. django_ledger/models/unit.py +0 -3
  36. django_ledger/models/utils.py +0 -3
  37. django_ledger/models/vendor.py +0 -3
  38. django_ledger/report/cash_flow_statement.py +1 -1
  39. django_ledger/static/.DS_Store +0 -0
  40. django_ledger/static/django_ledger/.DS_Store +0 -0
  41. django_ledger/static/django_ledger/logo_2/.DS_Store +0 -0
  42. django_ledger/static/django_ledger/logo_2/django_ledger_logo_dark.png +0 -0
  43. django_ledger/static/django_ledger/logo_2/django_ledger_logo_dark@0.5x.png +0 -0
  44. django_ledger/static/django_ledger/logo_2/django_ledger_logo_dark@2x.png +0 -0
  45. django_ledger/static/django_ledger/logo_2/django_ledger_logo_dark@3x.png +0 -0
  46. django_ledger/static/django_ledger/logo_2/djl-full-vert.png +0 -0
  47. django_ledger/static/django_ledger/logo_2/djl-full-vert@0.5x.png +0 -0
  48. django_ledger/static/django_ledger/logo_2/djl-full-vert@2x.png +0 -0
  49. django_ledger/static/django_ledger/logo_2/djl-full-vert@3x.png +0 -0
  50. django_ledger/static/django_ledger/logo_2/djl-logo-full-horiz.png +0 -0
  51. django_ledger/static/django_ledger/logo_2/djl-logo-full-horiz@0.5x.png +0 -0
  52. django_ledger/static/django_ledger/logo_2/djl-logo-full-horiz@2x.png +0 -0
  53. django_ledger/static/django_ledger/logo_2/djl-logo-full-horiz@3x.png +0 -0
  54. django_ledger/static/django_ledger/logo_2/djl-logo-full-vert.png +0 -0
  55. django_ledger/static/django_ledger/logo_2/djl-logo-full-vert@0.5x.png +0 -0
  56. django_ledger/static/django_ledger/logo_2/djl-logo-full-vert@2x.png +0 -0
  57. django_ledger/static/django_ledger/logo_2/djl-logo-full-vert@3x.png +0 -0
  58. django_ledger/static/django_ledger/logo_2/djl-logo.png +0 -0
  59. django_ledger/static/django_ledger/logo_2/djl-logo@0.5x.png +0 -0
  60. django_ledger/static/django_ledger/logo_2/djl-logo@2x.png +0 -0
  61. django_ledger/static/django_ledger/logo_2/djl-logo@3x.png +0 -0
  62. django_ledger/static/django_ledger/logo_2/djl-txt-full-horiz.png +0 -0
  63. django_ledger/static/django_ledger/logo_2/djl-txt-full-horiz@0.5x.png +0 -0
  64. django_ledger/static/django_ledger/logo_2/djl-txt-full-horiz@2x.png +0 -0
  65. django_ledger/static/django_ledger/logo_2/djl-txt-full-horiz@3x.png +0 -0
  66. django_ledger/static/django_ledger/logo_2/djl-txt-full-vert.png +0 -0
  67. django_ledger/static/django_ledger/logo_2/djl-txt-full-vert@0.5x.png +0 -0
  68. django_ledger/static/django_ledger/logo_2/djl-txt-full-vert@2x.png +0 -0
  69. django_ledger/static/django_ledger/logo_2/djl-txt-full-vert@3x.png +0 -0
  70. django_ledger/static/django_ledger/logo_2/djl-txt-horiz.png +0 -0
  71. django_ledger/static/django_ledger/logo_2/djl-txt-horiz@0.5x.png +0 -0
  72. django_ledger/static/django_ledger/logo_2/djl-txt-horiz@2x.png +0 -0
  73. django_ledger/static/django_ledger/logo_2/djl-txt-horiz@3x.png +0 -0
  74. django_ledger/templates/django_ledger/account/account_create.html +2 -2
  75. django_ledger/templates/django_ledger/account/account_update.html +1 -1
  76. django_ledger/templates/django_ledger/account/tags/account_txs_table.html +1 -0
  77. django_ledger/templates/django_ledger/account/tags/accounts_table.html +27 -18
  78. django_ledger/templates/django_ledger/bills/bill_detail.html +3 -3
  79. django_ledger/templates/django_ledger/expense/tags/expense_item_table.html +7 -0
  80. django_ledger/templates/django_ledger/invoice/invoice_detail.html +3 -3
  81. django_ledger/templatetags/django_ledger.py +7 -1
  82. django_ledger/tests/base.py +23 -7
  83. django_ledger/tests/test_accounts.py +145 -9
  84. django_ledger/urls/account.py +17 -24
  85. django_ledger/utils.py +8 -0
  86. django_ledger/views/__init__.py +1 -1
  87. django_ledger/views/account.py +80 -118
  88. django_ledger/views/auth.py +1 -1
  89. django_ledger/views/bank_account.py +9 -11
  90. django_ledger/views/bill.py +91 -80
  91. django_ledger/views/closing_entry.py +8 -0
  92. django_ledger/views/coa.py +2 -1
  93. django_ledger/views/customer.py +1 -1
  94. django_ledger/views/data_import.py +1 -1
  95. django_ledger/views/entity.py +1 -1
  96. django_ledger/views/estimate.py +13 -8
  97. django_ledger/views/feedback.py +1 -1
  98. django_ledger/views/financial_statement.py +1 -1
  99. django_ledger/views/home.py +1 -1
  100. django_ledger/views/inventory.py +9 -0
  101. django_ledger/views/invoice.py +5 -2
  102. django_ledger/views/item.py +58 -68
  103. django_ledger/views/journal_entry.py +1 -1
  104. django_ledger/views/ledger.py +3 -1
  105. django_ledger/views/mixins.py +9 -8
  106. django_ledger/views/purchase_order.py +1 -1
  107. django_ledger/views/transactions.py +1 -1
  108. django_ledger/views/unit.py +9 -0
  109. django_ledger/views/vendor.py +1 -1
  110. {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/AUTHORS.md +8 -2
  111. {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/METADATA +34 -43
  112. {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/RECORD +115 -79
  113. {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/WHEEL +1 -1
  114. {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/top_level.txt +0 -1
  115. {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/LICENSE +0 -0
@@ -2,28 +2,50 @@
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
- Contributions to this module:
6
- * Miguel Sanda <msanda@arrobalytics.com>
7
- * Pranav P Tulshyan <ptulshyan77@gmail.com>
5
+ AccountModel
6
+ ------------
7
+ The AccountModel is a fundamental component of the Django Ledger system, responsible for categorizing and organizing
8
+ financial transactions related to an entity's assets, liabilities, and equity.
8
9
 
9
- The AccountModel groups and sorts transactions involving the company's assets, liabilities and equities.
10
- Per accounting principles, an Account must be either a DEBIT-type balance account or a CREDIT-type balance account,
11
- depending on its purpose.
10
+ Account Types
11
+ -------------
12
12
 
13
- The AccountModel plays a major role when creating Journal Entries in a double entry accounting systems where
14
- a DEBIT to a DEBIT-type AccountModel will increase its balance, and a CREDIT to a DEBIT-type AccountModel will
15
- reduce its balance. Conversely, a CREDIT to a CREDIT-type AccountModel will increase its balance, and a
16
- DEBIT to a CREDIT-type AccountModel will reduce its balance.
13
+ In accordance with accounting principles, each AccountModel must be classified as either:
17
14
 
18
- It is entirely up to the user to adopt the chart of accounts that best suits the EntityModel.
19
- The user may choose to use the default Chart of Accounts provided by Django Ledger when creating a new EntityModel.
15
+ 1. **DEBIT-type balance account**
16
+ 2. **CREDIT-type balance account**
20
17
 
21
- In Django Ledger, all account models must be assigned a role from
22
- :func:`ACCOUNT_ROLES <django_ledger.io.roles.ACCOUNT_ROLES>`. Roles are a way to group accounts to a common namespace,
23
- regardless of its user-defined fields. Roles are an integral part to Django Ledger since they are critical when
24
- requesting and producing financial statements and financial ratio calculations.
18
+ The account type determines how transactions affect the account's balance.
25
19
 
26
- AccountModels may also contain parent/child relationships as implemented by the Django Treebeard functionality.
20
+ Double Entry Accounting
21
+ -----------------------
22
+
23
+ The AccountModel is crucial in implementing double entry accounting systems:
24
+
25
+ * For DEBIT-type accounts:
26
+ - A DEBIT increases the balance
27
+ - A CREDIT decreases the balance
28
+
29
+ * For CREDIT-type accounts:
30
+ - A CREDIT increases the balance
31
+ - A DEBIT decreases the balance
32
+
33
+ Chart of Accounts
34
+ -----------------
35
+
36
+ Users have the flexibility to adopt a chart of accounts that best suits their EntityModel. Django Ledger provides a
37
+ default Chart of Accounts when creating a new EntityModel, which can be customized as needed.
38
+
39
+ Account Roles
40
+ -------------
41
+
42
+ All AccountModels must be assigned a role from the `ACCOUNT_ROLES` function in `django_ledger.io.roles`.
43
+ Roles serve several purposes:
44
+
45
+ 1. Group accounts into common namespaces
46
+ 2. Provide consistency across user-defined fields
47
+ 3. Enable accurate generation of financial statements
48
+ 4. Facilitate financial ratio calculations
27
49
  """
28
50
  from itertools import groupby
29
51
  from random import randint
@@ -32,18 +54,19 @@ from uuid import uuid4
32
54
 
33
55
  from django.core.exceptions import ValidationError
34
56
  from django.db import models
35
- from django.db.models import Q
57
+ from django.db.models import Q, F, UniqueConstraint
36
58
  from django.db.models.signals import pre_save
37
59
  from django.urls import reverse
38
60
  from django.utils.translation import gettext_lazy as _
39
61
  from treebeard.mp_tree import MP_Node, MP_NodeManager, MP_NodeQuerySet
40
62
 
41
- from django_ledger.io.io_core import get_localdate
42
- from django_ledger.io.roles import (ACCOUNT_ROLE_CHOICES, BS_ROLES, GROUP_INVOICE, GROUP_BILL, validate_roles,
43
- GROUP_ASSETS,
44
- GROUP_LIABILITIES, GROUP_CAPITAL, GROUP_INCOME, GROUP_EXPENSES, GROUP_COGS,
45
- ROOT_GROUP, BS_BUCKETS, ROOT_ASSETS, ROOT_LIABILITIES,
46
- ROOT_CAPITAL, ROOT_INCOME, ROOT_EXPENSES, ROOT_COA)
63
+ from django_ledger.io.roles import (
64
+ ACCOUNT_ROLE_CHOICES, BS_ROLES, GROUP_INVOICE, GROUP_BILL, validate_roles,
65
+ GROUP_ASSETS, GROUP_LIABILITIES, GROUP_CAPITAL, GROUP_INCOME, GROUP_EXPENSES, GROUP_COGS,
66
+ ROOT_GROUP, BS_BUCKETS, ROOT_ASSETS, ROOT_LIABILITIES,
67
+ ROOT_CAPITAL, ROOT_INCOME, ROOT_EXPENSES, ROOT_COA, VALID_PARENTS,
68
+ ROLES_ORDER_ALL
69
+ )
47
70
  from django_ledger.models.mixins import CreateUpdateMixIn
48
71
  from django_ledger.models.utils import lazy_loader
49
72
  from django_ledger.settings import DJANGO_LEDGER_ACCOUNT_CODE_GENERATE, DJANGO_LEDGER_ACCOUNT_CODE_USE_PREFIX
@@ -61,381 +84,305 @@ class AccountModelValidationError(ValidationError):
61
84
 
62
85
  class AccountModelQuerySet(MP_NodeQuerySet):
63
86
  """
64
- A custom defined QuerySet, which inherits from the Materialized Path Tree implementation
65
- of Django Treebeard for tree-like model implementation.
87
+ Custom QuerySet for AccountModel inheriting from MP_NodeQuerySet.
66
88
  """
67
89
 
68
90
  def active(self):
69
91
  """
70
- Active accounts which can be used to create new transactions that show on drop-down menus and forms.
92
+ Filters the queryset to include only active items.
71
93
 
72
94
  Returns
73
- _______
95
+ -------
74
96
  AccountModelQuerySet
75
- A filtered AccountModelQuerySet of active accounts.
97
+ A filtered queryset containing only the items marked as active.
76
98
  """
77
99
  return self.filter(active=True)
78
100
 
79
101
  def inactive(self):
80
102
  """
81
- Inactive accounts cannot be used to create new transactions and don't show on drop-down menus and forms.
103
+ Filters and returns queryset entries where the active field is set to False.
82
104
 
83
105
  Returns
84
- _______
106
+ -------
85
107
  AccountModelQuerySet
86
- A filtered AccountModelQuerySet of inactive accounts.
108
+ A queryset containing entries with active=False.
87
109
  """
88
110
  return self.filter(active=False)
89
111
 
90
112
  def locked(self):
91
113
  """
92
- Filter locked elements.
93
-
94
- This method filters the elements based on the `locked` attribute and returns a filtered queryset.
114
+ Filters the queryset to include only locked AccountModels.
95
115
 
96
- Returns:
97
- A filtered queryset containing the locked elements.
116
+ Returns
117
+ -------
118
+ AccountModelQuerySet
119
+ A queryset containing only the objects with locked set to True.
98
120
  """
99
121
  return self.filter(locked=True)
100
122
 
101
123
  def unlocked(self):
102
124
  """
103
- Returns a filtered version of an object, excluding any locked items.
125
+ Returns a filtered list of items where the 'locked' attribute is set to False.
104
126
 
105
- Returns:
106
- A filtered version of the object, excluding any locked items.
127
+ Returns
128
+ -------
129
+ AccountModelQuerySet
130
+ A queryset of items with 'locked' attribute set to False
107
131
  """
108
132
  return self.filter(locked=False)
109
133
 
110
134
  def with_roles(self, roles: Union[List, str]):
111
135
  """
112
- This method is used to make query of accounts with a certain role. For instance, the fixed assets like
113
- Buildings have all been assigned the role of "asset_ppe_build" role is basically an aggregation of the
114
- accounts under a similar category. So, to query the list of all accounts under the role "asset_ppe_build",
115
- we can use this function.
136
+ Filter the accounts based on the specified roles. This method helps to retrieve accounts associated
137
+ with a particular role or a list of roles.
138
+
139
+ For example, to get all accounts categorized under the role "asset_ppe_build" (which might include
140
+ fixed assets like Buildings), you can utilize this method.
116
141
 
117
142
  Parameters
118
- __________
119
- roles: list or str
120
- Function accepts a single str instance of a role or a list of roles. For a list of roles , refer io.roles.py
143
+ ----------
144
+ roles : Union[List[str], str]
145
+ The role or a list of roles to filter the accounts by. If a single string is provided, it is converted
146
+ into a list containing that role.
121
147
 
122
148
  Returns
123
- _______
149
+ -------
124
150
  AccountModelQuerySet
125
- Returns a QuerySet filtered by user-provided list of Roles.
151
+ A QuerySet of accounts filtered by the provided roles.
126
152
  """
153
+ roles = validate_roles(roles)
127
154
  if isinstance(roles, str):
128
155
  roles = [roles]
129
156
  roles = validate_roles(roles)
130
157
  return self.filter(role__in=roles)
131
158
 
159
+ def with_codes(self, codes: Union[List, str]):
160
+ if isinstance(codes, str):
161
+ codes = [codes]
162
+ return self.filter(code__in=codes)
163
+
132
164
  def expenses(self):
133
165
  """
134
- Return the expenses filtered by the roles specified in GROUP_EXPENSES.
166
+ Retrieve a queryset containing expenses filtered by specified roles.
167
+
168
+ This method filters the expenses based on roles defined in the
169
+ `GROUP_EXPENSES` constant. It ensures that only the relevant expenses
170
+ associated with the specified roles are included in the queryset.
135
171
 
136
- Returns:
137
- QuerySet: A queryset containing the expenses filtered by the GROUP_EXPENSES roles..
172
+ Returns
173
+ -------
174
+ AccountModelQuerySet
175
+ A queryset consisting of expenses filtered according to the roles in `GROUP_EXPENSES`.
138
176
  """
139
177
  return self.filter(role__in=GROUP_EXPENSES)
140
178
 
141
179
  def is_coa_root(self):
142
180
  """
143
- Check if the account model instance is the Chart of Account Root.
181
+ Retrieves the Chart of Accounts (CoA) root node queryset.
182
+
183
+ A Chart of Accounts Root is a foundational element indicating the primary node in the
184
+ account hierarchy. This method filters the queryset to include only the Chart of Accounts (CoA)
185
+ root node.
144
186
 
145
- Returns:
146
- bool: True if the Account is the CoA Root, False otherwise.
187
+ Returns
188
+ -------
189
+ AccountModelQuerySet
147
190
  """
148
191
  return self.filter(role__in=ROOT_GROUP)
149
192
 
150
193
  def not_coa_root(self):
194
+ """
195
+ Exclude AccountModels with ROOT_GROUP role from the QuerySet.
151
196
 
197
+ Returns
198
+ -------
199
+ AccountModelQuerySet
200
+ A QuerySet excluding users with role in ROOT_GROUP.
201
+ """
152
202
  return self.exclude(role__in=ROOT_GROUP)
153
203
 
154
- def for_entity(self, entity_slug, user_model):
155
- if isinstance(self, lazy_loader.get_entity_model()):
156
- return self.filter(
157
- Q(coa_model__entity=entity_slug) &
158
- (
159
- Q(coa_model__entity__admin=user_model) |
160
- Q(coa_model__entity__managers__in=[user_model])
161
- )
162
- ).order_by('code')
163
- return self.filter(
164
- Q(coa_model__entity__slug__exact=entity_slug) &
165
- (
166
- Q(coa_model__entity__admin=user_model) |
167
- Q(coa_model__entity__managers__in=[user_model])
168
- )
169
- ).order_by('code')
170
-
171
204
  def gb_bs_role(self):
172
- accounts_gb = list((r, list(gb)) for r, gb in groupby(self, key=lambda acc: acc.get_bs_bucket()))
205
+ """
206
+ Groups accounts by Balance Sheet Bucket and then further groups them by role.
207
+
208
+ Returns
209
+ -------
210
+ List[Tuple]
211
+ A list where each element is a tuple. The first element of the tuple is the BS bucket,
212
+ and the second element is a list of tuples where each sub-tuple contains a role display
213
+ and a list of accounts that fall into that role within the BS bucket.
214
+ """
215
+ accounts_gb = list(
216
+ (r, sorted(list(gb), key=lambda acc: ROLES_ORDER_ALL.index(acc.role))) for r, gb in
217
+ groupby(self, key=lambda acc: acc.get_bs_bucket())
218
+ )
173
219
  return [
174
220
  (bsr, [
175
- (r, list(l)) for r, l in groupby(gb, key=lambda a: a.get_role_display())
221
+ (r, sorted(list(l), key=lambda acc: acc.code)) for r, l in
222
+ groupby(gb, key=lambda a: a.get_role_display())
176
223
  ]) for bsr, gb in accounts_gb
177
224
  ]
178
225
 
179
226
  def is_role_default(self):
180
- return self.not_coa_root().filter(role_default=True)
181
-
182
- def can_transact(self):
183
- return self.filter(
184
- Q(locked=False) & Q(active=True)
185
- )
186
-
187
-
188
- class AccountModelManager(MP_NodeManager):
189
- """
190
- AccountModelManager class provides methods to manage and retrieve AccountModel objects.
191
- It inherits from MP_NodeManager for tree-like model implementation.
192
- """
193
-
194
- def get_queryset(self) -> AccountModelQuerySet:
195
- """
196
- Sets the custom queryset as the default.
197
- """
198
- return AccountModelQuerySet(
199
- self.model,
200
- using=self._db
201
- ).order_by('path').select_related('coa_model')
202
-
203
- def for_user(self, user_model):
204
- qs = self.get_queryset()
205
- if user_model.is_superuser:
206
- return qs
207
- return qs.filter(
208
- Q(coa_model__entity__admin=user_model) |
209
- Q(coa_model__entity__managers__in=[user_model])
210
- )
211
-
212
- # todo: search for uses and pass EntityModel whenever possible.
213
- def for_entity(self,
214
- user_model,
215
- entity_slug,
216
- coa_slug: Optional[str] = None,
217
- select_coa_model: bool = True) -> AccountModelQuerySet:
218
227
  """
219
- Ensures that only accounts associated with the given EntityModel are returned.
220
-
221
- Parameters
222
- ----------
223
- entity_slug: EntityModel or str
224
- The EntityModel or EntityModel slug to pull accounts from. If slug is passed and coa_slug is None will
225
- result in an additional Database query to determine the default code of accounts.
226
- coa_slug: str
227
- Explicitly specify which chart of accounts to use. If None, will pull default Chart of Accounts.
228
- Discussed in detail in the CoA Model CoA slug, basically helps in identifying the complete Chart of
229
- Accounts for a particular EntityModel.
230
- user_model:
231
- The Django User Model making the request to check for permissions.
232
- select_coa_model: bool
233
- Pre fetches the CoA Model information in the QuerySet. Defaults to True.
228
+ Filter the queryset to include only entries where `role_default`
229
+ is set to True, excluding entries marked as 'coa_root'.
234
230
 
235
231
  Returns
236
232
  -------
237
233
  AccountModelQuerySet
238
- A QuerySet of all requested EntityModel Chart of Accounts.
234
+ Filtered queryset with `role_default` set to True and excluding 'coa_root' entries.
239
235
  """
240
- qs = self.for_user(user_model)
241
- if select_coa_model:
242
- qs = qs.select_related('coa_model')
243
-
244
- EntityModel = lazy_loader.get_entity_model()
245
- if isinstance(entity_slug, EntityModel):
246
- entity_model = entity_slug
247
- qs = qs.filter(coa_model__entity=entity_model)
248
- elif isinstance(entity_slug, str):
249
- qs = qs.filter(coa_model__entity__slug__exact=entity_slug)
250
- else:
251
- raise AccountModelValidationError(message='Must pass an instance of EntityModel or String for entity_slug.')
252
-
253
- if coa_slug:
254
- qs = qs.filter(coa_model__slug__exact=coa_slug)
255
- return qs.order_by('coa_model')
236
+ return self.not_coa_root().filter(role_default=True)
256
237
 
257
- def for_entity_available(self, user_model, entity_slug, coa_slug: Optional[str] = None) -> AccountModelQuerySet:
238
+ def can_transact(self):
258
239
  """
259
- Convenience method to pull only available and unlocked AccountModels for a specific EntityModel.
260
-
261
- Parameters
262
- ----------
263
- entity_slug: EntityModel or str
264
- The EntityModel or EntityModel slug to pull accounts from. If slug is passed and coa_slug is None will
265
- result in an additional Database query to determine the default code of accounts.
266
-
267
- coa_slug: str
268
- Explicitly specify which chart of accounts to use. If None, will pull default Chart of Accounts.
269
- Discussed in detail in the CoA Model CoA slug, basically helps in identifying the complete Chart of
270
- Accounts for a particular EntityModel.
271
-
272
- user_model:
273
- The Django User Model making the request to check for permissions.
240
+ Filter the queryset to include only accounts that can accept new transactions.
274
241
 
275
242
  Returns
276
243
  -------
277
- AccountModelQuerySet
278
- A QuerySet of all requested EntityModel Chart of Accounts.
244
+ QuerySet
245
+ A QuerySet containing the filtered results.
279
246
  """
280
- qs = self.for_entity(
281
- user_model=user_model,
282
- entity_slug=entity_slug,
283
- coa_slug=coa_slug)
284
- return qs.filter(
247
+ return self.filter(
248
+ Q(locked=False) &
285
249
  Q(active=True) &
250
+ Q(coa_model__active=True)
251
+ )
252
+
253
+ def available(self):
254
+ return self.filter(
286
255
  Q(locked=False) &
256
+ Q(active=True) &
287
257
  Q(coa_model__active=True)
288
258
  )
289
259
 
290
- def with_roles(self, roles: Union[list, str], entity_slug, user_model) -> AccountModelQuerySet:
260
+ def for_bill(self):
291
261
  """
292
- This method is used to make query of accounts with a certain role. For instance, the fixed assets like
293
- Buildings have all been assigned the role of "asset_ppe_build" role is basically an aggregation of the
294
- accounts under a similar category. So, to query the list of all accounts under the role "asset_ppe_build",
295
- we can use this function.
262
+ Retrieves only available and unlocked AccountModels for a specific EntityModel,
263
+ specifically for the creation and management of Bills. Roles within the 'GROUP_BILL'
264
+ context include: ASSET_CA_CASH, ASSET_CA_PREPAID, and LIABILITY_CL_ACC_PAYABLE.
296
265
 
297
- Parameters
298
- ----------
299
- entity_slug: EntityModel or str
300
- The EntityModel or EntityModel slug to pull accounts from. If slug is passed and coa_slug is None will
301
- result in an additional Database query to determine the default code of accounts.
302
- user_model
303
- The Django User Model making the request to check for permissions.
304
- roles: list or str
305
- Function accepts a single str instance of a role or a list of roles. For a list of roles , refer io.roles.py
306
266
  Returns
307
267
  -------
308
268
  AccountModelQuerySet
309
- Returns a QuerySet filtered by user-provided list of Roles.
269
+ A QuerySet of the requested EntityModel's chart of accounts.
310
270
  """
311
- roles = validate_roles(roles)
312
- if isinstance(roles, str):
313
- roles = [roles]
314
- qs = self.for_entity(entity_slug=entity_slug, user_model=user_model)
315
- return qs.filter(role__in=roles)
271
+ return self.available().filter(role__in=GROUP_BILL)
316
272
 
317
- def with_roles_available(self, roles: Union[list, str],
318
- entity_slug,
319
- user_model,
320
- coa_slug: Optional[str]) -> AccountModelQuerySet:
273
+ def for_invoice(self):
321
274
  """
322
- Convenience method to pull only available and unlocked AccountModels for a specific EntityModel and for a
323
- specific list of roles.
275
+ Retrieves available and unlocked AccountModels for a specific EntityModel, specifically for the creation
276
+ and management of Invoices.
324
277
 
325
- Parameters
326
- ----------
327
- entity_slug: EntityModel or str
328
- The EntityModel or EntityModel slug to pull accounts from. If slug is passed and coa_slug is None will
329
- result in an additional Database query to determine the default code of accounts.
330
- coa_slug: str
331
- Explicitly specify which chart of accounts to use. If None, will pull default Chart of Accounts.
332
- Discussed in detail in the CoA Model CoA slug, basically helps in identifying the complete Chart of
333
- Accounts for a particular EntityModel.
334
- user_model:
335
- The Django User Model making the request to check for permissions.
336
- roles: list or str
337
- Function accepts a single str instance of a role or a list of roles. For a list of roles , refer io.roles.py
278
+ This method ensures that only relevant accounts are pulled, as defined under the roles in `GROUP_INVOICE`.
279
+ These roles include: ASSET_CA_CASH, ASSET_CA_RECEIVABLES, and LIABILITY_CL_DEFERRED_REVENUE.
338
280
 
339
281
  Returns
340
282
  -------
341
283
  AccountModelQuerySet
342
- A QuerySet of all requested EntityModel Chart of Accounts.
284
+ A QuerySet containing the AccountModels relevant for the specified EntityModel and the roles defined
285
+ in `GROUP_INVOICE`.
343
286
  """
287
+ return self.available().filter(role__in=GROUP_INVOICE)
344
288
 
345
- if isinstance(roles, str):
346
- roles = [roles]
347
- roles = validate_roles(roles)
348
- qs = self.for_entity_available(entity_slug=entity_slug, user_model=user_model)
349
- return qs.filter(role__in=roles)
350
289
 
351
- def coa_roots(self, user_model, entity_slug, coa_slug) -> AccountModelQuerySet:
290
+ class AccountModelManager(MP_NodeManager):
291
+ """
292
+ AccountModelManager class provides methods to manage and retrieve AccountModel objects.
293
+ It inherits from MP_NodeManager for tree-like model implementation.
294
+ """
295
+
296
+ def get_queryset(self) -> AccountModelQuerySet:
352
297
  """
353
- Fetches the Code of Account Root Accounts.
298
+ Retrieve and return athe default AccountModel QuerySet.
354
299
 
355
- Parameters
356
- ----------
357
- entity_slug: EntityModel or str
358
- The EntityModel or EntityModel slug to pull accounts from. If slug is passed and coa_slug is None will
359
- result in an additional Database query to determine the default code of accounts.
360
- coa_slug: str
361
- Explicitly specify which chart of accounts to use. If None, will pull default Chart of Accounts.
362
- Discussed in detail in the CoA Model CoA slug, basically helps in identifying the complete Chart of
363
- Accounts for a particular EntityModel.
364
- user_model:
365
- The Django User Model making the request to check for permissions.
300
+ The query set is ordered by the 'path' field and uses 'select_related' to reduce the number of database queries
301
+ by retrieving the related 'coa_model'.
366
302
 
367
303
  Returns
368
304
  -------
369
-
305
+ AccountModelQuerySet
306
+ An instance of AccountModelQuerySet ordered by 'path' and prefetching related 'coa_model'.
370
307
  """
371
- qs = self.for_entity(user_model=user_model, entity_slug=entity_slug, coa_slug=coa_slug)
372
- return qs.is_coa_root()
308
+ return AccountModelQuerySet(
309
+ self.model,
310
+ using=self._db
311
+ ).order_by('path').select_related(
312
+ 'coa_model').annotate(
313
+ _coa_slug=F('coa_model__slug'),
314
+ _coa_active=F('coa_model__active'),
315
+ _entity_slug=F('coa_model__entity__slug'),
316
+ )
373
317
 
374
- def for_invoice(self, user_model, entity_slug: str, coa_slug: Optional[str] = None) -> AccountModelQuerySet:
318
+ def for_user(self, user_model) -> AccountModelQuerySet:
375
319
  """
376
- Convenience method to pull only available and unlocked AccountModels for a specific EntityModel relevant only
377
- for creating and management of Invoices. See :func:`GROUP_INVOICE <django_ledger.io.roles.GROUP_INVOICE>`.
378
-
379
- Roles in GROUP_INVOICE: ASSET_CA_CASH, ASSET_CA_RECEIVABLES, LIABILITY_CL_DEFERRED_REVENUE.
380
-
381
320
  Parameters
382
- __________
383
-
384
- entity_slug: EntityModel or str
385
- The EntityModel or EntityModel slug to pull accounts from. If slug is passed and coa_slug is None will
386
- result in an additional Database query to determine the default code of accounts.
387
-
388
- coa_slug: str
389
- Explicitly specify which chart of accounts to use. If None, will pull default Chart of Accounts.
390
- Discussed in detail in the CoA Model CoA slug, basically helps in identifying the complete Chart of
391
- Accounts for a particular EntityModel.
392
-
393
- user_model:
394
- The Django User Model making the request to check for permissions.
321
+ ----------
322
+ user_model : UserModel
323
+ The user model instance to use for filtering.
395
324
 
396
325
  Returns
397
- _______
326
+ -------
398
327
  AccountModelQuerySet
399
- A QuerySet of all requested EntityModel Chart of Accounts.
328
+ The filtered queryset based on the user's permissions. Superusers get the complete queryset whereas other
329
+ users get a filtered queryset based on their role as admin or manager in the entity.
400
330
  """
401
- qs = self.for_entity_available(
402
- user_model=user_model,
403
- entity_slug=entity_slug,
404
- coa_slug=coa_slug)
405
- return qs.filter(role__in=GROUP_INVOICE)
331
+ qs = self.get_queryset()
332
+ if user_model.is_superuser:
333
+ return qs
334
+ return qs.filter(
335
+ Q(coa_model__entity__admin=user_model) |
336
+ Q(coa_model__entity__managers__in=[user_model])
337
+ )
406
338
 
407
- def for_bill(self, user_model, entity_slug, coa_slug: Optional[str] = None) -> AccountModelQuerySet:
339
+ def for_entity(
340
+ self,
341
+ user_model,
342
+ entity_model,
343
+ coa_slug: Optional[str] = None
344
+ ) -> AccountModelQuerySet:
408
345
  """
409
- Convenience method to pull only available and unlocked AccountModels for a specific EntityModel relevant only
410
- for creating and management of Bills. See :func:`GROUP_BILL <django_ledger.io.roles.GROUP_BILL>`.
411
-
412
- Roles in GROUP_BILL: ASSET_CA_CASH, ASSET_CA_PREPAID, LIABILITY_CL_ACC_PAYABLE.
346
+ Retrieve accounts associated with a specified EntityModel and Chart of Accounts.
413
347
 
414
348
  Parameters
415
- __________
416
-
417
- entity_slug: EntityModel or str
418
- The EntityModel or EntityModel slug to pull accounts from. If slug is passed and coa_slug is None will
419
- result in an additional Database query to determine the default code of accounts.
420
-
421
- coa_slug: str
422
- Explicitly specify which chart of accounts to use. If None, will pull default Chart of Accounts.
423
- Discussed in detail in the CoA Model CoA slug, basically helps in identifying the complete Chart of
424
- Accounts for a particular EntityModel.
425
-
426
- user_model:
427
- The Django User Model making the request to check for permissions.
349
+ ----------
350
+ user_model : User
351
+ The Django User instance initiating the request. Used to check for required permissions.
352
+ entity_model : Union[EntityModel, str]
353
+ An instance of EntityModel or its slug. This determines the entity whose accounts are being retrieved.
354
+ A database query will be carried out to identify the default Chart of Accounts.
355
+ coa_slug : Optional[str], default=None
356
+ The slug for a specific Chart of Accounts to be used. If None, the default Chart of Accounts will be selected.
428
357
 
429
358
  Returns
430
- _______
359
+ -------
431
360
  AccountModelQuerySet
432
- A QuerySet of all requested EntityModel Chart of Accounts.
361
+ A QuerySet containing accounts associated with the specified EntityModel and Chart of Accounts.
362
+
363
+ Raises
364
+ ------
365
+ AccountModelValidationError
366
+ If the entity_model is neither an instance of EntityModel nor a string.
433
367
  """
434
- qs = self.for_entity_available(
435
- user_model=user_model,
436
- entity_slug=entity_slug,
437
- coa_slug=coa_slug)
438
- return qs.filter(role__in=GROUP_BILL)
368
+ qs = self.for_user(user_model)
369
+ EntityModel = lazy_loader.get_entity_model()
370
+
371
+ if isinstance(entity_model, EntityModel):
372
+ entity_model = entity_model
373
+ qs = qs.filter(coa_model__entity=entity_model)
374
+ elif isinstance(entity_model, str):
375
+ qs = qs.filter(coa_model__entity__slug__exact=entity_model)
376
+ else:
377
+ raise AccountModelValidationError(
378
+ message='Must pass an instance of EntityModel or String for entity_slug.'
379
+ )
380
+
381
+ return qs.filter(
382
+ coa_model__slug__exact=coa_slug
383
+ ) if coa_slug else qs.filter(
384
+ coa_model__slug__exact=F('coa_model__entity__default_coa__slug')
385
+ )
439
386
 
440
387
 
441
388
  def account_code_validator(value: str):
@@ -444,42 +391,33 @@ def account_code_validator(value: str):
444
391
 
445
392
 
446
393
  class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
447
- """ AccountModelAbstract
448
-
394
+ """
449
395
  Abstract class representing an Account Model.
450
396
 
451
397
  Attributes
452
398
  ----------
453
399
  BALANCE_TYPE : list
454
- List of choices for the balance type of the account.
455
-
400
+ List of choices for the balance type of the account. Options include 'Credit' and 'Debit'.
456
401
  uuid : UUIDField
457
- UUID field representing the primary key of the account.
458
-
402
+ Unique identifier for each account instance.
459
403
  code : CharField
460
- CharField representing the account code.
461
-
404
+ Code representing the account, constrained by length and specific validation rules.
462
405
  name : CharField
463
- CharField representing the account name.
464
-
406
+ Name of the account, constrained by length.
465
407
  role : CharField
466
- CharField representing the account role.
467
-
408
+ Role associated with the account, with specific predefined choices.
468
409
  role_default : BooleanField
469
- BooleanField representing whether the account is a default account for the role.
470
-
410
+ Flag indicating if this account is the default for its role.
471
411
  balance_type : CharField
472
- CharField representing the balance type of the account. Must be 'debit' or 'credit'.
473
-
412
+ Type of balance the account holds, must be either 'debit' or 'credit'.
474
413
  locked : BooleanField
475
- BooleanField representing whether the account is locked.
476
-
414
+ Indicates whether the account is locked.
477
415
  active : BooleanField
478
- BooleanField representing whether the account is active.
479
-
416
+ Indicates whether the account is active.
480
417
  coa_model : ForeignKey
481
- ForeignKey representing the associated ChartOfAccountModel.
418
+ Reference to the associated ChartOfAccountModel.
482
419
  """
420
+
483
421
  BALANCE_TYPE = [
484
422
  (CREDIT, _('Credit')),
485
423
  (DEBIT, _('Debit'))
@@ -495,7 +433,6 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
495
433
  active = models.BooleanField(default=False, verbose_name=_('Active'))
496
434
  coa_model = models.ForeignKey('django_ledger.ChartOfAccountModel',
497
435
  on_delete=models.CASCADE,
498
- editable=False,
499
436
  verbose_name=_('Chart of Accounts'))
500
437
  objects = AccountModelManager()
501
438
  node_order_by = ['uuid']
@@ -505,9 +442,17 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
505
442
  ordering = ['-created']
506
443
  verbose_name = _('Account')
507
444
  verbose_name_plural = _('Accounts')
508
- unique_together = [
509
- ('coa_model', 'code'),
510
- ('coa_model', 'role', 'role_default')
445
+ constraints = [
446
+ UniqueConstraint(
447
+ fields=('coa_model', 'code'),
448
+ name='unique_code_for_coa_model',
449
+ violation_error_message=_('Account codes must be unique for each Chart of Accounts Model.')
450
+ ),
451
+ UniqueConstraint(
452
+ fields=('coa_model', 'role', 'role_default'),
453
+ name='only_one_account_assigned_as_default_for_role',
454
+ violation_error_message=_('Only one default account for role permitted.')
455
+ )
511
456
  ]
512
457
  indexes = [
513
458
  models.Index(fields=['role']),
@@ -527,6 +472,17 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
527
472
  x5=self.code
528
473
  )
529
474
 
475
+ @property
476
+ def coa_slug(self):
477
+ try:
478
+ return getattr(self, '_coa_slug')
479
+ except AttributeError:
480
+ return self.coa_model.slug
481
+
482
+ @property
483
+ def entity_slug(self):
484
+ return getattr(self, '_entity_slug')
485
+
530
486
  @classmethod
531
487
  def create_account(cls,
532
488
  name: str,
@@ -537,30 +493,33 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
537
493
  active: bool = False,
538
494
  **kwargs):
539
495
  """
540
- Convenience Method to Create a new Account Model. This is the preferred method to create new Accounts in order
541
- to properly handle parent/child relationships between models.
496
+ Create a new AccountModel instance, managing parent/child relationships properly.
497
+
498
+ This convenience method ensures correct creation of new accounts, handling the intricate logic needed for
499
+ maintaining hierarchical relationships between accounts.
542
500
 
543
501
  Parameters
544
502
  ----------
545
- name: str
546
- The name of the new Entity.
547
- role: str
548
- Account role.
549
- balance_type: str
550
- Account Balance Type. Must be 'debit' or 'credit'.
551
- is_role_default: bool
552
- If True, assigns account as default for role. Only once default account per role is permitted.
553
- locked: bool
554
- Marks account as Locked. Defaults to False.
555
- active: bool
556
- Marks account as Active. Defaults to True.
557
-
503
+ name : str
504
+ Name of the new account entity.
505
+ role : str
506
+ Role assigned to the account.
507
+ balance_type : str
508
+ Type of balance associated with the account. Must be either 'debit' or 'credit'.
509
+ is_role_default : bool, optional
510
+ Indicates if the account should be the default for its role. Only one default account per role is allowed.
511
+ Defaults to False.
512
+ locked : bool, optional
513
+ Flags the account as locked. Defaults to False.
514
+ active : bool, optional
515
+ Flags the account as active. Defaults to True.
516
+ **kwargs : dict, optional
517
+ Additional attributes for account creation.
558
518
 
559
519
  Returns
560
520
  -------
561
521
  AccountModel
562
- The newly created AccountModel instance.
563
-
522
+ The newly created `AccountModel` instance.
564
523
  """
565
524
  account_model = cls(
566
525
  name=name,
@@ -578,25 +537,35 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
578
537
  @property
579
538
  def role_bs(self) -> str:
580
539
  """
581
- The principal role of the account on the balance sheet.
582
- Options are:
583
- * asset
584
- * liability
585
- * equity
540
+ Returns the principal role of the account on the balance sheet.
541
+
542
+ The principal role can be one of the following:
543
+ - 'asset'
544
+ - 'liability'
545
+ - 'equity'
586
546
 
587
547
  Returns
588
548
  -------
589
549
  str
590
- A String representing the principal role of the account on the balance sheet.
550
+ A string representing the principal role of the account on the balance sheet.
591
551
  """
592
552
  return BS_ROLES.get(self.role)
593
553
 
594
554
  def is_root_account(self):
555
+ """
556
+ Checks if the current user's role belongs to the ROOT_GROUP.
557
+
558
+ Returns
559
+ -------
560
+ bool
561
+ True if the role is in the ROOT_GROUP, False otherwise
562
+ """
595
563
  return self.role in ROOT_GROUP
596
564
 
597
565
  def is_debit(self) -> bool:
598
566
  """
599
- Checks if the account has a DEBIT balance.
567
+ Checks if the account has a DEBIT balance type.
568
+
600
569
  Returns
601
570
  -------
602
571
  bool
@@ -606,7 +575,8 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
606
575
 
607
576
  def is_credit(self):
608
577
  """
609
- Checks if the account has a CREDIT balance.
578
+ Checks if the Account Model has a CREDIT balance type.
579
+
610
580
  Returns
611
581
  -------
612
582
  bool
@@ -615,43 +585,203 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
615
585
  return self.balance_type == CREDIT
616
586
 
617
587
  def is_coa_root(self):
588
+ """
589
+ Check if the current Account Model role is 'ROOT_COA'.
590
+
591
+ Returns
592
+ -------
593
+ bool
594
+ True if the role is 'ROOT_COA', False otherwise.
595
+ """
618
596
  return self.role == ROOT_COA
619
597
 
620
598
  def is_asset(self) -> bool:
599
+ """
600
+ Determines if the current Account Model role of the instance is considered an asset.
601
+
602
+ Returns
603
+ -------
604
+ bool
605
+ True if the role is part of the GROUP_ASSETS, False otherwise.
606
+ """
621
607
  return self.role in GROUP_ASSETS
622
608
 
623
609
  def is_liability(self) -> bool:
610
+ """
611
+ Determines if the current Account Model role is considered a liability.
612
+
613
+ Returns
614
+ -------
615
+ bool
616
+ True if the role is part of GROUP_LIABILITIES, otherwise False.
617
+ """
624
618
  return self.role in GROUP_LIABILITIES
625
619
 
626
620
  def is_capital(self) -> bool:
621
+ """
622
+ Checks if the current Account Model role is in the capital group.
623
+
624
+ Returns
625
+ -------
626
+ bool
627
+ True if the role is in GROUP_CAPITAL, otherwise False.
628
+ """
627
629
  return self.role in GROUP_CAPITAL
628
630
 
629
631
  def is_income(self) -> bool:
632
+ """
633
+ Determines whether the current Account Model role belongs to the income group.
634
+
635
+ Parameters
636
+ ----------
637
+ self : object
638
+ The instance of the class containing attribute 'role'.
639
+
640
+ Returns
641
+ -------
642
+ bool
643
+ True if the role is in the GROUP_INCOME list, False otherwise.
644
+ """
630
645
  return self.role in GROUP_INCOME
631
646
 
632
647
  def is_cogs(self) -> bool:
648
+ """
649
+ Determines if the role of the object is part of the GROUP_COGS.
650
+
651
+ Returns
652
+ -------
653
+ bool
654
+ True if the object's role is part of the GROUP_COGS, False otherwise.
655
+ """
633
656
  return self.role in GROUP_COGS
634
657
 
635
658
  def is_expense(self) -> bool:
659
+ """
660
+ Checks if the current Account Model `role` is categorized under `GROUP_EXPENSES`.
661
+
662
+ Parameters
663
+ ----------
664
+ None
665
+
666
+ Returns
667
+ -------
668
+ bool
669
+ True if `role` is in `GROUP_EXPENSES`, otherwise False.
670
+ """
636
671
  return self.role in GROUP_EXPENSES
637
672
 
638
673
  def is_active(self) -> bool:
674
+ """
675
+ Determines if the current instance is active.
676
+
677
+ Returns
678
+ -------
679
+ bool
680
+ True if the instance is active, otherwise False
681
+ """
639
682
  return self.active is True
640
683
 
641
684
  def is_locked(self) -> bool:
685
+ """
686
+ Determines if the current object is locked.
687
+
688
+ Returns
689
+ -------
690
+ bool
691
+ True if the object is locked, False otherwise.
692
+
693
+ """
642
694
  return self.locked is True
643
695
 
696
+ def is_coa_active(self) -> bool:
697
+ try:
698
+ return getattr(self, '_coa_active')
699
+ except AttributeError:
700
+ pass
701
+ return self.coa_model.active
702
+
644
703
  def can_activate(self):
704
+ """
705
+ Determines if the object can be activated.
706
+
707
+ Returns
708
+ -------
709
+ bool
710
+ True if the object is inactive, otherwise False.
711
+ """
645
712
  return all([
646
713
  self.active is False
647
714
  ])
648
715
 
649
716
  def can_deactivate(self):
717
+ """
718
+ Determine if the object can be deactivated.
719
+
720
+ Checks if the `active` attribute is set to `True`.
721
+
722
+ Returns
723
+ -------
724
+ bool
725
+ True if the object is currently active and can be deactivated, otherwise False.
726
+ """
650
727
  return all([
651
728
  self.active is True
652
729
  ])
653
730
 
731
+ def can_lock(self):
732
+ return all([
733
+ self.locked is False
734
+ ])
735
+
736
+ def can_unlock(self):
737
+ return all([
738
+ self.locked is True
739
+ ])
740
+
741
+ def lock(self, commit: bool = True, raise_exception: bool = True, **kwargs):
742
+ if not self.can_lock():
743
+ if raise_exception:
744
+ raise AccountModelValidationError(
745
+ message=_(f'Cannot lock account {self.code}: {self.name}. Active: {self.is_active()}')
746
+ )
747
+ return
748
+
749
+ self.locked = True
750
+ if commit:
751
+ self.save(update_fields=[
752
+ 'locked',
753
+ 'updated'
754
+ ])
755
+
756
+ def unlock(self, commit: bool = True, raise_exception: bool = True, **kwargs):
757
+ if not self.can_unlock():
758
+ if raise_exception:
759
+ raise AccountModelValidationError(
760
+ message=_(f'Cannot unlock account {self.code}: {self.name}. Active: {self.is_active()}')
761
+ )
762
+ return
763
+
764
+ self.locked = False
765
+ if commit:
766
+ self.save(update_fields=[
767
+ 'locked',
768
+ 'updated'
769
+ ])
770
+
654
771
  def activate(self, commit: bool = True, raise_exception: bool = True, **kwargs):
772
+ """
773
+ Checks if the Account Model instance can be activated, then Activates the AccountModel instance.
774
+ Raises exception if AccountModel cannot be activated.
775
+
776
+ Parameters
777
+ ----------
778
+ commit : bool, optional
779
+ If True, commit the changes to the database by calling the save method.
780
+ raise_exception : bool, optional
781
+ If True, raises an AccountModelValidationError if the account cannot be activated.
782
+ kwargs : dict
783
+ Additional parameters that can be passed for further customization.
784
+ """
655
785
  if not self.can_activate():
656
786
  if raise_exception:
657
787
  raise AccountModelValidationError(
@@ -666,6 +796,19 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
666
796
  ])
667
797
 
668
798
  def deactivate(self, commit: bool = True, raise_exception: bool = True, **kwargs):
799
+ """
800
+ Checks if the Account Model instance can be de-activated, then De-activates the AccountModel instance.
801
+ Raises exception if AccountModel cannot be de-activated.
802
+
803
+ Parameters
804
+ ----------
805
+ commit : bool, optional
806
+ If True, commit the changes to the database by calling the save method.
807
+ raise_exception : bool, optional
808
+ If True, raises an AccountModelValidationError if the account cannot be activated.
809
+ kwargs : dict
810
+ Additional parameters that can be passed for further customization.
811
+ """
669
812
  if not self.can_deactivate():
670
813
  if raise_exception:
671
814
  raise AccountModelValidationError(
@@ -674,20 +817,51 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
674
817
  return
675
818
  self.active = False
676
819
  if commit:
677
- self.save(update_fields=[
678
- 'active',
679
- 'updated'
680
- ])
820
+ self.save(
821
+ update_fields=[
822
+ 'active',
823
+ 'updated'
824
+ ])
681
825
 
682
826
  def can_transact(self) -> bool:
827
+ """
828
+ Determines if a transaction can be performed based on multiple conditions.
829
+
830
+ Returns
831
+ -------
832
+ bool
833
+ True if all conditions are met, enabling a transaction; False otherwise.
834
+
835
+ Conditions:
836
+ 1. The chart of accounts (coa_model) must be active.
837
+ 2. The entity must not be locked.
838
+ 3. The entity itself must be active.
839
+ """
683
840
  return all([
684
- self.coa_model.is_active(),
685
- not self.is_locked(),
686
- self.is_active()
841
+ self.is_coa_active(),
842
+ not self.is_locked()
687
843
  ])
688
844
 
689
845
  def get_code_prefix(self) -> str:
846
+ """
847
+ Returns the code prefix based on the account type.
848
+
849
+ This method determines the account type by calling the respective
850
+ account type methods and returns the corresponding code prefix based on Accounting best practices..
690
851
 
852
+ Returns
853
+ -------
854
+ str
855
+ The code prefix for the account type. The possible values are:
856
+ '1' for assets, '2' for liabilities, '3' for capital,
857
+ '4' for income, '5' for cost of goods sold (COGS),
858
+ '6' for expenses.
859
+
860
+ Raises
861
+ ------
862
+ AccountModelValidationError
863
+ If the account role does not match any of the predefined categories.
864
+ """
691
865
  if self.is_asset():
692
866
  return '1'
693
867
  elif self.is_liability():
@@ -703,6 +877,19 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
703
877
  raise AccountModelValidationError(f'Invalid role match for role {self.role}...')
704
878
 
705
879
  def get_root_role(self) -> str:
880
+ """
881
+ Returns the root role corresponding to the account type.
882
+
883
+ Returns
884
+ -------
885
+ str
886
+ The root role corresponding to the account type.
887
+
888
+ Raises
889
+ ------
890
+ AccountModelValidationError
891
+ If no valid role match is found for the account's role.
892
+ """
706
893
  if self.is_asset():
707
894
  return ROOT_ASSETS
708
895
  elif self.is_liability():
@@ -720,10 +907,23 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
720
907
  raise AccountModelValidationError(f'Invalid role match for role {self.role}...')
721
908
 
722
909
  def get_account_move_choice_queryset(self):
910
+ """
911
+ Retrieves a filtered queryset of account models that the current Account Model instance
912
+ can be a child of.
913
+
914
+ The queryset is filtered based on the specified role and its hierarchical parent roles.
915
+ Account models with a UUID matching the current instance's UUID are excluded from the results.
916
+
917
+ Returns
918
+ -------
919
+ QuerySet
920
+ A filtered set of account models suitable for moving the current instance under.
921
+ """
723
922
  return self.coa_model.accountmodel_set.filter(
724
923
  role__in=[
725
924
  self.role,
726
- self.get_root_role()
925
+ self.get_root_role(),
926
+ *VALID_PARENTS.get(self.role, [])
727
927
  ],
728
928
  ).exclude(uuid__exact=self.uuid)
729
929
 
@@ -731,12 +931,41 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
731
931
  return BS_BUCKETS[self.get_code_prefix()]
732
932
 
733
933
  def is_indented(self):
934
+ """
935
+ Check if the current depth level is greater than 2.
936
+
937
+ Returns
938
+ -------
939
+ bool
940
+ True if the depth is greater than 2, False otherwise.
941
+ """
734
942
  return self.depth > 2
735
943
 
736
944
  def get_html_pixel_indent(self):
945
+ """
946
+ Calculates the pixel indentation for HTML elements based on the depth attribute for UI purposes
947
+
948
+ Returns
949
+ -------
950
+ str
951
+ The calculated pixel indentation as a string with 'px' suffix.
952
+ """
737
953
  return f'{(self.depth - 2) * 40}px'
738
954
 
739
955
  def generate_random_code(self):
956
+ """
957
+ Generates a random code for the account adding a prefix 1-6 depending on account role.
958
+
959
+ Raises
960
+ ------
961
+ AccountModelValidationError
962
+ If the account role is not assigned before code generation.
963
+
964
+ Returns
965
+ -------
966
+ str
967
+ A randomly generated code prefixed with a role-based prefix.
968
+ """
740
969
  if not self.role:
741
970
  raise AccountModelValidationError('Must assign account role before generate random code')
742
971
 
@@ -744,18 +973,76 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
744
973
  ri = randint(10000, 99999)
745
974
  return f'{prefix}{ri}'
746
975
 
747
- def get_absolute_url(self):
976
+ # URLS...
977
+ def get_absolute_url(self) -> str:
748
978
  return reverse(
749
- viewname='django_ledger:account-detail-year',
979
+ viewname='django_ledger:account-detail',
750
980
  kwargs={
751
981
  'account_pk': self.uuid,
752
- 'entity_slug': self.coa_model.entity.slug,
753
- 'year': get_localdate().year
982
+ 'entity_slug': self.entity_slug,
983
+ 'coa_slug': self.coa_slug
754
984
  }
755
985
  )
756
986
 
757
- def clean(self):
987
+ def get_update_url(self) -> str:
988
+ return reverse(
989
+ viewname='django_ledger:account-update',
990
+ kwargs={
991
+ 'account_pk': self.uuid,
992
+ 'entity_slug': self.entity_slug,
993
+ 'coa_slug': self.coa_slug
994
+ }
995
+ )
996
+
997
+ def get_action_deactivate_url(self) -> str:
998
+ return reverse(
999
+ viewname='django_ledger:account-action-deactivate',
1000
+ kwargs={
1001
+ 'account_pk': self.uuid,
1002
+ 'entity_slug': self.entity_slug,
1003
+ 'coa_slug': self.coa_slug
1004
+ }
1005
+ )
1006
+
1007
+ def get_action_activate_url(self) -> str:
1008
+ return reverse(
1009
+ viewname='django_ledger:account-action-activate',
1010
+ kwargs={
1011
+ 'account_pk': self.uuid,
1012
+ 'entity_slug': self.entity_slug,
1013
+ 'coa_slug': self.coa_slug
1014
+ }
1015
+ )
758
1016
 
1017
+ def get_action_lock_url(self) -> str:
1018
+ return reverse(
1019
+ viewname='django_ledger:account-action-lock',
1020
+ kwargs={
1021
+ 'account_pk': self.uuid,
1022
+ 'entity_slug': self.entity_slug,
1023
+ 'coa_slug': self.coa_slug
1024
+ }
1025
+ )
1026
+
1027
+ def get_action_unlock_url(self) -> str:
1028
+ return reverse(
1029
+ viewname='django_ledger:account-action-unlock',
1030
+ kwargs={
1031
+ 'account_pk': self.uuid,
1032
+ 'entity_slug': self.entity_slug,
1033
+ 'coa_slug': self.coa_slug
1034
+ }
1035
+ )
1036
+
1037
+ def get_coa_account_list_url(self) -> str:
1038
+ return reverse(
1039
+ viewname='django_ledger:account-list',
1040
+ kwargs={
1041
+ 'entity_slug': self.entity_slug,
1042
+ 'coa_slug': self.coa_slug
1043
+ })
1044
+
1045
+ def clean(self):
759
1046
  if not self.code and DJANGO_LEDGER_ACCOUNT_CODE_GENERATE:
760
1047
  self.code = self.generate_random_code()
761
1048