django-ledger 0.5.6.2__py3-none-any.whl → 0.5.6.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-ledger might be problematic. Click here for more details.
- django_ledger/__init__.py +1 -1
- django_ledger/admin/coa.py +3 -3
- django_ledger/forms/account.py +2 -0
- django_ledger/forms/coa.py +1 -6
- django_ledger/forms/transactions.py +3 -1
- django_ledger/io/io_core.py +95 -79
- django_ledger/io/io_digest.py +4 -5
- django_ledger/io/io_generator.py +5 -4
- django_ledger/io/io_library.py +241 -16
- django_ledger/io/roles.py +32 -10
- django_ledger/migrations/0015_remove_chartofaccountmodel_locked_and_more.py +22 -0
- django_ledger/models/accounts.py +13 -9
- django_ledger/models/bill.py +3 -3
- django_ledger/models/closing_entry.py +39 -28
- django_ledger/models/coa.py +244 -35
- django_ledger/models/entity.py +119 -51
- django_ledger/models/invoice.py +3 -2
- django_ledger/models/journal_entry.py +8 -4
- django_ledger/models/ledger.py +63 -11
- django_ledger/models/mixins.py +2 -2
- django_ledger/models/transactions.py +20 -11
- django_ledger/report/balance_sheet.py +1 -1
- django_ledger/report/cash_flow_statement.py +5 -5
- django_ledger/report/core.py +2 -2
- django_ledger/report/income_statement.py +2 -2
- django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
- django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +10 -11
- django_ledger/templates/django_ledger/account/account_create.html +17 -11
- django_ledger/templates/django_ledger/account/account_list.html +12 -9
- django_ledger/templates/django_ledger/account/tags/account_txs_table.html +6 -1
- django_ledger/templates/django_ledger/account/tags/accounts_table.html +97 -93
- django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html +17 -0
- django_ledger/templates/django_ledger/{code_of_accounts → chart_of_accounts}/coa_update.html +1 -4
- django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html +74 -0
- django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +1 -1
- django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +6 -6
- django_ledger/templates/django_ledger/includes/widget_ic.html +1 -1
- django_ledger/templates/django_ledger/invoice/invoice_list.html +91 -94
- django_ledger/templates/django_ledger/journal_entry/includes/card_journal_entry.html +16 -7
- django_ledger/templates/django_ledger/journal_entry/je_detail.html +1 -1
- django_ledger/templates/django_ledger/ledger/tags/ledgers_table.html +10 -0
- django_ledger/templatetags/django_ledger.py +9 -8
- django_ledger/tests/base.py +134 -8
- django_ledger/tests/test_accounts.py +16 -0
- django_ledger/tests/test_auth.py +5 -7
- django_ledger/tests/test_bill.py +1 -1
- django_ledger/tests/test_chart_of_accounts.py +46 -0
- django_ledger/tests/test_closing_entry.py +16 -19
- django_ledger/tests/test_entity.py +3 -3
- django_ledger/tests/test_io.py +192 -2
- django_ledger/tests/test_transactions.py +290 -0
- django_ledger/urls/account.py +18 -3
- django_ledger/urls/chart_of_accounts.py +21 -1
- django_ledger/urls/ledger.py +7 -0
- django_ledger/views/account.py +29 -4
- django_ledger/views/coa.py +79 -4
- django_ledger/views/djl_api.py +4 -1
- django_ledger/views/journal_entry.py +1 -1
- django_ledger/views/mixins.py +3 -0
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/METADATA +1 -1
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/RECORD +65 -59
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/AUTHORS.md +0 -0
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/LICENSE +0 -0
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/WHEEL +0 -0
- {django_ledger-0.5.6.2.dist-info → django_ledger-0.5.6.4.dist-info}/top_level.txt +0 -0
|
@@ -4,116 +4,113 @@
|
|
|
4
4
|
{% load django_ledger %}
|
|
5
5
|
|
|
6
6
|
{% block view_content %}
|
|
7
|
-
<div class="
|
|
8
|
-
<div class="
|
|
9
|
-
<div class="level">
|
|
10
|
-
<div class="level-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<
|
|
16
|
-
|
|
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
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
64
|
+
{% if page_obj %}
|
|
65
|
+
{% if page_obj.has_previous %}
|
|
47
66
|
<div class="level-item">
|
|
48
|
-
<a
|
|
49
|
-
|
|
50
|
-
<span class="icon is-small">{% icon 'ant-design:left-circle-outlined'
|
|
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
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
<span class="icon is-small">{% icon 'ant-design:right-circle-outlined'
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<a href="{% url 'django_ledger:invoice-list' entity_slug=view.kwargs.entity_slug %}">
|
|
107
|
-
{%
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
</a>
|
|
113
|
-
{% endfor %}
|
|
114
|
-
</p>
|
|
115
|
-
{% endif %}
|
|
116
|
-
|
|
117
|
-
</div>
|
|
112
|
+
{% endfor %}
|
|
113
|
+
</p>
|
|
114
|
+
{% endif %}
|
|
118
115
|
</div>
|
|
119
116
|
{% endblock %}
|
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
<div class="card-header-title is-size-2">{% trans 'Journal Entry Detail' %}</div>
|
|
7
7
|
</div>
|
|
8
8
|
<div class="card-content">
|
|
9
|
-
<h2 class="is-size-2">{
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
<h2 class="is-size-2 has-text-success">{{ journal_entry.je_number }}</h2>
|
|
10
|
+
|
|
11
|
+
{% if journal_entry.entity_unit %}
|
|
12
|
+
<h2 class="is-size-2 has-text-weight-light">{{ journal_entry.entity_unit.name }}</h2>
|
|
12
13
|
{% endif %}
|
|
14
|
+
|
|
15
|
+
<h2 class="is-size-2">{% trans 'Date' %}: {{ journal_entry.timestamp | date }}</h2>
|
|
16
|
+
|
|
17
|
+
|
|
13
18
|
<h3 class="is-size-4">{% trans 'Posted' %}:
|
|
14
19
|
{% if journal_entry.is_posted %}
|
|
15
20
|
<span class="icon has-text-success">{% icon 'ant-design:check-circle-filled' 24 %}</span>
|
|
@@ -34,16 +39,20 @@
|
|
|
34
39
|
</div>
|
|
35
40
|
<div class="card-footer">
|
|
36
41
|
{% if journal_entry.can_lock %}
|
|
37
|
-
<a href="{{ journal_entry.get_lock_url }}"
|
|
42
|
+
<a href="{{ journal_entry.get_lock_url }}"
|
|
43
|
+
class="card-footer-item has-text-success has-text-weight-bold">{% trans 'Lock' %}</a>
|
|
38
44
|
{% endif %}
|
|
39
45
|
{% if journal_entry.can_unlock %}
|
|
40
|
-
<a href="{{ journal_entry.get_unlock_url }}"
|
|
46
|
+
<a href="{{ journal_entry.get_unlock_url }}"
|
|
47
|
+
class="card-footer-item has-text-warning has-text-weight-bold">{% trans 'UnLock' %}</a>
|
|
41
48
|
{% endif %}
|
|
42
49
|
{% if journal_entry.can_post %}
|
|
43
|
-
<a href="{{ journal_entry.get_post_url }}"
|
|
50
|
+
<a href="{{ journal_entry.get_post_url }}"
|
|
51
|
+
class="card-footer-item has-text-success has-text-weight-bold">{% trans 'Post' %}</a>
|
|
44
52
|
{% endif %}
|
|
45
53
|
{% if journal_entry.can_unpost %}
|
|
46
|
-
<a href="{{ journal_entry.get_unpost_url }}"
|
|
54
|
+
<a href="{{ journal_entry.get_unpost_url }}"
|
|
55
|
+
class="card-footer-item has-text-danger has-text-weight-bold">{% trans 'UnPost' %}</a>
|
|
47
56
|
{% endif %}
|
|
48
57
|
|
|
49
58
|
</div>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
</a>
|
|
22
22
|
<a class="button is-dark"
|
|
23
23
|
href="{% url 'django_ledger:je-list' entity_slug=view.kwargs.entity_slug ledger_pk=view.kwargs.ledger_pk %}">
|
|
24
|
-
{% trans '
|
|
24
|
+
{% trans 'Ledger List' %}
|
|
25
25
|
</a>
|
|
26
26
|
{% if journal_entry.can_unlock %}
|
|
27
27
|
<a class="button is-warning"
|
|
@@ -115,6 +115,16 @@
|
|
|
115
115
|
<a href="{% url 'django_ledger:ledger-action-unpost' entity_slug=entity_slug ledger_pk=ledger.uuid %}"
|
|
116
116
|
class="dropdown-item has-text-warning has-text-weight-bold">{% trans 'UnPost' %}</a>
|
|
117
117
|
{% endif %}
|
|
118
|
+
|
|
119
|
+
{% if ledger.can_hide %}
|
|
120
|
+
<a href="{% url 'django_ledger:ledger-action-hide' entity_slug=entity_slug ledger_pk=ledger.uuid %}"
|
|
121
|
+
class="dropdown-item has-text-warning has-text-weight-bold">{% trans 'Hide' %}</a>
|
|
122
|
+
{% endif %}
|
|
123
|
+
{% if ledger.can_unhide %}
|
|
124
|
+
<a href="{% url 'django_ledger:ledger-action-unhide' entity_slug=entity_slug ledger_pk=ledger.uuid %}"
|
|
125
|
+
class="dropdown-item has-text-danger has-text-weight-bold">{% trans 'UnHide' %}</a>
|
|
126
|
+
{% endif %}
|
|
127
|
+
|
|
118
128
|
{% if ledger.can_delete %}
|
|
119
129
|
<a href="{% url 'django_ledger:ledger-delete' entity_slug=entity_slug ledger_pk=ledger.uuid %}"
|
|
120
130
|
class="dropdown-item has-text-danger has-text-weight-bold">{% trans 'Delete' %}</a>
|
|
@@ -48,6 +48,14 @@ def absolute(value):
|
|
|
48
48
|
return abs(value)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
@register.filter(name='reverse_sign')
|
|
52
|
+
def reverse_sign(value):
|
|
53
|
+
if value:
|
|
54
|
+
if isinstance(value, str):
|
|
55
|
+
value = float(value)
|
|
56
|
+
return -value
|
|
57
|
+
|
|
58
|
+
|
|
51
59
|
@register.filter(name='currency_format')
|
|
52
60
|
def currency_format(value):
|
|
53
61
|
if not value:
|
|
@@ -61,13 +69,6 @@ def percentage(value):
|
|
|
61
69
|
return '{0:,.2f}%'.format(value * 100)
|
|
62
70
|
|
|
63
71
|
|
|
64
|
-
@register.filter(name='reverse_sing')
|
|
65
|
-
def reverse_sign(value: float):
|
|
66
|
-
if value:
|
|
67
|
-
return -value
|
|
68
|
-
return 0
|
|
69
|
-
|
|
70
|
-
|
|
71
72
|
@register.filter(name='last_four')
|
|
72
73
|
def last_four(value: str):
|
|
73
74
|
if value:
|
|
@@ -737,7 +738,7 @@ def navigation_menu(context, style):
|
|
|
737
738
|
{
|
|
738
739
|
'type': 'link',
|
|
739
740
|
'title': 'Chart of Accounts',
|
|
740
|
-
'url': reverse('django_ledger:
|
|
741
|
+
'url': reverse('django_ledger:coa-list', kwargs={'entity_slug': ENTITY_SLUG})
|
|
741
742
|
},
|
|
742
743
|
{
|
|
743
744
|
'type': 'link',
|
django_ledger/tests/base.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
from datetime import date, timedelta, datetime
|
|
1
|
+
from datetime import date, timedelta, datetime, time
|
|
2
2
|
from decimal import Decimal
|
|
3
3
|
from itertools import cycle
|
|
4
4
|
from logging import getLogger, DEBUG
|
|
5
5
|
from random import randint, choice
|
|
6
|
-
from typing import Optional
|
|
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
|
|
@@ -13,7 +14,8 @@ from django.test.client import Client
|
|
|
13
14
|
from django.utils.timezone import get_default_timezone
|
|
14
15
|
|
|
15
16
|
from django_ledger.io.io_generator import EntityDataGenerator
|
|
16
|
-
from django_ledger.models
|
|
17
|
+
from django_ledger.models import JournalEntryModel, LedgerModel, TransactionModel, AccountModel, AccountModelQuerySet
|
|
18
|
+
from django_ledger.models.entity import EntityModel, EntityModelQuerySet, UserModel
|
|
17
19
|
|
|
18
20
|
UserModel = get_user_model()
|
|
19
21
|
|
|
@@ -44,11 +46,11 @@ class DjangoLedgerBaseTest(TestCase):
|
|
|
44
46
|
cls.USERNAME: str = 'testuser'
|
|
45
47
|
cls.PASSWORD: str = 'NeverUseThisPassword12345'
|
|
46
48
|
cls.USER_EMAIL: str = 'testuser@djangoledger.com'
|
|
47
|
-
cls.N: int =
|
|
49
|
+
cls.N: int = 1
|
|
48
50
|
|
|
49
51
|
cls.DAYS_FWD: int = randint(180, 180 * 3)
|
|
50
52
|
cls.TZ = get_default_timezone()
|
|
51
|
-
cls.START_DATE = cls.get_random_date()
|
|
53
|
+
cls.START_DATE = cls.get_random_date(as_datetime=True)
|
|
52
54
|
|
|
53
55
|
cls.CLIENT = Client(enforce_csrf_checks=False)
|
|
54
56
|
|
|
@@ -70,16 +72,28 @@ class DjangoLedgerBaseTest(TestCase):
|
|
|
70
72
|
cls.populate_entity_models()
|
|
71
73
|
|
|
72
74
|
@classmethod
|
|
73
|
-
def get_random_date(cls, as_datetime: bool =
|
|
75
|
+
def get_random_date(cls, as_datetime: bool = False) -> date:
|
|
74
76
|
dt = date(
|
|
75
77
|
year=choice(range(1990, 2020)),
|
|
76
78
|
month=choice(range(1, 13)),
|
|
77
79
|
day=choice(range(1, 28))
|
|
78
80
|
)
|
|
79
81
|
if as_datetime:
|
|
82
|
+
if not settings.USE_TZ:
|
|
83
|
+
return datetime.combine(
|
|
84
|
+
dt,
|
|
85
|
+
time(
|
|
86
|
+
hour=randint(1, 23),
|
|
87
|
+
minute=randint(1, 59)
|
|
88
|
+
),
|
|
89
|
+
)
|
|
80
90
|
return datetime.combine(
|
|
81
|
-
dt,
|
|
82
|
-
|
|
91
|
+
dt,
|
|
92
|
+
time(
|
|
93
|
+
hour=randint(1, 23),
|
|
94
|
+
minute=randint(1, 59)
|
|
95
|
+
),
|
|
96
|
+
tzinfo=ZoneInfo(settings.TIME_ZONE)
|
|
83
97
|
)
|
|
84
98
|
return dt
|
|
85
99
|
|
|
@@ -124,6 +138,28 @@ class DjangoLedgerBaseTest(TestCase):
|
|
|
124
138
|
return choice(self.ENTITY_MODEL_QUERYSET)
|
|
125
139
|
raise ValueError('EntityModels have not been populated.')
|
|
126
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
|
+
|
|
127
163
|
@classmethod
|
|
128
164
|
def create_entity_models(cls, save=True, n: int = 5):
|
|
129
165
|
cls.refresh_test_data(n)
|
|
@@ -152,3 +188,93 @@ class DjangoLedgerBaseTest(TestCase):
|
|
|
152
188
|
|
|
153
189
|
def get_random_draft_date(self):
|
|
154
190
|
return self.START_DATE + timedelta(days=randint(0, 365))
|
|
191
|
+
|
|
192
|
+
def get_random_account(self,
|
|
193
|
+
entity_model: EntityModel,
|
|
194
|
+
balance_type: Literal['credit', 'debit', None] = None) -> AccountModel:
|
|
195
|
+
"""
|
|
196
|
+
Returns 1 random AccountModel with the specified balance_type.
|
|
197
|
+
"""
|
|
198
|
+
account_qs: AccountModelQuerySet = entity_model.get_coa_accounts(active=True)
|
|
199
|
+
account_qs = account_qs.filter(balance_type=balance_type) if balance_type else account_qs
|
|
200
|
+
# limits the queryset in case of large querysets...
|
|
201
|
+
return choice(account_qs[:50])
|
|
202
|
+
|
|
203
|
+
def get_random_ledger(self,
|
|
204
|
+
entity_model: EntityModel,
|
|
205
|
+
qs_limit: int = 100,
|
|
206
|
+
posted: bool = True) -> LedgerModel:
|
|
207
|
+
"""
|
|
208
|
+
Returns 1 random LedgerModel object.
|
|
209
|
+
"""
|
|
210
|
+
ledger_model_qs = entity_model.get_ledgers(
|
|
211
|
+
posted=posted).filter(
|
|
212
|
+
journal_entries__count__gt=0
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# no need to check because data generator will always populate an entity with sample data.
|
|
216
|
+
# if not ledger_model.exists():
|
|
217
|
+
# for i in range(3):
|
|
218
|
+
# LedgerModel.objects.create(
|
|
219
|
+
# name=f"{i}Example Ledger {randint(10000, 99999)}",
|
|
220
|
+
# ledger_xid=f"{i}example-ledger-xid-{randint(10000, 99999)}",
|
|
221
|
+
# entity=entity_model,
|
|
222
|
+
# )
|
|
223
|
+
|
|
224
|
+
# limits the queryset in case of large querysets...
|
|
225
|
+
return choice(ledger_model_qs[:qs_limit])
|
|
226
|
+
|
|
227
|
+
def get_random_je(self,
|
|
228
|
+
entity_model: EntityModel,
|
|
229
|
+
ledger_model: Optional[LedgerModel] = None,
|
|
230
|
+
posted: bool = True,
|
|
231
|
+
qs_limit: int = 100
|
|
232
|
+
) -> JournalEntryModel:
|
|
233
|
+
""".
|
|
234
|
+
Returns 1 random JournalEntryModel object.
|
|
235
|
+
"""
|
|
236
|
+
if not ledger_model:
|
|
237
|
+
ledger_model: LedgerModel = self.get_random_ledger(
|
|
238
|
+
entity_model=entity_model,
|
|
239
|
+
qs_limit=qs_limit,
|
|
240
|
+
)
|
|
241
|
+
else:
|
|
242
|
+
entity_model.validate_ledger_model_for_entity(ledger_model)
|
|
243
|
+
journal_entry_qs = ledger_model.journal_entries.all()
|
|
244
|
+
|
|
245
|
+
# no need to check because data generator will always populate an entity with sample data.
|
|
246
|
+
# if not je_model.exists():
|
|
247
|
+
# for i in range(3):
|
|
248
|
+
# random_je_activity = choice([category[0] for category in JournalEntryModel.ACTIVITIES])
|
|
249
|
+
# JournalEntryModel.objects.create(
|
|
250
|
+
# je_number=f"{i}example-je-num-{randint(10000, 99999)}",
|
|
251
|
+
# description=f"{i}Random Journal Entry Desc {randint(10000, 99999)}",
|
|
252
|
+
# is_closing_entry=False,
|
|
253
|
+
# activity=random_je_activity,
|
|
254
|
+
# posted=False,
|
|
255
|
+
# locked=False,
|
|
256
|
+
# ledger=ledger_model,
|
|
257
|
+
# )
|
|
258
|
+
|
|
259
|
+
if posted:
|
|
260
|
+
journal_entry_qs = journal_entry_qs.posted()
|
|
261
|
+
|
|
262
|
+
journal_entry_qs = journal_entry_qs.select_related('ledger', 'ledger__entity')
|
|
263
|
+
# limits the queryset in case of large querysets...
|
|
264
|
+
return choice(journal_entry_qs[:qs_limit])
|
|
265
|
+
|
|
266
|
+
def get_random_transaction(self,
|
|
267
|
+
entity_model: EntityModel,
|
|
268
|
+
je_model: Optional[JournalEntryModel] = None,
|
|
269
|
+
posted: bool = True,
|
|
270
|
+
qs_limit: int = 100) -> TransactionModel:
|
|
271
|
+
"""
|
|
272
|
+
Returns all TransactionModel related to a random or specified JournalEntryModel.
|
|
273
|
+
"""
|
|
274
|
+
if not je_model:
|
|
275
|
+
je_model = self.get_random_je(entity_model=entity_model, posted=posted)
|
|
276
|
+
else:
|
|
277
|
+
ledger_model = je_model.ledger
|
|
278
|
+
entity_model.validate_ledger_model_for_entity(ledger_model)
|
|
279
|
+
txs_model_qs = je_model.transactionmodel_set.all()
|
|
280
|
+
return choice(txs_model_qs[:qs_limit])
|
|
@@ -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')
|
django_ledger/tests/test_auth.py
CHANGED
|
@@ -31,9 +31,8 @@ class AuthTest(DjangoLedgerBaseTest):
|
|
|
31
31
|
'password': self.PASSWORD + '1',
|
|
32
32
|
}, follow=False)
|
|
33
33
|
self.assertContains(response, text='Login', status_code=200)
|
|
34
|
-
self.assertFormError(
|
|
35
|
-
|
|
36
|
-
form='form',
|
|
34
|
+
self.assertFormError(field=None,
|
|
35
|
+
form=response.context_data['form'],
|
|
37
36
|
errors=[
|
|
38
37
|
'Please enter a correct username and password. Note that both fields may be case-sensitive.'
|
|
39
38
|
])
|
|
@@ -43,9 +42,8 @@ class AuthTest(DjangoLedgerBaseTest):
|
|
|
43
42
|
'password': self.PASSWORD,
|
|
44
43
|
}, follow=False)
|
|
45
44
|
self.assertContains(response, text='Login', status_code=200)
|
|
46
|
-
self.assertFormError(
|
|
47
|
-
|
|
48
|
-
form='form',
|
|
45
|
+
self.assertFormError(field=None,
|
|
46
|
+
form=response.context_data['form'],
|
|
49
47
|
errors=[
|
|
50
48
|
'Please enter a correct username and password. Note that both fields may be case-sensitive.'
|
|
51
49
|
])
|
|
@@ -71,5 +69,5 @@ class AuthTest(DjangoLedgerBaseTest):
|
|
|
71
69
|
# logout button is present...
|
|
72
70
|
self.assertContains(response, text='id="djl-el=logout-button-nav"')
|
|
73
71
|
|
|
74
|
-
response = self.client.
|
|
72
|
+
response = self.client.post(logout_url)
|
|
75
73
|
self.assertRedirects(response, expected_url=login_url)
|
django_ledger/tests/test_bill.py
CHANGED
|
@@ -313,7 +313,7 @@ class BillModelTests(DjangoLedgerBaseTest):
|
|
|
313
313
|
vendor_model = bill_model.vendor
|
|
314
314
|
bill_detail_url = bill_model.get_absolute_url()
|
|
315
315
|
|
|
316
|
-
with self.assertNumQueries(
|
|
316
|
+
with self.assertNumQueries(6):
|
|
317
317
|
bill_detail_response = self.CLIENT.get(bill_detail_url)
|
|
318
318
|
self.assertTrue(bill_detail_response.status_code, 200)
|
|
319
319
|
|
|
@@ -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...
|