Renamed "journal entry" to "voucher line item", and "entry type" to "side".

This commit is contained in:
2023-03-19 21:00:11 +08:00
parent 25c45b16ae
commit c1235608d8
58 changed files with 1961 additions and 1926 deletions

View File

@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, Account, Voucher, JournalEntry
from accounting.models import Currency, Account, Voucher, VoucherLineItem
from accounting.report.period import Period, PeriodChooser
from accounting.report.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport
@ -41,18 +41,18 @@ from accounting.utils.cast import be
from accounting.utils.pagination import Pagination
class ReportEntry:
"""An entry in the report."""
class ReportLineItem:
"""A line item in the report."""
def __init__(self, entry: JournalEntry | None = None):
"""Constructs the entry in the report.
def __init__(self, line_item: VoucherLineItem | None = None):
"""Constructs the line item in the report.
:param entry: The journal entry.
:param line_item: The voucher line item.
"""
self.is_brought_forward: bool = False
"""Whether this is the brought-forward entry."""
"""Whether this is the brought-forward line item."""
self.is_total: bool = False
"""Whether this is the total entry."""
"""Whether this is the total line item."""
self.date: date | None = None
"""The date."""
self.account: Account | None = None
@ -68,24 +68,24 @@ class ReportEntry:
self.note: str | None = None
"""The note."""
self.url: str | None = None
"""The URL to the journal entry."""
if entry is not None:
self.date = entry.voucher.date
self.account = entry.account
self.summary = entry.summary
self.income = None if entry.is_debit else entry.amount
self.expense = entry.amount if entry.is_debit else None
self.note = entry.voucher.note
"""The URL to the voucher line item."""
if line_item is not None:
self.date = line_item.voucher.date
self.account = line_item.account
self.summary = line_item.summary
self.income = None if line_item.is_debit else line_item.amount
self.expense = line_item.amount if line_item.is_debit else None
self.note = line_item.voucher.note
self.url = url_for("accounting.voucher.detail",
voucher=entry.voucher)
voucher=line_item.voucher)
class EntryCollector:
"""The report entry collector."""
class LineItemCollector:
"""The line item collector."""
def __init__(self, currency: Currency, account: IncomeExpensesAccount,
period: Period):
"""Constructs the report entry collector.
"""Constructs the line item collector.
:param currency: The currency.
:param account: The account.
@ -97,74 +97,74 @@ class EntryCollector:
"""The account."""
self.__period: Period = period
"""The period"""
self.brought_forward: ReportEntry | None
"""The brought-forward entry."""
self.entries: list[ReportEntry]
"""The log entries."""
self.total: ReportEntry | None
"""The total entry."""
self.brought_forward = self.__get_brought_forward_entry()
self.entries = self.__query_entries()
self.total = self.__get_total_entry()
self.brought_forward: ReportLineItem | None
"""The brought-forward line item."""
self.line_items: list[ReportLineItem]
"""The line items."""
self.total: ReportLineItem | None
"""The total line item."""
self.brought_forward = self.__get_brought_forward()
self.line_items = self.__query_line_items()
self.total = self.__get_total()
self.__populate_balance()
def __get_brought_forward_entry(self) -> ReportEntry | None:
"""Queries, composes and returns the brought-forward entry.
def __get_brought_forward(self) -> ReportLineItem | None:
"""Queries, composes and returns the brought-forward line item.
:return: The brought-forward entry, or None if the period starts from
the beginning.
:return: The brought-forward line item, or None if the period 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))
(VoucherLineItem.is_debit, VoucherLineItem.amount),
else_=-VoucherLineItem.amount))
select: sa.Select = sa.Select(balance_func)\
.join(Voucher).join(Account)\
.filter(be(JournalEntry.currency_code == self.__currency.code),
.filter(be(VoucherLineItem.currency_code == self.__currency.code),
self.__account_condition,
Voucher.date < self.__period.start)
balance: int | None = db.session.scalar(select)
if balance is None:
return None
entry: ReportEntry = ReportEntry()
entry.is_brought_forward = True
entry.date = self.__period.start
entry.account = Account.accumulated_change()
entry.summary = gettext("Brought forward")
line_item: ReportLineItem = ReportLineItem()
line_item.is_brought_forward = True
line_item.date = self.__period.start
line_item.account = Account.accumulated_change()
line_item.summary = gettext("Brought forward")
if balance > 0:
entry.income = balance
line_item.income = balance
elif balance < 0:
entry.expense = -balance
entry.balance = balance
return entry
line_item.expense = -balance
line_item.balance = balance
return line_item
def __query_entries(self) -> list[ReportEntry]:
"""Queries and returns the log entries.
def __query_line_items(self) -> list[ReportLineItem]:
"""Queries and returns the line items.
:return: The log entries.
:return: The line items.
"""
conditions: list[sa.BinaryExpression] \
= [JournalEntry.currency_code == self.__currency.code,
= [VoucherLineItem.currency_code == self.__currency.code,
self.__account_condition]
if self.__period.start is not None:
conditions.append(Voucher.date >= self.__period.start)
if self.__period.end is not None:
conditions.append(Voucher.date <= self.__period.end)
voucher_with_account: sa.Select = sa.Select(Voucher.id).\
join(JournalEntry).join(Account).filter(*conditions)
join(VoucherLineItem).join(Account).filter(*conditions)
return [ReportEntry(x)
for x in JournalEntry.query.join(Voucher).join(Account)
.filter(JournalEntry.voucher_id.in_(voucher_with_account),
JournalEntry.currency_code == self.__currency.code,
return [ReportLineItem(x)
for x in VoucherLineItem.query.join(Voucher).join(Account)
.filter(VoucherLineItem.voucher_id.in_(voucher_with_account),
VoucherLineItem.currency_code == self.__currency.code,
sa.not_(self.__account_condition))
.order_by(Voucher.date,
Voucher.no,
JournalEntry.is_debit,
JournalEntry.no)
.options(selectinload(JournalEntry.account),
selectinload(JournalEntry.voucher))]
VoucherLineItem.is_debit,
VoucherLineItem.no)
.options(selectinload(VoucherLineItem.account),
selectinload(VoucherLineItem.voucher))]
@property
def __account_condition(self) -> sa.BinaryExpression:
@ -175,38 +175,39 @@ class EntryCollector:
Account.base_code.startswith("22"))
return Account.id == self.__account.id
def __get_total_entry(self) -> ReportEntry | None:
"""Composes the total entry.
def __get_total(self) -> ReportLineItem | None:
"""Composes the total line item.
:return: The total entry, or None if there is no data.
:return: The total line item, or None if there is no data.
"""
if self.brought_forward is None and len(self.entries) == 0:
if self.brought_forward is None and len(self.line_items) == 0:
return None
entry: ReportEntry = ReportEntry()
entry.is_total = True
entry.summary = gettext("Total")
entry.income = sum([x.income for x in self.entries
if x.income is not None])
entry.expense = sum([x.expense for x in self.entries
if x.expense is not None])
entry.balance = entry.income - entry.expense
line_item: ReportLineItem = ReportLineItem()
line_item.is_total = True
line_item.summary = gettext("Total")
line_item.income = sum([x.income for x in self.line_items
if x.income is not None])
line_item.expense = sum([x.expense for x in self.line_items
if x.expense is not None])
line_item.balance = line_item.income - line_item.expense
if self.brought_forward is not None:
entry.balance = self.brought_forward.balance + entry.balance
return entry
line_item.balance \
= self.brought_forward.balance + line_item.balance
return line_item
def __populate_balance(self) -> None:
"""Populates the balance of the entries.
"""Populates the balance of the line items.
:return: None.
"""
balance: Decimal = 0 if self.brought_forward is None \
else self.brought_forward.balance
for entry in self.entries:
if entry.income is not None:
balance = balance + entry.income
if entry.expense is not None:
balance = balance - entry.expense
entry.balance = balance
for line_item in self.line_items:
if line_item.income is not None:
balance = balance + line_item.income
if line_item.expense is not None:
balance = balance - line_item.expense
line_item.balance = balance
class CSVRow(BaseCSVRow):
@ -261,19 +262,19 @@ class PageParams(BasePageParams):
account: IncomeExpensesAccount,
period: Period,
has_data: bool,
pagination: Pagination[ReportEntry],
brought_forward: ReportEntry | None,
entries: list[ReportEntry],
total: ReportEntry | None):
pagination: Pagination[ReportLineItem],
brought_forward: ReportLineItem | None,
line_items: list[ReportLineItem],
total: ReportLineItem | None):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param account: The account.
:param period: The period.
:param has_data: True if there is any data, or False otherwise.
:param brought_forward: The brought-forward entry.
:param entries: The log entries.
:param total: The total entry.
:param brought_forward: The brought-forward line item.
:param line_items: The line items.
:param total: The total line item.
"""
self.currency: Currency = currency
"""The currency."""
@ -283,14 +284,14 @@ class PageParams(BasePageParams):
"""The period."""
self.__has_data: bool = has_data
"""True if there is any data, or False otherwise."""
self.pagination: Pagination[ReportEntry] = pagination
self.pagination: Pagination[ReportLineItem] = pagination
"""The pagination."""
self.brought_forward: ReportEntry | None = brought_forward
"""The brought-forward entry."""
self.entries: list[ReportEntry] = entries
"""The report entries."""
self.total: ReportEntry | None = total
"""The total entry."""
self.brought_forward: ReportLineItem | None = brought_forward
"""The brought-forward line item."""
self.line_items: list[ReportLineItem] = line_items
"""The line items."""
self.total: ReportLineItem | None = total
"""The total line item."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: income_expenses_url(currency, account, x))
"""The period chooser."""
@ -342,14 +343,14 @@ class PageParams(BasePageParams):
income_expenses_url(self.currency, current_al,
self.period),
self.account.id == 0)]
in_use: sa.Select = sa.Select(JournalEntry.account_id)\
in_use: sa.Select = sa.Select(VoucherLineItem.account_id)\
.join(Account)\
.filter(be(JournalEntry.currency_code == self.currency.code),
.filter(be(VoucherLineItem.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)
.group_by(VoucherLineItem.account_id)
options.extend([OptionLink(str(x),
income_expenses_url(
self.currency,
@ -378,14 +379,15 @@ class IncomeExpenses(BaseReport):
"""The account."""
self.__period: Period = period
"""The period."""
collector: EntryCollector = EntryCollector(
collector: LineItemCollector = LineItemCollector(
self.__currency, self.__account, self.__period)
self.__brought_forward: ReportEntry | None = collector.brought_forward
"""The brought-forward entry."""
self.__entries: list[ReportEntry] = collector.entries
"""The report entries."""
self.__total: ReportEntry | None = collector.total
"""The total entry."""
self.__brought_forward: ReportLineItem | None \
= collector.brought_forward
"""The brought-forward line item."""
self.__line_items: list[ReportLineItem] = collector.line_items
"""The line items."""
self.__total: ReportLineItem | None = collector.total
"""The total line item."""
def csv(self) -> Response:
"""Returns the report as CSV for download.
@ -416,7 +418,7 @@ class IncomeExpenses(BaseReport):
None))
rows.extend([CSVRow(x.date, str(x.account).title(), x.summary,
x.income, x.expense, x.balance, x.note)
for x in self.__entries])
for x in self.__line_items])
if self.__total is not None:
rows.append(CSVRow(gettext("Total"), None, None,
self.__total.income, self.__total.expense,
@ -428,31 +430,31 @@ class IncomeExpenses(BaseReport):
:return: The report as HTML.
"""
all_entries: list[ReportEntry] = []
all_line_items: list[ReportLineItem] = []
if self.__brought_forward is not None:
all_entries.append(self.__brought_forward)
all_entries.extend(self.__entries)
all_line_items.append(self.__brought_forward)
all_line_items.extend(self.__line_items)
if self.__total is not None:
all_entries.append(self.__total)
pagination: Pagination[ReportEntry] \
= Pagination[ReportEntry](all_entries, is_reversed=True)
page_entries: list[ReportEntry] = pagination.list
has_data: bool = len(page_entries) > 0
brought_forward: ReportEntry | None = None
if len(page_entries) > 0 and page_entries[0].is_brought_forward:
brought_forward = page_entries[0]
page_entries = page_entries[1:]
total: ReportEntry | None = None
if len(page_entries) > 0 and page_entries[-1].is_total:
total = page_entries[-1]
page_entries = page_entries[:-1]
all_line_items.append(self.__total)
pagination: Pagination[ReportLineItem] \
= Pagination[ReportLineItem](all_line_items, is_reversed=True)
page_line_items: list[ReportLineItem] = pagination.list
has_data: bool = len(page_line_items) > 0
brought_forward: ReportLineItem | None = None
if len(page_line_items) > 0 and page_line_items[0].is_brought_forward:
brought_forward = page_line_items[0]
page_line_items = page_line_items[1:]
total: ReportLineItem | None = None
if len(page_line_items) > 0 and page_line_items[-1].is_total:
total = page_line_items[-1]
page_line_items = page_line_items[:-1]
params: PageParams = PageParams(currency=self.__currency,
account=self.__account,
period=self.__period,
has_data=has_data,
pagination=pagination,
brought_forward=brought_forward,
entries=page_entries,
line_items=page_line_items,
total=total)
return render_template("accounting/report/income-expenses.html",
report=params)