django-ledger 0.7.11__py3-none-any.whl → 0.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-ledger might be problematic. Click here for more details.
- django_ledger/__init__.py +1 -1
- django_ledger/context.py +12 -0
- django_ledger/forms/account.py +45 -46
- django_ledger/forms/bill.py +0 -4
- django_ledger/forms/closing_entry.py +13 -1
- django_ledger/forms/data_import.py +182 -63
- django_ledger/forms/estimate.py +3 -6
- django_ledger/forms/invoice.py +3 -7
- django_ledger/forms/item.py +10 -18
- django_ledger/forms/purchase_order.py +2 -4
- django_ledger/io/io_core.py +515 -400
- django_ledger/io/io_generator.py +7 -6
- django_ledger/io/io_library.py +1 -2
- django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
- django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
- django_ledger/models/__init__.py +2 -1
- django_ledger/models/accounts.py +109 -69
- django_ledger/models/bank_account.py +40 -23
- django_ledger/models/bill.py +386 -333
- django_ledger/models/chart_of_accounts.py +173 -105
- django_ledger/models/closing_entry.py +99 -48
- django_ledger/models/customer.py +100 -66
- django_ledger/models/data_import.py +818 -323
- django_ledger/models/deprecations.py +61 -0
- django_ledger/models/entity.py +891 -644
- django_ledger/models/estimate.py +57 -28
- django_ledger/models/invoice.py +46 -26
- django_ledger/models/items.py +503 -142
- django_ledger/models/journal_entry.py +61 -47
- django_ledger/models/ledger.py +106 -42
- django_ledger/models/mixins.py +424 -281
- django_ledger/models/purchase_order.py +39 -17
- django_ledger/models/receipt.py +1083 -0
- django_ledger/models/transactions.py +242 -139
- django_ledger/models/unit.py +93 -54
- django_ledger/models/utils.py +12 -2
- django_ledger/models/vendor.py +121 -70
- django_ledger/report/core.py +2 -14
- django_ledger/settings.py +57 -71
- django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
- django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +25 -0
- django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
- django_ledger/static/django_ledger/css/djl_styles.css +273 -0
- django_ledger/templates/django_ledger/bills/includes/card_bill.html +2 -2
- django_ledger/templates/django_ledger/components/menu.html +41 -26
- django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
- django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
- django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
- django_ledger/templates/django_ledger/customer/tags/customer_table.html +8 -6
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
- django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
- django_ledger/templates/django_ledger/entity/includes/card_entity.html +12 -6
- django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -1
- django_ledger/templates/django_ledger/financial_statements/cash_flow.html +4 -1
- django_ledger/templates/django_ledger/financial_statements/income_statement.html +4 -1
- django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +27 -3
- django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +16 -4
- django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +73 -18
- django_ledger/templates/django_ledger/includes/widget_ratios.html +18 -24
- django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +3 -3
- django_ledger/templates/django_ledger/layouts/base.html +7 -2
- django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
- django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
- django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
- django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
- django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +12 -7
- django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
- django_ledger/templatetags/django_ledger.py +338 -191
- django_ledger/tests/test_accounts.py +1 -2
- django_ledger/tests/test_io.py +17 -0
- django_ledger/tests/test_purchase_order.py +3 -3
- django_ledger/tests/test_transactions.py +1 -2
- django_ledger/urls/__init__.py +1 -4
- django_ledger/urls/customer.py +3 -0
- django_ledger/urls/data_import.py +3 -0
- django_ledger/urls/receipt.py +102 -0
- django_ledger/urls/vendor.py +1 -0
- django_ledger/views/__init__.py +1 -0
- django_ledger/views/bill.py +8 -11
- django_ledger/views/chart_of_accounts.py +6 -4
- django_ledger/views/closing_entry.py +11 -7
- django_ledger/views/customer.py +68 -30
- django_ledger/views/data_import.py +120 -66
- django_ledger/views/djl_api.py +3 -5
- django_ledger/views/entity.py +2 -4
- django_ledger/views/estimate.py +3 -7
- django_ledger/views/inventory.py +3 -5
- django_ledger/views/invoice.py +4 -6
- django_ledger/views/item.py +7 -11
- django_ledger/views/journal_entry.py +1 -2
- django_ledger/views/mixins.py +125 -93
- django_ledger/views/purchase_order.py +24 -35
- django_ledger/views/receipt.py +294 -0
- django_ledger/views/unit.py +1 -2
- django_ledger/views/vendor.py +54 -16
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/METADATA +43 -75
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/RECORD +104 -122
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +1 -1
- django_ledger-0.8.1.dist-info/top_level.txt +1 -0
- django_ledger/contrib/django_ledger_graphene/__init__.py +0 -0
- django_ledger/contrib/django_ledger_graphene/accounts/schema.py +0 -33
- django_ledger/contrib/django_ledger_graphene/api.py +0 -42
- django_ledger/contrib/django_ledger_graphene/apps.py +0 -6
- django_ledger/contrib/django_ledger_graphene/auth/mutations.py +0 -49
- django_ledger/contrib/django_ledger_graphene/auth/schema.py +0 -6
- django_ledger/contrib/django_ledger_graphene/bank_account/mutations.py +0 -61
- django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +0 -34
- django_ledger/contrib/django_ledger_graphene/bill/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/bill/schema.py +0 -34
- django_ledger/contrib/django_ledger_graphene/coa/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/coa/schema.py +0 -30
- django_ledger/contrib/django_ledger_graphene/customers/__init__.py +0 -0
- django_ledger/contrib/django_ledger_graphene/customers/mutations.py +0 -71
- django_ledger/contrib/django_ledger_graphene/customers/schema.py +0 -43
- django_ledger/contrib/django_ledger_graphene/data_import/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/data_import/schema.py +0 -0
- django_ledger/contrib/django_ledger_graphene/entity/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/entity/schema.py +0 -94
- django_ledger/contrib/django_ledger_graphene/item/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/item/schema.py +0 -31
- django_ledger/contrib/django_ledger_graphene/journal_entry/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +0 -35
- django_ledger/contrib/django_ledger_graphene/ledger/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/ledger/schema.py +0 -32
- django_ledger/contrib/django_ledger_graphene/purchase_order/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/purchase_order/schema.py +0 -31
- django_ledger/contrib/django_ledger_graphene/transaction/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/transaction/schema.py +0 -36
- django_ledger/contrib/django_ledger_graphene/unit/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/unit/schema.py +0 -27
- django_ledger/contrib/django_ledger_graphene/vendor/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/vendor/schema.py +0 -37
- django_ledger/contrib/django_ledger_graphene/views.py +0 -12
- django_ledger-0.7.11.dist-info/top_level.txt +0 -4
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/AUTHORS.md +0 -0
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/LICENSE +0 -0
|
@@ -17,8 +17,9 @@ The TransactionModel, together with the IOMixIn, is essential for ensuring seaml
|
|
|
17
17
|
financial statement production in the Django Ledger framework.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
+
import warnings
|
|
20
21
|
from datetime import datetime, date
|
|
21
|
-
from typing import List, Union,
|
|
22
|
+
from typing import List, Union, Set
|
|
22
23
|
from uuid import uuid4, UUID
|
|
23
24
|
|
|
24
25
|
from django.contrib.auth import get_user_model
|
|
@@ -27,13 +28,22 @@ from django.core.validators import MinValueValidator
|
|
|
27
28
|
from django.db import models
|
|
28
29
|
from django.db.models import Q, QuerySet, Manager, F
|
|
29
30
|
from django.db.models.signals import pre_save
|
|
31
|
+
from django.urls import reverse
|
|
30
32
|
from django.utils.translation import gettext_lazy as _
|
|
31
33
|
|
|
32
34
|
from django_ledger.io.io_core import validate_io_timestamp
|
|
33
|
-
from django_ledger.models import
|
|
35
|
+
from django_ledger.models import (
|
|
36
|
+
AccountModel,
|
|
37
|
+
BillModel,
|
|
38
|
+
EntityModel,
|
|
39
|
+
InvoiceModel,
|
|
40
|
+
LedgerModel,
|
|
41
|
+
)
|
|
42
|
+
from django_ledger.models.deprecations import deprecated_entity_slug_behavior
|
|
34
43
|
from django_ledger.models.mixins import CreateUpdateMixIn
|
|
35
44
|
from django_ledger.models.unit import EntityUnitModel
|
|
36
45
|
from django_ledger.models.utils import lazy_loader
|
|
46
|
+
from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
|
|
37
47
|
|
|
38
48
|
UserModel = get_user_model()
|
|
39
49
|
|
|
@@ -49,7 +59,37 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
49
59
|
based on common use cases.
|
|
50
60
|
"""
|
|
51
61
|
|
|
52
|
-
def
|
|
62
|
+
def for_user(self, user_model) -> 'TransactionModelQuerySet':
|
|
63
|
+
"""
|
|
64
|
+
Filters transactions accessible to a specific user based on their permissions.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
user_model : UserModel
|
|
69
|
+
The user object for which the transactions should be filtered.
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
TransactionModelQuerySet
|
|
74
|
+
A queryset containing transactions filtered by the user's access level.
|
|
75
|
+
|
|
76
|
+
Description
|
|
77
|
+
-----------
|
|
78
|
+
- Returns all `TransactionModel` objects for superusers.
|
|
79
|
+
- For regular users, it filters transactions where:
|
|
80
|
+
- The user is an admin of the entity associated with the ledger in the transaction.
|
|
81
|
+
- The user is a manager of the entity associated with the ledger in the transaction.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
if user_model.is_superuser:
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
return self.filter(
|
|
88
|
+
Q(journal_entry__ledger__entity__admin=user_model)
|
|
89
|
+
| Q(journal_entry__ledger__entity__managers__in=[user_model])
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def posted(self) -> 'TransactionModelQuerySet':
|
|
53
93
|
"""
|
|
54
94
|
Retrieves transactions that are part of a posted journal entry and ledger.
|
|
55
95
|
|
|
@@ -63,11 +103,12 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
63
103
|
A QuerySet containing only transactions that meet the "posted" criteria.
|
|
64
104
|
"""
|
|
65
105
|
return self.filter(
|
|
66
|
-
Q(journal_entry__posted=True) &
|
|
67
|
-
Q(journal_entry__ledger__posted=True)
|
|
106
|
+
Q(journal_entry__posted=True) & Q(journal_entry__ledger__posted=True)
|
|
68
107
|
)
|
|
69
108
|
|
|
70
|
-
def for_accounts(
|
|
109
|
+
def for_accounts(
|
|
110
|
+
self, account_list: List[Union[AccountModel, str, UUID]]
|
|
111
|
+
) -> 'TransactionModelQuerySet':
|
|
71
112
|
"""
|
|
72
113
|
Filters transactions based on the accounts they are associated with.
|
|
73
114
|
|
|
@@ -85,7 +126,9 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
85
126
|
|
|
86
127
|
if not isinstance(account_list, list) or not len(account_list) > 0:
|
|
87
128
|
raise TransactionModelValidationError(
|
|
88
|
-
message=_(
|
|
129
|
+
message=_(
|
|
130
|
+
'Account list must be a list of AccountModel, UUID or str objects (codes).'
|
|
131
|
+
)
|
|
89
132
|
)
|
|
90
133
|
if isinstance(account_list[0], str):
|
|
91
134
|
return self.filter(account__code__in=account_list)
|
|
@@ -94,10 +137,14 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
94
137
|
elif isinstance(account_list[0], AccountModel):
|
|
95
138
|
return self.filter(account__in=account_list)
|
|
96
139
|
raise TransactionModelValidationError(
|
|
97
|
-
message=_(
|
|
140
|
+
message=_(
|
|
141
|
+
'Account list must be a list of AccountModel, UUID or str objects (codes).'
|
|
142
|
+
)
|
|
98
143
|
)
|
|
99
144
|
|
|
100
|
-
def for_roles(
|
|
145
|
+
def for_roles(
|
|
146
|
+
self, role_list: Union[str, List[str], Set[str]]
|
|
147
|
+
) -> 'TransactionModelQuerySet':
|
|
101
148
|
"""
|
|
102
149
|
Fetches a QuerySet of TransactionModels which AccountModel has a specific role.
|
|
103
150
|
|
|
@@ -115,7 +162,9 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
115
162
|
return self.filter(account__role__in=[role_list])
|
|
116
163
|
return self.filter(account__role__in=role_list)
|
|
117
164
|
|
|
118
|
-
def for_unit(
|
|
165
|
+
def for_unit(
|
|
166
|
+
self, unit_slug: Union[str, EntityUnitModel]
|
|
167
|
+
) -> 'TransactionModelQuerySet':
|
|
119
168
|
"""
|
|
120
169
|
Filters transactions based on their associated entity unit.
|
|
121
170
|
|
|
@@ -133,7 +182,9 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
133
182
|
return self.filter(journal_entry__entity_unit=unit_slug)
|
|
134
183
|
return self.filter(journal_entry__entity_unit__slug__exact=unit_slug)
|
|
135
184
|
|
|
136
|
-
def for_activity(
|
|
185
|
+
def for_activity(
|
|
186
|
+
self, activity_list: Union[str, List[str], Set[str]]
|
|
187
|
+
) -> 'TransactionModelQuerySet':
|
|
137
188
|
"""
|
|
138
189
|
Filters transactions based on their associated activity or activities.
|
|
139
190
|
|
|
@@ -151,7 +202,9 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
151
202
|
return self.filter(journal_entry__activity__in=[activity_list])
|
|
152
203
|
return self.filter(journal_entry__activity__in=activity_list)
|
|
153
204
|
|
|
154
|
-
def to_date(
|
|
205
|
+
def to_date(
|
|
206
|
+
self, to_date: Union[str, date, datetime]
|
|
207
|
+
) -> 'TransactionModelQuerySet':
|
|
155
208
|
"""
|
|
156
209
|
Filters transactions occurring on or before a specific date or timestamp.
|
|
157
210
|
|
|
@@ -177,7 +230,9 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
177
230
|
return self.filter(journal_entry__timestamp__date__lte=to_date)
|
|
178
231
|
return self.filter(journal_entry__timestamp__lte=to_date)
|
|
179
232
|
|
|
180
|
-
def from_date(
|
|
233
|
+
def from_date(
|
|
234
|
+
self, from_date: Union[str, date, datetime]
|
|
235
|
+
) -> 'TransactionModelQuerySet':
|
|
181
236
|
"""
|
|
182
237
|
Filters transactions occurring on or after a specific date or timestamp.
|
|
183
238
|
|
|
@@ -203,7 +258,7 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
203
258
|
|
|
204
259
|
return self.filter(journal_entry__timestamp__gte=from_date)
|
|
205
260
|
|
|
206
|
-
def not_closing_entry(self):
|
|
261
|
+
def not_closing_entry(self) -> 'TransactionModelQuerySet':
|
|
207
262
|
"""
|
|
208
263
|
Filters transactions that are *not* part of a closing journal entry.
|
|
209
264
|
|
|
@@ -214,7 +269,7 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
214
269
|
"""
|
|
215
270
|
return self.filter(journal_entry__is_closing_entry=False)
|
|
216
271
|
|
|
217
|
-
def is_closing_entry(self):
|
|
272
|
+
def is_closing_entry(self) -> 'TransactionModelQuerySet':
|
|
218
273
|
"""
|
|
219
274
|
Filters transactions that are part of a closing journal entry.
|
|
220
275
|
|
|
@@ -225,7 +280,9 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
225
280
|
"""
|
|
226
281
|
return self.filter(journal_entry__is_closing_entry=True)
|
|
227
282
|
|
|
228
|
-
def for_ledger(
|
|
283
|
+
def for_ledger(
|
|
284
|
+
self, ledger_model: Union[LedgerModel, UUID, str]
|
|
285
|
+
) -> 'TransactionModelQuerySet':
|
|
229
286
|
"""
|
|
230
287
|
Filters transactions for a specific ledger under a given entity.
|
|
231
288
|
|
|
@@ -243,7 +300,7 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
243
300
|
return self.filter(journal_entry__ledger__uuid__exact=ledger_model)
|
|
244
301
|
return self.filter(journal_entry__ledger=ledger_model)
|
|
245
302
|
|
|
246
|
-
def for_journal_entry(self, je_model):
|
|
303
|
+
def for_journal_entry(self, je_model) -> 'TransactionModelQuerySet':
|
|
247
304
|
"""
|
|
248
305
|
Filters transactions for a specific journal entry under a given ledger and entity.
|
|
249
306
|
|
|
@@ -261,7 +318,9 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
261
318
|
return self.filter(journal_entry=je_model)
|
|
262
319
|
return self.filter(journal_entry__uuid__exact=je_model)
|
|
263
320
|
|
|
264
|
-
def for_bill(
|
|
321
|
+
def for_bill(
|
|
322
|
+
self, bill_model: Union[BillModel, str, UUID]
|
|
323
|
+
) -> 'TransactionModelQuerySet':
|
|
265
324
|
"""
|
|
266
325
|
Filters transactions for a specific bill under a given entity.
|
|
267
326
|
|
|
@@ -279,7 +338,9 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
279
338
|
return self.filter(journal_entry__ledger__billmodel=bill_model)
|
|
280
339
|
return self.filter(journal_entry__ledger__billmodel__uuid__exact=bill_model)
|
|
281
340
|
|
|
282
|
-
def for_invoice(
|
|
341
|
+
def for_invoice(
|
|
342
|
+
self, invoice_model: Union[InvoiceModel, str, UUID]
|
|
343
|
+
) -> 'TransactionModelQuerySet':
|
|
283
344
|
"""
|
|
284
345
|
Filters transactions for a specific invoice under a given entity.
|
|
285
346
|
|
|
@@ -295,9 +356,11 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
295
356
|
"""
|
|
296
357
|
if isinstance(invoice_model, InvoiceModel):
|
|
297
358
|
return self.filter(journal_entry__ledger__invoicemodel=invoice_model)
|
|
298
|
-
return self.filter(
|
|
359
|
+
return self.filter(
|
|
360
|
+
journal_entry__ledger__invoicemodel__uuid__exact=invoice_model
|
|
361
|
+
)
|
|
299
362
|
|
|
300
|
-
def with_annotated_details(self):
|
|
363
|
+
def with_annotated_details(self) -> 'TransactionModelQuerySet':
|
|
301
364
|
return self.annotate(
|
|
302
365
|
entity_unit_name=F('journal_entry__entity_unit__name'),
|
|
303
366
|
account_code=F('account__code'),
|
|
@@ -305,16 +368,16 @@ class TransactionModelQuerySet(QuerySet):
|
|
|
305
368
|
timestamp=F('journal_entry__timestamp'),
|
|
306
369
|
)
|
|
307
370
|
|
|
308
|
-
def is_cleared(self):
|
|
371
|
+
def is_cleared(self) -> 'TransactionModelQuerySet':
|
|
309
372
|
return self.filter(cleared=True)
|
|
310
373
|
|
|
311
|
-
def not_cleared(self):
|
|
374
|
+
def not_cleared(self) -> 'TransactionModelQuerySet':
|
|
312
375
|
return self.filter(cleared=False)
|
|
313
376
|
|
|
314
|
-
def is_reconciled(self):
|
|
377
|
+
def is_reconciled(self) -> 'TransactionModelQuerySet':
|
|
315
378
|
return self.filter(reconciled=True)
|
|
316
379
|
|
|
317
|
-
def not_reconciled(self):
|
|
380
|
+
def not_reconciled(self) -> 'TransactionModelQuerySet':
|
|
318
381
|
return self.filter(reconciled=False)
|
|
319
382
|
|
|
320
383
|
|
|
@@ -340,54 +403,27 @@ class TransactionModelManager(Manager):
|
|
|
340
403
|
"""
|
|
341
404
|
qs = TransactionModelQuerySet(self.model, using=self._db)
|
|
342
405
|
return qs.annotate(
|
|
406
|
+
_entity_slug=F('journal_entry__ledger__entity__slug'),
|
|
407
|
+
_ledger_uuid=F('journal_entry__ledger_id'),
|
|
343
408
|
timestamp=F('journal_entry__timestamp'),
|
|
344
|
-
_coa_id=F('account__coa_model_id')
|
|
409
|
+
_coa_id=F('account__coa_model_id'),
|
|
345
410
|
).select_related(
|
|
346
|
-
'journal_entry',
|
|
347
|
-
'account',
|
|
348
|
-
'account__coa_model',
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
def for_user(self, user_model) -> TransactionModelQuerySet:
|
|
352
|
-
"""
|
|
353
|
-
Filters transactions accessible to a specific user based on their permissions.
|
|
354
|
-
|
|
355
|
-
Parameters
|
|
356
|
-
----------
|
|
357
|
-
user_model : UserModel
|
|
358
|
-
The user object for which the transactions should be filtered.
|
|
359
|
-
|
|
360
|
-
Returns
|
|
361
|
-
-------
|
|
362
|
-
TransactionModelQuerySet
|
|
363
|
-
A queryset containing transactions filtered by the user's access level.
|
|
364
|
-
|
|
365
|
-
Description
|
|
366
|
-
-----------
|
|
367
|
-
- Returns all `TransactionModel` objects for superusers.
|
|
368
|
-
- For regular users, it filters transactions where:
|
|
369
|
-
- The user is an admin of the entity associated with the ledger in the transaction.
|
|
370
|
-
- The user is a manager of the entity associated with the ledger in the transaction.
|
|
371
|
-
"""
|
|
372
|
-
qs = self.get_queryset()
|
|
373
|
-
return qs.filter(
|
|
374
|
-
Q(journal_entry__ledger__entity__admin=user_model) |
|
|
375
|
-
Q(journal_entry__ledger__entity__managers__in=[user_model])
|
|
411
|
+
'journal_entry',
|
|
412
|
+
'account',
|
|
413
|
+
'account__coa_model',
|
|
376
414
|
)
|
|
377
415
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
416
|
+
@deprecated_entity_slug_behavior
|
|
417
|
+
def for_entity(
|
|
418
|
+
self, entity_model: EntityModel | str | UUID = None, **kwargs
|
|
419
|
+
) -> TransactionModelQuerySet:
|
|
381
420
|
"""
|
|
382
421
|
Filters transactions for a specific entity, optionally scoped to a specific user.
|
|
383
422
|
|
|
384
423
|
Parameters
|
|
385
424
|
----------
|
|
386
|
-
|
|
425
|
+
entity_model : Union[EntityModel, str, UUID]
|
|
387
426
|
Identifier for the entity. This can be an `EntityModel` object, a slug (str), or a UUID.
|
|
388
|
-
user_model : Optional[UserModel], optional
|
|
389
|
-
The user for whom transactions should be filtered. If provided, applies user-specific
|
|
390
|
-
filtering. Defaults to None.
|
|
391
427
|
|
|
392
428
|
Returns
|
|
393
429
|
-------
|
|
@@ -399,70 +435,85 @@ class TransactionModelManager(Manager):
|
|
|
399
435
|
- If `user_model` is provided, only transactions accessible by the user are included.
|
|
400
436
|
- Supports flexible filtering by accepting different forms of `entity_slug`.
|
|
401
437
|
"""
|
|
402
|
-
if user_model:
|
|
403
|
-
qs = self.for_user(user_model=user_model)
|
|
404
|
-
else:
|
|
405
|
-
qs = self.get_queryset()
|
|
406
438
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
439
|
+
qs = self.get_queryset()
|
|
440
|
+
if 'user_model' in kwargs:
|
|
441
|
+
warnings.warn(
|
|
442
|
+
'user_model parameter is deprecated and will be removed in a future release. '
|
|
443
|
+
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
444
|
+
DeprecationWarning,
|
|
445
|
+
stacklevel=2,
|
|
446
|
+
)
|
|
447
|
+
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
448
|
+
qs = qs.for_user(kwargs['user_model'])
|
|
449
|
+
|
|
450
|
+
if isinstance(entity_model, EntityModel):
|
|
451
|
+
qs = qs.filter(journal_entry__ledger__entity=entity_model)
|
|
452
|
+
elif isinstance(entity_model, UUID):
|
|
453
|
+
qs = qs.filter(journal_entry__ledger__entity_id=entity_model)
|
|
454
|
+
elif isinstance(entity_model, str):
|
|
455
|
+
qs = qs.filter(journal_entry__ledger__entity__slug__exact=entity_model)
|
|
456
|
+
else:
|
|
457
|
+
raise TransactionModelValidationError(
|
|
458
|
+
message='entity_model parameter must be either an EntityModel, String or a UUID.'
|
|
459
|
+
)
|
|
460
|
+
return qs
|
|
412
461
|
|
|
413
462
|
|
|
414
463
|
class TransactionModelAbstract(CreateUpdateMixIn):
|
|
415
464
|
"""
|
|
416
|
-
Abstract model for representing
|
|
417
|
-
|
|
418
|
-
This
|
|
419
|
-
|
|
420
|
-
and
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
465
|
+
Abstract base model class for representing financial transactions in a ledger system.
|
|
466
|
+
|
|
467
|
+
This class defines the structure and behavior of transactions in a generic ledger system,
|
|
468
|
+
including attributes such as transaction type, associated account, amount, description,
|
|
469
|
+
and status flags for cleared and reconciled state. It also includes methods and properties
|
|
470
|
+
to facilitate queries and determine transaction type.
|
|
471
|
+
|
|
472
|
+
Attributes
|
|
473
|
+
----------
|
|
474
|
+
uuid : UUID
|
|
475
|
+
Unique identifier of the transaction.
|
|
476
|
+
tx_type : str
|
|
477
|
+
Type of the transaction, either 'credit' or 'debit'.
|
|
478
|
+
journal_entry : ForeignKey
|
|
479
|
+
Reference to the associated JournalEntryModel instance for this transaction.
|
|
480
|
+
account : ForeignKey
|
|
481
|
+
Reference to the associated account in the Chart of Accounts.
|
|
482
|
+
amount : Decimal
|
|
483
|
+
Monetary amount for the transaction. Must be a non-negative value.
|
|
484
|
+
description : str, optional
|
|
485
|
+
Description of the transaction.
|
|
486
|
+
cleared : bool
|
|
487
|
+
Indicates if the transaction has been cleared. Defaults to False.
|
|
488
|
+
reconciled : bool
|
|
489
|
+
Indicates if the transaction has been reconciled. Defaults to False.
|
|
490
|
+
objects : TransactionModelManager
|
|
491
|
+
Custom manager for managing transaction models.
|
|
442
492
|
"""
|
|
443
493
|
|
|
444
494
|
CREDIT = 'credit'
|
|
445
495
|
DEBIT = 'debit'
|
|
446
|
-
TX_TYPE = [
|
|
447
|
-
(CREDIT, _('Credit')),
|
|
448
|
-
(DEBIT, _('Debit'))
|
|
449
|
-
]
|
|
496
|
+
TX_TYPE = [(CREDIT, _('Credit')), (DEBIT, _('Debit'))]
|
|
450
497
|
|
|
451
498
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
452
|
-
tx_type = models.CharField(
|
|
499
|
+
tx_type = models.CharField(
|
|
500
|
+
max_length=10, choices=TX_TYPE, verbose_name=_('Transaction Type')
|
|
501
|
+
)
|
|
453
502
|
|
|
454
503
|
journal_entry = models.ForeignKey(
|
|
455
504
|
'django_ledger.JournalEntryModel',
|
|
456
505
|
editable=False,
|
|
457
506
|
verbose_name=_('Journal Entry'),
|
|
458
507
|
help_text=_('Journal Entry to be associated with this transaction.'),
|
|
459
|
-
on_delete=models.CASCADE
|
|
508
|
+
on_delete=models.CASCADE,
|
|
460
509
|
)
|
|
461
510
|
account = models.ForeignKey(
|
|
462
511
|
'django_ledger.AccountModel',
|
|
463
512
|
verbose_name=_('Account'),
|
|
464
|
-
help_text=_(
|
|
465
|
-
|
|
513
|
+
help_text=_(
|
|
514
|
+
'Account from Chart of Accounts to be associated with this transaction.'
|
|
515
|
+
),
|
|
516
|
+
on_delete=models.PROTECT,
|
|
466
517
|
)
|
|
467
518
|
amount = models.DecimalField(
|
|
468
519
|
decimal_places=2,
|
|
@@ -470,18 +521,18 @@ class TransactionModelAbstract(CreateUpdateMixIn):
|
|
|
470
521
|
default=0.00,
|
|
471
522
|
verbose_name=_('Amount'),
|
|
472
523
|
help_text=_('Amount of the transaction.'),
|
|
473
|
-
validators=[MinValueValidator(0)]
|
|
524
|
+
validators=[MinValueValidator(0)],
|
|
474
525
|
)
|
|
475
526
|
description = models.CharField(
|
|
476
527
|
max_length=100,
|
|
477
528
|
null=True,
|
|
478
529
|
blank=True,
|
|
479
530
|
verbose_name=_('Transaction Description'),
|
|
480
|
-
help_text=_('A description to be included with this individual transaction.')
|
|
531
|
+
help_text=_('A description to be included with this individual transaction.'),
|
|
481
532
|
)
|
|
482
533
|
cleared = models.BooleanField(default=False, verbose_name=_('Cleared'))
|
|
483
534
|
reconciled = models.BooleanField(default=False, verbose_name=_('Reconciled'))
|
|
484
|
-
objects = TransactionModelManager()
|
|
535
|
+
objects = TransactionModelManager.from_queryset(TransactionModelQuerySet)()
|
|
485
536
|
|
|
486
537
|
class Meta:
|
|
487
538
|
abstract = True
|
|
@@ -504,14 +555,39 @@ class TransactionModelAbstract(CreateUpdateMixIn):
|
|
|
504
555
|
name=self.account.name,
|
|
505
556
|
balance_type=self.account.balance_type,
|
|
506
557
|
amount=self.amount,
|
|
507
|
-
tx_type=self.tx_type
|
|
558
|
+
tx_type=self.tx_type,
|
|
508
559
|
)
|
|
509
560
|
|
|
561
|
+
@property
|
|
562
|
+
def entity_slug(self) -> str:
|
|
563
|
+
try:
|
|
564
|
+
return getattr(self, '_entity_slug')
|
|
565
|
+
except AttributeError:
|
|
566
|
+
pass
|
|
567
|
+
return self.journal_entry.ledger.entity.slug
|
|
568
|
+
|
|
569
|
+
@property
|
|
570
|
+
def ledger_uuid(self) -> UUID:
|
|
571
|
+
try:
|
|
572
|
+
return getattr(self, '_ledger_uuid')
|
|
573
|
+
except AttributeError:
|
|
574
|
+
pass
|
|
575
|
+
return self.journal_entry.ledger.uuid
|
|
576
|
+
|
|
510
577
|
@property
|
|
511
578
|
def coa_id(self):
|
|
512
579
|
"""
|
|
513
|
-
|
|
514
|
-
|
|
580
|
+
Returns the Chart of Accounts (COA) ID associated with the current object.
|
|
581
|
+
|
|
582
|
+
The property attempts to retrieve the COA ID value stored internally, if it exists.
|
|
583
|
+
If the internal value is not set and the `account` attribute is not `None`,
|
|
584
|
+
the COA ID is obtained from the `account.coa_model_id` attribute. Otherwise,
|
|
585
|
+
it will return `None`.
|
|
586
|
+
|
|
587
|
+
Returns
|
|
588
|
+
-------
|
|
589
|
+
Any or None
|
|
590
|
+
The COA ID if it exists, otherwise `None`.
|
|
515
591
|
"""
|
|
516
592
|
try:
|
|
517
593
|
return getattr(self, '_coa_id')
|
|
@@ -520,12 +596,53 @@ class TransactionModelAbstract(CreateUpdateMixIn):
|
|
|
520
596
|
return None
|
|
521
597
|
return self.account.coa_model_id
|
|
522
598
|
|
|
523
|
-
def is_debit(self):
|
|
599
|
+
def is_debit(self) -> bool:
|
|
600
|
+
"""
|
|
601
|
+
Determines if the transaction type is a debit.
|
|
602
|
+
|
|
603
|
+
This method checks whether the transaction type of the current instance is
|
|
604
|
+
set to a debit type.
|
|
605
|
+
|
|
606
|
+
Returns
|
|
607
|
+
-------
|
|
608
|
+
bool
|
|
609
|
+
True if the transaction type is debit, otherwise False.
|
|
610
|
+
"""
|
|
524
611
|
return self.tx_type == self.DEBIT
|
|
525
612
|
|
|
526
|
-
def is_credit(self):
|
|
613
|
+
def is_credit(self) -> bool:
|
|
614
|
+
"""
|
|
615
|
+
Check if the transaction is of type 'credit'.
|
|
616
|
+
|
|
617
|
+
This method evaluates whether the transaction type of the current instance
|
|
618
|
+
matches the 'credit' transaction type.
|
|
619
|
+
|
|
620
|
+
Returns
|
|
621
|
+
-------
|
|
622
|
+
bool
|
|
623
|
+
True if the transaction type is 'credit', False otherwise.
|
|
624
|
+
"""
|
|
527
625
|
return self.tx_type == self.CREDIT
|
|
528
626
|
|
|
627
|
+
def get_ledger_detailr_url(self) -> str:
|
|
628
|
+
return reverse(
|
|
629
|
+
viewname='django_ledger:ledger-detail',
|
|
630
|
+
kwargs={
|
|
631
|
+
'ledger_pk': self.ledger_uuid,
|
|
632
|
+
'entity_slug': self.entity_slug,
|
|
633
|
+
},
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
def get_journal_entry_detail_url(self) -> str:
|
|
637
|
+
return reverse(
|
|
638
|
+
viewname='django_ledger:je-detail',
|
|
639
|
+
kwargs={
|
|
640
|
+
'je_pk': self.journal_entry_id,
|
|
641
|
+
'entity_slug': self.entity_slug,
|
|
642
|
+
'ledger_pk': self.ledger_uuid,
|
|
643
|
+
},
|
|
644
|
+
)
|
|
645
|
+
|
|
529
646
|
|
|
530
647
|
class TransactionModel(TransactionModelAbstract):
|
|
531
648
|
"""
|
|
@@ -543,15 +660,6 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs):
|
|
|
543
660
|
This function is executed before saving a `TransactionModel` instance,
|
|
544
661
|
ensuring that certain conditions are met to maintain data integrity.
|
|
545
662
|
|
|
546
|
-
Parameters
|
|
547
|
-
----------
|
|
548
|
-
instance : TransactionModel
|
|
549
|
-
The `TransactionModel` instance that is about to be saved.
|
|
550
|
-
kwargs : dict
|
|
551
|
-
Additional keyword arguments, such as the optional `bypass_account_state`.
|
|
552
|
-
|
|
553
|
-
Validations
|
|
554
|
-
-----------
|
|
555
663
|
The function performs the following validations:
|
|
556
664
|
1. **Account Transactionality**:
|
|
557
665
|
If the `bypass_account_state` flag is not provided or set to `False`,
|
|
@@ -565,6 +673,13 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs):
|
|
|
565
673
|
the transaction cannot be modified. The save process is halted if the
|
|
566
674
|
journal entry is marked as locked.
|
|
567
675
|
|
|
676
|
+
Parameters
|
|
677
|
+
----------
|
|
678
|
+
instance : TransactionModel
|
|
679
|
+
The `TransactionModel` instance that is about to be saved.
|
|
680
|
+
kwargs : dict
|
|
681
|
+
Additional keyword arguments, such as the optional `bypass_account_state`.
|
|
682
|
+
|
|
568
683
|
Raises
|
|
569
684
|
------
|
|
570
685
|
TransactionModelValidationError
|
|
@@ -578,17 +693,6 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs):
|
|
|
578
693
|
When the associated journal entry (`instance.journal_entry`) is locked,
|
|
579
694
|
preventing modification of any related transactions. The error message
|
|
580
695
|
describes the locked journal entry constraint.
|
|
581
|
-
|
|
582
|
-
Example
|
|
583
|
-
-------
|
|
584
|
-
```python
|
|
585
|
-
instance = TransactionModel(...)
|
|
586
|
-
try:
|
|
587
|
-
transactionmodel_presave(instance)
|
|
588
|
-
instance.save() # Save proceeds if no validation error occurs
|
|
589
|
-
except TransactionModelValidationError as e:
|
|
590
|
-
handle_error(str(e)) # Handle validation exception
|
|
591
|
-
```
|
|
592
696
|
"""
|
|
593
697
|
bypass_account_state = kwargs.get('bypass_account_state', False)
|
|
594
698
|
|
|
@@ -597,12 +701,11 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs):
|
|
|
597
701
|
message=_('Transactions cannot be linked to root accounts.')
|
|
598
702
|
)
|
|
599
703
|
|
|
600
|
-
if all([
|
|
601
|
-
not bypass_account_state,
|
|
602
|
-
not instance.account.can_transact()
|
|
603
|
-
]):
|
|
704
|
+
if all([not bypass_account_state, not instance.account.can_transact()]):
|
|
604
705
|
raise TransactionModelValidationError(
|
|
605
|
-
message=_(
|
|
706
|
+
message=_(
|
|
707
|
+
f'Cannot create or modify transactions on account model {instance.account}.'
|
|
708
|
+
)
|
|
606
709
|
)
|
|
607
710
|
if instance.journal_entry.is_locked():
|
|
608
711
|
raise TransactionModelValidationError(
|