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:
parent
e797cfeb8c
commit
ef9e5cb5b3
341
src/accounting/report/report_params.py
Normal file
341
src/accounting/report/report_params.py
Normal 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()]
|
@ -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()]
|
|
||||||
|
@ -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()
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user