Added the income and expenses.
This commit is contained in:
src/accounting
report
templates
accounting
report
@ -139,3 +139,26 @@ class LedgerPeriodChooser(PeriodChooser):
|
||||
return url_for("accounting.report.ledger",
|
||||
currency=self.currency, account=self.account,
|
||||
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."""
|
||||
self.__reports.append(self.__journal)
|
||||
self.__reports.append(self.__ledger)
|
||||
self.__reports.append(self.__income_expenses)
|
||||
for report in self.__reports:
|
||||
if report.is_active:
|
||||
self.current_report = report.title
|
||||
@ -97,6 +98,21 @@ class ReportChooser:
|
||||
return OptionLink(gettext("Ledger"), url,
|
||||
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]:
|
||||
"""Returns the iteration of the reports.
|
||||
|
||||
|
@ -117,3 +117,55 @@ class LedgerRow(ReportRow):
|
||||
"Credit": self.credit,
|
||||
"Balance": self.balance,
|
||||
"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."""
|
||||
LEDGER: str = "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 .period import Period
|
||||
from .period_choosers import PeriodChooser, JournalPeriodChooser, \
|
||||
LedgerPeriodChooser
|
||||
LedgerPeriodChooser, IncomeExpensesPeriodChooser
|
||||
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
|
||||
|
||||
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)
|
||||
for x in Account.query.filter(Account.id.in_(in_use))
|
||||
.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.utils.permission import has_permission, can_view
|
||||
from .period import Period
|
||||
from .reports import Journal, Ledger
|
||||
from .reports import Journal, Ledger, IncomeExpenses
|
||||
|
||||
bp: Blueprint = Blueprint("report", __name__)
|
||||
"""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":
|
||||
return report.as_csv_download()
|
||||
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()
|
||||
|
Reference in New Issue
Block a user