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.

Files changed (56) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/forms/account.py +45 -46
  3. django_ledger/forms/data_import.py +182 -64
  4. django_ledger/io/io_core.py +507 -374
  5. django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
  6. django_ledger/models/__init__.py +2 -1
  7. django_ledger/models/bill.py +337 -300
  8. django_ledger/models/customer.py +47 -34
  9. django_ledger/models/data_import.py +770 -289
  10. django_ledger/models/entity.py +882 -637
  11. django_ledger/models/mixins.py +421 -282
  12. django_ledger/models/receipt.py +1083 -0
  13. django_ledger/models/transactions.py +105 -41
  14. django_ledger/models/unit.py +42 -30
  15. django_ledger/models/utils.py +12 -2
  16. django_ledger/models/vendor.py +85 -66
  17. django_ledger/settings.py +1 -0
  18. django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
  19. django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +1 -13
  20. django_ledger/templates/django_ledger/bills/bill_update.html +1 -1
  21. django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
  22. django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
  23. django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
  24. django_ledger/templates/django_ledger/customer/tags/customer_table.html +3 -1
  25. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
  26. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
  27. django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
  28. django_ledger/templates/django_ledger/invoice/invoice_update.html +1 -1
  29. django_ledger/templates/django_ledger/layouts/base.html +3 -1
  30. django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
  31. django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
  32. django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
  33. django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
  34. django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
  35. django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
  36. django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +3 -2
  37. django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
  38. django_ledger/templatetags/django_ledger.py +338 -191
  39. django_ledger/urls/__init__.py +1 -0
  40. django_ledger/urls/customer.py +3 -0
  41. django_ledger/urls/data_import.py +3 -0
  42. django_ledger/urls/receipt.py +102 -0
  43. django_ledger/urls/vendor.py +1 -0
  44. django_ledger/views/__init__.py +1 -0
  45. django_ledger/views/customer.py +56 -14
  46. django_ledger/views/data_import.py +119 -66
  47. django_ledger/views/mixins.py +112 -86
  48. django_ledger/views/receipt.py +294 -0
  49. django_ledger/views/vendor.py +53 -14
  50. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/METADATA +1 -1
  51. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/RECORD +55 -45
  52. django_ledger/static/django_ledger/bundle/styles.bundle.js +0 -1
  53. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/WHEEL +0 -0
  54. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/licenses/AUTHORS.md +0 -0
  55. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/licenses/LICENSE +0 -0
  56. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/top_level.txt +0 -0
@@ -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 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
@@ -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 = (credits == debits)
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
- 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
+ )
217
221
 
218
222
  if not perform_correction and abs(diff):
219
223
  return False
220
224
 
221
- 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
+ ):
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(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
+ )
229
238
  else:
230
- 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
+ )
231
242
 
232
243
  if len(txs_candidates) > 0:
233
-
234
244
  tx = choice(txs_candidates)
235
245
 
236
- if any([
237
- diff > 0 and tx_type_choice == DEBIT,
238
- diff < 0 and tx_type_choice == CREDIT
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
- diff < 0 and tx_type_choice == DEBIT,
247
- diff > 0 and tx_type_choice == CREDIT
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
- Fetches the current local date, optionally considering time zone settings.
301
+ Fetches the current local date, optionally considering time zone settings.
288
302
 
289
- This function retrieves the current local date. If the global settings indicate
290
- the use of time zones (`USE_TZ` is True), the date is determined based on the
291
- local time zone. Otherwise, the date is based on the system's local time without
292
- 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.
293
307
 
294
- Returns
295
- -------
296
- date
297
- 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.
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
- dt: Union[str, date, datetime],
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, datetime.min.time(),
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
- from_date: Optional[Union[str, datetime, date]] = None,
391
- 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,
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(f'{activity} is invalid. Choices are {JournalEntryModel.VALID_ACTIVITIES}.')
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
- self.ce_from_date is not None,
515
- self.ce_to_date is not None,
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 : NoneType or Type
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 : NoneType or Type
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=_(f'IODatabaseMixIn not compatible with {self.__class__.__name__} model.')
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(self,
637
- entity_slug: Optional[str] = None,
638
- unit_slug: Optional[str] = None,
639
- user_model: Optional[UserModel] = None,
640
- from_date: Optional[Union[date, datetime]] = None,
641
- to_date: Optional[Union[date, datetime]] = None,
642
- by_activity: bool = False,
643
- by_tx_type: bool = False,
644
- by_period: bool = False,
645
- by_unit: bool = False,
646
- activity: Optional[str] = None,
647
- role: str = Optional[str],
648
- accounts: Optional[Union[str, List[str], Set[str]]] = None,
649
- posted: bool = True,
650
- exclude_zero_bal: bool = True,
651
- use_closing_entries: bool = False,
652
- **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:
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
- use_closing_entries : bool
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('Inconsistent entity_slug. '
728
- 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
+ )
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 use_closing_entries is not None:
769
- USE_CLOSING_ENTRIES = 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(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
+ )
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(io_date=to_date)
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 = 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
+ )
828
880
 
829
881
  if io_result.db_from_date:
830
- 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
+ )
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 = 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
+ )
861
919
  else:
862
920
  raise IOValidationError(
863
921
  message=f'Invalid value for cleared filter: {cleared_filter}. '
864
- f'Valid values are True, False'
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 = 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
+ )
872
934
  else:
873
935
  raise IOValidationError(
874
936
  message=f'Invalid value for reconciled filter: {reconciled_filter}. '
875
- f'Valid values are True, False'
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
- then=-F('amount')),
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 += ['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
+ ]
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 = 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
+ )
925
993
  return io_result
926
994
 
927
- def python_digest(self,
928
- user_model: Optional[UserModel] = None,
929
- entity_slug: Optional[str] = None,
930
- unit_slug: Optional[str] = None,
931
- to_date: Optional[Union[date, datetime, str]] = None,
932
- from_date: Optional[Union[date, datetime, str]] = None,
933
- equity_only: bool = False,
934
- activity: str = None,
935
- role: Optional[Union[Set[str], List[str]]] = None,
936
- accounts: Optional[Union[Set[str], List[str]]] = None,
937
- signs: bool = True,
938
- by_unit: bool = False,
939
- by_activity: bool = False,
940
- by_tx_type: bool = False,
941
- by_period: bool = False,
942
- use_closing_entries: bool = False,
943
- force_queryset_sorting: bool = False,
944
- **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:
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
- use_closing_entries : bool
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
- use_closing_entries=use_closing_entries,
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
- all([acc['role_bs'] == roles_module.BS_ASSET_ROLE,
1045
- acc['balance_type'] == CREDIT]),
1046
- all([acc['role_bs'] in (
1047
- roles_module.BS_LIABILITIES_ROLE,
1048
- roles_module.BS_EQUITY_ROLE
1049
- ),
1050
- acc['balance_type'] == DEBIT])
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(self,
1117
- entity_slug: Optional[str] = None,
1118
- unit_slug: Optional[str] = None,
1119
- to_date: Optional[Union[date, datetime, str]] = None,
1120
- from_date: Optional[Union[date, datetime, str]] = None,
1121
- user_model: Optional[UserModel] = None,
1122
- accounts: Optional[Union[Set[str], List[str]]] = None,
1123
- role: Optional[Union[Set[str], List[str]]] = None,
1124
- activity: Optional[str] = None,
1125
- signs: bool = True,
1126
- process_roles: bool = False,
1127
- process_groups: bool = False,
1128
- process_ratios: bool = False,
1129
- process_activity: bool = False,
1130
- equity_only: bool = False,
1131
- by_period: bool = False,
1132
- by_unit: bool = False,
1133
- by_activity: bool = False,
1134
- by_tx_type: bool = False,
1135
- balance_sheet_statement: bool = False,
1136
- income_statement: bool = False,
1137
- cash_flow_statement: bool = False,
1138
- use_closing_entry: Optional[bool] = None,
1139
- **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:
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
- process_groups,
1263
- balance_sheet_statement,
1264
- income_statement,
1265
- cash_flow_statement
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(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
+ )
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(self,
1305
- je_timestamp: Union[str, datetime, date],
1306
- je_txs: List[Dict],
1307
- je_posted: bool = False,
1308
- je_ledger_model=None,
1309
- je_unit_model=None,
1310
- je_desc=None,
1311
- je_origin=None,
1312
- force_je_retrieval: bool = False,
1313
- **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
+ ):
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
- # Validates that credits/debits balance.
1367
- check_tx_balance(je_txs, perform_correction=False)
1368
- je_timestamp = validate_io_timestamp(dt=je_timestamp)
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
- entity_model = self.get_entity_model_from_io()
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
- if entity_model.last_closing_date:
1373
- if isinstance(je_timestamp, datetime):
1374
- if entity_model.last_closing_date >= je_timestamp.date():
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
- message=_(
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
- elif isinstance(je_timestamp, date):
1380
- 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:
1381
1530
  raise IOValidationError(
1382
1531
  message=_(
1383
- 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
+ )
1384
1534
  )
1385
-
1386
- if self.is_ledger_model():
1387
- if self.is_locked():
1388
- raise IOValidationError(
1389
- 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,
1390
1544
  )
1391
-
1392
- # if calling from EntityModel must pass an instance of LedgerModel...
1393
- if all([
1394
- isinstance(self, lazy_loader.get_entity_model()),
1395
- je_ledger_model is None
1396
- ]):
1397
- raise IOValidationError('Committing from EntityModel requires an instance of LedgerModel')
1398
-
1399
- # Validates that the provided LedgerModel id valid...
1400
- if all([
1401
- isinstance(self, lazy_loader.get_entity_model()),
1402
- je_ledger_model is not None,
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
- else:
1431
- je_model = JournalEntryModel(
1432
- ledger=je_ledger_model,
1433
- entity_unit=je_unit_model,
1434
- description=je_desc,
1435
- timestamp=je_timestamp,
1436
- origin=je_origin,
1437
- posted=False,
1438
- locked=False
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
- for tx, txm_kwargs in txs_models:
1455
- if not getattr(tx, 'ledger_id', None):
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
- txs_models = TransactionModel.objects.bulk_create(i[0] for i in txs_models)
1464
- je_model.save(verify=True, post_on_verify=je_posted)
1465
- 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
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('ReportTuple',
1499
- field_names=[
1500
- 'balance_sheet_statement',
1501
- 'income_statement',
1502
- 'cash_flow_statement'
1503
- ])
1504
-
1505
- def digest_balance_sheet(self,
1506
- to_date: Union[date, datetime],
1507
- user_model: Optional[UserModel] = None,
1508
- txs_queryset: Optional[QuerySet] = None,
1509
- **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:
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(self,
1548
- to_date: Union[date, datetime],
1549
- subtitle: Optional[str] = None,
1550
- filepath: Optional[Path] = None,
1551
- filename: Optional[str] = None,
1552
- user_model: Optional[UserModel] = None,
1553
- save_pdf: bool = False,
1554
- **kwargs
1555
- ) -> 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:
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 = 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
+ )
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(self,
1624
- from_date: Union[date, datetime],
1625
- to_date: Union[date, datetime],
1626
- user_model: Optional[UserModel] = None,
1627
- txs_queryset: Optional[QuerySet] = None,
1628
- **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:
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(self,
1671
- from_date: Union[date, datetime],
1672
- to_date: Union[date, datetime],
1673
- subtitle: Optional[str] = None,
1674
- filepath: Optional[Path] = None,
1675
- filename: Optional[str] = None,
1676
- user_model: Optional[UserModel] = None,
1677
- txs_queryset: Optional[QuerySet] = None,
1678
- save_pdf: bool = False,
1679
- **kwargs
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 = 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
+ )
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(self,
1750
- from_date: Union[date, datetime],
1751
- to_date: Union[date, datetime],
1752
- user_model: Optional[UserModel] = None,
1753
- txs_queryset: Optional[QuerySet] = None,
1754
- **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:
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(self,
1792
- from_date: Union[date, datetime],
1793
- to_date: Union[date, datetime],
1794
- subtitle: Optional[str] = None,
1795
- filepath: Optional[Path] = None,
1796
- filename: Optional[str] = None,
1797
- user_model: Optional[UserModel] = None,
1798
- save_pdf: bool = False,
1799
- **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
+ ):
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 = 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
+ )
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(self,
1863
- from_date: Union[date, datetime],
1864
- to_date: Union[date, datetime],
1865
- user_model: Optional[UserModel] = None,
1866
- **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:
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(self,
1913
- from_date: Union[date, datetime],
1914
- to_date: Union[date, datetime],
1915
- dt_strfmt: str = '%Y%m%d',
1916
- user_model: Optional[UserModel] = None,
1917
- save_pdf: bool = False,
1918
- filepath: Optional[Path] = None,
1919
- **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:
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 = 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
+ )
1989
2115
  bs_report.create_pdf_report()
1990
- 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
+ )
1991
2119
 
1992
2120
  is_report.create_pdf_report()
1993
- 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
+ )
1994
2126
 
1995
2127
  cfs_report.create_pdf_report()
1996
- 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
+ )
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.