From 847f8dc402ce16ce6138a59020e523a5bb9e226d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BE=9D=E7=91=AA=E8=B2=93?= Date: Thu, 16 Jul 2020 22:12:59 +0800 Subject: [PATCH] Added the ledger in the accounting application. --- accounting/models.py | 26 +++ accounting/templates/accounting/ledger.html | 230 ++++++++++++++++++++ accounting/urls.py | 2 +- accounting/views/__init__.py | 93 ++++++++ 4 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 accounting/templates/accounting/ledger.html diff --git a/accounting/models.py b/accounting/models.py index 3fa6de0..e4e3197 100644 --- a/accounting/models.py +++ b/accounting/models.py @@ -279,6 +279,32 @@ class Record(models.Model): def has_order_hole(self, value): self._has_order_hole = value + _is_credit_card_paid = None + + @property + def is_credit_card_paid(self): + # TODO: To be done + if self._is_credit_card_paid is None: + self._is_credit_card_paid = True + return self._is_credit_card_paid + + @is_credit_card_paid.setter + def is_credit_card_paid(self, value): + self._is_credit_card_paid = value + + _is_existing_equipment = None + + @property + def is_existing_equipment(self): + # TODO: To be done + if self._is_existing_equipment is None: + self._is_existing_equipment = False + return self._is_existing_equipment + + @is_existing_equipment.setter + def is_existing_equipment(self, value): + self._is_existing_equipment = value + def __str__(self): """Returns the string representation of this accounting record.""" diff --git a/accounting/templates/accounting/ledger.html b/accounting/templates/accounting/ledger.html new file mode 100644 index 0000000..0ee31a2 --- /dev/null +++ b/accounting/templates/accounting/ledger.html @@ -0,0 +1,230 @@ +{% extends "base.html" %} +{% comment %} +The Mia Accounting Application +cash.html: The template for the ledger 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/16 +{% endcomment %} +{% load i18n %} +{% load humanize %} +{% load accounting %} + +{% block settings %} + {% blocktrans asvar title with subject=current_subject.title|title period=period.description context "Accounting|" %}Ledger for {{ subject }} in {{ period }}{% endblocktrans %} + {% setvar "title" title %} + {% setvar "use_period_chooser" True %} +{% endblock %} + +{% block content %} + +
+
+ + +
+ {% with current_report_icon="fas fa-file-invoice-dollar" %} + {% trans "Ledger" context "Accounting|" as current_report_title %} + {% include "accounting/include/report-chooser.html" %} + {% endwith %} +
+ + +
+ +
+ +{% include "mia_core/include/period-chooser.html" %} + +{% if records %} + {% include "mia_core/include/pagination.html" %} + + {# The table for large screens #} + + + + + + + + + + + + + + {% for record in records %} + + + + + + + + + + {% endfor %} + +
{% trans "Date" context "Accounting|" as text %}{{ text|force_escape }}{% trans "Subject" context "Accounting|" as text %}{{ text|force_escape }}{% trans "Summary" context "Accounting|" as text %}{{ text|force_escape }}{% trans "Debit" context "Accounting|" as text %}{{ text|force_escape }}{% trans "Credit" context "Accounting|" as text %}{{ text|force_escape }}{% trans "Balance" context "Accounting|" as text %}{{ text|force_escape }}{% trans "View" context "Accounting|" as text %}{{ text|force_escape }}
{{ record.transaction.date|smart_date }}{{ record.subject.title|title }}{{ record.summary|default:"" }}{% if not record.is_balanced %} + + {% trans "Unbalanced" context "Accounting|" as text %} + {{ text|force_escape }} + + {% endif %}{% if record.has_order_hole %} + + {% trans "Need Reorder" context "Accounting|" as text %} + {{ text|force_escape }} + + {% endif %}{% if not record.is_credit_card_paid %} + + {% trans "Unpaid" context "Accounting|" as text %} + {{ text|force_escape }} + + {% endif %}{% if record.is_existing_equipment %} + + {% trans "Existing" context "Accounting|" as text %} + {{ text|force_escape }} + + {% endif %}{{ record.debit_amount|accounting_amount }}{{ record.credit_amount|accounting_amount }}{{ record.balance|accounting_amount }} + {% if record.sn is not None %} + + + {% trans "View" context "Accounting|" as text %}{{ text|force_escape }} + + {% endif %} +
+ + {# The list for small screens #} + +{% else %} +

{{ _("There is currently no data.")|force_escape }}

+{% endif %} + +{% endblock %} diff --git a/accounting/urls.py b/accounting/urls.py index 89ca6bb..38908a0 100644 --- a/accounting/urls.py +++ b/accounting/urls.py @@ -51,7 +51,7 @@ urlpatterns = [ path("ledger", mia_core_views.todo, name="ledger.home"), path("ledger//", - mia_core_views.todo, name="ledger"), + views.ledger, name="ledger"), path("ledger-summary", mia_core_views.todo, name="ledger-summary.home"), path("ledger-summary/", diff --git a/accounting/views/__init__.py b/accounting/views/__init__.py index 66e560d..f01e9eb 100644 --- a/accounting/views/__init__.py +++ b/accounting/views/__init__.py @@ -18,6 +18,7 @@ """The view controllers of the accounting application. """ +import re from datetime import timedelta from django.db import connection @@ -357,3 +358,95 @@ ORDER BY month""", "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) + + +def _ledger_subjects(): + """Returns the subjects for the ledger reports. + + Returns: + list[Subject]: The subjects for the ledger reports. + """ + return list(Subject.objects.raw("""SELECT s.* + FROM accounting_subjects AS s + WHERE s.code IN (SELECT s.code + FROM accounting_subjects AS s + INNER JOIN (SELECT s.code + FROM accounting_subjects AS s + INNER JOIN accounting_records AS r ON r.subject_sn = s.sn + GROUP BY s.code) AS u + ON u.code LIKE s.code || '%' + GROUP BY s.code) + ORDER BY s.code""")) + + +@require_GET +@digest_login_required +def ledger(request, subject_code, period_spec): + """The ledger 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 = _ledger_subjects() + current_subject = None + for subject in subjects: + if subject.code == subject_code: + current_subject = subject + if current_subject is None: + raise Http404() + # The SQL query + select_records = """SELECT r.* + FROM accounting_records AS r + INNER JOIN accounting_transactions AS t ON r.transaction_sn = t.sn + INNER JOIN accounting_subjects AS s ON r.subject_sn = s.sn + WHERE t.date >= %s AND t.date <= %s AND s.code LIKE %s + ORDER BY t.date, t.ord, + CASE WHEN r.is_credit THEN 1 ELSE 2 END, r.ord""" + records = list(Record.objects.raw( + select_records, + [period.start, period.end, current_subject.code + "%"])) + if re.match("^[1-3]", current_subject.code) is not None: + select_balance_before = """SELECT + SUM(CASE WHEN is_credit THEN -1 ELSE 1 END * amount) + FROM (%s)""" % select_records + with connection.cursor() as cursor: + cursor.execute( + select_balance_before, + [data_start, + period.start - timedelta(days=1), + current_subject.code + "%"]) + row = cursor.fetchone() + balance = row[0] + record_brought_forward = Record( + transaction=Transaction(date=records[-1].transaction.date), + subject=current_subject, + summary=pgettext("Accounting|", "Brought Forward"), + is_credit=balance<0, + amount=abs(balance), + balance=balance, + ) + else: + balance = 0 + record_brought_forward = None + for record in records: + balance = balance + \ + (-1 if record.is_credit else 1) * record.amount + record.balance = balance + if record_brought_forward is not None: + records.insert(0, record_brought_forward) + pagination = Pagination(request, records, True) + records = pagination.records + _find_imbalanced(records) + _find_order_holes(records) + params = { + "records": records, + "pagination": pagination, + "current_subject": current_subject, + "period": period, + "reports": ReportUrl(ledger=current_subject, period=period), + "subjects": subjects, + } + return render(request, "accounting/ledger.html", params)