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

@ -25,7 +25,7 @@ from flask import render_template, Response
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, BaseAccount, Account, Voucher, \
JournalEntry
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
@ -124,13 +124,13 @@ class AccountCollector:
sub_conditions: list[sa.BinaryExpression] \
= [Account.base_code.startswith(x) for x in {"1", "2", "3"}]
conditions: list[sa.BinaryExpression] \
= [JournalEntry.currency_code == self.__currency.code,
= [VoucherLineItem.currency_code == self.__currency.code,
sa.or_(*sub_conditions)]
if self.__period.end is not None:
conditions.append(Voucher.date <= self.__period.end)
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntry.is_debit, JournalEntry.amount),
else_=-JournalEntry.amount)).label("balance")
(VoucherLineItem.is_debit, VoucherLineItem.amount),
else_=-VoucherLineItem.amount)).label("balance")
select_balance: sa.Select \
= sa.select(Account.id, Account.base_code, Account.no,
balance_func)\
@ -178,7 +178,7 @@ class AccountCollector:
if self.__period.start is None:
return None
conditions: list[sa.BinaryExpression] \
= [JournalEntry.currency_code == self.__currency.code,
= [VoucherLineItem.currency_code == self.__currency.code,
Voucher.date < self.__period.start]
return self.__query_balance(conditions)
@ -197,7 +197,7 @@ class AccountCollector:
:return: The net income or loss for current period.
"""
conditions: list[sa.BinaryExpression] \
= [JournalEntry.currency_code == self.__currency.code]
= [VoucherLineItem.currency_code == self.__currency.code]
if self.__period.start is not None:
conditions.append(Voucher.date >= self.__period.start)
if self.__period.end is not None:
@ -215,8 +215,8 @@ class AccountCollector:
conditions.extend([sa.not_(Account.base_code.startswith(x))
for x in {"1", "2", "3"}])
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_balance: sa.Select = sa.select(balance_func)\
.join(Voucher).join(Account).filter(*conditions)
return db.session.scalar(select_balance)

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)

View File

@ -25,7 +25,7 @@ from flask import render_template, Response
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, BaseAccount, Account, Voucher, \
JournalEntry
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
@ -256,15 +256,15 @@ class IncomeStatement(BaseReport):
sub_conditions: list[sa.BinaryExpression] \
= [Account.base_code.startswith(str(x)) for x in range(4, 10)]
conditions: list[sa.BinaryExpression] \
= [JournalEntry.currency_code == self.__currency.code,
= [VoucherLineItem.currency_code == self.__currency.code,
sa.or_(*sub_conditions)]
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)
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntry.is_debit, -JournalEntry.amount),
else_=JournalEntry.amount)).label("balance")
(VoucherLineItem.is_debit, -VoucherLineItem.amount),
else_=VoucherLineItem.amount)).label("balance")
select_balances: sa.Select = sa.select(Account.id, balance_func)\
.join(Voucher).join(Account)\
.filter(*conditions)\

View File

@ -25,7 +25,7 @@ from flask import render_template, Response
from sqlalchemy.orm import selectinload
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
@ -37,29 +37,29 @@ from accounting.report.utils.urls import journal_url
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):
"""Constructs the entry in the report.
def __init__(self, line_item: VoucherLineItem):
"""Constructs the line item in the report.
:param entry: The journal entry.
:param line_item: The voucher line item.
"""
self.entry: JournalEntry = entry
"""The journal entry."""
self.voucher: Voucher = entry.voucher
self.line_item: VoucherLineItem = line_item
"""The voucher line item."""
self.voucher: Voucher = line_item.voucher
"""The voucher."""
self.currency: Currency = entry.currency
self.currency: Currency = line_item.currency
"""The account."""
self.account: Account = entry.account
self.account: Account = line_item.account
"""The account."""
self.summary: str | None = entry.summary
self.summary: str | None = line_item.summary
"""The summary."""
self.debit: Decimal | None = entry.debit
self.debit: Decimal | None = line_item.debit
"""The debit amount."""
self.credit: Decimal | None = entry.credit
self.credit: Decimal | None = line_item.credit
"""The credit amount."""
self.amount: Decimal = entry.amount
self.amount: Decimal = line_item.amount
"""The amount."""
@ -110,19 +110,19 @@ class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, period: Period,
pagination: Pagination[JournalEntry],
entries: list[JournalEntry]):
pagination: Pagination[VoucherLineItem],
line_items: list[VoucherLineItem]):
"""Constructs the HTML page parameters.
:param period: The period.
:param entries: The journal entries.
:param line_items: The line items.
"""
self.period: Period = period
"""The period."""
self.pagination: Pagination[JournalEntry] = pagination
self.pagination: Pagination[VoucherLineItem] = pagination
"""The pagination."""
self.entries: list[JournalEntry] = entries
"""The entries."""
self.line_items: list[VoucherLineItem] = line_items
"""The line items."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: journal_url(x))
"""The period chooser."""
@ -133,7 +133,7 @@ class PageParams(BasePageParams):
:return: True if there is any data, or False otherwise.
"""
return len(self.entries) > 0
return len(self.line_items) > 0
@property
def report_chooser(self) -> ReportChooser:
@ -145,10 +145,10 @@ class PageParams(BasePageParams):
period=self.period)
def get_csv_rows(entries: list[JournalEntry]) -> list[CSVRow]:
"""Composes and returns the CSV rows from the report entries.
def get_csv_rows(line_items: list[VoucherLineItem]) -> list[CSVRow]:
"""Composes and returns the CSV rows from the line items.
:param entries: The report entries.
:param line_items: The line items.
:return: The CSV rows.
"""
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
@ -158,7 +158,7 @@ def get_csv_rows(entries: list[JournalEntry]) -> list[CSVRow]:
rows.extend([CSVRow(x.voucher.date, x.currency.code,
str(x.account).title(), x.summary,
x.debit, x.credit, x.voucher.note)
for x in entries])
for x in line_items])
return rows
@ -172,28 +172,28 @@ class Journal(BaseReport):
"""
self.__period: Period = period
"""The period."""
self.__entries: list[JournalEntry] = self.__query_entries()
"""The journal entries."""
self.__line_items: list[VoucherLineItem] = self.__query_line_items()
"""The line items."""
def __query_entries(self) -> list[JournalEntry]:
"""Queries and returns the journal entries.
def __query_line_items(self) -> list[VoucherLineItem]:
"""Queries and returns the line items.
:return: The journal entries.
:return: The line items.
"""
conditions: list[sa.BinaryExpression] = []
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)
return JournalEntry.query.join(Voucher)\
return VoucherLineItem.query.join(Voucher)\
.filter(*conditions)\
.order_by(Voucher.date,
Voucher.no,
JournalEntry.is_debit.desc(),
JournalEntry.no)\
.options(selectinload(JournalEntry.account),
selectinload(JournalEntry.currency),
selectinload(JournalEntry.voucher)).all()
VoucherLineItem.is_debit.desc(),
VoucherLineItem.no)\
.options(selectinload(VoucherLineItem.account),
selectinload(VoucherLineItem.currency),
selectinload(VoucherLineItem.voucher)).all()
def csv(self) -> Response:
"""Returns the report as CSV for download.
@ -201,17 +201,17 @@ class Journal(BaseReport):
:return: The response of the report for download.
"""
filename: str = f"journal-{period_spec(self.__period)}.csv"
return csv_download(filename, get_csv_rows(self.__entries))
return csv_download(filename, get_csv_rows(self.__line_items))
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
pagination: Pagination[JournalEntry] \
= Pagination[JournalEntry](self.__entries, is_reversed=True)
pagination: Pagination[VoucherLineItem] \
= Pagination[VoucherLineItem](self.__line_items, is_reversed=True)
params: PageParams = PageParams(period=self.__period,
pagination=pagination,
entries=pagination.list)
line_items=pagination.list)
return render_template("accounting/report/journal.html",
report=params)

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
@ -40,18 +40,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.summary: str | None = None
@ -65,22 +65,22 @@ 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.summary = entry.summary
self.debit = entry.amount if entry.is_debit else None
self.credit = None if entry.is_debit else entry.amount
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.summary = line_item.summary
self.debit = line_item.amount if line_item.is_debit else None
self.credit = None if line_item.is_debit else line_item.amount
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: Account, period: Period):
"""Constructs the report entry collector.
"""Constructs the line item collector.
:param currency: The currency.
:param account: The account.
@ -92,89 +92,90 @@ class EntryCollector:
"""The account."""
self.__period: Period = period
"""The period"""
self.brought_forward: ReportEntry | None
"""The brought-forward entry."""
self.entries: list[ReportEntry]
"""The report 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 report starts from
the beginning.
:return: The brought-forward line item, or None if the report starts
from the beginning.
"""
if self.__period.start is None:
return None
if self.__account.is_nominal:
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)\
.filter(be(JournalEntry.currency_code == self.__currency.code),
be(JournalEntry.account_id == self.__account.id),
.filter(be(VoucherLineItem.currency_code == self.__currency.code),
be(VoucherLineItem.account_id == self.__account.id),
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.summary = gettext("Brought forward")
line_item: ReportLineItem = ReportLineItem()
line_item.is_brought_forward = True
line_item.date = self.__period.start
line_item.summary = gettext("Brought forward")
if balance > 0:
entry.debit = balance
line_item.debit = balance
elif balance < 0:
entry.credit = -balance
entry.balance = balance
return entry
line_item.credit = -balance
line_item.balance = balance
return line_item
def __query_entries(self) -> list[ReportEntry]:
"""Queries and returns the report entries.
def __query_line_items(self) -> list[ReportLineItem]:
"""Queries and returns the line items.
:return: The report entries.
:return: The line items.
"""
conditions: list[sa.BinaryExpression] \
= [JournalEntry.currency_code == self.__currency.code,
JournalEntry.account_id == self.__account.id]
= [VoucherLineItem.currency_code == self.__currency.code,
VoucherLineItem.account_id == self.__account.id]
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)
return [ReportEntry(x) for x in JournalEntry.query.join(Voucher)
return [ReportLineItem(x) for x in VoucherLineItem.query.join(Voucher)
.filter(*conditions)
.order_by(Voucher.date,
Voucher.no,
JournalEntry.is_debit.desc(),
JournalEntry.no)
.options(selectinload(JournalEntry.voucher)).all()]
VoucherLineItem.is_debit.desc(),
VoucherLineItem.no)
.options(selectinload(VoucherLineItem.voucher)).all()]
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.debit = sum([x.debit for x in self.entries
if x.debit is not None])
entry.credit = sum([x.credit for x in self.entries
if x.credit is not None])
entry.balance = entry.debit - entry.credit
line_item: ReportLineItem = ReportLineItem()
line_item.is_total = True
line_item.summary = gettext("Total")
line_item.debit = sum([x.debit for x in self.line_items
if x.debit is not None])
line_item.credit = sum([x.credit for x in self.line_items
if x.credit is not None])
line_item.balance = line_item.debit - line_item.credit
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.
"""
@ -182,12 +183,12 @@ class EntryCollector:
return None
balance: Decimal = 0 if self.brought_forward is None \
else self.brought_forward.balance
for entry in self.entries:
if entry.debit is not None:
balance = balance + entry.debit
if entry.credit is not None:
balance = balance - entry.credit
entry.balance = balance
for line_item in self.line_items:
if line_item.debit is not None:
balance = balance + line_item.debit
if line_item.credit is not None:
balance = balance - line_item.credit
line_item.balance = balance
class CSVRow(BaseCSVRow):
@ -238,19 +239,19 @@ class PageParams(BasePageParams):
account: Account,
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 report 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."""
@ -260,14 +261,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 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: ledger_url(currency, account, x))
"""The period chooser."""
@ -306,9 +307,9 @@ class PageParams(BasePageParams):
:return: The account options.
"""
in_use: sa.Select = sa.Select(JournalEntry.account_id)\
.filter(be(JournalEntry.currency_code == self.currency.code))\
.group_by(JournalEntry.account_id)
in_use: sa.Select = sa.Select(VoucherLineItem.account_id)\
.filter(be(VoucherLineItem.currency_code == self.currency.code))\
.group_by(VoucherLineItem.account_id)
return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
x.id == self.account.id)
for x in Account.query.filter(Account.id.in_(in_use))
@ -331,14 +332,15 @@ class Ledger(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.
@ -367,7 +369,7 @@ class Ledger(BaseReport):
None))
rows.extend([CSVRow(x.date, x.summary,
x.debit, x.credit, 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,
self.__total.debit, self.__total.credit,
@ -379,31 +381,31 @@ class Ledger(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/ledger.html",
report=params)

View File

@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
from accounting.locale import gettext
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
Voucher, JournalEntry
Voucher, VoucherLineItem
from accounting.report.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport
from accounting.report.utils.csv_export import csv_download
@ -38,18 +38,18 @@ from accounting.utils.query import parse_query_keywords
from .journal import get_csv_rows
class EntryCollector:
"""The report entry collector."""
class LineItemCollector:
"""The line item collector."""
def __init__(self):
"""Constructs the report entry collector."""
self.entries: list[JournalEntry] = self.__query_entries()
"""The report entries."""
"""Constructs the line item collector."""
self.line_items: list[VoucherLineItem] = self.__query_line_items()
"""The line items."""
def __query_entries(self) -> list[JournalEntry]:
"""Queries and returns the journal entries.
def __query_line_items(self) -> list[VoucherLineItem]:
"""Queries and returns the line items.
:return: The journal entries.
:return: The line items.
"""
keywords: list[str] = parse_query_keywords(request.args.get("q"))
if len(keywords) == 0:
@ -57,26 +57,26 @@ class EntryCollector:
conditions: list[sa.BinaryExpression] = []
for k in keywords:
sub_conditions: list[sa.BinaryExpression] \
= [JournalEntry.summary.contains(k),
JournalEntry.account_id.in_(
= [VoucherLineItem.summary.contains(k),
VoucherLineItem.account_id.in_(
self.__get_account_condition(k)),
JournalEntry.currency_code.in_(
VoucherLineItem.currency_code.in_(
self.__get_currency_condition(k)),
JournalEntry.voucher_id.in_(
VoucherLineItem.voucher_id.in_(
self.__get_voucher_condition(k))]
try:
sub_conditions.append(JournalEntry.amount == Decimal(k))
sub_conditions.append(VoucherLineItem.amount == Decimal(k))
except ArithmeticError:
pass
conditions.append(sa.or_(*sub_conditions))
return JournalEntry.query.join(Voucher).filter(*conditions)\
return VoucherLineItem.query.join(Voucher).filter(*conditions)\
.order_by(Voucher.date,
Voucher.no,
JournalEntry.is_debit,
JournalEntry.no)\
.options(selectinload(JournalEntry.account),
selectinload(JournalEntry.currency),
selectinload(JournalEntry.voucher)).all()
VoucherLineItem.is_debit,
VoucherLineItem.no)\
.options(selectinload(VoucherLineItem.account),
selectinload(VoucherLineItem.currency),
selectinload(VoucherLineItem.voucher)).all()
@staticmethod
def __get_account_condition(k: str) -> sa.Select:
@ -149,16 +149,16 @@ class EntryCollector:
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, pagination: Pagination[JournalEntry],
entries: list[JournalEntry]):
def __init__(self, pagination: Pagination[VoucherLineItem],
line_items: list[VoucherLineItem]):
"""Constructs the HTML page parameters.
:param entries: The search result entries.
:param line_items: The search result line items.
"""
self.pagination: Pagination[JournalEntry] = pagination
self.pagination: Pagination[VoucherLineItem] = pagination
"""The pagination."""
self.entries: list[JournalEntry] = entries
"""The entries."""
self.line_items: list[VoucherLineItem] = line_items
"""The line items."""
@property
def has_data(self) -> bool:
@ -166,7 +166,7 @@ class PageParams(BasePageParams):
:return: True if there is any data, or False otherwise.
"""
return len(self.entries) > 0
return len(self.line_items) > 0
@property
def report_chooser(self) -> ReportChooser:
@ -182,8 +182,9 @@ class Search(BaseReport):
def __init__(self):
"""Constructs a search."""
self.__entries: list[JournalEntry] = EntryCollector().entries
"""The journal entries."""
self.__line_items: list[VoucherLineItem] \
= LineItemCollector().line_items
"""The line items."""
def csv(self) -> Response:
"""Returns the report as CSV for download.
@ -191,16 +192,16 @@ class Search(BaseReport):
:return: The response of the report for download.
"""
filename: str = "search-{q}.csv".format(q=request.args["q"])
return csv_download(filename, get_csv_rows(self.__entries))
return csv_download(filename, get_csv_rows(self.__line_items))
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
pagination: Pagination[JournalEntry] \
= Pagination[JournalEntry](self.__entries, is_reversed=True)
pagination: Pagination[VoucherLineItem] \
= Pagination[VoucherLineItem](self.__line_items, is_reversed=True)
params: PageParams = PageParams(pagination=pagination,
entries=pagination.list)
line_items=pagination.list)
return render_template("accounting/report/search.html",
report=params)

View File

@ -24,7 +24,7 @@ from flask import Response, render_template
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
@ -178,14 +178,14 @@ class TrialBalance(BaseReport):
:return: None.
"""
conditions: list[sa.BinaryExpression] \
= [JournalEntry.currency_code == self.__currency.code]
= [VoucherLineItem.currency_code == self.__currency.code]
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)
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntry.is_debit, JournalEntry.amount),
else_=-JournalEntry.amount)).label("balance")
(VoucherLineItem.is_debit, VoucherLineItem.amount),
else_=-VoucherLineItem.amount)).label("balance")
select_balances: sa.Select = sa.select(Account.id, balance_func)\
.join(Voucher).join(Account)\
.filter(*conditions)\