django-ledger 0.7.11__py3-none-any.whl → 0.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-ledger might be problematic. Click here for more details.
- django_ledger/__init__.py +1 -1
- django_ledger/context.py +12 -0
- django_ledger/forms/account.py +45 -46
- django_ledger/forms/bill.py +0 -4
- django_ledger/forms/closing_entry.py +13 -1
- django_ledger/forms/data_import.py +182 -63
- django_ledger/forms/estimate.py +3 -6
- django_ledger/forms/invoice.py +3 -7
- django_ledger/forms/item.py +10 -18
- django_ledger/forms/purchase_order.py +2 -4
- django_ledger/io/io_core.py +515 -400
- django_ledger/io/io_generator.py +7 -6
- django_ledger/io/io_library.py +1 -2
- django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
- django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
- django_ledger/models/__init__.py +2 -1
- django_ledger/models/accounts.py +109 -69
- django_ledger/models/bank_account.py +40 -23
- django_ledger/models/bill.py +386 -333
- django_ledger/models/chart_of_accounts.py +173 -105
- django_ledger/models/closing_entry.py +99 -48
- django_ledger/models/customer.py +100 -66
- django_ledger/models/data_import.py +818 -323
- django_ledger/models/deprecations.py +61 -0
- django_ledger/models/entity.py +891 -644
- django_ledger/models/estimate.py +57 -28
- django_ledger/models/invoice.py +46 -26
- django_ledger/models/items.py +503 -142
- django_ledger/models/journal_entry.py +61 -47
- django_ledger/models/ledger.py +106 -42
- django_ledger/models/mixins.py +424 -281
- django_ledger/models/purchase_order.py +39 -17
- django_ledger/models/receipt.py +1083 -0
- django_ledger/models/transactions.py +242 -139
- django_ledger/models/unit.py +93 -54
- django_ledger/models/utils.py +12 -2
- django_ledger/models/vendor.py +121 -70
- django_ledger/report/core.py +2 -14
- django_ledger/settings.py +57 -71
- django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
- django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +25 -0
- django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
- django_ledger/static/django_ledger/css/djl_styles.css +273 -0
- django_ledger/templates/django_ledger/bills/includes/card_bill.html +2 -2
- django_ledger/templates/django_ledger/components/menu.html +41 -26
- django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
- django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
- django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
- django_ledger/templates/django_ledger/customer/tags/customer_table.html +8 -6
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
- django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
- django_ledger/templates/django_ledger/entity/includes/card_entity.html +12 -6
- django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -1
- django_ledger/templates/django_ledger/financial_statements/cash_flow.html +4 -1
- django_ledger/templates/django_ledger/financial_statements/income_statement.html +4 -1
- django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +27 -3
- django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +16 -4
- django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +73 -18
- django_ledger/templates/django_ledger/includes/widget_ratios.html +18 -24
- django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +3 -3
- django_ledger/templates/django_ledger/layouts/base.html +7 -2
- django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
- django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
- django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
- django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
- django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +12 -7
- django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
- django_ledger/templatetags/django_ledger.py +338 -191
- django_ledger/tests/test_accounts.py +1 -2
- django_ledger/tests/test_io.py +17 -0
- django_ledger/tests/test_purchase_order.py +3 -3
- django_ledger/tests/test_transactions.py +1 -2
- django_ledger/urls/__init__.py +1 -4
- django_ledger/urls/customer.py +3 -0
- django_ledger/urls/data_import.py +3 -0
- django_ledger/urls/receipt.py +102 -0
- django_ledger/urls/vendor.py +1 -0
- django_ledger/views/__init__.py +1 -0
- django_ledger/views/bill.py +8 -11
- django_ledger/views/chart_of_accounts.py +6 -4
- django_ledger/views/closing_entry.py +11 -7
- django_ledger/views/customer.py +68 -30
- django_ledger/views/data_import.py +120 -66
- django_ledger/views/djl_api.py +3 -5
- django_ledger/views/entity.py +2 -4
- django_ledger/views/estimate.py +3 -7
- django_ledger/views/inventory.py +3 -5
- django_ledger/views/invoice.py +4 -6
- django_ledger/views/item.py +7 -11
- django_ledger/views/journal_entry.py +1 -2
- django_ledger/views/mixins.py +125 -93
- django_ledger/views/purchase_order.py +24 -35
- django_ledger/views/receipt.py +294 -0
- django_ledger/views/unit.py +1 -2
- django_ledger/views/vendor.py +54 -16
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/METADATA +43 -75
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/RECORD +104 -122
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +1 -1
- django_ledger-0.8.1.dist-info/top_level.txt +1 -0
- django_ledger/contrib/django_ledger_graphene/__init__.py +0 -0
- django_ledger/contrib/django_ledger_graphene/accounts/schema.py +0 -33
- django_ledger/contrib/django_ledger_graphene/api.py +0 -42
- django_ledger/contrib/django_ledger_graphene/apps.py +0 -6
- django_ledger/contrib/django_ledger_graphene/auth/mutations.py +0 -49
- django_ledger/contrib/django_ledger_graphene/auth/schema.py +0 -6
- django_ledger/contrib/django_ledger_graphene/bank_account/mutations.py +0 -61
- django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +0 -34
- django_ledger/contrib/django_ledger_graphene/bill/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/bill/schema.py +0 -34
- django_ledger/contrib/django_ledger_graphene/coa/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/coa/schema.py +0 -30
- django_ledger/contrib/django_ledger_graphene/customers/__init__.py +0 -0
- django_ledger/contrib/django_ledger_graphene/customers/mutations.py +0 -71
- django_ledger/contrib/django_ledger_graphene/customers/schema.py +0 -43
- django_ledger/contrib/django_ledger_graphene/data_import/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/data_import/schema.py +0 -0
- django_ledger/contrib/django_ledger_graphene/entity/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/entity/schema.py +0 -94
- django_ledger/contrib/django_ledger_graphene/item/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/item/schema.py +0 -31
- django_ledger/contrib/django_ledger_graphene/journal_entry/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +0 -35
- django_ledger/contrib/django_ledger_graphene/ledger/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/ledger/schema.py +0 -32
- django_ledger/contrib/django_ledger_graphene/purchase_order/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/purchase_order/schema.py +0 -31
- django_ledger/contrib/django_ledger_graphene/transaction/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/transaction/schema.py +0 -36
- django_ledger/contrib/django_ledger_graphene/unit/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/unit/schema.py +0 -27
- django_ledger/contrib/django_ledger_graphene/vendor/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/vendor/schema.py +0 -37
- django_ledger/contrib/django_ledger_graphene/views.py +0 -12
- django_ledger-0.7.11.dist-info/top_level.txt +0 -4
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/AUTHORS.md +0 -0
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/LICENSE +0 -0
django_ledger/io/io_core.py
CHANGED
|
@@ -87,40 +87,42 @@ Notes:
|
|
|
87
87
|
- The module is designed to work seamlessly with Django's ORM and custom models through utilities provided in
|
|
88
88
|
the Django Ledger framework.
|
|
89
89
|
"""
|
|
90
|
+
|
|
90
91
|
from collections import namedtuple
|
|
91
92
|
from dataclasses import dataclass
|
|
92
|
-
from datetime import
|
|
93
|
+
from datetime import date, datetime, timedelta
|
|
93
94
|
from itertools import groupby
|
|
94
95
|
from pathlib import Path
|
|
95
96
|
from random import choice
|
|
96
|
-
from typing import List,
|
|
97
|
+
from typing import Dict, List, Optional, Set, Tuple, Union
|
|
97
98
|
from zoneinfo import ZoneInfo
|
|
98
99
|
|
|
99
100
|
from django.conf import settings as global_settings
|
|
100
101
|
from django.contrib.auth import get_user_model
|
|
101
|
-
from django.core.exceptions import
|
|
102
|
-
from django.db
|
|
102
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
103
|
+
from django.db import transaction
|
|
104
|
+
from django.db.models import Case, DecimalField, F, QuerySet, Sum, When
|
|
103
105
|
from django.db.models.functions import TruncMonth
|
|
104
106
|
from django.http import Http404
|
|
105
107
|
from django.utils.dateparse import parse_date, parse_datetime
|
|
106
|
-
from django.utils.timezone import
|
|
108
|
+
from django.utils.timezone import is_naive, localdate, localtime, make_aware
|
|
107
109
|
from django.utils.translation import gettext_lazy as _
|
|
108
110
|
|
|
109
111
|
from django_ledger import settings
|
|
110
112
|
from django_ledger.exceptions import InvalidDateInputError, TransactionNotInBalanceError
|
|
111
|
-
from django_ledger.io import
|
|
113
|
+
from django_ledger.io import CREDIT, DEBIT
|
|
114
|
+
from django_ledger.io import roles as roles_module
|
|
112
115
|
from django_ledger.io.io_context import IODigestContextManager
|
|
113
116
|
from django_ledger.io.io_middleware import (
|
|
114
|
-
AccountRoleIOMiddleware,
|
|
115
117
|
AccountGroupIOMiddleware,
|
|
116
|
-
|
|
118
|
+
AccountRoleIOMiddleware,
|
|
117
119
|
BalanceSheetIOMiddleware,
|
|
120
|
+
CashFlowStatementIOMiddleware,
|
|
118
121
|
IncomeStatementIOMiddleware,
|
|
119
|
-
|
|
122
|
+
JEActivityIOMiddleware,
|
|
120
123
|
)
|
|
121
124
|
from django_ledger.io.ratios import FinancialRatioManager
|
|
122
125
|
from django_ledger.models.utils import lazy_loader
|
|
123
|
-
from django_ledger.settings import DJANGO_LEDGER_PDF_SUPPORT_ENABLED
|
|
124
126
|
|
|
125
127
|
UserModel = get_user_model()
|
|
126
128
|
|
|
@@ -177,7 +179,7 @@ def diff_tx_data(tx_data: list, raise_exception: bool = True):
|
|
|
177
179
|
else:
|
|
178
180
|
raise ValidationError('Only Dictionary or TransactionModel allowed.')
|
|
179
181
|
|
|
180
|
-
is_valid =
|
|
182
|
+
is_valid = credits == debits
|
|
181
183
|
diff = credits - debits
|
|
182
184
|
|
|
183
185
|
if not is_valid and abs(diff) > settings.DJANGO_LEDGER_TRANSACTION_MAX_TOLERANCE:
|
|
@@ -213,40 +215,51 @@ def check_tx_balance(tx_data: list, perform_correction: bool = False) -> bool:
|
|
|
213
215
|
tolerance (with or without correction). Returns False otherwise.
|
|
214
216
|
"""
|
|
215
217
|
if tx_data:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
IS_TX_MODEL, is_valid, diff = diff_tx_data(
|
|
219
|
+
tx_data, raise_exception=perform_correction
|
|
220
|
+
)
|
|
218
221
|
|
|
219
222
|
if not perform_correction and abs(diff):
|
|
220
223
|
return False
|
|
221
224
|
|
|
222
|
-
if
|
|
225
|
+
if (
|
|
226
|
+
not perform_correction
|
|
227
|
+
and abs(diff) > settings.DJANGO_LEDGER_TRANSACTION_MAX_TOLERANCE
|
|
228
|
+
):
|
|
223
229
|
return False
|
|
224
230
|
|
|
225
231
|
while not is_valid:
|
|
226
232
|
tx_type_choice = choice([DEBIT, CREDIT])
|
|
227
233
|
|
|
228
234
|
if IS_TX_MODEL:
|
|
229
|
-
txs_candidates = list(
|
|
235
|
+
txs_candidates = list(
|
|
236
|
+
tx for tx in tx_data if tx.tx_type == tx_type_choice
|
|
237
|
+
)
|
|
230
238
|
else:
|
|
231
|
-
txs_candidates = list(
|
|
239
|
+
txs_candidates = list(
|
|
240
|
+
tx for tx in tx_data if tx['tx_type'] == tx_type_choice
|
|
241
|
+
)
|
|
232
242
|
|
|
233
243
|
if len(txs_candidates) > 0:
|
|
234
|
-
|
|
235
244
|
tx = choice(txs_candidates)
|
|
236
245
|
|
|
237
|
-
if any(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
246
|
+
if any(
|
|
247
|
+
[
|
|
248
|
+
diff > 0 and tx_type_choice == DEBIT,
|
|
249
|
+
diff < 0 and tx_type_choice == CREDIT,
|
|
250
|
+
]
|
|
251
|
+
):
|
|
241
252
|
if IS_TX_MODEL:
|
|
242
253
|
tx.amount += settings.DJANGO_LEDGER_TRANSACTION_CORRECTION
|
|
243
254
|
else:
|
|
244
255
|
tx['amount'] += settings.DJANGO_LEDGER_TRANSACTION_CORRECTION
|
|
245
256
|
|
|
246
|
-
elif any(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
257
|
+
elif any(
|
|
258
|
+
[
|
|
259
|
+
diff < 0 and tx_type_choice == DEBIT,
|
|
260
|
+
diff > 0 and tx_type_choice == CREDIT,
|
|
261
|
+
]
|
|
262
|
+
):
|
|
250
263
|
if IS_TX_MODEL:
|
|
251
264
|
tx.amount -= settings.DJANGO_LEDGER_TRANSACTION_CORRECTION
|
|
252
265
|
else:
|
|
@@ -285,17 +298,17 @@ def get_localtime(tz=None) -> datetime:
|
|
|
285
298
|
|
|
286
299
|
def get_localdate() -> date:
|
|
287
300
|
"""
|
|
288
|
-
|
|
301
|
+
Fetches the current local date, optionally considering time zone settings.
|
|
289
302
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
303
|
+
This function retrieves the current local date. If the global settings indicate
|
|
304
|
+
the use of time zones (`USE_TZ` is True), the date is determined based on the
|
|
305
|
+
local time zone. Otherwise, the date is based on the system's local time without
|
|
306
|
+
any time zone consideration.
|
|
294
307
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
308
|
+
Returns
|
|
309
|
+
-------
|
|
310
|
+
date
|
|
311
|
+
The current local date, adjusted for the time zone setting if applicable.
|
|
299
312
|
"""
|
|
300
313
|
if global_settings.USE_TZ:
|
|
301
314
|
return localdate()
|
|
@@ -303,8 +316,7 @@ def get_localdate() -> date:
|
|
|
303
316
|
|
|
304
317
|
|
|
305
318
|
def validate_io_timestamp(
|
|
306
|
-
|
|
307
|
-
no_parse_localdate: bool = True
|
|
319
|
+
dt: Union[str, date, datetime], no_parse_localdate: bool = True
|
|
308
320
|
) -> Optional[Union[datetime, date]]:
|
|
309
321
|
"""
|
|
310
322
|
Validates and processes a given date or datetime input and returns a processed
|
|
@@ -351,10 +363,7 @@ def validate_io_timestamp(
|
|
|
351
363
|
|
|
352
364
|
if isinstance(dt, datetime):
|
|
353
365
|
if is_naive(dt):
|
|
354
|
-
return make_aware(
|
|
355
|
-
value=dt,
|
|
356
|
-
timezone=ZoneInfo('UTC')
|
|
357
|
-
)
|
|
366
|
+
return make_aware(value=dt, timezone=ZoneInfo('UTC'))
|
|
358
367
|
return dt
|
|
359
368
|
|
|
360
369
|
elif isinstance(dt, str):
|
|
@@ -364,23 +373,21 @@ def validate_io_timestamp(
|
|
|
364
373
|
# try to parse a datetime object from string...
|
|
365
374
|
fdt = parse_datetime(dt)
|
|
366
375
|
if not fdt:
|
|
367
|
-
raise InvalidDateInputError(
|
|
368
|
-
message=f'Could not parse date from {dt}'
|
|
369
|
-
)
|
|
376
|
+
raise InvalidDateInputError(message=f'Could not parse date from {dt}')
|
|
370
377
|
elif is_naive(fdt):
|
|
371
378
|
fdt = make_aware(fdt)
|
|
372
379
|
if global_settings.USE_TZ:
|
|
373
380
|
return make_aware(
|
|
374
381
|
datetime.combine(
|
|
375
|
-
fdt,
|
|
376
|
-
|
|
382
|
+
fdt,
|
|
383
|
+
datetime.min.time(),
|
|
384
|
+
)
|
|
385
|
+
)
|
|
377
386
|
return datetime.combine(fdt, datetime.min.time())
|
|
378
387
|
|
|
379
388
|
elif isinstance(dt, date):
|
|
380
389
|
if global_settings.USE_TZ:
|
|
381
|
-
return make_aware(
|
|
382
|
-
value=datetime.combine(dt, datetime.min.time())
|
|
383
|
-
)
|
|
390
|
+
return make_aware(value=datetime.combine(dt, datetime.min.time()))
|
|
384
391
|
return datetime.combine(dt, datetime.min.time())
|
|
385
392
|
|
|
386
393
|
if no_parse_localdate:
|
|
@@ -388,8 +395,8 @@ def validate_io_timestamp(
|
|
|
388
395
|
|
|
389
396
|
|
|
390
397
|
def validate_dates(
|
|
391
|
-
|
|
392
|
-
|
|
398
|
+
from_date: Optional[Union[str, datetime, date]] = None,
|
|
399
|
+
to_date: Optional[Union[str, datetime, date]] = None,
|
|
393
400
|
) -> Tuple[date, date]:
|
|
394
401
|
"""
|
|
395
402
|
Validates and converts the input dates to date objects. This function ensures that the
|
|
@@ -455,7 +462,9 @@ def validate_activity(activity: str, raise_404: bool = False):
|
|
|
455
462
|
JournalEntryModel = lazy_loader.get_journal_entry_model()
|
|
456
463
|
valid = activity in JournalEntryModel.VALID_ACTIVITIES
|
|
457
464
|
if activity and not valid:
|
|
458
|
-
exception = ValidationError(
|
|
465
|
+
exception = ValidationError(
|
|
466
|
+
f'{activity} is invalid. Choices are {JournalEntryModel.VALID_ACTIVITIES}.'
|
|
467
|
+
)
|
|
459
468
|
if raise_404:
|
|
460
469
|
raise Http404(exception)
|
|
461
470
|
raise exception
|
|
@@ -494,6 +503,7 @@ class IOResult:
|
|
|
494
503
|
A summary or aggregation of account balances derived from the processed
|
|
495
504
|
data.
|
|
496
505
|
"""
|
|
506
|
+
|
|
497
507
|
# DB Aggregation...
|
|
498
508
|
db_from_date: Optional[date] = None
|
|
499
509
|
db_to_date: Optional[date] = None
|
|
@@ -511,10 +521,12 @@ class IOResult:
|
|
|
511
521
|
|
|
512
522
|
@property
|
|
513
523
|
def is_bounded(self) -> bool:
|
|
514
|
-
return all(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
524
|
+
return all(
|
|
525
|
+
[
|
|
526
|
+
self.ce_from_date is not None,
|
|
527
|
+
self.ce_to_date is not None,
|
|
528
|
+
]
|
|
529
|
+
)
|
|
518
530
|
|
|
519
531
|
|
|
520
532
|
class IODatabaseMixIn:
|
|
@@ -529,16 +541,20 @@ class IODatabaseMixIn:
|
|
|
529
541
|
|
|
530
542
|
Attributes
|
|
531
543
|
----------
|
|
532
|
-
TRANSACTION_MODEL_CLASS
|
|
544
|
+
TRANSACTION_MODEL_CLASS: NoneType or Type
|
|
533
545
|
Specifies the Django model class for transactions. If None, a lazy loader
|
|
534
546
|
will be used to determine the model dynamically.
|
|
535
|
-
JOURNAL_ENTRY_MODEL_CLASS
|
|
547
|
+
JOURNAL_ENTRY_MODEL_CLASS: NoneType or Type
|
|
536
548
|
Specifies the Django model class for journal entries. If None, a lazy
|
|
537
549
|
loader will be used to determine the model dynamically.
|
|
550
|
+
STAGED_TRANSACTION_MODEL_CLASS: NoneType or Type
|
|
551
|
+
Specifies the Django model class for staged transactions. If None, a lazy
|
|
552
|
+
loader will be used to determine the model dynamically.
|
|
538
553
|
"""
|
|
539
554
|
|
|
540
555
|
TRANSACTION_MODEL_CLASS = None
|
|
541
556
|
JOURNAL_ENTRY_MODEL_CLASS = None
|
|
557
|
+
STAGED_TRANSACTION_MODEL_CLASS = None
|
|
542
558
|
|
|
543
559
|
def is_entity_model(self):
|
|
544
560
|
"""
|
|
@@ -596,7 +612,9 @@ class IODatabaseMixIn:
|
|
|
596
612
|
elif self.is_entity_unit_model():
|
|
597
613
|
return getattr(self, 'entity')
|
|
598
614
|
raise IOValidationError(
|
|
599
|
-
message=_(
|
|
615
|
+
message=_(
|
|
616
|
+
f'IODatabaseMixIn not compatible with {self.__class__.__name__} model.'
|
|
617
|
+
)
|
|
600
618
|
)
|
|
601
619
|
|
|
602
620
|
def get_transaction_model(self):
|
|
@@ -618,6 +636,25 @@ class IODatabaseMixIn:
|
|
|
618
636
|
return self.TRANSACTION_MODEL_CLASS
|
|
619
637
|
return lazy_loader.get_txs_model()
|
|
620
638
|
|
|
639
|
+
def get_staged_transaction_model(self):
|
|
640
|
+
"""
|
|
641
|
+
Retrieve the staged transaction model class used for handling imported transactions.
|
|
642
|
+
|
|
643
|
+
The method checks whether a specific transaction model class is explicitly
|
|
644
|
+
set via the `STAGED_TRANSACTION_MODEL_CLASS` attribute. If set, it returns that
|
|
645
|
+
class as the transaction model. If not set, it falls back to a default
|
|
646
|
+
transaction model obtained from the `lazy_loader.get_txs_model()` method.
|
|
647
|
+
|
|
648
|
+
Returns
|
|
649
|
+
-------
|
|
650
|
+
type
|
|
651
|
+
The transaction model class defined in `STAGED_TRANSACTION_MODEL_CLASS` or
|
|
652
|
+
the default transaction model provided by `lazy_loader.get_staged_txs_model()`.
|
|
653
|
+
"""
|
|
654
|
+
if self.STAGED_TRANSACTION_MODEL_CLASS is not None:
|
|
655
|
+
return self.STAGED_TRANSACTION_MODEL_CLASS
|
|
656
|
+
return lazy_loader.get_staged_txs_model()
|
|
657
|
+
|
|
621
658
|
def get_journal_entry_model(self):
|
|
622
659
|
"""
|
|
623
660
|
Retrieves the class model for journal entries. If the `JOURNAL_ENTRY_MODEL_CLASS`
|
|
@@ -634,23 +671,24 @@ class IODatabaseMixIn:
|
|
|
634
671
|
return self.JOURNAL_ENTRY_MODEL_CLASS
|
|
635
672
|
return lazy_loader.get_journal_entry_model()
|
|
636
673
|
|
|
637
|
-
def database_digest(
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
674
|
+
def database_digest(
|
|
675
|
+
self,
|
|
676
|
+
entity_slug: Optional[str] = None,
|
|
677
|
+
unit_slug: Optional[str] = None,
|
|
678
|
+
from_date: Optional[Union[date, datetime]] = None,
|
|
679
|
+
to_date: Optional[Union[date, datetime]] = None,
|
|
680
|
+
by_activity: bool = False,
|
|
681
|
+
by_tx_type: bool = False,
|
|
682
|
+
by_period: bool = False,
|
|
683
|
+
by_unit: bool = False,
|
|
684
|
+
activity: Optional[str] = None,
|
|
685
|
+
role: str = Optional[str],
|
|
686
|
+
accounts: Optional[Union[str, List[str], Set[str]]] = None,
|
|
687
|
+
posted: bool = True,
|
|
688
|
+
exclude_zero_bal: bool = True,
|
|
689
|
+
use_closing_entry: bool = False,
|
|
690
|
+
**kwargs,
|
|
691
|
+
) -> IOResult:
|
|
654
692
|
"""
|
|
655
693
|
Aggregates transaction data based on the provided parameters to generate a
|
|
656
694
|
digest of financial entries. This method is designed to work with various
|
|
@@ -703,7 +741,7 @@ class IODatabaseMixIn:
|
|
|
703
741
|
exclude_zero_bal : bool
|
|
704
742
|
If True, transactions with zero-balance amounts will be excluded.
|
|
705
743
|
Defaults to True.
|
|
706
|
-
|
|
744
|
+
use_closing_entry : bool
|
|
707
745
|
Specifies whether closing entries should be used to optimize database
|
|
708
746
|
aggregation. If not provided, the value is determined by the system-global
|
|
709
747
|
setting.
|
|
@@ -725,38 +763,37 @@ class IODatabaseMixIn:
|
|
|
725
763
|
if self.is_entity_model():
|
|
726
764
|
if entity_slug:
|
|
727
765
|
if entity_slug != self.slug:
|
|
728
|
-
raise IOValidationError(
|
|
729
|
-
|
|
766
|
+
raise IOValidationError(
|
|
767
|
+
'Inconsistent entity_slug. '
|
|
768
|
+
f'Provided {entity_slug} does not match actual {self.slug}'
|
|
769
|
+
)
|
|
730
770
|
if unit_slug:
|
|
731
|
-
|
|
732
771
|
txs_queryset_init = TransactionModel.objects.for_entity(
|
|
733
|
-
|
|
734
|
-
entity_slug=entity_slug or self.slug
|
|
772
|
+
entity_model=entity_slug or self.slug
|
|
735
773
|
).for_unit(unit_slug=unit_slug)
|
|
736
774
|
|
|
737
775
|
else:
|
|
738
776
|
txs_queryset_init = TransactionModel.objects.for_entity(
|
|
739
|
-
|
|
740
|
-
entity_slug=self
|
|
777
|
+
entity_model=self
|
|
741
778
|
)
|
|
742
779
|
elif self.is_entity_unit_model():
|
|
743
780
|
if not entity_slug:
|
|
744
781
|
raise IOValidationError(
|
|
745
|
-
'Calling digest from Entity Unit requires entity_slug explicitly for safety'
|
|
782
|
+
'Calling digest from Entity Unit requires entity_slug explicitly for safety'
|
|
783
|
+
)
|
|
746
784
|
|
|
747
785
|
txs_queryset_init = TransactionModel.objects.for_entity(
|
|
748
|
-
|
|
749
|
-
entity_slug=entity_slug,
|
|
786
|
+
entity_model=entity_slug
|
|
750
787
|
).for_unit(unit_slug=unit_slug or self)
|
|
751
788
|
|
|
752
789
|
elif self.is_ledger_model():
|
|
753
790
|
if not entity_slug:
|
|
754
791
|
raise IOValidationError(
|
|
755
|
-
'Calling digest from Ledger Model requires entity_slug explicitly for safety'
|
|
792
|
+
'Calling digest from Ledger Model requires entity_slug explicitly for safety'
|
|
793
|
+
)
|
|
756
794
|
|
|
757
795
|
txs_queryset_init = TransactionModel.objects.for_entity(
|
|
758
|
-
|
|
759
|
-
user_model=user_model,
|
|
796
|
+
entity_model=entity_slug
|
|
760
797
|
).for_ledger(ledger_model=self)
|
|
761
798
|
|
|
762
799
|
else:
|
|
@@ -770,8 +807,8 @@ class IODatabaseMixIn:
|
|
|
770
807
|
txs_queryset_to_closing_entry = txs_queryset_init.none()
|
|
771
808
|
|
|
772
809
|
USE_CLOSING_ENTRIES = settings.DJANGO_LEDGER_USE_CLOSING_ENTRIES
|
|
773
|
-
if
|
|
774
|
-
USE_CLOSING_ENTRIES =
|
|
810
|
+
if use_closing_entry is not None:
|
|
811
|
+
USE_CLOSING_ENTRIES = use_closing_entry
|
|
775
812
|
|
|
776
813
|
# use closing entries to minimize DB aggregation if possible and activated...
|
|
777
814
|
if USE_CLOSING_ENTRIES:
|
|
@@ -779,18 +816,23 @@ class IODatabaseMixIn:
|
|
|
779
816
|
entity_model = self.get_entity_model_from_io()
|
|
780
817
|
|
|
781
818
|
# looking up available dates...
|
|
782
|
-
ce_from_date = entity_model.get_closing_entry_for_date(
|
|
819
|
+
ce_from_date = entity_model.get_closing_entry_for_date(
|
|
820
|
+
io_date=from_date, inclusive=False
|
|
821
|
+
)
|
|
783
822
|
ce_to_date = entity_model.get_closing_entry_for_date(io_date=to_date)
|
|
784
823
|
|
|
785
824
|
# unbounded lookup, no date match
|
|
786
825
|
# finding the closest closing entry to aggregate from if present...
|
|
787
826
|
if not from_date and not ce_to_date:
|
|
788
|
-
ce_alt_from_date = entity_model.get_nearest_next_closing_entry(
|
|
827
|
+
ce_alt_from_date = entity_model.get_nearest_next_closing_entry(
|
|
828
|
+
io_date=to_date
|
|
829
|
+
)
|
|
789
830
|
|
|
790
831
|
# if there's a suitable closing entry...
|
|
791
832
|
if ce_alt_from_date:
|
|
792
833
|
txs_queryset_from_closing_entry = txs_queryset_closing_entry.filter(
|
|
793
|
-
journal_entry__timestamp__date=ce_alt_from_date
|
|
834
|
+
journal_entry__timestamp__date=ce_alt_from_date
|
|
835
|
+
)
|
|
794
836
|
io_result.ce_match = True
|
|
795
837
|
io_result.ce_from_date = ce_alt_from_date
|
|
796
838
|
|
|
@@ -803,7 +845,8 @@ class IODatabaseMixIn:
|
|
|
803
845
|
# unbounded lookup, exact to_date match...
|
|
804
846
|
elif not from_date and ce_to_date:
|
|
805
847
|
txs_queryset_to_closing_entry = txs_queryset_closing_entry.filter(
|
|
806
|
-
journal_entry__timestamp__date=ce_to_date
|
|
848
|
+
journal_entry__timestamp__date=ce_to_date
|
|
849
|
+
)
|
|
807
850
|
io_result.ce_match = True
|
|
808
851
|
io_result.ce_to_date = ce_to_date
|
|
809
852
|
|
|
@@ -815,10 +858,12 @@ class IODatabaseMixIn:
|
|
|
815
858
|
# bounded exact from_date and to_date match...
|
|
816
859
|
elif ce_from_date and ce_to_date:
|
|
817
860
|
txs_queryset_from_closing_entry = txs_queryset_closing_entry.filter(
|
|
818
|
-
journal_entry__timestamp__date=ce_from_date
|
|
861
|
+
journal_entry__timestamp__date=ce_from_date
|
|
862
|
+
)
|
|
819
863
|
|
|
820
864
|
txs_queryset_to_closing_entry = txs_queryset_closing_entry.filter(
|
|
821
|
-
journal_entry__timestamp__date=ce_to_date
|
|
865
|
+
journal_entry__timestamp__date=ce_to_date
|
|
866
|
+
)
|
|
822
867
|
|
|
823
868
|
io_result.ce_match = True
|
|
824
869
|
io_result.ce_from_date = ce_from_date
|
|
@@ -829,10 +874,14 @@ class IODatabaseMixIn:
|
|
|
829
874
|
io_result.db_to_date = None
|
|
830
875
|
txs_queryset_agg = TransactionModel.objects.none()
|
|
831
876
|
|
|
832
|
-
txs_queryset_closing_entry =
|
|
877
|
+
txs_queryset_closing_entry = (
|
|
878
|
+
txs_queryset_from_closing_entry | txs_queryset_to_closing_entry
|
|
879
|
+
)
|
|
833
880
|
|
|
834
881
|
if io_result.db_from_date:
|
|
835
|
-
txs_queryset_agg = txs_queryset_agg.from_date(
|
|
882
|
+
txs_queryset_agg = txs_queryset_agg.from_date(
|
|
883
|
+
from_date=io_result.db_from_date
|
|
884
|
+
)
|
|
836
885
|
|
|
837
886
|
if io_result.db_to_date:
|
|
838
887
|
txs_queryset_agg = txs_queryset_agg.to_date(to_date=io_result.db_to_date)
|
|
@@ -862,33 +911,42 @@ class IODatabaseMixIn:
|
|
|
862
911
|
cleared_filter = kwargs.get('cleared')
|
|
863
912
|
if cleared_filter is not None:
|
|
864
913
|
if cleared_filter in [True, False]:
|
|
865
|
-
txs_queryset =
|
|
914
|
+
txs_queryset = (
|
|
915
|
+
txs_queryset.is_cleared()
|
|
916
|
+
if cleared_filter
|
|
917
|
+
else txs_queryset.not_cleared()
|
|
918
|
+
)
|
|
866
919
|
else:
|
|
867
920
|
raise IOValidationError(
|
|
868
921
|
message=f'Invalid value for cleared filter: {cleared_filter}. '
|
|
869
|
-
|
|
922
|
+
f'Valid values are True, False'
|
|
870
923
|
)
|
|
871
924
|
|
|
872
925
|
# Reconciled transaction filter via KWARGS....
|
|
873
926
|
reconciled_filter = kwargs.get('reconciled')
|
|
874
927
|
if reconciled_filter is not None:
|
|
875
928
|
if reconciled_filter in [True, False]:
|
|
876
|
-
txs_queryset =
|
|
929
|
+
txs_queryset = (
|
|
930
|
+
txs_queryset.is_reconciled()
|
|
931
|
+
if reconciled_filter
|
|
932
|
+
else txs_queryset.not_reconciled()
|
|
933
|
+
)
|
|
877
934
|
else:
|
|
878
935
|
raise IOValidationError(
|
|
879
936
|
message=f'Invalid value for reconciled filter: {reconciled_filter}. '
|
|
880
|
-
|
|
937
|
+
f'Valid values are True, False'
|
|
881
938
|
)
|
|
882
939
|
|
|
883
940
|
if io_result.is_bounded:
|
|
884
941
|
txs_queryset = txs_queryset.annotate(
|
|
885
942
|
amount_io=Case(
|
|
886
943
|
When(
|
|
887
|
-
journal_entry__timestamp__date=ce_from_date,
|
|
888
|
-
|
|
944
|
+
journal_entry__timestamp__date=ce_from_date, then=-F('amount')
|
|
945
|
+
),
|
|
889
946
|
default=F('amount'),
|
|
890
|
-
output_field=DecimalField()
|
|
891
|
-
)
|
|
947
|
+
output_field=DecimalField(),
|
|
948
|
+
)
|
|
949
|
+
)
|
|
892
950
|
|
|
893
951
|
VALUES = [
|
|
894
952
|
'account__uuid',
|
|
@@ -900,6 +958,10 @@ class IODatabaseMixIn:
|
|
|
900
958
|
'tx_type',
|
|
901
959
|
]
|
|
902
960
|
|
|
961
|
+
if kwargs.get('for_test'):
|
|
962
|
+
VALUES.append('journal_entry__ledger_id')
|
|
963
|
+
VALUES.append('journal_entry__ledger__entity_id')
|
|
964
|
+
|
|
903
965
|
ANNOTATE = {'balance': Sum('amount')}
|
|
904
966
|
if io_result.is_bounded:
|
|
905
967
|
ANNOTATE = {'balance': Sum('amount_io')}
|
|
@@ -908,7 +970,10 @@ class IODatabaseMixIn:
|
|
|
908
970
|
|
|
909
971
|
if by_unit:
|
|
910
972
|
ORDER_BY.append('journal_entry__entity_unit__uuid')
|
|
911
|
-
VALUES += [
|
|
973
|
+
VALUES += [
|
|
974
|
+
'journal_entry__entity_unit__uuid',
|
|
975
|
+
'journal_entry__entity_unit__name',
|
|
976
|
+
]
|
|
912
977
|
|
|
913
978
|
if by_period:
|
|
914
979
|
ORDER_BY.append('journal_entry__timestamp')
|
|
@@ -922,27 +987,30 @@ class IODatabaseMixIn:
|
|
|
922
987
|
ORDER_BY.append('tx_type')
|
|
923
988
|
VALUES.append('tx_type')
|
|
924
989
|
|
|
925
|
-
io_result.txs_queryset =
|
|
990
|
+
io_result.txs_queryset = (
|
|
991
|
+
txs_queryset.values(*VALUES).annotate(**ANNOTATE).order_by(*ORDER_BY)
|
|
992
|
+
)
|
|
926
993
|
return io_result
|
|
927
994
|
|
|
928
|
-
def python_digest(
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
995
|
+
def python_digest(
|
|
996
|
+
self,
|
|
997
|
+
entity_slug: Optional[str] = None,
|
|
998
|
+
unit_slug: Optional[str] = None,
|
|
999
|
+
to_date: Optional[Union[date, datetime, str]] = None,
|
|
1000
|
+
from_date: Optional[Union[date, datetime, str]] = None,
|
|
1001
|
+
equity_only: bool = False,
|
|
1002
|
+
activity: str = None,
|
|
1003
|
+
role: Optional[Union[Set[str], List[str]]] = None,
|
|
1004
|
+
accounts: Optional[Union[Set[str], List[str]]] = None,
|
|
1005
|
+
signs: bool = True,
|
|
1006
|
+
by_unit: bool = False,
|
|
1007
|
+
by_activity: bool = False,
|
|
1008
|
+
by_tx_type: bool = False,
|
|
1009
|
+
by_period: bool = False,
|
|
1010
|
+
use_closing_entry: bool = False,
|
|
1011
|
+
force_queryset_sorting: bool = False,
|
|
1012
|
+
**kwargs,
|
|
1013
|
+
) -> IOResult:
|
|
946
1014
|
"""
|
|
947
1015
|
Computes and returns the digest of transactions for a given entity, unit,
|
|
948
1016
|
and optional filters such as date range, account role, and activity. The
|
|
@@ -981,7 +1049,7 @@ class IODatabaseMixIn:
|
|
|
981
1049
|
Whether to group the results by transaction type. Defaults to False.
|
|
982
1050
|
by_period : bool
|
|
983
1051
|
Whether to group the results by period (year and month). Defaults to False.
|
|
984
|
-
|
|
1052
|
+
use_closing_entry : bool
|
|
985
1053
|
Whether to include closing entries in the computation. Defaults to False.
|
|
986
1054
|
force_queryset_sorting : bool
|
|
987
1055
|
Whether to force sorting of the transaction queryset. Defaults to False.
|
|
@@ -999,7 +1067,6 @@ class IODatabaseMixIn:
|
|
|
999
1067
|
role = roles_module.GROUP_EARNINGS
|
|
1000
1068
|
|
|
1001
1069
|
io_result = self.database_digest(
|
|
1002
|
-
user_model=user_model,
|
|
1003
1070
|
entity_slug=entity_slug,
|
|
1004
1071
|
unit_slug=unit_slug,
|
|
1005
1072
|
to_date=to_date,
|
|
@@ -1011,10 +1078,9 @@ class IODatabaseMixIn:
|
|
|
1011
1078
|
activity=activity,
|
|
1012
1079
|
role=role,
|
|
1013
1080
|
accounts=accounts,
|
|
1014
|
-
|
|
1015
|
-
**kwargs
|
|
1016
|
-
|
|
1017
|
-
TransactionModel = self.get_transaction_model()
|
|
1081
|
+
use_closing_entry=use_closing_entry,
|
|
1082
|
+
**kwargs,
|
|
1083
|
+
)
|
|
1018
1084
|
|
|
1019
1085
|
for tx_model in io_result.txs_queryset:
|
|
1020
1086
|
if tx_model['account__balance_type'] != tx_model['tx_type']:
|
|
@@ -1041,15 +1107,26 @@ class IODatabaseMixIn:
|
|
|
1041
1107
|
|
|
1042
1108
|
if signs:
|
|
1043
1109
|
for acc in accounts_digest:
|
|
1044
|
-
if any(
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1110
|
+
if any(
|
|
1111
|
+
[
|
|
1112
|
+
all(
|
|
1113
|
+
[
|
|
1114
|
+
acc['role_bs'] == roles_module.BS_ASSET_ROLE,
|
|
1115
|
+
acc['balance_type'] == CREDIT,
|
|
1116
|
+
]
|
|
1117
|
+
),
|
|
1118
|
+
all(
|
|
1119
|
+
[
|
|
1120
|
+
acc['role_bs']
|
|
1121
|
+
in (
|
|
1122
|
+
roles_module.BS_LIABILITIES_ROLE,
|
|
1123
|
+
roles_module.BS_EQUITY_ROLE,
|
|
1124
|
+
),
|
|
1125
|
+
acc['balance_type'] == DEBIT,
|
|
1126
|
+
]
|
|
1127
|
+
),
|
|
1128
|
+
]
|
|
1129
|
+
):
|
|
1053
1130
|
acc['balance'] = -acc['balance']
|
|
1054
1131
|
|
|
1055
1132
|
io_result.accounts_digest = accounts_digest
|
|
@@ -1114,30 +1191,31 @@ class IODatabaseMixIn:
|
|
|
1114
1191
|
'balance': sum(a['balance'] for a in gl),
|
|
1115
1192
|
}
|
|
1116
1193
|
|
|
1117
|
-
def digest(
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1194
|
+
def digest(
|
|
1195
|
+
self,
|
|
1196
|
+
entity_slug: Optional[str] = None,
|
|
1197
|
+
unit_slug: Optional[str] = None,
|
|
1198
|
+
to_date: Optional[Union[date, datetime, str]] = None,
|
|
1199
|
+
from_date: Optional[Union[date, datetime, str]] = None,
|
|
1200
|
+
accounts: Optional[Union[Set[str], List[str]]] = None,
|
|
1201
|
+
role: Optional[Union[Set[str], List[str]]] = None,
|
|
1202
|
+
activity: Optional[str] = None,
|
|
1203
|
+
signs: bool = True,
|
|
1204
|
+
process_roles: bool = False,
|
|
1205
|
+
process_groups: bool = False,
|
|
1206
|
+
process_ratios: bool = False,
|
|
1207
|
+
process_activity: bool = False,
|
|
1208
|
+
equity_only: bool = False,
|
|
1209
|
+
by_period: bool = False,
|
|
1210
|
+
by_unit: bool = False,
|
|
1211
|
+
by_activity: bool = False,
|
|
1212
|
+
by_tx_type: bool = False,
|
|
1213
|
+
balance_sheet_statement: bool = False,
|
|
1214
|
+
income_statement: bool = False,
|
|
1215
|
+
cash_flow_statement: bool = False,
|
|
1216
|
+
use_closing_entry: Optional[bool] = None,
|
|
1217
|
+
**kwargs,
|
|
1218
|
+
) -> IODigestContextManager:
|
|
1141
1219
|
"""
|
|
1142
1220
|
Processes financial data and generates various financial statements, ratios, or activity digests
|
|
1143
1221
|
based on the provided arguments. The method applies specific processing pipelines, such as role
|
|
@@ -1227,7 +1305,6 @@ class IODatabaseMixIn:
|
|
|
1227
1305
|
io_state['by_tx_type'] = by_tx_type
|
|
1228
1306
|
|
|
1229
1307
|
io_result: IOResult = self.python_digest(
|
|
1230
|
-
user_model=user_model,
|
|
1231
1308
|
accounts=accounts,
|
|
1232
1309
|
role=role,
|
|
1233
1310
|
activity=activity,
|
|
@@ -1242,7 +1319,7 @@ class IODatabaseMixIn:
|
|
|
1242
1319
|
by_activity=by_activity,
|
|
1243
1320
|
by_tx_type=by_tx_type,
|
|
1244
1321
|
use_closing_entry=use_closing_entry,
|
|
1245
|
-
**kwargs
|
|
1322
|
+
**kwargs,
|
|
1246
1323
|
)
|
|
1247
1324
|
|
|
1248
1325
|
io_state['io_result'] = io_result
|
|
@@ -1252,40 +1329,43 @@ class IODatabaseMixIn:
|
|
|
1252
1329
|
|
|
1253
1330
|
if process_roles:
|
|
1254
1331
|
roles_mgr = AccountRoleIOMiddleware(
|
|
1255
|
-
io_data=io_state,
|
|
1256
|
-
by_period=by_period,
|
|
1257
|
-
by_unit=by_unit
|
|
1332
|
+
io_data=io_state, by_period=by_period, by_unit=by_unit
|
|
1258
1333
|
)
|
|
1259
1334
|
|
|
1260
1335
|
io_state = roles_mgr.digest()
|
|
1261
1336
|
|
|
1262
|
-
if any(
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1337
|
+
if any(
|
|
1338
|
+
[
|
|
1339
|
+
process_groups,
|
|
1340
|
+
balance_sheet_statement,
|
|
1341
|
+
income_statement,
|
|
1342
|
+
cash_flow_statement,
|
|
1343
|
+
]
|
|
1344
|
+
):
|
|
1268
1345
|
group_mgr = AccountGroupIOMiddleware(
|
|
1269
|
-
io_data=io_state,
|
|
1270
|
-
by_period=by_period,
|
|
1271
|
-
by_unit=by_unit
|
|
1346
|
+
io_data=io_state, by_period=by_period, by_unit=by_unit
|
|
1272
1347
|
)
|
|
1273
1348
|
io_state = group_mgr.digest()
|
|
1274
1349
|
|
|
1275
1350
|
# todo: migrate this to group manager...
|
|
1276
1351
|
io_state['group_account']['GROUP_ASSETS'].sort(
|
|
1277
|
-
key=lambda acc: roles_module.ROLES_ORDER_ASSETS.index(acc['role'])
|
|
1352
|
+
key=lambda acc: roles_module.ROLES_ORDER_ASSETS.index(acc['role'])
|
|
1353
|
+
)
|
|
1278
1354
|
io_state['group_account']['GROUP_LIABILITIES'].sort(
|
|
1279
|
-
key=lambda acc: roles_module.ROLES_ORDER_LIABILITIES.index(acc['role'])
|
|
1355
|
+
key=lambda acc: roles_module.ROLES_ORDER_LIABILITIES.index(acc['role'])
|
|
1356
|
+
)
|
|
1280
1357
|
io_state['group_account']['GROUP_CAPITAL'].sort(
|
|
1281
|
-
key=lambda acc: roles_module.ROLES_ORDER_CAPITAL.index(acc['role'])
|
|
1358
|
+
key=lambda acc: roles_module.ROLES_ORDER_CAPITAL.index(acc['role'])
|
|
1359
|
+
)
|
|
1282
1360
|
|
|
1283
1361
|
if process_ratios:
|
|
1284
1362
|
ratio_gen = FinancialRatioManager(io_data=io_state)
|
|
1285
1363
|
io_state = ratio_gen.digest()
|
|
1286
1364
|
|
|
1287
1365
|
if process_activity:
|
|
1288
|
-
activity_manager = JEActivityIOMiddleware(
|
|
1366
|
+
activity_manager = JEActivityIOMiddleware(
|
|
1367
|
+
io_data=io_state, by_unit=by_unit, by_period=by_period
|
|
1368
|
+
)
|
|
1289
1369
|
activity_manager.digest()
|
|
1290
1370
|
|
|
1291
1371
|
if balance_sheet_statement:
|
|
@@ -1302,16 +1382,18 @@ class IODatabaseMixIn:
|
|
|
1302
1382
|
|
|
1303
1383
|
return IODigestContextManager(io_state=io_state)
|
|
1304
1384
|
|
|
1305
|
-
def commit_txs(
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1385
|
+
def commit_txs(
|
|
1386
|
+
self,
|
|
1387
|
+
je_timestamp: Union[str, datetime, date],
|
|
1388
|
+
je_txs: List[Dict],
|
|
1389
|
+
je_posted: bool = False,
|
|
1390
|
+
je_ledger_model=None,
|
|
1391
|
+
je_unit_model=None,
|
|
1392
|
+
je_desc=None,
|
|
1393
|
+
je_origin=None,
|
|
1394
|
+
force_je_retrieval: bool = False,
|
|
1395
|
+
**kwargs,
|
|
1396
|
+
):
|
|
1315
1397
|
"""
|
|
1316
1398
|
Commits a set of financial transactions to a journal entry, after performing
|
|
1317
1399
|
validation checks. Validations include ensuring balanced transactions, ensuring
|
|
@@ -1363,107 +1445,133 @@ class IODatabaseMixIn:
|
|
|
1363
1445
|
"""
|
|
1364
1446
|
TransactionModel = self.get_transaction_model()
|
|
1365
1447
|
JournalEntryModel = self.get_journal_entry_model()
|
|
1448
|
+
StagedTransactionModel = self.get_staged_transaction_model()
|
|
1366
1449
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1450
|
+
with transaction.atomic():
|
|
1451
|
+
# Validates that credits/debits balance.
|
|
1452
|
+
check_tx_balance(je_txs, perform_correction=False)
|
|
1453
|
+
je_timestamp = validate_io_timestamp(dt=je_timestamp)
|
|
1370
1454
|
|
|
1371
|
-
|
|
1455
|
+
entity_model = self.get_entity_model_from_io()
|
|
1372
1456
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1457
|
+
if entity_model.last_closing_date:
|
|
1458
|
+
if isinstance(je_timestamp, datetime):
|
|
1459
|
+
if entity_model.last_closing_date >= je_timestamp.date():
|
|
1460
|
+
raise IOValidationError(
|
|
1461
|
+
message=_(
|
|
1462
|
+
f'Cannot commit transactions. The journal entry date {je_timestamp} is on a closed period.'
|
|
1463
|
+
)
|
|
1464
|
+
)
|
|
1465
|
+
elif isinstance(je_timestamp, date):
|
|
1466
|
+
if entity_model.last_closing_date >= je_timestamp:
|
|
1467
|
+
raise IOValidationError(
|
|
1468
|
+
message=_(
|
|
1469
|
+
f'Cannot commit transactions. The journal entry date {je_timestamp} is on a closed period.'
|
|
1470
|
+
)
|
|
1471
|
+
)
|
|
1472
|
+
|
|
1473
|
+
if self.is_ledger_model():
|
|
1474
|
+
if self.is_locked():
|
|
1475
|
+
raise IOValidationError(message=_('Cannot commit on locked ledger'))
|
|
1476
|
+
|
|
1477
|
+
# if calling from EntityModel must pass an instance of LedgerModel...
|
|
1478
|
+
if all(
|
|
1479
|
+
[
|
|
1480
|
+
isinstance(self, lazy_loader.get_entity_model()),
|
|
1481
|
+
je_ledger_model is None,
|
|
1482
|
+
]
|
|
1483
|
+
):
|
|
1484
|
+
raise IOValidationError(
|
|
1485
|
+
'Committing from EntityModel requires an instance of LedgerModel'
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
# Validates that the provided LedgerModel id valid...
|
|
1489
|
+
if all(
|
|
1490
|
+
[
|
|
1491
|
+
isinstance(self, lazy_loader.get_entity_model()),
|
|
1492
|
+
je_ledger_model is not None,
|
|
1493
|
+
]
|
|
1494
|
+
):
|
|
1495
|
+
if je_ledger_model.entity_id != self.uuid:
|
|
1376
1496
|
raise IOValidationError(
|
|
1377
|
-
|
|
1378
|
-
|
|
1497
|
+
f'LedgerModel {je_ledger_model} does not belong to {self}'
|
|
1498
|
+
)
|
|
1499
|
+
|
|
1500
|
+
# Validates that the provided EntityUnitModel id valid...
|
|
1501
|
+
if all(
|
|
1502
|
+
[
|
|
1503
|
+
isinstance(self, lazy_loader.get_entity_model()),
|
|
1504
|
+
je_unit_model is not None,
|
|
1505
|
+
]
|
|
1506
|
+
):
|
|
1507
|
+
if je_unit_model.entity_id != self.uuid:
|
|
1508
|
+
raise IOValidationError(
|
|
1509
|
+
f'EntityUnitModel {je_unit_model} does not belong to {self}'
|
|
1379
1510
|
)
|
|
1380
|
-
|
|
1381
|
-
|
|
1511
|
+
|
|
1512
|
+
if not je_ledger_model:
|
|
1513
|
+
je_ledger_model = self
|
|
1514
|
+
|
|
1515
|
+
if force_je_retrieval:
|
|
1516
|
+
try:
|
|
1517
|
+
if isinstance(je_timestamp, (datetime, str)):
|
|
1518
|
+
je_model = je_ledger_model.journal_entries.get(
|
|
1519
|
+
timestamp__exact=je_timestamp
|
|
1520
|
+
)
|
|
1521
|
+
elif isinstance(je_timestamp, date):
|
|
1522
|
+
je_model = je_ledger_model.journal_entries.get(
|
|
1523
|
+
timestamp__date__exact=je_timestamp
|
|
1524
|
+
)
|
|
1525
|
+
else:
|
|
1526
|
+
raise IOValidationError(
|
|
1527
|
+
message=_(f'Invalid timestamp type {type(je_timestamp)}')
|
|
1528
|
+
)
|
|
1529
|
+
except ObjectDoesNotExist:
|
|
1382
1530
|
raise IOValidationError(
|
|
1383
1531
|
message=_(
|
|
1384
|
-
f'
|
|
1532
|
+
f'Unable to retrieve Journal Entry model with Timestamp {je_timestamp}'
|
|
1533
|
+
)
|
|
1385
1534
|
)
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1535
|
+
else:
|
|
1536
|
+
je_model = JournalEntryModel(
|
|
1537
|
+
ledger=je_ledger_model,
|
|
1538
|
+
entity_unit=je_unit_model,
|
|
1539
|
+
description=je_desc,
|
|
1540
|
+
timestamp=je_timestamp,
|
|
1541
|
+
origin=je_origin,
|
|
1542
|
+
posted=False,
|
|
1543
|
+
locked=False,
|
|
1391
1544
|
)
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
if je_ledger_model.entity_id != self.uuid:
|
|
1406
|
-
raise IOValidationError(f'LedgerModel {je_ledger_model} does not belong to {self}')
|
|
1407
|
-
|
|
1408
|
-
# Validates that the provided EntityUnitModel id valid...
|
|
1409
|
-
if all([
|
|
1410
|
-
isinstance(self, lazy_loader.get_entity_model()),
|
|
1411
|
-
je_unit_model is not None,
|
|
1412
|
-
]):
|
|
1413
|
-
if je_unit_model.entity_id != self.uuid:
|
|
1414
|
-
raise IOValidationError(f'EntityUnitModel {je_unit_model} does not belong to {self}')
|
|
1415
|
-
|
|
1416
|
-
if not je_ledger_model:
|
|
1417
|
-
je_ledger_model = self
|
|
1418
|
-
|
|
1419
|
-
if force_je_retrieval:
|
|
1420
|
-
try:
|
|
1421
|
-
if isinstance(je_timestamp, (datetime, str)):
|
|
1422
|
-
je_model = je_ledger_model.journal_entries.get(timestamp__exact=je_timestamp)
|
|
1423
|
-
elif isinstance(je_timestamp, date):
|
|
1424
|
-
je_model = je_ledger_model.journal_entries.get(timestamp__date__exact=je_timestamp)
|
|
1425
|
-
else:
|
|
1426
|
-
raise IOValidationError(message=_(f'Invalid timestamp type {type(je_timestamp)}'))
|
|
1427
|
-
except ObjectDoesNotExist:
|
|
1428
|
-
raise IOValidationError(
|
|
1429
|
-
message=_(f'Unable to retrieve Journal Entry model with Timestamp {je_timestamp}')
|
|
1545
|
+
je_model.save(verify=False)
|
|
1546
|
+
|
|
1547
|
+
# todo: add method to process list of transaction models...
|
|
1548
|
+
txs_models = [
|
|
1549
|
+
(
|
|
1550
|
+
TransactionModel(
|
|
1551
|
+
account=txm_kwargs['account'],
|
|
1552
|
+
amount=txm_kwargs['amount'],
|
|
1553
|
+
tx_type=txm_kwargs['tx_type'],
|
|
1554
|
+
description=txm_kwargs['description'],
|
|
1555
|
+
journal_entry=je_model,
|
|
1556
|
+
),
|
|
1557
|
+
txm_kwargs,
|
|
1430
1558
|
)
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
)
|
|
1441
|
-
je_model.save(verify=False)
|
|
1442
|
-
|
|
1443
|
-
# todo: add method to process list of transaction models...
|
|
1444
|
-
txs_models = [
|
|
1445
|
-
(
|
|
1446
|
-
TransactionModel(
|
|
1447
|
-
account=txm_kwargs['account'],
|
|
1448
|
-
amount=txm_kwargs['amount'],
|
|
1449
|
-
tx_type=txm_kwargs['tx_type'],
|
|
1450
|
-
description=txm_kwargs['description'],
|
|
1451
|
-
journal_entry=je_model,
|
|
1452
|
-
), txm_kwargs) for txm_kwargs in je_txs
|
|
1453
|
-
]
|
|
1559
|
+
for txm_kwargs in je_txs
|
|
1560
|
+
]
|
|
1561
|
+
|
|
1562
|
+
for tx, txm_kwargs in txs_models:
|
|
1563
|
+
if not getattr(tx, 'ledger_id', None):
|
|
1564
|
+
tx.ledger_id = je_model.ledger_id
|
|
1565
|
+
if not getattr(tx, 'timestamp', None):
|
|
1566
|
+
tx.timestamp = je_model.timestamp
|
|
1567
|
+
staged_tx_model = txm_kwargs.get('staged_tx_model')
|
|
1454
1568
|
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
tx.ledger_id = je_model.ledger_id
|
|
1458
|
-
if not getattr(tx, 'timestamp', None):
|
|
1459
|
-
tx.timestamp = je_model.timestamp
|
|
1460
|
-
staged_tx_model = txm_kwargs.get('staged_tx_model')
|
|
1461
|
-
if staged_tx_model:
|
|
1462
|
-
staged_tx_model.transaction_model = tx
|
|
1569
|
+
if staged_tx_model:
|
|
1570
|
+
staged_tx_model.transaction_model = tx
|
|
1463
1571
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1572
|
+
txs_models = TransactionModel.objects.bulk_create(i[0] for i in txs_models)
|
|
1573
|
+
je_model.save(verify=True, post_on_verify=je_posted)
|
|
1574
|
+
return je_model, txs_models
|
|
1467
1575
|
|
|
1468
1576
|
|
|
1469
1577
|
class IOReportMixIn:
|
|
@@ -1492,22 +1600,27 @@ class IOReportMixIn:
|
|
|
1492
1600
|
`income_statement`, and `cash_flow_statement`. Each field represents a
|
|
1493
1601
|
respective financial report.
|
|
1494
1602
|
"""
|
|
1603
|
+
|
|
1495
1604
|
PDF_REPORT_ORIENTATION = 'P'
|
|
1496
1605
|
PDF_REPORT_MEASURE_UNIT = 'mm'
|
|
1497
1606
|
PDF_REPORT_PAGE_SIZE = 'Letter'
|
|
1498
1607
|
|
|
1499
|
-
ReportTuple = namedtuple(
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1608
|
+
ReportTuple = namedtuple(
|
|
1609
|
+
'ReportTuple',
|
|
1610
|
+
field_names=[
|
|
1611
|
+
'balance_sheet_statement',
|
|
1612
|
+
'income_statement',
|
|
1613
|
+
'cash_flow_statement',
|
|
1614
|
+
],
|
|
1615
|
+
)
|
|
1616
|
+
|
|
1617
|
+
def digest_balance_sheet(
|
|
1618
|
+
self,
|
|
1619
|
+
to_date: Union[date, datetime],
|
|
1620
|
+
user_model: Optional[UserModel] = None,
|
|
1621
|
+
txs_queryset: Optional[QuerySet] = None,
|
|
1622
|
+
**kwargs: Dict,
|
|
1623
|
+
) -> IODigestContextManager:
|
|
1511
1624
|
"""
|
|
1512
1625
|
Digest the balance sheet for a specific time period, user, and optionally a specific set
|
|
1513
1626
|
of transactions. Returns a context manager for digesting the specified balance sheet data.
|
|
@@ -1542,18 +1655,19 @@ class IOReportMixIn:
|
|
|
1542
1655
|
txs_queryset=txs_queryset,
|
|
1543
1656
|
as_io_digest=True,
|
|
1544
1657
|
signs=True,
|
|
1545
|
-
**kwargs
|
|
1658
|
+
**kwargs,
|
|
1546
1659
|
)
|
|
1547
1660
|
|
|
1548
|
-
def get_balance_sheet_statement(
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1661
|
+
def get_balance_sheet_statement(
|
|
1662
|
+
self,
|
|
1663
|
+
to_date: Union[date, datetime],
|
|
1664
|
+
subtitle: Optional[str] = None,
|
|
1665
|
+
filepath: Optional[Path] = None,
|
|
1666
|
+
filename: Optional[str] = None,
|
|
1667
|
+
user_model: Optional[UserModel] = None,
|
|
1668
|
+
save_pdf: bool = False,
|
|
1669
|
+
**kwargs,
|
|
1670
|
+
) -> IODigestContextManager:
|
|
1557
1671
|
"""
|
|
1558
1672
|
Generates a balance sheet statement with an option to save it as a PDF file.
|
|
1559
1673
|
|
|
@@ -1598,15 +1712,9 @@ class IOReportMixIn:
|
|
|
1598
1712
|
memory or in saved PDF format. If the `save_pdf` option is enabled, the PDF
|
|
1599
1713
|
report is saved at the specified location.
|
|
1600
1714
|
"""
|
|
1601
|
-
if not DJANGO_LEDGER_PDF_SUPPORT_ENABLED:
|
|
1602
|
-
raise IOValidationError(
|
|
1603
|
-
message=_('PDF support not enabled. Install PDF support from Pipfile.')
|
|
1604
|
-
)
|
|
1605
1715
|
|
|
1606
1716
|
io_digest = self.digest_balance_sheet(
|
|
1607
|
-
to_date=to_date,
|
|
1608
|
-
user_model=user_model,
|
|
1609
|
-
**kwargs
|
|
1717
|
+
to_date=to_date, user_model=user_model, **kwargs
|
|
1610
1718
|
)
|
|
1611
1719
|
|
|
1612
1720
|
BalanceSheetReport = lazy_loader.get_balance_sheet_report_class()
|
|
@@ -1615,22 +1723,26 @@ class IOReportMixIn:
|
|
|
1615
1723
|
self.PDF_REPORT_MEASURE_UNIT,
|
|
1616
1724
|
self.PDF_REPORT_PAGE_SIZE,
|
|
1617
1725
|
io_digest=io_digest,
|
|
1618
|
-
report_subtitle=subtitle
|
|
1726
|
+
report_subtitle=subtitle,
|
|
1619
1727
|
)
|
|
1620
1728
|
if save_pdf:
|
|
1621
|
-
base_dir =
|
|
1729
|
+
base_dir = (
|
|
1730
|
+
Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
|
|
1731
|
+
)
|
|
1622
1732
|
filename = report.get_pdf_filename() if not filename else filename
|
|
1623
1733
|
filepath = base_dir.joinpath(filename)
|
|
1624
1734
|
report.create_pdf_report()
|
|
1625
1735
|
report.output(filepath)
|
|
1626
1736
|
return report
|
|
1627
1737
|
|
|
1628
|
-
def digest_income_statement(
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1738
|
+
def digest_income_statement(
|
|
1739
|
+
self,
|
|
1740
|
+
from_date: Union[date, datetime],
|
|
1741
|
+
to_date: Union[date, datetime],
|
|
1742
|
+
user_model: Optional[UserModel] = None,
|
|
1743
|
+
txs_queryset: Optional[QuerySet] = None,
|
|
1744
|
+
**kwargs,
|
|
1745
|
+
) -> IODigestContextManager:
|
|
1634
1746
|
"""
|
|
1635
1747
|
Digest the income statement within the specified date range and optionally filter
|
|
1636
1748
|
by user and transaction queryset.
|
|
@@ -1669,20 +1781,21 @@ class IOReportMixIn:
|
|
|
1669
1781
|
txs_queryset=txs_queryset,
|
|
1670
1782
|
as_io_digest=True,
|
|
1671
1783
|
sings=True,
|
|
1672
|
-
**kwargs
|
|
1784
|
+
**kwargs,
|
|
1673
1785
|
)
|
|
1674
1786
|
|
|
1675
|
-
def get_income_statement(
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1787
|
+
def get_income_statement(
|
|
1788
|
+
self,
|
|
1789
|
+
from_date: Union[date, datetime],
|
|
1790
|
+
to_date: Union[date, datetime],
|
|
1791
|
+
subtitle: Optional[str] = None,
|
|
1792
|
+
filepath: Optional[Path] = None,
|
|
1793
|
+
filename: Optional[str] = None,
|
|
1794
|
+
user_model: Optional[UserModel] = None,
|
|
1795
|
+
txs_queryset: Optional[QuerySet] = None,
|
|
1796
|
+
save_pdf: bool = False,
|
|
1797
|
+
**kwargs,
|
|
1798
|
+
):
|
|
1686
1799
|
"""
|
|
1687
1800
|
Generates an income statement report for a specific time period and allows optional PDF
|
|
1688
1801
|
saving functionality. The function utilizes configurations, user-provided parameters,
|
|
@@ -1727,17 +1840,13 @@ class IOReportMixIn:
|
|
|
1727
1840
|
generated income statement report. If `save_pdf` is True, the report will also
|
|
1728
1841
|
be saved as a PDF file at the specified location.
|
|
1729
1842
|
"""
|
|
1730
|
-
if not DJANGO_LEDGER_PDF_SUPPORT_ENABLED:
|
|
1731
|
-
raise IOValidationError(
|
|
1732
|
-
message=_('PDF support not enabled. Install PDF support from Pipfile.')
|
|
1733
|
-
)
|
|
1734
1843
|
|
|
1735
1844
|
io_digest = self.digest_income_statement(
|
|
1736
1845
|
from_date=from_date,
|
|
1737
1846
|
to_date=to_date,
|
|
1738
1847
|
user_model=user_model,
|
|
1739
1848
|
txs_queryset=txs_queryset,
|
|
1740
|
-
**kwargs
|
|
1849
|
+
**kwargs,
|
|
1741
1850
|
)
|
|
1742
1851
|
IncomeStatementReport = lazy_loader.get_income_statement_report_class()
|
|
1743
1852
|
report = IncomeStatementReport(
|
|
@@ -1745,22 +1854,26 @@ class IOReportMixIn:
|
|
|
1745
1854
|
self.PDF_REPORT_MEASURE_UNIT,
|
|
1746
1855
|
self.PDF_REPORT_PAGE_SIZE,
|
|
1747
1856
|
io_digest=io_digest,
|
|
1748
|
-
report_subtitle=subtitle
|
|
1857
|
+
report_subtitle=subtitle,
|
|
1749
1858
|
)
|
|
1750
1859
|
if save_pdf:
|
|
1751
|
-
base_dir =
|
|
1860
|
+
base_dir = (
|
|
1861
|
+
Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
|
|
1862
|
+
)
|
|
1752
1863
|
filename = report.get_pdf_filename() if not filename else filename
|
|
1753
1864
|
filepath = base_dir.joinpath(filename)
|
|
1754
1865
|
report.create_pdf_report()
|
|
1755
1866
|
report.output(filepath)
|
|
1756
1867
|
return report
|
|
1757
1868
|
|
|
1758
|
-
def digest_cash_flow_statement(
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1869
|
+
def digest_cash_flow_statement(
|
|
1870
|
+
self,
|
|
1871
|
+
from_date: Union[date, datetime],
|
|
1872
|
+
to_date: Union[date, datetime],
|
|
1873
|
+
user_model: Optional[UserModel] = None,
|
|
1874
|
+
txs_queryset: Optional[QuerySet] = None,
|
|
1875
|
+
**kwargs,
|
|
1876
|
+
) -> IODigestContextManager:
|
|
1764
1877
|
"""
|
|
1765
1878
|
Generates a digest of the cash flow statement for a specified date range, user model,
|
|
1766
1879
|
and optional transaction query set. This method utilizes an internal digest
|
|
@@ -1794,18 +1907,20 @@ class IOReportMixIn:
|
|
|
1794
1907
|
txs_queryset=txs_queryset,
|
|
1795
1908
|
as_io_digest=True,
|
|
1796
1909
|
signs=True,
|
|
1797
|
-
**kwargs
|
|
1910
|
+
**kwargs,
|
|
1798
1911
|
)
|
|
1799
1912
|
|
|
1800
|
-
def get_cash_flow_statement(
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1913
|
+
def get_cash_flow_statement(
|
|
1914
|
+
self,
|
|
1915
|
+
from_date: Union[date, datetime],
|
|
1916
|
+
to_date: Union[date, datetime],
|
|
1917
|
+
subtitle: Optional[str] = None,
|
|
1918
|
+
filepath: Optional[Path] = None,
|
|
1919
|
+
filename: Optional[str] = None,
|
|
1920
|
+
user_model: Optional[UserModel] = None,
|
|
1921
|
+
save_pdf: bool = False,
|
|
1922
|
+
**kwargs,
|
|
1923
|
+
):
|
|
1809
1924
|
"""
|
|
1810
1925
|
Generates a cash flow statement report within a specified date range and provides
|
|
1811
1926
|
an option to save the report as a PDF file. The method retrieves financial data, processes
|
|
@@ -1844,16 +1959,9 @@ class IOReportMixIn:
|
|
|
1844
1959
|
IOValidationError
|
|
1845
1960
|
If PDF support is not enabled in the system's Django ledger configuration.
|
|
1846
1961
|
"""
|
|
1847
|
-
if not DJANGO_LEDGER_PDF_SUPPORT_ENABLED:
|
|
1848
|
-
raise IOValidationError(
|
|
1849
|
-
message=_('PDF support not enabled. Install PDF support from Pipfile.')
|
|
1850
|
-
)
|
|
1851
1962
|
|
|
1852
1963
|
io_digest = self.digest_cash_flow_statement(
|
|
1853
|
-
from_date=from_date,
|
|
1854
|
-
to_date=to_date,
|
|
1855
|
-
user_model=user_model,
|
|
1856
|
-
**kwargs
|
|
1964
|
+
from_date=from_date, to_date=to_date, user_model=user_model, **kwargs
|
|
1857
1965
|
)
|
|
1858
1966
|
|
|
1859
1967
|
CashFlowStatementReport = lazy_loader.get_cash_flow_statement_report_class()
|
|
@@ -1862,21 +1970,25 @@ class IOReportMixIn:
|
|
|
1862
1970
|
self.PDF_REPORT_MEASURE_UNIT,
|
|
1863
1971
|
self.PDF_REPORT_PAGE_SIZE,
|
|
1864
1972
|
io_digest=io_digest,
|
|
1865
|
-
report_subtitle=subtitle
|
|
1973
|
+
report_subtitle=subtitle,
|
|
1866
1974
|
)
|
|
1867
1975
|
if save_pdf:
|
|
1868
|
-
base_dir =
|
|
1976
|
+
base_dir = (
|
|
1977
|
+
Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
|
|
1978
|
+
)
|
|
1869
1979
|
filename = report.get_pdf_filename() if not filename else filename
|
|
1870
1980
|
filepath = base_dir.joinpath(filename)
|
|
1871
1981
|
report.create_pdf_report()
|
|
1872
1982
|
report.output(filepath)
|
|
1873
1983
|
return report
|
|
1874
1984
|
|
|
1875
|
-
def digest_financial_statements(
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1985
|
+
def digest_financial_statements(
|
|
1986
|
+
self,
|
|
1987
|
+
from_date: Union[date, datetime],
|
|
1988
|
+
to_date: Union[date, datetime],
|
|
1989
|
+
user_model: Optional[UserModel] = None,
|
|
1990
|
+
**kwargs,
|
|
1991
|
+
) -> IODigestContextManager:
|
|
1880
1992
|
"""
|
|
1881
1993
|
Digest financial statements within a given date range, allowing optional
|
|
1882
1994
|
customization through `kwargs`. The method processes and provides access
|
|
@@ -1919,17 +2031,19 @@ class IOReportMixIn:
|
|
|
1919
2031
|
income_statement=True,
|
|
1920
2032
|
cash_flow_statement=True,
|
|
1921
2033
|
as_io_digest=True,
|
|
1922
|
-
**kwargs
|
|
2034
|
+
**kwargs,
|
|
1923
2035
|
)
|
|
1924
2036
|
|
|
1925
|
-
def get_financial_statements(
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2037
|
+
def get_financial_statements(
|
|
2038
|
+
self,
|
|
2039
|
+
from_date: Union[date, datetime],
|
|
2040
|
+
to_date: Union[date, datetime],
|
|
2041
|
+
dt_strfmt: str = '%Y%m%d',
|
|
2042
|
+
user_model: Optional[UserModel] = None,
|
|
2043
|
+
save_pdf: bool = False,
|
|
2044
|
+
filepath: Optional[Path] = None,
|
|
2045
|
+
**kwargs,
|
|
2046
|
+
) -> ReportTuple:
|
|
1933
2047
|
"""
|
|
1934
2048
|
Generates financial statements for a specified date range, optionally saving them as
|
|
1935
2049
|
PDF files. This method consolidates the balance sheet, income statement, and cash flow
|
|
@@ -1968,16 +2082,8 @@ class IOReportMixIn:
|
|
|
1968
2082
|
IOValidationError
|
|
1969
2083
|
Raised if PDF support is not enabled in the application configuration.
|
|
1970
2084
|
"""
|
|
1971
|
-
if not DJANGO_LEDGER_PDF_SUPPORT_ENABLED:
|
|
1972
|
-
raise IOValidationError(
|
|
1973
|
-
message=_('PDF support not enabled. Install PDF support from Pipfile.')
|
|
1974
|
-
)
|
|
1975
|
-
|
|
1976
2085
|
io_digest = self.digest_financial_statements(
|
|
1977
|
-
from_date=from_date,
|
|
1978
|
-
to_date=to_date,
|
|
1979
|
-
user_model=user_model,
|
|
1980
|
-
**kwargs
|
|
2086
|
+
from_date=from_date, to_date=to_date, user_model=user_model, **kwargs
|
|
1981
2087
|
)
|
|
1982
2088
|
|
|
1983
2089
|
BalanceSheetReport = lazy_loader.get_balance_sheet_report_class()
|
|
@@ -1985,45 +2091,54 @@ class IOReportMixIn:
|
|
|
1985
2091
|
self.PDF_REPORT_ORIENTATION,
|
|
1986
2092
|
self.PDF_REPORT_MEASURE_UNIT,
|
|
1987
2093
|
self.PDF_REPORT_PAGE_SIZE,
|
|
1988
|
-
io_digest=io_digest
|
|
2094
|
+
io_digest=io_digest,
|
|
1989
2095
|
)
|
|
1990
2096
|
IncomeStatementReport = lazy_loader.get_income_statement_report_class()
|
|
1991
2097
|
is_report = IncomeStatementReport(
|
|
1992
2098
|
self.PDF_REPORT_ORIENTATION,
|
|
1993
2099
|
self.PDF_REPORT_MEASURE_UNIT,
|
|
1994
2100
|
self.PDF_REPORT_PAGE_SIZE,
|
|
1995
|
-
io_digest=io_digest
|
|
2101
|
+
io_digest=io_digest,
|
|
1996
2102
|
)
|
|
1997
2103
|
CashFlowStatementReport = lazy_loader.get_cash_flow_statement_report_class()
|
|
1998
2104
|
cfs_report = CashFlowStatementReport(
|
|
1999
2105
|
self.PDF_REPORT_ORIENTATION,
|
|
2000
2106
|
self.PDF_REPORT_MEASURE_UNIT,
|
|
2001
2107
|
self.PDF_REPORT_PAGE_SIZE,
|
|
2002
|
-
io_digest=io_digest
|
|
2108
|
+
io_digest=io_digest,
|
|
2003
2109
|
)
|
|
2004
2110
|
|
|
2005
2111
|
if save_pdf:
|
|
2006
|
-
base_dir =
|
|
2112
|
+
base_dir = (
|
|
2113
|
+
Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
|
|
2114
|
+
)
|
|
2007
2115
|
bs_report.create_pdf_report()
|
|
2008
|
-
bs_report.output(
|
|
2116
|
+
bs_report.output(
|
|
2117
|
+
base_dir.joinpath(bs_report.get_pdf_filename(dt_strfmt=dt_strfmt))
|
|
2118
|
+
)
|
|
2009
2119
|
|
|
2010
2120
|
is_report.create_pdf_report()
|
|
2011
|
-
is_report.output(
|
|
2121
|
+
is_report.output(
|
|
2122
|
+
base_dir.joinpath(
|
|
2123
|
+
is_report.get_pdf_filename(from_dt=from_date, dt_strfmt=dt_strfmt)
|
|
2124
|
+
)
|
|
2125
|
+
)
|
|
2012
2126
|
|
|
2013
2127
|
cfs_report.create_pdf_report()
|
|
2014
|
-
cfs_report.output(
|
|
2128
|
+
cfs_report.output(
|
|
2129
|
+
base_dir.joinpath(
|
|
2130
|
+
cfs_report.get_pdf_filename(from_dt=from_date, dt_strfmt=dt_strfmt)
|
|
2131
|
+
)
|
|
2132
|
+
)
|
|
2015
2133
|
|
|
2016
2134
|
return self.ReportTuple(
|
|
2017
2135
|
balance_sheet_statement=bs_report,
|
|
2018
2136
|
income_statement=is_report,
|
|
2019
|
-
cash_flow_statement=cfs_report
|
|
2137
|
+
cash_flow_statement=cfs_report,
|
|
2020
2138
|
)
|
|
2021
2139
|
|
|
2022
2140
|
|
|
2023
|
-
class IOMixIn(
|
|
2024
|
-
IODatabaseMixIn,
|
|
2025
|
-
IOReportMixIn
|
|
2026
|
-
):
|
|
2141
|
+
class IOMixIn(IODatabaseMixIn, IOReportMixIn):
|
|
2027
2142
|
"""
|
|
2028
2143
|
Provides input and output functionalities by mixing in database and
|
|
2029
2144
|
reporting environments.
|