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.

Files changed (93) hide show
  1. django_ledger/__init__.py +1 -4
  2. django_ledger/admin/__init__.py +1 -1
  3. django_ledger/admin/{coa.py → chart_of_accounts.py} +1 -1
  4. django_ledger/admin/entity.py +1 -1
  5. django_ledger/contrib/django_ledger_graphene/accounts/schema.py +1 -1
  6. django_ledger/forms/account.py +43 -38
  7. django_ledger/forms/bank_account.py +5 -2
  8. django_ledger/forms/bill.py +24 -36
  9. django_ledger/forms/chart_of_accounts.py +82 -0
  10. django_ledger/forms/customer.py +1 -1
  11. django_ledger/forms/data_import.py +3 -3
  12. django_ledger/forms/estimate.py +1 -1
  13. django_ledger/forms/invoice.py +5 -7
  14. django_ledger/forms/item.py +24 -15
  15. django_ledger/forms/transactions.py +3 -3
  16. django_ledger/io/io_core.py +4 -2
  17. django_ledger/io/io_library.py +1 -1
  18. django_ledger/io/io_middleware.py +5 -0
  19. django_ledger/migrations/0017_alter_accountmodel_unique_together_and_more.py +31 -0
  20. django_ledger/migrations/0018_transactionmodel_cleared_transactionmodel_reconciled_and_more.py +37 -0
  21. django_ledger/models/__init__.py +1 -1
  22. django_ledger/models/accounts.py +229 -265
  23. django_ledger/models/bank_account.py +6 -6
  24. django_ledger/models/bill.py +7 -6
  25. django_ledger/models/{coa.py → chart_of_accounts.py} +187 -72
  26. django_ledger/models/closing_entry.py +5 -10
  27. django_ledger/models/coa_default.py +10 -9
  28. django_ledger/models/customer.py +6 -6
  29. django_ledger/models/data_import.py +12 -8
  30. django_ledger/models/entity.py +96 -39
  31. django_ledger/models/estimate.py +6 -10
  32. django_ledger/models/invoice.py +14 -11
  33. django_ledger/models/items.py +23 -14
  34. django_ledger/models/journal_entry.py +73 -30
  35. django_ledger/models/ledger.py +8 -8
  36. django_ledger/models/mixins.py +0 -3
  37. django_ledger/models/purchase_order.py +9 -9
  38. django_ledger/models/signals.py +0 -3
  39. django_ledger/models/transactions.py +24 -7
  40. django_ledger/models/unit.py +4 -3
  41. django_ledger/models/utils.py +0 -3
  42. django_ledger/models/vendor.py +4 -3
  43. django_ledger/settings.py +28 -3
  44. django_ledger/templates/django_ledger/account/account_create.html +2 -2
  45. django_ledger/templates/django_ledger/account/account_update.html +1 -1
  46. django_ledger/templates/django_ledger/account/tags/account_txs_table.html +1 -0
  47. django_ledger/templates/django_ledger/account/tags/accounts_table.html +29 -19
  48. django_ledger/templates/django_ledger/bills/bill_detail.html +3 -3
  49. django_ledger/templates/django_ledger/chart_of_accounts/coa_create.html +25 -0
  50. django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html +25 -6
  51. django_ledger/templates/django_ledger/chart_of_accounts/coa_update.html +2 -2
  52. django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html +10 -4
  53. django_ledger/templates/django_ledger/expense/tags/expense_item_table.html +7 -0
  54. django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +2 -2
  55. django_ledger/templates/django_ledger/includes/footer.html +2 -2
  56. django_ledger/templates/django_ledger/invoice/invoice_detail.html +3 -3
  57. django_ledger/templatetags/django_ledger.py +7 -1
  58. django_ledger/tests/base.py +23 -7
  59. django_ledger/tests/test_accounts.py +145 -9
  60. django_ledger/urls/account.py +17 -24
  61. django_ledger/urls/chart_of_accounts.py +6 -0
  62. django_ledger/utils.py +9 -36
  63. django_ledger/views/__init__.py +2 -2
  64. django_ledger/views/account.py +91 -116
  65. django_ledger/views/auth.py +1 -1
  66. django_ledger/views/bank_account.py +9 -11
  67. django_ledger/views/bill.py +91 -80
  68. django_ledger/views/{coa.py → chart_of_accounts.py} +49 -44
  69. django_ledger/views/closing_entry.py +8 -0
  70. django_ledger/views/customer.py +1 -1
  71. django_ledger/views/data_import.py +1 -1
  72. django_ledger/views/entity.py +1 -1
  73. django_ledger/views/estimate.py +13 -8
  74. django_ledger/views/feedback.py +1 -1
  75. django_ledger/views/financial_statement.py +1 -1
  76. django_ledger/views/home.py +1 -1
  77. django_ledger/views/inventory.py +9 -0
  78. django_ledger/views/invoice.py +5 -2
  79. django_ledger/views/item.py +58 -68
  80. django_ledger/views/journal_entry.py +1 -1
  81. django_ledger/views/ledger.py +3 -1
  82. django_ledger/views/mixins.py +25 -13
  83. django_ledger/views/purchase_order.py +1 -1
  84. django_ledger/views/transactions.py +1 -1
  85. django_ledger/views/unit.py +9 -0
  86. django_ledger/views/vendor.py +1 -1
  87. {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/AUTHORS.md +8 -2
  88. {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/METADATA +33 -44
  89. {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/RECORD +92 -89
  90. {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/WHEEL +1 -1
  91. django_ledger/forms/coa.py +0 -47
  92. {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/LICENSE +0 -0
  93. {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="box">
7
- <div class="columns is-centered is-multiline">
8
- {% for coa_model in coa_list %}
9
- <div class="column is-6">
10
- {% include 'django_ledger/chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %}
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
- {% endfor %}
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="{{ coa.get_update_url }}" method="post">
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="{% url 'django_ledger:entity-dashboard' entity_slug=view.kwargs.entity_slug %}"
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
- {% if not coa_model.is_default %}
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">
@@ -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="{% url 'django_ledger:account-detail' entity_slug=tx_digest.entity_slug account_pk=acc.account_uuid %}"
63
+ <a href="{{ acc.get_absolute_url }}"
64
64
  class="dropdown-item has-text-success">{% trans 'Detail' %}</a>
65
- <a href="{% url 'django_ledger:account-update' entity_slug=tx_digest.entity_slug account_pk=acc.account_uuid %}"
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://arrobalytics.com" target="_blank">Arrobalytics</a>. The source code is licensed
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',
@@ -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
  ]
@@ -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: EntityModel):
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!')