django-ledger 0.6.4__py3-none-any.whl → 0.7.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 (77) hide show
  1. django_ledger/__init__.py +1 -4
  2. django_ledger/contrib/django_ledger_graphene/accounts/schema.py +1 -1
  3. django_ledger/forms/account.py +43 -38
  4. django_ledger/forms/bank_account.py +3 -2
  5. django_ledger/forms/bill.py +24 -36
  6. django_ledger/forms/customer.py +1 -1
  7. django_ledger/forms/data_import.py +3 -3
  8. django_ledger/forms/estimate.py +1 -1
  9. django_ledger/forms/invoice.py +5 -7
  10. django_ledger/forms/item.py +24 -15
  11. django_ledger/forms/transactions.py +3 -3
  12. django_ledger/io/io_core.py +4 -2
  13. django_ledger/io/io_middleware.py +5 -0
  14. django_ledger/migrations/0017_alter_accountmodel_unique_together_and_more.py +31 -0
  15. django_ledger/models/accounts.py +225 -265
  16. django_ledger/models/bank_account.py +0 -4
  17. django_ledger/models/bill.py +0 -3
  18. django_ledger/models/closing_entry.py +0 -3
  19. django_ledger/models/coa.py +59 -48
  20. django_ledger/models/coa_default.py +9 -8
  21. django_ledger/models/customer.py +0 -4
  22. django_ledger/models/data_import.py +0 -3
  23. django_ledger/models/entity.py +70 -37
  24. django_ledger/models/estimate.py +0 -9
  25. django_ledger/models/invoice.py +0 -3
  26. django_ledger/models/items.py +4 -6
  27. django_ledger/models/journal_entry.py +2 -5
  28. django_ledger/models/ledger.py +0 -3
  29. django_ledger/models/mixins.py +0 -3
  30. django_ledger/models/purchase_order.py +0 -4
  31. django_ledger/models/signals.py +0 -3
  32. django_ledger/models/transactions.py +2 -5
  33. django_ledger/models/unit.py +0 -3
  34. django_ledger/models/utils.py +0 -3
  35. django_ledger/models/vendor.py +0 -3
  36. django_ledger/templates/django_ledger/account/account_create.html +2 -2
  37. django_ledger/templates/django_ledger/account/account_update.html +1 -1
  38. django_ledger/templates/django_ledger/account/tags/account_txs_table.html +1 -0
  39. django_ledger/templates/django_ledger/account/tags/accounts_table.html +27 -18
  40. django_ledger/templates/django_ledger/bills/bill_detail.html +3 -3
  41. django_ledger/templates/django_ledger/expense/tags/expense_item_table.html +7 -0
  42. django_ledger/templates/django_ledger/invoice/invoice_detail.html +3 -3
  43. django_ledger/templatetags/django_ledger.py +7 -1
  44. django_ledger/tests/base.py +23 -7
  45. django_ledger/tests/test_accounts.py +145 -9
  46. django_ledger/urls/account.py +17 -24
  47. django_ledger/utils.py +8 -0
  48. django_ledger/views/__init__.py +1 -1
  49. django_ledger/views/account.py +80 -118
  50. django_ledger/views/auth.py +1 -1
  51. django_ledger/views/bank_account.py +9 -11
  52. django_ledger/views/bill.py +91 -80
  53. django_ledger/views/closing_entry.py +8 -0
  54. django_ledger/views/coa.py +2 -1
  55. django_ledger/views/customer.py +1 -1
  56. django_ledger/views/data_import.py +1 -1
  57. django_ledger/views/entity.py +1 -1
  58. django_ledger/views/estimate.py +13 -8
  59. django_ledger/views/feedback.py +1 -1
  60. django_ledger/views/financial_statement.py +1 -1
  61. django_ledger/views/home.py +1 -1
  62. django_ledger/views/inventory.py +9 -0
  63. django_ledger/views/invoice.py +5 -2
  64. django_ledger/views/item.py +58 -68
  65. django_ledger/views/journal_entry.py +1 -1
  66. django_ledger/views/ledger.py +3 -1
  67. django_ledger/views/mixins.py +9 -8
  68. django_ledger/views/purchase_order.py +1 -1
  69. django_ledger/views/transactions.py +1 -1
  70. django_ledger/views/unit.py +9 -0
  71. django_ledger/views/vendor.py +1 -1
  72. {django_ledger-0.6.4.dist-info → django_ledger-0.7.0.dist-info}/AUTHORS.md +8 -2
  73. {django_ledger-0.6.4.dist-info → django_ledger-0.7.0.dist-info}/METADATA +34 -43
  74. {django_ledger-0.6.4.dist-info → django_ledger-0.7.0.dist-info}/RECORD +77 -76
  75. {django_ledger-0.6.4.dist-info → django_ledger-0.7.0.dist-info}/WHEEL +1 -1
  76. {django_ledger-0.6.4.dist-info → django_ledger-0.7.0.dist-info}/LICENSE +0 -0
  77. {django_ledger-0.6.4.dist-info → django_ledger-0.7.0.dist-info}/top_level.txt +0 -0
@@ -2,9 +2,6 @@
2
2
  Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
3
  Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
4
 
5
- Contributions to this module:
6
- * Miguel Sanda <msanda@arrobalytics.com>
7
-
8
5
  The TransactionModel is the lowest accounting level where financial information is recorded. Every transaction with a
9
6
  financial implication must be part of a JournalEntryModel, which encapsulates a collection of TransactionModels.
10
7
  Transaction models cannot exist without being part of a validated JournalEntryModel. Orphan TransactionModels are not
@@ -24,7 +21,7 @@ from django.contrib.auth import get_user_model
24
21
  from django.core.exceptions import ValidationError
25
22
  from django.core.validators import MinValueValidator
26
23
  from django.db import models
27
- from django.db.models import Q, QuerySet, F
24
+ from django.db.models import Q, QuerySet, Manager
28
25
  from django.db.models.signals import pre_save
29
26
  from django.utils.translation import gettext_lazy as _
30
27
 
@@ -209,7 +206,7 @@ class TransactionModelQuerySet(QuerySet):
209
206
  return self.filter(journal_entry__is_closing_entry=True)
210
207
 
211
208
 
212
- class TransactionModelManager(models.Manager):
209
+ class TransactionModelManager(Manager):
213
210
  """
214
211
  A manager class for the TransactionModel.
215
212
  """
@@ -2,9 +2,6 @@
2
2
  Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
3
  Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
4
 
5
- Contributions to this module:
6
- * Miguel Sanda <msanda@arrobalytics.com>
7
-
8
5
  An EntityUnit is a logical, user-defined grouping which is assigned to JournalEntryModels to help segregate business
9
6
  operations into separate components. Examples of business units may include Departments (i.e. Human Resources, IT, etc.)
10
7
  office locations, a real estate property, or any other label relevant to the business.
@@ -1,9 +1,6 @@
1
1
  """
2
2
  Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
3
  Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
-
5
- Contributions to this module:
6
- * Miguel Sanda <msanda@arrobalytics.com>
7
4
  """
8
5
 
9
6
  from django.apps import apps
@@ -2,9 +2,6 @@
2
2
  Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
3
  Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
4
 
5
- Contributions to this module:
6
- * Miguel Sanda <msanda@arrobalytics.com>
7
-
8
5
  A Vendor refers to the person or entity that provides products and services to the business for a fee.
9
6
  Vendors are an integral part of the billing process as they are the providers of goods and services for the
10
7
  business.
@@ -13,12 +13,12 @@
13
13
  </div>
14
14
  <div class="column is-8-tablet is-6-desktop">
15
15
  <div class="box">
16
- <form method="post">
16
+ <form method="post" id="{{ form.form_id }}">
17
17
  {% csrf_token %}
18
18
  {{ form.as_p }}
19
19
  <button type="submit" class="button is-primary is-fullwidth djetler_my_1">Submit</button>
20
20
  <a class="button is-dark is-small is-fullwidth"
21
- href="{% url 'django_ledger:account-list' entity_slug=view.kwargs.entity_slug %}">Back</a>
21
+ href="{{ coa_model.get_account_list_url }}">Back</a>
22
22
  </form>
23
23
  </div>
24
24
  </div>
@@ -18,7 +18,7 @@
18
18
  {{ form.as_p }}
19
19
  <button type="submit" class="button is-primary is-fullwidth djetler_my_1">Submit</button>
20
20
  <a class="button is-dark is-small is-fullwidth"
21
- href="{% url 'django_ledger:account-list' entity_slug=view.kwargs.entity_slug %}">Back</a>
21
+ href="{{ coa_model.get_account_list_url }}">Back</a>
22
22
  </form>
23
23
  </div>
24
24
  </div>
@@ -32,6 +32,7 @@
32
32
  </div>
33
33
  <div class="dropdown-menu" id="dropdown-menu-{{ tx.uuid }}" role="menu">
34
34
  <div class="dropdown-content">
35
+ {# TODO: These URLs need to be replaced with the future mode method that generates it. #}
35
36
  <a href="{% url 'django_ledger:je-detail' entity_slug=entity_slug ledger_pk=tx.journal_entry.ledger.uuid je_pk=tx.journal_entry.uuid %}"
36
37
  class="dropdown-item has-text-success">View JE</a>
37
38
  {% if tx.journal_entry.ledger.billmodel %}
@@ -15,7 +15,6 @@
15
15
  <td></td>
16
16
  <td></td>
17
17
  <td></td>
18
- {# <td></td>#}
19
18
  <td></td>
20
19
  </tr>
21
20
  {% endif %}
@@ -25,6 +24,7 @@
25
24
  <th>{% trans 'CoA' %}</th>
26
25
  <th>{% trans 'Balance Type' %}</th>
27
26
  <th>{% trans 'Active' %}</th>
27
+ <th>{% trans 'Locked' %}</th>
28
28
  <th>{% trans 'CoA Role Default' %}</th>
29
29
  <th>{% trans 'Actions' %}</th>
30
30
  </tr>
@@ -49,28 +49,29 @@
49
49
  <td>{{ account.coa_model.name }}</td>
50
50
  <td>{{ account.get_balance_type_display }}</td>
51
51
  <td class="has-text-centered">
52
- {% if account.active %}
52
+ {% if account.is_active %}
53
53
  <span class="icon has-text-success-dark">
54
54
  {% icon 'ant-design:check-circle-filled' 24 %}
55
55
  </span>
56
- {% elif not account.active %}
56
+ {% else %}
57
57
  <span class="icon has-text-danger-dark">
58
58
  {% icon 'mdi:dangerous' 24 %}
59
59
  </span>
60
60
  {% endif %}
61
61
  </td>
62
+
63
+ <td class="has-text-centered">
64
+ {% if account.is_locked %}
65
+ <span class="icon has-text-success-dark">
66
+ {% icon 'ooui:lock' 24 %}
67
+ </span>
68
+ {% else %}
69
+ <span class="icon has-text-danger-dark">
70
+ {% icon 'ooui:un-lock' 24 %}
71
+ </span>
72
+ {% endif %}
73
+ </td>
62
74
 
63
- {# <td class="has-text-centered">#}
64
- {# {% if account.locked %}#}
65
- {# <span class="icon has-text-success-dark">#}
66
- {# {% icon 'bi:lock-fill' 24 %}#}
67
- {# </span>#}
68
- {# {% elif not account.locked %}#}
69
- {# <span class="icon has-text-danger-dark">#}
70
- {# {% icon 'bx:bx-lock-open-alt' 24 %}#}
71
- {# </span>#}
72
- {# {% endif %}#}
73
- {# </td>#}
74
75
  <td class="has-text-centered">
75
76
  {% if account.role_default %}
76
77
  <span class="icon has-text-success-dark">
@@ -94,18 +95,26 @@
94
95
  </div>
95
96
  <div class="dropdown-menu" id="dropdown-menu-{{ account.uuid }}" role="menu">
96
97
  <div class="dropdown-content">
97
- <a href="{% url 'django_ledger:account-detail' entity_slug=entity_slug account_pk=account.uuid %}"
98
+ <a href="{{ account.get_absolute_url }}"
98
99
  class="dropdown-item has-text-success">{% trans 'Detail' %}</a>
99
- <a href="{% url 'django_ledger:account-update' entity_slug=entity_slug account_pk=account.uuid %}"
100
+ <a href="{{ account.get_update_url }}"
100
101
  class="dropdown-item has-text-warning">{% trans 'Update' %}</a>
101
102
  {% if account.can_activate %}
102
- <a href="{% url 'django_ledger:account-action-activate' entity_slug=entity_slug account_pk=account.uuid %}"
103
+ <a href="{{ account.get_action_activate_url }}"
103
104
  class="dropdown-item has-text-success has-text-weight-bold">{% trans 'Activate' %}</a>
104
105
  {% endif %}
105
106
  {% if account.can_deactivate %}
106
- <a href="{% url 'django_ledger:account-action-deactivate' entity_slug=entity_slug account_pk=account.uuid %}"
107
+ <a href="{{ account.get_action_deactivate_url }}"
107
108
  class="dropdown-item has-text-danger has-text-weight-bold">{% trans 'Deactivate' %}</a>
108
109
  {% endif %}
110
+ {% if account.can_lock %}
111
+ <a href="{{ account.get_action_lock_url }}"
112
+ class="dropdown-item has-text-success has-text-weight-bold">{% trans 'Lock' %}</a>
113
+ {% endif %}
114
+ {% if account.can_unlock %}
115
+ <a href="{{ account.get_action_unlock_url }}"
116
+ class="dropdown-item has-text-danger has-text-weight-bold">{% trans 'Unlock' %}</a>
117
+ {% endif %}
109
118
  </div>
110
119
  </div>
111
120
  </div>
@@ -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">
@@ -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">
@@ -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',
@@ -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 = None
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) -> AccountModel:
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=True)
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
- # limits the queryset in case of large querysets...
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 django_ledger.models import EntityModelValidationError, AccountModel
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 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')
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
- with self.assertRaises(EntityModelValidationError, msg='New entities do not have default coa'):
12
- entity_model.get_default_coa()
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
- self.assertEqual(entity_model.get_default_coa(raise_exception=False),
15
- None,
16
- msg='No exception should be raised when raise_exception is False')
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)
@@ -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-coa'),
9
+ name='account-create'),
22
10
  path('<slug:entity_slug>/<slug:coa_slug>/list/',
23
11
  views.AccountModelListView.as_view(),
24
- name='account-list-coa'),
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-coa'),
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
  ]
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
@@ -3,7 +3,7 @@ Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
3
  Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
4
 
5
5
  Contributions to this module:
6
- Miguel Sanda <msanda@arrobalytics.com>
6
+ * Miguel Sanda <msanda@arrobalytics.com>
7
7
  """
8
8
 
9
9
  from django_ledger.views.account import *