Added the cash summary report in the accounting application.

This commit is contained in:
依瑪貓 2020-07-16 00:28:50 +08:00
parent 77da7862c6
commit 983c2a5533
5 changed files with 324 additions and 13 deletions

View File

@ -21,7 +21,9 @@
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.urls import reverse 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 from mia_core.utils import get_multi_language_attr
@ -262,3 +264,40 @@ class Record(models.Model):
class Meta: class Meta:
db_table = "accounting_records" db_table = "accounting_records"
ordering = ["is_credit", "ord"] 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

View 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 %}

View File

@ -47,7 +47,7 @@ urlpatterns = [
path("cash-summary", path("cash-summary",
mia_core_views.todo, name="cash-summary.home"), mia_core_views.todo, name="cash-summary.home"),
path("cash-summary/<str:subject_code>", path("cash-summary/<str:subject_code>",
mia_core_views.todo, name="cash-summary"), views.cash_summary, name="cash-summary"),
path("ledger", path("ledger",
mia_core_views.todo, name="ledger.home"), mia_core_views.todo, name="ledger.home"),
path("ledger/<str:subject_code>/<str:period_spec>", path("ledger/<str:subject_code>/<str:period_spec>",

View File

@ -29,7 +29,8 @@ from django.utils.timezone import localdate
from django.utils.translation import get_language, pgettext from django.utils.translation import get_language, pgettext
from django.views.decorators.http import require_GET 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 accounting.utils import ReportUrl
from mia import settings from mia import settings
from mia_core.digest_auth import digest_login_required 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))) reverse("accounting:cash", args=(subject_code, period_spec)))
@require_GET def _cash_subjects():
@digest_login_required """Returns the subjects for the cash account reports.
def cash(request, subject_code, period_spec):
"""The cash account report.""" Returns:
# The period list[Subject]: The subjects for the cash account reports.
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 = list(Subject.objects.raw("""SELECT s.* subjects = list(Subject.objects.raw("""SELECT s.*
FROM accounting_subjects AS s FROM accounting_subjects AS s
WHERE s.code IN (SELECT s1.code WHERE s.code IN (SELECT s1.code
@ -91,6 +87,21 @@ FROM accounting_subjects AS s
title=pgettext( title=pgettext(
"Accounting|", "current assets and liabilities"), "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 current_subject = None
for subject in subjects: for subject in subjects:
if subject.code == subject_code: 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"]], "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"]], "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)

View File

@ -23,6 +23,7 @@ from datetime import date
from django import template from django import template
from django.template import defaultfilters from django.template import defaultfilters
from django.utils.timezone import localdate
from django.utils.translation import gettext from django.utils.translation import gettext
register = template.Library() register = template.Library()
@ -45,3 +46,26 @@ def smart_date(value):
if date.today().year == value.year: if date.today().year == value.year:
return defaultfilters.date(value, "n/j(D)").replace("星期", "") return defaultfilters.date(value, "n/j(D)").replace("星期", "")
return defaultfilters.date(value, "Y/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")