django-ledger 0.5.5.4__py3-none-any.whl → 0.5.6.0__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 (52) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/admin/__init__.py +10 -0
  3. django_ledger/admin/coa.py +135 -0
  4. django_ledger/admin/entity.py +199 -0
  5. django_ledger/admin/ledger.py +283 -0
  6. django_ledger/forms/entity.py +4 -12
  7. django_ledger/forms/ledger.py +19 -0
  8. django_ledger/forms/transactions.py +1 -1
  9. django_ledger/io/__init__.py +4 -1
  10. django_ledger/io/{io_mixin.py → io_core.py} +95 -35
  11. django_ledger/io/io_digest.py +15 -0
  12. django_ledger/io/{data_generator.py → io_generator.py} +51 -8
  13. django_ledger/io/io_library.py +317 -0
  14. django_ledger/io/{io_context.py → io_middleware.py} +16 -9
  15. django_ledger/migrations/0001_initial.py +413 -132
  16. django_ledger/migrations/0014_ledgermodel_ledger_xid_and_more.py +22 -0
  17. django_ledger/models/accounts.py +68 -7
  18. django_ledger/models/bank_account.py +12 -11
  19. django_ledger/models/bill.py +5 -9
  20. django_ledger/models/closing_entry.py +14 -14
  21. django_ledger/models/coa.py +60 -18
  22. django_ledger/models/customer.py +5 -11
  23. django_ledger/models/data_import.py +12 -6
  24. django_ledger/models/entity.py +90 -12
  25. django_ledger/models/estimate.py +12 -9
  26. django_ledger/models/invoice.py +10 -4
  27. django_ledger/models/items.py +11 -6
  28. django_ledger/models/journal_entry.py +61 -18
  29. django_ledger/models/ledger.py +90 -24
  30. django_ledger/models/mixins.py +2 -3
  31. django_ledger/models/purchase_order.py +11 -7
  32. django_ledger/models/transactions.py +3 -1
  33. django_ledger/models/unit.py +22 -13
  34. django_ledger/models/vendor.py +12 -11
  35. django_ledger/report/cash_flow_statement.py +1 -1
  36. django_ledger/report/core.py +3 -2
  37. django_ledger/templates/django_ledger/journal_entry/includes/card_journal_entry.html +1 -1
  38. django_ledger/templates/django_ledger/journal_entry/je_list.html +3 -0
  39. django_ledger/templatetags/django_ledger.py +1 -1
  40. django_ledger/tests/base.py +1 -1
  41. django_ledger/tests/test_entity.py +1 -1
  42. django_ledger/urls/ledger.py +3 -0
  43. django_ledger/views/entity.py +9 -3
  44. django_ledger/views/ledger.py +14 -7
  45. django_ledger/views/mixins.py +9 -1
  46. {django_ledger-0.5.5.4.dist-info → django_ledger-0.5.6.0.dist-info}/METADATA +9 -9
  47. {django_ledger-0.5.5.4.dist-info → django_ledger-0.5.6.0.dist-info}/RECORD +51 -46
  48. {django_ledger-0.5.5.4.dist-info → django_ledger-0.5.6.0.dist-info}/top_level.txt +0 -1
  49. django_ledger/admin.py +0 -160
  50. {django_ledger-0.5.5.4.dist-info → django_ledger-0.5.6.0.dist-info}/AUTHORS.md +0 -0
  51. {django_ledger-0.5.5.4.dist-info → django_ledger-0.5.6.0.dist-info}/LICENSE +0 -0
  52. {django_ledger-0.5.5.4.dist-info → django_ledger-0.5.6.0.dist-info}/WHEEL +0 -0
@@ -10,7 +10,7 @@ Michael Noel <noel.michael87@gmail.com>
10
10
  from django.forms import ModelForm, modelformset_factory, BaseModelFormSet, TextInput, Select, ValidationError
11
11
  from django.utils.translation import gettext_lazy as _
12
12
 
13
- from django_ledger.io import check_tx_balance
13
+ from django_ledger.io.io_core import check_tx_balance
14
14
  from django_ledger.models.accounts import AccountModel
15
15
  from django_ledger.models.journal_entry import JournalEntryModel
16
16
  from django_ledger.models.transactions import TransactionModel
@@ -6,6 +6,9 @@ Contributions to this module:
6
6
  Miguel Sanda <msanda@arrobalytics.com>
7
7
  """
8
8
 
9
- from django_ledger.io.io_mixin import *
9
+ from django_ledger.io.io_digest import *
10
+ from django_ledger.io.io_middleware import *
10
11
  from django_ledger.io.ratios import *
11
12
  from django_ledger.io.roles import *
13
+ # due to circular import
14
+ # from django_ledger.io.io_library import IOLibrary
@@ -4,6 +4,15 @@ Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
4
 
5
5
  Contributions to this module:
6
6
  * Miguel Sanda <msanda@arrobalytics.com>
7
+
8
+ This module provides the building block interface for Django Ledger. The classes and functions contained in this module
9
+ provide an interface to Django Ledger to create and manage Transactions into the Database. It also provides an
10
+ optimized interface to push as much work as possible to the database without having to pull transactions from the
11
+ database into the Python memory.
12
+
13
+ The database records the individual transactions associated with each Journal Entry. However, this interface aggregates
14
+ transactions during the digest method based on a specific request. The Python interpreter is responsible for applying
15
+ accounting rules to the transactions associated with each Journal Entry so the appropriate account balances are computed.
7
16
  """
8
17
  from collections import namedtuple
9
18
  from dataclasses import dataclass
@@ -14,6 +23,7 @@ from random import choice
14
23
  from typing import List, Set, Union, Tuple, Optional, Dict
15
24
  from zoneinfo import ZoneInfo
16
25
 
26
+ from django.conf import settings as global_settings
17
27
  from django.contrib.auth import get_user_model
18
28
  from django.core.exceptions import ValidationError, ObjectDoesNotExist
19
29
  from django.db.models import Sum, QuerySet
@@ -26,14 +36,15 @@ from django.utils.translation import gettext_lazy as _
26
36
  from django_ledger import settings
27
37
  from django_ledger.exceptions import InvalidDateInputError, TransactionNotInBalanceError
28
38
  from django_ledger.io import roles as roles_module
29
- from django_ledger.io.io_context import (
30
- RoleContextManager, GroupContextManager, ActivityContextManager,
31
- BalanceSheetStatementContextManager, IncomeStatementContextManager,
32
- CashFlowStatementContextManager
39
+ from django_ledger.io.io_middleware import (
40
+ AccountRoleIOMiddleware, AccountGroupIOMiddleware, JEActivityIOMiddleware,
41
+ BalanceSheetIOMiddleware, IncomeStatementIOMiddleware,
42
+ CashFlowStatementIOMiddleware
33
43
  )
34
44
  from django_ledger.io.io_digest import IODigestContextManager
35
45
  from django_ledger.io.ratios import FinancialRatioManager
36
46
  from django_ledger.models.utils import lazy_loader
47
+ from django_ledger.settings import DJANGO_LEDGER_PDF_SUPPORT_ENABLED
37
48
 
38
49
  UserModel = get_user_model()
39
50
 
@@ -107,13 +118,6 @@ def validate_io_date(
107
118
  return
108
119
 
109
120
  if isinstance(dt, date):
110
- # dt = make_aware(
111
- # value=datetime.combine(
112
- # dt,
113
- # datetime.min.time()
114
- # ),
115
- # timezone=ZoneInfo('UTC')
116
- # )
117
121
  return dt
118
122
 
119
123
  elif isinstance(dt, datetime):
@@ -635,7 +639,7 @@ class IODatabaseMixIn:
635
639
  io_state['by_activity'] = by_activity
636
640
  io_state['by_tx_type'] = by_tx_type
637
641
 
638
- io_result = self.python_digest(
642
+ io_result: IOResult = self.python_digest(
639
643
  user_model=user_model,
640
644
  accounts=accounts,
641
645
  role=role,
@@ -656,8 +660,10 @@ class IODatabaseMixIn:
656
660
  io_state['io_result'] = io_result
657
661
  io_state['accounts'] = io_result.accounts_digest
658
662
 
663
+ # IO Middleware...
664
+
659
665
  if process_roles:
660
- roles_mgr = RoleContextManager(
666
+ roles_mgr = AccountRoleIOMiddleware(
661
667
  io_data=io_state,
662
668
  by_period=by_period,
663
669
  by_unit=by_unit
@@ -672,7 +678,7 @@ class IODatabaseMixIn:
672
678
  income_statement,
673
679
  cash_flow_statement
674
680
  ]):
675
- group_mgr = GroupContextManager(
681
+ group_mgr = AccountGroupIOMiddleware(
676
682
  io_data=io_state,
677
683
  by_period=by_period,
678
684
  by_unit=by_unit
@@ -692,19 +698,19 @@ class IODatabaseMixIn:
692
698
  io_state = ratio_gen.digest()
693
699
 
694
700
  if process_activity:
695
- activity_manager = ActivityContextManager(io_data=io_state, by_unit=by_unit, by_period=by_period)
701
+ activity_manager = JEActivityIOMiddleware(io_data=io_state, by_unit=by_unit, by_period=by_period)
696
702
  activity_manager.digest()
697
703
 
698
704
  if balance_sheet_statement:
699
- balance_sheet_mgr = BalanceSheetStatementContextManager(io_data=io_state)
705
+ balance_sheet_mgr = BalanceSheetIOMiddleware(io_data=io_state)
700
706
  io_state = balance_sheet_mgr.digest()
701
707
 
702
708
  if income_statement:
703
- income_statement_mgr = IncomeStatementContextManager(io_data=io_state)
709
+ income_statement_mgr = IncomeStatementIOMiddleware(io_data=io_state)
704
710
  io_state = income_statement_mgr.digest()
705
711
 
706
712
  if cash_flow_statement:
707
- cfs = CashFlowStatementContextManager(io_data=io_state)
713
+ cfs = CashFlowStatementIOMiddleware(io_data=io_state)
708
714
  io_state = cfs.digest()
709
715
 
710
716
  return IODigestContextManager(io_state=io_state)
@@ -722,7 +728,7 @@ class IODatabaseMixIn:
722
728
  Creates JE from TXS list using provided account_id.
723
729
 
724
730
  TXS = List[{
725
- 'account_id': Account Database UUID
731
+ 'account': Account Database UUID
726
732
  'tx_type': credit/debit,
727
733
  'amount': Decimal/Float/Integer,
728
734
  'description': string,
@@ -743,12 +749,32 @@ class IODatabaseMixIn:
743
749
  JournalEntryModel = lazy_loader.get_journal_entry_model()
744
750
  TransactionModel = lazy_loader.get_txs_model()
745
751
 
746
- # if isinstance(self, lazy_loader.get_entity_model()):
747
-
748
752
  # Validates that credits/debits balance.
749
753
  check_tx_balance(je_txs, perform_correction=False)
750
754
  je_timestamp = validate_io_date(dt=je_timestamp)
751
755
 
756
+ entity_model = self.get_entity_model_from_io()
757
+
758
+ if entity_model.last_closing_date:
759
+ if isinstance(je_timestamp, datetime):
760
+ if entity_model.last_closing_date >= je_timestamp.date():
761
+ raise IOValidationError(
762
+ message=_(
763
+ f'Cannot commit transactions. The journal entry date {je_timestamp} is on a closed period.')
764
+ )
765
+ elif isinstance(je_timestamp, date):
766
+ if entity_model.last_closing_date >= je_timestamp:
767
+ raise IOValidationError(
768
+ message=_(
769
+ f'Cannot commit transactions. The journal entry date {je_timestamp} is on a closed period.')
770
+ )
771
+
772
+ if self.is_ledger_model():
773
+ if self.is_locked():
774
+ raise IOValidationError(
775
+ message=_('Cannot commit on locked ledger')
776
+ )
777
+
752
778
  # if calling from EntityModel must pass an instance of LedgerModel...
753
779
  if all([
754
780
  isinstance(self, lazy_loader.get_entity_model()),
@@ -836,7 +862,7 @@ class IOReportMixIn:
836
862
  to_date: Union[date, datetime],
837
863
  user_model: Optional[UserModel] = None,
838
864
  txs_queryset: Optional[QuerySet] = None,
839
- **kwargs: Dict) -> Union[IODigestContextManager, Tuple[QuerySet, Dict]]:
865
+ **kwargs: Dict) -> IODigestContextManager:
840
866
  return self.digest(
841
867
  user_model=user_model,
842
868
  to_date=to_date,
@@ -854,7 +880,13 @@ class IOReportMixIn:
854
880
  user_model: Optional[UserModel] = None,
855
881
  save_pdf: bool = False,
856
882
  **kwargs
857
- ):
883
+ ) -> IODigestContextManager:
884
+
885
+ if not DJANGO_LEDGER_PDF_SUPPORT_ENABLED:
886
+ raise IOValidationError(
887
+ message=_('PDF support not enabled. Install PDF support from Pipfile.')
888
+ )
889
+
858
890
  io_digest = self.digest_balance_sheet(
859
891
  to_date=to_date,
860
892
  user_model=user_model,
@@ -870,7 +902,7 @@ class IOReportMixIn:
870
902
  report_subtitle=subtitle
871
903
  )
872
904
  if save_pdf:
873
- base_dir = Path(settings.BASE_DIR) if not filepath else Path(filepath)
905
+ base_dir = Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
874
906
  filename = report.get_pdf_filename() if not filename else filename
875
907
  filepath = base_dir.joinpath(filename)
876
908
  report.create_pdf_report()
@@ -882,7 +914,7 @@ class IOReportMixIn:
882
914
  to_date: Union[date, datetime],
883
915
  user_model: Optional[UserModel] = None,
884
916
  txs_queryset: Optional[QuerySet] = None,
885
- **kwargs) -> Union[IODigestContextManager, Tuple[QuerySet, Dict]]:
917
+ **kwargs) -> IODigestContextManager:
886
918
  return self.digest(
887
919
  user_model=user_model,
888
920
  from_date=from_date,
@@ -904,6 +936,11 @@ class IOReportMixIn:
904
936
  save_pdf: bool = False,
905
937
  **kwargs
906
938
  ):
939
+ if not DJANGO_LEDGER_PDF_SUPPORT_ENABLED:
940
+ raise IOValidationError(
941
+ message=_('PDF support not enabled. Install PDF support from Pipfile.')
942
+ )
943
+
907
944
  io_digest = self.digest_income_statement(
908
945
  from_date=from_date,
909
946
  to_date=to_date,
@@ -920,7 +957,7 @@ class IOReportMixIn:
920
957
  report_subtitle=subtitle
921
958
  )
922
959
  if save_pdf:
923
- base_dir = Path(settings.BASE_DIR) if not filepath else Path(filepath)
960
+ base_dir = Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
924
961
  filename = report.get_pdf_filename() if not filename else filename
925
962
  filepath = base_dir.joinpath(filename)
926
963
  report.create_pdf_report()
@@ -930,9 +967,9 @@ class IOReportMixIn:
930
967
  def digest_cash_flow_statement(self,
931
968
  from_date: Union[date, datetime],
932
969
  to_date: Union[date, datetime],
933
- user_model: UserModel,
970
+ user_model: Optional[UserModel] = None,
934
971
  txs_queryset: Optional[QuerySet] = None,
935
- **kwargs) -> Union[IODigestContextManager, Tuple[QuerySet, Dict]]:
972
+ **kwargs) -> IODigestContextManager:
936
973
  return self.digest(
937
974
  user_model=user_model,
938
975
  from_date=from_date,
@@ -953,6 +990,11 @@ class IOReportMixIn:
953
990
  save_pdf: bool = False,
954
991
  **kwargs):
955
992
 
993
+ if not DJANGO_LEDGER_PDF_SUPPORT_ENABLED:
994
+ raise IOValidationError(
995
+ message=_('PDF support not enabled. Install PDF support from Pipfile.')
996
+ )
997
+
956
998
  io_digest = self.digest_cash_flow_statement(
957
999
  from_date=from_date,
958
1000
  to_date=to_date,
@@ -969,13 +1011,30 @@ class IOReportMixIn:
969
1011
  report_subtitle=subtitle
970
1012
  )
971
1013
  if save_pdf:
972
- base_dir = Path(settings.BASE_DIR) if not filepath else Path(filepath)
1014
+ base_dir = Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
973
1015
  filename = report.get_pdf_filename() if not filename else filename
974
1016
  filepath = base_dir.joinpath(filename)
975
1017
  report.create_pdf_report()
976
1018
  report.output(filepath)
977
1019
  return report
978
1020
 
1021
+ def digest_financial_statements(self,
1022
+ from_date: Union[date, datetime],
1023
+ to_date: Union[date, datetime],
1024
+ user_model: Optional[UserModel] = None,
1025
+ **kwargs) -> IODigestContextManager:
1026
+
1027
+ return self.digest(
1028
+ from_date=from_date,
1029
+ to_date=to_date,
1030
+ user_model=user_model,
1031
+ balance_sheet_statement=True,
1032
+ income_statement=True,
1033
+ cash_flow_statement=True,
1034
+ as_io_digest=True,
1035
+ **kwargs
1036
+ )
1037
+
979
1038
  def get_financial_statements(self,
980
1039
  from_date: Union[date, datetime],
981
1040
  to_date: Union[date, datetime],
@@ -985,14 +1044,15 @@ class IOReportMixIn:
985
1044
  filepath: Optional[Path] = None,
986
1045
  **kwargs) -> ReportTuple:
987
1046
 
988
- io_digest = self.digest(
1047
+ if not DJANGO_LEDGER_PDF_SUPPORT_ENABLED:
1048
+ raise IOValidationError(
1049
+ message=_('PDF support not enabled. Install PDF support from Pipfile.')
1050
+ )
1051
+
1052
+ io_digest = self.digest_financial_statements(
989
1053
  from_date=from_date,
990
1054
  to_date=to_date,
991
1055
  user_model=user_model,
992
- balance_sheet_statement=True,
993
- income_statement=True,
994
- cash_flow_statement=True,
995
- as_io_digest=True,
996
1056
  **kwargs
997
1057
  )
998
1058
 
@@ -1019,7 +1079,7 @@ class IOReportMixIn:
1019
1079
  )
1020
1080
 
1021
1081
  if save_pdf:
1022
- base_dir = Path(settings.BASE_DIR) if not filepath else Path(filepath)
1082
+ base_dir = Path(global_settings.BASE_DIR) if not filepath else Path(filepath)
1023
1083
  bs_report.create_pdf_report()
1024
1084
  bs_report.output(base_dir.joinpath(bs_report.get_pdf_filename(dt_strfmt=dt_strfmt)))
1025
1085
 
@@ -1,3 +1,10 @@
1
+ """
2
+ Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
+ Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
+
5
+ Contributions to this module:
6
+ * Miguel Sanda <msanda@arrobalytics.com>
7
+ """
1
8
  from collections import defaultdict
2
9
  from datetime import date
3
10
  from typing import Dict, Optional
@@ -114,6 +121,14 @@ class IODigestContextManager:
114
121
  'IO Digest does not have cash flow statement information available.'
115
122
  )
116
123
 
124
+ # All Available Statements
125
+ def get_financial_statements_data(self) -> Dict:
126
+ return {
127
+ 'balance_sheet': self.get_balance_sheet_data() if self.has_balance_sheet() else None,
128
+ 'income_statement': self.get_income_statement_data() if self.has_income_statement() else None,
129
+ 'cash_flow_statement': self.get_cash_flow_statement_data() if self.has_cash_flow_statement() else None,
130
+ }
131
+
117
132
  # CLOSING ENTRIES...
118
133
 
119
134
  def get_closing_entry_data(self):
@@ -4,8 +4,18 @@ Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
4
 
5
5
  Contributions to this module:
6
6
  Miguel Sanda <msanda@arrobalytics.com>
7
- """
8
7
 
8
+ This is a random data generator module used during the testing of the API and for Educational purposes.
9
+
10
+ The class EntityDataGenerator will only work on new entities that contain no Transactions. This is with the intention
11
+ of avoiding unintentional commingling with an actual EntityModel with production data and the data generated randomly.
12
+
13
+ This class will conveniently create a Chart of Accounts and populate the database will Bills, Invoices and various
14
+ other Transactions. The user will be able to immediately browse the Entity data by clicking on the newly created entity's
15
+ details page.
16
+
17
+ All data generated is random and fake, not related to any other entity data.
18
+ """
9
19
  from datetime import date, timedelta, datetime
10
20
  from decimal import Decimal
11
21
  from itertools import groupby
@@ -15,12 +25,13 @@ from typing import Union, Optional
15
25
 
16
26
  from django.core.exceptions import ImproperlyConfigured, ValidationError
17
27
  from django.utils.timezone import localtime, localdate
28
+ from django.utils.translation import gettext_lazy as _
18
29
 
19
30
  from django_ledger.io.roles import (INCOME_OPERATIONAL, ASSET_CA_INVENTORY, COGS, ASSET_CA_CASH, ASSET_CA_PREPAID,
20
31
  LIABILITY_CL_DEFERRED_REVENUE, EXPENSE_OPERATIONAL, EQUITY_CAPITAL,
21
32
  ASSET_CA_RECEIVABLES, LIABILITY_CL_ACC_PAYABLE)
22
33
  from django_ledger.models import (EntityModel, TransactionModel, VendorModel, CustomerModel,
23
- EntityUnitModel, BankAccountModel, LedgerModel, UnitOfMeasureModel, ItemModel,
34
+ EntityUnitModel, BankAccountModel, UnitOfMeasureModel, ItemModel,
24
35
  BillModel, ItemTransactionModel, InvoiceModel,
25
36
  EstimateModel, LoggingMixIn, InvoiceModelValidationError, ChartOfAccountModel)
26
37
  from django_ledger.utils import (generate_random_sku, generate_random_upc, generate_random_item_id)
@@ -34,7 +45,28 @@ except ImportError:
34
45
  FAKER_IMPORTED = False
35
46
 
36
47
 
48
+ class EntityModelValidationError(ValidationError):
49
+ pass
50
+
51
+
37
52
  class EntityDataGenerator(LoggingMixIn):
53
+ """
54
+ A random data generator for Entity Models. Requires a user to me the entity model administrator.
55
+
56
+ Attributes
57
+ ----------
58
+ user_model : UserModel
59
+ The Django user model that administers the entity.
60
+ entity_model : EntityModel
61
+ The Entity model to populate.
62
+ start_dttm: datetime
63
+ The start datetime for new transactions. All transactions will be posted no earlier than this date.
64
+ capital_contribution: Decimal
65
+ The initial capital contribution amount for the Entity Model. This will help fund the entity.
66
+ days_forward: int
67
+ The number of days to span from the start_dttm for new transactions.
68
+
69
+ """
38
70
 
39
71
  def __init__(self,
40
72
  user_model,
@@ -50,6 +82,11 @@ class EntityDataGenerator(LoggingMixIn):
50
82
  if not FAKER_IMPORTED:
51
83
  raise ImproperlyConfigured('Must install Faker library to generate random data.')
52
84
 
85
+ if entity_model.admin != user_model:
86
+ raise EntityModelValidationError(
87
+ message=_(f'User {user_model} must have admin privileges for entity model {entity_model}.')
88
+ )
89
+
53
90
  self.fk = Faker(['en_US'])
54
91
  self.fk.add_provider(company)
55
92
  self.fk.add_provider(address)
@@ -97,7 +134,7 @@ class EntityDataGenerator(LoggingMixIn):
97
134
  def get_logger_name(self):
98
135
  return self.entity_model.slug
99
136
 
100
- def populate_entity(self):
137
+ def populate_entity(self, force_populate: bool = False):
101
138
 
102
139
  self.logger.info('Checking for existing transactions...')
103
140
  txs_qs = TransactionModel.objects.for_entity(
@@ -105,9 +142,10 @@ class EntityDataGenerator(LoggingMixIn):
105
142
  user_model=self.user_model
106
143
  )
107
144
 
108
- if txs_qs.count() > 0:
109
- raise ValidationError(
110
- f'Cannot populate random data on {self.entity_model.name} because it already has existing Transactions')
145
+ if txs_qs.count() > 0 and not force_populate:
146
+ raise EntityModelValidationError(
147
+ f'Cannot populate random data on {self.entity_model.name} because it already has existing Transactions'
148
+ )
111
149
 
112
150
  self.create_coa()
113
151
  self.logger.info(f'Pulling Entity {self.entity_model} accounts...')
@@ -156,9 +194,14 @@ class EntityDataGenerator(LoggingMixIn):
156
194
 
157
195
  def create_coa(self):
158
196
  entity_model = self.entity_model
159
- coa_model = entity_model.create_chart_of_accounts(assign_as_default=True, commit=True)
197
+
198
+ if not self.entity_model.has_default_coa():
199
+ coa_model = entity_model.create_chart_of_accounts(assign_as_default=True, commit=True)
200
+ else:
201
+ coa_model = entity_model.get_default_coa()
202
+
160
203
  entity_model.populate_default_coa(coa_model=coa_model, activate_accounts=True)
161
- self.default_coa = entity_model.default_coa
204
+ self.default_coa = coa_model
162
205
 
163
206
  def create_entity_units(self, nb_units: int = None):
164
207
  self.logger.info(f'Creating entity units...')