django-ledger 0.5.6.2__py3-none-any.whl → 0.5.6.4__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 (65) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/admin/coa.py +3 -3
  3. django_ledger/forms/account.py +2 -0
  4. django_ledger/forms/coa.py +1 -6
  5. django_ledger/forms/transactions.py +3 -1
  6. django_ledger/io/io_core.py +95 -79
  7. django_ledger/io/io_digest.py +4 -5
  8. django_ledger/io/io_generator.py +5 -4
  9. django_ledger/io/io_library.py +241 -16
  10. django_ledger/io/roles.py +32 -10
  11. django_ledger/migrations/0015_remove_chartofaccountmodel_locked_and_more.py +22 -0
  12. django_ledger/models/accounts.py +13 -9
  13. django_ledger/models/bill.py +3 -3
  14. django_ledger/models/closing_entry.py +39 -28
  15. django_ledger/models/coa.py +244 -35
  16. django_ledger/models/entity.py +119 -51
  17. django_ledger/models/invoice.py +3 -2
  18. django_ledger/models/journal_entry.py +8 -4
  19. django_ledger/models/ledger.py +63 -11
  20. django_ledger/models/mixins.py +2 -2
  21. django_ledger/models/transactions.py +20 -11
  22. django_ledger/report/balance_sheet.py +1 -1
  23. django_ledger/report/cash_flow_statement.py +5 -5
  24. django_ledger/report/core.py +2 -2
  25. django_ledger/report/income_statement.py +2 -2
  26. django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
  27. django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +10 -11
  28. django_ledger/templates/django_ledger/account/account_create.html +17 -11
  29. django_ledger/templates/django_ledger/account/account_list.html +12 -9
  30. django_ledger/templates/django_ledger/account/tags/account_txs_table.html +6 -1
  31. django_ledger/templates/django_ledger/account/tags/accounts_table.html +97 -93
  32. django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html +17 -0
  33. django_ledger/templates/django_ledger/{code_of_accounts → chart_of_accounts}/coa_update.html +1 -4
  34. django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html +74 -0
  35. django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +1 -1
  36. django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +6 -6
  37. django_ledger/templates/django_ledger/includes/widget_ic.html +1 -1
  38. django_ledger/templates/django_ledger/invoice/invoice_list.html +91 -94
  39. django_ledger/templates/django_ledger/journal_entry/includes/card_journal_entry.html +16 -7
  40. django_ledger/templates/django_ledger/journal_entry/je_detail.html +1 -1
  41. django_ledger/templates/django_ledger/ledger/tags/ledgers_table.html +10 -0
  42. django_ledger/templatetags/django_ledger.py +9 -8
  43. django_ledger/tests/base.py +134 -8
  44. django_ledger/tests/test_accounts.py +16 -0
  45. django_ledger/tests/test_auth.py +5 -7
  46. django_ledger/tests/test_bill.py +1 -1
  47. django_ledger/tests/test_chart_of_accounts.py +46 -0
  48. django_ledger/tests/test_closing_entry.py +16 -19
  49. django_ledger/tests/test_entity.py +3 -3
  50. django_ledger/tests/test_io.py +192 -2
  51. django_ledger/tests/test_transactions.py +290 -0
  52. django_ledger/urls/account.py +18 -3
  53. django_ledger/urls/chart_of_accounts.py +21 -1
  54. django_ledger/urls/ledger.py +7 -0
  55. django_ledger/views/account.py +29 -4
  56. django_ledger/views/coa.py +79 -4
  57. django_ledger/views/djl_api.py +4 -1
  58. django_ledger/views/journal_entry.py +1 -1
  59. django_ledger/views/mixins.py +3 -0
  60. {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/METADATA +1 -1
  61. {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/RECORD +65 -59
  62. {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/AUTHORS.md +0 -0
  63. {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/LICENSE +0 -0
  64. {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/WHEEL +0 -0
  65. {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/top_level.txt +0 -0
@@ -795,6 +795,10 @@ class EntityModelAbstract(MP_Node,
795
795
  def __str__(self):
796
796
  return f'EntityModel {self.slug}: {self.name}'
797
797
 
798
+ def __init__(self, *args, **kwargs):
799
+ super().__init__(*args, **kwargs)
800
+ self._CLOSING_ENTRY_DATES: Optional[List[date]] = None
801
+
798
802
  # ## Logging ###
799
803
  def get_logger_name(self):
800
804
  return f'EntityModel {self.uuid}'
@@ -986,6 +990,21 @@ class EntityModelAbstract(MP_Node,
986
990
  raise EntityModelValidationError(f'EntityModel {self.slug} does not have a default CoA')
987
991
  return self.default_coa
988
992
 
993
+ def set_default_coa(self, coa_model: Optional[Union[ChartOfAccountModel, str]], commit: bool = False):
994
+
995
+ # if str, will look up CoA Model by slug...
996
+ if isinstance(coa_model, str):
997
+ coa_model = self.chartofaccountmodel_set.get(slug=coa_model)
998
+ else:
999
+ self.validate_chart_of_accounts_for_entity(coa_model)
1000
+
1001
+ self.default_coa = coa_model
1002
+ if commit:
1003
+ self.save(update_fields=[
1004
+ 'default_coa',
1005
+ 'updated'
1006
+ ])
1007
+
989
1008
  def create_chart_of_accounts(self,
990
1009
  assign_as_default: bool = False,
991
1010
  coa_name: Optional[str] = None,
@@ -1071,10 +1090,10 @@ class EntityModelAbstract(MP_Node,
1071
1090
  coa_has_accounts = coa_accounts_qs.not_coa_root().exists()
1072
1091
 
1073
1092
  if not coa_has_accounts or force:
1074
- root_accounts = coa_accounts_qs.is_coa_root()
1093
+ root_account_qs = coa_accounts_qs.is_coa_root()
1075
1094
 
1076
1095
  root_maps = {
1077
- root_accounts.get(role__exact=k): [
1096
+ root_account_qs.get(role__exact=k): [
1078
1097
  AccountModel(
1079
1098
  code=a['code'],
1080
1099
  name=a['name'],
@@ -1096,12 +1115,14 @@ class EntityModelAbstract(MP_Node,
1096
1115
  pass
1097
1116
 
1098
1117
  account_model.clean()
1099
- coa_model.create_account(account_model)
1118
+ coa_model.allocate_account(account_model, root_account_qs=root_account_qs)
1100
1119
 
1101
1120
  else:
1102
1121
  if not ignore_if_default_coa:
1103
- raise EntityModelValidationError(f'Entity {self.name} already has existing accounts. '
1104
- 'Use force=True to bypass this check')
1122
+ raise EntityModelValidationError(
1123
+ f'Entity {self.name} already has existing accounts. '
1124
+ 'Use force=True to bypass this check'
1125
+ )
1105
1126
 
1106
1127
  # Model Validators....
1107
1128
  def validate_chart_of_accounts_for_entity(self,
@@ -1250,19 +1271,12 @@ class EntityModelAbstract(MP_Node,
1250
1271
  """
1251
1272
 
1252
1273
  if not coa_model:
1253
- account_model_qs = self.default_coa.accountmodel_set.all().select_related('coa_model', 'coa_model__entity')
1274
+ account_model_qs = self.default_coa.accountmodel_set.all().select_related(
1275
+ 'coa_model', 'coa_model__entity').not_coa_root()
1254
1276
  else:
1255
- account_model_qs = AccountModel.objects.filter(
1256
- coa_model__entity__uuid__exact=self.uuid
1257
- ).select_related('coa_model', 'coa_model__entity')
1258
-
1259
- if isinstance(coa_model, ChartOfAccountModel):
1260
- self.validate_chart_of_accounts_for_entity(coa_model=coa_model, raise_exception=True)
1261
- account_model_qs = coa_model.accountmodel_set.all()
1262
- if isinstance(coa_model, str):
1263
- account_model_qs = account_model_qs.filter(coa_model__slug__exact=coa_model)
1264
- elif isinstance(coa_model, UUID):
1265
- account_model_qs = account_model_qs.filter(coa_model__uuid__exact=coa_model)
1277
+ self.validate_chart_of_accounts_for_entity(coa_model=coa_model)
1278
+ account_model_qs = coa_model.accountmodel_set.select_related(
1279
+ 'coa_model', 'coa_model__entity').not_coa_root()
1266
1280
 
1267
1281
  if active:
1268
1282
  account_model_qs = account_model_qs.active()
@@ -1363,7 +1377,7 @@ class EntityModelAbstract(MP_Node,
1363
1377
  role: str,
1364
1378
  name: str,
1365
1379
  balance_type: str,
1366
- active: bool,
1380
+ active: bool = False,
1367
1381
  coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
1368
1382
  raise_exception: bool = True) -> AccountModel:
1369
1383
  """
@@ -1405,17 +1419,14 @@ class EntityModelAbstract(MP_Node,
1405
1419
  else:
1406
1420
  coa_model = self.default_coa
1407
1421
 
1408
- account_model = AccountModel(
1422
+ return coa_model.create_account(
1409
1423
  code=code,
1410
- name=name,
1411
1424
  role=role,
1412
- active=active,
1413
- balance_type=balance_type
1425
+ name=name,
1426
+ balance_type=balance_type,
1427
+ active=active
1414
1428
  )
1415
1429
 
1416
- account_model.clean()
1417
- return coa_model.create_account(account_model=account_model)
1418
-
1419
1430
  def create_account_by_kwargs(self,
1420
1431
  account_model_kwargs: Dict,
1421
1432
  coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None,
@@ -1453,9 +1464,29 @@ class EntityModelAbstract(MP_Node,
1453
1464
  else:
1454
1465
  coa_model = self.default_coa
1455
1466
 
1456
- account_model = AccountModel(**account_model_kwargs)
1457
- account_model.clean()
1458
- return coa_model, coa_model.create_account(account_model=account_model)
1467
+ # account_model = AccountModel(**account_model_kwargs)
1468
+ # account_model.clean()
1469
+ return coa_model, coa_model.create_account(**account_model_kwargs)
1470
+
1471
+ # ### LEDGER MANAGEMENT ####
1472
+ def get_ledgers(self, posted: bool = True):
1473
+ return self.ledgermodel_set.filter(posted=posted)
1474
+
1475
+ # ### JOURNAL ENTRY MANAGEMENT ####
1476
+ def get_journal_entries(self, ledger_model: LedgerModel, posted: bool = True):
1477
+
1478
+ if ledger_model:
1479
+ self.validate_ledger_model_for_entity(ledger_model)
1480
+ qs = ledger_model.journal_entries.all()
1481
+ if posted:
1482
+ return qs.posted()
1483
+ return qs
1484
+
1485
+ JournalEntryModel = lazy_loader.get_journal_entry_model()
1486
+ qs = JournalEntryModel.objects.for_entity(entity_slug=self)
1487
+ if posted:
1488
+ return qs.posted()
1489
+ return qs
1459
1490
 
1460
1491
  # ### VENDOR MANAGEMENT ####
1461
1492
  def get_vendors(self, active: bool = True) -> VendorModelQuerySet:
@@ -2658,11 +2689,12 @@ class EntityModelAbstract(MP_Node,
2658
2689
 
2659
2690
  self.meta[self.META_KEY_CLOSING_ENTRY_DATES] = [d.isoformat() for d in date_list]
2660
2691
  if commit:
2661
- self.save(update_fields=[
2662
- 'last_closing_date',
2663
- 'updated',
2664
- 'meta'
2665
- ])
2692
+ self.save(
2693
+ update_fields=[
2694
+ 'last_closing_date',
2695
+ 'updated',
2696
+ 'meta'
2697
+ ])
2666
2698
  return date_list
2667
2699
 
2668
2700
  def fetch_closing_entry_dates_meta(self, as_date: bool = True) -> List[date]:
@@ -2670,25 +2702,43 @@ class EntityModelAbstract(MP_Node,
2670
2702
  return list()
2671
2703
  date_list = self.meta[self.META_KEY_CLOSING_ENTRY_DATES]
2672
2704
  if as_date:
2673
- return [date.fromisoformat(dt) for dt in date_list]
2705
+ if self._CLOSING_ENTRY_DATES is None:
2706
+ self._CLOSING_ENTRY_DATES = [date.fromisoformat(dt) for dt in date_list]
2707
+ return self._CLOSING_ENTRY_DATES
2674
2708
  return date_list
2675
2709
 
2676
- def get_closing_entry_for_date(self, io_date: date, inclusive: bool = True) -> Optional[date]:
2710
+ def get_closing_entry_for_date(self, io_date: Union[date, datetime], inclusive: bool = True) -> Optional[date]:
2677
2711
  if io_date is None:
2678
2712
  return
2679
2713
  ce_date_list = self.fetch_closing_entry_dates_meta()
2714
+
2715
+ if not ce_date_list:
2716
+ return
2717
+
2718
+ if isinstance(io_date, datetime):
2719
+ io_date = io_date.date()
2720
+
2680
2721
  ce_lookup = io_date - timedelta(days=1) if not inclusive else io_date
2681
2722
  if ce_lookup in ce_date_list:
2682
2723
  return ce_lookup
2683
2724
 
2684
- def get_nearest_next_closing_entry(self, io_date: date) -> Optional[date]:
2725
+ def get_nearest_next_closing_entry(self, io_date: Union[date, datetime]) -> Optional[date]:
2685
2726
  if io_date is None:
2686
2727
  return
2728
+
2687
2729
  ce_date_list = self.fetch_closing_entry_dates_meta()
2688
2730
  if not len(ce_date_list):
2689
2731
  return
2732
+
2733
+ if all([
2734
+ isinstance(io_date, date),
2735
+ isinstance(io_date, datetime),
2736
+ ]):
2737
+ io_date = io_date.date()
2738
+
2690
2739
  if io_date > ce_date_list[0]:
2691
2740
  return ce_date_list[0]
2741
+
2692
2742
  for f, p in zip_longest(ce_date_list, ce_date_list[1:]):
2693
2743
  if p and p <= io_date < f:
2694
2744
  return p
@@ -2697,7 +2747,7 @@ class EntityModelAbstract(MP_Node,
2697
2747
  closing_date: Optional[date] = None,
2698
2748
  closing_entry_model=None,
2699
2749
  force_update: bool = False,
2700
- commit: bool = True):
2750
+ post_closing_entry: bool = True):
2701
2751
 
2702
2752
  if closing_entry_model and closing_date:
2703
2753
  raise EntityModelValidationError(
@@ -2709,13 +2759,18 @@ class EntityModelAbstract(MP_Node,
2709
2759
  )
2710
2760
 
2711
2761
  closing_entry_exists = False
2762
+
2712
2763
  if closing_entry_model:
2713
2764
  closing_date = closing_entry_model.closing_date
2714
2765
  self.validate_closing_entry_model(closing_entry_model, closing_date=closing_date)
2715
2766
  closing_entry_exists = True
2716
2767
  else:
2717
2768
  try:
2718
- closing_entry_model = self.closingentrymodel_set.defer('markdown_notes').get(
2769
+ closing_entry_model = self.closingentrymodel_set.select_related(
2770
+ 'ledger_model',
2771
+ 'ledger_model__entity'
2772
+ ).defer(
2773
+ 'markdown_notes').get(
2719
2774
  closing_date__exact=closing_date
2720
2775
  )
2721
2776
 
@@ -2724,29 +2779,34 @@ class EntityModelAbstract(MP_Node,
2724
2779
  pass
2725
2780
 
2726
2781
  if force_update or not closing_entry_exists or closing_entry_model:
2727
- ce_model, ce_txs = self.create_closing_entry_for_date(
2782
+ closing_entry_model, ce_txs = self.create_closing_entry_for_date(
2728
2783
  closing_date=closing_date,
2729
2784
  closing_entry_model=closing_entry_model,
2730
- closing_entry_exists=closing_entry_exists
2785
+ closing_entry_exists=closing_entry_exists,
2731
2786
  )
2732
2787
 
2733
- if commit:
2734
- self.save(update_fields=[
2735
- 'last_closing_date',
2736
- 'meta',
2737
- 'updated'
2738
- ])
2739
- return ce_model, ce_txs
2788
+ if post_closing_entry:
2789
+ closing_entry_model.mark_as_posted(commit=True)
2790
+ self.save_closing_entry_dates_meta(commit=True)
2791
+
2792
+ return closing_entry_model, ce_txs
2740
2793
  raise EntityModelValidationError(message=f'Closing Entry for Period {closing_date} already exists.')
2741
2794
 
2742
- def close_books_for_month(self, year: int, month: int, force_update: bool = False, commit: bool = True):
2795
+ def close_books_for_month(self, year: int, month: int, force_update: bool = False, post_closing_entry: bool = True):
2743
2796
  _, day = monthrange(year, month)
2744
2797
  closing_dt = date(year, month, day)
2745
- return self.close_entity_books(closing_date=closing_dt, force_update=force_update, commit=commit)
2798
+ return self.close_entity_books(
2799
+ closing_date=closing_dt,
2800
+ force_update=force_update,
2801
+ post_closing_entry=post_closing_entry,
2802
+ closing_entry_model=None
2803
+ )
2746
2804
 
2747
- def close_books_for_fiscal_year(self, fiscal_year: int, force_update: bool = False, commit: bool = True):
2805
+ def close_books_for_fiscal_year(self, fiscal_year: int, force_update: bool = False,
2806
+ post_closing_entry: bool = True):
2748
2807
  closing_dt = self.get_fy_end(year=fiscal_year)
2749
- return self.close_entity_books(closing_date=closing_dt, force_update=force_update, commit=commit)
2808
+ return self.close_entity_books(closing_date=closing_dt, force_update=force_update,
2809
+ post_closing_entry=post_closing_entry)
2750
2810
 
2751
2811
  # ### RANDOM DATA GENERATION ####
2752
2812
 
@@ -2909,6 +2969,14 @@ class EntityModelAbstract(MP_Node,
2909
2969
  'entity_slug': self.slug
2910
2970
  })
2911
2971
 
2972
+ def get_coa_list_url(self) -> str:
2973
+ return reverse(
2974
+ viewname='django_ledger:coa-list',
2975
+ kwargs={
2976
+ 'entity_slug': self.slug
2977
+ }
2978
+ )
2979
+
2912
2980
  def get_accounts_url(self) -> str:
2913
2981
  """
2914
2982
  The EntityModel Code of Accounts llist import URL.
@@ -441,13 +441,14 @@ class InvoiceModelAbstract(
441
441
  ledger_model.clean_fields()
442
442
  self.ledger = ledger_model
443
443
 
444
+ if commit_ledger or commit:
445
+ self.ledger.save()
446
+
444
447
  if self.can_generate_invoice_number():
445
448
  self.generate_invoice_number(commit=commit)
446
449
 
447
450
  self.clean()
448
451
 
449
- if commit_ledger or commit:
450
- self.ledger.save()
451
452
 
452
453
  if commit:
453
454
  self.save()
@@ -440,7 +440,7 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
440
440
  not self.is_posted(),
441
441
  ])
442
442
 
443
- def can_edit_timestamp(self) -> bool:
443
+ def can_edit(self) -> bool:
444
444
  return not self.is_locked()
445
445
 
446
446
  def is_posted(self):
@@ -1138,8 +1138,12 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
1138
1138
 
1139
1139
  if not self.timestamp:
1140
1140
  self.timestamp = get_localtime()
1141
- elif self.timestamp and self.timestamp > get_localtime():
1142
- raise JournalEntryValidationError(message='Cannot create JE Models with timestamp in the future.')
1141
+ elif all([
1142
+ self.timestamp,
1143
+ self.timestamp > get_localtime(),
1144
+ self.is_posted()
1145
+ ]):
1146
+ raise JournalEntryValidationError(message='Cannot Post JE Models with timestamp in the future.')
1143
1147
 
1144
1148
  self.generate_je_number(commit=True)
1145
1149
  if verify:
@@ -1148,7 +1152,7 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
1148
1152
  return TransactionModel.objects.none(), self.is_verified()
1149
1153
 
1150
1154
  def get_delete_message(self) -> str:
1151
- return _(f'Are you sure you want to delete JournalEntry Model {self.description} on Ledger {self.ledger.name}?')
1155
+ return _(f'Are you sure you want to delete JournalEntry Model {self.je_number} on Ledger {self.ledger.name}?')
1152
1156
 
1153
1157
  def delete(self, **kwargs):
1154
1158
  if not self.can_delete():
@@ -106,7 +106,7 @@ class LedgerModelQuerySet(models.QuerySet):
106
106
 
107
107
  def current(self):
108
108
  return self.filter(
109
- Q(earliest_timestamp__gt=F('entity__last_closing_date'))
109
+ Q(earliest_timestamp__date__gt=F('entity__last_closing_date'))
110
110
  | Q(earliest_timestamp__isnull=True)
111
111
  )
112
112
 
@@ -392,6 +392,28 @@ class LedgerModelAbstract(CreateUpdateMixIn, IOMixIn):
392
392
  self.is_posted()
393
393
  ])
394
394
 
395
+ def can_hide(self) -> bool:
396
+ """
397
+ Determines if the LedgerModel can be hidden.
398
+
399
+ Returns
400
+ -------
401
+ bool
402
+ True if can be hidden, else False.
403
+ """
404
+ return self.hidden is False
405
+
406
+ def can_unhide(self) -> bool:
407
+ """
408
+ Determines if the LedgerModel can be un-hidden.
409
+
410
+ Returns
411
+ -------
412
+ bool
413
+ True if can be un-hidden, else False.
414
+ """
415
+ return self.hidden is True
416
+
395
417
  def can_delete(self) -> bool:
396
418
  if all([
397
419
  not self.is_locked(),
@@ -448,7 +470,7 @@ class LedgerModelAbstract(CreateUpdateMixIn, IOMixIn):
448
470
  raise_exception:bool
449
471
  Raises LedgerModelValidationError if un-posting not allowed.
450
472
  """
451
- if self.can_unpost():
473
+ if not self.can_unpost():
452
474
  if raise_exception:
453
475
  raise LedgerModelValidationError(
454
476
  message=_(f'Ledger {self.uuid} cannot be unposted.')
@@ -511,22 +533,52 @@ class LedgerModelAbstract(CreateUpdateMixIn, IOMixIn):
511
533
  'updated'
512
534
  ])
513
535
 
536
+ def hide(self, commit: bool = False, raise_exception: bool = True, **kwargs):
537
+ if not self.can_hide():
538
+ if raise_exception:
539
+ raise LedgerModelValidationError(
540
+ message=_(f'Ledger {self.name} cannot be hidden. UUID: {self.uuid}')
541
+ )
542
+ return
543
+ self.hidden = True
544
+ if commit:
545
+ self.save(update_fields=[
546
+ 'hidden',
547
+ 'updated'
548
+ ])
549
+
550
+ def unhide(self, commit: bool = False, raise_exception: bool = True, **kwargs):
551
+ if not self.can_unhide():
552
+ if raise_exception:
553
+ raise LedgerModelValidationError(
554
+ message=_(f'Ledger {self.name} cannot be un-hidden. UUID: {self.uuid}')
555
+ )
556
+ return
557
+ self.hidden = False
558
+ if commit:
559
+ self.save(update_fields=[
560
+ 'hidden',
561
+ 'updated'
562
+ ])
563
+
514
564
  def delete(self, **kwargs):
515
565
  if not self.can_delete():
516
566
  raise LedgerModelValidationError(
517
567
  message=_(f'LedgerModel {self.name} cannot be deleted because posted is {self.is_posted()} '
518
568
  f'and locked is {self.is_locked()}')
519
569
  )
520
- earliest_je_timestamp = self.journal_entries.posted().order_by('-timestamp').values('timestamp').first()
521
- if earliest_je_timestamp is not None:
522
- earliest_date = earliest_je_timestamp['timestamp'].date()
523
- if earliest_date <= self.entity.last_closing_date:
524
- raise LedgerModelValidationError(
525
- message=_(
526
- f'Journal Entries with date {earliest_date} cannot be deleted because of lastest closing '
527
- f'entry on {self.get_entity_last_closing_date()}')
528
- )
529
570
 
571
+ # checks if ledger model has journal entries in a closed period...
572
+ if self.entity.has_closing_entry():
573
+ earliest_je_timestamp = self.journal_entries.posted().order_by('-timestamp').values('timestamp').first()
574
+ if earliest_je_timestamp is not None:
575
+ earliest_date = earliest_je_timestamp['timestamp'].date()
576
+ if earliest_date <= self.entity.last_closing_date:
577
+ raise LedgerModelValidationError(
578
+ message=_(
579
+ f'Journal Entries with date {earliest_date} cannot be deleted because of latest closing '
580
+ f'entry on {self.get_entity_last_closing_date()}')
581
+ )
530
582
  return super().delete(**kwargs)
531
583
 
532
584
  def get_entity_name(self) -> str:
@@ -26,7 +26,7 @@ from django.utils.encoding import force_str
26
26
  from django.utils.translation import gettext_lazy as _
27
27
  from markdown import markdown
28
28
 
29
- from django_ledger.io.io_core import validate_io_date, check_tx_balance, get_localtime, get_localdate
29
+ from django_ledger.io.io_core import validate_io_timestamp, check_tx_balance, get_localtime, get_localdate
30
30
  from django_ledger.models.utils import lazy_loader
31
31
 
32
32
  logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
@@ -748,7 +748,7 @@ class AccrualMixIn(models.Model):
748
748
  unit_uuids = list(set(k[1] for k in idx_keys))
749
749
 
750
750
  if je_timestamp:
751
- je_timestamp = validate_io_date(dt=je_timestamp)
751
+ je_timestamp = validate_io_timestamp(dt=je_timestamp)
752
752
 
753
753
  now_timestamp = get_localtime() if not je_timestamp else je_timestamp
754
754
  je_list = {
@@ -24,9 +24,10 @@ from django.core.exceptions import ValidationError
24
24
  from django.core.validators import MinValueValidator
25
25
  from django.db import models
26
26
  from django.db.models import Q, QuerySet
27
+ from django.db.models.signals import pre_save
27
28
  from django.utils.translation import gettext_lazy as _
28
29
 
29
- from django_ledger.io.io_core import validate_io_date
30
+ from django_ledger.io.io_core import validate_io_timestamp
30
31
  from django_ledger.models.accounts import AccountModel
31
32
  from django_ledger.models.bill import BillModel
32
33
  from django_ledger.models.entity import EntityModel
@@ -156,7 +157,7 @@ class TransactionModelQuerySet(QuerySet):
156
157
  """
157
158
 
158
159
  if isinstance(to_date, str):
159
- to_date = validate_io_date(to_date)
160
+ to_date = validate_io_timestamp(to_date)
160
161
 
161
162
  if isinstance(to_date, date):
162
163
  return self.filter(journal_entry__timestamp__date__lte=to_date)
@@ -180,7 +181,7 @@ class TransactionModelQuerySet(QuerySet):
180
181
  Returns a TransactionModelQuerySet with applied filters.
181
182
  """
182
183
  if isinstance(from_date, str):
183
- from_date = validate_io_date(from_date)
184
+ from_date = validate_io_timestamp(from_date)
184
185
 
185
186
  if isinstance(from_date, date):
186
187
  return self.filter(journal_entry__timestamp__date__gte=from_date)
@@ -219,7 +220,7 @@ class TransactionModelAdmin(models.Manager):
219
220
  TransactionModelQuerySet
220
221
  Returns a TransactionModelQuerySet with applied filters.
221
222
  """
222
- qs = self.get_queryset()
223
+ qs = self.get_queryset().select_related('journal_entry')
223
224
  if user_model.is_superuser:
224
225
  return qs
225
226
  return qs.filter(
@@ -284,10 +285,9 @@ class TransactionModelAdmin(models.Manager):
284
285
  Returns a TransactionModelQuerySet with applied filters.
285
286
  """
286
287
  qs = self.for_entity(user_model=user_model, entity_slug=entity_slug)
287
- if isinstance(ledger_model, LedgerModel):
288
- return qs.filter(journal_entry__ledger=ledger_model)
289
- elif isinstance(ledger_model, str) or isinstance(ledger_model, UUID):
288
+ if isinstance(ledger_model, UUID):
290
289
  return qs.filter(journal_entry__ledger__uuid__exact=ledger_model)
290
+ return qs.filter(journal_entry__ledger=ledger_model)
291
291
 
292
292
  def for_unit(self,
293
293
  entity_slug: Union[EntityModel, str],
@@ -410,8 +410,8 @@ class TransactionModelAdmin(models.Manager):
410
410
 
411
411
  class TransactionModelAbstract(CreateUpdateMixIn):
412
412
  """
413
- This is the main abstract class which the BillModel database will inherit from.
414
- The BillModel inherits functionality from the following MixIns:
413
+ This is the main abstract class which the TransactionModel database will inherit from.
414
+ The TransactionModel inherits functionality from the following MixIns:
415
415
 
416
416
  1. :func:`CreateUpdateMixIn <django_ledger.models.mixins.CreateUpdateMixIn>`
417
417
 
@@ -479,7 +479,6 @@ class TransactionModelAbstract(CreateUpdateMixIn):
479
479
  ]
480
480
 
481
481
  def __str__(self):
482
- # pylint: disable=no-member
483
482
  return '{x1}-{x2}/{x5}: {x3}/{x4}'.format(x1=self.account.code,
484
483
  x2=self.account.name,
485
484
  x3=self.amount,
@@ -487,7 +486,7 @@ class TransactionModelAbstract(CreateUpdateMixIn):
487
486
  x5=self.account.balance_type)
488
487
 
489
488
  def clean(self):
490
- if self.account.is_root_account():
489
+ if self.account_id and self.account.is_root_account():
491
490
  raise TransactionModelValidationError(
492
491
  message=_('Cannot transact on root accounts')
493
492
  )
@@ -497,3 +496,13 @@ class TransactionModel(TransactionModelAbstract):
497
496
  """
498
497
  Base Transaction Model From Abstract.
499
498
  """
499
+
500
+
501
+ def transactionmodel_presave(instance: TransactionModel, **kwargs):
502
+ if instance.journal_entry_id and instance.journal_entry.is_locked():
503
+ raise TransactionModelValidationError(
504
+ message=_('Cannot modify transactions on locked journal entries')
505
+ )
506
+
507
+
508
+ pre_save.connect(transactionmodel_presave, sender=TransactionModel)
@@ -233,7 +233,7 @@ class BalanceSheetReport(BaseReportSupport):
233
233
  if to_dt:
234
234
  to_dt = to_dt.strftime(dt_strfmt if dt_strfmt else self.IO_DIGEST.get_strftime_format())
235
235
  else:
236
- to_dt = self.IO_DIGEST.get_to_date(fmt=dt_strfmt, as_str=True)
236
+ to_dt = self.IO_DIGEST.get_to_datetime(fmt=dt_strfmt, as_str=True)
237
237
  f_name = f'{self.get_report_title()}_BalanceSheetStatement_{to_dt}.pdf'
238
238
  return f_name
239
239
 
@@ -54,7 +54,7 @@ class CashFlowStatementReport(BaseReportSupport):
54
54
  def print_starting_net_income(self):
55
55
  net_income = self.IO_DIGEST.IO_DATA['cash_flow_statement']['operating']['GROUP_CFS_NET_INCOME']['balance']
56
56
  self.print_section_title(
57
- title=f'Net Income as of {self.IO_DIGEST.get_from_date(as_str=True)}',
57
+ title=f'Net Income as of {self.IO_DIGEST.get_from_datetime(as_str=True)}',
58
58
  style='B',
59
59
  w=170)
60
60
  self.print_amount(amt=net_income, zoom=2)
@@ -197,8 +197,8 @@ class CashFlowStatementReport(BaseReportSupport):
197
197
 
198
198
  self.set_x(50)
199
199
  self.print_section_title(
200
- title=f'Net Cash Flow from {self.IO_DIGEST.get_from_date(as_str=True)} '
201
- f'through {self.IO_DIGEST.get_to_date(as_str=True)}',
200
+ title=f'Net Cash Flow from {self.IO_DIGEST.get_from_datetime(as_str=True)} '
201
+ f'through {self.IO_DIGEST.get_to_datetime(as_str=True)}',
202
202
  style='',
203
203
  zoom=0,
204
204
  align='R',
@@ -216,12 +216,12 @@ class CashFlowStatementReport(BaseReportSupport):
216
216
  if from_dt:
217
217
  from_dt = from_dt.strftime(dt_strfmt if dt_strfmt else self.IO_DIGEST.get_strftime_format())
218
218
  else:
219
- from_dt = self.IO_DIGEST.get_from_date(fmt=dt_strfmt, as_str=True)
219
+ from_dt = self.IO_DIGEST.get_from_datetime(fmt=dt_strfmt, as_str=True)
220
220
 
221
221
  if to_dt:
222
222
  to_dt = to_dt.strftime(dt_strfmt if dt_strfmt else self.IO_DIGEST.get_strftime_format())
223
223
  else:
224
- to_dt = self.IO_DIGEST.get_to_date(fmt=dt_strfmt, as_str=True)
224
+ to_dt = self.IO_DIGEST.get_to_datetime(fmt=dt_strfmt, as_str=True)
225
225
  f_name = f'{self.get_report_title()}_CashFlowStatement_{from_dt}_{to_dt}.pdf'
226
226
  return f_name
227
227
 
@@ -105,8 +105,8 @@ class BaseReportSupport(*load_support()):
105
105
  style='I'
106
106
  )
107
107
 
108
- from_date = self.IO_DIGEST.get_from_date(as_str=True)
109
- to_date = self.IO_DIGEST.get_to_date(as_str=True)
108
+ from_date = self.IO_DIGEST.get_from_datetime(as_str=True)
109
+ to_date = self.IO_DIGEST.get_to_datetime(as_str=True)
110
110
 
111
111
  if from_date and to_date:
112
112
  period = f'From {from_date} through {to_date}'
@@ -297,12 +297,12 @@ class IncomeStatementReport(BaseReportSupport):
297
297
  if from_dt:
298
298
  from_dt = from_dt.strftime(dt_strfmt if dt_strfmt else self.IO_DIGEST.get_strftime_format())
299
299
  else:
300
- from_dt = self.IO_DIGEST.get_from_date(fmt=dt_strfmt, as_str=True)
300
+ from_dt = self.IO_DIGEST.get_from_datetime(fmt=dt_strfmt, as_str=True)
301
301
 
302
302
  if to_dt:
303
303
  to_dt = to_dt.strftime(dt_strfmt if dt_strfmt else self.IO_DIGEST.get_strftime_format())
304
304
  else:
305
- to_dt = self.IO_DIGEST.get_to_date(fmt=dt_strfmt, as_str=True)
305
+ to_dt = self.IO_DIGEST.get_to_datetime(fmt=dt_strfmt, as_str=True)
306
306
 
307
307
  f_name = f'{self.get_report_title()}_IncomeStatement_{from_dt}_{to_dt}.pdf'
308
308
  return f_name