django-ledger 0.7.10__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-ledger might be problematic. Click here for more details.
- django_ledger/__init__.py +1 -1
- django_ledger/context.py +12 -0
- django_ledger/forms/bill.py +0 -4
- django_ledger/forms/closing_entry.py +13 -1
- django_ledger/forms/data_import.py +1 -1
- 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 +25 -32
- django_ledger/io/io_generator.py +7 -6
- django_ledger/io/io_library.py +1 -2
- django_ledger/migrations/0024_billmodel_entity_model_invoicemodel_entity_model.py +24 -0
- django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
- django_ledger/models/accounts.py +109 -69
- django_ledger/models/bank_account.py +40 -23
- django_ledger/models/bill.py +89 -63
- django_ledger/models/chart_of_accounts.py +173 -105
- django_ledger/models/closing_entry.py +99 -48
- django_ledger/models/customer.py +60 -39
- django_ledger/models/data_import.py +55 -41
- django_ledger/models/deprecations.py +61 -0
- django_ledger/models/entity.py +18 -16
- django_ledger/models/estimate.py +57 -28
- django_ledger/models/invoice.py +58 -28
- 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 +16 -10
- django_ledger/models/purchase_order.py +39 -17
- django_ledger/models/transactions.py +152 -113
- django_ledger/models/unit.py +57 -30
- django_ledger/models/vendor.py +75 -43
- django_ledger/report/core.py +2 -14
- django_ledger/settings.py +56 -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/customer/tags/customer_table.html +5 -5
- 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 +6 -1
- django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +9 -5
- 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 +0 -4
- django_ledger/views/bill.py +8 -13
- django_ledger/views/chart_of_accounts.py +6 -4
- django_ledger/views/closing_entry.py +11 -7
- django_ledger/views/customer.py +13 -17
- django_ledger/views/data_import.py +7 -6
- 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 +25 -19
- django_ledger/views/purchase_order.py +24 -35
- django_ledger/views/unit.py +1 -2
- django_ledger/views/vendor.py +1 -2
- {django_ledger-0.7.10.dist-info → django_ledger-0.8.0.dist-info}/METADATA +43 -75
- {django_ledger-0.7.10.dist-info → django_ledger-0.8.0.dist-info}/RECORD +80 -108
- {django_ledger-0.7.10.dist-info → django_ledger-0.8.0.dist-info}/WHEEL +1 -1
- django_ledger-0.8.0.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.10.dist-info/top_level.txt +0 -4
- {django_ledger-0.7.10.dist-info → django_ledger-0.8.0.dist-info/licenses}/AUTHORS.md +0 -0
- {django_ledger-0.7.10.dist-info → django_ledger-0.8.0.dist-info/licenses}/LICENSE +0 -0
django_ledger/models/bill.py
CHANGED
|
@@ -6,19 +6,12 @@ This module implements the BillModel, which represents an Invoice received from
|
|
|
6
6
|
the Vendor states the amount owed by the recipient for the purposes of supplying goods and/or services.
|
|
7
7
|
In addition to tracking the bill amount, it tracks the paid and due amount.
|
|
8
8
|
|
|
9
|
-
Examples
|
|
10
|
-
________
|
|
11
|
-
>>> user_model = request.user # django UserModel
|
|
12
|
-
>>> entity_slug = kwargs['entity_slug'] # may come from view kwargs
|
|
13
|
-
>>> bill_model = BillModel()
|
|
14
|
-
>>> ledger_model, bill_model = bill_model.configure(entity_slug=entity_slug, user_model=user_model)
|
|
15
|
-
>>> bill_model.save()
|
|
16
9
|
"""
|
|
17
|
-
|
|
10
|
+
import warnings
|
|
18
11
|
from datetime import date, datetime
|
|
19
12
|
from decimal import Decimal
|
|
20
13
|
from typing import Union, Optional, Tuple, Dict, List
|
|
21
|
-
from uuid import uuid4
|
|
14
|
+
from uuid import uuid4, UUID
|
|
22
15
|
|
|
23
16
|
from django.contrib.auth import get_user_model
|
|
24
17
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
|
@@ -31,6 +24,7 @@ from django.utils.translation import gettext_lazy as _
|
|
|
31
24
|
|
|
32
25
|
from django_ledger.io import ASSET_CA_CASH, ASSET_CA_PREPAID, LIABILITY_CL_ACC_PAYABLE
|
|
33
26
|
from django_ledger.io.io_core import get_localtime, get_localdate
|
|
27
|
+
from django_ledger.models.deprecations import deprecated_entity_slug_behavior
|
|
34
28
|
from django_ledger.models.entity import EntityModel
|
|
35
29
|
from django_ledger.models.items import ItemTransactionModelQuerySet, ItemTransactionModel, ItemModel, ItemModelQuerySet
|
|
36
30
|
from django_ledger.models.mixins import (
|
|
@@ -49,7 +43,8 @@ from django_ledger.models.signals import (
|
|
|
49
43
|
bill_status_void,
|
|
50
44
|
)
|
|
51
45
|
from django_ledger.models.utils import lazy_loader
|
|
52
|
-
from django_ledger.settings import (DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_BILL_NUMBER_PREFIX
|
|
46
|
+
from django_ledger.settings import (DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_BILL_NUMBER_PREFIX,
|
|
47
|
+
DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR)
|
|
53
48
|
|
|
54
49
|
UserModel = get_user_model()
|
|
55
50
|
|
|
@@ -66,6 +61,32 @@ class BillModelQuerySet(QuerySet):
|
|
|
66
61
|
building customized reports.
|
|
67
62
|
"""
|
|
68
63
|
|
|
64
|
+
def for_user(self, user_model) -> 'BillModelQuerySet':
|
|
65
|
+
"""
|
|
66
|
+
Fetches a QuerySet of BillModels that the UserModel as access to.
|
|
67
|
+
May include BillModels from multiple Entities.
|
|
68
|
+
|
|
69
|
+
The user has access to bills if:
|
|
70
|
+
1. Is listed as Manager of Entity.
|
|
71
|
+
2. Is the Admin of the Entity.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
__________
|
|
75
|
+
user_model
|
|
76
|
+
Logged in and authenticated django UserModel instance.
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
_______
|
|
80
|
+
BillModelQuerySet
|
|
81
|
+
Returns a BillModelQuerySet with applied filters.
|
|
82
|
+
"""
|
|
83
|
+
if user_model.is_superuser:
|
|
84
|
+
return self
|
|
85
|
+
return self.filter(
|
|
86
|
+
Q(ledger__entity__admin=user_model) |
|
|
87
|
+
Q(ledger__entity__managers__in=[user_model])
|
|
88
|
+
)
|
|
89
|
+
|
|
69
90
|
def draft(self):
|
|
70
91
|
"""
|
|
71
92
|
Default status of any bill that is created.
|
|
@@ -162,7 +183,7 @@ class BillModelQuerySet(QuerySet):
|
|
|
162
183
|
"""
|
|
163
184
|
return self.filter(date_due__lt=get_localdate())
|
|
164
185
|
|
|
165
|
-
def unpaid(self):
|
|
186
|
+
def unpaid(self) -> 'BillModelQuerySet':
|
|
166
187
|
"""
|
|
167
188
|
Unpaid bills are those that are approved but have not received 100% of the amount due.
|
|
168
189
|
Equivalent to approved().
|
|
@@ -177,82 +198,56 @@ class BillModelQuerySet(QuerySet):
|
|
|
177
198
|
|
|
178
199
|
class BillModelManager(Manager):
|
|
179
200
|
"""
|
|
180
|
-
A custom
|
|
201
|
+
A custom-defined BillModelManager that will act as an interface to handling the initial DB queries
|
|
181
202
|
to the BillModel. The default "get_queryset" has been overridden to refer the custom defined
|
|
182
203
|
"BillModelQuerySet".
|
|
183
204
|
"""
|
|
184
205
|
|
|
185
|
-
def get_queryset(self):
|
|
186
|
-
qs =
|
|
206
|
+
def get_queryset(self) -> BillModelQuerySet:
|
|
207
|
+
qs = BillModelQuerySet(self.model, using=self._db)
|
|
187
208
|
return qs.select_related(
|
|
188
209
|
'ledger',
|
|
189
210
|
'ledger__entity'
|
|
190
211
|
)
|
|
191
212
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
Fetches a QuerySet of BillModels that the UserModel as access to.
|
|
195
|
-
May include BillModels from multiple Entities.
|
|
196
|
-
|
|
197
|
-
The user has access to bills if:
|
|
198
|
-
1. Is listed as Manager of Entity.
|
|
199
|
-
2. Is the Admin of the Entity.
|
|
200
|
-
|
|
201
|
-
Parameters
|
|
202
|
-
__________
|
|
203
|
-
user_model
|
|
204
|
-
Logged in and authenticated django UserModel instance.
|
|
205
|
-
|
|
206
|
-
Examples
|
|
207
|
-
________
|
|
208
|
-
>>> request_user = request.user
|
|
209
|
-
>>> bill_model_qs = BillModel.objects.for_user(user_model=request_user)
|
|
210
|
-
|
|
211
|
-
Returns
|
|
212
|
-
_______
|
|
213
|
-
BillModelQuerySet
|
|
214
|
-
Returns a BillModelQuerySet with applied filters.
|
|
215
|
-
"""
|
|
216
|
-
qs = self.get_queryset()
|
|
217
|
-
if user_model.is_superuser:
|
|
218
|
-
return qs
|
|
219
|
-
return qs.filter(
|
|
220
|
-
Q(ledger__entity__admin=user_model) |
|
|
221
|
-
Q(ledger__entity__managers__in=[user_model])
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
def for_entity(self, entity_slug, user_model) -> BillModelQuerySet:
|
|
213
|
+
@deprecated_entity_slug_behavior
|
|
214
|
+
def for_entity(self, entity_model: EntityModel | str | UUID = None, **kwargs) -> BillModelQuerySet:
|
|
225
215
|
"""
|
|
226
216
|
Fetches a QuerySet of BillModels associated with a specific EntityModel & UserModel.
|
|
227
217
|
May pass an instance of EntityModel or a String representing the EntityModel slug.
|
|
228
218
|
|
|
229
219
|
Parameters
|
|
230
220
|
__________
|
|
231
|
-
|
|
221
|
+
entity_model: str or EntityModel
|
|
232
222
|
The entity slug or EntityModel used for filtering the QuerySet.
|
|
233
|
-
user_model
|
|
234
|
-
Logged in and authenticated django UserModel instance.
|
|
235
|
-
|
|
236
|
-
Examples
|
|
237
|
-
________
|
|
238
|
-
>>> request_user = request.user
|
|
239
|
-
>>> slug = kwargs['entity_slug'] # may come from request kwargs
|
|
240
|
-
>>> bill_model_qs = BillModel.objects.for_entity(user_model=request_user, entity_slug=slug)
|
|
241
223
|
|
|
242
224
|
Returns
|
|
243
225
|
_______
|
|
244
226
|
BillModelQuerySet
|
|
245
227
|
Returns a BillModelQuerySet with applied filters.
|
|
246
228
|
"""
|
|
247
|
-
qs = self.
|
|
248
|
-
if
|
|
249
|
-
|
|
250
|
-
|
|
229
|
+
qs = self.get_queryset()
|
|
230
|
+
if 'user_model' in kwargs:
|
|
231
|
+
warnings.warn(
|
|
232
|
+
'user_model parameter is deprecated and will be removed in a future release. '
|
|
233
|
+
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
234
|
+
DeprecationWarning,
|
|
235
|
+
stacklevel=2
|
|
251
236
|
)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
237
|
+
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
238
|
+
qs = qs.for_user(kwargs['user_model'])
|
|
239
|
+
|
|
240
|
+
if isinstance(entity_model, EntityModel):
|
|
241
|
+
qs = qs.filter(ledger__entity=entity_model)
|
|
242
|
+
elif isinstance(entity_model, str):
|
|
243
|
+
qs = qs.filter(ledger__entity__slug__exact=entity_model)
|
|
244
|
+
elif isinstance(entity_model, UUID):
|
|
245
|
+
qs = qs.filter(ledger__entity_id=entity_model)
|
|
246
|
+
else:
|
|
247
|
+
raise BillModelValidationError(
|
|
248
|
+
'Must pass EntityModel, slug or UUID'
|
|
255
249
|
)
|
|
250
|
+
return qs
|
|
256
251
|
|
|
257
252
|
|
|
258
253
|
class BillModelAbstract(
|
|
@@ -353,6 +348,13 @@ class BillModelAbstract(
|
|
|
353
348
|
|
|
354
349
|
# todo: implement Void Bill (& Invoice)....
|
|
355
350
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
351
|
+
entity_model = models.ForeignKey(
|
|
352
|
+
'django_ledger.EntityModel',
|
|
353
|
+
on_delete=models.CASCADE,
|
|
354
|
+
null=True,
|
|
355
|
+
blank=True,
|
|
356
|
+
editable=False
|
|
357
|
+
)
|
|
356
358
|
bill_number = models.SlugField(max_length=20, verbose_name=_('Bill Number'), editable=False)
|
|
357
359
|
bill_status = models.CharField(max_length=10,
|
|
358
360
|
choices=BILL_STATUS,
|
|
@@ -362,6 +364,26 @@ class BillModelAbstract(
|
|
|
362
364
|
vendor = models.ForeignKey('django_ledger.VendorModel',
|
|
363
365
|
on_delete=models.CASCADE,
|
|
364
366
|
verbose_name=_('Vendor'))
|
|
367
|
+
|
|
368
|
+
cash_account = models.ForeignKey('django_ledger.AccountModel',
|
|
369
|
+
on_delete=models.RESTRICT,
|
|
370
|
+
null=True,
|
|
371
|
+
blank=True,
|
|
372
|
+
verbose_name=_('Cash Account'),
|
|
373
|
+
related_name=f'{REL_NAME_PREFIX}_cash_account')
|
|
374
|
+
prepaid_account = models.ForeignKey('django_ledger.AccountModel',
|
|
375
|
+
on_delete=models.RESTRICT,
|
|
376
|
+
null=True,
|
|
377
|
+
blank=True,
|
|
378
|
+
verbose_name=_('Prepaid Account'),
|
|
379
|
+
related_name=f'{REL_NAME_PREFIX}_prepaid_account')
|
|
380
|
+
unearned_account = models.ForeignKey('django_ledger.AccountModel',
|
|
381
|
+
on_delete=models.RESTRICT,
|
|
382
|
+
null=True,
|
|
383
|
+
blank=True,
|
|
384
|
+
verbose_name=_('Unearned Account'),
|
|
385
|
+
related_name=f'{REL_NAME_PREFIX}_unearned_account')
|
|
386
|
+
|
|
365
387
|
additional_info = models.JSONField(blank=True,
|
|
366
388
|
null=True,
|
|
367
389
|
default=dict,
|
|
@@ -1173,6 +1195,7 @@ class BillModelAbstract(
|
|
|
1173
1195
|
|
|
1174
1196
|
if not itemtxs_qs.count():
|
|
1175
1197
|
raise BillModelValidationError(message=f'Cannot review a {self.__class__.__name__} without items...')
|
|
1198
|
+
|
|
1176
1199
|
if not self.amount_due:
|
|
1177
1200
|
raise BillModelValidationError(
|
|
1178
1201
|
f'Bill {self.bill_number} cannot be marked as in review. Amount due must be greater than 0.'
|
|
@@ -1927,5 +1950,8 @@ def billmodel_presave(instance: BillModel, **kwargs):
|
|
|
1927
1950
|
if instance.can_generate_bill_number():
|
|
1928
1951
|
instance.generate_bill_number(commit=False)
|
|
1929
1952
|
|
|
1953
|
+
if not instance.entity_model_id:
|
|
1954
|
+
instance.entity_model = instance.ledger.entity
|
|
1955
|
+
|
|
1930
1956
|
|
|
1931
1957
|
pre_save.connect(receiver=billmodel_presave, sender=BillModel)
|
|
@@ -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()
|