django-ledger 0.7.0__py3-none-any.whl → 0.7.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 (50) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/admin/__init__.py +1 -1
  3. django_ledger/admin/{coa.py → chart_of_accounts.py} +1 -1
  4. django_ledger/admin/entity.py +1 -1
  5. django_ledger/forms/account.py +1 -1
  6. django_ledger/forms/bank_account.py +2 -0
  7. django_ledger/forms/chart_of_accounts.py +82 -0
  8. django_ledger/io/io_library.py +1 -1
  9. django_ledger/migrations/0018_transactionmodel_cleared_transactionmodel_reconciled_and_more.py +37 -0
  10. django_ledger/models/__init__.py +1 -1
  11. django_ledger/models/accounts.py +4 -0
  12. django_ledger/models/bank_account.py +6 -2
  13. django_ledger/models/bill.py +7 -3
  14. django_ledger/models/{coa.py → chart_of_accounts.py} +131 -27
  15. django_ledger/models/closing_entry.py +5 -7
  16. django_ledger/models/coa_default.py +1 -1
  17. django_ledger/models/customer.py +6 -2
  18. django_ledger/models/data_import.py +33 -8
  19. django_ledger/models/entity.py +27 -3
  20. django_ledger/models/estimate.py +6 -1
  21. django_ledger/models/invoice.py +14 -8
  22. django_ledger/models/items.py +19 -8
  23. django_ledger/models/journal_entry.py +71 -25
  24. django_ledger/models/ledger.py +8 -5
  25. django_ledger/models/purchase_order.py +9 -5
  26. django_ledger/models/transactions.py +23 -3
  27. django_ledger/models/unit.py +4 -0
  28. django_ledger/models/vendor.py +4 -0
  29. django_ledger/settings.py +28 -3
  30. django_ledger/templates/django_ledger/account/tags/accounts_table.html +3 -2
  31. django_ledger/templates/django_ledger/chart_of_accounts/coa_create.html +25 -0
  32. django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html +25 -6
  33. django_ledger/templates/django_ledger/chart_of_accounts/coa_update.html +2 -2
  34. django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html +10 -4
  35. django_ledger/templates/django_ledger/data_import/tags/data_import_job_list_table.html +8 -0
  36. django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +2 -2
  37. django_ledger/templates/django_ledger/includes/footer.html +2 -2
  38. django_ledger/urls/chart_of_accounts.py +6 -0
  39. django_ledger/utils.py +1 -36
  40. django_ledger/views/__init__.py +1 -1
  41. django_ledger/views/account.py +16 -3
  42. django_ledger/views/{coa.py → chart_of_accounts.py} +48 -44
  43. django_ledger/views/mixins.py +16 -5
  44. {django_ledger-0.7.0.dist-info → django_ledger-0.7.2.dist-info}/METADATA +1 -3
  45. {django_ledger-0.7.0.dist-info → django_ledger-0.7.2.dist-info}/RECORD +49 -47
  46. django_ledger/forms/coa.py +0 -47
  47. {django_ledger-0.7.0.dist-info → django_ledger-0.7.2.dist-info}/AUTHORS.md +0 -0
  48. {django_ledger-0.7.0.dist-info → django_ledger-0.7.2.dist-info}/LICENSE +0 -0
  49. {django_ledger-0.7.0.dist-info → django_ledger-0.7.2.dist-info}/WHEEL +0 -0
  50. {django_ledger-0.7.0.dist-info → django_ledger-0.7.2.dist-info}/top_level.txt +0 -0
@@ -9,31 +9,48 @@ from uuid import uuid4
9
9
 
10
10
  from django.core.exceptions import ValidationError
11
11
  from django.db import models
12
- from django.db.models import Q, Count, Sum, Case, When, F, Value, DecimalField, BooleanField
12
+ from django.db.models import Q, Count, Sum, Case, When, F, Value, DecimalField, BooleanField, Manager, QuerySet
13
13
  from django.db.models.functions import Coalesce
14
14
  from django.db.models.signals import pre_save
15
15
  from django.utils.translation import gettext_lazy as _
16
16
 
17
17
  from django_ledger.io import ASSET_CA_CASH, CREDIT, DEBIT
18
+ from django_ledger.models import JournalEntryModel
18
19
  from django_ledger.models.mixins import CreateUpdateMixIn
19
20
  from django_ledger.models.utils import lazy_loader
20
21
 
21
- from django_ledger.models import JournalEntryModel
22
-
23
22
 
24
23
  class ImportJobModelValidationError(ValidationError):
25
24
  pass
26
25
 
27
26
 
28
- class ImportJobModelQuerySet(models.QuerySet):
27
+ class ImportJobModelQuerySet(QuerySet):
29
28
  pass
30
29
 
31
30
 
32
- class ImportJobModelManager(models.Manager):
31
+ class ImportJobModelManager(Manager):
33
32
 
34
33
  def get_queryset(self):
35
34
  qs = super().get_queryset()
36
- return qs.select_related(
35
+ return qs.annotate(
36
+ txs_count=Count('stagedtransactionmodel',
37
+ filter=Q(stagedtransactionmodel__parent__isnull=False)),
38
+ txs_mapped_count=Count(
39
+ 'stagedtransactionmodel__account_model_id',
40
+ filter=Q(stagedtransactionmodel__parent__isnull=False) | Q(
41
+ stagedtransactionmodel__parent__parent__isnull=False)
42
+
43
+ ),
44
+ ).annotate(
45
+ txs_pending=F('txs_count') - F('txs_mapped_count')
46
+ ).annotate(
47
+ is_complete=Case(
48
+ When(txs_count__exact=0, then=False),
49
+ When(txs_pending__exact=0, then=True),
50
+ default=False,
51
+ output_field=BooleanField()
52
+ ),
53
+ ).select_related(
37
54
  'bank_account_model',
38
55
  'bank_account_model__cash_account',
39
56
  'ledger_model'
@@ -102,7 +119,7 @@ class ImportJobModelAbstract(CreateUpdateMixIn):
102
119
  return _(f'Are you sure you want to delete Import Job {self.description}?')
103
120
 
104
121
 
105
- class StagedTransactionModelQuerySet(models.QuerySet):
122
+ class StagedTransactionModelQuerySet(QuerySet):
106
123
 
107
124
  def is_pending(self):
108
125
  return self.filter(transaction_model__isnull=True)
@@ -117,7 +134,7 @@ class StagedTransactionModelQuerySet(models.QuerySet):
117
134
  return self.filter(ready_to_import=True)
118
135
 
119
136
 
120
- class StagedTransactionModelManager(models.Manager):
137
+ class StagedTransactionModelManager(Manager):
121
138
 
122
139
  def get_queryset(self):
123
140
  qs = super().get_queryset()
@@ -502,6 +519,10 @@ class ImportJobModel(ImportJobModelAbstract):
502
519
  Transaction Import Job Model Base Class.
503
520
  """
504
521
 
522
+ class Meta(ImportJobModelAbstract.Meta):
523
+ swappable = 'DJANGO_LEDGER_IMPORT_JOB_MODEL'
524
+ abstract = False
525
+
505
526
 
506
527
  def importjobmodel_presave(instance: ImportJobModel, **kwargs):
507
528
  if instance.is_configured():
@@ -518,3 +539,7 @@ class StagedTransactionModel(StagedTransactionModelAbstract):
518
539
  """
519
540
  Staged Transaction Model Base Class.
520
541
  """
542
+
543
+ class Meta(StagedTransactionModelAbstract.Meta):
544
+ swappable = 'DJANGO_LEDGER_STAGED_TRANSACTION_MODEL'
545
+ abstract = False
@@ -33,7 +33,7 @@ from django.core.cache import caches
33
33
  from django.core.exceptions import ValidationError, ObjectDoesNotExist
34
34
  from django.core.validators import MinValueValidator
35
35
  from django.db import models
36
- from django.db.models import Q, F
36
+ from django.db.models import Q, F, Model
37
37
  from django.db.models.signals import pre_save
38
38
  from django.urls import reverse
39
39
  from django.utils.text import slugify
@@ -44,7 +44,7 @@ from django_ledger.io import roles as roles_module, validate_roles, IODigestCont
44
44
  from django_ledger.io.io_core import IOMixIn, get_localtime, get_localdate
45
45
  from django_ledger.models.accounts import AccountModel, AccountModelQuerySet, DEBIT, CREDIT
46
46
  from django_ledger.models.bank_account import BankAccountModelQuerySet, BankAccountModel
47
- from django_ledger.models.coa import ChartOfAccountModel, ChartOfAccountModelQuerySet
47
+ from django_ledger.models.chart_of_accounts import ChartOfAccountModel, ChartOfAccountModelQuerySet
48
48
  from django_ledger.models.coa_default import CHART_OF_ACCOUNTS_ROOT_MAP
49
49
  from django_ledger.models.customer import CustomerModelQueryset, CustomerModel
50
50
  from django_ledger.models.items import (ItemModelQuerySet, ItemTransactionModelQuerySet,
@@ -3038,6 +3038,22 @@ class EntityModelAbstract(MP_Node,
3038
3038
  }
3039
3039
  )
3040
3040
 
3041
+ def get_coa_list_inactive_url(self) -> str:
3042
+ return reverse(
3043
+ viewname='django_ledger:coa-list-inactive',
3044
+ kwargs={
3045
+ 'entity_slug': self.slug
3046
+ }
3047
+ )
3048
+
3049
+ def get_coa_create_url(self) -> str:
3050
+ return reverse(
3051
+ viewname='django_ledger:coa-create',
3052
+ kwargs={
3053
+ 'entity_slug': self.slug
3054
+ }
3055
+ )
3056
+
3041
3057
  def get_accounts_url(self) -> str:
3042
3058
  """
3043
3059
  The EntityModel Code of Accounts llist import URL.
@@ -3104,10 +3120,14 @@ class EntityModel(EntityModelAbstract):
3104
3120
  """
3105
3121
  Entity Model Base Class From Abstract
3106
3122
  """
3123
+ class Meta(EntityModelAbstract.Meta):
3124
+ swappable = 'DJANGO_LEDGER_ENTITY_MODEL'
3125
+ abstract = False
3126
+
3107
3127
 
3108
3128
 
3109
3129
  # ## ENTITY STATE....
3110
- class EntityStateModelAbstract(models.Model):
3130
+ class EntityStateModelAbstract(Model):
3111
3131
  KEY_JOURNAL_ENTRY = 'je'
3112
3132
  KEY_PURCHASE_ORDER = 'po'
3113
3133
  KEY_BILL = 'bill'
@@ -3168,6 +3188,10 @@ class EntityStateModel(EntityStateModelAbstract):
3168
3188
  Entity State Model Base Class from Abstract.
3169
3189
  """
3170
3190
 
3191
+ class Meta(EntityStateModelAbstract.Meta):
3192
+ swappable = 'DJANGO_LEDGER_ENTITY_STATE_MODEL'
3193
+ abstract = False
3194
+
3171
3195
 
3172
3196
  # ## ENTITY MANAGEMENT.....
3173
3197
  class EntityManagementModelAbstract(CreateUpdateMixIn):
@@ -1231,7 +1231,8 @@ class EstimateModelAbstract(CreateUpdateMixIn,
1231
1231
  'updated'
1232
1232
  ])
1233
1233
 
1234
- def update_state(self, itemtxs_qs: Optional[Union[ItemTransactionModelQuerySet, List[ItemTransactionModel]]] = None):
1234
+ def update_state(self,
1235
+ itemtxs_qs: Optional[Union[ItemTransactionModelQuerySet, List[ItemTransactionModel]]] = None):
1235
1236
  itemtxs_qs, _ = self.get_itemtxs_data(queryset=itemtxs_qs)
1236
1237
  self.update_cost_estimate(itemtxs_qs)
1237
1238
  self.update_revenue_estimate(itemtxs_qs)
@@ -1611,3 +1612,7 @@ class EstimateModel(EstimateModelAbstract):
1611
1612
  """
1612
1613
  Base EstimateModel Class.
1613
1614
  """
1615
+
1616
+ class Meta(EstimateModelAbstract.Meta):
1617
+ swappable = 'DJANGO_LEDGER_ESTIMATE_MODEL'
1618
+ abstract = False
@@ -33,14 +33,16 @@ from django.utils.translation import gettext_lazy as _
33
33
 
34
34
  from django_ledger.io import ASSET_CA_CASH, ASSET_CA_RECEIVABLES, LIABILITY_CL_DEFERRED_REVENUE
35
35
  from django_ledger.io.io_core import get_localtime, get_localdate
36
- from django_ledger.models import lazy_loader, ItemTransactionModelQuerySet, ItemModelQuerySet, ItemModel
36
+ from django_ledger.models import (
37
+ lazy_loader, ItemTransactionModelQuerySet,
38
+ ItemModelQuerySet, ItemModel, QuerySet, Manager
39
+ )
37
40
  from django_ledger.models.entity import EntityModel
38
41
  from django_ledger.models.mixins import (
39
42
  CreateUpdateMixIn, AccrualMixIn,
40
43
  MarkdownNotesMixIn, PaymentTermsMixIn,
41
44
  ItemizeMixIn
42
45
  )
43
-
44
46
  from django_ledger.models.signals import (
45
47
  invoice_status_draft,
46
48
  invoice_status_in_review,
@@ -49,7 +51,6 @@ from django_ledger.models.signals import (
49
51
  invoice_status_canceled,
50
52
  invoice_status_void
51
53
  )
52
-
53
54
  from django_ledger.settings import DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_INVOICE_NUMBER_PREFIX
54
55
 
55
56
  UserModel = get_user_model()
@@ -59,7 +60,7 @@ class InvoiceModelValidationError(ValidationError):
59
60
  pass
60
61
 
61
62
 
62
- class InvoiceModelQuerySet(models.QuerySet):
63
+ class InvoiceModelQuerySet(QuerySet):
63
64
  """
64
65
  A custom defined QuerySet for the InvoiceModel.
65
66
  This implements multiple methods or queries that we need to run to get a status of Invoices raised by the entity.
@@ -176,7 +177,7 @@ class InvoiceModelQuerySet(models.QuerySet):
176
177
  return self.filter(invoice_status__exact=InvoiceModel.INVOICE_STATUS_APPROVED)
177
178
 
178
179
 
179
- class InvoiceModelManager(models.Manager):
180
+ class InvoiceModelManager(Manager):
180
181
  """
181
182
  A custom defined InvoiceModel Manager that will act as an interface to handling the DB queries to the InvoiceModel.
182
183
  The default "get_queryset" has been overridden to refer the custom defined "InvoiceModelQuerySet"
@@ -572,9 +573,10 @@ class InvoiceModelAbstract(
572
573
  else:
573
574
  self.validate_itemtxs_qs(queryset)
574
575
 
575
- return queryset.select_related('item_model').order_by('item_model__earnings_account__uuid',
576
- 'entity_unit__uuid',
577
- 'item_model__earnings_account__balance_type').values(
576
+ return queryset.select_related('item_model').order_by(
577
+ 'item_model__earnings_account__uuid',
578
+ 'entity_unit__uuid',
579
+ 'item_model__earnings_account__balance_type').values(
578
580
  'item_model__earnings_account__uuid',
579
581
  'item_model__earnings_account__balance_type',
580
582
  'item_model__cogs_account__uuid',
@@ -1817,6 +1819,10 @@ class InvoiceModel(InvoiceModelAbstract):
1817
1819
  Base Invoice Model from Abstract.
1818
1820
  """
1819
1821
 
1822
+ class Meta(InvoiceModelAbstract.Meta):
1823
+ swappable = 'DJANGO_LEDGER_INVOICE_MODEL'
1824
+ abstract = False
1825
+
1820
1826
 
1821
1827
  def invoicemodel_presave(instance: InvoiceModel, **kwargs):
1822
1828
  if instance.can_generate_invoice_number():
@@ -25,7 +25,7 @@ from uuid import uuid4, UUID
25
25
  from django.core.exceptions import ValidationError, ObjectDoesNotExist
26
26
  from django.core.validators import MinValueValidator
27
27
  from django.db import models, transaction, IntegrityError
28
- from django.db.models import Q, Sum, F, ExpressionWrapper, DecimalField, Value, Case, When, QuerySet
28
+ from django.db.models import Q, Sum, F, ExpressionWrapper, DecimalField, Value, Case, When, QuerySet, Manager
29
29
  from django.db.models.functions import Coalesce
30
30
  from django.utils.translation import gettext_lazy as _
31
31
 
@@ -42,12 +42,12 @@ class ItemModelValidationError(ValidationError):
42
42
  pass
43
43
 
44
44
 
45
- class UnitOfMeasureModelQuerySet(models.QuerySet):
45
+ class UnitOfMeasureModelQuerySet(QuerySet):
46
46
  pass
47
47
 
48
48
 
49
49
  # UNIT OF MEASURES MODEL....
50
- class UnitOfMeasureModelManager(models.Manager):
50
+ class UnitOfMeasureModelManager(Manager):
51
51
  """
52
52
  A custom defined QuerySet Manager for the UnitOfMeasureModel.
53
53
  """
@@ -149,7 +149,7 @@ class UnitOfMeasureModelAbstract(CreateUpdateMixIn):
149
149
 
150
150
 
151
151
  # ITEM MODEL....
152
- class ItemModelQuerySet(models.QuerySet):
152
+ class ItemModelQuerySet(QuerySet):
153
153
  """
154
154
  A custom-defined ItemModelQuerySet that implements custom QuerySet methods related to the ItemModel.
155
155
  """
@@ -287,7 +287,7 @@ class ItemModelQuerySet(models.QuerySet):
287
287
  return self.inventory_all()
288
288
 
289
289
 
290
- class ItemModelManager(models.Manager):
290
+ class ItemModelManager(Manager):
291
291
  """
292
292
  A custom defined ItemModelManager that implement custom QuerySet methods related to the ItemModel
293
293
  """
@@ -849,7 +849,7 @@ class ItemModelAbstract(CreateUpdateMixIn):
849
849
 
850
850
 
851
851
  # ITEM TRANSACTION MODELS...
852
- class ItemTransactionModelQuerySet(models.QuerySet):
852
+ class ItemTransactionModelQuerySet(QuerySet):
853
853
 
854
854
  def is_received(self):
855
855
  return self.filter(po_item_status=ItemTransactionModel.STATUS_RECEIVED)
@@ -875,7 +875,7 @@ class ItemTransactionModelQuerySet(models.QuerySet):
875
875
  }
876
876
 
877
877
 
878
- class ItemTransactionModelManager(models.Manager):
878
+ class ItemTransactionModelManager(Manager):
879
879
 
880
880
  def for_user(self, user_model):
881
881
  qs = self.get_queryset()
@@ -1404,20 +1404,31 @@ class ItemTransactionModelAbstract(CreateUpdateMixIn):
1404
1404
 
1405
1405
 
1406
1406
  # FINAL MODEL CLASSES....
1407
-
1408
1407
  class UnitOfMeasureModel(UnitOfMeasureModelAbstract):
1409
1408
  """
1410
1409
  Base UnitOfMeasureModel from Abstract.
1411
1410
  """
1412
1411
 
1412
+ class Meta(UnitOfMeasureModelAbstract.Meta):
1413
+ abstract = False
1414
+ swappable = 'DJANGO_LEDGER_UNIT_OF_MEASURE_MODEL'
1415
+
1413
1416
 
1414
1417
  class ItemTransactionModel(ItemTransactionModelAbstract):
1415
1418
  """
1416
1419
  Base ItemTransactionModel from Abstract.
1417
1420
  """
1418
1421
 
1422
+ class Meta(ItemTransactionModelAbstract.Meta):
1423
+ abstract = False
1424
+ swappable = 'DJANGO_LEDGER_ITEM_TRANSACTION_MODEL'
1425
+
1419
1426
 
1420
1427
  class ItemModel(ItemModelAbstract):
1421
1428
  """
1422
1429
  Base ItemModel from Abstract.
1423
1430
  """
1431
+
1432
+ class Meta(ItemModelAbstract.Meta):
1433
+ abstract = False
1434
+ swappable = 'DJANGO_LEDGER_ITEM_MODEL'
@@ -487,15 +487,18 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
487
487
  """
488
488
  return self._verified
489
489
 
490
- def is_balance_valid(self, txs_qs: Optional[TransactionModelQuerySet] = None) -> bool:
490
+ # Transaction QuerySet
491
+ def is_balance_valid(self, txs_qs: TransactionModelQuerySet, raise_exception: bool = True) -> bool:
491
492
  """
492
493
  Checks if CREDITs and DEBITs are equal.
493
494
 
494
495
  Parameters
495
496
  ----------
496
497
  txs_qs: TransactionModelQuerySet
497
- Optional pre-fetched JE instance TransactionModelQuerySet. Will be validated if provided.
498
498
 
499
+ raise_exception: bool
500
+ Raises JournalEntryValidationError if TransactionModelQuerySet is not valid.
501
+
499
502
  Returns
500
503
  -------
501
504
  bool
@@ -503,32 +506,38 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
503
506
  """
504
507
  if len(txs_qs) > 0:
505
508
  balances = self.get_txs_balances(txs_qs=txs_qs, as_dict=True)
506
- return balances[CREDIT] == balances[DEBIT]
509
+ is_valid = balances[CREDIT] == balances[DEBIT]
510
+ if not is_valid:
511
+ if raise_exception:
512
+ raise JournalEntryValidationError(
513
+ message='Balance of {0} CREDITs are {1} does not match DEBITs {2}.'.format(
514
+ self,
515
+ balances[CREDIT],
516
+ balances[DEBIT]
517
+ )
518
+ )
519
+ return is_valid
507
520
  return True
508
521
 
509
- def is_cash_involved(self, txs_qs=None):
510
- return ASSET_CA_CASH in self.get_txs_roles(txs_qs=None)
511
-
512
- def is_operating(self):
513
- return self.activity in [
514
- self.OPERATING_ACTIVITY
515
- ]
522
+ def is_txs_qs_coa_valid(self, txs_qs: TransactionModelQuerySet) -> bool:
523
+ """
524
+ Validates that the Chart of Accounts (COA) is valid for the transactions.
525
+ Journal Entry transactions can only be associated with one Chart of Accounts (COA).
526
+
527
+
528
+ Parameters
529
+ ----------
530
+ txs_qs: TransactionModelQuerySet
516
531
 
517
- def is_financing(self):
518
- return self.activity in [
519
- self.FINANCING_EQUITY,
520
- self.FINANCING_LTD,
521
- self.FINANCING_DIVIDENDS,
522
- self.FINANCING_STD,
523
- self.FINANCING_OTHER
524
- ]
532
+ Returns
533
+ -------
534
+ True if Transaction CoAs are valid, otherwise False.
535
+ """
525
536
 
526
- def is_investing(self):
527
- return self.activity in [
528
- self.INVESTING_SECURITIES,
529
- self.INVESTING_PPE,
530
- self.INVESTING_OTHER
531
- ]
537
+ if len(txs_qs) > 0:
538
+ coa_count = len(set(tx.coa_id for tx in txs_qs))
539
+ return coa_count == 1
540
+ return True
532
541
 
533
542
  def is_txs_qs_valid(self, txs_qs: TransactionModelQuerySet, raise_exception: bool = True) -> bool:
534
543
  """
@@ -560,6 +569,30 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
560
569
  f'associated with LedgerModel {self.uuid}')
561
570
  return is_valid
562
571
 
572
+ def is_cash_involved(self, txs_qs=None):
573
+ return ASSET_CA_CASH in self.get_txs_roles(txs_qs=None)
574
+
575
+ def is_operating(self):
576
+ return self.activity in [
577
+ self.OPERATING_ACTIVITY
578
+ ]
579
+
580
+ def is_financing(self):
581
+ return self.activity in [
582
+ self.FINANCING_EQUITY,
583
+ self.FINANCING_LTD,
584
+ self.FINANCING_DIVIDENDS,
585
+ self.FINANCING_STD,
586
+ self.FINANCING_OTHER
587
+ ]
588
+
589
+ def is_investing(self):
590
+ return self.activity in [
591
+ self.INVESTING_SECURITIES,
592
+ self.INVESTING_PPE,
593
+ self.INVESTING_OTHER
594
+ ]
595
+
563
596
  def get_entity_unit_name(self, no_unit_name: str = ''):
564
597
  if self.entity_unit_id:
565
598
  return self.entity_unit.name
@@ -1155,6 +1188,15 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
1155
1188
  except JournalEntryValidationError as e:
1156
1189
  raise e
1157
1190
 
1191
+ # Transaction CoA if valid
1192
+
1193
+ try:
1194
+ is_coa_valid = self.is_txs_qs_coa_valid(txs_qs=txs_qs)
1195
+ if not is_coa_valid:
1196
+ raise JournalEntryValidationError('Transaction COA is not valid!')
1197
+ except JournalEntryValidationError as e:
1198
+ raise e
1199
+
1158
1200
  # if not len(txs_qs):
1159
1201
  # if raise_exception:
1160
1202
  # raise JournalEntryValidationError('Journal entry has no transactions.')
@@ -1163,7 +1205,7 @@ class JournalEntryModelAbstract(CreateUpdateMixIn):
1163
1205
  # if raise_exception:
1164
1206
  # raise JournalEntryValidationError('At least two transactions required.')
1165
1207
 
1166
- if all([is_balance_valid, is_txs_qs_valid]):
1208
+ if all([is_balance_valid, is_txs_qs_valid, is_coa_valid]):
1167
1209
  # activity flag...
1168
1210
  self.generate_activity(txs_qs=txs_qs, raise_exception=raise_exception)
1169
1211
  self._verified = True
@@ -1350,6 +1392,10 @@ class JournalEntryModel(JournalEntryModelAbstract):
1350
1392
  Journal Entry Model Base Class From Abstract
1351
1393
  """
1352
1394
 
1395
+ class Meta(JournalEntryModelAbstract.Meta):
1396
+ swappable = 'DJANGO_LEDGER_JOURNAL_ENTRY_MODEL'
1397
+ abstract = False
1398
+
1353
1399
 
1354
1400
  def journalentrymodel_presave(instance: JournalEntryModel, **kwargs):
1355
1401
  if instance._state.adding and not instance.ledger.can_edit_journal_entries():
@@ -20,7 +20,7 @@ which is the class responsible for making accounting queries to the Database in
20
20
  The digest() method executes all necessary aggregations and optimizations in order to push as much work to the Database
21
21
  layer as possible in order to minimize the amount of data being pulled for analysis into the Python memory.
22
22
 
23
- The Django Ledger core model follows the following structure: \n
23
+ The Django Ledger core model follows the following structure:
24
24
  EntityModel -< LedgerModel -< JournalEntryModel -< TransactionModel
25
25
  """
26
26
  from datetime import date
@@ -34,6 +34,10 @@ from django.db import models
34
34
  from django.db.models import Q, Min, F, Count
35
35
  from django.urls import reverse
36
36
  from django.utils.translation import gettext_lazy as _
37
+
38
+ from django_ledger.io.io_core import IOMixIn
39
+ from django_ledger.models import lazy_loader
40
+ from django_ledger.models.mixins import CreateUpdateMixIn
37
41
  from django_ledger.models.signals import (
38
42
  ledger_posted,
39
43
  ledger_unposted,
@@ -43,10 +47,6 @@ from django_ledger.models.signals import (
43
47
  ledger_unhidden
44
48
  )
45
49
 
46
- from django_ledger.io.io_core import IOMixIn
47
- from django_ledger.models import lazy_loader
48
- from django_ledger.models.mixins import CreateUpdateMixIn
49
-
50
50
  LEDGER_ID_CHARS = ascii_lowercase + digits
51
51
 
52
52
 
@@ -725,6 +725,9 @@ class LedgerModel(LedgerModelAbstract):
725
725
  """
726
726
  Base LedgerModel from Abstract.
727
727
  """
728
+ class Meta(LedgerModelAbstract.Meta):
729
+ swappable = 'DJANGO_LEDGER_LEDGER_MODEL'
730
+ abstract = False
728
731
 
729
732
 
730
733
  def ledgermodel_presave(instance: LedgerModel, **kwargs):
@@ -21,7 +21,7 @@ from django.contrib.auth import get_user_model
21
21
  from django.core.exceptions import ValidationError, ObjectDoesNotExist
22
22
  from django.core.validators import MinLengthValidator
23
23
  from django.db import models, transaction, IntegrityError
24
- from django.db.models import Q, Sum, Count, F
24
+ from django.db.models import Q, Sum, Count, F, Manager, QuerySet
25
25
  from django.db.models.functions import Coalesce
26
26
  from django.db.models.signals import pre_save
27
27
  from django.shortcuts import get_object_or_404
@@ -33,8 +33,6 @@ from django_ledger.models.bill import BillModel, BillModelQuerySet
33
33
  from django_ledger.models.entity import EntityModel
34
34
  from django_ledger.models.items import ItemTransactionModel, ItemTransactionModelQuerySet, ItemModelQuerySet, ItemModel
35
35
  from django_ledger.models.mixins import CreateUpdateMixIn, MarkdownNotesMixIn, ItemizeMixIn
36
- from django_ledger.models.utils import lazy_loader
37
- from django_ledger.settings import DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_PO_NUMBER_PREFIX
38
36
  from django_ledger.models.signals import (
39
37
  po_status_draft,
40
38
  po_status_void,
@@ -43,6 +41,8 @@ from django_ledger.models.signals import (
43
41
  po_status_canceled,
44
42
  po_status_in_review
45
43
  )
44
+ from django_ledger.models.utils import lazy_loader
45
+ from django_ledger.settings import DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_PO_NUMBER_PREFIX
46
46
 
47
47
  PO_NUMBER_CHARS = ascii_uppercase + digits
48
48
 
@@ -53,7 +53,7 @@ class PurchaseOrderModelValidationError(ValidationError):
53
53
  pass
54
54
 
55
55
 
56
- class PurchaseOrderModelQuerySet(models.QuerySet):
56
+ class PurchaseOrderModelQuerySet(QuerySet):
57
57
  """
58
58
  A custom defined PurchaseOrderModel QuerySet.
59
59
  """
@@ -100,7 +100,7 @@ class PurchaseOrderModelQuerySet(models.QuerySet):
100
100
  return self.filter(po_status__exact=PurchaseOrderModel.PO_STATUS_DRAFT)
101
101
 
102
102
 
103
- class PurchaseOrderModelManager(models.Manager):
103
+ class PurchaseOrderModelManager(Manager):
104
104
  """
105
105
  A custom defined PurchaseOrderModel Manager.
106
106
  """
@@ -1233,6 +1233,10 @@ class PurchaseOrderModel(PurchaseOrderModelAbstract):
1233
1233
  Purchase Order Base Model
1234
1234
  """
1235
1235
 
1236
+ class Meta(PurchaseOrderModelAbstract.Meta):
1237
+ swappable = 'DJANGO_LEDGER_PURCHASE_ORDER_MODEL'
1238
+ abstract = False
1239
+
1236
1240
 
1237
1241
  def purchaseordermodel_presave(instance: PurchaseOrderModel, **kwargs):
1238
1242
  if instance.can_generate_po_number():
@@ -21,7 +21,7 @@ from django.contrib.auth import get_user_model
21
21
  from django.core.exceptions import ValidationError
22
22
  from django.core.validators import MinValueValidator
23
23
  from django.db import models
24
- from django.db.models import Q, QuerySet, Manager
24
+ from django.db.models import Q, QuerySet, Manager, F
25
25
  from django.db.models.signals import pre_save
26
26
  from django.utils.translation import gettext_lazy as _
27
27
 
@@ -213,7 +213,9 @@ class TransactionModelManager(Manager):
213
213
 
214
214
  def get_queryset(self) -> TransactionModelQuerySet:
215
215
  qs = TransactionModelQuerySet(self.model, using=self._db)
216
- return qs.select_related(
216
+ return qs.annotate(
217
+ _coa_id=F('account__coa_model_id'),
218
+ ).select_related(
217
219
  'journal_entry',
218
220
  'account',
219
221
  'account__coa_model',
@@ -497,6 +499,9 @@ class TransactionModelAbstract(CreateUpdateMixIn):
497
499
  verbose_name=_('Tx Description'),
498
500
  help_text=_('A description to be included with this individual transaction'))
499
501
 
502
+ cleared = models.BooleanField(default=False, verbose_name=_('Cleared'))
503
+ reconciled = models.BooleanField(default=False, verbose_name=_('Reconciled'))
504
+
500
505
  objects = TransactionModelManager()
501
506
 
502
507
  class Meta:
@@ -509,7 +514,9 @@ class TransactionModelAbstract(CreateUpdateMixIn):
509
514
  models.Index(fields=['account']),
510
515
  models.Index(fields=['journal_entry']),
511
516
  models.Index(fields=['created']),
512
- models.Index(fields=['updated'])
517
+ models.Index(fields=['updated']),
518
+ models.Index(fields=['cleared']),
519
+ models.Index(fields=['reconciled']),
513
520
  ]
514
521
 
515
522
  def __str__(self):
@@ -519,6 +526,15 @@ class TransactionModelAbstract(CreateUpdateMixIn):
519
526
  x4=self.tx_type,
520
527
  x5=self.account.balance_type)
521
528
 
529
+ @property
530
+ def coa_id(self):
531
+ try:
532
+ return getattr(self, '_coa_id')
533
+ except AttributeError:
534
+ if self.account is None:
535
+ return None
536
+ return self.account.coa_model_id
537
+
522
538
  def clean(self):
523
539
  if self.account_id and self.account.is_root_account():
524
540
  raise TransactionModelValidationError(
@@ -531,6 +547,10 @@ class TransactionModel(TransactionModelAbstract):
531
547
  Base Transaction Model From Abstract.
532
548
  """
533
549
 
550
+ class Meta(TransactionModelAbstract.Meta):
551
+ abstract = False
552
+ swappable = 'DJANGO_LEDGER_TRANSACTION_MODEL'
553
+
534
554
 
535
555
  def transactionmodel_presave(instance: TransactionModel, **kwargs):
536
556
  """
@@ -223,3 +223,7 @@ class EntityUnitModel(EntityUnitModelAbstract):
223
223
  """
224
224
  Base Model Class for EntityUnitModel
225
225
  """
226
+
227
+ class Meta(EntityUnitModelAbstract.Meta):
228
+ swappable = 'DJANGO_LEDGER_ENTITY_UNIT_MODEL'
229
+ abstract = False
@@ -323,3 +323,7 @@ class VendorModel(VendorModelAbstract):
323
323
  """
324
324
  Base Vendor Model Implementation
325
325
  """
326
+
327
+ class Meta(VendorModelAbstract.Meta):
328
+ swappable = 'DJANGO_LEDGER_VENDOR_MODEL'
329
+ abstract = False