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.
- django_ledger/__init__.py +1 -1
- django_ledger/admin/coa.py +3 -3
- django_ledger/forms/account.py +2 -0
- django_ledger/forms/coa.py +1 -6
- django_ledger/forms/transactions.py +3 -1
- django_ledger/io/io_core.py +95 -79
- django_ledger/io/io_digest.py +4 -5
- django_ledger/io/io_generator.py +5 -4
- django_ledger/io/io_library.py +241 -16
- django_ledger/io/roles.py +32 -10
- django_ledger/migrations/0015_remove_chartofaccountmodel_locked_and_more.py +22 -0
- django_ledger/models/accounts.py +13 -9
- django_ledger/models/bill.py +3 -3
- django_ledger/models/closing_entry.py +39 -28
- django_ledger/models/coa.py +244 -35
- django_ledger/models/entity.py +119 -51
- django_ledger/models/invoice.py +3 -2
- django_ledger/models/journal_entry.py +8 -4
- django_ledger/models/ledger.py +63 -11
- django_ledger/models/mixins.py +2 -2
- django_ledger/models/transactions.py +20 -11
- django_ledger/report/balance_sheet.py +1 -1
- django_ledger/report/cash_flow_statement.py +5 -5
- django_ledger/report/core.py +2 -2
- django_ledger/report/income_statement.py +2 -2
- django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
- django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +10 -11
- django_ledger/templates/django_ledger/account/account_create.html +17 -11
- django_ledger/templates/django_ledger/account/account_list.html +12 -9
- django_ledger/templates/django_ledger/account/tags/account_txs_table.html +6 -1
- django_ledger/templates/django_ledger/account/tags/accounts_table.html +97 -93
- django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html +17 -0
- django_ledger/templates/django_ledger/{code_of_accounts → chart_of_accounts}/coa_update.html +1 -4
- django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html +74 -0
- django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +1 -1
- django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +6 -6
- django_ledger/templates/django_ledger/includes/widget_ic.html +1 -1
- django_ledger/templates/django_ledger/invoice/invoice_list.html +91 -94
- django_ledger/templates/django_ledger/journal_entry/includes/card_journal_entry.html +16 -7
- django_ledger/templates/django_ledger/journal_entry/je_detail.html +1 -1
- django_ledger/templates/django_ledger/ledger/tags/ledgers_table.html +10 -0
- django_ledger/templatetags/django_ledger.py +9 -8
- django_ledger/tests/base.py +134 -8
- django_ledger/tests/test_accounts.py +16 -0
- django_ledger/tests/test_auth.py +5 -7
- django_ledger/tests/test_bill.py +1 -1
- django_ledger/tests/test_chart_of_accounts.py +46 -0
- django_ledger/tests/test_closing_entry.py +16 -19
- django_ledger/tests/test_entity.py +3 -3
- django_ledger/tests/test_io.py +192 -2
- django_ledger/tests/test_transactions.py +290 -0
- django_ledger/urls/account.py +18 -3
- django_ledger/urls/chart_of_accounts.py +21 -1
- django_ledger/urls/ledger.py +7 -0
- django_ledger/views/account.py +29 -4
- django_ledger/views/coa.py +79 -4
- django_ledger/views/djl_api.py +4 -1
- django_ledger/views/journal_entry.py +1 -1
- django_ledger/views/mixins.py +3 -0
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/METADATA +1 -1
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/RECORD +65 -59
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/AUTHORS.md +0 -0
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/LICENSE +0 -0
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/WHEEL +0 -0
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/top_level.txt +0 -0
django_ledger/models/entity.py
CHANGED
|
@@ -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
|
-
|
|
1093
|
+
root_account_qs = coa_accounts_qs.is_coa_root()
|
|
1075
1094
|
|
|
1076
1095
|
root_maps = {
|
|
1077
|
-
|
|
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.
|
|
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(
|
|
1104
|
-
|
|
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(
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
-
|
|
1422
|
+
return coa_model.create_account(
|
|
1409
1423
|
code=code,
|
|
1410
|
-
name=name,
|
|
1411
1424
|
role=role,
|
|
1412
|
-
|
|
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(
|
|
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(
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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.
|
django_ledger/models/invoice.py
CHANGED
|
@@ -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
|
|
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
|
|
1142
|
-
|
|
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.
|
|
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():
|
django_ledger/models/ledger.py
CHANGED
|
@@ -106,7 +106,7 @@ class LedgerModelQuerySet(models.QuerySet):
|
|
|
106
106
|
|
|
107
107
|
def current(self):
|
|
108
108
|
return self.filter(
|
|
109
|
-
Q(
|
|
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:
|
django_ledger/models/mixins.py
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
|
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
|
|
414
|
-
The
|
|
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.
|
|
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.
|
|
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.
|
|
201
|
-
f'through {self.IO_DIGEST.
|
|
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.
|
|
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.
|
|
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
|
|
django_ledger/report/core.py
CHANGED
|
@@ -105,8 +105,8 @@ class BaseReportSupport(*load_support()):
|
|
|
105
105
|
style='I'
|
|
106
106
|
)
|
|
107
107
|
|
|
108
|
-
from_date = self.IO_DIGEST.
|
|
109
|
-
to_date = self.IO_DIGEST.
|
|
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.
|
|
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.
|
|
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
|