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.

Files changed (139) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/context.py +12 -0
  3. django_ledger/forms/account.py +45 -46
  4. django_ledger/forms/bill.py +0 -4
  5. django_ledger/forms/closing_entry.py +13 -1
  6. django_ledger/forms/data_import.py +182 -63
  7. django_ledger/forms/estimate.py +3 -6
  8. django_ledger/forms/invoice.py +3 -7
  9. django_ledger/forms/item.py +10 -18
  10. django_ledger/forms/purchase_order.py +2 -4
  11. django_ledger/io/io_core.py +515 -400
  12. django_ledger/io/io_generator.py +7 -6
  13. django_ledger/io/io_library.py +1 -2
  14. django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
  15. django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
  16. django_ledger/models/__init__.py +2 -1
  17. django_ledger/models/accounts.py +109 -69
  18. django_ledger/models/bank_account.py +40 -23
  19. django_ledger/models/bill.py +386 -333
  20. django_ledger/models/chart_of_accounts.py +173 -105
  21. django_ledger/models/closing_entry.py +99 -48
  22. django_ledger/models/customer.py +100 -66
  23. django_ledger/models/data_import.py +818 -323
  24. django_ledger/models/deprecations.py +61 -0
  25. django_ledger/models/entity.py +891 -644
  26. django_ledger/models/estimate.py +57 -28
  27. django_ledger/models/invoice.py +46 -26
  28. django_ledger/models/items.py +503 -142
  29. django_ledger/models/journal_entry.py +61 -47
  30. django_ledger/models/ledger.py +106 -42
  31. django_ledger/models/mixins.py +424 -281
  32. django_ledger/models/purchase_order.py +39 -17
  33. django_ledger/models/receipt.py +1083 -0
  34. django_ledger/models/transactions.py +242 -139
  35. django_ledger/models/unit.py +93 -54
  36. django_ledger/models/utils.py +12 -2
  37. django_ledger/models/vendor.py +121 -70
  38. django_ledger/report/core.py +2 -14
  39. django_ledger/settings.py +57 -71
  40. django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
  41. django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +25 -0
  42. django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
  43. django_ledger/static/django_ledger/css/djl_styles.css +273 -0
  44. django_ledger/templates/django_ledger/bills/includes/card_bill.html +2 -2
  45. django_ledger/templates/django_ledger/components/menu.html +41 -26
  46. django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
  47. django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
  48. django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
  49. django_ledger/templates/django_ledger/customer/tags/customer_table.html +8 -6
  50. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
  51. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
  52. django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
  53. django_ledger/templates/django_ledger/entity/includes/card_entity.html +12 -6
  54. django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -1
  55. django_ledger/templates/django_ledger/financial_statements/cash_flow.html +4 -1
  56. django_ledger/templates/django_ledger/financial_statements/income_statement.html +4 -1
  57. django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +27 -3
  58. django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +16 -4
  59. django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +73 -18
  60. django_ledger/templates/django_ledger/includes/widget_ratios.html +18 -24
  61. django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +3 -3
  62. django_ledger/templates/django_ledger/layouts/base.html +7 -2
  63. django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
  64. django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
  65. django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
  66. django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
  67. django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
  68. django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
  69. django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +12 -7
  70. django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
  71. django_ledger/templatetags/django_ledger.py +338 -191
  72. django_ledger/tests/test_accounts.py +1 -2
  73. django_ledger/tests/test_io.py +17 -0
  74. django_ledger/tests/test_purchase_order.py +3 -3
  75. django_ledger/tests/test_transactions.py +1 -2
  76. django_ledger/urls/__init__.py +1 -4
  77. django_ledger/urls/customer.py +3 -0
  78. django_ledger/urls/data_import.py +3 -0
  79. django_ledger/urls/receipt.py +102 -0
  80. django_ledger/urls/vendor.py +1 -0
  81. django_ledger/views/__init__.py +1 -0
  82. django_ledger/views/bill.py +8 -11
  83. django_ledger/views/chart_of_accounts.py +6 -4
  84. django_ledger/views/closing_entry.py +11 -7
  85. django_ledger/views/customer.py +68 -30
  86. django_ledger/views/data_import.py +120 -66
  87. django_ledger/views/djl_api.py +3 -5
  88. django_ledger/views/entity.py +2 -4
  89. django_ledger/views/estimate.py +3 -7
  90. django_ledger/views/inventory.py +3 -5
  91. django_ledger/views/invoice.py +4 -6
  92. django_ledger/views/item.py +7 -11
  93. django_ledger/views/journal_entry.py +1 -2
  94. django_ledger/views/mixins.py +125 -93
  95. django_ledger/views/purchase_order.py +24 -35
  96. django_ledger/views/receipt.py +294 -0
  97. django_ledger/views/unit.py +1 -2
  98. django_ledger/views/vendor.py +54 -16
  99. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/METADATA +43 -75
  100. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/RECORD +104 -122
  101. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +1 -1
  102. django_ledger-0.8.1.dist-info/top_level.txt +1 -0
  103. django_ledger/contrib/django_ledger_graphene/__init__.py +0 -0
  104. django_ledger/contrib/django_ledger_graphene/accounts/schema.py +0 -33
  105. django_ledger/contrib/django_ledger_graphene/api.py +0 -42
  106. django_ledger/contrib/django_ledger_graphene/apps.py +0 -6
  107. django_ledger/contrib/django_ledger_graphene/auth/mutations.py +0 -49
  108. django_ledger/contrib/django_ledger_graphene/auth/schema.py +0 -6
  109. django_ledger/contrib/django_ledger_graphene/bank_account/mutations.py +0 -61
  110. django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +0 -34
  111. django_ledger/contrib/django_ledger_graphene/bill/mutations.py +0 -0
  112. django_ledger/contrib/django_ledger_graphene/bill/schema.py +0 -34
  113. django_ledger/contrib/django_ledger_graphene/coa/mutations.py +0 -0
  114. django_ledger/contrib/django_ledger_graphene/coa/schema.py +0 -30
  115. django_ledger/contrib/django_ledger_graphene/customers/__init__.py +0 -0
  116. django_ledger/contrib/django_ledger_graphene/customers/mutations.py +0 -71
  117. django_ledger/contrib/django_ledger_graphene/customers/schema.py +0 -43
  118. django_ledger/contrib/django_ledger_graphene/data_import/mutations.py +0 -0
  119. django_ledger/contrib/django_ledger_graphene/data_import/schema.py +0 -0
  120. django_ledger/contrib/django_ledger_graphene/entity/mutations.py +0 -0
  121. django_ledger/contrib/django_ledger_graphene/entity/schema.py +0 -94
  122. django_ledger/contrib/django_ledger_graphene/item/mutations.py +0 -0
  123. django_ledger/contrib/django_ledger_graphene/item/schema.py +0 -31
  124. django_ledger/contrib/django_ledger_graphene/journal_entry/mutations.py +0 -0
  125. django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +0 -35
  126. django_ledger/contrib/django_ledger_graphene/ledger/mutations.py +0 -0
  127. django_ledger/contrib/django_ledger_graphene/ledger/schema.py +0 -32
  128. django_ledger/contrib/django_ledger_graphene/purchase_order/mutations.py +0 -0
  129. django_ledger/contrib/django_ledger_graphene/purchase_order/schema.py +0 -31
  130. django_ledger/contrib/django_ledger_graphene/transaction/mutations.py +0 -0
  131. django_ledger/contrib/django_ledger_graphene/transaction/schema.py +0 -36
  132. django_ledger/contrib/django_ledger_graphene/unit/mutations.py +0 -0
  133. django_ledger/contrib/django_ledger_graphene/unit/schema.py +0 -27
  134. django_ledger/contrib/django_ledger_graphene/vendor/mutations.py +0 -0
  135. django_ledger/contrib/django_ledger_graphene/vendor/schema.py +0 -37
  136. django_ledger/contrib/django_ledger_graphene/views.py +0 -12
  137. django_ledger-0.7.11.dist-info/top_level.txt +0 -4
  138. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/AUTHORS.md +0 -0
  139. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/LICENSE +0 -0
@@ -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 datetime, date, timedelta
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, Set, Union, Tuple, Optional, Dict
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 ValidationError, ObjectDoesNotExist
102
- from django.db.models import Sum, QuerySet, F, DecimalField, When, Case
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 make_aware, is_naive, localtime, localdate
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 roles as roles_module, CREDIT, DEBIT
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
- JEActivityIOMiddleware,
118
+ AccountRoleIOMiddleware,
117
119
  BalanceSheetIOMiddleware,
120
+ CashFlowStatementIOMiddleware,
118
121
  IncomeStatementIOMiddleware,
119
- CashFlowStatementIOMiddleware
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 = (credits == debits)
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
- IS_TX_MODEL, is_valid, diff = diff_tx_data(tx_data, raise_exception=perform_correction)
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 not perform_correction and abs(diff) > settings.DJANGO_LEDGER_TRANSACTION_MAX_TOLERANCE:
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(tx for tx in tx_data if tx.tx_type == tx_type_choice)
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(tx for tx in tx_data if tx['tx_type'] == tx_type_choice)
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
- diff > 0 and tx_type_choice == DEBIT,
239
- diff < 0 and tx_type_choice == CREDIT
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
- diff < 0 and tx_type_choice == DEBIT,
248
- diff > 0 and tx_type_choice == CREDIT
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
- Fetches the current local date, optionally considering time zone settings.
301
+ Fetches the current local date, optionally considering time zone settings.
289
302
 
290
- This function retrieves the current local date. If the global settings indicate
291
- the use of time zones (`USE_TZ` is True), the date is determined based on the
292
- local time zone. Otherwise, the date is based on the system's local time without
293
- any time zone consideration.
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
- Returns
296
- -------
297
- date
298
- The current local date, adjusted for the time zone setting if applicable.
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
- dt: Union[str, date, datetime],
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, datetime.min.time(),
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
- from_date: Optional[Union[str, datetime, date]] = None,
392
- to_date: Optional[Union[str, datetime, date]] = None
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(f'{activity} is invalid. Choices are {JournalEntryModel.VALID_ACTIVITIES}.')
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
- self.ce_from_date is not None,
516
- self.ce_to_date is not None,
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 : NoneType or Type
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 : NoneType or Type
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=_(f'IODatabaseMixIn not compatible with {self.__class__.__name__} model.')
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(self,
638
- entity_slug: Optional[str] = None,
639
- unit_slug: Optional[str] = None,
640
- user_model: Optional[UserModel] = None,
641
- from_date: Optional[Union[date, datetime]] = None,
642
- to_date: Optional[Union[date, datetime]] = None,
643
- by_activity: bool = False,
644
- by_tx_type: bool = False,
645
- by_period: bool = False,
646
- by_unit: bool = False,
647
- activity: Optional[str] = None,
648
- role: str = Optional[str],
649
- accounts: Optional[Union[str, List[str], Set[str]]] = None,
650
- posted: bool = True,
651
- exclude_zero_bal: bool = True,
652
- use_closing_entries: bool = False,
653
- **kwargs) -> IOResult:
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
- use_closing_entries : bool
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('Inconsistent entity_slug. '
729
- f'Provided {entity_slug} does not match actual {self.slug}')
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
- user_model=user_model,
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
- user_model=user_model,
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
- user_model=user_model,
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
- entity_slug=entity_slug,
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 use_closing_entries is not None:
774
- USE_CLOSING_ENTRIES = 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(io_date=from_date, inclusive=False)
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(io_date=to_date)
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 = txs_queryset_from_closing_entry | txs_queryset_to_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(from_date=io_result.db_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 = txs_queryset.is_cleared() if cleared_filter else txs_queryset.not_cleared()
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
- f'Valid values are True, False'
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 = txs_queryset.is_reconciled() if reconciled_filter else txs_queryset.not_reconciled()
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
- f'Valid values are True, False'
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
- then=-F('amount')),
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 += ['journal_entry__entity_unit__uuid', 'journal_entry__entity_unit__name']
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 = txs_queryset.values(*VALUES).annotate(**ANNOTATE).order_by(*ORDER_BY)
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(self,
929
- user_model: Optional[UserModel] = None,
930
- entity_slug: Optional[str] = None,
931
- unit_slug: Optional[str] = None,
932
- to_date: Optional[Union[date, datetime, str]] = None,
933
- from_date: Optional[Union[date, datetime, str]] = None,
934
- equity_only: bool = False,
935
- activity: str = None,
936
- role: Optional[Union[Set[str], List[str]]] = None,
937
- accounts: Optional[Union[Set[str], List[str]]] = None,
938
- signs: bool = True,
939
- by_unit: bool = False,
940
- by_activity: bool = False,
941
- by_tx_type: bool = False,
942
- by_period: bool = False,
943
- use_closing_entries: bool = False,
944
- force_queryset_sorting: bool = False,
945
- **kwargs) -> IOResult:
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
- use_closing_entries : bool
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
- use_closing_entries=use_closing_entries,
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
- all([acc['role_bs'] == roles_module.BS_ASSET_ROLE,
1046
- acc['balance_type'] == CREDIT]),
1047
- all([acc['role_bs'] in (
1048
- roles_module.BS_LIABILITIES_ROLE,
1049
- roles_module.BS_EQUITY_ROLE
1050
- ),
1051
- acc['balance_type'] == DEBIT])
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(self,
1118
- entity_slug: Optional[str] = None,
1119
- unit_slug: Optional[str] = None,
1120
- to_date: Optional[Union[date, datetime, str]] = None,
1121
- from_date: Optional[Union[date, datetime, str]] = None,
1122
- user_model: Optional[UserModel] = None,
1123
- accounts: Optional[Union[Set[str], List[str]]] = None,
1124
- role: Optional[Union[Set[str], List[str]]] = None,
1125
- activity: Optional[str] = None,
1126
- signs: bool = True,
1127
- process_roles: bool = False,
1128
- process_groups: bool = False,
1129
- process_ratios: bool = False,
1130
- process_activity: bool = False,
1131
- equity_only: bool = False,
1132
- by_period: bool = False,
1133
- by_unit: bool = False,
1134
- by_activity: bool = False,
1135
- by_tx_type: bool = False,
1136
- balance_sheet_statement: bool = False,
1137
- income_statement: bool = False,
1138
- cash_flow_statement: bool = False,
1139
- use_closing_entry: Optional[bool] = None,
1140
- **kwargs) -> IODigestContextManager:
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
- process_groups,
1264
- balance_sheet_statement,
1265
- income_statement,
1266
- cash_flow_statement
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(io_data=io_state, by_unit=by_unit, by_period=by_period)
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(self,
1306
- je_timestamp: Union[str, datetime, date],
1307
- je_txs: List[Dict],
1308
- je_posted: bool = False,
1309
- je_ledger_model=None,
1310
- je_unit_model=None,
1311
- je_desc=None,
1312
- je_origin=None,
1313
- force_je_retrieval: bool = False,
1314
- **kwargs):
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
- # Validates that credits/debits balance.
1368
- check_tx_balance(je_txs, perform_correction=False)
1369
- je_timestamp = validate_io_timestamp(dt=je_timestamp)
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
- entity_model = self.get_entity_model_from_io()
1455
+ entity_model = self.get_entity_model_from_io()
1372
1456
 
1373
- if entity_model.last_closing_date:
1374
- if isinstance(je_timestamp, datetime):
1375
- if entity_model.last_closing_date >= je_timestamp.date():
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
- message=_(
1378
- f'Cannot commit transactions. The journal entry date {je_timestamp} is on a closed period.')
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
- elif isinstance(je_timestamp, date):
1381
- if entity_model.last_closing_date >= je_timestamp:
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'Cannot commit transactions. The journal entry date {je_timestamp} is on a closed period.')
1532
+ f'Unable to retrieve Journal Entry model with Timestamp {je_timestamp}'
1533
+ )
1385
1534
  )
1386
-
1387
- if self.is_ledger_model():
1388
- if self.is_locked():
1389
- raise IOValidationError(
1390
- message=_('Cannot commit on locked ledger')
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
- # if calling from EntityModel must pass an instance of LedgerModel...
1394
- if all([
1395
- isinstance(self, lazy_loader.get_entity_model()),
1396
- je_ledger_model is None
1397
- ]):
1398
- raise IOValidationError('Committing from EntityModel requires an instance of LedgerModel')
1399
-
1400
- # Validates that the provided LedgerModel id valid...
1401
- if all([
1402
- isinstance(self, lazy_loader.get_entity_model()),
1403
- je_ledger_model is not None,
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
- else:
1432
- je_model = JournalEntryModel(
1433
- ledger=je_ledger_model,
1434
- entity_unit=je_unit_model,
1435
- description=je_desc,
1436
- timestamp=je_timestamp,
1437
- origin=je_origin,
1438
- posted=False,
1439
- locked=False
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
- for tx, txm_kwargs in txs_models:
1456
- if not getattr(tx, 'ledger_id', None):
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
- txs_models = TransactionModel.objects.bulk_create(i[0] for i in txs_models)
1465
- je_model.save(verify=True, post_on_verify=je_posted)
1466
- return je_model, txs_models
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('ReportTuple',
1500
- field_names=[
1501
- 'balance_sheet_statement',
1502
- 'income_statement',
1503
- 'cash_flow_statement'
1504
- ])
1505
-
1506
- def digest_balance_sheet(self,
1507
- to_date: Union[date, datetime],
1508
- user_model: Optional[UserModel] = None,
1509
- txs_queryset: Optional[QuerySet] = None,
1510
- **kwargs: Dict) -> IODigestContextManager:
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(self,
1549
- to_date: Union[date, datetime],
1550
- subtitle: Optional[str] = None,
1551
- filepath: Optional[Path] = None,
1552
- filename: Optional[str] = None,
1553
- user_model: Optional[UserModel] = None,
1554
- save_pdf: bool = False,
1555
- **kwargs
1556
- ) -> IODigestContextManager:
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 = Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
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(self,
1629
- from_date: Union[date, datetime],
1630
- to_date: Union[date, datetime],
1631
- user_model: Optional[UserModel] = None,
1632
- txs_queryset: Optional[QuerySet] = None,
1633
- **kwargs) -> IODigestContextManager:
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(self,
1676
- from_date: Union[date, datetime],
1677
- to_date: Union[date, datetime],
1678
- subtitle: Optional[str] = None,
1679
- filepath: Optional[Path] = None,
1680
- filename: Optional[str] = None,
1681
- user_model: Optional[UserModel] = None,
1682
- txs_queryset: Optional[QuerySet] = None,
1683
- save_pdf: bool = False,
1684
- **kwargs
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 = Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
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(self,
1759
- from_date: Union[date, datetime],
1760
- to_date: Union[date, datetime],
1761
- user_model: Optional[UserModel] = None,
1762
- txs_queryset: Optional[QuerySet] = None,
1763
- **kwargs) -> IODigestContextManager:
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(self,
1801
- from_date: Union[date, datetime],
1802
- to_date: Union[date, datetime],
1803
- subtitle: Optional[str] = None,
1804
- filepath: Optional[Path] = None,
1805
- filename: Optional[str] = None,
1806
- user_model: Optional[UserModel] = None,
1807
- save_pdf: bool = False,
1808
- **kwargs):
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 = Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
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(self,
1876
- from_date: Union[date, datetime],
1877
- to_date: Union[date, datetime],
1878
- user_model: Optional[UserModel] = None,
1879
- **kwargs) -> IODigestContextManager:
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(self,
1926
- from_date: Union[date, datetime],
1927
- to_date: Union[date, datetime],
1928
- dt_strfmt: str = '%Y%m%d',
1929
- user_model: Optional[UserModel] = None,
1930
- save_pdf: bool = False,
1931
- filepath: Optional[Path] = None,
1932
- **kwargs) -> ReportTuple:
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 = Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
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(base_dir.joinpath(bs_report.get_pdf_filename(dt_strfmt=dt_strfmt)))
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(base_dir.joinpath(is_report.get_pdf_filename(from_dt=from_date, dt_strfmt=dt_strfmt)))
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(base_dir.joinpath(cfs_report.get_pdf_filename(from_dt=from_date, dt_strfmt=dt_strfmt)))
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.