Added the cash summary report in the accounting application.
This commit is contained in:
parent
77da7862c6
commit
983c2a5533
@ -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
|
||||||
|
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",
|
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>",
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user