django-ledger 0.6.4__py3-none-any.whl → 0.7.1__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 -4
- django_ledger/admin/__init__.py +1 -1
- django_ledger/admin/{coa.py → chart_of_accounts.py} +1 -1
- django_ledger/admin/entity.py +1 -1
- django_ledger/contrib/django_ledger_graphene/accounts/schema.py +1 -1
- django_ledger/forms/account.py +43 -38
- django_ledger/forms/bank_account.py +5 -2
- django_ledger/forms/bill.py +24 -36
- django_ledger/forms/chart_of_accounts.py +82 -0
- django_ledger/forms/customer.py +1 -1
- django_ledger/forms/data_import.py +3 -3
- django_ledger/forms/estimate.py +1 -1
- django_ledger/forms/invoice.py +5 -7
- django_ledger/forms/item.py +24 -15
- django_ledger/forms/transactions.py +3 -3
- django_ledger/io/io_core.py +4 -2
- django_ledger/io/io_library.py +1 -1
- django_ledger/io/io_middleware.py +5 -0
- django_ledger/migrations/0017_alter_accountmodel_unique_together_and_more.py +31 -0
- django_ledger/migrations/0018_transactionmodel_cleared_transactionmodel_reconciled_and_more.py +37 -0
- django_ledger/models/__init__.py +1 -1
- django_ledger/models/accounts.py +229 -265
- django_ledger/models/bank_account.py +6 -6
- django_ledger/models/bill.py +7 -6
- django_ledger/models/{coa.py → chart_of_accounts.py} +187 -72
- django_ledger/models/closing_entry.py +5 -10
- django_ledger/models/coa_default.py +10 -9
- django_ledger/models/customer.py +6 -6
- django_ledger/models/data_import.py +12 -8
- django_ledger/models/entity.py +96 -39
- django_ledger/models/estimate.py +6 -10
- django_ledger/models/invoice.py +14 -11
- django_ledger/models/items.py +23 -14
- django_ledger/models/journal_entry.py +73 -30
- django_ledger/models/ledger.py +8 -8
- django_ledger/models/mixins.py +0 -3
- django_ledger/models/purchase_order.py +9 -9
- django_ledger/models/signals.py +0 -3
- django_ledger/models/transactions.py +24 -7
- django_ledger/models/unit.py +4 -3
- django_ledger/models/utils.py +0 -3
- django_ledger/models/vendor.py +4 -3
- django_ledger/settings.py +28 -3
- django_ledger/templates/django_ledger/account/account_create.html +2 -2
- django_ledger/templates/django_ledger/account/account_update.html +1 -1
- django_ledger/templates/django_ledger/account/tags/account_txs_table.html +1 -0
- django_ledger/templates/django_ledger/account/tags/accounts_table.html +29 -19
- django_ledger/templates/django_ledger/bills/bill_detail.html +3 -3
- django_ledger/templates/django_ledger/chart_of_accounts/coa_create.html +25 -0
- django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html +25 -6
- django_ledger/templates/django_ledger/chart_of_accounts/coa_update.html +2 -2
- django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html +10 -4
- django_ledger/templates/django_ledger/expense/tags/expense_item_table.html +7 -0
- django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +2 -2
- django_ledger/templates/django_ledger/includes/footer.html +2 -2
- django_ledger/templates/django_ledger/invoice/invoice_detail.html +3 -3
- django_ledger/templatetags/django_ledger.py +7 -1
- django_ledger/tests/base.py +23 -7
- django_ledger/tests/test_accounts.py +145 -9
- django_ledger/urls/account.py +17 -24
- django_ledger/urls/chart_of_accounts.py +6 -0
- django_ledger/utils.py +9 -36
- django_ledger/views/__init__.py +2 -2
- django_ledger/views/account.py +91 -116
- django_ledger/views/auth.py +1 -1
- django_ledger/views/bank_account.py +9 -11
- django_ledger/views/bill.py +91 -80
- django_ledger/views/{coa.py → chart_of_accounts.py} +49 -44
- django_ledger/views/closing_entry.py +8 -0
- django_ledger/views/customer.py +1 -1
- django_ledger/views/data_import.py +1 -1
- django_ledger/views/entity.py +1 -1
- django_ledger/views/estimate.py +13 -8
- django_ledger/views/feedback.py +1 -1
- django_ledger/views/financial_statement.py +1 -1
- django_ledger/views/home.py +1 -1
- django_ledger/views/inventory.py +9 -0
- django_ledger/views/invoice.py +5 -2
- django_ledger/views/item.py +58 -68
- django_ledger/views/journal_entry.py +1 -1
- django_ledger/views/ledger.py +3 -1
- django_ledger/views/mixins.py +25 -13
- django_ledger/views/purchase_order.py +1 -1
- django_ledger/views/transactions.py +1 -1
- django_ledger/views/unit.py +9 -0
- django_ledger/views/vendor.py +1 -1
- {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/AUTHORS.md +8 -2
- {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/METADATA +33 -44
- {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/RECORD +92 -89
- {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/WHEEL +1 -1
- django_ledger/forms/coa.py +0 -47
- {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/LICENSE +0 -0
- {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/top_level.txt +0 -0
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<div class="level-item has-text-centered">
|
|
30
30
|
<div>
|
|
31
31
|
<p class="heading">{% trans 'Cash Account' %}:
|
|
32
|
-
<a href="{% url 'django_ledger:account-detail' account_pk=bill.cash_account.uuid entity_slug=view.kwargs.entity_slug %}"
|
|
32
|
+
<a href="{% url 'django_ledger:account-detail' account_pk=bill.cash_account.uuid coa_slug=bill.cash_account.coa_model.slug entity_slug=view.kwargs.entity_slug %}"
|
|
33
33
|
class="has-text-danger">{{ bill.cash_account.code }}</a>
|
|
34
34
|
<p class="title" id="djl-bill-detail-amount-paid">
|
|
35
35
|
{% currency_symbol %}{{ bill.get_amount_cash | absolute | currency_format }}</p>
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
<div class="level-item has-text-centered">
|
|
41
41
|
<div>
|
|
42
42
|
<p class="heading">{% trans 'Prepaid Account' %}:
|
|
43
|
-
<a href="{% url 'django_ledger:account-detail' account_pk=bill.prepaid_account.uuid entity_slug=view.kwargs.entity_slug %}"
|
|
43
|
+
<a href="{% url 'django_ledger:account-detail' account_pk=bill.prepaid_account.uuid coa_slug=bill.prepaid_account.coa_model.slug entity_slug=view.kwargs.entity_slug %}"
|
|
44
44
|
class="has-text-danger">{{ bill.prepaid_account.code }}</a>
|
|
45
45
|
</p>
|
|
46
46
|
<p class="title has-text-success" id="djl-bill-detail-amount-prepaid">
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
<div class="level-item has-text-centered">
|
|
51
51
|
<div>
|
|
52
52
|
<p class="heading">{% trans 'Accounts Payable' %}:
|
|
53
|
-
<a href="{% url 'django_ledger:account-detail' account_pk=bill.unearned_account.uuid entity_slug=view.kwargs.entity_slug %}"
|
|
53
|
+
<a href="{% url 'django_ledger:account-detail' account_pk=bill.unearned_account.uuid coa_slug=bill.unearned_account.coa_model.slug entity_slug=view.kwargs.entity_slug %}"
|
|
54
54
|
class="has-text-danger">{{ bill.unearned_account.code }}</a>
|
|
55
55
|
</p>
|
|
56
56
|
<p class="title has-text-danger" id="djl-bill-detail-amount-unearned">
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{% extends 'django_ledger/layouts/content_layout_1.html' %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
{% load static %}
|
|
4
|
+
{% load django_ledger %}
|
|
5
|
+
|
|
6
|
+
{% block view_content %}
|
|
7
|
+
<div class="box">
|
|
8
|
+
<div class="columns is-centered is-multiline">
|
|
9
|
+
<div class="column is-12 has-text-centered">
|
|
10
|
+
<h1 class="is-size-2">{% trans 'Create Chart of Accounts' %}</h1>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="column is-8-tablet is-6-desktop">
|
|
13
|
+
<div class="box">
|
|
14
|
+
<form method="post" id="{{ form.get_form_id }}">
|
|
15
|
+
{% csrf_token %}
|
|
16
|
+
{{ form.as_p }}
|
|
17
|
+
<button type="submit" class="button is-primary is-fullwidth djetler_my_1">Submit</button>
|
|
18
|
+
<a class="button is-dark is-small is-fullwidth"
|
|
19
|
+
href="{{ entity_model.get_coa_list_url }}">Back</a>
|
|
20
|
+
</form>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
{% endblock %}
|
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
{% extends 'django_ledger/layouts/content_layout_1.html' %}
|
|
2
2
|
{% load i18n %}
|
|
3
3
|
{% load static %}
|
|
4
|
+
{% load icon from django_ledger %}
|
|
4
5
|
|
|
5
6
|
{% block view_content %}
|
|
6
|
-
<div class="
|
|
7
|
-
<div class="
|
|
8
|
-
|
|
9
|
-
<div class="column is-
|
|
10
|
-
|
|
7
|
+
<div class="card">
|
|
8
|
+
<div class="card-content">
|
|
9
|
+
<div class="columns">
|
|
10
|
+
<div class="column is-12">
|
|
11
|
+
<h1 class="is-size-1">Chart of Accounts
|
|
12
|
+
<a href="{{ entity_model.get_coa_create_url }}">
|
|
13
|
+
<span class="icon is-large has-text-success">{% icon 'carbon:add-alt' 60 %}</span></a>
|
|
14
|
+
</h1>
|
|
15
|
+
{% if not inactive %}
|
|
16
|
+
<a class="button is-warning" href="{{ entity_model.get_coa_list_inactive_url }}">
|
|
17
|
+
{% trans 'Show Inactive' %}</a>
|
|
18
|
+
{% else %}
|
|
19
|
+
<a class="button is-warning" href="{{ entity_model.get_coa_list_url }}">
|
|
20
|
+
{% trans 'Show Active' %}</a>
|
|
21
|
+
{% endif %}
|
|
22
|
+
|
|
11
23
|
</div>
|
|
12
|
-
|
|
24
|
+
</div>
|
|
25
|
+
<div class="columns is-centered is-multiline">
|
|
26
|
+
{% for coa_model in coa_list %}
|
|
27
|
+
<div class="column is-6">
|
|
28
|
+
{% include 'django_ledger/chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %}
|
|
29
|
+
</div>
|
|
30
|
+
{% endfor %}
|
|
31
|
+
</div>
|
|
13
32
|
</div>
|
|
14
33
|
</div>
|
|
15
34
|
{% endblock %}
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
<div class="columns is-centered">
|
|
7
7
|
<div class="column is-8-tablet is-6-desktop">
|
|
8
8
|
<div class="box">
|
|
9
|
-
<form action="{{
|
|
9
|
+
<form action="{{ coa_model.get_update_url }}" id="{{ form.form_id }}" method="post">
|
|
10
10
|
{% csrf_token %}
|
|
11
11
|
{{ form.as_p }}
|
|
12
12
|
<button class="button is-primary is-outlined is-fullwidth djetler_my_1"
|
|
13
13
|
type="submit">Update
|
|
14
14
|
</button>
|
|
15
15
|
<a class="button is-small is-dark is-fullwidth"
|
|
16
|
-
href="{
|
|
16
|
+
href="{{ coa_model.get_coa_list_url }}"
|
|
17
17
|
type="submit">Back</a>
|
|
18
18
|
</form>
|
|
19
19
|
</div>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
<div class="card">
|
|
7
|
-
<div class="card-header" {% if coa_model.is_default %}style="background-color: #04e304"{% endif %}>
|
|
7
|
+
<div class="card-header" {% if coa_model.is_default %}style="background-color: #04e304" {% elif not coa_model.is_active %}style="background-color: #d34e4e"{% endif %}>
|
|
8
8
|
<div class="card-header-icon">
|
|
9
9
|
{% icon "ic:baseline-business" 36 %}
|
|
10
10
|
</div>
|
|
@@ -42,6 +42,12 @@
|
|
|
42
42
|
</p>
|
|
43
43
|
</div>
|
|
44
44
|
|
|
45
|
+
<footer class="card-footer">
|
|
46
|
+
<a href="{{ coa_model.get_update_url }}"
|
|
47
|
+
class="card-footer-item has-text-warning has-text-weight-bold">
|
|
48
|
+
{% trans 'Update' %}
|
|
49
|
+
</a>
|
|
50
|
+
</footer>
|
|
45
51
|
<footer class="card-footer">
|
|
46
52
|
<a href="{{ coa_model.get_account_list_url }}"
|
|
47
53
|
class="card-footer-item has-text-success has-text-weight-bold">
|
|
@@ -51,14 +57,14 @@
|
|
|
51
57
|
class="card-footer-item has-text-info has-text-weight-bold">
|
|
52
58
|
{% trans 'Add Account' %}
|
|
53
59
|
</a>
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
</footer>
|
|
61
|
+
<footer class="card-footer">
|
|
62
|
+
{% if coa_model.can_mark_as_default %}
|
|
56
63
|
<a href="{{ coa_model.mark_as_default_url }}"
|
|
57
64
|
class="card-footer-item has-text-danger has-text-weight-bold">
|
|
58
65
|
{% trans 'Mark as Default' %}
|
|
59
66
|
</a>
|
|
60
67
|
{% endif %}
|
|
61
|
-
|
|
62
68
|
{% if coa_model.can_deactivate %}
|
|
63
69
|
<a href="{{ coa_model.mark_as_inactive_url }}"
|
|
64
70
|
class="card-footer-item has-text-warning has-text-weight-bold">
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<th>{% trans 'Item' %}</th>
|
|
10
10
|
<th>{% trans 'UOM' %}</th>
|
|
11
11
|
<th>{% trans 'Expense Account' %}</th>
|
|
12
|
+
<th>{% trans 'Is Active' %}</th>
|
|
12
13
|
<th>{% trans 'Actions' %}</th>
|
|
13
14
|
</tr>
|
|
14
15
|
</thead>
|
|
@@ -19,6 +20,12 @@
|
|
|
19
20
|
<td>{{ expense_item.name }}</td>
|
|
20
21
|
<td>{{ expense_item.uom }}</td>
|
|
21
22
|
<td>{{ expense_item.expense_account }}</td>
|
|
23
|
+
<td class="has-text-centered">
|
|
24
|
+
{% if expense_item.is_active %}
|
|
25
|
+
<span class="icon is-small has-text-success">{% icon 'bi:check-circle-fill' 24 %}</span>
|
|
26
|
+
{% else %}
|
|
27
|
+
<span class="icon is-small has-text-danger">{% icon 'healthicons:no' 24 %}</span>
|
|
28
|
+
{% endif %} </td>
|
|
22
29
|
<td>
|
|
23
30
|
<div class="dropdown is-right is-hoverable" id="invoice-action-{{ invoice.uuid }}">
|
|
24
31
|
<div class="dropdown-trigger">
|
django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html
CHANGED
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
</div>
|
|
61
61
|
<div class="dropdown-menu" id="dropdown-menu-{{ acc.uuid }}" role="menu">
|
|
62
62
|
<div class="dropdown-content">
|
|
63
|
-
<a href="{
|
|
63
|
+
<a href="{{ acc.get_absolute_url }}"
|
|
64
64
|
class="dropdown-item has-text-success">{% trans 'Detail' %}</a>
|
|
65
|
-
<a href="{
|
|
65
|
+
<a href="{{ acc.get_update_url }}"
|
|
66
66
|
class="dropdown-item has-text-warning">{% trans 'Update' %}</a>
|
|
67
67
|
</div>
|
|
68
68
|
</div>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<footer class="footer">
|
|
2
2
|
<div class="content has-text-centered">
|
|
3
3
|
<p>
|
|
4
|
-
<strong>Django Ledger</strong> by <a href="https://miguelsanda.com" target="_blank">Miguel Sanda</a> and
|
|
5
|
-
<a href="https://
|
|
4
|
+
<strong>Django Ledger</strong> by <a href="https://www.miguelsanda.com" target="_blank">Miguel Sanda</a> and
|
|
5
|
+
<a href="https://miguelsanda.com" target="_blank">EDMA Group Inc</a>. The source code is licensed
|
|
6
6
|
<a href="https://opensource.org/licenses/GPL-3.0" target="_blank">GPL 3.0</a>
|
|
7
7
|
</p>
|
|
8
8
|
{% if user.is_superuser %}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
<div class="level-item has-text-centered">
|
|
29
29
|
<div>
|
|
30
30
|
<p class="heading">{% trans 'Cash Account' %}:
|
|
31
|
-
<a href="{% url 'django_ledger:account-detail' account_pk=invoice.cash_account.uuid entity_slug=view.kwargs.entity_slug %}"
|
|
31
|
+
<a href="{% url 'django_ledger:account-detail' account_pk=invoice.cash_account.uuid coa_slug=invoice.cash_account.coa_model.slug entity_slug=view.kwargs.entity_slug %}"
|
|
32
32
|
class="has-text-danger">{{ invoice.cash_account.code }}</a>
|
|
33
33
|
<p class="title">
|
|
34
34
|
{% currency_symbol %}{{ invoice.get_amount_cash | absolute | currency_format }}</p>
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
<div class="level-item has-text-centered">
|
|
40
40
|
<div>
|
|
41
41
|
<p class="heading">{% trans 'Accounts Receivable' %}:
|
|
42
|
-
<a href="{% url 'django_ledger:account-detail' account_pk=invoice.prepaid_account.uuid entity_slug=view.kwargs.entity_slug %}"
|
|
42
|
+
<a href="{% url 'django_ledger:account-detail' account_pk=invoice.prepaid_account.uuid coa_slug=invoice.prepaid_account.coa_model.slug entity_slug=view.kwargs.entity_slug %}"
|
|
43
43
|
class="has-text-danger">{{ invoice.prepaid_account.code }}</a>
|
|
44
44
|
</p>
|
|
45
45
|
<p class="title has-text-success">
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
<div class="level-item has-text-centered">
|
|
50
50
|
<div>
|
|
51
51
|
<p class="heading">{% trans 'Deferred Revenue' %}:
|
|
52
|
-
<a href="{% url 'django_ledger:account-detail' account_pk=invoice.unearned_account.uuid entity_slug=view.kwargs.entity_slug %}"
|
|
52
|
+
<a href="{% url 'django_ledger:account-detail' account_pk=invoice.unearned_account.uuid coa_slug=invoice.unearned_account.coa_model.slug entity_slug=view.kwargs.entity_slug %}"
|
|
53
53
|
class="has-text-danger">{{ invoice.unearned_account.code }}</a>
|
|
54
54
|
|
|
55
55
|
</p>
|
|
@@ -542,6 +542,9 @@ def period_navigation(context, base_url: str):
|
|
|
542
542
|
if context['view'].kwargs.get('unit_slug'):
|
|
543
543
|
kwargs['unit_slug'] = context['view'].kwargs.get('unit_slug')
|
|
544
544
|
|
|
545
|
+
if context['view'].kwargs.get('coa_slug'):
|
|
546
|
+
kwargs['coa_slug'] = context['view'].kwargs.get('coa_slug')
|
|
547
|
+
|
|
545
548
|
ctx = dict()
|
|
546
549
|
ctx['year'] = context['year']
|
|
547
550
|
ctx['has_year'] = context.get('has_year')
|
|
@@ -567,12 +570,15 @@ def period_navigation(context, base_url: str):
|
|
|
567
570
|
'year': dt.year,
|
|
568
571
|
'month': dt.month
|
|
569
572
|
}
|
|
573
|
+
|
|
570
574
|
if 'unit_slug' in kwargs:
|
|
571
575
|
KWARGS_CURRENT_MONTH['unit_slug'] = kwargs['unit_slug']
|
|
572
576
|
if 'account_pk' in kwargs:
|
|
573
577
|
KWARGS_CURRENT_MONTH['account_pk'] = kwargs['account_pk']
|
|
574
578
|
if 'ledger_pk' in kwargs:
|
|
575
579
|
KWARGS_CURRENT_MONTH['ledger_pk'] = kwargs['ledger_pk']
|
|
580
|
+
if 'coa_slug' in kwargs:
|
|
581
|
+
KWARGS_CURRENT_MONTH['coa_slug'] = kwargs['coa_slug']
|
|
576
582
|
|
|
577
583
|
ctx['current_month_url'] = reverse(f'django_ledger:{base_url}-month',
|
|
578
584
|
kwargs=KWARGS_CURRENT_MONTH)
|
|
@@ -743,7 +749,7 @@ def navigation_menu(context, style):
|
|
|
743
749
|
{
|
|
744
750
|
'type': 'link',
|
|
745
751
|
'title': 'Ledgers',
|
|
746
|
-
'url': reverse('django_ledger:ledger-list', kwargs={'entity_slug': ENTITY_SLUG})
|
|
752
|
+
'url': reverse('django_ledger:ledger-list-visible', kwargs={'entity_slug': ENTITY_SLUG})
|
|
747
753
|
},
|
|
748
754
|
{
|
|
749
755
|
'type': 'link',
|
django_ledger/tests/base.py
CHANGED
|
@@ -8,7 +8,7 @@ from zoneinfo import ZoneInfo
|
|
|
8
8
|
|
|
9
9
|
from django.conf import settings
|
|
10
10
|
from django.contrib.auth import get_user_model
|
|
11
|
-
from django.core.exceptions import ObjectDoesNotExist
|
|
11
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
12
12
|
from django.test import TestCase
|
|
13
13
|
from django.test.client import Client
|
|
14
14
|
from django.utils.timezone import get_default_timezone
|
|
@@ -30,7 +30,7 @@ class DjangoLedgerBaseTest(TestCase):
|
|
|
30
30
|
TEST_DATA = list()
|
|
31
31
|
CLIENT = None
|
|
32
32
|
TZ = None
|
|
33
|
-
N =
|
|
33
|
+
N = 1
|
|
34
34
|
USER_EMAIL = None
|
|
35
35
|
PASSWORD = None
|
|
36
36
|
USERNAME = None
|
|
@@ -46,7 +46,6 @@ class DjangoLedgerBaseTest(TestCase):
|
|
|
46
46
|
cls.USERNAME: str = 'testuser'
|
|
47
47
|
cls.PASSWORD: str = 'NeverUseThisPassword12345'
|
|
48
48
|
cls.USER_EMAIL: str = 'testuser@djangoledger.com'
|
|
49
|
-
cls.N: int = 1
|
|
50
49
|
|
|
51
50
|
cls.DAYS_FWD: int = randint(180, 180 * 3)
|
|
52
51
|
cls.TZ = get_default_timezone()
|
|
@@ -191,14 +190,15 @@ class DjangoLedgerBaseTest(TestCase):
|
|
|
191
190
|
|
|
192
191
|
def get_random_account(self,
|
|
193
192
|
entity_model: EntityModel,
|
|
194
|
-
balance_type: Literal['credit', 'debit', None] = None
|
|
193
|
+
balance_type: Literal['credit', 'debit', None] = None,
|
|
194
|
+
active: bool = True,
|
|
195
|
+
locked: bool = False) -> AccountModel:
|
|
195
196
|
"""
|
|
196
197
|
Returns 1 random AccountModel with the specified balance_type.
|
|
197
198
|
"""
|
|
198
|
-
account_qs: AccountModelQuerySet = entity_model.get_coa_accounts(active=
|
|
199
|
+
account_qs: AccountModelQuerySet = entity_model.get_coa_accounts(active=active, locked=locked)
|
|
199
200
|
account_qs = account_qs.filter(balance_type=balance_type) if balance_type else account_qs
|
|
200
|
-
|
|
201
|
-
return choice(account_qs[:50])
|
|
201
|
+
return choice(account_qs)
|
|
202
202
|
|
|
203
203
|
def get_random_ledger(self,
|
|
204
204
|
entity_model: EntityModel,
|
|
@@ -278,3 +278,19 @@ class DjangoLedgerBaseTest(TestCase):
|
|
|
278
278
|
entity_model.validate_ledger_model_for_entity(ledger_model)
|
|
279
279
|
txs_model_qs = je_model.transactionmodel_set.all()
|
|
280
280
|
return choice(txs_model_qs[:qs_limit])
|
|
281
|
+
|
|
282
|
+
def resolve_url_patterns(self, url_patterns):
|
|
283
|
+
self.URL_PATTERNS = {
|
|
284
|
+
p.name: set(p.pattern.converters.keys()) for p in url_patterns
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
def resolve_url_kwars(self):
|
|
288
|
+
url_patterns = getattr(self, 'URL_PATTERNS', None)
|
|
289
|
+
if not url_patterns:
|
|
290
|
+
raise ValidationError(
|
|
291
|
+
message='Must call resolve_url_patterns before calling resolve_url_kwars.'
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return set.union(*[
|
|
295
|
+
set(v) for v in self.URL_PATTERNS.values()
|
|
296
|
+
])
|
|
@@ -1,16 +1,152 @@
|
|
|
1
|
-
from
|
|
1
|
+
from random import choice
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
|
|
4
|
+
from django.urls import reverse
|
|
5
|
+
|
|
6
|
+
from django_ledger.forms.account import AccountModelCreateForm
|
|
7
|
+
from django_ledger.io import roles, CREDIT
|
|
8
|
+
from django_ledger.models import EntityModel
|
|
9
|
+
from django_ledger.models.accounts import AccountModel
|
|
2
10
|
from django_ledger.tests.base import DjangoLedgerBaseTest
|
|
11
|
+
from django_ledger.urls.account import urlpatterns as account_urls
|
|
3
12
|
|
|
4
13
|
|
|
5
14
|
class AccountModelTests(DjangoLedgerBaseTest):
|
|
15
|
+
N = 2
|
|
16
|
+
|
|
17
|
+
def setUp(self):
|
|
18
|
+
self.resolve_url_patterns(
|
|
19
|
+
url_patterns=account_urls
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def test_protected_views(self):
|
|
23
|
+
|
|
24
|
+
self.logout_client()
|
|
25
|
+
entity_model = self.get_random_entity_model()
|
|
26
|
+
account_model: AccountModel = self.get_random_account(entity_model=entity_model)
|
|
27
|
+
|
|
28
|
+
for path, kwargs in self.URL_PATTERNS.items():
|
|
29
|
+
url_kwargs = dict()
|
|
30
|
+
url_kwargs['entity_slug'] = entity_model.slug
|
|
31
|
+
|
|
32
|
+
if 'coa_slug' in kwargs:
|
|
33
|
+
url_kwargs['coa_slug'] = account_model.coa_slug
|
|
34
|
+
if 'account_pk' in kwargs:
|
|
35
|
+
url_kwargs['account_pk'] = account_model.uuid
|
|
36
|
+
if 'year' in kwargs:
|
|
37
|
+
url_kwargs['year'] = self.get_random_date().year
|
|
38
|
+
if 'quarter' in kwargs:
|
|
39
|
+
url_kwargs['quarter'] = choice(range(1, 5))
|
|
40
|
+
if 'month' in kwargs:
|
|
41
|
+
url_kwargs['month'] = self.get_random_date().month
|
|
42
|
+
if 'day' in kwargs:
|
|
43
|
+
url_kwargs['day'] = choice(range(1, 29))
|
|
44
|
+
|
|
45
|
+
url = reverse(f'django_ledger:{path}', kwargs=url_kwargs)
|
|
46
|
+
response = self.CLIENT.get(url, follow=False)
|
|
47
|
+
redirect_url = urlparse(response.url)
|
|
48
|
+
redirect_path = redirect_url.path
|
|
49
|
+
login_path = reverse(viewname='django_ledger:login')
|
|
50
|
+
|
|
51
|
+
self.assertEqual(response.status_code, 302, msg=f'{path} view is not protected.')
|
|
52
|
+
self.assertEqual(redirect_path, login_path, msg=f'{path} view not redirecting to correct auth URL.')
|
|
53
|
+
|
|
54
|
+
def test_account_create(self):
|
|
55
|
+
|
|
56
|
+
entity_model = self.get_random_entity_model()
|
|
57
|
+
account_create_url = reverse(
|
|
58
|
+
viewname='django_ledger:account-create',
|
|
59
|
+
kwargs={
|
|
60
|
+
'entity_slug': entity_model.slug,
|
|
61
|
+
'coa_slug': entity_model.default_coa_slug
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
self.login_client()
|
|
66
|
+
response = self.CLIENT.get(account_create_url)
|
|
67
|
+
|
|
68
|
+
# check if user can access page...
|
|
69
|
+
self.assertEqual(response.status_code, 200, msg="Fail to GET Account Create page.")
|
|
70
|
+
|
|
71
|
+
# check if account create form is rendered...
|
|
72
|
+
account_create_form: AccountModelCreateForm = response.context['form']
|
|
73
|
+
self.assertContains(response, account_create_form.form_id, count=1)
|
|
74
|
+
|
|
75
|
+
# check if all fields are rendered...
|
|
76
|
+
self.assertContains(response, 'name="code"')
|
|
77
|
+
self.assertContains(response, 'name="name"')
|
|
78
|
+
self.assertContains(response, 'name="role"')
|
|
79
|
+
self.assertContains(response, 'name="role_default"')
|
|
80
|
+
self.assertContains(response, 'name="balance_type"')
|
|
81
|
+
self.assertContains(response, 'name="active"')
|
|
82
|
+
self.assertContains(response, 'name="coa_model"')
|
|
83
|
+
|
|
84
|
+
# creates new account...
|
|
85
|
+
NEW_ACCOUNT_CODE = '404000'
|
|
86
|
+
form_data = {
|
|
87
|
+
'code': NEW_ACCOUNT_CODE,
|
|
88
|
+
'name': 'Test Income Account',
|
|
89
|
+
'role': roles.INCOME_OPERATIONAL,
|
|
90
|
+
'role_default': False,
|
|
91
|
+
'balance_type': CREDIT,
|
|
92
|
+
'active': True
|
|
93
|
+
}
|
|
94
|
+
response_create = self.CLIENT.post(account_create_url, data=form_data)
|
|
95
|
+
self.assertEqual(response_create.status_code, 302)
|
|
96
|
+
self.assertTrue(AccountModel.objects.for_entity(
|
|
97
|
+
entity_model=entity_model,
|
|
98
|
+
user_model=self.user_model,
|
|
99
|
+
coa_slug=entity_model.default_coa_slug,
|
|
100
|
+
).with_codes(codes=NEW_ACCOUNT_CODE).exists())
|
|
101
|
+
|
|
102
|
+
# cannot create an account with same code again...
|
|
103
|
+
response_create = self.CLIENT.post(account_create_url, data=form_data)
|
|
104
|
+
self.assertEqual(response_create.status_code, 200)
|
|
105
|
+
self.assertContains(response_create, 'Account with this Chart of Accounts and Account Code already exists')
|
|
106
|
+
|
|
107
|
+
def test_account_activation(self):
|
|
108
|
+
|
|
109
|
+
entity_model: EntityModel = self.get_random_entity_model()
|
|
110
|
+
account_model: AccountModel = self.get_random_account(entity_model=entity_model, active=True)
|
|
111
|
+
|
|
112
|
+
self.assertTrue(account_model.can_deactivate())
|
|
113
|
+
self.assertTrue(account_model.active)
|
|
114
|
+
|
|
115
|
+
account_model.deactivate(commit=True)
|
|
116
|
+
self.assertFalse(account_model.can_deactivate())
|
|
117
|
+
self.assertFalse(account_model.active)
|
|
118
|
+
|
|
119
|
+
account_model.activate(commit=True)
|
|
120
|
+
self.assertTrue(account_model.can_deactivate())
|
|
121
|
+
self.assertTrue(account_model.active)
|
|
122
|
+
|
|
123
|
+
def test_account_lock(self):
|
|
124
|
+
entity_model: EntityModel = self.get_random_entity_model()
|
|
125
|
+
account_model: AccountModel = self.get_random_account(entity_model=entity_model, active=True, locked=False)
|
|
126
|
+
|
|
127
|
+
self.assertTrue(account_model.can_lock())
|
|
128
|
+
self.assertFalse(account_model.can_unlock())
|
|
129
|
+
|
|
130
|
+
account_model.lock(commit=True)
|
|
131
|
+
self.assertFalse(account_model.can_lock())
|
|
132
|
+
self.assertTrue(account_model.can_unlock())
|
|
133
|
+
|
|
134
|
+
account_model.unlock(commit=True)
|
|
135
|
+
self.assertTrue(account_model.can_lock())
|
|
136
|
+
self.assertFalse(account_model.can_unlock())
|
|
6
137
|
|
|
7
|
-
def
|
|
8
|
-
entity_model = self.
|
|
9
|
-
self.
|
|
138
|
+
def test_annotations(self):
|
|
139
|
+
entity_model: EntityModel = self.get_random_entity_model()
|
|
140
|
+
account_model: AccountModel = self.get_random_account(entity_model=entity_model, active=True, locked=False)
|
|
10
141
|
|
|
11
|
-
|
|
12
|
-
|
|
142
|
+
self.assertEqual(account_model.entity_slug, entity_model.slug)
|
|
143
|
+
self.assertEqual(account_model.coa_slug, account_model.coa_model.slug)
|
|
144
|
+
self.assertEqual(account_model.coa_model.active, account_model.is_coa_active())
|
|
13
145
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
146
|
+
def test_can_transact(self):
|
|
147
|
+
entity_model: EntityModel = self.get_random_entity_model()
|
|
148
|
+
account_model: AccountModel = self.get_random_account(entity_model=entity_model, active=True, locked=False)
|
|
149
|
+
self.assertTrue(account_model.can_transact())
|
|
150
|
+
account_model.lock(commit=False)
|
|
151
|
+
self.assertFalse(account_model.can_transact())
|
|
152
|
+
account_model.unlock(commit=False)
|
django_ledger/urls/account.py
CHANGED
|
@@ -4,54 +4,47 @@ from django_ledger import views
|
|
|
4
4
|
|
|
5
5
|
urlpatterns = [
|
|
6
6
|
|
|
7
|
-
# NO COA SLUG USES DEFAULT COA....
|
|
8
|
-
path('<slug:entity_slug>/create/',
|
|
9
|
-
views.AccountModelCreateView.as_view(),
|
|
10
|
-
name='account-create'),
|
|
11
|
-
path('<slug:entity_slug>/list/',
|
|
12
|
-
views.AccountModelListView.as_view(),
|
|
13
|
-
name='account-list'),
|
|
14
|
-
path('<slug:entity_slug>/list/active/',
|
|
15
|
-
views.AccountModelListView.as_view(active_only=True),
|
|
16
|
-
name='account-list-active'),
|
|
17
|
-
|
|
18
|
-
# EXPLICIT COA...
|
|
19
7
|
path('<slug:entity_slug>/<slug:coa_slug>/create/',
|
|
20
8
|
views.AccountModelCreateView.as_view(),
|
|
21
|
-
name='account-create
|
|
9
|
+
name='account-create'),
|
|
22
10
|
path('<slug:entity_slug>/<slug:coa_slug>/list/',
|
|
23
11
|
views.AccountModelListView.as_view(),
|
|
24
|
-
name='account-list
|
|
12
|
+
name='account-list'),
|
|
25
13
|
path('<slug:entity_slug>/<slug:coa_slug>/list/active/',
|
|
26
14
|
views.AccountModelListView.as_view(active_only=True),
|
|
27
|
-
name='account-list-active
|
|
15
|
+
name='account-list-active'),
|
|
28
16
|
|
|
29
17
|
# Account Transaction Detail....
|
|
30
|
-
path('<slug:entity_slug>/update/<uuid:account_pk>/',
|
|
18
|
+
path('<slug:entity_slug>/<slug:coa_slug>/update/<uuid:account_pk>/',
|
|
31
19
|
views.AccountModelUpdateView.as_view(),
|
|
32
20
|
name='account-update'),
|
|
33
|
-
path('<slug:entity_slug>/detail/<uuid:account_pk>/',
|
|
21
|
+
path('<slug:entity_slug>/<slug:coa_slug>/detail/<uuid:account_pk>/',
|
|
34
22
|
views.AccountModelDetailView.as_view(),
|
|
35
23
|
name='account-detail'),
|
|
36
|
-
path('<slug:entity_slug>/detail/<uuid:account_pk>/year/<int:year>/',
|
|
24
|
+
path('<slug:entity_slug>/<slug:coa_slug>/detail/<uuid:account_pk>/year/<int:year>/',
|
|
37
25
|
views.AccountModelYearDetailView.as_view(),
|
|
38
26
|
name='account-detail-year'),
|
|
39
|
-
path('<slug:entity_slug>/detail/<uuid:account_pk>/quarter/<int:year>/<int:quarter>/',
|
|
27
|
+
path('<slug:entity_slug>/<slug:coa_slug>/detail/<uuid:account_pk>/quarter/<int:year>/<int:quarter>/',
|
|
40
28
|
views.AccountModelQuarterDetailView.as_view(),
|
|
41
29
|
name='account-detail-quarter'),
|
|
42
|
-
path('<slug:entity_slug>/detail/<uuid:account_pk>/month/<int:year>/<int:month>/',
|
|
30
|
+
path('<slug:entity_slug>/<slug:coa_slug>/detail/<uuid:account_pk>/month/<int:year>/<int:month>/',
|
|
43
31
|
views.AccountModelMonthDetailView.as_view(),
|
|
44
32
|
name='account-detail-month'),
|
|
45
|
-
path('<slug:entity_slug>/detail/<uuid:account_pk>/date/<int:year>/<int:month>/<int:day>/',
|
|
33
|
+
path('<slug:entity_slug>/<slug:coa_slug>/detail/<uuid:account_pk>/date/<int:year>/<int:month>/<int:day>/',
|
|
46
34
|
views.AccountModelDateDetailView.as_view(),
|
|
47
35
|
name='account-detail-date'),
|
|
48
36
|
|
|
49
37
|
# Account Actions...
|
|
50
|
-
path('<slug:entity_slug>/action/<uuid:account_pk>/activate/',
|
|
38
|
+
path('<slug:entity_slug>/<slug:coa_slug>/action/<uuid:account_pk>/activate/',
|
|
51
39
|
views.AccountModelModelActionView.as_view(action_name='activate'),
|
|
52
40
|
name='account-action-activate'),
|
|
53
|
-
path('<slug:entity_slug>/action/<uuid:account_pk>/deactivate/',
|
|
41
|
+
path('<slug:entity_slug>/<slug:coa_slug>/action/<uuid:account_pk>/deactivate/',
|
|
54
42
|
views.AccountModelModelActionView.as_view(action_name='deactivate'),
|
|
55
43
|
name='account-action-deactivate'),
|
|
56
|
-
|
|
44
|
+
path('<slug:entity_slug>/<slug:coa_slug>/action/<uuid:account_pk>/lock/',
|
|
45
|
+
views.AccountModelModelActionView.as_view(action_name='lock'),
|
|
46
|
+
name='account-action-lock'),
|
|
47
|
+
path('<slug:entity_slug>/<slug:coa_slug>/action/<uuid:account_pk>/unlock/',
|
|
48
|
+
views.AccountModelModelActionView.as_view(action_name='unlock'),
|
|
49
|
+
name='account-action-unlock')
|
|
57
50
|
]
|
|
@@ -6,6 +6,12 @@ urlpatterns = [
|
|
|
6
6
|
path('<slug:entity_slug>/list/',
|
|
7
7
|
views.ChartOfAccountModelListView.as_view(),
|
|
8
8
|
name='coa-list'),
|
|
9
|
+
path('<slug:entity_slug>/list/inactive/',
|
|
10
|
+
views.ChartOfAccountModelListView.as_view(inactive=True),
|
|
11
|
+
name='coa-list-inactive'),
|
|
12
|
+
path('<slug:entity_slug>/create/',
|
|
13
|
+
views.ChartOfAccountModelCreateView.as_view(),
|
|
14
|
+
name='coa-create'),
|
|
9
15
|
path('<slug:entity_slug>/detail/<slug:coa_slug>/',
|
|
10
16
|
views.ChartOfAccountModelListView.as_view(),
|
|
11
17
|
name='coa-detail'),
|
django_ledger/utils.py
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
"""
|
|
8
|
+
|
|
1
9
|
from datetime import date
|
|
2
10
|
from importlib import import_module
|
|
3
11
|
from itertools import groupby
|
|
@@ -10,7 +18,6 @@ from django.db.models import QuerySet
|
|
|
10
18
|
from django.utils.dateparse import parse_date
|
|
11
19
|
|
|
12
20
|
from django_ledger.io.io_core import get_localdate
|
|
13
|
-
from django_ledger.models import EntityModel
|
|
14
21
|
|
|
15
22
|
UserModel = get_user_model()
|
|
16
23
|
|
|
@@ -44,7 +51,7 @@ def get_default_unit_session_key():
|
|
|
44
51
|
return 'djl_default_unit_model'
|
|
45
52
|
|
|
46
53
|
|
|
47
|
-
def set_default_entity(request, entity_model
|
|
54
|
+
def set_default_entity(request, entity_model):
|
|
48
55
|
session_key = get_default_entity_session_key()
|
|
49
56
|
if not request.session.get(session_key):
|
|
50
57
|
request.session[session_key] = {
|
|
@@ -103,37 +110,3 @@ def get_end_date_from_session(entity_slug: str, request) -> date:
|
|
|
103
110
|
end_date = request.session.get(session_end_date_filter)
|
|
104
111
|
end_date = parse_date(end_date) if end_date else get_localdate()
|
|
105
112
|
return end_date
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def load_model_class(model_path: str):
|
|
109
|
-
"""
|
|
110
|
-
Loads a Python Model Class by using a string.
|
|
111
|
-
This functionality is inspired by the Django Blog Zinnia project.
|
|
112
|
-
This function allows for extension and customization of the stardard Django Ledger Models.
|
|
113
|
-
|
|
114
|
-
Examples
|
|
115
|
-
________
|
|
116
|
-
>>> model_class = load_model_class(model_path='module.models.MyModel')
|
|
117
|
-
|
|
118
|
-
Parameters
|
|
119
|
-
----------
|
|
120
|
-
model_path: str
|
|
121
|
-
The model path to load.
|
|
122
|
-
|
|
123
|
-
Returns
|
|
124
|
-
-------
|
|
125
|
-
The loaded Python model Class.
|
|
126
|
-
|
|
127
|
-
Raises
|
|
128
|
-
______
|
|
129
|
-
ImportError or AttributeError if unable to load model.
|
|
130
|
-
"""
|
|
131
|
-
dot = model_path.rindex('.')
|
|
132
|
-
module_name = model_path[:dot]
|
|
133
|
-
klass_name = model_path[dot + 1:]
|
|
134
|
-
try:
|
|
135
|
-
klass = getattr(import_module(module_name), klass_name)
|
|
136
|
-
return klass
|
|
137
|
-
except (ImportError, AttributeError) as e:
|
|
138
|
-
print(e)
|
|
139
|
-
raise ImproperlyConfigured(f'Model {model_path} cannot be imported!')
|