Added the income and expenses.
This commit is contained in:
parent
39723b1299
commit
39807ef480
@ -139,3 +139,26 @@ class LedgerPeriodChooser(PeriodChooser):
|
|||||||
return url_for("accounting.report.ledger",
|
return url_for("accounting.report.ledger",
|
||||||
currency=self.currency, account=self.account,
|
currency=self.currency, account=self.account,
|
||||||
period=period)
|
period=period)
|
||||||
|
|
||||||
|
|
||||||
|
class IncomeExpensesPeriodChooser(PeriodChooser):
|
||||||
|
"""The income-expenses period chooser."""
|
||||||
|
|
||||||
|
def __init__(self, currency: Currency, account: Account):
|
||||||
|
"""Constructs the income-expenses period chooser."""
|
||||||
|
self.currency: Currency = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.account: Account = account
|
||||||
|
"""The account."""
|
||||||
|
first: Transaction | None \
|
||||||
|
= Transaction.query.order_by(Transaction.date).first()
|
||||||
|
super(IncomeExpensesPeriodChooser, self).__init__(
|
||||||
|
None if first is None else first.date)
|
||||||
|
|
||||||
|
def _url_for(self, period: Period) -> str:
|
||||||
|
if period.is_default:
|
||||||
|
return url_for("accounting.report.income-expenses-default",
|
||||||
|
currency=self.currency, account=self.account)
|
||||||
|
return url_for("accounting.report.income-expenses",
|
||||||
|
currency=self.currency, account=self.account,
|
||||||
|
period=period)
|
||||||
|
@ -66,6 +66,7 @@ class ReportChooser:
|
|||||||
"""The title of the current report."""
|
"""The title of the current report."""
|
||||||
self.__reports.append(self.__journal)
|
self.__reports.append(self.__journal)
|
||||||
self.__reports.append(self.__ledger)
|
self.__reports.append(self.__ledger)
|
||||||
|
self.__reports.append(self.__income_expenses)
|
||||||
for report in self.__reports:
|
for report in self.__reports:
|
||||||
if report.is_active:
|
if report.is_active:
|
||||||
self.current_report = report.title
|
self.current_report = report.title
|
||||||
@ -97,6 +98,21 @@ class ReportChooser:
|
|||||||
return OptionLink(gettext("Ledger"), url,
|
return OptionLink(gettext("Ledger"), url,
|
||||||
self.__active_report == ReportType.LEDGER)
|
self.__active_report == ReportType.LEDGER)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __income_expenses(self) -> OptionLink:
|
||||||
|
"""Returns the income and expenses.
|
||||||
|
|
||||||
|
:return: The income and expenses.
|
||||||
|
"""
|
||||||
|
url: str = url_for("accounting.report.income-expenses-default",
|
||||||
|
currency=self.__currency, account=self.__account) \
|
||||||
|
if self.__period.is_default \
|
||||||
|
else url_for("accounting.report.income-expenses",
|
||||||
|
currency=self.__currency, account=self.__account,
|
||||||
|
period=self.__period)
|
||||||
|
return OptionLink(gettext("Income and Expenses"), url,
|
||||||
|
self.__active_report == ReportType.INCOME_EXPENSES)
|
||||||
|
|
||||||
def __iter__(self) -> t.Iterator[OptionLink]:
|
def __iter__(self) -> t.Iterator[OptionLink]:
|
||||||
"""Returns the iteration of the reports.
|
"""Returns the iteration of the reports.
|
||||||
|
|
||||||
|
@ -117,3 +117,55 @@ class LedgerRow(ReportRow):
|
|||||||
"Credit": self.credit,
|
"Credit": self.credit,
|
||||||
"Balance": self.balance,
|
"Balance": self.balance,
|
||||||
"Note": self.note}
|
"Note": self.note}
|
||||||
|
|
||||||
|
|
||||||
|
class IncomeExpensesRow(ReportRow):
|
||||||
|
"""A row in the income and expenses."""
|
||||||
|
|
||||||
|
def __init__(self, entry: JournalEntry | None = None):
|
||||||
|
"""Constructs the row in the income and expenses.
|
||||||
|
|
||||||
|
:param entry: The journal entry.
|
||||||
|
"""
|
||||||
|
self.entry: JournalEntry | None = None
|
||||||
|
"""The journal entry."""
|
||||||
|
self.transaction: Transaction | None = None
|
||||||
|
"""The transaction."""
|
||||||
|
self.is_total: bool = False
|
||||||
|
"""Whether this is the total row."""
|
||||||
|
self.date: date | None = None
|
||||||
|
"""The date."""
|
||||||
|
self.account: Account | None = None
|
||||||
|
"""The date."""
|
||||||
|
self.summary: str | None = None
|
||||||
|
"""The summary."""
|
||||||
|
self.income: Decimal | None = None
|
||||||
|
"""The income amount."""
|
||||||
|
self.expense: Decimal | None = None
|
||||||
|
"""The expense amount."""
|
||||||
|
self.balance: Decimal | None = None
|
||||||
|
"""The balance."""
|
||||||
|
self.note: str | None = None
|
||||||
|
"""The note."""
|
||||||
|
if entry is not None:
|
||||||
|
self.entry = entry
|
||||||
|
self.summary = entry.summary
|
||||||
|
self.income = None if entry.is_debit else entry.amount
|
||||||
|
self.expense = entry.amount if entry.is_debit else None
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, t.Any]:
|
||||||
|
if self.is_total:
|
||||||
|
return {"Date": "Total",
|
||||||
|
"Account": None,
|
||||||
|
"Summary": None,
|
||||||
|
"Income": self.income,
|
||||||
|
"Expense": self.expense,
|
||||||
|
"Balance": self.balance,
|
||||||
|
"Note": None}
|
||||||
|
return {"Date": self.date,
|
||||||
|
"Account": str(self.account),
|
||||||
|
"Summary": self.summary,
|
||||||
|
"Income": self.income,
|
||||||
|
"Expense": self.expense,
|
||||||
|
"Balance": self.balance,
|
||||||
|
"Note": self.note}
|
||||||
|
@ -26,3 +26,5 @@ class ReportType(Enum):
|
|||||||
"""The journal."""
|
"""The journal."""
|
||||||
LEDGER: str = "ledger"
|
LEDGER: str = "ledger"
|
||||||
"""The ledger."""
|
"""The ledger."""
|
||||||
|
INCOME_EXPENSES: str = "income-expenses"
|
||||||
|
"""The income and expenses."""
|
||||||
|
@ -36,9 +36,9 @@ from accounting.utils.txn_types import TransactionType
|
|||||||
from .option_link import OptionLink
|
from .option_link import OptionLink
|
||||||
from .period import Period
|
from .period import Period
|
||||||
from .period_choosers import PeriodChooser, JournalPeriodChooser, \
|
from .period_choosers import PeriodChooser, JournalPeriodChooser, \
|
||||||
LedgerPeriodChooser
|
LedgerPeriodChooser, IncomeExpensesPeriodChooser
|
||||||
from .report_chooser import ReportChooser
|
from .report_chooser import ReportChooser
|
||||||
from .report_rows import ReportRow, JournalRow, LedgerRow
|
from .report_rows import ReportRow, JournalRow, LedgerRow, IncomeExpensesRow
|
||||||
from .report_type import ReportType
|
from .report_type import ReportType
|
||||||
|
|
||||||
T = t.TypeVar("T", bound=ReportRow)
|
T = t.TypeVar("T", bound=ReportRow)
|
||||||
@ -417,3 +417,214 @@ class Ledger(JournalEntryReport[LedgerRow]):
|
|||||||
return [OptionLink(str(x), get_url(x), x.id == self.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))
|
for x in Account.query.filter(Account.id.in_(in_use))
|
||||||
.order_by(Account.base_code, Account.no).all()]
|
.order_by(Account.base_code, Account.no).all()]
|
||||||
|
|
||||||
|
|
||||||
|
class IncomeExpenses(JournalEntryReport[IncomeExpensesRow]):
|
||||||
|
"""The income and expenses."""
|
||||||
|
|
||||||
|
def __init__(self, currency: Currency, account: Account, period: Period):
|
||||||
|
"""Constructs an income and expenses.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
:param period: The period.
|
||||||
|
"""
|
||||||
|
super().__init__(period)
|
||||||
|
self.currency: Currency = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.account: Account = account
|
||||||
|
"""The account."""
|
||||||
|
self.total_row: IncomeExpensesRow | None = None
|
||||||
|
"""The total row to show on the template."""
|
||||||
|
|
||||||
|
def get_rows(self) -> list[IncomeExpensesRow]:
|
||||||
|
brought_forward: IncomeExpensesRow | None \
|
||||||
|
= self.__get_brought_forward_row()
|
||||||
|
rows: list[IncomeExpensesRow] \
|
||||||
|
= [IncomeExpensesRow(x) for x in self.__query_entries()]
|
||||||
|
total: IncomeExpensesRow = self.__get_total_row(brought_forward, rows)
|
||||||
|
self.__populate_balance(brought_forward, rows)
|
||||||
|
if brought_forward is not None:
|
||||||
|
rows.insert(0, brought_forward)
|
||||||
|
rows.append(total)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def __get_brought_forward_row(self) -> IncomeExpensesRow | None:
|
||||||
|
"""Queries, composes and returns the brought-forward row.
|
||||||
|
|
||||||
|
:return: The brought-forward row, or None if the income-expenses starts
|
||||||
|
from the beginning.
|
||||||
|
"""
|
||||||
|
if self.period.start is None:
|
||||||
|
return None
|
||||||
|
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||||
|
(JournalEntry.is_debit, JournalEntry.amount),
|
||||||
|
else_=-JournalEntry.amount))
|
||||||
|
select: sa.Select = sa.Select(balance_func).join(Transaction)\
|
||||||
|
.filter(JournalEntry.currency_code == self.currency.code,
|
||||||
|
JournalEntry.account_id == self.account.id,
|
||||||
|
Transaction.date < self.period.start)
|
||||||
|
balance: int | None = db.session.scalar(select)
|
||||||
|
if balance is None:
|
||||||
|
return None
|
||||||
|
row: IncomeExpensesRow = IncomeExpensesRow()
|
||||||
|
row.date = self.period.start
|
||||||
|
row.summary = gettext("Brought forward")
|
||||||
|
if balance > 0:
|
||||||
|
row.income = balance
|
||||||
|
elif balance < 0:
|
||||||
|
row.expense = -balance
|
||||||
|
row.balance = balance
|
||||||
|
return row
|
||||||
|
|
||||||
|
def __query_entries(self) -> list[JournalEntry]:
|
||||||
|
"""Queries and returns the journal entries.
|
||||||
|
|
||||||
|
:return: The journal entries.
|
||||||
|
"""
|
||||||
|
conditions1: list[sa.BinaryExpression] \
|
||||||
|
= [JournalEntry.currency_code == self.currency.code,
|
||||||
|
JournalEntry.account_id == self.account.id]
|
||||||
|
if self.period.start is not None:
|
||||||
|
conditions1.append(Transaction.date >= self.period.start)
|
||||||
|
if self.period.end is not None:
|
||||||
|
conditions1.append(Transaction.date <= self.period.end)
|
||||||
|
select1: sa.Select = sa.Select(Transaction.id).join(JournalEntry)\
|
||||||
|
.filter(*conditions1)
|
||||||
|
|
||||||
|
conditions: list[sa.BinaryExpression] \
|
||||||
|
= [JournalEntry.transaction_id.in_(select1),
|
||||||
|
JournalEntry.currency_code == self.currency.code,
|
||||||
|
JournalEntry.account_id != self.account.id]
|
||||||
|
return JournalEntry.query.join(Transaction).filter(*conditions)\
|
||||||
|
.order_by(Transaction.date,
|
||||||
|
sa.desc(JournalEntry.is_debit),
|
||||||
|
JournalEntry.no)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __get_total_row(brought_forward: IncomeExpensesRow | None,
|
||||||
|
rows: list[IncomeExpensesRow]) -> IncomeExpensesRow:
|
||||||
|
"""Composes the total row.
|
||||||
|
|
||||||
|
:param brought_forward: The brought-forward row.
|
||||||
|
:param rows: The rows.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
row: IncomeExpensesRow = IncomeExpensesRow()
|
||||||
|
row.is_total = True
|
||||||
|
row.summary = gettext("Total")
|
||||||
|
row.income = sum([x.income for x in rows if x.income is not None])
|
||||||
|
row.expense = sum([x.expense for x in rows if x.expense is not None])
|
||||||
|
row.balance = row.income - row.expense
|
||||||
|
if brought_forward is not None:
|
||||||
|
row.balance = brought_forward.balance + row.balance
|
||||||
|
return row
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __populate_balance(brought_forward: IncomeExpensesRow | None,
|
||||||
|
rows: list[IncomeExpensesRow]) -> None:
|
||||||
|
"""Populates the balance of the rows.
|
||||||
|
|
||||||
|
:param brought_forward: The brought-forward row.
|
||||||
|
:param rows: The rows.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
balance: Decimal = 0 if brought_forward is None \
|
||||||
|
else brought_forward.balance
|
||||||
|
for row in rows:
|
||||||
|
if row.income is not None:
|
||||||
|
balance = balance + row.income
|
||||||
|
if row.expense is not None:
|
||||||
|
balance = balance - row.expense
|
||||||
|
row.balance = balance
|
||||||
|
|
||||||
|
def populate_rows(self, rows: list[IncomeExpensesRow]) -> None:
|
||||||
|
transactions: dict[int, Transaction] \
|
||||||
|
= {x.id: x for x in Transaction.query.filter(
|
||||||
|
Transaction.id.in_({x.entry.transaction_id for x in rows
|
||||||
|
if x.entry is not None}))}
|
||||||
|
accounts: dict[int, Account] \
|
||||||
|
= {x.id: x for x in Account.query.filter(
|
||||||
|
Account.id.in_({x.entry.account_id for x in rows
|
||||||
|
if x.entry is not None}))}
|
||||||
|
for row in rows:
|
||||||
|
if row.entry is not None:
|
||||||
|
row.transaction = transactions[row.entry.transaction_id]
|
||||||
|
row.date = row.transaction.date
|
||||||
|
row.note = row.transaction.note
|
||||||
|
row.account = accounts[row.entry.account_id]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def csv_field_names(self) -> list[str]:
|
||||||
|
return ["Date", "Account", "Summary", "Income", "Expense", "Balance",
|
||||||
|
"Note"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def csv_filename(self) -> str:
|
||||||
|
return "income-expenses-{currency}-{account}-{period}.csv".format(
|
||||||
|
currency=self.currency.code, account=self.account.code,
|
||||||
|
period=self.period.spec)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def period_chooser(self) -> PeriodChooser:
|
||||||
|
return IncomeExpensesPeriodChooser(self.currency, self.account)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def report_chooser(self) -> ReportChooser:
|
||||||
|
return ReportChooser(ReportType.INCOME_EXPENSES,
|
||||||
|
currency=self.currency,
|
||||||
|
account=self.account,
|
||||||
|
period=self.period)
|
||||||
|
|
||||||
|
def as_html_page(self) -> str:
|
||||||
|
pagination: Pagination = Pagination[IncomeExpensesRow](self.rows)
|
||||||
|
rows: list[IncomeExpensesRow] = pagination.list
|
||||||
|
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",
|
||||||
|
list=rows, pagination=pagination, report=self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def currency_options(self) -> list[OptionLink]:
|
||||||
|
"""Returns the currency options.
|
||||||
|
|
||||||
|
:return: The currency options.
|
||||||
|
"""
|
||||||
|
def get_url(currency: Currency):
|
||||||
|
if self.period.is_default:
|
||||||
|
return url_for("accounting.report.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: set[int] = set(db.session.scalars(
|
||||||
|
sa.select(JournalEntry.account_id)
|
||||||
|
.filter(JournalEntry.currency_code == self.currency.code)
|
||||||
|
.group_by(JournalEntry.account_id)).all())
|
||||||
|
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()]
|
||||||
|
@ -22,7 +22,7 @@ from flask import Blueprint, request, Response
|
|||||||
from accounting.models import Currency, Account
|
from accounting.models import Currency, Account
|
||||||
from accounting.utils.permission import has_permission, can_view
|
from accounting.utils.permission import has_permission, can_view
|
||||||
from .period import Period
|
from .period import Period
|
||||||
from .reports import Journal, Ledger
|
from .reports import Journal, Ledger, IncomeExpenses
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("report", __name__)
|
bp: Blueprint = Blueprint("report", __name__)
|
||||||
"""The view blueprint for the reports."""
|
"""The view blueprint for the reports."""
|
||||||
@ -103,3 +103,48 @@ def __get_ledger_list(currency: Currency, account: Account, period: 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.as_csv_download()
|
||||||
return report.as_html_page()
|
return report.as_html_page()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("income-expenses/<currency:currency>/<account:account>",
|
||||||
|
endpoint="income-expenses-default")
|
||||||
|
@has_permission(can_view)
|
||||||
|
def get_default_income_expenses_list(currency: Currency, account: Account) \
|
||||||
|
-> str | Response:
|
||||||
|
"""Returns the income and expenses in the default period.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
:return: The income and expenses in the default period.
|
||||||
|
"""
|
||||||
|
return __get_income_expenses_list(currency, account, Period.get_instance())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get(
|
||||||
|
"income-expenses/<currency:currency>/<account:account>/<period:period>",
|
||||||
|
endpoint="income-expenses")
|
||||||
|
@has_permission(can_view)
|
||||||
|
def get_income_expenses_list(currency: Currency, account: Account,
|
||||||
|
period: Period) -> str | Response:
|
||||||
|
"""Returns the income and expenses.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
:param period: The period.
|
||||||
|
:return: The income and expenses in the period.
|
||||||
|
"""
|
||||||
|
return __get_income_expenses_list(currency, account, period)
|
||||||
|
|
||||||
|
|
||||||
|
def __get_income_expenses_list(currency: Currency, account: Account,
|
||||||
|
period: Period) -> str | Response:
|
||||||
|
"""Returns the income and expenses.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
:param period: The period.
|
||||||
|
:return: The income and expenses in the period.
|
||||||
|
"""
|
||||||
|
report: IncomeExpenses = IncomeExpenses(currency, account, period)
|
||||||
|
if "as" in request.args and request.args["as"] == "csv":
|
||||||
|
return report.as_csv_download()
|
||||||
|
return report.as_html_page()
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
income-expenses-mobile-row.html: The row in the income and expenses for the mobile devices
|
||||||
|
|
||||||
|
Copyright (c) 2023 imacat.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
|
First written: 2023/3/5
|
||||||
|
#}
|
||||||
|
<div>
|
||||||
|
{% if item.date is not none or item.account is not none %}
|
||||||
|
<div class="text-muted small">
|
||||||
|
{% if item.date is not none %}
|
||||||
|
{{ item.date|accounting_format_date }}
|
||||||
|
{% endif %}
|
||||||
|
{% if item.account is not none %}
|
||||||
|
{{ item.account.title|title }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if item.summary is not none %}
|
||||||
|
<div>{{ item.summary }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% if item.income is not none %}
|
||||||
|
<span class="badge rounded-pill bg-success">+{{ item.income|accounting_format_amount }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if item.expense is not none %}
|
||||||
|
<span class="badge rounded-pill bg-warning">-{{ item.expense|accounting_format_amount }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="badge rounded-pill bg-primary">{{ item.balance|accounting_format_amount }}</span>
|
||||||
|
</div>
|
211
src/accounting/templates/accounting/report/income-expenses.html
Normal file
211
src/accounting/templates/accounting/report/income-expenses.html
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
income-expenses.html: The income and expenses
|
||||||
|
|
||||||
|
Copyright (c) 2023 imacat.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
|
First written: 2023/3/5
|
||||||
|
#}
|
||||||
|
{% extends "accounting/base.html" %}
|
||||||
|
|
||||||
|
{% block accounting_scripts %}
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}{% block title %}{{ _("Income and Expenses of %(account)s in %(currency)s %(period)s", currency=report.currency, account=report.account|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="btn-group mb-2 d-none d-md-inline-flex">
|
||||||
|
{% if accounting_can_edit() %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
{{ A_("New") }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Expense") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Income") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
|
||||||
|
{{ A_("Transfer") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
{{ report.currency.name|title }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title|title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-clipboard"></i>
|
||||||
|
{{ report.account.title|title }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for account in report.account_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
|
||||||
|
{{ account.title|title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ report.period.desc|title }}
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
|
||||||
|
<i class="fa-solid fa-download"></i>
|
||||||
|
{{ A_("Download") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with txn_types = report.txn_types %}
|
||||||
|
{% include "accounting/include/add-txn-material-fab.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="btn-group btn-actions mb-3 d-md-none">
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title|title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for account in report.account_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
|
||||||
|
{{ account.title|title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ A_("Period") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with period = report.period, period_chooser = report.period_chooser %}
|
||||||
|
{% include "accounting/report/include/period-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% if list %}
|
||||||
|
{% include "accounting/include/pagination.html" %}
|
||||||
|
|
||||||
|
<table class="table table-striped table-hover d-none d-md-table accounting-ledger-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{{ A_("Date") }}</th>
|
||||||
|
<th scope="col">{{ A_("Account") }}</th>
|
||||||
|
<th scope="col">{{ A_("Summary") }}</th>
|
||||||
|
<th class="accounting-amount" scope="col">{{ A_("Income") }}</th>
|
||||||
|
<th class="accounting-amount" scope="col">{{ A_("Expense") }}</th>
|
||||||
|
<th class="accounting-amount" scope="col">{{ A_("Balance") }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in list %}
|
||||||
|
<tr {% if item.transaction is not none %} class="accounting-clickable accounting-table-row-link" data-href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}" {% endif %}>
|
||||||
|
<td>{{ item.date|accounting_format_date }}</td>
|
||||||
|
<td>{{ item.account.title|title }}</td>
|
||||||
|
<td>{{ "" if item.summary is none else item.summary }}</td>
|
||||||
|
<td class="accounting-amount">{{ "" if item.income is none else item.income|accounting_format_amount }}</td>
|
||||||
|
<td class="accounting-amount">{{ "" if item.expense is none else item.expense|accounting_format_amount }}</td>
|
||||||
|
<td class="accounting-amount">{{ item.balance|accounting_format_amount }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{% if report.total_row is not none %}
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3">{{ A_("Total") }}</td>
|
||||||
|
<td class="accounting-amount">{{ report.total_row.income|accounting_format_amount }}</td>
|
||||||
|
<td class="accounting-amount">{{ report.total_row.expense|accounting_format_amount }}</td>
|
||||||
|
<td class="accounting-amount">{{ report.total_row.balance|accounting_format_amount }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="list-group d-md-none">
|
||||||
|
{% for item in list %}
|
||||||
|
{% if item.transaction is not none %}
|
||||||
|
<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">
|
||||||
|
{% include "accounting/report/include/income-expenses-mobile-row.html" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if report.total_row is not none %}
|
||||||
|
{% with item = report.total_row %}
|
||||||
|
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||||
|
{% include "accounting/report/include/income-expenses-mobile-row.html" %}
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ A_("There is no data.") }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user