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.
- django_ledger/__init__.py +1 -4
- django_ledger/admin/coa.py +1 -2
- django_ledger/contrib/django_ledger_graphene/accounts/schema.py +1 -1
- django_ledger/forms/account.py +60 -44
- django_ledger/forms/bank_account.py +3 -2
- django_ledger/forms/bill.py +24 -36
- django_ledger/forms/customer.py +1 -1
- django_ledger/forms/data_import.py +3 -3
- django_ledger/forms/estimate.py +1 -1
- django_ledger/forms/invoice.py +5 -7
- django_ledger/forms/item.py +24 -15
- django_ledger/forms/transactions.py +3 -3
- django_ledger/io/io_core.py +4 -2
- django_ledger/io/io_middleware.py +5 -0
- django_ledger/io/roles.py +6 -0
- django_ledger/migrations/0017_alter_accountmodel_unique_together_and_more.py +31 -0
- django_ledger/models/accounts.py +629 -342
- django_ledger/models/bank_account.py +0 -4
- django_ledger/models/bill.py +0 -3
- django_ledger/models/closing_entry.py +0 -3
- django_ledger/models/coa.py +59 -48
- django_ledger/models/coa_default.py +9 -8
- django_ledger/models/customer.py +0 -4
- django_ledger/models/data_import.py +0 -3
- django_ledger/models/entity.py +80 -40
- django_ledger/models/estimate.py +0 -9
- django_ledger/models/invoice.py +0 -3
- django_ledger/models/items.py +4 -6
- django_ledger/models/journal_entry.py +2 -5
- django_ledger/models/ledger.py +0 -3
- django_ledger/models/mixins.py +0 -3
- django_ledger/models/purchase_order.py +0 -4
- django_ledger/models/signals.py +0 -3
- django_ledger/models/transactions.py +2 -5
- django_ledger/models/unit.py +0 -3
- django_ledger/models/utils.py +0 -3
- django_ledger/models/vendor.py +0 -3
- django_ledger/report/cash_flow_statement.py +1 -1
- django_ledger/static/.DS_Store +0 -0
- django_ledger/static/django_ledger/.DS_Store +0 -0
- django_ledger/static/django_ledger/logo_2/.DS_Store +0 -0
- django_ledger/static/django_ledger/logo_2/django_ledger_logo_dark.png +0 -0
- django_ledger/static/django_ledger/logo_2/django_ledger_logo_dark@0.5x.png +0 -0
- django_ledger/static/django_ledger/logo_2/django_ledger_logo_dark@2x.png +0 -0
- django_ledger/static/django_ledger/logo_2/django_ledger_logo_dark@3x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-full-vert.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-full-vert@0.5x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-full-vert@2x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-full-vert@3x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-logo-full-horiz.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-logo-full-horiz@0.5x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-logo-full-horiz@2x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-logo-full-horiz@3x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-logo-full-vert.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-logo-full-vert@0.5x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-logo-full-vert@2x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-logo-full-vert@3x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-logo.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-logo@0.5x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-logo@2x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-logo@3x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-txt-full-horiz.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-txt-full-horiz@0.5x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-txt-full-horiz@2x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-txt-full-horiz@3x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-txt-full-vert.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-txt-full-vert@0.5x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-txt-full-vert@2x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-txt-full-vert@3x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-txt-horiz.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-txt-horiz@0.5x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-txt-horiz@2x.png +0 -0
- django_ledger/static/django_ledger/logo_2/djl-txt-horiz@3x.png +0 -0
- django_ledger/templates/django_ledger/account/account_create.html +2 -2
- django_ledger/templates/django_ledger/account/account_update.html +1 -1
- django_ledger/templates/django_ledger/account/tags/account_txs_table.html +1 -0
- django_ledger/templates/django_ledger/account/tags/accounts_table.html +27 -18
- django_ledger/templates/django_ledger/bills/bill_detail.html +3 -3
- django_ledger/templates/django_ledger/expense/tags/expense_item_table.html +7 -0
- django_ledger/templates/django_ledger/invoice/invoice_detail.html +3 -3
- django_ledger/templatetags/django_ledger.py +7 -1
- django_ledger/tests/base.py +23 -7
- django_ledger/tests/test_accounts.py +145 -9
- django_ledger/urls/account.py +17 -24
- django_ledger/utils.py +8 -0
- django_ledger/views/__init__.py +1 -1
- django_ledger/views/account.py +80 -118
- django_ledger/views/auth.py +1 -1
- django_ledger/views/bank_account.py +9 -11
- django_ledger/views/bill.py +91 -80
- django_ledger/views/closing_entry.py +8 -0
- django_ledger/views/coa.py +2 -1
- django_ledger/views/customer.py +1 -1
- django_ledger/views/data_import.py +1 -1
- django_ledger/views/entity.py +1 -1
- django_ledger/views/estimate.py +13 -8
- django_ledger/views/feedback.py +1 -1
- django_ledger/views/financial_statement.py +1 -1
- django_ledger/views/home.py +1 -1
- django_ledger/views/inventory.py +9 -0
- django_ledger/views/invoice.py +5 -2
- django_ledger/views/item.py +58 -68
- django_ledger/views/journal_entry.py +1 -1
- django_ledger/views/ledger.py +3 -1
- django_ledger/views/mixins.py +9 -8
- django_ledger/views/purchase_order.py +1 -1
- django_ledger/views/transactions.py +1 -1
- django_ledger/views/unit.py +9 -0
- django_ledger/views/vendor.py +1 -1
- {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/AUTHORS.md +8 -2
- {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/METADATA +34 -43
- {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/RECORD +115 -79
- {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/WHEEL +1 -1
- {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/top_level.txt +0 -1
- {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/LICENSE +0 -0
django_ledger/models/accounts.py
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
depending on its purpose.
|
|
10
|
+
Account Types
|
|
11
|
+
-------------
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
19
|
-
|
|
15
|
+
1. **DEBIT-type balance account**
|
|
16
|
+
2. **CREDIT-type balance account**
|
|
20
17
|
|
|
21
|
-
|
|
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
|
-
|
|
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.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
+
Filters the queryset to include only active items.
|
|
71
93
|
|
|
72
94
|
Returns
|
|
73
|
-
|
|
95
|
+
-------
|
|
74
96
|
AccountModelQuerySet
|
|
75
|
-
A filtered
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
125
|
+
Returns a filtered list of items where the 'locked' attribute is set to False.
|
|
104
126
|
|
|
105
|
-
Returns
|
|
106
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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:
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
234
|
+
Filtered queryset with `role_default` set to True and excluding 'coa_root' entries.
|
|
239
235
|
"""
|
|
240
|
-
|
|
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
|
|
238
|
+
def can_transact(self):
|
|
258
239
|
"""
|
|
259
|
-
|
|
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
|
-
|
|
278
|
-
A QuerySet
|
|
244
|
+
QuerySet
|
|
245
|
+
A QuerySet containing the filtered results.
|
|
279
246
|
"""
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
260
|
+
def for_bill(self):
|
|
291
261
|
"""
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
269
|
+
A QuerySet of the requested EntityModel's chart of accounts.
|
|
310
270
|
"""
|
|
311
|
-
|
|
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
|
|
318
|
-
entity_slug,
|
|
319
|
-
user_model,
|
|
320
|
-
coa_slug: Optional[str]) -> AccountModelQuerySet:
|
|
273
|
+
def for_invoice(self):
|
|
321
274
|
"""
|
|
322
|
-
|
|
323
|
-
|
|
275
|
+
Retrieves available and unlocked AccountModels for a specific EntityModel, specifically for the creation
|
|
276
|
+
and management of Invoices.
|
|
324
277
|
|
|
325
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
298
|
+
Retrieve and return athe default AccountModel QuerySet.
|
|
354
299
|
|
|
355
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
339
|
+
def for_entity(
|
|
340
|
+
self,
|
|
341
|
+
user_model,
|
|
342
|
+
entity_model,
|
|
343
|
+
coa_slug: Optional[str] = None
|
|
344
|
+
) -> AccountModelQuerySet:
|
|
408
345
|
"""
|
|
409
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
coa_slug: str
|
|
422
|
-
|
|
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
|
|
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.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
458
|
-
|
|
402
|
+
Unique identifier for each account instance.
|
|
459
403
|
code : CharField
|
|
460
|
-
|
|
461
|
-
|
|
404
|
+
Code representing the account, constrained by length and specific validation rules.
|
|
462
405
|
name : CharField
|
|
463
|
-
|
|
464
|
-
|
|
406
|
+
Name of the account, constrained by length.
|
|
465
407
|
role : CharField
|
|
466
|
-
|
|
467
|
-
|
|
408
|
+
Role associated with the account, with specific predefined choices.
|
|
468
409
|
role_default : BooleanField
|
|
469
|
-
|
|
470
|
-
|
|
410
|
+
Flag indicating if this account is the default for its role.
|
|
471
411
|
balance_type : CharField
|
|
472
|
-
|
|
473
|
-
|
|
412
|
+
Type of balance the account holds, must be either 'debit' or 'credit'.
|
|
474
413
|
locked : BooleanField
|
|
475
|
-
|
|
476
|
-
|
|
414
|
+
Indicates whether the account is locked.
|
|
477
415
|
active : BooleanField
|
|
478
|
-
|
|
479
|
-
|
|
416
|
+
Indicates whether the account is active.
|
|
480
417
|
coa_model : ForeignKey
|
|
481
|
-
|
|
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
|
-
|
|
509
|
-
(
|
|
510
|
-
|
|
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
|
-
|
|
541
|
-
|
|
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
|
-
|
|
547
|
-
role: str
|
|
548
|
-
|
|
549
|
-
balance_type: str
|
|
550
|
-
|
|
551
|
-
is_role_default: bool
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
|
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
|
|
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(
|
|
678
|
-
|
|
679
|
-
|
|
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.
|
|
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
|
-
|
|
976
|
+
# URLS...
|
|
977
|
+
def get_absolute_url(self) -> str:
|
|
748
978
|
return reverse(
|
|
749
|
-
viewname='django_ledger:account-detail
|
|
979
|
+
viewname='django_ledger:account-detail',
|
|
750
980
|
kwargs={
|
|
751
981
|
'account_pk': self.uuid,
|
|
752
|
-
'entity_slug': self.
|
|
753
|
-
'
|
|
982
|
+
'entity_slug': self.entity_slug,
|
|
983
|
+
'coa_slug': self.coa_slug
|
|
754
984
|
}
|
|
755
985
|
)
|
|
756
986
|
|
|
757
|
-
def
|
|
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
|
|