django-ledger 0.7.2__py3-none-any.whl → 0.7.4__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/contrib/django_ledger_graphene/journal_entry/schema.py +2 -3
- django_ledger/contrib/django_ledger_graphene/transaction/schema.py +9 -7
- django_ledger/forms/account.py +4 -1
- django_ledger/forms/journal_entry.py +19 -12
- django_ledger/forms/transactions.py +8 -12
- django_ledger/io/io_core.py +17 -12
- django_ledger/io/io_library.py +3 -3
- django_ledger/migrations/0001_initial.py +1 -1
- django_ledger/migrations/0019_alter_transactionmodel_amount_and_more.py +33 -0
- django_ledger/models/bill.py +17 -2
- django_ledger/models/chart_of_accounts.py +4 -0
- django_ledger/models/closing_entry.py +8 -6
- django_ledger/models/data_import.py +1 -0
- django_ledger/models/invoice.py +12 -4
- django_ledger/models/journal_entry.py +843 -481
- django_ledger/models/ledger.py +45 -4
- django_ledger/models/mixins.py +5 -5
- django_ledger/models/transactions.py +303 -305
- django_ledger/models/unit.py +42 -22
- django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
- django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
- django_ledger/templates/django_ledger/account/tags/account_txs_table.html +1 -1
- django_ledger/templates/django_ledger/account/tags/accounts_table.html +1 -1
- django_ledger/templates/django_ledger/bills/bill_detail.html +1 -1
- django_ledger/templates/django_ledger/bills/bill_update.html +1 -1
- django_ledger/templates/django_ledger/components/icon.html +1 -1
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +8 -1
- django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -0
- django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +4 -4
- django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +118 -0
- django_ledger/templates/django_ledger/includes/nav.html +9 -5
- django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +69 -69
- django_ledger/templates/django_ledger/invoice/invoice_detail.html +1 -1
- django_ledger/templates/django_ledger/journal_entry/je_create.html +2 -3
- django_ledger/templates/django_ledger/journal_entry/je_delete.html +2 -3
- django_ledger/templates/django_ledger/journal_entry/je_detail.html +1 -1
- django_ledger/templates/django_ledger/journal_entry/je_detail_txs.html +8 -8
- django_ledger/templates/django_ledger/journal_entry/je_list.html +16 -13
- django_ledger/templates/django_ledger/journal_entry/je_update.html +2 -3
- django_ledger/templates/django_ledger/journal_entry/tags/je_table.html +24 -24
- django_ledger/templates/django_ledger/journal_entry/tags/je_txs_table.html +17 -14
- django_ledger/templates/django_ledger/ledger/tags/ledgers_table.html +38 -37
- django_ledger/templates/django_ledger/transactions/tags/txs_table.html +69 -0
- django_ledger/templatetags/django_ledger.py +35 -48
- django_ledger/urls/account.py +4 -4
- django_ledger/views/account.py +8 -8
- django_ledger/views/journal_entry.py +84 -101
- django_ledger/views/ledger.py +16 -21
- django_ledger/views/mixins.py +17 -28
- {django_ledger-0.7.2.dist-info → django_ledger-0.7.4.dist-info}/METADATA +8 -3
- {django_ledger-0.7.2.dist-info → django_ledger-0.7.4.dist-info}/RECORD +56 -104
- assets/node_modules/node-gyp/gyp/gyp_main.py +0 -45
- assets/node_modules/node-gyp/gyp/pylib/gyp/MSVSNew.py +0 -367
- assets/node_modules/node-gyp/gyp/pylib/gyp/MSVSProject.py +0 -206
- assets/node_modules/node-gyp/gyp/pylib/gyp/MSVSSettings.py +0 -1270
- assets/node_modules/node-gyp/gyp/pylib/gyp/MSVSSettings_test.py +0 -1547
- assets/node_modules/node-gyp/gyp/pylib/gyp/MSVSToolFile.py +0 -59
- assets/node_modules/node-gyp/gyp/pylib/gyp/MSVSUserFile.py +0 -153
- assets/node_modules/node-gyp/gyp/pylib/gyp/MSVSUtil.py +0 -271
- assets/node_modules/node-gyp/gyp/pylib/gyp/MSVSVersion.py +0 -574
- assets/node_modules/node-gyp/gyp/pylib/gyp/__init__.py +0 -666
- assets/node_modules/node-gyp/gyp/pylib/gyp/common.py +0 -654
- assets/node_modules/node-gyp/gyp/pylib/gyp/common_test.py +0 -78
- assets/node_modules/node-gyp/gyp/pylib/gyp/easy_xml.py +0 -165
- assets/node_modules/node-gyp/gyp/pylib/gyp/easy_xml_test.py +0 -109
- assets/node_modules/node-gyp/gyp/pylib/gyp/flock_tool.py +0 -55
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/__init__.py +0 -0
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/analyzer.py +0 -808
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/android.py +0 -1173
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/cmake.py +0 -1321
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/compile_commands_json.py +0 -120
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/dump_dependency_json.py +0 -103
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/eclipse.py +0 -464
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/gypd.py +0 -89
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/gypsh.py +0 -58
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py +0 -2518
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/msvs.py +0 -3978
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/msvs_test.py +0 -44
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/ninja.py +0 -2936
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/ninja_test.py +0 -55
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/xcode.py +0 -1394
- assets/node_modules/node-gyp/gyp/pylib/gyp/generator/xcode_test.py +0 -25
- assets/node_modules/node-gyp/gyp/pylib/gyp/input.py +0 -3137
- assets/node_modules/node-gyp/gyp/pylib/gyp/input_test.py +0 -98
- assets/node_modules/node-gyp/gyp/pylib/gyp/mac_tool.py +0 -771
- assets/node_modules/node-gyp/gyp/pylib/gyp/msvs_emulation.py +0 -1271
- assets/node_modules/node-gyp/gyp/pylib/gyp/ninja_syntax.py +0 -174
- assets/node_modules/node-gyp/gyp/pylib/gyp/simple_copy.py +0 -61
- assets/node_modules/node-gyp/gyp/pylib/gyp/win_tool.py +0 -374
- assets/node_modules/node-gyp/gyp/pylib/gyp/xcode_emulation.py +0 -1939
- assets/node_modules/node-gyp/gyp/pylib/gyp/xcode_ninja.py +0 -302
- assets/node_modules/node-gyp/gyp/pylib/gyp/xcodeproj_file.py +0 -3197
- assets/node_modules/node-gyp/gyp/pylib/gyp/xml_fix.py +0 -65
- assets/node_modules/node-gyp/gyp/setup.py +0 -42
- assets/node_modules/node-gyp/gyp/test_gyp.py +0 -260
- assets/node_modules/node-gyp/gyp/tools/graphviz.py +0 -102
- assets/node_modules/node-gyp/gyp/tools/pretty_gyp.py +0 -156
- assets/node_modules/node-gyp/gyp/tools/pretty_sln.py +0 -181
- assets/node_modules/node-gyp/gyp/tools/pretty_vcproj.py +0 -339
- assets/node_modules/node-gyp/test/fixtures/test-charmap.py +0 -31
- assets/node_modules/node-gyp/update-gyp.py +0 -46
- {django_ledger-0.7.2.dist-info → django_ledger-0.7.4.dist-info}/AUTHORS.md +0 -0
- {django_ledger-0.7.2.dist-info → django_ledger-0.7.4.dist-info}/LICENSE +0 -0
- {django_ledger-0.7.2.dist-info → django_ledger-0.7.4.dist-info}/WHEEL +0 -0
- {django_ledger-0.7.2.dist-info → django_ledger-0.7.4.dist-info}/top_level.txt +0 -0
|
@@ -34,7 +34,7 @@ from uuid import uuid4, UUID
|
|
|
34
34
|
|
|
35
35
|
from django.core.exceptions import FieldError, ObjectDoesNotExist, ValidationError
|
|
36
36
|
from django.db import models, transaction, IntegrityError
|
|
37
|
-
from django.db.models import Q, Sum, QuerySet, F, Manager
|
|
37
|
+
from django.db.models import Q, Sum, QuerySet, F, Manager, Count
|
|
38
38
|
from django.db.models.functions import Coalesce
|
|
39
39
|
from django.db.models.signals import pre_save
|
|
40
40
|
from django.urls import reverse
|
|
@@ -46,10 +46,12 @@ from django_ledger.io.roles import (
|
|
|
46
46
|
ASSET_CA_CASH, GROUP_CFS_FIN_DIVIDENDS, GROUP_CFS_FIN_ISSUING_EQUITY,
|
|
47
47
|
GROUP_CFS_FIN_LT_DEBT_PAYMENTS, GROUP_CFS_FIN_ST_DEBT_PAYMENTS,
|
|
48
48
|
GROUP_CFS_INVESTING_AND_FINANCING, GROUP_CFS_INVESTING_PPE,
|
|
49
|
-
GROUP_CFS_INVESTING_SECURITIES,
|
|
49
|
+
GROUP_CFS_INVESTING_SECURITIES,
|
|
50
|
+
validate_roles
|
|
50
51
|
)
|
|
51
52
|
from django_ledger.models.accounts import CREDIT, DEBIT
|
|
52
53
|
from django_ledger.models.entity import EntityStateModel, EntityModel
|
|
54
|
+
from django_ledger.models.ledger import LedgerModel
|
|
53
55
|
from django_ledger.models.mixins import CreateUpdateMixIn
|
|
54
56
|
from django_ledger.models.signals import (
|
|
55
57
|
journal_entry_unlocked,
|
|
@@ -58,12 +60,12 @@ from django_ledger.models.signals import (
|
|
|
58
60
|
journal_entry_unposted
|
|
59
61
|
)
|
|
60
62
|
from django_ledger.models.transactions import TransactionModelQuerySet, TransactionModel
|
|
61
|
-
from django_ledger.models.utils import lazy_loader
|
|
62
63
|
from django_ledger.settings import (
|
|
63
64
|
DJANGO_LEDGER_JE_NUMBER_PREFIX,
|
|
64
65
|
DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING,
|
|
65
66
|
DJANGO_LEDGER_JE_NUMBER_NO_UNIT_PREFIX
|
|
66
67
|
)
|
|
68
|
+
from django_ledger.io import roles
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
class JournalEntryValidationError(ValidationError):
|
|
@@ -72,167 +74,224 @@ class JournalEntryValidationError(ValidationError):
|
|
|
72
74
|
|
|
73
75
|
class JournalEntryModelQuerySet(QuerySet):
|
|
74
76
|
"""
|
|
75
|
-
|
|
77
|
+
A custom QuerySet for working with Journal Entry models, providing additional
|
|
78
|
+
convenience methods and validations for specific use cases.
|
|
79
|
+
|
|
80
|
+
This class enhances Django's default QuerySet by adding tailored methods
|
|
81
|
+
to manage and filter Journal Entries, such as handling posted, unposted,
|
|
82
|
+
locked entries, and querying entries associated with specific ledgers.
|
|
76
83
|
"""
|
|
77
84
|
|
|
78
85
|
def create(self, verify_on_save: bool = False, force_create: bool = False, **kwargs):
|
|
79
86
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
Creates a new Journal Entry while enforcing business logic validations.
|
|
88
|
+
|
|
89
|
+
This method overrides Django's default `create()` to ensure that Journal Entries
|
|
90
|
+
cannot be created in a "posted" state unless explicitly overridden.
|
|
91
|
+
Additionally, it offers optional pre-save verification.
|
|
83
92
|
|
|
84
93
|
Parameters
|
|
85
94
|
----------
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
verify_on_save : bool
|
|
96
|
+
If True, performs a verification step before saving. This avoids
|
|
97
|
+
additional database queries for validation when creating new entries.
|
|
98
|
+
Should be used when the Journal Entry needs no transactional validation.
|
|
99
|
+
force_create : bool
|
|
100
|
+
If True, allows the creation of a Journal Entry even in a "posted"
|
|
101
|
+
state. Use with caution and only if you are certain of the consequences.
|
|
102
|
+
**kwargs : dict
|
|
103
|
+
Additional keyword arguments passed to instantiate the Journal Entry model.
|
|
94
104
|
|
|
95
105
|
Returns
|
|
96
106
|
-------
|
|
97
107
|
JournalEntryModel
|
|
98
|
-
The newly created Journal Entry
|
|
108
|
+
The newly created Journal Entry.
|
|
109
|
+
|
|
110
|
+
Raises
|
|
111
|
+
------
|
|
112
|
+
FieldError
|
|
113
|
+
Raised if attempting to create a "posted" Journal Entry without
|
|
114
|
+
setting `force_create=True`.
|
|
99
115
|
"""
|
|
100
116
|
is_posted = kwargs.get('posted')
|
|
101
|
-
|
|
102
117
|
if is_posted and not force_create:
|
|
103
|
-
raise FieldError(
|
|
118
|
+
raise FieldError("Cannot create Journal Entries in a posted state without 'force_create=True'.")
|
|
104
119
|
|
|
105
120
|
obj = self.model(**kwargs)
|
|
106
121
|
self._for_write = True
|
|
107
122
|
|
|
108
|
-
#
|
|
109
|
-
# New JEs using the create() method don't have any transactions to validate.
|
|
110
|
-
# therefore, it is not necessary to query DB to balance TXS.
|
|
123
|
+
# Save the object with optional pre-save verification.
|
|
111
124
|
obj.save(force_insert=True, using=self.db, verify=verify_on_save)
|
|
112
125
|
return obj
|
|
113
126
|
|
|
114
127
|
def posted(self):
|
|
115
128
|
"""
|
|
116
|
-
Filters the QuerySet to only posted Journal Entries.
|
|
129
|
+
Filters the QuerySet to include only "posted" Journal Entries.
|
|
117
130
|
|
|
118
131
|
Returns
|
|
119
132
|
-------
|
|
120
133
|
JournalEntryModelQuerySet
|
|
121
|
-
A QuerySet
|
|
134
|
+
A filtered QuerySet containing only posted Journal Entries.
|
|
122
135
|
"""
|
|
123
136
|
return self.filter(posted=True)
|
|
124
137
|
|
|
125
138
|
def unposted(self):
|
|
139
|
+
"""
|
|
140
|
+
Filters the QuerySet to include only "unposted" Journal Entries.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
JournalEntryModelQuerySet
|
|
145
|
+
A filtered QuerySet containing only unposted Journal Entries.
|
|
146
|
+
"""
|
|
126
147
|
return self.filter(posted=False)
|
|
127
148
|
|
|
128
149
|
def locked(self):
|
|
129
150
|
"""
|
|
130
|
-
Filters the QuerySet to only locked Journal Entries.
|
|
151
|
+
Filters the QuerySet to include only "locked" Journal Entries.
|
|
131
152
|
|
|
132
153
|
Returns
|
|
133
154
|
-------
|
|
134
155
|
JournalEntryModelQuerySet
|
|
135
|
-
A QuerySet
|
|
156
|
+
A filtered QuerySet containing only locked Journal Entries.
|
|
136
157
|
"""
|
|
137
|
-
|
|
138
158
|
return self.filter(locked=True)
|
|
139
159
|
|
|
140
160
|
def unlocked(self):
|
|
161
|
+
"""
|
|
162
|
+
Filters the QuerySet to include only "unlocked" Journal Entries.
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
JournalEntryModelQuerySet
|
|
167
|
+
A filtered QuerySet containing only unlocked Journal Entries.
|
|
168
|
+
"""
|
|
141
169
|
return self.filter(locked=False)
|
|
142
170
|
|
|
171
|
+
def for_ledger(self, ledger_pk: Union[str, UUID, LedgerModel]):
|
|
172
|
+
"""
|
|
173
|
+
Filters the QuerySet to include Journal Entries associated with a specific Ledger.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
ledger_pk : str, UUID, or LedgerModel
|
|
178
|
+
The LedgerModel instance, its UUID, or a string representation of the UUID
|
|
179
|
+
to identify the Ledger.
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
JournalEntryModelQuerySet
|
|
184
|
+
A filtered QuerySet of Journal Entries associated with the specified Ledger.
|
|
185
|
+
"""
|
|
186
|
+
if isinstance(ledger_pk, LedgerModel):
|
|
187
|
+
return self.filter(ledger=ledger_pk)
|
|
188
|
+
return self.filter(ledger__uuid__exact=ledger_pk)
|
|
189
|
+
|
|
143
190
|
|
|
144
191
|
class JournalEntryModelManager(Manager):
|
|
145
192
|
"""
|
|
146
|
-
A custom
|
|
147
|
-
|
|
193
|
+
A custom manager for the JournalEntryModel that extends Django's default
|
|
194
|
+
Manager with additional query features. It allows complex query handling
|
|
195
|
+
based on relationships to the `EntityModel` and the authenticated `UserModel`.
|
|
196
|
+
|
|
197
|
+
This manager provides utility methods for generating filtered querysets
|
|
198
|
+
(e.g., entries associated with specific users or entities), as well as
|
|
199
|
+
annotations for convenience in query results.
|
|
148
200
|
"""
|
|
149
201
|
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
202
|
+
def get_queryset(self) -> JournalEntryModelQuerySet:
|
|
203
|
+
"""
|
|
204
|
+
Returns the default queryset for JournalEntryModel with additional
|
|
205
|
+
annotations applied.
|
|
206
|
+
|
|
207
|
+
Annotations:
|
|
208
|
+
- `_entity_slug`: The slug of the related `EntityModel`.
|
|
209
|
+
- `txs_count`: The count of transactions (related `TransactionModel` instances)
|
|
210
|
+
for each journal entry.
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
JournalEntryModelQuerySet
|
|
215
|
+
A custom queryset enhanced with annotations.
|
|
216
|
+
"""
|
|
217
|
+
qs = JournalEntryModelQuerySet(self.model, using=self._db)
|
|
218
|
+
return qs.annotate(
|
|
219
|
+
_entity_uuid=F('ledger__entity_id'),
|
|
220
|
+
_entity_slug=F('ledger__entity__slug'), # Annotates the entity slug
|
|
221
|
+
_entity_last_closing_date=F('ledger__entity__last_closing_date'),
|
|
222
|
+
_ledger_is_locked=F('ledger__locked'),
|
|
223
|
+
txs_count=Count('transactionmodel') # Annotates the count of transactions
|
|
157
224
|
)
|
|
158
225
|
|
|
159
|
-
def
|
|
226
|
+
def for_user(self, user_model) -> JournalEntryModelQuerySet:
|
|
160
227
|
"""
|
|
161
|
-
|
|
162
|
-
|
|
228
|
+
Filters the JournalEntryModel queryset for the given user.
|
|
229
|
+
|
|
230
|
+
- Superusers will have access to all journal entries.
|
|
231
|
+
- Other authenticated users will only see entries for entities where
|
|
232
|
+
they are admins or managers.
|
|
163
233
|
|
|
164
234
|
Parameters
|
|
165
235
|
----------
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
user_model
|
|
169
|
-
Logged in and authenticated django UserModel instance.
|
|
170
|
-
|
|
171
|
-
Examples
|
|
172
|
-
--------
|
|
173
|
-
>>> request_user = request.user
|
|
174
|
-
>>> slug = kwargs['entity_slug'] # may come from request kwargs
|
|
175
|
-
>>> journal_entry_qs = JournalEntryModel.objects.for_entity(user_model=request_user, entity_slug=slug)
|
|
236
|
+
user_model : UserModel
|
|
237
|
+
An authenticated Django user object.
|
|
176
238
|
|
|
177
239
|
Returns
|
|
178
|
-
|
|
240
|
+
-------
|
|
179
241
|
JournalEntryModelQuerySet
|
|
180
|
-
|
|
242
|
+
A filtered queryset restricted by the user's entity relationships.
|
|
181
243
|
"""
|
|
182
|
-
qs = self.
|
|
183
|
-
if
|
|
184
|
-
return qs
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
Q(
|
|
244
|
+
qs = self.get_queryset()
|
|
245
|
+
if user_model.is_superuser:
|
|
246
|
+
return qs
|
|
247
|
+
|
|
248
|
+
return qs.filter(
|
|
249
|
+
Q(ledger__entity__admin=user_model) | # Entries for entities where the user is admin
|
|
250
|
+
Q(ledger__entity__managers__in=[user_model]) # Entries for entities where the user is a manager
|
|
189
251
|
)
|
|
190
252
|
|
|
191
|
-
def
|
|
253
|
+
def for_entity(self, entity_slug: Union[str, EntityModel], user_model) -> JournalEntryModelQuerySet:
|
|
192
254
|
"""
|
|
193
|
-
|
|
194
|
-
|
|
255
|
+
Filters the JournalEntryModel queryset for a specific entity and user.
|
|
256
|
+
|
|
257
|
+
This method provides a way to fetch journal entries related to a specific
|
|
258
|
+
`EntityModel`, identified by its slug or model instance, with additional
|
|
259
|
+
filtering scoped to the user.
|
|
195
260
|
|
|
196
261
|
Parameters
|
|
197
|
-
|
|
198
|
-
entity_slug: str or EntityModel
|
|
199
|
-
The entity
|
|
200
|
-
user_model
|
|
201
|
-
|
|
202
|
-
ledger_pk: str or UUID
|
|
203
|
-
The LedgerModel uuid as a string or UUID.
|
|
204
|
-
|
|
205
|
-
Examples
|
|
206
|
-
________
|
|
207
|
-
>>> request_user = request.user
|
|
208
|
-
>>> slug = kwargs['entity_slug'] # may come from request kwargs
|
|
209
|
-
>>> ledger_pk = kwargs['ledger_pk'] # may come from request kwargs
|
|
210
|
-
>>> journal_entry_qs = JournalEntryModel.objects.for_ledger(ledger_pk=ledger_pk, user_model=request_user, entity_slug=slug)
|
|
262
|
+
----------
|
|
263
|
+
entity_slug : str or EntityModel
|
|
264
|
+
The slug of the entity (or an instance of `EntityModel`) used for filtering.
|
|
265
|
+
user_model : UserModel
|
|
266
|
+
An authenticated Django user object.
|
|
211
267
|
|
|
212
268
|
Returns
|
|
213
|
-
|
|
269
|
+
-------
|
|
214
270
|
JournalEntryModelQuerySet
|
|
215
|
-
|
|
271
|
+
A customized queryset containing journal entries associated with the
|
|
272
|
+
given entity and restricted by the user's access permissions.
|
|
216
273
|
"""
|
|
217
|
-
qs = self.
|
|
218
|
-
|
|
274
|
+
qs = self.for_user(user_model)
|
|
275
|
+
|
|
276
|
+
# Handle the `entity_slug` as either a string or an EntityModel instance
|
|
277
|
+
if isinstance(entity_slug, EntityModel):
|
|
278
|
+
return qs.filter(ledger__entity=entity_slug)
|
|
279
|
+
|
|
280
|
+
return qs.filter(ledger__entity__slug__iexact=entity_slug) # Case-insensitive slug match
|
|
219
281
|
|
|
220
282
|
|
|
221
283
|
class ActivityEnum(Enum):
|
|
222
284
|
"""
|
|
223
|
-
|
|
285
|
+
Represents the database prefixes used for different types of accounting activities.
|
|
224
286
|
|
|
225
287
|
Attributes
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
FINANCING: str
|
|
235
|
-
The database representation prefix of a Journal Entry that is an Financing Activity.
|
|
288
|
+
----------
|
|
289
|
+
OPERATING : str
|
|
290
|
+
Prefix for a Journal Entry categorized as an Operating Activity.
|
|
291
|
+
INVESTING : str
|
|
292
|
+
Prefix for a Journal Entry categorized as an Investing Activity.
|
|
293
|
+
FINANCING : str
|
|
294
|
+
Prefix for a Journal Entry categorized as a Financing Activity.
|
|
236
295
|
"""
|
|
237
296
|
OPERATING = 'op'
|
|
238
297
|
INVESTING = 'inv'
|
|
@@ -241,51 +300,50 @@ class ActivityEnum(Enum):
|
|
|
241
300
|
|
|
242
301
|
class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
243
302
|
"""
|
|
244
|
-
|
|
303
|
+
Abstract base model for handling journal entries in the bookkeeping system.
|
|
245
304
|
|
|
246
305
|
Attributes
|
|
247
306
|
----------
|
|
248
|
-
uuid: UUID
|
|
249
|
-
|
|
250
|
-
je_number: str
|
|
251
|
-
A
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
A
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
ledger: LedgerModel
|
|
275
|
-
The LedgerModel associated with this JournalEntryModel. Cannot be null.
|
|
307
|
+
uuid : UUID
|
|
308
|
+
A unique identifier (primary key) for the journal entry, generated using uuid4().
|
|
309
|
+
je_number : str
|
|
310
|
+
A human-readable, unique, alphanumeric identifier for the journal entry (e.g., Voucher or Document Number).
|
|
311
|
+
Includes the fiscal year as a prefix for organizational purposes.
|
|
312
|
+
timestamp : datetime
|
|
313
|
+
The date of the journal entry, used for financial statements. This timestamp applies to associated transactions.
|
|
314
|
+
description : str
|
|
315
|
+
An optional user-defined description for the journal entry.
|
|
316
|
+
entity_unit : EntityUnitModel
|
|
317
|
+
A reference to a logical and self-contained structure within the `EntityModel`.
|
|
318
|
+
Provides context for the journal entry. See `EntityUnitModel` documentation for details.
|
|
319
|
+
activity : str
|
|
320
|
+
Indicates the nature of the activity associated with the journal entry.
|
|
321
|
+
Must be one of the predefined `ACTIVITIES` (e.g., Operating, Financing, Investing) and is programmatically determined.
|
|
322
|
+
origin : str
|
|
323
|
+
Describes the origin or trigger for the journal entry (e.g., reconciliations, migrations, auto-generated).
|
|
324
|
+
Max length: 30 characters.
|
|
325
|
+
posted : bool
|
|
326
|
+
Determines whether the journal entry has been posted (affecting the books). Defaults to `False`.
|
|
327
|
+
locked : bool
|
|
328
|
+
Indicates whether the journal entry is locked, preventing further modifications. Defaults to `False`.
|
|
329
|
+
ledger : LedgerModel
|
|
330
|
+
A reference to the LedgerModel associated with this journal entry. This field is mandatory.
|
|
331
|
+
is_closing_entry : bool
|
|
332
|
+
Indicates if the journal entry is a closing entry. Defaults to `False`.
|
|
276
333
|
"""
|
|
334
|
+
|
|
335
|
+
# Constants for activity types
|
|
277
336
|
OPERATING_ACTIVITY = ActivityEnum.OPERATING.value
|
|
278
337
|
FINANCING_OTHER = ActivityEnum.FINANCING.value
|
|
279
338
|
INVESTING_OTHER = ActivityEnum.INVESTING.value
|
|
280
|
-
|
|
281
339
|
INVESTING_SECURITIES = f'{ActivityEnum.INVESTING.value}_securities'
|
|
282
340
|
INVESTING_PPE = f'{ActivityEnum.INVESTING.value}_ppe'
|
|
283
|
-
|
|
284
341
|
FINANCING_STD = f'{ActivityEnum.FINANCING.value}_std'
|
|
285
342
|
FINANCING_LTD = f'{ActivityEnum.FINANCING.value}_ltd'
|
|
286
343
|
FINANCING_EQUITY = f'{ActivityEnum.FINANCING.value}_equity'
|
|
287
344
|
FINANCING_DIVIDENDS = f'{ActivityEnum.FINANCING.value}_dividends'
|
|
288
345
|
|
|
346
|
+
# Activity categories for dropdown
|
|
289
347
|
ACTIVITIES = [
|
|
290
348
|
(_('Operating'), (
|
|
291
349
|
(OPERATING_ACTIVITY, _('Operating')),
|
|
@@ -304,36 +362,43 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
304
362
|
)),
|
|
305
363
|
]
|
|
306
364
|
|
|
365
|
+
# Utility mappings for activity validation
|
|
307
366
|
VALID_ACTIVITIES = list(chain.from_iterable([[a[0] for a in cat[1]] for cat in ACTIVITIES]))
|
|
308
367
|
MAP_ACTIVITIES = dict(chain.from_iterable([[(a[0], cat[0]) for a in cat[1]] for cat in ACTIVITIES]))
|
|
309
368
|
NON_OPERATIONAL_ACTIVITIES = [a for a in VALID_ACTIVITIES if ActivityEnum.OPERATING.value not in a]
|
|
310
369
|
|
|
370
|
+
# Field definitions
|
|
311
371
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
312
372
|
je_number = models.SlugField(max_length=25, editable=False, verbose_name=_('Journal Entry Number'))
|
|
313
373
|
timestamp = models.DateTimeField(verbose_name=_('Timestamp'), default=localtime)
|
|
314
374
|
description = models.CharField(max_length=70, blank=True, null=True, verbose_name=_('Description'))
|
|
315
|
-
entity_unit = models.ForeignKey(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
375
|
+
entity_unit = models.ForeignKey(
|
|
376
|
+
'django_ledger.EntityUnitModel',
|
|
377
|
+
on_delete=models.RESTRICT,
|
|
378
|
+
blank=True,
|
|
379
|
+
null=True,
|
|
380
|
+
verbose_name=_('Associated Entity Unit')
|
|
381
|
+
)
|
|
382
|
+
activity = models.CharField(
|
|
383
|
+
choices=ACTIVITIES,
|
|
384
|
+
max_length=20,
|
|
385
|
+
null=True,
|
|
386
|
+
blank=True,
|
|
387
|
+
editable=False,
|
|
388
|
+
verbose_name=_('Activity')
|
|
389
|
+
)
|
|
326
390
|
origin = models.CharField(max_length=30, blank=True, null=True, verbose_name=_('Origin'))
|
|
327
391
|
posted = models.BooleanField(default=False, verbose_name=_('Posted'))
|
|
328
392
|
locked = models.BooleanField(default=False, verbose_name=_('Locked'))
|
|
329
393
|
is_closing_entry = models.BooleanField(default=False)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
394
|
+
ledger = models.ForeignKey(
|
|
395
|
+
'django_ledger.LedgerModel',
|
|
396
|
+
verbose_name=_('Ledger'),
|
|
397
|
+
related_name='journal_entries',
|
|
398
|
+
on_delete=models.CASCADE
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Custom manager
|
|
337
402
|
objects = JournalEntryModelManager.from_queryset(queryset_class=JournalEntryModelQuerySet)()
|
|
338
403
|
|
|
339
404
|
class Meta:
|
|
@@ -352,157 +417,225 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
352
417
|
models.Index(fields=['is_closing_entry']),
|
|
353
418
|
]
|
|
354
419
|
|
|
355
|
-
def __str__(self):
|
|
356
|
-
if self.je_number:
|
|
357
|
-
return 'JE: {x1} (posted={p}, locked={l}) - Desc: {x2}'.format(
|
|
358
|
-
x1=self.je_number,
|
|
359
|
-
x2=self.description,
|
|
360
|
-
p=self.posted,
|
|
361
|
-
l=self.locked
|
|
362
|
-
)
|
|
363
|
-
return 'JE ID: {x1} (posted={p}, locked={l}) - Desc: {x2}'.format(
|
|
364
|
-
x1=self.pk,
|
|
365
|
-
x2=self.description,
|
|
366
|
-
p=self.posted,
|
|
367
|
-
l=self.locked
|
|
368
|
-
)
|
|
369
|
-
|
|
370
420
|
def __init__(self, *args, **kwargs):
|
|
371
421
|
super().__init__(*args, **kwargs)
|
|
372
422
|
self._verified = False
|
|
373
|
-
self._last_closing_date: Optional[date] = None
|
|
374
423
|
|
|
375
|
-
def
|
|
424
|
+
def __str__(self):
|
|
425
|
+
if self.je_number:
|
|
426
|
+
return f"JE: {self.je_number} (posted={self.posted}, locked={self.locked}) - Desc: {self.description or ''}"
|
|
427
|
+
return f"JE ID: {self.pk} (posted={self.posted}, locked={self.locked}) - Desc: {self.description or ''}"
|
|
428
|
+
|
|
429
|
+
@property
|
|
430
|
+
def entity_uuid(self):
|
|
431
|
+
try:
|
|
432
|
+
return getattr(self, '_entity_uuid')
|
|
433
|
+
except AttributeError:
|
|
434
|
+
pass
|
|
435
|
+
return self.ledger.entity_id
|
|
436
|
+
|
|
437
|
+
@property
|
|
438
|
+
def entity_slug(self):
|
|
376
439
|
"""
|
|
377
|
-
|
|
440
|
+
Retrieves the unique slug associated with the entity.
|
|
441
|
+
|
|
442
|
+
The property first attempts to return the value stored in the `_entity_slug`
|
|
443
|
+
attribute if it exists. If `_entity_slug` is not set, it falls back to the
|
|
444
|
+
`ledger.entity.slug` attribute.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
str: The slug value from `_entity_slug` if available, or `ledger.entity.slug` otherwise.
|
|
448
|
+
"""
|
|
449
|
+
try:
|
|
450
|
+
return getattr(self, '_entity_slug')
|
|
451
|
+
except AttributeError:
|
|
452
|
+
pass
|
|
453
|
+
return self.ledger.entity.slug
|
|
454
|
+
|
|
455
|
+
@property
|
|
456
|
+
def entity_last_closing_date(self) -> Optional[date]:
|
|
457
|
+
"""
|
|
458
|
+
Retrieves the last closing date for an entity, if available.
|
|
459
|
+
|
|
460
|
+
This property returns the date of the most recent closing event
|
|
461
|
+
associated with the entity. If no closing date exists, the
|
|
462
|
+
result will be None.
|
|
463
|
+
|
|
464
|
+
Returns
|
|
465
|
+
-------
|
|
466
|
+
Optional[date]
|
|
467
|
+
The date of the last closing event, or None if no closing
|
|
468
|
+
date is available.
|
|
469
|
+
"""
|
|
470
|
+
return self.get_entity_last_closing_date()
|
|
471
|
+
|
|
472
|
+
def validate_for_entity(self, entity_model: Union[EntityModel, str, UUID], raise_exception: bool = True) -> bool:
|
|
473
|
+
"""
|
|
474
|
+
Validates whether the given entity_model owns thr Journal Entry instance.
|
|
475
|
+
|
|
476
|
+
This method checks if the provided entity_model owns the Journal Entry model instance.
|
|
477
|
+
The entity_model can be of type `EntityModel`, `str`, or
|
|
478
|
+
`UUID`. The method performs type-specific checks to ensure proper validation
|
|
479
|
+
and returns the validation result.
|
|
378
480
|
|
|
379
481
|
Parameters
|
|
380
482
|
----------
|
|
381
|
-
|
|
382
|
-
|
|
483
|
+
entity_model : Union[EntityModel, str, UUID]
|
|
484
|
+
The entity to validate against. It can either be an instance of the
|
|
485
|
+
`EntityModel`, a string representation of a UUID, or a UUID object.
|
|
383
486
|
|
|
384
487
|
Returns
|
|
385
488
|
-------
|
|
386
489
|
bool
|
|
387
|
-
True if
|
|
490
|
+
A boolean value. True if the given entity_model corresponds to the current
|
|
491
|
+
entity's UUID, otherwise False.
|
|
388
492
|
"""
|
|
493
|
+
if isinstance(entity_model, str):
|
|
494
|
+
is_valid = str(self.entity_uuid) == entity_model
|
|
495
|
+
elif isinstance(entity_model, UUID):
|
|
496
|
+
is_valid = self.entity_uuid == entity_model
|
|
497
|
+
else:
|
|
498
|
+
is_valid = self.entity_uuid == entity_model.uuid
|
|
389
499
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
not self.is_in_locked_period()
|
|
396
|
-
])
|
|
500
|
+
if not is_valid and raise_exception:
|
|
501
|
+
raise JournalEntryValidationError(
|
|
502
|
+
message='The Journal Entry does not belong to the provided entity.'
|
|
503
|
+
)
|
|
504
|
+
return is_valid
|
|
397
505
|
|
|
398
|
-
def
|
|
506
|
+
def ledger_is_locked(self):
|
|
399
507
|
"""
|
|
400
|
-
Determines
|
|
508
|
+
Determines whether the ledger is locked.
|
|
509
|
+
|
|
510
|
+
This method checks the current state of the ledger to determine if it is
|
|
511
|
+
locked and unavailable for further operations. It looks for an annotated
|
|
512
|
+
attribute `_ledger_is_locked` and returns its value if found. If the
|
|
513
|
+
attribute is not set, it delegates the check to the actual `is_locked`
|
|
514
|
+
method of the `ledger` object.
|
|
401
515
|
|
|
402
516
|
Returns
|
|
403
517
|
-------
|
|
404
518
|
bool
|
|
405
|
-
|
|
519
|
+
A boolean value indicating whether the ledger is locked.
|
|
406
520
|
"""
|
|
521
|
+
try:
|
|
522
|
+
return getattr(self, '_ledger_is_locked')
|
|
523
|
+
except AttributeError:
|
|
524
|
+
pass
|
|
525
|
+
return self.ledger.is_locked()
|
|
526
|
+
|
|
527
|
+
def can_post(self, ignore_verify: bool = True) -> bool:
|
|
528
|
+
"""Determines if the journal entry can be posted."""
|
|
529
|
+
return all([
|
|
530
|
+
self.is_locked(),
|
|
531
|
+
not self.is_posted(),
|
|
532
|
+
self.is_verified() if not ignore_verify else True, # avoids db queries, will be verified before saving
|
|
533
|
+
not self.ledger_is_locked(),
|
|
534
|
+
not self.is_in_locked_period()
|
|
535
|
+
])
|
|
536
|
+
|
|
537
|
+
def can_unpost(self) -> bool:
|
|
538
|
+
"""Checks if the journal entry can be un-posted."""
|
|
407
539
|
return all([
|
|
408
540
|
self.is_posted(),
|
|
409
|
-
not self.
|
|
541
|
+
not self.ledger_is_locked(),
|
|
410
542
|
not self.is_in_locked_period()
|
|
411
543
|
])
|
|
412
544
|
|
|
413
545
|
def can_lock(self) -> bool:
|
|
414
|
-
"""
|
|
415
|
-
Determines if a JournalEntryModel can be locked.
|
|
416
|
-
Locked JournalEntryModels cannot be modified.
|
|
417
|
-
|
|
418
|
-
Returns
|
|
419
|
-
-------
|
|
420
|
-
bool
|
|
421
|
-
True if JournalEntryModel can be locked, otherwise False.
|
|
422
|
-
"""
|
|
546
|
+
"""Determines if the journal entry can be locked."""
|
|
423
547
|
return all([
|
|
424
548
|
not self.is_locked(),
|
|
425
|
-
not self.
|
|
549
|
+
not self.ledger_is_locked()
|
|
426
550
|
])
|
|
427
551
|
|
|
428
552
|
def can_unlock(self) -> bool:
|
|
429
|
-
"""
|
|
430
|
-
Determines if a JournalEntryModel can be un-locked.
|
|
431
|
-
Locked transactions cannot be modified.
|
|
432
|
-
|
|
433
|
-
Returns
|
|
434
|
-
-------
|
|
435
|
-
bool
|
|
436
|
-
True if JournalEntryModel can be un-locked, otherwise False.
|
|
437
|
-
"""
|
|
553
|
+
"""Checks if the journal entry can be unlocked."""
|
|
438
554
|
return all([
|
|
439
555
|
self.is_locked(),
|
|
440
556
|
not self.is_posted(),
|
|
441
557
|
not self.is_in_locked_period(),
|
|
442
|
-
not self.
|
|
558
|
+
not self.ledger_is_locked()
|
|
443
559
|
])
|
|
444
560
|
|
|
445
561
|
def can_delete(self) -> bool:
|
|
562
|
+
"""Checks if the journal entry can be deleted."""
|
|
446
563
|
return all([
|
|
447
564
|
not self.is_locked(),
|
|
448
565
|
not self.is_posted(),
|
|
449
566
|
])
|
|
450
567
|
|
|
451
568
|
def can_edit(self) -> bool:
|
|
452
|
-
|
|
569
|
+
"""Checks if the journal entry is editable."""
|
|
570
|
+
return all([
|
|
571
|
+
not self.is_locked(),
|
|
572
|
+
|
|
573
|
+
])
|
|
453
574
|
|
|
454
|
-
def is_posted(self):
|
|
575
|
+
def is_posted(self) -> bool:
|
|
576
|
+
"""Returns whether the journal entry has been posted."""
|
|
455
577
|
return self.posted is True
|
|
456
578
|
|
|
457
579
|
def is_in_locked_period(self, new_timestamp: Optional[Union[date, datetime]] = None) -> bool:
|
|
458
|
-
|
|
580
|
+
"""
|
|
581
|
+
Checks if the current Journal Entry falls within a locked period.
|
|
582
|
+
|
|
583
|
+
Parameters
|
|
584
|
+
----------
|
|
585
|
+
new_timestamp: Optional[Union[date, datetime]])
|
|
586
|
+
An optional date or timestamp to be checked instead of the current timestamp.
|
|
587
|
+
|
|
588
|
+
Returns
|
|
589
|
+
-------
|
|
590
|
+
bool: True if the Journal Entry is in a locked period, otherwise False.
|
|
591
|
+
"""
|
|
592
|
+
last_closing_date = self.entity_last_closing_date
|
|
459
593
|
if last_closing_date is not None:
|
|
460
594
|
if not new_timestamp:
|
|
461
595
|
return last_closing_date >= self.timestamp.date()
|
|
462
596
|
elif isinstance(new_timestamp, datetime):
|
|
463
597
|
return last_closing_date >= new_timestamp.date()
|
|
464
|
-
|
|
465
|
-
return last_closing_date >= new_timestamp
|
|
598
|
+
return last_closing_date >= new_timestamp
|
|
466
599
|
return False
|
|
467
600
|
|
|
468
|
-
def is_locked(self):
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
601
|
+
def is_locked(self) -> bool:
|
|
602
|
+
"""
|
|
603
|
+
Determines if the Journal Entry is locked.
|
|
604
|
+
|
|
605
|
+
A Journal Entry is considered locked if it is posted, explicitly marked
|
|
606
|
+
as locked, falls within a locked period, or the associated ledger is locked.
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
bool: True if the Journal Entry is locked, otherwise False.
|
|
610
|
+
"""
|
|
611
|
+
return self.is_posted() or any([
|
|
612
|
+
self.locked,
|
|
613
|
+
self.is_in_locked_period(),
|
|
614
|
+
self.ledger_is_locked()
|
|
477
615
|
])
|
|
478
616
|
|
|
479
617
|
def is_verified(self) -> bool:
|
|
480
618
|
"""
|
|
481
|
-
|
|
619
|
+
Checks if the Journal Entry is verified.
|
|
482
620
|
|
|
483
|
-
Returns
|
|
484
|
-
|
|
485
|
-
bool
|
|
486
|
-
True if is verified, otherwise False.
|
|
621
|
+
Returns:
|
|
622
|
+
bool: True if the Journal Entry is verified, otherwise False.
|
|
487
623
|
"""
|
|
488
624
|
return self._verified
|
|
489
625
|
|
|
490
|
-
# Transaction QuerySet
|
|
491
626
|
def is_balance_valid(self, txs_qs: TransactionModelQuerySet, raise_exception: bool = True) -> bool:
|
|
492
627
|
"""
|
|
493
|
-
|
|
628
|
+
Validates whether the DEBITs and CREDITs of the transactions balance correctly.
|
|
494
629
|
|
|
495
|
-
Parameters
|
|
496
|
-
|
|
497
|
-
|
|
630
|
+
Parameters:
|
|
631
|
+
txs_qs (TransactionModelQuerySet): A QuerySet containing transactions to validate.
|
|
632
|
+
raise_exception (bool): Whether to raise a JournalEntryValidationError if the validation fails.
|
|
498
633
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
bool
|
|
505
|
-
True if JE balances are valid (i.e. are equal).
|
|
634
|
+
Returns:
|
|
635
|
+
bool: True if the transactions are balanced, otherwise False.
|
|
636
|
+
|
|
637
|
+
Raises:
|
|
638
|
+
JournalEntryValidationError: If the transactions are not balanced and raise_exception is True.
|
|
506
639
|
"""
|
|
507
640
|
if len(txs_qs) > 0:
|
|
508
641
|
balances = self.get_txs_balances(txs_qs=txs_qs, as_dict=True)
|
|
@@ -519,65 +652,78 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
519
652
|
return is_valid
|
|
520
653
|
return True
|
|
521
654
|
|
|
522
|
-
def is_txs_qs_coa_valid(self, txs_qs: TransactionModelQuerySet) -> bool:
|
|
655
|
+
def is_txs_qs_coa_valid(self, txs_qs: TransactionModelQuerySet, raise_exception: bool = True) -> bool:
|
|
523
656
|
"""
|
|
524
|
-
Validates that the Chart of Accounts (COA)
|
|
525
|
-
Journal Entry transactions can only be associated with one Chart of Accounts (COA).
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
Parameters
|
|
529
|
-
----------
|
|
530
|
-
txs_qs: TransactionModelQuerySet
|
|
657
|
+
Validates that all transactions in the QuerySet are associated with the same Chart of Accounts (COA).
|
|
531
658
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
True if Transaction CoAs are valid, otherwise False.
|
|
535
|
-
"""
|
|
659
|
+
Parameters:
|
|
660
|
+
txs_qs (TransactionModelQuerySet): A QuerySet containing transactions to validate.
|
|
536
661
|
|
|
662
|
+
Returns:
|
|
663
|
+
bool: True if all transactions have the same Chart of Accounts, otherwise False.
|
|
664
|
+
"""
|
|
537
665
|
if len(txs_qs) > 0:
|
|
538
666
|
coa_count = len(set(tx.coa_id for tx in txs_qs))
|
|
539
|
-
|
|
667
|
+
is_valid = coa_count == 1
|
|
668
|
+
if not is_valid and raise_exception:
|
|
669
|
+
raise JournalEntryValidationError(
|
|
670
|
+
message='All transactions in the QuerySet must be associated with the same Chart of Accounts.'
|
|
671
|
+
)
|
|
672
|
+
return is_valid
|
|
540
673
|
return True
|
|
541
674
|
|
|
542
675
|
def is_txs_qs_valid(self, txs_qs: TransactionModelQuerySet, raise_exception: bool = True) -> bool:
|
|
543
676
|
"""
|
|
544
|
-
Validates
|
|
677
|
+
Validates whether the given Transaction QuerySet belongs to the current Journal Entry.
|
|
545
678
|
|
|
546
|
-
Parameters
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
The queryset to validate.
|
|
679
|
+
Parameters:
|
|
680
|
+
txs_qs (TransactionModelQuerySet): A QuerySet containing transactions to validate.
|
|
681
|
+
raise_exception (bool): Whether to raise a JournalEntryValidationError if the validation fails.
|
|
550
682
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
Raises
|
|
555
|
-
------
|
|
556
|
-
JournalEntryValidationError if JE model is invalid and raise_exception is True.
|
|
683
|
+
Returns:
|
|
684
|
+
bool: True if all transactions belong to the Journal Entry, otherwise False.
|
|
557
685
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
bool
|
|
561
|
-
True if valid, otherwise False.
|
|
686
|
+
Raises:
|
|
687
|
+
JournalEntryValidationError: If validation fails and raise_exception is True.
|
|
562
688
|
"""
|
|
563
689
|
if not isinstance(txs_qs, TransactionModelQuerySet):
|
|
564
690
|
raise JournalEntryValidationError('Must pass an instance of TransactionModelQuerySet')
|
|
565
691
|
|
|
566
692
|
is_valid = all(tx.journal_entry_id == self.uuid for tx in txs_qs)
|
|
567
693
|
if not is_valid and raise_exception:
|
|
568
|
-
raise JournalEntryValidationError(
|
|
569
|
-
|
|
694
|
+
raise JournalEntryValidationError(
|
|
695
|
+
f'Invalid TransactionModelQuerySet. All transactions must be associated with Journal Entry {self.uuid}.'
|
|
696
|
+
)
|
|
570
697
|
return is_valid
|
|
571
698
|
|
|
572
|
-
def is_cash_involved(self, txs_qs=None):
|
|
573
|
-
|
|
699
|
+
def is_cash_involved(self, txs_qs: Optional[TransactionModelQuerySet] = None) -> bool:
|
|
700
|
+
"""
|
|
701
|
+
Checks if the transactions involve cash assets.
|
|
574
702
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
self.OPERATING_ACTIVITY
|
|
578
|
-
]
|
|
703
|
+
Parameters:
|
|
704
|
+
txs_qs (Optional[TransactionModelQuerySet]): Transactions to evaluate. If None, defaults to class behavior.
|
|
579
705
|
|
|
580
|
-
|
|
706
|
+
Returns:
|
|
707
|
+
bool: True if cash assets are involved, otherwise False.
|
|
708
|
+
"""
|
|
709
|
+
return roles.ASSET_CA_CASH in self.get_txs_roles(txs_qs)
|
|
710
|
+
|
|
711
|
+
def is_operating(self) -> bool:
|
|
712
|
+
"""
|
|
713
|
+
Checks if the Journal Entry is categorized as an operating activity.
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
bool: True if the activity is operating, otherwise False.
|
|
717
|
+
"""
|
|
718
|
+
return self.activity in [self.OPERATING_ACTIVITY]
|
|
719
|
+
|
|
720
|
+
def is_financing(self) -> bool:
|
|
721
|
+
"""
|
|
722
|
+
Checks if the Journal Entry is categorized as a financing activity.
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
bool: True if the activity is financing, otherwise False.
|
|
726
|
+
"""
|
|
581
727
|
return self.activity in [
|
|
582
728
|
self.FINANCING_EQUITY,
|
|
583
729
|
self.FINANCING_LTD,
|
|
@@ -586,19 +732,44 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
586
732
|
self.FINANCING_OTHER
|
|
587
733
|
]
|
|
588
734
|
|
|
589
|
-
def is_investing(self):
|
|
735
|
+
def is_investing(self) -> bool:
|
|
736
|
+
"""
|
|
737
|
+
Checks if the Journal Entry is categorized as an investing activity.
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
bool: True if the activity is investing, otherwise False.
|
|
741
|
+
"""
|
|
590
742
|
return self.activity in [
|
|
591
743
|
self.INVESTING_SECURITIES,
|
|
592
744
|
self.INVESTING_PPE,
|
|
593
745
|
self.INVESTING_OTHER
|
|
594
746
|
]
|
|
595
747
|
|
|
596
|
-
def get_entity_unit_name(self, no_unit_name: str =
|
|
748
|
+
def get_entity_unit_name(self, no_unit_name: str = "") -> str:
|
|
749
|
+
"""
|
|
750
|
+
Retrieves the name of the entity unit associated with the Journal Entry.
|
|
751
|
+
|
|
752
|
+
Parameters:
|
|
753
|
+
no_unit_name (str): The fallback name to return if no unit is associated.
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
str: The name of the entity unit, or the fallback provided.
|
|
757
|
+
"""
|
|
597
758
|
if self.entity_unit_id:
|
|
598
759
|
return self.entity_unit.name
|
|
599
760
|
return no_unit_name
|
|
600
761
|
|
|
601
762
|
def get_entity_last_closing_date(self) -> Optional[date]:
|
|
763
|
+
"""
|
|
764
|
+
Retrieves the last closing date for the entity associated with the Journal Entry.
|
|
765
|
+
|
|
766
|
+
Returns:
|
|
767
|
+
Optional[date]: The last closing date if one exists, otherwise None.
|
|
768
|
+
"""
|
|
769
|
+
try:
|
|
770
|
+
return getattr(self, '_entity_last_closing_date')
|
|
771
|
+
except AttributeError:
|
|
772
|
+
pass
|
|
602
773
|
return self.ledger.entity.last_closing_date
|
|
603
774
|
|
|
604
775
|
def mark_as_posted(self,
|
|
@@ -609,21 +780,16 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
609
780
|
**kwargs):
|
|
610
781
|
"""
|
|
611
782
|
Posted transactions show on the EntityModel ledger and financial statements.
|
|
612
|
-
|
|
613
783
|
Parameters
|
|
614
784
|
----------
|
|
615
785
|
commit: bool
|
|
616
786
|
Commits changes into the Database, Defaults to False.
|
|
617
|
-
|
|
618
787
|
verify: bool
|
|
619
788
|
Verifies JournalEntryModel before marking as posted. Defaults to False.
|
|
620
|
-
|
|
621
789
|
force_lock: bool
|
|
622
790
|
Forces to lock the JournalEntry before is posted.
|
|
623
|
-
|
|
624
791
|
raise_exception: bool
|
|
625
792
|
Raises JournalEntryValidationError if cannot post. Defaults to False.
|
|
626
|
-
|
|
627
793
|
kwargs: dict
|
|
628
794
|
Additional keyword arguments.
|
|
629
795
|
"""
|
|
@@ -635,7 +801,6 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
635
801
|
message=_('Cannot post an empty Journal Entry.')
|
|
636
802
|
)
|
|
637
803
|
return
|
|
638
|
-
|
|
639
804
|
if force_lock and not self.is_locked():
|
|
640
805
|
try:
|
|
641
806
|
self.mark_as_locked(commit=False, raise_exception=True)
|
|
@@ -643,13 +808,11 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
643
808
|
if raise_exception:
|
|
644
809
|
raise e
|
|
645
810
|
return
|
|
646
|
-
|
|
647
811
|
if not self.can_post(ignore_verify=False):
|
|
648
812
|
if raise_exception:
|
|
649
813
|
raise JournalEntryValidationError(f'Journal Entry {self.uuid} cannot post.'
|
|
650
814
|
f' Is verified: {self.is_verified()}')
|
|
651
815
|
return
|
|
652
|
-
|
|
653
816
|
if not self.is_posted():
|
|
654
817
|
self.posted = True
|
|
655
818
|
if self.is_posted():
|
|
@@ -680,10 +843,8 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
680
843
|
----------
|
|
681
844
|
commit: bool
|
|
682
845
|
Commits changes into the Database, Defaults to False.
|
|
683
|
-
|
|
684
846
|
raise_exception: bool
|
|
685
847
|
Raises JournalEntryValidationError if cannot post. Defaults to False.
|
|
686
|
-
|
|
687
848
|
kwargs: dict
|
|
688
849
|
Additional keyword arguments.
|
|
689
850
|
"""
|
|
@@ -723,10 +884,8 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
723
884
|
----------
|
|
724
885
|
commit: bool
|
|
725
886
|
Commits changes into the Database, Defaults to False.
|
|
726
|
-
|
|
727
887
|
raise_exception: bool
|
|
728
888
|
Raises JournalEntryValidationError if cannot lock. Defaults to False.
|
|
729
|
-
|
|
730
889
|
kwargs: dict
|
|
731
890
|
Additional keyword arguments.
|
|
732
891
|
"""
|
|
@@ -735,7 +894,6 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
735
894
|
if raise_exception:
|
|
736
895
|
raise JournalEntryValidationError(f'Journal Entry {self.uuid} is already locked.')
|
|
737
896
|
return
|
|
738
|
-
|
|
739
897
|
if not self.is_locked():
|
|
740
898
|
self.generate_activity(force_update=True)
|
|
741
899
|
self.locked = True
|
|
@@ -770,9 +928,9 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
770
928
|
if raise_exception:
|
|
771
929
|
raise JournalEntryValidationError(f'Journal Entry {self.uuid} is already unlocked.')
|
|
772
930
|
return
|
|
773
|
-
|
|
774
931
|
if self.is_locked():
|
|
775
932
|
self.locked = False
|
|
933
|
+
self.activity = None
|
|
776
934
|
if not self.is_locked():
|
|
777
935
|
if commit:
|
|
778
936
|
self.save(verify=False)
|
|
@@ -789,138 +947,129 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
789
947
|
|
|
790
948
|
def get_transaction_queryset(self, select_accounts: bool = True) -> TransactionModelQuerySet:
|
|
791
949
|
"""
|
|
792
|
-
|
|
950
|
+
Retrieves the `TransactionModelQuerySet` associated with this `JournalEntryModel` instance.
|
|
793
951
|
|
|
794
952
|
Parameters
|
|
795
953
|
----------
|
|
796
|
-
select_accounts: bool
|
|
797
|
-
|
|
954
|
+
select_accounts : bool, optional
|
|
955
|
+
If True, prefetches the related `AccountModel` for each transaction. Defaults to True.
|
|
798
956
|
|
|
799
957
|
Returns
|
|
800
958
|
-------
|
|
801
959
|
TransactionModelQuerySet
|
|
802
|
-
|
|
960
|
+
A queryset containing transactions related to this journal entry. If `select_accounts` is
|
|
961
|
+
True, the accounts are included in the query as well.
|
|
803
962
|
"""
|
|
804
963
|
if select_accounts:
|
|
805
964
|
return self.transactionmodel_set.all().select_related('account')
|
|
806
965
|
return self.transactionmodel_set.all()
|
|
807
966
|
|
|
808
|
-
def get_txs_balances(
|
|
809
|
-
|
|
810
|
-
|
|
967
|
+
def get_txs_balances(
|
|
968
|
+
self,
|
|
969
|
+
txs_qs: Optional[TransactionModelQuerySet] = None,
|
|
970
|
+
as_dict: bool = False
|
|
971
|
+
) -> Union[TransactionModelQuerySet, Dict[str, Decimal]]:
|
|
811
972
|
"""
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
973
|
+
Calculates the total CREDIT and DEBIT balances for the journal entry.
|
|
974
|
+
|
|
975
|
+
This method performs an aggregate database query to compute the sum of CREDITs and
|
|
976
|
+
DEBITs across the transactions related to this journal entry. Optionally, a pre-fetched
|
|
977
|
+
`TransactionModelQuerySet` can be supplied for efficiency. Validation is performed to
|
|
978
|
+
ensure that all transactions belong to this journal entry.
|
|
817
979
|
|
|
818
980
|
Parameters
|
|
819
981
|
----------
|
|
820
|
-
txs_qs: TransactionModelQuerySet
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
>>> je_model: JournalEntryModel = je_qs.first() # any existing JournalEntryModel QuerySet...
|
|
831
|
-
>>> balances = je_model.get_txs_balances()
|
|
832
|
-
>>> balances
|
|
833
|
-
Returns exactly two records:
|
|
834
|
-
<TransactionModelQuerySet [{'tx_type': 'credit', 'amount__sum': Decimal('2301.5')},
|
|
835
|
-
{'tx_type': 'debit', 'amount__sum': Decimal('2301.5')}]>
|
|
836
|
-
|
|
837
|
-
Examples
|
|
838
|
-
--------
|
|
839
|
-
>>> balances = je_model.get_txs_balances(as_dict=True)
|
|
840
|
-
>>> balances
|
|
841
|
-
Returns a dictionary:
|
|
842
|
-
{'credit': Decimal('2301.5'), 'debit': Decimal('2301.5')}
|
|
982
|
+
txs_qs : TransactionModelQuerySet, optional
|
|
983
|
+
A pre-fetched queryset of transactions. If None, the queryset is fetched automatically.
|
|
984
|
+
as_dict : bool, optional
|
|
985
|
+
If True, returns the results as a dictionary with keys "credit" and "debit". Defaults to False.
|
|
986
|
+
|
|
987
|
+
Returns
|
|
988
|
+
-------
|
|
989
|
+
Union[TransactionModelQuerySet, Dict[str, Decimal]]
|
|
990
|
+
If `as_dict` is False, returns a queryset of aggregated balances. If `as_dict` is True,
|
|
991
|
+
returns a dictionary containing the CREDIT and DEBIT totals.
|
|
843
992
|
|
|
844
993
|
Raises
|
|
845
994
|
------
|
|
846
995
|
JournalEntryValidationError
|
|
847
|
-
If
|
|
848
|
-
|
|
849
|
-
Returns
|
|
850
|
-
-------
|
|
851
|
-
TransactionModelQuerySet or dict
|
|
852
|
-
An aggregated queryset containing exactly two records. The total CREDIT or DEBIT amount as Decimal.
|
|
996
|
+
If the provided queryset is invalid or does not belong to this journal entry.
|
|
853
997
|
"""
|
|
854
998
|
if not txs_qs:
|
|
855
999
|
txs_qs = self.get_transaction_queryset(select_accounts=False)
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
if not is_valid:
|
|
865
|
-
raise JournalEntryValidationError(
|
|
866
|
-
message='Invalid Transaction QuerySet used. Must be from same Journal Entry'
|
|
867
|
-
)
|
|
1000
|
+
elif not isinstance(txs_qs, TransactionModelQuerySet):
|
|
1001
|
+
raise JournalEntryValidationError(
|
|
1002
|
+
f"Expected a TransactionModelQuerySet, got {type(txs_qs).__name__}"
|
|
1003
|
+
)
|
|
1004
|
+
elif not self.is_txs_qs_valid(txs_qs):
|
|
1005
|
+
raise JournalEntryValidationError(
|
|
1006
|
+
"Invalid TransactionModelQuerySet. All transactions must belong to the same journal entry."
|
|
1007
|
+
)
|
|
868
1008
|
|
|
869
1009
|
balances = txs_qs.values('tx_type').annotate(
|
|
870
|
-
amount__sum=Coalesce(Sum('amount'),
|
|
871
|
-
|
|
872
|
-
output_field=models.DecimalField()))
|
|
1010
|
+
amount__sum=Coalesce(Sum('amount'), Decimal('0.00'), output_field=models.DecimalField())
|
|
1011
|
+
)
|
|
873
1012
|
|
|
874
1013
|
if as_dict:
|
|
875
|
-
return {
|
|
876
|
-
tx['tx_type']: tx['amount__sum'] for tx in balances
|
|
877
|
-
}
|
|
1014
|
+
return {tx['tx_type']: tx['amount__sum'] for tx in balances}
|
|
878
1015
|
return balances
|
|
879
1016
|
|
|
880
|
-
def get_txs_roles(
|
|
881
|
-
|
|
882
|
-
|
|
1017
|
+
def get_txs_roles(
|
|
1018
|
+
self,
|
|
1019
|
+
txs_qs: Optional[TransactionModelQuerySet] = None,
|
|
1020
|
+
exclude_cash_role: bool = False
|
|
1021
|
+
) -> Set[str]:
|
|
883
1022
|
"""
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1023
|
+
Retrieves the set of account roles involved in the journal entry's transactions.
|
|
1024
|
+
|
|
1025
|
+
This method extracts the roles associated with the accounts linked to each transaction.
|
|
1026
|
+
Optionally, the CASH role can be excluded from the results.
|
|
887
1027
|
|
|
888
1028
|
Parameters
|
|
889
1029
|
----------
|
|
890
|
-
txs_qs: TransactionModelQuerySet
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
exclude_cash_role: bool
|
|
895
|
-
Removes CASH role from the Set if present.
|
|
896
|
-
Useful in some cases where cash role must be excluded for additional validation.
|
|
1030
|
+
txs_qs : TransactionModelQuerySet, optional
|
|
1031
|
+
A pre-fetched queryset of transactions. If None, the queryset is fetched automatically.
|
|
1032
|
+
exclude_cash_role : bool, optional
|
|
1033
|
+
If True, excludes the CASH role from the result. Defaults to False.
|
|
897
1034
|
|
|
898
1035
|
Returns
|
|
899
1036
|
-------
|
|
900
|
-
|
|
901
|
-
|
|
1037
|
+
Set[str]
|
|
1038
|
+
A set of account roles associated with this journal entry's transactions.
|
|
902
1039
|
"""
|
|
903
1040
|
if not txs_qs:
|
|
904
1041
|
txs_qs = self.get_transaction_queryset(select_accounts=True)
|
|
905
1042
|
else:
|
|
906
1043
|
self.is_txs_qs_valid(txs_qs)
|
|
907
1044
|
|
|
908
|
-
|
|
1045
|
+
roles = {tx.account.role for tx in txs_qs}
|
|
1046
|
+
|
|
909
1047
|
if exclude_cash_role:
|
|
910
|
-
|
|
911
|
-
|
|
1048
|
+
roles.discard(ASSET_CA_CASH)
|
|
1049
|
+
|
|
1050
|
+
return roles
|
|
912
1051
|
|
|
913
1052
|
def has_activity(self) -> bool:
|
|
1053
|
+
"""
|
|
1054
|
+
Checks if the journal entry has an associated activity.
|
|
1055
|
+
|
|
1056
|
+
Returns
|
|
1057
|
+
-------
|
|
1058
|
+
bool
|
|
1059
|
+
True if an activity is defined for the journal entry, otherwise False.
|
|
1060
|
+
"""
|
|
914
1061
|
return self.activity is not None
|
|
915
1062
|
|
|
916
1063
|
def get_activity_name(self) -> Optional[str]:
|
|
917
1064
|
"""
|
|
918
|
-
|
|
1065
|
+
Gets the name of the activity associated with this journal entry.
|
|
1066
|
+
|
|
1067
|
+
The activity indicates its categorization based on GAAP (e.g., operating, investing, financing).
|
|
919
1068
|
|
|
920
1069
|
Returns
|
|
921
1070
|
-------
|
|
922
|
-
str
|
|
923
|
-
|
|
1071
|
+
Optional[str]
|
|
1072
|
+
The activity name if defined, otherwise None.
|
|
924
1073
|
"""
|
|
925
1074
|
if self.activity:
|
|
926
1075
|
if self.is_operating():
|
|
@@ -929,13 +1078,38 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
929
1078
|
return ActivityEnum.INVESTING.value
|
|
930
1079
|
elif self.is_financing():
|
|
931
1080
|
return ActivityEnum.FINANCING.value
|
|
1081
|
+
return None
|
|
932
1082
|
|
|
933
1083
|
@classmethod
|
|
934
|
-
def get_activity_from_roles(
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1084
|
+
def get_activity_from_roles(
|
|
1085
|
+
cls,
|
|
1086
|
+
role_set: Union[List[str], Set[str]],
|
|
1087
|
+
validate: bool = False,
|
|
1088
|
+
raise_exception: bool = True
|
|
1089
|
+
) -> Optional[str]:
|
|
1090
|
+
"""
|
|
1091
|
+
Determines the financial activity type (e.g., operating, investing, financing)
|
|
1092
|
+
based on a set of account roles.
|
|
1093
|
+
|
|
1094
|
+
Parameters
|
|
1095
|
+
----------
|
|
1096
|
+
role_set : Union[List[str], Set[str]]
|
|
1097
|
+
The set of roles to analyze.
|
|
1098
|
+
validate : bool, optional
|
|
1099
|
+
If True, validates the roles before analysis. Defaults to False.
|
|
1100
|
+
raise_exception : bool, optional
|
|
1101
|
+
If True, raises an exception if multiple activities are detected. Defaults to True.
|
|
1102
|
+
|
|
1103
|
+
Returns
|
|
1104
|
+
-------
|
|
1105
|
+
Optional[str]
|
|
1106
|
+
The detected activity name, or None if no activity type is matched.
|
|
938
1107
|
|
|
1108
|
+
Raises
|
|
1109
|
+
------
|
|
1110
|
+
JournalEntryValidationError
|
|
1111
|
+
If multiple activities are detected and `raise_exception` is True.
|
|
1112
|
+
"""
|
|
939
1113
|
if validate:
|
|
940
1114
|
role_set = validate_roles(roles=role_set)
|
|
941
1115
|
else:
|
|
@@ -1014,39 +1188,68 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
1014
1188
|
txs_qs: Optional[TransactionModelQuerySet] = None,
|
|
1015
1189
|
raise_exception: bool = True,
|
|
1016
1190
|
force_update: bool = False) -> Optional[str]:
|
|
1191
|
+
"""
|
|
1192
|
+
Generates the activity for the Journal Entry model based on its transactions.
|
|
1017
1193
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
txs_is_valid = True
|
|
1028
|
-
if not txs_qs:
|
|
1029
|
-
txs_qs = self.get_transaction_queryset(select_accounts=False)
|
|
1030
|
-
else:
|
|
1031
|
-
try:
|
|
1032
|
-
txs_is_valid = self.is_txs_qs_valid(txs_qs=txs_qs, raise_exception=raise_exception)
|
|
1033
|
-
except JournalEntryValidationError as e:
|
|
1034
|
-
if raise_exception:
|
|
1035
|
-
raise e
|
|
1194
|
+
Parameters
|
|
1195
|
+
----------
|
|
1196
|
+
txs_qs : Optional[TransactionModelQuerySet], default None
|
|
1197
|
+
Queryset of TransactionModel instances for validation. If None, transactions are queried.
|
|
1198
|
+
raise_exception : bool, default True
|
|
1199
|
+
Determines whether exceptions are raised during processing.
|
|
1200
|
+
force_update : bool, default False
|
|
1201
|
+
Forces the regeneration of activity even if it exists.
|
|
1036
1202
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1203
|
+
Returns
|
|
1204
|
+
-------
|
|
1205
|
+
Optional[str]
|
|
1206
|
+
Generated activity or None if not applicable.
|
|
1207
|
+
"""
|
|
1208
|
+
if not self._state.adding:
|
|
1209
|
+
if raise_exception and self.is_closing_entry:
|
|
1210
|
+
raise_exception = False
|
|
1211
|
+
|
|
1212
|
+
if any([
|
|
1213
|
+
not self.has_activity(),
|
|
1214
|
+
not self.is_locked(),
|
|
1215
|
+
force_update
|
|
1216
|
+
]):
|
|
1217
|
+
|
|
1218
|
+
txs_is_valid = True
|
|
1219
|
+
if not txs_qs:
|
|
1220
|
+
txs_qs = self.get_transaction_queryset(select_accounts=False)
|
|
1041
1221
|
else:
|
|
1042
|
-
|
|
1043
|
-
|
|
1222
|
+
try:
|
|
1223
|
+
txs_is_valid = self.is_txs_qs_valid(txs_qs=txs_qs, raise_exception=raise_exception)
|
|
1224
|
+
except JournalEntryValidationError as e:
|
|
1225
|
+
if raise_exception:
|
|
1226
|
+
raise e
|
|
1227
|
+
|
|
1228
|
+
if txs_is_valid:
|
|
1229
|
+
cash_is_involved = self.is_cash_involved(txs_qs=txs_qs)
|
|
1230
|
+
if not cash_is_involved:
|
|
1231
|
+
self.activity = None
|
|
1232
|
+
else:
|
|
1233
|
+
role_list = self.get_txs_roles(txs_qs, exclude_cash_role=True)
|
|
1234
|
+
self.activity = self.get_activity_from_roles(role_set=role_list)
|
|
1044
1235
|
return self.activity
|
|
1045
1236
|
|
|
1046
1237
|
# todo: add entity_model as parameter on all functions...
|
|
1047
1238
|
# todo: outsource this function to EntityStateModel...?...
|
|
1048
1239
|
def _get_next_state_model(self, raise_exception: bool = True) -> EntityStateModel:
|
|
1240
|
+
"""
|
|
1241
|
+
Retrieves or creates the next state model for the Journal Entry.
|
|
1242
|
+
|
|
1243
|
+
Parameters
|
|
1244
|
+
----------
|
|
1245
|
+
raise_exception : bool, default True
|
|
1246
|
+
Determines if exceptions should be raised when the entity state is not found.
|
|
1049
1247
|
|
|
1248
|
+
Returns
|
|
1249
|
+
-------
|
|
1250
|
+
EntityStateModel
|
|
1251
|
+
The state model with an incremented sequence.
|
|
1252
|
+
"""
|
|
1050
1253
|
entity_model = EntityModel.objects.get(uuid__exact=self.ledger.entity_id)
|
|
1051
1254
|
fy_key = entity_model.get_fy_for_date(dt=self.timestamp)
|
|
1052
1255
|
|
|
@@ -1082,15 +1285,12 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
1082
1285
|
|
|
1083
1286
|
def can_generate_je_number(self) -> bool:
|
|
1084
1287
|
"""
|
|
1085
|
-
Checks if
|
|
1086
|
-
Conditions are:
|
|
1087
|
-
* The JournalEntryModel must have a LedgerModel instance assigned.
|
|
1088
|
-
* The JournalEntryModel instance must not have a pre-existing JE number.
|
|
1288
|
+
Checks if a Journal Entry Number can be generated.
|
|
1089
1289
|
|
|
1090
1290
|
Returns
|
|
1091
1291
|
-------
|
|
1092
1292
|
bool
|
|
1093
|
-
True if
|
|
1293
|
+
True if the Journal Entry can generate a JE number, otherwise False.
|
|
1094
1294
|
"""
|
|
1095
1295
|
return all([
|
|
1096
1296
|
self.ledger_id,
|
|
@@ -1099,19 +1299,17 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
1099
1299
|
|
|
1100
1300
|
def generate_je_number(self, commit: bool = False) -> str:
|
|
1101
1301
|
"""
|
|
1102
|
-
|
|
1103
|
-
will result in two additional queries if the Journal Entry LedgerModel & EntityUnitModel are not cached in
|
|
1104
|
-
QuerySet via select_related('ledger', 'entity_unit').
|
|
1302
|
+
Generates the Journal Entry number in an atomic transaction.
|
|
1105
1303
|
|
|
1106
1304
|
Parameters
|
|
1107
1305
|
----------
|
|
1108
|
-
commit: bool
|
|
1109
|
-
|
|
1306
|
+
commit : bool, default False
|
|
1307
|
+
Saves the generated JE number in the database.
|
|
1110
1308
|
|
|
1111
1309
|
Returns
|
|
1112
1310
|
-------
|
|
1113
1311
|
str
|
|
1114
|
-
|
|
1312
|
+
The generated or existing JE number.
|
|
1115
1313
|
"""
|
|
1116
1314
|
if self.can_generate_je_number():
|
|
1117
1315
|
|
|
@@ -1141,30 +1339,24 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
1141
1339
|
**kwargs) -> Tuple[TransactionModelQuerySet, bool]:
|
|
1142
1340
|
|
|
1143
1341
|
"""
|
|
1144
|
-
Verifies the
|
|
1145
|
-
* All TransactionModels associated with the JE instance are in balance (i.e. the sum of CREDITs and DEBITs are equal).
|
|
1146
|
-
* If the JournalEntryModel is using cash, a cash flow activity is assigned.
|
|
1342
|
+
Verifies the validity of the Journal Entry model instance.
|
|
1147
1343
|
|
|
1148
1344
|
Parameters
|
|
1149
1345
|
----------
|
|
1150
|
-
txs_qs: TransactionModelQuerySet
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
Additional function key-word args.
|
|
1159
|
-
|
|
1160
|
-
Raises
|
|
1161
|
-
------
|
|
1162
|
-
JournalEntryValidationError if JE instance could not be verified.
|
|
1346
|
+
txs_qs : Optional[TransactionModelQuerySet], default None
|
|
1347
|
+
Queryset of TransactionModel instances to validate. If None, transactions are queried.
|
|
1348
|
+
force_verify : bool, default False
|
|
1349
|
+
Forces re-verification even if already verified.
|
|
1350
|
+
raise_exception : bool, default True
|
|
1351
|
+
Determines if exceptions are raised on validation failure.
|
|
1352
|
+
kwargs : dict
|
|
1353
|
+
Additional options.
|
|
1163
1354
|
|
|
1164
1355
|
Returns
|
|
1165
1356
|
-------
|
|
1166
|
-
|
|
1167
|
-
The TransactionModelQuerySet
|
|
1357
|
+
Tuple[TransactionModelQuerySet, bool]
|
|
1358
|
+
- The TransactionModelQuerySet associated with the JournalEntryModel.
|
|
1359
|
+
- A boolean indicating whether verification was successful.
|
|
1168
1360
|
"""
|
|
1169
1361
|
|
|
1170
1362
|
if not self.is_verified() or force_verify:
|
|
@@ -1176,6 +1368,7 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
1176
1368
|
is_txs_qs_valid = True
|
|
1177
1369
|
else:
|
|
1178
1370
|
try:
|
|
1371
|
+
# if provided, it is verified...
|
|
1179
1372
|
is_txs_qs_valid = self.is_txs_qs_valid(raise_exception=raise_exception, txs_qs=txs_qs)
|
|
1180
1373
|
except JournalEntryValidationError as e:
|
|
1181
1374
|
raise e
|
|
@@ -1188,7 +1381,7 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
1188
1381
|
except JournalEntryValidationError as e:
|
|
1189
1382
|
raise e
|
|
1190
1383
|
|
|
1191
|
-
# Transaction CoA if valid
|
|
1384
|
+
# Transaction CoA if valid...
|
|
1192
1385
|
|
|
1193
1386
|
try:
|
|
1194
1387
|
is_coa_valid = self.is_txs_qs_coa_valid(txs_qs=txs_qs)
|
|
@@ -1205,34 +1398,45 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
1205
1398
|
# if raise_exception:
|
|
1206
1399
|
# raise JournalEntryValidationError('At least two transactions required.')
|
|
1207
1400
|
|
|
1208
|
-
if all([
|
|
1401
|
+
if all([
|
|
1402
|
+
is_balance_valid,
|
|
1403
|
+
is_txs_qs_valid,
|
|
1404
|
+
is_coa_valid
|
|
1405
|
+
]):
|
|
1209
1406
|
# activity flag...
|
|
1210
1407
|
self.generate_activity(txs_qs=txs_qs, raise_exception=raise_exception)
|
|
1211
1408
|
self._verified = True
|
|
1212
1409
|
return txs_qs, self.is_verified()
|
|
1213
|
-
return
|
|
1410
|
+
return self.get_transaction_queryset(), self.is_verified()
|
|
1214
1411
|
|
|
1215
1412
|
def clean(self,
|
|
1216
1413
|
verify: bool = False,
|
|
1217
1414
|
raise_exception: bool = True,
|
|
1218
1415
|
txs_qs: Optional[TransactionModelQuerySet] = None) -> Tuple[TransactionModelQuerySet, bool]:
|
|
1219
1416
|
"""
|
|
1220
|
-
|
|
1417
|
+
Cleans the JournalEntryModel instance, optionally verifying it and generating a Journal Entry (JE) number if required.
|
|
1221
1418
|
|
|
1222
1419
|
Parameters
|
|
1223
1420
|
----------
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
txs_qs: TransactionModelQuerySet
|
|
1229
|
-
|
|
1230
|
-
JournalEntryModel instance.
|
|
1421
|
+
verify : bool, optional
|
|
1422
|
+
If True, attempts to verify the JournalEntryModel during the cleaning process. Default is False.
|
|
1423
|
+
raise_exception : bool, optional
|
|
1424
|
+
If True, raises an exception when the instance fails verification. Default is True.
|
|
1425
|
+
txs_qs : TransactionModelQuerySet, optional
|
|
1426
|
+
A pre-fetched TransactionModelQuerySet. If provided, avoids additional database queries. The provided queryset is
|
|
1427
|
+
validated against the JournalEntryModel instance.
|
|
1231
1428
|
|
|
1232
1429
|
Returns
|
|
1233
1430
|
-------
|
|
1234
|
-
|
|
1235
|
-
|
|
1431
|
+
Tuple[TransactionModelQuerySet, bool]
|
|
1432
|
+
A tuple containing:
|
|
1433
|
+
- The validated TransactionModelQuerySet for the JournalEntryModel instance.
|
|
1434
|
+
- A boolean indicating whether the instance passed verification.
|
|
1435
|
+
|
|
1436
|
+
Raises
|
|
1437
|
+
------
|
|
1438
|
+
JournalEntryValidationError
|
|
1439
|
+
If the instance has a timestamp in the future and is posted, or if verification fails and `raise_exception` is True.
|
|
1236
1440
|
"""
|
|
1237
1441
|
|
|
1238
1442
|
if txs_qs:
|
|
@@ -1251,137 +1455,293 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
|
|
|
1251
1455
|
if verify:
|
|
1252
1456
|
txs_qs, verified = self.verify()
|
|
1253
1457
|
return txs_qs, self.is_verified()
|
|
1254
|
-
return
|
|
1458
|
+
return self.get_transaction_queryset(), self.is_verified()
|
|
1255
1459
|
|
|
1256
1460
|
def get_delete_message(self) -> str:
|
|
1461
|
+
"""
|
|
1462
|
+
Generates a confirmation message for deleting the JournalEntryModel instance.
|
|
1463
|
+
|
|
1464
|
+
Returns
|
|
1465
|
+
-------
|
|
1466
|
+
str
|
|
1467
|
+
A confirmation message including the Journal Entry number and Ledger name.
|
|
1468
|
+
"""
|
|
1257
1469
|
return _(f'Are you sure you want to delete JournalEntry Model {self.je_number} on Ledger {self.ledger.name}?')
|
|
1258
1470
|
|
|
1259
1471
|
def delete(self, **kwargs):
|
|
1472
|
+
"""
|
|
1473
|
+
Deletes the JournalEntryModel instance, ensuring it is allowed to be deleted.
|
|
1474
|
+
|
|
1475
|
+
Parameters
|
|
1476
|
+
----------
|
|
1477
|
+
**kwargs : dict
|
|
1478
|
+
Additional arguments passed to the parent delete method.
|
|
1479
|
+
|
|
1480
|
+
Raises
|
|
1481
|
+
------
|
|
1482
|
+
JournalEntryValidationError
|
|
1483
|
+
If the instance is not eligible for deletion.
|
|
1484
|
+
"""
|
|
1260
1485
|
if not self.can_delete():
|
|
1261
1486
|
raise JournalEntryValidationError(
|
|
1262
1487
|
message=_(f'JournalEntryModel {self.uuid} cannot be deleted...')
|
|
1263
1488
|
)
|
|
1264
1489
|
return super().delete(**kwargs)
|
|
1265
1490
|
|
|
1266
|
-
def save(
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1491
|
+
def save(
|
|
1492
|
+
self,
|
|
1493
|
+
verify: bool = True,
|
|
1494
|
+
post_on_verify: bool = False,
|
|
1495
|
+
*args,
|
|
1496
|
+
**kwargs
|
|
1497
|
+
):
|
|
1271
1498
|
"""
|
|
1272
|
-
|
|
1273
|
-
before saving into database.
|
|
1499
|
+
Saves the JournalEntryModel instance, with optional verification and posting prior to saving.
|
|
1274
1500
|
|
|
1275
1501
|
Parameters
|
|
1276
1502
|
----------
|
|
1277
|
-
verify: bool
|
|
1278
|
-
If True, verifies
|
|
1279
|
-
post_on_verify: bool
|
|
1280
|
-
|
|
1503
|
+
verify : bool, optional
|
|
1504
|
+
If True, verifies the transactions of the JournalEntryModel before saving. Default is True.
|
|
1505
|
+
post_on_verify : bool, optional
|
|
1506
|
+
If True, posts the JournalEntryModel if verification is successful and `can_post()` is True. Default is False.
|
|
1281
1507
|
|
|
1282
1508
|
Returns
|
|
1283
1509
|
-------
|
|
1284
1510
|
JournalEntryModel
|
|
1285
|
-
The saved instance.
|
|
1511
|
+
The saved JournalEntryModel instance.
|
|
1512
|
+
|
|
1513
|
+
Raises
|
|
1514
|
+
------
|
|
1515
|
+
JournalEntryValidationError
|
|
1516
|
+
If the instance fails verification or encounters an issue during save.
|
|
1286
1517
|
"""
|
|
1287
1518
|
try:
|
|
1519
|
+
# Generate the Journal Entry number prior to verification and saving
|
|
1288
1520
|
self.generate_je_number(commit=False)
|
|
1521
|
+
|
|
1289
1522
|
if verify:
|
|
1290
1523
|
txs_qs, is_verified = self.clean(verify=True)
|
|
1291
1524
|
if self.is_verified() and post_on_verify:
|
|
1292
|
-
#
|
|
1293
|
-
# self.mark_as_locked(commit=False, raise_exception=True)
|
|
1525
|
+
# Mark as posted if verification succeeds and posting is requested
|
|
1294
1526
|
self.mark_as_posted(commit=False, verify=False, force_lock=True, raise_exception=True)
|
|
1527
|
+
|
|
1295
1528
|
except ValidationError as e:
|
|
1296
1529
|
if self.can_unpost():
|
|
1297
1530
|
self.mark_as_unposted(raise_exception=True)
|
|
1298
1531
|
raise JournalEntryValidationError(
|
|
1299
|
-
f'
|
|
1532
|
+
f'Error validating Journal Entry ID: {self.uuid}: {e.message}'
|
|
1533
|
+
)
|
|
1300
1534
|
except Exception as e:
|
|
1301
|
-
#
|
|
1302
|
-
# no JE can be posted if not fully validated...
|
|
1535
|
+
# Safety net for unexpected errors during save
|
|
1303
1536
|
self.posted = False
|
|
1304
1537
|
self._verified = False
|
|
1305
1538
|
self.save(update_fields=['posted', 'updated'], verify=False)
|
|
1306
1539
|
raise JournalEntryValidationError(e)
|
|
1307
1540
|
|
|
1541
|
+
# Prevent saving an unverified Journal Entry
|
|
1308
1542
|
if not self.is_verified() and verify:
|
|
1309
1543
|
raise JournalEntryValidationError(message='Cannot save an unverified Journal Entry.')
|
|
1310
1544
|
|
|
1311
1545
|
return super(JournalEntryModelAbstract, self).save(*args, **kwargs)
|
|
1312
1546
|
|
|
1313
1547
|
# URLS Generation...
|
|
1548
|
+
|
|
1314
1549
|
def get_absolute_url(self) -> str:
|
|
1550
|
+
"""
|
|
1551
|
+
Generates the URL to view the details of the journal entry.
|
|
1315
1552
|
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1553
|
+
Returns
|
|
1554
|
+
-------
|
|
1555
|
+
str
|
|
1556
|
+
The absolute URL for the journal entry details.
|
|
1557
|
+
"""
|
|
1558
|
+
return reverse('django_ledger:je-detail', kwargs={
|
|
1559
|
+
'je_pk': self.uuid,
|
|
1560
|
+
'ledger_pk': self.ledger_id,
|
|
1561
|
+
'entity_slug': self.entity_slug
|
|
1562
|
+
})
|
|
1322
1563
|
|
|
1323
1564
|
def get_detail_url(self) -> str:
|
|
1324
1565
|
"""
|
|
1325
|
-
|
|
1326
|
-
Results in additional Database query if entity field is not selected in QuerySet.
|
|
1566
|
+
Generates the detail URL for the journal entry.
|
|
1327
1567
|
|
|
1328
1568
|
Returns
|
|
1329
1569
|
-------
|
|
1330
1570
|
str
|
|
1331
|
-
URL
|
|
1571
|
+
The URL for updating or viewing journal entry details.
|
|
1332
1572
|
"""
|
|
1333
|
-
return reverse('django_ledger:je-detail',
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1573
|
+
return reverse('django_ledger:je-detail', kwargs={
|
|
1574
|
+
'entity_slug': self.entity_slug,
|
|
1575
|
+
'ledger_pk': self.ledger_id,
|
|
1576
|
+
'je_pk': self.uuid
|
|
1577
|
+
})
|
|
1578
|
+
|
|
1579
|
+
def get_journal_entry_list_url(self) -> str:
|
|
1580
|
+
"""
|
|
1581
|
+
Constructs the URL to access the list of journal entries
|
|
1582
|
+
associated with a specific ledger and entity.
|
|
1583
|
+
|
|
1584
|
+
Returns
|
|
1585
|
+
-------
|
|
1586
|
+
str
|
|
1587
|
+
The URL for the journal entry list.
|
|
1588
|
+
"""
|
|
1589
|
+
return reverse('django_ledger:je-list', kwargs={
|
|
1590
|
+
'entity_slug': self.entity_slug,
|
|
1591
|
+
'ledger_pk': self.ledger_id
|
|
1592
|
+
})
|
|
1593
|
+
|
|
1594
|
+
def get_journal_entry_create_url(self) -> str:
|
|
1595
|
+
"""
|
|
1596
|
+
Constructs the URL to create a new journal entry
|
|
1597
|
+
associated with a specific ledger and entity.
|
|
1598
|
+
|
|
1599
|
+
Returns
|
|
1600
|
+
-------
|
|
1601
|
+
str
|
|
1602
|
+
The URL to create a journal entry.
|
|
1603
|
+
"""
|
|
1604
|
+
return reverse('django_ledger:je-create', kwargs={
|
|
1605
|
+
'entity_slug': self.entity_slug,
|
|
1606
|
+
'ledger_pk': self.ledger_id
|
|
1607
|
+
})
|
|
1339
1608
|
|
|
1340
1609
|
def get_detail_txs_url(self) -> str:
|
|
1341
1610
|
"""
|
|
1342
|
-
|
|
1343
|
-
Results in additional Database query if entity field is not selected in QuerySet.
|
|
1611
|
+
Generates the URL to view transaction details of the journal entry.
|
|
1344
1612
|
|
|
1345
1613
|
Returns
|
|
1346
1614
|
-------
|
|
1347
1615
|
str
|
|
1348
|
-
URL
|
|
1616
|
+
The URL for transaction details of the journal entry.
|
|
1617
|
+
"""
|
|
1618
|
+
return reverse('django_ledger:je-detail-txs', kwargs={
|
|
1619
|
+
'entity_slug': self.entity_slug,
|
|
1620
|
+
'ledger_pk': self.ledger_id,
|
|
1621
|
+
'je_pk': self.uuid
|
|
1622
|
+
})
|
|
1623
|
+
|
|
1624
|
+
def get_unlock_url(self) -> str:
|
|
1625
|
+
"""
|
|
1626
|
+
Generates the URL to mark the journal entry as unlocked.
|
|
1627
|
+
|
|
1628
|
+
Returns
|
|
1629
|
+
-------
|
|
1630
|
+
str
|
|
1631
|
+
The URL for unlocking the journal entry.
|
|
1632
|
+
"""
|
|
1633
|
+
return reverse('django_ledger:je-mark-as-unlocked', kwargs={
|
|
1634
|
+
'entity_slug': self.entity_slug,
|
|
1635
|
+
'ledger_pk': self.ledger_id,
|
|
1636
|
+
'je_pk': self.uuid
|
|
1637
|
+
})
|
|
1638
|
+
|
|
1639
|
+
def get_lock_url(self) -> str:
|
|
1640
|
+
"""
|
|
1641
|
+
Generates the URL to mark the journal entry as locked.
|
|
1642
|
+
|
|
1643
|
+
Returns
|
|
1644
|
+
-------
|
|
1645
|
+
str
|
|
1646
|
+
The URL for locking the journal entry.
|
|
1647
|
+
"""
|
|
1648
|
+
return reverse('django_ledger:je-mark-as-locked', kwargs={
|
|
1649
|
+
'entity_slug': self.entity_slug,
|
|
1650
|
+
'ledger_pk': self.ledger_id,
|
|
1651
|
+
'je_pk': self.uuid
|
|
1652
|
+
})
|
|
1653
|
+
|
|
1654
|
+
def get_post_url(self) -> str:
|
|
1349
1655
|
"""
|
|
1350
|
-
|
|
1656
|
+
Generates the URL to mark the journal entry as posted.
|
|
1657
|
+
|
|
1658
|
+
Returns
|
|
1659
|
+
-------
|
|
1660
|
+
str
|
|
1661
|
+
The URL for posting the journal entry.
|
|
1662
|
+
"""
|
|
1663
|
+
return reverse('django_ledger:je-mark-as-posted', kwargs={
|
|
1664
|
+
'entity_slug': self.entity_slug,
|
|
1665
|
+
'ledger_pk': self.ledger_id,
|
|
1666
|
+
'je_pk': self.uuid
|
|
1667
|
+
})
|
|
1668
|
+
|
|
1669
|
+
def get_unpost_url(self) -> str:
|
|
1670
|
+
"""
|
|
1671
|
+
Generates the URL to mark the journal entry as unposted.
|
|
1672
|
+
|
|
1673
|
+
Returns
|
|
1674
|
+
-------
|
|
1675
|
+
str
|
|
1676
|
+
The URL for unposting the journal entry.
|
|
1677
|
+
"""
|
|
1678
|
+
return reverse('django_ledger:je-mark-as-unposted', kwargs={
|
|
1679
|
+
'entity_slug': self.entity_slug,
|
|
1680
|
+
'ledger_pk': self.ledger_id,
|
|
1681
|
+
'je_pk': self.uuid
|
|
1682
|
+
})
|
|
1683
|
+
|
|
1684
|
+
# Action URLS....
|
|
1685
|
+
def get_action_post_url(self) -> str:
|
|
1686
|
+
"""
|
|
1687
|
+
Generates the URL used to mark the journal entry as posted.
|
|
1688
|
+
|
|
1689
|
+
Returns
|
|
1690
|
+
-------
|
|
1691
|
+
str
|
|
1692
|
+
The generated URL for marking the journal entry as posted.
|
|
1693
|
+
"""
|
|
1694
|
+
return reverse('django_ledger:je-mark-as-posted',
|
|
1351
1695
|
kwargs={
|
|
1352
|
-
'entity_slug': self.
|
|
1696
|
+
'entity_slug': self.entity_slug,
|
|
1353
1697
|
'ledger_pk': self.ledger_id,
|
|
1354
1698
|
'je_pk': self.uuid
|
|
1355
1699
|
})
|
|
1356
1700
|
|
|
1357
|
-
def
|
|
1358
|
-
|
|
1701
|
+
def get_action_unpost_url(self) -> str:
|
|
1702
|
+
"""
|
|
1703
|
+
Generates the URL used to mark the journal entry as unposted.
|
|
1704
|
+
|
|
1705
|
+
Returns
|
|
1706
|
+
-------
|
|
1707
|
+
str
|
|
1708
|
+
The generated URL for marking the journal entry as unposted.
|
|
1709
|
+
"""
|
|
1710
|
+
return reverse('django_ledger:je-mark-as-unposted',
|
|
1359
1711
|
kwargs={
|
|
1360
|
-
'entity_slug': self.
|
|
1712
|
+
'entity_slug': self.entity_slug,
|
|
1361
1713
|
'ledger_pk': self.ledger_id,
|
|
1362
1714
|
'je_pk': self.uuid
|
|
1363
1715
|
})
|
|
1364
1716
|
|
|
1365
|
-
def
|
|
1717
|
+
def get_action_lock_url(self) -> str:
|
|
1718
|
+
"""
|
|
1719
|
+
Generates the URL used to mark the journal entry as locked.
|
|
1720
|
+
|
|
1721
|
+
Returns
|
|
1722
|
+
-------
|
|
1723
|
+
str
|
|
1724
|
+
The generated URL for marking the journal entry as locked.
|
|
1725
|
+
"""
|
|
1366
1726
|
return reverse('django_ledger:je-mark-as-locked',
|
|
1367
1727
|
kwargs={
|
|
1368
|
-
'entity_slug': self.
|
|
1728
|
+
'entity_slug': self.entity_slug,
|
|
1369
1729
|
'ledger_pk': self.ledger_id,
|
|
1370
1730
|
'je_pk': self.uuid
|
|
1371
1731
|
})
|
|
1372
1732
|
|
|
1373
|
-
def
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
'entity_slug': self.ledger.entity.slug,
|
|
1377
|
-
'ledger_pk': self.ledger_id,
|
|
1378
|
-
'je_pk': self.uuid
|
|
1379
|
-
})
|
|
1733
|
+
def get_action_unlock_url(self) -> str:
|
|
1734
|
+
"""
|
|
1735
|
+
Generates the URL used to mark the journal entry as unlocked.
|
|
1380
1736
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1737
|
+
Returns
|
|
1738
|
+
-------
|
|
1739
|
+
str
|
|
1740
|
+
The generated URL for marking the journal entry as unlocked.
|
|
1741
|
+
"""
|
|
1742
|
+
return reverse('django_ledger:je-mark-as-unlocked',
|
|
1383
1743
|
kwargs={
|
|
1384
|
-
'entity_slug': self.
|
|
1744
|
+
'entity_slug': self.entity_slug,
|
|
1385
1745
|
'ledger_pk': self.ledger_id,
|
|
1386
1746
|
'je_pk': self.uuid
|
|
1387
1747
|
})
|
|
@@ -1398,10 +1758,12 @@ class JournalEntryModel(JournalEntryModelAbstract):
|
|
|
1398
1758
|
|
|
1399
1759
|
|
|
1400
1760
|
def journalentrymodel_presave(instance: JournalEntryModel, **kwargs):
|
|
1401
|
-
if instance._state.adding
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1761
|
+
if instance._state.adding:
|
|
1762
|
+
# cannot add journal entries to a locked ledger...
|
|
1763
|
+
if instance.ledger_is_locked():
|
|
1764
|
+
raise JournalEntryValidationError(
|
|
1765
|
+
message=_(f'Cannot add Journal Entries to locked LedgerModel {instance.ledger_id}')
|
|
1766
|
+
)
|
|
1405
1767
|
|
|
1406
1768
|
|
|
1407
1769
|
pre_save.connect(journalentrymodel_presave, sender=JournalEntryModel)
|