django-ledger 0.5.6.3__py3-none-any.whl → 0.5.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-ledger might be problematic. Click here for more details.

Files changed (31) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/admin/coa.py +3 -3
  3. django_ledger/forms/account.py +1 -1
  4. django_ledger/forms/coa.py +1 -6
  5. django_ledger/io/roles.py +25 -9
  6. django_ledger/migrations/0015_remove_chartofaccountmodel_locked_and_more.py +22 -0
  7. django_ledger/models/accounts.py +9 -9
  8. django_ledger/models/coa.py +244 -35
  9. django_ledger/models/entity.py +43 -28
  10. django_ledger/templates/django_ledger/account/account_create.html +17 -11
  11. django_ledger/templates/django_ledger/account/account_list.html +12 -9
  12. django_ledger/templates/django_ledger/account/tags/accounts_table.html +97 -93
  13. django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html +17 -0
  14. django_ledger/templates/django_ledger/{code_of_accounts → chart_of_accounts}/coa_update.html +1 -4
  15. django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html +74 -0
  16. django_ledger/templates/django_ledger/invoice/invoice_list.html +91 -94
  17. django_ledger/templatetags/django_ledger.py +1 -1
  18. django_ledger/tests/base.py +23 -1
  19. django_ledger/tests/test_accounts.py +16 -0
  20. django_ledger/tests/test_chart_of_accounts.py +46 -0
  21. django_ledger/urls/account.py +18 -3
  22. django_ledger/urls/chart_of_accounts.py +21 -1
  23. django_ledger/views/account.py +26 -3
  24. django_ledger/views/coa.py +79 -4
  25. django_ledger/views/mixins.py +3 -0
  26. {django_ledger-0.5.6.3.dist-info → django_ledger-0.5.6.4.dist-info}/METADATA +1 -1
  27. {django_ledger-0.5.6.3.dist-info → django_ledger-0.5.6.4.dist-info}/RECORD +31 -26
  28. {django_ledger-0.5.6.3.dist-info → django_ledger-0.5.6.4.dist-info}/AUTHORS.md +0 -0
  29. {django_ledger-0.5.6.3.dist-info → django_ledger-0.5.6.4.dist-info}/LICENSE +0 -0
  30. {django_ledger-0.5.6.3.dist-info → django_ledger-0.5.6.4.dist-info}/WHEEL +0 -0
  31. {django_ledger-0.5.6.3.dist-info → django_ledger-0.5.6.4.dist-info}/top_level.txt +0 -0
@@ -4,116 +4,113 @@
4
4
  {% load django_ledger %}
5
5
 
6
6
  {% block view_content %}
7
- <div class="card">
8
- <div class="card-content">
9
- <div class="level">
10
- <div class="level-left">
7
+ <div class="box">
8
+ <div class="level">
9
+ <div class="level-left">
10
+ <div class="level-item">
11
+ {% if month %}
12
+ <h1 class="is-size-1 has-text-weight-thin">{{ month | date:'F Y' }}</h1>
13
+ {% elif year %}
14
+ <h1 class="is-size-1 has-text-weight-thin">Year {{ year | date:'Y' }}
15
+ {% trans 'Invoices' %}</h1>
16
+ {% else %}
17
+ <h1 class="is-size-1 has-text-weight-thin">{% trans 'Latest Invoices' %}</h1>
18
+ {% endif %}
19
+ </div>
20
+ <div class="level-item">
21
+ <a href="{% url 'django_ledger:invoice-create' entity_slug=view.kwargs.entity_slug %}">
22
+ <span class="icon is-large has-text-success">{% icon 'carbon:add-alt' 60 %}</span></a>
23
+ </div>
24
+ </div>
25
+ <div class="level-right">
26
+ {% if previous_month %}
11
27
  <div class="level-item">
12
- {% if month %}
13
- <h1 class="is-size-1 has-text-weight-thin">{{ month | date:'F Y' }}</h1>
14
- {% elif year %}
15
- <h1 class="is-size-1 has-text-weight-thin">Year {{ year | date:'Y' }}
16
- {% trans 'Invoices' %}</h1>
17
- {% else %}
18
- <h1 class="is-size-1 has-text-weight-thin">{% trans 'Latest Invoices' %}</h1>
19
- {% endif %}
28
+ <a class="button is-small is-dark is-outlined"
29
+ href="{% url 'django_ledger:bill-list-month' year=previous_month.year month=previous_month.month entity_slug=view.kwargs.entity_slug %}">
30
+ <span class="icon is-small">{% icon 'ant-design:left-circle-outlined' 16 %}</span>
31
+ <span>{{ previous_month | date:'F Y' }}</span>
32
+ </a>
20
33
  </div>
34
+ {% endif %}
35
+ {% if next_month %}
21
36
  <div class="level-item">
22
- <a href="{% url 'django_ledger:invoice-create' entity_slug=view.kwargs.entity_slug %}">
23
- <span class="icon is-large has-text-success">{% icon 'carbon:add-alt' 60 %}</span></a>
37
+ <a class="button is-small is-dark is-outlined"
38
+ href="{% url 'django_ledger:bill-list-month' year=next_month.year month=next_month.month entity_slug=view.kwargs.entity_slug %}">
39
+ <span class="icon is-small">{% icon 'ant-design:right-circle-outlined' 16 %}</span>
40
+ <span>{{ next_month | date:'F Y' }}</span>
41
+ </a>
24
42
  </div>
25
- </div>
26
- <div class="level-right">
27
- {% if previous_month %}
28
- <div class="level-item">
29
- <a class="button is-small is-dark is-outlined"
30
- href="{% url 'django_ledger:bill-list-month' year=previous_month.year month=previous_month.month entity_slug=view.kwargs.entity_slug %}">
31
- <span class="icon is-small">{% icon 'ant-design:left-circle-outlined' 16 %}</span>
32
- <span>{{ previous_month | date:'F Y' }}</span>
33
- </a>
34
- </div>
35
- {% endif %}
36
- {% if next_month %}
37
- <div class="level-item">
38
- <a class="button is-small is-dark is-outlined"
39
- href="{% url 'django_ledger:bill-list-month' year=next_month.year month=next_month.month entity_slug=view.kwargs.entity_slug %}">
40
- <span class="icon is-small">{% icon 'ant-design:right-circle-outlined' 16 %}</span>
41
- <span>{{ next_month | date:'F Y' }}</span>
42
- </a>
43
- </div>
44
- {% endif %}
43
+ {% endif %}
44
+
45
+ {% if previous_year %}
46
+ <div class="level-item">
47
+ <a class="button is-small is-dark is-outlined"
48
+ href="{% url 'django_ledger:invoice-list-year' year=previous_year.year entity_slug=view.kwargs.entity_slug %}">
49
+ <span class="icon is-small">{% icon 'ant-design:left-circle-outlined' 16 %}</span>
50
+ <span>{{ previous_year | date:'Y' }}</span>
51
+ </a>
52
+ </div>
53
+ {% endif %}
54
+ {% if next_year %}
55
+ <div class="level-item">
56
+ <a class="button is-small is-dark is-outlined"
57
+ href="{% url 'django_ledger:invoice-list-year' year=next_year.year entity_slug=view.kwargs.entity_slug %}">
58
+ <span class="icon is-small">{% icon 'ant-design:right-circle-outlined' 16 %}</span>
59
+ <span>{{ next_year | date:'Y' }}</span>
60
+ </a>
61
+ </div>
62
+ {% endif %}
45
63
 
46
- {% if previous_year %}
64
+ {% if page_obj %}
65
+ {% if page_obj.has_previous %}
47
66
  <div class="level-item">
48
- <a class="button is-small is-dark is-outlined"
49
- href="{% url 'django_ledger:invoice-list-year' year=previous_year.year entity_slug=view.kwargs.entity_slug %}">
50
- <span class="icon is-small">{% icon 'ant-design:left-circle-outlined' 16 %}</span>
51
- <span>{{ previous_year | date:'Y' }}</span>
67
+ <a href="?page={{ page_obj.previous_page_number }}"
68
+ class="button is-small is-dark is-outlined">
69
+ <span class="icon is-small">{% icon 'ant-design:left-circle-outlined' 24 %}</span>
52
70
  </a>
53
71
  </div>
54
72
  {% endif %}
55
- {% if next_year %}
73
+ <div class="level-item">
74
+ <p class="is-italic">page {{ page_obj.number }}
75
+ of {{ page_obj.paginator.num_pages }}</p>
76
+ </div>
77
+ {% if page_obj.has_next %}
56
78
  <div class="level-item">
57
- <a class="button is-small is-dark is-outlined"
58
- href="{% url 'django_ledger:invoice-list-year' year=next_year.year entity_slug=view.kwargs.entity_slug %}">
59
- <span class="icon is-small">{% icon 'ant-design:right-circle-outlined' 16 %}</span>
60
- <span>{{ next_year | date:'Y' }}</span>
79
+ <a href="?page={{ page_obj.next_page_number }}"
80
+ class="button is-small is-dark is-outlined">
81
+ <span class="icon is-small">{% icon 'ant-design:right-circle-outlined' 24 %}</span>
61
82
  </a>
62
83
  </div>
63
84
  {% endif %}
64
85
 
65
- {% if page_obj %}
66
- {% if page_obj.has_previous %}
67
- <div class="level-item">
68
- <a href="?page={{ page_obj.previous_page_number }}"
69
- class="button is-small is-dark is-outlined">
70
- <span class="icon is-small">{% icon 'ant-design:left-circle-outlined' 24 %}</span>
71
- </a>
72
- </div>
73
- {% endif %}
74
- <div class="level-item">
75
- <p class="is-italic">page {{ page_obj.number }}
76
- of {{ page_obj.paginator.num_pages }}</p>
77
- </div>
78
- {% if page_obj.has_next %}
79
- <div class="level-item">
80
- <a href="?page={{ page_obj.next_page_number }}"
81
- class="button is-small is-dark is-outlined">
82
- <span class="icon is-small">{% icon 'ant-design:right-circle-outlined' 24 %}</span>
83
- </a>
84
- </div>
85
- {% endif %}
86
-
87
- {% endif %}
88
- </div>
86
+ {% endif %}
89
87
  </div>
90
- {% invoice_table invoice_list %}
91
- {% if year %}
92
- <h5 class="is-size-5">Go to month:</h5>
93
- <p>
94
- <a href="{% url 'django_ledger:invoice-list' entity_slug=view.kwargs.entity_slug %}">
95
- {% trans 'All' %} |
88
+ </div>
89
+ {% invoice_table invoice_list %}
90
+ {% if year %}
91
+ <h5 class="is-size-5">Go to month:</h5>
92
+ <p>
93
+ <a href="{% url 'django_ledger:invoice-list' entity_slug=view.kwargs.entity_slug %}">
94
+ {% trans 'All' %} |
95
+ </a>
96
+ {% for date in date_list %}
97
+ <a href="{% url 'django_ledger:invoice-list-month' entity_slug=view.kwargs.entity_slug year=date.year month=date.month %}">
98
+ {{ date | date:'F' }} {% if not forloop.last %}>{% endif %}
96
99
  </a>
97
- {% for date in date_list %}
98
- <a href="{% url 'django_ledger:invoice-list-month' entity_slug=view.kwargs.entity_slug year=date.year month=date.month %}">
99
- {{ date | date:'F' }} {% if not forloop.last %}>{% endif %}
100
- </a>
101
- {% endfor %}
102
- </p>
103
- {% else %}
104
- <h5 class="is-size-5">Go to year:</h5>
105
- <p>
106
- <a href="{% url 'django_ledger:invoice-list' entity_slug=view.kwargs.entity_slug %}">
107
- {% trans 'All' %} |
100
+ {% endfor %}
101
+ </p>
102
+ {% else %}
103
+ <h5 class="is-size-5">Go to year:</h5>
104
+ <p>
105
+ <a href="{% url 'django_ledger:invoice-list' entity_slug=view.kwargs.entity_slug %}">
106
+ {% trans 'All' %} |
107
+ </a>
108
+ {% for date in date_list %}
109
+ <a href="{% url 'django_ledger:invoice-list-year' entity_slug=view.kwargs.entity_slug year=date.year %}">
110
+ {{ date.year }} {% if not forloop.last %}>{% endif %}
108
111
  </a>
109
- {% for date in date_list %}
110
- <a href="{% url 'django_ledger:invoice-list-year' entity_slug=view.kwargs.entity_slug year=date.year %}">
111
- {{ date.year }} {% if not forloop.last %}>{% endif %}
112
- </a>
113
- {% endfor %}
114
- </p>
115
- {% endif %}
116
-
117
- </div>
112
+ {% endfor %}
113
+ </p>
114
+ {% endif %}
118
115
  </div>
119
116
  {% endblock %}
@@ -738,7 +738,7 @@ def navigation_menu(context, style):
738
738
  {
739
739
  'type': 'link',
740
740
  'title': 'Chart of Accounts',
741
- 'url': reverse('django_ledger:account-list', kwargs={'entity_slug': ENTITY_SLUG})
741
+ 'url': reverse('django_ledger:coa-list', kwargs={'entity_slug': ENTITY_SLUG})
742
742
  },
743
743
  {
744
744
  'type': 'link',
@@ -6,6 +6,7 @@ from random import randint, choice
6
6
  from typing import Optional, Literal
7
7
  from zoneinfo import ZoneInfo
8
8
 
9
+ from django.conf import settings
9
10
  from django.contrib.auth import get_user_model
10
11
  from django.core.exceptions import ObjectDoesNotExist
11
12
  from django.test import TestCase
@@ -15,7 +16,6 @@ from django.utils.timezone import get_default_timezone
15
16
  from django_ledger.io.io_generator import EntityDataGenerator
16
17
  from django_ledger.models import JournalEntryModel, LedgerModel, TransactionModel, AccountModel, AccountModelQuerySet
17
18
  from django_ledger.models.entity import EntityModel, EntityModelQuerySet, UserModel
18
- from django.conf import settings
19
19
 
20
20
  UserModel = get_user_model()
21
21
 
@@ -138,6 +138,28 @@ class DjangoLedgerBaseTest(TestCase):
138
138
  return choice(self.ENTITY_MODEL_QUERYSET)
139
139
  raise ValueError('EntityModels have not been populated.')
140
140
 
141
+ def create_entity_model(self, use_accrual_method: bool = False, fy_start_month: int = 1) -> EntityModel:
142
+ """
143
+ Creates a new blank EntityModel for testing purposes.
144
+
145
+ Parameters
146
+ ----------
147
+ use_accrual_method: bool
148
+ Whether to use the accrual method. Defaults to False.
149
+ fy_start_month:
150
+ The month to start the financial year. Defaults to 1 (January).
151
+
152
+ Returns
153
+ -------
154
+ EntityModel
155
+ """
156
+ return EntityModel.create_entity(
157
+ name='Testing Inc-{randint(100000, 999999)',
158
+ use_accrual_method=use_accrual_method,
159
+ fy_start_month=fy_start_month,
160
+ admin=self.user_model
161
+ )
162
+
141
163
  @classmethod
142
164
  def create_entity_models(cls, save=True, n: int = 5):
143
165
  cls.refresh_test_data(n)
@@ -0,0 +1,16 @@
1
+ from django_ledger.models import EntityModelValidationError, AccountModel
2
+ from django_ledger.tests.base import DjangoLedgerBaseTest
3
+
4
+
5
+ class AccountModelTests(DjangoLedgerBaseTest):
6
+
7
+ def test_no_default_coa(self):
8
+ entity_model = self.create_entity_model()
9
+ self.assertFalse(entity_model.has_default_coa(), msg='New entities do not had default coa')
10
+
11
+ with self.assertRaises(EntityModelValidationError, msg='New entities do not have default coa'):
12
+ entity_model.get_default_coa()
13
+
14
+ self.assertEqual(entity_model.get_default_coa(raise_exception=False),
15
+ None,
16
+ msg='No exception should be raised when raise_exception is False')
@@ -0,0 +1,46 @@
1
+ from django_ledger.models import EntityModelValidationError, ChartOfAccountModel
2
+ from django_ledger.tests.base import DjangoLedgerBaseTest
3
+
4
+
5
+ class ChartOfAccountsTests(DjangoLedgerBaseTest):
6
+
7
+ def test_no_default_coa(self):
8
+ entity_model = self.create_entity_model()
9
+ self.assertFalse(entity_model.has_default_coa(), msg='New entities do not had default coa')
10
+
11
+ with self.assertRaises(EntityModelValidationError, msg='New entities do not have default coa'):
12
+ entity_model.get_default_coa()
13
+
14
+ self.assertEqual(entity_model.get_default_coa(raise_exception=False), None,
15
+ msg='No exception should be raised when raise_exception is False')
16
+
17
+ def test_set_default_coa(self):
18
+ entity_model = self.create_entity_model()
19
+ coa_model = entity_model.create_chart_of_accounts(coa_name='Now CoA For Testing', assign_as_default=False)
20
+ self.assertTrue(isinstance(coa_model, ChartOfAccountModel))
21
+ self.assertFalse(entity_model.has_default_coa())
22
+
23
+ def test_create_coa(self):
24
+ entity_model = self.create_entity_model()
25
+ coa_model = entity_model.create_chart_of_accounts(coa_name='Now CoA For Testing', assign_as_default=False)
26
+
27
+ account_model_qs = coa_model.accountmodel_set.all()
28
+
29
+ account_count = account_model_qs.count()
30
+ self.assertTrue(account_count, 7)
31
+
32
+ ROOT_ACCOUNT_CODES = [
33
+ '00000000', # root_account
34
+ '01000000', # asset account root
35
+ '02000000', # liability accounts root
36
+ '03000000', # capital accounts root
37
+ '04000000', # income accounts root
38
+ '05000000', # cogs accounts root
39
+ '06000000', # expenses accounts root
40
+ ]
41
+
42
+ for account in account_model_qs:
43
+ self.assertTrue(account.code in ROOT_ACCOUNT_CODES)
44
+
45
+ # todo: cannot transact on root account
46
+ # todo: validate parent/child relationship...
@@ -3,15 +3,30 @@ from django.urls import path
3
3
  from django_ledger import views
4
4
 
5
5
  urlpatterns = [
6
+
7
+ # NO COA SLUG USES DEFAULT COA....
8
+ path('<slug:entity_slug>/create/',
9
+ views.AccountModelCreateView.as_view(),
10
+ name='account-create'),
6
11
  path('<slug:entity_slug>/list/',
7
12
  views.AccountModelListView.as_view(),
8
13
  name='account-list'),
9
14
  path('<slug:entity_slug>/list/active/',
10
15
  views.AccountModelListView.as_view(active_only=True),
11
16
  name='account-list-active'),
12
- path('<slug:entity_slug>/create/',
17
+
18
+ # EXPLICIT COA...
19
+ path('<slug:entity_slug>/<slug:coa_slug>/create/',
13
20
  views.AccountModelCreateView.as_view(),
14
- name='account-create'),
21
+ name='account-create-coa'),
22
+ path('<slug:entity_slug>/<slug:coa_slug>/list/',
23
+ views.AccountModelListView.as_view(),
24
+ name='account-list-coa'),
25
+ path('<slug:entity_slug>/<slug:coa_slug>/list/active/',
26
+ views.AccountModelListView.as_view(active_only=True),
27
+ name='account-list-active-coa'),
28
+
29
+ # Account Transaction Detail....
15
30
  path('<slug:entity_slug>/update/<uuid:account_pk>/',
16
31
  views.AccountModelUpdateView.as_view(),
17
32
  name='account-update'),
@@ -31,7 +46,7 @@ urlpatterns = [
31
46
  views.AccountModelDateDetailView.as_view(),
32
47
  name='account-detail-date'),
33
48
 
34
- # Actions...
49
+ # Account Actions...
35
50
  path('<slug:entity_slug>/action/<uuid:account_pk>/activate/',
36
51
  views.AccountModelModelActionView.as_view(action_name='activate'),
37
52
  name='account-action-activate'),
@@ -3,5 +3,25 @@ from django.urls import path
3
3
  from django_ledger import views
4
4
 
5
5
  urlpatterns = [
6
- path('<slug:entity_slug>/<slug:coa_slug>/update/', views.ChartOfAccountsUpdateView.as_view(), name='coa-update'),
6
+ path('<slug:entity_slug>/list/',
7
+ views.ChartOfAccountsListView.as_view(),
8
+ name='coa-list'),
9
+ path('<slug:entity_slug>/detail/<slug:coa_slug>/',
10
+ views.ChartOfAccountsListView.as_view(),
11
+ name='coa-detail'),
12
+ path('<slug:entity_slug>/update/<slug:coa_slug>/',
13
+ views.ChartOfAccountsUpdateView.as_view(),
14
+ name='coa-update'),
15
+
16
+ # ACTIONS....
17
+ path('<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/',
18
+ views.CharOfAccountModelActionView.as_view(action_name='mark_as_default'),
19
+ name='coa-action-mark-as-default'),
20
+ path('<slug:entity_slug>/action/<slug:coa_slug>/mark-as-active/',
21
+ views.CharOfAccountModelActionView.as_view(action_name='mark_as_active'),
22
+ name='coa-action-mark-as-active'),
23
+ path('<slug:entity_slug>/action/<slug:coa_slug>/mark-as-inactive/',
24
+ views.CharOfAccountModelActionView.as_view(action_name='mark_as_inactive'),
25
+ name='coa-action-mark-as-inactive'),
26
+
7
27
  ]
@@ -30,13 +30,31 @@ class BaseAccountModelViewQuerySetMixIn:
30
30
 
31
31
  def get_queryset(self):
32
32
  if self.queryset is None:
33
- self.queryset = AccountModel.objects.for_entity(
33
+ qs = AccountModel.objects.for_entity(
34
34
  entity_slug=self.kwargs['entity_slug'],
35
35
  user_model=self.request.user,
36
- ).select_related('coa_model', 'coa_model__entity').order_by(
36
+ ).select_related(
37
+ 'coa_model',
38
+ 'coa_model__entity'
39
+ ).order_by(
37
40
  'coa_model', 'role', 'code').not_coa_root()
41
+
42
+ if self.kwargs.get('coa_slug'):
43
+ qs = qs.filter(coa_model__slug__exact=self.kwargs.get('coa_slug'))
44
+
45
+ self.queryset = qs
38
46
  return super().get_queryset()
39
47
 
48
+ def get_context_data(self, *args, **kwargs):
49
+ context = super().get_context_data(*args, **kwargs)
50
+ entity_model = self.get_authorized_entity_instance()
51
+ if self.kwargs.get('coa_slug'):
52
+ coa_model_qs = entity_model.chartofaccountmodel_set.all()
53
+ context['coa_model'] = get_object_or_404(coa_model_qs, slug__exact=self.kwargs['coa_slug'])
54
+ else:
55
+ context['coa_model'] = entity_model.default_coa
56
+ return context
57
+
40
58
 
41
59
  # Account Views ----
42
60
  class AccountModelListView(DjangoLedgerSecurityMixIn, BaseAccountModelViewQuerySetMixIn, ListView):
@@ -105,6 +123,11 @@ class AccountModelCreateView(DjangoLedgerSecurityMixIn, BaseAccountModelViewQuer
105
123
  **self.get_form_kwargs()
106
124
  )
107
125
 
126
+ def get_context_data(self, *args, **kwargs):
127
+ context = super().get_context_data(*args, **kwargs)
128
+ context['header_subtitle'] = f'CoA: {context["coa_model"].name}'
129
+ return context
130
+
108
131
  def form_valid(self, form):
109
132
  EntityModel = lazy_loader.get_entity_model()
110
133
  entity_model_qs = EntityModel.objects.for_user(user_model=self.request.user).select_related('default_coa')
@@ -115,7 +138,7 @@ class AccountModelCreateView(DjangoLedgerSecurityMixIn, BaseAccountModelViewQuer
115
138
  entity_model.create_chart_of_accounts(assign_as_default=True, commit=True)
116
139
 
117
140
  coa_model = entity_model.default_coa
118
- coa_model.create_account(account_model=account_model)
141
+ coa_model.allocate_account(account_model=account_model)
119
142
  return HttpResponseRedirect(self.get_success_url())
120
143
 
121
144
  def get_success_url(self):
@@ -5,10 +5,13 @@ Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
5
5
  Contributions to this module:
6
6
  Miguel Sanda <msanda@arrobalytics.com>
7
7
  """
8
-
8
+ from django.contrib import messages
9
+ from django.core.exceptions import ImproperlyConfigured, ValidationError
10
+ from django.db.models import Count, Q
9
11
  from django.urls import reverse
10
12
  from django.utils.translation import gettext_lazy as _
11
- from django.views.generic import UpdateView
13
+ from django.views.generic import UpdateView, ListView, RedirectView
14
+ from django.views.generic.detail import SingleObjectMixin
12
15
 
13
16
  from django_ledger.forms.coa import ChartOfAccountsModelUpdateForm
14
17
  from django_ledger.models.coa import ChartOfAccountModel
@@ -23,14 +26,49 @@ class ChartOfAccountsModelModelViewQuerySetMixIn:
23
26
  self.queryset = ChartOfAccountModel.objects.for_entity(
24
27
  entity_slug=self.kwargs['entity_slug'],
25
28
  user_model=self.request.user,
26
- ).select_related('entity')
29
+ ).select_related('entity').order_by('-updated')
27
30
  return super().get_queryset()
28
31
 
29
32
 
33
+ class ChartOfAccountsListView(DjangoLedgerSecurityMixIn, ChartOfAccountsModelModelViewQuerySetMixIn, ListView):
34
+ template_name = 'django_ledger/chart_of_accounts/coa_list.html'
35
+ extra_context = {
36
+ 'header_title': _('Chart of Account List'),
37
+ }
38
+ context_object_name = 'coa_list'
39
+
40
+ def get_context_data(self, *, object_list=None, **kwargs):
41
+ context = super().get_context_data(object_list=None, **kwargs)
42
+ context['header_subtitle'] = self.AUTHORIZED_ENTITY_MODEL.name
43
+ context['header_subtitle_icon'] = 'gravity-ui:hierarchy'
44
+ return context
45
+
46
+ def get_queryset(self):
47
+ qs = super().get_queryset()
48
+ return qs.annotate(
49
+ accountmodel_total__count=Count(
50
+ 'accountmodel',
51
+ # excludes coa root accounts...
52
+ filter=Q(accountmodel__depth__gt=2)
53
+ ),
54
+ accountmodel_locked__count=Count(
55
+ 'accountmodel',
56
+ # excludes coa root accounts...
57
+ filter=Q(accountmodel__depth__gt=2) & Q(accountmodel__locked=True)
58
+ ),
59
+ accountmodel_active__count=Count(
60
+ 'accountmodel',
61
+ # excludes coa root accounts...
62
+ filter=Q(accountmodel__depth__gt=2) & Q(accountmodel__active=True)
63
+ ),
64
+
65
+ )
66
+
67
+
30
68
  class ChartOfAccountsUpdateView(DjangoLedgerSecurityMixIn, ChartOfAccountsModelModelViewQuerySetMixIn, UpdateView):
31
69
  context_object_name = 'coa'
32
70
  slug_url_kwarg = 'coa_slug'
33
- template_name = 'django_ledger/code_of_accounts/coa_update.html'
71
+ template_name = 'django_ledger/chart_of_accounts/coa_update.html'
34
72
  form_class = ChartOfAccountsModelUpdateForm
35
73
 
36
74
  def get_context_data(self, **kwargs):
@@ -45,3 +83,40 @@ class ChartOfAccountsUpdateView(DjangoLedgerSecurityMixIn, ChartOfAccountsModelM
45
83
  kwargs={
46
84
  'entity_slug': entity_slug
47
85
  })
86
+
87
+
88
+ # todo: centralize this functionality into a separate class for ALL Action views...
89
+ class CharOfAccountModelActionView(DjangoLedgerSecurityMixIn,
90
+ RedirectView,
91
+ ChartOfAccountsModelModelViewQuerySetMixIn,
92
+ SingleObjectMixin):
93
+ http_method_names = ['get']
94
+ slug_url_kwarg = 'coa_slug'
95
+ action_name = None
96
+ commit = True
97
+
98
+ def get_redirect_url(self, *args, **kwargs):
99
+ return reverse('django_ledger:coa-list',
100
+ kwargs={
101
+ 'entity_slug': kwargs['entity_slug']
102
+ })
103
+
104
+ def get(self, request, *args, **kwargs):
105
+ kwargs['user_model'] = self.request.user
106
+ if not self.action_name:
107
+ raise ImproperlyConfigured('View attribute action_name is required.')
108
+ response = super(CharOfAccountModelActionView, self).get(request, *args, **kwargs)
109
+ coa_model: ChartOfAccountModel = self.get_object()
110
+
111
+ try:
112
+ getattr(coa_model, self.action_name)(commit=self.commit, **kwargs)
113
+ messages.add_message(request, level=messages.SUCCESS, extra_tags='is-success',
114
+ message=_('Successfully updated {} Default Chart of Account to '.format(
115
+ self.AUTHORIZED_ENTITY_MODEL.name) +
116
+ '{}'.format(coa_model.name)))
117
+ except ValidationError as e:
118
+ messages.add_message(request,
119
+ message=e.message,
120
+ level=messages.ERROR,
121
+ extra_tags='is-danger')
122
+ return response
@@ -304,6 +304,9 @@ class DjangoLedgerSecurityMixIn(PermissionRequiredMixin):
304
304
  user_model=self.request.user).only(
305
305
  'uuid', 'slug', 'name', 'default_coa', 'admin')
306
306
 
307
+ def get_authorized_entity_instance(self) -> Optional[EntityModel]:
308
+ return self.AUTHORIZED_ENTITY_MODEL
309
+
307
310
  def has_permission(self):
308
311
  if self.request.user.is_superuser:
309
312
  if 'entity_slug' in self.kwargs:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-ledger
3
- Version: 0.5.6.3
3
+ Version: 0.5.6.4
4
4
  Summary: Bookkeeping & Financial analysis backend for Django. Balance Sheet, Income Statements, Chart of Accounts, Entities
5
5
  Author-email: Miguel Sanda <msanda@arrobalytics.com>
6
6
  Maintainer-email: Miguel Sanda <msanda@arrobalytics.com>