Added trial balance.
This commit is contained in:
parent
480e2d2d8f
commit
cca43c68a6
@ -159,3 +159,22 @@ class IncomeExpensesPeriodChooser(PeriodChooser):
|
||||
return url_for("accounting.report.income-expenses",
|
||||
currency=self.currency, account=self.account,
|
||||
period=period)
|
||||
|
||||
|
||||
class TrialBalancePeriodChooser(PeriodChooser):
|
||||
"""The trial balance period chooser."""
|
||||
|
||||
def __init__(self, currency: Currency):
|
||||
"""Constructs the trial balance period chooser."""
|
||||
self.currency: Currency = currency
|
||||
"""The currency."""
|
||||
first: Transaction | None \
|
||||
= Transaction.query.order_by(Transaction.date).first()
|
||||
super().__init__(None if first is None else first.date)
|
||||
|
||||
def _url_for(self, period: Period) -> str:
|
||||
if period.is_default:
|
||||
return url_for("accounting.report.trial-balance-default",
|
||||
currency=self.currency)
|
||||
return url_for("accounting.report.trial-balance",
|
||||
currency=self.currency, period=period)
|
||||
|
@ -67,6 +67,7 @@ class ReportChooser:
|
||||
self.__reports.append(self.__journal)
|
||||
self.__reports.append(self.__ledger)
|
||||
self.__reports.append(self.__income_expenses)
|
||||
self.__reports.append(self.__trial_balance)
|
||||
for report in self.__reports:
|
||||
if report.is_active:
|
||||
self.current_report = report.title
|
||||
@ -113,6 +114,20 @@ class ReportChooser:
|
||||
return OptionLink(gettext("Income and Expenses"), url,
|
||||
self.__active_report == ReportType.INCOME_EXPENSES)
|
||||
|
||||
@property
|
||||
def __trial_balance(self) -> OptionLink:
|
||||
"""Returns the trial balance.
|
||||
|
||||
:return: The trial balance.
|
||||
"""
|
||||
url: str = url_for("accounting.report.trial-balance-default",
|
||||
currency=self.__currency) \
|
||||
if self.__period.is_default \
|
||||
else url_for("accounting.report.trial-balance",
|
||||
currency=self.__currency, period=self.__period)
|
||||
return OptionLink(gettext("Trial Balance"), url,
|
||||
self.__active_report == ReportType.TRIAL_BALANCE)
|
||||
|
||||
def __iter__(self) -> t.Iterator[OptionLink]:
|
||||
"""Returns the iteration of the reports.
|
||||
|
||||
|
@ -22,6 +22,7 @@ from abc import ABC, abstractmethod
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import JournalEntry, Transaction, Account, Currency
|
||||
|
||||
|
||||
@ -169,3 +170,39 @@ class IncomeExpensesRow(ReportRow):
|
||||
"Expense": self.expense,
|
||||
"Balance": self.balance,
|
||||
"Note": self.note}
|
||||
|
||||
|
||||
class TrialBalanceRow(ReportRow):
|
||||
"""A row in the trial balance."""
|
||||
|
||||
def __init__(self, account: Account | None = None,
|
||||
balance: Decimal | None = None):
|
||||
"""Constructs the row in the trial balance.
|
||||
|
||||
:param account: The account.
|
||||
:param balance: The balance.
|
||||
"""
|
||||
self.is_total: bool = False
|
||||
"""Whether this is the total row."""
|
||||
self.account: Account | None = account
|
||||
"""The date."""
|
||||
self.debit: Decimal | None = None
|
||||
"""The debit amount."""
|
||||
self.credit: Decimal | None = None
|
||||
"""The credit amount."""
|
||||
self.url: str | None = None
|
||||
"""The URL."""
|
||||
if balance is not None:
|
||||
if balance > 0:
|
||||
self.debit = balance
|
||||
if balance < 0:
|
||||
self.credit = -balance
|
||||
|
||||
def as_dict(self) -> dict[str, t.Any]:
|
||||
if self.is_total:
|
||||
return {"Account": gettext("Total"),
|
||||
"Debit": self.debit,
|
||||
"Credit": self.credit}
|
||||
return {"Account": str(self.account).title(),
|
||||
"Debit": self.debit,
|
||||
"Credit": self.credit}
|
||||
|
@ -28,3 +28,5 @@ class ReportType(Enum):
|
||||
"""The ledger."""
|
||||
INCOME_EXPENSES: str = "income-expenses"
|
||||
"""The income and expenses."""
|
||||
TRIAL_BALANCE: str = "trial-balance"
|
||||
"""The trial balance."""
|
||||
|
@ -36,9 +36,10 @@ from accounting.utils.txn_types import TransactionType
|
||||
from .option_link import OptionLink
|
||||
from .period import Period
|
||||
from .period_choosers import PeriodChooser, JournalPeriodChooser, \
|
||||
LedgerPeriodChooser, IncomeExpensesPeriodChooser
|
||||
LedgerPeriodChooser, IncomeExpensesPeriodChooser, TrialBalancePeriodChooser
|
||||
from .report_chooser import ReportChooser
|
||||
from .report_rows import ReportRow, JournalRow, LedgerRow, IncomeExpensesRow
|
||||
from .report_rows import ReportRow, JournalRow, LedgerRow, IncomeExpensesRow, \
|
||||
TrialBalanceRow
|
||||
from .report_type import ReportType
|
||||
|
||||
T = t.TypeVar("T", bound=ReportRow)
|
||||
@ -629,3 +630,135 @@ class IncomeExpenses(JournalEntryReport[IncomeExpensesRow]):
|
||||
return [OptionLink(str(x), get_url(x), x.id == self.account.id)
|
||||
for x in Account.query.filter(Account.id.in_(in_use))
|
||||
.order_by(Account.base_code, Account.no).all()]
|
||||
|
||||
|
||||
class TrialBalance(JournalEntryReport[TrialBalanceRow]):
|
||||
"""The trial balance."""
|
||||
|
||||
def __init__(self, currency: Currency, period: Period):
|
||||
"""Constructs a trial balance.
|
||||
|
||||
:param currency: The currency.
|
||||
:param period: The period.
|
||||
"""
|
||||
super().__init__(period)
|
||||
self.currency: Currency = currency
|
||||
"""The currency."""
|
||||
self.total_row: TrialBalanceRow | None = None
|
||||
"""The total row."""
|
||||
|
||||
def get_rows(self) -> list[TrialBalanceRow]:
|
||||
rows: list[TrialBalanceRow] = self.__query_balances()
|
||||
self.__populate_url(rows)
|
||||
total_row: TrialBalanceRow = self.__get_total_row(rows)
|
||||
rows.append(total_row)
|
||||
return rows
|
||||
|
||||
def __query_balances(self) -> list[TrialBalanceRow]:
|
||||
"""Queries and returns the balances.
|
||||
|
||||
:return: The balances.
|
||||
"""
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [JournalEntry.currency_code == self.currency.code]
|
||||
if self.period.start is not None:
|
||||
conditions.append(Transaction.date >= self.period.start)
|
||||
if self.period.end is not None:
|
||||
conditions.append(Transaction.date <= self.period.end)
|
||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||
(JournalEntry.is_debit, JournalEntry.amount),
|
||||
else_=-JournalEntry.amount)).label("balance")
|
||||
select_trial_balance: sa.Select \
|
||||
= sa.select(JournalEntry.account_id, balance_func)\
|
||||
.join(Transaction)\
|
||||
.filter(*conditions)\
|
||||
.group_by(JournalEntry.account_id)
|
||||
balances: list[sa.Row] = db.session.execute(select_trial_balance).all()
|
||||
accounts: dict[int, Account] \
|
||||
= {x.id: x for x in Account.query
|
||||
.filter(Account.id.in_([x.account_id for x in balances])).all()}
|
||||
return [TrialBalanceRow(accounts[x.account_id], x.balance)
|
||||
for x in balances]
|
||||
|
||||
def __populate_url(self, rows: list[TrialBalanceRow]) -> None:
|
||||
"""Populates the URL of the trial balance rows.
|
||||
|
||||
:param rows: The trial balance rows.
|
||||
:return: None.
|
||||
"""
|
||||
def get_url(account: Account) -> str:
|
||||
"""Returns the ledger URL of an account.
|
||||
|
||||
:param account: The account.
|
||||
:return: The ledger URL of the account.
|
||||
"""
|
||||
if self.period.is_default:
|
||||
return url_for("accounting.report.ledger-default",
|
||||
currency=self.currency, account=account)
|
||||
return url_for("accounting.report.ledger",
|
||||
currency=self.currency, account=account,
|
||||
period=self.period)
|
||||
|
||||
for row in rows:
|
||||
row.url = get_url(row.account)
|
||||
|
||||
@staticmethod
|
||||
def __get_total_row(rows: list[TrialBalanceRow]) -> TrialBalanceRow:
|
||||
"""Composes the total row.
|
||||
|
||||
:param rows: The rows.
|
||||
:return: None.
|
||||
"""
|
||||
row: TrialBalanceRow = TrialBalanceRow()
|
||||
row.is_total = True
|
||||
row.debit = sum([x.debit for x in rows if x.debit is not None])
|
||||
row.credit = sum([x.credit for x in rows if x.credit is not None])
|
||||
return row
|
||||
|
||||
def populate_rows(self, rows: list[T]) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def csv_field_names(self) -> list[str]:
|
||||
return ["Account", "Debit", "Credit"]
|
||||
|
||||
@property
|
||||
def csv_filename(self) -> str:
|
||||
return f"trial-balance-{self.period.spec}.csv"
|
||||
|
||||
@property
|
||||
def period_chooser(self) -> PeriodChooser:
|
||||
return TrialBalancePeriodChooser(self.currency)
|
||||
|
||||
@property
|
||||
def report_chooser(self) -> ReportChooser:
|
||||
return ReportChooser(ReportType.TRIAL_BALANCE, period=self.period)
|
||||
|
||||
def as_html_page(self) -> str:
|
||||
pagination: Pagination = Pagination[TrialBalanceRow](self.rows)
|
||||
rows: list[TrialBalanceRow] = pagination.list
|
||||
if len(rows) > 0 and rows[-1].is_total:
|
||||
self.total_row = rows[-1]
|
||||
rows = rows[:-1]
|
||||
return render_template("accounting/report/trial-balance.html",
|
||||
list=rows, pagination=pagination, report=self)
|
||||
|
||||
@property
|
||||
def currency_options(self) -> list[OptionLink]:
|
||||
"""Returns the currency options.
|
||||
|
||||
:return: The currency options.
|
||||
"""
|
||||
def get_url(currency: Currency):
|
||||
if self.period.is_default:
|
||||
return url_for("accounting.report.trial-balance-default",
|
||||
currency=currency)
|
||||
return url_for("accounting.report.trial-balance",
|
||||
currency=currency, period=self.period)
|
||||
|
||||
in_use: set[str] = set(db.session.scalars(
|
||||
sa.select(JournalEntry.currency_code)
|
||||
.group_by(JournalEntry.currency_code)).all())
|
||||
return [OptionLink(str(x), get_url(x), x.code == self.currency.code)
|
||||
for x in Currency.query.filter(Currency.code.in_(in_use))
|
||||
.order_by(Currency.code).all()]
|
||||
|
@ -22,7 +22,7 @@ from flask import Blueprint, request, Response
|
||||
from accounting.models import Currency, Account
|
||||
from accounting.utils.permission import has_permission, can_view
|
||||
from .period import Period
|
||||
from .reports import Journal, Ledger, IncomeExpenses
|
||||
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance
|
||||
|
||||
bp: Blueprint = Blueprint("report", __name__)
|
||||
"""The view blueprint for the reports."""
|
||||
@ -148,3 +148,43 @@ def __get_income_expenses_list(currency: Currency, account: Account,
|
||||
if "as" in request.args and request.args["as"] == "csv":
|
||||
return report.as_csv_download()
|
||||
return report.as_html_page()
|
||||
|
||||
|
||||
@bp.get("trial-balance/<currency:currency>",
|
||||
endpoint="trial-balance-default")
|
||||
@has_permission(can_view)
|
||||
def get_default_trial_balance_list(currency: Currency) -> str | Response:
|
||||
"""Returns the trial balance in the default period.
|
||||
|
||||
:param currency: The currency.
|
||||
:return: The trial balance in the default period.
|
||||
"""
|
||||
return __get_trial_balance_list(currency, Period.get_instance())
|
||||
|
||||
|
||||
@bp.get("trial-balance/<currency:currency>/<period:period>",
|
||||
endpoint="trial-balance")
|
||||
@has_permission(can_view)
|
||||
def get_trial_balance_list(currency: Currency, period: Period) \
|
||||
-> str | Response:
|
||||
"""Returns the trial balance.
|
||||
|
||||
:param currency: The currency.
|
||||
:param period: The period.
|
||||
:return: The trial balance in the period.
|
||||
"""
|
||||
return __get_trial_balance_list(currency, period)
|
||||
|
||||
|
||||
def __get_trial_balance_list(currency: Currency, period: Period) \
|
||||
-> str | Response:
|
||||
"""Returns the trial balance.
|
||||
|
||||
:param currency: The currency.
|
||||
:param period: The period.
|
||||
:return: The trial balance in the period.
|
||||
"""
|
||||
report: TrialBalance = TrialBalance(currency, period)
|
||||
if "as" in request.args and request.args["as"] == "csv":
|
||||
return report.as_csv_download()
|
||||
return report.as_html_page()
|
||||
|
@ -127,6 +127,41 @@ td.accounting-amount {
|
||||
font-weight: bolder;
|
||||
font-style: italic;
|
||||
}
|
||||
.accounting-report-card {
|
||||
padding: 2em 1.5em;
|
||||
margin: 1em;
|
||||
background-color: #F8F9FA;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
}
|
||||
.accounting-report-card h2 {
|
||||
border-bottom: thick double slategray;
|
||||
}
|
||||
.accounting-report-row {
|
||||
border: none;
|
||||
}
|
||||
a.accounting-report-row:hover {
|
||||
color: inherit;
|
||||
}
|
||||
.accounting-trial-balance-row.accounting-trial-balance-header {
|
||||
border-bottom: thick double slategray;
|
||||
font-weight: bolder;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.accounting-trial-balance-row.accounting-trial-balance-header .accounting-amount {
|
||||
font-style: normal;
|
||||
}
|
||||
.accounting-trial-balance-row > div {
|
||||
width: 50%;
|
||||
}
|
||||
.accounting-trial-balance-row .accounting-amount {
|
||||
width: 50%;
|
||||
font-style: italic;
|
||||
}
|
||||
.accounting-trial-balance-row.accounting-trial-balance-total {
|
||||
border-top: thick double slategray;
|
||||
font-weight: bolder;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* The Material Design text field (floating form control in Bootstrap) */
|
||||
.accounting-material-text-field {
|
||||
|
155
src/accounting/templates/accounting/report/trial-balance.html
Normal file
155
src/accounting/templates/accounting/report/trial-balance.html
Normal file
@ -0,0 +1,155 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
trial-balance.html: The trial balance
|
||||
|
||||
Copyright (c) 2023 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: 2023/3/5
|
||||
#}
|
||||
{% extends "accounting/base.html" %}
|
||||
|
||||
{% block accounting_scripts %}
|
||||
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}{% block title %}{{ _("Trial Balance of %(currency)s %(period)s", currency=report.currency, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="btn-group mb-2 d-none d-md-inline-flex">
|
||||
{% if accounting_can_edit() %}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
{{ A_("New") }}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||
{{ A_("Cash Expense") }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||
{{ A_("Cash Income") }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
|
||||
{{ A_("Transfer") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% with report_chooser = report.report_chooser %}
|
||||
{% include "accounting/report/include/report-chooser.html" %}
|
||||
{% endwith %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa-solid fa-money-bill-wave"></i>
|
||||
{{ report.currency.name|title }}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for currency in report.currency_options %}
|
||||
<li>
|
||||
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||
{{ currency.title|title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||
<i class="fa-solid fa-calendar-day"></i>
|
||||
{{ report.period.desc|title }}
|
||||
</button>
|
||||
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
|
||||
<i class="fa-solid fa-download"></i>
|
||||
{{ A_("Download") }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% with txn_types = report.txn_types %}
|
||||
{% include "accounting/include/add-txn-material-fab.html" %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="btn-group btn-actions mb-3 d-md-none">
|
||||
{% with report_chooser = report.report_chooser %}
|
||||
{% include "accounting/report/include/report-chooser.html" %}
|
||||
{% endwith %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa-solid fa-money-bill-wave"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for currency in report.currency_options %}
|
||||
<li>
|
||||
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||
{{ currency.title|title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||
<i class="fa-solid fa-calendar-day"></i>
|
||||
{{ A_("Period") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% with period = report.period, period_chooser = report.period_chooser %}
|
||||
{% include "accounting/report/include/period-chooser.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% if list %}
|
||||
<div class="accounting-report-card">
|
||||
<div class="d-none d-sm-flex justify-content-center mb-3">
|
||||
<h2 class="text-center">{{ _("Trial Balance of %(currency)s %(period)s", currency=report.currency, period=report.period.desc|title) }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="list-group accounting-list-group-stripped accounting-list-group-hover">
|
||||
<div class="list-group-item d-flex justify-content-between accounting-report-row accounting-trial-balance-row accounting-trial-balance-header">
|
||||
<div>{{ A_("Account") }}</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="accounting-amount">{{ A_("Debit") }}</div>
|
||||
<div class="accounting-amount">{{ A_("Credit") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% for item in list %}
|
||||
<a class="list-group-item d-flex justify-content-between accounting-report-row accounting-trial-balance-row" href="{{ item.url }}">
|
||||
<div>{{ item.account|title }}</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="accounting-amount">{{ "" if item.debit is none else item.debit|accounting_format_amount }}</div>
|
||||
<div class="accounting-amount">{{ "" if item.credit is none else item.credit|accounting_format_amount }}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
<div class="list-group-item d-flex justify-content-between accounting-report-row accounting-trial-balance-row accounting-trial-balance-total">
|
||||
<div>{{ A_("Total") }}</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="accounting-amount">{{ report.total_row.debit|accounting_format_amount }}</div>
|
||||
<div class="accounting-amount">{{ report.total_row.credit|accounting_format_amount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{{ A_("There is no data.") }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user