Renamed "voucher line item" to "journal entry line item".

This commit is contained in:
依瑪貓 2023-03-20 20:52:35 +08:00
parent e26af6f3fc
commit 8f909965a9
34 changed files with 336 additions and 303 deletions

View File

@ -100,11 +100,11 @@ def init_accounts_command(username: str) -> None:
def __is_need_offset(base_code: str) -> bool: def __is_need_offset(base_code: str) -> bool:
"""Checks that whether voucher line items in the account need offset. """Checks that whether journal entry line items in the account need offset.
:param base_code: The code of the base account. :param base_code: The code of the base account.
:return: True if voucher line items in the account need offset, or False :return: True if journal entry line items in the account need offset, or
otherwise. False otherwise.
""" """
# Assets # Assets
if base_code[0] == "1": if base_code[0] == "1":

View File

@ -82,7 +82,7 @@ class AccountForm(FlaskForm):
"""The title.""" """The title."""
is_need_offset = BooleanField( is_need_offset = BooleanField(
validators=[NoOffsetNominalAccount()]) validators=[NoOffsetNominalAccount()])
"""Whether the the voucher line items of this account need offset.""" """Whether the the journal entry line items of this account need offset."""
def populate_obj(self, obj: Account) -> None: def populate_obj(self, obj: Account) -> None:
"""Populates the form data into an account object. """Populates the form data into an account object.

View File

@ -115,7 +115,7 @@ class Account(db.Model):
title_l10n = db.Column("title", db.String, nullable=False) title_l10n = db.Column("title", db.String, nullable=False)
"""The title.""" """The title."""
is_need_offset = db.Column(db.Boolean, nullable=False, default=False) is_need_offset = db.Column(db.Boolean, nullable=False, default=False)
"""Whether the voucher line items of this account need offset.""" """Whether the journal entry line items of this account need offset."""
created_at = db.Column(db.DateTime(timezone=True), nullable=False, created_at = db.Column(db.DateTime(timezone=True), nullable=False,
server_default=db.func.now()) server_default=db.func.now())
"""The time of creation.""" """The time of creation."""
@ -139,8 +139,9 @@ class Account(db.Model):
l10n = db.relationship("AccountL10n", back_populates="account", l10n = db.relationship("AccountL10n", back_populates="account",
lazy=False) lazy=False)
"""The localized titles.""" """The localized titles."""
line_items = db.relationship("VoucherLineItem", back_populates="account") line_items = db.relationship("JournalEntryLineItem",
"""The voucher line items.""" back_populates="account")
"""The journal entry line items."""
CASH_CODE: str = "1111-001" CASH_CODE: str = "1111-001"
"""The code of the cash account,""" """The code of the cash account,"""
@ -363,8 +364,9 @@ class Currency(db.Model):
l10n = db.relationship("CurrencyL10n", back_populates="currency", l10n = db.relationship("CurrencyL10n", back_populates="currency",
lazy=False) lazy=False)
"""The localized names.""" """The localized names."""
line_items = db.relationship("VoucherLineItem", back_populates="currency") line_items = db.relationship("JournalEntryLineItem",
"""The voucher line items.""" back_populates="currency")
"""The journal entry line items."""
def __str__(self) -> str: def __str__(self) -> str:
"""Returns the string representation of the currency. """Returns the string representation of the currency.
@ -450,8 +452,8 @@ class CurrencyL10n(db.Model):
class VoucherCurrency: class VoucherCurrency:
"""A currency in a voucher.""" """A currency in a voucher."""
def __init__(self, code: str, debit: list[VoucherLineItem], def __init__(self, code: str, debit: list[JournalEntryLineItem],
credit: list[VoucherLineItem]): credit: list[JournalEntryLineItem]):
"""Constructs the currency in the voucher. """Constructs the currency in the voucher.
:param code: The currency code. :param code: The currency code.
@ -460,9 +462,9 @@ class VoucherCurrency:
""" """
self.code: str = code self.code: str = code
"""The currency code.""" """The currency code."""
self.debit: list[VoucherLineItem] = debit self.debit: list[JournalEntryLineItem] = debit
"""The debit line items.""" """The debit line items."""
self.credit: list[VoucherLineItem] = credit self.credit: list[JournalEntryLineItem] = credit
"""The credit line items.""" """The credit line items."""
@property @property
@ -523,7 +525,8 @@ class Voucher(db.Model):
"""The ID of the updator.""" """The ID of the updator."""
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id) updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
"""The updator.""" """The updator."""
line_items = db.relationship("VoucherLineItem", back_populates="voucher") line_items = db.relationship("JournalEntryLineItem",
back_populates="voucher")
"""The line items.""" """The line items."""
def __str__(self) -> str: def __str__(self) -> str:
@ -543,10 +546,10 @@ class Voucher(db.Model):
:return: The currency categories. :return: The currency categories.
""" """
line_items: list[VoucherLineItem] = sorted(self.line_items, line_items: list[JournalEntryLineItem] = sorted(self.line_items,
key=lambda x: x.no) key=lambda x: x.no)
codes: list[str] = [] codes: list[str] = []
by_currency: dict[str, list[VoucherLineItem]] = {} by_currency: dict[str, list[JournalEntryLineItem]] = {}
for line_item in line_items: for line_item in line_items:
if line_item.currency_code not in by_currency: if line_item.currency_code not in by_currency:
codes.append(line_item.currency_code) codes.append(line_item.currency_code)
@ -606,14 +609,14 @@ class Voucher(db.Model):
:return: None. :return: None.
""" """
VoucherLineItem.query\ JournalEntryLineItem.query\
.filter(VoucherLineItem.voucher_id == self.id).delete() .filter(JournalEntryLineItem.voucher_id == self.id).delete()
db.session.delete(self) db.session.delete(self)
class VoucherLineItem(db.Model): class JournalEntryLineItem(db.Model):
"""A line item in the voucher.""" """A line item in the journal entry."""
__tablename__ = "accounting_voucher_line_items" __tablename__ = "accounting_journal_entry_line_items"
"""The table name.""" """The table name."""
id = db.Column(db.Integer, nullable=False, primary_key=True, id = db.Column(db.Integer, nullable=False, primary_key=True,
autoincrement=False) autoincrement=False)
@ -633,11 +636,11 @@ class VoucherLineItem(db.Model):
db.ForeignKey(id, onupdate="CASCADE"), db.ForeignKey(id, onupdate="CASCADE"),
nullable=True) nullable=True)
"""The ID of the original line item.""" """The ID of the original line item."""
original_line_item = db.relationship("VoucherLineItem", original_line_item = db.relationship("JournalEntryLineItem",
back_populates="offsets", back_populates="offsets",
remote_side=id, passive_deletes=True) remote_side=id, passive_deletes=True)
"""The original line item.""" """The original line item."""
offsets = db.relationship("VoucherLineItem", offsets = db.relationship("JournalEntryLineItem",
back_populates="original_line_item") back_populates="original_line_item")
"""The offset items.""" """The offset items."""
currency_code = db.Column(db.String, currency_code = db.Column(db.String,

View File

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

View File

@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account, Voucher, VoucherLineItem from accounting.models import Currency, Account, Voucher, JournalEntryLineItem
from accounting.report.period import Period, PeriodChooser from accounting.report.period import Period, PeriodChooser
from accounting.report.utils.base_page_params import BasePageParams from accounting.report.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport from accounting.report.utils.base_report import BaseReport
@ -44,10 +44,10 @@ from accounting.utils.pagination import Pagination
class ReportLineItem: class ReportLineItem:
"""A line item in the report.""" """A line item in the report."""
def __init__(self, line_item: VoucherLineItem | None = None): def __init__(self, line_item: JournalEntryLineItem | None = None):
"""Constructs the line item in the report. """Constructs the line item in the report.
:param line_item: The voucher line item. :param line_item: The journal entry line item.
""" """
self.is_brought_forward: bool = False self.is_brought_forward: bool = False
"""Whether this is the brought-forward line item.""" """Whether this is the brought-forward line item."""
@ -68,7 +68,7 @@ class ReportLineItem:
self.note: str | None = None self.note: str | None = None
"""The note.""" """The note."""
self.url: str | None = None self.url: str | None = None
"""The URL to the voucher line item.""" """The URL to the journal entry line item."""
if line_item is not None: if line_item is not None:
self.date = line_item.voucher.date self.date = line_item.voucher.date
self.account = line_item.account self.account = line_item.account
@ -117,11 +117,12 @@ class LineItemCollector:
if self.__period.start is None: if self.__period.start is None:
return None return None
balance_func: sa.Function = sa.func.sum(sa.case( balance_func: sa.Function = sa.func.sum(sa.case(
(VoucherLineItem.is_debit, VoucherLineItem.amount), (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-VoucherLineItem.amount)) else_=-JournalEntryLineItem.amount))
select: sa.Select = sa.Select(balance_func)\ select: sa.Select = sa.Select(balance_func)\
.join(Voucher).join(Account)\ .join(Voucher).join(Account)\
.filter(be(VoucherLineItem.currency_code == self.__currency.code), .filter(be(JournalEntryLineItem.currency_code
== self.__currency.code),
self.__account_condition, self.__account_condition,
Voucher.date < self.__period.start) Voucher.date < self.__period.start)
balance: int | None = db.session.scalar(select) balance: int | None = db.session.scalar(select)
@ -145,26 +146,28 @@ class LineItemCollector:
:return: The line items. :return: The line items.
""" """
conditions: list[sa.BinaryExpression] \ conditions: list[sa.BinaryExpression] \
= [VoucherLineItem.currency_code == self.__currency.code, = [JournalEntryLineItem.currency_code == self.__currency.code,
self.__account_condition] self.__account_condition]
if self.__period.start is not None: if self.__period.start is not None:
conditions.append(Voucher.date >= self.__period.start) conditions.append(Voucher.date >= self.__period.start)
if self.__period.end is not None: if self.__period.end is not None:
conditions.append(Voucher.date <= self.__period.end) conditions.append(Voucher.date <= self.__period.end)
voucher_with_account: sa.Select = sa.Select(Voucher.id).\ voucher_with_account: sa.Select = sa.Select(Voucher.id).\
join(VoucherLineItem).join(Account).filter(*conditions) join(JournalEntryLineItem).join(Account).filter(*conditions)
return [ReportLineItem(x) return [ReportLineItem(x)
for x in VoucherLineItem.query.join(Voucher).join(Account) for x in JournalEntryLineItem.query.join(Voucher).join(Account)
.filter(VoucherLineItem.voucher_id.in_(voucher_with_account), .filter(JournalEntryLineItem.voucher_id
VoucherLineItem.currency_code == self.__currency.code, .in_(voucher_with_account),
JournalEntryLineItem.currency_code
== self.__currency.code,
sa.not_(self.__account_condition)) sa.not_(self.__account_condition))
.order_by(Voucher.date, .order_by(Voucher.date,
Voucher.no, Voucher.no,
VoucherLineItem.is_debit, JournalEntryLineItem.is_debit,
VoucherLineItem.no) JournalEntryLineItem.no)
.options(selectinload(VoucherLineItem.account), .options(selectinload(JournalEntryLineItem.account),
selectinload(VoucherLineItem.voucher))] selectinload(JournalEntryLineItem.voucher))]
@property @property
def __account_condition(self) -> sa.BinaryExpression: def __account_condition(self) -> sa.BinaryExpression:
@ -343,14 +346,15 @@ class PageParams(BasePageParams):
income_expenses_url(self.currency, current_al, income_expenses_url(self.currency, current_al,
self.period), self.period),
self.account.id == 0)] self.account.id == 0)]
in_use: sa.Select = sa.Select(VoucherLineItem.account_id)\ in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
.join(Account)\ .join(Account)\
.filter(be(VoucherLineItem.currency_code == self.currency.code), .filter(be(JournalEntryLineItem.currency_code
== self.currency.code),
sa.or_(Account.base_code.startswith("11"), sa.or_(Account.base_code.startswith("11"),
Account.base_code.startswith("12"), Account.base_code.startswith("12"),
Account.base_code.startswith("21"), Account.base_code.startswith("21"),
Account.base_code.startswith("22")))\ Account.base_code.startswith("22")))\
.group_by(VoucherLineItem.account_id) .group_by(JournalEntryLineItem.account_id)
options.extend([OptionLink(str(x), options.extend([OptionLink(str(x),
income_expenses_url( income_expenses_url(
self.currency, self.currency,

View File

@ -25,7 +25,7 @@ from flask import render_template, Response
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, BaseAccount, Account, Voucher, \ from accounting.models import Currency, BaseAccount, Account, Voucher, \
VoucherLineItem JournalEntryLineItem
from accounting.report.period import Period, PeriodChooser from accounting.report.period import Period, PeriodChooser
from accounting.report.utils.base_page_params import BasePageParams from accounting.report.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport from accounting.report.utils.base_report import BaseReport
@ -256,15 +256,15 @@ class IncomeStatement(BaseReport):
sub_conditions: list[sa.BinaryExpression] \ sub_conditions: list[sa.BinaryExpression] \
= [Account.base_code.startswith(str(x)) for x in range(4, 10)] = [Account.base_code.startswith(str(x)) for x in range(4, 10)]
conditions: list[sa.BinaryExpression] \ conditions: list[sa.BinaryExpression] \
= [VoucherLineItem.currency_code == self.__currency.code, = [JournalEntryLineItem.currency_code == self.__currency.code,
sa.or_(*sub_conditions)] sa.or_(*sub_conditions)]
if self.__period.start is not None: if self.__period.start is not None:
conditions.append(Voucher.date >= self.__period.start) conditions.append(Voucher.date >= self.__period.start)
if self.__period.end is not None: if self.__period.end is not None:
conditions.append(Voucher.date <= self.__period.end) conditions.append(Voucher.date <= self.__period.end)
balance_func: sa.Function = sa.func.sum(sa.case( balance_func: sa.Function = sa.func.sum(sa.case(
(VoucherLineItem.is_debit, -VoucherLineItem.amount), (JournalEntryLineItem.is_debit, -JournalEntryLineItem.amount),
else_=VoucherLineItem.amount)).label("balance") else_=JournalEntryLineItem.amount)).label("balance")
select_balances: sa.Select = sa.select(Account.id, balance_func)\ select_balances: sa.Select = sa.select(Account.id, balance_func)\
.join(Voucher).join(Account)\ .join(Voucher).join(Account)\
.filter(*conditions)\ .filter(*conditions)\

View File

@ -25,7 +25,7 @@ from flask import render_template, Response
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account, Voucher, VoucherLineItem from accounting.models import Currency, Account, Voucher, JournalEntryLineItem
from accounting.report.period import Period, PeriodChooser from accounting.report.period import Period, PeriodChooser
from accounting.report.utils.base_page_params import BasePageParams from accounting.report.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport from accounting.report.utils.base_report import BaseReport
@ -40,13 +40,13 @@ from accounting.utils.pagination import Pagination
class ReportLineItem: class ReportLineItem:
"""A line item in the report.""" """A line item in the report."""
def __init__(self, line_item: VoucherLineItem): def __init__(self, line_item: JournalEntryLineItem):
"""Constructs the line item in the report. """Constructs the line item in the report.
:param line_item: The voucher line item. :param line_item: The journal entry line item.
""" """
self.line_item: VoucherLineItem = line_item self.line_item: JournalEntryLineItem = line_item
"""The voucher line item.""" """The journal entry line item."""
self.voucher: Voucher = line_item.voucher self.voucher: Voucher = line_item.voucher
"""The voucher.""" """The voucher."""
self.currency: Currency = line_item.currency self.currency: Currency = line_item.currency
@ -110,8 +110,8 @@ class PageParams(BasePageParams):
"""The HTML page parameters.""" """The HTML page parameters."""
def __init__(self, period: Period, def __init__(self, period: Period,
pagination: Pagination[VoucherLineItem], pagination: Pagination[JournalEntryLineItem],
line_items: list[VoucherLineItem]): line_items: list[JournalEntryLineItem]):
"""Constructs the HTML page parameters. """Constructs the HTML page parameters.
:param period: The period. :param period: The period.
@ -119,9 +119,9 @@ class PageParams(BasePageParams):
""" """
self.period: Period = period self.period: Period = period
"""The period.""" """The period."""
self.pagination: Pagination[VoucherLineItem] = pagination self.pagination: Pagination[JournalEntryLineItem] = pagination
"""The pagination.""" """The pagination."""
self.line_items: list[VoucherLineItem] = line_items self.line_items: list[JournalEntryLineItem] = line_items
"""The line items.""" """The line items."""
self.period_chooser: PeriodChooser = PeriodChooser( self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: journal_url(x)) lambda x: journal_url(x))
@ -145,7 +145,7 @@ class PageParams(BasePageParams):
period=self.period) period=self.period)
def get_csv_rows(line_items: list[VoucherLineItem]) -> list[CSVRow]: def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:
"""Composes and returns the CSV rows from the line items. """Composes and returns the CSV rows from the line items.
:param line_items: The line items. :param line_items: The line items.
@ -172,10 +172,11 @@ class Journal(BaseReport):
""" """
self.__period: Period = period self.__period: Period = period
"""The period.""" """The period."""
self.__line_items: list[VoucherLineItem] = self.__query_line_items() self.__line_items: list[JournalEntryLineItem] \
= self.__query_line_items()
"""The line items.""" """The line items."""
def __query_line_items(self) -> list[VoucherLineItem]: def __query_line_items(self) -> list[JournalEntryLineItem]:
"""Queries and returns the line items. """Queries and returns the line items.
:return: The line items. :return: The line items.
@ -185,15 +186,15 @@ class Journal(BaseReport):
conditions.append(Voucher.date >= self.__period.start) conditions.append(Voucher.date >= self.__period.start)
if self.__period.end is not None: if self.__period.end is not None:
conditions.append(Voucher.date <= self.__period.end) conditions.append(Voucher.date <= self.__period.end)
return VoucherLineItem.query.join(Voucher)\ return JournalEntryLineItem.query.join(Voucher)\
.filter(*conditions)\ .filter(*conditions)\
.order_by(Voucher.date, .order_by(Voucher.date,
Voucher.no, Voucher.no,
VoucherLineItem.is_debit.desc(), JournalEntryLineItem.is_debit.desc(),
VoucherLineItem.no)\ JournalEntryLineItem.no)\
.options(selectinload(VoucherLineItem.account), .options(selectinload(JournalEntryLineItem.account),
selectinload(VoucherLineItem.currency), selectinload(JournalEntryLineItem.currency),
selectinload(VoucherLineItem.voucher)).all() selectinload(JournalEntryLineItem.voucher)).all()
def csv(self) -> Response: def csv(self) -> Response:
"""Returns the report as CSV for download. """Returns the report as CSV for download.
@ -208,8 +209,9 @@ class Journal(BaseReport):
:return: The report as HTML. :return: The report as HTML.
""" """
pagination: Pagination[VoucherLineItem] \ pagination: Pagination[JournalEntryLineItem] \
= Pagination[VoucherLineItem](self.__line_items, is_reversed=True) = Pagination[JournalEntryLineItem](self.__line_items,
is_reversed=True)
params: PageParams = PageParams(period=self.__period, params: PageParams = PageParams(period=self.__period,
pagination=pagination, pagination=pagination,
line_items=pagination.list) line_items=pagination.list)

View File

@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account, Voucher, VoucherLineItem from accounting.models import Currency, Account, Voucher, JournalEntryLineItem
from accounting.report.period import Period, PeriodChooser from accounting.report.period import Period, PeriodChooser
from accounting.report.utils.base_page_params import BasePageParams from accounting.report.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport from accounting.report.utils.base_report import BaseReport
@ -43,10 +43,10 @@ from accounting.utils.pagination import Pagination
class ReportLineItem: class ReportLineItem:
"""A line item in the report.""" """A line item in the report."""
def __init__(self, line_item: VoucherLineItem | None = None): def __init__(self, line_item: JournalEntryLineItem | None = None):
"""Constructs the line item in the report. """Constructs the line item in the report.
:param line_item: The voucher line item. :param line_item: The journal entry line item.
""" """
self.is_brought_forward: bool = False self.is_brought_forward: bool = False
"""Whether this is the brought-forward line item.""" """Whether this is the brought-forward line item."""
@ -65,7 +65,7 @@ class ReportLineItem:
self.note: str | None = None self.note: str | None = None
"""The note.""" """The note."""
self.url: str | None = None self.url: str | None = None
"""The URL to the voucher line item.""" """The URL to the journal entry line item."""
if line_item is not None: if line_item is not None:
self.date = line_item.voucher.date self.date = line_item.voucher.date
self.description = line_item.description self.description = line_item.description
@ -114,11 +114,13 @@ class LineItemCollector:
if self.__account.is_nominal: if self.__account.is_nominal:
return None return None
balance_func: sa.Function = sa.func.sum(sa.case( balance_func: sa.Function = sa.func.sum(sa.case(
(VoucherLineItem.is_debit, VoucherLineItem.amount), (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-VoucherLineItem.amount)) else_=-JournalEntryLineItem.amount))
select: sa.Select = sa.Select(balance_func).join(Voucher)\ select: sa.Select = sa.Select(balance_func).join(Voucher)\
.filter(be(VoucherLineItem.currency_code == self.__currency.code), .filter(be(JournalEntryLineItem.currency_code
be(VoucherLineItem.account_id == self.__account.id), == self.__currency.code),
be(JournalEntryLineItem.account_id
== self.__account.id),
Voucher.date < self.__period.start) Voucher.date < self.__period.start)
balance: int | None = db.session.scalar(select) balance: int | None = db.session.scalar(select)
if balance is None: if balance is None:
@ -140,19 +142,20 @@ class LineItemCollector:
:return: The line items. :return: The line items.
""" """
conditions: list[sa.BinaryExpression] \ conditions: list[sa.BinaryExpression] \
= [VoucherLineItem.currency_code == self.__currency.code, = [JournalEntryLineItem.currency_code == self.__currency.code,
VoucherLineItem.account_id == self.__account.id] JournalEntryLineItem.account_id == self.__account.id]
if self.__period.start is not None: if self.__period.start is not None:
conditions.append(Voucher.date >= self.__period.start) conditions.append(Voucher.date >= self.__period.start)
if self.__period.end is not None: if self.__period.end is not None:
conditions.append(Voucher.date <= self.__period.end) conditions.append(Voucher.date <= self.__period.end)
return [ReportLineItem(x) for x in VoucherLineItem.query.join(Voucher) return [ReportLineItem(x) for x in JournalEntryLineItem.query
.join(Voucher)
.filter(*conditions) .filter(*conditions)
.order_by(Voucher.date, .order_by(Voucher.date,
Voucher.no, Voucher.no,
VoucherLineItem.is_debit.desc(), JournalEntryLineItem.is_debit.desc(),
VoucherLineItem.no) JournalEntryLineItem.no)
.options(selectinload(VoucherLineItem.voucher)).all()] .options(selectinload(JournalEntryLineItem.voucher)).all()]
def __get_total(self) -> ReportLineItem | None: def __get_total(self) -> ReportLineItem | None:
"""Composes the total line item. """Composes the total line item.
@ -307,9 +310,10 @@ class PageParams(BasePageParams):
:return: The account options. :return: The account options.
""" """
in_use: sa.Select = sa.Select(VoucherLineItem.account_id)\ in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
.filter(be(VoucherLineItem.currency_code == self.currency.code))\ .filter(be(JournalEntryLineItem.currency_code
.group_by(VoucherLineItem.account_id) == self.currency.code))\
.group_by(JournalEntryLineItem.account_id)
return [OptionLink(str(x), ledger_url(self.currency, x, self.period), return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
x.id == self.account.id) 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))

View File

@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \ from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
Voucher, VoucherLineItem Voucher, JournalEntryLineItem
from accounting.report.utils.base_page_params import BasePageParams from accounting.report.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport from accounting.report.utils.base_report import BaseReport
from accounting.report.utils.csv_export import csv_download from accounting.report.utils.csv_export import csv_download
@ -43,10 +43,10 @@ class LineItemCollector:
def __init__(self): def __init__(self):
"""Constructs the line item collector.""" """Constructs the line item collector."""
self.line_items: list[VoucherLineItem] = self.__query_line_items() self.line_items: list[JournalEntryLineItem] = self.__query_line_items()
"""The line items.""" """The line items."""
def __query_line_items(self) -> list[VoucherLineItem]: def __query_line_items(self) -> list[JournalEntryLineItem]:
"""Queries and returns the line items. """Queries and returns the line items.
:return: The line items. :return: The line items.
@ -57,26 +57,27 @@ class LineItemCollector:
conditions: list[sa.BinaryExpression] = [] conditions: list[sa.BinaryExpression] = []
for k in keywords: for k in keywords:
sub_conditions: list[sa.BinaryExpression] \ sub_conditions: list[sa.BinaryExpression] \
= [VoucherLineItem.description.contains(k), = [JournalEntryLineItem.description.contains(k),
VoucherLineItem.account_id.in_( JournalEntryLineItem.account_id.in_(
self.__get_account_condition(k)), self.__get_account_condition(k)),
VoucherLineItem.currency_code.in_( JournalEntryLineItem.currency_code.in_(
self.__get_currency_condition(k)), self.__get_currency_condition(k)),
VoucherLineItem.voucher_id.in_( JournalEntryLineItem.voucher_id.in_(
self.__get_voucher_condition(k))] self.__get_voucher_condition(k))]
try: try:
sub_conditions.append(VoucherLineItem.amount == Decimal(k)) sub_conditions.append(
JournalEntryLineItem.amount == Decimal(k))
except ArithmeticError: except ArithmeticError:
pass pass
conditions.append(sa.or_(*sub_conditions)) conditions.append(sa.or_(*sub_conditions))
return VoucherLineItem.query.join(Voucher).filter(*conditions)\ return JournalEntryLineItem.query.join(Voucher).filter(*conditions)\
.order_by(Voucher.date, .order_by(Voucher.date,
Voucher.no, Voucher.no,
VoucherLineItem.is_debit, JournalEntryLineItem.is_debit,
VoucherLineItem.no)\ JournalEntryLineItem.no)\
.options(selectinload(VoucherLineItem.account), .options(selectinload(JournalEntryLineItem.account),
selectinload(VoucherLineItem.currency), selectinload(JournalEntryLineItem.currency),
selectinload(VoucherLineItem.voucher)).all() selectinload(JournalEntryLineItem.voucher)).all()
@staticmethod @staticmethod
def __get_account_condition(k: str) -> sa.Select: def __get_account_condition(k: str) -> sa.Select:
@ -149,15 +150,15 @@ class LineItemCollector:
class PageParams(BasePageParams): class PageParams(BasePageParams):
"""The HTML page parameters.""" """The HTML page parameters."""
def __init__(self, pagination: Pagination[VoucherLineItem], def __init__(self, pagination: Pagination[JournalEntryLineItem],
line_items: list[VoucherLineItem]): line_items: list[JournalEntryLineItem]):
"""Constructs the HTML page parameters. """Constructs the HTML page parameters.
:param line_items: The search result line items. :param line_items: The search result line items.
""" """
self.pagination: Pagination[VoucherLineItem] = pagination self.pagination: Pagination[JournalEntryLineItem] = pagination
"""The pagination.""" """The pagination."""
self.line_items: list[VoucherLineItem] = line_items self.line_items: list[JournalEntryLineItem] = line_items
"""The line items.""" """The line items."""
@property @property
@ -182,7 +183,7 @@ class Search(BaseReport):
def __init__(self): def __init__(self):
"""Constructs a search.""" """Constructs a search."""
self.__line_items: list[VoucherLineItem] \ self.__line_items: list[JournalEntryLineItem] \
= LineItemCollector().line_items = LineItemCollector().line_items
"""The line items.""" """The line items."""
@ -199,8 +200,9 @@ class Search(BaseReport):
:return: The report as HTML. :return: The report as HTML.
""" """
pagination: Pagination[VoucherLineItem] \ pagination: Pagination[JournalEntryLineItem] \
= Pagination[VoucherLineItem](self.__line_items, is_reversed=True) = Pagination[JournalEntryLineItem](self.__line_items,
is_reversed=True)
params: PageParams = PageParams(pagination=pagination, params: PageParams = PageParams(pagination=pagination,
line_items=pagination.list) line_items=pagination.list)
return render_template("accounting/report/search.html", return render_template("accounting/report/search.html",

View File

@ -24,7 +24,7 @@ from flask import Response, render_template
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account, Voucher, VoucherLineItem from accounting.models import Currency, Account, Voucher, JournalEntryLineItem
from accounting.report.period import Period, PeriodChooser from accounting.report.period import Period, PeriodChooser
from accounting.report.utils.base_page_params import BasePageParams from accounting.report.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport from accounting.report.utils.base_report import BaseReport
@ -178,14 +178,14 @@ class TrialBalance(BaseReport):
:return: None. :return: None.
""" """
conditions: list[sa.BinaryExpression] \ conditions: list[sa.BinaryExpression] \
= [VoucherLineItem.currency_code == self.__currency.code] = [JournalEntryLineItem.currency_code == self.__currency.code]
if self.__period.start is not None: if self.__period.start is not None:
conditions.append(Voucher.date >= self.__period.start) conditions.append(Voucher.date >= self.__period.start)
if self.__period.end is not None: if self.__period.end is not None:
conditions.append(Voucher.date <= self.__period.end) conditions.append(Voucher.date <= self.__period.end)
balance_func: sa.Function = sa.func.sum(sa.case( balance_func: sa.Function = sa.func.sum(sa.case(
(VoucherLineItem.is_debit, VoucherLineItem.amount), (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-VoucherLineItem.amount)).label("balance") else_=-JournalEntryLineItem.amount)).label("balance")
select_balances: sa.Select = sa.select(Account.id, balance_func)\ select_balances: sa.Select = sa.select(Account.id, balance_func)\
.join(Voucher).join(Account)\ .join(Voucher).join(Account)\
.filter(*conditions)\ .filter(*conditions)\

View File

@ -26,7 +26,7 @@ import sqlalchemy as sa
from flask import request from flask import request
from accounting import db from accounting import db
from accounting.models import Currency, VoucherLineItem from accounting.models import Currency, JournalEntryLineItem
from accounting.utils.voucher_types import VoucherType from accounting.utils.voucher_types import VoucherType
from .option_link import OptionLink from .option_link import OptionLink
from .report_chooser import ReportChooser from .report_chooser import ReportChooser
@ -81,8 +81,8 @@ class BasePageParams(ABC):
:return: The currency options. :return: The currency options.
""" """
in_use: set[str] = set(db.session.scalars( in_use: set[str] = set(db.session.scalars(
sa.select(VoucherLineItem.currency_code) sa.select(JournalEntryLineItem.currency_code)
.group_by(VoucherLineItem.currency_code)).all()) .group_by(JournalEntryLineItem.currency_code)).all())
return [OptionLink(str(x), get_url(x), x.code == active_currency.code) return [OptionLink(str(x), get_url(x), x.code == active_currency.code)
for x in Currency.query.filter(Currency.code.in_(in_use)) for x in Currency.query.filter(Currency.code.in_(in_use))
.order_by(Currency.code).all()] .order_by(Currency.code).all()]

View File

@ -166,14 +166,14 @@
.accounting-list-group-hover .list-group-item:hover { .accounting-list-group-hover .list-group-item:hover {
background-color: #ececec; background-color: #ececec;
} }
.accounting-voucher-line-item { .accounting-journal-entry-line-item {
border: none; border: none;
} }
.accounting-voucher-line-item-header { .accounting-journal-entry-line-item-header {
font-weight: bolder; font-weight: bolder;
border-bottom: thick double slategray; border-bottom: thick double slategray;
} }
.list-group-item.accounting-voucher-line-item-total { .list-group-item.accounting-journal-entry-line-item-total {
font-weight: bolder; font-weight: bolder;
border-top: thick double slategray; border-top: thick double slategray;
} }

View File

@ -30,7 +30,7 @@ class AccountSelector {
/** /**
* The line item editor * The line item editor
* @type {VoucherLineItemEditor} * @type {JournalEntryLineItemEditor}
*/ */
#lineItemEditor; #lineItemEditor;
@ -85,7 +85,7 @@ class AccountSelector {
/** /**
* Constructs an account selector. * Constructs an account selector.
* *
* @param lineItemEditor {VoucherLineItemEditor} the line item editor * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
* @param debitCredit {string} either "debit" or "credit" * @param debitCredit {string} either "debit" or "credit"
*/ */
constructor(lineItemEditor, debitCredit) { constructor(lineItemEditor, debitCredit) {
@ -210,7 +210,7 @@ class AccountSelector {
/** /**
* Returns the account selector instances. * Returns the account selector instances.
* *
* @param lineItemEditor {VoucherLineItemEditor} the line item editor * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
* @return {{debit: AccountSelector, credit: AccountSelector}} * @return {{debit: AccountSelector, credit: AccountSelector}}
*/ */
static getInstances(lineItemEditor) { static getInstances(lineItemEditor) {

View File

@ -30,7 +30,7 @@ class DescriptionEditor {
/** /**
* The line item editor * The line item editor
* @type {VoucherLineItemEditor} * @type {JournalEntryLineItemEditor}
*/ */
#lineItemEditor; #lineItemEditor;
@ -109,7 +109,7 @@ class DescriptionEditor {
/** /**
* Constructs a description editor. * Constructs a description editor.
* *
* @param lineItemEditor {VoucherLineItemEditor} the line item editor * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
* @param debitCredit {string} either "debit" or "credit" * @param debitCredit {string} either "debit" or "credit"
*/ */
constructor(lineItemEditor, debitCredit) { constructor(lineItemEditor, debitCredit) {
@ -246,7 +246,7 @@ class DescriptionEditor {
/** /**
* Returns the description editor instances. * Returns the description editor instances.
* *
* @param lineItemEditor {VoucherLineItemEditor} the line item editor * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
* @return {{debit: DescriptionEditor, credit: DescriptionEditor}} * @return {{debit: DescriptionEditor, credit: DescriptionEditor}}
*/ */
static getInstances(lineItemEditor) { static getInstances(lineItemEditor) {

View File

@ -1,5 +1,5 @@
/* The Mia! Accounting Flask Project /* The Mia! Accounting Flask Project
* voucher-line-item-editor.js: The JavaScript for the voucher line item editor * journal-entry-line-item-editor.js: The JavaScript for the journal entry line item editor
*/ */
/* Copyright (c) 2023 imacat. /* Copyright (c) 2023 imacat.
@ -23,10 +23,10 @@
"use strict"; "use strict";
/** /**
* The voucher line item editor. * The journal entry line item editor.
* *
*/ */
class VoucherLineItemEditor { class JournalEntryLineItemEditor {
/** /**
* The voucher form * The voucher form
@ -35,7 +35,7 @@ class VoucherLineItemEditor {
form; form;
/** /**
* The voucher line item editor * The journal entry line item editor
* @type {HTMLFormElement} * @type {HTMLFormElement}
*/ */
#element; #element;
@ -137,7 +137,7 @@ class VoucherLineItemEditor {
#amountError; #amountError;
/** /**
* The voucher line item to edit * The journal entry line item to edit
* @type {LineItemSubForm|null} * @type {LineItemSubForm|null}
*/ */
lineItem; lineItem;
@ -149,7 +149,7 @@ class VoucherLineItemEditor {
#debitCreditSubForm; #debitCreditSubForm;
/** /**
* Whether the voucher line item needs offset * Whether the journal entry line item needs offset
* @type {boolean} * @type {boolean}
*/ */
isNeedOffset = false; isNeedOffset = false;
@ -215,7 +215,7 @@ class VoucherLineItemEditor {
originalLineItemSelector; originalLineItemSelector;
/** /**
* Constructs a new voucher line item editor. * Constructs a new journal entry line item editor.
* *
* @param form {VoucherForm} the voucher form * @param form {VoucherForm} the voucher form
*/ */
@ -476,7 +476,7 @@ class VoucherLineItemEditor {
} }
/** /**
* The callback when adding a new voucher line item. * The callback when adding a new journal entry line item.
* *
* @param debitCredit {DebitCreditSubForm} the debit or credit sub-form * @param debitCredit {DebitCreditSubForm} the debit or credit sub-form
*/ */
@ -512,9 +512,9 @@ class VoucherLineItemEditor {
} }
/** /**
* The callback when editing a voucher line item. * The callback when editing a journal entry line item.
* *
* @param lineItem {LineItemSubForm} the voucher line item sub-form * @param lineItem {LineItemSubForm} the journal entry line item sub-form
*/ */
onEdit(lineItem) { onEdit(lineItem) {
this.lineItem = lineItem; this.lineItem = lineItem;

View File

@ -30,7 +30,7 @@ class OriginalLineItemSelector {
/** /**
* The line item editor * The line item editor
* @type {VoucherLineItemEditor} * @type {JournalEntryLineItemEditor}
*/ */
lineItemEditor; lineItemEditor;
@ -84,7 +84,7 @@ class OriginalLineItemSelector {
/** /**
* Constructs an original line item selector. * Constructs an original line item selector.
* *
* @param lineItemEditor {VoucherLineItemEditor} the line item editor * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
*/ */
constructor(lineItemEditor) { constructor(lineItemEditor) {
this.lineItemEditor = lineItemEditor; this.lineItemEditor = lineItemEditor;

View File

@ -100,7 +100,7 @@ class VoucherForm {
/** /**
* The line item editor * The line item editor
* @type {VoucherLineItemEditor} * @type {JournalEntryLineItemEditor}
*/ */
lineItemEditor; lineItemEditor;
@ -121,7 +121,7 @@ class VoucherForm {
this.#addCurrencyButton = document.getElementById("accounting-add-currency"); this.#addCurrencyButton = document.getElementById("accounting-add-currency");
this.#note = document.getElementById("accounting-note"); this.#note = document.getElementById("accounting-note");
this.#noteError = document.getElementById("accounting-note-error"); this.#noteError = document.getElementById("accounting-note-error");
this.lineItemEditor = new VoucherLineItemEditor(this); this.lineItemEditor = new JournalEntryLineItemEditor(this);
this.#addCurrencyButton.onclick = () => { this.#addCurrencyButton.onclick = () => {
const newIndex = 1 + (this.#currencies.length === 0? 0: Math.max(...this.#currencies.map((currency) => currency.index))); const newIndex = 1 + (this.#currencies.length === 0? 0: Math.max(...this.#currencies.map((currency) => currency.index)));
@ -993,7 +993,7 @@ class LineItemSubForm {
/** /**
* Stores the data into the line item sub-form. * Stores the data into the line item sub-form.
* *
* @param editor {VoucherLineItemEditor} the line item editor * @param editor {JournalEntryLineItemEditor} the line item editor
*/ */
save(editor) { save(editor) {
if (editor.isNeedOffset) { if (editor.isNeedOffset) {

View File

@ -34,11 +34,11 @@ First written: 2023/2/26
<div class="mb-2 fw-bolder">{{ currency.name }}</div> <div class="mb-2 fw-bolder">{{ currency.name }}</div>
<ul class="list-group accounting-list-group-stripped accounting-list-group-hover"> <ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
<li class="list-group-item accounting-voucher-line-item accounting-voucher-line-item-header">{{ A_("Content") }}</li> <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Content") }}</li>
{% with line_items = currency.debit %} {% with line_items = currency.debit %}
{% include "accounting/voucher/include/detail-line-items.html" %} {% include "accounting/voucher/include/detail-line-items.html" %}
{% endwith %} {% endwith %}
<li class="list-group-item accounting-voucher-line-item accounting-voucher-line-item-total"> <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div>{{ A_("Total") }}</div> <div>{{ A_("Total") }}</div>
<div>{{ currency.debit_total|accounting_format_amount }}</div> <div>{{ currency.debit_total|accounting_format_amount }}</div>

View File

@ -21,7 +21,7 @@ First written: 2023/3/14
#} #}
{# <ul> For SonarQube not to complain about incorrect HTML #} {# <ul> For SonarQube not to complain about incorrect HTML #}
{% for line_item in line_items %} {% for line_item in line_items %}
<li class="list-group-item accounting-voucher-line-item"> <li class="list-group-item accounting-journal-entry-line-item">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div> <div>
<div class="small">{{ line_item.account }}</div> <div class="small">{{ line_item.account }}</div>

View File

@ -24,7 +24,7 @@ First written: 2023/2/26
{% block accounting_scripts %} {% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/voucher-form.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/voucher-form.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/voucher-line-item-editor.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/journal-entry-line-item-editor.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/account-selector.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/account-selector.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/original-line-item-selector.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/original-line-item-selector.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/description-editor.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/description-editor.js") }}"></script>
@ -88,7 +88,7 @@ First written: 2023/2/26
</div> </div>
</form> </form>
{% include "accounting/voucher/include/voucher-line-item-editor-modal.html" %} {% include "accounting/voucher/include/journal-entry-line-item-editor-modal.html" %}
{% block form_modals %}{% endblock %} {% block form_modals %}{% endblock %}
{% include "accounting/voucher/include/original-line-item-selector-modal.html" %} {% include "accounting/voucher/include/original-line-item-selector-modal.html" %}

View File

@ -1,6 +1,6 @@
{# {#
The Mia! Accounting Flask Project The Mia! Accounting Flask Project
voucher-line-item-editor-modal.html: The modal of the voucher line item editor journal-entry-line-item-editor-modal: The modal of the journal entry line item editor
Copyright (c) 2023 imacat. Copyright (c) 2023 imacat.

View File

@ -34,11 +34,11 @@ First written: 2023/2/26
<div class="mb-2 fw-bolder">{{ currency.name }}</div> <div class="mb-2 fw-bolder">{{ currency.name }}</div>
<ul class="list-group accounting-list-group-stripped accounting-list-group-hover"> <ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
<li class="list-group-item accounting-voucher-line-item accounting-voucher-line-item-header">{{ A_("Content") }}</li> <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Content") }}</li>
{% with line_items = currency.credit %} {% with line_items = currency.credit %}
{% include "accounting/voucher/include/detail-line-items.html" %} {% include "accounting/voucher/include/detail-line-items.html" %}
{% endwith %} {% endwith %}
<li class="list-group-item accounting-voucher-line-item accounting-voucher-line-item-total"> <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div>{{ A_("Total") }}</div> <div>{{ A_("Total") }}</div>
<div>{{ currency.debit_total|accounting_format_amount }}</div> <div>{{ currency.debit_total|accounting_format_amount }}</div>

View File

@ -30,11 +30,11 @@ First written: 2023/2/26
{# The debit line items #} {# The debit line items #}
<div class="col-sm-6 mb-2"> <div class="col-sm-6 mb-2">
<ul class="list-group accounting-list-group-stripped accounting-list-group-hover"> <ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
<li class="list-group-item accounting-voucher-line-item accounting-voucher-line-item-header">{{ A_("Debit") }}</li> <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Debit") }}</li>
{% with line_items = currency.debit %} {% with line_items = currency.debit %}
{% include "accounting/voucher/include/detail-line-items.html" %} {% include "accounting/voucher/include/detail-line-items.html" %}
{% endwith %} {% endwith %}
<li class="list-group-item accounting-voucher-line-item accounting-voucher-line-item-total"> <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div>{{ A_("Total") }}</div> <div>{{ A_("Total") }}</div>
<div>{{ currency.debit_total|accounting_format_amount }}</div> <div>{{ currency.debit_total|accounting_format_amount }}</div>
@ -46,11 +46,11 @@ First written: 2023/2/26
{# The credit line items #} {# The credit line items #}
<div class="col-sm-6 mb-2"> <div class="col-sm-6 mb-2">
<ul class="list-group accounting-list-group-stripped accounting-list-group-hover"> <ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
<li class="list-group-item accounting-voucher-line-item accounting-voucher-line-item-header">{{ A_("Credit") }}</li> <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Credit") }}</li>
{% with line_items = currency.credit %} {% with line_items = currency.credit %}
{% include "accounting/voucher/include/detail-line-items.html" %} {% include "accounting/voucher/include/detail-line-items.html" %}
{% endwith %} {% endwith %}
<li class="list-group-item accounting-voucher-line-item accounting-voucher-line-item-total"> <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div>{{ A_("Total") }}</div> <div>{{ A_("Total") }}</div>
<div>{{ currency.debit_total|accounting_format_amount }}</div> <div>{{ currency.debit_total|accounting_format_amount }}</div>

View File

@ -23,7 +23,7 @@ from flask import abort
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from werkzeug.routing import BaseConverter from werkzeug.routing import BaseConverter
from accounting.models import Voucher, VoucherLineItem from accounting.models import Voucher, JournalEntryLineItem
from accounting.utils.voucher_types import VoucherType from accounting.utils.voucher_types import VoucherType
@ -37,11 +37,11 @@ class VoucherConverter(BaseConverter):
:param value: The voucher ID. :param value: The voucher ID.
:return: The corresponding voucher. :return: The corresponding voucher.
""" """
voucher: Voucher | None = Voucher.query.join(VoucherLineItem)\ voucher: Voucher | None = Voucher.query.join(JournalEntryLineItem)\
.filter(Voucher.id == value)\ .filter(Voucher.id == value)\
.options(selectinload(Voucher.line_items) .options(selectinload(Voucher.line_items)
.selectinload(VoucherLineItem.offsets) .selectinload(JournalEntryLineItem.offsets)
.selectinload(VoucherLineItem.voucher))\ .selectinload(JournalEntryLineItem.voucher))\
.first() .first()
if voucher is None: if voucher is None:
abort(404) abort(404)

View File

@ -28,7 +28,7 @@ from wtforms.validators import DataRequired
from accounting import db from accounting import db
from accounting.locale import lazy_gettext from accounting.locale import lazy_gettext
from accounting.models import Currency, VoucherLineItem from accounting.models import Currency, JournalEntryLineItem
from accounting.voucher.utils.offset_alias import offset_alias from accounting.voucher.utils.offset_alias import offset_alias
from accounting.utils.cast import be from accounting.utils.cast import be
from accounting.utils.strip_text import strip_text from accounting.utils.strip_text import strip_text
@ -65,8 +65,8 @@ class SameCurrencyAsOriginalLineItems:
if len(original_line_item_id) == 0: if len(original_line_item_id) == 0:
return return
original_line_item_currency_codes: set[str] = set(db.session.scalars( original_line_item_currency_codes: set[str] = set(db.session.scalars(
sa.select(VoucherLineItem.currency_code) sa.select(JournalEntryLineItem.currency_code)
.filter(VoucherLineItem.id.in_(original_line_item_id))).all()) .filter(JournalEntryLineItem.id.in_(original_line_item_id))).all())
for currency_code in original_line_item_currency_codes: for currency_code in original_line_item_currency_codes:
if field.data != currency_code: if field.data != currency_code:
raise ValidationError(lazy_gettext( raise ValidationError(lazy_gettext(
@ -83,13 +83,16 @@ class KeepCurrencyWhenHavingOffset:
if field.data is None: if field.data is None:
return return
offset: sa.Alias = offset_alias() offset: sa.Alias = offset_alias()
original_line_items: list[VoucherLineItem] = VoucherLineItem.query\ original_line_items: list[JournalEntryLineItem]\
.join(offset, be(VoucherLineItem.id = JournalEntryLineItem.query\
.join(offset, be(JournalEntryLineItem.id
== offset.c.original_line_item_id), == offset.c.original_line_item_id),
isouter=True)\ isouter=True)\
.filter(VoucherLineItem.id.in_({x.eid.data for x in form.line_items .filter(JournalEntryLineItem.id
if x.eid.data is not None}))\ .in_({x.eid.data for x in form.line_items
.group_by(VoucherLineItem.id, VoucherLineItem.currency_code)\ if x.eid.data is not None}))\
.group_by(JournalEntryLineItem.id,
JournalEntryLineItem.currency_code)\
.having(sa.func.count(offset.c.id) > 0).all() .having(sa.func.count(offset.c.id) > 0).all()
for original_line_item in original_line_items: for original_line_item in original_line_items:
if original_line_item.currency_code != field.data: if original_line_item.currency_code != field.data:
@ -159,8 +162,9 @@ class CurrencyForm(FlaskForm):
return True return True
line_item_id: set[int] = {x.eid.data for x in line_item_forms line_item_id: set[int] = {x.eid.data for x in line_item_forms
if x.eid.data is not None} if x.eid.data is not None}
select: sa.Select = sa.select(sa.func.count(VoucherLineItem.id))\ select: sa.Select = sa.select(sa.func.count(JournalEntryLineItem.id))\
.filter(VoucherLineItem.original_line_item_id.in_(line_item_id)) .filter(JournalEntryLineItem.original_line_item_id
.in_(line_item_id))
return db.session.scalar(select) > 0 return db.session.scalar(select) > 0

View File

@ -30,7 +30,7 @@ from wtforms.validators import DataRequired, Optional
from accounting import db from accounting import db
from accounting.locale import lazy_gettext from accounting.locale import lazy_gettext
from accounting.models import Account, VoucherLineItem from accounting.models import Account, JournalEntryLineItem
from accounting.template_filters import format_amount from accounting.template_filters import format_amount
from accounting.utils.cast import be from accounting.utils.cast import be
from accounting.utils.random_id import new_id from accounting.utils.random_id import new_id
@ -48,7 +48,7 @@ class OriginalLineItemExists:
def __call__(self, form: FlaskForm, field: IntegerField) -> None: def __call__(self, form: FlaskForm, field: IntegerField) -> None:
if field.data is None: if field.data is None:
return return
if db.session.get(VoucherLineItem, field.data) is None: if db.session.get(JournalEntryLineItem, field.data) is None:
raise ValidationError(lazy_gettext( raise ValidationError(lazy_gettext(
"The original line item does not exist.")) "The original line item does not exist."))
@ -60,8 +60,8 @@ class OriginalLineItemOppositeDebitCredit:
def __call__(self, form: FlaskForm, field: IntegerField) -> None: def __call__(self, form: FlaskForm, field: IntegerField) -> None:
if field.data is None: if field.data is None:
return return
original_line_item: VoucherLineItem | None \ original_line_item: JournalEntryLineItem | None \
= db.session.get(VoucherLineItem, field.data) = db.session.get(JournalEntryLineItem, field.data)
if original_line_item is None: if original_line_item is None:
return return
if isinstance(form, CreditLineItemForm) \ if isinstance(form, CreditLineItemForm) \
@ -80,8 +80,8 @@ class OriginalLineItemNeedOffset:
def __call__(self, form: FlaskForm, field: IntegerField) -> None: def __call__(self, form: FlaskForm, field: IntegerField) -> None:
if field.data is None: if field.data is None:
return return
original_line_item: VoucherLineItem | None \ original_line_item: JournalEntryLineItem | None \
= db.session.get(VoucherLineItem, field.data) = db.session.get(JournalEntryLineItem, field.data)
if original_line_item is None: if original_line_item is None:
return return
if not original_line_item.account.is_need_offset: if not original_line_item.account.is_need_offset:
@ -96,8 +96,8 @@ class OriginalLineItemNotOffset:
def __call__(self, form: FlaskForm, field: IntegerField) -> None: def __call__(self, form: FlaskForm, field: IntegerField) -> None:
if field.data is None: if field.data is None:
return return
original_line_item: VoucherLineItem | None \ original_line_item: JournalEntryLineItem | None \
= db.session.get(VoucherLineItem, field.data) = db.session.get(JournalEntryLineItem, field.data)
if original_line_item is None: if original_line_item is None:
return return
if original_line_item.original_line_item_id is not None: if original_line_item.original_line_item_id is not None:
@ -152,8 +152,9 @@ class SameAccountAsOriginalLineItem:
assert isinstance(form, LineItemForm) assert isinstance(form, LineItemForm)
if field.data is None or form.original_line_item_id.data is None: if field.data is None or form.original_line_item_id.data is None:
return return
original_line_item: VoucherLineItem | None \ original_line_item: JournalEntryLineItem | None \
= db.session.get(VoucherLineItem, form.original_line_item_id.data) = db.session.get(JournalEntryLineItem,
form.original_line_item_id.data)
if original_line_item is None: if original_line_item is None:
return return
if field.data != original_line_item.account_code: if field.data != original_line_item.account_code:
@ -168,9 +169,10 @@ class KeepAccountWhenHavingOffset:
assert isinstance(form, LineItemForm) assert isinstance(form, LineItemForm)
if field.data is None or form.eid.data is None: if field.data is None or form.eid.data is None:
return return
line_item: VoucherLineItem | None = db.session.query(VoucherLineItem)\ line_item: JournalEntryLineItem | None = db.session\
.filter(VoucherLineItem.id == form.eid.data)\ .query(JournalEntryLineItem)\
.options(selectinload(VoucherLineItem.offsets)).first() .filter(JournalEntryLineItem.id == form.eid.data)\
.options(selectinload(JournalEntryLineItem.offsets)).first()
if line_item is None or len(line_item.offsets) == 0: if line_item is None or len(line_item.offsets) == 0:
return return
if field.data != line_item.account_code: if field.data != line_item.account_code:
@ -229,8 +231,9 @@ class NotExceedingOriginalLineItemNetBalance:
assert isinstance(form, LineItemForm) assert isinstance(form, LineItemForm)
if field.data is None or form.original_line_item_id.data is None: if field.data is None or form.original_line_item_id.data is None:
return return
original_line_item: VoucherLineItem | None \ original_line_item: JournalEntryLineItem | None \
= db.session.get(VoucherLineItem, form.original_line_item_id.data) = db.session.get(JournalEntryLineItem,
form.original_line_item_id.data)
if original_line_item is None: if original_line_item is None:
return return
is_debit: bool = isinstance(form, DebitLineItemForm) is_debit: bool = isinstance(form, DebitLineItemForm)
@ -239,13 +242,14 @@ class NotExceedingOriginalLineItemNetBalance:
existing_line_item_id \ existing_line_item_id \
= {x.id for x in form.voucher_form.obj.line_items} = {x.id for x in form.voucher_form.obj.line_items}
offset_total_func: sa.Function = sa.func.sum(sa.case( offset_total_func: sa.Function = sa.func.sum(sa.case(
(be(VoucherLineItem.is_debit == is_debit), VoucherLineItem.amount), (be(JournalEntryLineItem.is_debit == is_debit),
else_=-VoucherLineItem.amount)) JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount))
offset_total_but_form: Decimal | None = db.session.scalar( offset_total_but_form: Decimal | None = db.session.scalar(
sa.select(offset_total_func) sa.select(offset_total_func)
.filter(be(VoucherLineItem.original_line_item_id .filter(be(JournalEntryLineItem.original_line_item_id
== original_line_item.id), == original_line_item.id),
VoucherLineItem.id.not_in(existing_line_item_id))) JournalEntryLineItem.id.not_in(existing_line_item_id)))
if offset_total_but_form is None: if offset_total_but_form is None:
offset_total_but_form = Decimal("0") offset_total_but_form = Decimal("0")
offset_total_on_form: Decimal = sum( offset_total_on_form: Decimal = sum(
@ -269,9 +273,11 @@ class NotLessThanOffsetTotal:
return return
is_debit: bool = isinstance(form, DebitLineItemForm) is_debit: bool = isinstance(form, DebitLineItemForm)
select_offset_total: sa.Select = sa.select(sa.func.sum(sa.case( select_offset_total: sa.Select = sa.select(sa.func.sum(sa.case(
(VoucherLineItem.is_debit != is_debit, VoucherLineItem.amount), (JournalEntryLineItem.is_debit != is_debit,
else_=-VoucherLineItem.amount)))\ JournalEntryLineItem.amount),
.filter(be(VoucherLineItem.original_line_item_id == form.eid.data)) else_=-JournalEntryLineItem.amount)))\
.filter(be(JournalEntryLineItem.original_line_item_id
== form.eid.data))
offset_total: Decimal | None = db.session.scalar(select_offset_total) offset_total: Decimal | None = db.session.scalar(select_offset_total)
if offset_total is not None and field.data < offset_total: if offset_total is not None and field.data < offset_total:
raise ValidationError(lazy_gettext( raise ValidationError(lazy_gettext(
@ -319,16 +325,16 @@ class LineItemForm(FlaskForm):
return str(account) return str(account)
@property @property
def __original_line_item(self) -> VoucherLineItem | None: def __original_line_item(self) -> JournalEntryLineItem | None:
"""Returns the original line item. """Returns the original line item.
:return: The original line item. :return: The original line item.
""" """
if not hasattr(self, "____original_line_item"): if not hasattr(self, "____original_line_item"):
def get_line_item() -> VoucherLineItem | None: def get_line_item() -> JournalEntryLineItem | None:
if self.original_line_item_id.data is None: if self.original_line_item_id.data is None:
return None return None
return db.session.get(VoucherLineItem, return db.session.get(JournalEntryLineItem,
self.original_line_item_id.data) self.original_line_item_id.data)
setattr(self, "____original_line_item", get_line_item()) setattr(self, "____original_line_item", get_line_item())
return getattr(self, "____original_line_item") return getattr(self, "____original_line_item")
@ -371,22 +377,22 @@ class LineItemForm(FlaskForm):
return account is not None and account.is_need_offset return account is not None and account.is_need_offset
@property @property
def offsets(self) -> list[VoucherLineItem]: def offsets(self) -> list[JournalEntryLineItem]:
"""Returns the offsets. """Returns the offsets.
:return: The offsets. :return: The offsets.
""" """
if not hasattr(self, "__offsets"): if not hasattr(self, "__offsets"):
def get_offsets() -> list[VoucherLineItem]: def get_offsets() -> list[JournalEntryLineItem]:
if not self.is_need_offset or self.eid.data is None: if not self.is_need_offset or self.eid.data is None:
return [] return []
return VoucherLineItem.query\ return JournalEntryLineItem.query\
.filter(VoucherLineItem.original_line_item_id .filter(JournalEntryLineItem.original_line_item_id
== self.eid.data)\ == self.eid.data)\
.options(selectinload(VoucherLineItem.voucher), .options(selectinload(JournalEntryLineItem.voucher),
selectinload(VoucherLineItem.account), selectinload(JournalEntryLineItem.account),
selectinload(VoucherLineItem.offsets) selectinload(JournalEntryLineItem.offsets)
.selectinload(VoucherLineItem.voucher)).all() .selectinload(JournalEntryLineItem.voucher)).all()
setattr(self, "__offsets", get_offsets()) setattr(self, "__offsets", get_offsets())
return getattr(self, "__offsets") return getattr(self, "__offsets")
@ -460,7 +466,7 @@ class DebitLineItemForm(LineItemForm):
NotLessThanOffsetTotal()]) NotLessThanOffsetTotal()])
"""The amount.""" """The amount."""
def populate_obj(self, obj: VoucherLineItem) -> None: def populate_obj(self, obj: JournalEntryLineItem) -> None:
"""Populates the form data into a line item object. """Populates the form data into a line item object.
:param obj: The line item object. :param obj: The line item object.
@ -468,7 +474,7 @@ class DebitLineItemForm(LineItemForm):
""" """
is_new: bool = obj.id is None is_new: bool = obj.id is None
if is_new: if is_new:
obj.id = new_id(VoucherLineItem) obj.id = new_id(JournalEntryLineItem)
obj.original_line_item_id = self.original_line_item_id.data obj.original_line_item_id = self.original_line_item_id.data
obj.account_id = Account.find_by_code(self.account_code.data).id obj.account_id = Account.find_by_code(self.account_code.data).id
obj.description = self.description.data obj.description = self.description.data
@ -510,7 +516,7 @@ class CreditLineItemForm(LineItemForm):
NotLessThanOffsetTotal()]) NotLessThanOffsetTotal()])
"""The amount.""" """The amount."""
def populate_obj(self, obj: VoucherLineItem) -> None: def populate_obj(self, obj: JournalEntryLineItem) -> None:
"""Populates the form data into a line item object. """Populates the form data into a line item object.
:param obj: The line item object. :param obj: The line item object.
@ -518,7 +524,7 @@ class CreditLineItemForm(LineItemForm):
""" """
is_new: bool = obj.id is None is_new: bool = obj.id is None
if is_new: if is_new:
obj.id = new_id(VoucherLineItem) obj.id = new_id(JournalEntryLineItem)
obj.original_line_item_id = self.original_line_item_id.data obj.original_line_item_id = self.original_line_item_id.data
obj.account_id = Account.find_by_code(self.account_code.data).id obj.account_id = Account.find_by_code(self.account_code.data).id
obj.description = self.description.data obj.description = self.description.data

View File

@ -30,7 +30,7 @@ from wtforms.validators import DataRequired, ValidationError
from accounting import db from accounting import db
from accounting.locale import lazy_gettext from accounting.locale import lazy_gettext
from accounting.models import Voucher, Account, VoucherLineItem, \ from accounting.models import Voucher, Account, JournalEntryLineItem, \
VoucherCurrency VoucherCurrency
from accounting.voucher.utils.account_option import AccountOption from accounting.voucher.utils.account_option import AccountOption
from accounting.voucher.utils.original_line_items import \ from accounting.voucher.utils.original_line_items import \
@ -133,7 +133,8 @@ class VoucherForm(FlaskForm):
"""Whether we need the payable original line items.""" """Whether we need the payable original line items."""
self._is_need_receivable: bool = False self._is_need_receivable: bool = False
"""Whether we need the receivable original line items.""" """Whether we need the receivable original line items."""
self.__original_line_item_options: list[VoucherLineItem] | None = None self.__original_line_item_options: list[JournalEntryLineItem] | None \
= None
"""The options of the original line items.""" """The options of the original line items."""
self.__net_balance_exceeded: dict[int, LazyString] | None = None self.__net_balance_exceeded: dict[int, LazyString] | None = None
"""The original line items whose net balances were exceeded by the """The original line items whose net balances were exceeded by the
@ -161,8 +162,8 @@ class VoucherForm(FlaskForm):
to_delete: set[int] = {x.id for x in obj.line_items to_delete: set[int] = {x.id for x in obj.line_items
if x.id not in collector.to_keep} if x.id not in collector.to_keep}
if len(to_delete) > 0: if len(to_delete) > 0:
VoucherLineItem.query\ JournalEntryLineItem.query\
.filter(VoucherLineItem.id.in_(to_delete)).delete() .filter(JournalEntryLineItem.id.in_(to_delete)).delete()
self.is_modified = True self.is_modified = True
if is_new or db.session.is_modified(obj): if is_new or db.session.is_modified(obj):
@ -222,9 +223,9 @@ class VoucherForm(FlaskForm):
= [AccountOption(x) for x in Account.debit() = [AccountOption(x) for x in Account.debit()
if not (x.code[0] == "2" and x.is_need_offset)] if not (x.code[0] == "2" and x.is_need_offset)]
in_use: set[int] = set(db.session.scalars( in_use: set[int] = set(db.session.scalars(
sa.select(VoucherLineItem.account_id) sa.select(JournalEntryLineItem.account_id)
.filter(VoucherLineItem.is_debit) .filter(JournalEntryLineItem.is_debit)
.group_by(VoucherLineItem.account_id)).all()) .group_by(JournalEntryLineItem.account_id)).all())
for account in accounts: for account in accounts:
account.is_in_use = account.id in in_use account.is_in_use = account.id in in_use
return accounts return accounts
@ -239,9 +240,9 @@ class VoucherForm(FlaskForm):
= [AccountOption(x) for x in Account.credit() = [AccountOption(x) for x in Account.credit()
if not (x.code[0] == "1" and x.is_need_offset)] if not (x.code[0] == "1" and x.is_need_offset)]
in_use: set[int] = set(db.session.scalars( in_use: set[int] = set(db.session.scalars(
sa.select(VoucherLineItem.account_id) sa.select(JournalEntryLineItem.account_id)
.filter(sa.not_(VoucherLineItem.is_debit)) .filter(sa.not_(JournalEntryLineItem.is_debit))
.group_by(VoucherLineItem.account_id)).all()) .group_by(JournalEntryLineItem.account_id)).all())
for account in accounts: for account in accounts:
account.is_in_use = account.id in in_use account.is_in_use = account.id in in_use
return accounts return accounts
@ -264,7 +265,7 @@ class VoucherForm(FlaskForm):
return DescriptionEditor() return DescriptionEditor()
@property @property
def original_line_item_options(self) -> list[VoucherLineItem]: def original_line_item_options(self) -> list[JournalEntryLineItem]:
"""Returns the selectable original line items. """Returns the selectable original line items.
:return: The selectable original line items. :return: The selectable original line items.
@ -289,8 +290,8 @@ class VoucherForm(FlaskForm):
if len(original_line_item_id) == 0: if len(original_line_item_id) == 0:
return None return None
select: sa.Select = sa.select(sa.func.max(Voucher.date))\ select: sa.Select = sa.select(sa.func.max(Voucher.date))\
.join(VoucherLineItem)\ .join(JournalEntryLineItem)\
.filter(VoucherLineItem.id.in_(original_line_item_id)) .filter(JournalEntryLineItem.id.in_(original_line_item_id))
return db.session.scalar(select) return db.session.scalar(select)
@property @property
@ -302,8 +303,9 @@ class VoucherForm(FlaskForm):
line_item_id: set[int] = {x.eid.data for x in self.line_items line_item_id: set[int] = {x.eid.data for x in self.line_items
if x.eid.data is not None} if x.eid.data is not None}
select: sa.Select = sa.select(sa.func.min(Voucher.date))\ select: sa.Select = sa.select(sa.func.min(Voucher.date))\
.join(VoucherLineItem)\ .join(JournalEntryLineItem)\
.filter(VoucherLineItem.original_line_item_id.in_(line_item_id)) .filter(JournalEntryLineItem.original_line_item_id
.in_(line_item_id))
return db.session.scalar(select) return db.session.scalar(select)
@ -324,9 +326,9 @@ class LineItemCollector(t.Generic[T], ABC):
"""The voucher form.""" """The voucher form."""
self.__obj: Voucher = obj self.__obj: Voucher = obj
"""The voucher object.""" """The voucher object."""
self.__line_items: list[VoucherLineItem] = list(obj.line_items) self.__line_items: list[JournalEntryLineItem] = list(obj.line_items)
"""The existing line items.""" """The existing line items."""
self.__line_items_by_id: dict[int, VoucherLineItem] \ self.__line_items_by_id: dict[int, JournalEntryLineItem] \
= {x.id: x for x in self.__line_items} = {x.id: x for x in self.__line_items}
"""A dictionary from the line item ID to their line items.""" """A dictionary from the line item ID to their line items."""
self.__no_by_id: dict[int, int] \ self.__no_by_id: dict[int, int] \
@ -357,7 +359,7 @@ class LineItemCollector(t.Generic[T], ABC):
:param no: The number of the line item. :param no: The number of the line item.
:return: None. :return: None.
""" """
line_item: VoucherLineItem | None \ line_item: JournalEntryLineItem | None \
= self.__line_items_by_id.get(form.eid.data) = self.__line_items_by_id.get(form.eid.data)
if line_item is not None: if line_item is not None:
line_item.currency_code = currency_code line_item.currency_code = currency_code
@ -366,7 +368,7 @@ class LineItemCollector(t.Generic[T], ABC):
if db.session.is_modified(line_item): if db.session.is_modified(line_item):
self.form.is_modified = True self.form.is_modified = True
else: else:
line_item = VoucherLineItem() line_item = JournalEntryLineItem()
line_item.currency_code = currency_code line_item.currency_code = currency_code
form.populate_obj(line_item) form.populate_obj(line_item)
line_item.no = no line_item.no = no
@ -386,10 +388,10 @@ class LineItemCollector(t.Generic[T], ABC):
:param no: The number of the line item. :param no: The number of the line item.
:return: None. :return: None.
""" """
candidates: list[VoucherLineItem] \ candidates: list[JournalEntryLineItem] \
= [x for x in self.__line_items = [x for x in self.__line_items
if x.is_debit == is_debit and x.currency_code == currency_code] if x.is_debit == is_debit and x.currency_code == currency_code]
line_item: VoucherLineItem line_item: JournalEntryLineItem
if len(candidates) > 0: if len(candidates) > 0:
candidates.sort(key=lambda x: x.no) candidates.sort(key=lambda x: x.no)
line_item = candidates[0] line_item = candidates[0]
@ -400,8 +402,8 @@ class LineItemCollector(t.Generic[T], ABC):
if db.session.is_modified(line_item): if db.session.is_modified(line_item):
self.form.is_modified = True self.form.is_modified = True
else: else:
line_item = VoucherLineItem() line_item = JournalEntryLineItem()
line_item.id = new_id(VoucherLineItem) line_item.id = new_id(JournalEntryLineItem)
line_item.is_debit = is_debit line_item.is_debit = is_debit
line_item.currency_code = currency_code line_item.currency_code = currency_code
line_item.account_id = Account.cash().id line_item.account_id = Account.cash().id

View File

@ -22,7 +22,7 @@ import typing as t
import sqlalchemy as sa import sqlalchemy as sa
from accounting import db from accounting import db
from accounting.models import Account, VoucherLineItem from accounting.models import Account, JournalEntryLineItem
class DescriptionAccount: class DescriptionAccount:
@ -206,22 +206,25 @@ class DescriptionEditor:
"""The debit tags.""" """The debit tags."""
self.credit: DescriptionDebitCredit = DescriptionDebitCredit("credit") self.credit: DescriptionDebitCredit = DescriptionDebitCredit("credit")
"""The credit tags.""" """The credit tags."""
debit_credit: sa.Label = sa.case((VoucherLineItem.is_debit, "debit"), debit_credit: sa.Label = sa.case(
else_="credit").label("debit_credit") (JournalEntryLineItem.is_debit, "debit"),
else_="credit").label("debit_credit")
tag_type: sa.Label = sa.case( tag_type: sa.Label = sa.case(
(VoucherLineItem.description.like("_%—_%—_%→_%"), "bus"), (JournalEntryLineItem.description.like("_%—_%—_%→_%"), "bus"),
(sa.or_(VoucherLineItem.description.like("_%—_%→_%"), (sa.or_(JournalEntryLineItem.description.like("_%—_%→_%"),
VoucherLineItem.description.like("_%—_%↔_%")), "travel"), JournalEntryLineItem.description.like("_%—_%↔_%")),
"travel"),
else_="general").label("tag_type") else_="general").label("tag_type")
tag: sa.Label = get_prefix(VoucherLineItem.description, "")\ tag: sa.Label = get_prefix(JournalEntryLineItem.description, "")\
.label("tag") .label("tag")
select: sa.Select = sa.Select(debit_credit, tag_type, tag, select: sa.Select = sa.Select(debit_credit, tag_type, tag,
VoucherLineItem.account_id, JournalEntryLineItem.account_id,
sa.func.count().label("freq"))\ sa.func.count().label("freq"))\
.filter(VoucherLineItem.description.is_not(None), .filter(JournalEntryLineItem.description.is_not(None),
VoucherLineItem.description.like("_%—_%"), JournalEntryLineItem.description.like("_%—_%"),
VoucherLineItem.original_line_item_id.is_(None))\ JournalEntryLineItem.original_line_item_id.is_(None))\
.group_by(debit_credit, tag_type, tag, VoucherLineItem.account_id) .group_by(debit_credit, tag_type, tag,
JournalEntryLineItem.account_id)
result: list[sa.Row] = db.session.execute(select).all() result: list[sa.Row] = db.session.execute(select).all()
accounts: dict[int, Account] \ accounts: dict[int, Account] \
= {x.id: x for x in Account.query = {x.id: x for x in Account.query

View File

@ -21,7 +21,7 @@ import typing as t
import sqlalchemy as sa import sqlalchemy as sa
from accounting.models import VoucherLineItem from accounting.models import JournalEntryLineItem
def offset_alias() -> sa.Alias: def offset_alias() -> sa.Alias:
@ -36,4 +36,4 @@ def offset_alias() -> sa.Alias:
def as_alias(alias: t.Any) -> sa.Alias: def as_alias(alias: t.Any) -> sa.Alias:
return alias return alias
return as_alias(sa.alias(as_from(VoucherLineItem), name="offset")) return as_alias(sa.alias(as_from(JournalEntryLineItem), name="offset"))

View File

@ -23,14 +23,14 @@ import sqlalchemy as sa
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from accounting import db from accounting import db
from accounting.models import Account, Voucher, VoucherLineItem from accounting.models import Account, Voucher, JournalEntryLineItem
from accounting.utils.cast import be from accounting.utils.cast import be
from .offset_alias import offset_alias from .offset_alias import offset_alias
def get_selectable_original_line_items( def get_selectable_original_line_items(
line_item_id_on_form: set[int], is_payable: bool, line_item_id_on_form: set[int], is_payable: bool,
is_receivable: bool) -> list[VoucherLineItem]: is_receivable: bool) -> list[JournalEntryLineItem]:
"""Queries and returns the selectable original line items, with their net """Queries and returns the selectable original line items, with their net
balances. The offset amounts of the form is excluded. balances. The offset amounts of the form is excluded.
@ -43,37 +43,40 @@ def get_selectable_original_line_items(
""" """
assert is_payable or is_receivable assert is_payable or is_receivable
offset: sa.Alias = offset_alias() offset: sa.Alias = offset_alias()
net_balance: sa.Label = (VoucherLineItem.amount + sa.func.sum(sa.case( net_balance: sa.Label = (JournalEntryLineItem.amount + sa.func.sum(sa.case(
(offset.c.id.in_(line_item_id_on_form), 0), (offset.c.id.in_(line_item_id_on_form), 0),
(be(offset.c.is_debit == VoucherLineItem.is_debit), offset.c.amount), (be(offset.c.is_debit == JournalEntryLineItem.is_debit),
offset.c.amount),
else_=-offset.c.amount))).label("net_balance") else_=-offset.c.amount))).label("net_balance")
conditions: list[sa.BinaryExpression] = [Account.is_need_offset] conditions: list[sa.BinaryExpression] = [Account.is_need_offset]
sub_conditions: list[sa.BinaryExpression] = [] sub_conditions: list[sa.BinaryExpression] = []
if is_payable: if is_payable:
sub_conditions.append(sa.and_(Account.base_code.startswith("2"), sub_conditions.append(sa.and_(Account.base_code.startswith("2"),
sa.not_(VoucherLineItem.is_debit))) sa.not_(JournalEntryLineItem.is_debit)))
if is_receivable: if is_receivable:
sub_conditions.append(sa.and_(Account.base_code.startswith("1"), sub_conditions.append(sa.and_(Account.base_code.startswith("1"),
VoucherLineItem.is_debit)) JournalEntryLineItem.is_debit))
conditions.append(sa.or_(*sub_conditions)) conditions.append(sa.or_(*sub_conditions))
select_net_balances: sa.Select \ select_net_balances: sa.Select \
= sa.select(VoucherLineItem.id, net_balance)\ = sa.select(JournalEntryLineItem.id, net_balance)\
.join(Account)\ .join(Account)\
.join(offset, be(VoucherLineItem.id == offset.c.original_line_item_id), .join(offset, be(JournalEntryLineItem.id
== offset.c.original_line_item_id),
isouter=True)\ isouter=True)\
.filter(*conditions)\ .filter(*conditions)\
.group_by(VoucherLineItem.id)\ .group_by(JournalEntryLineItem.id)\
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0)) .having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
net_balances: dict[int, Decimal] \ net_balances: dict[int, Decimal] \
= {x.id: x.net_balance = {x.id: x.net_balance
for x in db.session.execute(select_net_balances).all()} for x in db.session.execute(select_net_balances).all()}
line_items: list[VoucherLineItem] = VoucherLineItem.query\ line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query\
.filter(VoucherLineItem.id.in_({x for x in net_balances}))\ .filter(JournalEntryLineItem.id.in_({x for x in net_balances}))\
.join(Voucher)\ .join(Voucher)\
.order_by(Voucher.date, VoucherLineItem.is_debit, VoucherLineItem.no)\ .order_by(Voucher.date, JournalEntryLineItem.is_debit,
.options(selectinload(VoucherLineItem.currency), JournalEntryLineItem.no)\
selectinload(VoucherLineItem.account), .options(selectinload(JournalEntryLineItem.currency),
selectinload(VoucherLineItem.voucher)).all() selectinload(JournalEntryLineItem.account),
selectinload(JournalEntryLineItem.voucher)).all()
for line_item in line_items: for line_item in line_items:
line_item.net_balance = line_item.amount \ line_item.net_balance = line_item.amount \
if net_balances[line_item.id] is None \ if net_balances[line_item.id] is None \

View File

@ -42,7 +42,7 @@ class DescriptionEditorTestCase(unittest.TestCase):
runner: FlaskCliRunner = self.app.test_cli_runner() runner: FlaskCliRunner = self.app.test_cli_runner()
with self.app.app_context(): with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \ from accounting.models import BaseAccount, Voucher, \
VoucherLineItem JournalEntryLineItem
result: Result result: Result
result = runner.invoke(args="init-db") result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
@ -56,7 +56,7 @@ class DescriptionEditorTestCase(unittest.TestCase):
"-u", "editor"]) "-u", "editor"])
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
Voucher.query.delete() Voucher.query.delete()
VoucherLineItem.query.delete() JournalEntryLineItem.query.delete()
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")

View File

@ -27,7 +27,7 @@ from flask.testing import FlaskCliRunner
from test_site import db from test_site import db
from testlib import create_test_app, get_client from testlib import create_test_app, get_client
from testlib_offset import TestData, VoucherLineItemData, VoucherData, \ from testlib_offset import TestData, JournalEntryLineItemData, VoucherData, \
CurrencyData CurrencyData
from testlib_voucher import Accounts, match_voucher_detail from testlib_voucher import Accounts, match_voucher_detail
@ -49,7 +49,7 @@ class OffsetTestCase(unittest.TestCase):
runner: FlaskCliRunner = self.app.test_cli_runner() runner: FlaskCliRunner = self.app.test_cli_runner()
with self.app.app_context(): with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \ from accounting.models import BaseAccount, Voucher, \
VoucherLineItem JournalEntryLineItem
result: Result result: Result
result = runner.invoke(args="init-db") result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
@ -63,7 +63,7 @@ class OffsetTestCase(unittest.TestCase):
"-u", "editor"]) "-u", "editor"])
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
Voucher.query.delete() Voucher.query.delete()
VoucherLineItem.query.delete() JournalEntryLineItem.query.delete()
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")
self.data: TestData = TestData(self.app, self.client, self.csrf_token) self.data: TestData = TestData(self.app, self.client, self.csrf_token)
@ -84,15 +84,15 @@ class OffsetTestCase(unittest.TestCase):
self.data.e_r_or3d.voucher.days, [CurrencyData( self.data.e_r_or3d.voucher.days, [CurrencyData(
"USD", "USD",
[], [],
[VoucherLineItemData(Accounts.RECEIVABLE, [JournalEntryLineItemData(Accounts.RECEIVABLE,
self.data.e_r_or1d.description, "300", self.data.e_r_or1d.description, "300",
original_line_item=self.data.e_r_or1d), original_line_item=self.data.e_r_or1d),
VoucherLineItemData(Accounts.RECEIVABLE, JournalEntryLineItemData(Accounts.RECEIVABLE,
self.data.e_r_or1d.description, "100", self.data.e_r_or1d.description, "100",
original_line_item=self.data.e_r_or1d), original_line_item=self.data.e_r_or1d),
VoucherLineItemData(Accounts.RECEIVABLE, JournalEntryLineItemData(Accounts.RECEIVABLE,
self.data.e_r_or3d.description, "100", self.data.e_r_or3d.description, "100",
original_line_item=self.data.e_r_or3d)])]) original_line_item=self.data.e_r_or3d)])])
# Non-existing original line item ID # Non-existing original line item ID
form = voucher_data.new_form(self.csrf_token) form = voucher_data.new_form(self.csrf_token)
@ -399,15 +399,15 @@ class OffsetTestCase(unittest.TestCase):
voucher_data: VoucherData = VoucherData( voucher_data: VoucherData = VoucherData(
self.data.e_p_or3c.voucher.days, [CurrencyData( self.data.e_p_or3c.voucher.days, [CurrencyData(
"USD", "USD",
[VoucherLineItemData(Accounts.PAYABLE, [JournalEntryLineItemData(Accounts.PAYABLE,
self.data.e_p_or1c.description, "500", self.data.e_p_or1c.description, "500",
original_line_item=self.data.e_p_or1c), original_line_item=self.data.e_p_or1c),
VoucherLineItemData(Accounts.PAYABLE, JournalEntryLineItemData(Accounts.PAYABLE,
self.data.e_p_or1c.description, "300", self.data.e_p_or1c.description, "300",
original_line_item=self.data.e_p_or1c), original_line_item=self.data.e_p_or1c),
VoucherLineItemData(Accounts.PAYABLE, JournalEntryLineItemData(Accounts.PAYABLE,
self.data.e_p_or3c.description, "120", self.data.e_p_or3c.description, "120",
original_line_item=self.data.e_p_or3c)], original_line_item=self.data.e_p_or3c)],
[])]) [])])
# Non-existing original line item ID # Non-existing original line item ID

View File

@ -53,7 +53,7 @@ class CashReceiptVoucherTestCase(unittest.TestCase):
runner: FlaskCliRunner = self.app.test_cli_runner() runner: FlaskCliRunner = self.app.test_cli_runner()
with self.app.app_context(): with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \ from accounting.models import BaseAccount, Voucher, \
VoucherLineItem JournalEntryLineItem
result: Result result: Result
result = runner.invoke(args="init-db") result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
@ -67,7 +67,7 @@ class CashReceiptVoucherTestCase(unittest.TestCase):
"-u", "editor"]) "-u", "editor"])
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
Voucher.query.delete() Voucher.query.delete()
VoucherLineItem.query.delete() JournalEntryLineItem.query.delete()
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")
@ -625,7 +625,7 @@ class CashDisbursementVoucherTestCase(unittest.TestCase):
runner: FlaskCliRunner = self.app.test_cli_runner() runner: FlaskCliRunner = self.app.test_cli_runner()
with self.app.app_context(): with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \ from accounting.models import BaseAccount, Voucher, \
VoucherLineItem JournalEntryLineItem
result: Result result: Result
result = runner.invoke(args="init-db") result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
@ -639,7 +639,7 @@ class CashDisbursementVoucherTestCase(unittest.TestCase):
"-u", "editor"]) "-u", "editor"])
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
Voucher.query.delete() Voucher.query.delete()
VoucherLineItem.query.delete() JournalEntryLineItem.query.delete()
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")
@ -1204,7 +1204,7 @@ class TransferVoucherTestCase(unittest.TestCase):
runner: FlaskCliRunner = self.app.test_cli_runner() runner: FlaskCliRunner = self.app.test_cli_runner()
with self.app.app_context(): with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \ from accounting.models import BaseAccount, Voucher, \
VoucherLineItem JournalEntryLineItem
result: Result result: Result
result = runner.invoke(args="init-db") result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
@ -1218,7 +1218,7 @@ class TransferVoucherTestCase(unittest.TestCase):
"-u", "editor"]) "-u", "editor"])
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
Voucher.query.delete() Voucher.query.delete()
VoucherLineItem.query.delete() JournalEntryLineItem.query.delete()
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")
@ -2056,7 +2056,7 @@ class VoucherReorderTestCase(unittest.TestCase):
runner: FlaskCliRunner = self.app.test_cli_runner() runner: FlaskCliRunner = self.app.test_cli_runner()
with self.app.app_context(): with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \ from accounting.models import BaseAccount, Voucher, \
VoucherLineItem JournalEntryLineItem
result: Result result: Result
result = runner.invoke(args="init-db") result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
@ -2070,7 +2070,7 @@ class VoucherReorderTestCase(unittest.TestCase):
"-u", "editor"]) "-u", "editor"])
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
Voucher.query.delete() Voucher.query.delete()
VoucherLineItem.query.delete() JournalEntryLineItem.query.delete()
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")

View File

@ -29,22 +29,22 @@ from test_site import db
from testlib_voucher import Accounts, match_voucher_detail, NEXT_URI from testlib_voucher import Accounts, match_voucher_detail, NEXT_URI
class VoucherLineItemData: class JournalEntryLineItemData:
"""The voucher line item data.""" """The journal entry line item data."""
def __init__(self, account: str, description: str, amount: str, def __init__(self, account: str, description: str, amount: str,
original_line_item: VoucherLineItemData | None = None): original_line_item: JournalEntryLineItemData | None = None):
"""Constructs the voucher line item data. """Constructs the journal entry line item data.
:param account: The account code. :param account: The account code.
:param description: The description. :param description: The description.
:param amount: The amount. :param amount: The amount.
:param original_line_item: The original voucher line item. :param original_line_item: The original journal entry line item.
""" """
self.voucher: VoucherData | None = None self.voucher: VoucherData | None = None
self.id: int = -1 self.id: int = -1
self.no: int = -1 self.no: int = -1
self.original_line_item: VoucherLineItemData | None \ self.original_line_item: JournalEntryLineItemData | None \
= original_line_item = original_line_item
self.account: str = account self.account: str = account
self.description: str = description self.description: str = description
@ -77,8 +77,8 @@ class VoucherLineItemData:
class CurrencyData: class CurrencyData:
"""The voucher currency data.""" """The voucher currency data."""
def __init__(self, currency: str, debit: list[VoucherLineItemData], def __init__(self, currency: str, debit: list[JournalEntryLineItemData],
credit: list[VoucherLineItemData]): credit: list[JournalEntryLineItemData]):
"""Constructs the voucher currency data. """Constructs the voucher currency data.
:param currency: The currency code. :param currency: The currency code.
@ -86,8 +86,8 @@ class CurrencyData:
:param credit: The credit line items. :param credit: The credit line items.
""" """
self.code: str = currency self.code: str = currency
self.debit: list[VoucherLineItemData] = debit self.debit: list[JournalEntryLineItemData] = debit
self.credit: list[VoucherLineItemData] = credit self.credit: list[JournalEntryLineItemData] = credit
def form(self, index: int, is_update: bool) -> dict[str, str]: def form(self, index: int, is_update: bool) -> dict[str, str]:
"""Returns the currency as form data. """Returns the currency as form data.
@ -175,7 +175,7 @@ class TestData:
self.csrf_token: str = csrf_token self.csrf_token: str = csrf_token
def couple(description: str, amount: str, debit: str, credit: str) \ def couple(description: str, amount: str, debit: str, credit: str) \
-> tuple[VoucherLineItemData, VoucherLineItemData]: -> tuple[JournalEntryLineItemData, JournalEntryLineItemData]:
"""Returns a couple of debit-credit line items. """Returns a couple of debit-credit line items.
:param description: The description. :param description: The description.
@ -184,8 +184,8 @@ class TestData:
:param credit: The credit account code. :param credit: The credit account code.
:return: The debit line item and credit line item. :return: The debit line item and credit line item.
""" """
return VoucherLineItemData(debit, description, amount),\ return JournalEntryLineItemData(debit, description, amount),\
VoucherLineItemData(credit, description, amount) JournalEntryLineItemData(credit, description, amount)
# Receivable original line items # Receivable original line items
self.e_r_or1d, self.e_r_or1c = couple( self.e_r_or1d, self.e_r_or1c = couple(