django-ledger 0.5.5.4__py3-none-any.whl → 0.5.6.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/admin/__init__.py +10 -0
- django_ledger/admin/coa.py +135 -0
- django_ledger/admin/entity.py +199 -0
- django_ledger/admin/ledger.py +283 -0
- django_ledger/forms/entity.py +4 -12
- django_ledger/forms/ledger.py +19 -0
- django_ledger/forms/transactions.py +1 -1
- django_ledger/io/__init__.py +4 -1
- django_ledger/io/{io_mixin.py → io_core.py} +95 -35
- django_ledger/io/io_digest.py +15 -0
- django_ledger/io/{data_generator.py → io_generator.py} +51 -8
- django_ledger/io/io_library.py +317 -0
- django_ledger/io/{io_context.py → io_middleware.py} +16 -9
- django_ledger/migrations/0001_initial.py +413 -132
- django_ledger/migrations/0014_ledgermodel_ledger_xid_and_more.py +22 -0
- django_ledger/models/accounts.py +68 -7
- django_ledger/models/bank_account.py +12 -11
- django_ledger/models/bill.py +5 -9
- django_ledger/models/closing_entry.py +14 -14
- django_ledger/models/coa.py +60 -18
- django_ledger/models/customer.py +5 -11
- django_ledger/models/data_import.py +12 -6
- django_ledger/models/entity.py +90 -12
- django_ledger/models/estimate.py +12 -9
- django_ledger/models/invoice.py +10 -4
- django_ledger/models/items.py +11 -6
- django_ledger/models/journal_entry.py +61 -18
- django_ledger/models/ledger.py +90 -24
- django_ledger/models/mixins.py +2 -3
- django_ledger/models/purchase_order.py +11 -7
- django_ledger/models/transactions.py +3 -1
- django_ledger/models/unit.py +22 -13
- django_ledger/models/vendor.py +12 -11
- django_ledger/report/cash_flow_statement.py +1 -1
- django_ledger/report/core.py +3 -2
- django_ledger/templates/django_ledger/journal_entry/includes/card_journal_entry.html +1 -1
- django_ledger/templates/django_ledger/journal_entry/je_list.html +3 -0
- django_ledger/templatetags/django_ledger.py +1 -1
- django_ledger/tests/base.py +1 -1
- django_ledger/tests/test_entity.py +1 -1
- django_ledger/urls/ledger.py +3 -0
- django_ledger/views/entity.py +9 -3
- django_ledger/views/ledger.py +14 -7
- django_ledger/views/mixins.py +9 -1
- {django_ledger-0.5.5.4.dist-info → django_ledger-0.5.6.0.dist-info}/METADATA +9 -9
- {django_ledger-0.5.5.4.dist-info → django_ledger-0.5.6.0.dist-info}/RECORD +51 -46
- {django_ledger-0.5.5.4.dist-info → django_ledger-0.5.6.0.dist-info}/top_level.txt +0 -1
- django_ledger/admin.py +0 -160
- {django_ledger-0.5.5.4.dist-info → django_ledger-0.5.6.0.dist-info}/AUTHORS.md +0 -0
- {django_ledger-0.5.5.4.dist-info → django_ledger-0.5.6.0.dist-info}/LICENSE +0 -0
- {django_ledger-0.5.5.4.dist-info → django_ledger-0.5.6.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Generated by Django 4.2.6 on 2024-01-15 13:11
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('django_ledger', '0013_stagedtransactionmodel_bundle_split'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='ledgermodel',
|
|
15
|
+
name='ledger_xid',
|
|
16
|
+
field=models.SlugField(allow_unicode=True, blank=True, help_text='User Defined Ledger ID', max_length=150, null=True, verbose_name='Ledger Slug'),
|
|
17
|
+
),
|
|
18
|
+
migrations.AlterUniqueTogether(
|
|
19
|
+
name='ledgermodel',
|
|
20
|
+
unique_together={('entity', 'ledger_xid')},
|
|
21
|
+
),
|
|
22
|
+
]
|
django_ledger/models/accounts.py
CHANGED
|
@@ -34,6 +34,8 @@ from django.core.exceptions import ValidationError
|
|
|
34
34
|
from django.db import models
|
|
35
35
|
from django.db.models import Q
|
|
36
36
|
from django.db.models.signals import pre_save
|
|
37
|
+
from django.urls import reverse
|
|
38
|
+
from django.utils.timezone import localdate
|
|
37
39
|
from django.utils.translation import gettext_lazy as _
|
|
38
40
|
from treebeard.mp_tree import MP_Node, MP_NodeManager, MP_NodeQuerySet
|
|
39
41
|
|
|
@@ -42,8 +44,8 @@ from django_ledger.io.roles import (ACCOUNT_ROLE_CHOICES, BS_ROLES, GROUP_INVOIC
|
|
|
42
44
|
GROUP_LIABILITIES, GROUP_CAPITAL, GROUP_INCOME, GROUP_EXPENSES, GROUP_COGS,
|
|
43
45
|
ROOT_GROUP, BS_BUCKETS, ROOT_ASSETS, ROOT_LIABILITIES,
|
|
44
46
|
ROOT_CAPITAL, ROOT_INCOME, ROOT_EXPENSES, ROOT_COA)
|
|
45
|
-
from django_ledger.models import lazy_loader
|
|
46
47
|
from django_ledger.models.mixins import CreateUpdateMixIn
|
|
48
|
+
from django_ledger.models.utils import lazy_loader
|
|
47
49
|
from django_ledger.settings import DJANGO_LEDGER_ACCOUNT_CODE_GENERATE, DJANGO_LEDGER_ACCOUNT_CODE_USE_PREFIX
|
|
48
50
|
|
|
49
51
|
DEBIT = 'debit'
|
|
@@ -158,6 +160,11 @@ class AccountModelManager(MP_NodeManager):
|
|
|
158
160
|
"""
|
|
159
161
|
return AccountModelQuerySet(self.model).order_by('path')
|
|
160
162
|
|
|
163
|
+
def for_user(self, user_model):
|
|
164
|
+
qs = self.get_queryset()
|
|
165
|
+
if user_model.is_superuser:
|
|
166
|
+
return qs
|
|
167
|
+
|
|
161
168
|
# todo: search for uses and pass EntityModel whenever possible.
|
|
162
169
|
def for_entity(self,
|
|
163
170
|
user_model,
|
|
@@ -186,7 +193,7 @@ class AccountModelManager(MP_NodeManager):
|
|
|
186
193
|
AccountModelQuerySet
|
|
187
194
|
A QuerySet of all requested EntityModel Chart of Accounts.
|
|
188
195
|
"""
|
|
189
|
-
qs = self.
|
|
196
|
+
qs = self.for_user(user_model)
|
|
190
197
|
if select_coa_model:
|
|
191
198
|
qs = qs.select_related('coa_model')
|
|
192
199
|
|
|
@@ -201,11 +208,7 @@ class AccountModelManager(MP_NodeManager):
|
|
|
201
208
|
|
|
202
209
|
if coa_slug:
|
|
203
210
|
qs = qs.filter(coa_model__slug__exact=coa_slug)
|
|
204
|
-
|
|
205
|
-
return qs.filter(
|
|
206
|
-
Q(coa_model__entity__admin=user_model) |
|
|
207
|
-
Q(coa_model__entity__managers__in=[user_model])
|
|
208
|
-
).order_by('coa_model')
|
|
211
|
+
return qs.order_by('coa_model')
|
|
209
212
|
|
|
210
213
|
def for_entity_available(self, user_model, entity_slug, coa_slug: Optional[str] = None) -> AccountModelQuerySet:
|
|
211
214
|
"""
|
|
@@ -486,6 +489,54 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
|
|
|
486
489
|
x5=self.code
|
|
487
490
|
)
|
|
488
491
|
|
|
492
|
+
@classmethod
|
|
493
|
+
def create_account(cls,
|
|
494
|
+
name: str,
|
|
495
|
+
role: bool,
|
|
496
|
+
balance_type: str,
|
|
497
|
+
is_role_default: bool = False,
|
|
498
|
+
locked: bool = False,
|
|
499
|
+
active: bool = False,
|
|
500
|
+
**kwargs):
|
|
501
|
+
"""
|
|
502
|
+
Convenience Method to Create a new Account Model. This is the preferred method to create new Accounts in order
|
|
503
|
+
to properly handle parent/child relationships between models.
|
|
504
|
+
|
|
505
|
+
Parameters
|
|
506
|
+
----------
|
|
507
|
+
name: str
|
|
508
|
+
The name of the new Entity.
|
|
509
|
+
role: str
|
|
510
|
+
Account role.
|
|
511
|
+
balance_type: str
|
|
512
|
+
Account Balance Type. Must be 'debit' or 'credit'.
|
|
513
|
+
is_role_default: bool
|
|
514
|
+
If True, assigns account as default for role. Only once default account per role is permitted.
|
|
515
|
+
locked: bool
|
|
516
|
+
Marks account as Locked. Defaults to False.
|
|
517
|
+
active: bool
|
|
518
|
+
Marks account as Active. Defaults to True.
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
Returns
|
|
522
|
+
-------
|
|
523
|
+
AccountModel
|
|
524
|
+
The newly created AccountModel instance.
|
|
525
|
+
|
|
526
|
+
"""
|
|
527
|
+
account_model = cls(
|
|
528
|
+
name=name,
|
|
529
|
+
role=role,
|
|
530
|
+
balance_type=balance_type,
|
|
531
|
+
role_default=is_role_default,
|
|
532
|
+
locked=locked,
|
|
533
|
+
active=active,
|
|
534
|
+
**kwargs
|
|
535
|
+
)
|
|
536
|
+
account_model.clean()
|
|
537
|
+
account_model = cls.add_root(instance=account_model)
|
|
538
|
+
return account_model
|
|
539
|
+
|
|
489
540
|
@property
|
|
490
541
|
def role_bs(self) -> str:
|
|
491
542
|
"""
|
|
@@ -649,6 +700,16 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
|
|
|
649
700
|
ri = randint(10000, 99999)
|
|
650
701
|
return f'{prefix}{ri}'
|
|
651
702
|
|
|
703
|
+
def get_absolute_url(self):
|
|
704
|
+
return reverse(
|
|
705
|
+
viewname='django_ledger:account-detail-year',
|
|
706
|
+
kwargs={
|
|
707
|
+
'account_pk': self.uuid,
|
|
708
|
+
'entity_slug': self.coa_model.entity.slug,
|
|
709
|
+
'year': localdate().year
|
|
710
|
+
}
|
|
711
|
+
)
|
|
712
|
+
|
|
652
713
|
def clean(self):
|
|
653
714
|
|
|
654
715
|
if not self.code and DJANGO_LEDGER_ACCOUNT_CODE_GENERATE:
|
|
@@ -64,6 +64,15 @@ class BankAccountModelManager(models.Manager):
|
|
|
64
64
|
Custom defined Model Manager for the BankAccountModel.
|
|
65
65
|
"""
|
|
66
66
|
|
|
67
|
+
def for_user(self, user_model):
|
|
68
|
+
qs = self.get_queryset()
|
|
69
|
+
if user_model.is_superuser:
|
|
70
|
+
return qs
|
|
71
|
+
return qs.filter(
|
|
72
|
+
Q(entity_model__admin=user_model) |
|
|
73
|
+
Q(entity_model__managers__in=[user_model])
|
|
74
|
+
)
|
|
75
|
+
|
|
67
76
|
def for_entity(self, entity_slug, user_model) -> BankAccountModelQuerySet:
|
|
68
77
|
"""
|
|
69
78
|
Allows only the authorized user to query the BankAccountModel for a given EntityModel.
|
|
@@ -76,21 +85,13 @@ class BankAccountModelManager(models.Manager):
|
|
|
76
85
|
user_model
|
|
77
86
|
Logged in and authenticated django UserModel instance.
|
|
78
87
|
"""
|
|
79
|
-
qs = self.
|
|
88
|
+
qs = self.for_user(user_model)
|
|
80
89
|
if isinstance(entity_slug, lazy_loader.get_entity_model()):
|
|
81
90
|
return qs.filter(
|
|
82
|
-
Q(entity_model=entity_slug)
|
|
83
|
-
(
|
|
84
|
-
Q(entity_model__admin=user_model) |
|
|
85
|
-
Q(entity_model__managers__in=[user_model])
|
|
86
|
-
)
|
|
91
|
+
Q(entity_model=entity_slug)
|
|
87
92
|
)
|
|
88
93
|
return qs.filter(
|
|
89
|
-
Q(entity_model__slug__exact=entity_slug)
|
|
90
|
-
(
|
|
91
|
-
Q(entity_model__admin=user_model) |
|
|
92
|
-
Q(entity_model__managers__in=[user_model])
|
|
93
|
-
)
|
|
94
|
+
Q(entity_model__slug__exact=entity_slug)
|
|
94
95
|
)
|
|
95
96
|
|
|
96
97
|
|
django_ledger/models/bill.py
CHANGED
|
@@ -205,6 +205,8 @@ class BillModelManager(models.Manager):
|
|
|
205
205
|
Returns a BillModelQuerySet with applied filters.
|
|
206
206
|
"""
|
|
207
207
|
qs = self.get_queryset()
|
|
208
|
+
if user_model.is_superuser:
|
|
209
|
+
return qs
|
|
208
210
|
return qs.filter(
|
|
209
211
|
Q(ledger__entity__admin=user_model) |
|
|
210
212
|
Q(ledger__entity__managers__in=[user_model])
|
|
@@ -233,20 +235,14 @@ class BillModelManager(models.Manager):
|
|
|
233
235
|
BillModelQuerySet
|
|
234
236
|
Returns a BillModelQuerySet with applied filters.
|
|
235
237
|
"""
|
|
236
|
-
qs = self.
|
|
238
|
+
qs = self.for_user(user_model)
|
|
237
239
|
if isinstance(entity_slug, EntityModel):
|
|
238
240
|
return qs.filter(
|
|
239
|
-
Q(ledger__entity=entity_slug)
|
|
240
|
-
Q(ledger__entity__admin=user_model) |
|
|
241
|
-
Q(ledger__entity__managers__in=[user_model])
|
|
242
|
-
)
|
|
241
|
+
Q(ledger__entity=entity_slug)
|
|
243
242
|
)
|
|
244
243
|
elif isinstance(entity_slug, str):
|
|
245
244
|
return qs.filter(
|
|
246
|
-
Q(ledger__entity__slug__exact=entity_slug)
|
|
247
|
-
Q(ledger__entity__admin=user_model) |
|
|
248
|
-
Q(ledger__entity__managers__in=[user_model])
|
|
249
|
-
)
|
|
245
|
+
Q(ledger__entity__slug__exact=entity_slug)
|
|
250
246
|
)
|
|
251
247
|
|
|
252
248
|
|
|
@@ -35,23 +35,23 @@ class ClosingEntryModelQuerySet(models.QuerySet):
|
|
|
35
35
|
|
|
36
36
|
class ClosingEntryModelManager(models.Manager):
|
|
37
37
|
|
|
38
|
+
def for_user(self, user_model):
|
|
39
|
+
qs = self.get_queryset()
|
|
40
|
+
if user_model.is_superuser:
|
|
41
|
+
return qs
|
|
42
|
+
return qs.filter(
|
|
43
|
+
Q(entity_model__admin=user_model) |
|
|
44
|
+
Q(entity_model__managers__in=[user_model])
|
|
45
|
+
)
|
|
46
|
+
|
|
38
47
|
def for_entity(self, entity_slug, user_model):
|
|
48
|
+
qs = self.for_user(user_model)
|
|
39
49
|
if isinstance(entity_slug, lazy_loader.get_entity_model()):
|
|
40
|
-
return
|
|
41
|
-
Q(entity_model=entity_slug)
|
|
42
|
-
(
|
|
43
|
-
Q(entity_model__admin=user_model) |
|
|
44
|
-
Q(entity_model__managers__in=[user_model])
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
)
|
|
48
|
-
return self.get_queryset().filter(
|
|
49
|
-
Q(entity_model__slug__exact=entity_slug) &
|
|
50
|
-
(
|
|
51
|
-
Q(entity_model__admin=user_model) |
|
|
52
|
-
Q(entity_model__managers__in=[user_model])
|
|
50
|
+
return qs.filter(
|
|
51
|
+
Q(entity_model=entity_slug)
|
|
53
52
|
)
|
|
54
|
-
|
|
53
|
+
return qs.filter(
|
|
54
|
+
Q(entity_model__slug__exact=entity_slug)
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
|
django_ledger/models/coa.py
CHANGED
|
@@ -22,6 +22,8 @@ when no explicit CoA is specified, the default behavior is to use the EntityMode
|
|
|
22
22
|
Accounts can be used when creating Journal Entries**. No commingling between CoAs is allowed in order to preserve the
|
|
23
23
|
integrity of the Journal Entry.
|
|
24
24
|
"""
|
|
25
|
+
from random import choices
|
|
26
|
+
from string import ascii_lowercase, digits
|
|
25
27
|
from typing import Optional, Union
|
|
26
28
|
from uuid import uuid4
|
|
27
29
|
|
|
@@ -29,6 +31,7 @@ from django.contrib.auth import get_user_model
|
|
|
29
31
|
from django.core.exceptions import ValidationError
|
|
30
32
|
from django.db import models
|
|
31
33
|
from django.db.models import Q
|
|
34
|
+
from django.db.models.signals import pre_save, post_save
|
|
32
35
|
from django.utils.translation import gettext_lazy as _
|
|
33
36
|
|
|
34
37
|
from django_ledger.io import (ROOT_COA, ROOT_GROUP_LEVEL_2, ROOT_GROUP_META, ROOT_ASSETS,
|
|
@@ -41,6 +44,8 @@ from django_ledger.settings import logger
|
|
|
41
44
|
|
|
42
45
|
UserModel = get_user_model()
|
|
43
46
|
|
|
47
|
+
SLUG_SUFFIX = ascii_lowercase + digits
|
|
48
|
+
|
|
44
49
|
|
|
45
50
|
class ChartOfAccountsModelValidationError(ValidationError):
|
|
46
51
|
pass
|
|
@@ -84,7 +89,7 @@ class ChartOfAccountModelManager(models.Manager):
|
|
|
84
89
|
Q(entity__admin=user_model) |
|
|
85
90
|
Q(entity__managers__in=[user_model])
|
|
86
91
|
)
|
|
87
|
-
)
|
|
92
|
+
).select_related('entity')
|
|
88
93
|
|
|
89
94
|
def for_entity(self, entity_slug, user_model) -> ChartOfAccountModelQuerySet:
|
|
90
95
|
"""
|
|
@@ -127,7 +132,7 @@ class ChartOfAccountModelManager(models.Manager):
|
|
|
127
132
|
Q(entity__admin=user_model) |
|
|
128
133
|
Q(entity__managers__in=[user_model])
|
|
129
134
|
)
|
|
130
|
-
)
|
|
135
|
+
).select_related('entity')
|
|
131
136
|
|
|
132
137
|
|
|
133
138
|
class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
@@ -173,10 +178,9 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
173
178
|
]
|
|
174
179
|
|
|
175
180
|
def __str__(self):
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
# pass
|
|
181
|
+
if self.name is not None:
|
|
182
|
+
return f'{self.name} ({self.slug})'
|
|
183
|
+
return self.slug
|
|
180
184
|
|
|
181
185
|
def get_coa_root_accounts_qs(self) -> AccountModelQuerySet:
|
|
182
186
|
return self.accountmodel_set.all().is_coa_root()
|
|
@@ -222,28 +226,43 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
222
226
|
root_account = self.get_coa_root_account()
|
|
223
227
|
return AccountModel.dump_bulk(parent=root_account)
|
|
224
228
|
|
|
229
|
+
def generate_slug(self, raise_exception: bool = False) -> str:
|
|
230
|
+
if self.slug:
|
|
231
|
+
if raise_exception:
|
|
232
|
+
raise ChartOfAccountsModelValidationError(
|
|
233
|
+
message=_(f'CoA {self.uuid} already has a slug')
|
|
234
|
+
)
|
|
235
|
+
return
|
|
236
|
+
self.slug = f'coa-{self.entity.slug[-5:]}-' + ''.join(choices(SLUG_SUFFIX, k=15))
|
|
237
|
+
|
|
225
238
|
def configure(self, raise_exception: bool = True):
|
|
239
|
+
|
|
240
|
+
self.generate_slug()
|
|
241
|
+
|
|
226
242
|
root_accounts_qs = self.get_coa_root_accounts_qs()
|
|
227
243
|
existing_root_roles = list(set(acc.role for acc in root_accounts_qs))
|
|
228
244
|
|
|
229
245
|
if len(existing_root_roles) > 0:
|
|
230
|
-
|
|
246
|
+
if raise_exception:
|
|
247
|
+
raise ChartOfAccountsModelValidationError(message=f'Root Nodes already Exist in CoA {self.uuid}...')
|
|
248
|
+
return
|
|
231
249
|
|
|
232
250
|
if ROOT_COA not in existing_root_roles:
|
|
233
251
|
# add coa root...
|
|
234
252
|
role_meta = ROOT_GROUP_META[ROOT_COA]
|
|
235
253
|
account_pk = uuid4()
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
254
|
+
root_account = AccountModel(
|
|
255
|
+
uuid=account_pk,
|
|
256
|
+
code=role_meta['code'],
|
|
257
|
+
name=role_meta['title'],
|
|
258
|
+
coa_model=self,
|
|
259
|
+
role=ROOT_COA,
|
|
260
|
+
role_default=True,
|
|
261
|
+
active=False,
|
|
262
|
+
locked=True,
|
|
263
|
+
balance_type=role_meta['balance_type']
|
|
264
|
+
)
|
|
265
|
+
coa_root_account_model = AccountModel.add_root(instance=root_account)
|
|
247
266
|
|
|
248
267
|
coa_root_account_model = AccountModel.objects.get(uuid__exact=account_pk)
|
|
249
268
|
|
|
@@ -258,11 +277,17 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
258
277
|
name=role_meta['title'],
|
|
259
278
|
coa_model=self,
|
|
260
279
|
role=root_role,
|
|
280
|
+
role_default=True,
|
|
261
281
|
active=False,
|
|
262
282
|
locked=True,
|
|
263
283
|
balance_type=role_meta['balance_type']
|
|
264
284
|
))
|
|
265
285
|
|
|
286
|
+
def is_default(self) -> bool:
|
|
287
|
+
if self.entity_id is None:
|
|
288
|
+
return False
|
|
289
|
+
return self.entity.default_coa_id == self.uuid
|
|
290
|
+
|
|
266
291
|
def validate_account_model_qs(self, account_model_qs: AccountModelQuerySet):
|
|
267
292
|
if not isinstance(account_model_qs, AccountModelQuerySet):
|
|
268
293
|
raise ChartOfAccountsModelValidationError(
|
|
@@ -301,8 +326,25 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
301
326
|
non_root_accounts_qs.update(locked=False)
|
|
302
327
|
return non_root_accounts_qs
|
|
303
328
|
|
|
329
|
+
def clean(self):
|
|
330
|
+
self.generate_slug()
|
|
331
|
+
|
|
304
332
|
|
|
305
333
|
class ChartOfAccountModel(ChartOfAccountModelAbstract):
|
|
306
334
|
"""
|
|
307
335
|
Base ChartOfAccounts Model
|
|
308
336
|
"""
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# def chartofaccountmodel_presave(instance: ChartOfAccountModel, **kwargs):
|
|
340
|
+
# if instance._state.adding:
|
|
341
|
+
# instance.configure(raise_exception=True)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def chartofaccountmodel_postsave(instance: ChartOfAccountModel, **kwargs):
|
|
345
|
+
if instance._state.adding:
|
|
346
|
+
instance.configure(raise_exception=True)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# pre_save.connect(receiver=chartofaccountmodel_presave, sender=ChartOfAccountModel)
|
|
350
|
+
post_save.connect(receiver=chartofaccountmodel_postsave, sender=ChartOfAccountModel)
|
django_ledger/models/customer.py
CHANGED
|
@@ -107,6 +107,8 @@ class CustomerModelManager(models.Manager):
|
|
|
107
107
|
>>> customer_model_qs = CustomerModel.objects.for_user(user_model=request_user)
|
|
108
108
|
"""
|
|
109
109
|
qs = self.get_queryset()
|
|
110
|
+
if user_model.is_superuser:
|
|
111
|
+
return qs
|
|
110
112
|
return qs.filter(
|
|
111
113
|
Q(entity__admin=user_model) |
|
|
112
114
|
Q(entity__managers__in=[user_model])
|
|
@@ -135,22 +137,14 @@ class CustomerModelManager(models.Manager):
|
|
|
135
137
|
CustomerModelQueryset
|
|
136
138
|
A filtered CustomerModel QuerySet.
|
|
137
139
|
"""
|
|
138
|
-
qs = self.
|
|
140
|
+
qs = self.for_user(user_model)
|
|
139
141
|
|
|
140
142
|
if isinstance(entity_slug, lazy_loader.get_entity_model()):
|
|
141
143
|
return qs.filter(
|
|
142
|
-
Q(entity_model=entity_slug)
|
|
143
|
-
(
|
|
144
|
-
Q(entity_model__admin=user_model) |
|
|
145
|
-
Q(entity_model__managers__in=[user_model])
|
|
146
|
-
)
|
|
144
|
+
Q(entity_model=entity_slug)
|
|
147
145
|
)
|
|
148
146
|
return qs.filter(
|
|
149
|
-
Q(entity_model__slug__exact=entity_slug)
|
|
150
|
-
(
|
|
151
|
-
Q(entity_model__admin=user_model) |
|
|
152
|
-
Q(entity_model__managers__in=[user_model])
|
|
153
|
-
)
|
|
147
|
+
Q(entity_model__slug__exact=entity_slug)
|
|
154
148
|
)
|
|
155
149
|
|
|
156
150
|
|
|
@@ -42,14 +42,20 @@ class ImportJobModelManager(models.Manager):
|
|
|
42
42
|
'ledger_model'
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
-
def
|
|
45
|
+
def for_user(self, user_model):
|
|
46
46
|
qs = self.get_queryset()
|
|
47
|
+
if user_model.is_superuser:
|
|
48
|
+
return qs
|
|
47
49
|
return qs.filter(
|
|
48
|
-
Q(
|
|
49
|
-
(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
Q(bank_account_model__entity_model__admin=user_model) |
|
|
51
|
+
Q(bank_account_model__entity_model__managers__in=[user_model])
|
|
52
|
+
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def for_entity(self, entity_slug: str, user_model):
|
|
56
|
+
qs = self.for_user(user_model)
|
|
57
|
+
return qs.filter(
|
|
58
|
+
Q(bank_account_model__entity_model__slug__exact=entity_slug)
|
|
53
59
|
)
|
|
54
60
|
|
|
55
61
|
|
django_ledger/models/entity.py
CHANGED
|
@@ -27,7 +27,7 @@ from decimal import Decimal
|
|
|
27
27
|
from itertools import zip_longest
|
|
28
28
|
from random import choices
|
|
29
29
|
from string import ascii_lowercase, digits
|
|
30
|
-
from typing import Tuple, Union, Optional, List, Dict
|
|
30
|
+
from typing import Tuple, Union, Optional, List, Dict, Set
|
|
31
31
|
from uuid import uuid4, UUID
|
|
32
32
|
|
|
33
33
|
from django.contrib.auth import get_user_model
|
|
@@ -36,7 +36,7 @@ from django.core.cache import caches
|
|
|
36
36
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
|
37
37
|
from django.core.validators import MinValueValidator
|
|
38
38
|
from django.db import models
|
|
39
|
-
from django.db.models import Q
|
|
39
|
+
from django.db.models import Q
|
|
40
40
|
from django.db.models.signals import pre_save
|
|
41
41
|
from django.urls import reverse
|
|
42
42
|
from django.utils.text import slugify
|
|
@@ -45,7 +45,7 @@ from django.utils.translation import gettext_lazy as _
|
|
|
45
45
|
from treebeard.mp_tree import MP_Node, MP_NodeManager, MP_NodeQuerySet
|
|
46
46
|
|
|
47
47
|
from django_ledger.io import roles as roles_module, validate_roles, IODigestContextManager
|
|
48
|
-
from django_ledger.io.
|
|
48
|
+
from django_ledger.io.io_core import IOMixIn
|
|
49
49
|
from django_ledger.models.accounts import AccountModel, AccountModelQuerySet, DEBIT, CREDIT
|
|
50
50
|
from django_ledger.models.bank_account import BankAccountModelQuerySet, BankAccountModel
|
|
51
51
|
from django_ledger.models.coa import ChartOfAccountModel, ChartOfAccountModelQuerySet
|
|
@@ -114,7 +114,7 @@ class EntityModelManager(MP_NodeManager):
|
|
|
114
114
|
|
|
115
115
|
def get_queryset(self):
|
|
116
116
|
"""Sets the custom queryset as the default."""
|
|
117
|
-
qs =
|
|
117
|
+
qs = EntityModelQuerySet(self.model).order_by('path')
|
|
118
118
|
return qs.order_by('path').select_related('admin', 'default_coa')
|
|
119
119
|
|
|
120
120
|
def for_user(self, user_model):
|
|
@@ -135,6 +135,8 @@ class EntityModelManager(MP_NodeManager):
|
|
|
135
135
|
2. Is a manager.
|
|
136
136
|
"""
|
|
137
137
|
qs = self.get_queryset()
|
|
138
|
+
if user_model.is_superuser:
|
|
139
|
+
return qs
|
|
138
140
|
return qs.filter(
|
|
139
141
|
Q(admin=user_model) |
|
|
140
142
|
Q(managers__in=[user_model])
|
|
@@ -890,8 +892,15 @@ class EntityModelAbstract(MP_Node,
|
|
|
890
892
|
return user_model.id == self.admin_id
|
|
891
893
|
|
|
892
894
|
# #### LEDGER MANAGEMENT....
|
|
893
|
-
def create_ledger(self, name):
|
|
894
|
-
|
|
895
|
+
def create_ledger(self, name: str, ledger_xid: Optional[str] = None, posted: bool = False, commit: bool = True):
|
|
896
|
+
if commit:
|
|
897
|
+
return self.ledgermodel_set.create(name=name, ledger_xid=ledger_xid, posted=posted)
|
|
898
|
+
return LedgerModel(
|
|
899
|
+
entity=self,
|
|
900
|
+
posted=posted,
|
|
901
|
+
name=name,
|
|
902
|
+
ledger_xid=ledger_xid
|
|
903
|
+
)
|
|
895
904
|
|
|
896
905
|
# #### SLUG GENERATION ###
|
|
897
906
|
@staticmethod
|
|
@@ -1007,10 +1016,10 @@ class EntityModelAbstract(MP_Node,
|
|
|
1007
1016
|
coa_name = 'Default CoA'
|
|
1008
1017
|
|
|
1009
1018
|
chart_of_accounts = ChartOfAccountModel(
|
|
1010
|
-
slug=self.slug + ''.join(choices(ENTITY_RANDOM_SLUG_SUFFIX, k=6)) + '-coa',
|
|
1011
1019
|
name=coa_name,
|
|
1012
1020
|
entity=self
|
|
1013
1021
|
)
|
|
1022
|
+
|
|
1014
1023
|
chart_of_accounts.clean()
|
|
1015
1024
|
chart_of_accounts.save()
|
|
1016
1025
|
chart_of_accounts.configure()
|
|
@@ -1293,7 +1302,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
1293
1302
|
return self.get_coa_accounts(active=active, order_by=order_by)
|
|
1294
1303
|
|
|
1295
1304
|
def get_accounts_with_codes(self,
|
|
1296
|
-
code_list: Union[str, List[str]],
|
|
1305
|
+
code_list: Union[str, List[str], Set[str]],
|
|
1297
1306
|
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None
|
|
1298
1307
|
) -> AccountModelQuerySet:
|
|
1299
1308
|
"""
|
|
@@ -1351,12 +1360,72 @@ class EntityModelAbstract(MP_Node,
|
|
|
1351
1360
|
return account_model_qs.get(role__exact=role)
|
|
1352
1361
|
|
|
1353
1362
|
def create_account(self,
|
|
1354
|
-
|
|
1363
|
+
code: str,
|
|
1364
|
+
role: str,
|
|
1365
|
+
name: str,
|
|
1366
|
+
balance_type: str,
|
|
1367
|
+
active: bool,
|
|
1355
1368
|
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
1356
|
-
raise_exception: bool = True) ->
|
|
1369
|
+
raise_exception: bool = True) -> AccountModel:
|
|
1357
1370
|
"""
|
|
1358
1371
|
Creates a new AccountModel for the EntityModel.
|
|
1359
1372
|
|
|
1373
|
+
Parameters
|
|
1374
|
+
----------
|
|
1375
|
+
code: str
|
|
1376
|
+
The AccountModel code of the new Account.
|
|
1377
|
+
role: str
|
|
1378
|
+
The choice of role that the new Account belongs to.
|
|
1379
|
+
name: str
|
|
1380
|
+
The name of the new Account.
|
|
1381
|
+
balance_type: str
|
|
1382
|
+
The balance type of the new account. Possible values are 'debit' or 'credit'.
|
|
1383
|
+
active: bool
|
|
1384
|
+
Active status of the new account.
|
|
1385
|
+
coa_model: ChartOfAccountModel, UUID, str
|
|
1386
|
+
The ChartOfAccountsModel UUID, model instance or slug to pull accounts from. Uses default Coa if not
|
|
1387
|
+
provided.
|
|
1388
|
+
raise_exception: bool
|
|
1389
|
+
Raises EntityModelValidationError if ChartOfAccountsModel is not valid for the EntityModel instance.
|
|
1390
|
+
|
|
1391
|
+
Returns
|
|
1392
|
+
-------
|
|
1393
|
+
A tuple of ChartOfAccountModel, AccountModel
|
|
1394
|
+
The ChartOfAccountModel and AccountModel instance just created.
|
|
1395
|
+
"""
|
|
1396
|
+
if coa_model:
|
|
1397
|
+
if isinstance(coa_model, UUID):
|
|
1398
|
+
coa_model = self.chartofaccountsmodel_set.get(uuid__exact=coa_model)
|
|
1399
|
+
elif isinstance(coa_model, str):
|
|
1400
|
+
coa_model = self.chartofaccountsmodel_set.get(slug__exact=coa_model)
|
|
1401
|
+
elif isinstance(coa_model, ChartOfAccountModel):
|
|
1402
|
+
self.validate_chart_of_accounts_for_entity(
|
|
1403
|
+
coa_model=coa_model,
|
|
1404
|
+
raise_exception=raise_exception
|
|
1405
|
+
)
|
|
1406
|
+
else:
|
|
1407
|
+
coa_model = self.default_coa
|
|
1408
|
+
|
|
1409
|
+
account_model = AccountModel(
|
|
1410
|
+
code=code,
|
|
1411
|
+
name=name,
|
|
1412
|
+
role=role,
|
|
1413
|
+
active=active,
|
|
1414
|
+
balance_type=balance_type
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
account_model.clean()
|
|
1418
|
+
return coa_model.create_account(account_model=account_model)
|
|
1419
|
+
|
|
1420
|
+
def create_account_by_kwargs(self,
|
|
1421
|
+
account_model_kwargs: Dict,
|
|
1422
|
+
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
1423
|
+
raise_exception: bool = True) -> Tuple[ChartOfAccountModel, AccountModel]:
|
|
1424
|
+
"""
|
|
1425
|
+
Creates a new AccountModel for the EntityModel by passing AccountModel KWARGS.
|
|
1426
|
+
This is a legacy method for creating a new AccountModel for the EntityModel.
|
|
1427
|
+
Will be deprecated in favor of create_account().
|
|
1428
|
+
|
|
1360
1429
|
Parameters
|
|
1361
1430
|
----------
|
|
1362
1431
|
coa_model: ChartOfAccountModel, UUID, str
|
|
@@ -1378,7 +1447,10 @@ class EntityModelAbstract(MP_Node,
|
|
|
1378
1447
|
elif isinstance(coa_model, str):
|
|
1379
1448
|
coa_model = self.chartofaccountsmodel_set.get(slug__exact=coa_model)
|
|
1380
1449
|
elif isinstance(coa_model, ChartOfAccountModel):
|
|
1381
|
-
self.validate_chart_of_accounts_for_entity(
|
|
1450
|
+
self.validate_chart_of_accounts_for_entity(
|
|
1451
|
+
coa_model=coa_model,
|
|
1452
|
+
raise_exception=raise_exception
|
|
1453
|
+
)
|
|
1382
1454
|
else:
|
|
1383
1455
|
coa_model = self.default_coa
|
|
1384
1456
|
|
|
@@ -2692,6 +2764,12 @@ class EntityModelAbstract(MP_Node,
|
|
|
2692
2764
|
data_generator.populate_entity()
|
|
2693
2765
|
|
|
2694
2766
|
# URLS ----
|
|
2767
|
+
def get_absolute_url(self):
|
|
2768
|
+
return reverse(viewname='django_ledger:entity-dashboard',
|
|
2769
|
+
kwargs={
|
|
2770
|
+
'entity_slug': self.slug
|
|
2771
|
+
})
|
|
2772
|
+
|
|
2695
2773
|
def get_dashboard_url(self) -> str:
|
|
2696
2774
|
"""
|
|
2697
2775
|
The EntityModel Dashboard URL.
|
|
@@ -2954,7 +3032,7 @@ class EntityStateModelAbstract(models.Model):
|
|
|
2954
3032
|
]
|
|
2955
3033
|
|
|
2956
3034
|
def __str__(self):
|
|
2957
|
-
return f'{self.__class__.__name__} {self.
|
|
3035
|
+
return f'{self.__class__.__name__} {self.entity_model_id}: FY: {self.fiscal_year}, KEY: {self.get_key_display()}'
|
|
2958
3036
|
|
|
2959
3037
|
|
|
2960
3038
|
class EntityStateModel(EntityStateModelAbstract):
|