Split the report parameters from the report class so that it works better with both CSV export and HTML templates.

This commit is contained in:
依瑪貓 2023-03-06 23:37:20 +08:00
parent e797cfeb8c
commit ef9e5cb5b3
7 changed files with 560 additions and 391 deletions

View File

@ -0,0 +1,341 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/6
# 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.
"""The page parameters of a report.
"""
import typing as t
from abc import ABC
import sqlalchemy as sa
from flask import url_for
from accounting import db
from accounting.models import Currency, Account, JournalEntry
from accounting.report.option_link import OptionLink
from accounting.report.period import Period
from accounting.report.period_choosers import PeriodChooser, \
JournalPeriodChooser, LedgerPeriodChooser, IncomeExpensesPeriodChooser, \
TrialBalancePeriodChooser
from accounting.report.report_chooser import ReportChooser
from accounting.report.report_rows import JournalRow, LedgerRow, \
IncomeExpensesRow, TrialBalanceRow
from accounting.report.report_type import ReportType
from accounting.utils.pagination import Pagination
from accounting.utils.txn_types import TransactionType
T = t.TypeVar("T")
class ReportParams(t.Generic[T], ABC):
"""The parameters of a report page."""
def __init__(self,
period_chooser: PeriodChooser,
report_chooser: ReportChooser,
data_rows: list[T],
is_paged: bool,
filler: t.Callable[[list[T]], None] | None = None,
brought_forward: T | None = None,
total: T | None = None):
"""Constructs the parameters of a report page.
:param period_chooser: The period chooser.
:param report_chooser: The report chooser.
:param filler: The callback to fill in the related data to the rows.
:param data_rows: The data rows.
:param is_paged: True to use pagination, or False otherwise.
:param brought_forward: The brought-forward row, if any.
:param total: The total row, if any.
"""
self.txn_types: t.Type[TransactionType] = TransactionType
"""The transaction types."""
self.period_chooser: PeriodChooser = period_chooser
"""The period chooser."""
self.report_chooser: ReportChooser = report_chooser
"""The report chooser."""
self.data_rows: list[T] = data_rows
"""The data rows"""
self.brought_forward: T | None = brought_forward
"""The brought-forward row."""
self.total: T | None = total
"""The total row."""
self.pagination: Pagination[T] | None = None
"""The pagination."""
self.has_data: bool = len(self.data_rows) > 0
"""True if there is any data in the page, or False otherwise."""
if is_paged:
all_rows: list[T] = []
if brought_forward is not None:
all_rows.append(brought_forward)
all_rows.extend(data_rows)
if self.total is not None:
all_rows.append(total)
self.pagination = Pagination[T](all_rows)
rows = self.pagination.list
self.has_data = len(rows) > 0
if len(rows) > 0 and rows[0] == brought_forward:
rows = rows[1:]
else:
self.brought_forward = None
if len(rows) > 0 and rows[-1] == total:
rows = rows[:-1]
else:
self.total = None
self.data_rows = rows
if filler is not None:
filler(self.data_rows)
class JournalParams(ReportParams[JournalRow]):
"""The parameters of a journal page."""
def __init__(self,
period: Period,
data_rows: list[JournalRow],
filler: t.Callable[[list[JournalRow]], None]):
"""Constructs the parameters for the journal page.
:param period: The period.
:param data_rows: The data rows.
:param filler: The callback to fill in the related data to the rows.
"""
super().__init__(
period_chooser=JournalPeriodChooser(),
report_chooser=ReportChooser(ReportType.JOURNAL,
period=period),
data_rows=data_rows,
is_paged=True,
filler=filler)
self.period: Period | None = period
"""The period."""
class LedgerParams(ReportParams[LedgerRow]):
"""The parameters of a ledger page."""
def __init__(self,
currency: Currency,
account: Account,
period: Period,
data_rows: list[LedgerRow],
filler: t.Callable[[list[LedgerRow]], None],
brought_forward: LedgerRow | None,
total: LedgerRow):
"""Constructs the parameters for the ledger page.
:param currency: The currency.
:param account: The account.
:param period: The period.
:param data_rows: The data rows.
:param filler: The callback to fill in the related data to the rows.
:param brought_forward: The brought-forward row, if any.
:param total: The total row, if any.
"""
super().__init__(
period_chooser=IncomeExpensesPeriodChooser(currency, account),
report_chooser=ReportChooser(ReportType.LEDGER,
currency=currency,
account=account,
period=period),
data_rows=data_rows,
is_paged=True,
filler=filler,
brought_forward=brought_forward,
total=total)
self.currency: Currency = currency
"""The currency."""
self.account: Account = account
"""The account."""
self.period: Period | None = period
"""The period."""
@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.ledger-default",
currency=currency, account=self.account)
return url_for("accounting.report.ledger",
currency=currency, account=self.account,
period=self.period)
in_use: sa.Select = sa.Select(JournalEntry.currency_code)\
.group_by(JournalEntry.currency_code)
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()]
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
def get_url(account: 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)
in_use: sa.Select = sa.Select(JournalEntry.account_id)\
.filter(JournalEntry.currency_code == self.currency.code)\
.group_by(JournalEntry.account_id)
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 IncomeExpensesParams(ReportParams[IncomeExpensesRow]):
"""The parameters of an income and expenses page."""
def __init__(self,
currency: Currency,
account: Account,
period: Period,
data_rows: list[IncomeExpensesRow],
filler: t.Callable[[list[IncomeExpensesRow]], None],
brought_forward: IncomeExpensesRow | None,
total: IncomeExpensesRow):
"""Constructs the parameters for the income and expenses page.
:param currency: The currency.
:param account: The account.
:param period: The period.
:param data_rows: The data rows.
:param filler: The callback to fill in the related data to the rows.
:param brought_forward: The brought-forward row, if any.
:param total: The total row, if any.
"""
super().__init__(
period_chooser=LedgerPeriodChooser(currency, account),
report_chooser=ReportChooser(ReportType.INCOME_EXPENSES,
currency=currency,
account=account,
period=period),
data_rows=data_rows,
is_paged=True,
filler=filler,
brought_forward=brought_forward,
total=total)
self.currency: Currency = currency
"""The currency."""
self.account: Account = account
"""The account."""
self.period: Period = period
"""The period."""
@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.income-expenses-default",
currency=currency, account=self.account)
return url_for("accounting.report.income-expenses",
currency=currency, account=self.account,
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()]
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
def get_url(account: Account):
if self.period.is_default:
return url_for("accounting.report.income-expenses-default",
currency=self.currency, account=account)
return url_for("accounting.report.income-expenses",
currency=self.currency, account=account,
period=self.period)
in_use: sa.Select = sa.Select(JournalEntry.account_id)\
.join(Account)\
.filter(JournalEntry.currency_code == self.currency.code,
sa.or_(Account.base_code.startswith("11"),
Account.base_code.startswith("12"),
Account.base_code.startswith("21"),
Account.base_code.startswith("22")))\
.group_by(JournalEntry.account_id)
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 TrialBalanceParams(ReportParams[TrialBalanceRow]):
"""The parameters of a trial balance page."""
def __init__(self,
currency: Currency,
period: Period,
data_rows: list[TrialBalanceRow],
total: TrialBalanceRow):
"""Constructs the parameters for the trial balance page.
:param currency: The currency.
:param period: The period.
:param data_rows: The data rows.
:param total: The total row, if any.
"""
super().__init__(
period_chooser=TrialBalancePeriodChooser(currency),
report_chooser=ReportChooser(ReportType.TRIAL_BALANCE,
currency=currency,
period=period),
data_rows=data_rows,
is_paged=False,
total=total)
self.currency: Currency = currency
"""The currency."""
self.period: Period | None = period
"""The period."""
@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()]

View File

@ -22,56 +22,48 @@ import typing as t
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from decimal import Decimal from decimal import Decimal
from io import StringIO from io import StringIO
from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \
urlunparse
import sqlalchemy as sa import sqlalchemy as sa
from flask import Response, render_template, request, url_for from flask import Response, render_template, url_for
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.utils.pagination import Pagination from accounting.report.period import Period
from accounting.utils.txn_types import TransactionType from accounting.report.report_params import JournalParams, LedgerParams, \
from .option_link import OptionLink IncomeExpensesParams, TrialBalanceParams
from .period import Period from accounting.report.report_rows import JournalRow, LedgerRow, \
from .period_choosers import PeriodChooser, JournalPeriodChooser, \ IncomeExpensesRow, TrialBalanceRow
LedgerPeriodChooser, IncomeExpensesPeriodChooser, TrialBalancePeriodChooser
from .report_chooser import ReportChooser
from .report_rows import ReportRow, JournalRow, LedgerRow, IncomeExpensesRow, \
TrialBalanceRow
from .report_type import ReportType
T = t.TypeVar("T", bound=ReportRow) T = t.TypeVar("T")
"""The row class in the report."""
class JournalEntryReport(t.Generic[T], ABC): class Report(t.Generic[T], ABC):
"""A report based on a journal entry.""" """A report."""
def __init__(self, period: Period): def __init__(self):
"""Constructs a journal. """Constructs a report."""
self.data_rows: list[T]
:param period: The period. """The data rows."""
""" self.brought_forward: T | None
self.period: Period = period """The brought-forward row."""
"""The period.""" self.total: T | None
self.__rows: list[T] | None = None """The total row."""
"""The rows in the report.""" self.data_rows, self.brought_forward, self.total = self.get_rows()
@abstractmethod @abstractmethod
def get_rows(self) -> list[T]: def get_rows(self) -> tuple[list[T], T | None, T | None]:
"""Returns the rows, without pagination. """Returns the data rows, the brought-forward row, and the total row.
:return: The rows. :return: The data rows, the brought-forward row, and the total row.
""" """
@staticmethod
@abstractmethod @abstractmethod
def populate_rows(self, rows: list[T]) -> None: def populate_rows(rows: list[JournalRow]) -> None:
"""Populates the transaction, currency, account, and other data to the """Fills in the related data to the data rows.
given rows.
:param rows: The rows. :param rows: The data rows.
:return: None. :return: None.
""" """
@ -86,103 +78,71 @@ class JournalEntryReport(t.Generic[T], ABC):
@property @property
@abstractmethod @abstractmethod
def csv_filename(self) -> str: def csv_filename(self) -> str:
"""Returns the CSV file name. """Returns the CSV download file name.
:return: The CSV file name. :return: The CSV download file name.
""" """
@property def csv(self) -> Response:
@abstractmethod """Returns the report as CSV for download.
def period_chooser(self) -> PeriodChooser:
"""Returns the period chooser.
:return: The period chooser. :return: The response of the report for download.
""" """
rows: list[T] = []
@property if self.brought_forward is not None:
@abstractmethod rows.append(self.brought_forward)
def report_chooser(self) -> ReportChooser: rows.extend(self.data_rows)
"""Returns the report chooser. if self.total is not None:
rows.append(self.total)
:return: The report chooser. self.populate_rows(rows)
"""
@abstractmethod
def as_html_page(self) -> str:
"""Returns the report as an HTML page.
:return: The report as an HTML page.
"""
@property
def rows(self) -> list[T]:
"""Returns the journal entries.
:return: The journal entries.
"""
if self.__rows is None:
self.__rows = self.get_rows()
return self.__rows
@property
def txn_types(self) -> t.Type[TransactionType]:
"""Returns the transaction types.
:return: The transaction types.
"""
return TransactionType
@property
def csv_uri(self) -> str:
"""Returns the URI to download the report as CSV.
:return: The URI to download the report as CSV.
"""
uri: str = request.full_path if request.query_string else request.path
uri_p: ParseResult = urlparse(uri)
params: list[tuple[str, str]] = parse_qsl(uri_p.query)
params = [x for x in params if x[0] != "as"]
params.append(("as", "csv"))
parts: list[str] = list(uri_p)
parts[4] = urlencode(params)
return urlunparse(parts)
def as_csv_download(self) -> Response:
"""Returns the report as CSV download.
:return: The CSV download response.
"""
self.populate_rows(self.rows)
with StringIO() as fp: with StringIO() as fp:
writer: csv.DictWriter = csv.DictWriter( writer: csv.DictWriter = csv.DictWriter(
fp, fieldnames=self.csv_field_names) fp, fieldnames=self.csv_field_names)
writer.writeheader() writer.writeheader()
writer.writerows([x.as_dict() for x in self.rows]) writer.writerows([x.as_dict() for x in rows])
fp.seek(0) fp.seek(0)
response: Response = Response(fp.read(), mimetype="text/csv") response: Response = Response(fp.read(), mimetype="text/csv")
response.headers["Content-Disposition"] \ response.headers["Content-Disposition"] \
= f"attachment; filename={self.csv_filename}" = f"attachment; filename={self.csv_filename}"
return response return response
@abstractmethod
def html(self) -> str:
"""Composes and returns the report as HTML.
class Journal(JournalEntryReport[JournalRow]): :return: The report as HTML.
"""
class Journal(Report[JournalRow]):
"""The journal.""" """The journal."""
def get_rows(self) -> list[JournalRow]: def __init__(self, period: Period):
"""Constructs a journal.
:param period: The period.
"""
self.period: Period = period
"""The period."""
super().__init__()
def get_rows(self) -> tuple[list[T], T | None, T | None]:
conditions: list[sa.BinaryExpression] = [] conditions: list[sa.BinaryExpression] = []
if self.period.start is not None: if self.period.start is not None:
conditions.append(Transaction.date >= self.period.start) conditions.append(Transaction.date >= self.period.start)
if self.period.end is not None: if self.period.end is not None:
conditions.append(Transaction.date <= self.period.end) conditions.append(Transaction.date <= self.period.end)
return [JournalRow(x) for x in db.session rows: list[JournalRow] = [JournalRow(x) for x in db.session
.query(JournalEntry) .query(JournalEntry)
.join(Transaction) .join(Transaction)
.filter(*conditions) .filter(*conditions)
.order_by(Transaction.date, .order_by(Transaction.date,
JournalEntry.is_debit.desc(), JournalEntry.is_debit.desc(),
JournalEntry.no).all()] JournalEntry.no).all()]
return rows, None, None
def populate_rows(self, rows: list[JournalRow]) -> None: @staticmethod
def populate_rows(rows: list[JournalRow]) -> None:
transactions: dict[int, Transaction] \ transactions: dict[int, Transaction] \
= {x.id: x for x in Transaction.query.filter( = {x.id: x for x in Transaction.query.filter(
Transaction.id.in_({x.entry.transaction_id for x in rows}))} Transaction.id.in_({x.entry.transaction_id for x in rows}))}
@ -206,24 +166,16 @@ class Journal(JournalEntryReport[JournalRow]):
def csv_filename(self) -> str: def csv_filename(self) -> str:
return f"journal-{self.period.spec}.csv" return f"journal-{self.period.spec}.csv"
@property def html(self) -> str:
def period_chooser(self) -> PeriodChooser: params: JournalParams = JournalParams(
return JournalPeriodChooser() period=self.period,
data_rows=self.data_rows,
@property filler=self.populate_rows)
def report_chooser(self) -> ReportChooser:
return ReportChooser(ReportType.JOURNAL,
period=self.period)
def as_html_page(self) -> str:
pagination: Pagination = Pagination[JournalRow](self.rows)
rows: list[JournalRow] = pagination.list
self.populate_rows(rows)
return render_template("accounting/report/journal.html", return render_template("accounting/report/journal.html",
list=rows, pagination=pagination, report=self) report=params)
class Ledger(JournalEntryReport[LedgerRow]): class Ledger(Report[LedgerRow]):
"""The ledger.""" """The ledger."""
def __init__(self, currency: Currency, account: Account, period: Period): def __init__(self, currency: Currency, account: Account, period: Period):
@ -233,23 +185,20 @@ class Ledger(JournalEntryReport[LedgerRow]):
:param account: The account. :param account: The account.
:param period: The period. :param period: The period.
""" """
super().__init__(period)
self.currency: Currency = currency self.currency: Currency = currency
"""The currency.""" """The currency."""
self.account: Account = account self.account: Account = account
"""The account.""" """The account."""
self.total_row: LedgerRow | None = None self.period: Period = period
"""The total row to show on the template.""" """The period."""
super().__init__()
def get_rows(self) -> list[LedgerRow]: def get_rows(self) -> tuple[list[T], T | None, T | None]:
brought_forward: LedgerRow | None = self.__get_brought_forward_row() brought_forward: LedgerRow | None = self.__get_brought_forward_row()
rows: list[LedgerRow] = [LedgerRow(x) for x in self.__query_entries()] rows: list[LedgerRow] = [LedgerRow(x) for x in self.__query_entries()]
total: LedgerRow = self.__get_total_row(brought_forward, rows) total: LedgerRow = self.__get_total_row(brought_forward, rows)
self.__populate_balance(brought_forward, rows) self.__populate_balance(brought_forward, rows)
if brought_forward is not None: return rows, brought_forward, total
rows.insert(0, brought_forward)
rows.append(total)
return rows
def __get_brought_forward_row(self) -> LedgerRow | None: def __get_brought_forward_row(self) -> LedgerRow | None:
"""Queries, composes and returns the brought-forward row. """Queries, composes and returns the brought-forward row.
@ -334,7 +283,8 @@ class Ledger(JournalEntryReport[LedgerRow]):
balance = balance - row.credit balance = balance - row.credit
row.balance = balance row.balance = balance
def populate_rows(self, rows: list[LedgerRow]) -> None: @staticmethod
def populate_rows(rows: list[LedgerRow]) -> None:
transactions: dict[int, Transaction] \ transactions: dict[int, Transaction] \
= {x.id: x for x in Transaction.query.filter( = {x.id: x for x in Transaction.query.filter(
Transaction.id.in_({x.entry.transaction_id for x in rows Transaction.id.in_({x.entry.transaction_id for x in rows
@ -355,70 +305,20 @@ class Ledger(JournalEntryReport[LedgerRow]):
currency=self.currency.code, account=self.account.code, currency=self.currency.code, account=self.account.code,
period=self.period.spec) period=self.period.spec)
@property def html(self) -> str:
def period_chooser(self) -> PeriodChooser: params: LedgerParams = LedgerParams(
return LedgerPeriodChooser(self.currency, self.account)
@property
def report_chooser(self) -> ReportChooser:
return ReportChooser(ReportType.LEDGER,
currency=self.currency, currency=self.currency,
account=self.account, account=self.account,
period=self.period) period=self.period,
data_rows=self.data_rows,
def as_html_page(self) -> str: filler=self.populate_rows,
pagination: Pagination = Pagination[LedgerRow](self.rows) brought_forward=self.brought_forward,
rows: list[LedgerRow] = pagination.list total=self.total)
self.populate_rows(rows)
if len(rows) > 0 and rows[-1].is_total:
self.total_row = rows[-1]
rows = rows[:-1]
return render_template("accounting/report/ledger.html", return render_template("accounting/report/ledger.html",
list=rows, pagination=pagination, report=self) report=params)
@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.ledger-default",
currency=currency, account=self.account)
return url_for("accounting.report.ledger",
currency=currency, account=self.account,
period=self.period)
in_use: sa.Select = sa.Select(JournalEntry.currency_code)\
.group_by(JournalEntry.currency_code)
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()]
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
def get_url(account: 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)
in_use: sa.Select = sa.Select(JournalEntry.account_id)\
.filter(JournalEntry.currency_code == self.currency.code)\
.group_by(JournalEntry.account_id)
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 IncomeExpenses(JournalEntryReport[IncomeExpensesRow]): class IncomeExpenses(Report[IncomeExpensesRow]):
"""The income and expenses.""" """The income and expenses."""
def __init__(self, currency: Currency, account: Account, period: Period): def __init__(self, currency: Currency, account: Account, period: Period):
@ -428,25 +328,22 @@ class IncomeExpenses(JournalEntryReport[IncomeExpensesRow]):
:param account: The account. :param account: The account.
:param period: The period. :param period: The period.
""" """
super().__init__(period)
self.currency: Currency = currency self.currency: Currency = currency
"""The currency.""" """The currency."""
self.account: Account = account self.account: Account = account
"""The account.""" """The account."""
self.total_row: IncomeExpensesRow | None = None self.period: Period = period
"""The total row to show on the template.""" """The period."""
super().__init__()
def get_rows(self) -> list[IncomeExpensesRow]: def get_rows(self) -> tuple[list[T], T | None, T | None]:
brought_forward: IncomeExpensesRow | None \ brought_forward: IncomeExpensesRow | None \
= self.__get_brought_forward_row() = self.__get_brought_forward_row()
rows: list[IncomeExpensesRow] \ rows: list[IncomeExpensesRow] \
= [IncomeExpensesRow(x) for x in self.__query_entries()] = [IncomeExpensesRow(x) for x in self.__query_entries()]
total: IncomeExpensesRow = self.__get_total_row(brought_forward, rows) total: IncomeExpensesRow = self.__get_total_row(brought_forward, rows)
self.__populate_balance(brought_forward, rows) self.__populate_balance(brought_forward, rows)
if brought_forward is not None: return rows, brought_forward, total
rows.insert(0, brought_forward)
rows.append(total)
return rows
def __get_brought_forward_row(self) -> IncomeExpensesRow | None: def __get_brought_forward_row(self) -> IncomeExpensesRow | None:
"""Queries, composes and returns the brought-forward row. """Queries, composes and returns the brought-forward row.
@ -468,6 +365,7 @@ class IncomeExpenses(JournalEntryReport[IncomeExpensesRow]):
return None return None
row: IncomeExpensesRow = IncomeExpensesRow() row: IncomeExpensesRow = IncomeExpensesRow()
row.date = self.period.start row.date = self.period.start
row.account = Account.find_by_code("3351-001")
row.summary = gettext("Brought forward") row.summary = gettext("Brought forward")
if balance > 0: if balance > 0:
row.income = balance row.income = balance
@ -536,7 +434,8 @@ class IncomeExpenses(JournalEntryReport[IncomeExpensesRow]):
balance = balance - row.expense balance = balance - row.expense
row.balance = balance row.balance = balance
def populate_rows(self, rows: list[IncomeExpensesRow]) -> None: @staticmethod
def populate_rows(rows: list[IncomeExpensesRow]) -> None:
transactions: dict[int, Transaction] \ transactions: dict[int, Transaction] \
= {x.id: x for x in Transaction.query.filter( = {x.id: x for x in Transaction.query.filter(
Transaction.id.in_({x.entry.transaction_id for x in rows Transaction.id.in_({x.entry.transaction_id for x in rows
@ -563,96 +462,39 @@ class IncomeExpenses(JournalEntryReport[IncomeExpensesRow]):
currency=self.currency.code, account=self.account.code, currency=self.currency.code, account=self.account.code,
period=self.period.spec) period=self.period.spec)
@property def html(self) -> str:
def period_chooser(self) -> PeriodChooser: params: IncomeExpensesParams = IncomeExpensesParams(
return IncomeExpensesPeriodChooser(self.currency, self.account)
@property
def report_chooser(self) -> ReportChooser:
return ReportChooser(ReportType.INCOME_EXPENSES,
currency=self.currency, currency=self.currency,
account=self.account, account=self.account,
period=self.period) period=self.period,
data_rows=self.data_rows,
def as_html_page(self) -> str: filler=self.populate_rows,
pagination: Pagination = Pagination[IncomeExpensesRow](self.rows) brought_forward=self.brought_forward,
rows: list[IncomeExpensesRow] = pagination.list total=self.total)
self.populate_rows(rows)
if len(rows) > 0 and rows[-1].is_total:
self.total_row = rows[-1]
rows = rows[:-1]
return render_template("accounting/report/income-expenses.html", return render_template("accounting/report/income-expenses.html",
list=rows, pagination=pagination, report=self) report=params)
@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.income-expenses-default",
currency=currency, account=self.account)
return url_for("accounting.report.income-expenses",
currency=currency, account=self.account,
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()]
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
def get_url(account: Account):
if self.period.is_default:
return url_for("accounting.report.income-expenses-default",
currency=self.currency, account=account)
return url_for("accounting.report.income-expenses",
currency=self.currency, account=account,
period=self.period)
in_use: sa.Select = sa.Select(JournalEntry.account_id)\
.join(Account)\
.filter(JournalEntry.currency_code == self.currency.code,
sa.or_(Account.base_code.startswith("11"),
Account.base_code.startswith("12"),
Account.base_code.startswith("21"),
Account.base_code.startswith("22")))\
.group_by(JournalEntry.account_id)
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]): class TrialBalance(Report[TrialBalanceRow]):
"""The trial balance.""" """The trial balance."""
def __init__(self, currency: Currency, period: Period): def __init__(self, currency: Currency, period: Period):
"""Constructs a trial balance. """Constructs an income and expenses.
:param currency: The currency. :param currency: The currency.
:param period: The period. :param period: The period.
""" """
super().__init__(period)
self.currency: Currency = currency self.currency: Currency = currency
"""The currency.""" """The currency."""
self.total_row: TrialBalanceRow | None = None self.period: Period = period
"""The total row.""" """The period."""
super().__init__()
def get_rows(self) -> list[TrialBalanceRow]: def get_rows(self) -> tuple[list[T], T | None, T | None]:
rows: list[TrialBalanceRow] = self.__query_balances() rows: list[TrialBalanceRow] = self.__query_balances()
self.__populate_url(rows) self.__populate_url(rows)
total_row: TrialBalanceRow = self.__get_total_row(rows) total_row: TrialBalanceRow = self.__get_total_row(rows)
rows.append(total_row) return rows, None, total_row
return rows
def __query_balances(self) -> list[TrialBalanceRow]: def __query_balances(self) -> list[TrialBalanceRow]:
"""Queries and returns the balances. """Queries and returns the balances.
@ -670,9 +512,10 @@ class TrialBalance(JournalEntryReport[TrialBalanceRow]):
else_=-JournalEntry.amount)).label("balance") else_=-JournalEntry.amount)).label("balance")
select_trial_balance: sa.Select \ select_trial_balance: sa.Select \
= sa.select(JournalEntry.account_id, balance_func)\ = sa.select(JournalEntry.account_id, balance_func)\
.join(Transaction)\ .join(Transaction).join(Account)\
.filter(*conditions)\ .filter(*conditions)\
.group_by(JournalEntry.account_id) .group_by(JournalEntry.account_id)\
.order_by(Account.base_code, Account.no)
balances: list[sa.Row] = db.session.execute(select_trial_balance).all() balances: list[sa.Row] = db.session.execute(select_trial_balance).all()
accounts: dict[int, Account] \ accounts: dict[int, Account] \
= {x.id: x for x in Account.query = {x.id: x for x in Account.query
@ -715,7 +558,8 @@ class TrialBalance(JournalEntryReport[TrialBalanceRow]):
row.credit = sum([x.credit for x in rows if x.credit is not None]) row.credit = sum([x.credit for x in rows if x.credit is not None])
return row return row
def populate_rows(self, rows: list[T]) -> None: @staticmethod
def populate_rows(rows: list[JournalRow]) -> None:
pass pass
@property @property
@ -726,41 +570,11 @@ class TrialBalance(JournalEntryReport[TrialBalanceRow]):
def csv_filename(self) -> str: def csv_filename(self) -> str:
return f"trial-balance-{self.period.spec}.csv" return f"trial-balance-{self.period.spec}.csv"
@property def html(self) -> str:
def period_chooser(self) -> PeriodChooser: params: TrialBalanceParams = TrialBalanceParams(
return TrialBalancePeriodChooser(self.currency)
@property
def report_chooser(self) -> ReportChooser:
return ReportChooser(ReportType.TRIAL_BALANCE,
currency=self.currency, currency=self.currency,
period=self.period) period=self.period,
data_rows=self.data_rows,
def as_html_page(self) -> str: total=self.total)
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", return render_template("accounting/report/trial-balance.html",
list=rows, pagination=pagination, report=self) report=params)
@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()]

View File

@ -57,8 +57,8 @@ def __get_journal_list(period: Period) -> str | Response:
""" """
report: Journal = Journal(period) report: Journal = Journal(period)
if "as" in request.args and request.args["as"] == "csv": if "as" in request.args and request.args["as"] == "csv":
return report.as_csv_download() return report.csv()
return report.as_html_page() return report.html()
@bp.get("ledger/<currency:currency>/<account:account>", @bp.get("ledger/<currency:currency>/<account:account>",
@ -101,8 +101,8 @@ def __get_ledger_list(currency: Currency, account: Account, period: Period) \
""" """
report: Ledger = Ledger(currency, account, period) report: Ledger = Ledger(currency, account, period)
if "as" in request.args and request.args["as"] == "csv": if "as" in request.args and request.args["as"] == "csv":
return report.as_csv_download() return report.csv()
return report.as_html_page() return report.html()
@bp.get("income-expenses/<currency:currency>/<account:account>", @bp.get("income-expenses/<currency:currency>/<account:account>",
@ -146,8 +146,8 @@ def __get_income_expenses_list(currency: Currency, account: Account,
""" """
report: IncomeExpenses = IncomeExpenses(currency, account, period) report: IncomeExpenses = IncomeExpenses(currency, account, period)
if "as" in request.args and request.args["as"] == "csv": if "as" in request.args and request.args["as"] == "csv":
return report.as_csv_download() return report.csv()
return report.as_html_page() return report.html()
@bp.get("trial-balance/<currency:currency>", @bp.get("trial-balance/<currency:currency>",
@ -186,5 +186,5 @@ def __get_trial_balance_list(currency: Currency, period: Period) \
""" """
report: TrialBalance = TrialBalance(currency, period) report: TrialBalance = TrialBalance(currency, period)
if "as" in request.args and request.args["as"] == "csv": if "as" in request.args and request.args["as"] == "csv":
return report.as_csv_download() return report.csv()
return report.as_html_page() return report.html()

View File

@ -146,8 +146,10 @@ First written: 2023/3/5
{% include "accounting/report/include/period-chooser.html" %} {% include "accounting/report/include/period-chooser.html" %}
{% endwith %} {% endwith %}
{% if list %} {% if report.has_data %}
{% with pagination = report.pagination %}
{% include "accounting/include/pagination.html" %} {% include "accounting/include/pagination.html" %}
{% endwith %}
<div class="d-none d-md-block accounting-report-table accounting-income-expenses-table"> <div class="d-none d-md-block accounting-report-table accounting-income-expenses-table">
<div class="accounting-report-table-header"> <div class="accounting-report-table-header">
@ -161,8 +163,19 @@ First written: 2023/3/5
</div> </div>
</div> </div>
<div class="accounting-report-table-body"> <div class="accounting-report-table-body">
{% for item in list %} {% if report.brought_forward %}
{% if item.transaction %} {% with item = report.brought_forward %}
<div class="accounting-report-table-row">
<div>{{ item.date|accounting_format_date }}</div>
<div>{{ item.account.title|title }}</div>
<div>{{ item.summary|accounting_default }}</div>
<div class="accounting-amount">{{ item.income|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ item.expense|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ item.balance|accounting_format_amount }}</div>
</div>
{% endwith %}
{% endif %}
{% for item in report.data_rows %}
<a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}"> <a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}">
<div>{{ item.date|accounting_format_date }}</div> <div>{{ item.date|accounting_format_date }}</div>
<div>{{ item.account.title|title }}</div> <div>{{ item.account.title|title }}</div>
@ -171,44 +184,37 @@ First written: 2023/3/5
<div class="accounting-amount">{{ item.expense|accounting_format_amount|accounting_default }}</div> <div class="accounting-amount">{{ item.expense|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ item.balance|accounting_format_amount }}</div> <div class="accounting-amount">{{ item.balance|accounting_format_amount }}</div>
</a> </a>
{% else %}
<div class="accounting-report-table-row">
<div>{{ item.date|accounting_format_date }}</div>
<div>{{ item.account.title|title }}</div>
<div>{{ item.summary|accounting_default }}</div>
<div class="accounting-amount">{{ item.income|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ item.expense|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ item.balance|accounting_format_amount }}</div>
</div>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% if report.total_row %} {% if report.total %}
{% with item = report.total %}
<div class="accounting-report-table-footer"> <div class="accounting-report-table-footer">
<div class="accounting-report-table-row"> <div class="accounting-report-table-row">
<div>{{ A_("Total") }}</div> <div>{{ A_("Total") }}</div>
<div class="accounting-amount">{{ report.total_row.income|accounting_format_amount }}</div> <div class="accounting-amount">{{ item.income|accounting_format_amount }}</div>
<div class="accounting-amount">{{ report.total_row.expense|accounting_format_amount }}</div> <div class="accounting-amount">{{ item.expense|accounting_format_amount }}</div>
<div class="accounting-amount">{{ report.total_row.balance|accounting_format_amount }}</div> <div class="accounting-amount">{{ item.balance|accounting_format_amount }}</div>
</div> </div>
</div> </div>
{% endwith %}
{% endif %} {% endif %}
</div> </div>
<div class="list-group d-md-none"> <div class="list-group d-md-none">
{% for item in list %} {% if report.brought_forward %}
{% if item.transaction is not none %} {% with item = report.brought_forward %}
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}">
{% include "accounting/report/include/income-expenses-mobile-row.html" %}
</a>
{% else %}
<div class="list-group-item list-group-item-action d-flex justify-content-between"> <div class="list-group-item list-group-item-action d-flex justify-content-between">
{% include "accounting/report/include/income-expenses-mobile-row.html" %} {% include "accounting/report/include/income-expenses-mobile-row.html" %}
</div> </div>
{% endwith %}
{% endif %} {% endif %}
{% for item in report.data_rows %}
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}">
{% include "accounting/report/include/income-expenses-mobile-row.html" %}
</a>
{% endfor %} {% endfor %}
{% if report.total_row is not none %} {% if report.total is not none %}
{% with item = report.total_row %} {% with item = report.total %}
<div class="list-group-item list-group-item-action d-flex justify-content-between"> <div class="list-group-item list-group-item-action d-flex justify-content-between">
{% include "accounting/report/include/income-expenses-mobile-row.html" %} {% include "accounting/report/include/income-expenses-mobile-row.html" %}
</div> </div>

View File

@ -88,8 +88,10 @@ First written: 2023/3/4
{% include "accounting/report/include/period-chooser.html" %} {% include "accounting/report/include/period-chooser.html" %}
{% endwith %} {% endwith %}
{% if list %} {% if report.has_data %}
{% with pagination = report.pagination %}
{% include "accounting/include/pagination.html" %} {% include "accounting/include/pagination.html" %}
{% endwith %}
<div class="d-none d-md-block accounting-report-table accounting-journal-table"> <div class="d-none d-md-block accounting-report-table accounting-journal-table">
<div class="accounting-report-table-header"> <div class="accounting-report-table-header">
@ -103,7 +105,7 @@ First written: 2023/3/4
</div> </div>
</div> </div>
<div class="accounting-report-table-body"> <div class="accounting-report-table-body">
{% for item in list %} {% for item in report.data_rows %}
<a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}"> <a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}">
<div>{{ item.transaction.date|accounting_format_date }}</div> <div>{{ item.transaction.date|accounting_format_date }}</div>
<div>{{ item.currency.name }}</div> <div>{{ item.currency.name }}</div>
@ -117,7 +119,7 @@ First written: 2023/3/4
</div> </div>
<div class="list-group d-md-none"> <div class="list-group d-md-none">
{% for item in list %} {% for item in report.data_rows %}
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}"> <a class="list-group-item list-group-item-action" href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div {% if not item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}> <div {% if not item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>

View File

@ -146,8 +146,10 @@ First written: 2023/3/5
{% include "accounting/report/include/period-chooser.html" %} {% include "accounting/report/include/period-chooser.html" %}
{% endwith %} {% endwith %}
{% if list %} {% if report.has_data %}
{% with pagination = report.pagination %}
{% include "accounting/include/pagination.html" %} {% include "accounting/include/pagination.html" %}
{% endwith %}
<div class="d-none d-md-block accounting-report-table accounting-ledger-table"> <div class="d-none d-md-block accounting-report-table accounting-ledger-table">
<div class="accounting-report-table-header"> <div class="accounting-report-table-header">
@ -160,8 +162,18 @@ First written: 2023/3/5
</div> </div>
</div> </div>
<div class="accounting-report-table-body"> <div class="accounting-report-table-body">
{% for item in list %} {% if report.brought_forward %}
{% if item.transaction %} {% with item = report.brought_forward %}
<div class="accounting-report-table-row">
<div>{{ item.date|accounting_format_date }}</div>
<div>{{ item.summary|accounting_default }}</div>
<div class="accounting-amount">{{ item.debit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ item.credit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ item.balance|accounting_format_amount }}</div>
</div>
{% endwith %}
{% endif %}
{% for item in report.data_rows %}
<a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}"> <a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}">
<div>{{ item.date|accounting_format_date }}</div> <div>{{ item.date|accounting_format_date }}</div>
<div>{{ item.summary|accounting_default }}</div> <div>{{ item.summary|accounting_default }}</div>
@ -169,43 +181,37 @@ First written: 2023/3/5
<div class="accounting-amount">{{ item.credit|accounting_format_amount|accounting_default }}</div> <div class="accounting-amount">{{ item.credit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ item.balance|accounting_format_amount }}</div> <div class="accounting-amount">{{ item.balance|accounting_format_amount }}</div>
</a> </a>
{% else %}
<div class="accounting-report-table-row">
<div>{{ item.date|accounting_format_date }}</div>
<div>{{ item.summary|accounting_default }}</div>
<div class="accounting-amount">{{ item.debit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ item.credit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ item.balance|accounting_format_amount }}</div>
</div>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% if report.total_row %} {% if report.total %}
{% with item = report.total %}
<div class="accounting-report-table-footer"> <div class="accounting-report-table-footer">
<div class="accounting-report-table-row"> <div class="accounting-report-table-row">
<div>{{ A_("Total") }}</div> <div>{{ A_("Total") }}</div>
<div class="accounting-amount">{{ report.total_row.debit|accounting_format_amount }}</div> <div class="accounting-amount">{{ item.debit|accounting_format_amount }}</div>
<div class="accounting-amount">{{ report.total_row.credit|accounting_format_amount }}</div> <div class="accounting-amount">{{ item.credit|accounting_format_amount }}</div>
<div class="accounting-amount">{{ report.total_row.balance|accounting_format_amount }}</div> <div class="accounting-amount">{{ item.balance|accounting_format_amount }}</div>
</div> </div>
</div> </div>
{% endwith %}
{% endif %} {% endif %}
</div> </div>
<div class="list-group d-md-none"> <div class="list-group d-md-none">
{% for item in list %} {% if report.brought_forward %}
{% if item.transaction is not none %} {% with item = report.brought_forward %}
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}">
{% include "accounting/report/include/ledger-mobile-row.html" %}
</a>
{% else %}
<div class="list-group-item list-group-item-action d-flex justify-content-between"> <div class="list-group-item list-group-item-action d-flex justify-content-between">
{% include "accounting/report/include/ledger-mobile-row.html" %} {% include "accounting/report/include/ledger-mobile-row.html" %}
</div> </div>
{% endwith %}
{% endif %} {% endif %}
{% for item in report.data_rows %}
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}">
{% include "accounting/report/include/ledger-mobile-row.html" %}
</a>
{% endfor %} {% endfor %}
{% if report.total_row is not none %} {% if report.total is not none %}
{% with item = report.total_row %} {% with item = report.total %}
<div class="list-group-item list-group-item-action d-flex justify-content-between"> <div class="list-group-item list-group-item-action d-flex justify-content-between">
{% include "accounting/report/include/ledger-mobile-row.html" %} {% include "accounting/report/include/ledger-mobile-row.html" %}
</div> </div>

View File

@ -116,7 +116,7 @@ First written: 2023/3/5
{% include "accounting/report/include/period-chooser.html" %} {% include "accounting/report/include/period-chooser.html" %}
{% endwith %} {% endwith %}
{% if list %} {% if report.has_data %}
<div class="accounting-sheet"> <div class="accounting-sheet">
<div class="d-none d-sm-flex justify-content-center mb-3"> <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.name|title, period=report.period.desc|title) }}</h2> <h2 class="text-center">{{ _("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
@ -131,7 +131,7 @@ First written: 2023/3/5
</div> </div>
</div> </div>
<div class="accounting-report-table-body"> <div class="accounting-report-table-body">
{% for item in list %} {% for item in report.data_rows %}
<a class="accounting-report-table-row" href="{{ item.url }}"> <a class="accounting-report-table-row" href="{{ item.url }}">
<div> <div>
<span class="d-none d-md-inline">{{ item.account.code }}</span> <span class="d-none d-md-inline">{{ item.account.code }}</span>
@ -145,8 +145,8 @@ First written: 2023/3/5
<div class="accounting-report-table-footer"> <div class="accounting-report-table-footer">
<div class="accounting-report-table-row"> <div class="accounting-report-table-row">
<div>{{ A_("Total") }}</div> <div>{{ A_("Total") }}</div>
<div class="accounting-amount">{{ report.total_row.debit|accounting_format_amount }}</div> <div class="accounting-amount">{{ report.total.debit|accounting_format_amount }}</div>
<div class="accounting-amount">{{ report.total_row.credit|accounting_format_amount }}</div> <div class="accounting-amount">{{ report.total.credit|accounting_format_amount }}</div>
</div> </div>
</div> </div>
</div> </div>