django-ledger 0.8.2__py3-none-any.whl → 0.8.2.2__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/forms/data_import.py +63 -110
- django_ledger/io/roles.py +238 -303
- django_ledger/migrations/0027_alter_accountmodel_role_alter_receiptmodel_amount_and_more.py +159 -0
- django_ledger/models/chart_of_accounts.py +113 -180
- django_ledger/models/coa_default.py +1 -0
- django_ledger/models/customer.py +17 -26
- django_ledger/models/data_import.py +457 -153
- django_ledger/models/receipt.py +90 -121
- django_ledger/templates/django_ledger/data_import/data_import_job_txs.html +6 -0
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +66 -36
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +10 -10
- django_ledger/urls/data_import.py +3 -0
- django_ledger/views/data_import.py +37 -21
- {django_ledger-0.8.2.dist-info → django_ledger-0.8.2.2.dist-info}/METADATA +1 -1
- {django_ledger-0.8.2.dist-info → django_ledger-0.8.2.2.dist-info}/RECORD +20 -19
- {django_ledger-0.8.2.dist-info → django_ledger-0.8.2.2.dist-info}/WHEEL +0 -0
- {django_ledger-0.8.2.dist-info → django_ledger-0.8.2.2.dist-info}/licenses/AUTHORS.md +0 -0
- {django_ledger-0.8.2.dist-info → django_ledger-0.8.2.2.dist-info}/licenses/LICENSE +0 -0
- {django_ledger-0.8.2.dist-info → django_ledger-0.8.2.2.dist-info}/top_level.txt +0 -0
|
@@ -39,13 +39,13 @@ 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
43
|
import warnings
|
|
43
44
|
from random import choices
|
|
44
45
|
from string import ascii_lowercase, digits
|
|
45
46
|
from typing import Optional, Union, Dict
|
|
46
47
|
from uuid import uuid4, UUID
|
|
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
|
|
@@ -56,9 +56,16 @@ from django.urls import reverse
|
|
|
56
56
|
from django.utils.translation import gettext_lazy as _
|
|
57
57
|
|
|
58
58
|
from django_ledger.io import (
|
|
59
|
-
ROOT_COA,
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
ROOT_COA,
|
|
60
|
+
ROOT_GROUP_LEVEL_2,
|
|
61
|
+
ROOT_GROUP_META,
|
|
62
|
+
ROOT_ASSETS,
|
|
63
|
+
ROOT_LIABILITIES,
|
|
64
|
+
ROOT_CAPITAL,
|
|
65
|
+
ROOT_INCOME,
|
|
66
|
+
ROOT_COGS,
|
|
67
|
+
ROOT_EXPENSES,
|
|
68
|
+
ROOT_GROUP,
|
|
62
69
|
)
|
|
63
70
|
from django_ledger.models import lazy_loader
|
|
64
71
|
from django_ledger.models.accounts import AccountModel, AccountModelQuerySet
|
|
@@ -70,15 +77,12 @@ UserModel = get_user_model()
|
|
|
70
77
|
|
|
71
78
|
SLUG_SUFFIX = ascii_lowercase + digits
|
|
72
79
|
|
|
73
|
-
app_config = apps.get_app_config('django_ledger')
|
|
74
|
-
|
|
75
80
|
|
|
76
81
|
class ChartOfAccountsModelValidationError(ValidationError):
|
|
77
82
|
pass
|
|
78
83
|
|
|
79
84
|
|
|
80
85
|
class ChartOfAccountModelQuerySet(QuerySet):
|
|
81
|
-
|
|
82
86
|
def active(self) -> 'ChartOfAccountModelQuerySet':
|
|
83
87
|
"""
|
|
84
88
|
QuerySet method to retrieve active items.
|
|
@@ -112,12 +116,7 @@ class ChartOfAccountModelQuerySet(QuerySet):
|
|
|
112
116
|
if user_model.is_superuser:
|
|
113
117
|
return self
|
|
114
118
|
|
|
115
|
-
return self.filter(
|
|
116
|
-
(
|
|
117
|
-
Q(entity__admin=user_model) |
|
|
118
|
-
Q(entity__managers__in=[user_model])
|
|
119
|
-
)
|
|
120
|
-
)
|
|
119
|
+
return self.filter((Q(entity__admin=user_model) | Q(entity__managers__in=[user_model])))
|
|
121
120
|
|
|
122
121
|
|
|
123
122
|
class ChartOfAccountModelManager(Manager):
|
|
@@ -128,49 +127,52 @@ class ChartOfAccountModelManager(Manager):
|
|
|
128
127
|
|
|
129
128
|
def get_queryset(self) -> ChartOfAccountModelQuerySet:
|
|
130
129
|
qs = ChartOfAccountModelQuerySet(self.model, using=self._db)
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
'accountmodel',
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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()),
|
|
130
|
+
return (
|
|
131
|
+
qs.annotate(
|
|
132
|
+
_entity_slug=F('entity__slug'),
|
|
133
|
+
accountmodel_total__count=Count(
|
|
134
|
+
'accountmodel',
|
|
135
|
+
# excludes coa root accounts...
|
|
136
|
+
filter=Q(accountmodel__depth__gt=2),
|
|
137
|
+
),
|
|
138
|
+
accountmodel_locked__count=Count(
|
|
139
|
+
'accountmodel',
|
|
140
|
+
# excludes coa root accounts...
|
|
141
|
+
filter=Q(accountmodel__depth__gt=2) & Q(accountmodel__locked=True),
|
|
142
|
+
),
|
|
143
|
+
accountmodel_active__count=Count(
|
|
144
|
+
'accountmodel',
|
|
145
|
+
# excludes coa root accounts...
|
|
146
|
+
filter=Q(accountmodel__depth__gt=2) & Q(accountmodel__active=True),
|
|
147
|
+
),
|
|
148
|
+
# Root-group presence and uniqueness checks:
|
|
149
|
+
accountmodel_rootgroup__count=Count('accountmodel', filter=Q(accountmodel__role__in=ROOT_GROUP)),
|
|
150
|
+
accountmodel_rootgroup_roles__distinct_count=Count(
|
|
151
|
+
'accountmodel__role',
|
|
152
|
+
filter=Q(accountmodel__role__in=ROOT_GROUP_META),
|
|
153
|
+
distinct=True,
|
|
164
154
|
),
|
|
165
|
-
default=Value(False, output_field=BooleanField()),
|
|
166
|
-
output_field=BooleanField()
|
|
167
155
|
)
|
|
168
|
-
|
|
156
|
+
.annotate(
|
|
157
|
+
configured=models.Case(
|
|
158
|
+
models.When(
|
|
159
|
+
Q(accountmodel_rootgroup__count__gte=1)
|
|
160
|
+
& Q(accountmodel_rootgroup__count=F('accountmodel_rootgroup_roles__distinct_count')),
|
|
161
|
+
then=Value(True, output_field=BooleanField()),
|
|
162
|
+
),
|
|
163
|
+
default=Value(False, output_field=BooleanField()),
|
|
164
|
+
output_field=BooleanField(),
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
.select_related('entity')
|
|
168
|
+
)
|
|
169
169
|
|
|
170
170
|
@deprecated_entity_slug_behavior
|
|
171
|
-
def for_entity(
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
def for_entity(
|
|
172
|
+
self,
|
|
173
|
+
entity_model: 'Union[EntityModel | str | UUID]' = None, # noqa: F821
|
|
174
|
+
**kwargs, # noqa: F821
|
|
175
|
+
) -> ChartOfAccountModelQuerySet:
|
|
174
176
|
"""
|
|
175
177
|
Fetches a QuerySet of ChartOfAccountsModel associated with a specific EntityModel & UserModel.
|
|
176
178
|
May pass an instance of EntityModel or a String representing the EntityModel slug.
|
|
@@ -195,7 +197,7 @@ class ChartOfAccountModelManager(Manager):
|
|
|
195
197
|
'user_model parameter is deprecated and will be removed in a future release. '
|
|
196
198
|
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
197
199
|
DeprecationWarning,
|
|
198
|
-
stacklevel=2
|
|
200
|
+
stacklevel=2,
|
|
199
201
|
)
|
|
200
202
|
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
201
203
|
qs = qs.for_user(kwargs['user_model'])
|
|
@@ -232,9 +234,7 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
232
234
|
"""
|
|
233
235
|
|
|
234
236
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
235
|
-
entity = models.ForeignKey('django_ledger.EntityModel',
|
|
236
|
-
verbose_name=_('Entity'),
|
|
237
|
-
on_delete=models.CASCADE)
|
|
237
|
+
entity = models.ForeignKey('django_ledger.EntityModel', verbose_name=_('Entity'), on_delete=models.CASCADE)
|
|
238
238
|
active = models.BooleanField(default=True, verbose_name=_('Is Active'))
|
|
239
239
|
description = models.TextField(verbose_name=_('CoA Description'), null=True, blank=True)
|
|
240
240
|
objects = ChartOfAccountModelManager.from_queryset(queryset_class=ChartOfAccountModelQuerySet)()
|
|
@@ -244,9 +244,7 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
244
244
|
ordering = ['-created']
|
|
245
245
|
verbose_name = _('Chart of Account')
|
|
246
246
|
verbose_name_plural = _('Chart of Accounts')
|
|
247
|
-
indexes = [
|
|
248
|
-
models.Index(fields=['entity'])
|
|
249
|
-
]
|
|
247
|
+
indexes = [models.Index(fields=['entity'])]
|
|
250
248
|
|
|
251
249
|
def __str__(self):
|
|
252
250
|
if self.name is not None:
|
|
@@ -265,7 +263,7 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
265
263
|
return getattr(self, 'configured')
|
|
266
264
|
except AttributeError:
|
|
267
265
|
pass
|
|
268
|
-
account_qs = self.accountmodel_set.filter(role__in=
|
|
266
|
+
account_qs = self.accountmodel_set.filter(role__in=ROOT_GROUP)
|
|
269
267
|
self.configured = len(account_qs) == len(ROOT_GROUP)
|
|
270
268
|
return self.configured
|
|
271
269
|
|
|
@@ -305,7 +303,7 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
305
303
|
role_default=True,
|
|
306
304
|
active=False,
|
|
307
305
|
locked=True,
|
|
308
|
-
balance_type=role_meta['balance_type']
|
|
306
|
+
balance_type=role_meta['balance_type'],
|
|
309
307
|
)
|
|
310
308
|
AccountModel.add_root(instance=root_account)
|
|
311
309
|
|
|
@@ -326,8 +324,9 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
326
324
|
role_default=True,
|
|
327
325
|
active=False,
|
|
328
326
|
locked=True,
|
|
329
|
-
balance_type=role_meta['balance_type']
|
|
330
|
-
)
|
|
327
|
+
balance_type=role_meta['balance_type'],
|
|
328
|
+
)
|
|
329
|
+
)
|
|
331
330
|
self.configured = True
|
|
332
331
|
|
|
333
332
|
def get_coa_root_accounts_qs(self) -> AccountModelQuerySet:
|
|
@@ -350,10 +349,12 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
350
349
|
qs = self.get_coa_root_accounts_qs()
|
|
351
350
|
return qs.get(role__exact=ROOT_COA)
|
|
352
351
|
|
|
353
|
-
def get_account_root_node(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
352
|
+
def get_account_root_node(
|
|
353
|
+
self,
|
|
354
|
+
account_model: AccountModel,
|
|
355
|
+
root_account_qs: Optional[AccountModelQuerySet] = None,
|
|
356
|
+
as_queryset: bool = False,
|
|
357
|
+
) -> AccountModel:
|
|
357
358
|
"""
|
|
358
359
|
Fetches the root node of the ChartOfAccountModel instance. The root node is the highest level of the CoA
|
|
359
360
|
hierarchy. It can be used to traverse the hierarchy of the CoA structure downstream.
|
|
@@ -389,7 +390,6 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
389
390
|
self.configure(raise_exception=True)
|
|
390
391
|
|
|
391
392
|
if not account_model.is_root_account():
|
|
392
|
-
|
|
393
393
|
if not root_account_qs:
|
|
394
394
|
root_account_qs = self.get_coa_root_accounts_qs()
|
|
395
395
|
|
|
@@ -406,16 +406,16 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
406
406
|
elif account_model.is_expense():
|
|
407
407
|
qs = root_account_qs.filter(code__exact=ROOT_GROUP_META[ROOT_EXPENSES]['code'])
|
|
408
408
|
else:
|
|
409
|
-
raise ChartOfAccountsModelValidationError(
|
|
410
|
-
|
|
411
|
-
|
|
409
|
+
raise ChartOfAccountsModelValidationError(
|
|
410
|
+
message=f'Unable to locate Balance Sheet'
|
|
411
|
+
' root node for account code: '
|
|
412
|
+
f'{account_model.code} {account_model.name}'
|
|
413
|
+
)
|
|
412
414
|
if as_queryset:
|
|
413
415
|
return qs
|
|
414
416
|
return qs.get()
|
|
415
417
|
|
|
416
|
-
raise ChartOfAccountsModelValidationError(
|
|
417
|
-
message='Adding Root account to Chart of Accounts is not allowed.'
|
|
418
|
-
)
|
|
418
|
+
raise ChartOfAccountsModelValidationError(message='Adding Root account to Chart of Accounts is not allowed.')
|
|
419
419
|
|
|
420
420
|
def get_non_root_coa_accounts_qs(self) -> AccountModelQuerySet:
|
|
421
421
|
"""
|
|
@@ -486,19 +486,12 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
486
486
|
"""
|
|
487
487
|
if self.slug:
|
|
488
488
|
if raise_exception:
|
|
489
|
-
raise ChartOfAccountsModelValidationError(
|
|
490
|
-
message=_(f'CoA {self.uuid} already has a slug')
|
|
491
|
-
)
|
|
489
|
+
raise ChartOfAccountsModelValidationError(message=_(f'CoA {self.uuid} already has a slug'))
|
|
492
490
|
return
|
|
493
491
|
self.slug = f'coa-{self.entity.slug[-5:]}-' + ''.join(choices(SLUG_SUFFIX, k=15))
|
|
494
492
|
|
|
495
493
|
if commit:
|
|
496
|
-
self.save(
|
|
497
|
-
update_fields=[
|
|
498
|
-
'slug',
|
|
499
|
-
'updated'
|
|
500
|
-
]
|
|
501
|
-
)
|
|
494
|
+
self.save(update_fields=['slug', 'updated'])
|
|
502
495
|
|
|
503
496
|
def is_default(self) -> bool:
|
|
504
497
|
"""
|
|
@@ -540,18 +533,16 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
540
533
|
|
|
541
534
|
"""
|
|
542
535
|
if not isinstance(account_model_qs, AccountModelQuerySet):
|
|
543
|
-
raise ChartOfAccountsModelValidationError(
|
|
544
|
-
message='Must pass an instance of AccountModelQuerySet'
|
|
545
|
-
)
|
|
536
|
+
raise ChartOfAccountsModelValidationError(message='Must pass an instance of AccountModelQuerySet')
|
|
546
537
|
for acc_model in account_model_qs:
|
|
547
538
|
if not acc_model.coa_model_id == self.uuid:
|
|
548
|
-
raise ChartOfAccountsModelValidationError(
|
|
549
|
-
message=f'Invalid root queryset for CoA {self.name}'
|
|
550
|
-
)
|
|
539
|
+
raise ChartOfAccountsModelValidationError(message=f'Invalid root queryset for CoA {self.name}')
|
|
551
540
|
|
|
552
|
-
def insert_account(
|
|
553
|
-
|
|
554
|
-
|
|
541
|
+
def insert_account(
|
|
542
|
+
self,
|
|
543
|
+
account_model: AccountModel,
|
|
544
|
+
root_account_qs: Optional[AccountModelQuerySet] = None,
|
|
545
|
+
):
|
|
555
546
|
"""
|
|
556
547
|
This method inserts the given account model into the chart of accounts (COA) instance.
|
|
557
548
|
It first verifies if the account model's COA model ID matches the COA's UUID. If not, it
|
|
@@ -599,21 +590,22 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
599
590
|
self.validate_account_model_qs(root_account_qs)
|
|
600
591
|
|
|
601
592
|
account_root_node: AccountModel = self.get_account_root_node(
|
|
602
|
-
account_model=account_model,
|
|
603
|
-
root_account_qs=root_account_qs
|
|
593
|
+
account_model=account_model, root_account_qs=root_account_qs
|
|
604
594
|
)
|
|
605
595
|
|
|
606
596
|
account_root_node.add_child(instance=account_model)
|
|
607
597
|
coa_accounts_qs = self.get_non_root_coa_accounts_qs()
|
|
608
598
|
return coa_accounts_qs.get(uuid__exact=account_model.uuid)
|
|
609
599
|
|
|
610
|
-
def create_account(
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
600
|
+
def create_account(
|
|
601
|
+
self,
|
|
602
|
+
code: str,
|
|
603
|
+
role: str,
|
|
604
|
+
name: str,
|
|
605
|
+
balance_type: str,
|
|
606
|
+
active: bool,
|
|
607
|
+
root_account_qs: Optional[AccountModelQuerySet] = None,
|
|
608
|
+
):
|
|
617
609
|
"""
|
|
618
610
|
Proper method for inserting a new Account Model into a CoA.
|
|
619
611
|
Use this in liu of the direct instantiation of the AccountModel of using the django related manager.
|
|
@@ -644,14 +636,11 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
644
636
|
role=role,
|
|
645
637
|
active=active,
|
|
646
638
|
balance_type=balance_type,
|
|
647
|
-
coa_model=self
|
|
639
|
+
coa_model=self,
|
|
648
640
|
)
|
|
649
641
|
account_model.clean()
|
|
650
642
|
|
|
651
|
-
account_model = self.insert_account(
|
|
652
|
-
account_model=account_model,
|
|
653
|
-
root_account_qs=root_account_qs
|
|
654
|
-
)
|
|
643
|
+
account_model = self.insert_account(account_model=account_model, root_account_qs=root_account_qs)
|
|
655
644
|
return account_model
|
|
656
645
|
|
|
657
646
|
# ACTIONS -----
|
|
@@ -692,18 +681,10 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
692
681
|
self.entity.default_coa_id = self.uuid
|
|
693
682
|
self.clean()
|
|
694
683
|
if commit:
|
|
695
|
-
self.entity.save(
|
|
696
|
-
update_fields=[
|
|
697
|
-
'default_coa_id',
|
|
698
|
-
'updated'
|
|
699
|
-
]
|
|
700
|
-
)
|
|
684
|
+
self.entity.save(update_fields=['default_coa_id', 'updated'])
|
|
701
685
|
|
|
702
686
|
def can_mark_as_default(self):
|
|
703
|
-
return all([
|
|
704
|
-
self.is_active(),
|
|
705
|
-
not self.is_default()
|
|
706
|
-
])
|
|
687
|
+
return all([self.is_active(), not self.is_default()])
|
|
707
688
|
|
|
708
689
|
def can_activate(self) -> bool:
|
|
709
690
|
"""
|
|
@@ -723,10 +704,7 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
723
704
|
-------
|
|
724
705
|
True if the object can be deactivated, False otherwise.
|
|
725
706
|
"""
|
|
726
|
-
return all([
|
|
727
|
-
self.is_active(),
|
|
728
|
-
not self.is_default()
|
|
729
|
-
])
|
|
707
|
+
return all([self.is_active(), not self.is_default()])
|
|
730
708
|
|
|
731
709
|
def mark_as_active(self, commit: bool = False, raise_exception: bool = False, **kwargs):
|
|
732
710
|
"""
|
|
@@ -741,19 +719,13 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
741
719
|
"""
|
|
742
720
|
if self.is_active():
|
|
743
721
|
if raise_exception:
|
|
744
|
-
raise ChartOfAccountsModelValidationError(
|
|
745
|
-
message=_('The Chart of Accounts is currently active.')
|
|
746
|
-
)
|
|
722
|
+
raise ChartOfAccountsModelValidationError(message=_('The Chart of Accounts is currently active.'))
|
|
747
723
|
return
|
|
748
724
|
|
|
749
725
|
self.active = True
|
|
750
726
|
self.clean()
|
|
751
727
|
if commit:
|
|
752
|
-
self.save(
|
|
753
|
-
update_fields=[
|
|
754
|
-
'active',
|
|
755
|
-
'updated'
|
|
756
|
-
])
|
|
728
|
+
self.save(update_fields=['active', 'updated'])
|
|
757
729
|
|
|
758
730
|
def mark_as_inactive(self, commit: bool = False, raise_exception: bool = False, **kwargs):
|
|
759
731
|
"""
|
|
@@ -768,19 +740,13 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
768
740
|
"""
|
|
769
741
|
if not self.is_active():
|
|
770
742
|
if raise_exception:
|
|
771
|
-
raise ChartOfAccountsModelValidationError(
|
|
772
|
-
message=_('The Chart of Accounts is currently not active.')
|
|
773
|
-
)
|
|
743
|
+
raise ChartOfAccountsModelValidationError(message=_('The Chart of Accounts is currently not active.'))
|
|
774
744
|
return
|
|
775
745
|
|
|
776
746
|
self.active = False
|
|
777
747
|
self.clean()
|
|
778
748
|
if commit:
|
|
779
|
-
self.save(
|
|
780
|
-
update_fields=[
|
|
781
|
-
'active',
|
|
782
|
-
'updated'
|
|
783
|
-
])
|
|
749
|
+
self.save(update_fields=['active', 'updated'])
|
|
784
750
|
|
|
785
751
|
# URLS....
|
|
786
752
|
def mark_as_default_url(self) -> str:
|
|
@@ -794,10 +760,7 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
794
760
|
"""
|
|
795
761
|
return reverse(
|
|
796
762
|
viewname='django_ledger:coa-action-mark-as-default',
|
|
797
|
-
kwargs={
|
|
798
|
-
'entity_slug': self.entity_slug,
|
|
799
|
-
'coa_slug': self.slug
|
|
800
|
-
}
|
|
763
|
+
kwargs={'entity_slug': self.entity_slug, 'coa_slug': self.slug},
|
|
801
764
|
)
|
|
802
765
|
|
|
803
766
|
def mark_as_active_url(self) -> str:
|
|
@@ -811,10 +774,7 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
811
774
|
"""
|
|
812
775
|
return reverse(
|
|
813
776
|
viewname='django_ledger:coa-action-mark-as-active',
|
|
814
|
-
kwargs={
|
|
815
|
-
'entity_slug': self.entity_slug,
|
|
816
|
-
'coa_slug': self.slug
|
|
817
|
-
}
|
|
777
|
+
kwargs={'entity_slug': self.entity_slug, 'coa_slug': self.slug},
|
|
818
778
|
)
|
|
819
779
|
|
|
820
780
|
def mark_as_inactive_url(self) -> str:
|
|
@@ -828,74 +788,49 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
828
788
|
"""
|
|
829
789
|
return reverse(
|
|
830
790
|
viewname='django_ledger:coa-action-mark-as-inactive',
|
|
831
|
-
kwargs={
|
|
832
|
-
'entity_slug': self.entity_slug,
|
|
833
|
-
'coa_slug': self.slug
|
|
834
|
-
}
|
|
791
|
+
kwargs={'entity_slug': self.entity_slug, 'coa_slug': self.slug},
|
|
835
792
|
)
|
|
836
793
|
|
|
837
794
|
def get_coa_list_url(self):
|
|
838
|
-
return reverse(
|
|
839
|
-
viewname='django_ledger:coa-list',
|
|
840
|
-
kwargs={
|
|
841
|
-
'entity_slug': self.entity_slug
|
|
842
|
-
}
|
|
843
|
-
)
|
|
795
|
+
return reverse(viewname='django_ledger:coa-list', kwargs={'entity_slug': self.entity_slug})
|
|
844
796
|
|
|
845
797
|
def get_coa_list_inactive_url(self):
|
|
846
798
|
return reverse(
|
|
847
799
|
viewname='django_ledger:coa-list-inactive',
|
|
848
|
-
kwargs={
|
|
849
|
-
'entity_slug': self.entity_slug
|
|
850
|
-
}
|
|
800
|
+
kwargs={'entity_slug': self.entity_slug},
|
|
851
801
|
)
|
|
852
802
|
|
|
853
803
|
def get_coa_create_url(self):
|
|
854
804
|
return reverse(
|
|
855
805
|
viewname='django_ledger:coa-create',
|
|
856
|
-
kwargs={
|
|
857
|
-
'entity_slug': self.entity_slug
|
|
858
|
-
}
|
|
806
|
+
kwargs={'entity_slug': self.entity_slug},
|
|
859
807
|
)
|
|
860
808
|
|
|
861
809
|
def get_absolute_url(self) -> str:
|
|
862
810
|
return reverse(
|
|
863
811
|
viewname='django_ledger:coa-detail',
|
|
864
|
-
kwargs={
|
|
865
|
-
'entity_slug': self.entity_slug,
|
|
866
|
-
'coa_slug': self.slug
|
|
867
|
-
}
|
|
812
|
+
kwargs={'entity_slug': self.entity_slug, 'coa_slug': self.slug},
|
|
868
813
|
)
|
|
869
814
|
|
|
870
815
|
def get_update_url(self) -> str:
|
|
871
816
|
return reverse(
|
|
872
817
|
viewname='django_ledger:coa-update',
|
|
873
|
-
kwargs={
|
|
874
|
-
'entity_slug': self.entity_slug,
|
|
875
|
-
'coa_slug': self.slug
|
|
876
|
-
}
|
|
818
|
+
kwargs={'entity_slug': self.entity_slug, 'coa_slug': self.slug},
|
|
877
819
|
)
|
|
878
820
|
|
|
879
821
|
def get_account_list_url(self):
|
|
880
|
-
|
|
881
822
|
if not self.slug:
|
|
882
823
|
self.generate_slug(commit=True)
|
|
883
824
|
|
|
884
825
|
return reverse(
|
|
885
826
|
viewname='django_ledger:account-list',
|
|
886
|
-
kwargs={
|
|
887
|
-
'entity_slug': self.entity_slug,
|
|
888
|
-
'coa_slug': self.slug
|
|
889
|
-
}
|
|
827
|
+
kwargs={'entity_slug': self.entity_slug, 'coa_slug': self.slug},
|
|
890
828
|
)
|
|
891
829
|
|
|
892
830
|
def get_create_coa_account_url(self):
|
|
893
831
|
return reverse(
|
|
894
832
|
viewname='django_ledger:account-create',
|
|
895
|
-
kwargs={
|
|
896
|
-
'coa_slug': self.slug,
|
|
897
|
-
'entity_slug': self.entity_slug
|
|
898
|
-
}
|
|
833
|
+
kwargs={'coa_slug': self.slug, 'entity_slug': self.entity_slug},
|
|
899
834
|
)
|
|
900
835
|
|
|
901
836
|
def clean(self):
|
|
@@ -915,9 +850,7 @@ class ChartOfAccountModel(ChartOfAccountModelAbstract):
|
|
|
915
850
|
def chartofaccountsmodel_presave(instance: ChartOfAccountModelAbstract, **kwargs):
|
|
916
851
|
instance.generate_slug()
|
|
917
852
|
if instance.is_default() and not instance.active:
|
|
918
|
-
raise ChartOfAccountsModelValidationError(
|
|
919
|
-
_('Default Chart of Accounts cannot be deactivated.')
|
|
920
|
-
)
|
|
853
|
+
raise ChartOfAccountsModelValidationError(_('Default Chart of Accounts cannot be deactivated.'))
|
|
921
854
|
|
|
922
855
|
|
|
923
856
|
@receiver(post_save, sender=ChartOfAccountModel)
|
|
@@ -109,6 +109,7 @@ Default Chart of Accounts Table
|
|
|
109
109
|
4050 in_other credit Other Income root_income
|
|
110
110
|
4020 in_passive credit Investing Income root_income
|
|
111
111
|
2010 lia_cl_acc_payable credit Accounts Payable root_liabilities
|
|
112
|
+
2015 lia_cl_credit_line credit Accounts Payable root_liabilities
|
|
112
113
|
2060 lia_cl_def_rev credit Deferred Revenues root_liabilities
|
|
113
114
|
2030 lia_cl_int_payable credit Interest Payable root_liabilities
|
|
114
115
|
2050 lia_cl_ltd_mat credit Current Maturities LT Debt root_liabilities
|
django_ledger/models/customer.py
CHANGED
|
@@ -54,7 +54,7 @@ class CustomerModelQueryset(QuerySet):
|
|
|
54
54
|
reports.
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
|
-
def for_user(self, user_model):
|
|
57
|
+
def for_user(self, user_model) -> 'CustomerModelQueryset':
|
|
58
58
|
"""
|
|
59
59
|
Fetches a QuerySet of BillModels that the UserModel as access to.
|
|
60
60
|
May include BillModels from multiple Entities.
|
|
@@ -70,12 +70,9 @@ class CustomerModelQueryset(QuerySet):
|
|
|
70
70
|
"""
|
|
71
71
|
if user_model.is_superuser:
|
|
72
72
|
return self
|
|
73
|
-
return self.filter(
|
|
74
|
-
Q(entity_model__admin=user_model)
|
|
75
|
-
| Q(entity_model__managers__in=[user_model])
|
|
76
|
-
)
|
|
73
|
+
return self.filter(Q(entity_model__admin=user_model) | Q(entity_model__managers__in=[user_model]))
|
|
77
74
|
|
|
78
|
-
def active(self) ->
|
|
75
|
+
def active(self) -> 'CustomerModelQueryset':
|
|
79
76
|
"""
|
|
80
77
|
Active customers can be assigned to new Invoices and show on dropdown menus and views.
|
|
81
78
|
|
|
@@ -86,7 +83,7 @@ class CustomerModelQueryset(QuerySet):
|
|
|
86
83
|
"""
|
|
87
84
|
return self.filter(active=True)
|
|
88
85
|
|
|
89
|
-
def inactive(self) ->
|
|
86
|
+
def inactive(self) -> 'CustomerModelQueryset':
|
|
90
87
|
"""
|
|
91
88
|
Active customers can be assigned to new Invoices and show on dropdown menus and views.
|
|
92
89
|
Marking CustomerModels as inactive can help reduce Database load to populate select inputs and also inactivate
|
|
@@ -100,7 +97,7 @@ class CustomerModelQueryset(QuerySet):
|
|
|
100
97
|
"""
|
|
101
98
|
return self.filter(active=False)
|
|
102
99
|
|
|
103
|
-
def hidden(self) ->
|
|
100
|
+
def hidden(self) -> 'CustomerModelQueryset':
|
|
104
101
|
"""
|
|
105
102
|
Hidden customers do not show on dropdown menus, but may be used via APIs or any other method that does not
|
|
106
103
|
involve the UI.
|
|
@@ -112,7 +109,7 @@ class CustomerModelQueryset(QuerySet):
|
|
|
112
109
|
"""
|
|
113
110
|
return self.filter(hidden=True)
|
|
114
111
|
|
|
115
|
-
def visible(self) ->
|
|
112
|
+
def visible(self) -> 'CustomerModelQueryset':
|
|
116
113
|
"""
|
|
117
114
|
Visible customers show on dropdown menus and views. Visible customers are active and not hidden.
|
|
118
115
|
|
|
@@ -130,10 +127,11 @@ class CustomerModelManager(Manager):
|
|
|
130
127
|
CustomerModel.
|
|
131
128
|
"""
|
|
132
129
|
|
|
130
|
+
def get_queryset(self) -> CustomerModelQueryset:
|
|
131
|
+
return CustomerModelQueryset(self.model, using=self._db)
|
|
132
|
+
|
|
133
133
|
@deprecated_entity_slug_behavior
|
|
134
|
-
def for_entity(
|
|
135
|
-
self, entity_model: 'EntityModel | str | UUID' = None, **kwargs
|
|
136
|
-
) -> CustomerModelQueryset:
|
|
134
|
+
def for_entity(self, entity_model: 'EntityModel | str | UUID', **kwargs) -> CustomerModelQueryset: # noqa: F821
|
|
137
135
|
"""
|
|
138
136
|
Fetches a QuerySet of CustomerModel associated with a specific EntityModel & UserModel.
|
|
139
137
|
May pass an instance of EntityModel or a String representing the EntityModel slug.
|
|
@@ -161,16 +159,15 @@ class CustomerModelManager(Manager):
|
|
|
161
159
|
qs = qs.for_user(kwargs['user_model'])
|
|
162
160
|
|
|
163
161
|
if isinstance(entity_model, EntityModel):
|
|
164
|
-
|
|
162
|
+
return qs.filter(entity_model=entity_model)
|
|
165
163
|
elif isinstance(entity_model, str):
|
|
166
|
-
|
|
164
|
+
return qs.filter(entity_model__slug__exact=entity_model)
|
|
167
165
|
elif isinstance(entity_model, UUID):
|
|
168
|
-
|
|
166
|
+
return qs.filter(entity_model_id=entity_model)
|
|
169
167
|
else:
|
|
170
168
|
raise CustomerModelValidationError(
|
|
171
169
|
message='Must pass EntityModel, slug or EntityModel UUID',
|
|
172
170
|
)
|
|
173
|
-
return qs
|
|
174
171
|
|
|
175
172
|
|
|
176
173
|
class CustomerModelAbstract(ContactInfoMixIn, TaxCollectionMixIn, CreateUpdateMixIn):
|
|
@@ -232,9 +229,7 @@ class CustomerModelAbstract(ContactInfoMixIn, TaxCollectionMixIn, CreateUpdateMi
|
|
|
232
229
|
description = models.TextField()
|
|
233
230
|
active = models.BooleanField(default=True)
|
|
234
231
|
hidden = models.BooleanField(default=False)
|
|
235
|
-
picture = models.ImageField(
|
|
236
|
-
upload_to=customer_picture_upload_to, null=True, blank=True
|
|
237
|
-
)
|
|
232
|
+
picture = models.ImageField(upload_to=customer_picture_upload_to, null=True, blank=True)
|
|
238
233
|
|
|
239
234
|
additional_info = models.JSONField(null=True, blank=True, default=dict)
|
|
240
235
|
|
|
@@ -292,9 +287,7 @@ class CustomerModelAbstract(ContactInfoMixIn, TaxCollectionMixIn, CreateUpdateMi
|
|
|
292
287
|
'key__exact': EntityStateModel.KEY_CUSTOMER,
|
|
293
288
|
}
|
|
294
289
|
|
|
295
|
-
state_model_qs = EntityStateModel.objects.filter(
|
|
296
|
-
**LOOKUP
|
|
297
|
-
).select_for_update()
|
|
290
|
+
state_model_qs = EntityStateModel.objects.filter(**LOOKUP).select_for_update()
|
|
298
291
|
state_model = state_model_qs.get()
|
|
299
292
|
state_model.sequence = F('sequence') + 1
|
|
300
293
|
state_model.save()
|
|
@@ -344,11 +337,9 @@ class CustomerModelAbstract(ContactInfoMixIn, TaxCollectionMixIn, CreateUpdateMi
|
|
|
344
337
|
|
|
345
338
|
return self.customer_number
|
|
346
339
|
|
|
347
|
-
def validate_for_entity(self, entity_model: 'EntityModel'):
|
|
340
|
+
def validate_for_entity(self, entity_model: 'EntityModel'): # noqa: F821
|
|
348
341
|
if entity_model.uuid != self.entity_model_id:
|
|
349
|
-
raise CustomerModelValidationError(
|
|
350
|
-
'EntityModel does not belong to this Vendor'
|
|
351
|
-
)
|
|
342
|
+
raise CustomerModelValidationError('EntityModel does not belong to this Vendor')
|
|
352
343
|
|
|
353
344
|
def clean(self):
|
|
354
345
|
"""
|