django-ledger 0.7.11__py3-none-any.whl → 0.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-ledger might be problematic. Click here for more details.
- django_ledger/__init__.py +1 -1
- django_ledger/context.py +12 -0
- django_ledger/forms/account.py +45 -46
- django_ledger/forms/bill.py +0 -4
- django_ledger/forms/closing_entry.py +13 -1
- django_ledger/forms/data_import.py +182 -63
- django_ledger/forms/estimate.py +3 -6
- django_ledger/forms/invoice.py +3 -7
- django_ledger/forms/item.py +10 -18
- django_ledger/forms/purchase_order.py +2 -4
- django_ledger/io/io_core.py +515 -400
- django_ledger/io/io_generator.py +7 -6
- django_ledger/io/io_library.py +1 -2
- django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
- django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
- django_ledger/models/__init__.py +2 -1
- django_ledger/models/accounts.py +109 -69
- django_ledger/models/bank_account.py +40 -23
- django_ledger/models/bill.py +386 -333
- django_ledger/models/chart_of_accounts.py +173 -105
- django_ledger/models/closing_entry.py +99 -48
- django_ledger/models/customer.py +100 -66
- django_ledger/models/data_import.py +818 -323
- django_ledger/models/deprecations.py +61 -0
- django_ledger/models/entity.py +891 -644
- django_ledger/models/estimate.py +57 -28
- django_ledger/models/invoice.py +46 -26
- django_ledger/models/items.py +503 -142
- django_ledger/models/journal_entry.py +61 -47
- django_ledger/models/ledger.py +106 -42
- django_ledger/models/mixins.py +424 -281
- django_ledger/models/purchase_order.py +39 -17
- django_ledger/models/receipt.py +1083 -0
- django_ledger/models/transactions.py +242 -139
- django_ledger/models/unit.py +93 -54
- django_ledger/models/utils.py +12 -2
- django_ledger/models/vendor.py +121 -70
- django_ledger/report/core.py +2 -14
- django_ledger/settings.py +57 -71
- django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
- django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +25 -0
- django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
- django_ledger/static/django_ledger/css/djl_styles.css +273 -0
- django_ledger/templates/django_ledger/bills/includes/card_bill.html +2 -2
- django_ledger/templates/django_ledger/components/menu.html +41 -26
- django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
- django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
- django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
- django_ledger/templates/django_ledger/customer/tags/customer_table.html +8 -6
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
- django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
- django_ledger/templates/django_ledger/entity/includes/card_entity.html +12 -6
- django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -1
- django_ledger/templates/django_ledger/financial_statements/cash_flow.html +4 -1
- django_ledger/templates/django_ledger/financial_statements/income_statement.html +4 -1
- django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +27 -3
- django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +16 -4
- django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +73 -18
- django_ledger/templates/django_ledger/includes/widget_ratios.html +18 -24
- django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +3 -3
- django_ledger/templates/django_ledger/layouts/base.html +7 -2
- django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
- django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
- django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
- django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
- django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +12 -7
- django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
- django_ledger/templatetags/django_ledger.py +338 -191
- django_ledger/tests/test_accounts.py +1 -2
- django_ledger/tests/test_io.py +17 -0
- django_ledger/tests/test_purchase_order.py +3 -3
- django_ledger/tests/test_transactions.py +1 -2
- django_ledger/urls/__init__.py +1 -4
- django_ledger/urls/customer.py +3 -0
- django_ledger/urls/data_import.py +3 -0
- django_ledger/urls/receipt.py +102 -0
- django_ledger/urls/vendor.py +1 -0
- django_ledger/views/__init__.py +1 -0
- django_ledger/views/bill.py +8 -11
- django_ledger/views/chart_of_accounts.py +6 -4
- django_ledger/views/closing_entry.py +11 -7
- django_ledger/views/customer.py +68 -30
- django_ledger/views/data_import.py +120 -66
- django_ledger/views/djl_api.py +3 -5
- django_ledger/views/entity.py +2 -4
- django_ledger/views/estimate.py +3 -7
- django_ledger/views/inventory.py +3 -5
- django_ledger/views/invoice.py +4 -6
- django_ledger/views/item.py +7 -11
- django_ledger/views/journal_entry.py +1 -2
- django_ledger/views/mixins.py +125 -93
- django_ledger/views/purchase_order.py +24 -35
- django_ledger/views/receipt.py +294 -0
- django_ledger/views/unit.py +1 -2
- django_ledger/views/vendor.py +54 -16
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/METADATA +43 -75
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/RECORD +104 -122
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +1 -1
- django_ledger-0.8.1.dist-info/top_level.txt +1 -0
- django_ledger/contrib/django_ledger_graphene/__init__.py +0 -0
- django_ledger/contrib/django_ledger_graphene/accounts/schema.py +0 -33
- django_ledger/contrib/django_ledger_graphene/api.py +0 -42
- django_ledger/contrib/django_ledger_graphene/apps.py +0 -6
- django_ledger/contrib/django_ledger_graphene/auth/mutations.py +0 -49
- django_ledger/contrib/django_ledger_graphene/auth/schema.py +0 -6
- django_ledger/contrib/django_ledger_graphene/bank_account/mutations.py +0 -61
- django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +0 -34
- django_ledger/contrib/django_ledger_graphene/bill/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/bill/schema.py +0 -34
- django_ledger/contrib/django_ledger_graphene/coa/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/coa/schema.py +0 -30
- django_ledger/contrib/django_ledger_graphene/customers/__init__.py +0 -0
- django_ledger/contrib/django_ledger_graphene/customers/mutations.py +0 -71
- django_ledger/contrib/django_ledger_graphene/customers/schema.py +0 -43
- django_ledger/contrib/django_ledger_graphene/data_import/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/data_import/schema.py +0 -0
- django_ledger/contrib/django_ledger_graphene/entity/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/entity/schema.py +0 -94
- django_ledger/contrib/django_ledger_graphene/item/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/item/schema.py +0 -31
- django_ledger/contrib/django_ledger_graphene/journal_entry/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +0 -35
- django_ledger/contrib/django_ledger_graphene/ledger/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/ledger/schema.py +0 -32
- django_ledger/contrib/django_ledger_graphene/purchase_order/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/purchase_order/schema.py +0 -31
- django_ledger/contrib/django_ledger_graphene/transaction/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/transaction/schema.py +0 -36
- django_ledger/contrib/django_ledger_graphene/unit/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/unit/schema.py +0 -27
- django_ledger/contrib/django_ledger_graphene/vendor/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/vendor/schema.py +0 -37
- django_ledger/contrib/django_ledger_graphene/views.py +0 -12
- django_ledger-0.7.11.dist-info/top_level.txt +0 -4
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/AUTHORS.md +0 -0
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/LICENSE +0 -0
django_ledger/models/entity.py
CHANGED
|
@@ -17,6 +17,7 @@ EntityModels may also have different financial reporting periods, (also known as
|
|
|
17
17
|
specified at the time of creation. All key functionality around the Fiscal Year is encapsulated in the
|
|
18
18
|
EntityReportMixIn.
|
|
19
19
|
"""
|
|
20
|
+
|
|
20
21
|
from calendar import monthrange
|
|
21
22
|
from collections import defaultdict
|
|
22
23
|
from datetime import date, datetime, timedelta
|
|
@@ -24,36 +25,56 @@ from decimal import Decimal
|
|
|
24
25
|
from itertools import zip_longest
|
|
25
26
|
from random import choices
|
|
26
27
|
from string import ascii_lowercase, digits
|
|
27
|
-
from typing import
|
|
28
|
-
from uuid import
|
|
28
|
+
from typing import Dict, List, Optional, Set, Tuple, Union
|
|
29
|
+
from uuid import UUID, uuid4
|
|
29
30
|
|
|
30
31
|
from django.contrib.auth import get_user_model
|
|
31
32
|
from django.core import serializers
|
|
32
33
|
from django.core.cache import caches
|
|
33
|
-
from django.core.exceptions import
|
|
34
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
34
35
|
from django.core.validators import MinValueValidator
|
|
35
36
|
from django.db import models
|
|
36
|
-
from django.db.models import
|
|
37
|
+
from django.db.models import F, Model, Q
|
|
37
38
|
from django.db.models.signals import pre_save
|
|
38
39
|
from django.urls import reverse
|
|
39
40
|
from django.utils.text import slugify
|
|
40
41
|
from django.utils.translation import gettext_lazy as _
|
|
41
42
|
from treebeard.mp_tree import MP_Node, MP_NodeManager, MP_NodeQuerySet
|
|
42
43
|
|
|
43
|
-
from django_ledger.io import
|
|
44
|
-
from django_ledger.io
|
|
45
|
-
from django_ledger.
|
|
46
|
-
|
|
47
|
-
from django_ledger.models.
|
|
44
|
+
from django_ledger.io import IODigestContextManager, validate_roles
|
|
45
|
+
from django_ledger.io import roles as roles_module
|
|
46
|
+
from django_ledger.io.io_core import IOMixIn, get_localdate, get_localtime
|
|
47
|
+
|
|
48
|
+
from django_ledger.models.accounts import (
|
|
49
|
+
CREDIT,
|
|
50
|
+
DEBIT,
|
|
51
|
+
AccountModel,
|
|
52
|
+
AccountModelQuerySet,
|
|
53
|
+
)
|
|
54
|
+
from django_ledger.models.bank_account import BankAccountModel, BankAccountModelQuerySet
|
|
55
|
+
from django_ledger.models.chart_of_accounts import (
|
|
56
|
+
ChartOfAccountModel,
|
|
57
|
+
ChartOfAccountModelQuerySet,
|
|
58
|
+
)
|
|
48
59
|
from django_ledger.models.coa_default import CHART_OF_ACCOUNTS_ROOT_MAP
|
|
49
|
-
from django_ledger.models.customer import
|
|
50
|
-
from django_ledger.models.items import (
|
|
51
|
-
|
|
60
|
+
from django_ledger.models.customer import CustomerModel, CustomerModelQueryset
|
|
61
|
+
from django_ledger.models.items import (
|
|
62
|
+
ItemModel,
|
|
63
|
+
ItemModelQuerySet,
|
|
64
|
+
ItemTransactionModelQuerySet,
|
|
65
|
+
UnitOfMeasureModel,
|
|
66
|
+
UnitOfMeasureModelQuerySet,
|
|
67
|
+
)
|
|
52
68
|
from django_ledger.models.ledger import LedgerModel
|
|
53
|
-
from django_ledger.models.mixins import
|
|
69
|
+
from django_ledger.models.mixins import (
|
|
70
|
+
ContactInfoMixIn,
|
|
71
|
+
CreateUpdateMixIn,
|
|
72
|
+
LoggingMixIn,
|
|
73
|
+
SlugNameMixIn,
|
|
74
|
+
)
|
|
54
75
|
from django_ledger.models.unit import EntityUnitModel
|
|
55
76
|
from django_ledger.models.utils import lazy_loader
|
|
56
|
-
from django_ledger.models.vendor import
|
|
77
|
+
from django_ledger.models.vendor import VendorModel, VendorModelQuerySet
|
|
57
78
|
from django_ledger.settings import DJANGO_LEDGER_DEFAULT_CLOSING_ENTRY_CACHE_TIMEOUT
|
|
58
79
|
|
|
59
80
|
UserModel = get_user_model()
|
|
@@ -71,7 +92,7 @@ class EntityModelQuerySet(MP_NodeQuerySet):
|
|
|
71
92
|
Inherits from the Materialized Path Node QuerySet Class from Django Treebeard.
|
|
72
93
|
"""
|
|
73
94
|
|
|
74
|
-
def hidden(self):
|
|
95
|
+
def hidden(self) -> 'EntityModelQuerySet':
|
|
75
96
|
"""
|
|
76
97
|
A QuerySet of all hidden EntityModel.
|
|
77
98
|
|
|
@@ -82,7 +103,7 @@ class EntityModelQuerySet(MP_NodeQuerySet):
|
|
|
82
103
|
"""
|
|
83
104
|
return self.filter(hidden=True)
|
|
84
105
|
|
|
85
|
-
def visible(self):
|
|
106
|
+
def visible(self) -> 'EntityModelQuerySet':
|
|
86
107
|
"""
|
|
87
108
|
A Queryset of all visible EntityModel.
|
|
88
109
|
|
|
@@ -108,15 +129,15 @@ class EntityModelManager(MP_NodeManager):
|
|
|
108
129
|
|
|
109
130
|
"""
|
|
110
131
|
|
|
111
|
-
def get_queryset(self):
|
|
132
|
+
def get_queryset(self) -> EntityModelQuerySet:
|
|
112
133
|
"""Sets the custom queryset as the default."""
|
|
113
|
-
qs = EntityModelQuerySet(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
qs = EntityModelQuerySet(self.model, using=self._db).order_by('path')
|
|
135
|
+
return (
|
|
136
|
+
qs.order_by('path')
|
|
137
|
+
.select_related('admin', 'default_coa')
|
|
138
|
+
.annotate(
|
|
139
|
+
_default_coa_slug=F('default_coa__slug'),
|
|
140
|
+
)
|
|
120
141
|
)
|
|
121
142
|
|
|
122
143
|
def for_user(self, user_model, authorized_superuser: bool = False):
|
|
@@ -141,10 +162,7 @@ class EntityModelManager(MP_NodeManager):
|
|
|
141
162
|
qs = self.get_queryset()
|
|
142
163
|
if user_model.is_superuser and authorized_superuser:
|
|
143
164
|
return qs
|
|
144
|
-
return qs.filter(
|
|
145
|
-
Q(admin=user_model) |
|
|
146
|
-
Q(managers__in=[user_model])
|
|
147
|
-
)
|
|
165
|
+
return qs.filter(Q(admin=user_model) | Q(managers__in=[user_model]))
|
|
148
166
|
|
|
149
167
|
|
|
150
168
|
class EntityModelFiscalPeriodMixIn:
|
|
@@ -153,6 +171,7 @@ class EntityModelFiscalPeriodMixIn:
|
|
|
153
171
|
EntityModel. At the moment of creation, an EntityModel must be assigned a calendar month which is going to
|
|
154
172
|
determine the start of the Fiscal Year.
|
|
155
173
|
"""
|
|
174
|
+
|
|
156
175
|
VALID_QUARTERS = list(range(1, 5))
|
|
157
176
|
VALID_MONTHS = list(range(1, 13))
|
|
158
177
|
|
|
@@ -179,7 +198,9 @@ class EntityModelFiscalPeriodMixIn:
|
|
|
179
198
|
# current object is not an entity, get current entity and fetch its fy_start_month value
|
|
180
199
|
|
|
181
200
|
# if current object is a detail view with an object...
|
|
182
|
-
obj = getattr(self, 'object')
|
|
201
|
+
obj = getattr(self, 'object', None) or getattr(
|
|
202
|
+
self, 'AUTHORIZED_ENTITY_MODEL'
|
|
203
|
+
)
|
|
183
204
|
if isinstance(obj, EntityModel):
|
|
184
205
|
entity = obj
|
|
185
206
|
elif isinstance(obj, LedgerModel):
|
|
@@ -251,7 +272,9 @@ class EntityModelFiscalPeriodMixIn:
|
|
|
251
272
|
"""
|
|
252
273
|
if fy_start_month:
|
|
253
274
|
self.validate_month(fy_start_month)
|
|
254
|
-
fy_start_month =
|
|
275
|
+
fy_start_month = (
|
|
276
|
+
self.get_fy_start_month() if not fy_start_month else fy_start_month
|
|
277
|
+
)
|
|
255
278
|
return date(year, fy_start_month, 1)
|
|
256
279
|
|
|
257
280
|
def get_fy_end(self, year: int, fy_start_month: int = None) -> date:
|
|
@@ -273,12 +296,16 @@ class EntityModelFiscalPeriodMixIn:
|
|
|
273
296
|
"""
|
|
274
297
|
if fy_start_month:
|
|
275
298
|
self.validate_month(fy_start_month)
|
|
276
|
-
fy_start_month =
|
|
299
|
+
fy_start_month = (
|
|
300
|
+
self.get_fy_start_month() if not fy_start_month else fy_start_month
|
|
301
|
+
)
|
|
277
302
|
ye = year if fy_start_month == 1 else year + 1
|
|
278
303
|
me = 12 if fy_start_month == 1 else fy_start_month - 1
|
|
279
304
|
return date(ye, me, monthrange(ye, me)[1])
|
|
280
305
|
|
|
281
|
-
def get_quarter_start(
|
|
306
|
+
def get_quarter_start(
|
|
307
|
+
self, year: int, quarter: int, fy_start_month: int = None
|
|
308
|
+
) -> date:
|
|
282
309
|
"""
|
|
283
310
|
The fiscal year quarter starting date of the EntityModel, according to its settings.
|
|
284
311
|
|
|
@@ -300,7 +327,9 @@ class EntityModelFiscalPeriodMixIn:
|
|
|
300
327
|
"""
|
|
301
328
|
if fy_start_month:
|
|
302
329
|
self.validate_month(fy_start_month)
|
|
303
|
-
fy_start_month =
|
|
330
|
+
fy_start_month = (
|
|
331
|
+
self.get_fy_start_month() if not fy_start_month else fy_start_month
|
|
332
|
+
)
|
|
304
333
|
self.validate_quarter(quarter)
|
|
305
334
|
quarter_month_start = (quarter - 1) * 3 + fy_start_month
|
|
306
335
|
year_start = year
|
|
@@ -309,7 +338,9 @@ class EntityModelFiscalPeriodMixIn:
|
|
|
309
338
|
year_start = year + 1
|
|
310
339
|
return date(year_start, quarter_month_start, 1)
|
|
311
340
|
|
|
312
|
-
def get_quarter_end(
|
|
341
|
+
def get_quarter_end(
|
|
342
|
+
self, year: int, quarter: int, fy_start_month: int = None
|
|
343
|
+
) -> date:
|
|
313
344
|
"""
|
|
314
345
|
The fiscal year quarter ending date of the EntityModel, according to its settings.
|
|
315
346
|
|
|
@@ -331,16 +362,22 @@ class EntityModelFiscalPeriodMixIn:
|
|
|
331
362
|
"""
|
|
332
363
|
if fy_start_month:
|
|
333
364
|
self.validate_month(fy_start_month)
|
|
334
|
-
fy_start_month =
|
|
365
|
+
fy_start_month = (
|
|
366
|
+
self.get_fy_start_month() if not fy_start_month else fy_start_month
|
|
367
|
+
)
|
|
335
368
|
self.validate_quarter(quarter)
|
|
336
369
|
quarter_month_end = quarter * 3 + fy_start_month - 1
|
|
337
370
|
year_end = year
|
|
338
371
|
if quarter_month_end > 12:
|
|
339
372
|
quarter_month_end -= 12
|
|
340
373
|
year_end += 1
|
|
341
|
-
return date(
|
|
374
|
+
return date(
|
|
375
|
+
year_end, quarter_month_end, monthrange(year_end, quarter_month_end)[1]
|
|
376
|
+
)
|
|
342
377
|
|
|
343
|
-
def get_fiscal_year_dates(
|
|
378
|
+
def get_fiscal_year_dates(
|
|
379
|
+
self, year: int, fy_start_month: int = None
|
|
380
|
+
) -> Tuple[date, date]:
|
|
344
381
|
"""
|
|
345
382
|
Convenience method to get in one shot both, fiscal year start and end dates.
|
|
346
383
|
|
|
@@ -365,7 +402,9 @@ class EntityModelFiscalPeriodMixIn:
|
|
|
365
402
|
ed = self.get_fy_end(year, fy_start_month)
|
|
366
403
|
return sd, ed
|
|
367
404
|
|
|
368
|
-
def get_fiscal_quarter_dates(
|
|
405
|
+
def get_fiscal_quarter_dates(
|
|
406
|
+
self, year: int, quarter: int, fy_start_month: int = None
|
|
407
|
+
) -> Tuple[date, date]:
|
|
369
408
|
"""
|
|
370
409
|
Convenience method to get in one shot both, fiscal year quarter start and end dates.
|
|
371
410
|
|
|
@@ -394,7 +433,9 @@ class EntityModelFiscalPeriodMixIn:
|
|
|
394
433
|
qe = self.get_quarter_end(year, quarter, fy_start_month)
|
|
395
434
|
return qs, qe
|
|
396
435
|
|
|
397
|
-
def get_fy_for_date(
|
|
436
|
+
def get_fy_for_date(
|
|
437
|
+
self, dt: Union[date, datetime], as_str: bool = False
|
|
438
|
+
) -> Union[str, int]:
|
|
398
439
|
"""
|
|
399
440
|
Given a known date, returns the EntityModel fiscal year associated with the given date.
|
|
400
441
|
|
|
@@ -428,32 +469,39 @@ class EntityModelClosingEntryMixIn:
|
|
|
428
469
|
Closing Entries provide
|
|
429
470
|
"""
|
|
430
471
|
|
|
431
|
-
def validate_closing_entry_model(
|
|
472
|
+
def validate_closing_entry_model(
|
|
473
|
+
self, closing_entry_model, closing_date: Optional[date] = None
|
|
474
|
+
):
|
|
432
475
|
if isinstance(self, EntityModel):
|
|
433
476
|
if self.uuid != closing_entry_model.entity_model_id:
|
|
434
477
|
raise EntityModelValidationError(
|
|
435
|
-
message=_(
|
|
478
|
+
message=_(
|
|
479
|
+
f'The Closing Entry Model {closing_entry_model} does not belong to Entity {self.name}'
|
|
480
|
+
)
|
|
436
481
|
)
|
|
437
482
|
if closing_date and closing_entry_model.closing_date != closing_date:
|
|
438
483
|
raise EntityModelValidationError(
|
|
439
|
-
message=_(
|
|
440
|
-
|
|
484
|
+
message=_(
|
|
485
|
+
f'The Closing Entry Model date {closing_entry_model.closing_date} '
|
|
486
|
+
f'does not match explicitly provided closing_date {closing_date}'
|
|
487
|
+
)
|
|
441
488
|
)
|
|
442
489
|
|
|
443
490
|
# ---> Closing Entry IO Digest <---
|
|
444
|
-
def get_closing_entry_digest(
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
491
|
+
def get_closing_entry_digest(
|
|
492
|
+
self,
|
|
493
|
+
to_date: date,
|
|
494
|
+
from_date: Optional[date] = None,
|
|
495
|
+
user_model: Optional[UserModel] = None,
|
|
496
|
+
closing_entry_model=None,
|
|
497
|
+
**kwargs: Dict,
|
|
498
|
+
) -> Tuple:
|
|
450
499
|
ClosingEntryModel = lazy_loader.get_closing_entry_model()
|
|
451
500
|
ClosingEntryTransactionModel = lazy_loader.get_closing_entry_transaction_model()
|
|
452
501
|
|
|
453
502
|
if not closing_entry_model:
|
|
454
503
|
closing_entry_model = ClosingEntryModel(
|
|
455
|
-
entity_model=self,
|
|
456
|
-
closing_date=to_date
|
|
504
|
+
entity_model=self, closing_date=to_date
|
|
457
505
|
)
|
|
458
506
|
closing_entry_model.clean()
|
|
459
507
|
else:
|
|
@@ -466,7 +514,7 @@ class EntityModelClosingEntryMixIn:
|
|
|
466
514
|
by_unit=True,
|
|
467
515
|
by_activity=True,
|
|
468
516
|
signs=False,
|
|
469
|
-
**kwargs
|
|
517
|
+
**kwargs,
|
|
470
518
|
)
|
|
471
519
|
ce_data = io_digest.get_closing_entry_data()
|
|
472
520
|
|
|
@@ -477,8 +525,9 @@ class EntityModelClosingEntryMixIn:
|
|
|
477
525
|
unit_model_id=ce['unit_uuid'],
|
|
478
526
|
tx_type=ce['balance_type'],
|
|
479
527
|
activity=ce['activity'],
|
|
480
|
-
balance=ce['balance']
|
|
481
|
-
)
|
|
528
|
+
balance=ce['balance'],
|
|
529
|
+
)
|
|
530
|
+
for ce in ce_data
|
|
482
531
|
]
|
|
483
532
|
|
|
484
533
|
for ce in ce_txs_list:
|
|
@@ -486,25 +535,25 @@ class EntityModelClosingEntryMixIn:
|
|
|
486
535
|
|
|
487
536
|
return closing_entry_model, ce_txs_list
|
|
488
537
|
|
|
489
|
-
def get_closing_entry_digest_for_date(
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
**kwargs) -> Tuple:
|
|
538
|
+
def get_closing_entry_digest_for_date(
|
|
539
|
+
self, closing_date: date, closing_entry_model=None, **kwargs
|
|
540
|
+
) -> Tuple:
|
|
493
541
|
return self.get_closing_entry_digest(
|
|
494
|
-
to_date=closing_date,
|
|
495
|
-
closing_entry_model=closing_entry_model,
|
|
496
|
-
**kwargs
|
|
542
|
+
to_date=closing_date, closing_entry_model=closing_entry_model, **kwargs
|
|
497
543
|
)
|
|
498
544
|
|
|
499
|
-
def get_closing_entry_digest_for_month(
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
**kwargs: Dict) -> Tuple:
|
|
545
|
+
def get_closing_entry_digest_for_month(
|
|
546
|
+
self, year: int, month: int, **kwargs: Dict
|
|
547
|
+
) -> Tuple:
|
|
503
548
|
_, day_end = monthrange(year, month)
|
|
504
549
|
closing_date = date(year=year, month=month, day=day_end)
|
|
505
|
-
return self.get_closing_entry_digest_for_date(
|
|
550
|
+
return self.get_closing_entry_digest_for_date(
|
|
551
|
+
closing_date=closing_date, **kwargs
|
|
552
|
+
)
|
|
506
553
|
|
|
507
|
-
def get_closing_entry_digest_for_fiscal_year(
|
|
554
|
+
def get_closing_entry_digest_for_fiscal_year(
|
|
555
|
+
self, fiscal_year: int, **kwargs: Dict
|
|
556
|
+
) -> Tuple:
|
|
508
557
|
closing_date = getattr(self, 'get_fy_end')(year=fiscal_year)
|
|
509
558
|
return self.get_closing_entry_digest_for_date(to_date=closing_date, **kwargs)
|
|
510
559
|
|
|
@@ -512,8 +561,8 @@ class EntityModelClosingEntryMixIn:
|
|
|
512
561
|
def get_closing_entry_queryset_for_date(self, closing_date: date):
|
|
513
562
|
ClosingEntryTransactionModel = lazy_loader.get_closing_entry_transaction_model()
|
|
514
563
|
return ClosingEntryTransactionModel.objects.for_entity(
|
|
515
|
-
|
|
516
|
-
).
|
|
564
|
+
entity_model=self,
|
|
565
|
+
).for_closing_date(closing_date)
|
|
517
566
|
|
|
518
567
|
def get_closing_entry_queryset_for_month(self, year: int, month: int):
|
|
519
568
|
_, end_day = monthrange(year, month)
|
|
@@ -525,27 +574,28 @@ class EntityModelClosingEntryMixIn:
|
|
|
525
574
|
return self.get_closing_entry_queryset_for_date(closing_date=closing_date)
|
|
526
575
|
|
|
527
576
|
# ----> Create Closing Entries <----
|
|
528
|
-
def create_closing_entry_for_date(
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
closing_entry_exists=True):
|
|
532
|
-
|
|
577
|
+
def create_closing_entry_for_date(
|
|
578
|
+
self, closing_date: date, closing_entry_model=None, closing_entry_exists=True
|
|
579
|
+
):
|
|
533
580
|
if closing_entry_model:
|
|
534
|
-
self.validate_closing_entry_model(
|
|
581
|
+
self.validate_closing_entry_model(
|
|
582
|
+
closing_entry_model, closing_date=closing_date
|
|
583
|
+
)
|
|
535
584
|
|
|
536
585
|
if closing_date > get_localdate():
|
|
537
586
|
raise EntityModelValidationError(
|
|
538
|
-
message=_(
|
|
587
|
+
message=_(
|
|
588
|
+
f'Cannot create closing entry with a future date {closing_date}.'
|
|
589
|
+
)
|
|
539
590
|
)
|
|
540
591
|
|
|
541
|
-
if closing_entry_model is None
|
|
592
|
+
if closing_entry_model is None:
|
|
542
593
|
self.closingentrymodel_set.filter(closing_date__exact=closing_date).delete()
|
|
543
594
|
else:
|
|
544
595
|
closing_entry_model.closingentrytransactionmodel_set.all().delete()
|
|
545
596
|
|
|
546
597
|
closing_entry_model, ce_txs_list = self.get_closing_entry_digest_for_date(
|
|
547
|
-
closing_date=closing_date,
|
|
548
|
-
closing_entry_model=closing_entry_model
|
|
598
|
+
closing_date=closing_date, closing_entry_model=closing_entry_model
|
|
549
599
|
)
|
|
550
600
|
|
|
551
601
|
if closing_entry_model is not None:
|
|
@@ -553,8 +603,7 @@ class EntityModelClosingEntryMixIn:
|
|
|
553
603
|
|
|
554
604
|
ClosingEntryTransactionModel = lazy_loader.get_closing_entry_transaction_model()
|
|
555
605
|
return closing_entry_model, ClosingEntryTransactionModel.objects.bulk_create(
|
|
556
|
-
objs=ce_txs_list,
|
|
557
|
-
batch_size=100
|
|
606
|
+
objs=ce_txs_list, batch_size=100
|
|
558
607
|
)
|
|
559
608
|
|
|
560
609
|
def create_closing_entry_for_month(self, year: int, month: int):
|
|
@@ -583,21 +632,26 @@ class EntityModelClosingEntryMixIn:
|
|
|
583
632
|
return f'closing_entry_{end_dt_str}_{self.uuid}'
|
|
584
633
|
|
|
585
634
|
# ----> Closing Entry Caching Month < -----
|
|
586
|
-
def get_closing_entry_cache_for_date(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
635
|
+
def get_closing_entry_cache_for_date(
|
|
636
|
+
self,
|
|
637
|
+
closing_date: date,
|
|
638
|
+
cache_name: str = 'default',
|
|
639
|
+
force_cache_update: bool = False,
|
|
640
|
+
cache_timeout: Optional[int] = None,
|
|
641
|
+
**kwargs,
|
|
642
|
+
):
|
|
593
643
|
if not force_cache_update:
|
|
594
644
|
cache_system = caches[cache_name]
|
|
595
|
-
ce_cache_key = self.get_closing_entry_cache_key_for_date(
|
|
645
|
+
ce_cache_key = self.get_closing_entry_cache_key_for_date(
|
|
646
|
+
closing_date=closing_date
|
|
647
|
+
)
|
|
596
648
|
ce_ser = cache_system.get(ce_cache_key)
|
|
597
649
|
|
|
598
650
|
# if closing entry is in cache...
|
|
599
651
|
if ce_ser:
|
|
600
|
-
ce_qs_serde_gen = serializers.deserialize(
|
|
652
|
+
ce_qs_serde_gen = serializers.deserialize(
|
|
653
|
+
format='json', stream_or_string=ce_ser
|
|
654
|
+
)
|
|
601
655
|
return list(ce.object for ce in ce_qs_serde_gen)
|
|
602
656
|
return
|
|
603
657
|
|
|
@@ -605,16 +659,18 @@ class EntityModelClosingEntryMixIn:
|
|
|
605
659
|
closing_date=closing_date,
|
|
606
660
|
cache_name=cache_name,
|
|
607
661
|
cache_timeout=cache_timeout,
|
|
608
|
-
**kwargs
|
|
609
|
-
|
|
610
|
-
def get_closing_entry_cache_for_month(self,
|
|
611
|
-
year: int,
|
|
612
|
-
month: int,
|
|
613
|
-
cache_name: str = 'default',
|
|
614
|
-
force_cache_update: bool = False,
|
|
615
|
-
cache_timeout: Optional[int] = None,
|
|
616
|
-
**kwargs):
|
|
662
|
+
**kwargs,
|
|
663
|
+
)
|
|
617
664
|
|
|
665
|
+
def get_closing_entry_cache_for_month(
|
|
666
|
+
self,
|
|
667
|
+
year: int,
|
|
668
|
+
month: int,
|
|
669
|
+
cache_name: str = 'default',
|
|
670
|
+
force_cache_update: bool = False,
|
|
671
|
+
cache_timeout: Optional[int] = None,
|
|
672
|
+
**kwargs,
|
|
673
|
+
):
|
|
618
674
|
_, day = monthrange(year, month)
|
|
619
675
|
closing_date = date(year, month, day)
|
|
620
676
|
return self.get_closing_entry_cache_for_date(
|
|
@@ -622,33 +678,39 @@ class EntityModelClosingEntryMixIn:
|
|
|
622
678
|
cache_name=cache_name,
|
|
623
679
|
force_cache_update=force_cache_update,
|
|
624
680
|
cache_timeout=cache_timeout,
|
|
625
|
-
**kwargs
|
|
681
|
+
**kwargs,
|
|
626
682
|
)
|
|
627
683
|
|
|
628
|
-
def get_closing_entry_cache_for_fiscal_year(
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
684
|
+
def get_closing_entry_cache_for_fiscal_year(
|
|
685
|
+
self,
|
|
686
|
+
fiscal_year: int,
|
|
687
|
+
cache_name: str = 'default',
|
|
688
|
+
force_cache_update: bool = False,
|
|
689
|
+
cache_timeout: Optional[int] = None,
|
|
690
|
+
**kwargs,
|
|
691
|
+
):
|
|
634
692
|
closing_date: date = getattr(self, 'get_fy_end')(year=fiscal_year)
|
|
635
693
|
return self.get_closing_entry_cache_for_date(
|
|
636
694
|
closing_date=closing_date,
|
|
637
695
|
cache_name=cache_name,
|
|
638
696
|
force_cache_update=force_cache_update,
|
|
639
697
|
cache_timeout=cache_timeout,
|
|
640
|
-
**kwargs
|
|
698
|
+
**kwargs,
|
|
641
699
|
)
|
|
642
700
|
|
|
643
701
|
# ---> SAVE CLOSING ENTRY <---
|
|
644
|
-
def save_closing_entry_cache_for_date(
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
702
|
+
def save_closing_entry_cache_for_date(
|
|
703
|
+
self,
|
|
704
|
+
closing_date: date,
|
|
705
|
+
cache_name: str = 'default',
|
|
706
|
+
cache_timeout: Optional[int] = None,
|
|
707
|
+
**kwargs,
|
|
708
|
+
):
|
|
649
709
|
cache_system = caches[cache_name]
|
|
650
710
|
ce_qs = self.get_closing_entry_queryset_for_date(closing_date=closing_date)
|
|
651
|
-
ce_cache_key = self.get_closing_entry_cache_key_for_date(
|
|
711
|
+
ce_cache_key = self.get_closing_entry_cache_key_for_date(
|
|
712
|
+
closing_date=closing_date
|
|
713
|
+
)
|
|
652
714
|
ce_ser = serializers.serialize(format='json', queryset=ce_qs)
|
|
653
715
|
|
|
654
716
|
if not cache_timeout:
|
|
@@ -657,43 +719,49 @@ class EntityModelClosingEntryMixIn:
|
|
|
657
719
|
cache_system.set(ce_cache_key, ce_ser, cache_timeout, **kwargs)
|
|
658
720
|
return list(ce_qs)
|
|
659
721
|
|
|
660
|
-
def save_closing_entry_cache_for_month(
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
722
|
+
def save_closing_entry_cache_for_month(
|
|
723
|
+
self,
|
|
724
|
+
year: int,
|
|
725
|
+
month: int,
|
|
726
|
+
cache_name: str = 'default',
|
|
727
|
+
cache_timeout: Optional[int] = None,
|
|
728
|
+
**kwargs,
|
|
729
|
+
):
|
|
666
730
|
_, day = monthrange(year, month)
|
|
667
731
|
closing_date = date(year, month, day)
|
|
668
732
|
return self.save_closing_entry_cache_for_date(
|
|
669
733
|
closing_date=closing_date,
|
|
670
734
|
cache_name=cache_name,
|
|
671
735
|
cache_timeout=cache_timeout,
|
|
672
|
-
**kwargs
|
|
736
|
+
**kwargs,
|
|
673
737
|
)
|
|
674
738
|
|
|
675
|
-
def save_closing_entry_cache_for_fiscal_year(
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
739
|
+
def save_closing_entry_cache_for_fiscal_year(
|
|
740
|
+
self,
|
|
741
|
+
fiscal_year: int,
|
|
742
|
+
cache_name: str = 'default',
|
|
743
|
+
cache_timeout: Optional[int] = None,
|
|
744
|
+
**kwargs,
|
|
745
|
+
):
|
|
680
746
|
closing_date: date = getattr(self, 'get_fy_end')(year=fiscal_year)
|
|
681
747
|
return self.save_closing_entry_cache_for_date(
|
|
682
748
|
closing_date=closing_date,
|
|
683
749
|
cache_name=cache_name,
|
|
684
750
|
cache_timeout=cache_timeout,
|
|
685
|
-
**kwargs
|
|
751
|
+
**kwargs,
|
|
686
752
|
)
|
|
687
753
|
|
|
688
754
|
|
|
689
|
-
class EntityModelAbstract(
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
755
|
+
class EntityModelAbstract(
|
|
756
|
+
MP_Node,
|
|
757
|
+
SlugNameMixIn,
|
|
758
|
+
CreateUpdateMixIn,
|
|
759
|
+
ContactInfoMixIn,
|
|
760
|
+
IOMixIn,
|
|
761
|
+
LoggingMixIn,
|
|
762
|
+
EntityModelFiscalPeriodMixIn,
|
|
763
|
+
EntityModelClosingEntryMixIn,
|
|
764
|
+
):
|
|
697
765
|
"""
|
|
698
766
|
The base implementation of the EntityModel. The EntityModel represents the Company, Corporation, Legal Entity,
|
|
699
767
|
Enterprise or Person that engage and operate as a business. The base model inherit from the Materialized Path Node
|
|
@@ -765,37 +833,46 @@ class EntityModelAbstract(MP_Node,
|
|
|
765
833
|
|
|
766
834
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
767
835
|
name = models.CharField(max_length=150, verbose_name=_('Entity Name'))
|
|
768
|
-
default_coa = models.OneToOneField(
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
836
|
+
default_coa = models.OneToOneField(
|
|
837
|
+
'django_ledger.ChartOfAccountModel',
|
|
838
|
+
verbose_name=_('Default Chart of Accounts'),
|
|
839
|
+
blank=True,
|
|
840
|
+
null=True,
|
|
841
|
+
on_delete=models.PROTECT,
|
|
842
|
+
)
|
|
843
|
+
admin = models.ForeignKey(
|
|
844
|
+
UserModel,
|
|
845
|
+
on_delete=models.CASCADE,
|
|
846
|
+
related_name='admin_of',
|
|
847
|
+
verbose_name=_('Admin'),
|
|
848
|
+
)
|
|
849
|
+
managers = models.ManyToManyField(
|
|
850
|
+
UserModel,
|
|
851
|
+
through='EntityManagementModel',
|
|
852
|
+
related_name='managed_by',
|
|
853
|
+
verbose_name=_('Managers'),
|
|
854
|
+
)
|
|
781
855
|
|
|
782
856
|
hidden = models.BooleanField(default=False)
|
|
783
|
-
accrual_method = models.BooleanField(
|
|
784
|
-
|
|
785
|
-
|
|
857
|
+
accrual_method = models.BooleanField(
|
|
858
|
+
default=False, verbose_name=_('Use Accrual Method')
|
|
859
|
+
)
|
|
860
|
+
fy_start_month = models.IntegerField(
|
|
861
|
+
choices=FY_MONTHS, default=1, verbose_name=_('Fiscal Year Start')
|
|
862
|
+
)
|
|
863
|
+
last_closing_date = models.DateField(
|
|
864
|
+
null=True, blank=True, verbose_name=_('Last Closing Entry Date')
|
|
865
|
+
)
|
|
786
866
|
picture = models.ImageField(blank=True, null=True)
|
|
787
867
|
meta = models.JSONField(default=dict, null=True, blank=True)
|
|
788
868
|
objects = EntityModelManager.from_queryset(queryset_class=EntityModelQuerySet)()
|
|
789
869
|
|
|
790
|
-
|
|
791
870
|
class Meta:
|
|
792
871
|
abstract = True
|
|
793
872
|
ordering = ['-created']
|
|
794
873
|
verbose_name = _('Entity')
|
|
795
874
|
verbose_name_plural = _('Entities')
|
|
796
|
-
indexes = [
|
|
797
|
-
models.Index(fields=['admin'])
|
|
798
|
-
]
|
|
875
|
+
indexes = [models.Index(fields=['admin'])]
|
|
799
876
|
|
|
800
877
|
def __str__(self):
|
|
801
878
|
return f'EntityModel {self.slug}: {self.name}'
|
|
@@ -817,12 +894,14 @@ class EntityModelAbstract(MP_Node,
|
|
|
817
894
|
|
|
818
895
|
# ## ENTITY CREATION ###
|
|
819
896
|
@classmethod
|
|
820
|
-
def create_entity(
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
897
|
+
def create_entity(
|
|
898
|
+
cls,
|
|
899
|
+
name: str,
|
|
900
|
+
use_accrual_method: bool,
|
|
901
|
+
admin: UserModel,
|
|
902
|
+
fy_start_month: int,
|
|
903
|
+
parent_entity=None,
|
|
904
|
+
):
|
|
826
905
|
"""
|
|
827
906
|
Convenience Method to Create a new Entity Model. This is the preferred method to create new Entities in order
|
|
828
907
|
to properly handle potential parent/child relationships between EntityModels.
|
|
@@ -849,7 +928,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
849
928
|
name=name,
|
|
850
929
|
accrual_method=use_accrual_method,
|
|
851
930
|
fy_start_month=fy_start_month,
|
|
852
|
-
admin=admin
|
|
931
|
+
admin=admin,
|
|
853
932
|
)
|
|
854
933
|
entity_model.clean()
|
|
855
934
|
entity_model = cls.add_root(instance=entity_model)
|
|
@@ -857,22 +936,28 @@ class EntityModelAbstract(MP_Node,
|
|
|
857
936
|
if isinstance(parent_entity, str):
|
|
858
937
|
# get by slug...
|
|
859
938
|
try:
|
|
860
|
-
parent_entity_model = EntityModel.objects.get(
|
|
939
|
+
parent_entity_model = EntityModel.objects.get(
|
|
940
|
+
slug__exact=parent_entity, admin=admin
|
|
941
|
+
)
|
|
861
942
|
except ObjectDoesNotExist:
|
|
862
943
|
raise EntityModelValidationError(
|
|
863
944
|
message=_(
|
|
864
945
|
f'Invalid Parent Entity. '
|
|
865
|
-
f'Entity with slug {parent_entity} is not administered by {admin.username}'
|
|
946
|
+
f'Entity with slug {parent_entity} is not administered by {admin.username}'
|
|
947
|
+
)
|
|
866
948
|
)
|
|
867
949
|
elif isinstance(parent_entity, UUID):
|
|
868
950
|
# get by uuid...
|
|
869
951
|
try:
|
|
870
|
-
parent_entity_model = EntityModel.objects.get(
|
|
952
|
+
parent_entity_model = EntityModel.objects.get(
|
|
953
|
+
uuid__exact=parent_entity, admin=admin
|
|
954
|
+
)
|
|
871
955
|
except ObjectDoesNotExist:
|
|
872
956
|
raise EntityModelValidationError(
|
|
873
957
|
message=_(
|
|
874
958
|
f'Invalid Parent Entity. '
|
|
875
|
-
f'Entity with UUID {parent_entity} is not administered by {admin.username}'
|
|
959
|
+
f'Entity with UUID {parent_entity} is not administered by {admin.username}'
|
|
960
|
+
)
|
|
876
961
|
)
|
|
877
962
|
elif isinstance(parent_entity, cls):
|
|
878
963
|
# EntityModel instance provided...
|
|
@@ -880,7 +965,8 @@ class EntityModelAbstract(MP_Node,
|
|
|
880
965
|
raise EntityModelValidationError(
|
|
881
966
|
message=_(
|
|
882
967
|
f'Invalid Parent Entity. '
|
|
883
|
-
f'Entity {parent_entity} is not administered by {admin.username}'
|
|
968
|
+
f'Entity {parent_entity} is not administered by {admin.username}'
|
|
969
|
+
)
|
|
884
970
|
)
|
|
885
971
|
parent_entity_model = parent_entity
|
|
886
972
|
else:
|
|
@@ -907,15 +993,18 @@ class EntityModelAbstract(MP_Node,
|
|
|
907
993
|
return user_model.id == self.admin_id
|
|
908
994
|
|
|
909
995
|
# #### LEDGER MANAGEMENT....
|
|
910
|
-
def create_ledger(
|
|
996
|
+
def create_ledger(
|
|
997
|
+
self,
|
|
998
|
+
name: str,
|
|
999
|
+
ledger_xid: Optional[str] = None,
|
|
1000
|
+
posted: bool = False,
|
|
1001
|
+
commit: bool = True,
|
|
1002
|
+
):
|
|
911
1003
|
if commit:
|
|
912
|
-
return self.ledgermodel_set.create(
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
name=name,
|
|
917
|
-
ledger_xid=ledger_xid
|
|
918
|
-
)
|
|
1004
|
+
return self.ledgermodel_set.create(
|
|
1005
|
+
name=name, ledger_xid=ledger_xid, posted=posted
|
|
1006
|
+
)
|
|
1007
|
+
return LedgerModel(entity=self, posted=posted, name=name, ledger_xid=ledger_xid)
|
|
919
1008
|
|
|
920
1009
|
# #### SLUG GENERATION ###
|
|
921
1010
|
@staticmethod
|
|
@@ -937,10 +1026,12 @@ class EntityModelAbstract(MP_Node,
|
|
|
937
1026
|
entity_slug = f'{slug}-{suffix}'
|
|
938
1027
|
return entity_slug
|
|
939
1028
|
|
|
940
|
-
def generate_slug(
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1029
|
+
def generate_slug(
|
|
1030
|
+
self,
|
|
1031
|
+
commit: bool = False,
|
|
1032
|
+
raise_exception: bool = True,
|
|
1033
|
+
force_update: bool = False,
|
|
1034
|
+
) -> str:
|
|
944
1035
|
"""
|
|
945
1036
|
Convenience method to create the EntityModel slug.
|
|
946
1037
|
|
|
@@ -958,16 +1049,15 @@ class EntityModelAbstract(MP_Node,
|
|
|
958
1049
|
if not force_update and self.slug:
|
|
959
1050
|
if raise_exception:
|
|
960
1051
|
raise ValidationError(
|
|
961
|
-
message=_(
|
|
1052
|
+
message=_(
|
|
1053
|
+
f'Cannot replace existing slug {self.slug}. Use force_update=True if needed.'
|
|
1054
|
+
)
|
|
962
1055
|
)
|
|
963
1056
|
|
|
964
1057
|
self.slug = self.generate_slug_from_name(self.name)
|
|
965
1058
|
|
|
966
1059
|
if commit:
|
|
967
|
-
self.save(update_fields=[
|
|
968
|
-
'slug',
|
|
969
|
-
'updated'
|
|
970
|
-
])
|
|
1060
|
+
self.save(update_fields=['slug', 'updated'])
|
|
971
1061
|
return self.slug
|
|
972
1062
|
|
|
973
1063
|
# #### CHART OF ACCOUNTS ####
|
|
@@ -982,7 +1072,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
982
1072
|
"""
|
|
983
1073
|
return self.default_coa_id is not None
|
|
984
1074
|
|
|
985
|
-
def get_default_coa(
|
|
1075
|
+
def get_default_coa(
|
|
1076
|
+
self, raise_exception: bool = True
|
|
1077
|
+
) -> Optional[ChartOfAccountModel]:
|
|
986
1078
|
"""
|
|
987
1079
|
Fetches the EntityModel default Chart of Account.
|
|
988
1080
|
|
|
@@ -999,11 +1091,14 @@ class EntityModelAbstract(MP_Node,
|
|
|
999
1091
|
|
|
1000
1092
|
if not self.default_coa_id:
|
|
1001
1093
|
if raise_exception:
|
|
1002
|
-
raise EntityModelValidationError(
|
|
1094
|
+
raise EntityModelValidationError(
|
|
1095
|
+
f'EntityModel {self.slug} does not have a default CoA'
|
|
1096
|
+
)
|
|
1003
1097
|
return self.default_coa
|
|
1004
1098
|
|
|
1005
|
-
def set_default_coa(
|
|
1006
|
-
|
|
1099
|
+
def set_default_coa(
|
|
1100
|
+
self, coa_model: Optional[Union[ChartOfAccountModel, str]], commit: bool = False
|
|
1101
|
+
):
|
|
1007
1102
|
# if str, will look up CoA Model by slug...
|
|
1008
1103
|
if isinstance(coa_model, str):
|
|
1009
1104
|
coa_model = self.chartofaccountmodel_set.get(slug=coa_model)
|
|
@@ -1012,15 +1107,14 @@ class EntityModelAbstract(MP_Node,
|
|
|
1012
1107
|
|
|
1013
1108
|
self.default_coa = coa_model
|
|
1014
1109
|
if commit:
|
|
1015
|
-
self.save(update_fields=[
|
|
1016
|
-
'default_coa',
|
|
1017
|
-
'updated'
|
|
1018
|
-
])
|
|
1110
|
+
self.save(update_fields=['default_coa', 'updated'])
|
|
1019
1111
|
|
|
1020
|
-
def create_chart_of_accounts(
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1112
|
+
def create_chart_of_accounts(
|
|
1113
|
+
self,
|
|
1114
|
+
assign_as_default: bool = False,
|
|
1115
|
+
coa_name: Optional[str] = None,
|
|
1116
|
+
commit: bool = False,
|
|
1117
|
+
) -> ChartOfAccountModel:
|
|
1024
1118
|
"""
|
|
1025
1119
|
Creates a Chart of Accounts for the Entity Model and optionally assign it as the default Chart of Accounts.
|
|
1026
1120
|
EntityModel must have a default Chart of Accounts before being able to transact.
|
|
@@ -1045,10 +1139,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
1045
1139
|
if not coa_name:
|
|
1046
1140
|
coa_name = 'Default CoA'
|
|
1047
1141
|
|
|
1048
|
-
chart_of_accounts = ChartOfAccountModel(
|
|
1049
|
-
name=coa_name,
|
|
1050
|
-
entity=self
|
|
1051
|
-
)
|
|
1142
|
+
chart_of_accounts = ChartOfAccountModel(name=coa_name, entity=self)
|
|
1052
1143
|
|
|
1053
1144
|
chart_of_accounts.clean()
|
|
1054
1145
|
chart_of_accounts.save()
|
|
@@ -1057,18 +1148,17 @@ class EntityModelAbstract(MP_Node,
|
|
|
1057
1148
|
if assign_as_default:
|
|
1058
1149
|
self.default_coa = chart_of_accounts
|
|
1059
1150
|
if commit:
|
|
1060
|
-
self.save(update_fields=[
|
|
1061
|
-
'default_coa',
|
|
1062
|
-
'updated'
|
|
1063
|
-
])
|
|
1151
|
+
self.save(update_fields=['default_coa', 'updated'])
|
|
1064
1152
|
return chart_of_accounts
|
|
1065
1153
|
|
|
1066
|
-
def populate_default_coa(
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1154
|
+
def populate_default_coa(
|
|
1155
|
+
self,
|
|
1156
|
+
activate_accounts: bool = False,
|
|
1157
|
+
force: bool = False,
|
|
1158
|
+
ignore_if_default_coa: bool = True,
|
|
1159
|
+
coa_model: Optional[ChartOfAccountModel] = None,
|
|
1160
|
+
commit: bool = True,
|
|
1161
|
+
):
|
|
1072
1162
|
"""
|
|
1073
1163
|
Populates the EntityModel default CoA with the default Chart of Account list provided by Django Ledger or user
|
|
1074
1164
|
defined. See DJANGO_LEDGER_DEFAULT_COA setting.
|
|
@@ -1113,13 +1203,18 @@ class EntityModelAbstract(MP_Node,
|
|
|
1113
1203
|
balance_type=a['balance_type'],
|
|
1114
1204
|
active=activate_accounts,
|
|
1115
1205
|
coa_model=coa_model,
|
|
1116
|
-
)
|
|
1206
|
+
)
|
|
1207
|
+
for a in v
|
|
1208
|
+
]
|
|
1209
|
+
for k, v in CHART_OF_ACCOUNTS_ROOT_MAP.items()
|
|
1117
1210
|
}
|
|
1118
1211
|
|
|
1119
1212
|
for root_acc, acc_model_list in root_maps.items():
|
|
1120
1213
|
roles_set = set(account_model.role for account_model in acc_model_list)
|
|
1121
1214
|
for i, account_model in enumerate(acc_model_list):
|
|
1122
|
-
account_model.role_default =
|
|
1215
|
+
account_model.role_default = (
|
|
1216
|
+
True if account_model.role in roles_set else False
|
|
1217
|
+
)
|
|
1123
1218
|
|
|
1124
1219
|
try:
|
|
1125
1220
|
roles_set.remove(account_model.role)
|
|
@@ -1127,7 +1222,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
1127
1222
|
pass
|
|
1128
1223
|
|
|
1129
1224
|
account_model.clean()
|
|
1130
|
-
coa_model.insert_account(
|
|
1225
|
+
coa_model.insert_account(
|
|
1226
|
+
account_model, root_account_qs=root_account_qs
|
|
1227
|
+
)
|
|
1131
1228
|
|
|
1132
1229
|
else:
|
|
1133
1230
|
if not ignore_if_default_coa:
|
|
@@ -1156,9 +1253,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
1156
1253
|
return coa_model_qs
|
|
1157
1254
|
|
|
1158
1255
|
# Model Validators....
|
|
1159
|
-
def validate_chart_of_accounts_for_entity(
|
|
1160
|
-
|
|
1161
|
-
|
|
1256
|
+
def validate_chart_of_accounts_for_entity(
|
|
1257
|
+
self, coa_model: ChartOfAccountModel, raise_exception: bool = True
|
|
1258
|
+
) -> bool:
|
|
1162
1259
|
"""
|
|
1163
1260
|
Validates the CoA Model against the EntityModel instance.
|
|
1164
1261
|
|
|
@@ -1178,13 +1275,16 @@ class EntityModelAbstract(MP_Node,
|
|
|
1178
1275
|
return True
|
|
1179
1276
|
if raise_exception:
|
|
1180
1277
|
raise EntityModelValidationError(
|
|
1181
|
-
f'Invalid ChartOfAccounts model {coa_model.slug} for EntityModel {self.slug}'
|
|
1278
|
+
f'Invalid ChartOfAccounts model {coa_model.slug} for EntityModel {self.slug}'
|
|
1279
|
+
)
|
|
1182
1280
|
return False
|
|
1183
1281
|
|
|
1184
|
-
def validate_account_model_for_coa(
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1282
|
+
def validate_account_model_for_coa(
|
|
1283
|
+
self,
|
|
1284
|
+
account_model: AccountModel,
|
|
1285
|
+
coa_model: ChartOfAccountModel,
|
|
1286
|
+
raise_exception: bool = True,
|
|
1287
|
+
) -> bool:
|
|
1188
1288
|
"""
|
|
1189
1289
|
Validates that the AccountModel provided belongs to the CoA Model provided.
|
|
1190
1290
|
|
|
@@ -1202,7 +1302,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
1202
1302
|
bool
|
|
1203
1303
|
True if valid, else False.
|
|
1204
1304
|
"""
|
|
1205
|
-
valid = self.validate_chart_of_accounts_for_entity(
|
|
1305
|
+
valid = self.validate_chart_of_accounts_for_entity(
|
|
1306
|
+
coa_model, raise_exception=raise_exception
|
|
1307
|
+
)
|
|
1206
1308
|
if not valid:
|
|
1207
1309
|
return valid
|
|
1208
1310
|
if valid and account_model.coa_model_id == coa_model.uuid:
|
|
@@ -1216,17 +1318,23 @@ class EntityModelAbstract(MP_Node,
|
|
|
1216
1318
|
@staticmethod
|
|
1217
1319
|
def validate_account_model_for_role(account_model: AccountModel, role: str):
|
|
1218
1320
|
if account_model.role != role:
|
|
1219
|
-
raise EntityModelValidationError(
|
|
1321
|
+
raise EntityModelValidationError(
|
|
1322
|
+
f'Invalid account role: {account_model.role}, expected {role}'
|
|
1323
|
+
)
|
|
1220
1324
|
|
|
1221
|
-
def validate_ledger_model_for_entity(
|
|
1325
|
+
def validate_ledger_model_for_entity(
|
|
1326
|
+
self, ledger_model: Union[LedgerModel, UUID, str]
|
|
1327
|
+
):
|
|
1222
1328
|
if ledger_model.entity_id != self.uuid:
|
|
1223
|
-
raise EntityModelValidationError(
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
order_by: Optional[Tuple[str]] = ('code',),
|
|
1227
|
-
active: bool = True) -> Tuple[
|
|
1228
|
-
ChartOfAccountModelQuerySet, Dict[ChartOfAccountModel, AccountModelQuerySet]]:
|
|
1329
|
+
raise EntityModelValidationError(
|
|
1330
|
+
f'Invalid LedgerModel {ledger_model.uuid} for entity {self.slug}'
|
|
1331
|
+
)
|
|
1229
1332
|
|
|
1333
|
+
def get_all_coa_accounts(
|
|
1334
|
+
self, order_by: Optional[Tuple[str]] = ('code',), active: bool = True
|
|
1335
|
+
) -> Tuple[
|
|
1336
|
+
ChartOfAccountModelQuerySet, Dict[ChartOfAccountModel, AccountModelQuerySet]
|
|
1337
|
+
]:
|
|
1230
1338
|
"""
|
|
1231
1339
|
Fetches all the AccountModels associated with the EntityModel grouped by ChartOfAccountModel.
|
|
1232
1340
|
|
|
@@ -1243,17 +1351,23 @@ class EntityModelAbstract(MP_Node,
|
|
|
1243
1351
|
The ChartOfAccountModelQuerySet and a grouping of AccountModels by ChartOfAccountModel as keys.
|
|
1244
1352
|
"""
|
|
1245
1353
|
|
|
1246
|
-
account_model_qs =
|
|
1247
|
-
entity_id=self.uuid
|
|
1248
|
-
|
|
1354
|
+
account_model_qs = (
|
|
1355
|
+
ChartOfAccountModel.objects.filter(entity_id=self.uuid)
|
|
1356
|
+
.select_related('entity')
|
|
1357
|
+
.prefetch_related('accountmodel_set')
|
|
1358
|
+
)
|
|
1249
1359
|
|
|
1250
1360
|
return account_model_qs, {
|
|
1251
|
-
coa_model: coa_model.accountmodel_set.filter(active=active).order_by(
|
|
1252
|
-
|
|
1361
|
+
coa_model: coa_model.accountmodel_set.filter(active=active).order_by(
|
|
1362
|
+
*order_by
|
|
1363
|
+
)
|
|
1364
|
+
for coa_model in account_model_qs
|
|
1253
1365
|
}
|
|
1254
1366
|
|
|
1255
1367
|
# ##### ACCOUNT MANAGEMENT ######
|
|
1256
|
-
def get_all_accounts(
|
|
1368
|
+
def get_all_accounts(
|
|
1369
|
+
self, active: bool = True, order_by: Optional[Tuple[str]] = ('code',)
|
|
1370
|
+
) -> AccountModelQuerySet:
|
|
1257
1371
|
"""
|
|
1258
1372
|
Fetches all AccountModelQuerySet associated with the EntityModel.
|
|
1259
1373
|
|
|
@@ -1279,13 +1393,14 @@ class EntityModelAbstract(MP_Node,
|
|
|
1279
1393
|
account_model_qs = account_model_qs.order_by(*order_by)
|
|
1280
1394
|
return account_model_qs
|
|
1281
1395
|
|
|
1282
|
-
def get_coa_accounts(
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1396
|
+
def get_coa_accounts(
|
|
1397
|
+
self,
|
|
1398
|
+
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
1399
|
+
active: bool = True,
|
|
1400
|
+
locked: bool = False,
|
|
1401
|
+
order_by: Optional[Tuple] = ('code',),
|
|
1402
|
+
return_coa_model: bool = False,
|
|
1403
|
+
) -> Union[AccountModelQuerySet, Tuple[ChartOfAccountModel, AccountModelQuerySet]]:
|
|
1289
1404
|
"""
|
|
1290
1405
|
Fetches the AccountModelQuerySet for a specific ChartOfAccountModel.
|
|
1291
1406
|
|
|
@@ -1309,9 +1424,13 @@ class EntityModelAbstract(MP_Node,
|
|
|
1309
1424
|
if not coa_model:
|
|
1310
1425
|
coa_model = self.default_coa
|
|
1311
1426
|
elif isinstance(coa_model, UUID):
|
|
1312
|
-
coa_model = self.chartofaccountmodel_set.select_related('entity').get(
|
|
1427
|
+
coa_model = self.chartofaccountmodel_set.select_related('entity').get(
|
|
1428
|
+
uuid__exact=coa_model
|
|
1429
|
+
)
|
|
1313
1430
|
elif isinstance(coa_model, str):
|
|
1314
|
-
coa_model = self.chartofaccountmodel_set.select_related('entity').get(
|
|
1431
|
+
coa_model = self.chartofaccountmodel_set.select_related('entity').get(
|
|
1432
|
+
slug__exact=coa_model
|
|
1433
|
+
)
|
|
1315
1434
|
elif isinstance(coa_model, ChartOfAccountModel):
|
|
1316
1435
|
self.validate_chart_of_accounts_for_entity(coa_model=coa_model)
|
|
1317
1436
|
else:
|
|
@@ -1319,7 +1438,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
1319
1438
|
f'CoA Model {coa_model} must be an instance of ChartOfAccountModel, UUID, str or None.'
|
|
1320
1439
|
)
|
|
1321
1440
|
|
|
1322
|
-
account_model_qs = coa_model.accountmodel_set.select_related(
|
|
1441
|
+
account_model_qs = coa_model.accountmodel_set.select_related(
|
|
1442
|
+
'coa_model', 'coa_model__entity'
|
|
1443
|
+
).not_coa_root()
|
|
1323
1444
|
|
|
1324
1445
|
if active:
|
|
1325
1446
|
account_model_qs = account_model_qs.active()
|
|
@@ -1334,10 +1455,12 @@ class EntityModelAbstract(MP_Node,
|
|
|
1334
1455
|
return coa_model, account_model_qs
|
|
1335
1456
|
return account_model_qs
|
|
1336
1457
|
|
|
1337
|
-
def get_default_coa_accounts(
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1458
|
+
def get_default_coa_accounts(
|
|
1459
|
+
self,
|
|
1460
|
+
active: bool = True,
|
|
1461
|
+
order_by: Optional[Tuple[str]] = ('code',),
|
|
1462
|
+
raise_exception: bool = True,
|
|
1463
|
+
) -> Optional[AccountModelQuerySet]:
|
|
1341
1464
|
"""
|
|
1342
1465
|
Fetches the default AccountModelQuerySet.
|
|
1343
1466
|
|
|
@@ -1362,10 +1485,11 @@ class EntityModelAbstract(MP_Node,
|
|
|
1362
1485
|
|
|
1363
1486
|
return self.get_coa_accounts(active=active, order_by=order_by)
|
|
1364
1487
|
|
|
1365
|
-
def get_accounts_with_codes(
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1488
|
+
def get_accounts_with_codes(
|
|
1489
|
+
self,
|
|
1490
|
+
code_list: Union[str, List[str], Set[str]],
|
|
1491
|
+
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
1492
|
+
) -> AccountModelQuerySet:
|
|
1369
1493
|
"""
|
|
1370
1494
|
Fetches the AccountModelQuerySet with provided code list.
|
|
1371
1495
|
|
|
@@ -1392,9 +1516,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
1392
1516
|
return account_model_qs.filter(code__exact=code_list)
|
|
1393
1517
|
return account_model_qs.filter(code__in=code_list)
|
|
1394
1518
|
|
|
1395
|
-
def get_default_account_for_role(
|
|
1396
|
-
|
|
1397
|
-
|
|
1519
|
+
def get_default_account_for_role(
|
|
1520
|
+
self, role: str, coa_model: Optional[ChartOfAccountModel] = None
|
|
1521
|
+
) -> AccountModel:
|
|
1398
1522
|
"""
|
|
1399
1523
|
Gets the given role default AccountModel from the provided CoA.
|
|
1400
1524
|
CoA will be validated against the EntityModel instance.
|
|
@@ -1420,14 +1544,16 @@ class EntityModelAbstract(MP_Node,
|
|
|
1420
1544
|
account_model_qs = coa_model.accountmodel_set.all().is_role_default()
|
|
1421
1545
|
return account_model_qs.get(role__exact=role)
|
|
1422
1546
|
|
|
1423
|
-
def create_account(
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1547
|
+
def create_account(
|
|
1548
|
+
self,
|
|
1549
|
+
code: str,
|
|
1550
|
+
role: str,
|
|
1551
|
+
name: str,
|
|
1552
|
+
balance_type: str,
|
|
1553
|
+
active: bool = False,
|
|
1554
|
+
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
1555
|
+
raise_exception: bool = True,
|
|
1556
|
+
) -> AccountModel:
|
|
1431
1557
|
"""
|
|
1432
1558
|
Creates a new AccountModel for the EntityModel.
|
|
1433
1559
|
|
|
@@ -1461,24 +1587,21 @@ class EntityModelAbstract(MP_Node,
|
|
|
1461
1587
|
coa_model = self.chartofaccountsmodel_set.get(slug__exact=coa_model)
|
|
1462
1588
|
elif isinstance(coa_model, ChartOfAccountModel):
|
|
1463
1589
|
self.validate_chart_of_accounts_for_entity(
|
|
1464
|
-
coa_model=coa_model,
|
|
1465
|
-
raise_exception=raise_exception
|
|
1590
|
+
coa_model=coa_model, raise_exception=raise_exception
|
|
1466
1591
|
)
|
|
1467
1592
|
else:
|
|
1468
1593
|
coa_model = self.default_coa
|
|
1469
1594
|
|
|
1470
1595
|
return coa_model.create_account(
|
|
1471
|
-
code=code,
|
|
1472
|
-
role=role,
|
|
1473
|
-
name=name,
|
|
1474
|
-
balance_type=balance_type,
|
|
1475
|
-
active=active
|
|
1596
|
+
code=code, role=role, name=name, balance_type=balance_type, active=active
|
|
1476
1597
|
)
|
|
1477
1598
|
|
|
1478
|
-
def create_account_by_kwargs(
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1599
|
+
def create_account_by_kwargs(
|
|
1600
|
+
self,
|
|
1601
|
+
account_model_kwargs: Dict,
|
|
1602
|
+
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
1603
|
+
raise_exception: bool = True,
|
|
1604
|
+
) -> Tuple[ChartOfAccountModel, AccountModel]:
|
|
1482
1605
|
"""
|
|
1483
1606
|
Creates a new AccountModel for the EntityModel by passing AccountModel KWARGS.
|
|
1484
1607
|
This is a legacy method for creating a new AccountModel for the EntityModel.
|
|
@@ -1506,8 +1629,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
1506
1629
|
coa_model = self.chartofaccountsmodel_set.get(slug__exact=coa_model)
|
|
1507
1630
|
elif isinstance(coa_model, ChartOfAccountModel):
|
|
1508
1631
|
self.validate_chart_of_accounts_for_entity(
|
|
1509
|
-
coa_model=coa_model,
|
|
1510
|
-
raise_exception=raise_exception
|
|
1632
|
+
coa_model=coa_model, raise_exception=raise_exception
|
|
1511
1633
|
)
|
|
1512
1634
|
else:
|
|
1513
1635
|
coa_model = self.default_coa
|
|
@@ -1516,16 +1638,11 @@ class EntityModelAbstract(MP_Node,
|
|
|
1516
1638
|
# account_model.clean()
|
|
1517
1639
|
return coa_model, coa_model.create_account(**account_model_kwargs)
|
|
1518
1640
|
|
|
1519
|
-
def get_account_balance(
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
**kwargs):
|
|
1523
|
-
|
|
1641
|
+
def get_account_balance(
|
|
1642
|
+
self, account_codes: List[str], to_date: Union[datetime, date, str], **kwargs
|
|
1643
|
+
):
|
|
1524
1644
|
io_context = self.digest(
|
|
1525
|
-
entity_model=self.slug,
|
|
1526
|
-
accounts=account_codes,
|
|
1527
|
-
to_date=to_date,
|
|
1528
|
-
**kwargs
|
|
1645
|
+
entity_model=self.slug, accounts=account_codes, to_date=to_date, **kwargs
|
|
1529
1646
|
)
|
|
1530
1647
|
|
|
1531
1648
|
return io_context
|
|
@@ -1538,7 +1655,6 @@ class EntityModelAbstract(MP_Node,
|
|
|
1538
1655
|
|
|
1539
1656
|
# ### JOURNAL ENTRY MANAGEMENT ####
|
|
1540
1657
|
def get_journal_entries(self, ledger_model: LedgerModel, posted: bool = True):
|
|
1541
|
-
|
|
1542
1658
|
if ledger_model:
|
|
1543
1659
|
self.validate_ledger_model_for_entity(ledger_model)
|
|
1544
1660
|
qs = ledger_model.journal_entries.all()
|
|
@@ -1547,7 +1663,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
1547
1663
|
return qs
|
|
1548
1664
|
|
|
1549
1665
|
JournalEntryModel = lazy_loader.get_journal_entry_model()
|
|
1550
|
-
qs = JournalEntryModel.objects.for_entity(
|
|
1666
|
+
qs = JournalEntryModel.objects.for_entity(entity_model=self)
|
|
1551
1667
|
if posted:
|
|
1552
1668
|
return qs.posted()
|
|
1553
1669
|
return qs
|
|
@@ -1580,7 +1696,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
1580
1696
|
vendor_model_qs = self.get_vendors()
|
|
1581
1697
|
return vendor_model_qs.get(uuid__exact=vendor_uuid)
|
|
1582
1698
|
|
|
1583
|
-
def create_vendor(
|
|
1699
|
+
def create_vendor(
|
|
1700
|
+
self, vendor_model_kwargs: Dict, commit: bool = True
|
|
1701
|
+
) -> VendorModel:
|
|
1584
1702
|
"""
|
|
1585
1703
|
Creates a new VendorModel associated with the EntityModel instance.
|
|
1586
1704
|
|
|
@@ -1631,9 +1749,13 @@ class EntityModelAbstract(MP_Node,
|
|
|
1631
1749
|
|
|
1632
1750
|
def validate_customer(self, customer_model: CustomerModel):
|
|
1633
1751
|
if customer_model.entity_model_id != self.uuid:
|
|
1634
|
-
raise EntityModelValidationError(
|
|
1752
|
+
raise EntityModelValidationError(
|
|
1753
|
+
f'Invalid CustomerModel {self.uuid} for EntityModel {self.uuid}...'
|
|
1754
|
+
)
|
|
1635
1755
|
|
|
1636
|
-
def create_customer(
|
|
1756
|
+
def create_customer(
|
|
1757
|
+
self, customer_model_kwargs: Dict, commit: bool = True
|
|
1758
|
+
) -> CustomerModel:
|
|
1637
1759
|
"""
|
|
1638
1760
|
Creates a new CustomerModel associated with the EntityModel instance.
|
|
1639
1761
|
|
|
@@ -1654,6 +1776,11 @@ class EntityModelAbstract(MP_Node,
|
|
|
1654
1776
|
customer_model.save()
|
|
1655
1777
|
return customer_model
|
|
1656
1778
|
|
|
1779
|
+
# ### RECEIPT MANAGEMENT ####
|
|
1780
|
+
def get_receipts(self):
|
|
1781
|
+
ReceiptModel = lazy_loader.get_receipt_model()
|
|
1782
|
+
return ReceiptModel.objects.for_entity(entity_model=self)
|
|
1783
|
+
|
|
1657
1784
|
# ### BILL MANAGEMENT ####
|
|
1658
1785
|
def get_bills(self):
|
|
1659
1786
|
"""
|
|
@@ -1668,18 +1795,20 @@ class EntityModelAbstract(MP_Node,
|
|
|
1668
1795
|
ledger__entity__uuid__exact=self.uuid
|
|
1669
1796
|
).select_related('ledger', 'ledger__entity', 'vendor')
|
|
1670
1797
|
|
|
1671
|
-
def create_bill(
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1798
|
+
def create_bill(
|
|
1799
|
+
self,
|
|
1800
|
+
vendor_model: Union[VendorModel, UUID, str],
|
|
1801
|
+
terms: str,
|
|
1802
|
+
date_draft: Optional[Union[date, datetime]] = None,
|
|
1803
|
+
xref: Optional[str] = None,
|
|
1804
|
+
cash_account: Optional[AccountModel] = None,
|
|
1805
|
+
prepaid_account: Optional[AccountModel] = None,
|
|
1806
|
+
payable_account: Optional[AccountModel] = None,
|
|
1807
|
+
additional_info: Optional[Dict] = None,
|
|
1808
|
+
ledger_name: Optional[str] = None,
|
|
1809
|
+
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
1810
|
+
commit: bool = True,
|
|
1811
|
+
):
|
|
1683
1812
|
"""
|
|
1684
1813
|
Creates a new BillModel for the EntityModel instance.
|
|
1685
1814
|
Bill will have DRAFT status.
|
|
@@ -1720,13 +1849,17 @@ class EntityModelAbstract(MP_Node,
|
|
|
1720
1849
|
|
|
1721
1850
|
if isinstance(vendor_model, VendorModel):
|
|
1722
1851
|
if not vendor_model.entity_model_id == self.uuid:
|
|
1723
|
-
raise EntityModelValidationError(
|
|
1852
|
+
raise EntityModelValidationError(
|
|
1853
|
+
f'VendorModel {vendor_model.uuid} belongs to a different EntityModel.'
|
|
1854
|
+
)
|
|
1724
1855
|
elif isinstance(vendor_model, UUID):
|
|
1725
1856
|
vendor_model = self.get_vendor_by_uuid(vendor_uuid=vendor_model)
|
|
1726
1857
|
elif isinstance(vendor_model, str):
|
|
1727
1858
|
vendor_model = self.get_vendor_by_number(vendor_number=vendor_model)
|
|
1728
1859
|
else:
|
|
1729
|
-
raise EntityModelValidationError(
|
|
1860
|
+
raise EntityModelValidationError(
|
|
1861
|
+
'VendorModel must be an instance of VendorModel, UUID or str.'
|
|
1862
|
+
)
|
|
1730
1863
|
|
|
1731
1864
|
account_model_qs = self.get_coa_accounts(coa_model=coa_model, active=True)
|
|
1732
1865
|
|
|
@@ -1734,8 +1867,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
1734
1867
|
roles=[
|
|
1735
1868
|
roles_module.ASSET_CA_CASH,
|
|
1736
1869
|
roles_module.ASSET_CA_PREPAID,
|
|
1737
|
-
roles_module.LIABILITY_CL_ACC_PAYABLE
|
|
1738
|
-
]
|
|
1870
|
+
roles_module.LIABILITY_CL_ACC_PAYABLE,
|
|
1871
|
+
]
|
|
1872
|
+
).is_role_default()
|
|
1739
1873
|
|
|
1740
1874
|
# evaluates the queryset...
|
|
1741
1875
|
len(account_model_qs)
|
|
@@ -1745,20 +1879,26 @@ class EntityModelAbstract(MP_Node,
|
|
|
1745
1879
|
vendor=vendor_model,
|
|
1746
1880
|
terms=terms,
|
|
1747
1881
|
additional_info=additional_info,
|
|
1748
|
-
cash_account=account_model_qs.get(role=roles_module.ASSET_CA_CASH)
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
)
|
|
1882
|
+
cash_account=account_model_qs.get(role=roles_module.ASSET_CA_CASH)
|
|
1883
|
+
if not cash_account
|
|
1884
|
+
else cash_account,
|
|
1885
|
+
prepaid_account=account_model_qs.get(role=roles_module.ASSET_CA_PREPAID)
|
|
1886
|
+
if not prepaid_account
|
|
1887
|
+
else prepaid_account,
|
|
1752
1888
|
unearned_account=account_model_qs.get(
|
|
1753
1889
|
role=roles_module.LIABILITY_CL_ACC_PAYABLE
|
|
1754
|
-
)
|
|
1890
|
+
)
|
|
1891
|
+
if not payable_account
|
|
1892
|
+
else payable_account,
|
|
1755
1893
|
)
|
|
1756
1894
|
|
|
1757
|
-
_, bill_model = bill_model.configure(
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1895
|
+
_, bill_model = bill_model.configure(
|
|
1896
|
+
entity_slug=self,
|
|
1897
|
+
ledger_name=ledger_name,
|
|
1898
|
+
date_draft=date_draft,
|
|
1899
|
+
commit=commit,
|
|
1900
|
+
commit_ledger=commit,
|
|
1901
|
+
)
|
|
1762
1902
|
|
|
1763
1903
|
return bill_model
|
|
1764
1904
|
|
|
@@ -1780,18 +1920,19 @@ class EntityModelAbstract(MP_Node,
|
|
|
1780
1920
|
ledger__entity__uuid__exact=self.uuid
|
|
1781
1921
|
).select_related('ledger', 'ledger__entity', 'customer')
|
|
1782
1922
|
|
|
1783
|
-
def create_invoice(
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1923
|
+
def create_invoice(
|
|
1924
|
+
self,
|
|
1925
|
+
customer_model: Union[VendorModel, UUID, str],
|
|
1926
|
+
terms: str,
|
|
1927
|
+
cash_account: Optional[AccountModel] = None,
|
|
1928
|
+
prepaid_account: Optional[AccountModel] = None,
|
|
1929
|
+
payable_account: Optional[AccountModel] = None,
|
|
1930
|
+
additional_info: Optional[Dict] = None,
|
|
1931
|
+
ledger_name: Optional[str] = None,
|
|
1932
|
+
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
1933
|
+
date_draft: Optional[date] = None,
|
|
1934
|
+
commit: bool = True,
|
|
1935
|
+
):
|
|
1795
1936
|
"""
|
|
1796
1937
|
Creates a new InvoiceModel for the EntityModel instance.
|
|
1797
1938
|
Invoice will have DRAFT status.
|
|
@@ -1831,21 +1972,25 @@ class EntityModelAbstract(MP_Node,
|
|
|
1831
1972
|
if isinstance(customer_model, CustomerModel):
|
|
1832
1973
|
if not customer_model.entity_model_id == self.uuid:
|
|
1833
1974
|
raise EntityModelValidationError(
|
|
1834
|
-
f'CustomerModel {customer_model.uuid} belongs to a different EntityModel.'
|
|
1975
|
+
f'CustomerModel {customer_model.uuid} belongs to a different EntityModel.'
|
|
1976
|
+
)
|
|
1835
1977
|
elif isinstance(customer_model, UUID):
|
|
1836
1978
|
customer_model = self.get_customer_by_uuid(customer_uuid=customer_model)
|
|
1837
1979
|
elif isinstance(customer_model, str):
|
|
1838
1980
|
customer_model = self.get_customer_by_number(customer_number=customer_model)
|
|
1839
1981
|
else:
|
|
1840
|
-
raise EntityModelValidationError(
|
|
1982
|
+
raise EntityModelValidationError(
|
|
1983
|
+
'CustomerModel must be an instance of CustomerModel, UUID or str.'
|
|
1984
|
+
)
|
|
1841
1985
|
|
|
1842
1986
|
account_model_qs = self.get_coa_accounts(coa_model=coa_model, active=True)
|
|
1843
1987
|
account_model_qs = account_model_qs.with_roles(
|
|
1844
1988
|
roles=[
|
|
1845
1989
|
roles_module.ASSET_CA_CASH,
|
|
1846
1990
|
roles_module.ASSET_CA_RECEIVABLES,
|
|
1847
|
-
roles_module.LIABILITY_CL_DEFERRED_REVENUE
|
|
1848
|
-
]
|
|
1991
|
+
roles_module.LIABILITY_CL_DEFERRED_REVENUE,
|
|
1992
|
+
]
|
|
1993
|
+
).is_role_default()
|
|
1849
1994
|
|
|
1850
1995
|
# evaluates the queryset...
|
|
1851
1996
|
len(account_model_qs)
|
|
@@ -1854,20 +1999,26 @@ class EntityModelAbstract(MP_Node,
|
|
|
1854
1999
|
customer=customer_model,
|
|
1855
2000
|
additional_info=additional_info,
|
|
1856
2001
|
terms=terms,
|
|
1857
|
-
cash_account=account_model_qs.get(role=roles_module.ASSET_CA_CASH)
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
)
|
|
2002
|
+
cash_account=account_model_qs.get(role=roles_module.ASSET_CA_CASH)
|
|
2003
|
+
if not cash_account
|
|
2004
|
+
else cash_account,
|
|
2005
|
+
prepaid_account=account_model_qs.get(role=roles_module.ASSET_CA_RECEIVABLES)
|
|
2006
|
+
if not prepaid_account
|
|
2007
|
+
else prepaid_account,
|
|
1861
2008
|
unearned_account=account_model_qs.get(
|
|
1862
2009
|
role=roles_module.LIABILITY_CL_DEFERRED_REVENUE
|
|
1863
|
-
)
|
|
2010
|
+
)
|
|
2011
|
+
if not payable_account
|
|
2012
|
+
else payable_account,
|
|
1864
2013
|
)
|
|
1865
2014
|
|
|
1866
|
-
_, invoice_model = invoice_model.configure(
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
2015
|
+
_, invoice_model = invoice_model.configure(
|
|
2016
|
+
entity_slug=self,
|
|
2017
|
+
ledger_name=ledger_name,
|
|
2018
|
+
commit=commit,
|
|
2019
|
+
date_draft=date_draft,
|
|
2020
|
+
commit_ledger=commit,
|
|
2021
|
+
)
|
|
1871
2022
|
|
|
1872
2023
|
return invoice_model
|
|
1873
2024
|
|
|
@@ -1882,11 +2033,13 @@ class EntityModelAbstract(MP_Node,
|
|
|
1882
2033
|
"""
|
|
1883
2034
|
return self.purchaseordermodel_set.all().select_related('entity')
|
|
1884
2035
|
|
|
1885
|
-
def create_purchase_order(
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2036
|
+
def create_purchase_order(
|
|
2037
|
+
self,
|
|
2038
|
+
po_title: Optional[str] = None,
|
|
2039
|
+
estimate_model=None,
|
|
2040
|
+
date_draft: Optional[date] = None,
|
|
2041
|
+
commit: bool = True,
|
|
2042
|
+
):
|
|
1890
2043
|
"""
|
|
1891
2044
|
Creates a new PurchaseOrderModel for the EntityModel instance.
|
|
1892
2045
|
PO will have DRAFT status.
|
|
@@ -1914,7 +2067,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
1914
2067
|
draft_date=date_draft,
|
|
1915
2068
|
estimate_model=estimate_model,
|
|
1916
2069
|
commit=commit,
|
|
1917
|
-
po_title=po_title
|
|
2070
|
+
po_title=po_title,
|
|
1918
2071
|
)
|
|
1919
2072
|
|
|
1920
2073
|
# ### ESTIMATE/CONTRACT MANAGEMENT ####
|
|
@@ -1928,12 +2081,14 @@ class EntityModelAbstract(MP_Node,
|
|
|
1928
2081
|
"""
|
|
1929
2082
|
return self.estimatemodel_set.all().select_related('entity')
|
|
1930
2083
|
|
|
1931
|
-
def create_estimate(
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2084
|
+
def create_estimate(
|
|
2085
|
+
self,
|
|
2086
|
+
estimate_title: str,
|
|
2087
|
+
contract_terms: str,
|
|
2088
|
+
customer_model: Union[CustomerModel, UUID, str],
|
|
2089
|
+
date_draft: Optional[date] = None,
|
|
2090
|
+
commit: bool = True,
|
|
2091
|
+
):
|
|
1937
2092
|
"""
|
|
1938
2093
|
Creates a new EstimateModel for the EntityModel instance.
|
|
1939
2094
|
Estimate will have DRAFT status.
|
|
@@ -1963,7 +2118,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
1963
2118
|
elif isinstance(customer_model, UUID):
|
|
1964
2119
|
customer_model = self.get_customer_by_uuid(customer_uuid=customer_model)
|
|
1965
2120
|
else:
|
|
1966
|
-
raise EntityModelValidationError(
|
|
2121
|
+
raise EntityModelValidationError(
|
|
2122
|
+
'CustomerModel must be an instance of CustomerModel, UUID or str.'
|
|
2123
|
+
)
|
|
1967
2124
|
|
|
1968
2125
|
EstimateModel = lazy_loader.get_estimate_model()
|
|
1969
2126
|
estimate_model = EstimateModel(terms=contract_terms)
|
|
@@ -1972,7 +2129,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
1972
2129
|
date_draft=date_draft,
|
|
1973
2130
|
customer_model=customer_model,
|
|
1974
2131
|
estimate_title=estimate_title,
|
|
1975
|
-
commit=commit
|
|
2132
|
+
commit=commit,
|
|
1976
2133
|
)
|
|
1977
2134
|
|
|
1978
2135
|
# ### BANK ACCOUNT MANAGEMENT ####
|
|
@@ -1994,15 +2151,16 @@ class EntityModelAbstract(MP_Node,
|
|
|
1994
2151
|
bank_account_qs = bank_account_qs.active()
|
|
1995
2152
|
return bank_account_qs
|
|
1996
2153
|
|
|
1997
|
-
def create_bank_account(
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2154
|
+
def create_bank_account(
|
|
2155
|
+
self,
|
|
2156
|
+
name: str,
|
|
2157
|
+
account_type: str,
|
|
2158
|
+
active=False,
|
|
2159
|
+
account_model: Optional[AccountModel] = None,
|
|
2160
|
+
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
2161
|
+
bank_account_model_kwargs: Optional[Dict] = None,
|
|
2162
|
+
commit: bool = True,
|
|
2163
|
+
):
|
|
2006
2164
|
"""
|
|
2007
2165
|
Create a bank account entry for the entity model with specified attributes and validation.
|
|
2008
2166
|
|
|
@@ -2042,13 +2200,14 @@ class EntityModelAbstract(MP_Node,
|
|
|
2042
2200
|
|
|
2043
2201
|
if account_type not in BankAccountModel.VALID_ACCOUNT_TYPES:
|
|
2044
2202
|
raise EntityModelValidationError(
|
|
2045
|
-
_(
|
|
2203
|
+
_(
|
|
2204
|
+
f'Invalid Account Type: choices are {BankAccountModel.VALID_ACCOUNT_TYPES}'
|
|
2205
|
+
)
|
|
2206
|
+
)
|
|
2046
2207
|
|
|
2047
2208
|
account_model_qs = self.get_coa_accounts(coa_model=coa_model, active=True)
|
|
2048
2209
|
account_model_qs = account_model_qs.with_roles(
|
|
2049
|
-
roles=[
|
|
2050
|
-
BankAccountModel.ACCOUNT_TYPE_DEFAULT_ROLE_MAPPING[account_type]
|
|
2051
|
-
]
|
|
2210
|
+
roles=[BankAccountModel.ACCOUNT_TYPE_DEFAULT_ROLE_MAPPING[account_type]]
|
|
2052
2211
|
).is_role_default()
|
|
2053
2212
|
|
|
2054
2213
|
bank_account_model = BankAccountModel(
|
|
@@ -2056,8 +2215,10 @@ class EntityModelAbstract(MP_Node,
|
|
|
2056
2215
|
entity_model=self,
|
|
2057
2216
|
account_type=account_type,
|
|
2058
2217
|
active=active,
|
|
2059
|
-
account_model=account_model_qs.get()
|
|
2060
|
-
|
|
2218
|
+
account_model=account_model_qs.get()
|
|
2219
|
+
if not account_model
|
|
2220
|
+
else account_model,
|
|
2221
|
+
**bank_account_model_kwargs,
|
|
2061
2222
|
)
|
|
2062
2223
|
|
|
2063
2224
|
bank_account_model.clean()
|
|
@@ -2066,7 +2227,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
2066
2227
|
return bank_account_model
|
|
2067
2228
|
|
|
2068
2229
|
# #### ITEM MANAGEMENT ###
|
|
2069
|
-
def validate_item_qs(
|
|
2230
|
+
def validate_item_qs(
|
|
2231
|
+
self, item_qs: ItemModelQuerySet, raise_exception: bool = True
|
|
2232
|
+
) -> bool:
|
|
2070
2233
|
"""
|
|
2071
2234
|
Validates the given ItemModelQuerySet against the EntityModel instance.
|
|
2072
2235
|
Parameters
|
|
@@ -2084,7 +2247,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
2084
2247
|
for item_model in item_qs:
|
|
2085
2248
|
if item_model.entity_id != self.uuid:
|
|
2086
2249
|
if raise_exception:
|
|
2087
|
-
raise EntityModelValidationError(
|
|
2250
|
+
raise EntityModelValidationError(
|
|
2251
|
+
f'Invalid item_qs provided for entity {self.slug}...'
|
|
2252
|
+
)
|
|
2088
2253
|
return False
|
|
2089
2254
|
return True
|
|
2090
2255
|
|
|
@@ -2098,7 +2263,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
2098
2263
|
"""
|
|
2099
2264
|
return self.unitofmeasuremodel_set.all().select_related('entity')
|
|
2100
2265
|
|
|
2101
|
-
def create_uom(
|
|
2266
|
+
def create_uom(
|
|
2267
|
+
self, name: str, unit_abbr: str, active: bool = True, commit: bool = True
|
|
2268
|
+
) -> UnitOfMeasureModel:
|
|
2102
2269
|
"""
|
|
2103
2270
|
Creates a new Unit of Measure Model associated with the EntityModel instance
|
|
2104
2271
|
|
|
@@ -2118,10 +2285,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
2118
2285
|
UnitOfMeasureModel
|
|
2119
2286
|
"""
|
|
2120
2287
|
uom_model = UnitOfMeasureModel(
|
|
2121
|
-
name=name,
|
|
2122
|
-
unit_abbr=unit_abbr,
|
|
2123
|
-
is_active=active,
|
|
2124
|
-
entity=self
|
|
2288
|
+
name=name, unit_abbr=unit_abbr, is_active=active, entity=self
|
|
2125
2289
|
)
|
|
2126
2290
|
uom_model.clean()
|
|
2127
2291
|
uom_model.clean_fields()
|
|
@@ -2150,7 +2314,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
2150
2314
|
'inventory_account',
|
|
2151
2315
|
'cogs_account',
|
|
2152
2316
|
'earnings_account',
|
|
2153
|
-
'expense_account'
|
|
2317
|
+
'expense_account',
|
|
2154
2318
|
)
|
|
2155
2319
|
if active:
|
|
2156
2320
|
return qs.active()
|
|
@@ -2174,12 +2338,14 @@ class EntityModelAbstract(MP_Node,
|
|
|
2174
2338
|
qs = self.get_items_all(active=active)
|
|
2175
2339
|
return qs.products()
|
|
2176
2340
|
|
|
2177
|
-
def create_item_product(
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2341
|
+
def create_item_product(
|
|
2342
|
+
self,
|
|
2343
|
+
name: str,
|
|
2344
|
+
item_type: str,
|
|
2345
|
+
uom_model: Union[UUID, UnitOfMeasureModel],
|
|
2346
|
+
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
2347
|
+
commit: bool = True,
|
|
2348
|
+
) -> ItemModel:
|
|
2183
2349
|
"""
|
|
2184
2350
|
Creates a new items of type PRODUCT.
|
|
2185
2351
|
|
|
@@ -2201,18 +2367,23 @@ class EntityModelAbstract(MP_Node,
|
|
|
2201
2367
|
The created Product.
|
|
2202
2368
|
"""
|
|
2203
2369
|
if isinstance(uom_model, UUID):
|
|
2204
|
-
uom_model = self.unitofmeasuremodel_set.select_related('entity').get(
|
|
2370
|
+
uom_model = self.unitofmeasuremodel_set.select_related('entity').get(
|
|
2371
|
+
uuid__exact=uom_model
|
|
2372
|
+
)
|
|
2205
2373
|
elif isinstance(uom_model, UnitOfMeasureModel):
|
|
2206
2374
|
if uom_model.entity_id != self.uuid:
|
|
2207
|
-
raise EntityModelValidationError(
|
|
2375
|
+
raise EntityModelValidationError(
|
|
2376
|
+
f'Invalid UnitOfMeasureModel for entity {self.slug}...'
|
|
2377
|
+
)
|
|
2208
2378
|
|
|
2209
2379
|
account_model_qs = self.get_coa_accounts(coa_model=coa_model, active=True)
|
|
2210
2380
|
account_model_qs = account_model_qs.with_roles(
|
|
2211
2381
|
roles=[
|
|
2212
2382
|
roles_module.ASSET_CA_INVENTORY,
|
|
2213
2383
|
roles_module.COGS,
|
|
2214
|
-
roles_module.INCOME_OPERATIONAL
|
|
2215
|
-
]
|
|
2384
|
+
roles_module.INCOME_OPERATIONAL,
|
|
2385
|
+
]
|
|
2386
|
+
).is_role_default()
|
|
2216
2387
|
|
|
2217
2388
|
# evaluates the queryset...
|
|
2218
2389
|
len(account_model_qs)
|
|
@@ -2223,9 +2394,13 @@ class EntityModelAbstract(MP_Node,
|
|
|
2223
2394
|
uom=uom_model,
|
|
2224
2395
|
item_role=ItemModel.ITEM_ROLE_PRODUCT,
|
|
2225
2396
|
item_type=item_type,
|
|
2226
|
-
inventory_account=account_model_qs.filter(
|
|
2227
|
-
|
|
2228
|
-
|
|
2397
|
+
inventory_account=account_model_qs.filter(
|
|
2398
|
+
role=roles_module.ASSET_CA_INVENTORY
|
|
2399
|
+
).get(),
|
|
2400
|
+
earnings_account=account_model_qs.filter(
|
|
2401
|
+
role=roles_module.INCOME_OPERATIONAL
|
|
2402
|
+
).get(),
|
|
2403
|
+
cogs_account=account_model_qs.filter(role=roles_module.COGS).get(),
|
|
2229
2404
|
)
|
|
2230
2405
|
product_model.clean()
|
|
2231
2406
|
product_model.clean_fields()
|
|
@@ -2251,11 +2426,13 @@ class EntityModelAbstract(MP_Node,
|
|
|
2251
2426
|
qs = self.get_items_all(active=active)
|
|
2252
2427
|
return qs.services()
|
|
2253
2428
|
|
|
2254
|
-
def create_item_service(
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2429
|
+
def create_item_service(
|
|
2430
|
+
self,
|
|
2431
|
+
name: str,
|
|
2432
|
+
uom_model: Union[UUID, UnitOfMeasureModel],
|
|
2433
|
+
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
2434
|
+
commit: bool = True,
|
|
2435
|
+
) -> ItemModel:
|
|
2259
2436
|
"""
|
|
2260
2437
|
Creates a new items of type SERVICE.
|
|
2261
2438
|
|
|
@@ -2277,17 +2454,19 @@ class EntityModelAbstract(MP_Node,
|
|
|
2277
2454
|
"""
|
|
2278
2455
|
|
|
2279
2456
|
if isinstance(uom_model, UUID):
|
|
2280
|
-
uom_model = self.unitofmeasuremodel_set.select_related('entity').get(
|
|
2457
|
+
uom_model = self.unitofmeasuremodel_set.select_related('entity').get(
|
|
2458
|
+
uuid__exact=uom_model
|
|
2459
|
+
)
|
|
2281
2460
|
elif isinstance(uom_model, UnitOfMeasureModel):
|
|
2282
2461
|
if uom_model.entity_id != self.uuid:
|
|
2283
|
-
raise EntityModelValidationError(
|
|
2462
|
+
raise EntityModelValidationError(
|
|
2463
|
+
f'Invalid UnitOfMeasureModel for entity {self.slug}...'
|
|
2464
|
+
)
|
|
2284
2465
|
|
|
2285
2466
|
account_model_qs = self.get_coa_accounts(coa_model=coa_model, active=True)
|
|
2286
2467
|
account_model_qs = account_model_qs.with_roles(
|
|
2287
|
-
roles=[
|
|
2288
|
-
|
|
2289
|
-
roles_module.INCOME_OPERATIONAL
|
|
2290
|
-
]).is_role_default()
|
|
2468
|
+
roles=[roles_module.COGS, roles_module.INCOME_OPERATIONAL]
|
|
2469
|
+
).is_role_default()
|
|
2291
2470
|
|
|
2292
2471
|
# evaluates the queryset...
|
|
2293
2472
|
len(account_model_qs)
|
|
@@ -2298,8 +2477,10 @@ class EntityModelAbstract(MP_Node,
|
|
|
2298
2477
|
uom=uom_model,
|
|
2299
2478
|
item_role=ItemModel.ITEM_ROLE_SERVICE,
|
|
2300
2479
|
item_type=ItemModel.ITEM_TYPE_LABOR,
|
|
2301
|
-
earnings_account=account_model_qs.filter(
|
|
2302
|
-
|
|
2480
|
+
earnings_account=account_model_qs.filter(
|
|
2481
|
+
role=roles_module.INCOME_OPERATIONAL
|
|
2482
|
+
).get(),
|
|
2483
|
+
cogs_account=account_model_qs.filter(role=roles_module.COGS).get(),
|
|
2303
2484
|
)
|
|
2304
2485
|
service_model.clean()
|
|
2305
2486
|
service_model.clean_fields()
|
|
@@ -2325,14 +2506,15 @@ class EntityModelAbstract(MP_Node,
|
|
|
2325
2506
|
qs = self.get_items_all(active=active)
|
|
2326
2507
|
return qs.expenses()
|
|
2327
2508
|
|
|
2328
|
-
def create_item_expense(
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2509
|
+
def create_item_expense(
|
|
2510
|
+
self,
|
|
2511
|
+
name: str,
|
|
2512
|
+
expense_type: str,
|
|
2513
|
+
uom_model: Union[UUID, UnitOfMeasureModel],
|
|
2514
|
+
expense_account: Optional[Union[UUID, AccountModel]] = None,
|
|
2515
|
+
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
2516
|
+
commit: bool = True,
|
|
2517
|
+
) -> ItemModel:
|
|
2336
2518
|
"""
|
|
2337
2519
|
Creates a new items of type EXPENSE.
|
|
2338
2520
|
|
|
@@ -2357,10 +2539,14 @@ class EntityModelAbstract(MP_Node,
|
|
|
2357
2539
|
ItemModel
|
|
2358
2540
|
"""
|
|
2359
2541
|
if isinstance(uom_model, UUID):
|
|
2360
|
-
uom_model = self.unitofmeasuremodel_set.select_related('entity').get(
|
|
2542
|
+
uom_model = self.unitofmeasuremodel_set.select_related('entity').get(
|
|
2543
|
+
uuid__exact=uom_model
|
|
2544
|
+
)
|
|
2361
2545
|
elif isinstance(uom_model, UnitOfMeasureModel):
|
|
2362
2546
|
if uom_model.entity_id != self.uuid:
|
|
2363
|
-
raise EntityModelValidationError(
|
|
2547
|
+
raise EntityModelValidationError(
|
|
2548
|
+
f'Invalid UnitOfMeasureModel for entity {self.slug}...'
|
|
2549
|
+
)
|
|
2364
2550
|
|
|
2365
2551
|
account_model_qs = self.get_coa_accounts(coa_model=coa_model, active=True)
|
|
2366
2552
|
account_model_qs = account_model_qs.with_roles(
|
|
@@ -2372,7 +2558,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
2372
2558
|
expense_account = account_model_qs.get(uuid__exact=expense_account)
|
|
2373
2559
|
elif isinstance(expense_account, AccountModel):
|
|
2374
2560
|
if expense_account.coa_model.entity_id != self.uuid:
|
|
2375
|
-
raise EntityModelValidationError(
|
|
2561
|
+
raise EntityModelValidationError(
|
|
2562
|
+
f'Invalid account for entity {self.slug}...'
|
|
2563
|
+
)
|
|
2376
2564
|
|
|
2377
2565
|
expense_item_model = ItemModel(
|
|
2378
2566
|
entity=self,
|
|
@@ -2380,7 +2568,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
2380
2568
|
uom=uom_model,
|
|
2381
2569
|
item_role=ItemModel.ITEM_ROLE_EXPENSE,
|
|
2382
2570
|
item_type=expense_type,
|
|
2383
|
-
expense_account=expense_account
|
|
2571
|
+
expense_account=expense_account,
|
|
2384
2572
|
)
|
|
2385
2573
|
expense_item_model.clean()
|
|
2386
2574
|
expense_item_model.clean_fields()
|
|
@@ -2426,13 +2614,15 @@ class EntityModelAbstract(MP_Node,
|
|
|
2426
2614
|
qs = self.get_items_all(active=active)
|
|
2427
2615
|
return qs.inventory_wip()
|
|
2428
2616
|
|
|
2429
|
-
def create_item_inventory(
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2617
|
+
def create_item_inventory(
|
|
2618
|
+
self,
|
|
2619
|
+
name: str,
|
|
2620
|
+
uom_model: Union[UUID, UnitOfMeasureModel],
|
|
2621
|
+
item_type: str,
|
|
2622
|
+
inventory_account: Optional[Union[UUID, AccountModel]] = None,
|
|
2623
|
+
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
2624
|
+
commit: bool = True,
|
|
2625
|
+
):
|
|
2436
2626
|
"""
|
|
2437
2627
|
Creates a new items of type INVENTORY.
|
|
2438
2628
|
|
|
@@ -2458,10 +2648,14 @@ class EntityModelAbstract(MP_Node,
|
|
|
2458
2648
|
ItemModel
|
|
2459
2649
|
"""
|
|
2460
2650
|
if isinstance(uom_model, UUID):
|
|
2461
|
-
uom_model = self.unitofmeasuremodel_set.select_related('entity').get(
|
|
2651
|
+
uom_model = self.unitofmeasuremodel_set.select_related('entity').get(
|
|
2652
|
+
uuid__exact=uom_model
|
|
2653
|
+
)
|
|
2462
2654
|
elif isinstance(uom_model, UnitOfMeasureModel):
|
|
2463
2655
|
if uom_model.entity_id != self.uuid:
|
|
2464
|
-
raise EntityModelValidationError(
|
|
2656
|
+
raise EntityModelValidationError(
|
|
2657
|
+
f'Invalid UnitOfMeasureModel for entity {self.slug}...'
|
|
2658
|
+
)
|
|
2465
2659
|
|
|
2466
2660
|
account_model_qs = self.get_coa_accounts(coa_model=coa_model, active=True)
|
|
2467
2661
|
account_model_qs = account_model_qs.with_roles(
|
|
@@ -2473,9 +2667,13 @@ class EntityModelAbstract(MP_Node,
|
|
|
2473
2667
|
inventory_account = account_model_qs.get(uuid__exact=inventory_account)
|
|
2474
2668
|
elif isinstance(inventory_account, AccountModel):
|
|
2475
2669
|
if inventory_account.coa_model.entity_id != self.uuid:
|
|
2476
|
-
raise EntityModelValidationError(
|
|
2670
|
+
raise EntityModelValidationError(
|
|
2671
|
+
f'Invalid account for entity {self.slug}...'
|
|
2672
|
+
)
|
|
2477
2673
|
elif inventory_account.coa_model_id != coa_model.uuid:
|
|
2478
|
-
raise EntityModelValidationError(
|
|
2674
|
+
raise EntityModelValidationError(
|
|
2675
|
+
f'Invalid account for coa {coa_model.slug}...'
|
|
2676
|
+
)
|
|
2479
2677
|
|
|
2480
2678
|
inventory_item_model = ItemModel(
|
|
2481
2679
|
name=name,
|
|
@@ -2483,7 +2681,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
2483
2681
|
entity=self,
|
|
2484
2682
|
item_type=item_type,
|
|
2485
2683
|
item_role=ItemModel.ITEM_ROLE_INVENTORY,
|
|
2486
|
-
inventory_account=inventory_account
|
|
2684
|
+
inventory_account=inventory_account,
|
|
2487
2685
|
)
|
|
2488
2686
|
inventory_item_model.clean()
|
|
2489
2687
|
inventory_item_model.clean_fields()
|
|
@@ -2523,42 +2721,49 @@ class EntityModelAbstract(MP_Node,
|
|
|
2523
2721
|
'count': i['quantity_onhand'],
|
|
2524
2722
|
'value': i['value_onhand'],
|
|
2525
2723
|
'avg_cost': i['cost_average']
|
|
2526
|
-
if i['quantity_onhand']
|
|
2527
|
-
|
|
2724
|
+
if i['quantity_onhand']
|
|
2725
|
+
else Decimal('0.00'),
|
|
2726
|
+
}
|
|
2727
|
+
for i in counted_qs
|
|
2528
2728
|
}
|
|
2529
2729
|
recorded_map = {
|
|
2530
2730
|
(i['uuid'], i['name'], i['uom__name']): {
|
|
2531
2731
|
'count': i['inventory_received'] or Decimal.from_float(0.0),
|
|
2532
2732
|
'value': i['inventory_received_value'] or Decimal.from_float(0.0),
|
|
2533
2733
|
'avg_cost': i['inventory_received_value'] / i['inventory_received']
|
|
2534
|
-
if i['inventory_received']
|
|
2535
|
-
|
|
2734
|
+
if i['inventory_received']
|
|
2735
|
+
else Decimal('0.00'),
|
|
2736
|
+
}
|
|
2737
|
+
for i in recorded_qs
|
|
2536
2738
|
}
|
|
2537
2739
|
|
|
2538
2740
|
# todo: change this to use a groupby then sum...
|
|
2539
2741
|
item_ids = list(set(list(counted_map.keys()) + list(recorded_map)))
|
|
2540
|
-
adjustment = defaultdict(
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2742
|
+
adjustment = defaultdict(
|
|
2743
|
+
lambda: {
|
|
2744
|
+
# keeps track of inventory recounts...
|
|
2745
|
+
'counted': Decimal('0.000'),
|
|
2746
|
+
'counted_value': Decimal('0.00'),
|
|
2747
|
+
'counted_avg_cost': Decimal('0.00'),
|
|
2748
|
+
# keeps track of inventory level...
|
|
2749
|
+
'recorded': Decimal('0.000'),
|
|
2750
|
+
'recorded_value': Decimal('0.00'),
|
|
2751
|
+
'recorded_avg_cost': Decimal('0.00'),
|
|
2752
|
+
# keeps track of necessary inventory adjustment...
|
|
2753
|
+
'count_diff': Decimal('0.000'),
|
|
2754
|
+
'value_diff': Decimal('0.00'),
|
|
2755
|
+
'avg_cost_diff': Decimal('0.00'),
|
|
2756
|
+
}
|
|
2757
|
+
)
|
|
2556
2758
|
|
|
2557
2759
|
for uid in item_ids:
|
|
2558
|
-
|
|
2559
2760
|
count_data = counted_map.get(uid)
|
|
2560
2761
|
if count_data:
|
|
2561
|
-
avg_cost =
|
|
2762
|
+
avg_cost = (
|
|
2763
|
+
count_data['value'] / count_data['count']
|
|
2764
|
+
if count_data['count']
|
|
2765
|
+
else Decimal('0.000')
|
|
2766
|
+
)
|
|
2562
2767
|
|
|
2563
2768
|
adjustment[uid]['counted'] = count_data['count']
|
|
2564
2769
|
adjustment[uid]['counted_value'] = count_data['value']
|
|
@@ -2571,7 +2776,11 @@ class EntityModelAbstract(MP_Node,
|
|
|
2571
2776
|
recorded_data = recorded_map.get(uid)
|
|
2572
2777
|
if recorded_data:
|
|
2573
2778
|
counted = recorded_data['count']
|
|
2574
|
-
avg_cost =
|
|
2779
|
+
avg_cost = (
|
|
2780
|
+
recorded_data['value'] / counted
|
|
2781
|
+
if recorded_data['count']
|
|
2782
|
+
else Decimal('0.000')
|
|
2783
|
+
)
|
|
2575
2784
|
|
|
2576
2785
|
adjustment[uid]['recorded'] = counted
|
|
2577
2786
|
adjustment[uid]['recorded_value'] = recorded_data['value']
|
|
@@ -2582,8 +2791,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
2582
2791
|
adjustment[uid]['avg_cost_diff'] -= avg_cost
|
|
2583
2792
|
return adjustment
|
|
2584
2793
|
|
|
2585
|
-
def update_inventory(
|
|
2586
|
-
|
|
2794
|
+
def update_inventory(
|
|
2795
|
+
self, commit: bool = False
|
|
2796
|
+
) -> Tuple[defaultdict, ItemTransactionModelQuerySet, ItemModelQuerySet]:
|
|
2587
2797
|
"""
|
|
2588
2798
|
Triggers an inventory recount with optional commitment of transaction.
|
|
2589
2799
|
|
|
@@ -2603,9 +2813,13 @@ class EntityModelAbstract(MP_Node,
|
|
|
2603
2813
|
ItemTransactionModel = lazy_loader.get_item_transaction_model()
|
|
2604
2814
|
ItemModel = lazy_loader.get_item_model()
|
|
2605
2815
|
|
|
2606
|
-
counted_qs: ItemTransactionModelQuerySet =
|
|
2816
|
+
counted_qs: ItemTransactionModelQuerySet = (
|
|
2817
|
+
ItemTransactionModel.objects.inventory_count(entity_model=self.slug)
|
|
2818
|
+
)
|
|
2607
2819
|
recorded_qs: ItemModelQuerySet = self.recorded_inventory(as_values=False)
|
|
2608
|
-
recorded_qs_values = self.recorded_inventory(
|
|
2820
|
+
recorded_qs_values = self.recorded_inventory(
|
|
2821
|
+
item_qs=recorded_qs, as_values=True
|
|
2822
|
+
)
|
|
2609
2823
|
|
|
2610
2824
|
adj = self.inventory_adjustment(counted_qs, recorded_qs_values)
|
|
2611
2825
|
|
|
@@ -2618,18 +2832,16 @@ class EntityModelAbstract(MP_Node,
|
|
|
2618
2832
|
updated_items.append(item_model)
|
|
2619
2833
|
|
|
2620
2834
|
if commit:
|
|
2621
|
-
ItemModel.objects.bulk_update(
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
'updated'
|
|
2626
|
-
])
|
|
2835
|
+
ItemModel.objects.bulk_update(
|
|
2836
|
+
updated_items,
|
|
2837
|
+
fields=['inventory_received', 'inventory_received_value', 'updated'],
|
|
2838
|
+
)
|
|
2627
2839
|
|
|
2628
2840
|
return adj, counted_qs, recorded_qs
|
|
2629
2841
|
|
|
2630
|
-
def recorded_inventory(
|
|
2631
|
-
|
|
2632
|
-
|
|
2842
|
+
def recorded_inventory(
|
|
2843
|
+
self, item_qs: Optional[ItemModelQuerySet] = None, as_values: bool = True
|
|
2844
|
+
) -> ItemModelQuerySet:
|
|
2633
2845
|
"""
|
|
2634
2846
|
Recorded inventory on the books marked as received. PurchaseOrderModel drives the ordering and receiving of
|
|
2635
2847
|
inventory. Once inventory is marked as "received" recorded inventory of each item is updated by calling
|
|
@@ -2658,21 +2870,27 @@ class EntityModelAbstract(MP_Node,
|
|
|
2658
2870
|
recorded_qs = item_qs
|
|
2659
2871
|
if as_values:
|
|
2660
2872
|
return recorded_qs.values(
|
|
2661
|
-
'uuid',
|
|
2873
|
+
'uuid',
|
|
2874
|
+
'name',
|
|
2875
|
+
'uom__name',
|
|
2876
|
+
'inventory_received',
|
|
2877
|
+
'inventory_received_value',
|
|
2878
|
+
)
|
|
2662
2879
|
return recorded_qs
|
|
2663
2880
|
|
|
2664
2881
|
# COMMON TRANSACTIONS...
|
|
2665
|
-
def deposit_capital(
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2882
|
+
def deposit_capital(
|
|
2883
|
+
self,
|
|
2884
|
+
amount: Union[Decimal, float],
|
|
2885
|
+
cash_account: Optional[Union[AccountModel, BankAccountModel]] = None,
|
|
2886
|
+
capital_account: Optional[AccountModel] = None,
|
|
2887
|
+
description: Optional[str] = None,
|
|
2888
|
+
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
|
|
2889
|
+
ledger_model: Optional[Union[LedgerModel, UUID]] = None,
|
|
2890
|
+
ledger_posted: bool = False,
|
|
2891
|
+
je_timestamp: Optional[Union[datetime, date, str]] = None,
|
|
2892
|
+
je_posted: bool = False,
|
|
2893
|
+
):
|
|
2676
2894
|
if coa_model:
|
|
2677
2895
|
self.validate_chart_of_accounts_for_entity(coa_model)
|
|
2678
2896
|
else:
|
|
@@ -2697,16 +2915,28 @@ class EntityModelAbstract(MP_Node,
|
|
|
2697
2915
|
if cash_account:
|
|
2698
2916
|
if isinstance(cash_account, BankAccountModel):
|
|
2699
2917
|
cash_account = cash_account.account_model
|
|
2700
|
-
self.validate_account_model_for_coa(
|
|
2701
|
-
|
|
2918
|
+
self.validate_account_model_for_coa(
|
|
2919
|
+
account_model=cash_account, coa_model=coa_model
|
|
2920
|
+
)
|
|
2921
|
+
self.validate_account_model_for_role(
|
|
2922
|
+
cash_account, roles_module.ASSET_CA_CASH
|
|
2923
|
+
)
|
|
2702
2924
|
else:
|
|
2703
|
-
cash_account = account_model_qs.filter(
|
|
2925
|
+
cash_account = account_model_qs.filter(
|
|
2926
|
+
role__exact=roles_module.ASSET_CA_CASH
|
|
2927
|
+
).get()
|
|
2704
2928
|
|
|
2705
2929
|
if capital_account:
|
|
2706
|
-
self.validate_account_model_for_coa(
|
|
2707
|
-
|
|
2930
|
+
self.validate_account_model_for_coa(
|
|
2931
|
+
account_model=capital_account, coa_model=coa_model
|
|
2932
|
+
)
|
|
2933
|
+
self.validate_account_model_for_role(
|
|
2934
|
+
capital_account, roles_module.EQUITY_CAPITAL
|
|
2935
|
+
)
|
|
2708
2936
|
else:
|
|
2709
|
-
capital_account = account_model_qs.filter(
|
|
2937
|
+
capital_account = account_model_qs.filter(
|
|
2938
|
+
role__exact=roles_module.EQUITY_CAPITAL
|
|
2939
|
+
).get()
|
|
2710
2940
|
|
|
2711
2941
|
if not je_timestamp:
|
|
2712
2942
|
je_timestamp = get_localtime()
|
|
@@ -2715,36 +2945,42 @@ class EntityModelAbstract(MP_Node,
|
|
|
2715
2945
|
description = f'Capital Deposit on {je_timestamp.isoformat()}...'
|
|
2716
2946
|
|
|
2717
2947
|
txs = list()
|
|
2718
|
-
txs.append(
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2948
|
+
txs.append(
|
|
2949
|
+
{
|
|
2950
|
+
'account': cash_account,
|
|
2951
|
+
'tx_type': DEBIT,
|
|
2952
|
+
'amount': amount,
|
|
2953
|
+
'description': description,
|
|
2954
|
+
}
|
|
2955
|
+
)
|
|
2956
|
+
txs.append(
|
|
2957
|
+
{
|
|
2958
|
+
'account': capital_account,
|
|
2959
|
+
'tx_type': CREDIT,
|
|
2960
|
+
'amount': amount,
|
|
2961
|
+
'description': description,
|
|
2962
|
+
}
|
|
2963
|
+
)
|
|
2730
2964
|
|
|
2731
2965
|
if not ledger_model:
|
|
2732
2966
|
ledger_model = self.ledgermodel_set.create(
|
|
2733
2967
|
name=f'Capital Deposit on {je_timestamp.isoformat()}.',
|
|
2734
|
-
posted=ledger_posted
|
|
2968
|
+
posted=ledger_posted,
|
|
2735
2969
|
)
|
|
2736
2970
|
else:
|
|
2737
2971
|
if isinstance(ledger_model, LedgerModel):
|
|
2738
2972
|
self.validate_ledger_model_for_entity(ledger_model)
|
|
2739
2973
|
else:
|
|
2740
|
-
ledger_model_qs = LedgerModel.objects.filter(
|
|
2974
|
+
ledger_model_qs = LedgerModel.objects.filter(
|
|
2975
|
+
entity__uuid__exact=self.uuid
|
|
2976
|
+
)
|
|
2741
2977
|
ledger_model = ledger_model_qs.get(uuid__exact=ledger_model)
|
|
2742
2978
|
|
|
2743
2979
|
self.commit_txs(
|
|
2744
2980
|
je_timestamp=je_timestamp,
|
|
2745
2981
|
je_txs=txs,
|
|
2746
2982
|
je_posted=je_posted,
|
|
2747
|
-
je_ledger_model=ledger_model
|
|
2983
|
+
je_ledger_model=ledger_model,
|
|
2748
2984
|
)
|
|
2749
2985
|
|
|
2750
2986
|
return ledger_model
|
|
@@ -2756,14 +2992,22 @@ class EntityModelAbstract(MP_Node,
|
|
|
2756
2992
|
def get_closing_entries(self):
|
|
2757
2993
|
return self.closingentrymodel_set.all()
|
|
2758
2994
|
|
|
2759
|
-
def get_closing_entry_dates_list_meta(
|
|
2995
|
+
def get_closing_entry_dates_list_meta(
|
|
2996
|
+
self, as_iso: bool = True
|
|
2997
|
+
) -> List[Union[date, str]]:
|
|
2760
2998
|
date_list = self.meta[self.META_KEY_CLOSING_ENTRY_DATES]
|
|
2761
2999
|
if as_iso:
|
|
2762
3000
|
return date_list
|
|
2763
3001
|
return [date.fromisoformat(d) for d in date_list]
|
|
2764
3002
|
|
|
2765
|
-
def compute_closing_entry_dates_list(
|
|
2766
|
-
|
|
3003
|
+
def compute_closing_entry_dates_list(
|
|
3004
|
+
self, as_iso: bool = True
|
|
3005
|
+
) -> List[Union[date, str]]:
|
|
3006
|
+
closing_entry_qs = (
|
|
3007
|
+
self.closingentrymodel_set.order_by('-closing_date')
|
|
3008
|
+
.only('closing_date')
|
|
3009
|
+
.posted()
|
|
3010
|
+
)
|
|
2767
3011
|
if as_iso:
|
|
2768
3012
|
return [ce.closing_date.isoformat() for ce in closing_entry_qs]
|
|
2769
3013
|
return [ce.closing_date for ce in closing_entry_qs]
|
|
@@ -2776,14 +3020,11 @@ class EntityModelAbstract(MP_Node,
|
|
|
2776
3020
|
except IndexError:
|
|
2777
3021
|
self.last_closing_date = None
|
|
2778
3022
|
|
|
2779
|
-
self.meta[self.META_KEY_CLOSING_ENTRY_DATES] = [
|
|
3023
|
+
self.meta[self.META_KEY_CLOSING_ENTRY_DATES] = [
|
|
3024
|
+
d.isoformat() for d in date_list
|
|
3025
|
+
]
|
|
2780
3026
|
if commit:
|
|
2781
|
-
self.save(
|
|
2782
|
-
update_fields=[
|
|
2783
|
-
'last_closing_date',
|
|
2784
|
-
'updated',
|
|
2785
|
-
'meta'
|
|
2786
|
-
])
|
|
3027
|
+
self.save(update_fields=['last_closing_date', 'updated', 'meta'])
|
|
2787
3028
|
return date_list
|
|
2788
3029
|
|
|
2789
3030
|
def fetch_closing_entry_dates_meta(self, as_date: bool = True) -> List[date]:
|
|
@@ -2796,7 +3037,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
2796
3037
|
return self._CLOSING_ENTRY_DATES
|
|
2797
3038
|
return date_list
|
|
2798
3039
|
|
|
2799
|
-
def get_closing_entry_for_date(
|
|
3040
|
+
def get_closing_entry_for_date(
|
|
3041
|
+
self, io_date: Union[date, datetime], inclusive: bool = True
|
|
3042
|
+
) -> Optional[date]:
|
|
2800
3043
|
if io_date is None:
|
|
2801
3044
|
return
|
|
2802
3045
|
ce_date_list = self.fetch_closing_entry_dates_meta()
|
|
@@ -2811,7 +3054,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
2811
3054
|
if ce_lookup in ce_date_list:
|
|
2812
3055
|
return ce_lookup
|
|
2813
3056
|
|
|
2814
|
-
def get_nearest_next_closing_entry(
|
|
3057
|
+
def get_nearest_next_closing_entry(
|
|
3058
|
+
self, io_date: Union[date, datetime]
|
|
3059
|
+
) -> Optional[date]:
|
|
2815
3060
|
if io_date is None:
|
|
2816
3061
|
return
|
|
2817
3062
|
|
|
@@ -2819,10 +3064,12 @@ class EntityModelAbstract(MP_Node,
|
|
|
2819
3064
|
if not len(ce_date_list):
|
|
2820
3065
|
return
|
|
2821
3066
|
|
|
2822
|
-
if all(
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
3067
|
+
if all(
|
|
3068
|
+
[
|
|
3069
|
+
isinstance(io_date, date),
|
|
3070
|
+
isinstance(io_date, datetime),
|
|
3071
|
+
]
|
|
3072
|
+
):
|
|
2826
3073
|
io_date = io_date.date()
|
|
2827
3074
|
|
|
2828
3075
|
if io_date > ce_date_list[0]:
|
|
@@ -2832,35 +3079,42 @@ class EntityModelAbstract(MP_Node,
|
|
|
2832
3079
|
if p and p <= io_date < f:
|
|
2833
3080
|
return p
|
|
2834
3081
|
|
|
2835
|
-
def close_entity_books(
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
3082
|
+
def close_entity_books(
|
|
3083
|
+
self,
|
|
3084
|
+
closing_date: Optional[date] = None,
|
|
3085
|
+
closing_entry_model=None,
|
|
3086
|
+
force_update: bool = False,
|
|
3087
|
+
post_closing_entry: bool = True,
|
|
3088
|
+
):
|
|
2841
3089
|
if closing_entry_model and closing_date:
|
|
2842
3090
|
raise EntityModelValidationError(
|
|
2843
|
-
message=_(
|
|
3091
|
+
message=_(
|
|
3092
|
+
'Closing books must be called by providing closing_date or closing_entry_model, not both.'
|
|
3093
|
+
)
|
|
2844
3094
|
)
|
|
2845
3095
|
elif not closing_date and not closing_entry_model:
|
|
2846
3096
|
raise EntityModelValidationError(
|
|
2847
|
-
message=_(
|
|
3097
|
+
message=_(
|
|
3098
|
+
'Closing books must be called by providing closing_date or closing_entry_model.'
|
|
3099
|
+
)
|
|
2848
3100
|
)
|
|
2849
3101
|
|
|
2850
3102
|
closing_entry_exists = False
|
|
2851
3103
|
|
|
2852
3104
|
if closing_entry_model:
|
|
2853
3105
|
closing_date = closing_entry_model.closing_date
|
|
2854
|
-
self.validate_closing_entry_model(
|
|
3106
|
+
self.validate_closing_entry_model(
|
|
3107
|
+
closing_entry_model, closing_date=closing_date
|
|
3108
|
+
)
|
|
2855
3109
|
closing_entry_exists = True
|
|
2856
3110
|
else:
|
|
2857
3111
|
try:
|
|
2858
|
-
closing_entry_model =
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
'markdown_notes')
|
|
2863
|
-
closing_date__exact=closing_date
|
|
3112
|
+
closing_entry_model = (
|
|
3113
|
+
self.closingentrymodel_set.select_related(
|
|
3114
|
+
'ledger_model', 'ledger_model__entity'
|
|
3115
|
+
)
|
|
3116
|
+
.defer('markdown_notes')
|
|
3117
|
+
.get(closing_date__exact=closing_date)
|
|
2864
3118
|
)
|
|
2865
3119
|
|
|
2866
3120
|
closing_entry_exists = True
|
|
@@ -2879,27 +3133,44 @@ class EntityModelAbstract(MP_Node,
|
|
|
2879
3133
|
self.save_closing_entry_dates_meta(commit=True)
|
|
2880
3134
|
|
|
2881
3135
|
return closing_entry_model, ce_txs
|
|
2882
|
-
raise EntityModelValidationError(
|
|
3136
|
+
raise EntityModelValidationError(
|
|
3137
|
+
message=f'Closing Entry for Period {closing_date} already exists.'
|
|
3138
|
+
)
|
|
2883
3139
|
|
|
2884
|
-
def close_books_for_month(
|
|
3140
|
+
def close_books_for_month(
|
|
3141
|
+
self,
|
|
3142
|
+
year: int,
|
|
3143
|
+
month: int,
|
|
3144
|
+
force_update: bool = False,
|
|
3145
|
+
post_closing_entry: bool = True,
|
|
3146
|
+
):
|
|
2885
3147
|
_, day = monthrange(year, month)
|
|
2886
3148
|
closing_dt = date(year, month, day)
|
|
2887
3149
|
return self.close_entity_books(
|
|
2888
3150
|
closing_date=closing_dt,
|
|
2889
3151
|
force_update=force_update,
|
|
2890
3152
|
post_closing_entry=post_closing_entry,
|
|
2891
|
-
closing_entry_model=None
|
|
3153
|
+
closing_entry_model=None,
|
|
2892
3154
|
)
|
|
2893
3155
|
|
|
2894
|
-
def close_books_for_fiscal_year(
|
|
2895
|
-
|
|
3156
|
+
def close_books_for_fiscal_year(
|
|
3157
|
+
self,
|
|
3158
|
+
fiscal_year: int,
|
|
3159
|
+
force_update: bool = False,
|
|
3160
|
+
post_closing_entry: bool = True,
|
|
3161
|
+
):
|
|
2896
3162
|
closing_dt = self.get_fy_end(year=fiscal_year)
|
|
2897
|
-
return self.close_entity_books(
|
|
2898
|
-
|
|
3163
|
+
return self.close_entity_books(
|
|
3164
|
+
closing_date=closing_dt,
|
|
3165
|
+
force_update=force_update,
|
|
3166
|
+
post_closing_entry=post_closing_entry,
|
|
3167
|
+
)
|
|
2899
3168
|
|
|
2900
3169
|
# ### RANDOM DATA GENERATION ####
|
|
2901
3170
|
|
|
2902
|
-
def populate_random_data(
|
|
3171
|
+
def populate_random_data(
|
|
3172
|
+
self, start_date: date, days_forward=180, tx_quantity: int = 25
|
|
3173
|
+
):
|
|
2903
3174
|
EntityDataGenerator = lazy_loader.get_entity_data_generator()
|
|
2904
3175
|
data_generator = EntityDataGenerator(
|
|
2905
3176
|
user_model=self.admin,
|
|
@@ -2907,16 +3178,15 @@ class EntityModelAbstract(MP_Node,
|
|
|
2907
3178
|
start_dttm=start_date,
|
|
2908
3179
|
entity_model=self,
|
|
2909
3180
|
capital_contribution=Decimal.from_float(50000.00),
|
|
2910
|
-
tx_quantity=tx_quantity
|
|
3181
|
+
tx_quantity=tx_quantity,
|
|
2911
3182
|
)
|
|
2912
3183
|
data_generator.populate_entity()
|
|
2913
3184
|
|
|
2914
3185
|
# URLS ----
|
|
2915
3186
|
def get_absolute_url(self):
|
|
2916
|
-
return reverse(
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
})
|
|
3187
|
+
return reverse(
|
|
3188
|
+
viewname='django_ledger:entity-dashboard', kwargs={'entity_slug': self.slug}
|
|
3189
|
+
)
|
|
2920
3190
|
|
|
2921
3191
|
def get_dashboard_url(self) -> str:
|
|
2922
3192
|
"""
|
|
@@ -2927,10 +3197,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
2927
3197
|
str
|
|
2928
3198
|
EntityModel dashboard URL as a string.
|
|
2929
3199
|
"""
|
|
2930
|
-
return reverse(
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
})
|
|
3200
|
+
return reverse(
|
|
3201
|
+
'django_ledger:entity-dashboard', kwargs={'entity_slug': self.slug}
|
|
3202
|
+
)
|
|
2934
3203
|
|
|
2935
3204
|
def get_manage_url(self) -> str:
|
|
2936
3205
|
"""
|
|
@@ -2941,10 +3210,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
2941
3210
|
str
|
|
2942
3211
|
EntityModel manage URL as a string.
|
|
2943
3212
|
"""
|
|
2944
|
-
return reverse('django_ledger:entity-update',
|
|
2945
|
-
kwargs={
|
|
2946
|
-
'entity_slug': self.slug
|
|
2947
|
-
})
|
|
3213
|
+
return reverse('django_ledger:entity-update', kwargs={'entity_slug': self.slug})
|
|
2948
3214
|
|
|
2949
3215
|
def get_ledgers_url(self) -> str:
|
|
2950
3216
|
"""
|
|
@@ -2955,10 +3221,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
2955
3221
|
str
|
|
2956
3222
|
EntityModel ledger list URL as a string.
|
|
2957
3223
|
"""
|
|
2958
|
-
return reverse('django_ledger:ledger-list',
|
|
2959
|
-
kwargs={
|
|
2960
|
-
'entity_slug': self.slug
|
|
2961
|
-
})
|
|
3224
|
+
return reverse('django_ledger:ledger-list', kwargs={'entity_slug': self.slug})
|
|
2962
3225
|
|
|
2963
3226
|
def get_bills_url(self) -> str:
|
|
2964
3227
|
"""
|
|
@@ -2969,10 +3232,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
2969
3232
|
str
|
|
2970
3233
|
EntityModel bill list URL as a string.
|
|
2971
3234
|
"""
|
|
2972
|
-
return reverse('django_ledger:bill-list',
|
|
2973
|
-
kwargs={
|
|
2974
|
-
'entity_slug': self.slug
|
|
2975
|
-
})
|
|
3235
|
+
return reverse('django_ledger:bill-list', kwargs={'entity_slug': self.slug})
|
|
2976
3236
|
|
|
2977
3237
|
def get_invoices_url(self) -> str:
|
|
2978
3238
|
"""
|
|
@@ -2983,10 +3243,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
2983
3243
|
str
|
|
2984
3244
|
EntityModel invoice list URL as a string.
|
|
2985
3245
|
"""
|
|
2986
|
-
return reverse('django_ledger:invoice-list',
|
|
2987
|
-
kwargs={
|
|
2988
|
-
'entity_slug': self.slug
|
|
2989
|
-
})
|
|
3246
|
+
return reverse('django_ledger:invoice-list', kwargs={'entity_slug': self.slug})
|
|
2990
3247
|
|
|
2991
3248
|
def get_banks_url(self) -> str:
|
|
2992
3249
|
"""
|
|
@@ -2997,10 +3254,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
2997
3254
|
str
|
|
2998
3255
|
EntityModel bank account list URL as a string.
|
|
2999
3256
|
"""
|
|
3000
|
-
return reverse(
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
})
|
|
3257
|
+
return reverse(
|
|
3258
|
+
'django_ledger:bank-account-list', kwargs={'entity_slug': self.slug}
|
|
3259
|
+
)
|
|
3004
3260
|
|
|
3005
3261
|
def get_balance_sheet_url(self) -> str:
|
|
3006
3262
|
"""
|
|
@@ -3011,10 +3267,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
3011
3267
|
str
|
|
3012
3268
|
EntityModel Balance Sheet Statement URL as a string.
|
|
3013
3269
|
"""
|
|
3014
|
-
return reverse('django_ledger:entity-bs',
|
|
3015
|
-
kwargs={
|
|
3016
|
-
'entity_slug': self.slug
|
|
3017
|
-
})
|
|
3270
|
+
return reverse('django_ledger:entity-bs', kwargs={'entity_slug': self.slug})
|
|
3018
3271
|
|
|
3019
3272
|
def get_income_statement_url(self) -> str:
|
|
3020
3273
|
"""
|
|
@@ -3025,10 +3278,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
3025
3278
|
str
|
|
3026
3279
|
EntityModel Income Statement URL as a string.
|
|
3027
3280
|
"""
|
|
3028
|
-
return reverse('django_ledger:entity-ic',
|
|
3029
|
-
kwargs={
|
|
3030
|
-
'entity_slug': self.slug
|
|
3031
|
-
})
|
|
3281
|
+
return reverse('django_ledger:entity-ic', kwargs={'entity_slug': self.slug})
|
|
3032
3282
|
|
|
3033
3283
|
def get_cashflow_statement_url(self) -> str:
|
|
3034
3284
|
"""
|
|
@@ -3039,10 +3289,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
3039
3289
|
str
|
|
3040
3290
|
EntityModel Cashflow Statement URL as a string.
|
|
3041
3291
|
"""
|
|
3042
|
-
return reverse('django_ledger:entity-cf',
|
|
3043
|
-
kwargs={
|
|
3044
|
-
'entity_slug': self.slug
|
|
3045
|
-
})
|
|
3292
|
+
return reverse('django_ledger:entity-cf', kwargs={'entity_slug': self.slug})
|
|
3046
3293
|
|
|
3047
3294
|
def get_data_import_url(self) -> str:
|
|
3048
3295
|
"""
|
|
@@ -3053,33 +3300,24 @@ class EntityModelAbstract(MP_Node,
|
|
|
3053
3300
|
str
|
|
3054
3301
|
EntityModel transaction import URL as a string.
|
|
3055
3302
|
"""
|
|
3056
|
-
return reverse(
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
})
|
|
3303
|
+
return reverse(
|
|
3304
|
+
'django_ledger:data-import-jobs-list', kwargs={'entity_slug': self.slug}
|
|
3305
|
+
)
|
|
3060
3306
|
|
|
3061
3307
|
def get_coa_list_url(self) -> str:
|
|
3062
3308
|
return reverse(
|
|
3063
|
-
viewname='django_ledger:coa-list',
|
|
3064
|
-
kwargs={
|
|
3065
|
-
'entity_slug': self.slug
|
|
3066
|
-
}
|
|
3309
|
+
viewname='django_ledger:coa-list', kwargs={'entity_slug': self.slug}
|
|
3067
3310
|
)
|
|
3068
3311
|
|
|
3069
3312
|
def get_coa_list_inactive_url(self) -> str:
|
|
3070
3313
|
return reverse(
|
|
3071
3314
|
viewname='django_ledger:coa-list-inactive',
|
|
3072
|
-
kwargs={
|
|
3073
|
-
'entity_slug': self.slug
|
|
3074
|
-
}
|
|
3315
|
+
kwargs={'entity_slug': self.slug},
|
|
3075
3316
|
)
|
|
3076
3317
|
|
|
3077
3318
|
def get_coa_create_url(self) -> str:
|
|
3078
3319
|
return reverse(
|
|
3079
|
-
viewname='django_ledger:coa-create',
|
|
3080
|
-
kwargs={
|
|
3081
|
-
'entity_slug': self.slug
|
|
3082
|
-
}
|
|
3320
|
+
viewname='django_ledger:coa-create', kwargs={'entity_slug': self.slug}
|
|
3083
3321
|
)
|
|
3084
3322
|
|
|
3085
3323
|
def get_accounts_url(self) -> str:
|
|
@@ -3091,10 +3329,12 @@ class EntityModelAbstract(MP_Node,
|
|
|
3091
3329
|
str
|
|
3092
3330
|
EntityModel Code of Accounts llist import URL as a string.
|
|
3093
3331
|
"""
|
|
3094
|
-
return reverse(
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3332
|
+
return reverse(
|
|
3333
|
+
'django_ledger:account-list',
|
|
3334
|
+
kwargs={
|
|
3335
|
+
'entity_slug': self.slug,
|
|
3336
|
+
},
|
|
3337
|
+
)
|
|
3098
3338
|
|
|
3099
3339
|
def get_customers_url(self) -> str:
|
|
3100
3340
|
"""
|
|
@@ -3105,10 +3345,12 @@ class EntityModelAbstract(MP_Node,
|
|
|
3105
3345
|
str
|
|
3106
3346
|
EntityModel customers list URL as a string.
|
|
3107
3347
|
"""
|
|
3108
|
-
return reverse(
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3348
|
+
return reverse(
|
|
3349
|
+
'django_ledger:customer-list',
|
|
3350
|
+
kwargs={
|
|
3351
|
+
'entity_slug': self.slug,
|
|
3352
|
+
},
|
|
3353
|
+
)
|
|
3112
3354
|
|
|
3113
3355
|
def get_vendors_url(self) -> str:
|
|
3114
3356
|
"""
|
|
@@ -3119,10 +3361,12 @@ class EntityModelAbstract(MP_Node,
|
|
|
3119
3361
|
str
|
|
3120
3362
|
EntityModel vendors list URL as a string.
|
|
3121
3363
|
"""
|
|
3122
|
-
return reverse(
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3364
|
+
return reverse(
|
|
3365
|
+
'django_ledger:vendor-list',
|
|
3366
|
+
kwargs={
|
|
3367
|
+
'entity_slug': self.slug,
|
|
3368
|
+
},
|
|
3369
|
+
)
|
|
3126
3370
|
|
|
3127
3371
|
def get_delete_url(self) -> str:
|
|
3128
3372
|
"""
|
|
@@ -3133,10 +3377,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
3133
3377
|
str
|
|
3134
3378
|
EntityModel delete URL as a string.
|
|
3135
3379
|
"""
|
|
3136
|
-
return reverse('django_ledger:entity-delete',
|
|
3137
|
-
kwargs={
|
|
3138
|
-
'entity_slug': self.slug
|
|
3139
|
-
})
|
|
3380
|
+
return reverse('django_ledger:entity-delete', kwargs={'entity_slug': self.slug})
|
|
3140
3381
|
|
|
3141
3382
|
def clean(self):
|
|
3142
3383
|
if not self.slug:
|
|
@@ -3163,6 +3404,7 @@ class EntityStateModelAbstract(Model):
|
|
|
3163
3404
|
KEY_VENDOR = 'vendor'
|
|
3164
3405
|
KEY_CUSTOMER = 'customer'
|
|
3165
3406
|
KEY_ITEM = 'item'
|
|
3407
|
+
KEY_RECEIPT = 'receipt'
|
|
3166
3408
|
|
|
3167
3409
|
KEY_CHOICES = [
|
|
3168
3410
|
(KEY_JOURNAL_ENTRY, _('Journal Entry')),
|
|
@@ -3173,38 +3415,36 @@ class EntityStateModelAbstract(Model):
|
|
|
3173
3415
|
]
|
|
3174
3416
|
|
|
3175
3417
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
3176
|
-
entity_model = models.ForeignKey(
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3418
|
+
entity_model = models.ForeignKey(
|
|
3419
|
+
'django_ledger.EntityModel',
|
|
3420
|
+
on_delete=models.CASCADE,
|
|
3421
|
+
verbose_name=_('Entity Model'),
|
|
3422
|
+
)
|
|
3423
|
+
entity_unit = models.ForeignKey(
|
|
3424
|
+
'django_ledger.EntityUnitModel',
|
|
3425
|
+
on_delete=models.RESTRICT,
|
|
3426
|
+
verbose_name=_('Entity Unit'),
|
|
3427
|
+
blank=True,
|
|
3428
|
+
null=True,
|
|
3429
|
+
)
|
|
3184
3430
|
fiscal_year = models.SmallIntegerField(
|
|
3185
3431
|
verbose_name=_('Fiscal Year'),
|
|
3186
3432
|
validators=[MinValueValidator(limit_value=1900)],
|
|
3187
3433
|
null=True,
|
|
3188
|
-
blank=True
|
|
3434
|
+
blank=True,
|
|
3189
3435
|
)
|
|
3190
3436
|
key = models.CharField(choices=KEY_CHOICES, max_length=10)
|
|
3191
|
-
sequence = models.BigIntegerField(
|
|
3437
|
+
sequence = models.BigIntegerField(
|
|
3438
|
+
default=0, validators=[MinValueValidator(limit_value=0)]
|
|
3439
|
+
)
|
|
3192
3440
|
|
|
3193
3441
|
class Meta:
|
|
3194
3442
|
abstract = True
|
|
3195
3443
|
indexes = [
|
|
3196
3444
|
models.Index(fields=['key']),
|
|
3197
|
-
models.Index(
|
|
3198
|
-
fields=[
|
|
3199
|
-
'entity_model',
|
|
3200
|
-
'fiscal_year',
|
|
3201
|
-
'entity_unit',
|
|
3202
|
-
'key'
|
|
3203
|
-
])
|
|
3204
|
-
]
|
|
3205
|
-
unique_together = [
|
|
3206
|
-
('entity_model', 'entity_unit', 'fiscal_year', 'key')
|
|
3445
|
+
models.Index(fields=['entity_model', 'fiscal_year', 'entity_unit', 'key']),
|
|
3207
3446
|
]
|
|
3447
|
+
unique_together = [('entity_model', 'entity_unit', 'fiscal_year', 'key')]
|
|
3208
3448
|
|
|
3209
3449
|
def __str__(self):
|
|
3210
3450
|
return f'{self.__class__.__name__} {self.entity_model_id}: FY: {self.fiscal_year}, KEY: {self.get_key_display()}'
|
|
@@ -3224,31 +3464,38 @@ class EntityManagementModelAbstract(CreateUpdateMixIn):
|
|
|
3224
3464
|
"""
|
|
3225
3465
|
Entity Management Model responsible for manager permissions to read/write.
|
|
3226
3466
|
"""
|
|
3467
|
+
|
|
3227
3468
|
PERMISSIONS = [
|
|
3228
3469
|
('read', _('Read Permissions')),
|
|
3229
3470
|
('write', _('Read/Write Permissions')),
|
|
3230
|
-
('suspended', _('No Permissions'))
|
|
3471
|
+
('suspended', _('No Permissions')),
|
|
3231
3472
|
]
|
|
3232
3473
|
|
|
3233
3474
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
3234
|
-
entity = models.ForeignKey(
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3475
|
+
entity = models.ForeignKey(
|
|
3476
|
+
'django_ledger.EntityModel',
|
|
3477
|
+
on_delete=models.CASCADE,
|
|
3478
|
+
verbose_name=_('Entity'),
|
|
3479
|
+
related_name='entity_permissions',
|
|
3480
|
+
)
|
|
3481
|
+
user = models.ForeignKey(
|
|
3482
|
+
UserModel,
|
|
3483
|
+
on_delete=models.CASCADE,
|
|
3484
|
+
verbose_name=_('Manager'),
|
|
3485
|
+
related_name='entity_permissions',
|
|
3486
|
+
)
|
|
3487
|
+
permission_level = models.CharField(
|
|
3488
|
+
max_length=10,
|
|
3489
|
+
default='read',
|
|
3490
|
+
choices=PERMISSIONS,
|
|
3491
|
+
verbose_name=_('Permission Level'),
|
|
3492
|
+
)
|
|
3246
3493
|
|
|
3247
3494
|
class Meta:
|
|
3248
3495
|
abstract = True
|
|
3249
3496
|
indexes = [
|
|
3250
3497
|
models.Index(fields=['entity', 'user']),
|
|
3251
|
-
models.Index(fields=['user', 'entity'])
|
|
3498
|
+
models.Index(fields=['user', 'entity']),
|
|
3252
3499
|
]
|
|
3253
3500
|
|
|
3254
3501
|
|