django-ledger 0.5.5.5__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 (45) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/admin/entity.py +16 -2
  3. django_ledger/admin/ledger.py +2 -1
  4. django_ledger/forms/entity.py +4 -12
  5. django_ledger/forms/ledger.py +19 -0
  6. django_ledger/forms/transactions.py +1 -1
  7. django_ledger/io/__init__.py +4 -1
  8. django_ledger/io/{io_mixin.py → io_core.py} +49 -28
  9. django_ledger/io/io_digest.py +7 -0
  10. django_ledger/io/{data_generator.py → io_generator.py} +51 -8
  11. django_ledger/io/io_library.py +317 -0
  12. django_ledger/io/{io_context.py → io_middleware.py} +16 -9
  13. django_ledger/migrations/0001_initial.py +413 -132
  14. django_ledger/migrations/0014_ledgermodel_ledger_xid_and_more.py +22 -0
  15. django_ledger/models/accounts.py +8 -7
  16. django_ledger/models/bank_account.py +12 -11
  17. django_ledger/models/bill.py +5 -9
  18. django_ledger/models/closing_entry.py +14 -14
  19. django_ledger/models/coa.py +1 -1
  20. django_ledger/models/customer.py +5 -11
  21. django_ledger/models/data_import.py +12 -6
  22. django_ledger/models/entity.py +88 -10
  23. django_ledger/models/estimate.py +12 -9
  24. django_ledger/models/invoice.py +10 -4
  25. django_ledger/models/items.py +11 -6
  26. django_ledger/models/journal_entry.py +6 -13
  27. django_ledger/models/ledger.py +65 -15
  28. django_ledger/models/mixins.py +2 -3
  29. django_ledger/models/purchase_order.py +11 -7
  30. django_ledger/models/transactions.py +3 -1
  31. django_ledger/models/unit.py +13 -14
  32. django_ledger/models/vendor.py +12 -11
  33. django_ledger/templates/django_ledger/journal_entry/je_list.html +3 -0
  34. django_ledger/templatetags/django_ledger.py +1 -1
  35. django_ledger/tests/base.py +1 -1
  36. django_ledger/urls/ledger.py +3 -0
  37. django_ledger/views/entity.py +9 -3
  38. django_ledger/views/ledger.py +14 -7
  39. django_ledger/views/mixins.py +9 -1
  40. {django_ledger-0.5.5.5.dist-info → django_ledger-0.5.6.0.dist-info}/METADATA +8 -8
  41. {django_ledger-0.5.5.5.dist-info → django_ledger-0.5.6.0.dist-info}/RECORD +45 -43
  42. {django_ledger-0.5.5.5.dist-info → django_ledger-0.5.6.0.dist-info}/AUTHORS.md +0 -0
  43. {django_ledger-0.5.5.5.dist-info → django_ledger-0.5.6.0.dist-info}/LICENSE +0 -0
  44. {django_ledger-0.5.5.5.dist-info → django_ledger-0.5.6.0.dist-info}/WHEEL +0 -0
  45. {django_ledger-0.5.5.5.dist-info → django_ledger-0.5.6.0.dist-info}/top_level.txt +0 -0
django_ledger/__init__.py CHANGED
@@ -9,7 +9,7 @@ Contributions to this module:
9
9
  default_app_config = 'django_ledger.apps.DjangoLedgerConfig'
10
10
 
11
11
  """Django Ledger"""
12
- __version__ = '0.5.5.5'
12
+ __version__ = '0.5.6.0'
13
13
  __license__ = 'GPLv3 License'
14
14
 
15
15
  __author__ = 'Miguel Sanda'
@@ -1,3 +1,4 @@
1
+ from datetime import timedelta
1
2
  from uuid import uuid4
2
3
 
3
4
  from django.contrib.admin import TabularInline, ModelAdmin
@@ -8,7 +9,7 @@ from django.utils.html import format_html
8
9
  from django.utils.timezone import localtime
9
10
 
10
11
  from django_ledger.admin.coa import ChartOfAccountsInLine
11
- from django_ledger.models import EntityUnitModel
12
+ from django_ledger.models import EntityUnitModel, ItemTransactionModel, TransactionModel
12
13
  from django_ledger.models.entity import EntityModel, EntityManagementModel
13
14
 
14
15
 
@@ -50,6 +51,7 @@ class EntityUnitModelInLine(TabularInline):
50
51
 
51
52
  class EntityModelAdmin(ModelAdmin):
52
53
  list_display = [
54
+ 'slug',
53
55
  'name',
54
56
  'accrual_method',
55
57
  'last_closing_date',
@@ -108,7 +110,8 @@ class EntityModelAdmin(ModelAdmin):
108
110
  EntityManagementInLine
109
111
  ]
110
112
  actions = [
111
- 'add_code_of_accounts'
113
+ 'add_code_of_accounts',
114
+ 'populate_random_data'
112
115
  ]
113
116
 
114
117
  class Meta:
@@ -171,6 +174,14 @@ class EntityModelAdmin(ModelAdmin):
171
174
  assign_as_default=False
172
175
  )
173
176
 
177
+ def populate_random_data(self, request, queryset):
178
+ for entity_model in queryset:
179
+ start_date = localtime() - timedelta(days=180)
180
+ entity_model.populate_random_data(
181
+ start_date=start_date,
182
+ days_forward=180,
183
+ tx_quantity=25)
184
+
174
185
  def get_coa_count(self, obj):
175
186
  return obj.chartofaccountmodel__count
176
187
 
@@ -183,3 +194,6 @@ class EntityModelAdmin(ModelAdmin):
183
194
  EntityModel.add_root(instance=obj)
184
195
  return
185
196
  super().save_model(request, obj, form, change)
197
+
198
+ def has_delete_permission(self, request, obj=None):
199
+ return False
@@ -106,7 +106,8 @@ class LedgerModelAdmin(ModelAdmin):
106
106
  ]
107
107
  list_filter = [
108
108
  'posted',
109
- 'locked'
109
+ 'locked',
110
+ 'entity__name'
110
111
  ]
111
112
  list_display = [
112
113
  'name',
@@ -20,15 +20,6 @@ class EntityModelCreateForm(ModelForm):
20
20
  default_coa = BooleanField(required=False, initial=False, label=_('Populate Default CoA'))
21
21
  activate_all_accounts = BooleanField(required=False, initial=False, label=_('Activate All Accounts'))
22
22
  generate_sample_data = BooleanField(required=False, initial=False, label=_('Fill With Sample Data?'))
23
- tx_quantity = IntegerField(required=True, initial=50, label=_('Number of Transaction Loops'),
24
- validators=[
25
- MinValueValidator(limit_value=0),
26
- MaxValueValidator(limit_value=100)
27
- ],
28
- widget=TextInput(attrs={
29
- 'class': DJANGO_LEDGER_FORM_INPUT_CLASSES
30
- }))
31
-
32
23
 
33
24
  def clean_name(self):
34
25
  name = self.cleaned_data.get('name')
@@ -200,7 +191,8 @@ class EntityModelUpdateForm(ModelForm):
200
191
  'placeholder': _('Website...')
201
192
  }
202
193
  ),
203
- 'fy_start_month': Select(attrs={
204
- 'class': 'input'
205
- })
194
+ 'fy_start_month': Select(
195
+ attrs={
196
+ 'class': 'input'
197
+ })
206
198
  }
@@ -1,4 +1,6 @@
1
+ from django.core.exceptions import ValidationError
1
2
  from django.forms import ModelForm, TextInput, Select
3
+ from django.utils.translation import gettext_lazy as _
2
4
 
3
5
  from django_ledger.models.ledger import LedgerModel
4
6
  from django_ledger.settings import DJANGO_LEDGER_FORM_INPUT_CLASSES
@@ -11,10 +13,19 @@ class LedgerModelCreateForm(ModelForm):
11
13
  self.ENTITY_SLUG: str = entity_slug
12
14
  self.USER_MODEL = user_model
13
15
 
16
+ def validate_unique(self):
17
+ exclude = self._get_validation_exclusions()
18
+ exclude.remove('entity')
19
+ try:
20
+ self.instance.validate_unique(exclude=exclude)
21
+ except ValidationError as e:
22
+ self._update_errors(e)
23
+
14
24
  class Meta:
15
25
  model = LedgerModel
16
26
  fields = [
17
27
  'name',
28
+ 'ledger_xid'
18
29
  ]
19
30
  widgets = {
20
31
  'name': TextInput(
@@ -22,6 +33,14 @@ class LedgerModelCreateForm(ModelForm):
22
33
  'class': DJANGO_LEDGER_FORM_INPUT_CLASSES
23
34
  }
24
35
  ),
36
+ 'ledger_xid': TextInput(
37
+ attrs={
38
+ 'class': DJANGO_LEDGER_FORM_INPUT_CLASSES
39
+ }
40
+ ),
41
+ }
42
+ labels = {
43
+ 'ledger_xid': _('Ledger External ID')
25
44
  }
26
45
 
27
46
 
@@ -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,10 +36,10 @@ 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
@@ -108,13 +118,6 @@ def validate_io_date(
108
118
  return
109
119
 
110
120
  if isinstance(dt, date):
111
- # dt = make_aware(
112
- # value=datetime.combine(
113
- # dt,
114
- # datetime.min.time()
115
- # ),
116
- # timezone=ZoneInfo('UTC')
117
- # )
118
121
  return dt
119
122
 
120
123
  elif isinstance(dt, datetime):
@@ -636,7 +639,7 @@ class IODatabaseMixIn:
636
639
  io_state['by_activity'] = by_activity
637
640
  io_state['by_tx_type'] = by_tx_type
638
641
 
639
- io_result = self.python_digest(
642
+ io_result: IOResult = self.python_digest(
640
643
  user_model=user_model,
641
644
  accounts=accounts,
642
645
  role=role,
@@ -657,8 +660,10 @@ class IODatabaseMixIn:
657
660
  io_state['io_result'] = io_result
658
661
  io_state['accounts'] = io_result.accounts_digest
659
662
 
663
+ # IO Middleware...
664
+
660
665
  if process_roles:
661
- roles_mgr = RoleContextManager(
666
+ roles_mgr = AccountRoleIOMiddleware(
662
667
  io_data=io_state,
663
668
  by_period=by_period,
664
669
  by_unit=by_unit
@@ -673,7 +678,7 @@ class IODatabaseMixIn:
673
678
  income_statement,
674
679
  cash_flow_statement
675
680
  ]):
676
- group_mgr = GroupContextManager(
681
+ group_mgr = AccountGroupIOMiddleware(
677
682
  io_data=io_state,
678
683
  by_period=by_period,
679
684
  by_unit=by_unit
@@ -693,19 +698,19 @@ class IODatabaseMixIn:
693
698
  io_state = ratio_gen.digest()
694
699
 
695
700
  if process_activity:
696
- 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)
697
702
  activity_manager.digest()
698
703
 
699
704
  if balance_sheet_statement:
700
- balance_sheet_mgr = BalanceSheetStatementContextManager(io_data=io_state)
705
+ balance_sheet_mgr = BalanceSheetIOMiddleware(io_data=io_state)
701
706
  io_state = balance_sheet_mgr.digest()
702
707
 
703
708
  if income_statement:
704
- income_statement_mgr = IncomeStatementContextManager(io_data=io_state)
709
+ income_statement_mgr = IncomeStatementIOMiddleware(io_data=io_state)
705
710
  io_state = income_statement_mgr.digest()
706
711
 
707
712
  if cash_flow_statement:
708
- cfs = CashFlowStatementContextManager(io_data=io_state)
713
+ cfs = CashFlowStatementIOMiddleware(io_data=io_state)
709
714
  io_state = cfs.digest()
710
715
 
711
716
  return IODigestContextManager(io_state=io_state)
@@ -744,12 +749,32 @@ class IODatabaseMixIn:
744
749
  JournalEntryModel = lazy_loader.get_journal_entry_model()
745
750
  TransactionModel = lazy_loader.get_txs_model()
746
751
 
747
- # if isinstance(self, lazy_loader.get_entity_model()):
748
-
749
752
  # Validates that credits/debits balance.
750
753
  check_tx_balance(je_txs, perform_correction=False)
751
754
  je_timestamp = validate_io_date(dt=je_timestamp)
752
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
+
753
778
  # if calling from EntityModel must pass an instance of LedgerModel...
754
779
  if all([
755
780
  isinstance(self, lazy_loader.get_entity_model()),
@@ -877,7 +902,7 @@ class IOReportMixIn:
877
902
  report_subtitle=subtitle
878
903
  )
879
904
  if save_pdf:
880
- 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)
881
906
  filename = report.get_pdf_filename() if not filename else filename
882
907
  filepath = base_dir.joinpath(filename)
883
908
  report.create_pdf_report()
@@ -932,7 +957,7 @@ class IOReportMixIn:
932
957
  report_subtitle=subtitle
933
958
  )
934
959
  if save_pdf:
935
- 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)
936
961
  filename = report.get_pdf_filename() if not filename else filename
937
962
  filepath = base_dir.joinpath(filename)
938
963
  report.create_pdf_report()
@@ -986,7 +1011,7 @@ class IOReportMixIn:
986
1011
  report_subtitle=subtitle
987
1012
  )
988
1013
  if save_pdf:
989
- 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)
990
1015
  filename = report.get_pdf_filename() if not filename else filename
991
1016
  filepath = base_dir.joinpath(filename)
992
1017
  report.create_pdf_report()
@@ -1028,10 +1053,6 @@ class IOReportMixIn:
1028
1053
  from_date=from_date,
1029
1054
  to_date=to_date,
1030
1055
  user_model=user_model,
1031
- balance_sheet_statement=True,
1032
- income_statement=True,
1033
- cash_flow_statement=True,
1034
- as_io_digest=True,
1035
1056
  **kwargs
1036
1057
  )
1037
1058
 
@@ -1058,7 +1079,7 @@ class IOReportMixIn:
1058
1079
  )
1059
1080
 
1060
1081
  if save_pdf:
1061
- 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)
1062
1083
  bs_report.create_pdf_report()
1063
1084
  bs_report.output(base_dir.joinpath(bs_report.get_pdf_filename(dt_strfmt=dt_strfmt)))
1064
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
@@ -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...')