Added the cash summary report in the accounting application.
This commit is contained in:
		| @@ -21,7 +21,9 @@ | ||||
| from django.conf import settings | ||||
| from django.db import models | ||||
| from django.urls import reverse | ||||
| from django.utils import dateformat | ||||
|  | ||||
| from mia_core.template_filters import smart_month | ||||
| from mia_core.utils import get_multi_language_attr | ||||
|  | ||||
|  | ||||
| @@ -262,3 +264,40 @@ class Record(models.Model): | ||||
|     class Meta: | ||||
|         db_table = "accounting_records" | ||||
|         ordering = ["is_credit", "ord"] | ||||
|  | ||||
|  | ||||
| class RecordSummary(models.Model): | ||||
|     """A summary record.""" | ||||
|     month = models.DateField(primary_key=True) | ||||
|     credit_amount = models.PositiveIntegerField() | ||||
|     debit_amount = models.PositiveIntegerField() | ||||
|  | ||||
|     _label = None | ||||
|  | ||||
|     @property | ||||
|     def label(self): | ||||
|         if self._label is None: | ||||
|             self._label = smart_month(self.month) | ||||
|         return self._label | ||||
|  | ||||
|     @label.setter | ||||
|     def label(self, value): | ||||
|         self._label = value | ||||
|  | ||||
|     @property | ||||
|     def balance(self): | ||||
|         return self.credit_amount - self.debit_amount | ||||
|  | ||||
|     _cumulative_balance = None | ||||
|  | ||||
|     @property | ||||
|     def cumulative_balance(self): | ||||
|         return self._cumulative_balance | ||||
|  | ||||
|     @cumulative_balance.setter | ||||
|     def cumulative_balance(self, value): | ||||
|         self._cumulative_balance = value | ||||
|  | ||||
|     class Meta: | ||||
|         db_table = None | ||||
|         managed = False | ||||
|   | ||||
							
								
								
									
										150
									
								
								accounting/templates/accounting/cash_summary.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								accounting/templates/accounting/cash_summary.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | ||||
| {% extends "base.html" %} | ||||
| {% comment %} | ||||
| The Mia Accounting Application | ||||
| cash.html: The template for the accounting cash reports | ||||
|  | ||||
|  Copyright (c) 2020 imacat. | ||||
|  | ||||
|  Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  you may not use this file except in compliance with the License. | ||||
|  You may obtain a copy of the License at | ||||
|  | ||||
|      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|  Unless required by applicable law or agreed to in writing, software | ||||
|  distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  See the License for the specific language governing permissions and | ||||
|  limitations under the License. | ||||
|  | ||||
| Author: imacat@mail.imacat.idv.tw (imacat) | ||||
| First written: 2020/7/15 | ||||
| {% endcomment %} | ||||
| {% load i18n %} | ||||
| {% load humanize %} | ||||
| {% load accounting %} | ||||
|  | ||||
| {% block settings %} | ||||
|   {% blocktrans asvar title with subject=current_subject.title|title context "Accounting|" %}Cash Summary for {{ subject }}{% endblocktrans %} | ||||
|   {% setvar "title" title %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
|  | ||||
| <div class="btn-group btn-actions"> | ||||
|   <div class="btn-group"> | ||||
|     <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown"> | ||||
|       <i class="fas fa-edit"></i> | ||||
|       {% trans "New" context "Accounting|" as text %} | ||||
|       {{ text|force_escape }} | ||||
|     </button> | ||||
|     <div class="dropdown-menu"> | ||||
|       {% url "accounting:transaction.create" "expense" as url %} | ||||
|       <a class="dropdown-item" href="{% url_query url r=request.get_full_path %}"> | ||||
|         {% trans "Cash Expense" context "Accounting|" as text %} | ||||
|         {{ text|force_escape }} | ||||
|       </a> | ||||
|       {% url "accounting:transaction.create" "income" as url %} | ||||
|       <a class="dropdown-item" href="{% url_query url r=request.get_full_path %}"> | ||||
|         {% trans "Cash Income" context "Accounting|" as text %} | ||||
|         {{ text|force_escape }} | ||||
|       </a> | ||||
|       {% url "accounting:transaction.create" "transfer" as url %} | ||||
|       <a class="dropdown-item" href="{% url_query url r=request.get_full_path %}"> | ||||
|         {% trans "Transfer" context "Accounting|" as text %} | ||||
|         {{ text|force_escape }} | ||||
|       </a> | ||||
|     </div> | ||||
|   </div> | ||||
|   {% with current_report_icon="fas fa-money-bill-wave" %} | ||||
|     {% trans "Cash Summary" context "Accounting|" as current_report_title %} | ||||
|     {% include "accounting/include/report-chooser.html" %} | ||||
|   {% endwith %} | ||||
|   <div class="btn-group"> | ||||
|     <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown"> | ||||
|       <span class="d-none d-md-inline">{{ current_subject.title|title }}</span> | ||||
|       <span class="d-md-none">{% trans "Subject" context "Accounting|" as text %}{{ text|force_escape }}</span> | ||||
|     </button> | ||||
|     <div class="dropdown-menu subject-picker"> | ||||
|       <div class="dropdown-header">{% trans "Shortcuts" context "Accounting|Subject|" as text %}{{ text|force_escape }}</div> | ||||
|       {% for subject in shortcut_subjects %} | ||||
|         <a class="dropdown-item {% if subject.code == current_subject.code %}{% endif %}>" href="{% url "accounting:cash-summary" subject.code %}">{{ subject.title|title }}</a> | ||||
|       {% endfor %} | ||||
|       <div class="dropdown-header">{% trans "All" context "Accounting|Subject|" as text %}{{ text|force_escape }}</div> | ||||
|       {% for subject in all_sibjects %} | ||||
|         <a class="dropdown-item {% if subject.code == current_subject.code %}{% endif %}>" href="{% url "accounting:cash-summary" subject.code %}">{{ subject.title|title }}</a> | ||||
|       {% endfor %} | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| {% if records %} | ||||
|   {% include "mia_core/include/pagination.html" %} | ||||
|  | ||||
|   {# The table for large screens #} | ||||
|   <table class="table table-striped table-hover d-none d-sm-table general-journal-table"> | ||||
|     <thead> | ||||
|       <tr> | ||||
|         <th scope="col">{% trans "Month" context "Accounting|" as text %}{{ text|force_escape }}</th> | ||||
|         <th class="amount" scope="col">{% trans "Income" context "Accounting|" as text %}{{ text|force_escape }}</th> | ||||
|         <th class="amount" scope="col">{% trans "Expense" context "Accounting|" as text %}{{ text|force_escape }}</th> | ||||
|         <th class="amount" scope="col">{% trans "Balance" context "Accounting|" as text %}{{ text|force_escape }}</th> | ||||
|         <th class="amount" scope="col">{% trans "Cumulative Balance" context "Accounting|" as text %}{{ text|force_escape }}</th> | ||||
|         <th class="actions" scope="col">{% trans "View" context "Accounting|" as text %}{{ text|force_escape }}</th> | ||||
|       </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|       {% for record in records %} | ||||
|         <tr class="{% if record.balance < 0 %} table-danger {% endif %}"> | ||||
|           <td>{{ record.label }}</td> | ||||
|           <td class="amount">{{ record.credit_amount|accounting_amount }}</td> | ||||
|           <td class="amount">{{ record.debit_amount|accounting_amount }}</td> | ||||
|           <td class="amount {% if record.balance < 0 %} text-danger {% endif %}">{{ record.balance|accounting_amount }}</td> | ||||
|           <td class="amount {% if record.cumulative_balance < 0 %} text-danger {% endif %}">{{ record.cumulative_balance|accounting_amount }}</td> | ||||
|           <td class="actions"> | ||||
|             {% if record.month is not None %} | ||||
|               <a class="btn btn-info" role="button" href="{% url "accounting:cash" current_subject.code record.month|date:"Y-m" %}"> | ||||
|                 <i class="fas fa-eye"></i> | ||||
|                 <span class="d-none d-lg-inline">{% trans "View" context "Accounting|" as text %}{{ text|force_escape }}</span> | ||||
|               </a> | ||||
|             {% endif %} | ||||
|           </td> | ||||
|         </tr> | ||||
|       {% endfor %} | ||||
|     </tbody> | ||||
|   </table> | ||||
|  | ||||
|   {# The list for small screens #} | ||||
|   <ul class="list-group d-sm-none"> | ||||
|     {% for record in records %} | ||||
|       <li class="list-group-item {% if record.balance < 0 %} list-group-item-danger {% endif %}"> | ||||
|         {% if record.month is not None %} | ||||
|           <a class="list-group-item-action d-flex justify-content-between align-items-center" href="{% url "accounting:cash" current_subject.code record.month|date:"Y-m" %}"> | ||||
|             {{ record.label }} | ||||
|             <div> | ||||
|               <span class="badge badge-success badge-pill">{{ record.credit_amount|accounting_amount }}</span> | ||||
|               <span class="badge badge-warning badge-pill">{{ record.debit_amount|accounting_amount }}</span> | ||||
|               <span class="badge {% if record.balance < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">{{ record.balance|intcomma:False }}</span> | ||||
|               <span class="badge {% if record.cumulative_balance < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">{{ record.cumulative_balance|intcomma:False }}</span> | ||||
|             </div> | ||||
|           </a> | ||||
|         {% else %} | ||||
|           <div class="d-flex justify-content-between align-items-center"> | ||||
|             {{ record.label }} | ||||
|             <div> | ||||
|               <span class="badge badge-success badge-pill">{{ record.credit_amount|accounting_amount }}</span> | ||||
|               <span class="badge badge-warning badge-pill">{{ record.debit_amount|accounting_amount }}</span> | ||||
|               <span class="badge {% if record.balance < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">{{ record.balance|intcomma:False }}</span> | ||||
|               <span class="badge {% if record.cumulative_balance < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">{{ record.cumulative_balance|intcomma:False }}</span> | ||||
|             </div> | ||||
|           </div> | ||||
|         {% endif %} | ||||
|       </li> | ||||
|     {% endfor %} | ||||
|   </ul> | ||||
|  | ||||
| {% else %} | ||||
|   <p>{{ _("There is currently no data.")|force_escape }}</p> | ||||
| {% endif %} | ||||
|  | ||||
| {% endblock %} | ||||
| @@ -47,7 +47,7 @@ urlpatterns = [ | ||||
|     path("cash-summary", | ||||
|          mia_core_views.todo, name="cash-summary.home"), | ||||
|     path("cash-summary/<str:subject_code>", | ||||
|          mia_core_views.todo, name="cash-summary"), | ||||
|          views.cash_summary, name="cash-summary"), | ||||
|     path("ledger", | ||||
|          mia_core_views.todo, name="ledger.home"), | ||||
|     path("ledger/<str:subject_code>/<str:period_spec>", | ||||
|   | ||||
| @@ -29,7 +29,8 @@ from django.utils.timezone import localdate | ||||
| from django.utils.translation import get_language, pgettext | ||||
| from django.views.decorators.http import require_GET | ||||
|  | ||||
| from accounting.models import Record, Transaction, Subject | ||||
| from accounting.models import Record, Transaction, Subject, \ | ||||
|     RecordSummary | ||||
| from accounting.utils import ReportUrl | ||||
| from mia import settings | ||||
| from mia_core.digest_auth import digest_login_required | ||||
| @@ -64,17 +65,12 @@ def cash_home(request): | ||||
|         reverse("accounting:cash", args=(subject_code, period_spec))) | ||||
|  | ||||
|  | ||||
| @require_GET | ||||
| @digest_login_required | ||||
| def cash(request, subject_code, period_spec): | ||||
|     """The cash account report.""" | ||||
|     # The period | ||||
|     first_txn = Transaction.objects.order_by("date").first() | ||||
|     data_start = first_txn.date if first_txn is not None else None | ||||
|     last_txn = Transaction.objects.order_by("-date").first() | ||||
|     data_end = last_txn.date if last_txn is not None else None | ||||
|     period = Period(period_spec, data_start, data_end) | ||||
|     # The subject | ||||
| def _cash_subjects(): | ||||
|     """Returns the subjects for the cash account reports. | ||||
|  | ||||
|     Returns: | ||||
|         list[Subject]: The subjects for the cash account reports. | ||||
|     """ | ||||
|     subjects = list(Subject.objects.raw("""SELECT s.* | ||||
| FROM accounting_subjects AS s | ||||
|   WHERE s.code IN (SELECT s1.code | ||||
| @@ -91,6 +87,21 @@ FROM accounting_subjects AS s | ||||
|         title=pgettext( | ||||
|             "Accounting|", "current assets and liabilities"), | ||||
|     )) | ||||
|     return subjects | ||||
|  | ||||
|  | ||||
| @require_GET | ||||
| @digest_login_required | ||||
| def cash(request, subject_code, period_spec): | ||||
|     """The cash account report.""" | ||||
|     # The period | ||||
|     first_txn = Transaction.objects.order_by("date").first() | ||||
|     data_start = first_txn.date if first_txn is not None else None | ||||
|     last_txn = Transaction.objects.order_by("-date").first() | ||||
|     data_end = last_txn.date if last_txn is not None else None | ||||
|     period = Period(period_spec, data_start, data_end) | ||||
|     # The subject | ||||
|     subjects = _cash_subjects() | ||||
|     current_subject = None | ||||
|     for subject in subjects: | ||||
|         if subject.code == subject_code: | ||||
| @@ -219,3 +230,90 @@ ORDER BY | ||||
|         "shortcut_subjects": [x for x in subjects if x.code in settings.ACCOUNTING["CASH_SHORTCUT_SUBJECTS"]], | ||||
|         "all_sibjects": [x for x in subjects if x.code not in settings.ACCOUNTING["CASH_SHORTCUT_SUBJECTS"]], | ||||
|     }) | ||||
|  | ||||
|  | ||||
| def cash_summary(request, subject_code): | ||||
|     """The cash account summary report.""" | ||||
|     # The subject | ||||
|     subjects = _cash_subjects() | ||||
|     current_subject = None | ||||
|     for subject in subjects: | ||||
|         if subject.code == subject_code: | ||||
|             current_subject = subject | ||||
|     if current_subject is None: | ||||
|         raise Http404() | ||||
|     if connection.vendor == "postgresql": | ||||
|         month_definition = "CAST(DATE_TRUNC('month', t.date) AS date)" | ||||
|     elif connection.vendor == "sqlite": | ||||
|         month_definition = "DATE(t.date, 'start of month')" | ||||
|     else: | ||||
|         month_definition = None | ||||
|     # The SQL query | ||||
|     if current_subject.code == "0": | ||||
|         records = list(RecordSummary.objects.raw("""SELECT | ||||
|   """ + month_definition + """ AS month, | ||||
|   SUM(CASE WHEN r.is_credit THEN r.amount ELSE 0 END) AS credit_amount, | ||||
|   SUM(CASE WHEN r.is_credit THEN 0 ELSE r.amount END) AS debit_amount | ||||
| FROM accounting_records AS r | ||||
|   INNER JOIN (SELECT | ||||
|       t1.sn AS sn, | ||||
|       t1.date AS date, | ||||
|       t1.ord AS ord | ||||
|     FROM accounting_records AS r1 | ||||
|       LEFT JOIN accounting_transactions AS t1 ON r1.transaction_sn=t1.sn | ||||
|       LEFT JOIN accounting_subjects AS s1 ON r1.subject_sn = s1.sn | ||||
|     WHERE s1.code LIKE '11%%' | ||||
|       OR s1.code LIKE '12%%' | ||||
|       OR s1.code LIKE '21%%' | ||||
|       OR s1.code LIKE '22%%' | ||||
|     GROUP BY t1.sn) AS t | ||||
|   ON r.transaction_sn=t.sn | ||||
|   LEFT JOIN accounting_subjects AS s ON r.subject_sn = s.sn | ||||
| WHERE s.code NOT LIKE '11%%' | ||||
|   AND s.code NOT LIKE '12%%' | ||||
|   AND s.code NOT LIKE '21%%' | ||||
|   AND s.code NOT LIKE '22%%' | ||||
| GROUP BY month | ||||
| ORDER BY month""")) | ||||
|     else: | ||||
|         records = list(RecordSummary.objects.raw("""SELECT | ||||
|   """ + month_definition + """ AS month, | ||||
|   SUM(CASE WHEN r.is_credit THEN r.amount ELSE 0 END) AS credit_amount, | ||||
|   SUM(CASE WHEN r.is_credit THEN 0 ELSE r.amount END) AS debit_amount | ||||
| FROM accounting_records AS r | ||||
|   INNER JOIN (SELECT | ||||
|       t1.sn AS sn, | ||||
|       t1.date AS date, | ||||
|       t1.ord AS ord | ||||
|     FROM accounting_records AS r1 | ||||
|       LEFT JOIN accounting_transactions AS t1 ON r1.transaction_sn=t1.sn | ||||
|       LEFT JOIN accounting_subjects AS s1 ON r1.subject_sn = s1.sn | ||||
|     WHERE s1.code LIKE %s | ||||
|     GROUP BY t1.sn) AS t | ||||
|   ON r.transaction_sn=t.sn | ||||
|   LEFT JOIN accounting_subjects AS s ON r.subject_sn = s.sn | ||||
| WHERE s.code NOT LIKE %s | ||||
| GROUP BY month | ||||
| ORDER BY month""", | ||||
|             [current_subject.code + "%", | ||||
|              current_subject.code + "%"])) | ||||
|     cumulative_balance = 0 | ||||
|     for record in records: | ||||
|         cumulative_balance = cumulative_balance + record.balance | ||||
|         record.cumulative_balance = cumulative_balance | ||||
|     records.append(RecordSummary( | ||||
|         label=pgettext("Accounting|", "Total"), | ||||
|         credit_amount=sum([x.credit_amount for x in records]), | ||||
|         debit_amount=sum([x.debit_amount for x in records]), | ||||
|         cumulative_balance=cumulative_balance, | ||||
|     )) | ||||
|     pagination = Pagination(request, records, True) | ||||
|     params = { | ||||
|         "records": pagination.records, | ||||
|         "pagination": pagination, | ||||
|         "current_subject": current_subject, | ||||
|         "reports": ReportUrl(cash=current_subject), | ||||
|         "shortcut_subjects": [x for x in subjects if x.code in settings.ACCOUNTING["CASH_SHORTCUT_SUBJECTS"]], | ||||
|         "all_subjects": [x for x in subjects if x.code not in settings.ACCOUNTING["CASH_SHORTCUT_SUBJECTS"]], | ||||
|     } | ||||
|     return render(request, "accounting/cash_summary.html", params) | ||||
|   | ||||
| @@ -23,6 +23,7 @@ from datetime import date | ||||
|  | ||||
| from django import template | ||||
| from django.template import defaultfilters | ||||
| from django.utils.timezone import localdate | ||||
| from django.utils.translation import gettext | ||||
|  | ||||
| register = template.Library() | ||||
| @@ -45,3 +46,26 @@ def smart_date(value): | ||||
|     if date.today().year == value.year: | ||||
|         return defaultfilters.date(value, "n/j(D)").replace("星期", "") | ||||
|     return defaultfilters.date(value, "Y/n/j(D)").replace("星期", "") | ||||
|  | ||||
|  | ||||
| @register.filter | ||||
| def smart_month(value): | ||||
|     """Formats the month for human friendliness. | ||||
|  | ||||
|     Args: | ||||
|         value (datetime.date): The month. | ||||
|  | ||||
|     Returns: | ||||
|         str: The human-friendly format of the month. | ||||
|     """ | ||||
|     today = localdate() | ||||
|     if value.year == today.year and value.month == today.month: | ||||
|         return gettext("This Month") | ||||
|     month = today.month - 1 | ||||
|     year = today.year | ||||
|     if month < 1: | ||||
|         month = 12 | ||||
|         year = year - 1 | ||||
|     if value.year == year and value.month == month: | ||||
|         return gettext("Last Month") | ||||
|     return defaultfilters.date(value, "Y/n") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user