django-ledger 0.7.3__py3-none-any.whl → 0.7.4.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/contrib/django_ledger_graphene/journal_entry/schema.py +2 -3
- django_ledger/contrib/django_ledger_graphene/transaction/schema.py +9 -7
- django_ledger/forms/journal_entry.py +19 -12
- django_ledger/forms/transactions.py +8 -12
- django_ledger/io/io_core.py +14 -11
- 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/invoice.py +12 -4
- django_ledger/models/journal_entry.py +843 -481
- django_ledger/models/ledger.py +45 -4
- django_ledger/models/transactions.py +303 -305
- django_ledger/models/unit.py +42 -22
- 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/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 +25 -45
- django_ledger/urls/account.py +4 -4
- django_ledger/views/account.py +7 -7
- django_ledger/views/journal_entry.py +84 -101
- django_ledger/views/ledger.py +16 -21
- django_ledger/views/mixins.py +11 -10
- {django_ledger-0.7.3.dist-info → django_ledger-0.7.4.1.dist-info}/METADATA +8 -3
- {django_ledger-0.7.3.dist-info → django_ledger-0.7.4.1.dist-info}/RECORD +42 -40
- {django_ledger-0.7.3.dist-info → django_ledger-0.7.4.1.dist-info}/AUTHORS.md +0 -0
- {django_ledger-0.7.3.dist-info → django_ledger-0.7.4.1.dist-info}/LICENSE +0 -0
- {django_ledger-0.7.3.dist-info → django_ledger-0.7.4.1.dist-info}/WHEEL +0 -0
- {django_ledger-0.7.3.dist-info → django_ledger-0.7.4.1.dist-info}/top_level.txt +0 -0
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
|
|
3
3
|
Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
|
|
4
4
|
|
|
5
|
-
The TransactionModel
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
querying and
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
The TransactionModel serves as the foundational accounting entity where all financial transactions are recorded.
|
|
6
|
+
Every transaction must be associated with a JournalEntryModel, which represents a collection
|
|
7
|
+
of related transactions. This strict association ensures that standalone TransactionModels—or orphan transactions—do not
|
|
8
|
+
exist, a constraint enforced at the database level.
|
|
9
|
+
|
|
10
|
+
Each transaction performs either a CREDIT or a DEBIT operation on the designated AccountModel, upholding standard
|
|
11
|
+
accounting principles. The TransactionModel API integrates the IOMixIn, a critical component for generating financial
|
|
12
|
+
statements. This mixin facilitates efficient querying and aggregation directly at the database level, eliminating the need
|
|
13
|
+
to load all TransactionModels into memory. This database-driven approach significantly improves performance and simplifies
|
|
14
|
+
the process of generating accurate financial reports.
|
|
15
|
+
|
|
16
|
+
The TransactionModel, together with the IOMixIn, is essential for ensuring seamless, efficient, and reliable
|
|
17
|
+
financial statement production in the Django Ledger framework.
|
|
15
18
|
"""
|
|
19
|
+
|
|
16
20
|
from datetime import datetime, date
|
|
17
|
-
from typing import List, Union, Optional
|
|
21
|
+
from typing import List, Union, Optional, Set
|
|
18
22
|
from uuid import uuid4, UUID
|
|
19
23
|
|
|
20
24
|
from django.contrib.auth import get_user_model
|
|
@@ -26,11 +30,7 @@ from django.db.models.signals import pre_save
|
|
|
26
30
|
from django.utils.translation import gettext_lazy as _
|
|
27
31
|
|
|
28
32
|
from django_ledger.io.io_core import validate_io_timestamp
|
|
29
|
-
from django_ledger.models
|
|
30
|
-
from django_ledger.models.bill import BillModel
|
|
31
|
-
from django_ledger.models.entity import EntityModel
|
|
32
|
-
from django_ledger.models.invoice import InvoiceModel
|
|
33
|
-
from django_ledger.models.ledger import LedgerModel
|
|
33
|
+
from django_ledger.models import AccountModel, BillModel, EntityModel, InvoiceModel, LedgerModel
|
|
34
34
|
from django_ledger.models.mixins import CreateUpdateMixIn
|
|
35
35
|
from django_ledger.models.unit import EntityUnitModel
|
|
36
36
|
from django_ledger.models.utils import lazy_loader
|
|
@@ -44,21 +44,23 @@ class TransactionModelValidationError(ValidationError):
|
|
|
44
44
|
|
|
45
45
|
class TransactionModelQuerySet(QuerySet):
|
|
46
46
|
"""
|
|
47
|
-
A custom QuerySet class for
|
|
48
|
-
|
|
47
|
+
A custom QuerySet class tailored for `TransactionModel` objects. It includes a collection
|
|
48
|
+
of methods to efficiently and safely retrieve and filter transactions from the database
|
|
49
|
+
based on common use cases.
|
|
49
50
|
"""
|
|
50
51
|
|
|
51
52
|
def posted(self) -> QuerySet:
|
|
52
53
|
"""
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
Retrieves transactions that are part of a posted journal entry and ledger.
|
|
55
|
+
|
|
56
|
+
A transaction is considered "posted" if:
|
|
57
|
+
- It belongs to a journal entry marked as *posted*.
|
|
58
|
+
- Its associated journal entry is part of a ledger marked as *posted*.
|
|
57
59
|
|
|
58
60
|
Returns
|
|
59
61
|
-------
|
|
60
62
|
TransactionModelQuerySet
|
|
61
|
-
A QuerySet
|
|
63
|
+
A QuerySet containing only transactions that meet the "posted" criteria.
|
|
62
64
|
"""
|
|
63
65
|
return self.filter(
|
|
64
66
|
Q(journal_entry__posted=True) &
|
|
@@ -67,23 +69,24 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
67
69
|
|
|
68
70
|
def for_accounts(self, account_list: List[str or AccountModel]):
|
|
69
71
|
"""
|
|
70
|
-
|
|
72
|
+
Filters transactions based on the accounts they are associated with.
|
|
71
73
|
|
|
72
74
|
Parameters
|
|
73
75
|
----------
|
|
74
|
-
account_list: list
|
|
75
|
-
A
|
|
76
|
+
account_list : list of str or AccountModel
|
|
77
|
+
A list containing account codes (strings) or `AccountModel` instances.
|
|
78
|
+
Transactions will be filtered to match these accounts.
|
|
76
79
|
|
|
77
80
|
Returns
|
|
78
81
|
-------
|
|
79
82
|
TransactionModelQuerySet
|
|
80
|
-
|
|
83
|
+
A QuerySet filtered for transactions associated with the specified accounts.
|
|
81
84
|
"""
|
|
82
85
|
if isinstance(account_list, list) > 0 and isinstance(account_list[0], str):
|
|
83
86
|
return self.filter(account__code__in=account_list)
|
|
84
87
|
return self.filter(account__in=account_list)
|
|
85
88
|
|
|
86
|
-
def for_roles(self, role_list: Union[str, List[str]]):
|
|
89
|
+
def for_roles(self, role_list: Union[str, List[str], Set[str]]):
|
|
87
90
|
"""
|
|
88
91
|
Fetches a QuerySet of TransactionModels which AccountModel has a specific role.
|
|
89
92
|
|
|
@@ -103,35 +106,35 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
103
106
|
|
|
104
107
|
def for_unit(self, unit_slug: Union[str, EntityUnitModel]):
|
|
105
108
|
"""
|
|
106
|
-
|
|
109
|
+
Filters transactions based on their associated entity unit.
|
|
107
110
|
|
|
108
111
|
Parameters
|
|
109
112
|
----------
|
|
110
|
-
unit_slug: str or EntityUnitModel
|
|
111
|
-
A string representing the
|
|
113
|
+
unit_slug : str or EntityUnitModel
|
|
114
|
+
A string representing the slug of the entity unit or an `EntityUnitModel` instance.
|
|
112
115
|
|
|
113
116
|
Returns
|
|
114
117
|
-------
|
|
115
118
|
TransactionModelQuerySet
|
|
116
|
-
|
|
119
|
+
A QuerySet filtered for transactions linked to the specified unit.
|
|
117
120
|
"""
|
|
118
121
|
if isinstance(unit_slug, EntityUnitModel):
|
|
119
|
-
return self.filter(
|
|
120
|
-
return self.filter(
|
|
122
|
+
return self.filter(journal_entry__entity_unit=unit_slug)
|
|
123
|
+
return self.filter(journal_entry__entity_unit__slug__exact=unit_slug)
|
|
121
124
|
|
|
122
|
-
def for_activity(self, activity_list: Union[str, List[str]]):
|
|
125
|
+
def for_activity(self, activity_list: Union[str, List[str], Set[str]]):
|
|
123
126
|
"""
|
|
124
|
-
|
|
127
|
+
Filters transactions based on their associated activity or activities.
|
|
125
128
|
|
|
126
129
|
Parameters
|
|
127
130
|
----------
|
|
128
|
-
activity_list: str or list
|
|
129
|
-
A
|
|
131
|
+
activity_list : str or list of str
|
|
132
|
+
A single activity or a list of activities to filter transactions by.
|
|
130
133
|
|
|
131
134
|
Returns
|
|
132
135
|
-------
|
|
133
136
|
TransactionModelQuerySet
|
|
134
|
-
|
|
137
|
+
A QuerySet filtered for transactions linked to the specified activity or activities.
|
|
135
138
|
"""
|
|
136
139
|
if isinstance(activity_list, str):
|
|
137
140
|
return self.filter(journal_entry__activity__in=[activity_list])
|
|
@@ -139,20 +142,21 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
139
142
|
|
|
140
143
|
def to_date(self, to_date: Union[str, date, datetime]):
|
|
141
144
|
"""
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
Filters transactions occurring on or before a specific date or timestamp.
|
|
146
|
+
|
|
147
|
+
If `to_date` is a naive datetime (no timezone), it is assumed to be in local time
|
|
148
|
+
based on Django settings.
|
|
145
149
|
|
|
146
150
|
Parameters
|
|
147
151
|
----------
|
|
148
|
-
to_date: str
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
to_date : str, date, or datetime
|
|
153
|
+
The maximum date or timestamp for filtering. When using a date (not datetime),
|
|
154
|
+
the filter is inclusive (e.g., "2022-12-20" includes all transactions from that day).
|
|
151
155
|
|
|
152
156
|
Returns
|
|
153
157
|
-------
|
|
154
158
|
TransactionModelQuerySet
|
|
155
|
-
|
|
159
|
+
A QuerySet filtered to include transactions up to the specified date or timestamp.
|
|
156
160
|
"""
|
|
157
161
|
|
|
158
162
|
if isinstance(to_date, str):
|
|
@@ -164,20 +168,21 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
164
168
|
|
|
165
169
|
def from_date(self, from_date: Union[str, date, datetime]):
|
|
166
170
|
"""
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
Filters transactions occurring on or after a specific date or timestamp.
|
|
172
|
+
|
|
173
|
+
If `from_date` is a naive datetime (no timezone), it is assumed to be in local time
|
|
174
|
+
based on Django settings.
|
|
170
175
|
|
|
171
176
|
Parameters
|
|
172
177
|
----------
|
|
173
|
-
from_date: str
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
from_date : str, date, or datetime
|
|
179
|
+
The minimum date or timestamp for filtering. When using a date (not datetime),
|
|
180
|
+
the filter is inclusive (e.g., "2022-12-20" includes all transactions from that day).
|
|
176
181
|
|
|
177
182
|
Returns
|
|
178
183
|
-------
|
|
179
184
|
TransactionModelQuerySet
|
|
180
|
-
|
|
185
|
+
A QuerySet filtered to include transactions from the specified date or timestamp onwards.
|
|
181
186
|
"""
|
|
182
187
|
if isinstance(from_date, str):
|
|
183
188
|
from_date = validate_io_timestamp(from_date)
|
|
@@ -189,319 +194,269 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
189
194
|
|
|
190
195
|
def not_closing_entry(self):
|
|
191
196
|
"""
|
|
192
|
-
|
|
197
|
+
Filters transactions that are *not* part of a closing journal entry.
|
|
193
198
|
|
|
194
|
-
Returns
|
|
195
|
-
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
TransactionModelQuerySet
|
|
202
|
+
A QuerySet with transactions where the `journal_entry__is_closing_entry` field is False.
|
|
196
203
|
"""
|
|
197
204
|
return self.filter(journal_entry__is_closing_entry=False)
|
|
198
205
|
|
|
199
206
|
def is_closing_entry(self):
|
|
200
207
|
"""
|
|
201
|
-
|
|
208
|
+
Filters transactions that are part of a closing journal entry.
|
|
202
209
|
|
|
203
|
-
Returns
|
|
204
|
-
|
|
210
|
+
Returns
|
|
211
|
+
-------
|
|
212
|
+
TransactionModelQuerySet
|
|
213
|
+
A QuerySet with transactions where the `journal_entry__is_closing_entry` field is True.
|
|
205
214
|
"""
|
|
206
215
|
return self.filter(journal_entry__is_closing_entry=True)
|
|
207
216
|
|
|
208
|
-
|
|
209
|
-
class TransactionModelManager(Manager):
|
|
210
|
-
"""
|
|
211
|
-
A manager class for the TransactionModel.
|
|
212
|
-
"""
|
|
213
|
-
|
|
214
|
-
def get_queryset(self) -> TransactionModelQuerySet:
|
|
215
|
-
qs = TransactionModelQuerySet(self.model, using=self._db)
|
|
216
|
-
return qs.annotate(
|
|
217
|
-
_coa_id=F('account__coa_model_id'),
|
|
218
|
-
).select_related(
|
|
219
|
-
'journal_entry',
|
|
220
|
-
'account',
|
|
221
|
-
'account__coa_model',
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
def for_user(self, user_model) -> TransactionModelQuerySet:
|
|
217
|
+
def for_ledger(self, ledger_model: Union[LedgerModel, UUID, str]):
|
|
225
218
|
"""
|
|
219
|
+
Filters transactions for a specific ledger under a given entity.
|
|
220
|
+
|
|
226
221
|
Parameters
|
|
227
222
|
----------
|
|
228
|
-
|
|
229
|
-
The
|
|
223
|
+
ledger_model : Union[LedgerModel, UUID]
|
|
224
|
+
The ledger model or its UUID to filter by.
|
|
230
225
|
|
|
231
226
|
Returns
|
|
232
227
|
-------
|
|
233
228
|
TransactionModelQuerySet
|
|
234
|
-
A queryset
|
|
235
|
-
|
|
236
|
-
Raises
|
|
237
|
-
------
|
|
238
|
-
None
|
|
239
|
-
|
|
240
|
-
Description
|
|
241
|
-
-----------
|
|
242
|
-
This method filters the transactions based on the user's permissions.
|
|
243
|
-
If the user is a superuser, all transactions are returned. Otherwise, the transactions are filtered based on
|
|
244
|
-
the user's relationship to the entities associated with the transactions. Specifically, the transactions are
|
|
245
|
-
filtered to include only those where either the user is an admin of the entity associated with the transaction's
|
|
246
|
-
ledger or the user is one of the managers of the entity associated with the transaction's ledger.
|
|
229
|
+
A queryset containing transactions associated with the given ledger and entity.
|
|
247
230
|
"""
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
Q(journal_entry__ledger__entity__managers__in=[user_model])
|
|
252
|
-
)
|
|
231
|
+
if isinstance(ledger_model, UUID):
|
|
232
|
+
return self.filter(journal_entry__ledger__uuid__exact=ledger_model)
|
|
233
|
+
return self.filter(journal_entry__ledger=ledger_model)
|
|
253
234
|
|
|
254
|
-
def
|
|
255
|
-
entity_slug: Union[EntityModel, str, UUID],
|
|
256
|
-
user_model: Optional[UserModel] = None,
|
|
257
|
-
) -> TransactionModelQuerySet:
|
|
235
|
+
def for_journal_entry(self, je_model):
|
|
258
236
|
"""
|
|
237
|
+
Filters transactions for a specific journal entry under a given ledger and entity.
|
|
238
|
+
|
|
259
239
|
Parameters
|
|
260
240
|
----------
|
|
261
|
-
|
|
262
|
-
The
|
|
263
|
-
Can be an instance of EntityModel, a string representing the slug, or a UUID.
|
|
264
|
-
user_model : Optional[UserModel], optional
|
|
265
|
-
The user model for which to filter transactions.
|
|
266
|
-
If provided, only transactions associated with the specified user will be returned.
|
|
267
|
-
Defaults to None.
|
|
241
|
+
je_model : Union[JournalEntryModel, UUID]
|
|
242
|
+
The journal entry model or its UUID to filter by.
|
|
268
243
|
|
|
269
244
|
Returns
|
|
270
245
|
-------
|
|
271
246
|
TransactionModelQuerySet
|
|
272
|
-
A
|
|
247
|
+
A queryset containing transactions associated with the given journal entry.
|
|
273
248
|
"""
|
|
249
|
+
if isinstance(je_model, lazy_loader.get_journal_entry_model()):
|
|
250
|
+
return self.filter(journal_entry=je_model)
|
|
251
|
+
return self.filter(journal_entry__uuid__exact=je_model)
|
|
274
252
|
|
|
275
|
-
|
|
276
|
-
qs = self.for_user(user_model=user_model)
|
|
277
|
-
else:
|
|
278
|
-
qs = self.get_queryset()
|
|
279
|
-
|
|
280
|
-
if isinstance(entity_slug, EntityModel):
|
|
281
|
-
return qs.filter(journal_entry__ledger__entity=entity_slug)
|
|
282
|
-
elif isinstance(entity_slug, UUID):
|
|
283
|
-
return qs.filter(journal_entry__ledger__entity_id=entity_slug)
|
|
284
|
-
return qs.filter(journal_entry__ledger__entity__slug__exact=entity_slug)
|
|
285
|
-
|
|
286
|
-
def for_ledger(self,
|
|
287
|
-
entity_slug: Union[EntityModel, str],
|
|
288
|
-
ledger_model: Union[LedgerModel, UUID],
|
|
289
|
-
user_model: Optional[UserModel] = None):
|
|
253
|
+
def for_bill(self, bill_model: Union[BillModel, str, UUID]):
|
|
290
254
|
"""
|
|
255
|
+
Filters transactions for a specific bill under a given entity.
|
|
256
|
+
|
|
291
257
|
Parameters
|
|
292
258
|
----------
|
|
293
|
-
|
|
294
|
-
The
|
|
295
|
-
ledger_model : Union[LedgerModel, UUID]
|
|
296
|
-
The ledger model or UUID of the ledger for which to filter the journal entries.
|
|
297
|
-
user_model : Optional[UserModel], optional
|
|
298
|
-
The user model associated with the entity. Default is None.
|
|
259
|
+
bill_model : Union[BillModel, str, UUID]
|
|
260
|
+
The bill model or its UUID to filter by.
|
|
299
261
|
|
|
300
262
|
Returns
|
|
301
263
|
-------
|
|
302
|
-
|
|
303
|
-
|
|
264
|
+
TransactionModelQuerySet
|
|
265
|
+
A queryset containing transactions related to the specified bill.
|
|
304
266
|
"""
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return qs.filter(journal_entry__ledger=ledger_model)
|
|
267
|
+
if isinstance(bill_model, BillModel):
|
|
268
|
+
return self.filter(journal_entry__ledger__billmodel=bill_model)
|
|
269
|
+
return self.filter(journal_entry__ledger__billmodel__uuid__exact=bill_model)
|
|
309
270
|
|
|
310
|
-
def
|
|
311
|
-
entity_slug: Union[EntityModel, str],
|
|
312
|
-
unit_slug: str = Union[EntityUnitModel, str],
|
|
313
|
-
user_model: Optional[UserModel] = None):
|
|
271
|
+
def for_invoice(self, invoice_model: Union[InvoiceModel, str, UUID]):
|
|
314
272
|
"""
|
|
315
|
-
|
|
273
|
+
Filters transactions for a specific invoice under a given entity.
|
|
316
274
|
|
|
317
275
|
Parameters
|
|
318
276
|
----------
|
|
319
|
-
|
|
320
|
-
The
|
|
321
|
-
unit_slug : Union[EntityUnitModel, str]
|
|
322
|
-
The entity unit model or slug used to filter the queryset.
|
|
323
|
-
user_model : Optional[UserModel], optional
|
|
324
|
-
The user model to consider for filtering the queryset, by default None.
|
|
277
|
+
invoice_model : Union[InvoiceModel, str, UUID]
|
|
278
|
+
The invoice model or its UUID to filter by.
|
|
325
279
|
|
|
326
280
|
Returns
|
|
327
281
|
-------
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
Notes
|
|
332
|
-
-----
|
|
333
|
-
- If `unit_slug` is an instance of `EntityUnitModel`, the queryset is filtered using `journal_entry__entity_unit=unit_slug`.
|
|
334
|
-
- If `unit_slug` is a string, the queryset is filtered using `journal_entry__entity_unit__slug__exact=unit_slug`.
|
|
282
|
+
TransactionModelQuerySet
|
|
283
|
+
A queryset containing transactions related to the specified invoice.
|
|
335
284
|
"""
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
285
|
+
if isinstance(invoice_model, InvoiceModel):
|
|
286
|
+
return self.filter(journal_entry__ledger__invoicemodel=invoice_model)
|
|
287
|
+
return self.filter(journal_entry__ledger__invoicemodel__uuid__exact=invoice_model)
|
|
288
|
+
|
|
289
|
+
def with_annotated_details(self):
|
|
290
|
+
return self.annotate(
|
|
291
|
+
entity_unit_name=F('journal_entry__entity_unit__name'),
|
|
292
|
+
account_code=F('account__code'),
|
|
293
|
+
account_name=F('account__name'),
|
|
294
|
+
timestamp=F('journal_entry__timestamp'),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class TransactionModelManager(Manager):
|
|
299
|
+
"""
|
|
300
|
+
A custom manager for `TransactionModel` designed to add helper methods for
|
|
301
|
+
querying and filtering `TransactionModel` objects efficiently based on use cases like
|
|
302
|
+
user permissions, associated entities, ledgers, journal entries, and more.
|
|
303
|
+
|
|
304
|
+
This manager leverages `TransactionModelQuerySet` for complex query construction and
|
|
305
|
+
integrates advanced filtering options based on user roles, entities, and other relationships.
|
|
306
|
+
"""
|
|
340
307
|
|
|
341
|
-
def
|
|
342
|
-
entity_slug: Union[EntityModel, str],
|
|
343
|
-
ledger_model: Union[LedgerModel, str, UUID],
|
|
344
|
-
je_model,
|
|
345
|
-
user_model: Optional[UserModel] = None):
|
|
308
|
+
def get_queryset(self) -> TransactionModelQuerySet:
|
|
346
309
|
"""
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
entity_slug : Union[EntityModel, str]
|
|
350
|
-
The entity slug or instance of EntityModel representing the entity for which the journal entry is requested.
|
|
351
|
-
ledger_model : Union[LedgerModel, str, UUID]
|
|
352
|
-
The ledger model or its identifier (str or UUID) representing the ledger for which the journal entry
|
|
353
|
-
is requested.
|
|
354
|
-
je_model : Type[JournalEntryModel]
|
|
355
|
-
The journal entry model or its identifier (str or UUID) representing the journal entry to filter by.
|
|
356
|
-
user_model : Optional[UserModel], default=None
|
|
357
|
-
An optional user model instance representing the user for whom the journal entry is requested.
|
|
310
|
+
Retrieves the base queryset for `TransactionModel`, annotated and pre-loaded
|
|
311
|
+
with commonly used related fields.
|
|
358
312
|
|
|
359
313
|
Returns
|
|
360
314
|
-------
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
315
|
+
TransactionModelQuerySet
|
|
316
|
+
A custom queryset with essential annotations and relationships preloaded.
|
|
364
317
|
"""
|
|
365
|
-
qs = self.
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
318
|
+
qs = TransactionModelQuerySet(self.model, using=self._db)
|
|
319
|
+
return qs.annotate(
|
|
320
|
+
_coa_id=F('account__coa_model_id') # Annotates the `coa_model_id` from the related `account`.
|
|
321
|
+
).select_related(
|
|
322
|
+
'journal_entry', # Pre-loads the related Journal Entry.
|
|
323
|
+
'account', # Pre-loads the Account associated with the Transaction.
|
|
324
|
+
'account__coa_model', # Pre-loads the Chart of Accounts related to the Account.
|
|
325
|
+
)
|
|
372
326
|
|
|
373
|
-
def
|
|
374
|
-
user_model,
|
|
375
|
-
entity_slug: str,
|
|
376
|
-
bill_model: Union[BillModel, str, UUID]):
|
|
327
|
+
def for_user(self, user_model) -> TransactionModelQuerySet:
|
|
377
328
|
"""
|
|
329
|
+
Filters transactions accessible to a specific user based on their permissions.
|
|
330
|
+
|
|
378
331
|
Parameters
|
|
379
332
|
----------
|
|
380
|
-
user_model :
|
|
381
|
-
|
|
382
|
-
entity_slug : str
|
|
383
|
-
The slug of the entity.
|
|
384
|
-
bill_model : Union[BillModel, str, UUID]
|
|
385
|
-
An instance of bill model or a string/UUID representing the UUID of the bill model.
|
|
333
|
+
user_model : UserModel
|
|
334
|
+
The user object for which the transactions should be filtered.
|
|
386
335
|
|
|
387
336
|
Returns
|
|
388
337
|
-------
|
|
389
|
-
|
|
390
|
-
A
|
|
338
|
+
TransactionModelQuerySet
|
|
339
|
+
A queryset containing transactions filtered by the user's access level.
|
|
391
340
|
|
|
341
|
+
Description
|
|
342
|
+
-----------
|
|
343
|
+
- Returns all `TransactionModel` objects for superusers.
|
|
344
|
+
- For regular users, it filters transactions where:
|
|
345
|
+
- The user is an admin of the entity associated with the ledger in the transaction.
|
|
346
|
+
- The user is a manager of the entity associated with the ledger in the transaction.
|
|
392
347
|
"""
|
|
393
|
-
qs = self.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return qs.filter(journal_entry__ledger__billmodel__uuid__exact=bill_model)
|
|
348
|
+
qs = self.get_queryset()
|
|
349
|
+
return qs.filter(
|
|
350
|
+
Q(journal_entry__ledger__entity__admin=user_model) |
|
|
351
|
+
Q(journal_entry__ledger__entity__managers__in=[user_model])
|
|
352
|
+
)
|
|
399
353
|
|
|
400
|
-
def
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
invoice_model: Union[InvoiceModel, str, UUID]):
|
|
354
|
+
def for_entity(self,
|
|
355
|
+
entity_slug: Union[EntityModel, str, UUID],
|
|
356
|
+
user_model: Optional[UserModel] = None) -> TransactionModelQuerySet:
|
|
404
357
|
"""
|
|
358
|
+
Filters transactions for a specific entity, optionally scoped to a specific user.
|
|
359
|
+
|
|
405
360
|
Parameters
|
|
406
361
|
----------
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
The
|
|
411
|
-
|
|
412
|
-
The invoice model or its identifier used for filtering.
|
|
362
|
+
entity_slug : Union[EntityModel, str, UUID]
|
|
363
|
+
Identifier for the entity. This can be an `EntityModel` object, a slug (str), or a UUID.
|
|
364
|
+
user_model : Optional[UserModel], optional
|
|
365
|
+
The user for whom transactions should be filtered. If provided, applies user-specific
|
|
366
|
+
filtering. Defaults to None.
|
|
413
367
|
|
|
414
368
|
Returns
|
|
415
369
|
-------
|
|
416
|
-
|
|
417
|
-
|
|
370
|
+
TransactionModelQuerySet
|
|
371
|
+
A queryset containing transactions associated with the specified entity.
|
|
372
|
+
|
|
373
|
+
Notes
|
|
374
|
+
-----
|
|
375
|
+
- If `user_model` is provided, only transactions accessible by the user are included.
|
|
376
|
+
- Supports flexible filtering by accepting different forms of `entity_slug`.
|
|
418
377
|
"""
|
|
419
|
-
|
|
420
|
-
user_model=user_model
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
378
|
+
if user_model:
|
|
379
|
+
qs = self.for_user(user_model=user_model)
|
|
380
|
+
else:
|
|
381
|
+
qs = self.get_queryset()
|
|
382
|
+
|
|
383
|
+
if isinstance(entity_slug, EntityModel):
|
|
384
|
+
return qs.filter(journal_entry__ledger__entity=entity_slug)
|
|
385
|
+
elif isinstance(entity_slug, UUID):
|
|
386
|
+
return qs.filter(journal_entry__ledger__entity_id=entity_slug)
|
|
387
|
+
return qs.filter(journal_entry__ledger__entity__slug__exact=entity_slug)
|
|
425
388
|
|
|
426
389
|
|
|
427
390
|
class TransactionModelAbstract(CreateUpdateMixIn):
|
|
428
391
|
"""
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
-
|
|
439
|
-
|
|
440
|
-
- TX_TYPE: A list of
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
-
|
|
455
|
-
It has a maximum of 2 decimal places and a maximum of 20 digits.
|
|
456
|
-
It defaults to 0.00 and accepts a minimum value of 0.
|
|
457
|
-
|
|
458
|
-
- description: A CharField representing the description of the transaction.
|
|
459
|
-
It has a maximum length of 100 characters and is optional.
|
|
460
|
-
|
|
461
|
-
- objects: An instance of the TransactionModelAdmin class.
|
|
462
|
-
|
|
463
|
-
Methods
|
|
464
|
-
-------
|
|
465
|
-
|
|
466
|
-
- clean(): Performs validation on the transaction instance.
|
|
467
|
-
Raises a TransactionModelValidationError if the account is a root account.
|
|
468
|
-
|
|
392
|
+
Abstract model for representing a financial transaction in the ledger system.
|
|
393
|
+
|
|
394
|
+
This model defines the core structure and behavior that every transaction record is
|
|
395
|
+
expected to have, including fields like transaction type, associated account, amount,
|
|
396
|
+
and additional metadata used for validation and functionality.
|
|
397
|
+
|
|
398
|
+
Attributes:
|
|
399
|
+
-----------
|
|
400
|
+
Constants:
|
|
401
|
+
- CREDIT: Constant representing a credit transaction.
|
|
402
|
+
- DEBIT: Constant representing a debit transaction.
|
|
403
|
+
- TX_TYPE: A list of choices providing options for transaction types, including CREDIT and DEBIT.
|
|
404
|
+
|
|
405
|
+
Fields:
|
|
406
|
+
- uuid (UUIDField): The unique identifier for the transaction. Automatically generated, non-editable, and primary key.
|
|
407
|
+
- tx_type (CharField): Specifies the transaction type (CREDIT or DEBIT). Choices are based on the TX_TYPE constant. Maximum length is 10 characters.
|
|
408
|
+
- journal_entry (ForeignKey): References the related journal entry from the `django_ledger.JournalEntryModel`.
|
|
409
|
+
This field is not editable and is essential for linking transactions to journal entries.
|
|
410
|
+
- account (ForeignKey): References the associated account from `django_ledger.AccountModel`. Protected from being deleted.
|
|
411
|
+
- amount (DecimalField): Represents the transaction amount, up to 20 digits and 2 decimal places.
|
|
412
|
+
The default value is 0.00, and it enforces a minimum value of 0.
|
|
413
|
+
- description (CharField): Optional field for a brief description of the transaction.
|
|
414
|
+
The maximum length is 100 characters.
|
|
415
|
+
- cleared (BooleanField): Indicates whether the transaction has been cleared. Defaults to False.
|
|
416
|
+
- reconciled (BooleanField): Indicates whether the transaction has been reconciled. Defaults to False.
|
|
417
|
+
- objects (TransactionModelManager): Custom model manager providing advanced helper methods for querying and filtering transactions.
|
|
469
418
|
"""
|
|
470
419
|
|
|
471
420
|
CREDIT = 'credit'
|
|
472
421
|
DEBIT = 'debit'
|
|
473
|
-
|
|
474
422
|
TX_TYPE = [
|
|
475
423
|
(CREDIT, _('Credit')),
|
|
476
424
|
(DEBIT, _('Debit'))
|
|
477
425
|
]
|
|
478
426
|
|
|
479
427
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
480
|
-
tx_type = models.CharField(max_length=10, choices=TX_TYPE, verbose_name=_('
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
428
|
+
tx_type = models.CharField(max_length=10, choices=TX_TYPE, verbose_name=_('Transaction Type'))
|
|
429
|
+
|
|
430
|
+
journal_entry = models.ForeignKey(
|
|
431
|
+
'django_ledger.JournalEntryModel',
|
|
432
|
+
editable=False,
|
|
433
|
+
verbose_name=_('Journal Entry'),
|
|
434
|
+
help_text=_('Journal Entry to be associated with this transaction.'),
|
|
435
|
+
on_delete=models.CASCADE
|
|
436
|
+
)
|
|
437
|
+
account = models.ForeignKey(
|
|
438
|
+
'django_ledger.AccountModel',
|
|
439
|
+
verbose_name=_('Account'),
|
|
440
|
+
help_text=_('Account from Chart of Accounts to be associated with this transaction.'),
|
|
441
|
+
on_delete=models.PROTECT
|
|
442
|
+
)
|
|
443
|
+
amount = models.DecimalField(
|
|
444
|
+
decimal_places=2,
|
|
445
|
+
max_digits=20,
|
|
446
|
+
default=0.00,
|
|
447
|
+
verbose_name=_('Amount'),
|
|
448
|
+
help_text=_('Amount of the transaction.'),
|
|
449
|
+
validators=[MinValueValidator(0)]
|
|
450
|
+
)
|
|
451
|
+
description = models.CharField(
|
|
452
|
+
max_length=100,
|
|
453
|
+
null=True,
|
|
454
|
+
blank=True,
|
|
455
|
+
verbose_name=_('Transaction Description'),
|
|
456
|
+
help_text=_('A description to be included with this individual transaction.')
|
|
457
|
+
)
|
|
502
458
|
cleared = models.BooleanField(default=False, verbose_name=_('Cleared'))
|
|
503
459
|
reconciled = models.BooleanField(default=False, verbose_name=_('Reconciled'))
|
|
504
|
-
|
|
505
460
|
objects = TransactionModelManager()
|
|
506
461
|
|
|
507
462
|
class Meta:
|
|
@@ -520,14 +475,20 @@ class TransactionModelAbstract(CreateUpdateMixIn):
|
|
|
520
475
|
]
|
|
521
476
|
|
|
522
477
|
def __str__(self):
|
|
523
|
-
return '{
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
478
|
+
return '{code}-{name}/{balance_type}: {amount}/{tx_type}'.format(
|
|
479
|
+
code=self.account.code,
|
|
480
|
+
name=self.account.name,
|
|
481
|
+
balance_type=self.account.balance_type,
|
|
482
|
+
amount=self.amount,
|
|
483
|
+
tx_type=self.tx_type
|
|
484
|
+
)
|
|
528
485
|
|
|
529
486
|
@property
|
|
530
487
|
def coa_id(self):
|
|
488
|
+
"""
|
|
489
|
+
Fetch the Chart of Accounts (CoA) ID associated with the transaction's account.
|
|
490
|
+
Returns `None` if the account is not set.
|
|
491
|
+
"""
|
|
531
492
|
try:
|
|
532
493
|
return getattr(self, '_coa_id')
|
|
533
494
|
except AttributeError:
|
|
@@ -535,11 +496,11 @@ class TransactionModelAbstract(CreateUpdateMixIn):
|
|
|
535
496
|
return None
|
|
536
497
|
return self.account.coa_model_id
|
|
537
498
|
|
|
538
|
-
def
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
499
|
+
def is_debit(self):
|
|
500
|
+
return self.tx_type == self.DEBIT
|
|
501
|
+
|
|
502
|
+
def is_credit(self):
|
|
503
|
+
return self.tx_type == self.CREDIT
|
|
543
504
|
|
|
544
505
|
|
|
545
506
|
class TransactionModel(TransactionModelAbstract):
|
|
@@ -554,27 +515,65 @@ class TransactionModel(TransactionModelAbstract):
|
|
|
554
515
|
|
|
555
516
|
def transactionmodel_presave(instance: TransactionModel, **kwargs):
|
|
556
517
|
"""
|
|
518
|
+
Pre-save validation for the TransactionModel instance.
|
|
519
|
+
|
|
520
|
+
This function is executed before saving a `TransactionModel` instance,
|
|
521
|
+
ensuring that certain conditions are met to maintain data integrity.
|
|
522
|
+
|
|
557
523
|
Parameters
|
|
558
524
|
----------
|
|
559
525
|
instance : TransactionModel
|
|
560
|
-
The
|
|
526
|
+
The `TransactionModel` instance that is about to be saved.
|
|
561
527
|
kwargs : dict
|
|
562
|
-
Additional keyword arguments
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
528
|
+
Additional keyword arguments, such as the optional `bypass_account_state`.
|
|
529
|
+
|
|
530
|
+
Validations
|
|
531
|
+
-----------
|
|
532
|
+
The function performs the following validations:
|
|
533
|
+
1. **Account Transactionality**:
|
|
534
|
+
If the `bypass_account_state` flag is not provided or set to `False`,
|
|
535
|
+
it verifies whether the associated account can process transactions
|
|
536
|
+
by calling `instance.account.can_transact()`. If the account cannot
|
|
537
|
+
process transactions, the save operation is interrupted to prevent
|
|
538
|
+
invalid data.
|
|
539
|
+
|
|
540
|
+
2. **Journal Entry Lock**:
|
|
541
|
+
If the associated journal entry (`instance.journal_entry`) is locked,
|
|
542
|
+
the transaction cannot be modified. The save process is halted if the
|
|
543
|
+
journal entry is marked as locked.
|
|
568
544
|
|
|
569
545
|
Raises
|
|
570
546
|
------
|
|
571
547
|
TransactionModelValidationError
|
|
572
|
-
|
|
573
|
-
-
|
|
574
|
-
|
|
575
|
-
|
|
548
|
+
Raised in the following scenarios:
|
|
549
|
+
- **Account Transactionality Failure**:
|
|
550
|
+
When `bypass_account_state` is `False` or not provided, and the
|
|
551
|
+
associated account (`instance.account`) cannot process transactions.
|
|
552
|
+
The exception contains a message identifying the account.
|
|
553
|
+
|
|
554
|
+
- **Locked Journal Entry**:
|
|
555
|
+
When the associated journal entry (`instance.journal_entry`) is locked,
|
|
556
|
+
preventing modification of any related transactions. The error message
|
|
557
|
+
describes the locked journal entry constraint.
|
|
558
|
+
|
|
559
|
+
Example
|
|
560
|
+
-------
|
|
561
|
+
```python
|
|
562
|
+
instance = TransactionModel(...)
|
|
563
|
+
try:
|
|
564
|
+
transactionmodel_presave(instance)
|
|
565
|
+
instance.save() # Save proceeds if no validation error occurs
|
|
566
|
+
except TransactionModelValidationError as e:
|
|
567
|
+
handle_error(str(e)) # Handle validation exception
|
|
568
|
+
```
|
|
576
569
|
"""
|
|
577
570
|
bypass_account_state = kwargs.get('bypass_account_state', False)
|
|
571
|
+
|
|
572
|
+
if instance.account_id and instance.account.is_root_account():
|
|
573
|
+
raise TransactionModelValidationError(
|
|
574
|
+
message=_('Transactions cannot be linked to root accounts.')
|
|
575
|
+
)
|
|
576
|
+
|
|
578
577
|
if all([
|
|
579
578
|
not bypass_account_state,
|
|
580
579
|
not instance.account.can_transact()
|
|
@@ -582,7 +581,6 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs):
|
|
|
582
581
|
raise TransactionModelValidationError(
|
|
583
582
|
message=_(f'Cannot create or modify transactions on account model {instance.account}.')
|
|
584
583
|
)
|
|
585
|
-
|
|
586
584
|
if instance.journal_entry.is_locked():
|
|
587
585
|
raise TransactionModelValidationError(
|
|
588
586
|
message=_('Cannot modify transactions on locked journal entries.')
|