2020-07-21 08:20:57 +08:00
|
|
|
# The accounting application of the Mia project.
|
|
|
|
# by imacat <imacat@mail.imacat.idv.tw>, 2020/6/30
|
|
|
|
|
|
|
|
# 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.
|
|
|
|
|
|
|
|
"""The view controllers of the accounting application.
|
|
|
|
|
|
|
|
"""
|
2020-08-05 23:24:52 +08:00
|
|
|
import json
|
2020-07-21 08:20:57 +08:00
|
|
|
import re
|
2020-08-14 00:36:29 +08:00
|
|
|
from typing import Dict, Optional
|
2020-07-21 08:20:57 +08:00
|
|
|
|
2020-08-08 10:16:30 +08:00
|
|
|
from django.contrib import messages
|
2020-08-02 11:01:36 +08:00
|
|
|
from django.db import transaction
|
2020-08-12 22:52:56 +08:00
|
|
|
from django.db.models import Sum, Case, When, F, Q, Count, BooleanField, \
|
2020-08-14 00:36:29 +08:00
|
|
|
ExpressionWrapper, Exists, OuterRef
|
2020-08-12 22:52:56 +08:00
|
|
|
from django.db.models.functions import TruncMonth, Coalesce
|
2020-08-13 07:25:35 +08:00
|
|
|
from django.http import JsonResponse, HttpResponseRedirect, Http404, \
|
|
|
|
HttpRequest, HttpResponse
|
2020-08-09 19:05:57 +08:00
|
|
|
from django.shortcuts import render, redirect
|
2020-08-05 23:24:52 +08:00
|
|
|
from django.template.loader import render_to_string
|
2020-07-21 08:20:57 +08:00
|
|
|
from django.urls import reverse
|
2020-08-01 07:49:56 +08:00
|
|
|
from django.utils import timezone
|
2020-07-30 00:10:11 +08:00
|
|
|
from django.utils.decorators import method_decorator
|
2020-08-06 00:41:29 +08:00
|
|
|
from django.utils.translation import gettext as _, gettext_noop
|
2020-07-28 08:22:42 +08:00
|
|
|
from django.views.decorators.http import require_GET, require_POST
|
2020-08-09 14:07:47 +08:00
|
|
|
from django.views.generic import RedirectView, ListView, DetailView
|
2020-07-21 08:20:57 +08:00
|
|
|
|
2020-07-29 15:07:47 +08:00
|
|
|
from mia_core.period import Period
|
2020-08-16 22:38:35 +08:00
|
|
|
from mia_core.utils import Pagination, get_multi_lingual_search, \
|
|
|
|
PaginationException
|
2020-08-14 00:36:29 +08:00
|
|
|
from mia_core.views import DeleteView, FormView
|
2020-08-12 13:38:54 +08:00
|
|
|
from . import utils
|
2020-08-16 22:38:35 +08:00
|
|
|
from .forms import AccountForm, TransactionForm, TransactionSortForm
|
2020-08-03 22:48:43 +08:00
|
|
|
from .models import Record, Transaction, Account
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
@method_decorator(require_GET, name="dispatch")
|
|
|
|
class CashDefaultView(RedirectView):
|
|
|
|
"""The default cash account."""
|
|
|
|
query_string = True
|
|
|
|
pattern_name = "accounting:cash"
|
2020-07-23 14:17:05 +08:00
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
2020-08-12 13:38:54 +08:00
|
|
|
kwargs["account"] = utils.get_default_cash_account()
|
2020-07-30 00:10:11 +08:00
|
|
|
kwargs["period"] = Period.default_spec()
|
|
|
|
return super().get_redirect_url(*args, **kwargs)
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
@require_GET
|
2020-08-13 07:25:35 +08:00
|
|
|
def cash(request: HttpRequest, account: Account,
|
|
|
|
period: Period) -> HttpResponse:
|
2020-07-21 08:20:57 +08:00
|
|
|
"""The cash account.
|
|
|
|
|
|
|
|
Args:
|
2020-08-13 07:25:35 +08:00
|
|
|
request: The request.
|
|
|
|
account: The account.
|
|
|
|
period: The period.
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
Returns:
|
2020-08-13 07:25:35 +08:00
|
|
|
The response.
|
2020-07-21 08:20:57 +08:00
|
|
|
"""
|
|
|
|
# The accounting records
|
2020-07-23 08:33:53 +08:00
|
|
|
if account.code == "0":
|
2020-07-21 19:38:01 +08:00
|
|
|
records = list(
|
|
|
|
Record.objects
|
2020-08-02 00:08:53 +08:00
|
|
|
.filter(
|
2020-07-21 20:57:59 +08:00
|
|
|
Q(transaction__in=Transaction.objects.filter(
|
|
|
|
Q(date__gte=period.start),
|
|
|
|
Q(date__lte=period.end),
|
|
|
|
(Q(record__account__code__startswith="11") |
|
|
|
|
Q(record__account__code__startswith="12") |
|
|
|
|
Q(record__account__code__startswith="21") |
|
|
|
|
Q(record__account__code__startswith="22")))),
|
|
|
|
~Q(account__code__startswith="11"),
|
|
|
|
~Q(account__code__startswith="12"),
|
|
|
|
~Q(account__code__startswith="21"),
|
|
|
|
~Q(account__code__startswith="22"))
|
2020-08-02 00:08:53 +08:00
|
|
|
.order_by("transaction__date", "transaction__ord",
|
|
|
|
"is_credit", "ord"))
|
2020-07-30 00:10:11 +08:00
|
|
|
balance_before = Record.objects \
|
2020-07-21 19:41:53 +08:00
|
|
|
.filter(
|
2020-08-02 00:08:53 +08:00
|
|
|
Q(transaction__date__lt=period.start),
|
|
|
|
(Q(account__code__startswith="11") |
|
|
|
|
Q(account__code__startswith="12") |
|
|
|
|
Q(account__code__startswith="21") |
|
|
|
|
Q(account__code__startswith="21"))) \
|
2020-07-21 08:20:57 +08:00
|
|
|
.aggregate(
|
2020-08-02 00:08:53 +08:00
|
|
|
balance=Coalesce(Sum(Case(
|
|
|
|
When(is_credit=True, then=-1),
|
|
|
|
default=1) * F("amount")), 0))["balance"]
|
2020-07-21 08:20:57 +08:00
|
|
|
else:
|
2020-07-21 19:38:01 +08:00
|
|
|
records = list(
|
|
|
|
Record.objects
|
2020-08-02 00:08:53 +08:00
|
|
|
.filter(
|
2020-07-21 20:57:59 +08:00
|
|
|
Q(transaction__in=Transaction.objects.filter(
|
|
|
|
Q(date__gte=period.start),
|
|
|
|
Q(date__lte=period.end),
|
2020-07-23 08:33:53 +08:00
|
|
|
Q(record__account__code__startswith=account.code))),
|
|
|
|
~Q(account__code__startswith=account.code))
|
2020-08-02 00:08:53 +08:00
|
|
|
.order_by("transaction__date", "transaction__ord",
|
|
|
|
"is_credit", "ord"))
|
2020-07-30 00:10:11 +08:00
|
|
|
balance_before = Record.objects \
|
2020-07-21 19:41:53 +08:00
|
|
|
.filter(
|
2020-08-02 00:08:53 +08:00
|
|
|
transaction__date__lt=period.start,
|
|
|
|
account__code__startswith=account.code) \
|
2020-07-21 08:20:57 +08:00
|
|
|
.aggregate(
|
2020-08-02 00:08:53 +08:00
|
|
|
balance=Coalesce(Sum(Case(When(
|
|
|
|
is_credit=True, then=-1),
|
|
|
|
default=1) * F("amount")), 0))["balance"]
|
2020-07-21 08:20:57 +08:00
|
|
|
balance = balance_before
|
|
|
|
for record in records:
|
|
|
|
sign = 1 if record.is_credit else -1
|
|
|
|
balance = balance + sign * record.amount
|
|
|
|
record.balance = balance
|
|
|
|
record_sum = Record(
|
2020-08-02 00:08:53 +08:00
|
|
|
transaction=(Transaction(date=records[-1].transaction.date)
|
|
|
|
if len(records) > 0
|
|
|
|
else Transaction(date=timezone.localdate())),
|
2020-07-23 08:33:53 +08:00
|
|
|
account=account,
|
2020-08-06 00:41:29 +08:00
|
|
|
summary=_("Total"),
|
2020-07-21 08:20:57 +08:00
|
|
|
)
|
2020-08-02 17:55:07 +08:00
|
|
|
record_sum.balance = balance
|
2020-07-21 08:20:57 +08:00
|
|
|
record_sum.credit_amount = sum([
|
|
|
|
x.amount for x in records if x.is_credit])
|
|
|
|
record_sum.debit_amount = sum([
|
|
|
|
x.amount for x in records if not x.is_credit])
|
2020-08-02 17:55:07 +08:00
|
|
|
record_balance_before = Record(
|
2020-07-21 08:20:57 +08:00
|
|
|
transaction=Transaction(date=period.start),
|
2020-08-02 18:20:36 +08:00
|
|
|
account=Account.objects.get(code=Account.ACCUMULATED_BALANCE),
|
2020-07-21 08:20:57 +08:00
|
|
|
is_credit=balance_before >= 0,
|
|
|
|
amount=abs(balance_before),
|
2020-08-02 17:55:07 +08:00
|
|
|
)
|
|
|
|
record_balance_before.balance = balance_before
|
|
|
|
records.insert(0, record_balance_before)
|
2020-07-21 08:20:57 +08:00
|
|
|
records.append(record_sum)
|
2020-08-05 10:04:44 +08:00
|
|
|
try:
|
|
|
|
pagination = Pagination(request, records, True)
|
|
|
|
except PaginationException as e:
|
2020-08-09 19:05:57 +08:00
|
|
|
return redirect(e.url)
|
2020-07-21 08:20:57 +08:00
|
|
|
records = pagination.items
|
2020-08-12 13:38:54 +08:00
|
|
|
utils.find_imbalanced(records)
|
|
|
|
utils.find_order_holes(records)
|
|
|
|
accounts = utils.get_cash_accounts()
|
|
|
|
shortcut_accounts = utils.get_cash_shortcut_accounts()
|
2020-08-12 07:46:55 +08:00
|
|
|
return render(request, "accounting/report-cash.html", {
|
2020-08-08 13:53:48 +08:00
|
|
|
"record_list": records,
|
2020-07-21 08:20:57 +08:00
|
|
|
"pagination": pagination,
|
2020-07-21 10:38:47 +08:00
|
|
|
"shortcut_accounts": [x for x in accounts
|
|
|
|
if x.code in shortcut_accounts],
|
|
|
|
"all_accounts": [x for x in accounts
|
|
|
|
if x.code not in shortcut_accounts],
|
2020-07-21 08:20:57 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
@method_decorator(require_GET, name="dispatch")
|
|
|
|
class CashSummaryDefaultView(RedirectView):
|
|
|
|
"""The default cash account summary."""
|
|
|
|
query_string = True
|
|
|
|
pattern_name = "accounting:cash-summary"
|
2020-07-21 08:20:57 +08:00
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
2020-08-12 13:38:54 +08:00
|
|
|
kwargs["account"] = utils.get_default_cash_account()
|
2020-07-30 00:10:11 +08:00
|
|
|
return super().get_redirect_url(*args, **kwargs)
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
@require_GET
|
2020-08-13 07:25:35 +08:00
|
|
|
def cash_summary(request: HttpRequest, account: Account) -> HttpResponse:
|
2020-07-21 08:20:57 +08:00
|
|
|
"""The cash account summary.
|
|
|
|
|
|
|
|
Args:
|
2020-08-13 07:25:35 +08:00
|
|
|
request: The request.
|
|
|
|
account: The account.
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
Returns:
|
2020-08-13 07:25:35 +08:00
|
|
|
The response.
|
2020-07-21 08:20:57 +08:00
|
|
|
"""
|
2020-07-21 10:38:47 +08:00
|
|
|
# The account
|
2020-08-12 13:38:54 +08:00
|
|
|
accounts = utils.get_cash_accounts()
|
2020-07-21 08:20:57 +08:00
|
|
|
# The month summaries
|
2020-07-23 08:33:53 +08:00
|
|
|
if account.code == "0":
|
2020-08-12 13:38:54 +08:00
|
|
|
months = [utils.MonthlySummary(**x) for x in Record.objects
|
2020-08-02 00:08:53 +08:00
|
|
|
.filter(
|
|
|
|
Q(transaction__in=Transaction.objects.filter(
|
|
|
|
Q(record__account__code__startswith="11") |
|
|
|
|
Q(record__account__code__startswith="12") |
|
|
|
|
Q(record__account__code__startswith="21") |
|
|
|
|
Q(record__account__code__startswith="22"))),
|
|
|
|
~Q(account__code__startswith="11"),
|
|
|
|
~Q(account__code__startswith="12"),
|
|
|
|
~Q(account__code__startswith="21"),
|
|
|
|
~Q(account__code__startswith="22"))
|
|
|
|
.annotate(month=TruncMonth("transaction__date"))
|
|
|
|
.values("month")
|
|
|
|
.order_by("month")
|
|
|
|
.annotate(
|
|
|
|
debit=Coalesce(
|
|
|
|
Sum(Case(When(is_credit=False, then=F("amount")))),
|
|
|
|
0),
|
|
|
|
credit=Coalesce(
|
|
|
|
Sum(Case(When(is_credit=True, then=F("amount")))),
|
|
|
|
0),
|
|
|
|
balance=Sum(Case(
|
|
|
|
When(is_credit=False, then=-F("amount")),
|
|
|
|
default=F("amount"))))]
|
2020-07-21 08:20:57 +08:00
|
|
|
else:
|
2020-08-12 13:38:54 +08:00
|
|
|
months = [utils.MonthlySummary(**x) for x in Record.objects
|
2020-08-02 00:08:53 +08:00
|
|
|
.filter(
|
|
|
|
Q(transaction__in=Transaction.objects.filter(
|
|
|
|
record__account__code__startswith=account.code)),
|
|
|
|
~Q(account__code__startswith=account.code))
|
|
|
|
.annotate(month=TruncMonth("transaction__date"))
|
|
|
|
.values("month")
|
|
|
|
.order_by("month")
|
|
|
|
.annotate(
|
|
|
|
debit=Coalesce(
|
|
|
|
Sum(Case(When(is_credit=False, then=F("amount")))),
|
|
|
|
0),
|
|
|
|
credit=Coalesce(
|
|
|
|
Sum(Case(When(is_credit=True, then=F("amount")))),
|
|
|
|
0),
|
|
|
|
balance=Sum(Case(
|
|
|
|
When(is_credit=False, then=-F("amount")),
|
|
|
|
default=F("amount"))))]
|
2020-07-21 08:20:57 +08:00
|
|
|
cumulative_balance = 0
|
|
|
|
for month in months:
|
|
|
|
cumulative_balance = cumulative_balance + month.balance
|
|
|
|
month.cumulative_balance = cumulative_balance
|
2020-08-12 13:38:54 +08:00
|
|
|
months.append(utils.MonthlySummary(
|
2020-08-06 00:41:29 +08:00
|
|
|
label=_("Total"),
|
2020-07-21 08:20:57 +08:00
|
|
|
credit=sum([x.credit for x in months]),
|
|
|
|
debit=sum([x.debit for x in months]),
|
|
|
|
balance=sum([x.balance for x in months]),
|
2020-08-03 22:48:43 +08:00
|
|
|
cumulative_balance=cumulative_balance,
|
|
|
|
))
|
2020-08-05 13:38:12 +08:00
|
|
|
try:
|
|
|
|
pagination = Pagination(request, months, True)
|
|
|
|
except PaginationException as e:
|
2020-08-09 19:05:57 +08:00
|
|
|
return redirect(e.url)
|
2020-08-12 13:38:54 +08:00
|
|
|
shortcut_accounts = utils.get_cash_shortcut_accounts()
|
2020-08-12 07:46:55 +08:00
|
|
|
return render(request, "accounting/report-cash-summary.html", {
|
2020-08-08 13:53:48 +08:00
|
|
|
"month_list": pagination.items,
|
2020-07-21 08:20:57 +08:00
|
|
|
"pagination": pagination,
|
2020-07-21 10:38:47 +08:00
|
|
|
"shortcut_accounts": [x for x in accounts if
|
|
|
|
x.code in shortcut_accounts],
|
|
|
|
"all_accounts": [x for x in accounts if
|
|
|
|
x.code not in shortcut_accounts],
|
2020-07-21 08:20:57 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
@method_decorator(require_GET, name="dispatch")
|
|
|
|
class LedgerDefaultView(RedirectView):
|
|
|
|
"""The default ledger."""
|
|
|
|
query_string = True
|
|
|
|
pattern_name = "accounting:ledger"
|
2020-07-21 08:20:57 +08:00
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
2020-08-12 13:38:54 +08:00
|
|
|
kwargs["account"] = utils.get_default_ledger_account()
|
2020-07-30 00:10:11 +08:00
|
|
|
kwargs["period"] = Period.default_spec()
|
|
|
|
return super().get_redirect_url(*args, **kwargs)
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
@require_GET
|
2020-08-13 07:25:35 +08:00
|
|
|
def ledger(request: HttpRequest, account: Account,
|
|
|
|
period: Period) -> HttpResponse:
|
2020-07-21 08:20:57 +08:00
|
|
|
"""The ledger.
|
|
|
|
|
|
|
|
Args:
|
2020-08-13 07:25:35 +08:00
|
|
|
request: The request.
|
|
|
|
account: The account.
|
|
|
|
period: The period.
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
Returns:
|
2020-08-13 07:25:35 +08:00
|
|
|
The response.
|
2020-07-21 08:20:57 +08:00
|
|
|
"""
|
|
|
|
# The accounting records
|
2020-07-21 19:38:01 +08:00
|
|
|
records = list(
|
|
|
|
Record.objects
|
2020-08-02 00:08:53 +08:00
|
|
|
.filter(
|
2020-07-21 20:57:59 +08:00
|
|
|
transaction__date__gte=period.start,
|
|
|
|
transaction__date__lte=period.end,
|
2020-07-23 08:33:53 +08:00
|
|
|
account__code__startswith=account.code)
|
2020-08-02 00:08:53 +08:00
|
|
|
.order_by("transaction__date", "transaction__ord", "is_credit",
|
|
|
|
"ord"))
|
2020-07-23 08:33:53 +08:00
|
|
|
if re.match("^[1-3]", account.code) is not None:
|
2020-07-30 00:10:11 +08:00
|
|
|
balance = Record.objects \
|
2020-07-21 19:41:53 +08:00
|
|
|
.filter(
|
2020-08-02 00:08:53 +08:00
|
|
|
transaction__date__lt=period.start,
|
|
|
|
account__code__startswith=account.code) \
|
2020-07-21 08:20:57 +08:00
|
|
|
.aggregate(
|
2020-08-02 00:08:53 +08:00
|
|
|
balance=Coalesce(Sum(Case(When(
|
|
|
|
is_credit=True, then=-1),
|
|
|
|
default=1) * F("amount")), 0))["balance"]
|
2020-07-21 08:20:57 +08:00
|
|
|
record_brought_forward = Record(
|
|
|
|
transaction=Transaction(date=period.start),
|
2020-07-23 08:33:53 +08:00
|
|
|
account=account,
|
2020-08-06 00:41:29 +08:00
|
|
|
summary=_("Brought Forward"),
|
2020-07-21 08:20:57 +08:00
|
|
|
is_credit=balance < 0,
|
|
|
|
amount=abs(balance),
|
|
|
|
)
|
2020-08-02 17:55:07 +08:00
|
|
|
record_brought_forward.balance = balance
|
2020-07-21 08:20:57 +08:00
|
|
|
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)
|
2020-08-05 13:38:12 +08:00
|
|
|
try:
|
|
|
|
pagination = Pagination(request, records, True)
|
|
|
|
except PaginationException as e:
|
2020-08-09 19:05:57 +08:00
|
|
|
return redirect(e.url)
|
2020-07-21 08:20:57 +08:00
|
|
|
records = pagination.items
|
2020-08-12 13:38:54 +08:00
|
|
|
utils.find_imbalanced(records)
|
|
|
|
utils.find_order_holes(records)
|
|
|
|
utils.find_payable_records(account, records)
|
|
|
|
utils.find_existing_equipments(account, records)
|
2020-08-12 07:46:55 +08:00
|
|
|
return render(request, "accounting/report-ledger.html", {
|
2020-08-08 13:53:48 +08:00
|
|
|
"record_list": records,
|
2020-07-21 08:20:57 +08:00
|
|
|
"pagination": pagination,
|
2020-08-12 13:38:54 +08:00
|
|
|
"accounts": utils.get_ledger_accounts(),
|
2020-07-21 08:20:57 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
@method_decorator(require_GET, name="dispatch")
|
|
|
|
class LedgerSummaryDefaultView(RedirectView):
|
|
|
|
"""The default ledger summary."""
|
|
|
|
query_string = True
|
|
|
|
pattern_name = "accounting:ledger-summary"
|
2020-07-21 08:20:57 +08:00
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
2020-08-12 13:38:54 +08:00
|
|
|
kwargs["account"] = utils.get_default_ledger_account()
|
2020-07-30 00:10:11 +08:00
|
|
|
return super().get_redirect_url(*args, **kwargs)
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
@require_GET
|
2020-08-13 07:25:35 +08:00
|
|
|
def ledger_summary(request: HttpRequest, account: Account) -> HttpResponse:
|
2020-07-21 08:20:57 +08:00
|
|
|
"""The ledger summary report.
|
|
|
|
|
|
|
|
Args:
|
2020-08-13 07:25:35 +08:00
|
|
|
request: The request.
|
|
|
|
account: The account.
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
Returns:
|
2020-08-13 07:25:35 +08:00
|
|
|
The response.
|
2020-07-21 08:20:57 +08:00
|
|
|
"""
|
|
|
|
# The month summaries
|
2020-08-12 13:38:54 +08:00
|
|
|
months = [utils.MonthlySummary(**x) for x in Record.objects
|
2020-08-02 00:08:53 +08:00
|
|
|
.filter(account__code__startswith=account.code)
|
|
|
|
.annotate(month=TruncMonth("transaction__date"))
|
|
|
|
.values("month")
|
|
|
|
.order_by("month")
|
|
|
|
.annotate(
|
|
|
|
debit=Coalesce(
|
|
|
|
Sum(Case(When(is_credit=False, then=F("amount")))),
|
|
|
|
0),
|
|
|
|
credit=Coalesce(
|
|
|
|
Sum(Case(When(is_credit=True, then=F("amount")))),
|
|
|
|
0),
|
|
|
|
balance=Sum(Case(
|
|
|
|
When(is_credit=False, then=F("amount")),
|
|
|
|
default=-F("amount"))))]
|
2020-07-21 08:20:57 +08:00
|
|
|
cumulative_balance = 0
|
|
|
|
for month in months:
|
|
|
|
cumulative_balance = cumulative_balance + month.balance
|
|
|
|
month.cumulative_balance = cumulative_balance
|
2020-08-12 13:38:54 +08:00
|
|
|
months.append(utils.MonthlySummary(
|
2020-08-06 00:41:29 +08:00
|
|
|
label=_("Total"),
|
2020-07-21 08:20:57 +08:00
|
|
|
credit=sum([x.credit for x in months]),
|
|
|
|
debit=sum([x.debit for x in months]),
|
|
|
|
balance=sum([x.balance for x in months]),
|
2020-08-03 22:48:43 +08:00
|
|
|
cumulative_balance=cumulative_balance,
|
|
|
|
))
|
2020-08-05 13:38:12 +08:00
|
|
|
try:
|
|
|
|
pagination = Pagination(request, months, True)
|
|
|
|
except PaginationException as e:
|
2020-08-09 19:05:57 +08:00
|
|
|
return redirect(e.url)
|
2020-08-12 07:46:55 +08:00
|
|
|
return render(request, "accounting/report-ledger-summary.html", {
|
2020-08-08 13:53:48 +08:00
|
|
|
"month_list": pagination.items,
|
2020-07-21 08:20:57 +08:00
|
|
|
"pagination": pagination,
|
2020-08-12 13:38:54 +08:00
|
|
|
"accounts": utils.get_ledger_accounts(),
|
2020-07-21 08:20:57 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
@method_decorator(require_GET, name="dispatch")
|
|
|
|
class JournalDefaultView(RedirectView):
|
|
|
|
"""The default journal."""
|
|
|
|
query_string = True
|
|
|
|
pattern_name = "accounting:journal"
|
2020-07-21 08:20:57 +08:00
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
|
|
kwargs["period"] = Period.default_spec()
|
|
|
|
return super().get_redirect_url(*args, **kwargs)
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
@require_GET
|
2020-08-13 07:25:35 +08:00
|
|
|
def journal(request: HttpRequest, period: Period) -> HttpResponse:
|
2020-07-21 08:20:57 +08:00
|
|
|
"""The journal.
|
|
|
|
|
|
|
|
Args:
|
2020-08-13 07:25:35 +08:00
|
|
|
request: The request.
|
|
|
|
period: The period.
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
Returns:
|
2020-08-13 07:25:35 +08:00
|
|
|
The response.
|
2020-07-21 08:20:57 +08:00
|
|
|
"""
|
|
|
|
# The accounting records
|
2020-07-30 00:10:11 +08:00
|
|
|
records = Record.objects \
|
2020-07-21 19:38:01 +08:00
|
|
|
.filter(
|
2020-08-02 00:08:53 +08:00
|
|
|
transaction__date__gte=period.start,
|
|
|
|
transaction__date__lte=period.end) \
|
2020-07-22 08:25:43 +08:00
|
|
|
.order_by("transaction__date", "transaction__ord", "is_credit", "ord")
|
2020-07-21 08:20:57 +08:00
|
|
|
# The brought-forward records
|
2020-07-30 00:10:11 +08:00
|
|
|
brought_forward_accounts = Account.objects \
|
2020-07-21 19:41:53 +08:00
|
|
|
.filter(
|
2020-08-02 00:08:53 +08:00
|
|
|
Q(code__startswith="1")
|
|
|
|
| Q(code__startswith="2")
|
|
|
|
| Q(code__startswith="3")) \
|
2020-07-21 08:20:57 +08:00
|
|
|
.annotate(balance=Sum(
|
2020-08-02 00:08:53 +08:00
|
|
|
Case(
|
|
|
|
When(record__is_credit=True, then=-1),
|
|
|
|
default=1
|
|
|
|
) * F("record__amount"),
|
|
|
|
filter=Q(record__transaction__date__lt=period.start))) \
|
2020-07-22 10:34:53 +08:00
|
|
|
.filter(~Q(balance=0))
|
2020-07-21 08:20:57 +08:00
|
|
|
debit_records = [Record(
|
|
|
|
transaction=Transaction(date=period.start),
|
2020-07-21 10:38:47 +08:00
|
|
|
account=x,
|
2020-07-21 08:20:57 +08:00
|
|
|
is_credit=False,
|
|
|
|
amount=x.balance
|
2020-07-21 10:38:47 +08:00
|
|
|
) for x in brought_forward_accounts if x.balance > 0]
|
2020-07-21 08:20:57 +08:00
|
|
|
credit_records = [Record(
|
|
|
|
transaction=Transaction(date=period.start),
|
2020-07-21 10:38:47 +08:00
|
|
|
account=x,
|
2020-07-21 08:20:57 +08:00
|
|
|
is_credit=True,
|
|
|
|
amount=-x.balance
|
2020-07-21 10:38:47 +08:00
|
|
|
) for x in brought_forward_accounts if x.balance < 0]
|
2020-07-21 08:20:57 +08:00
|
|
|
sum_debits = sum([x.amount for x in debit_records])
|
|
|
|
sum_credits = sum([x.amount for x in credit_records])
|
|
|
|
if sum_debits < sum_credits:
|
|
|
|
debit_records.append(Record(
|
|
|
|
transaction=Transaction(date=period.start),
|
2020-08-02 18:20:36 +08:00
|
|
|
account=Account.objects.get(code=Account.ACCUMULATED_BALANCE),
|
2020-07-21 08:20:57 +08:00
|
|
|
is_credit=False,
|
|
|
|
amount=sum_credits - sum_debits
|
|
|
|
))
|
|
|
|
elif sum_debits > sum_credits:
|
|
|
|
credit_records.append(Record(
|
|
|
|
transaction=Transaction(date=period.start),
|
2020-08-02 18:20:36 +08:00
|
|
|
account=Account.objects.get(code=Account.ACCUMULATED_BALANCE),
|
2020-07-21 08:20:57 +08:00
|
|
|
is_credit=True,
|
|
|
|
amount=sum_debits - sum_credits
|
|
|
|
))
|
2020-08-02 00:08:53 +08:00
|
|
|
records = list(debit_records) + list(credit_records) + list(records)
|
2020-08-05 13:38:12 +08:00
|
|
|
try:
|
|
|
|
pagination = Pagination(request, records, True)
|
|
|
|
except PaginationException as e:
|
2020-08-09 19:05:57 +08:00
|
|
|
return redirect(e.url)
|
2020-08-12 07:46:55 +08:00
|
|
|
return render(request, "accounting/report-journal.html", {
|
2020-08-08 13:53:48 +08:00
|
|
|
"record_list": pagination.items,
|
2020-07-21 08:20:57 +08:00
|
|
|
"pagination": pagination,
|
|
|
|
})
|
|
|
|
|
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
@method_decorator(require_GET, name="dispatch")
|
|
|
|
class TrialBalanceDefaultView(RedirectView):
|
|
|
|
"""The default trial balance."""
|
|
|
|
query_string = True
|
|
|
|
pattern_name = "accounting:trial-balance"
|
2020-07-21 08:20:57 +08:00
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
|
|
kwargs["period"] = Period.default_spec()
|
|
|
|
return super().get_redirect_url(*args, **kwargs)
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
@require_GET
|
2020-08-13 07:25:35 +08:00
|
|
|
def trial_balance(request: HttpRequest, period: Period) -> HttpResponse:
|
2020-07-21 08:20:57 +08:00
|
|
|
"""The trial balance.
|
|
|
|
|
|
|
|
Args:
|
2020-08-13 07:25:35 +08:00
|
|
|
request: The request.
|
|
|
|
period: The period.
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
Returns:
|
2020-08-13 07:25:35 +08:00
|
|
|
The response.
|
2020-07-21 08:20:57 +08:00
|
|
|
"""
|
|
|
|
# The accounts
|
|
|
|
nominal = list(
|
2020-07-21 19:38:01 +08:00
|
|
|
Account.objects
|
2020-08-02 00:08:53 +08:00
|
|
|
.filter(
|
2020-07-21 20:57:59 +08:00
|
|
|
Q(record__transaction__date__gte=period.start),
|
|
|
|
Q(record__transaction__date__lte=period.end),
|
|
|
|
~(Q(code__startswith="1")
|
|
|
|
| Q(code__startswith="2")
|
|
|
|
| Q(code__startswith="3")))
|
2020-08-02 00:08:53 +08:00
|
|
|
.annotate(
|
2020-08-02 19:42:48 +08:00
|
|
|
amount=Sum(Case(
|
2020-07-21 20:57:59 +08:00
|
|
|
When(record__is_credit=True, then=-1),
|
|
|
|
default=1) * F("record__amount")))
|
2020-08-11 11:13:01 +08:00
|
|
|
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
2020-08-02 00:08:53 +08:00
|
|
|
.annotate(
|
2020-08-02 19:42:48 +08:00
|
|
|
debit_amount=Case(
|
|
|
|
When(amount__gt=0, then=F("amount")),
|
2020-07-21 20:57:59 +08:00
|
|
|
default=None),
|
2020-08-02 19:42:48 +08:00
|
|
|
credit_amount=Case(
|
|
|
|
When(amount__lt=0, then=-F("amount")),
|
2020-07-21 20:57:59 +08:00
|
|
|
default=None))
|
2020-08-02 00:08:53 +08:00
|
|
|
.order_by("code"))
|
2020-07-21 08:20:57 +08:00
|
|
|
real = list(
|
2020-07-21 10:38:47 +08:00
|
|
|
Account.objects
|
2020-08-02 00:08:53 +08:00
|
|
|
.filter(
|
2020-07-21 20:57:59 +08:00
|
|
|
Q(record__transaction__date__lte=period.end),
|
|
|
|
(Q(code__startswith="1")
|
|
|
|
| Q(code__startswith="2")
|
|
|
|
| Q(code__startswith="3")),
|
2020-08-02 18:20:36 +08:00
|
|
|
~Q(code=Account.ACCUMULATED_BALANCE))
|
2020-08-02 00:08:53 +08:00
|
|
|
.annotate(
|
2020-08-02 19:42:48 +08:00
|
|
|
amount=Sum(Case(
|
2020-07-21 20:57:59 +08:00
|
|
|
When(record__is_credit=True, then=-1),
|
|
|
|
default=1) * F("record__amount")))
|
2020-08-11 11:13:01 +08:00
|
|
|
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
2020-08-02 00:08:53 +08:00
|
|
|
.annotate(
|
2020-08-02 19:42:48 +08:00
|
|
|
debit_amount=Case(
|
|
|
|
When(amount__gt=0, then=F("amount")),
|
2020-07-21 20:57:59 +08:00
|
|
|
default=None),
|
2020-08-02 19:42:48 +08:00
|
|
|
credit_amount=Case(
|
|
|
|
When(amount__lt=0, then=-F("amount")),
|
2020-07-21 20:57:59 +08:00
|
|
|
default=None))
|
2020-08-02 00:08:53 +08:00
|
|
|
.order_by("code"))
|
2020-07-30 00:10:11 +08:00
|
|
|
balance = Record.objects \
|
2020-07-21 19:38:01 +08:00
|
|
|
.filter(
|
2020-08-02 00:08:53 +08:00
|
|
|
(Q(transaction__date__lt=period.start)
|
|
|
|
& ~(Q(account__code__startswith="1")
|
|
|
|
| Q(account__code__startswith="2")
|
|
|
|
| Q(account__code__startswith="3")))
|
|
|
|
| (Q(transaction__date__lte=period.end)
|
2020-08-02 18:20:36 +08:00
|
|
|
& Q(account__code=Account.ACCUMULATED_BALANCE))) \
|
2020-07-21 08:20:57 +08:00
|
|
|
.aggregate(
|
2020-08-02 00:08:53 +08:00
|
|
|
balance=Sum(Case(
|
|
|
|
When(is_credit=True, then=-1),
|
|
|
|
default=1) * F("amount")))["balance"]
|
2020-07-21 08:20:57 +08:00
|
|
|
if balance is not None and balance != 0:
|
2020-08-02 18:20:36 +08:00
|
|
|
brought_forward = Account.objects.get(
|
|
|
|
code=Account.ACCUMULATED_BALANCE)
|
2020-07-21 08:20:57 +08:00
|
|
|
if balance > 0:
|
2020-08-02 19:42:48 +08:00
|
|
|
brought_forward.debit_amount = balance
|
|
|
|
brought_forward.credit_amount = None
|
2020-07-21 08:20:57 +08:00
|
|
|
else:
|
2020-08-02 19:42:48 +08:00
|
|
|
brought_forward.debit_amount = None
|
|
|
|
brought_forward.credit_amount = -balance
|
2020-07-21 08:20:57 +08:00
|
|
|
real.append(brought_forward)
|
|
|
|
accounts = nominal + real
|
|
|
|
accounts.sort(key=lambda x: x.code)
|
2020-07-21 10:38:47 +08:00
|
|
|
total_account = Account()
|
2020-08-06 00:41:29 +08:00
|
|
|
total_account.title = _("Total")
|
2020-08-02 19:42:48 +08:00
|
|
|
total_account.debit_amount = sum([x.debit_amount for x in accounts
|
|
|
|
if x.debit_amount is not None])
|
|
|
|
total_account.credit_amount = sum([x.credit_amount for x in accounts
|
|
|
|
if x.credit_amount is not None])
|
2020-08-12 07:46:55 +08:00
|
|
|
return render(request, "accounting/report-trial-balance.html", {
|
2020-08-08 13:53:48 +08:00
|
|
|
"account_list": accounts,
|
2020-07-21 08:20:57 +08:00
|
|
|
"total_item": total_account,
|
|
|
|
})
|
|
|
|
|
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
@method_decorator(require_GET, name="dispatch")
|
|
|
|
class IncomeStatementDefaultView(RedirectView):
|
|
|
|
"""The default income statement."""
|
|
|
|
query_string = True
|
|
|
|
pattern_name = "accounting:income-statement"
|
2020-07-21 08:20:57 +08:00
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
|
|
kwargs["period"] = Period.default_spec()
|
|
|
|
return super().get_redirect_url(*args, **kwargs)
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
@require_GET
|
2020-08-13 07:25:35 +08:00
|
|
|
def income_statement(request: HttpRequest, period: Period) -> HttpResponse:
|
2020-07-21 08:20:57 +08:00
|
|
|
"""The income statement.
|
|
|
|
|
|
|
|
Args:
|
2020-08-13 07:25:35 +08:00
|
|
|
request: The request.
|
|
|
|
period: The period.
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
Returns:
|
2020-08-13 07:25:35 +08:00
|
|
|
The response.
|
2020-07-21 08:20:57 +08:00
|
|
|
"""
|
|
|
|
# The accounts
|
|
|
|
accounts = list(
|
2020-07-21 19:38:01 +08:00
|
|
|
Account.objects
|
2020-08-02 00:08:53 +08:00
|
|
|
.filter(
|
2020-07-21 20:57:59 +08:00
|
|
|
Q(record__transaction__date__gte=period.start),
|
|
|
|
Q(record__transaction__date__lte=period.end),
|
|
|
|
~(Q(code__startswith="1")
|
|
|
|
| Q(code__startswith="2")
|
|
|
|
| Q(code__startswith="3")))
|
2020-08-02 00:08:53 +08:00
|
|
|
.annotate(
|
2020-08-02 19:42:48 +08:00
|
|
|
amount=Sum(Case(
|
2020-07-21 20:57:59 +08:00
|
|
|
When(record__is_credit=True, then=1),
|
|
|
|
default=-1) * F("record__amount")))
|
2020-08-11 11:13:01 +08:00
|
|
|
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
2020-08-02 00:08:53 +08:00
|
|
|
.order_by("code"))
|
2020-07-21 10:38:47 +08:00
|
|
|
groups = list(Account.objects.filter(
|
2020-07-21 08:20:57 +08:00
|
|
|
code__in=[x.code[:2] for x in accounts]))
|
2020-07-21 10:38:47 +08:00
|
|
|
sections = list(Account.objects.filter(
|
2020-07-21 08:20:57 +08:00
|
|
|
Q(code="4") | Q(code="5") | Q(code="6")
|
|
|
|
| Q(code="7") | Q(code="8") | Q(code="9")).order_by("code"))
|
|
|
|
cumulative_accounts = {
|
2020-08-06 00:41:29 +08:00
|
|
|
"5": Account(title=_("Gross Income")),
|
|
|
|
"6": Account(title=_("Operating Income")),
|
|
|
|
"7": Account(title=_("Before Tax Income")),
|
|
|
|
"8": Account(title=_("After Tax Income")),
|
2020-08-02 18:20:36 +08:00
|
|
|
"9": Account.objects.get(code=Account.NET_CHANGE),
|
2020-07-21 08:20:57 +08:00
|
|
|
}
|
|
|
|
cumulative_total = 0
|
|
|
|
for section in sections:
|
|
|
|
section.groups = [x for x in groups
|
|
|
|
if x.code[:1] == section.code]
|
|
|
|
for group in section.groups:
|
|
|
|
group.details = [x for x in accounts
|
|
|
|
if x.code[:2] == group.code]
|
2020-08-02 19:42:48 +08:00
|
|
|
group.amount = sum([x.amount
|
|
|
|
for x in group.details])
|
|
|
|
section.amount = sum([x.amount for x in section.groups])
|
|
|
|
cumulative_total = cumulative_total + section.amount
|
2020-07-21 08:20:57 +08:00
|
|
|
if section.code in cumulative_accounts:
|
2020-07-30 00:10:11 +08:00
|
|
|
section.cumulative_total \
|
2020-07-21 08:20:57 +08:00
|
|
|
= cumulative_accounts[section.code]
|
2020-08-02 19:42:48 +08:00
|
|
|
section.cumulative_total.amount = cumulative_total
|
2020-07-21 08:20:57 +08:00
|
|
|
else:
|
|
|
|
section.cumulative_total = None
|
|
|
|
section.has_next = True
|
|
|
|
sections[-1].has_next = False
|
2020-08-12 07:46:55 +08:00
|
|
|
return render(request, "accounting/report-income-statement.html", {
|
2020-08-08 13:53:48 +08:00
|
|
|
"section_list": sections,
|
2020-07-21 08:20:57 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
@method_decorator(require_GET, name="dispatch")
|
|
|
|
class BalanceSheetDefaultView(RedirectView):
|
|
|
|
"""The default balance sheet."""
|
|
|
|
query_string = True
|
|
|
|
pattern_name = "accounting:balance-sheet"
|
2020-07-21 08:20:57 +08:00
|
|
|
|
2020-07-30 00:10:11 +08:00
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
|
|
kwargs["period"] = Period.default_spec()
|
|
|
|
return super().get_redirect_url(*args, **kwargs)
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
@require_GET
|
2020-08-13 07:25:35 +08:00
|
|
|
def balance_sheet(request: HttpRequest, period: Period) -> HttpResponse:
|
2020-07-21 08:20:57 +08:00
|
|
|
"""The balance sheet.
|
|
|
|
|
|
|
|
Args:
|
2020-08-13 07:25:35 +08:00
|
|
|
request: The request.
|
|
|
|
period: The period.
|
2020-07-21 08:20:57 +08:00
|
|
|
|
|
|
|
Returns:
|
2020-08-13 07:25:35 +08:00
|
|
|
The response.
|
2020-07-21 08:20:57 +08:00
|
|
|
"""
|
|
|
|
# The accounts
|
|
|
|
accounts = list(
|
2020-07-21 10:38:47 +08:00
|
|
|
Account.objects
|
2020-08-02 00:08:53 +08:00
|
|
|
.filter(
|
2020-07-21 20:57:59 +08:00
|
|
|
Q(record__transaction__date__lte=period.end),
|
|
|
|
(Q(code__startswith="1")
|
|
|
|
| Q(code__startswith="2")
|
|
|
|
| Q(code__startswith="3")),
|
2020-08-02 18:20:36 +08:00
|
|
|
~Q(code=Account.ACCUMULATED_BALANCE))
|
2020-08-02 00:08:53 +08:00
|
|
|
.annotate(
|
2020-08-02 19:42:48 +08:00
|
|
|
amount=Sum(Case(
|
2020-07-21 20:57:59 +08:00
|
|
|
When(record__is_credit=True, then=-1),
|
|
|
|
default=1) * F("record__amount")))
|
2020-08-11 11:13:01 +08:00
|
|
|
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
2020-08-02 00:08:53 +08:00
|
|
|
.order_by("code"))
|
2020-07-21 13:38:18 +08:00
|
|
|
for account in accounts:
|
2020-08-17 23:05:01 +08:00
|
|
|
account.url = reverse("accounting:ledger", args=[account, period],
|
|
|
|
current_app=request.resolver_match.namespace)
|
2020-07-30 00:10:11 +08:00
|
|
|
balance = Record.objects \
|
2020-07-21 08:20:57 +08:00
|
|
|
.filter(
|
2020-08-02 00:08:53 +08:00
|
|
|
Q(transaction__date__lt=period.start)
|
|
|
|
& ~((Q(account__code__startswith="1")
|
|
|
|
| Q(account__code__startswith="2")
|
|
|
|
| Q(account__code__startswith="3"))
|
2020-08-02 18:20:36 +08:00
|
|
|
& ~Q(account__code=Account.ACCUMULATED_BALANCE))) \
|
2020-07-21 08:20:57 +08:00
|
|
|
.aggregate(
|
2020-08-02 00:08:53 +08:00
|
|
|
balance=Sum(Case(
|
|
|
|
When(is_credit=True, then=-1),
|
|
|
|
default=1) * F("amount")))["balance"]
|
2020-07-21 08:20:57 +08:00
|
|
|
if balance is not None and balance != 0:
|
2020-08-02 18:20:36 +08:00
|
|
|
brought_forward = Account.objects.get(
|
|
|
|
code=Account.ACCUMULATED_BALANCE)
|
2020-08-02 19:42:48 +08:00
|
|
|
brought_forward.amount = balance
|
2020-07-21 13:38:18 +08:00
|
|
|
brought_forward.url = reverse(
|
2020-08-17 23:05:01 +08:00
|
|
|
"accounting:income-statement", args=[period.period_before()],
|
|
|
|
current_app=request.resolver_match.namespace)
|
2020-07-21 08:20:57 +08:00
|
|
|
accounts.append(brought_forward)
|
2020-07-30 00:10:11 +08:00
|
|
|
balance = Record.objects \
|
2020-07-21 08:20:57 +08:00
|
|
|
.filter(
|
2020-08-02 00:08:53 +08:00
|
|
|
Q(transaction__date__gte=period.start)
|
|
|
|
& Q(transaction__date__lte=period.end)
|
|
|
|
& ~((Q(account__code__startswith="1")
|
|
|
|
| Q(account__code__startswith="2")
|
|
|
|
| Q(account__code__startswith="3"))
|
2020-08-02 18:20:36 +08:00
|
|
|
& ~Q(account__code=Account.ACCUMULATED_BALANCE))) \
|
2020-07-21 08:20:57 +08:00
|
|
|
.aggregate(
|
2020-08-02 00:08:53 +08:00
|
|
|
balance=Sum(Case(
|
|
|
|
When(is_credit=True, then=-1),
|
|
|
|
default=1) * F("amount")))["balance"]
|
2020-07-21 08:20:57 +08:00
|
|
|
if balance is not None and balance != 0:
|
2020-08-02 18:20:36 +08:00
|
|
|
net_change = Account.objects.get(code=Account.NET_CHANGE)
|
2020-08-02 19:42:48 +08:00
|
|
|
net_change.amount = balance
|
2020-08-02 18:20:36 +08:00
|
|
|
net_change.url = reverse(
|
2020-08-17 23:05:01 +08:00
|
|
|
"accounting:income-statement", args=[period],
|
|
|
|
current_app=request.resolver_match.namespace)
|
2020-08-02 18:20:36 +08:00
|
|
|
accounts.append(net_change)
|
2020-07-22 10:26:04 +08:00
|
|
|
for account in [x for x in accounts if x.code[0] in "23"]:
|
2020-08-02 19:42:48 +08:00
|
|
|
account.amount = -account.amount
|
2020-07-21 10:38:47 +08:00
|
|
|
groups = list(Account.objects.filter(
|
2020-07-21 08:20:57 +08:00
|
|
|
code__in=[x.code[:2] for x in accounts]))
|
2020-07-21 10:38:47 +08:00
|
|
|
sections = list(Account.objects.filter(
|
2020-07-21 08:20:57 +08:00
|
|
|
Q(code="1") | Q(code="2") | Q(code="3")).order_by("code"))
|
|
|
|
for section in sections:
|
|
|
|
section.groups = [x for x in groups
|
|
|
|
if x.code[:1] == section.code]
|
|
|
|
for group in section.groups:
|
|
|
|
group.details = [x for x in accounts
|
|
|
|
if x.code[:2] == group.code]
|
2020-08-02 19:42:48 +08:00
|
|
|
group.amount = sum([x.amount
|
|
|
|
for x in group.details])
|
|
|
|
section.amount = sum([x.amount for x in section.groups])
|
2020-07-21 08:20:57 +08:00
|
|
|
by_code = {x.code: x for x in sections}
|
2020-08-12 07:46:55 +08:00
|
|
|
return render(request, "accounting/report-balance-sheet.html", {
|
2020-07-21 11:58:50 +08:00
|
|
|
"assets": by_code["1"],
|
|
|
|
"liabilities": by_code["2"],
|
|
|
|
"owners_equity": by_code["3"],
|
2020-07-21 08:20:57 +08:00
|
|
|
})
|
2020-07-21 08:26:23 +08:00
|
|
|
|
|
|
|
|
2020-07-21 10:04:29 +08:00
|
|
|
@require_GET
|
2020-08-13 07:25:35 +08:00
|
|
|
def search(request: HttpRequest) -> HttpResponse:
|
2020-07-21 10:04:29 +08:00
|
|
|
"""The search.
|
|
|
|
|
|
|
|
Args:
|
2020-08-13 07:25:35 +08:00
|
|
|
request: The request.
|
2020-07-21 10:04:29 +08:00
|
|
|
|
|
|
|
Returns:
|
2020-08-13 07:25:35 +08:00
|
|
|
The response.
|
2020-07-21 10:04:29 +08:00
|
|
|
"""
|
|
|
|
# The accounting records
|
|
|
|
query = request.GET.get("q")
|
|
|
|
if query is None:
|
|
|
|
records = []
|
|
|
|
else:
|
|
|
|
records = Record.objects.filter(
|
2020-07-21 10:38:47 +08:00
|
|
|
get_multi_lingual_search("account__title", query)
|
|
|
|
| Q(account__code__icontains=query)
|
2020-07-21 10:04:29 +08:00
|
|
|
| Q(summary__icontains=query)
|
2020-08-08 13:27:17 +08:00
|
|
|
| Q(transaction__notes__icontains=query))
|
2020-08-05 13:38:12 +08:00
|
|
|
try:
|
|
|
|
pagination = Pagination(request, records, True)
|
|
|
|
except PaginationException as e:
|
2020-08-09 19:05:57 +08:00
|
|
|
return redirect(e.url)
|
2020-07-21 10:04:29 +08:00
|
|
|
return render(request, "accounting/search.html", {
|
2020-08-08 13:53:48 +08:00
|
|
|
"record_list": pagination.items,
|
2020-07-21 10:04:29 +08:00
|
|
|
"pagination": pagination,
|
|
|
|
})
|
2020-07-23 22:02:26 +08:00
|
|
|
|
|
|
|
|
2020-08-08 16:08:15 +08:00
|
|
|
@method_decorator(require_GET, name="dispatch")
|
|
|
|
class TransactionView(DetailView):
|
|
|
|
"""The view of the details of an accounting transaction."""
|
|
|
|
context_object_name = "txn"
|
2020-07-23 22:02:26 +08:00
|
|
|
|
2020-08-08 16:08:15 +08:00
|
|
|
def get_object(self, queryset=None):
|
2020-08-14 00:41:31 +08:00
|
|
|
return self.kwargs["txn"]
|
2020-07-23 22:02:26 +08:00
|
|
|
|
2020-08-08 16:08:15 +08:00
|
|
|
def get_template_names(self):
|
2020-08-17 20:51:47 +08:00
|
|
|
model_name = self.get_object().__class__.__name__.lower()
|
|
|
|
txn_type = self.kwargs["txn_type"]
|
|
|
|
return [F"accounting/{model_name}_{txn_type}_detail.html"]
|
2020-07-27 22:36:28 +08:00
|
|
|
|
|
|
|
|
2020-08-16 13:27:31 +08:00
|
|
|
class TransactionFormView(FormView):
|
|
|
|
"""The form to create or update an accounting transaction."""
|
|
|
|
model = Transaction
|
|
|
|
form_class = TransactionForm
|
|
|
|
not_modified_message = gettext_noop("This transaction was not modified.")
|
|
|
|
success_message = gettext_noop("This transaction was saved successfully.")
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
"""Returns the context data for the template."""
|
|
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
context["summary_categories"] = utils.get_summary_categories()
|
|
|
|
context["new_record_template"] = self._get_new_record_template_json()
|
|
|
|
return context
|
|
|
|
|
|
|
|
def get_template_name(self) -> str:
|
|
|
|
"""Returns the name of the template."""
|
|
|
|
model_name = self.model.__name__.lower()
|
2020-08-17 20:51:47 +08:00
|
|
|
return F"accounting/{model_name}_{self.txn_type}_form.html"
|
2020-08-16 13:27:31 +08:00
|
|
|
|
|
|
|
def _get_new_record_template_json(self) -> str:
|
|
|
|
context = {"record_type": "TTT", "no": "NNN"}
|
|
|
|
template_name = "accounting/include/record_form-transfer.html"\
|
|
|
|
if self.txn_type == "transfer"\
|
|
|
|
else "accounting/include/record_form-non-transfer.html"
|
|
|
|
return json.dumps(render_to_string(template_name, context))
|
|
|
|
|
|
|
|
def make_form_from_post(self, post: Dict[str, str]) -> TransactionForm:
|
|
|
|
"""Creates and returns the form from the POST data."""
|
|
|
|
form = TransactionForm(post)
|
|
|
|
form.txn_type = self.txn_type
|
|
|
|
form.transaction = self.get_object()
|
|
|
|
return form
|
|
|
|
|
|
|
|
def make_form_from_model(self, obj: Transaction) -> TransactionForm:
|
|
|
|
"""Creates and returns the form from a data model."""
|
|
|
|
return utils.make_txn_form_from_model(self.txn_type, obj)
|
|
|
|
|
|
|
|
def fill_model_from_form(self, obj: Transaction,
|
|
|
|
form: TransactionForm) -> None:
|
|
|
|
"""Fills in the data model from the form."""
|
|
|
|
obj.old_date = obj.date
|
|
|
|
utils.fill_txn_from_post(self.txn_type, obj, form.data)
|
|
|
|
obj.current_user = self.request.user
|
|
|
|
|
|
|
|
def get_object(self) -> Optional[Account]:
|
|
|
|
"""Returns the current object, or None on a create form."""
|
|
|
|
return self.kwargs.get("txn")
|
|
|
|
|
2020-08-17 22:23:30 +08:00
|
|
|
def get_success_url(self) -> str:
|
|
|
|
"""Returns the URL on success."""
|
|
|
|
return reverse("accounting:transactions.detail",
|
2020-08-17 22:49:28 +08:00
|
|
|
args=[self.txn_type, self.get_object()],
|
2020-08-17 22:23:30 +08:00
|
|
|
current_app=self.request.resolver_match.namespace)
|
|
|
|
|
2020-08-16 13:27:31 +08:00
|
|
|
@property
|
|
|
|
def txn_type(self) -> str:
|
2020-08-17 22:18:40 +08:00
|
|
|
"""Returns the transaction type of this form."""
|
2020-08-16 13:27:31 +08:00
|
|
|
return self.kwargs["txn_type"]
|
2020-08-03 00:02:51 +08:00
|
|
|
|
|
|
|
|
2020-08-09 13:19:20 +08:00
|
|
|
@method_decorator(require_POST, name="dispatch")
|
|
|
|
class TransactionDeleteView(DeleteView):
|
|
|
|
"""The view to delete an accounting transaction."""
|
2020-08-09 14:07:47 +08:00
|
|
|
success_message = gettext_noop(
|
|
|
|
"This transaction was deleted successfully.")
|
2020-08-05 08:23:20 +08:00
|
|
|
|
2020-08-09 13:19:20 +08:00
|
|
|
def get_object(self, queryset=None):
|
2020-08-14 00:41:31 +08:00
|
|
|
return self.kwargs["txn"]
|
2020-08-09 13:19:20 +08:00
|
|
|
|
|
|
|
def get_success_url(self):
|
2020-08-17 23:05:01 +08:00
|
|
|
return self.request.GET.get("r")\
|
|
|
|
or reverse("accounting:home",
|
|
|
|
current_app=self.request.resolver_match.namespace)
|
2020-08-05 08:23:20 +08:00
|
|
|
|
2020-08-05 09:20:13 +08:00
|
|
|
|
2020-08-16 22:38:35 +08:00
|
|
|
class TransactionSortFormView(FormView):
|
|
|
|
"""The form to sort the transactions in a same day."""
|
2020-08-17 20:51:47 +08:00
|
|
|
template_name = "accounting/transaction_sort_form.html"
|
2020-08-16 22:38:35 +08:00
|
|
|
form_class = TransactionSortForm
|
|
|
|
not_modified_message = gettext_noop(
|
|
|
|
"The transaction orders were not modified.")
|
|
|
|
success_message = gettext_noop(
|
|
|
|
"The transaction orders were saved successfully.")
|
|
|
|
|
|
|
|
def get_form(self, **kwargs):
|
|
|
|
"""Returns the form for the template."""
|
|
|
|
form = super().get_form()
|
|
|
|
if form.txn_list is None:
|
|
|
|
form.date = self.kwargs["date"]
|
|
|
|
form.txn_list = Transaction.objects.filter(date=form.date)\
|
|
|
|
.order_by("ord").all()
|
|
|
|
if len(form.txn_list) < 2:
|
|
|
|
raise Http404
|
|
|
|
return form
|
2020-08-06 23:51:20 +08:00
|
|
|
|
2020-08-16 22:38:35 +08:00
|
|
|
def make_form_from_post(self, post: Dict[str, str]) -> TransactionSortForm:
|
|
|
|
"""Creates and returns the form from the POST data."""
|
|
|
|
return TransactionSortForm.from_post(self.kwargs["date"], post)
|
2020-08-06 23:51:20 +08:00
|
|
|
|
2020-08-16 22:38:35 +08:00
|
|
|
def form_valid(self, form: TransactionSortForm) -> HttpResponseRedirect:
|
|
|
|
"""Handles the action when the POST form is valid."""
|
|
|
|
modified = [x for x in form.txn_orders if x.txn.ord != x.order]
|
2020-08-06 23:51:20 +08:00
|
|
|
if len(modified) == 0:
|
2020-08-16 22:38:35 +08:00
|
|
|
message = self.get_not_modified_message(form.cleaned_data)
|
2020-08-12 22:47:55 +08:00
|
|
|
else:
|
|
|
|
with transaction.atomic():
|
|
|
|
for x in modified:
|
2020-08-16 22:38:35 +08:00
|
|
|
Transaction.objects.filter(pk=x.txn.pk).update(ord=x.order)
|
|
|
|
message = self.get_success_message(form.cleaned_data)
|
|
|
|
messages.success(self.request, message)
|
2020-08-17 22:44:43 +08:00
|
|
|
return redirect(self.get_success_url())
|
|
|
|
|
|
|
|
def get_success_url(self) -> str:
|
|
|
|
"""Returns the URL on success."""
|
|
|
|
return self.request.GET.get("r")\
|
2020-08-17 23:23:41 +08:00
|
|
|
or reverse("accounting:home",
|
|
|
|
current_app=self.request.resolver_match.namespace)
|
2020-08-06 23:51:20 +08:00
|
|
|
|
|
|
|
|
2020-08-07 10:11:09 +08:00
|
|
|
@method_decorator(require_GET, name="dispatch")
|
|
|
|
class AccountListView(ListView):
|
|
|
|
"""The view to list the accounts."""
|
2020-08-11 20:40:45 +08:00
|
|
|
queryset = Account.objects\
|
|
|
|
.annotate(is_parent_and_in_use=ExpressionWrapper(
|
2020-08-14 00:36:29 +08:00
|
|
|
Exists(Account.objects.filter(parent=OuterRef("pk")))
|
|
|
|
& Exists(Record.objects.filter(account=OuterRef("pk"))),
|
2020-08-11 20:40:45 +08:00
|
|
|
output_field=BooleanField()))\
|
|
|
|
.order_by("code")
|
2020-08-07 10:11:09 +08:00
|
|
|
|
|
|
|
|
2020-08-08 15:18:39 +08:00
|
|
|
@method_decorator(require_GET, name="dispatch")
|
|
|
|
class AccountView(DetailView):
|
|
|
|
"""The view of an account."""
|
|
|
|
def get_object(self, queryset=None):
|
2020-08-14 00:41:31 +08:00
|
|
|
return self.kwargs["account"]
|
2020-08-08 15:18:39 +08:00
|
|
|
|
|
|
|
|
2020-08-14 00:36:29 +08:00
|
|
|
class AccountFormView(FormView):
|
2020-08-16 09:47:18 +08:00
|
|
|
"""The form to create or update an account."""
|
2020-08-14 00:36:29 +08:00
|
|
|
model = Account
|
2020-08-16 09:50:05 +08:00
|
|
|
form_class = AccountForm
|
2020-08-14 00:36:29 +08:00
|
|
|
not_modified_message = gettext_noop("This account was not modified.")
|
|
|
|
success_message = gettext_noop("This account was saved successfully.")
|
|
|
|
|
|
|
|
def make_form_from_post(self, post: Dict[str, str]) -> AccountForm:
|
|
|
|
"""Creates and returns the form from the POST data."""
|
|
|
|
form = AccountForm(post)
|
2020-08-14 00:44:14 +08:00
|
|
|
form.account = self.get_object()
|
2020-08-14 00:36:29 +08:00
|
|
|
return form
|
|
|
|
|
|
|
|
def make_form_from_model(self, obj: Account) -> AccountForm:
|
|
|
|
"""Creates and returns the form from a data model."""
|
2020-08-09 11:52:12 +08:00
|
|
|
form = AccountForm({
|
2020-08-14 00:36:29 +08:00
|
|
|
"code": obj.code,
|
|
|
|
"title": obj.title,
|
2020-08-09 11:52:12 +08:00
|
|
|
})
|
2020-08-14 00:36:29 +08:00
|
|
|
form.account = obj
|
|
|
|
return form
|
|
|
|
|
|
|
|
def fill_model_from_form(self, obj: Account, form: AccountForm) -> None:
|
|
|
|
"""Fills in the data model from the form."""
|
|
|
|
obj.code = form["code"].value()
|
|
|
|
obj.title = form["title"].value()
|
|
|
|
obj.current_user = self.request.user
|
|
|
|
|
2020-08-14 00:44:14 +08:00
|
|
|
def get_object(self) -> Optional[Account]:
|
2020-08-14 00:36:29 +08:00
|
|
|
"""Returns the current object, or None on a create form."""
|
2020-08-16 13:43:35 +08:00
|
|
|
return self.kwargs.get("account")
|
2020-08-09 16:22:51 +08:00
|
|
|
|
2020-08-17 22:27:54 +08:00
|
|
|
def get_success_url(self) -> str:
|
|
|
|
"""Returns the URL on success."""
|
2020-08-17 22:49:28 +08:00
|
|
|
return reverse("accounting:accounts.detail", args=[self.get_object()],
|
2020-08-17 22:27:54 +08:00
|
|
|
current_app=self.request.resolver_match.namespace)
|
|
|
|
|
2020-08-09 16:22:51 +08:00
|
|
|
|
2020-08-09 17:25:51 +08:00
|
|
|
@require_POST
|
2020-08-13 07:25:35 +08:00
|
|
|
def account_delete(request: HttpRequest,
|
|
|
|
account: Account) -> HttpResponseRedirect:
|
2020-08-09 22:12:41 +08:00
|
|
|
"""The view to delete an account.
|
2020-08-09 17:25:51 +08:00
|
|
|
|
|
|
|
Args:
|
|
|
|
request (HttpRequest): The request.
|
|
|
|
account (Account): The account.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
HttpResponseRedirect: The response.
|
|
|
|
"""
|
|
|
|
if account.is_in_use:
|
|
|
|
message = gettext_noop("This account is in use.")
|
|
|
|
messages.error(request, message)
|
2020-08-09 19:05:57 +08:00
|
|
|
return redirect("accounting:accounts.detail", account)
|
2020-08-09 17:25:51 +08:00
|
|
|
account.delete()
|
|
|
|
message = gettext_noop("This account was deleted successfully.")
|
|
|
|
messages.success(request, message)
|
2020-08-09 19:05:57 +08:00
|
|
|
return redirect("accounting:accounts")
|
2020-08-09 17:25:51 +08:00
|
|
|
|
|
|
|
|
2020-08-09 11:52:12 +08:00
|
|
|
@require_GET
|
2020-08-13 07:25:35 +08:00
|
|
|
def api_account_list(request: HttpRequest) -> JsonResponse:
|
2020-08-09 11:52:12 +08:00
|
|
|
"""The API view to return all the accounts.
|
|
|
|
|
|
|
|
Args:
|
2020-08-13 07:25:35 +08:00
|
|
|
request: The request.
|
2020-08-09 11:52:12 +08:00
|
|
|
|
|
|
|
Returns:
|
2020-08-13 07:25:35 +08:00
|
|
|
The response.
|
2020-08-09 11:52:12 +08:00
|
|
|
"""
|
|
|
|
return JsonResponse({x.code: x.title for x in Account.objects.all()})
|
|
|
|
|
|
|
|
|
2020-08-03 00:02:51 +08:00
|
|
|
@require_GET
|
2020-08-13 07:25:35 +08:00
|
|
|
def api_account_options(request: HttpRequest) -> JsonResponse:
|
2020-08-09 22:12:41 +08:00
|
|
|
"""The API view to return the account options.
|
2020-08-03 00:02:51 +08:00
|
|
|
|
|
|
|
Args:
|
2020-08-13 07:25:35 +08:00
|
|
|
request: The request.
|
2020-08-03 00:02:51 +08:00
|
|
|
|
|
|
|
Returns:
|
2020-08-13 07:25:35 +08:00
|
|
|
The response.
|
2020-08-03 00:02:51 +08:00
|
|
|
"""
|
|
|
|
accounts = Account.objects\
|
|
|
|
.annotate(children_count=Count("child_set"))\
|
|
|
|
.filter(children_count=0)\
|
|
|
|
.annotate(record_count=Count("record"))\
|
|
|
|
.annotate(is_in_use=Case(
|
|
|
|
When(record_count__gt=0, then=True),
|
|
|
|
default=False,
|
2020-08-11 11:06:42 +08:00
|
|
|
output_field=BooleanField()))\
|
|
|
|
.order_by("code")
|
2020-08-03 00:02:51 +08:00
|
|
|
for x in accounts:
|
|
|
|
x.is_for_debit = re.match("^([1235689]|7[5678])", x.code) is not None
|
|
|
|
x.is_for_credit = re.match("^([123489]|7[1234])", x.code) is not None
|
2020-08-07 10:36:14 +08:00
|
|
|
return JsonResponse({
|
2020-08-06 00:41:29 +08:00
|
|
|
"header_in_use": _("---Accounts In Use---"),
|
2020-08-03 00:02:51 +08:00
|
|
|
"debit_in_use": [x.option_data for x in accounts
|
|
|
|
if x.is_for_debit and x.is_in_use],
|
|
|
|
"credit_in_use": [x.option_data for x in accounts
|
|
|
|
if x.is_for_credit and x.is_in_use],
|
2020-08-06 00:41:29 +08:00
|
|
|
"header_not_in_use": _("---Accounts Not In Use---"),
|
2020-08-03 00:02:51 +08:00
|
|
|
"debit_not_in_use": [x.option_data for x in accounts
|
|
|
|
if x.is_for_debit and not x.is_in_use],
|
|
|
|
"credit_not_in_use": [x.option_data for x in accounts
|
|
|
|
if x.is_for_credit and not x.is_in_use],
|
2020-08-07 10:36:14 +08:00
|
|
|
})
|