django-ledger 0.7.0__py3-none-any.whl → 0.7.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/admin/__init__.py +1 -1
- django_ledger/admin/{coa.py → chart_of_accounts.py} +1 -1
- django_ledger/admin/entity.py +1 -1
- django_ledger/forms/account.py +1 -1
- django_ledger/forms/bank_account.py +2 -0
- django_ledger/forms/chart_of_accounts.py +82 -0
- django_ledger/io/io_library.py +1 -1
- django_ledger/migrations/0018_transactionmodel_cleared_transactionmodel_reconciled_and_more.py +37 -0
- django_ledger/models/__init__.py +1 -1
- django_ledger/models/accounts.py +4 -0
- django_ledger/models/bank_account.py +6 -2
- django_ledger/models/bill.py +7 -3
- django_ledger/models/{coa.py → chart_of_accounts.py} +131 -27
- django_ledger/models/closing_entry.py +5 -7
- django_ledger/models/coa_default.py +1 -1
- django_ledger/models/customer.py +6 -2
- django_ledger/models/data_import.py +33 -8
- django_ledger/models/entity.py +27 -3
- django_ledger/models/estimate.py +6 -1
- django_ledger/models/invoice.py +14 -8
- django_ledger/models/items.py +19 -8
- django_ledger/models/journal_entry.py +71 -25
- django_ledger/models/ledger.py +8 -5
- django_ledger/models/purchase_order.py +9 -5
- django_ledger/models/transactions.py +23 -3
- django_ledger/models/unit.py +4 -0
- django_ledger/models/vendor.py +4 -0
- django_ledger/settings.py +28 -3
- django_ledger/templates/django_ledger/account/tags/accounts_table.html +3 -2
- django_ledger/templates/django_ledger/chart_of_accounts/coa_create.html +25 -0
- django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html +25 -6
- django_ledger/templates/django_ledger/chart_of_accounts/coa_update.html +2 -2
- django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html +10 -4
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_list_table.html +8 -0
- django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +2 -2
- django_ledger/templates/django_ledger/includes/footer.html +2 -2
- django_ledger/urls/chart_of_accounts.py +6 -0
- django_ledger/utils.py +1 -36
- django_ledger/views/__init__.py +1 -1
- django_ledger/views/account.py +16 -3
- django_ledger/views/{coa.py → chart_of_accounts.py} +48 -44
- django_ledger/views/mixins.py +16 -5
- {django_ledger-0.7.0.dist-info → django_ledger-0.7.2.dist-info}/METADATA +1 -3
- {django_ledger-0.7.0.dist-info → django_ledger-0.7.2.dist-info}/RECORD +49 -47
- django_ledger/forms/coa.py +0 -47
- {django_ledger-0.7.0.dist-info → django_ledger-0.7.2.dist-info}/AUTHORS.md +0 -0
- {django_ledger-0.7.0.dist-info → django_ledger-0.7.2.dist-info}/LICENSE +0 -0
- {django_ledger-0.7.0.dist-info → django_ledger-0.7.2.dist-info}/WHEEL +0 -0
- {django_ledger-0.7.0.dist-info → django_ledger-0.7.2.dist-info}/top_level.txt +0 -0
django_ledger/__init__.py
CHANGED
django_ledger/admin/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from django.contrib import admin
|
|
2
2
|
|
|
3
|
-
from django_ledger.admin.
|
|
3
|
+
from django_ledger.admin.chart_of_accounts import ChartOfAccountsModelAdmin
|
|
4
4
|
from django_ledger.admin.entity import EntityModelAdmin
|
|
5
5
|
from django_ledger.admin.ledger import LedgerModelAdmin
|
|
6
6
|
from django_ledger.models import EntityModel, ChartOfAccountModel, LedgerModel
|
|
@@ -3,7 +3,7 @@ from django.db.models import Count
|
|
|
3
3
|
from django.forms import ModelForm, BooleanField, BaseInlineFormSet
|
|
4
4
|
|
|
5
5
|
from django_ledger.models.accounts import AccountModel
|
|
6
|
-
from django_ledger.models.
|
|
6
|
+
from django_ledger.models.chart_of_accounts import ChartOfAccountModel
|
|
7
7
|
from django_ledger.models.entity import EntityModel
|
|
8
8
|
|
|
9
9
|
|
django_ledger/admin/entity.py
CHANGED
|
@@ -7,7 +7,7 @@ from django.forms import BaseInlineFormSet
|
|
|
7
7
|
from django.urls import reverse
|
|
8
8
|
from django.utils.html import format_html
|
|
9
9
|
|
|
10
|
-
from django_ledger.admin.
|
|
10
|
+
from django_ledger.admin.chart_of_accounts import ChartOfAccountsInLine
|
|
11
11
|
from django_ledger.io.io_core import get_localtime
|
|
12
12
|
from django_ledger.models import EntityUnitModel
|
|
13
13
|
from django_ledger.models.entity import EntityModel, EntityManagementModel
|
django_ledger/forms/account.py
CHANGED
|
@@ -38,7 +38,7 @@ class AccountModelCreateForm(ModelForm):
|
|
|
38
38
|
self.fields['role'].choices = ACCOUNT_CHOICES_NO_ROOT
|
|
39
39
|
self.fields['code'].required = False
|
|
40
40
|
self.fields['coa_model'].disabled = True
|
|
41
|
-
self.fields['coa_model'].required =
|
|
41
|
+
self.fields['coa_model'].required = True
|
|
42
42
|
|
|
43
43
|
self.form_id: str = self.get_form_id()
|
|
44
44
|
|
|
@@ -13,6 +13,8 @@ class BankAccountCreateForm(ModelForm):
|
|
|
13
13
|
super().__init__(*args, **kwargs)
|
|
14
14
|
self.ENTITY_SLUG = entity_slug
|
|
15
15
|
self.USER_MODEL = user_model
|
|
16
|
+
|
|
17
|
+
# todo: only the accounts that do not hava an associated bank account should be available to pick from...
|
|
16
18
|
account_qs = AccountModel.objects.for_entity(
|
|
17
19
|
user_model=self.USER_MODEL,
|
|
18
20
|
entity_model=self.ENTITY_SLUG
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from random import randint
|
|
2
|
+
|
|
3
|
+
from django.forms import ModelForm, TextInput, Textarea, HiddenInput
|
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
|
5
|
+
|
|
6
|
+
from django_ledger.models.chart_of_accounts import ChartOfAccountModel
|
|
7
|
+
from django_ledger.models.entity import EntityModel
|
|
8
|
+
from django_ledger.settings import DJANGO_LEDGER_FORM_INPUT_CLASSES
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ChartOfAccountsModelCreateForm(ModelForm):
|
|
12
|
+
FORM_ID_SEP = '___'
|
|
13
|
+
|
|
14
|
+
def __init__(self, entity_model: EntityModel, *args, **kwargs):
|
|
15
|
+
self.ENTITY_MODEL = entity_model
|
|
16
|
+
super().__init__(*args, **kwargs)
|
|
17
|
+
self.fields['entity'].disabled = True
|
|
18
|
+
self.fields['entity'].required = True
|
|
19
|
+
self.form_id: str = self.get_form_id()
|
|
20
|
+
|
|
21
|
+
def clean_entity(self):
|
|
22
|
+
return self.ENTITY_MODEL
|
|
23
|
+
|
|
24
|
+
def get_form_id(self) -> str:
|
|
25
|
+
return f'coa-model-create-form-{self.ENTITY_MODEL.slug}{self.FORM_ID_SEP}{randint(100000, 999999)}'
|
|
26
|
+
|
|
27
|
+
class Meta:
|
|
28
|
+
model = ChartOfAccountModel
|
|
29
|
+
fields = [
|
|
30
|
+
'entity',
|
|
31
|
+
'name',
|
|
32
|
+
'description'
|
|
33
|
+
]
|
|
34
|
+
labels = {
|
|
35
|
+
'name': _('Name'),
|
|
36
|
+
'description': _('Description'),
|
|
37
|
+
}
|
|
38
|
+
widgets = {
|
|
39
|
+
'entity': HiddenInput(),
|
|
40
|
+
'name': TextInput(
|
|
41
|
+
attrs={
|
|
42
|
+
'class': DJANGO_LEDGER_FORM_INPUT_CLASSES
|
|
43
|
+
}),
|
|
44
|
+
'description': Textarea(
|
|
45
|
+
attrs={
|
|
46
|
+
'class': DJANGO_LEDGER_FORM_INPUT_CLASSES
|
|
47
|
+
}
|
|
48
|
+
),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ChartOfAccountsModelUpdateForm(ModelForm):
|
|
53
|
+
|
|
54
|
+
FORM_ID_SEP = '___'
|
|
55
|
+
|
|
56
|
+
def __init__(self, *args, **kwargs):
|
|
57
|
+
super().__init__(*args, **kwargs)
|
|
58
|
+
self.form_id: str = self.get_form_id()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_form_id(self) -> str:
|
|
62
|
+
instance: ChartOfAccountModel = self.instance
|
|
63
|
+
return f'coa-model-update-form-{instance.slug}{self.FORM_ID_SEP}{randint(100000, 999999)}'
|
|
64
|
+
|
|
65
|
+
class Meta:
|
|
66
|
+
model = ChartOfAccountModel
|
|
67
|
+
fields = [
|
|
68
|
+
'name',
|
|
69
|
+
'active'
|
|
70
|
+
]
|
|
71
|
+
labels = {
|
|
72
|
+
'name': _('Name'),
|
|
73
|
+
'description': _('Description'),
|
|
74
|
+
}
|
|
75
|
+
widgets = {
|
|
76
|
+
'name': TextInput(attrs={
|
|
77
|
+
'class': DJANGO_LEDGER_FORM_INPUT_CLASSES
|
|
78
|
+
}),
|
|
79
|
+
'description': Textarea(attrs={
|
|
80
|
+
'class': DJANGO_LEDGER_FORM_INPUT_CLASSES
|
|
81
|
+
}),
|
|
82
|
+
}
|
django_ledger/io/io_library.py
CHANGED
|
@@ -22,7 +22,7 @@ from django.utils.translation import gettext_lazy as _
|
|
|
22
22
|
|
|
23
23
|
from django_ledger.io.io_core import get_localtime
|
|
24
24
|
from django_ledger.models.accounts import AccountModel, AccountModelQuerySet, CREDIT, DEBIT
|
|
25
|
-
from django_ledger.models.
|
|
25
|
+
from django_ledger.models.chart_of_accounts import ChartOfAccountModel
|
|
26
26
|
from django_ledger.models.entity import EntityModel
|
|
27
27
|
from django_ledger.models.ledger import LedgerModel, LedgerModelQuerySet
|
|
28
28
|
|
django_ledger/migrations/0018_transactionmodel_cleared_transactionmodel_reconciled_and_more.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Generated by Django 5.1.2 on 2024-11-20 22:26
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('django_ledger', '0017_alter_accountmodel_unique_together_and_more'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name='transactionmodel',
|
|
16
|
+
name='cleared',
|
|
17
|
+
field=models.BooleanField(default=False, verbose_name='Cleared'),
|
|
18
|
+
),
|
|
19
|
+
migrations.AddField(
|
|
20
|
+
model_name='transactionmodel',
|
|
21
|
+
name='reconciled',
|
|
22
|
+
field=models.BooleanField(default=False, verbose_name='Reconciled'),
|
|
23
|
+
),
|
|
24
|
+
migrations.AlterField(
|
|
25
|
+
model_name='chartofaccountmodel',
|
|
26
|
+
name='entity',
|
|
27
|
+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_ledger.entitymodel', verbose_name='Entity'),
|
|
28
|
+
),
|
|
29
|
+
migrations.AddIndex(
|
|
30
|
+
model_name='transactionmodel',
|
|
31
|
+
index=models.Index(fields=['cleared'], name='django_ledg_cleared_335c0b_idx'),
|
|
32
|
+
),
|
|
33
|
+
migrations.AddIndex(
|
|
34
|
+
model_name='transactionmodel',
|
|
35
|
+
index=models.Index(fields=['reconciled'], name='django_ledg_reconci_75dbc7_idx'),
|
|
36
|
+
),
|
|
37
|
+
]
|
django_ledger/models/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from django_ledger.models.mixins import *
|
|
2
2
|
from django_ledger.models.bank_account import *
|
|
3
|
-
from django_ledger.models.
|
|
3
|
+
from django_ledger.models.chart_of_accounts import *
|
|
4
4
|
from django_ledger.models.bill import *
|
|
5
5
|
from django_ledger.models.invoice import *
|
|
6
6
|
from django_ledger.models.items import *
|
django_ledger/models/accounts.py
CHANGED
|
@@ -1058,6 +1058,10 @@ class AccountModel(AccountModelAbstract):
|
|
|
1058
1058
|
Base Account Model from Account Model Abstract Class
|
|
1059
1059
|
"""
|
|
1060
1060
|
|
|
1061
|
+
class Meta(AccountModelAbstract.Meta):
|
|
1062
|
+
swappable = 'DJANGO_LEDGER_ACCOUNT_MODEL'
|
|
1063
|
+
abstract = False
|
|
1064
|
+
|
|
1061
1065
|
|
|
1062
1066
|
def accountmodel_presave(instance: AccountModel, **kwargs):
|
|
1063
1067
|
if instance.role_default is False:
|
|
@@ -91,7 +91,7 @@ class BankAccountModelManager(models.Manager):
|
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
class
|
|
94
|
+
class BankAccountModelAbstract(BankAccountInfoMixIn, CreateUpdateMixIn):
|
|
95
95
|
"""
|
|
96
96
|
This is the main abstract class which the BankAccountModel database will inherit from.
|
|
97
97
|
The BankAccountModel inherits functionality from the following MixIns:
|
|
@@ -204,7 +204,11 @@ class BackAccountModelAbstract(BankAccountInfoMixIn, CreateUpdateMixIn):
|
|
|
204
204
|
])
|
|
205
205
|
|
|
206
206
|
|
|
207
|
-
class BankAccountModel(
|
|
207
|
+
class BankAccountModel(BankAccountModelAbstract):
|
|
208
208
|
"""
|
|
209
209
|
Base Bank Account Model Implementation
|
|
210
210
|
"""
|
|
211
|
+
|
|
212
|
+
class Meta(BankAccountModelAbstract.Meta):
|
|
213
|
+
swappable = 'DJANGO_LEDGER_BANK_ACCOUNT_MODEL'
|
|
214
|
+
abstract = False
|
django_ledger/models/bill.py
CHANGED
|
@@ -23,7 +23,7 @@ from uuid import uuid4
|
|
|
23
23
|
from django.contrib.auth import get_user_model
|
|
24
24
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
|
25
25
|
from django.db import models, transaction, IntegrityError
|
|
26
|
-
from django.db.models import Q, Sum, F, Count
|
|
26
|
+
from django.db.models import Q, Sum, F, Count, QuerySet, Manager
|
|
27
27
|
from django.db.models.signals import pre_save
|
|
28
28
|
from django.shortcuts import get_object_or_404
|
|
29
29
|
from django.urls import reverse
|
|
@@ -53,7 +53,7 @@ class BillModelValidationError(ValidationError):
|
|
|
53
53
|
pass
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
class BillModelQuerySet(
|
|
56
|
+
class BillModelQuerySet(QuerySet):
|
|
57
57
|
"""
|
|
58
58
|
A custom defined QuerySet for the BillModel. This implements multiple methods or queries needed to get a filtered
|
|
59
59
|
QuerySet based on the BillModel status. For example, we might want to have list of bills which are paid, unpaid,
|
|
@@ -170,7 +170,7 @@ class BillModelQuerySet(models.QuerySet):
|
|
|
170
170
|
return self.filter(bill_status__exact=BillModel.BILL_STATUS_APPROVED)
|
|
171
171
|
|
|
172
172
|
|
|
173
|
-
class BillModelManager(
|
|
173
|
+
class BillModelManager(Manager):
|
|
174
174
|
"""
|
|
175
175
|
A custom defined BillModelManager that will act as an interface to handling the initial DB queries
|
|
176
176
|
to the BillModel. The default "get_queryset" has been overridden to refer the custom defined
|
|
@@ -1904,6 +1904,10 @@ class BillModel(BillModelAbstract):
|
|
|
1904
1904
|
Base BillModel from Abstract.
|
|
1905
1905
|
"""
|
|
1906
1906
|
|
|
1907
|
+
class Meta(BillModelAbstract.Meta):
|
|
1908
|
+
swappable = 'DJANGO_LEDGER_BILL_MODEL'
|
|
1909
|
+
abstract = False
|
|
1910
|
+
|
|
1907
1911
|
|
|
1908
1912
|
def billmodel_presave(instance: BillModel, **kwargs):
|
|
1909
1913
|
if instance.can_generate_bill_number():
|
|
@@ -5,17 +5,41 @@ Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
|
|
|
5
5
|
Chart Of Accounts
|
|
6
6
|
-----------------
|
|
7
7
|
|
|
8
|
-
A Chart of Accounts (CoA) is a
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
A Chart of Accounts (CoA) is a fundamental component of financial management in Django Ledger. It serves as the
|
|
9
|
+
backbone of financial statements and is organized within a ChartOfAccountModel.
|
|
10
|
+
|
|
11
|
+
### Key Features
|
|
12
|
+
|
|
13
|
+
- **Account Roles**: The CoA includes various account types such as:
|
|
14
|
+
- Cash
|
|
15
|
+
- Accounts Receivable
|
|
16
|
+
- Expenses
|
|
17
|
+
- Liabilities
|
|
18
|
+
- Income
|
|
19
|
+
|
|
20
|
+
- **Hierarchical Structure**: Accounts are logically grouped to form financial statements. For example, the Balance
|
|
21
|
+
Sheet may have a structure like this:
|
|
22
|
+
- Fixed Assets
|
|
23
|
+
- Tangible Assets
|
|
24
|
+
- Building
|
|
25
|
+
- Plant & Equipment
|
|
26
|
+
- Machinery
|
|
27
|
+
- Intangible Assets
|
|
28
|
+
|
|
29
|
+
- **Financial Statement Preparation**: Individual account balances are aggregated based on the CoA and AccountModel
|
|
30
|
+
roles to create comprehensive financial statements.
|
|
31
|
+
|
|
32
|
+
### Usage in EntityModel
|
|
33
|
+
|
|
34
|
+
- Every EntityModel must have a default CoA to create any type of transaction.
|
|
35
|
+
- If no explicit CoA is specified, the EntityModel's default CoA is used.
|
|
36
|
+
- Only ONE Chart of Accounts can be used when creating Journal Entries.
|
|
37
|
+
- Commingling between different CoAs is not allowed to maintain the integrity of Journal Entries.
|
|
38
|
+
|
|
39
|
+
This structure ensures a clear and organized approach to financial management within Django Ledger, facilitating
|
|
40
|
+
accurate record-keeping and reporting.
|
|
18
41
|
"""
|
|
42
|
+
|
|
19
43
|
from random import choices
|
|
20
44
|
from string import ascii_lowercase, digits
|
|
21
45
|
from typing import Optional, Union, Dict
|
|
@@ -25,7 +49,9 @@ from django.apps import apps
|
|
|
25
49
|
from django.contrib.auth import get_user_model
|
|
26
50
|
from django.core.exceptions import ValidationError
|
|
27
51
|
from django.db import models
|
|
28
|
-
from django.db.models import Q, F
|
|
52
|
+
from django.db.models import Q, F, Count, Manager, QuerySet
|
|
53
|
+
from django.db.models.signals import pre_save, post_save
|
|
54
|
+
from django.dispatch import receiver
|
|
29
55
|
from django.urls import reverse
|
|
30
56
|
from django.utils.translation import gettext_lazy as _
|
|
31
57
|
|
|
@@ -47,7 +73,7 @@ class ChartOfAccountsModelValidationError(ValidationError):
|
|
|
47
73
|
pass
|
|
48
74
|
|
|
49
75
|
|
|
50
|
-
class ChartOfAccountModelQuerySet(
|
|
76
|
+
class ChartOfAccountModelQuerySet(QuerySet):
|
|
51
77
|
|
|
52
78
|
def active(self):
|
|
53
79
|
"""
|
|
@@ -56,7 +82,7 @@ class ChartOfAccountModelQuerySet(models.QuerySet):
|
|
|
56
82
|
return self.filter(active=True)
|
|
57
83
|
|
|
58
84
|
|
|
59
|
-
class ChartOfAccountModelManager(
|
|
85
|
+
class ChartOfAccountModelManager(Manager):
|
|
60
86
|
"""
|
|
61
87
|
A custom defined ChartOfAccountModelManager that will act as an interface to handling the initial DB queries
|
|
62
88
|
to the ChartOfAccountModel.
|
|
@@ -65,8 +91,23 @@ class ChartOfAccountModelManager(models.Manager):
|
|
|
65
91
|
def get_queryset(self):
|
|
66
92
|
qs = super().get_queryset()
|
|
67
93
|
return qs.annotate(
|
|
68
|
-
_entity_slug=F('entity__slug')
|
|
69
|
-
|
|
94
|
+
_entity_slug=F('entity__slug'),
|
|
95
|
+
accountmodel_total__count=Count(
|
|
96
|
+
'accountmodel',
|
|
97
|
+
# excludes coa root accounts...
|
|
98
|
+
filter=Q(accountmodel__depth__gt=2)
|
|
99
|
+
),
|
|
100
|
+
accountmodel_locked__count=Count(
|
|
101
|
+
'accountmodel',
|
|
102
|
+
# excludes coa root accounts...
|
|
103
|
+
filter=Q(accountmodel__depth__gt=2) & Q(accountmodel__locked=True)
|
|
104
|
+
),
|
|
105
|
+
accountmodel_active__count=Count(
|
|
106
|
+
'accountmodel',
|
|
107
|
+
# excludes coa root accounts...
|
|
108
|
+
filter=Q(accountmodel__depth__gt=2) & Q(accountmodel__active=True)
|
|
109
|
+
),
|
|
110
|
+
).select_related('entity')
|
|
70
111
|
|
|
71
112
|
def for_user(self, user_model) -> ChartOfAccountModelQuerySet:
|
|
72
113
|
"""
|
|
@@ -91,9 +132,9 @@ class ChartOfAccountModelManager(models.Manager):
|
|
|
91
132
|
Q(entity__admin=user_model) |
|
|
92
133
|
Q(entity__managers__in=[user_model])
|
|
93
134
|
)
|
|
94
|
-
)
|
|
135
|
+
)
|
|
95
136
|
|
|
96
|
-
def for_entity(self,
|
|
137
|
+
def for_entity(self, entity_model, user_model) -> ChartOfAccountModelQuerySet:
|
|
97
138
|
"""
|
|
98
139
|
Fetches a QuerySet of ChartOfAccountsModel associated with a specific EntityModel & UserModel.
|
|
99
140
|
May pass an instance of EntityModel or a String representing the EntityModel slug.
|
|
@@ -113,9 +154,9 @@ class ChartOfAccountModelManager(models.Manager):
|
|
|
113
154
|
Returns a ChartOfAccountQuerySet with applied filters.
|
|
114
155
|
"""
|
|
115
156
|
qs = self.for_user(user_model)
|
|
116
|
-
if isinstance(
|
|
117
|
-
return qs.filter(entity=
|
|
118
|
-
return qs.filter(entity__slug__iexact=
|
|
157
|
+
if isinstance(entity_model, lazy_loader.get_entity_model()):
|
|
158
|
+
return qs.filter(entity=entity_model)
|
|
159
|
+
return qs.filter(entity__slug__iexact=entity_model)
|
|
119
160
|
|
|
120
161
|
|
|
121
162
|
class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
@@ -138,7 +179,6 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
138
179
|
|
|
139
180
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
140
181
|
entity = models.ForeignKey('django_ledger.EntityModel',
|
|
141
|
-
editable=False,
|
|
142
182
|
verbose_name=_('Entity'),
|
|
143
183
|
on_delete=models.CASCADE)
|
|
144
184
|
active = models.BooleanField(default=True, verbose_name=_('Is Active'))
|
|
@@ -292,7 +332,7 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
292
332
|
root_account = self.get_coa_root_node()
|
|
293
333
|
return AccountModel.dump_bulk(parent=root_account)
|
|
294
334
|
|
|
295
|
-
def generate_slug(self, raise_exception: bool = False) -> str:
|
|
335
|
+
def generate_slug(self, commit: bool = False, raise_exception: bool = False) -> str:
|
|
296
336
|
"""
|
|
297
337
|
Generates and assigns a slug based on the ChartOfAccounts model instance EntityModel information.
|
|
298
338
|
|
|
@@ -321,6 +361,14 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
321
361
|
return
|
|
322
362
|
self.slug = f'coa-{self.entity.slug[-5:]}-' + ''.join(choices(SLUG_SUFFIX, k=15))
|
|
323
363
|
|
|
364
|
+
if commit:
|
|
365
|
+
self.save(
|
|
366
|
+
update_fields=[
|
|
367
|
+
'slug',
|
|
368
|
+
'updated'
|
|
369
|
+
]
|
|
370
|
+
)
|
|
371
|
+
|
|
324
372
|
def configure(self, raise_exception: bool = True):
|
|
325
373
|
"""
|
|
326
374
|
A method that properly configures the ChartOfAccounts model and creates the appropriate hierarchy boilerplate
|
|
@@ -333,7 +381,7 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
333
381
|
Whether to raise an exception if root nodes already exist in the Chart of Accounts (default is True).
|
|
334
382
|
This indicates that the ChartOfAccountModel instance is already configured.
|
|
335
383
|
"""
|
|
336
|
-
self.generate_slug()
|
|
384
|
+
self.generate_slug(commit=False)
|
|
337
385
|
|
|
338
386
|
root_accounts_qs = self.get_coa_root_accounts_qs()
|
|
339
387
|
existing_root_roles = list(set(acc.role for acc in root_accounts_qs))
|
|
@@ -546,6 +594,7 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
546
594
|
account_qs.update(locked=False)
|
|
547
595
|
return account_qs
|
|
548
596
|
|
|
597
|
+
|
|
549
598
|
def mark_as_default(self, commit: bool = False, raise_exception: bool = False, **kwargs):
|
|
550
599
|
"""
|
|
551
600
|
Marks the current Chart of Accounts instances as default for the EntityModel.
|
|
@@ -563,6 +612,12 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
563
612
|
message=_(f'The Chart of Accounts {self.slug} is already default')
|
|
564
613
|
)
|
|
565
614
|
return
|
|
615
|
+
if not self.can_mark_as_default():
|
|
616
|
+
if raise_exception:
|
|
617
|
+
raise ChartOfAccountsModelValidationError(
|
|
618
|
+
message=_(f'The Chart of Accounts {self.slug} cannot be marked as default')
|
|
619
|
+
)
|
|
620
|
+
return
|
|
566
621
|
self.entity.default_coa_id = self.uuid
|
|
567
622
|
self.clean()
|
|
568
623
|
if commit:
|
|
@@ -573,6 +628,12 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
573
628
|
]
|
|
574
629
|
)
|
|
575
630
|
|
|
631
|
+
def can_mark_as_default(self):
|
|
632
|
+
return all([
|
|
633
|
+
self.is_active(),
|
|
634
|
+
not self.is_default()
|
|
635
|
+
])
|
|
636
|
+
|
|
576
637
|
def can_activate(self) -> bool:
|
|
577
638
|
"""
|
|
578
639
|
Check if the ChartOffAccountModel instance can be activated.
|
|
@@ -710,6 +771,22 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
710
771
|
}
|
|
711
772
|
)
|
|
712
773
|
|
|
774
|
+
def get_coa_list_inactive_url(self):
|
|
775
|
+
return reverse(
|
|
776
|
+
viewname='django_ledger:coa-list-inactive',
|
|
777
|
+
kwargs={
|
|
778
|
+
'entity_slug': self.entity_slug
|
|
779
|
+
}
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
def get_coa_create_url(self):
|
|
783
|
+
return reverse(
|
|
784
|
+
viewname='django_ledger:coa-create',
|
|
785
|
+
kwargs={
|
|
786
|
+
'entity_slug': self.entity_slug
|
|
787
|
+
}
|
|
788
|
+
)
|
|
789
|
+
|
|
713
790
|
def get_absolute_url(self) -> str:
|
|
714
791
|
return reverse(
|
|
715
792
|
viewname='django_ledger:coa-detail',
|
|
@@ -719,7 +796,20 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
719
796
|
}
|
|
720
797
|
)
|
|
721
798
|
|
|
799
|
+
def get_update_url(self) -> str:
|
|
800
|
+
return reverse(
|
|
801
|
+
viewname='django_ledger:coa-update',
|
|
802
|
+
kwargs={
|
|
803
|
+
'entity_slug': self.entity_slug,
|
|
804
|
+
'coa_slug': self.slug
|
|
805
|
+
}
|
|
806
|
+
)
|
|
807
|
+
|
|
722
808
|
def get_account_list_url(self):
|
|
809
|
+
|
|
810
|
+
if not self.slug:
|
|
811
|
+
self.generate_slug(commit=True)
|
|
812
|
+
|
|
723
813
|
return reverse(
|
|
724
814
|
viewname='django_ledger:account-list',
|
|
725
815
|
kwargs={
|
|
@@ -739,13 +829,27 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
|
|
|
739
829
|
|
|
740
830
|
def clean(self):
|
|
741
831
|
self.generate_slug()
|
|
742
|
-
if self.is_default() and not self.active:
|
|
743
|
-
raise ChartOfAccountsModelValidationError(
|
|
744
|
-
_('Default Chart of Accounts cannot be deactivated.')
|
|
745
|
-
)
|
|
746
832
|
|
|
747
833
|
|
|
748
834
|
class ChartOfAccountModel(ChartOfAccountModelAbstract):
|
|
749
835
|
"""
|
|
750
836
|
Base ChartOfAccounts Model
|
|
751
837
|
"""
|
|
838
|
+
class Meta(ChartOfAccountModelAbstract.Meta):
|
|
839
|
+
swappable = 'DJANGO_LEDGER_CHART_OF_ACCOUNTS_MODEL'
|
|
840
|
+
abstract = False
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
@receiver(pre_save, sender=ChartOfAccountModel)
|
|
844
|
+
def chartofaccountsmodel_presave(instance: ChartOfAccountModelAbstract, **kwargs):
|
|
845
|
+
instance.generate_slug()
|
|
846
|
+
if instance.is_default() and not instance.active:
|
|
847
|
+
raise ChartOfAccountsModelValidationError(
|
|
848
|
+
_('Default Chart of Accounts cannot be deactivated.')
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
@receiver(post_save, sender=ChartOfAccountModel)
|
|
853
|
+
def chartofaccountsmodel_postsave(instance: ChartOfAccountModelAbstract, **kwargs):
|
|
854
|
+
if instance._state.adding:
|
|
855
|
+
instance.configure()
|
|
@@ -9,6 +9,7 @@ from itertools import groupby, chain
|
|
|
9
9
|
from typing import Optional
|
|
10
10
|
from uuid import uuid4, UUID
|
|
11
11
|
|
|
12
|
+
from django.conf import settings
|
|
12
13
|
from django.core.exceptions import ValidationError
|
|
13
14
|
from django.core.validators import MinValueValidator
|
|
14
15
|
from django.db import models
|
|
@@ -23,6 +24,7 @@ from django_ledger.models.ledger import LedgerModel
|
|
|
23
24
|
from django_ledger.models.mixins import CreateUpdateMixIn, MarkdownNotesMixIn
|
|
24
25
|
from django_ledger.models.transactions import TransactionModel
|
|
25
26
|
from django_ledger.models.utils import lazy_loader
|
|
27
|
+
from django_ledger.settings import DJANGO_LEDGER_LEDGER_MODEL
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
class ClosingEntryValidationError(ValidationError):
|
|
@@ -338,7 +340,9 @@ class ClosingEntryModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn):
|
|
|
338
340
|
|
|
339
341
|
|
|
340
342
|
class ClosingEntryModel(ClosingEntryModelAbstract):
|
|
341
|
-
|
|
343
|
+
class Meta(ClosingEntryModelAbstract.Meta):
|
|
344
|
+
swappable = 'DJANGO_LEDGER_CLOSING_ENTRY_MODEL'
|
|
345
|
+
abstract = False
|
|
342
346
|
|
|
343
347
|
|
|
344
348
|
# todo: Remove this model!
|
|
@@ -349,12 +353,6 @@ class ClosingEntryTransactionModelQuerySet(models.QuerySet):
|
|
|
349
353
|
|
|
350
354
|
class ClosingEntryTransactionModelManager(models.Manager):
|
|
351
355
|
|
|
352
|
-
# def get_queryset(self):
|
|
353
|
-
# return super().get_queryset().select_related(
|
|
354
|
-
# 'closing_entry_model',
|
|
355
|
-
# 'closing_entry_model__entity_model'
|
|
356
|
-
# )
|
|
357
|
-
|
|
358
356
|
def for_entity(self, entity_slug):
|
|
359
357
|
qs = self.get_queryset()
|
|
360
358
|
if isinstance(entity_slug, lazy_loader.get_entity_model()):
|
|
@@ -33,8 +33,8 @@ Default Chart of Accounts Table
|
|
|
33
33
|
1910 asset_adjustment debit Securities Unrealized Gains/Losses root_assets
|
|
34
34
|
1920 asset_adjustment debit PPE Unrealized Gains/Losses root_assets
|
|
35
35
|
1010 asset_ca_cash debit Cash root_assets
|
|
36
|
-
1200 asset_ca_inv debit Inventory root_assets
|
|
37
36
|
1050 asset_ca_mkt_sec debit Short Term Investments root_assets
|
|
37
|
+
1200 asset_ca_inv debit Inventory root_assets
|
|
38
38
|
1300 asset_ca_prepaid debit Prepaid Expenses root_assets
|
|
39
39
|
1100 asset_ca_recv debit Accounts Receivable root_assets
|
|
40
40
|
1110 asset_ca_uncoll credit Uncollectibles root_assets
|
django_ledger/models/customer.py
CHANGED
|
@@ -10,7 +10,7 @@ from uuid import uuid4
|
|
|
10
10
|
|
|
11
11
|
from django.core.exceptions import ObjectDoesNotExist
|
|
12
12
|
from django.db import models, transaction, IntegrityError
|
|
13
|
-
from django.db.models import Q, F, QuerySet
|
|
13
|
+
from django.db.models import Q, F, QuerySet, Manager
|
|
14
14
|
from django.utils.translation import gettext_lazy as _
|
|
15
15
|
|
|
16
16
|
from django_ledger.models.mixins import ContactInfoMixIn, CreateUpdateMixIn, TaxCollectionMixIn
|
|
@@ -77,7 +77,7 @@ class CustomerModelQueryset(QuerySet):
|
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
class CustomerModelManager(
|
|
80
|
+
class CustomerModelManager(Manager):
|
|
81
81
|
"""
|
|
82
82
|
A custom defined CustomerModelManager that will act as an interface to handling the DB queries to the
|
|
83
83
|
CustomerModel.
|
|
@@ -329,3 +329,7 @@ class CustomerModel(CustomerModelAbstract):
|
|
|
329
329
|
"""
|
|
330
330
|
Base Customer Model Implementation
|
|
331
331
|
"""
|
|
332
|
+
|
|
333
|
+
class Meta(CustomerModelAbstract.Meta):
|
|
334
|
+
swappable = 'DJANGO_LEDGER_CUSTOMER_MODEL'
|
|
335
|
+
abstract = False
|