django-ledger 0.7.11__py3-none-any.whl → 0.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-ledger might be problematic. Click here for more details.
- django_ledger/__init__.py +1 -1
- django_ledger/context.py +12 -0
- django_ledger/forms/account.py +45 -46
- django_ledger/forms/bill.py +0 -4
- django_ledger/forms/closing_entry.py +13 -1
- django_ledger/forms/data_import.py +182 -63
- django_ledger/forms/estimate.py +3 -6
- django_ledger/forms/invoice.py +3 -7
- django_ledger/forms/item.py +10 -18
- django_ledger/forms/purchase_order.py +2 -4
- django_ledger/io/io_core.py +515 -400
- django_ledger/io/io_generator.py +7 -6
- django_ledger/io/io_library.py +1 -2
- django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
- django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
- django_ledger/models/__init__.py +2 -1
- django_ledger/models/accounts.py +109 -69
- django_ledger/models/bank_account.py +40 -23
- django_ledger/models/bill.py +386 -333
- django_ledger/models/chart_of_accounts.py +173 -105
- django_ledger/models/closing_entry.py +99 -48
- django_ledger/models/customer.py +100 -66
- django_ledger/models/data_import.py +818 -323
- django_ledger/models/deprecations.py +61 -0
- django_ledger/models/entity.py +891 -644
- django_ledger/models/estimate.py +57 -28
- django_ledger/models/invoice.py +46 -26
- django_ledger/models/items.py +503 -142
- django_ledger/models/journal_entry.py +61 -47
- django_ledger/models/ledger.py +106 -42
- django_ledger/models/mixins.py +424 -281
- django_ledger/models/purchase_order.py +39 -17
- django_ledger/models/receipt.py +1083 -0
- django_ledger/models/transactions.py +242 -139
- django_ledger/models/unit.py +93 -54
- django_ledger/models/utils.py +12 -2
- django_ledger/models/vendor.py +121 -70
- django_ledger/report/core.py +2 -14
- django_ledger/settings.py +57 -71
- django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
- django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +25 -0
- django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
- django_ledger/static/django_ledger/css/djl_styles.css +273 -0
- django_ledger/templates/django_ledger/bills/includes/card_bill.html +2 -2
- django_ledger/templates/django_ledger/components/menu.html +41 -26
- django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
- django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
- django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
- django_ledger/templates/django_ledger/customer/tags/customer_table.html +8 -6
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
- django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
- django_ledger/templates/django_ledger/entity/includes/card_entity.html +12 -6
- django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -1
- django_ledger/templates/django_ledger/financial_statements/cash_flow.html +4 -1
- django_ledger/templates/django_ledger/financial_statements/income_statement.html +4 -1
- django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +27 -3
- django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +16 -4
- django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +73 -18
- django_ledger/templates/django_ledger/includes/widget_ratios.html +18 -24
- django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +3 -3
- django_ledger/templates/django_ledger/layouts/base.html +7 -2
- django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
- django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
- django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
- django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
- django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +12 -7
- django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
- django_ledger/templatetags/django_ledger.py +338 -191
- django_ledger/tests/test_accounts.py +1 -2
- django_ledger/tests/test_io.py +17 -0
- django_ledger/tests/test_purchase_order.py +3 -3
- django_ledger/tests/test_transactions.py +1 -2
- django_ledger/urls/__init__.py +1 -4
- django_ledger/urls/customer.py +3 -0
- django_ledger/urls/data_import.py +3 -0
- django_ledger/urls/receipt.py +102 -0
- django_ledger/urls/vendor.py +1 -0
- django_ledger/views/__init__.py +1 -0
- django_ledger/views/bill.py +8 -11
- django_ledger/views/chart_of_accounts.py +6 -4
- django_ledger/views/closing_entry.py +11 -7
- django_ledger/views/customer.py +68 -30
- django_ledger/views/data_import.py +120 -66
- django_ledger/views/djl_api.py +3 -5
- django_ledger/views/entity.py +2 -4
- django_ledger/views/estimate.py +3 -7
- django_ledger/views/inventory.py +3 -5
- django_ledger/views/invoice.py +4 -6
- django_ledger/views/item.py +7 -11
- django_ledger/views/journal_entry.py +1 -2
- django_ledger/views/mixins.py +125 -93
- django_ledger/views/purchase_order.py +24 -35
- django_ledger/views/receipt.py +294 -0
- django_ledger/views/unit.py +1 -2
- django_ledger/views/vendor.py +54 -16
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/METADATA +43 -75
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/RECORD +104 -122
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +1 -1
- django_ledger-0.8.1.dist-info/top_level.txt +1 -0
- django_ledger/contrib/django_ledger_graphene/__init__.py +0 -0
- django_ledger/contrib/django_ledger_graphene/accounts/schema.py +0 -33
- django_ledger/contrib/django_ledger_graphene/api.py +0 -42
- django_ledger/contrib/django_ledger_graphene/apps.py +0 -6
- django_ledger/contrib/django_ledger_graphene/auth/mutations.py +0 -49
- django_ledger/contrib/django_ledger_graphene/auth/schema.py +0 -6
- django_ledger/contrib/django_ledger_graphene/bank_account/mutations.py +0 -61
- django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +0 -34
- django_ledger/contrib/django_ledger_graphene/bill/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/bill/schema.py +0 -34
- django_ledger/contrib/django_ledger_graphene/coa/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/coa/schema.py +0 -30
- django_ledger/contrib/django_ledger_graphene/customers/__init__.py +0 -0
- django_ledger/contrib/django_ledger_graphene/customers/mutations.py +0 -71
- django_ledger/contrib/django_ledger_graphene/customers/schema.py +0 -43
- django_ledger/contrib/django_ledger_graphene/data_import/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/data_import/schema.py +0 -0
- django_ledger/contrib/django_ledger_graphene/entity/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/entity/schema.py +0 -94
- django_ledger/contrib/django_ledger_graphene/item/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/item/schema.py +0 -31
- django_ledger/contrib/django_ledger_graphene/journal_entry/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +0 -35
- django_ledger/contrib/django_ledger_graphene/ledger/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/ledger/schema.py +0 -32
- django_ledger/contrib/django_ledger_graphene/purchase_order/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/purchase_order/schema.py +0 -31
- django_ledger/contrib/django_ledger_graphene/transaction/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/transaction/schema.py +0 -36
- django_ledger/contrib/django_ledger_graphene/unit/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/unit/schema.py +0 -27
- django_ledger/contrib/django_ledger_graphene/vendor/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/vendor/schema.py +0 -37
- django_ledger/contrib/django_ledger_graphene/views.py +0 -12
- django_ledger-0.7.11.dist-info/top_level.txt +0 -4
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/AUTHORS.md +0 -0
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/LICENSE +0 -0
|
@@ -39,28 +39,32 @@ roles to create comprehensive financial statements.
|
|
|
39
39
|
This structure ensures a clear and organized approach to financial management within Django Ledger, facilitating
|
|
40
40
|
accurate record-keeping and reporting.
|
|
41
41
|
"""
|
|
42
|
-
|
|
42
|
+
import warnings
|
|
43
43
|
from random import choices
|
|
44
44
|
from string import ascii_lowercase, digits
|
|
45
45
|
from typing import Optional, Union, Dict
|
|
46
|
-
from uuid import uuid4
|
|
46
|
+
from uuid import uuid4, UUID
|
|
47
47
|
|
|
48
48
|
from django.apps import apps
|
|
49
49
|
from django.contrib.auth import get_user_model
|
|
50
50
|
from django.core.exceptions import ValidationError
|
|
51
51
|
from django.db import models
|
|
52
|
-
from django.db.models import Q, F, Count, Manager, QuerySet
|
|
52
|
+
from django.db.models import Q, F, Count, Manager, QuerySet, BooleanField, Value
|
|
53
53
|
from django.db.models.signals import pre_save, post_save
|
|
54
54
|
from django.dispatch import receiver
|
|
55
55
|
from django.urls import reverse
|
|
56
56
|
from django.utils.translation import gettext_lazy as _
|
|
57
57
|
|
|
58
|
-
from django_ledger.io import (
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
from django_ledger.io import (
|
|
59
|
+
ROOT_COA, ROOT_GROUP_LEVEL_2, ROOT_GROUP_META, ROOT_ASSETS,
|
|
60
|
+
ROOT_LIABILITIES, ROOT_CAPITAL,
|
|
61
|
+
ROOT_INCOME, ROOT_COGS, ROOT_EXPENSES, ROOT_GROUP
|
|
62
|
+
)
|
|
61
63
|
from django_ledger.models import lazy_loader
|
|
62
64
|
from django_ledger.models.accounts import AccountModel, AccountModelQuerySet
|
|
65
|
+
from django_ledger.models.deprecations import deprecated_entity_slug_behavior
|
|
63
66
|
from django_ledger.models.mixins import CreateUpdateMixIn, SlugNameMixIn
|
|
67
|
+
from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
|
|
64
68
|
|
|
65
69
|
UserModel = get_user_model()
|
|
66
70
|
|
|
@@ -75,21 +79,55 @@ class ChartOfAccountsModelValidationError(ValidationError):
|
|
|
75
79
|
|
|
76
80
|
class ChartOfAccountModelQuerySet(QuerySet):
|
|
77
81
|
|
|
78
|
-
def active(self):
|
|
82
|
+
def active(self) -> 'ChartOfAccountModelQuerySet':
|
|
79
83
|
"""
|
|
80
84
|
QuerySet method to retrieve active items.
|
|
81
85
|
"""
|
|
82
86
|
return self.filter(active=True)
|
|
83
87
|
|
|
88
|
+
def not_active(self) -> 'ChartOfAccountModelQuerySet':
|
|
89
|
+
"""≤
|
|
90
|
+
QuerySet method to retrieve not active items.
|
|
91
|
+
"""
|
|
92
|
+
return self.filter(active=False)
|
|
93
|
+
|
|
94
|
+
def for_user(self, user_model) -> 'ChartOfAccountModelQuerySet':
|
|
95
|
+
"""
|
|
96
|
+
Fetches a QuerySet of ChartOfAccountModel that the UserModel as access to. May include ChartOfAccountModel from
|
|
97
|
+
multiple Entities. The user has access to bills if:
|
|
98
|
+
1. Is listed as Manager of Entity.
|
|
99
|
+
2. Is the Admin of the Entity.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
user_model
|
|
104
|
+
Logged in and authenticated django UserModel instance.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
ChartOfAccountQuerySet
|
|
109
|
+
Returns a ChartOfAccountQuerySet with applied filters.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
if user_model.is_superuser:
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
return self.filter(
|
|
116
|
+
(
|
|
117
|
+
Q(entity__admin=user_model) |
|
|
118
|
+
Q(entity__managers__in=[user_model])
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
84
122
|
|
|
85
123
|
class ChartOfAccountModelManager(Manager):
|
|
86
124
|
"""
|
|
87
|
-
A custom
|
|
125
|
+
A custom-defined ChartOfAccountModelManager that will act as an interface to handling the initial DB queries
|
|
88
126
|
to the ChartOfAccountModel.
|
|
89
127
|
"""
|
|
90
128
|
|
|
91
|
-
def get_queryset(self):
|
|
92
|
-
qs =
|
|
129
|
+
def get_queryset(self) -> ChartOfAccountModelQuerySet:
|
|
130
|
+
qs = ChartOfAccountModelQuerySet(self.model, using=self._db)
|
|
93
131
|
return qs.annotate(
|
|
94
132
|
_entity_slug=F('entity__slug'),
|
|
95
133
|
accountmodel_total__count=Count(
|
|
@@ -107,34 +145,32 @@ class ChartOfAccountModelManager(Manager):
|
|
|
107
145
|
# excludes coa root accounts...
|
|
108
146
|
filter=Q(accountmodel__depth__gt=2) & Q(accountmodel__active=True)
|
|
109
147
|
),
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
qs = self.get_queryset()
|
|
130
|
-
return qs.filter(
|
|
131
|
-
(
|
|
132
|
-
Q(entity__admin=user_model) |
|
|
133
|
-
Q(entity__managers__in=[user_model])
|
|
148
|
+
# Root-group presence and uniqueness checks:
|
|
149
|
+
accountmodel_rootgroup__count=Count(
|
|
150
|
+
'accountmodel',
|
|
151
|
+
filter=Q(accountmodel__role__in=ROOT_GROUP)
|
|
152
|
+
),
|
|
153
|
+
accountmodel_rootgroup_roles__distinct_count=Count(
|
|
154
|
+
'accountmodel__role',
|
|
155
|
+
filter=Q(accountmodel__role__in=ROOT_GROUP_META),
|
|
156
|
+
distinct=True
|
|
157
|
+
),
|
|
158
|
+
).annotate(
|
|
159
|
+
configured=models.Case(
|
|
160
|
+
models.When(
|
|
161
|
+
Q(accountmodel_rootgroup__count__gte=1) &
|
|
162
|
+
Q(accountmodel_rootgroup__count=F('accountmodel_rootgroup_roles__distinct_count')),
|
|
163
|
+
then=Value(True, output_field=BooleanField()),
|
|
164
|
+
),
|
|
165
|
+
default=Value(False, output_field=BooleanField()),
|
|
166
|
+
output_field=BooleanField()
|
|
134
167
|
)
|
|
135
|
-
)
|
|
168
|
+
).select_related('entity')
|
|
136
169
|
|
|
137
|
-
|
|
170
|
+
@deprecated_entity_slug_behavior
|
|
171
|
+
def for_entity(self,
|
|
172
|
+
entity_model: Union['EntityModel | str | UUID'] = None,
|
|
173
|
+
**kwargs) -> ChartOfAccountModelQuerySet:
|
|
138
174
|
"""
|
|
139
175
|
Fetches a QuerySet of ChartOfAccountsModel associated with a specific EntityModel & UserModel.
|
|
140
176
|
May pass an instance of EntityModel or a String representing the EntityModel slug.
|
|
@@ -145,18 +181,36 @@ class ChartOfAccountModelManager(Manager):
|
|
|
145
181
|
entity_slug: str or EntityModel
|
|
146
182
|
The entity slug or EntityModel used for filtering the QuerySet.
|
|
147
183
|
|
|
148
|
-
user_model
|
|
149
|
-
Logged in and authenticated django UserModel instance.
|
|
150
|
-
|
|
151
184
|
Returns
|
|
152
185
|
-------
|
|
153
186
|
ChartOfAccountQuerySet
|
|
154
187
|
Returns a ChartOfAccountQuerySet with applied filters.
|
|
155
188
|
"""
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
189
|
+
|
|
190
|
+
EntityModel = lazy_loader.get_entity_model()
|
|
191
|
+
|
|
192
|
+
qs = self.get_queryset()
|
|
193
|
+
if 'user_model' in kwargs:
|
|
194
|
+
warnings.warn(
|
|
195
|
+
'user_model parameter is deprecated and will be removed in a future release. '
|
|
196
|
+
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
197
|
+
DeprecationWarning,
|
|
198
|
+
stacklevel=2
|
|
199
|
+
)
|
|
200
|
+
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
201
|
+
qs = qs.for_user(kwargs['user_model'])
|
|
202
|
+
|
|
203
|
+
if isinstance(entity_model, EntityModel):
|
|
204
|
+
qs = qs.filter(entity=entity_model)
|
|
205
|
+
elif isinstance(entity_model, str):
|
|
206
|
+
qs = qs.filter(entity__slug=entity_model)
|
|
207
|
+
elif isinstance(entity_model, UUID):
|
|
208
|
+
qs = qs.filter(entity_id=entity_model)
|
|
209
|
+
else:
|
|
210
|
+
raise ChartOfAccountsModelValidationError(
|
|
211
|
+
message='Must pass an instance of EntityModel, String or UUID for entity_slug.'
|
|
212
|
+
)
|
|
213
|
+
return qs
|
|
160
214
|
|
|
161
215
|
|
|
162
216
|
class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
@@ -202,11 +256,80 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
202
256
|
@property
|
|
203
257
|
def entity_slug(self) -> str:
|
|
204
258
|
try:
|
|
205
|
-
# from QS annotation...
|
|
206
259
|
return getattr(self, '_entity_slug')
|
|
207
260
|
except AttributeError:
|
|
208
261
|
return self.entity.slug
|
|
209
262
|
|
|
263
|
+
def is_configured(self) -> bool:
|
|
264
|
+
try:
|
|
265
|
+
return getattr(self, 'configured')
|
|
266
|
+
except AttributeError:
|
|
267
|
+
pass
|
|
268
|
+
account_qs = self.accountmodel_set.filter(role__in=[ROOT_GROUP])
|
|
269
|
+
self.configured = len(account_qs) == len(ROOT_GROUP)
|
|
270
|
+
return self.configured
|
|
271
|
+
|
|
272
|
+
def configure(self, raise_exception: bool = True):
|
|
273
|
+
"""
|
|
274
|
+
A method that properly configures the ChartOfAccounts model and creates the appropriate hierarchy boilerplate
|
|
275
|
+
to support the insertion of new accounts into the chart of account model tree.
|
|
276
|
+
This method must be called every time the ChartOfAccounts model is created.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
raise_exception : bool, optional
|
|
281
|
+
Whether to raise an exception if root nodes already exist in the Chart of Accounts (default is True).
|
|
282
|
+
This indicates that the ChartOfAccountModel instance is already configured.
|
|
283
|
+
"""
|
|
284
|
+
self.generate_slug(commit=False)
|
|
285
|
+
|
|
286
|
+
if not self.is_configured():
|
|
287
|
+
root_accounts_qs = self.get_coa_root_accounts_qs()
|
|
288
|
+
existing_root_roles = list(set(acc.role for acc in root_accounts_qs))
|
|
289
|
+
|
|
290
|
+
if len(existing_root_roles) > 0:
|
|
291
|
+
if raise_exception:
|
|
292
|
+
raise ChartOfAccountsModelValidationError(message=f'Root Nodes already Exist in CoA {self.uuid}...')
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
if ROOT_COA not in existing_root_roles:
|
|
296
|
+
# add coa root...
|
|
297
|
+
role_meta = ROOT_GROUP_META[ROOT_COA]
|
|
298
|
+
account_pk = uuid4()
|
|
299
|
+
root_account = AccountModel(
|
|
300
|
+
uuid=account_pk,
|
|
301
|
+
code=role_meta['code'],
|
|
302
|
+
name=role_meta['title'],
|
|
303
|
+
coa_model=self,
|
|
304
|
+
role=ROOT_COA,
|
|
305
|
+
role_default=True,
|
|
306
|
+
active=False,
|
|
307
|
+
locked=True,
|
|
308
|
+
balance_type=role_meta['balance_type']
|
|
309
|
+
)
|
|
310
|
+
AccountModel.add_root(instance=root_account)
|
|
311
|
+
|
|
312
|
+
# must retrieve root model after added pero django-treebeard documentation...
|
|
313
|
+
coa_root_account_model = AccountModel.objects.get(uuid__exact=account_pk)
|
|
314
|
+
|
|
315
|
+
for root_role in ROOT_GROUP_LEVEL_2:
|
|
316
|
+
if root_role not in existing_root_roles:
|
|
317
|
+
account_pk = uuid4()
|
|
318
|
+
role_meta = ROOT_GROUP_META[root_role]
|
|
319
|
+
coa_root_account_model.add_child(
|
|
320
|
+
instance=AccountModel(
|
|
321
|
+
uuid=account_pk,
|
|
322
|
+
code=role_meta['code'],
|
|
323
|
+
name=role_meta['title'],
|
|
324
|
+
coa_model=self,
|
|
325
|
+
role=root_role,
|
|
326
|
+
role_default=True,
|
|
327
|
+
active=False,
|
|
328
|
+
locked=True,
|
|
329
|
+
balance_type=role_meta['balance_type']
|
|
330
|
+
))
|
|
331
|
+
self.configured = True
|
|
332
|
+
|
|
210
333
|
def get_coa_root_accounts_qs(self) -> AccountModelQuerySet:
|
|
211
334
|
"""
|
|
212
335
|
Retrieves the root accounts in the chart of accounts.
|
|
@@ -261,6 +384,10 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
261
384
|
message=_(f'The account model {account_model} is not part of the chart of accounts {self.name}.'),
|
|
262
385
|
)
|
|
263
386
|
|
|
387
|
+
# Chart of Accounts hasn't been configured...
|
|
388
|
+
if not self.is_configured():
|
|
389
|
+
self.configure(raise_exception=True)
|
|
390
|
+
|
|
264
391
|
if not account_model.is_root_account():
|
|
265
392
|
|
|
266
393
|
if not root_account_qs:
|
|
@@ -373,65 +500,6 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
373
500
|
]
|
|
374
501
|
)
|
|
375
502
|
|
|
376
|
-
def configure(self, raise_exception: bool = True):
|
|
377
|
-
"""
|
|
378
|
-
A method that properly configures the ChartOfAccounts model and creates the appropriate hierarchy boilerplate
|
|
379
|
-
to support the insertion of new accounts into the chart of account model tree.
|
|
380
|
-
This method must be called every time the ChartOfAccounts model is created.
|
|
381
|
-
|
|
382
|
-
Parameters
|
|
383
|
-
----------
|
|
384
|
-
raise_exception : bool, optional
|
|
385
|
-
Whether to raise an exception if root nodes already exist in the Chart of Accounts (default is True).
|
|
386
|
-
This indicates that the ChartOfAccountModel instance is already configured.
|
|
387
|
-
"""
|
|
388
|
-
self.generate_slug(commit=False)
|
|
389
|
-
|
|
390
|
-
root_accounts_qs = self.get_coa_root_accounts_qs()
|
|
391
|
-
existing_root_roles = list(set(acc.role for acc in root_accounts_qs))
|
|
392
|
-
|
|
393
|
-
if len(existing_root_roles) > 0:
|
|
394
|
-
if raise_exception:
|
|
395
|
-
raise ChartOfAccountsModelValidationError(message=f'Root Nodes already Exist in CoA {self.uuid}...')
|
|
396
|
-
return
|
|
397
|
-
|
|
398
|
-
if ROOT_COA not in existing_root_roles:
|
|
399
|
-
# add coa root...
|
|
400
|
-
role_meta = ROOT_GROUP_META[ROOT_COA]
|
|
401
|
-
account_pk = uuid4()
|
|
402
|
-
root_account = AccountModel(
|
|
403
|
-
uuid=account_pk,
|
|
404
|
-
code=role_meta['code'],
|
|
405
|
-
name=role_meta['title'],
|
|
406
|
-
coa_model=self,
|
|
407
|
-
role=ROOT_COA,
|
|
408
|
-
role_default=True,
|
|
409
|
-
active=False,
|
|
410
|
-
locked=True,
|
|
411
|
-
balance_type=role_meta['balance_type']
|
|
412
|
-
)
|
|
413
|
-
AccountModel.add_root(instance=root_account)
|
|
414
|
-
|
|
415
|
-
# must retrieve root model after added pero django-treebeard documentation...
|
|
416
|
-
coa_root_account_model = AccountModel.objects.get(uuid__exact=account_pk)
|
|
417
|
-
|
|
418
|
-
for root_role in ROOT_GROUP_LEVEL_2:
|
|
419
|
-
if root_role not in existing_root_roles:
|
|
420
|
-
account_pk = uuid4()
|
|
421
|
-
role_meta = ROOT_GROUP_META[root_role]
|
|
422
|
-
coa_root_account_model.add_child(
|
|
423
|
-
instance=AccountModel(
|
|
424
|
-
uuid=account_pk,
|
|
425
|
-
code=role_meta['code'],
|
|
426
|
-
name=role_meta['title'],
|
|
427
|
-
coa_model=self,
|
|
428
|
-
role=root_role,
|
|
429
|
-
role_default=True,
|
|
430
|
-
active=False,
|
|
431
|
-
locked=True,
|
|
432
|
-
balance_type=role_meta['balance_type']
|
|
433
|
-
))
|
|
434
|
-
|
|
435
503
|
def is_default(self) -> bool:
|
|
436
504
|
"""
|
|
437
505
|
Check if the ChartOfAccountModel instance is set as the default for the EntityModel.
|
|
@@ -598,7 +666,6 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
598
666
|
account_qs.update(locked=False)
|
|
599
667
|
return account_qs
|
|
600
668
|
|
|
601
|
-
|
|
602
669
|
def mark_as_default(self, commit: bool = False, raise_exception: bool = False, **kwargs):
|
|
603
670
|
"""
|
|
604
671
|
Marks the current Chart of Accounts instances as default for the EntityModel.
|
|
@@ -839,6 +906,7 @@ class ChartOfAccountModel(ChartOfAccountModelAbstract):
|
|
|
839
906
|
"""
|
|
840
907
|
Base ChartOfAccounts Model
|
|
841
908
|
"""
|
|
909
|
+
|
|
842
910
|
class Meta(ChartOfAccountModelAbstract.Meta):
|
|
843
911
|
abstract = False
|
|
844
912
|
|
|
@@ -854,5 +922,5 @@ def chartofaccountsmodel_presave(instance: ChartOfAccountModelAbstract, **kwargs
|
|
|
854
922
|
|
|
855
923
|
@receiver(post_save, sender=ChartOfAccountModel)
|
|
856
924
|
def chartofaccountsmodel_postsave(instance: ChartOfAccountModelAbstract, **kwargs):
|
|
857
|
-
if instance.
|
|
925
|
+
if not instance.is_configured():
|
|
858
926
|
instance.configure()
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
|
|
3
3
|
Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
|
|
4
4
|
"""
|
|
5
|
-
|
|
6
|
-
from datetime import datetime, time
|
|
5
|
+
import warnings
|
|
6
|
+
from datetime import datetime, time, date
|
|
7
7
|
from decimal import Decimal
|
|
8
8
|
from itertools import groupby, chain
|
|
9
9
|
from typing import Optional
|
|
@@ -12,58 +12,75 @@ from uuid import uuid4, UUID
|
|
|
12
12
|
from django.core.exceptions import ValidationError
|
|
13
13
|
from django.core.validators import MinValueValidator
|
|
14
14
|
from django.db import models
|
|
15
|
-
from django.db.models import Q, Count
|
|
15
|
+
from django.db.models import Q, Manager, QuerySet, Count
|
|
16
16
|
from django.db.models.signals import pre_save
|
|
17
17
|
from django.urls import reverse
|
|
18
18
|
from django.utils.timezone import make_aware
|
|
19
19
|
from django.utils.translation import gettext_lazy as _
|
|
20
20
|
|
|
21
|
+
from django_ledger.models import EntityModel, deprecated_entity_slug_behavior, lazy_loader
|
|
21
22
|
from django_ledger.models.journal_entry import JournalEntryModel
|
|
22
23
|
from django_ledger.models.ledger import LedgerModel
|
|
23
24
|
from django_ledger.models.mixins import CreateUpdateMixIn, MarkdownNotesMixIn
|
|
24
25
|
from django_ledger.models.transactions import TransactionModel
|
|
25
|
-
from django_ledger.
|
|
26
|
+
from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class ClosingEntryValidationError(ValidationError):
|
|
29
30
|
pass
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
class ClosingEntryModelQuerySet(
|
|
33
|
+
class ClosingEntryModelQuerySet(QuerySet):
|
|
33
34
|
|
|
34
|
-
def
|
|
35
|
+
def for_user(self, user_model):
|
|
36
|
+
if user_model.is_superuser:
|
|
37
|
+
return self
|
|
38
|
+
return self.filter(
|
|
39
|
+
Q(entity_model__admin=user_model) |
|
|
40
|
+
Q(entity_model__managers__in=[user_model])
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def posted(self) -> 'ClosingEntryModelQuerySet':
|
|
35
44
|
return self.filter(posted=True)
|
|
36
45
|
|
|
37
|
-
def not_posted(self):
|
|
46
|
+
def not_posted(self) -> 'ClosingEntryModelQuerySet':
|
|
38
47
|
return self.filter(posted=False)
|
|
39
48
|
|
|
40
49
|
|
|
41
|
-
class ClosingEntryModelManager(
|
|
50
|
+
class ClosingEntryModelManager(Manager):
|
|
42
51
|
|
|
43
|
-
def get_queryset(self):
|
|
52
|
+
def get_queryset(self) -> ClosingEntryModelQuerySet:
|
|
44
53
|
qs = ClosingEntryModelQuerySet(self.model, using=self._db)
|
|
45
54
|
return qs.annotate(
|
|
46
55
|
ce_txs_count=Count('closingentrytransactionmodel')
|
|
47
56
|
)
|
|
48
57
|
|
|
49
|
-
|
|
58
|
+
@deprecated_entity_slug_behavior
|
|
59
|
+
def for_entity(self, entity_model: EntityModel | str | UUID = None, **kwargs) -> ClosingEntryModelQuerySet:
|
|
60
|
+
|
|
50
61
|
qs = self.get_queryset()
|
|
51
|
-
if user_model.is_superuser:
|
|
52
|
-
return qs
|
|
53
|
-
return qs.filter(
|
|
54
|
-
Q(entity_model__admin=user_model) |
|
|
55
|
-
Q(entity_model__managers__in=[user_model])
|
|
56
|
-
)
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
if 'user_model' in kwargs:
|
|
64
|
+
warnings.warn(
|
|
65
|
+
'user_model parameter is deprecated and will be removed in a future release. '
|
|
66
|
+
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
67
|
+
DeprecationWarning,
|
|
68
|
+
stacklevel=2
|
|
63
69
|
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
71
|
+
qs = qs.for_user(kwargs['user_model'])
|
|
72
|
+
|
|
73
|
+
if isinstance(entity_model, EntityModel):
|
|
74
|
+
qs = qs.filter(entity_model=entity_model)
|
|
75
|
+
elif isinstance(entity_model, str):
|
|
76
|
+
qs = qs.filter(entity_model__slug__exact=entity_model)
|
|
77
|
+
elif isinstance(entity_model, UUID):
|
|
78
|
+
qs = qs.filter(entity_model_id=entity_model)
|
|
79
|
+
else:
|
|
80
|
+
raise ClosingEntryValidationError(
|
|
81
|
+
message='Must pass EntityModel, slug or UUID'
|
|
82
|
+
)
|
|
83
|
+
return qs
|
|
67
84
|
|
|
68
85
|
|
|
69
86
|
class ClosingEntryModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn):
|
|
@@ -247,7 +264,7 @@ class ClosingEntryModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn):
|
|
|
247
264
|
self.posted = False
|
|
248
265
|
|
|
249
266
|
TransactionModel.objects.for_entity(
|
|
250
|
-
|
|
267
|
+
entity_model=self.entity_model_id
|
|
251
268
|
).for_ledger(ledger_model=self.ledger_model).delete()
|
|
252
269
|
|
|
253
270
|
self.ledger_model.journal_entries.all().delete()
|
|
@@ -321,7 +338,7 @@ class ClosingEntryModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn):
|
|
|
321
338
|
self.ledger_model.unpost(commit=True, raise_exception=True)
|
|
322
339
|
|
|
323
340
|
TransactionModel.objects.for_entity(
|
|
324
|
-
|
|
341
|
+
entity_model=self.entity_model_id
|
|
325
342
|
).for_ledger(ledger_model=self.ledger_model).delete()
|
|
326
343
|
|
|
327
344
|
return self.ledger_model.delete()
|
|
@@ -361,19 +378,62 @@ class ClosingEntryModel(ClosingEntryModelAbstract):
|
|
|
361
378
|
abstract = False
|
|
362
379
|
|
|
363
380
|
|
|
364
|
-
|
|
365
|
-
|
|
381
|
+
def closingentrymodel_presave(instance: ClosingEntryModel, **kwargs):
|
|
382
|
+
instance.create_entry_ledger(commit=False)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
pre_save.connect(closingentrymodel_presave, sender=ClosingEntryModel)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class ClosingEntryTransactionModelQuerySet(QuerySet):
|
|
366
389
|
|
|
390
|
+
def for_user(self, user_model) -> 'ClosingEntryTransactionModelQuerySet':
|
|
391
|
+
if user_model.is_superuser:
|
|
392
|
+
return self
|
|
393
|
+
return self.filter(
|
|
394
|
+
Q(closing_entry_model__entity_model__admin=user_model) |
|
|
395
|
+
Q(closing_entry_model__entity_model__managers__in=[user_model])
|
|
396
|
+
)
|
|
367
397
|
|
|
368
|
-
|
|
398
|
+
def for_closing_date(self, closing_date: date) -> 'ClosingEntryTransactionModelQuerySet':
|
|
399
|
+
return self.filter(
|
|
400
|
+
closing_entry_model__closing_date__exact=closing_date
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class ClosingEntryTransactionModelManager(Manager):
|
|
405
|
+
|
|
406
|
+
def get_queryset(self) -> ClosingEntryTransactionModelQuerySet:
|
|
407
|
+
return ClosingEntryTransactionModelQuerySet(self.model, using=self._db)
|
|
408
|
+
|
|
409
|
+
@deprecated_entity_slug_behavior
|
|
410
|
+
def for_entity(self, entity_model: EntityModel | str | UUID = None,
|
|
411
|
+
**kwargs) -> ClosingEntryTransactionModelQuerySet:
|
|
412
|
+
EntityModel = lazy_loader.get_entity(entity_model)
|
|
369
413
|
|
|
370
|
-
def for_entity(self, entity_slug):
|
|
371
414
|
qs = self.get_queryset()
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
415
|
+
|
|
416
|
+
if 'user_model' in kwargs:
|
|
417
|
+
warnings.warn(
|
|
418
|
+
'user_model parameter is deprecated and will be removed in a future release. '
|
|
419
|
+
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
420
|
+
DeprecationWarning,
|
|
421
|
+
stacklevel=2
|
|
422
|
+
)
|
|
423
|
+
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
424
|
+
qs = qs.for_user(kwargs['user_model'])
|
|
425
|
+
|
|
426
|
+
if isinstance(entity_model, EntityModel):
|
|
427
|
+
qs = qs.filter(closing_entry_model__entity_model=entity_model)
|
|
428
|
+
elif isinstance(entity_model, UUID):
|
|
429
|
+
qs = qs.filter(closing_entry_model__entity_model_id=entity_model)
|
|
430
|
+
elif isinstance(entity_model, str):
|
|
431
|
+
qs = qs.filter(closing_entry_model__slug__exact=entity_model)
|
|
432
|
+
else:
|
|
433
|
+
raise ClosingEntryValidationError(
|
|
434
|
+
message=_('Must pass EntityModel or UUID or str')
|
|
435
|
+
)
|
|
436
|
+
return qs
|
|
377
437
|
|
|
378
438
|
|
|
379
439
|
class ClosingEntryTransactionModelAbstract(CreateUpdateMixIn):
|
|
@@ -453,13 +513,11 @@ class ClosingEntryTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
453
513
|
def __str__(self):
|
|
454
514
|
return f'{self.__class__.__name__}: {self.closing_entry_model.closing_date.strftime("%D")} | {self.balance}'
|
|
455
515
|
|
|
456
|
-
def is_debit(self) ->
|
|
457
|
-
|
|
458
|
-
return self.tx_type == TransactionModel.DEBIT
|
|
516
|
+
def is_debit(self) -> bool:
|
|
517
|
+
return self.tx_type == TransactionModel.DEBIT
|
|
459
518
|
|
|
460
|
-
def is_credit(self) ->
|
|
461
|
-
|
|
462
|
-
return self.tx_type == TransactionModel.CREDIT
|
|
519
|
+
def is_credit(self) -> bool:
|
|
520
|
+
return self.tx_type == TransactionModel.CREDIT
|
|
463
521
|
|
|
464
522
|
def adjust_tx_type_for_negative_balance(self):
|
|
465
523
|
if self.balance < Decimal('0.00'):
|
|
@@ -485,13 +543,6 @@ class ClosingEntryTransactionModel(ClosingEntryTransactionModelAbstract):
|
|
|
485
543
|
abstract = False
|
|
486
544
|
|
|
487
545
|
|
|
488
|
-
def closingentrymodel_presave(instance: ClosingEntryModel, **kwargs):
|
|
489
|
-
instance.create_entry_ledger(commit=False)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
pre_save.connect(closingentrymodel_presave, sender=ClosingEntryModel)
|
|
493
|
-
|
|
494
|
-
|
|
495
546
|
def closingentrytransactionmodel_presave(instance: ClosingEntryTransactionModel, **kwargs):
|
|
496
547
|
instance.adjust_tx_type_for_negative_balance()
|
|
497
548
|
|