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:
"""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.
:return: True if voucher line items in the account need offset, or False
otherwise.
:return: True if journal entry line items in the account need offset, or
False otherwise.
"""
# Assets
if base_code[0] == "1":

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
from accounting.locale import gettext
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
Voucher, VoucherLineItem
Voucher, JournalEntryLineItem
from accounting.report.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport
from accounting.report.utils.csv_export import csv_download
@ -43,10 +43,10 @@ class LineItemCollector:
def __init__(self):
"""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."""
def __query_line_items(self) -> list[VoucherLineItem]:
def __query_line_items(self) -> list[JournalEntryLineItem]:
"""Queries and returns the line items.
:return: The line items.
@ -57,26 +57,27 @@ class LineItemCollector:
conditions: list[sa.BinaryExpression] = []
for k in keywords:
sub_conditions: list[sa.BinaryExpression] \
= [VoucherLineItem.description.contains(k),
VoucherLineItem.account_id.in_(
= [JournalEntryLineItem.description.contains(k),
JournalEntryLineItem.account_id.in_(
self.__get_account_condition(k)),
VoucherLineItem.currency_code.in_(
JournalEntryLineItem.currency_code.in_(
self.__get_currency_condition(k)),
VoucherLineItem.voucher_id.in_(
JournalEntryLineItem.voucher_id.in_(
self.__get_voucher_condition(k))]
try:
sub_conditions.append(VoucherLineItem.amount == Decimal(k))
sub_conditions.append(
JournalEntryLineItem.amount == Decimal(k))
except ArithmeticError:
pass
conditions.append(sa.or_(*sub_conditions))
return VoucherLineItem.query.join(Voucher).filter(*conditions)\
return JournalEntryLineItem.query.join(Voucher).filter(*conditions)\
.order_by(Voucher.date,
Voucher.no,
VoucherLineItem.is_debit,
VoucherLineItem.no)\
.options(selectinload(VoucherLineItem.account),
selectinload(VoucherLineItem.currency),
selectinload(VoucherLineItem.voucher)).all()
JournalEntryLineItem.is_debit,
JournalEntryLineItem.no)\
.options(selectinload(JournalEntryLineItem.account),
selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.voucher)).all()
@staticmethod
def __get_account_condition(k: str) -> sa.Select:
@ -149,15 +150,15 @@ class LineItemCollector:
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, pagination: Pagination[VoucherLineItem],
line_items: list[VoucherLineItem]):
def __init__(self, pagination: Pagination[JournalEntryLineItem],
line_items: list[JournalEntryLineItem]):
"""Constructs the HTML page parameters.
:param line_items: The search result line items.
"""
self.pagination: Pagination[VoucherLineItem] = pagination
self.pagination: Pagination[JournalEntryLineItem] = pagination
"""The pagination."""
self.line_items: list[VoucherLineItem] = line_items
self.line_items: list[JournalEntryLineItem] = line_items
"""The line items."""
@property
@ -182,7 +183,7 @@ class Search(BaseReport):
def __init__(self):
"""Constructs a search."""
self.__line_items: list[VoucherLineItem] \
self.__line_items: list[JournalEntryLineItem] \
= LineItemCollector().line_items
"""The line items."""
@ -199,8 +200,9 @@ class Search(BaseReport):
:return: The report as HTML.
"""
pagination: Pagination[VoucherLineItem] \
= Pagination[VoucherLineItem](self.__line_items, is_reversed=True)
pagination: Pagination[JournalEntryLineItem] \
= Pagination[JournalEntryLineItem](self.__line_items,
is_reversed=True)
params: PageParams = PageParams(pagination=pagination,
line_items=pagination.list)
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.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.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport
@ -178,14 +178,14 @@ class TrialBalance(BaseReport):
:return: None.
"""
conditions: list[sa.BinaryExpression] \
= [VoucherLineItem.currency_code == self.__currency.code]
= [JournalEntryLineItem.currency_code == self.__currency.code]
if self.__period.start is not None:
conditions.append(Voucher.date >= self.__period.start)
if self.__period.end is not None:
conditions.append(Voucher.date <= self.__period.end)
balance_func: sa.Function = sa.func.sum(sa.case(
(VoucherLineItem.is_debit, VoucherLineItem.amount),
else_=-VoucherLineItem.amount)).label("balance")
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount)).label("balance")
select_balances: sa.Select = sa.select(Account.id, balance_func)\
.join(Voucher).join(Account)\
.filter(*conditions)\

View File

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

View File

@ -166,14 +166,14 @@
.accounting-list-group-hover .list-group-item:hover {
background-color: #ececec;
}
.accounting-voucher-line-item {
.accounting-journal-entry-line-item {
border: none;
}
.accounting-voucher-line-item-header {
.accounting-journal-entry-line-item-header {
font-weight: bolder;
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;
border-top: thick double slategray;
}

View File

@ -30,7 +30,7 @@ class AccountSelector {
/**
* The line item editor
* @type {VoucherLineItemEditor}
* @type {JournalEntryLineItemEditor}
*/
#lineItemEditor;
@ -85,7 +85,7 @@ class AccountSelector {
/**
* 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"
*/
constructor(lineItemEditor, debitCredit) {
@ -210,7 +210,7 @@ class AccountSelector {
/**
* Returns the account selector instances.
*
* @param lineItemEditor {VoucherLineItemEditor} the line item editor
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
* @return {{debit: AccountSelector, credit: AccountSelector}}
*/
static getInstances(lineItemEditor) {

View File

@ -30,7 +30,7 @@ class DescriptionEditor {
/**
* The line item editor
* @type {VoucherLineItemEditor}
* @type {JournalEntryLineItemEditor}
*/
#lineItemEditor;
@ -109,7 +109,7 @@ class DescriptionEditor {
/**
* 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"
*/
constructor(lineItemEditor, debitCredit) {
@ -246,7 +246,7 @@ class DescriptionEditor {
/**
* Returns the description editor instances.
*
* @param lineItemEditor {VoucherLineItemEditor} the line item editor
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
* @return {{debit: DescriptionEditor, credit: DescriptionEditor}}
*/
static getInstances(lineItemEditor) {

View File

@ -1,5 +1,5 @@
/* 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.
@ -23,10 +23,10 @@
"use strict";
/**
* The voucher line item editor.
* The journal entry line item editor.
*
*/
class VoucherLineItemEditor {
class JournalEntryLineItemEditor {
/**
* The voucher form
@ -35,7 +35,7 @@ class VoucherLineItemEditor {
form;
/**
* The voucher line item editor
* The journal entry line item editor
* @type {HTMLFormElement}
*/
#element;
@ -137,7 +137,7 @@ class VoucherLineItemEditor {
#amountError;
/**
* The voucher line item to edit
* The journal entry line item to edit
* @type {LineItemSubForm|null}
*/
lineItem;
@ -149,7 +149,7 @@ class VoucherLineItemEditor {
#debitCreditSubForm;
/**
* Whether the voucher line item needs offset
* Whether the journal entry line item needs offset
* @type {boolean}
*/
isNeedOffset = false;
@ -215,7 +215,7 @@ class VoucherLineItemEditor {
originalLineItemSelector;
/**
* Constructs a new voucher line item editor.
* Constructs a new journal entry line item editor.
*
* @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
*/
@ -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) {
this.lineItem = lineItem;

View File

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

View File

@ -100,7 +100,7 @@ class VoucherForm {
/**
* The line item editor
* @type {VoucherLineItemEditor}
* @type {JournalEntryLineItemEditor}
*/
lineItemEditor;
@ -121,7 +121,7 @@ class VoucherForm {
this.#addCurrencyButton = document.getElementById("accounting-add-currency");
this.#note = document.getElementById("accounting-note");
this.#noteError = document.getElementById("accounting-note-error");
this.lineItemEditor = new VoucherLineItemEditor(this);
this.lineItemEditor = new JournalEntryLineItemEditor(this);
this.#addCurrencyButton.onclick = () => {
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.
*
* @param editor {VoucherLineItemEditor} the line item editor
* @param editor {JournalEntryLineItemEditor} the line item editor
*/
save(editor) {
if (editor.isNeedOffset) {

View File

@ -34,11 +34,11 @@ First written: 2023/2/26
<div class="mb-2 fw-bolder">{{ currency.name }}</div>
<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 %}
{% include "accounting/voucher/include/detail-line-items.html" %}
{% 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>{{ A_("Total") }}</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 #}
{% 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>
<div class="small">{{ line_item.account }}</div>

View File

@ -24,7 +24,7 @@ First written: 2023/2/26
{% 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/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/original-line-item-selector.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/description-editor.js") }}"></script>
@ -88,7 +88,7 @@ First written: 2023/2/26
</div>
</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 %}
{% include "accounting/voucher/include/original-line-item-selector-modal.html" %}

View File

@ -1,6 +1,6 @@
{#
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.

View File

@ -34,11 +34,11 @@ First written: 2023/2/26
<div class="mb-2 fw-bolder">{{ currency.name }}</div>
<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 %}
{% include "accounting/voucher/include/detail-line-items.html" %}
{% 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>{{ A_("Total") }}</div>
<div>{{ currency.debit_total|accounting_format_amount }}</div>

View File

@ -30,11 +30,11 @@ First written: 2023/2/26
{# The debit line items #}
<div class="col-sm-6 mb-2">
<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 %}
{% include "accounting/voucher/include/detail-line-items.html" %}
{% 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>{{ A_("Total") }}</div>
<div>{{ currency.debit_total|accounting_format_amount }}</div>
@ -46,11 +46,11 @@ First written: 2023/2/26
{# The credit line items #}
<div class="col-sm-6 mb-2">
<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 %}
{% include "accounting/voucher/include/detail-line-items.html" %}
{% 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>{{ A_("Total") }}</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 werkzeug.routing import BaseConverter
from accounting.models import Voucher, VoucherLineItem
from accounting.models import Voucher, JournalEntryLineItem
from accounting.utils.voucher_types import VoucherType
@ -37,11 +37,11 @@ class VoucherConverter(BaseConverter):
:param value: The voucher ID.
:return: The corresponding voucher.
"""
voucher: Voucher | None = Voucher.query.join(VoucherLineItem)\
voucher: Voucher | None = Voucher.query.join(JournalEntryLineItem)\
.filter(Voucher.id == value)\
.options(selectinload(Voucher.line_items)
.selectinload(VoucherLineItem.offsets)
.selectinload(VoucherLineItem.voucher))\
.selectinload(JournalEntryLineItem.offsets)
.selectinload(JournalEntryLineItem.voucher))\
.first()
if voucher is None:
abort(404)

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ import typing as t
import sqlalchemy as sa
from accounting.models import VoucherLineItem
from accounting.models import JournalEntryLineItem
def offset_alias() -> sa.Alias:
@ -36,4 +36,4 @@ def offset_alias() -> sa.Alias:
def as_alias(alias: t.Any) -> sa.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 accounting import db
from accounting.models import Account, Voucher, VoucherLineItem
from accounting.models import Account, Voucher, JournalEntryLineItem
from accounting.utils.cast import be
from .offset_alias import offset_alias
def get_selectable_original_line_items(
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
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
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),
(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")
conditions: list[sa.BinaryExpression] = [Account.is_need_offset]
sub_conditions: list[sa.BinaryExpression] = []
if is_payable:
sub_conditions.append(sa.and_(Account.base_code.startswith("2"),
sa.not_(VoucherLineItem.is_debit)))
sa.not_(JournalEntryLineItem.is_debit)))
if is_receivable:
sub_conditions.append(sa.and_(Account.base_code.startswith("1"),
VoucherLineItem.is_debit))
JournalEntryLineItem.is_debit))
conditions.append(sa.or_(*sub_conditions))
select_net_balances: sa.Select \
= sa.select(VoucherLineItem.id, net_balance)\
= sa.select(JournalEntryLineItem.id, net_balance)\
.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)\
.filter(*conditions)\
.group_by(VoucherLineItem.id)\
.group_by(JournalEntryLineItem.id)\
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
net_balances: dict[int, Decimal] \
= {x.id: x.net_balance
for x in db.session.execute(select_net_balances).all()}
line_items: list[VoucherLineItem] = VoucherLineItem.query\
.filter(VoucherLineItem.id.in_({x for x in net_balances}))\
line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query\
.filter(JournalEntryLineItem.id.in_({x for x in net_balances}))\
.join(Voucher)\
.order_by(Voucher.date, VoucherLineItem.is_debit, VoucherLineItem.no)\
.options(selectinload(VoucherLineItem.currency),
selectinload(VoucherLineItem.account),
selectinload(VoucherLineItem.voucher)).all()
.order_by(Voucher.date, JournalEntryLineItem.is_debit,
JournalEntryLineItem.no)\
.options(selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.account),
selectinload(JournalEntryLineItem.voucher)).all()
for line_item in line_items:
line_item.net_balance = line_item.amount \
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()
with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \
VoucherLineItem
JournalEntryLineItem
result: Result
result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0)
@ -56,7 +56,7 @@ class DescriptionEditorTestCase(unittest.TestCase):
"-u", "editor"])
self.assertEqual(result.exit_code, 0)
Voucher.query.delete()
VoucherLineItem.query.delete()
JournalEntryLineItem.query.delete()
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 testlib import create_test_app, get_client
from testlib_offset import TestData, VoucherLineItemData, VoucherData, \
from testlib_offset import TestData, JournalEntryLineItemData, VoucherData, \
CurrencyData
from testlib_voucher import Accounts, match_voucher_detail
@ -49,7 +49,7 @@ class OffsetTestCase(unittest.TestCase):
runner: FlaskCliRunner = self.app.test_cli_runner()
with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \
VoucherLineItem
JournalEntryLineItem
result: Result
result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0)
@ -63,7 +63,7 @@ class OffsetTestCase(unittest.TestCase):
"-u", "editor"])
self.assertEqual(result.exit_code, 0)
Voucher.query.delete()
VoucherLineItem.query.delete()
JournalEntryLineItem.query.delete()
self.client, self.csrf_token = get_client(self.app, "editor")
self.data: TestData = TestData(self.app, self.client, self.csrf_token)
@ -84,13 +84,13 @@ class OffsetTestCase(unittest.TestCase):
self.data.e_r_or3d.voucher.days, [CurrencyData(
"USD",
[],
[VoucherLineItemData(Accounts.RECEIVABLE,
[JournalEntryLineItemData(Accounts.RECEIVABLE,
self.data.e_r_or1d.description, "300",
original_line_item=self.data.e_r_or1d),
VoucherLineItemData(Accounts.RECEIVABLE,
JournalEntryLineItemData(Accounts.RECEIVABLE,
self.data.e_r_or1d.description, "100",
original_line_item=self.data.e_r_or1d),
VoucherLineItemData(Accounts.RECEIVABLE,
JournalEntryLineItemData(Accounts.RECEIVABLE,
self.data.e_r_or3d.description, "100",
original_line_item=self.data.e_r_or3d)])])
@ -399,13 +399,13 @@ class OffsetTestCase(unittest.TestCase):
voucher_data: VoucherData = VoucherData(
self.data.e_p_or3c.voucher.days, [CurrencyData(
"USD",
[VoucherLineItemData(Accounts.PAYABLE,
[JournalEntryLineItemData(Accounts.PAYABLE,
self.data.e_p_or1c.description, "500",
original_line_item=self.data.e_p_or1c),
VoucherLineItemData(Accounts.PAYABLE,
JournalEntryLineItemData(Accounts.PAYABLE,
self.data.e_p_or1c.description, "300",
original_line_item=self.data.e_p_or1c),
VoucherLineItemData(Accounts.PAYABLE,
JournalEntryLineItemData(Accounts.PAYABLE,
self.data.e_p_or3c.description, "120",
original_line_item=self.data.e_p_or3c)],
[])])

View File

@ -53,7 +53,7 @@ class CashReceiptVoucherTestCase(unittest.TestCase):
runner: FlaskCliRunner = self.app.test_cli_runner()
with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \
VoucherLineItem
JournalEntryLineItem
result: Result
result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0)
@ -67,7 +67,7 @@ class CashReceiptVoucherTestCase(unittest.TestCase):
"-u", "editor"])
self.assertEqual(result.exit_code, 0)
Voucher.query.delete()
VoucherLineItem.query.delete()
JournalEntryLineItem.query.delete()
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()
with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \
VoucherLineItem
JournalEntryLineItem
result: Result
result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0)
@ -639,7 +639,7 @@ class CashDisbursementVoucherTestCase(unittest.TestCase):
"-u", "editor"])
self.assertEqual(result.exit_code, 0)
Voucher.query.delete()
VoucherLineItem.query.delete()
JournalEntryLineItem.query.delete()
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()
with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \
VoucherLineItem
JournalEntryLineItem
result: Result
result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0)
@ -1218,7 +1218,7 @@ class TransferVoucherTestCase(unittest.TestCase):
"-u", "editor"])
self.assertEqual(result.exit_code, 0)
Voucher.query.delete()
VoucherLineItem.query.delete()
JournalEntryLineItem.query.delete()
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()
with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \
VoucherLineItem
JournalEntryLineItem
result: Result
result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0)
@ -2070,7 +2070,7 @@ class VoucherReorderTestCase(unittest.TestCase):
"-u", "editor"])
self.assertEqual(result.exit_code, 0)
Voucher.query.delete()
VoucherLineItem.query.delete()
JournalEntryLineItem.query.delete()
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
class VoucherLineItemData:
"""The voucher line item data."""
class JournalEntryLineItemData:
"""The journal entry line item data."""
def __init__(self, account: str, description: str, amount: str,
original_line_item: VoucherLineItemData | None = None):
"""Constructs the voucher line item data.
original_line_item: JournalEntryLineItemData | None = None):
"""Constructs the journal entry line item data.
:param account: The account code.
:param description: The description.
: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.id: int = -1
self.no: int = -1
self.original_line_item: VoucherLineItemData | None \
self.original_line_item: JournalEntryLineItemData | None \
= original_line_item
self.account: str = account
self.description: str = description
@ -77,8 +77,8 @@ class VoucherLineItemData:
class CurrencyData:
"""The voucher currency data."""
def __init__(self, currency: str, debit: list[VoucherLineItemData],
credit: list[VoucherLineItemData]):
def __init__(self, currency: str, debit: list[JournalEntryLineItemData],
credit: list[JournalEntryLineItemData]):
"""Constructs the voucher currency data.
:param currency: The currency code.
@ -86,8 +86,8 @@ class CurrencyData:
:param credit: The credit line items.
"""
self.code: str = currency
self.debit: list[VoucherLineItemData] = debit
self.credit: list[VoucherLineItemData] = credit
self.debit: list[JournalEntryLineItemData] = debit
self.credit: list[JournalEntryLineItemData] = credit
def form(self, index: int, is_update: bool) -> dict[str, str]:
"""Returns the currency as form data.
@ -175,7 +175,7 @@ class TestData:
self.csrf_token: str = csrf_token
def couple(description: str, amount: str, debit: str, credit: str) \
-> tuple[VoucherLineItemData, VoucherLineItemData]:
-> tuple[JournalEntryLineItemData, JournalEntryLineItemData]:
"""Returns a couple of debit-credit line items.
:param description: The description.
@ -184,8 +184,8 @@ class TestData:
:param credit: The credit account code.
:return: The debit line item and credit line item.
"""
return VoucherLineItemData(debit, description, amount),\
VoucherLineItemData(credit, description, amount)
return JournalEntryLineItemData(debit, description, amount),\
JournalEntryLineItemData(credit, description, amount)
# Receivable original line items
self.e_r_or1d, self.e_r_or1c = couple(