django-ledger 0.8.0__py3-none-any.whl → 0.8.2__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/forms/account.py +45 -46
- django_ledger/forms/data_import.py +182 -64
- django_ledger/io/io_core.py +507 -374
- django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
- django_ledger/models/__init__.py +2 -1
- django_ledger/models/bill.py +337 -300
- django_ledger/models/customer.py +47 -34
- django_ledger/models/data_import.py +770 -289
- django_ledger/models/entity.py +882 -637
- django_ledger/models/mixins.py +421 -282
- django_ledger/models/receipt.py +1083 -0
- django_ledger/models/transactions.py +105 -41
- django_ledger/models/unit.py +42 -30
- django_ledger/models/utils.py +12 -2
- django_ledger/models/vendor.py +85 -66
- django_ledger/settings.py +1 -0
- django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
- django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +1 -13
- django_ledger/templates/django_ledger/bills/bill_update.html +1 -1
- 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 +3 -1
- 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/invoice/invoice_update.html +1 -1
- django_ledger/templates/django_ledger/layouts/base.html +3 -1
- 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 +3 -2
- django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
- django_ledger/templatetags/django_ledger.py +338 -191
- django_ledger/urls/__init__.py +1 -0
- 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/customer.py +56 -14
- django_ledger/views/data_import.py +119 -66
- django_ledger/views/mixins.py +112 -86
- django_ledger/views/receipt.py +294 -0
- django_ledger/views/vendor.py +53 -14
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/METADATA +1 -1
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/RECORD +55 -45
- django_ledger/static/django_ledger/bundle/styles.bundle.js +0 -1
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/WHEEL +0 -0
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/licenses/AUTHORS.md +0 -0
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/licenses/LICENSE +0 -0
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/top_level.txt +0 -0
django_ledger/io/io_core.py
CHANGED
|
@@ -87,36 +87,39 @@ 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
|
|
@@ -176,7 +179,7 @@ def diff_tx_data(tx_data: list, raise_exception: bool = True):
|
|
|
176
179
|
else:
|
|
177
180
|
raise ValidationError('Only Dictionary or TransactionModel allowed.')
|
|
178
181
|
|
|
179
|
-
is_valid =
|
|
182
|
+
is_valid = credits == debits
|
|
180
183
|
diff = credits - debits
|
|
181
184
|
|
|
182
185
|
if not is_valid and abs(diff) > settings.DJANGO_LEDGER_TRANSACTION_MAX_TOLERANCE:
|
|
@@ -212,40 +215,51 @@ def check_tx_balance(tx_data: list, perform_correction: bool = False) -> bool:
|
|
|
212
215
|
tolerance (with or without correction). Returns False otherwise.
|
|
213
216
|
"""
|
|
214
217
|
if tx_data:
|
|
215
|
-
|
|
216
|
-
|
|
218
|
+
IS_TX_MODEL, is_valid, diff = diff_tx_data(
|
|
219
|
+
tx_data, raise_exception=perform_correction
|
|
220
|
+
)
|
|
217
221
|
|
|
218
222
|
if not perform_correction and abs(diff):
|
|
219
223
|
return False
|
|
220
224
|
|
|
221
|
-
if
|
|
225
|
+
if (
|
|
226
|
+
not perform_correction
|
|
227
|
+
and abs(diff) > settings.DJANGO_LEDGER_TRANSACTION_MAX_TOLERANCE
|
|
228
|
+
):
|
|
222
229
|
return False
|
|
223
230
|
|
|
224
231
|
while not is_valid:
|
|
225
232
|
tx_type_choice = choice([DEBIT, CREDIT])
|
|
226
233
|
|
|
227
234
|
if IS_TX_MODEL:
|
|
228
|
-
txs_candidates = list(
|
|
235
|
+
txs_candidates = list(
|
|
236
|
+
tx for tx in tx_data if tx.tx_type == tx_type_choice
|
|
237
|
+
)
|
|
229
238
|
else:
|
|
230
|
-
txs_candidates = list(
|
|
239
|
+
txs_candidates = list(
|
|
240
|
+
tx for tx in tx_data if tx['tx_type'] == tx_type_choice
|
|
241
|
+
)
|
|
231
242
|
|
|
232
243
|
if len(txs_candidates) > 0:
|
|
233
|
-
|
|
234
244
|
tx = choice(txs_candidates)
|
|
235
245
|
|
|
236
|
-
if any(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
246
|
+
if any(
|
|
247
|
+
[
|
|
248
|
+
diff > 0 and tx_type_choice == DEBIT,
|
|
249
|
+
diff < 0 and tx_type_choice == CREDIT,
|
|
250
|
+
]
|
|
251
|
+
):
|
|
240
252
|
if IS_TX_MODEL:
|
|
241
253
|
tx.amount += settings.DJANGO_LEDGER_TRANSACTION_CORRECTION
|
|
242
254
|
else:
|
|
243
255
|
tx['amount'] += settings.DJANGO_LEDGER_TRANSACTION_CORRECTION
|
|
244
256
|
|
|
245
|
-
elif any(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
257
|
+
elif any(
|
|
258
|
+
[
|
|
259
|
+
diff < 0 and tx_type_choice == DEBIT,
|
|
260
|
+
diff > 0 and tx_type_choice == CREDIT,
|
|
261
|
+
]
|
|
262
|
+
):
|
|
249
263
|
if IS_TX_MODEL:
|
|
250
264
|
tx.amount -= settings.DJANGO_LEDGER_TRANSACTION_CORRECTION
|
|
251
265
|
else:
|
|
@@ -284,17 +298,17 @@ def get_localtime(tz=None) -> datetime:
|
|
|
284
298
|
|
|
285
299
|
def get_localdate() -> date:
|
|
286
300
|
"""
|
|
287
|
-
|
|
301
|
+
Fetches the current local date, optionally considering time zone settings.
|
|
288
302
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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.
|
|
293
307
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
308
|
+
Returns
|
|
309
|
+
-------
|
|
310
|
+
date
|
|
311
|
+
The current local date, adjusted for the time zone setting if applicable.
|
|
298
312
|
"""
|
|
299
313
|
if global_settings.USE_TZ:
|
|
300
314
|
return localdate()
|
|
@@ -302,8 +316,7 @@ def get_localdate() -> date:
|
|
|
302
316
|
|
|
303
317
|
|
|
304
318
|
def validate_io_timestamp(
|
|
305
|
-
|
|
306
|
-
no_parse_localdate: bool = True
|
|
319
|
+
dt: Union[str, date, datetime], no_parse_localdate: bool = True
|
|
307
320
|
) -> Optional[Union[datetime, date]]:
|
|
308
321
|
"""
|
|
309
322
|
Validates and processes a given date or datetime input and returns a processed
|
|
@@ -350,10 +363,7 @@ def validate_io_timestamp(
|
|
|
350
363
|
|
|
351
364
|
if isinstance(dt, datetime):
|
|
352
365
|
if is_naive(dt):
|
|
353
|
-
return make_aware(
|
|
354
|
-
value=dt,
|
|
355
|
-
timezone=ZoneInfo('UTC')
|
|
356
|
-
)
|
|
366
|
+
return make_aware(value=dt, timezone=ZoneInfo('UTC'))
|
|
357
367
|
return dt
|
|
358
368
|
|
|
359
369
|
elif isinstance(dt, str):
|
|
@@ -363,23 +373,21 @@ def validate_io_timestamp(
|
|
|
363
373
|
# try to parse a datetime object from string...
|
|
364
374
|
fdt = parse_datetime(dt)
|
|
365
375
|
if not fdt:
|
|
366
|
-
raise InvalidDateInputError(
|
|
367
|
-
message=f'Could not parse date from {dt}'
|
|
368
|
-
)
|
|
376
|
+
raise InvalidDateInputError(message=f'Could not parse date from {dt}')
|
|
369
377
|
elif is_naive(fdt):
|
|
370
378
|
fdt = make_aware(fdt)
|
|
371
379
|
if global_settings.USE_TZ:
|
|
372
380
|
return make_aware(
|
|
373
381
|
datetime.combine(
|
|
374
|
-
fdt,
|
|
375
|
-
|
|
382
|
+
fdt,
|
|
383
|
+
datetime.min.time(),
|
|
384
|
+
)
|
|
385
|
+
)
|
|
376
386
|
return datetime.combine(fdt, datetime.min.time())
|
|
377
387
|
|
|
378
388
|
elif isinstance(dt, date):
|
|
379
389
|
if global_settings.USE_TZ:
|
|
380
|
-
return make_aware(
|
|
381
|
-
value=datetime.combine(dt, datetime.min.time())
|
|
382
|
-
)
|
|
390
|
+
return make_aware(value=datetime.combine(dt, datetime.min.time()))
|
|
383
391
|
return datetime.combine(dt, datetime.min.time())
|
|
384
392
|
|
|
385
393
|
if no_parse_localdate:
|
|
@@ -387,8 +395,8 @@ def validate_io_timestamp(
|
|
|
387
395
|
|
|
388
396
|
|
|
389
397
|
def validate_dates(
|
|
390
|
-
|
|
391
|
-
|
|
398
|
+
from_date: Optional[Union[str, datetime, date]] = None,
|
|
399
|
+
to_date: Optional[Union[str, datetime, date]] = None,
|
|
392
400
|
) -> Tuple[date, date]:
|
|
393
401
|
"""
|
|
394
402
|
Validates and converts the input dates to date objects. This function ensures that the
|
|
@@ -454,7 +462,9 @@ def validate_activity(activity: str, raise_404: bool = False):
|
|
|
454
462
|
JournalEntryModel = lazy_loader.get_journal_entry_model()
|
|
455
463
|
valid = activity in JournalEntryModel.VALID_ACTIVITIES
|
|
456
464
|
if activity and not valid:
|
|
457
|
-
exception = ValidationError(
|
|
465
|
+
exception = ValidationError(
|
|
466
|
+
f'{activity} is invalid. Choices are {JournalEntryModel.VALID_ACTIVITIES}.'
|
|
467
|
+
)
|
|
458
468
|
if raise_404:
|
|
459
469
|
raise Http404(exception)
|
|
460
470
|
raise exception
|
|
@@ -493,6 +503,7 @@ class IOResult:
|
|
|
493
503
|
A summary or aggregation of account balances derived from the processed
|
|
494
504
|
data.
|
|
495
505
|
"""
|
|
506
|
+
|
|
496
507
|
# DB Aggregation...
|
|
497
508
|
db_from_date: Optional[date] = None
|
|
498
509
|
db_to_date: Optional[date] = None
|
|
@@ -510,10 +521,12 @@ class IOResult:
|
|
|
510
521
|
|
|
511
522
|
@property
|
|
512
523
|
def is_bounded(self) -> bool:
|
|
513
|
-
return all(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
524
|
+
return all(
|
|
525
|
+
[
|
|
526
|
+
self.ce_from_date is not None,
|
|
527
|
+
self.ce_to_date is not None,
|
|
528
|
+
]
|
|
529
|
+
)
|
|
517
530
|
|
|
518
531
|
|
|
519
532
|
class IODatabaseMixIn:
|
|
@@ -528,16 +541,20 @@ class IODatabaseMixIn:
|
|
|
528
541
|
|
|
529
542
|
Attributes
|
|
530
543
|
----------
|
|
531
|
-
TRANSACTION_MODEL_CLASS
|
|
544
|
+
TRANSACTION_MODEL_CLASS: NoneType or Type
|
|
532
545
|
Specifies the Django model class for transactions. If None, a lazy loader
|
|
533
546
|
will be used to determine the model dynamically.
|
|
534
|
-
JOURNAL_ENTRY_MODEL_CLASS
|
|
547
|
+
JOURNAL_ENTRY_MODEL_CLASS: NoneType or Type
|
|
535
548
|
Specifies the Django model class for journal entries. If None, a lazy
|
|
536
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.
|
|
537
553
|
"""
|
|
538
554
|
|
|
539
555
|
TRANSACTION_MODEL_CLASS = None
|
|
540
556
|
JOURNAL_ENTRY_MODEL_CLASS = None
|
|
557
|
+
STAGED_TRANSACTION_MODEL_CLASS = None
|
|
541
558
|
|
|
542
559
|
def is_entity_model(self):
|
|
543
560
|
"""
|
|
@@ -595,7 +612,9 @@ class IODatabaseMixIn:
|
|
|
595
612
|
elif self.is_entity_unit_model():
|
|
596
613
|
return getattr(self, 'entity')
|
|
597
614
|
raise IOValidationError(
|
|
598
|
-
message=_(
|
|
615
|
+
message=_(
|
|
616
|
+
f'IODatabaseMixIn not compatible with {self.__class__.__name__} model.'
|
|
617
|
+
)
|
|
599
618
|
)
|
|
600
619
|
|
|
601
620
|
def get_transaction_model(self):
|
|
@@ -617,6 +636,25 @@ class IODatabaseMixIn:
|
|
|
617
636
|
return self.TRANSACTION_MODEL_CLASS
|
|
618
637
|
return lazy_loader.get_txs_model()
|
|
619
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
|
+
|
|
620
658
|
def get_journal_entry_model(self):
|
|
621
659
|
"""
|
|
622
660
|
Retrieves the class model for journal entries. If the `JOURNAL_ENTRY_MODEL_CLASS`
|
|
@@ -633,23 +671,24 @@ class IODatabaseMixIn:
|
|
|
633
671
|
return self.JOURNAL_ENTRY_MODEL_CLASS
|
|
634
672
|
return lazy_loader.get_journal_entry_model()
|
|
635
673
|
|
|
636
|
-
def database_digest(
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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:
|
|
653
692
|
"""
|
|
654
693
|
Aggregates transaction data based on the provided parameters to generate a
|
|
655
694
|
digest of financial entries. This method is designed to work with various
|
|
@@ -702,7 +741,7 @@ class IODatabaseMixIn:
|
|
|
702
741
|
exclude_zero_bal : bool
|
|
703
742
|
If True, transactions with zero-balance amounts will be excluded.
|
|
704
743
|
Defaults to True.
|
|
705
|
-
|
|
744
|
+
use_closing_entry : bool
|
|
706
745
|
Specifies whether closing entries should be used to optimize database
|
|
707
746
|
aggregation. If not provided, the value is determined by the system-global
|
|
708
747
|
setting.
|
|
@@ -724,10 +763,11 @@ class IODatabaseMixIn:
|
|
|
724
763
|
if self.is_entity_model():
|
|
725
764
|
if entity_slug:
|
|
726
765
|
if entity_slug != self.slug:
|
|
727
|
-
raise IOValidationError(
|
|
728
|
-
|
|
766
|
+
raise IOValidationError(
|
|
767
|
+
'Inconsistent entity_slug. '
|
|
768
|
+
f'Provided {entity_slug} does not match actual {self.slug}'
|
|
769
|
+
)
|
|
729
770
|
if unit_slug:
|
|
730
|
-
|
|
731
771
|
txs_queryset_init = TransactionModel.objects.for_entity(
|
|
732
772
|
entity_model=entity_slug or self.slug
|
|
733
773
|
).for_unit(unit_slug=unit_slug)
|
|
@@ -739,7 +779,8 @@ class IODatabaseMixIn:
|
|
|
739
779
|
elif self.is_entity_unit_model():
|
|
740
780
|
if not entity_slug:
|
|
741
781
|
raise IOValidationError(
|
|
742
|
-
'Calling digest from Entity Unit requires entity_slug explicitly for safety'
|
|
782
|
+
'Calling digest from Entity Unit requires entity_slug explicitly for safety'
|
|
783
|
+
)
|
|
743
784
|
|
|
744
785
|
txs_queryset_init = TransactionModel.objects.for_entity(
|
|
745
786
|
entity_model=entity_slug
|
|
@@ -748,7 +789,8 @@ class IODatabaseMixIn:
|
|
|
748
789
|
elif self.is_ledger_model():
|
|
749
790
|
if not entity_slug:
|
|
750
791
|
raise IOValidationError(
|
|
751
|
-
'Calling digest from Ledger Model requires entity_slug explicitly for safety'
|
|
792
|
+
'Calling digest from Ledger Model requires entity_slug explicitly for safety'
|
|
793
|
+
)
|
|
752
794
|
|
|
753
795
|
txs_queryset_init = TransactionModel.objects.for_entity(
|
|
754
796
|
entity_model=entity_slug
|
|
@@ -765,8 +807,8 @@ class IODatabaseMixIn:
|
|
|
765
807
|
txs_queryset_to_closing_entry = txs_queryset_init.none()
|
|
766
808
|
|
|
767
809
|
USE_CLOSING_ENTRIES = settings.DJANGO_LEDGER_USE_CLOSING_ENTRIES
|
|
768
|
-
if
|
|
769
|
-
USE_CLOSING_ENTRIES =
|
|
810
|
+
if use_closing_entry is not None:
|
|
811
|
+
USE_CLOSING_ENTRIES = use_closing_entry
|
|
770
812
|
|
|
771
813
|
# use closing entries to minimize DB aggregation if possible and activated...
|
|
772
814
|
if USE_CLOSING_ENTRIES:
|
|
@@ -774,18 +816,23 @@ class IODatabaseMixIn:
|
|
|
774
816
|
entity_model = self.get_entity_model_from_io()
|
|
775
817
|
|
|
776
818
|
# looking up available dates...
|
|
777
|
-
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
|
+
)
|
|
778
822
|
ce_to_date = entity_model.get_closing_entry_for_date(io_date=to_date)
|
|
779
823
|
|
|
780
824
|
# unbounded lookup, no date match
|
|
781
825
|
# finding the closest closing entry to aggregate from if present...
|
|
782
826
|
if not from_date and not ce_to_date:
|
|
783
|
-
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
|
+
)
|
|
784
830
|
|
|
785
831
|
# if there's a suitable closing entry...
|
|
786
832
|
if ce_alt_from_date:
|
|
787
833
|
txs_queryset_from_closing_entry = txs_queryset_closing_entry.filter(
|
|
788
|
-
journal_entry__timestamp__date=ce_alt_from_date
|
|
834
|
+
journal_entry__timestamp__date=ce_alt_from_date
|
|
835
|
+
)
|
|
789
836
|
io_result.ce_match = True
|
|
790
837
|
io_result.ce_from_date = ce_alt_from_date
|
|
791
838
|
|
|
@@ -798,7 +845,8 @@ class IODatabaseMixIn:
|
|
|
798
845
|
# unbounded lookup, exact to_date match...
|
|
799
846
|
elif not from_date and ce_to_date:
|
|
800
847
|
txs_queryset_to_closing_entry = txs_queryset_closing_entry.filter(
|
|
801
|
-
journal_entry__timestamp__date=ce_to_date
|
|
848
|
+
journal_entry__timestamp__date=ce_to_date
|
|
849
|
+
)
|
|
802
850
|
io_result.ce_match = True
|
|
803
851
|
io_result.ce_to_date = ce_to_date
|
|
804
852
|
|
|
@@ -810,10 +858,12 @@ class IODatabaseMixIn:
|
|
|
810
858
|
# bounded exact from_date and to_date match...
|
|
811
859
|
elif ce_from_date and ce_to_date:
|
|
812
860
|
txs_queryset_from_closing_entry = txs_queryset_closing_entry.filter(
|
|
813
|
-
journal_entry__timestamp__date=ce_from_date
|
|
861
|
+
journal_entry__timestamp__date=ce_from_date
|
|
862
|
+
)
|
|
814
863
|
|
|
815
864
|
txs_queryset_to_closing_entry = txs_queryset_closing_entry.filter(
|
|
816
|
-
journal_entry__timestamp__date=ce_to_date
|
|
865
|
+
journal_entry__timestamp__date=ce_to_date
|
|
866
|
+
)
|
|
817
867
|
|
|
818
868
|
io_result.ce_match = True
|
|
819
869
|
io_result.ce_from_date = ce_from_date
|
|
@@ -824,10 +874,14 @@ class IODatabaseMixIn:
|
|
|
824
874
|
io_result.db_to_date = None
|
|
825
875
|
txs_queryset_agg = TransactionModel.objects.none()
|
|
826
876
|
|
|
827
|
-
txs_queryset_closing_entry =
|
|
877
|
+
txs_queryset_closing_entry = (
|
|
878
|
+
txs_queryset_from_closing_entry | txs_queryset_to_closing_entry
|
|
879
|
+
)
|
|
828
880
|
|
|
829
881
|
if io_result.db_from_date:
|
|
830
|
-
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
|
+
)
|
|
831
885
|
|
|
832
886
|
if io_result.db_to_date:
|
|
833
887
|
txs_queryset_agg = txs_queryset_agg.to_date(to_date=io_result.db_to_date)
|
|
@@ -857,33 +911,42 @@ class IODatabaseMixIn:
|
|
|
857
911
|
cleared_filter = kwargs.get('cleared')
|
|
858
912
|
if cleared_filter is not None:
|
|
859
913
|
if cleared_filter in [True, False]:
|
|
860
|
-
txs_queryset =
|
|
914
|
+
txs_queryset = (
|
|
915
|
+
txs_queryset.is_cleared()
|
|
916
|
+
if cleared_filter
|
|
917
|
+
else txs_queryset.not_cleared()
|
|
918
|
+
)
|
|
861
919
|
else:
|
|
862
920
|
raise IOValidationError(
|
|
863
921
|
message=f'Invalid value for cleared filter: {cleared_filter}. '
|
|
864
|
-
|
|
922
|
+
f'Valid values are True, False'
|
|
865
923
|
)
|
|
866
924
|
|
|
867
925
|
# Reconciled transaction filter via KWARGS....
|
|
868
926
|
reconciled_filter = kwargs.get('reconciled')
|
|
869
927
|
if reconciled_filter is not None:
|
|
870
928
|
if reconciled_filter in [True, False]:
|
|
871
|
-
txs_queryset =
|
|
929
|
+
txs_queryset = (
|
|
930
|
+
txs_queryset.is_reconciled()
|
|
931
|
+
if reconciled_filter
|
|
932
|
+
else txs_queryset.not_reconciled()
|
|
933
|
+
)
|
|
872
934
|
else:
|
|
873
935
|
raise IOValidationError(
|
|
874
936
|
message=f'Invalid value for reconciled filter: {reconciled_filter}. '
|
|
875
|
-
|
|
937
|
+
f'Valid values are True, False'
|
|
876
938
|
)
|
|
877
939
|
|
|
878
940
|
if io_result.is_bounded:
|
|
879
941
|
txs_queryset = txs_queryset.annotate(
|
|
880
942
|
amount_io=Case(
|
|
881
943
|
When(
|
|
882
|
-
journal_entry__timestamp__date=ce_from_date,
|
|
883
|
-
|
|
944
|
+
journal_entry__timestamp__date=ce_from_date, then=-F('amount')
|
|
945
|
+
),
|
|
884
946
|
default=F('amount'),
|
|
885
|
-
output_field=DecimalField()
|
|
886
|
-
)
|
|
947
|
+
output_field=DecimalField(),
|
|
948
|
+
)
|
|
949
|
+
)
|
|
887
950
|
|
|
888
951
|
VALUES = [
|
|
889
952
|
'account__uuid',
|
|
@@ -907,7 +970,10 @@ class IODatabaseMixIn:
|
|
|
907
970
|
|
|
908
971
|
if by_unit:
|
|
909
972
|
ORDER_BY.append('journal_entry__entity_unit__uuid')
|
|
910
|
-
VALUES += [
|
|
973
|
+
VALUES += [
|
|
974
|
+
'journal_entry__entity_unit__uuid',
|
|
975
|
+
'journal_entry__entity_unit__name',
|
|
976
|
+
]
|
|
911
977
|
|
|
912
978
|
if by_period:
|
|
913
979
|
ORDER_BY.append('journal_entry__timestamp')
|
|
@@ -921,27 +987,30 @@ class IODatabaseMixIn:
|
|
|
921
987
|
ORDER_BY.append('tx_type')
|
|
922
988
|
VALUES.append('tx_type')
|
|
923
989
|
|
|
924
|
-
io_result.txs_queryset =
|
|
990
|
+
io_result.txs_queryset = (
|
|
991
|
+
txs_queryset.values(*VALUES).annotate(**ANNOTATE).order_by(*ORDER_BY)
|
|
992
|
+
)
|
|
925
993
|
return io_result
|
|
926
994
|
|
|
927
|
-
def python_digest(
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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:
|
|
945
1014
|
"""
|
|
946
1015
|
Computes and returns the digest of transactions for a given entity, unit,
|
|
947
1016
|
and optional filters such as date range, account role, and activity. The
|
|
@@ -980,7 +1049,7 @@ class IODatabaseMixIn:
|
|
|
980
1049
|
Whether to group the results by transaction type. Defaults to False.
|
|
981
1050
|
by_period : bool
|
|
982
1051
|
Whether to group the results by period (year and month). Defaults to False.
|
|
983
|
-
|
|
1052
|
+
use_closing_entry : bool
|
|
984
1053
|
Whether to include closing entries in the computation. Defaults to False.
|
|
985
1054
|
force_queryset_sorting : bool
|
|
986
1055
|
Whether to force sorting of the transaction queryset. Defaults to False.
|
|
@@ -998,7 +1067,6 @@ class IODatabaseMixIn:
|
|
|
998
1067
|
role = roles_module.GROUP_EARNINGS
|
|
999
1068
|
|
|
1000
1069
|
io_result = self.database_digest(
|
|
1001
|
-
user_model=user_model,
|
|
1002
1070
|
entity_slug=entity_slug,
|
|
1003
1071
|
unit_slug=unit_slug,
|
|
1004
1072
|
to_date=to_date,
|
|
@@ -1010,10 +1078,9 @@ class IODatabaseMixIn:
|
|
|
1010
1078
|
activity=activity,
|
|
1011
1079
|
role=role,
|
|
1012
1080
|
accounts=accounts,
|
|
1013
|
-
|
|
1014
|
-
**kwargs
|
|
1015
|
-
|
|
1016
|
-
TransactionModel = self.get_transaction_model()
|
|
1081
|
+
use_closing_entry=use_closing_entry,
|
|
1082
|
+
**kwargs,
|
|
1083
|
+
)
|
|
1017
1084
|
|
|
1018
1085
|
for tx_model in io_result.txs_queryset:
|
|
1019
1086
|
if tx_model['account__balance_type'] != tx_model['tx_type']:
|
|
@@ -1040,15 +1107,26 @@ class IODatabaseMixIn:
|
|
|
1040
1107
|
|
|
1041
1108
|
if signs:
|
|
1042
1109
|
for acc in accounts_digest:
|
|
1043
|
-
if any(
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
+
):
|
|
1052
1130
|
acc['balance'] = -acc['balance']
|
|
1053
1131
|
|
|
1054
1132
|
io_result.accounts_digest = accounts_digest
|
|
@@ -1113,30 +1191,31 @@ class IODatabaseMixIn:
|
|
|
1113
1191
|
'balance': sum(a['balance'] for a in gl),
|
|
1114
1192
|
}
|
|
1115
1193
|
|
|
1116
|
-
def digest(
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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:
|
|
1140
1219
|
"""
|
|
1141
1220
|
Processes financial data and generates various financial statements, ratios, or activity digests
|
|
1142
1221
|
based on the provided arguments. The method applies specific processing pipelines, such as role
|
|
@@ -1226,7 +1305,6 @@ class IODatabaseMixIn:
|
|
|
1226
1305
|
io_state['by_tx_type'] = by_tx_type
|
|
1227
1306
|
|
|
1228
1307
|
io_result: IOResult = self.python_digest(
|
|
1229
|
-
user_model=user_model,
|
|
1230
1308
|
accounts=accounts,
|
|
1231
1309
|
role=role,
|
|
1232
1310
|
activity=activity,
|
|
@@ -1241,7 +1319,7 @@ class IODatabaseMixIn:
|
|
|
1241
1319
|
by_activity=by_activity,
|
|
1242
1320
|
by_tx_type=by_tx_type,
|
|
1243
1321
|
use_closing_entry=use_closing_entry,
|
|
1244
|
-
**kwargs
|
|
1322
|
+
**kwargs,
|
|
1245
1323
|
)
|
|
1246
1324
|
|
|
1247
1325
|
io_state['io_result'] = io_result
|
|
@@ -1251,40 +1329,43 @@ class IODatabaseMixIn:
|
|
|
1251
1329
|
|
|
1252
1330
|
if process_roles:
|
|
1253
1331
|
roles_mgr = AccountRoleIOMiddleware(
|
|
1254
|
-
io_data=io_state,
|
|
1255
|
-
by_period=by_period,
|
|
1256
|
-
by_unit=by_unit
|
|
1332
|
+
io_data=io_state, by_period=by_period, by_unit=by_unit
|
|
1257
1333
|
)
|
|
1258
1334
|
|
|
1259
1335
|
io_state = roles_mgr.digest()
|
|
1260
1336
|
|
|
1261
|
-
if any(
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1337
|
+
if any(
|
|
1338
|
+
[
|
|
1339
|
+
process_groups,
|
|
1340
|
+
balance_sheet_statement,
|
|
1341
|
+
income_statement,
|
|
1342
|
+
cash_flow_statement,
|
|
1343
|
+
]
|
|
1344
|
+
):
|
|
1267
1345
|
group_mgr = AccountGroupIOMiddleware(
|
|
1268
|
-
io_data=io_state,
|
|
1269
|
-
by_period=by_period,
|
|
1270
|
-
by_unit=by_unit
|
|
1346
|
+
io_data=io_state, by_period=by_period, by_unit=by_unit
|
|
1271
1347
|
)
|
|
1272
1348
|
io_state = group_mgr.digest()
|
|
1273
1349
|
|
|
1274
1350
|
# todo: migrate this to group manager...
|
|
1275
1351
|
io_state['group_account']['GROUP_ASSETS'].sort(
|
|
1276
|
-
key=lambda acc: roles_module.ROLES_ORDER_ASSETS.index(acc['role'])
|
|
1352
|
+
key=lambda acc: roles_module.ROLES_ORDER_ASSETS.index(acc['role'])
|
|
1353
|
+
)
|
|
1277
1354
|
io_state['group_account']['GROUP_LIABILITIES'].sort(
|
|
1278
|
-
key=lambda acc: roles_module.ROLES_ORDER_LIABILITIES.index(acc['role'])
|
|
1355
|
+
key=lambda acc: roles_module.ROLES_ORDER_LIABILITIES.index(acc['role'])
|
|
1356
|
+
)
|
|
1279
1357
|
io_state['group_account']['GROUP_CAPITAL'].sort(
|
|
1280
|
-
key=lambda acc: roles_module.ROLES_ORDER_CAPITAL.index(acc['role'])
|
|
1358
|
+
key=lambda acc: roles_module.ROLES_ORDER_CAPITAL.index(acc['role'])
|
|
1359
|
+
)
|
|
1281
1360
|
|
|
1282
1361
|
if process_ratios:
|
|
1283
1362
|
ratio_gen = FinancialRatioManager(io_data=io_state)
|
|
1284
1363
|
io_state = ratio_gen.digest()
|
|
1285
1364
|
|
|
1286
1365
|
if process_activity:
|
|
1287
|
-
activity_manager = JEActivityIOMiddleware(
|
|
1366
|
+
activity_manager = JEActivityIOMiddleware(
|
|
1367
|
+
io_data=io_state, by_unit=by_unit, by_period=by_period
|
|
1368
|
+
)
|
|
1288
1369
|
activity_manager.digest()
|
|
1289
1370
|
|
|
1290
1371
|
if balance_sheet_statement:
|
|
@@ -1301,16 +1382,18 @@ class IODatabaseMixIn:
|
|
|
1301
1382
|
|
|
1302
1383
|
return IODigestContextManager(io_state=io_state)
|
|
1303
1384
|
|
|
1304
|
-
def commit_txs(
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
+
):
|
|
1314
1397
|
"""
|
|
1315
1398
|
Commits a set of financial transactions to a journal entry, after performing
|
|
1316
1399
|
validation checks. Validations include ensuring balanced transactions, ensuring
|
|
@@ -1362,107 +1445,133 @@ class IODatabaseMixIn:
|
|
|
1362
1445
|
"""
|
|
1363
1446
|
TransactionModel = self.get_transaction_model()
|
|
1364
1447
|
JournalEntryModel = self.get_journal_entry_model()
|
|
1448
|
+
StagedTransactionModel = self.get_staged_transaction_model()
|
|
1449
|
+
|
|
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)
|
|
1454
|
+
|
|
1455
|
+
entity_model = self.get_entity_model_from_io()
|
|
1365
1456
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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
|
+
)
|
|
1369
1487
|
|
|
1370
|
-
|
|
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:
|
|
1496
|
+
raise IOValidationError(
|
|
1497
|
+
f'LedgerModel {je_ledger_model} does not belong to {self}'
|
|
1498
|
+
)
|
|
1371
1499
|
|
|
1372
|
-
|
|
1373
|
-
if
|
|
1374
|
-
|
|
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:
|
|
1375
1508
|
raise IOValidationError(
|
|
1376
|
-
|
|
1377
|
-
f'Cannot commit transactions. The journal entry date {je_timestamp} is on a closed period.')
|
|
1509
|
+
f'EntityUnitModel {je_unit_model} does not belong to {self}'
|
|
1378
1510
|
)
|
|
1379
|
-
|
|
1380
|
-
|
|
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:
|
|
1381
1530
|
raise IOValidationError(
|
|
1382
1531
|
message=_(
|
|
1383
|
-
f'
|
|
1532
|
+
f'Unable to retrieve Journal Entry model with Timestamp {je_timestamp}'
|
|
1533
|
+
)
|
|
1384
1534
|
)
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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,
|
|
1390
1544
|
)
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
if je_ledger_model.entity_id != self.uuid:
|
|
1405
|
-
raise IOValidationError(f'LedgerModel {je_ledger_model} does not belong to {self}')
|
|
1406
|
-
|
|
1407
|
-
# Validates that the provided EntityUnitModel id valid...
|
|
1408
|
-
if all([
|
|
1409
|
-
isinstance(self, lazy_loader.get_entity_model()),
|
|
1410
|
-
je_unit_model is not None,
|
|
1411
|
-
]):
|
|
1412
|
-
if je_unit_model.entity_id != self.uuid:
|
|
1413
|
-
raise IOValidationError(f'EntityUnitModel {je_unit_model} does not belong to {self}')
|
|
1414
|
-
|
|
1415
|
-
if not je_ledger_model:
|
|
1416
|
-
je_ledger_model = self
|
|
1417
|
-
|
|
1418
|
-
if force_je_retrieval:
|
|
1419
|
-
try:
|
|
1420
|
-
if isinstance(je_timestamp, (datetime, str)):
|
|
1421
|
-
je_model = je_ledger_model.journal_entries.get(timestamp__exact=je_timestamp)
|
|
1422
|
-
elif isinstance(je_timestamp, date):
|
|
1423
|
-
je_model = je_ledger_model.journal_entries.get(timestamp__date__exact=je_timestamp)
|
|
1424
|
-
else:
|
|
1425
|
-
raise IOValidationError(message=_(f'Invalid timestamp type {type(je_timestamp)}'))
|
|
1426
|
-
except ObjectDoesNotExist:
|
|
1427
|
-
raise IOValidationError(
|
|
1428
|
-
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,
|
|
1429
1558
|
)
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
)
|
|
1440
|
-
je_model.save(verify=False)
|
|
1441
|
-
|
|
1442
|
-
# todo: add method to process list of transaction models...
|
|
1443
|
-
txs_models = [
|
|
1444
|
-
(
|
|
1445
|
-
TransactionModel(
|
|
1446
|
-
account=txm_kwargs['account'],
|
|
1447
|
-
amount=txm_kwargs['amount'],
|
|
1448
|
-
tx_type=txm_kwargs['tx_type'],
|
|
1449
|
-
description=txm_kwargs['description'],
|
|
1450
|
-
journal_entry=je_model,
|
|
1451
|
-
), txm_kwargs) for txm_kwargs in je_txs
|
|
1452
|
-
]
|
|
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')
|
|
1453
1568
|
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
tx.ledger_id = je_model.ledger_id
|
|
1457
|
-
if not getattr(tx, 'timestamp', None):
|
|
1458
|
-
tx.timestamp = je_model.timestamp
|
|
1459
|
-
staged_tx_model = txm_kwargs.get('staged_tx_model')
|
|
1460
|
-
if staged_tx_model:
|
|
1461
|
-
staged_tx_model.transaction_model = tx
|
|
1569
|
+
if staged_tx_model:
|
|
1570
|
+
staged_tx_model.transaction_model = tx
|
|
1462
1571
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
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
|
|
1466
1575
|
|
|
1467
1576
|
|
|
1468
1577
|
class IOReportMixIn:
|
|
@@ -1491,22 +1600,27 @@ class IOReportMixIn:
|
|
|
1491
1600
|
`income_statement`, and `cash_flow_statement`. Each field represents a
|
|
1492
1601
|
respective financial report.
|
|
1493
1602
|
"""
|
|
1603
|
+
|
|
1494
1604
|
PDF_REPORT_ORIENTATION = 'P'
|
|
1495
1605
|
PDF_REPORT_MEASURE_UNIT = 'mm'
|
|
1496
1606
|
PDF_REPORT_PAGE_SIZE = 'Letter'
|
|
1497
1607
|
|
|
1498
|
-
ReportTuple = namedtuple(
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
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:
|
|
1510
1624
|
"""
|
|
1511
1625
|
Digest the balance sheet for a specific time period, user, and optionally a specific set
|
|
1512
1626
|
of transactions. Returns a context manager for digesting the specified balance sheet data.
|
|
@@ -1541,18 +1655,19 @@ class IOReportMixIn:
|
|
|
1541
1655
|
txs_queryset=txs_queryset,
|
|
1542
1656
|
as_io_digest=True,
|
|
1543
1657
|
signs=True,
|
|
1544
|
-
**kwargs
|
|
1658
|
+
**kwargs,
|
|
1545
1659
|
)
|
|
1546
1660
|
|
|
1547
|
-
def get_balance_sheet_statement(
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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:
|
|
1556
1671
|
"""
|
|
1557
1672
|
Generates a balance sheet statement with an option to save it as a PDF file.
|
|
1558
1673
|
|
|
@@ -1599,9 +1714,7 @@ class IOReportMixIn:
|
|
|
1599
1714
|
"""
|
|
1600
1715
|
|
|
1601
1716
|
io_digest = self.digest_balance_sheet(
|
|
1602
|
-
to_date=to_date,
|
|
1603
|
-
user_model=user_model,
|
|
1604
|
-
**kwargs
|
|
1717
|
+
to_date=to_date, user_model=user_model, **kwargs
|
|
1605
1718
|
)
|
|
1606
1719
|
|
|
1607
1720
|
BalanceSheetReport = lazy_loader.get_balance_sheet_report_class()
|
|
@@ -1610,22 +1723,26 @@ class IOReportMixIn:
|
|
|
1610
1723
|
self.PDF_REPORT_MEASURE_UNIT,
|
|
1611
1724
|
self.PDF_REPORT_PAGE_SIZE,
|
|
1612
1725
|
io_digest=io_digest,
|
|
1613
|
-
report_subtitle=subtitle
|
|
1726
|
+
report_subtitle=subtitle,
|
|
1614
1727
|
)
|
|
1615
1728
|
if save_pdf:
|
|
1616
|
-
base_dir =
|
|
1729
|
+
base_dir = (
|
|
1730
|
+
Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
|
|
1731
|
+
)
|
|
1617
1732
|
filename = report.get_pdf_filename() if not filename else filename
|
|
1618
1733
|
filepath = base_dir.joinpath(filename)
|
|
1619
1734
|
report.create_pdf_report()
|
|
1620
1735
|
report.output(filepath)
|
|
1621
1736
|
return report
|
|
1622
1737
|
|
|
1623
|
-
def digest_income_statement(
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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:
|
|
1629
1746
|
"""
|
|
1630
1747
|
Digest the income statement within the specified date range and optionally filter
|
|
1631
1748
|
by user and transaction queryset.
|
|
@@ -1664,20 +1781,21 @@ class IOReportMixIn:
|
|
|
1664
1781
|
txs_queryset=txs_queryset,
|
|
1665
1782
|
as_io_digest=True,
|
|
1666
1783
|
sings=True,
|
|
1667
|
-
**kwargs
|
|
1784
|
+
**kwargs,
|
|
1668
1785
|
)
|
|
1669
1786
|
|
|
1670
|
-
def get_income_statement(
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
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
|
+
):
|
|
1681
1799
|
"""
|
|
1682
1800
|
Generates an income statement report for a specific time period and allows optional PDF
|
|
1683
1801
|
saving functionality. The function utilizes configurations, user-provided parameters,
|
|
@@ -1728,7 +1846,7 @@ class IOReportMixIn:
|
|
|
1728
1846
|
to_date=to_date,
|
|
1729
1847
|
user_model=user_model,
|
|
1730
1848
|
txs_queryset=txs_queryset,
|
|
1731
|
-
**kwargs
|
|
1849
|
+
**kwargs,
|
|
1732
1850
|
)
|
|
1733
1851
|
IncomeStatementReport = lazy_loader.get_income_statement_report_class()
|
|
1734
1852
|
report = IncomeStatementReport(
|
|
@@ -1736,22 +1854,26 @@ class IOReportMixIn:
|
|
|
1736
1854
|
self.PDF_REPORT_MEASURE_UNIT,
|
|
1737
1855
|
self.PDF_REPORT_PAGE_SIZE,
|
|
1738
1856
|
io_digest=io_digest,
|
|
1739
|
-
report_subtitle=subtitle
|
|
1857
|
+
report_subtitle=subtitle,
|
|
1740
1858
|
)
|
|
1741
1859
|
if save_pdf:
|
|
1742
|
-
base_dir =
|
|
1860
|
+
base_dir = (
|
|
1861
|
+
Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
|
|
1862
|
+
)
|
|
1743
1863
|
filename = report.get_pdf_filename() if not filename else filename
|
|
1744
1864
|
filepath = base_dir.joinpath(filename)
|
|
1745
1865
|
report.create_pdf_report()
|
|
1746
1866
|
report.output(filepath)
|
|
1747
1867
|
return report
|
|
1748
1868
|
|
|
1749
|
-
def digest_cash_flow_statement(
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
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:
|
|
1755
1877
|
"""
|
|
1756
1878
|
Generates a digest of the cash flow statement for a specified date range, user model,
|
|
1757
1879
|
and optional transaction query set. This method utilizes an internal digest
|
|
@@ -1785,18 +1907,20 @@ class IOReportMixIn:
|
|
|
1785
1907
|
txs_queryset=txs_queryset,
|
|
1786
1908
|
as_io_digest=True,
|
|
1787
1909
|
signs=True,
|
|
1788
|
-
**kwargs
|
|
1910
|
+
**kwargs,
|
|
1789
1911
|
)
|
|
1790
1912
|
|
|
1791
|
-
def get_cash_flow_statement(
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
+
):
|
|
1800
1924
|
"""
|
|
1801
1925
|
Generates a cash flow statement report within a specified date range and provides
|
|
1802
1926
|
an option to save the report as a PDF file. The method retrieves financial data, processes
|
|
@@ -1837,10 +1961,7 @@ class IOReportMixIn:
|
|
|
1837
1961
|
"""
|
|
1838
1962
|
|
|
1839
1963
|
io_digest = self.digest_cash_flow_statement(
|
|
1840
|
-
from_date=from_date,
|
|
1841
|
-
to_date=to_date,
|
|
1842
|
-
user_model=user_model,
|
|
1843
|
-
**kwargs
|
|
1964
|
+
from_date=from_date, to_date=to_date, user_model=user_model, **kwargs
|
|
1844
1965
|
)
|
|
1845
1966
|
|
|
1846
1967
|
CashFlowStatementReport = lazy_loader.get_cash_flow_statement_report_class()
|
|
@@ -1849,21 +1970,25 @@ class IOReportMixIn:
|
|
|
1849
1970
|
self.PDF_REPORT_MEASURE_UNIT,
|
|
1850
1971
|
self.PDF_REPORT_PAGE_SIZE,
|
|
1851
1972
|
io_digest=io_digest,
|
|
1852
|
-
report_subtitle=subtitle
|
|
1973
|
+
report_subtitle=subtitle,
|
|
1853
1974
|
)
|
|
1854
1975
|
if save_pdf:
|
|
1855
|
-
base_dir =
|
|
1976
|
+
base_dir = (
|
|
1977
|
+
Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
|
|
1978
|
+
)
|
|
1856
1979
|
filename = report.get_pdf_filename() if not filename else filename
|
|
1857
1980
|
filepath = base_dir.joinpath(filename)
|
|
1858
1981
|
report.create_pdf_report()
|
|
1859
1982
|
report.output(filepath)
|
|
1860
1983
|
return report
|
|
1861
1984
|
|
|
1862
|
-
def digest_financial_statements(
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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:
|
|
1867
1992
|
"""
|
|
1868
1993
|
Digest financial statements within a given date range, allowing optional
|
|
1869
1994
|
customization through `kwargs`. The method processes and provides access
|
|
@@ -1906,17 +2031,19 @@ class IOReportMixIn:
|
|
|
1906
2031
|
income_statement=True,
|
|
1907
2032
|
cash_flow_statement=True,
|
|
1908
2033
|
as_io_digest=True,
|
|
1909
|
-
**kwargs
|
|
2034
|
+
**kwargs,
|
|
1910
2035
|
)
|
|
1911
2036
|
|
|
1912
|
-
def get_financial_statements(
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
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:
|
|
1920
2047
|
"""
|
|
1921
2048
|
Generates financial statements for a specified date range, optionally saving them as
|
|
1922
2049
|
PDF files. This method consolidates the balance sheet, income statement, and cash flow
|
|
@@ -1956,10 +2083,7 @@ class IOReportMixIn:
|
|
|
1956
2083
|
Raised if PDF support is not enabled in the application configuration.
|
|
1957
2084
|
"""
|
|
1958
2085
|
io_digest = self.digest_financial_statements(
|
|
1959
|
-
from_date=from_date,
|
|
1960
|
-
to_date=to_date,
|
|
1961
|
-
user_model=user_model,
|
|
1962
|
-
**kwargs
|
|
2086
|
+
from_date=from_date, to_date=to_date, user_model=user_model, **kwargs
|
|
1963
2087
|
)
|
|
1964
2088
|
|
|
1965
2089
|
BalanceSheetReport = lazy_loader.get_balance_sheet_report_class()
|
|
@@ -1967,45 +2091,54 @@ class IOReportMixIn:
|
|
|
1967
2091
|
self.PDF_REPORT_ORIENTATION,
|
|
1968
2092
|
self.PDF_REPORT_MEASURE_UNIT,
|
|
1969
2093
|
self.PDF_REPORT_PAGE_SIZE,
|
|
1970
|
-
io_digest=io_digest
|
|
2094
|
+
io_digest=io_digest,
|
|
1971
2095
|
)
|
|
1972
2096
|
IncomeStatementReport = lazy_loader.get_income_statement_report_class()
|
|
1973
2097
|
is_report = IncomeStatementReport(
|
|
1974
2098
|
self.PDF_REPORT_ORIENTATION,
|
|
1975
2099
|
self.PDF_REPORT_MEASURE_UNIT,
|
|
1976
2100
|
self.PDF_REPORT_PAGE_SIZE,
|
|
1977
|
-
io_digest=io_digest
|
|
2101
|
+
io_digest=io_digest,
|
|
1978
2102
|
)
|
|
1979
2103
|
CashFlowStatementReport = lazy_loader.get_cash_flow_statement_report_class()
|
|
1980
2104
|
cfs_report = CashFlowStatementReport(
|
|
1981
2105
|
self.PDF_REPORT_ORIENTATION,
|
|
1982
2106
|
self.PDF_REPORT_MEASURE_UNIT,
|
|
1983
2107
|
self.PDF_REPORT_PAGE_SIZE,
|
|
1984
|
-
io_digest=io_digest
|
|
2108
|
+
io_digest=io_digest,
|
|
1985
2109
|
)
|
|
1986
2110
|
|
|
1987
2111
|
if save_pdf:
|
|
1988
|
-
base_dir =
|
|
2112
|
+
base_dir = (
|
|
2113
|
+
Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
|
|
2114
|
+
)
|
|
1989
2115
|
bs_report.create_pdf_report()
|
|
1990
|
-
bs_report.output(
|
|
2116
|
+
bs_report.output(
|
|
2117
|
+
base_dir.joinpath(bs_report.get_pdf_filename(dt_strfmt=dt_strfmt))
|
|
2118
|
+
)
|
|
1991
2119
|
|
|
1992
2120
|
is_report.create_pdf_report()
|
|
1993
|
-
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
|
+
)
|
|
1994
2126
|
|
|
1995
2127
|
cfs_report.create_pdf_report()
|
|
1996
|
-
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
|
+
)
|
|
1997
2133
|
|
|
1998
2134
|
return self.ReportTuple(
|
|
1999
2135
|
balance_sheet_statement=bs_report,
|
|
2000
2136
|
income_statement=is_report,
|
|
2001
|
-
cash_flow_statement=cfs_report
|
|
2137
|
+
cash_flow_statement=cfs_report,
|
|
2002
2138
|
)
|
|
2003
2139
|
|
|
2004
2140
|
|
|
2005
|
-
class IOMixIn(
|
|
2006
|
-
IODatabaseMixIn,
|
|
2007
|
-
IOReportMixIn
|
|
2008
|
-
):
|
|
2141
|
+
class IOMixIn(IODatabaseMixIn, IOReportMixIn):
|
|
2009
2142
|
"""
|
|
2010
2143
|
Provides input and output functionalities by mixing in database and
|
|
2011
2144
|
reporting environments.
|