Renamed "journal entry" to "voucher line item", and "entry type" to "side".
This commit is contained in:
parent
25c45b16ae
commit
c1235608d8
@ -100,10 +100,11 @@ def init_accounts_command(username: str) -> None:
|
||||
|
||||
|
||||
def __is_need_offset(base_code: str) -> bool:
|
||||
"""Checks that whether entries in the account need offset.
|
||||
"""Checks that whether voucher line items in the account need offset.
|
||||
|
||||
:param base_code: The code of the base account.
|
||||
:return: True if entries in the account need offset, or False otherwise.
|
||||
:return: True if voucher line items in the account need offset, or False
|
||||
otherwise.
|
||||
"""
|
||||
# Assets
|
||||
if base_code[0] == "1":
|
||||
|
@ -82,7 +82,7 @@ class AccountForm(FlaskForm):
|
||||
"""The title."""
|
||||
is_need_offset = BooleanField(
|
||||
validators=[NoOffsetNominalAccount()])
|
||||
"""Whether the the entries of this account need offset."""
|
||||
"""Whether the the voucher line items of this account need offset."""
|
||||
|
||||
def populate_obj(self, obj: Account) -> None:
|
||||
"""Populates the form data into an account object.
|
||||
|
@ -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 entries of this account need offset."""
|
||||
"""Whether the voucher 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,8 @@ class Account(db.Model):
|
||||
l10n = db.relationship("AccountL10n", back_populates="account",
|
||||
lazy=False)
|
||||
"""The localized titles."""
|
||||
entries = db.relationship("JournalEntry", back_populates="account")
|
||||
"""The journal entries."""
|
||||
line_items = db.relationship("VoucherLineItem", back_populates="account")
|
||||
"""The voucher line items."""
|
||||
|
||||
CASH_CODE: str = "1111-001"
|
||||
"""The code of the cash account,"""
|
||||
@ -363,8 +363,8 @@ class Currency(db.Model):
|
||||
l10n = db.relationship("CurrencyL10n", back_populates="currency",
|
||||
lazy=False)
|
||||
"""The localized names."""
|
||||
entries = db.relationship("JournalEntry", back_populates="currency")
|
||||
"""The journal entries."""
|
||||
line_items = db.relationship("VoucherLineItem", back_populates="currency")
|
||||
"""The voucher line items."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Returns the string representation of the currency.
|
||||
@ -450,20 +450,20 @@ class CurrencyL10n(db.Model):
|
||||
class VoucherCurrency:
|
||||
"""A currency in a voucher."""
|
||||
|
||||
def __init__(self, code: str, debit: list[JournalEntry],
|
||||
credit: list[JournalEntry]):
|
||||
def __init__(self, code: str, debit: list[VoucherLineItem],
|
||||
credit: list[VoucherLineItem]):
|
||||
"""Constructs the currency in the voucher.
|
||||
|
||||
:param code: The currency code.
|
||||
:param debit: The debit entries.
|
||||
:param credit: The credit entries.
|
||||
:param debit: The debit line items.
|
||||
:param credit: The credit line items.
|
||||
"""
|
||||
self.code: str = code
|
||||
"""The currency code."""
|
||||
self.debit: list[JournalEntry] = debit
|
||||
"""The debit entries."""
|
||||
self.credit: list[JournalEntry] = credit
|
||||
"""The credit entries."""
|
||||
self.debit: list[VoucherLineItem] = debit
|
||||
"""The debit line items."""
|
||||
self.credit: list[VoucherLineItem] = credit
|
||||
"""The credit line items."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@ -475,17 +475,17 @@ class VoucherCurrency:
|
||||
|
||||
@property
|
||||
def debit_total(self) -> Decimal:
|
||||
"""Returns the total amount of the debit journal entries.
|
||||
"""Returns the total amount of the debit line items.
|
||||
|
||||
:return: The total amount of the debit journal entries.
|
||||
:return: The total amount of the debit line items.
|
||||
"""
|
||||
return sum([x.amount for x in self.debit])
|
||||
|
||||
@property
|
||||
def credit_total(self) -> str:
|
||||
"""Returns the total amount of the credit journal entries.
|
||||
"""Returns the total amount of the credit line items.
|
||||
|
||||
:return: The total amount of the credit journal entries.
|
||||
:return: The total amount of the credit line items.
|
||||
"""
|
||||
return sum([x.amount for x in self.credit])
|
||||
|
||||
@ -523,8 +523,8 @@ class Voucher(db.Model):
|
||||
"""The ID of the updator."""
|
||||
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
|
||||
"""The updator."""
|
||||
entries = db.relationship("JournalEntry", back_populates="voucher")
|
||||
"""The journal entries."""
|
||||
line_items = db.relationship("VoucherLineItem", back_populates="voucher")
|
||||
"""The line items."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Returns the string representation of this voucher.
|
||||
@ -539,18 +539,19 @@ class Voucher(db.Model):
|
||||
|
||||
@property
|
||||
def currencies(self) -> list[VoucherCurrency]:
|
||||
"""Returns the journal entries categorized by their currencies.
|
||||
"""Returns the line items categorized by their currencies.
|
||||
|
||||
:return: The currency categories.
|
||||
"""
|
||||
entries: list[JournalEntry] = sorted(self.entries, key=lambda x: x.no)
|
||||
line_items: list[VoucherLineItem] = sorted(self.line_items,
|
||||
key=lambda x: x.no)
|
||||
codes: list[str] = []
|
||||
by_currency: dict[str, list[JournalEntry]] = {}
|
||||
for entry in entries:
|
||||
if entry.currency_code not in by_currency:
|
||||
codes.append(entry.currency_code)
|
||||
by_currency[entry.currency_code] = []
|
||||
by_currency[entry.currency_code].append(entry)
|
||||
by_currency: dict[str, list[VoucherLineItem]] = {}
|
||||
for line_item in line_items:
|
||||
if line_item.currency_code not in by_currency:
|
||||
codes.append(line_item.currency_code)
|
||||
by_currency[line_item.currency_code] = []
|
||||
by_currency[line_item.currency_code].append(line_item)
|
||||
return [VoucherCurrency(code=x,
|
||||
debit=[y for y in by_currency[x]
|
||||
if y.is_debit],
|
||||
@ -593,8 +594,8 @@ class Voucher(db.Model):
|
||||
"""
|
||||
if not hasattr(self, "__can_delete"):
|
||||
def has_offset() -> bool:
|
||||
for entry in self.entries:
|
||||
if len(entry.offsets) > 0:
|
||||
for line_item in self.line_items:
|
||||
if len(line_item.offsets) > 0:
|
||||
return True
|
||||
return False
|
||||
setattr(self, "__can_delete", not has_offset())
|
||||
@ -605,50 +606,52 @@ class Voucher(db.Model):
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
JournalEntry.query\
|
||||
.filter(JournalEntry.voucher_id == self.id).delete()
|
||||
VoucherLineItem.query\
|
||||
.filter(VoucherLineItem.voucher_id == self.id).delete()
|
||||
db.session.delete(self)
|
||||
|
||||
|
||||
class JournalEntry(db.Model):
|
||||
"""An accounting journal entry."""
|
||||
__tablename__ = "accounting_journal_entries"
|
||||
class VoucherLineItem(db.Model):
|
||||
"""A line item in the voucher."""
|
||||
__tablename__ = "accounting_voucher_line_items"
|
||||
"""The table name."""
|
||||
id = db.Column(db.Integer, nullable=False, primary_key=True,
|
||||
autoincrement=False)
|
||||
"""The entry ID."""
|
||||
"""The line item ID."""
|
||||
voucher_id = db.Column(db.Integer,
|
||||
db.ForeignKey(Voucher.id, onupdate="CASCADE",
|
||||
ondelete="CASCADE"),
|
||||
nullable=False)
|
||||
"""The voucher ID."""
|
||||
voucher = db.relationship(Voucher, back_populates="entries")
|
||||
voucher = db.relationship(Voucher, back_populates="line_items")
|
||||
"""The voucher."""
|
||||
is_debit = db.Column(db.Boolean, nullable=False)
|
||||
"""True for a debit entry, or False for a credit entry."""
|
||||
"""True for a debit line item, or False for a credit line item."""
|
||||
no = db.Column(db.Integer, nullable=False)
|
||||
"""The entry number under the voucher and debit or credit."""
|
||||
original_entry_id = db.Column(db.Integer,
|
||||
db.ForeignKey(id, onupdate="CASCADE"),
|
||||
nullable=True)
|
||||
"""The ID of the original entry."""
|
||||
original_entry = db.relationship("JournalEntry", back_populates="offsets",
|
||||
remote_side=id, passive_deletes=True)
|
||||
"""The original entry."""
|
||||
offsets = db.relationship("JournalEntry", back_populates="original_entry")
|
||||
"""The offset entries."""
|
||||
"""The line item number under the voucher and debit or credit."""
|
||||
original_line_item_id = db.Column(db.Integer,
|
||||
db.ForeignKey(id, onupdate="CASCADE"),
|
||||
nullable=True)
|
||||
"""The ID of the original line item."""
|
||||
original_line_item = db.relationship("VoucherLineItem",
|
||||
back_populates="offsets",
|
||||
remote_side=id, passive_deletes=True)
|
||||
"""The original line item."""
|
||||
offsets = db.relationship("VoucherLineItem",
|
||||
back_populates="original_line_item")
|
||||
"""The offset items."""
|
||||
currency_code = db.Column(db.String,
|
||||
db.ForeignKey(Currency.code, onupdate="CASCADE"),
|
||||
nullable=False)
|
||||
"""The currency code."""
|
||||
currency = db.relationship(Currency, back_populates="entries")
|
||||
currency = db.relationship(Currency, back_populates="line_items")
|
||||
"""The currency."""
|
||||
account_id = db.Column(db.Integer,
|
||||
db.ForeignKey(Account.id,
|
||||
onupdate="CASCADE"),
|
||||
nullable=False)
|
||||
"""The account ID."""
|
||||
account = db.relationship(Account, back_populates="entries", lazy=False)
|
||||
account = db.relationship(Account, back_populates="line_items", lazy=False)
|
||||
"""The account."""
|
||||
summary = db.Column(db.String, nullable=True)
|
||||
"""The summary."""
|
||||
@ -656,9 +659,9 @@ class JournalEntry(db.Model):
|
||||
"""The amount."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Returns the string representation of the journal entry.
|
||||
"""Returns the string representation of the line item.
|
||||
|
||||
:return: The string representation of the journal entry.
|
||||
:return: The string representation of the line item.
|
||||
"""
|
||||
if not hasattr(self, "__str"):
|
||||
from accounting.template_filters import format_date, format_amount
|
||||
@ -672,10 +675,10 @@ class JournalEntry(db.Model):
|
||||
|
||||
@property
|
||||
def eid(self) -> int | None:
|
||||
"""Returns the journal entry ID. This is the alternative name of the
|
||||
"""Returns the line item ID. This is the alternative name of the
|
||||
ID field, to work with WTForms.
|
||||
|
||||
:return: The journal entry ID.
|
||||
:return: The line item ID.
|
||||
"""
|
||||
return self.id
|
||||
|
||||
@ -691,15 +694,15 @@ class JournalEntry(db.Model):
|
||||
def debit(self) -> Decimal | None:
|
||||
"""Returns the debit amount.
|
||||
|
||||
:return: The debit amount, or None if this is not a debit entry.
|
||||
:return: The debit amount, or None if this is not a debit line item.
|
||||
"""
|
||||
return self.amount if self.is_debit else None
|
||||
|
||||
@property
|
||||
def is_need_offset(self) -> bool:
|
||||
"""Returns whether the entry needs offset.
|
||||
"""Returns whether the line item needs offset.
|
||||
|
||||
:return: True if the entry needs offset, or False otherwise.
|
||||
:return: True if the line item needs offset, or False otherwise.
|
||||
"""
|
||||
if not self.account.is_need_offset:
|
||||
return False
|
||||
@ -713,7 +716,7 @@ class JournalEntry(db.Model):
|
||||
def credit(self) -> Decimal | None:
|
||||
"""Returns the credit amount.
|
||||
|
||||
:return: The credit amount, or None if this is not a credit entry.
|
||||
:return: The credit amount, or None if this is not a credit line item.
|
||||
"""
|
||||
return None if self.is_debit else self.amount
|
||||
|
||||
|
@ -25,7 +25,7 @@ from flask import render_template, Response
|
||||
from accounting import db
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, BaseAccount, Account, Voucher, \
|
||||
JournalEntry
|
||||
VoucherLineItem
|
||||
from accounting.report.period import Period, PeriodChooser
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
from accounting.report.utils.base_report import BaseReport
|
||||
@ -124,13 +124,13 @@ class AccountCollector:
|
||||
sub_conditions: list[sa.BinaryExpression] \
|
||||
= [Account.base_code.startswith(x) for x in {"1", "2", "3"}]
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [JournalEntry.currency_code == self.__currency.code,
|
||||
= [VoucherLineItem.currency_code == self.__currency.code,
|
||||
sa.or_(*sub_conditions)]
|
||||
if self.__period.end is not None:
|
||||
conditions.append(Voucher.date <= self.__period.end)
|
||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||
(JournalEntry.is_debit, JournalEntry.amount),
|
||||
else_=-JournalEntry.amount)).label("balance")
|
||||
(VoucherLineItem.is_debit, VoucherLineItem.amount),
|
||||
else_=-VoucherLineItem.amount)).label("balance")
|
||||
select_balance: sa.Select \
|
||||
= sa.select(Account.id, Account.base_code, Account.no,
|
||||
balance_func)\
|
||||
@ -178,7 +178,7 @@ class AccountCollector:
|
||||
if self.__period.start is None:
|
||||
return None
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [JournalEntry.currency_code == self.__currency.code,
|
||||
= [VoucherLineItem.currency_code == self.__currency.code,
|
||||
Voucher.date < self.__period.start]
|
||||
return self.__query_balance(conditions)
|
||||
|
||||
@ -197,7 +197,7 @@ class AccountCollector:
|
||||
:return: The net income or loss for current period.
|
||||
"""
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [JournalEntry.currency_code == self.__currency.code]
|
||||
= [VoucherLineItem.currency_code == self.__currency.code]
|
||||
if self.__period.start is not None:
|
||||
conditions.append(Voucher.date >= self.__period.start)
|
||||
if self.__period.end is not None:
|
||||
@ -215,8 +215,8 @@ class AccountCollector:
|
||||
conditions.extend([sa.not_(Account.base_code.startswith(x))
|
||||
for x in {"1", "2", "3"}])
|
||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||
(JournalEntry.is_debit, JournalEntry.amount),
|
||||
else_=-JournalEntry.amount))
|
||||
(VoucherLineItem.is_debit, VoucherLineItem.amount),
|
||||
else_=-VoucherLineItem.amount))
|
||||
select_balance: sa.Select = sa.select(balance_func)\
|
||||
.join(Voucher).join(Account).filter(*conditions)
|
||||
return db.session.scalar(select_balance)
|
||||
|
@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, Account, Voucher, JournalEntry
|
||||
from accounting.models import Currency, Account, Voucher, VoucherLineItem
|
||||
from accounting.report.period import Period, PeriodChooser
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
from accounting.report.utils.base_report import BaseReport
|
||||
@ -41,18 +41,18 @@ from accounting.utils.cast import be
|
||||
from accounting.utils.pagination import Pagination
|
||||
|
||||
|
||||
class ReportEntry:
|
||||
"""An entry in the report."""
|
||||
class ReportLineItem:
|
||||
"""A line item in the report."""
|
||||
|
||||
def __init__(self, entry: JournalEntry | None = None):
|
||||
"""Constructs the entry in the report.
|
||||
def __init__(self, line_item: VoucherLineItem | None = None):
|
||||
"""Constructs the line item in the report.
|
||||
|
||||
:param entry: The journal entry.
|
||||
:param line_item: The voucher line item.
|
||||
"""
|
||||
self.is_brought_forward: bool = False
|
||||
"""Whether this is the brought-forward entry."""
|
||||
"""Whether this is the brought-forward line item."""
|
||||
self.is_total: bool = False
|
||||
"""Whether this is the total entry."""
|
||||
"""Whether this is the total line item."""
|
||||
self.date: date | None = None
|
||||
"""The date."""
|
||||
self.account: Account | None = None
|
||||
@ -68,24 +68,24 @@ class ReportEntry:
|
||||
self.note: str | None = None
|
||||
"""The note."""
|
||||
self.url: str | None = None
|
||||
"""The URL to the journal entry."""
|
||||
if entry is not None:
|
||||
self.date = entry.voucher.date
|
||||
self.account = entry.account
|
||||
self.summary = entry.summary
|
||||
self.income = None if entry.is_debit else entry.amount
|
||||
self.expense = entry.amount if entry.is_debit else None
|
||||
self.note = entry.voucher.note
|
||||
"""The URL to the voucher line item."""
|
||||
if line_item is not None:
|
||||
self.date = line_item.voucher.date
|
||||
self.account = line_item.account
|
||||
self.summary = line_item.summary
|
||||
self.income = None if line_item.is_debit else line_item.amount
|
||||
self.expense = line_item.amount if line_item.is_debit else None
|
||||
self.note = line_item.voucher.note
|
||||
self.url = url_for("accounting.voucher.detail",
|
||||
voucher=entry.voucher)
|
||||
voucher=line_item.voucher)
|
||||
|
||||
|
||||
class EntryCollector:
|
||||
"""The report entry collector."""
|
||||
class LineItemCollector:
|
||||
"""The line item collector."""
|
||||
|
||||
def __init__(self, currency: Currency, account: IncomeExpensesAccount,
|
||||
period: Period):
|
||||
"""Constructs the report entry collector.
|
||||
"""Constructs the line item collector.
|
||||
|
||||
:param currency: The currency.
|
||||
:param account: The account.
|
||||
@ -97,74 +97,74 @@ class EntryCollector:
|
||||
"""The account."""
|
||||
self.__period: Period = period
|
||||
"""The period"""
|
||||
self.brought_forward: ReportEntry | None
|
||||
"""The brought-forward entry."""
|
||||
self.entries: list[ReportEntry]
|
||||
"""The log entries."""
|
||||
self.total: ReportEntry | None
|
||||
"""The total entry."""
|
||||
self.brought_forward = self.__get_brought_forward_entry()
|
||||
self.entries = self.__query_entries()
|
||||
self.total = self.__get_total_entry()
|
||||
self.brought_forward: ReportLineItem | None
|
||||
"""The brought-forward line item."""
|
||||
self.line_items: list[ReportLineItem]
|
||||
"""The line items."""
|
||||
self.total: ReportLineItem | None
|
||||
"""The total line item."""
|
||||
self.brought_forward = self.__get_brought_forward()
|
||||
self.line_items = self.__query_line_items()
|
||||
self.total = self.__get_total()
|
||||
self.__populate_balance()
|
||||
|
||||
def __get_brought_forward_entry(self) -> ReportEntry | None:
|
||||
"""Queries, composes and returns the brought-forward entry.
|
||||
def __get_brought_forward(self) -> ReportLineItem | None:
|
||||
"""Queries, composes and returns the brought-forward line item.
|
||||
|
||||
:return: The brought-forward entry, or None if the period starts from
|
||||
the beginning.
|
||||
:return: The brought-forward line item, or None if the period starts
|
||||
from the beginning.
|
||||
"""
|
||||
if self.__period.start is None:
|
||||
return None
|
||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||
(JournalEntry.is_debit, JournalEntry.amount),
|
||||
else_=-JournalEntry.amount))
|
||||
(VoucherLineItem.is_debit, VoucherLineItem.amount),
|
||||
else_=-VoucherLineItem.amount))
|
||||
select: sa.Select = sa.Select(balance_func)\
|
||||
.join(Voucher).join(Account)\
|
||||
.filter(be(JournalEntry.currency_code == self.__currency.code),
|
||||
.filter(be(VoucherLineItem.currency_code == self.__currency.code),
|
||||
self.__account_condition,
|
||||
Voucher.date < self.__period.start)
|
||||
balance: int | None = db.session.scalar(select)
|
||||
if balance is None:
|
||||
return None
|
||||
entry: ReportEntry = ReportEntry()
|
||||
entry.is_brought_forward = True
|
||||
entry.date = self.__period.start
|
||||
entry.account = Account.accumulated_change()
|
||||
entry.summary = gettext("Brought forward")
|
||||
line_item: ReportLineItem = ReportLineItem()
|
||||
line_item.is_brought_forward = True
|
||||
line_item.date = self.__period.start
|
||||
line_item.account = Account.accumulated_change()
|
||||
line_item.summary = gettext("Brought forward")
|
||||
if balance > 0:
|
||||
entry.income = balance
|
||||
line_item.income = balance
|
||||
elif balance < 0:
|
||||
entry.expense = -balance
|
||||
entry.balance = balance
|
||||
return entry
|
||||
line_item.expense = -balance
|
||||
line_item.balance = balance
|
||||
return line_item
|
||||
|
||||
def __query_entries(self) -> list[ReportEntry]:
|
||||
"""Queries and returns the log entries.
|
||||
def __query_line_items(self) -> list[ReportLineItem]:
|
||||
"""Queries and returns the line items.
|
||||
|
||||
:return: The log entries.
|
||||
:return: The line items.
|
||||
"""
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [JournalEntry.currency_code == self.__currency.code,
|
||||
= [VoucherLineItem.currency_code == self.__currency.code,
|
||||
self.__account_condition]
|
||||
if self.__period.start is not None:
|
||||
conditions.append(Voucher.date >= self.__period.start)
|
||||
if self.__period.end is not None:
|
||||
conditions.append(Voucher.date <= self.__period.end)
|
||||
voucher_with_account: sa.Select = sa.Select(Voucher.id).\
|
||||
join(JournalEntry).join(Account).filter(*conditions)
|
||||
join(VoucherLineItem).join(Account).filter(*conditions)
|
||||
|
||||
return [ReportEntry(x)
|
||||
for x in JournalEntry.query.join(Voucher).join(Account)
|
||||
.filter(JournalEntry.voucher_id.in_(voucher_with_account),
|
||||
JournalEntry.currency_code == self.__currency.code,
|
||||
return [ReportLineItem(x)
|
||||
for x in VoucherLineItem.query.join(Voucher).join(Account)
|
||||
.filter(VoucherLineItem.voucher_id.in_(voucher_with_account),
|
||||
VoucherLineItem.currency_code == self.__currency.code,
|
||||
sa.not_(self.__account_condition))
|
||||
.order_by(Voucher.date,
|
||||
Voucher.no,
|
||||
JournalEntry.is_debit,
|
||||
JournalEntry.no)
|
||||
.options(selectinload(JournalEntry.account),
|
||||
selectinload(JournalEntry.voucher))]
|
||||
VoucherLineItem.is_debit,
|
||||
VoucherLineItem.no)
|
||||
.options(selectinload(VoucherLineItem.account),
|
||||
selectinload(VoucherLineItem.voucher))]
|
||||
|
||||
@property
|
||||
def __account_condition(self) -> sa.BinaryExpression:
|
||||
@ -175,38 +175,39 @@ class EntryCollector:
|
||||
Account.base_code.startswith("22"))
|
||||
return Account.id == self.__account.id
|
||||
|
||||
def __get_total_entry(self) -> ReportEntry | None:
|
||||
"""Composes the total entry.
|
||||
def __get_total(self) -> ReportLineItem | None:
|
||||
"""Composes the total line item.
|
||||
|
||||
:return: The total entry, or None if there is no data.
|
||||
:return: The total line item, or None if there is no data.
|
||||
"""
|
||||
if self.brought_forward is None and len(self.entries) == 0:
|
||||
if self.brought_forward is None and len(self.line_items) == 0:
|
||||
return None
|
||||
entry: ReportEntry = ReportEntry()
|
||||
entry.is_total = True
|
||||
entry.summary = gettext("Total")
|
||||
entry.income = sum([x.income for x in self.entries
|
||||
if x.income is not None])
|
||||
entry.expense = sum([x.expense for x in self.entries
|
||||
if x.expense is not None])
|
||||
entry.balance = entry.income - entry.expense
|
||||
line_item: ReportLineItem = ReportLineItem()
|
||||
line_item.is_total = True
|
||||
line_item.summary = gettext("Total")
|
||||
line_item.income = sum([x.income for x in self.line_items
|
||||
if x.income is not None])
|
||||
line_item.expense = sum([x.expense for x in self.line_items
|
||||
if x.expense is not None])
|
||||
line_item.balance = line_item.income - line_item.expense
|
||||
if self.brought_forward is not None:
|
||||
entry.balance = self.brought_forward.balance + entry.balance
|
||||
return entry
|
||||
line_item.balance \
|
||||
= self.brought_forward.balance + line_item.balance
|
||||
return line_item
|
||||
|
||||
def __populate_balance(self) -> None:
|
||||
"""Populates the balance of the entries.
|
||||
"""Populates the balance of the line items.
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
balance: Decimal = 0 if self.brought_forward is None \
|
||||
else self.brought_forward.balance
|
||||
for entry in self.entries:
|
||||
if entry.income is not None:
|
||||
balance = balance + entry.income
|
||||
if entry.expense is not None:
|
||||
balance = balance - entry.expense
|
||||
entry.balance = balance
|
||||
for line_item in self.line_items:
|
||||
if line_item.income is not None:
|
||||
balance = balance + line_item.income
|
||||
if line_item.expense is not None:
|
||||
balance = balance - line_item.expense
|
||||
line_item.balance = balance
|
||||
|
||||
|
||||
class CSVRow(BaseCSVRow):
|
||||
@ -261,19 +262,19 @@ class PageParams(BasePageParams):
|
||||
account: IncomeExpensesAccount,
|
||||
period: Period,
|
||||
has_data: bool,
|
||||
pagination: Pagination[ReportEntry],
|
||||
brought_forward: ReportEntry | None,
|
||||
entries: list[ReportEntry],
|
||||
total: ReportEntry | None):
|
||||
pagination: Pagination[ReportLineItem],
|
||||
brought_forward: ReportLineItem | None,
|
||||
line_items: list[ReportLineItem],
|
||||
total: ReportLineItem | None):
|
||||
"""Constructs the HTML page parameters.
|
||||
|
||||
:param currency: The currency.
|
||||
:param account: The account.
|
||||
:param period: The period.
|
||||
:param has_data: True if there is any data, or False otherwise.
|
||||
:param brought_forward: The brought-forward entry.
|
||||
:param entries: The log entries.
|
||||
:param total: The total entry.
|
||||
:param brought_forward: The brought-forward line item.
|
||||
:param line_items: The line items.
|
||||
:param total: The total line item.
|
||||
"""
|
||||
self.currency: Currency = currency
|
||||
"""The currency."""
|
||||
@ -283,14 +284,14 @@ class PageParams(BasePageParams):
|
||||
"""The period."""
|
||||
self.__has_data: bool = has_data
|
||||
"""True if there is any data, or False otherwise."""
|
||||
self.pagination: Pagination[ReportEntry] = pagination
|
||||
self.pagination: Pagination[ReportLineItem] = pagination
|
||||
"""The pagination."""
|
||||
self.brought_forward: ReportEntry | None = brought_forward
|
||||
"""The brought-forward entry."""
|
||||
self.entries: list[ReportEntry] = entries
|
||||
"""The report entries."""
|
||||
self.total: ReportEntry | None = total
|
||||
"""The total entry."""
|
||||
self.brought_forward: ReportLineItem | None = brought_forward
|
||||
"""The brought-forward line item."""
|
||||
self.line_items: list[ReportLineItem] = line_items
|
||||
"""The line items."""
|
||||
self.total: ReportLineItem | None = total
|
||||
"""The total line item."""
|
||||
self.period_chooser: PeriodChooser = PeriodChooser(
|
||||
lambda x: income_expenses_url(currency, account, x))
|
||||
"""The period chooser."""
|
||||
@ -342,14 +343,14 @@ class PageParams(BasePageParams):
|
||||
income_expenses_url(self.currency, current_al,
|
||||
self.period),
|
||||
self.account.id == 0)]
|
||||
in_use: sa.Select = sa.Select(JournalEntry.account_id)\
|
||||
in_use: sa.Select = sa.Select(VoucherLineItem.account_id)\
|
||||
.join(Account)\
|
||||
.filter(be(JournalEntry.currency_code == self.currency.code),
|
||||
.filter(be(VoucherLineItem.currency_code == self.currency.code),
|
||||
sa.or_(Account.base_code.startswith("11"),
|
||||
Account.base_code.startswith("12"),
|
||||
Account.base_code.startswith("21"),
|
||||
Account.base_code.startswith("22")))\
|
||||
.group_by(JournalEntry.account_id)
|
||||
.group_by(VoucherLineItem.account_id)
|
||||
options.extend([OptionLink(str(x),
|
||||
income_expenses_url(
|
||||
self.currency,
|
||||
@ -378,14 +379,15 @@ class IncomeExpenses(BaseReport):
|
||||
"""The account."""
|
||||
self.__period: Period = period
|
||||
"""The period."""
|
||||
collector: EntryCollector = EntryCollector(
|
||||
collector: LineItemCollector = LineItemCollector(
|
||||
self.__currency, self.__account, self.__period)
|
||||
self.__brought_forward: ReportEntry | None = collector.brought_forward
|
||||
"""The brought-forward entry."""
|
||||
self.__entries: list[ReportEntry] = collector.entries
|
||||
"""The report entries."""
|
||||
self.__total: ReportEntry | None = collector.total
|
||||
"""The total entry."""
|
||||
self.__brought_forward: ReportLineItem | None \
|
||||
= collector.brought_forward
|
||||
"""The brought-forward line item."""
|
||||
self.__line_items: list[ReportLineItem] = collector.line_items
|
||||
"""The line items."""
|
||||
self.__total: ReportLineItem | None = collector.total
|
||||
"""The total line item."""
|
||||
|
||||
def csv(self) -> Response:
|
||||
"""Returns the report as CSV for download.
|
||||
@ -416,7 +418,7 @@ class IncomeExpenses(BaseReport):
|
||||
None))
|
||||
rows.extend([CSVRow(x.date, str(x.account).title(), x.summary,
|
||||
x.income, x.expense, x.balance, x.note)
|
||||
for x in self.__entries])
|
||||
for x in self.__line_items])
|
||||
if self.__total is not None:
|
||||
rows.append(CSVRow(gettext("Total"), None, None,
|
||||
self.__total.income, self.__total.expense,
|
||||
@ -428,31 +430,31 @@ class IncomeExpenses(BaseReport):
|
||||
|
||||
:return: The report as HTML.
|
||||
"""
|
||||
all_entries: list[ReportEntry] = []
|
||||
all_line_items: list[ReportLineItem] = []
|
||||
if self.__brought_forward is not None:
|
||||
all_entries.append(self.__brought_forward)
|
||||
all_entries.extend(self.__entries)
|
||||
all_line_items.append(self.__brought_forward)
|
||||
all_line_items.extend(self.__line_items)
|
||||
if self.__total is not None:
|
||||
all_entries.append(self.__total)
|
||||
pagination: Pagination[ReportEntry] \
|
||||
= Pagination[ReportEntry](all_entries, is_reversed=True)
|
||||
page_entries: list[ReportEntry] = pagination.list
|
||||
has_data: bool = len(page_entries) > 0
|
||||
brought_forward: ReportEntry | None = None
|
||||
if len(page_entries) > 0 and page_entries[0].is_brought_forward:
|
||||
brought_forward = page_entries[0]
|
||||
page_entries = page_entries[1:]
|
||||
total: ReportEntry | None = None
|
||||
if len(page_entries) > 0 and page_entries[-1].is_total:
|
||||
total = page_entries[-1]
|
||||
page_entries = page_entries[:-1]
|
||||
all_line_items.append(self.__total)
|
||||
pagination: Pagination[ReportLineItem] \
|
||||
= Pagination[ReportLineItem](all_line_items, is_reversed=True)
|
||||
page_line_items: list[ReportLineItem] = pagination.list
|
||||
has_data: bool = len(page_line_items) > 0
|
||||
brought_forward: ReportLineItem | None = None
|
||||
if len(page_line_items) > 0 and page_line_items[0].is_brought_forward:
|
||||
brought_forward = page_line_items[0]
|
||||
page_line_items = page_line_items[1:]
|
||||
total: ReportLineItem | None = None
|
||||
if len(page_line_items) > 0 and page_line_items[-1].is_total:
|
||||
total = page_line_items[-1]
|
||||
page_line_items = page_line_items[:-1]
|
||||
params: PageParams = PageParams(currency=self.__currency,
|
||||
account=self.__account,
|
||||
period=self.__period,
|
||||
has_data=has_data,
|
||||
pagination=pagination,
|
||||
brought_forward=brought_forward,
|
||||
entries=page_entries,
|
||||
line_items=page_line_items,
|
||||
total=total)
|
||||
return render_template("accounting/report/income-expenses.html",
|
||||
report=params)
|
||||
|
@ -25,7 +25,7 @@ from flask import render_template, Response
|
||||
from accounting import db
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, BaseAccount, Account, Voucher, \
|
||||
JournalEntry
|
||||
VoucherLineItem
|
||||
from accounting.report.period import Period, PeriodChooser
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
from accounting.report.utils.base_report import BaseReport
|
||||
@ -256,15 +256,15 @@ class IncomeStatement(BaseReport):
|
||||
sub_conditions: list[sa.BinaryExpression] \
|
||||
= [Account.base_code.startswith(str(x)) for x in range(4, 10)]
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [JournalEntry.currency_code == self.__currency.code,
|
||||
= [VoucherLineItem.currency_code == self.__currency.code,
|
||||
sa.or_(*sub_conditions)]
|
||||
if self.__period.start is not None:
|
||||
conditions.append(Voucher.date >= self.__period.start)
|
||||
if self.__period.end is not None:
|
||||
conditions.append(Voucher.date <= self.__period.end)
|
||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||
(JournalEntry.is_debit, -JournalEntry.amount),
|
||||
else_=JournalEntry.amount)).label("balance")
|
||||
(VoucherLineItem.is_debit, -VoucherLineItem.amount),
|
||||
else_=VoucherLineItem.amount)).label("balance")
|
||||
select_balances: sa.Select = sa.select(Account.id, balance_func)\
|
||||
.join(Voucher).join(Account)\
|
||||
.filter(*conditions)\
|
||||
|
@ -25,7 +25,7 @@ from flask import render_template, Response
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, Account, Voucher, JournalEntry
|
||||
from accounting.models import Currency, Account, Voucher, VoucherLineItem
|
||||
from accounting.report.period import Period, PeriodChooser
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
from accounting.report.utils.base_report import BaseReport
|
||||
@ -37,29 +37,29 @@ from accounting.report.utils.urls import journal_url
|
||||
from accounting.utils.pagination import Pagination
|
||||
|
||||
|
||||
class ReportEntry:
|
||||
"""An entry in the report."""
|
||||
class ReportLineItem:
|
||||
"""A line item in the report."""
|
||||
|
||||
def __init__(self, entry: JournalEntry):
|
||||
"""Constructs the entry in the report.
|
||||
def __init__(self, line_item: VoucherLineItem):
|
||||
"""Constructs the line item in the report.
|
||||
|
||||
:param entry: The journal entry.
|
||||
:param line_item: The voucher line item.
|
||||
"""
|
||||
self.entry: JournalEntry = entry
|
||||
"""The journal entry."""
|
||||
self.voucher: Voucher = entry.voucher
|
||||
self.line_item: VoucherLineItem = line_item
|
||||
"""The voucher line item."""
|
||||
self.voucher: Voucher = line_item.voucher
|
||||
"""The voucher."""
|
||||
self.currency: Currency = entry.currency
|
||||
self.currency: Currency = line_item.currency
|
||||
"""The account."""
|
||||
self.account: Account = entry.account
|
||||
self.account: Account = line_item.account
|
||||
"""The account."""
|
||||
self.summary: str | None = entry.summary
|
||||
self.summary: str | None = line_item.summary
|
||||
"""The summary."""
|
||||
self.debit: Decimal | None = entry.debit
|
||||
self.debit: Decimal | None = line_item.debit
|
||||
"""The debit amount."""
|
||||
self.credit: Decimal | None = entry.credit
|
||||
self.credit: Decimal | None = line_item.credit
|
||||
"""The credit amount."""
|
||||
self.amount: Decimal = entry.amount
|
||||
self.amount: Decimal = line_item.amount
|
||||
"""The amount."""
|
||||
|
||||
|
||||
@ -110,19 +110,19 @@ class PageParams(BasePageParams):
|
||||
"""The HTML page parameters."""
|
||||
|
||||
def __init__(self, period: Period,
|
||||
pagination: Pagination[JournalEntry],
|
||||
entries: list[JournalEntry]):
|
||||
pagination: Pagination[VoucherLineItem],
|
||||
line_items: list[VoucherLineItem]):
|
||||
"""Constructs the HTML page parameters.
|
||||
|
||||
:param period: The period.
|
||||
:param entries: The journal entries.
|
||||
:param line_items: The line items.
|
||||
"""
|
||||
self.period: Period = period
|
||||
"""The period."""
|
||||
self.pagination: Pagination[JournalEntry] = pagination
|
||||
self.pagination: Pagination[VoucherLineItem] = pagination
|
||||
"""The pagination."""
|
||||
self.entries: list[JournalEntry] = entries
|
||||
"""The entries."""
|
||||
self.line_items: list[VoucherLineItem] = line_items
|
||||
"""The line items."""
|
||||
self.period_chooser: PeriodChooser = PeriodChooser(
|
||||
lambda x: journal_url(x))
|
||||
"""The period chooser."""
|
||||
@ -133,7 +133,7 @@ class PageParams(BasePageParams):
|
||||
|
||||
:return: True if there is any data, or False otherwise.
|
||||
"""
|
||||
return len(self.entries) > 0
|
||||
return len(self.line_items) > 0
|
||||
|
||||
@property
|
||||
def report_chooser(self) -> ReportChooser:
|
||||
@ -145,10 +145,10 @@ class PageParams(BasePageParams):
|
||||
period=self.period)
|
||||
|
||||
|
||||
def get_csv_rows(entries: list[JournalEntry]) -> list[CSVRow]:
|
||||
"""Composes and returns the CSV rows from the report entries.
|
||||
def get_csv_rows(line_items: list[VoucherLineItem]) -> list[CSVRow]:
|
||||
"""Composes and returns the CSV rows from the line items.
|
||||
|
||||
:param entries: The report entries.
|
||||
:param line_items: The line items.
|
||||
:return: The CSV rows.
|
||||
"""
|
||||
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
|
||||
@ -158,7 +158,7 @@ def get_csv_rows(entries: list[JournalEntry]) -> list[CSVRow]:
|
||||
rows.extend([CSVRow(x.voucher.date, x.currency.code,
|
||||
str(x.account).title(), x.summary,
|
||||
x.debit, x.credit, x.voucher.note)
|
||||
for x in entries])
|
||||
for x in line_items])
|
||||
return rows
|
||||
|
||||
|
||||
@ -172,28 +172,28 @@ class Journal(BaseReport):
|
||||
"""
|
||||
self.__period: Period = period
|
||||
"""The period."""
|
||||
self.__entries: list[JournalEntry] = self.__query_entries()
|
||||
"""The journal entries."""
|
||||
self.__line_items: list[VoucherLineItem] = self.__query_line_items()
|
||||
"""The line items."""
|
||||
|
||||
def __query_entries(self) -> list[JournalEntry]:
|
||||
"""Queries and returns the journal entries.
|
||||
def __query_line_items(self) -> list[VoucherLineItem]:
|
||||
"""Queries and returns the line items.
|
||||
|
||||
:return: The journal entries.
|
||||
:return: The line items.
|
||||
"""
|
||||
conditions: list[sa.BinaryExpression] = []
|
||||
if self.__period.start is not None:
|
||||
conditions.append(Voucher.date >= self.__period.start)
|
||||
if self.__period.end is not None:
|
||||
conditions.append(Voucher.date <= self.__period.end)
|
||||
return JournalEntry.query.join(Voucher)\
|
||||
return VoucherLineItem.query.join(Voucher)\
|
||||
.filter(*conditions)\
|
||||
.order_by(Voucher.date,
|
||||
Voucher.no,
|
||||
JournalEntry.is_debit.desc(),
|
||||
JournalEntry.no)\
|
||||
.options(selectinload(JournalEntry.account),
|
||||
selectinload(JournalEntry.currency),
|
||||
selectinload(JournalEntry.voucher)).all()
|
||||
VoucherLineItem.is_debit.desc(),
|
||||
VoucherLineItem.no)\
|
||||
.options(selectinload(VoucherLineItem.account),
|
||||
selectinload(VoucherLineItem.currency),
|
||||
selectinload(VoucherLineItem.voucher)).all()
|
||||
|
||||
def csv(self) -> Response:
|
||||
"""Returns the report as CSV for download.
|
||||
@ -201,17 +201,17 @@ class Journal(BaseReport):
|
||||
:return: The response of the report for download.
|
||||
"""
|
||||
filename: str = f"journal-{period_spec(self.__period)}.csv"
|
||||
return csv_download(filename, get_csv_rows(self.__entries))
|
||||
return csv_download(filename, get_csv_rows(self.__line_items))
|
||||
|
||||
def html(self) -> str:
|
||||
"""Composes and returns the report as HTML.
|
||||
|
||||
:return: The report as HTML.
|
||||
"""
|
||||
pagination: Pagination[JournalEntry] \
|
||||
= Pagination[JournalEntry](self.__entries, is_reversed=True)
|
||||
pagination: Pagination[VoucherLineItem] \
|
||||
= Pagination[VoucherLineItem](self.__line_items, is_reversed=True)
|
||||
params: PageParams = PageParams(period=self.__period,
|
||||
pagination=pagination,
|
||||
entries=pagination.list)
|
||||
line_items=pagination.list)
|
||||
return render_template("accounting/report/journal.html",
|
||||
report=params)
|
||||
|
@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, Account, Voucher, JournalEntry
|
||||
from accounting.models import Currency, Account, Voucher, VoucherLineItem
|
||||
from accounting.report.period import Period, PeriodChooser
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
from accounting.report.utils.base_report import BaseReport
|
||||
@ -40,18 +40,18 @@ from accounting.utils.cast import be
|
||||
from accounting.utils.pagination import Pagination
|
||||
|
||||
|
||||
class ReportEntry:
|
||||
"""An entry in the report."""
|
||||
class ReportLineItem:
|
||||
"""A line item in the report."""
|
||||
|
||||
def __init__(self, entry: JournalEntry | None = None):
|
||||
"""Constructs the entry in the report.
|
||||
def __init__(self, line_item: VoucherLineItem | None = None):
|
||||
"""Constructs the line item in the report.
|
||||
|
||||
:param entry: The journal entry.
|
||||
:param line_item: The voucher line item.
|
||||
"""
|
||||
self.is_brought_forward: bool = False
|
||||
"""Whether this is the brought-forward entry."""
|
||||
"""Whether this is the brought-forward line item."""
|
||||
self.is_total: bool = False
|
||||
"""Whether this is the total entry."""
|
||||
"""Whether this is the total line item."""
|
||||
self.date: date | None = None
|
||||
"""The date."""
|
||||
self.summary: str | None = None
|
||||
@ -65,22 +65,22 @@ class ReportEntry:
|
||||
self.note: str | None = None
|
||||
"""The note."""
|
||||
self.url: str | None = None
|
||||
"""The URL to the journal entry."""
|
||||
if entry is not None:
|
||||
self.date = entry.voucher.date
|
||||
self.summary = entry.summary
|
||||
self.debit = entry.amount if entry.is_debit else None
|
||||
self.credit = None if entry.is_debit else entry.amount
|
||||
self.note = entry.voucher.note
|
||||
"""The URL to the voucher line item."""
|
||||
if line_item is not None:
|
||||
self.date = line_item.voucher.date
|
||||
self.summary = line_item.summary
|
||||
self.debit = line_item.amount if line_item.is_debit else None
|
||||
self.credit = None if line_item.is_debit else line_item.amount
|
||||
self.note = line_item.voucher.note
|
||||
self.url = url_for("accounting.voucher.detail",
|
||||
voucher=entry.voucher)
|
||||
voucher=line_item.voucher)
|
||||
|
||||
|
||||
class EntryCollector:
|
||||
"""The report entry collector."""
|
||||
class LineItemCollector:
|
||||
"""The line item collector."""
|
||||
|
||||
def __init__(self, currency: Currency, account: Account, period: Period):
|
||||
"""Constructs the report entry collector.
|
||||
"""Constructs the line item collector.
|
||||
|
||||
:param currency: The currency.
|
||||
:param account: The account.
|
||||
@ -92,89 +92,90 @@ class EntryCollector:
|
||||
"""The account."""
|
||||
self.__period: Period = period
|
||||
"""The period"""
|
||||
self.brought_forward: ReportEntry | None
|
||||
"""The brought-forward entry."""
|
||||
self.entries: list[ReportEntry]
|
||||
"""The report entries."""
|
||||
self.total: ReportEntry | None
|
||||
"""The total entry."""
|
||||
self.brought_forward = self.__get_brought_forward_entry()
|
||||
self.entries = self.__query_entries()
|
||||
self.total = self.__get_total_entry()
|
||||
self.brought_forward: ReportLineItem | None
|
||||
"""The brought-forward line item."""
|
||||
self.line_items: list[ReportLineItem]
|
||||
"""The line items."""
|
||||
self.total: ReportLineItem | None
|
||||
"""The total line item."""
|
||||
self.brought_forward = self.__get_brought_forward()
|
||||
self.line_items = self.__query_line_items()
|
||||
self.total = self.__get_total()
|
||||
self.__populate_balance()
|
||||
|
||||
def __get_brought_forward_entry(self) -> ReportEntry | None:
|
||||
"""Queries, composes and returns the brought-forward entry.
|
||||
def __get_brought_forward(self) -> ReportLineItem | None:
|
||||
"""Queries, composes and returns the brought-forward line item.
|
||||
|
||||
:return: The brought-forward entry, or None if the report starts from
|
||||
the beginning.
|
||||
:return: The brought-forward line item, or None if the report starts
|
||||
from the beginning.
|
||||
"""
|
||||
if self.__period.start is None:
|
||||
return None
|
||||
if self.__account.is_nominal:
|
||||
return None
|
||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||
(JournalEntry.is_debit, JournalEntry.amount),
|
||||
else_=-JournalEntry.amount))
|
||||
(VoucherLineItem.is_debit, VoucherLineItem.amount),
|
||||
else_=-VoucherLineItem.amount))
|
||||
select: sa.Select = sa.Select(balance_func).join(Voucher)\
|
||||
.filter(be(JournalEntry.currency_code == self.__currency.code),
|
||||
be(JournalEntry.account_id == self.__account.id),
|
||||
.filter(be(VoucherLineItem.currency_code == self.__currency.code),
|
||||
be(VoucherLineItem.account_id == self.__account.id),
|
||||
Voucher.date < self.__period.start)
|
||||
balance: int | None = db.session.scalar(select)
|
||||
if balance is None:
|
||||
return None
|
||||
entry: ReportEntry = ReportEntry()
|
||||
entry.is_brought_forward = True
|
||||
entry.date = self.__period.start
|
||||
entry.summary = gettext("Brought forward")
|
||||
line_item: ReportLineItem = ReportLineItem()
|
||||
line_item.is_brought_forward = True
|
||||
line_item.date = self.__period.start
|
||||
line_item.summary = gettext("Brought forward")
|
||||
if balance > 0:
|
||||
entry.debit = balance
|
||||
line_item.debit = balance
|
||||
elif balance < 0:
|
||||
entry.credit = -balance
|
||||
entry.balance = balance
|
||||
return entry
|
||||
line_item.credit = -balance
|
||||
line_item.balance = balance
|
||||
return line_item
|
||||
|
||||
def __query_entries(self) -> list[ReportEntry]:
|
||||
"""Queries and returns the report entries.
|
||||
def __query_line_items(self) -> list[ReportLineItem]:
|
||||
"""Queries and returns the line items.
|
||||
|
||||
:return: The report entries.
|
||||
:return: The line items.
|
||||
"""
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [JournalEntry.currency_code == self.__currency.code,
|
||||
JournalEntry.account_id == self.__account.id]
|
||||
= [VoucherLineItem.currency_code == self.__currency.code,
|
||||
VoucherLineItem.account_id == self.__account.id]
|
||||
if self.__period.start is not None:
|
||||
conditions.append(Voucher.date >= self.__period.start)
|
||||
if self.__period.end is not None:
|
||||
conditions.append(Voucher.date <= self.__period.end)
|
||||
return [ReportEntry(x) for x in JournalEntry.query.join(Voucher)
|
||||
return [ReportLineItem(x) for x in VoucherLineItem.query.join(Voucher)
|
||||
.filter(*conditions)
|
||||
.order_by(Voucher.date,
|
||||
Voucher.no,
|
||||
JournalEntry.is_debit.desc(),
|
||||
JournalEntry.no)
|
||||
.options(selectinload(JournalEntry.voucher)).all()]
|
||||
VoucherLineItem.is_debit.desc(),
|
||||
VoucherLineItem.no)
|
||||
.options(selectinload(VoucherLineItem.voucher)).all()]
|
||||
|
||||
def __get_total_entry(self) -> ReportEntry | None:
|
||||
"""Composes the total entry.
|
||||
def __get_total(self) -> ReportLineItem | None:
|
||||
"""Composes the total line item.
|
||||
|
||||
:return: The total entry, or None if there is no data.
|
||||
:return: The total line item, or None if there is no data.
|
||||
"""
|
||||
if self.brought_forward is None and len(self.entries) == 0:
|
||||
if self.brought_forward is None and len(self.line_items) == 0:
|
||||
return None
|
||||
entry: ReportEntry = ReportEntry()
|
||||
entry.is_total = True
|
||||
entry.summary = gettext("Total")
|
||||
entry.debit = sum([x.debit for x in self.entries
|
||||
if x.debit is not None])
|
||||
entry.credit = sum([x.credit for x in self.entries
|
||||
if x.credit is not None])
|
||||
entry.balance = entry.debit - entry.credit
|
||||
line_item: ReportLineItem = ReportLineItem()
|
||||
line_item.is_total = True
|
||||
line_item.summary = gettext("Total")
|
||||
line_item.debit = sum([x.debit for x in self.line_items
|
||||
if x.debit is not None])
|
||||
line_item.credit = sum([x.credit for x in self.line_items
|
||||
if x.credit is not None])
|
||||
line_item.balance = line_item.debit - line_item.credit
|
||||
if self.brought_forward is not None:
|
||||
entry.balance = self.brought_forward.balance + entry.balance
|
||||
return entry
|
||||
line_item.balance \
|
||||
= self.brought_forward.balance + line_item.balance
|
||||
return line_item
|
||||
|
||||
def __populate_balance(self) -> None:
|
||||
"""Populates the balance of the entries.
|
||||
"""Populates the balance of the line items.
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
@ -182,12 +183,12 @@ class EntryCollector:
|
||||
return None
|
||||
balance: Decimal = 0 if self.brought_forward is None \
|
||||
else self.brought_forward.balance
|
||||
for entry in self.entries:
|
||||
if entry.debit is not None:
|
||||
balance = balance + entry.debit
|
||||
if entry.credit is not None:
|
||||
balance = balance - entry.credit
|
||||
entry.balance = balance
|
||||
for line_item in self.line_items:
|
||||
if line_item.debit is not None:
|
||||
balance = balance + line_item.debit
|
||||
if line_item.credit is not None:
|
||||
balance = balance - line_item.credit
|
||||
line_item.balance = balance
|
||||
|
||||
|
||||
class CSVRow(BaseCSVRow):
|
||||
@ -238,19 +239,19 @@ class PageParams(BasePageParams):
|
||||
account: Account,
|
||||
period: Period,
|
||||
has_data: bool,
|
||||
pagination: Pagination[ReportEntry],
|
||||
brought_forward: ReportEntry | None,
|
||||
entries: list[ReportEntry],
|
||||
total: ReportEntry | None):
|
||||
pagination: Pagination[ReportLineItem],
|
||||
brought_forward: ReportLineItem | None,
|
||||
line_items: list[ReportLineItem],
|
||||
total: ReportLineItem | None):
|
||||
"""Constructs the HTML page parameters.
|
||||
|
||||
:param currency: The currency.
|
||||
:param account: The account.
|
||||
:param period: The period.
|
||||
:param has_data: True if there is any data, or False otherwise.
|
||||
:param brought_forward: The brought-forward entry.
|
||||
:param entries: The report entries.
|
||||
:param total: The total entry.
|
||||
:param brought_forward: The brought-forward line item.
|
||||
:param line_items: The line items.
|
||||
:param total: The total line item.
|
||||
"""
|
||||
self.currency: Currency = currency
|
||||
"""The currency."""
|
||||
@ -260,14 +261,14 @@ class PageParams(BasePageParams):
|
||||
"""The period."""
|
||||
self.__has_data: bool = has_data
|
||||
"""True if there is any data, or False otherwise."""
|
||||
self.pagination: Pagination[ReportEntry] = pagination
|
||||
self.pagination: Pagination[ReportLineItem] = pagination
|
||||
"""The pagination."""
|
||||
self.brought_forward: ReportEntry | None = brought_forward
|
||||
"""The brought-forward entry."""
|
||||
self.entries: list[ReportEntry] = entries
|
||||
"""The entries."""
|
||||
self.total: ReportEntry | None = total
|
||||
"""The total entry."""
|
||||
self.brought_forward: ReportLineItem | None = brought_forward
|
||||
"""The brought-forward line item."""
|
||||
self.line_items: list[ReportLineItem] = line_items
|
||||
"""The line items."""
|
||||
self.total: ReportLineItem | None = total
|
||||
"""The total line item."""
|
||||
self.period_chooser: PeriodChooser = PeriodChooser(
|
||||
lambda x: ledger_url(currency, account, x))
|
||||
"""The period chooser."""
|
||||
@ -306,9 +307,9 @@ class PageParams(BasePageParams):
|
||||
|
||||
:return: The account options.
|
||||
"""
|
||||
in_use: sa.Select = sa.Select(JournalEntry.account_id)\
|
||||
.filter(be(JournalEntry.currency_code == self.currency.code))\
|
||||
.group_by(JournalEntry.account_id)
|
||||
in_use: sa.Select = sa.Select(VoucherLineItem.account_id)\
|
||||
.filter(be(VoucherLineItem.currency_code == self.currency.code))\
|
||||
.group_by(VoucherLineItem.account_id)
|
||||
return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
|
||||
x.id == self.account.id)
|
||||
for x in Account.query.filter(Account.id.in_(in_use))
|
||||
@ -331,14 +332,15 @@ class Ledger(BaseReport):
|
||||
"""The account."""
|
||||
self.__period: Period = period
|
||||
"""The period."""
|
||||
collector: EntryCollector = EntryCollector(
|
||||
collector: LineItemCollector = LineItemCollector(
|
||||
self.__currency, self.__account, self.__period)
|
||||
self.__brought_forward: ReportEntry | None = collector.brought_forward
|
||||
"""The brought-forward entry."""
|
||||
self.__entries: list[ReportEntry] = collector.entries
|
||||
"""The report entries."""
|
||||
self.__total: ReportEntry | None = collector.total
|
||||
"""The total entry."""
|
||||
self.__brought_forward: ReportLineItem | None \
|
||||
= collector.brought_forward
|
||||
"""The brought-forward line item."""
|
||||
self.__line_items: list[ReportLineItem] = collector.line_items
|
||||
"""The line items."""
|
||||
self.__total: ReportLineItem | None = collector.total
|
||||
"""The total line item."""
|
||||
|
||||
def csv(self) -> Response:
|
||||
"""Returns the report as CSV for download.
|
||||
@ -367,7 +369,7 @@ class Ledger(BaseReport):
|
||||
None))
|
||||
rows.extend([CSVRow(x.date, x.summary,
|
||||
x.debit, x.credit, x.balance, x.note)
|
||||
for x in self.__entries])
|
||||
for x in self.__line_items])
|
||||
if self.__total is not None:
|
||||
rows.append(CSVRow(gettext("Total"), None,
|
||||
self.__total.debit, self.__total.credit,
|
||||
@ -379,31 +381,31 @@ class Ledger(BaseReport):
|
||||
|
||||
:return: The report as HTML.
|
||||
"""
|
||||
all_entries: list[ReportEntry] = []
|
||||
all_line_items: list[ReportLineItem] = []
|
||||
if self.__brought_forward is not None:
|
||||
all_entries.append(self.__brought_forward)
|
||||
all_entries.extend(self.__entries)
|
||||
all_line_items.append(self.__brought_forward)
|
||||
all_line_items.extend(self.__line_items)
|
||||
if self.__total is not None:
|
||||
all_entries.append(self.__total)
|
||||
pagination: Pagination[ReportEntry] \
|
||||
= Pagination[ReportEntry](all_entries, is_reversed=True)
|
||||
page_entries: list[ReportEntry] = pagination.list
|
||||
has_data: bool = len(page_entries) > 0
|
||||
brought_forward: ReportEntry | None = None
|
||||
if len(page_entries) > 0 and page_entries[0].is_brought_forward:
|
||||
brought_forward = page_entries[0]
|
||||
page_entries = page_entries[1:]
|
||||
total: ReportEntry | None = None
|
||||
if len(page_entries) > 0 and page_entries[-1].is_total:
|
||||
total = page_entries[-1]
|
||||
page_entries = page_entries[:-1]
|
||||
all_line_items.append(self.__total)
|
||||
pagination: Pagination[ReportLineItem] \
|
||||
= Pagination[ReportLineItem](all_line_items, is_reversed=True)
|
||||
page_line_items: list[ReportLineItem] = pagination.list
|
||||
has_data: bool = len(page_line_items) > 0
|
||||
brought_forward: ReportLineItem | None = None
|
||||
if len(page_line_items) > 0 and page_line_items[0].is_brought_forward:
|
||||
brought_forward = page_line_items[0]
|
||||
page_line_items = page_line_items[1:]
|
||||
total: ReportLineItem | None = None
|
||||
if len(page_line_items) > 0 and page_line_items[-1].is_total:
|
||||
total = page_line_items[-1]
|
||||
page_line_items = page_line_items[:-1]
|
||||
params: PageParams = PageParams(currency=self.__currency,
|
||||
account=self.__account,
|
||||
period=self.__period,
|
||||
has_data=has_data,
|
||||
pagination=pagination,
|
||||
brought_forward=brought_forward,
|
||||
entries=page_entries,
|
||||
line_items=page_line_items,
|
||||
total=total)
|
||||
return render_template("accounting/report/ledger.html",
|
||||
report=params)
|
||||
|
@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
|
||||
Voucher, JournalEntry
|
||||
Voucher, VoucherLineItem
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
from accounting.report.utils.base_report import BaseReport
|
||||
from accounting.report.utils.csv_export import csv_download
|
||||
@ -38,18 +38,18 @@ from accounting.utils.query import parse_query_keywords
|
||||
from .journal import get_csv_rows
|
||||
|
||||
|
||||
class EntryCollector:
|
||||
"""The report entry collector."""
|
||||
class LineItemCollector:
|
||||
"""The line item collector."""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructs the report entry collector."""
|
||||
self.entries: list[JournalEntry] = self.__query_entries()
|
||||
"""The report entries."""
|
||||
"""Constructs the line item collector."""
|
||||
self.line_items: list[VoucherLineItem] = self.__query_line_items()
|
||||
"""The line items."""
|
||||
|
||||
def __query_entries(self) -> list[JournalEntry]:
|
||||
"""Queries and returns the journal entries.
|
||||
def __query_line_items(self) -> list[VoucherLineItem]:
|
||||
"""Queries and returns the line items.
|
||||
|
||||
:return: The journal entries.
|
||||
:return: The line items.
|
||||
"""
|
||||
keywords: list[str] = parse_query_keywords(request.args.get("q"))
|
||||
if len(keywords) == 0:
|
||||
@ -57,26 +57,26 @@ class EntryCollector:
|
||||
conditions: list[sa.BinaryExpression] = []
|
||||
for k in keywords:
|
||||
sub_conditions: list[sa.BinaryExpression] \
|
||||
= [JournalEntry.summary.contains(k),
|
||||
JournalEntry.account_id.in_(
|
||||
= [VoucherLineItem.summary.contains(k),
|
||||
VoucherLineItem.account_id.in_(
|
||||
self.__get_account_condition(k)),
|
||||
JournalEntry.currency_code.in_(
|
||||
VoucherLineItem.currency_code.in_(
|
||||
self.__get_currency_condition(k)),
|
||||
JournalEntry.voucher_id.in_(
|
||||
VoucherLineItem.voucher_id.in_(
|
||||
self.__get_voucher_condition(k))]
|
||||
try:
|
||||
sub_conditions.append(JournalEntry.amount == Decimal(k))
|
||||
sub_conditions.append(VoucherLineItem.amount == Decimal(k))
|
||||
except ArithmeticError:
|
||||
pass
|
||||
conditions.append(sa.or_(*sub_conditions))
|
||||
return JournalEntry.query.join(Voucher).filter(*conditions)\
|
||||
return VoucherLineItem.query.join(Voucher).filter(*conditions)\
|
||||
.order_by(Voucher.date,
|
||||
Voucher.no,
|
||||
JournalEntry.is_debit,
|
||||
JournalEntry.no)\
|
||||
.options(selectinload(JournalEntry.account),
|
||||
selectinload(JournalEntry.currency),
|
||||
selectinload(JournalEntry.voucher)).all()
|
||||
VoucherLineItem.is_debit,
|
||||
VoucherLineItem.no)\
|
||||
.options(selectinload(VoucherLineItem.account),
|
||||
selectinload(VoucherLineItem.currency),
|
||||
selectinload(VoucherLineItem.voucher)).all()
|
||||
|
||||
@staticmethod
|
||||
def __get_account_condition(k: str) -> sa.Select:
|
||||
@ -149,16 +149,16 @@ class EntryCollector:
|
||||
class PageParams(BasePageParams):
|
||||
"""The HTML page parameters."""
|
||||
|
||||
def __init__(self, pagination: Pagination[JournalEntry],
|
||||
entries: list[JournalEntry]):
|
||||
def __init__(self, pagination: Pagination[VoucherLineItem],
|
||||
line_items: list[VoucherLineItem]):
|
||||
"""Constructs the HTML page parameters.
|
||||
|
||||
:param entries: The search result entries.
|
||||
:param line_items: The search result line items.
|
||||
"""
|
||||
self.pagination: Pagination[JournalEntry] = pagination
|
||||
self.pagination: Pagination[VoucherLineItem] = pagination
|
||||
"""The pagination."""
|
||||
self.entries: list[JournalEntry] = entries
|
||||
"""The entries."""
|
||||
self.line_items: list[VoucherLineItem] = line_items
|
||||
"""The line items."""
|
||||
|
||||
@property
|
||||
def has_data(self) -> bool:
|
||||
@ -166,7 +166,7 @@ class PageParams(BasePageParams):
|
||||
|
||||
:return: True if there is any data, or False otherwise.
|
||||
"""
|
||||
return len(self.entries) > 0
|
||||
return len(self.line_items) > 0
|
||||
|
||||
@property
|
||||
def report_chooser(self) -> ReportChooser:
|
||||
@ -182,8 +182,9 @@ class Search(BaseReport):
|
||||
|
||||
def __init__(self):
|
||||
"""Constructs a search."""
|
||||
self.__entries: list[JournalEntry] = EntryCollector().entries
|
||||
"""The journal entries."""
|
||||
self.__line_items: list[VoucherLineItem] \
|
||||
= LineItemCollector().line_items
|
||||
"""The line items."""
|
||||
|
||||
def csv(self) -> Response:
|
||||
"""Returns the report as CSV for download.
|
||||
@ -191,16 +192,16 @@ class Search(BaseReport):
|
||||
:return: The response of the report for download.
|
||||
"""
|
||||
filename: str = "search-{q}.csv".format(q=request.args["q"])
|
||||
return csv_download(filename, get_csv_rows(self.__entries))
|
||||
return csv_download(filename, get_csv_rows(self.__line_items))
|
||||
|
||||
def html(self) -> str:
|
||||
"""Composes and returns the report as HTML.
|
||||
|
||||
:return: The report as HTML.
|
||||
"""
|
||||
pagination: Pagination[JournalEntry] \
|
||||
= Pagination[JournalEntry](self.__entries, is_reversed=True)
|
||||
pagination: Pagination[VoucherLineItem] \
|
||||
= Pagination[VoucherLineItem](self.__line_items, is_reversed=True)
|
||||
params: PageParams = PageParams(pagination=pagination,
|
||||
entries=pagination.list)
|
||||
line_items=pagination.list)
|
||||
return render_template("accounting/report/search.html",
|
||||
report=params)
|
||||
|
@ -24,7 +24,7 @@ from flask import Response, render_template
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, Account, Voucher, JournalEntry
|
||||
from accounting.models import Currency, Account, Voucher, VoucherLineItem
|
||||
from accounting.report.period import Period, PeriodChooser
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
from accounting.report.utils.base_report import BaseReport
|
||||
@ -178,14 +178,14 @@ class TrialBalance(BaseReport):
|
||||
:return: None.
|
||||
"""
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [JournalEntry.currency_code == self.__currency.code]
|
||||
= [VoucherLineItem.currency_code == self.__currency.code]
|
||||
if self.__period.start is not None:
|
||||
conditions.append(Voucher.date >= self.__period.start)
|
||||
if self.__period.end is not None:
|
||||
conditions.append(Voucher.date <= self.__period.end)
|
||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||
(JournalEntry.is_debit, JournalEntry.amount),
|
||||
else_=-JournalEntry.amount)).label("balance")
|
||||
(VoucherLineItem.is_debit, VoucherLineItem.amount),
|
||||
else_=-VoucherLineItem.amount)).label("balance")
|
||||
select_balances: sa.Select = sa.select(Account.id, balance_func)\
|
||||
.join(Voucher).join(Account)\
|
||||
.filter(*conditions)\
|
||||
|
@ -26,7 +26,7 @@ import sqlalchemy as sa
|
||||
from flask import request
|
||||
|
||||
from accounting import db
|
||||
from accounting.models import Currency, JournalEntry
|
||||
from accounting.models import Currency, VoucherLineItem
|
||||
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(JournalEntry.currency_code)
|
||||
.group_by(JournalEntry.currency_code)).all())
|
||||
sa.select(VoucherLineItem.currency_code)
|
||||
.group_by(VoucherLineItem.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()]
|
||||
|
@ -117,29 +117,29 @@
|
||||
}
|
||||
|
||||
/* Links between objects */
|
||||
.accounting-original-entry {
|
||||
.accounting-original-line-item {
|
||||
border-top: thin solid darkslategray;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
.accounting-original-entry a {
|
||||
.accounting-original-line-item a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.accounting-original-entry a:hover {
|
||||
.accounting-original-line-item a:hover {
|
||||
color: inherit;
|
||||
}
|
||||
.accounting-offset-entries {
|
||||
.accounting-offset-line-items {
|
||||
border-top: thin solid darkslategray;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
.accounting-offset-entries ul li {
|
||||
.accounting-offset-line-items ul li {
|
||||
list-style: none;
|
||||
}
|
||||
.accounting-offset-entries ul li a {
|
||||
.accounting-offset-line-items ul li a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.accounting-offset-entries ul li a:hover {
|
||||
.accounting-offset-line-items ul li a:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@ -156,31 +156,28 @@
|
||||
.accounting-currency-content {
|
||||
width: calc(100% - 3rem);
|
||||
}
|
||||
.accounting-entry-content {
|
||||
.accounting-line-item-content {
|
||||
width: calc(100% - 3rem);
|
||||
background-color: transparent;
|
||||
}
|
||||
.accounting-entry-control {
|
||||
border-color: transparent;
|
||||
}
|
||||
.accounting-list-group-stripped .list-group-item:nth-child(2n+1) {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
.accounting-list-group-hover .list-group-item:hover {
|
||||
background-color: #ececec;
|
||||
}
|
||||
.accounting-voucher-entry {
|
||||
.accounting-voucher-line-item {
|
||||
border: none;
|
||||
}
|
||||
.accounting-voucher-entry-header {
|
||||
.accounting-voucher-line-item-header {
|
||||
font-weight: bolder;
|
||||
border-bottom: thick double slategray;
|
||||
}
|
||||
.list-group-item.accounting-voucher-entry-total {
|
||||
.list-group-item.accounting-voucher-line-item-total {
|
||||
font-weight: bolder;
|
||||
border-top: thick double slategray;
|
||||
}
|
||||
.accounting-entry-editor-original-entry-content {
|
||||
.accounting-line-item-editor-original-line-item-content {
|
||||
width: calc(100% - 3rem);
|
||||
}
|
||||
|
||||
|
@ -29,16 +29,16 @@
|
||||
class AccountSelector {
|
||||
|
||||
/**
|
||||
* The journal entry editor
|
||||
* @type {JournalEntryEditor}
|
||||
* The line item editor
|
||||
* @type {VoucherLineItemEditor}
|
||||
*/
|
||||
#entryEditor;
|
||||
#lineItemEditor;
|
||||
|
||||
/**
|
||||
* The entry type
|
||||
* The side, either "debit" or "credit"
|
||||
* @type {string}
|
||||
*/
|
||||
#entryType;
|
||||
#side;
|
||||
|
||||
/**
|
||||
* The prefix of the HTML ID and class
|
||||
@ -85,13 +85,13 @@ class AccountSelector {
|
||||
/**
|
||||
* Constructs an account selector.
|
||||
*
|
||||
* @param entryEditor {JournalEntryEditor} the journal entry editor
|
||||
* @param entryType {string} the entry type, either "debit" or "credit"
|
||||
* @param lineItemEditor {VoucherLineItemEditor} the line item editor
|
||||
* @param side {string} the side, either "debit" or "credit"
|
||||
*/
|
||||
constructor(entryEditor, entryType) {
|
||||
this.#entryEditor = entryEditor
|
||||
this.#entryType = entryType;
|
||||
this.#prefix = "accounting-account-selector-" + entryType;
|
||||
constructor(lineItemEditor, side) {
|
||||
this.#lineItemEditor = lineItemEditor
|
||||
this.#side = side;
|
||||
this.#prefix = "accounting-account-selector-" + side;
|
||||
this.#query = document.getElementById(this.#prefix + "-query");
|
||||
this.#queryNoResult = document.getElementById(this.#prefix + "-option-no-result");
|
||||
this.#optionList = document.getElementById(this.#prefix + "-option-list");
|
||||
@ -103,9 +103,9 @@ class AccountSelector {
|
||||
this.#more.classList.add("d-none");
|
||||
this.#filterOptions();
|
||||
};
|
||||
this.#clearButton.onclick = () => this.#entryEditor.clearAccount();
|
||||
this.#clearButton.onclick = () => this.#lineItemEditor.clearAccount();
|
||||
for (const option of this.#options) {
|
||||
option.onclick = () => this.#entryEditor.saveAccount(option.dataset.code, option.dataset.content, option.classList.contains("accounting-account-is-need-offset"));
|
||||
option.onclick = () => this.#lineItemEditor.saveAccount(option.dataset.code, option.dataset.content, option.classList.contains("accounting-account-is-need-offset"));
|
||||
}
|
||||
this.#query.addEventListener("input", () => {
|
||||
this.#filterOptions();
|
||||
@ -143,9 +143,9 @@ class AccountSelector {
|
||||
* @return {string[]} the account codes that are used in the form
|
||||
*/
|
||||
#getCodesUsedInForm() {
|
||||
const inUse = this.#entryEditor.form.getAccountCodesUsed(this.#entryType);
|
||||
if (this.#entryEditor.accountCode !== null) {
|
||||
inUse.push(this.#entryEditor.accountCode);
|
||||
const inUse = this.#lineItemEditor.form.getAccountCodesUsed(this.#side);
|
||||
if (this.#lineItemEditor.accountCode !== null) {
|
||||
inUse.push(this.#lineItemEditor.accountCode);
|
||||
}
|
||||
return inUse
|
||||
}
|
||||
@ -190,13 +190,13 @@ class AccountSelector {
|
||||
this.#more.classList.remove("d-none");
|
||||
this.#filterOptions();
|
||||
for (const option of this.#options) {
|
||||
if (option.dataset.code === this.#entryEditor.accountCode) {
|
||||
if (option.dataset.code === this.#lineItemEditor.accountCode) {
|
||||
option.classList.add("active");
|
||||
} else {
|
||||
option.classList.remove("active");
|
||||
}
|
||||
}
|
||||
if (this.#entryEditor.accountCode === null) {
|
||||
if (this.#lineItemEditor.accountCode === null) {
|
||||
this.#clearButton.classList.add("btn-secondary");
|
||||
this.#clearButton.classList.remove("btn-danger");
|
||||
this.#clearButton.disabled = true;
|
||||
@ -210,14 +210,14 @@ class AccountSelector {
|
||||
/**
|
||||
* Returns the account selector instances.
|
||||
*
|
||||
* @param entryEditor {JournalEntryEditor} the journal entry editor
|
||||
* @param lineItemEditor {VoucherLineItemEditor} the line item editor
|
||||
* @return {{debit: AccountSelector, credit: AccountSelector}}
|
||||
*/
|
||||
static getInstances(entryEditor) {
|
||||
static getInstances(lineItemEditor) {
|
||||
const selectors = {}
|
||||
const modals = Array.from(document.getElementsByClassName("accounting-account-selector"));
|
||||
for (const modal of modals) {
|
||||
selectors[modal.dataset.entryType] = new AccountSelector(entryEditor, modal.dataset.entryType);
|
||||
selectors[modal.dataset.side] = new AccountSelector(lineItemEditor, modal.dataset.side);
|
||||
}
|
||||
return selectors;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
* original-entry-selector.js: The JavaScript for the original entry selector
|
||||
* original-line-item-selector.js: The JavaScript for the original line item selector
|
||||
*/
|
||||
|
||||
/* Copyright (c) 2023 imacat.
|
||||
@ -23,22 +23,22 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The original entry selector.
|
||||
* The original line item selector.
|
||||
*
|
||||
*/
|
||||
class OriginalEntrySelector {
|
||||
class OriginalLineItemSelector {
|
||||
|
||||
/**
|
||||
* The journal entry editor
|
||||
* @type {JournalEntryEditor}
|
||||
* The line item editor
|
||||
* @type {VoucherLineItemEditor}
|
||||
*/
|
||||
entryEditor;
|
||||
lineItemEditor;
|
||||
|
||||
/**
|
||||
* The prefix of the HTML ID and class
|
||||
* @type {string}
|
||||
*/
|
||||
#prefix = "accounting-original-entry-selector";
|
||||
#prefix = "accounting-original-line-item-selector";
|
||||
|
||||
/**
|
||||
* The query input
|
||||
@ -60,13 +60,13 @@ class OriginalEntrySelector {
|
||||
|
||||
/**
|
||||
* The options
|
||||
* @type {OriginalEntry[]}
|
||||
* @type {OriginalLineItem[]}
|
||||
*/
|
||||
#options;
|
||||
|
||||
/**
|
||||
* The options by their ID
|
||||
* @type {Object.<string, OriginalEntry>}
|
||||
* @type {Object.<string, OriginalLineItem>}
|
||||
*/
|
||||
#optionById;
|
||||
|
||||
@ -77,21 +77,21 @@ class OriginalEntrySelector {
|
||||
#currencyCode;
|
||||
|
||||
/**
|
||||
* The entry
|
||||
* The side, either "credit" or "debit"
|
||||
*/
|
||||
#entryType;
|
||||
#side;
|
||||
|
||||
/**
|
||||
* Constructs an original entry selector.
|
||||
* Constructs an original line item selector.
|
||||
*
|
||||
* @param entryEditor {JournalEntryEditor} the journal entry editor
|
||||
* @param lineItemEditor {VoucherLineItemEditor} the line item editor
|
||||
*/
|
||||
constructor(entryEditor) {
|
||||
this.entryEditor = entryEditor;
|
||||
constructor(lineItemEditor) {
|
||||
this.lineItemEditor = lineItemEditor;
|
||||
this.#query = document.getElementById(this.#prefix + "-query");
|
||||
this.#queryNoResult = document.getElementById(this.#prefix + "-option-no-result");
|
||||
this.#optionList = document.getElementById(this.#prefix + "-option-list");
|
||||
this.#options = Array.from(document.getElementsByClassName(this.#prefix + "-option")).map((element) => new OriginalEntry(this, element));
|
||||
this.#options = Array.from(document.getElementsByClassName(this.#prefix + "-option")).map((element) => new OriginalLineItem(this, element));
|
||||
this.#optionById = {};
|
||||
for (const option of this.#options) {
|
||||
this.#optionById[option.id] = option;
|
||||
@ -102,44 +102,44 @@ class OriginalEntrySelector {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the net balance for an original entry.
|
||||
* Returns the net balance for an original line item.
|
||||
*
|
||||
* @param currentEntry {JournalEntrySubForm} the journal entry sub-form that is currently editing
|
||||
* @param currentLineItem {LineItemSubForm} the line item sub-form that is currently editing
|
||||
* @param form {VoucherForm} the voucher form
|
||||
* @param originalEntryId {string} the ID of the original entry
|
||||
* @return {Decimal} the net balance of the original entry
|
||||
* @param originalLineItemId {string} the ID of the original line item
|
||||
* @return {Decimal} the net balance of the original line item
|
||||
*/
|
||||
getNetBalance(currentEntry, form, originalEntryId) {
|
||||
const otherEntries = form.getEntries().filter((entry) => entry !== currentEntry);
|
||||
getNetBalance(currentLineItem, form, originalLineItemId) {
|
||||
const otherLineItems = form.getLineItems().filter((lineItem) => lineItem !== currentLineItem);
|
||||
let otherOffset = new Decimal(0);
|
||||
for (const otherEntry of otherEntries) {
|
||||
if (otherEntry.getOriginalEntryId() === originalEntryId) {
|
||||
const amount = otherEntry.getAmount();
|
||||
for (const otherLineItem of otherLineItems) {
|
||||
if (otherLineItem.getOriginalLineItemId() === originalLineItemId) {
|
||||
const amount = otherLineItem.getAmount();
|
||||
if (amount !== null) {
|
||||
otherOffset = otherOffset.plus(amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.#optionById[originalEntryId].bareNetBalance.minus(otherOffset);
|
||||
return this.#optionById[originalLineItemId].bareNetBalance.minus(otherOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the net balances, subtracting the offset amounts on the form but the currently editing journal entry
|
||||
* Updates the net balances, subtracting the offset amounts on the form but the currently editing line item
|
||||
*
|
||||
*/
|
||||
#updateNetBalances() {
|
||||
const otherEntries = this.entryEditor.form.getEntries().filter((entry) => entry !== this.entryEditor.entry);
|
||||
const otherLineItems = this.lineItemEditor.form.getLineItems().filter((lineItem) => lineItem !== this.lineItemEditor.lineItem);
|
||||
const otherOffsets = {}
|
||||
for (const otherEntry of otherEntries) {
|
||||
const otherOriginalEntryId = otherEntry.getOriginalEntryId();
|
||||
const amount = otherEntry.getAmount();
|
||||
if (otherOriginalEntryId === null || amount === null) {
|
||||
for (const otherLineItem of otherLineItems) {
|
||||
const otherOriginalLineItemId = otherLineItem.getOriginalLineItemId();
|
||||
const amount = otherLineItem.getAmount();
|
||||
if (otherOriginalLineItemId === null || amount === null) {
|
||||
continue;
|
||||
}
|
||||
if (!(otherOriginalEntryId in otherOffsets)) {
|
||||
otherOffsets[otherOriginalEntryId] = new Decimal("0");
|
||||
if (!(otherOriginalLineItemId in otherOffsets)) {
|
||||
otherOffsets[otherOriginalLineItemId] = new Decimal("0");
|
||||
}
|
||||
otherOffsets[otherOriginalEntryId] = otherOffsets[otherOriginalEntryId].plus(amount);
|
||||
otherOffsets[otherOriginalLineItemId] = otherOffsets[otherOriginalLineItemId].plus(amount);
|
||||
}
|
||||
for (const option of this.#options) {
|
||||
if (option.id in otherOffsets) {
|
||||
@ -157,7 +157,7 @@ class OriginalEntrySelector {
|
||||
#filterOptions() {
|
||||
let hasAnyMatched = false;
|
||||
for (const option of this.#options) {
|
||||
if (option.isMatched(this.#entryType, this.#currencyCode, this.#query.value)) {
|
||||
if (option.isMatched(this.#side, this.#currencyCode, this.#query.value)) {
|
||||
option.setShown(true);
|
||||
hasAnyMatched = true;
|
||||
} else {
|
||||
@ -174,14 +174,14 @@ class OriginalEntrySelector {
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback when the original entry selector is shown.
|
||||
* The callback when the original line item selector is shown.
|
||||
*
|
||||
*/
|
||||
onOpen() {
|
||||
this.#currencyCode = this.entryEditor.getCurrencyCode();
|
||||
this.#entryType = this.entryEditor.entryType;
|
||||
this.#currencyCode = this.lineItemEditor.getCurrencyCode();
|
||||
this.#side = this.lineItemEditor.side;
|
||||
for (const option of this.#options) {
|
||||
option.setActive(option.id === this.entryEditor.originalEntryId);
|
||||
option.setActive(option.id === this.lineItemEditor.originalLineItemId);
|
||||
}
|
||||
this.#query.value = "";
|
||||
this.#updateNetBalances();
|
||||
@ -190,14 +190,14 @@ class OriginalEntrySelector {
|
||||
}
|
||||
|
||||
/**
|
||||
* An original entry.
|
||||
* An original line item.
|
||||
*
|
||||
*/
|
||||
class OriginalEntry {
|
||||
class OriginalLineItem {
|
||||
|
||||
/**
|
||||
* The original entry selector
|
||||
* @type {OriginalEntrySelector}
|
||||
* The original line item selector
|
||||
* @type {OriginalLineItemSelector}
|
||||
*/
|
||||
#selector;
|
||||
|
||||
@ -220,10 +220,10 @@ class OriginalEntry {
|
||||
date;
|
||||
|
||||
/**
|
||||
* The entry type, either "debit" or "credit"
|
||||
* The side, either "debit" or "credit"
|
||||
* @type {string}
|
||||
*/
|
||||
#entryType;
|
||||
#side;
|
||||
|
||||
/**
|
||||
* The currency code
|
||||
@ -268,7 +268,7 @@ class OriginalEntry {
|
||||
netBalanceText;
|
||||
|
||||
/**
|
||||
* The text representation of the original entry
|
||||
* The text representation of the original line item
|
||||
* @type {string}
|
||||
*/
|
||||
text;
|
||||
@ -280,9 +280,9 @@ class OriginalEntry {
|
||||
#queryValues;
|
||||
|
||||
/**
|
||||
* Constructs an original entry.
|
||||
* Constructs an original line item.
|
||||
*
|
||||
* @param selector {OriginalEntrySelector} the original entry selector
|
||||
* @param selector {OriginalLineItemSelector} the original line item selector
|
||||
* @param element {HTMLLIElement} the element
|
||||
*/
|
||||
constructor(selector, element) {
|
||||
@ -290,17 +290,17 @@ class OriginalEntry {
|
||||
this.#element = element;
|
||||
this.id = element.dataset.id;
|
||||
this.date = element.dataset.date;
|
||||
this.#entryType = element.dataset.entryType;
|
||||
this.#side = element.dataset.side;
|
||||
this.#currencyCode = element.dataset.currencyCode;
|
||||
this.accountCode = element.dataset.accountCode;
|
||||
this.accountText = element.dataset.accountText;
|
||||
this.summary = element.dataset.summary;
|
||||
this.bareNetBalance = new Decimal(element.dataset.netBalance);
|
||||
this.netBalance = this.bareNetBalance;
|
||||
this.netBalanceText = document.getElementById("accounting-original-entry-selector-option-" + this.id + "-net-balance");
|
||||
this.netBalanceText = document.getElementById("accounting-original-line-item-selector-option-" + this.id + "-net-balance");
|
||||
this.text = element.dataset.text;
|
||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||
this.#element.onclick = () => this.#selector.entryEditor.saveOriginalEntry(this);
|
||||
this.#element.onclick = () => this.#selector.lineItemEditor.saveOriginalLineItem(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -335,31 +335,31 @@ class OriginalEntry {
|
||||
/**
|
||||
* Returns whether the original matches.
|
||||
*
|
||||
* @param entryType {string} the entry type, either "debit" or "credit"
|
||||
* @param side {string} the side, either "debit" or "credit"
|
||||
* @param currencyCode {string} the currency code
|
||||
* @param query {string|null} the query term
|
||||
*/
|
||||
isMatched(entryType, currencyCode, query = null) {
|
||||
isMatched(side, currencyCode, query = null) {
|
||||
return this.netBalance.greaterThan(0)
|
||||
&& this.date <= this.#selector.entryEditor.form.getDate()
|
||||
&& this.#isEntryTypeMatches(entryType)
|
||||
&& this.date <= this.#selector.lineItemEditor.form.getDate()
|
||||
&& this.#isSideMatches(side)
|
||||
&& this.#currencyCode === currencyCode
|
||||
&& this.#isQueryMatches(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the original entry matches the entry type.
|
||||
* Returns whether the original line item matches the debit or credit side.
|
||||
*
|
||||
* @param entryType {string} the entry type, either "debit" or credit
|
||||
* @param side {string} the side, either "debit" or credit
|
||||
* @return {boolean} true if the option matches, or false otherwise
|
||||
*/
|
||||
#isEntryTypeMatches(entryType) {
|
||||
return (entryType === "debit" && this.#entryType === "credit")
|
||||
|| (entryType === "credit" && this.#entryType === "debit");
|
||||
#isSideMatches(side) {
|
||||
return (side === "debit" && this.#side === "credit")
|
||||
|| (side === "credit" && this.#side === "debit");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the original entry matches the query.
|
||||
* Returns whether the original line item matches the query.
|
||||
*
|
||||
* @param query {string|null} the query term
|
||||
* @return {boolean} true if the option matches, or false otherwise
|
@ -29,10 +29,10 @@
|
||||
class SummaryEditor {
|
||||
|
||||
/**
|
||||
* The journal entry editor
|
||||
* @type {JournalEntryEditor}
|
||||
* The line item editor
|
||||
* @type {VoucherLineItemEditor}
|
||||
*/
|
||||
#entryEditor;
|
||||
#lineItemEditor;
|
||||
|
||||
/**
|
||||
* The summary editor form
|
||||
@ -53,10 +53,10 @@ class SummaryEditor {
|
||||
#modal;
|
||||
|
||||
/**
|
||||
* The entry type, either "debit" or "credit"
|
||||
* The side, either "debit" or "credit"
|
||||
* @type {string}
|
||||
*/
|
||||
entryType;
|
||||
side;
|
||||
|
||||
/**
|
||||
* The current tab
|
||||
@ -71,7 +71,7 @@ class SummaryEditor {
|
||||
summary;
|
||||
|
||||
/**
|
||||
* The button to the original entry selector
|
||||
* The button to the original line item selector
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
#offsetButton;
|
||||
@ -109,13 +109,13 @@ class SummaryEditor {
|
||||
/**
|
||||
* Constructs a summary editor.
|
||||
*
|
||||
* @param entryEditor {JournalEntryEditor} the journal entry editor
|
||||
* @param entryType {string} the entry type, either "debit" or "credit"
|
||||
* @param lineItemEditor {VoucherLineItemEditor} the line item editor
|
||||
* @param side {string} the side, either "debit" or "credit"
|
||||
*/
|
||||
constructor(entryEditor, entryType) {
|
||||
this.#entryEditor = entryEditor;
|
||||
this.entryType = entryType;
|
||||
this.prefix = "accounting-summary-editor-" + entryType;
|
||||
constructor(lineItemEditor, side) {
|
||||
this.#lineItemEditor = lineItemEditor;
|
||||
this.side = side;
|
||||
this.prefix = "accounting-summary-editor-" + side;
|
||||
this.#form = document.getElementById(this.prefix);
|
||||
this.#modal = document.getElementById(this.prefix + "-modal");
|
||||
this.summary = document.getElementById(this.prefix + "-summary");
|
||||
@ -132,7 +132,7 @@ class SummaryEditor {
|
||||
this.currentTab = this.tabPlanes.general;
|
||||
this.#initializeSuggestedAccounts();
|
||||
this.summary.onchange = () => this.#onSummaryChange();
|
||||
this.#offsetButton.onclick = () => this.#entryEditor.originalEntrySelector.onOpen();
|
||||
this.#offsetButton.onclick = () => this.#lineItemEditor.originalLineItemSelector.onOpen();
|
||||
this.#form.onsubmit = () => {
|
||||
if (this.currentTab.validate()) {
|
||||
this.#submit();
|
||||
@ -215,9 +215,9 @@ class SummaryEditor {
|
||||
#submit() {
|
||||
bootstrap.Modal.getOrCreateInstance(this.#modal).hide();
|
||||
if (this.#selectedAccount !== null) {
|
||||
this.#entryEditor.saveSummaryWithAccount(this.summary.value, this.#selectedAccount.dataset.code, this.#selectedAccount.dataset.text, this.#selectedAccount.classList.contains("accounting-account-is-need-offset"));
|
||||
this.#lineItemEditor.saveSummaryWithAccount(this.summary.value, this.#selectedAccount.dataset.code, this.#selectedAccount.dataset.text, this.#selectedAccount.classList.contains("accounting-account-is-need-offset"));
|
||||
} else {
|
||||
this.#entryEditor.saveSummary(this.summary.value);
|
||||
this.#lineItemEditor.saveSummary(this.summary.value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,7 +227,7 @@ class SummaryEditor {
|
||||
*/
|
||||
onOpen() {
|
||||
this.#reset();
|
||||
this.summary.value = this.#entryEditor.summary === null? "": this.#entryEditor.summary;
|
||||
this.summary.value = this.#lineItemEditor.summary === null? "": this.#lineItemEditor.summary;
|
||||
this.#onSummaryChange();
|
||||
}
|
||||
|
||||
@ -246,14 +246,14 @@ class SummaryEditor {
|
||||
/**
|
||||
* Returns the summary editor instances.
|
||||
*
|
||||
* @param entryEditor {JournalEntryEditor} the journal entry editor
|
||||
* @param lineItemEditor {VoucherLineItemEditor} the line item editor
|
||||
* @return {{debit: SummaryEditor, credit: SummaryEditor}}
|
||||
*/
|
||||
static getInstances(entryEditor) {
|
||||
static getInstances(lineItemEditor) {
|
||||
const editors = {}
|
||||
const forms = Array.from(document.getElementsByClassName("accounting-summary-editor"));
|
||||
for (const form of forms) {
|
||||
editors[form.dataset.entryType] = new SummaryEditor(entryEditor, form.dataset.entryType);
|
||||
editors[form.dataset.side] = new SummaryEditor(lineItemEditor, form.dataset.side);
|
||||
}
|
||||
return editors;
|
||||
}
|
||||
|
@ -39,10 +39,10 @@ class VoucherForm {
|
||||
#element;
|
||||
|
||||
/**
|
||||
* The template to add a new journal entry
|
||||
* The template to add a new line item
|
||||
* @type {string}
|
||||
*/
|
||||
entryTemplate;
|
||||
lineItemTemplate;
|
||||
|
||||
/**
|
||||
* The date
|
||||
@ -99,10 +99,10 @@ class VoucherForm {
|
||||
#noteError;
|
||||
|
||||
/**
|
||||
* The journal entry editor
|
||||
* @type {JournalEntryEditor}
|
||||
* The line item editor
|
||||
* @type {VoucherLineItemEditor}
|
||||
*/
|
||||
entryEditor;
|
||||
lineItemEditor;
|
||||
|
||||
/**
|
||||
* Constructs the voucher form.
|
||||
@ -110,7 +110,7 @@ class VoucherForm {
|
||||
*/
|
||||
constructor() {
|
||||
this.#element = document.getElementById("accounting-form");
|
||||
this.entryTemplate = this.#element.dataset.entryTemplate;
|
||||
this.lineItemTemplate = this.#element.dataset.lineItemTemplate;
|
||||
this.#date = document.getElementById("accounting-date");
|
||||
this.#dateError = document.getElementById("accounting-date-error");
|
||||
this.#currencyControl = document.getElementById("accounting-currencies");
|
||||
@ -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.entryEditor = new JournalEntryEditor(this);
|
||||
this.lineItemEditor = new VoucherLineItemEditor(this);
|
||||
|
||||
this.#addCurrencyButton.onclick = () => {
|
||||
const newIndex = 1 + (this.#currencies.length === 0? 0: Math.max(...this.#currencies.map((currency) => currency.index)));
|
||||
@ -162,14 +162,14 @@ class VoucherForm {
|
||||
this.#currencies[0].deleteButton.classList.add("d-none");
|
||||
} else {
|
||||
for (const currency of this.#currencies) {
|
||||
let isAnyEntryMatched = false;
|
||||
for (const entry of currency.getEntries()) {
|
||||
if (entry.isMatched) {
|
||||
isAnyEntryMatched = true;
|
||||
let isAnyLineItemMatched = false;
|
||||
for (const lineItem of currency.getLineItems()) {
|
||||
if (lineItem.isMatched) {
|
||||
isAnyLineItemMatched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isAnyEntryMatched) {
|
||||
if (isAnyLineItemMatched) {
|
||||
currency.deleteButton.classList.add("d-none");
|
||||
} else {
|
||||
currency.deleteButton.classList.remove("d-none");
|
||||
@ -193,27 +193,27 @@ class VoucherForm {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the journal entries in the form.
|
||||
* Returns all the line items in the form.
|
||||
*
|
||||
* @param entryType {string|null} the entry type, either "debit" or "credit", or null for both
|
||||
* @return {JournalEntrySubForm[]} all the journal entry sub-forms
|
||||
* @param side {string|null} the side, either "debit" or "credit", or null for both
|
||||
* @return {LineItemSubForm[]} all the line item sub-forms
|
||||
*/
|
||||
getEntries(entryType = null) {
|
||||
const entries = [];
|
||||
getLineItems(side = null) {
|
||||
const lineItems = [];
|
||||
for (const currency of this.#currencies) {
|
||||
entries.push(...currency.getEntries(entryType));
|
||||
lineItems.push(...currency.getLineItems(side));
|
||||
}
|
||||
return entries;
|
||||
return lineItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account codes used in the form.
|
||||
*
|
||||
* @param entryType {string} the entry type, either "debit" or "credit"
|
||||
* @param side {string} the side, either "debit" or "credit"
|
||||
* @return {string[]} the account codes used in the form
|
||||
*/
|
||||
getAccountCodesUsed(entryType) {
|
||||
return this.getEntries(entryType).map((entry) => entry.getAccountCode())
|
||||
getAccountCodesUsed(side) {
|
||||
return this.getLineItems(side).map((lineItem) => lineItem.getAccountCode())
|
||||
.filter((code) => code !== null);
|
||||
}
|
||||
|
||||
@ -231,16 +231,16 @@ class VoucherForm {
|
||||
*
|
||||
*/
|
||||
updateMinDate() {
|
||||
let lastOriginalEntryDate = null;
|
||||
for (const entry of this.getEntries()) {
|
||||
const date = entry.getOriginalEntryDate();
|
||||
let lastOriginalLineItemDate = null;
|
||||
for (const lineItem of this.getLineItems()) {
|
||||
const date = lineItem.getOriginalLineItemDate();
|
||||
if (date !== null) {
|
||||
if (lastOriginalEntryDate === null || lastOriginalEntryDate < date) {
|
||||
lastOriginalEntryDate = date;
|
||||
if (lastOriginalLineItemDate === null || lastOriginalLineItemDate < date) {
|
||||
lastOriginalLineItemDate = date;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.#date.min = lastOriginalEntryDate === null? "": lastOriginalEntryDate;
|
||||
this.#date.min = lastOriginalLineItemDate === null? "": lastOriginalLineItemDate;
|
||||
this.#validateDate();
|
||||
}
|
||||
|
||||
@ -272,7 +272,7 @@ class VoucherForm {
|
||||
}
|
||||
if (this.#date.value < this.#date.min) {
|
||||
this.#date.classList.add("is-invalid");
|
||||
this.#dateError.innerText = A_("The date cannot be earlier than the original entries.");
|
||||
this.#dateError.innerText = A_("The date cannot be earlier than the original line items.");
|
||||
return false;
|
||||
}
|
||||
this.#date.classList.remove("is-invalid");
|
||||
@ -407,13 +407,13 @@ class CurrencySubForm {
|
||||
|
||||
/**
|
||||
* The debit side
|
||||
* @type {DebitCreditSideSubForm|null}
|
||||
* @type {SideSubForm|null}
|
||||
*/
|
||||
#debit;
|
||||
|
||||
/**
|
||||
* The credit side
|
||||
* @type {DebitCreditSideSubForm|null}
|
||||
* @type {SideSubForm|null}
|
||||
*/
|
||||
#credit;
|
||||
|
||||
@ -435,9 +435,9 @@ class CurrencySubForm {
|
||||
this.#codeSelect = document.getElementById(this.#prefix + "-code-select");
|
||||
this.deleteButton = document.getElementById(this.#prefix + "-delete");
|
||||
const debitElement = document.getElementById(this.#prefix + "-debit");
|
||||
this.#debit = debitElement === null? null: new DebitCreditSideSubForm(this, debitElement, "debit");
|
||||
this.#debit = debitElement === null? null: new SideSubForm(this, debitElement, "debit");
|
||||
const creditElement = document.getElementById(this.#prefix + "-credit");
|
||||
this.#credit = creditElement == null? null: new DebitCreditSideSubForm(this, creditElement, "credit");
|
||||
this.#credit = creditElement == null? null: new SideSubForm(this, creditElement, "credit");
|
||||
this.#codeSelect.onchange = () => this.#code.value = this.#codeSelect.value;
|
||||
this.deleteButton.onclick = () => {
|
||||
this.element.parentElement.removeChild(this.element);
|
||||
@ -455,21 +455,21 @@ class CurrencySubForm {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the journal entries in the form.
|
||||
* Returns all the line items in the form.
|
||||
*
|
||||
* @param entryType {string|null} the entry type, either "debit" or "credit", or null for both
|
||||
* @return {JournalEntrySubForm[]} all the journal entry sub-forms
|
||||
* @param side {string|null} the side, either "debit" or "credit", or null for both
|
||||
* @return {LineItemSubForm[]} all the line item sub-forms
|
||||
*/
|
||||
getEntries(entryType = null) {
|
||||
const entries = []
|
||||
for (const side of [this.#debit, this.#credit]) {
|
||||
if (side !== null ) {
|
||||
if (entryType === null || side.entryType === entryType) {
|
||||
entries.push(...side.entries);
|
||||
getLineItems(side = null) {
|
||||
const lineItems = []
|
||||
for (const sideSubForm of [this.#debit, this.#credit]) {
|
||||
if (sideSubForm !== null ) {
|
||||
if (side === null || sideSubForm.side === side) {
|
||||
lineItems.push(...sideSubForm.lineItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
return lineItems;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -478,8 +478,8 @@ class CurrencySubForm {
|
||||
*/
|
||||
updateCodeSelectorStatus() {
|
||||
let isEnabled = true;
|
||||
for (const entry of this.getEntries()) {
|
||||
if (entry.getOriginalEntryId() !== null) {
|
||||
for (const lineItem of this.getLineItems()) {
|
||||
if (lineItem.getOriginalLineItemId() !== null) {
|
||||
isEnabled = false;
|
||||
break;
|
||||
}
|
||||
@ -527,7 +527,7 @@ class CurrencySubForm {
|
||||
* The debit or credit side sub-form
|
||||
*
|
||||
*/
|
||||
class DebitCreditSideSubForm {
|
||||
class SideSubForm {
|
||||
|
||||
/**
|
||||
* The currency sub-form
|
||||
@ -548,10 +548,10 @@ class DebitCreditSideSubForm {
|
||||
#currencyIndex;
|
||||
|
||||
/**
|
||||
* The entry type, either "debit" or "credit"
|
||||
* The side, either "debit" or "credit"
|
||||
* @type {string}
|
||||
*/
|
||||
entryType;
|
||||
side;
|
||||
|
||||
/**
|
||||
* The prefix of the HTML ID and class
|
||||
@ -566,16 +566,16 @@ class DebitCreditSideSubForm {
|
||||
#error;
|
||||
|
||||
/**
|
||||
* The journal entry list
|
||||
* The line item list
|
||||
* @type {HTMLUListElement}
|
||||
*/
|
||||
#entryList;
|
||||
#lineItemList;
|
||||
|
||||
/**
|
||||
* The journal entry sub-forms
|
||||
* @type {JournalEntrySubForm[]}
|
||||
* The line item sub-forms
|
||||
* @type {LineItemSubForm[]}
|
||||
*/
|
||||
entries;
|
||||
lineItems;
|
||||
|
||||
/**
|
||||
* The total
|
||||
@ -584,82 +584,82 @@ class DebitCreditSideSubForm {
|
||||
#total;
|
||||
|
||||
/**
|
||||
* The button to add a new entry
|
||||
* The button to add a new line item
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
#addEntryButton;
|
||||
#addLineItemButton;
|
||||
|
||||
/**
|
||||
* Constructs a debit or credit side sub-form
|
||||
*
|
||||
* @param currency {CurrencySubForm} the currency sub-form
|
||||
* @param element {HTMLDivElement} the element
|
||||
* @param entryType {string} the entry type, either "debit" or "credit"
|
||||
* @param side {string} the side, either "debit" or "credit"
|
||||
*/
|
||||
constructor(currency, element, entryType) {
|
||||
constructor(currency, element, side) {
|
||||
this.currency = currency;
|
||||
this.#element = element;
|
||||
this.#currencyIndex = currency.index;
|
||||
this.entryType = entryType;
|
||||
this.#prefix = "accounting-currency-" + String(this.#currencyIndex) + "-" + entryType;
|
||||
this.side = side;
|
||||
this.#prefix = "accounting-currency-" + String(this.#currencyIndex) + "-" + side;
|
||||
this.#error = document.getElementById(this.#prefix + "-error");
|
||||
this.#entryList = document.getElementById(this.#prefix + "-list");
|
||||
this.#lineItemList = document.getElementById(this.#prefix + "-list");
|
||||
// noinspection JSValidateTypes
|
||||
this.entries = Array.from(document.getElementsByClassName(this.#prefix)).map((element) => new JournalEntrySubForm(this, element));
|
||||
this.lineItems = Array.from(document.getElementsByClassName(this.#prefix)).map((element) => new LineItemSubForm(this, element));
|
||||
this.#total = document.getElementById(this.#prefix + "-total");
|
||||
this.#addEntryButton = document.getElementById(this.#prefix + "-add-entry");
|
||||
this.#addEntryButton.onclick = () => this.currency.form.entryEditor.onAddNew(this);
|
||||
this.#resetDeleteJournalEntryButtons();
|
||||
this.#addLineItemButton = document.getElementById(this.#prefix + "-add-line-item");
|
||||
this.#addLineItemButton.onclick = () => this.currency.form.lineItemEditor.onAddNew(this);
|
||||
this.#resetDeleteLineItemButtons();
|
||||
this.#initializeDragAndDropReordering();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new journal entry sub-form
|
||||
* Adds a new line item sub-form
|
||||
*
|
||||
* @returns {JournalEntrySubForm} the newly-added journal entry sub-form
|
||||
* @returns {LineItemSubForm} the newly-added line item sub-form
|
||||
*/
|
||||
addJournalEntry() {
|
||||
const newIndex = 1 + (this.entries.length === 0? 0: Math.max(...this.entries.map((entry) => entry.entryIndex)));
|
||||
const html = this.currency.form.entryTemplate
|
||||
addLineItem() {
|
||||
const newIndex = 1 + (this.lineItems.length === 0? 0: Math.max(...this.lineItems.map((lineItem) => lineItem.lineItemIndex)));
|
||||
const html = this.currency.form.lineItemTemplate
|
||||
.replaceAll("CURRENCY_INDEX", escapeHtml(String(this.#currencyIndex)))
|
||||
.replaceAll("ENTRY_TYPE", escapeHtml(this.entryType))
|
||||
.replaceAll("ENTRY_INDEX", escapeHtml(String(newIndex)));
|
||||
this.#entryList.insertAdjacentHTML("beforeend", html);
|
||||
const entry = new JournalEntrySubForm(this, document.getElementById(this.#prefix + "-" + String(newIndex)));
|
||||
this.entries.push(entry);
|
||||
this.#resetDeleteJournalEntryButtons();
|
||||
.replaceAll("SIDE", escapeHtml(this.side))
|
||||
.replaceAll("LINE_ITEM_INDEX", escapeHtml(String(newIndex)));
|
||||
this.#lineItemList.insertAdjacentHTML("beforeend", html);
|
||||
const lineItem = new LineItemSubForm(this, document.getElementById(this.#prefix + "-" + String(newIndex)));
|
||||
this.lineItems.push(lineItem);
|
||||
this.#resetDeleteLineItemButtons();
|
||||
this.#initializeDragAndDropReordering();
|
||||
this.validate();
|
||||
return entry;
|
||||
return lineItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a journal entry sub-form
|
||||
* Deletes a line item sub-form
|
||||
*
|
||||
* @param entry {JournalEntrySubForm}
|
||||
* @param lineItem {LineItemSubForm}
|
||||
*/
|
||||
deleteJournalEntry(entry) {
|
||||
const index = this.entries.indexOf(entry);
|
||||
this.entries.splice(index, 1);
|
||||
deleteLineItem(lineItem) {
|
||||
const index = this.lineItems.indexOf(lineItem);
|
||||
this.lineItems.splice(index, 1);
|
||||
this.updateTotal();
|
||||
this.currency.updateCodeSelectorStatus();
|
||||
this.currency.form.updateMinDate();
|
||||
this.#resetDeleteJournalEntryButtons();
|
||||
this.#resetDeleteLineItemButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the buttons to delete the journal entry sub-forms
|
||||
* Resets the buttons to delete the line item sub-forms
|
||||
*
|
||||
*/
|
||||
#resetDeleteJournalEntryButtons() {
|
||||
if (this.entries.length === 1) {
|
||||
this.entries[0].deleteButton.classList.add("d-none");
|
||||
#resetDeleteLineItemButtons() {
|
||||
if (this.lineItems.length === 1) {
|
||||
this.lineItems[0].deleteButton.classList.add("d-none");
|
||||
} else {
|
||||
for (const entry of this.entries) {
|
||||
if (entry.isMatched) {
|
||||
entry.deleteButton.classList.add("d-none");
|
||||
for (const lineItem of this.lineItems) {
|
||||
if (lineItem.isMatched) {
|
||||
lineItem.deleteButton.classList.add("d-none");
|
||||
} else {
|
||||
entry.deleteButton.classList.remove("d-none");
|
||||
lineItem.deleteButton.classList.remove("d-none");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -672,8 +672,8 @@ class DebitCreditSideSubForm {
|
||||
*/
|
||||
getTotal() {
|
||||
let total = new Decimal("0");
|
||||
for (const entry of this.entries) {
|
||||
const amount = entry.getAmount();
|
||||
for (const lineItem of this.lineItems) {
|
||||
const amount = lineItem.getAmount();
|
||||
if (amount !== null) {
|
||||
total = total.plus(amount);
|
||||
}
|
||||
@ -695,11 +695,11 @@ class DebitCreditSideSubForm {
|
||||
*
|
||||
*/
|
||||
#initializeDragAndDropReordering() {
|
||||
initializeDragAndDropReordering(this.#entryList, () => {
|
||||
const entryId = Array.from(this.#entryList.children).map((entry) => entry.id);
|
||||
this.entries.sort((a, b) => entryId.indexOf(a.element.id) - entryId.indexOf(b.element.id));
|
||||
for (let i = 0; i < this.entries.length; i++) {
|
||||
this.entries[i].no.value = String(i + 1);
|
||||
initializeDragAndDropReordering(this.#lineItemList, () => {
|
||||
const lineItemId = Array.from(this.#lineItemList.children).map((lineItem) => lineItem.id);
|
||||
this.lineItems.sort((a, b) => lineItemId.indexOf(a.element.id) - lineItemId.indexOf(b.element.id));
|
||||
for (let i = 0; i < this.lineItems.length; i++) {
|
||||
this.lineItems[i].no.value = String(i + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -712,8 +712,8 @@ class DebitCreditSideSubForm {
|
||||
validate() {
|
||||
let isValid = true;
|
||||
isValid = this.#validateReal() && isValid;
|
||||
for (const entry of this.entries) {
|
||||
isValid = entry.validate() && isValid;
|
||||
for (const lineItem of this.lineItems) {
|
||||
isValid = lineItem.validate() && isValid;
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
@ -724,9 +724,9 @@ class DebitCreditSideSubForm {
|
||||
* @returns {boolean} true if valid, or false otherwise
|
||||
*/
|
||||
#validateReal() {
|
||||
if (this.entries.length === 0) {
|
||||
if (this.lineItems.length === 0) {
|
||||
this.#element.classList.add("is-invalid");
|
||||
this.#error.innerText = A_("Please add some journal entries.");
|
||||
this.#error.innerText = A_("Please add some line items.");
|
||||
return false;
|
||||
}
|
||||
this.#element.classList.remove("is-invalid");
|
||||
@ -736,16 +736,16 @@ class DebitCreditSideSubForm {
|
||||
}
|
||||
|
||||
/**
|
||||
* The journal entry sub-form.
|
||||
* The line item sub-form.
|
||||
*
|
||||
*/
|
||||
class JournalEntrySubForm {
|
||||
class LineItemSubForm {
|
||||
|
||||
/**
|
||||
* The debit or credit entry side sub-form
|
||||
* @type {DebitCreditSideSubForm}
|
||||
* The debit or credit side sub-form
|
||||
* @type {SideSubForm}
|
||||
*/
|
||||
side;
|
||||
sideSubForm;
|
||||
|
||||
/**
|
||||
* The element
|
||||
@ -754,19 +754,19 @@ class JournalEntrySubForm {
|
||||
element;
|
||||
|
||||
/**
|
||||
* The entry type, either "debit" or "credit"
|
||||
* The side, either "debit" or "credit"
|
||||
* @type {string}
|
||||
*/
|
||||
entryType;
|
||||
side;
|
||||
|
||||
/**
|
||||
* The entry index
|
||||
* The line item index
|
||||
* @type {number}
|
||||
*/
|
||||
entryIndex;
|
||||
lineItemIndex;
|
||||
|
||||
/**
|
||||
* Whether this is an original entry with offsets
|
||||
* Whether this is an original line item with offsets
|
||||
* @type {boolean}
|
||||
*/
|
||||
isMatched;
|
||||
@ -820,19 +820,19 @@ class JournalEntrySubForm {
|
||||
#summaryText;
|
||||
|
||||
/**
|
||||
* The ID of the original entry
|
||||
* The ID of the original line item
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
#originalEntryId;
|
||||
#originalLineItemId;
|
||||
|
||||
/**
|
||||
* The text of the original entry
|
||||
* The text of the original line item
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#originalEntryText;
|
||||
#originalLineItemText;
|
||||
|
||||
/**
|
||||
* The offset entries
|
||||
* The offset items
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
#offsets;
|
||||
@ -850,24 +850,24 @@ class JournalEntrySubForm {
|
||||
#amountText;
|
||||
|
||||
/**
|
||||
* The button to delete journal entry
|
||||
* The button to delete line item
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
deleteButton;
|
||||
|
||||
/**
|
||||
* Constructs the journal entry sub-form.
|
||||
* Constructs the line item sub-form.
|
||||
*
|
||||
* @param side {DebitCreditSideSubForm} the debit or credit entry side sub-form
|
||||
* @param side {SideSubForm} the debit or credit side sub-form
|
||||
* @param element {HTMLLIElement} the element
|
||||
*/
|
||||
constructor(side, element) {
|
||||
this.side = side;
|
||||
this.sideSubForm = side;
|
||||
this.element = element;
|
||||
this.entryType = element.dataset.entryType;
|
||||
this.entryIndex = parseInt(element.dataset.entryIndex);
|
||||
this.isMatched = element.classList.contains("accounting-matched-entry");
|
||||
this.#prefix = "accounting-currency-" + element.dataset.currencyIndex + "-" + this.entryType + "-" + this.entryIndex;
|
||||
this.side = element.dataset.side;
|
||||
this.lineItemIndex = parseInt(element.dataset.lineItemIndex);
|
||||
this.isMatched = element.classList.contains("accounting-matched-line-item");
|
||||
this.#prefix = "accounting-currency-" + element.dataset.currencyIndex + "-" + this.side + "-" + this.lineItemIndex;
|
||||
this.#control = document.getElementById(this.#prefix + "-control");
|
||||
this.#error = document.getElementById(this.#prefix + "-error");
|
||||
this.no = document.getElementById(this.#prefix + "-no");
|
||||
@ -875,53 +875,53 @@ class JournalEntrySubForm {
|
||||
this.#accountText = document.getElementById(this.#prefix + "-account-text");
|
||||
this.#summary = document.getElementById(this.#prefix + "-summary");
|
||||
this.#summaryText = document.getElementById(this.#prefix + "-summary-text");
|
||||
this.#originalEntryId = document.getElementById(this.#prefix + "-original-entry-id");
|
||||
this.#originalEntryText = document.getElementById(this.#prefix + "-original-entry-text");
|
||||
this.#originalLineItemId = document.getElementById(this.#prefix + "-original-line-item-id");
|
||||
this.#originalLineItemText = document.getElementById(this.#prefix + "-original-line-item-text");
|
||||
this.#offsets = document.getElementById(this.#prefix + "-offsets");
|
||||
this.#amount = document.getElementById(this.#prefix + "-amount");
|
||||
this.#amountText = document.getElementById(this.#prefix + "-amount-text");
|
||||
this.deleteButton = document.getElementById(this.#prefix + "-delete");
|
||||
this.#control.onclick = () => this.side.currency.form.entryEditor.onEdit(this);
|
||||
this.#control.onclick = () => this.sideSubForm.currency.form.lineItemEditor.onEdit(this);
|
||||
this.deleteButton.onclick = () => {
|
||||
this.element.parentElement.removeChild(this.element);
|
||||
this.side.deleteJournalEntry(this);
|
||||
this.sideSubForm.deleteLineItem(this);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the entry is an original entry.
|
||||
* Returns whether the line item needs offset.
|
||||
*
|
||||
* @return {boolean} true if the entry is an original entry, or false otherwise
|
||||
* @return {boolean} true if the line item needs offset, or false otherwise
|
||||
*/
|
||||
isNeedOffset() {
|
||||
return "isNeedOffset" in this.element.dataset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the original entry.
|
||||
* Returns the ID of the original line item.
|
||||
*
|
||||
* @return {string|null} the ID of the original entry
|
||||
* @return {string|null} the ID of the original line item
|
||||
*/
|
||||
getOriginalEntryId() {
|
||||
return this.#originalEntryId.value === ""? null: this.#originalEntryId.value;
|
||||
getOriginalLineItemId() {
|
||||
return this.#originalLineItemId.value === ""? null: this.#originalLineItemId.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the date of the original entry.
|
||||
* Returns the date of the original line item.
|
||||
*
|
||||
* @return {string|null} the date of the original entry
|
||||
* @return {string|null} the date of the original line item
|
||||
*/
|
||||
getOriginalEntryDate() {
|
||||
return this.#originalEntryId.dataset.date === ""? null: this.#originalEntryId.dataset.date;
|
||||
getOriginalLineItemDate() {
|
||||
return this.#originalLineItemId.dataset.date === ""? null: this.#originalLineItemId.dataset.date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text of the original entry.
|
||||
* Returns the text of the original line item.
|
||||
*
|
||||
* @return {string|null} the text of the original entry
|
||||
* @return {string|null} the text of the original line item
|
||||
*/
|
||||
getOriginalEntryText() {
|
||||
return this.#originalEntryId.dataset.text === ""? null: this.#originalEntryId.dataset.text;
|
||||
getOriginalLineItemText() {
|
||||
return this.#originalLineItemId.dataset.text === ""? null: this.#originalLineItemId.dataset.text;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -991,9 +991,9 @@ class JournalEntrySubForm {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the data into the journal entry sub-form.
|
||||
* Stores the data into the line item sub-form.
|
||||
*
|
||||
* @param editor {JournalEntryEditor} the journal entry editor
|
||||
* @param editor {VoucherLineItemEditor} the line item editor
|
||||
*/
|
||||
save(editor) {
|
||||
if (editor.isNeedOffset) {
|
||||
@ -1001,15 +1001,15 @@ class JournalEntrySubForm {
|
||||
} else {
|
||||
this.#offsets.classList.add("d-none");
|
||||
}
|
||||
this.#originalEntryId.value = editor.originalEntryId === null? "": editor.originalEntryId;
|
||||
this.#originalEntryId.dataset.date = editor.originalEntryDate === null? "": editor.originalEntryDate;
|
||||
this.#originalEntryId.dataset.text = editor.originalEntryText === null? "": editor.originalEntryText;
|
||||
if (editor.originalEntryText === null) {
|
||||
this.#originalEntryText.classList.add("d-none");
|
||||
this.#originalEntryText.innerText = "";
|
||||
this.#originalLineItemId.value = editor.originalLineItemId === null? "": editor.originalLineItemId;
|
||||
this.#originalLineItemId.dataset.date = editor.originalLineItemDate === null? "": editor.originalLineItemDate;
|
||||
this.#originalLineItemId.dataset.text = editor.originalLineItemText === null? "": editor.originalLineItemText;
|
||||
if (editor.originalLineItemText === null) {
|
||||
this.#originalLineItemText.classList.add("d-none");
|
||||
this.#originalLineItemText.innerText = "";
|
||||
} else {
|
||||
this.#originalEntryText.classList.remove("d-none");
|
||||
this.#originalEntryText.innerText = A_("Offset %(entry)s", {entry: editor.originalEntryText});
|
||||
this.#originalLineItemText.classList.remove("d-none");
|
||||
this.#originalLineItemText.innerText = A_("Offset %(item)s", {item: editor.originalLineItemText});
|
||||
}
|
||||
this.#accountCode.value = editor.accountCode === null? "": editor.accountCode;
|
||||
this.#accountCode.dataset.text = editor.accountText === null? "": editor.accountText;
|
||||
@ -1019,9 +1019,9 @@ class JournalEntrySubForm {
|
||||
this.#amount.value = editor.amount;
|
||||
this.#amountText.innerText = formatDecimal(new Decimal(editor.amount));
|
||||
this.validate();
|
||||
this.side.updateTotal();
|
||||
this.side.currency.updateCodeSelectorStatus();
|
||||
this.side.currency.form.updateMinDate();
|
||||
this.sideSubForm.updateTotal();
|
||||
this.sideSubForm.currency.updateCodeSelectorStatus();
|
||||
this.sideSubForm.currency.form.updateMinDate();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
* journal-entry-editor.js: The JavaScript for the journal entry editor
|
||||
* voucher-line-item-editor.js: The JavaScript for the voucher line item editor
|
||||
*/
|
||||
|
||||
/* Copyright (c) 2023 imacat.
|
||||
@ -23,10 +23,10 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The journal entry editor.
|
||||
* The voucher line item editor.
|
||||
*
|
||||
*/
|
||||
class JournalEntryEditor {
|
||||
class VoucherLineItemEditor {
|
||||
|
||||
/**
|
||||
* The voucher form
|
||||
@ -35,7 +35,7 @@ class JournalEntryEditor {
|
||||
form;
|
||||
|
||||
/**
|
||||
* The journal entry editor
|
||||
* The voucher line item editor
|
||||
* @type {HTMLFormElement}
|
||||
*/
|
||||
#element;
|
||||
@ -47,46 +47,46 @@ class JournalEntryEditor {
|
||||
#modal;
|
||||
|
||||
/**
|
||||
* The entry type, either "debit" or "credit"
|
||||
* The side, either "debit" or "credit"
|
||||
* @type {string}
|
||||
*/
|
||||
entryType;
|
||||
side;
|
||||
|
||||
/**
|
||||
* The prefix of the HTML ID and class
|
||||
* @type {string}
|
||||
*/
|
||||
#prefix = "accounting-entry-editor"
|
||||
#prefix = "accounting-line-item-editor"
|
||||
|
||||
/**
|
||||
* The container of the original entry
|
||||
* The container of the original line item
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#originalEntryContainer;
|
||||
#originalLineItemContainer;
|
||||
|
||||
/**
|
||||
* The control of the original entry
|
||||
* The control of the original line item
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#originalEntryControl;
|
||||
#originalLineItemControl;
|
||||
|
||||
/**
|
||||
* The original entry
|
||||
* The original line item
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#originalEntryText;
|
||||
#originalLineItemText;
|
||||
|
||||
/**
|
||||
* The error message of the original entry
|
||||
* The error message of the original line item
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#originalEntryError;
|
||||
#originalLineItemError;
|
||||
|
||||
/**
|
||||
* The delete button of the original entry
|
||||
* The delete button of the original line item
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
#originalEntryDelete;
|
||||
#originalLineItemDelete;
|
||||
|
||||
/**
|
||||
* The control of the summary
|
||||
@ -137,40 +137,40 @@ class JournalEntryEditor {
|
||||
#amountError;
|
||||
|
||||
/**
|
||||
* The journal entry to edit
|
||||
* @type {JournalEntrySubForm|null}
|
||||
* The voucher line item to edit
|
||||
* @type {LineItemSubForm|null}
|
||||
*/
|
||||
entry;
|
||||
lineItem;
|
||||
|
||||
/**
|
||||
* The debit or credit entry side sub-form
|
||||
* @type {DebitCreditSideSubForm}
|
||||
* The debit or credit side sub-form
|
||||
* @type {SideSubForm}
|
||||
*/
|
||||
#side;
|
||||
#sideSubForm;
|
||||
|
||||
/**
|
||||
* Whether the journal entry needs offset
|
||||
* Whether the voucher line item needs offset
|
||||
* @type {boolean}
|
||||
*/
|
||||
isNeedOffset = false;
|
||||
|
||||
/**
|
||||
* The ID of the original entry
|
||||
* The ID of the original line item
|
||||
* @type {string|null}
|
||||
*/
|
||||
originalEntryId = null;
|
||||
originalLineItemId = null;
|
||||
|
||||
/**
|
||||
* The date of the original entry
|
||||
* The date of the original line item
|
||||
* @type {string|null}
|
||||
*/
|
||||
originalEntryDate = null;
|
||||
originalLineItemDate = null;
|
||||
|
||||
/**
|
||||
* The text of the original entry
|
||||
* The text of the original line item
|
||||
* @type {string|null}
|
||||
*/
|
||||
originalEntryText = null;
|
||||
originalLineItemText = null;
|
||||
|
||||
/**
|
||||
* The account code
|
||||
@ -209,13 +209,13 @@ class JournalEntryEditor {
|
||||
#accountSelectors;
|
||||
|
||||
/**
|
||||
* The original entry selector
|
||||
* @type {OriginalEntrySelector}
|
||||
* The original line item selector
|
||||
* @type {OriginalLineItemSelector}
|
||||
*/
|
||||
originalEntrySelector;
|
||||
originalLineItemSelector;
|
||||
|
||||
/**
|
||||
* Constructs a new journal entry editor.
|
||||
* Constructs a new voucher line item editor.
|
||||
*
|
||||
* @param form {VoucherForm} the voucher form
|
||||
*/
|
||||
@ -223,11 +223,11 @@ class JournalEntryEditor {
|
||||
this.form = form;
|
||||
this.#element = document.getElementById(this.#prefix);
|
||||
this.#modal = document.getElementById(this.#prefix + "-modal");
|
||||
this.#originalEntryContainer = document.getElementById(this.#prefix + "-original-entry-container");
|
||||
this.#originalEntryControl = document.getElementById(this.#prefix + "-original-entry-control");
|
||||
this.#originalEntryText = document.getElementById(this.#prefix + "-original-entry");
|
||||
this.#originalEntryError = document.getElementById(this.#prefix + "-original-entry-error");
|
||||
this.#originalEntryDelete = document.getElementById(this.#prefix + "-original-entry-delete");
|
||||
this.#originalLineItemContainer = document.getElementById(this.#prefix + "-original-line-item-container");
|
||||
this.#originalLineItemControl = document.getElementById(this.#prefix + "-original-line-item-control");
|
||||
this.#originalLineItemText = document.getElementById(this.#prefix + "-original-line-item");
|
||||
this.#originalLineItemError = document.getElementById(this.#prefix + "-original-line-item-error");
|
||||
this.#originalLineItemDelete = document.getElementById(this.#prefix + "-original-line-item-delete");
|
||||
this.#summaryControl = document.getElementById(this.#prefix + "-summary-control");
|
||||
this.#summaryText = document.getElementById(this.#prefix + "-summary");
|
||||
this.#summaryError = document.getElementById(this.#prefix + "-summary-error");
|
||||
@ -238,19 +238,19 @@ class JournalEntryEditor {
|
||||
this.#amountError = document.getElementById(this.#prefix + "-amount-error");
|
||||
this.#summaryEditors = SummaryEditor.getInstances(this);
|
||||
this.#accountSelectors = AccountSelector.getInstances(this);
|
||||
this.originalEntrySelector = new OriginalEntrySelector(this);
|
||||
this.#originalEntryControl.onclick = () => this.originalEntrySelector.onOpen()
|
||||
this.#originalEntryDelete.onclick = () => this.clearOriginalEntry();
|
||||
this.#summaryControl.onclick = () => this.#summaryEditors[this.entryType].onOpen();
|
||||
this.#accountControl.onclick = () => this.#accountSelectors[this.entryType].onOpen();
|
||||
this.originalLineItemSelector = new OriginalLineItemSelector(this);
|
||||
this.#originalLineItemControl.onclick = () => this.originalLineItemSelector.onOpen()
|
||||
this.#originalLineItemDelete.onclick = () => this.clearOriginalLineItem();
|
||||
this.#summaryControl.onclick = () => this.#summaryEditors[this.side].onOpen();
|
||||
this.#accountControl.onclick = () => this.#accountSelectors[this.side].onOpen();
|
||||
this.#amountInput.onchange = () => this.#validateAmount();
|
||||
this.#element.onsubmit = () => {
|
||||
if (this.#validate()) {
|
||||
if (this.entry === null) {
|
||||
this.entry = this.#side.addJournalEntry();
|
||||
if (this.lineItem === null) {
|
||||
this.lineItem = this.#sideSubForm.addLineItem();
|
||||
}
|
||||
this.amount = this.#amountInput.value;
|
||||
this.entry.save(this);
|
||||
this.lineItem.save(this);
|
||||
bootstrap.Modal.getInstance(this.#modal).hide();
|
||||
}
|
||||
return false;
|
||||
@ -258,48 +258,48 @@ class JournalEntryEditor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the original entry from the original entry selector.
|
||||
* Saves the original line item from the original line item selector.
|
||||
*
|
||||
* @param originalEntry {OriginalEntry} the original entry
|
||||
* @param originalLineItem {OriginalLineItem} the original line item
|
||||
*/
|
||||
saveOriginalEntry(originalEntry) {
|
||||
saveOriginalLineItem(originalLineItem) {
|
||||
this.isNeedOffset = false;
|
||||
this.#originalEntryContainer.classList.remove("d-none");
|
||||
this.#originalEntryControl.classList.add("accounting-not-empty");
|
||||
this.originalEntryId = originalEntry.id;
|
||||
this.originalEntryDate = originalEntry.date;
|
||||
this.originalEntryText = originalEntry.text;
|
||||
this.#originalEntryText.innerText = originalEntry.text;
|
||||
this.#originalLineItemContainer.classList.remove("d-none");
|
||||
this.#originalLineItemControl.classList.add("accounting-not-empty");
|
||||
this.originalLineItemId = originalLineItem.id;
|
||||
this.originalLineItemDate = originalLineItem.date;
|
||||
this.originalLineItemText = originalLineItem.text;
|
||||
this.#originalLineItemText.innerText = originalLineItem.text;
|
||||
this.#setEnableSummaryAccount(false);
|
||||
if (originalEntry.summary === "") {
|
||||
if (originalLineItem.summary === "") {
|
||||
this.#summaryControl.classList.remove("accounting-not-empty");
|
||||
} else {
|
||||
this.#summaryControl.classList.add("accounting-not-empty");
|
||||
}
|
||||
this.summary = originalEntry.summary === ""? null: originalEntry.summary;
|
||||
this.#summaryText.innerText = originalEntry.summary;
|
||||
this.summary = originalLineItem.summary === ""? null: originalLineItem.summary;
|
||||
this.#summaryText.innerText = originalLineItem.summary;
|
||||
this.#accountControl.classList.add("accounting-not-empty");
|
||||
this.accountCode = originalEntry.accountCode;
|
||||
this.accountText = originalEntry.accountText;
|
||||
this.#accountText.innerText = originalEntry.accountText;
|
||||
this.#amountInput.value = String(originalEntry.netBalance);
|
||||
this.#amountInput.max = String(originalEntry.netBalance);
|
||||
this.accountCode = originalLineItem.accountCode;
|
||||
this.accountText = originalLineItem.accountText;
|
||||
this.#accountText.innerText = originalLineItem.accountText;
|
||||
this.#amountInput.value = String(originalLineItem.netBalance);
|
||||
this.#amountInput.max = String(originalLineItem.netBalance);
|
||||
this.#amountInput.min = "0";
|
||||
this.#validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the original entry.
|
||||
* Clears the original line item.
|
||||
*
|
||||
*/
|
||||
clearOriginalEntry() {
|
||||
clearOriginalLineItem() {
|
||||
this.isNeedOffset = false;
|
||||
this.#originalEntryContainer.classList.add("d-none");
|
||||
this.#originalEntryControl.classList.remove("accounting-not-empty");
|
||||
this.originalEntryId = null;
|
||||
this.originalEntryDate = null;
|
||||
this.originalEntryText = null;
|
||||
this.#originalEntryText.innerText = "";
|
||||
this.#originalLineItemContainer.classList.add("d-none");
|
||||
this.#originalLineItemControl.classList.remove("accounting-not-empty");
|
||||
this.originalLineItemId = null;
|
||||
this.originalLineItemDate = null;
|
||||
this.originalLineItemText = null;
|
||||
this.#originalLineItemText.innerText = "";
|
||||
this.#setEnableSummaryAccount(true);
|
||||
this.#accountControl.classList.remove("accounting-not-empty");
|
||||
this.accountCode = null;
|
||||
@ -314,7 +314,7 @@ class JournalEntryEditor {
|
||||
* @return {string} the currency code
|
||||
*/
|
||||
getCurrencyCode() {
|
||||
return this.#side.currency.getCurrencyCode();
|
||||
return this.#sideSubForm.currency.getCurrencyCode();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -340,7 +340,7 @@ class JournalEntryEditor {
|
||||
* @param summary {string} the summary
|
||||
* @param accountCode {string} the account code
|
||||
* @param accountText {string} the account text
|
||||
* @param isAccountNeedOffset {boolean} true if the journal entries in the account need offset, or false otherwise
|
||||
* @param isAccountNeedOffset {boolean} true if the line items in the account need offset, or false otherwise
|
||||
*/
|
||||
saveSummaryWithAccount(summary, accountCode, accountText, isAccountNeedOffset) {
|
||||
this.isNeedOffset = isAccountNeedOffset;
|
||||
@ -370,7 +370,7 @@ class JournalEntryEditor {
|
||||
*
|
||||
* @param code {string} the account code
|
||||
* @param text {string} the account text
|
||||
* @param isNeedOffset {boolean} true if the journal entries in the account need offset or false otherwise
|
||||
* @param isNeedOffset {boolean} true if the line items in the account need offset or false otherwise
|
||||
*/
|
||||
saveAccount(code, text, isNeedOffset) {
|
||||
this.isNeedOffset = isNeedOffset;
|
||||
@ -388,7 +388,7 @@ class JournalEntryEditor {
|
||||
*/
|
||||
#validate() {
|
||||
let isValid = true;
|
||||
isValid = this.#validateOriginalEntry() && isValid;
|
||||
isValid = this.#validateOriginalLineItem() && isValid;
|
||||
isValid = this.#validateSummary() && isValid;
|
||||
isValid = this.#validateAccount() && isValid;
|
||||
isValid = this.#validateAmount() && isValid
|
||||
@ -396,14 +396,14 @@ class JournalEntryEditor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the original entry.
|
||||
* Validates the original line item.
|
||||
*
|
||||
* @return {boolean} true if valid, or false otherwise
|
||||
* @private
|
||||
*/
|
||||
#validateOriginalEntry() {
|
||||
this.#originalEntryControl.classList.remove("is-invalid");
|
||||
this.#originalEntryError.innerText = "";
|
||||
#validateOriginalLineItem() {
|
||||
this.#originalLineItemControl.classList.remove("is-invalid");
|
||||
this.#originalLineItemError.innerText = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -458,7 +458,7 @@ class JournalEntryEditor {
|
||||
if (this.#amountInput.max !== "") {
|
||||
if (amount.greaterThan(new Decimal(this.#amountInput.max))) {
|
||||
this.#amountInput.classList.add("is-invalid");
|
||||
this.#amountError.innerText = A_("The amount must not exceed the net balance %(balance)s of the original entry.", {balance: new Decimal(this.#amountInput.max)});
|
||||
this.#amountError.innerText = A_("The amount must not exceed the net balance %(balance)s of the original line item.", {balance: new Decimal(this.#amountInput.max)});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -476,22 +476,22 @@ class JournalEntryEditor {
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback when adding a new journal entry.
|
||||
* The callback when adding a new voucher line item.
|
||||
*
|
||||
* @param side {DebitCreditSideSubForm} the debit or credit side sub-form
|
||||
* @param side {SideSubForm} the debit or credit side sub-form
|
||||
*/
|
||||
onAddNew(side) {
|
||||
this.entry = null;
|
||||
this.#side = side;
|
||||
this.entryType = this.#side.entryType;
|
||||
this.lineItem = null;
|
||||
this.#sideSubForm = side;
|
||||
this.side = this.#sideSubForm.side;
|
||||
this.isNeedOffset = false;
|
||||
this.#originalEntryContainer.classList.add("d-none");
|
||||
this.#originalEntryControl.classList.remove("accounting-not-empty");
|
||||
this.#originalEntryControl.classList.remove("is-invalid");
|
||||
this.originalEntryId = null;
|
||||
this.originalEntryDate = null;
|
||||
this.originalEntryText = null;
|
||||
this.#originalEntryText.innerText = "";
|
||||
this.#originalLineItemContainer.classList.add("d-none");
|
||||
this.#originalLineItemControl.classList.remove("accounting-not-empty");
|
||||
this.#originalLineItemControl.classList.remove("is-invalid");
|
||||
this.originalLineItemId = null;
|
||||
this.originalLineItemDate = null;
|
||||
this.originalLineItemText = null;
|
||||
this.#originalLineItemText.innerText = "";
|
||||
this.#setEnableSummaryAccount(true);
|
||||
this.#summaryControl.classList.remove("accounting-not-empty");
|
||||
this.#summaryControl.classList.remove("is-invalid");
|
||||
@ -512,46 +512,46 @@ class JournalEntryEditor {
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback when editing a journal entry.
|
||||
* The callback when editing a voucher line item.
|
||||
*
|
||||
* @param entry {JournalEntrySubForm} the journal entry sub-form
|
||||
* @param lineItem {LineItemSubForm} the voucher line item sub-form
|
||||
*/
|
||||
onEdit(entry) {
|
||||
this.entry = entry;
|
||||
this.#side = entry.side;
|
||||
this.entryType = this.#side.entryType;
|
||||
this.isNeedOffset = entry.isNeedOffset();
|
||||
this.originalEntryId = entry.getOriginalEntryId();
|
||||
this.originalEntryDate = entry.getOriginalEntryDate();
|
||||
this.originalEntryText = entry.getOriginalEntryText();
|
||||
this.#originalEntryText.innerText = this.originalEntryText;
|
||||
if (this.originalEntryId === null) {
|
||||
this.#originalEntryContainer.classList.add("d-none");
|
||||
this.#originalEntryControl.classList.remove("accounting-not-empty");
|
||||
onEdit(lineItem) {
|
||||
this.lineItem = lineItem;
|
||||
this.#sideSubForm = lineItem.sideSubForm;
|
||||
this.side = this.#sideSubForm.side;
|
||||
this.isNeedOffset = lineItem.isNeedOffset();
|
||||
this.originalLineItemId = lineItem.getOriginalLineItemId();
|
||||
this.originalLineItemDate = lineItem.getOriginalLineItemDate();
|
||||
this.originalLineItemText = lineItem.getOriginalLineItemText();
|
||||
this.#originalLineItemText.innerText = this.originalLineItemText;
|
||||
if (this.originalLineItemId === null) {
|
||||
this.#originalLineItemContainer.classList.add("d-none");
|
||||
this.#originalLineItemControl.classList.remove("accounting-not-empty");
|
||||
} else {
|
||||
this.#originalEntryContainer.classList.remove("d-none");
|
||||
this.#originalEntryControl.classList.add("accounting-not-empty");
|
||||
this.#originalLineItemContainer.classList.remove("d-none");
|
||||
this.#originalLineItemControl.classList.add("accounting-not-empty");
|
||||
}
|
||||
this.#setEnableSummaryAccount(!entry.isMatched && this.originalEntryId === null);
|
||||
this.summary = entry.getSummary();
|
||||
this.#setEnableSummaryAccount(!lineItem.isMatched && this.originalLineItemId === null);
|
||||
this.summary = lineItem.getSummary();
|
||||
if (this.summary === null) {
|
||||
this.#summaryControl.classList.remove("accounting-not-empty");
|
||||
} else {
|
||||
this.#summaryControl.classList.add("accounting-not-empty");
|
||||
}
|
||||
this.#summaryText.innerText = this.summary === null? "": this.summary;
|
||||
if (entry.getAccountCode() === null) {
|
||||
if (lineItem.getAccountCode() === null) {
|
||||
this.#accountControl.classList.remove("accounting-not-empty");
|
||||
} else {
|
||||
this.#accountControl.classList.add("accounting-not-empty");
|
||||
}
|
||||
this.accountCode = entry.getAccountCode();
|
||||
this.accountText = entry.getAccountText();
|
||||
this.accountCode = lineItem.getAccountCode();
|
||||
this.accountText = lineItem.getAccountText();
|
||||
this.#accountText.innerText = this.accountText;
|
||||
this.#amountInput.value = entry.getAmount() === null? "": String(entry.getAmount());
|
||||
this.#amountInput.value = lineItem.getAmount() === null? "": String(lineItem.getAmount());
|
||||
const maxAmount = this.#getMaxAmount();
|
||||
this.#amountInput.max = maxAmount === null? "": maxAmount;
|
||||
this.#amountInput.min = entry.getAmountMin() === null? "": String(entry.getAmountMin());
|
||||
this.#amountInput.min = lineItem.getAmountMin() === null? "": String(lineItem.getAmountMin());
|
||||
this.#validate();
|
||||
}
|
||||
|
||||
@ -561,10 +561,10 @@ class JournalEntryEditor {
|
||||
* @return {Decimal|null} the max amount
|
||||
*/
|
||||
#getMaxAmount() {
|
||||
if (this.originalEntryId === null) {
|
||||
if (this.originalLineItemId === null) {
|
||||
return null;
|
||||
}
|
||||
return this.originalEntrySelector.getNetBalance(this.entry, this.form, this.originalEntryId);
|
||||
return this.originalLineItemSelector.getNetBalance(this.lineItem, this.form, this.originalLineItemId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -575,11 +575,11 @@ class JournalEntryEditor {
|
||||
#setEnableSummaryAccount(isEnabled) {
|
||||
if (isEnabled) {
|
||||
this.#summaryControl.dataset.bsToggle = "modal";
|
||||
this.#summaryControl.dataset.bsTarget = "#accounting-summary-editor-" + this.#side.entryType + "-modal";
|
||||
this.#summaryControl.dataset.bsTarget = "#accounting-summary-editor-" + this.#sideSubForm.side + "-modal";
|
||||
this.#summaryControl.classList.remove("accounting-disabled");
|
||||
this.#summaryControl.classList.add("accounting-clickable");
|
||||
this.#accountControl.dataset.bsToggle = "modal";
|
||||
this.#accountControl.dataset.bsTarget = "#accounting-account-selector-" + this.#side.entryType + "-modal";
|
||||
this.#accountControl.dataset.bsTarget = "#accounting-account-selector-" + this.#sideSubForm.side + "-modal";
|
||||
this.#accountControl.classList.remove("accounting-disabled");
|
||||
this.#accountControl.classList.add("accounting-clickable");
|
||||
} else {
|
@ -65,7 +65,7 @@ First written: 2023/2/1
|
||||
<div id="accounting-is-need-offset-control" class="form-check form-switch mb-3 {% if form.base_code.data[0] not in ["1", "2", "3"] %} d-none {% endif %}">
|
||||
<input id="accounting-is-need-offset" class="form-check-input" type="checkbox" name="is_need_offset" value="1" {% if form.is_need_offset.data %} checked="checked" {% endif %}>
|
||||
<label class="form-check-label" for="accounting-is-need-offset">
|
||||
{{ A_("The entries in the account need offset.") }}
|
||||
{{ A_("The line items in the account need offset.") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
@ -19,9 +19,9 @@ income-expenses-row-desktop.html: The row in the income and expenses log for the
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/3/8
|
||||
#}
|
||||
<div>{{ entry.date|accounting_format_date }}</div>
|
||||
<div>{{ entry.account.title|title }}</div>
|
||||
<div>{{ entry.summary|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ entry.income|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ entry.expense|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
|
||||
<div>{{ line_item.date|accounting_format_date }}</div>
|
||||
<div>{{ line_item.account.title|title }}</div>
|
||||
<div>{{ line_item.summary|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ line_item.income|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ line_item.expense|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount {% if line_item.balance < 0 %} text-danger {% endif %}">{{ line_item.balance|accounting_report_format_amount }}</div>
|
||||
|
@ -20,31 +20,31 @@ Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/3/5
|
||||
#}
|
||||
<div>
|
||||
{% if entry.date or entry.account %}
|
||||
{% if line_item.date or line_item.account %}
|
||||
<div class="text-muted small">
|
||||
{% if entry.date %}
|
||||
{{ entry.date|accounting_format_date }}
|
||||
{% if line_item.date %}
|
||||
{{ line_item.date|accounting_format_date }}
|
||||
{% endif %}
|
||||
{% if entry.account %}
|
||||
{{ entry.account.title|title }}
|
||||
{% if line_item.account %}
|
||||
{{ line_item.account.title|title }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if entry.summary %}
|
||||
<div>{{ entry.summary }}</div>
|
||||
{% if line_item.summary %}
|
||||
<div>{{ line_item.summary }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="text-nowrap">
|
||||
{% if entry.income %}
|
||||
<span class="badge rounded-pill bg-success">+{{ entry.income|accounting_format_amount }}</span>
|
||||
{% if line_item.income %}
|
||||
<span class="badge rounded-pill bg-success">+{{ line_item.income|accounting_format_amount }}</span>
|
||||
{% endif %}
|
||||
{% if entry.expense %}
|
||||
<span class="badge rounded-pill bg-warning">-{{ entry.expense|accounting_format_amount }}</span>
|
||||
{% if line_item.expense %}
|
||||
<span class="badge rounded-pill bg-warning">-{{ line_item.expense|accounting_format_amount }}</span>
|
||||
{% endif %}
|
||||
{% if entry.balance < 0 %}
|
||||
<span class="badge rounded-pill bg-danger">{{ entry.balance|accounting_format_amount }}</span>
|
||||
{% if line_item.balance < 0 %}
|
||||
<span class="badge rounded-pill bg-danger">{{ line_item.balance|accounting_format_amount }}</span>
|
||||
{% else %}
|
||||
<span class="badge rounded-pill bg-primary">{{ entry.balance|accounting_format_amount }}</span>
|
||||
<span class="badge rounded-pill bg-primary">{{ line_item.balance|accounting_format_amount }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -19,10 +19,10 @@ ledger-row-desktop.html: The row in the ledger for the desktop computers
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/3/8
|
||||
#}
|
||||
<div>{{ entry.date|accounting_format_date }}</div>
|
||||
<div>{{ entry.summary|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ entry.debit|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ entry.credit|accounting_format_amount|accounting_default }}</div>
|
||||
<div>{{ line_item.date|accounting_format_date }}</div>
|
||||
<div>{{ line_item.summary|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ line_item.debit|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ line_item.credit|accounting_format_amount|accounting_default }}</div>
|
||||
{% if report.account.is_real %}
|
||||
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
|
||||
<div class="accounting-amount {% if line_item.balance < 0 %} text-danger {% endif %}">{{ line_item.balance|accounting_report_format_amount }}</div>
|
||||
{% endif %}
|
||||
|
@ -20,24 +20,24 @@ Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/3/5
|
||||
#}
|
||||
<div>
|
||||
{% if entry.date %}
|
||||
{% if line_item.date %}
|
||||
<div class="text-muted small">
|
||||
{{ entry.date|accounting_format_date }}
|
||||
{{ line_item.date|accounting_format_date }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if entry.summary %}
|
||||
<div>{{ entry.summary }}</div>
|
||||
{% if line_item.summary %}
|
||||
<div>{{ line_item.summary }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% if entry.debit %}
|
||||
<span class="badge rounded-pill bg-success">+{{ entry.debit|accounting_format_amount }}</span>
|
||||
{% if line_item.debit %}
|
||||
<span class="badge rounded-pill bg-success">+{{ line_item.debit|accounting_format_amount }}</span>
|
||||
{% endif %}
|
||||
{% if entry.credit %}
|
||||
<span class="badge rounded-pill bg-warning">-{{ entry.credit|accounting_format_amount }}</span>
|
||||
{% if line_item.credit %}
|
||||
<span class="badge rounded-pill bg-warning">-{{ line_item.credit|accounting_format_amount }}</span>
|
||||
{% endif %}
|
||||
{% if report.account.is_real %}
|
||||
<span class="badge rounded-pill bg-primary">{{ entry.balance|accounting_format_amount }}</span>
|
||||
<span class="badge rounded-pill bg-primary">{{ line_item.balance|accounting_format_amount }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -62,26 +62,26 @@ First written: 2023/3/5
|
||||
</div>
|
||||
<div class="accounting-report-table-body">
|
||||
{% if report.brought_forward %}
|
||||
{% with entry = report.brought_forward %}
|
||||
{% with line_item = report.brought_forward %}
|
||||
<div class="accounting-report-table-row">
|
||||
{% include "accounting/report/include/income-expenses-row-desktop.html" %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% for entry in report.entries %}
|
||||
<a class="accounting-report-table-row" href="{{ entry.url|accounting_append_next }}">
|
||||
{% for line_item in report.line_items %}
|
||||
<a class="accounting-report-table-row" href="{{ line_item.url|accounting_append_next }}">
|
||||
{% include "accounting/report/include/income-expenses-row-desktop.html" %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if report.total %}
|
||||
{% with entry = report.total %}
|
||||
{% with line_item = report.total %}
|
||||
<div class="accounting-report-table-footer">
|
||||
<div class="accounting-report-table-row">
|
||||
<div>{{ A_("Total") }}</div>
|
||||
<div class="accounting-amount">{{ entry.income|accounting_format_amount }}</div>
|
||||
<div class="accounting-amount">{{ entry.expense|accounting_format_amount }}</div>
|
||||
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
|
||||
<div class="accounting-amount">{{ line_item.income|accounting_format_amount }}</div>
|
||||
<div class="accounting-amount">{{ line_item.expense|accounting_format_amount }}</div>
|
||||
<div class="accounting-amount {% if line_item.balance < 0 %} text-danger {% endif %}">{{ line_item.balance|accounting_report_format_amount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
@ -90,19 +90,19 @@ First written: 2023/3/5
|
||||
|
||||
<div class="list-group d-md-none">
|
||||
{% if report.brought_forward %}
|
||||
{% with entry = report.brought_forward %}
|
||||
{% with line_item = report.brought_forward %}
|
||||
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{% include "accounting/report/include/income-expenses-row-mobile.html" %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% for entry in report.entries %}
|
||||
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ entry.url|accounting_append_next }}">
|
||||
{% for line_item in report.line_items %}
|
||||
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ line_item.url|accounting_append_next }}">
|
||||
{% include "accounting/report/include/income-expenses-row-mobile.html" %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if report.total %}
|
||||
{% with entry = report.total %}
|
||||
{% with line_item = report.total %}
|
||||
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{% include "accounting/report/include/income-expenses-row-mobile.html" %}
|
||||
</div>
|
||||
|
@ -59,41 +59,41 @@ First written: 2023/3/4
|
||||
</div>
|
||||
</div>
|
||||
<div class="accounting-report-table-body">
|
||||
{% for entry in report.entries %}
|
||||
<a class="accounting-report-table-row" href="{{ url_for("accounting.voucher.detail", voucher=entry.voucher)|accounting_append_next }}">
|
||||
<div>{{ entry.voucher.date|accounting_format_date }}</div>
|
||||
<div>{{ entry.currency.name }}</div>
|
||||
{% for line_item in report.line_items %}
|
||||
<a class="accounting-report-table-row" href="{{ url_for("accounting.voucher.detail", voucher=line_item.voucher)|accounting_append_next }}">
|
||||
<div>{{ line_item.voucher.date|accounting_format_date }}</div>
|
||||
<div>{{ line_item.currency.name }}</div>
|
||||
<div>
|
||||
<span class="d-none d-md-inline">{{ entry.account.code }}</span>
|
||||
{{ entry.account.title|title }}
|
||||
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
|
||||
{{ line_item.account.title|title }}
|
||||
</div>
|
||||
<div>{{ entry.summary|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ entry.debit|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ entry.credit|accounting_format_amount|accounting_default }}</div>
|
||||
<div>{{ line_item.summary|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ line_item.debit|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ line_item.credit|accounting_format_amount|accounting_default }}</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-group d-md-none">
|
||||
{% for entry in report.entries %}
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.voucher.detail", voucher=entry.voucher)|accounting_append_next }}">
|
||||
{% for line_item in report.line_items %}
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.voucher.detail", voucher=line_item.voucher)|accounting_append_next }}">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div {% if not entry.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
|
||||
<div {% if not line_item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
|
||||
<div class="text-muted small">
|
||||
{{ entry.voucher.date|accounting_format_date }}
|
||||
{{ entry.account.title|title }}
|
||||
{% if entry.currency.code != accounting_default_currency_code() %}
|
||||
<span class="badge rounded-pill bg-info">{{ entry.currency.code }}</span>
|
||||
{{ line_item.voucher.date|accounting_format_date }}
|
||||
{{ line_item.account.title|title }}
|
||||
{% if line_item.currency.code != accounting_default_currency_code() %}
|
||||
<span class="badge rounded-pill bg-info">{{ line_item.currency.code }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if entry.summary is not none %}
|
||||
<div>{{ entry.summary }}</div>
|
||||
{% if line_item.summary is not none %}
|
||||
<div>{{ line_item.summary }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="badge rounded-pill bg-info">{{ entry.amount|accounting_format_amount }}</span>
|
||||
<span class="badge rounded-pill bg-info">{{ line_item.amount|accounting_format_amount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -63,27 +63,27 @@ First written: 2023/3/5
|
||||
</div>
|
||||
<div class="accounting-report-table-body">
|
||||
{% if report.brought_forward %}
|
||||
{% with entry = report.brought_forward %}
|
||||
{% with line_item = report.brought_forward %}
|
||||
<div class="accounting-report-table-row">
|
||||
{% include "accounting/report/include/ledger-row-desktop.html" %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% for entry in report.entries %}
|
||||
<a class="accounting-report-table-row" href="{{ entry.url|accounting_append_next }}">
|
||||
{% for line_item in report.line_items %}
|
||||
<a class="accounting-report-table-row" href="{{ line_item.url|accounting_append_next }}">
|
||||
{% include "accounting/report/include/ledger-row-desktop.html" %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if report.total %}
|
||||
{% with entry = report.total %}
|
||||
{% with line_item = report.total %}
|
||||
<div class="accounting-report-table-footer">
|
||||
<div class="accounting-report-table-row">
|
||||
<div>{{ A_("Total") }}</div>
|
||||
<div class="accounting-amount">{{ entry.debit|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ entry.credit|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ line_item.debit|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ line_item.credit|accounting_format_amount|accounting_default }}</div>
|
||||
{% if report.account.is_real %}
|
||||
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
|
||||
<div class="accounting-amount {% if line_item.balance < 0 %} text-danger {% endif %}">{{ line_item.balance|accounting_report_format_amount }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@ -93,19 +93,19 @@ First written: 2023/3/5
|
||||
|
||||
<div class="list-group d-md-none">
|
||||
{% if report.brought_forward %}
|
||||
{% with entry = report.brought_forward %}
|
||||
{% with line_item = report.brought_forward %}
|
||||
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{% include "accounting/report/include/ledger-row-mobile.html" %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% for entry in report.entries %}
|
||||
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ entry.url|accounting_append_next }}">
|
||||
{% for line_item in report.line_items %}
|
||||
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ line_item.url|accounting_append_next }}">
|
||||
{% include "accounting/report/include/ledger-row-mobile.html" %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if report.total %}
|
||||
{% with entry = report.total %}
|
||||
{% with line_item = report.total %}
|
||||
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{% include "accounting/report/include/ledger-row-mobile.html" %}
|
||||
</div>
|
||||
|
@ -56,41 +56,41 @@ First written: 2023/3/8
|
||||
</div>
|
||||
</div>
|
||||
<div class="accounting-report-table-body">
|
||||
{% for entry in report.entries %}
|
||||
<a class="accounting-report-table-row" href="{{ url_for("accounting.voucher.detail", voucher=entry.voucher)|accounting_append_next }}">
|
||||
<div>{{ entry.voucher.date|accounting_format_date }}</div>
|
||||
<div>{{ entry.currency.name }}</div>
|
||||
{% for line_item in report.line_items %}
|
||||
<a class="accounting-report-table-row" href="{{ url_for("accounting.voucher.detail", voucher=line_item.voucher)|accounting_append_next }}">
|
||||
<div>{{ line_item.voucher.date|accounting_format_date }}</div>
|
||||
<div>{{ line_item.currency.name }}</div>
|
||||
<div>
|
||||
<span class="d-none d-md-inline">{{ entry.account.code }}</span>
|
||||
{{ entry.account.title|title }}
|
||||
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
|
||||
{{ line_item.account.title|title }}
|
||||
</div>
|
||||
<div>{{ entry.summary|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ entry.debit|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ entry.credit|accounting_format_amount|accounting_default }}</div>
|
||||
<div>{{ line_item.summary|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ line_item.debit|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ line_item.credit|accounting_format_amount|accounting_default }}</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-group d-md-none">
|
||||
{% for entry in report.entries %}
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.voucher.detail", voucher=entry.voucher)|accounting_append_next }}">
|
||||
{% for line_item in report.line_items %}
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.voucher.detail", voucher=line_item.voucher)|accounting_append_next }}">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div {% if not entry.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
|
||||
<div {% if not line_item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
|
||||
<div class="text-muted small">
|
||||
{{ entry.voucher.date|accounting_format_date }}
|
||||
{{ entry.account.title|title }}
|
||||
{% if entry.currency.code != accounting_default_currency_code() %}
|
||||
<span class="badge rounded-pill bg-info">{{ entry.currency.code }}</span>
|
||||
{{ line_item.voucher.date|accounting_format_date }}
|
||||
{{ line_item.account.title|title }}
|
||||
{% if line_item.currency.code != accounting_default_currency_code() %}
|
||||
<span class="badge rounded-pill bg-info">{{ line_item.currency.code }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if entry.summary is not none %}
|
||||
<div>{{ entry.summary }}</div>
|
||||
{% if line_item.summary is not none %}
|
||||
<div>{{ line_item.summary }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="badge rounded-pill bg-info">{{ entry.amount|accounting_format_amount }}</span>
|
||||
<span class="badge rounded-pill bg-info">{{ line_item.amount|accounting_format_amount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -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-entry accounting-voucher-entry-header">{{ A_("Content") }}</li>
|
||||
{% with entries = currency.debit %}
|
||||
{% include "accounting/voucher/include/detail-entries.html" %}
|
||||
<li class="list-group-item accounting-voucher-line-item accounting-voucher-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-entry accounting-voucher-entry-total">
|
||||
<li class="list-group-item accounting-voucher-line-item accounting-voucher-line-item-total">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>{{ A_("Total") }}</div>
|
||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
||||
|
@ -44,31 +44,31 @@ First written: 2023/2/25
|
||||
<div class="mb-3">
|
||||
<div id="accounting-currency-{{ currency_index }}-debit" class="form-control accounting-material-text-field accounting-not-empty {% if debit_errors %} is-invalid {% endif %}">
|
||||
<label class="form-label" for="accounting-currency-{{ currency_index }}-debit">{{ A_("Content") }}</label>
|
||||
<ul id="accounting-currency-{{ currency_index }}-debit-list" class="list-group accounting-entry-list">
|
||||
{% for entry_form in debit_forms %}
|
||||
<ul id="accounting-currency-{{ currency_index }}-debit-list" class="list-group accounting-line-item-list">
|
||||
{% for line_item_form in debit_forms %}
|
||||
{% with currency_index = currency_index,
|
||||
entry_type = "debit",
|
||||
entry_index = loop.index,
|
||||
entry_id = entry_form.eid.data,
|
||||
only_one_entry_form = debit_forms|length == 1,
|
||||
account_code_data = entry_form.account_code.data|accounting_default,
|
||||
account_code_error = entry_form.account_code.errors,
|
||||
account_text = entry_form.account_text,
|
||||
summary_data = entry_form.summary.data|accounting_default,
|
||||
summary_errors = entry_form.summary.errors,
|
||||
original_entry_id_data = entry_form.original_entry_id.data|accounting_default,
|
||||
original_entry_date = entry_form.original_entry_date|accounting_default,
|
||||
original_entry_text = entry_form.original_entry_text|accounting_default,
|
||||
is_need_offset = entry_form.is_need_offset,
|
||||
offset_entries = entry_form.offsets,
|
||||
offset_total = entry_form.offset_total|accounting_default("0"),
|
||||
net_balance_data = entry_form.net_balance,
|
||||
net_balance_text = entry_form.net_balance|accounting_format_amount,
|
||||
amount_data = entry_form.amount.data|accounting_voucher_format_amount_input,
|
||||
amount_errors = entry_form.amount.errors,
|
||||
amount_text = entry_form.amount.data|accounting_format_amount,
|
||||
entry_errors = entry_form.all_errors %}
|
||||
{% include "accounting/voucher/include/form-entry-item.html" %}
|
||||
side = "debit",
|
||||
line_item_index = loop.index,
|
||||
line_item_id = line_item_form.eid.data,
|
||||
only_one_line_item_form = debit_forms|length == 1,
|
||||
account_code_data = line_item_form.account_code.data|accounting_default,
|
||||
account_code_error = line_item_form.account_code.errors,
|
||||
account_text = line_item_form.account_text,
|
||||
summary_data = line_item_form.summary.data|accounting_default,
|
||||
summary_errors = line_item_form.summary.errors,
|
||||
original_line_item_id_data = line_item_form.original_line_item_id.data|accounting_default,
|
||||
original_line_item_date = line_item_form.original_line_item_date|accounting_default,
|
||||
original_line_item_text = line_item_form.original_line_item_text|accounting_default,
|
||||
is_need_offset = line_item_form.is_need_offset,
|
||||
offset_items = line_item_form.offsets,
|
||||
offset_total = line_item_form.offset_total|accounting_default("0"),
|
||||
net_balance_data = line_item_form.net_balance,
|
||||
net_balance_text = line_item_form.net_balance|accounting_format_amount,
|
||||
amount_data = line_item_form.amount.data|accounting_voucher_format_amount_input,
|
||||
amount_errors = line_item_form.amount.errors,
|
||||
amount_text = line_item_form.amount.data|accounting_format_amount,
|
||||
line_item_errors = line_item_form.all_errors %}
|
||||
{% include "accounting/voucher/include/form-line-item.html" %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@ -79,7 +79,7 @@ First written: 2023/2/25
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="accounting-currency-{{ currency_index }}-debit-add-entry" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-entry-type="debit" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal">
|
||||
<button id="accounting-currency-{{ currency_index }}-debit-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-side="debit" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||
<i class="fas fa-plus"></i>
|
||||
{{ A_("New") }}
|
||||
</button>
|
||||
|
@ -50,7 +50,7 @@ First written: 2023/2/25
|
||||
{% with summary_editor = form.summary_editor.debit %}
|
||||
{% include "accounting/voucher/include/summary-editor-modal.html" %}
|
||||
{% endwith %}
|
||||
{% with entry_type = "debit",
|
||||
{% with side = "debit",
|
||||
account_options = form.debit_account_options %}
|
||||
{% include "accounting/voucher/include/account-selector-modal.html" %}
|
||||
{% endwith %}
|
||||
|
@ -19,35 +19,35 @@ account-selector-modal.html: The modal for the account selector
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
<div id="accounting-account-selector-{{ entry_type }}-modal" class="modal fade accounting-account-selector" data-entry-type="{{ entry_type }}" tabindex="-1" aria-labelledby="accounting-account-selector-{{ entry_type }}-modal-label" aria-hidden="true">
|
||||
<div id="accounting-account-selector-{{ side }}-modal" class="modal fade accounting-account-selector" data-side="{{ side }}" tabindex="-1" aria-labelledby="accounting-account-selector-{{ side }}-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="accounting-account-selector-{{ entry_type }}-modal-label">{{ A_("Select Account") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal" aria-label="{{ A_("Close") }}"></button>
|
||||
<h1 class="modal-title fs-5" id="accounting-account-selector-{{ side }}-modal-label">{{ A_("Select Account") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal" aria-label="{{ A_("Close") }}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="input-group mb-2">
|
||||
<input id="accounting-account-selector-{{ entry_type }}-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
||||
<label class="input-group-text" for="accounting-account-selector-{{ entry_type }}-query">
|
||||
<input id="accounting-account-selector-{{ side }}-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
||||
<label class="input-group-text" for="accounting-account-selector-{{ side }}-query">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
{{ A_("Search") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ul id="accounting-account-selector-{{ entry_type }}-option-list" class="list-group accounting-selector-list">
|
||||
<ul id="accounting-account-selector-{{ side }}-option-list" class="list-group accounting-selector-list">
|
||||
{% for account in account_options %}
|
||||
<li id="accounting-account-selector-{{ entry_type }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ entry_type }}-option {% if account.is_in_use %} accounting-account-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-content="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal">
|
||||
<li id="accounting-account-selector-{{ side }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ side }}-option {% if account.is_in_use %} accounting-account-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-content="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||
{{ account }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li id="accounting-account-selector-{{ entry_type }}-more" class="list-group-item accounting-clickable">{{ A_("More…") }}</li>
|
||||
<li id="accounting-account-selector-{{ side }}-more" class="list-group-item accounting-clickable">{{ A_("More…") }}</li>
|
||||
</ul>
|
||||
<p id="accounting-account-selector-{{ entry_type }}-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
|
||||
<p id="accounting-account-selector-{{ side }}-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal">{{ A_("Cancel") }}</button>
|
||||
<button id="accounting-account-selector-{{ entry_type }}-btn-clear" type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal">{{ A_("Clear") }}</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">{{ A_("Cancel") }}</button>
|
||||
<button id="accounting-account-selector-{{ side }}-btn-clear" type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">{{ A_("Clear") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
detail-entries-item: The journal entries in the voucher detail
|
||||
detail-line-items-item: The line items in the voucher detail
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -20,28 +20,28 @@ Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/3/14
|
||||
#}
|
||||
{# <ul> For SonarQube not to complain about incorrect HTML #}
|
||||
{% for entry in entries %}
|
||||
<li class="list-group-item accounting-voucher-entry">
|
||||
{% for line_item in line_items %}
|
||||
<li class="list-group-item accounting-voucher-line-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<div class="small">{{ entry.account }}</div>
|
||||
{% if entry.summary is not none %}
|
||||
<div>{{ entry.summary }}</div>
|
||||
<div class="small">{{ line_item.account }}</div>
|
||||
{% if line_item.summary is not none %}
|
||||
<div>{{ line_item.summary }}</div>
|
||||
{% endif %}
|
||||
{% if entry.original_entry %}
|
||||
<div class="fst-italic small accounting-original-entry">
|
||||
<a href="{{ url_for("accounting.voucher.detail", voucher=entry.original_entry.voucher)|accounting_append_next }}">
|
||||
{{ A_("Offset %(entry)s", entry=entry.original_entry) }}
|
||||
{% if line_item.original_line_item %}
|
||||
<div class="fst-italic small accounting-original-line-item">
|
||||
<a href="{{ url_for("accounting.voucher.detail", voucher=line_item.original_line_item.voucher)|accounting_append_next }}">
|
||||
{{ A_("Offset %(item)s", item=line_item.original_line_item) }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if entry.is_need_offset %}
|
||||
<div class="fst-italic small accounting-offset-entries">
|
||||
{% if entry.offsets %}
|
||||
{% if line_item.is_need_offset %}
|
||||
<div class="fst-italic small accounting-offset-line-items">
|
||||
{% if line_item.offsets %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>{{ A_("Offsets") }}</div>
|
||||
<ul class="ms-2 ps-0">
|
||||
{% for offset in entry.offsets %}
|
||||
{% for offset in line_item.offsets %}
|
||||
<li>
|
||||
<a href="{{ url_for("accounting.voucher.detail", voucher=offset.voucher)|accounting_append_next }}">
|
||||
{{ offset.voucher.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}
|
||||
@ -50,10 +50,10 @@ First written: 2023/3/14
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if entry.balance %}
|
||||
{% if line_item.balance %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>{{ A_("Net balance") }}</div>
|
||||
<div>{{ entry.balance|accounting_format_amount }}</div>
|
||||
<div>{{ line_item.balance|accounting_format_amount }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="d-flex justify-content-between">
|
||||
@ -68,7 +68,7 @@ First written: 2023/3/14
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>{{ entry.amount|accounting_format_amount }}</div>
|
||||
<div>{{ line_item.amount|accounting_format_amount }}</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
@ -1,74 +0,0 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
entry-sub-form.html: The journal entry sub-form in the voucher form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
{# <ul> For SonarQube not to complain about incorrect HTML #}
|
||||
<li id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}" class="list-group-item list-group-item-action d-flex justify-content-between accounting-currency-{{ currency_index }}-{{ entry_type }} {% if offset_entries %} accounting-matched-entry {% endif %}" data-currency-index="{{ currency_index }}" data-entry-type="{{ entry_type }}" data-entry-index="{{ entry_index }}" {% if is_need_offset %} data-is-need-offset="true" {% endif %}>
|
||||
{% if entry_id %}
|
||||
<input type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-eid" value="{{ entry_id }}">
|
||||
{% endif %}
|
||||
<input id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-no" type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-no" value="{{ entry_index }}">
|
||||
<input id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-original-entry-id" class="accounting-original-entry-id" type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-original_entry_id" value="{{ original_entry_id_data }}" data-date="{{ original_entry_date }}" data-text="{{ original_entry_text }}">
|
||||
<input id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-account-code" type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-account_code" value="{{ account_code_data }}" data-text="{{ account_text }}">
|
||||
<input id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-summary" type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-summary" value="{{ summary_data }}">
|
||||
<input id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-amount" type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-amount" value="{{ amount_data }}" data-min="{{ offset_total }}">
|
||||
<div class="accounting-entry-content">
|
||||
<div id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-control" class="form-control clickable d-flex justify-content-between {% if entry_errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal">
|
||||
<div>
|
||||
<div id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-account-text" class="small">{{ account_text }}</div>
|
||||
<div id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-summary-text">{{ summary_data }}</div>
|
||||
<div id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-original-entry-text" class="fst-italic small accounting-original-entry {% if not original_entry_text %} d-none {% endif %}">
|
||||
{% if original_entry_text %}{{ A_("Offset %(entry)s", entry=original_entry_text) }}{% endif %}
|
||||
</div>
|
||||
<div id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-offsets" class="fst-italic small accounting-offset-entries {% if not is_need_offset %} d-none {% endif %}">
|
||||
{% if offset_entries %}
|
||||
<div class="d-flex justify-content-between {% if not offset_entries %} d-none {% endif %}">
|
||||
<div>{{ A_("Offsets") }}</div>
|
||||
<ul class="ms-2 ps-0">
|
||||
{% for offset in offset_entries %}
|
||||
<li>{{ offset.voucher.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if net_balance_data == 0 %}
|
||||
<div>{{ A_("Fully offset") }}</div>
|
||||
{% else %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>{{ A_("Net balance") }}</div>
|
||||
<div>{{ net_balance_text }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ A_("Unmatched") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div><span id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-amount-text" class="badge rounded-pill bg-primary">{{ amount_text }}</span></div>
|
||||
</div>
|
||||
<div id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-error" class="invalid-feedback">{% if entry_errors %}{{ entry_errors[0] }}{% endif %}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_entry_form or offset_entries %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{# </ul> For SonarQube not to complain about incorrect HTML #}
|
@ -0,0 +1,74 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
form-line-item.html: The line item sub-form in the voucher form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
{# <ul> For SonarQube not to complain about incorrect HTML #}
|
||||
<li id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}" class="list-group-item list-group-item-action d-flex justify-content-between accounting-currency-{{ currency_index }}-{{ side }} {% if offset_items %} accounting-matched-line-item {% endif %}" data-currency-index="{{ currency_index }}" data-side="{{ side }}" data-line-item-index="{{ line_item_index }}" {% if is_need_offset %} data-is-need-offset="true" {% endif %}>
|
||||
{% if line_item_id %}
|
||||
<input type="hidden" name="currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-eid" value="{{ line_item_id }}">
|
||||
{% endif %}
|
||||
<input id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-no" type="hidden" name="currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-no" value="{{ line_item_index }}">
|
||||
<input id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-original-line-item-id" class="accounting-original-line-item-id" type="hidden" name="currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-original_line_item_id" value="{{ original_line_item_id_data }}" data-date="{{ original_line_item_date }}" data-text="{{ original_line_item_text }}">
|
||||
<input id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-account-code" type="hidden" name="currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-account_code" value="{{ account_code_data }}" data-text="{{ account_text }}">
|
||||
<input id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-summary" type="hidden" name="currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-summary" value="{{ summary_data }}">
|
||||
<input id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-amount" type="hidden" name="currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-amount" value="{{ amount_data }}" data-min="{{ offset_total }}">
|
||||
<div class="accounting-line-item-content">
|
||||
<div id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-control" class="form-control clickable d-flex justify-content-between {% if line_item_errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||
<div>
|
||||
<div id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-account-text" class="small">{{ account_text }}</div>
|
||||
<div id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-summary-text">{{ summary_data }}</div>
|
||||
<div id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-original-line-item-text" class="fst-italic small accounting-original-line-item {% if not original_line_item_text %} d-none {% endif %}">
|
||||
{% if original_line_item_text %}{{ A_("Offset %(item)s", item=original_line_item_text) }}{% endif %}
|
||||
</div>
|
||||
<div id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-offsets" class="fst-italic small accounting-offset-line-items {% if not is_need_offset %} d-none {% endif %}">
|
||||
{% if offset_items %}
|
||||
<div class="d-flex justify-content-between {% if not offset_items %} d-none {% endif %}">
|
||||
<div>{{ A_("Offsets") }}</div>
|
||||
<ul class="ms-2 ps-0">
|
||||
{% for offset in offset_items %}
|
||||
<li>{{ offset.voucher.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if net_balance_data == 0 %}
|
||||
<div>{{ A_("Fully offset") }}</div>
|
||||
{% else %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>{{ A_("Net balance") }}</div>
|
||||
<div>{{ net_balance_text }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ A_("Unmatched") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div><span id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-amount-text" class="badge rounded-pill bg-primary">{{ amount_text }}</span></div>
|
||||
</div>
|
||||
<div id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-error" class="invalid-feedback">{% if line_item_errors %}{{ line_item_errors[0] }}{% endif %}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_line_item_form or offset_items %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}-{{ side }}-{{ line_item_index }}">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{# </ul> For SonarQube not to complain about incorrect HTML #}
|
@ -24,9 +24,9 @@ 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/journal-entry-editor.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/voucher-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-entry-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/summary-editor.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@ -39,7 +39,7 @@ First written: 2023/2/26
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form id="accounting-form" action="{% block action_url %}{% endblock %}" method="post" data-currency-template="{{ currency_template }}" data-entry-template="{{ entry_template }}">
|
||||
<form id="accounting-form" action="{% block action_url %}{% endblock %}" method="post" data-currency-template="{{ currency_template }}" data-line-item-template="{{ line_item_template }}">
|
||||
{{ form.csrf_token }}
|
||||
{% if request.args.next %}
|
||||
<input type="hidden" name="next" value="{{ request.args.next }}">
|
||||
@ -88,8 +88,8 @@ First written: 2023/2/26
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% include "accounting/voucher/include/journal-entry-editor-modal.html" %}
|
||||
{% include "accounting/voucher/include/voucher-line-item-editor-modal.html" %}
|
||||
{% block form_modals %}{% endblock %}
|
||||
{% include "accounting/voucher/include/original-entry-selector-modal.html" %}
|
||||
{% include "accounting/voucher/include/original-line-item-selector-modal.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
@ -1,76 +0,0 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
journal-entry-editor-modal.html: The modal of the journal entry editor
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
<form id="accounting-entry-editor">
|
||||
<div id="accounting-entry-editor-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-entry-editor-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="accounting-entry-editor-modal-label">{{ A_("Journal Entry Content") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ A_("Close") }}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="accounting-entry-editor-original-entry-container" class="d-flex justify-content-between mb-3">
|
||||
<div class="accounting-entry-editor-original-entry-content">
|
||||
<div id="accounting-entry-editor-original-entry-control" class="form-control accounting-clickable accounting-material-text-field" data-bs-toggle="modal" data-bs-target="#accounting-original-entry-selector-modal">
|
||||
<label class="form-label" for="accounting-entry-editor-original-entry">{{ A_("Original Entry") }}</label>
|
||||
<div id="accounting-entry-editor-original-entry"></div>
|
||||
</div>
|
||||
<div id="accounting-entry-editor-original-entry-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="accounting-entry-editor-original-entry-delete" class="btn btn-danger rounded-circle" type="button">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div id="accounting-entry-editor-summary-control" class="form-control accounting-clickable accounting-material-text-field" data-bs-toggle="modal" data-bs-target="">
|
||||
<label class="form-label" for="accounting-entry-editor-summary">{{ A_("Summary") }}</label>
|
||||
<div id="accounting-entry-editor-summary"></div>
|
||||
</div>
|
||||
<div id="accounting-entry-editor-summary-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div id="accounting-entry-editor-account-control" class="form-control accounting-clickable accounting-material-text-field" data-bs-toggle="modal" data-bs-target="">
|
||||
<label class="form-label" for="accounting-entry-editor-account">{{ A_("Account") }}</label>
|
||||
<div id="accounting-entry-editor-account"></div>
|
||||
</div>
|
||||
<div id="accounting-entry-editor-account-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input id="accounting-entry-editor-amount" class="form-control" type="number" value="" min="0" max="" step="0.01" placeholder=" " required="required">
|
||||
<label for="accounting-entry-editor-amount">{{ A_("Amount") }}</label>
|
||||
<div id="accounting-entry-editor-amount-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
|
||||
<button type="submit" class="btn btn-primary">{{ A_("Save") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -1,56 +0,0 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
original-entry-selector-modal.html: The modal of the original entry selector
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
<div id="accounting-original-entry-selector-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-original-entry-selector-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="accounting-original-entry-selector-modal-label">{{ A_("Select Original Entry") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal" aria-label="{{ A_("Close") }}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="input-group mb-2">
|
||||
<input id="accounting-original-entry-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
||||
<label class="input-group-text" for="accounting-original-entry-selector-query">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
{{ A_("Search") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ul id="accounting-original-entry-selector-option-list" class="list-group accounting-selector-list">
|
||||
{% for entry in form.original_entry_options %}
|
||||
<li id="accounting-original-entry-selector-option-{{ entry.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-entry-selector-option" data-id="{{ entry.id }}" data-date="{{ entry.voucher.date }}" data-entry-type="{{ "debit" if entry.is_debit else "credit" }}" data-currency-code="{{ entry.currency.code }}" data-account-code="{{ entry.account_code }}" data-account-text="{{ entry.account }}" data-summary="{{ entry.summary|accounting_default }}" data-net-balance="{{ entry.net_balance|accounting_voucher_format_amount_input }}" data-text="{{ entry }}" data-query-values="{{ entry.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal">
|
||||
<div>{{ entry.voucher.date|accounting_format_date }} {{ entry.summary|accounting_default }}</div>
|
||||
<div>
|
||||
<span class="badge bg-primary rounded-pill">
|
||||
<span id="accounting-original-entry-selector-option-{{ entry.id }}-net-balance">{{ entry.net_balance|accounting_format_amount }}</span>
|
||||
/ {{ entry.amount|accounting_format_amount }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p id="accounting-original-entry-selector-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,56 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
original-line-item-selector-modal.html: The modal of the original line item selector
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
<div id="accounting-original-line-item-selector-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-original-line-item-selector-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="accounting-original-line-item-selector-modal-label">{{ A_("Select Original Line Item") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal" aria-label="{{ A_("Close") }}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="input-group mb-2">
|
||||
<input id="accounting-original-line-item-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
||||
<label class="input-group-text" for="accounting-original-line-item-selector-query">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
{{ A_("Search") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ul id="accounting-original-line-item-selector-option-list" class="list-group accounting-selector-list">
|
||||
{% for line_item in form.original_line_item_options %}
|
||||
<li id="accounting-original-line-item-selector-option-{{ line_item.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-line-item-selector-option" data-id="{{ line_item.id }}" data-date="{{ line_item.voucher.date }}" data-side="{{ "debit" if line_item.is_debit else "credit" }}" data-currency-code="{{ line_item.currency.code }}" data-account-code="{{ line_item.account_code }}" data-account-text="{{ line_item.account }}" data-summary="{{ line_item.summary|accounting_default }}" data-net-balance="{{ line_item.net_balance|accounting_voucher_format_amount_input }}" data-text="{{ line_item }}" data-query-values="{{ line_item.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||
<div>{{ line_item.voucher.date|accounting_format_date }} {{ line_item.summary|accounting_default }}</div>
|
||||
<div>
|
||||
<span class="badge bg-primary rounded-pill">
|
||||
<span id="accounting-original-line-item-selector-option-{{ line_item.id }}-net-balance">{{ line_item.net_balance|accounting_format_amount }}</span>
|
||||
/ {{ line_item.amount|accounting_format_amount }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p id="accounting-original-line-item-selector-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,20 +19,20 @@ summary-editor-modal.html: The modal of the summary editor
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/28
|
||||
#}
|
||||
<form id="accounting-summary-editor-{{ summary_editor.type }}" class="accounting-summary-editor" data-entry-type="{{ summary_editor.type }}">
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-modal-label" aria-hidden="true">
|
||||
<form id="accounting-summary-editor-{{ summary_editor.side }}" class="accounting-summary-editor" data-side="{{ summary_editor.side }}">
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-summary-editor-{{ summary_editor.side }}-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="accounting-summary-editor-{{ summary_editor.type }}-modal-label">
|
||||
<label for="accounting-summary-editor-{{ summary_editor.type }}-summary">{{ A_("Summary") }}</label>
|
||||
<h1 class="modal-title fs-5" id="accounting-summary-editor-{{ summary_editor.side }}-modal-label">
|
||||
<label for="accounting-summary-editor-{{ summary_editor.side }}-summary">{{ A_("Summary") }}</label>
|
||||
</h1>
|
||||
<button class="btn-close" type="button" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal" aria-label="{{ A_("Close") }}"></button>
|
||||
<button class="btn-close" type="button" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal" aria-label="{{ A_("Close") }}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<input id="accounting-summary-editor-{{ summary_editor.type }}-summary" class="form-control" type="text" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-modal-label">
|
||||
<button id="accounting-summary-editor-{{ summary_editor.type }}-offset" class="btn btn-primary text-nowrap ms-2" type="button" data-bs-toggle="modal" data-bs-target="#accounting-original-entry-selector-modal">
|
||||
<input id="accounting-summary-editor-{{ summary_editor.side }}-summary" class="form-control" type="text" aria-labelledby="accounting-summary-editor-{{ summary_editor.side }}-modal-label">
|
||||
<button id="accounting-summary-editor-{{ summary_editor.side }}-offset" class="btn btn-primary text-nowrap ms-2" type="button" data-bs-toggle="modal" data-bs-target="#accounting-original-line-item-selector-modal">
|
||||
{{ A_("Offset...") }}
|
||||
</button>
|
||||
</div>
|
||||
@ -40,43 +40,43 @@ First written: 2023/2/28
|
||||
{# Tab navigation #}
|
||||
<ul class="nav nav-tabs mb-2">
|
||||
<li class="nav-item">
|
||||
<span id="accounting-summary-editor-{{ summary_editor.type }}-general-tab" class="nav-link active accounting-clickable" aria-current="page">
|
||||
<span id="accounting-summary-editor-{{ summary_editor.side }}-general-tab" class="nav-link active accounting-clickable" aria-current="page">
|
||||
{{ A_("General") }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span id="accounting-summary-editor-{{ summary_editor.type }}-travel-tab" class="nav-link accounting-clickable" aria-current="false">
|
||||
<span id="accounting-summary-editor-{{ summary_editor.side }}-travel-tab" class="nav-link accounting-clickable" aria-current="false">
|
||||
{{ A_("Travel") }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span id="accounting-summary-editor-{{ summary_editor.type }}-bus-tab" class="nav-link accounting-clickable" aria-current="false">
|
||||
<span id="accounting-summary-editor-{{ summary_editor.side }}-bus-tab" class="nav-link accounting-clickable" aria-current="false">
|
||||
{{ A_("Bus") }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span id="accounting-summary-editor-{{ summary_editor.type }}-regular-tab" class="nav-link accounting-clickable" aria-current="false">
|
||||
<span id="accounting-summary-editor-{{ summary_editor.side }}-regular-tab" class="nav-link accounting-clickable" aria-current="false">
|
||||
{{ A_("Regular") }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span id="accounting-summary-editor-{{ summary_editor.type }}-annotation-tab" class="nav-link accounting-clickable" aria-current="false">
|
||||
<span id="accounting-summary-editor-{{ summary_editor.side }}-annotation-tab" class="nav-link accounting-clickable" aria-current="false">
|
||||
{{ A_("Annotation") }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{# A general summary with a tag #}
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-general-page" aria-current="page" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-general-tab">
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-general-page" aria-current="page" aria-labelledby="accounting-summary-editor-{{ summary_editor.side }}-general-tab">
|
||||
<div class="form-floating mb-2">
|
||||
<input id="accounting-summary-editor-{{ summary_editor.type }}-general-tag" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-general-tag">{{ A_("Tag") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-general-tag-error" class="invalid-feedback"></div>
|
||||
<input id="accounting-summary-editor-{{ summary_editor.side }}-general-tag" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.side }}-general-tag">{{ A_("Tag") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-general-tag-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% for tag in summary_editor.general.tags %}
|
||||
<button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.type }}-general-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
|
||||
<button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.side }}-general-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
|
||||
{{ tag }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
@ -84,16 +84,16 @@ First written: 2023/2/28
|
||||
</div>
|
||||
|
||||
{# A general trip with the origin and distination #}
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-travel-page" class="d-none" aria-current="false" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-travel-tab">
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-travel-page" class="d-none" aria-current="false" aria-labelledby="accounting-summary-editor-{{ summary_editor.side }}-travel-tab">
|
||||
<div class="form-floating mb-2">
|
||||
<input id="accounting-summary-editor-{{ summary_editor.type }}-travel-tag" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-travel-tag">{{ A_("Tag") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-travel-tag-error" class="invalid-feedback"></div>
|
||||
<input id="accounting-summary-editor-{{ summary_editor.side }}-travel-tag" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.side }}-travel-tag">{{ A_("Tag") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-travel-tag-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% for tag in summary_editor.travel.tags %}
|
||||
<button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.type }}-travel-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
|
||||
<button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.side }}-travel-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
|
||||
{{ tag }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
@ -101,40 +101,40 @@ First written: 2023/2/28
|
||||
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<div class="form-floating">
|
||||
<input id="accounting-summary-editor-{{ summary_editor.type }}-travel-from" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-travel-from">{{ A_("From") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-travel-from-error" class="invalid-feedback"></div>
|
||||
<input id="accounting-summary-editor-{{ summary_editor.side }}-travel-from" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.side }}-travel-from">{{ A_("From") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-travel-from-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="btn-group-vertical ms-1 me-1">
|
||||
<button class="btn btn-primary accounting-summary-editor-{{ summary_editor.type }}-travel-direction accounting-default" type="button" tabindex="-1" data-arrow="→">→</button>
|
||||
<button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.type }}-travel-direction" type="button" tabindex="-1" data-arrow="↔">↔</button>
|
||||
<button class="btn btn-primary accounting-summary-editor-{{ summary_editor.side }}-travel-direction accounting-default" type="button" tabindex="-1" data-arrow="→">→</button>
|
||||
<button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.side }}-travel-direction" type="button" tabindex="-1" data-arrow="↔">↔</button>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<input id="accounting-summary-editor-{{ summary_editor.type }}-travel-to" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-travel-to">{{ A_("To") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-travel-to-error" class="invalid-feedback"></div>
|
||||
<input id="accounting-summary-editor-{{ summary_editor.side }}-travel-to" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.side }}-travel-to">{{ A_("To") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-travel-to-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# A bus trip with the route name or route number, the origin and distination #}
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-bus-page" class="d-none" aria-current="false" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-bus-tab">
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-bus-page" class="d-none" aria-current="false" aria-labelledby="accounting-summary-editor-{{ summary_editor.side }}-bus-tab">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<div class="form-floating me-2">
|
||||
<input id="accounting-summary-editor-{{ summary_editor.type }}-bus-tag" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-bus-tag">{{ A_("Tag") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-bus-tag-error" class="invalid-feedback"></div>
|
||||
<input id="accounting-summary-editor-{{ summary_editor.side }}-bus-tag" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.side }}-bus-tag">{{ A_("Tag") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-bus-tag-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<input id="accounting-summary-editor-{{ summary_editor.type }}-bus-route" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-bus-route">{{ A_("Route") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-bus-route-error" class="invalid-feedback"></div>
|
||||
<input id="accounting-summary-editor-{{ summary_editor.side }}-bus-route" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.side }}-bus-route">{{ A_("Route") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-bus-route-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% for tag in summary_editor.bus.tags %}
|
||||
<button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.type }}-bus-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
|
||||
<button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.side }}-bus-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
|
||||
{{ tag }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
@ -142,50 +142,50 @@ First written: 2023/2/28
|
||||
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<div class="form-floating me-2">
|
||||
<input id="accounting-summary-editor-{{ summary_editor.type }}-bus-from" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-bus-from">{{ A_("From") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-bus-from-error" class="invalid-feedback"></div>
|
||||
<input id="accounting-summary-editor-{{ summary_editor.side }}-bus-from" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.side }}-bus-from">{{ A_("From") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-bus-from-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<input id="accounting-summary-editor-{{ summary_editor.type }}-bus-to" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-bus-to">{{ A_("To") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-bus-to-error" class="invalid-feedback"></div>
|
||||
<input id="accounting-summary-editor-{{ summary_editor.side }}-bus-to" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.side }}-bus-to">{{ A_("To") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-bus-to-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# A regular income or payment #}
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-regular-page" class="d-none" aria-current="false" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-regular-tab">
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-regular-page" class="d-none" aria-current="false" aria-labelledby="accounting-summary-editor-{{ summary_editor.side }}-regular-tab">
|
||||
{# TODO: To be done #}
|
||||
</div>
|
||||
|
||||
{# The annotation #}
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-annotation-page" class="d-none" aria-current="false" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-annotation-tab">
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-annotation-page" class="d-none" aria-current="false" aria-labelledby="accounting-summary-editor-{{ summary_editor.side }}-annotation-tab">
|
||||
<div class="form-floating">
|
||||
<input id="accounting-summary-editor-{{ summary_editor.type }}-annotation-number" class="form-control" type="number" min="1" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-annotation-number">{{ A_("The number of items") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-annotation-number-error" class="invalid-feedback"></div>
|
||||
<input id="accounting-summary-editor-{{ summary_editor.side }}-annotation-number" class="form-control" type="number" min="1" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.side }}-annotation-number">{{ A_("The number of items") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-annotation-number-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mt-2">
|
||||
<input id="accounting-summary-editor-{{ summary_editor.type }}-annotation-note" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-annotation-note">{{ A_("Note") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.type }}-annotation-note-error" class="invalid-feedback"></div>
|
||||
<input id="accounting-summary-editor-{{ summary_editor.side }}-annotation-note" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.side }}-annotation-note">{{ A_("Note") }}</label>
|
||||
<div id="accounting-summary-editor-{{ summary_editor.side }}-annotation-note-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# The suggested accounts #}
|
||||
<div class="mt-3">
|
||||
{% for account in summary_editor.accounts %}
|
||||
<button class="btn btn-outline-primary d-none accounting-summary-editor-{{ summary_editor.type }}-account {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" type="button" data-code="{{ account.code }}" data-text="{{ account }}">
|
||||
<button class="btn btn-outline-primary d-none accounting-summary-editor-{{ summary_editor.side }}-account {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" type="button" data-code="{{ account.code }}" data-text="{{ account }}">
|
||||
{{ account }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal">{{ A_("Cancel") }}</button>
|
||||
<button id="accounting-summary-editor-{{ summary_editor.type }}-btn-save" type="submit" class="btn btn-primary">{{ A_("Save") }}</button>
|
||||
<button class="btn btn-secondary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">{{ A_("Cancel") }}</button>
|
||||
<button id="accounting-summary-editor-{{ summary_editor.side }}-btn-save" type="submit" class="btn btn-primary">{{ A_("Save") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,76 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
voucher-line-item-editor-modal.html: The modal of the voucher line item editor
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
<form id="accounting-line-item-editor">
|
||||
<div id="accounting-line-item-editor-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-line-item-editor-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="accounting-line-item-editor-modal-label">{{ A_("Line Item Content") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ A_("Close") }}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="accounting-line-item-editor-original-line-item-container" class="d-flex justify-content-between mb-3">
|
||||
<div class="accounting-line-item-editor-original-line-item-content">
|
||||
<div id="accounting-line-item-editor-original-line-item-control" class="form-control accounting-clickable accounting-material-text-field" data-bs-toggle="modal" data-bs-target="#accounting-original-line-item-selector-modal">
|
||||
<label class="form-label" for="accounting-line-item-editor-original-line-item">{{ A_("Original Line Item") }}</label>
|
||||
<div id="accounting-line-item-editor-original-line-item"></div>
|
||||
</div>
|
||||
<div id="accounting-line-item-editor-original-line-item-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="accounting-line-item-editor-original-line-item-delete" class="btn btn-danger rounded-circle" type="button">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div id="accounting-line-item-editor-summary-control" class="form-control accounting-clickable accounting-material-text-field" data-bs-toggle="modal" data-bs-target="">
|
||||
<label class="form-label" for="accounting-line-item-editor-summary">{{ A_("Summary") }}</label>
|
||||
<div id="accounting-line-item-editor-summary"></div>
|
||||
</div>
|
||||
<div id="accounting-line-item-editor-summary-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div id="accounting-line-item-editor-account-control" class="form-control accounting-clickable accounting-material-text-field" data-bs-toggle="modal" data-bs-target="">
|
||||
<label class="form-label" for="accounting-line-item-editor-account">{{ A_("Account") }}</label>
|
||||
<div id="accounting-line-item-editor-account"></div>
|
||||
</div>
|
||||
<div id="accounting-line-item-editor-account-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input id="accounting-line-item-editor-amount" class="form-control" type="number" value="" min="0" max="" step="0.01" placeholder=" " required="required">
|
||||
<label for="accounting-line-item-editor-amount">{{ A_("Amount") }}</label>
|
||||
<div id="accounting-line-item-editor-amount-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
|
||||
<button type="submit" class="btn btn-primary">{{ A_("Save") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -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-entry accounting-voucher-entry-header">{{ A_("Content") }}</li>
|
||||
{% with entries = currency.credit %}
|
||||
{% include "accounting/voucher/include/detail-entries.html" %}
|
||||
<li class="list-group-item accounting-voucher-line-item accounting-voucher-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-entry accounting-voucher-entry-total">
|
||||
<li class="list-group-item accounting-voucher-line-item accounting-voucher-line-item-total">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>{{ A_("Total") }}</div>
|
||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
||||
|
@ -44,31 +44,31 @@ First written: 2023/2/25
|
||||
<div class="mb-3">
|
||||
<div id="accounting-currency-{{ currency_index }}-credit" class="form-control accounting-material-text-field accounting-not-empty {% if credit_errors %} is-invalid {% endif %}">
|
||||
<label class="form-label" for="accounting-currency-{{ currency_index }}-credit">{{ A_("Content") }}</label>
|
||||
<ul id="accounting-currency-{{ currency_index }}-credit-list" class="list-group accounting-entry-list">
|
||||
{% for entry_form in credit_forms %}
|
||||
<ul id="accounting-currency-{{ currency_index }}-credit-list" class="list-group accounting-line-item-list">
|
||||
{% for line_item_form in credit_forms %}
|
||||
{% with currency_index = currency_index,
|
||||
entry_type = "credit",
|
||||
entry_index = loop.index,
|
||||
only_one_entry_form = debit_forms|length == 1,
|
||||
entry_id = entry_form.eid.data,
|
||||
account_code_data = entry_form.account_code.data|accounting_default,
|
||||
account_code_error = entry_form.account_code.errors,
|
||||
account_text = entry_form.account_text,
|
||||
summary_data = entry_form.summary.data|accounting_default,
|
||||
summary_errors = entry_form.summary.errors,
|
||||
original_entry_id_data = entry_form.original_entry_id.data|accounting_default,
|
||||
original_entry_date = entry_form.original_entry_date|accounting_default,
|
||||
original_entry_text = entry_form.original_entry_text|accounting_default,
|
||||
is_need_offset = entry_form.is_need_offset,
|
||||
offset_entries = entry_form.offsets,
|
||||
offset_total = entry_form.offset_total|accounting_default("0"),
|
||||
net_balance_data = entry_form.net_balance,
|
||||
net_balance_text = entry_form.net_balance|accounting_format_amount,
|
||||
amount_data = entry_form.amount.data|accounting_voucher_format_amount_input,
|
||||
amount_errors = entry_form.amount.errors,
|
||||
amount_text = entry_form.amount.data|accounting_format_amount,
|
||||
entry_errors = entry_form.all_errors %}
|
||||
{% include "accounting/voucher/include/form-entry-item.html" %}
|
||||
side = "credit",
|
||||
line_item_index = loop.index,
|
||||
only_one_line_item_form = debit_forms|length == 1,
|
||||
line_item_id = line_item_form.eid.data,
|
||||
account_code_data = line_item_form.account_code.data|accounting_default,
|
||||
account_code_error = line_item_form.account_code.errors,
|
||||
account_text = line_item_form.account_text,
|
||||
summary_data = line_item_form.summary.data|accounting_default,
|
||||
summary_errors = line_item_form.summary.errors,
|
||||
original_line_item_id_data = line_item_form.original_line_item_id.data|accounting_default,
|
||||
original_line_item_date = line_item_form.original_line_item_date|accounting_default,
|
||||
original_line_item_text = line_item_form.original_line_item_text|accounting_default,
|
||||
is_need_offset = line_item_form.is_need_offset,
|
||||
offset_items = line_item_form.offsets,
|
||||
offset_total = line_item_form.offset_total|accounting_default("0"),
|
||||
net_balance_data = line_item_form.net_balance,
|
||||
net_balance_text = line_item_form.net_balance|accounting_format_amount,
|
||||
amount_data = line_item_form.amount.data|accounting_voucher_format_amount_input,
|
||||
amount_errors = line_item_form.amount.errors,
|
||||
amount_text = line_item_form.amount.data|accounting_format_amount,
|
||||
line_item_errors = line_item_form.all_errors %}
|
||||
{% include "accounting/voucher/include/form-line-item.html" %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@ -79,7 +79,7 @@ First written: 2023/2/25
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="accounting-currency-{{ currency_index }}-credit-add-entry" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-entry-type="credit" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal">
|
||||
<button id="accounting-currency-{{ currency_index }}-credit-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-side="credit" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||
<i class="fas fa-plus"></i>
|
||||
{{ A_("New") }}
|
||||
</button>
|
||||
|
@ -50,7 +50,7 @@ First written: 2023/2/25
|
||||
{% with summary_editor = form.summary_editor.credit %}
|
||||
{% include "accounting/voucher/include/summary-editor-modal.html" %}
|
||||
{% endwith %}
|
||||
{% with entry_type = "credit",
|
||||
{% with side = "credit",
|
||||
account_options = form.credit_account_options %}
|
||||
{% include "accounting/voucher/include/account-selector-modal.html" %}
|
||||
{% endwith %}
|
||||
|
@ -27,14 +27,14 @@ First written: 2023/2/26
|
||||
<div class="mb-2 fw-bolder">{{ currency.name }}</div>
|
||||
|
||||
<div class="row">
|
||||
{# The debit entries #}
|
||||
{# 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-entry accounting-voucher-entry-header">{{ A_("Debit") }}</li>
|
||||
{% with entries = currency.debit %}
|
||||
{% include "accounting/voucher/include/detail-entries.html" %}
|
||||
<li class="list-group-item accounting-voucher-line-item accounting-voucher-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-entry accounting-voucher-entry-total">
|
||||
<li class="list-group-item accounting-voucher-line-item accounting-voucher-line-item-total">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>{{ A_("Total") }}</div>
|
||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
||||
@ -43,14 +43,14 @@ First written: 2023/2/26
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{# The credit entries #}
|
||||
{# 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-entry accounting-voucher-entry-header">{{ A_("Credit") }}</li>
|
||||
{% with entries = currency.credit %}
|
||||
{% include "accounting/voucher/include/detail-entries.html" %}
|
||||
<li class="list-group-item accounting-voucher-line-item accounting-voucher-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-entry accounting-voucher-entry-total">
|
||||
<li class="list-group-item accounting-voucher-line-item accounting-voucher-line-item-total">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>{{ A_("Total") }}</div>
|
||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
||||
|
@ -42,35 +42,35 @@ First written: 2023/2/25
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{# The debit entries #}
|
||||
{# The debit line items #}
|
||||
<div class="col-sm-6 mb-3">
|
||||
<div id="accounting-currency-{{ currency_index }}-debit" class="form-control accounting-material-text-field accounting-not-empty {% if debit_errors %} is-invalid {% endif %}">
|
||||
<label class="form-label" for="accounting-currency-{{ currency_index }}-debit">{{ A_("Debit") }}</label>
|
||||
<ul id="accounting-currency-{{ currency_index }}-debit-list" class="list-group accounting-entry-list accounting-currency-{{ currency_index }}-entry-list">
|
||||
{% for entry_form in debit_forms %}
|
||||
<ul id="accounting-currency-{{ currency_index }}-debit-list" class="list-group accounting-line-item-list">
|
||||
{% for line_item_form in debit_forms %}
|
||||
{% with currency_index = currency_index,
|
||||
entry_type = "debit",
|
||||
entry_index = loop.index,
|
||||
only_one_entry_form = debit_forms|length == 1,
|
||||
entry_id = entry_form.eid.data,
|
||||
account_code_data = entry_form.account_code.data|accounting_default,
|
||||
account_code_error = entry_form.account_code.errors,
|
||||
account_text = entry_form.account_text,
|
||||
summary_data = entry_form.summary.data|accounting_default,
|
||||
summary_errors = entry_form.summary.errors,
|
||||
original_entry_id_data = entry_form.original_entry_id.data|accounting_default,
|
||||
original_entry_date = entry_form.original_entry_date|accounting_default,
|
||||
original_entry_text = entry_form.original_entry_text|accounting_default,
|
||||
is_need_offset = entry_form.is_need_offset,
|
||||
offset_entries = entry_form.offsets,
|
||||
offset_total = entry_form.offset_total|accounting_default,
|
||||
net_balance_data = entry_form.net_balance,
|
||||
net_balance_text = entry_form.net_balance|accounting_format_amount,
|
||||
amount_data = entry_form.amount.data|accounting_voucher_format_amount_input,
|
||||
amount_errors = entry_form.amount.errors,
|
||||
amount_text = entry_form.amount.data|accounting_format_amount,
|
||||
entry_errors = entry_form.all_errors %}
|
||||
{% include "accounting/voucher/include/form-entry-item.html" %}
|
||||
side = "debit",
|
||||
line_item_index = loop.index,
|
||||
only_one_line_item_form = debit_forms|length == 1,
|
||||
line_item_id = line_item_form.eid.data,
|
||||
account_code_data = line_item_form.account_code.data|accounting_default,
|
||||
account_code_error = line_item_form.account_code.errors,
|
||||
account_text = line_item_form.account_text,
|
||||
summary_data = line_item_form.summary.data|accounting_default,
|
||||
summary_errors = line_item_form.summary.errors,
|
||||
original_line_item_id_data = line_item_form.original_line_item_id.data|accounting_default,
|
||||
original_line_item_date = line_item_form.original_line_item_date|accounting_default,
|
||||
original_line_item_text = line_item_form.original_line_item_text|accounting_default,
|
||||
is_need_offset = line_item_form.is_need_offset,
|
||||
offset_items = line_item_form.offsets,
|
||||
offset_total = line_item_form.offset_total|accounting_default,
|
||||
net_balance_data = line_item_form.net_balance,
|
||||
net_balance_text = line_item_form.net_balance|accounting_format_amount,
|
||||
amount_data = line_item_form.amount.data|accounting_voucher_format_amount_input,
|
||||
amount_errors = line_item_form.amount.errors,
|
||||
amount_text = line_item_form.amount.data|accounting_format_amount,
|
||||
line_item_errors = line_item_form.all_errors %}
|
||||
{% include "accounting/voucher/include/form-line-item.html" %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@ -81,7 +81,7 @@ First written: 2023/2/25
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="accounting-currency-{{ currency_index }}-debit-add-entry" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-entry-type="debit" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal">
|
||||
<button id="accounting-currency-{{ currency_index }}-debit-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-side="debit" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||
<i class="fas fa-plus"></i>
|
||||
{{ A_("New") }}
|
||||
</button>
|
||||
@ -90,35 +90,35 @@ First written: 2023/2/25
|
||||
<div id="accounting-currency-{{ currency_index }}-debit-error" class="invalid-feedback">{% if debit_errors %}{{ debit_errors[0] }}{% endif %}</div>
|
||||
</div>
|
||||
|
||||
{# The credit entries #}
|
||||
{# The credit line items #}
|
||||
<div class="col-sm-6 mb-3">
|
||||
<div id="accounting-currency-{{ currency_index }}-credit" class="form-control accounting-material-text-field accounting-not-empty {% if credit_errors %} is-invalid {% endif %}">
|
||||
<label class="form-label" for="accounting-currency-{{ currency_index }}-credit">{{ A_("Credit") }}</label>
|
||||
<ul id="accounting-currency-{{ currency_index }}-credit-list" class="list-group accounting-entry-list">
|
||||
{% for entry_form in credit_forms %}
|
||||
<ul id="accounting-currency-{{ currency_index }}-credit-list" class="list-group accounting-line-item-list">
|
||||
{% for line_item_form in credit_forms %}
|
||||
{% with currency_index = currency_index,
|
||||
entry_type = "credit",
|
||||
entry_index = loop.index,
|
||||
only_one_entry_form = debit_forms|length == 1,
|
||||
entry_id = entry_form.eid.data,
|
||||
account_code_data = entry_form.account_code.data|accounting_default,
|
||||
account_code_error = entry_form.account_code.errors,
|
||||
account_text = entry_form.account_text,
|
||||
summary_data = entry_form.summary.data|accounting_default,
|
||||
summary_errors = entry_form.summary.errors,
|
||||
original_entry_id_data = entry_form.original_entry_id.data|accounting_default,
|
||||
original_entry_date = entry_form.original_entry_date|accounting_default,
|
||||
original_entry_text = entry_form.original_entry_text|accounting_default,
|
||||
is_need_offset = entry_form.is_need_offset,
|
||||
offset_entries = entry_form.offsets,
|
||||
offset_total = entry_form.offset_total|accounting_default("0"),
|
||||
net_balance_data = entry_form.net_balance,
|
||||
net_balance_text = entry_form.net_balance|accounting_format_amount,
|
||||
amount_data = entry_form.amount.data|accounting_voucher_format_amount_input,
|
||||
amount_errors = entry_form.amount.errors,
|
||||
amount_text = entry_form.amount.data|accounting_format_amount,
|
||||
entry_errors = entry_form.all_errors %}
|
||||
{% include "accounting/voucher/include/form-entry-item.html" %}
|
||||
side = "credit",
|
||||
line_item_index = loop.index,
|
||||
only_one_line_item_form = debit_forms|length == 1,
|
||||
line_item_id = line_item_form.eid.data,
|
||||
account_code_data = line_item_form.account_code.data|accounting_default,
|
||||
account_code_error = line_item_form.account_code.errors,
|
||||
account_text = line_item_form.account_text,
|
||||
summary_data = line_item_form.summary.data|accounting_default,
|
||||
summary_errors = line_item_form.summary.errors,
|
||||
original_line_item_id_data = line_item_form.original_line_item_id.data|accounting_default,
|
||||
original_line_item_date = line_item_form.original_line_item_date|accounting_default,
|
||||
original_line_item_text = line_item_form.original_line_item_text|accounting_default,
|
||||
is_need_offset = line_item_form.is_need_offset,
|
||||
offset_items = line_item_form.offsets,
|
||||
offset_total = line_item_form.offset_total|accounting_default("0"),
|
||||
net_balance_data = line_item_form.net_balance,
|
||||
net_balance_text = line_item_form.net_balance|accounting_format_amount,
|
||||
amount_data = line_item_form.amount.data|accounting_voucher_format_amount_input,
|
||||
amount_errors = line_item_form.amount.errors,
|
||||
amount_text = line_item_form.amount.data|accounting_format_amount,
|
||||
line_item_errors = line_item_form.all_errors %}
|
||||
{% include "accounting/voucher/include/form-line-item.html" %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@ -129,7 +129,7 @@ First written: 2023/2/25
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="accounting-currency-{{ currency_index }}-credit-add-entry" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-entry-type="credit" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal">
|
||||
<button id="accounting-currency-{{ currency_index }}-credit-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-side="credit" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||
<i class="fas fa-plus"></i>
|
||||
{{ A_("New") }}
|
||||
</button>
|
||||
|
@ -57,11 +57,11 @@ First written: 2023/2/25
|
||||
{% with summary_editor = form.summary_editor.credit %}
|
||||
{% include "accounting/voucher/include/summary-editor-modal.html" %}
|
||||
{% endwith %}
|
||||
{% with entry_type = "debit",
|
||||
{% with side = "debit",
|
||||
account_options = form.debit_account_options %}
|
||||
{% include "accounting/voucher/include/account-selector-modal.html" %}
|
||||
{% endwith %}
|
||||
{% with entry_type = "credit",
|
||||
{% with side = "credit",
|
||||
account_options = form.credit_account_options %}
|
||||
{% include "accounting/voucher/include/account-selector-modal.html" %}
|
||||
{% endwith %}
|
||||
|
@ -23,7 +23,7 @@ from flask import abort
|
||||
from sqlalchemy.orm import selectinload
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
from accounting.models import Voucher, JournalEntry
|
||||
from accounting.models import Voucher, VoucherLineItem
|
||||
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(JournalEntry)\
|
||||
voucher: Voucher | None = Voucher.query.join(VoucherLineItem)\
|
||||
.filter(Voucher.id == value)\
|
||||
.options(selectinload(Voucher.entries)
|
||||
.selectinload(JournalEntry.offsets)
|
||||
.selectinload(JournalEntry.voucher))\
|
||||
.options(selectinload(Voucher.line_items)
|
||||
.selectinload(VoucherLineItem.offsets)
|
||||
.selectinload(VoucherLineItem.voucher))\
|
||||
.first()
|
||||
if voucher is None:
|
||||
abort(404)
|
||||
|
@ -28,11 +28,11 @@ from wtforms.validators import DataRequired
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import lazy_gettext
|
||||
from accounting.models import Currency, JournalEntry
|
||||
from accounting.models import Currency, VoucherLineItem
|
||||
from accounting.voucher.utils.offset_alias import offset_alias
|
||||
from accounting.utils.cast import be
|
||||
from accounting.utils.strip_text import strip_text
|
||||
from .journal_entry import JournalEntryForm, CreditEntryForm, DebitEntryForm
|
||||
from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
|
||||
|
||||
CURRENCY_REQUIRED: DataRequired = DataRequired(
|
||||
lazy_gettext("Please select the currency."))
|
||||
@ -50,26 +50,28 @@ class CurrencyExists:
|
||||
"The currency does not exist."))
|
||||
|
||||
|
||||
class SameCurrencyAsOriginalEntries:
|
||||
"""The validator to check if the currency is the same as the original
|
||||
entries."""
|
||||
class SameCurrencyAsOriginalLineItems:
|
||||
"""The validator to check if the currency is the same as the
|
||||
original line items."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||
assert isinstance(form, CurrencyForm)
|
||||
if field.data is None:
|
||||
return
|
||||
original_entry_id: set[int] = {x.original_entry_id.data
|
||||
for x in form.entries
|
||||
if x.original_entry_id.data is not None}
|
||||
if len(original_entry_id) == 0:
|
||||
original_line_item_id: set[int] \
|
||||
= {x.original_line_item_id.data
|
||||
for x in form.line_items
|
||||
if x.original_line_item_id.data is not None}
|
||||
if len(original_line_item_id) == 0:
|
||||
return
|
||||
original_entry_currency_codes: set[str] = set(db.session.scalars(
|
||||
sa.select(JournalEntry.currency_code)
|
||||
.filter(JournalEntry.id.in_(original_entry_id))).all())
|
||||
for currency_code in original_entry_currency_codes:
|
||||
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())
|
||||
for currency_code in original_line_item_currency_codes:
|
||||
if field.data != currency_code:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"The currency must be the same as the original entry."))
|
||||
"The currency must be the same as the"
|
||||
" original line item."))
|
||||
|
||||
|
||||
class KeepCurrencyWhenHavingOffset:
|
||||
@ -81,31 +83,32 @@ class KeepCurrencyWhenHavingOffset:
|
||||
if field.data is None:
|
||||
return
|
||||
offset: sa.Alias = offset_alias()
|
||||
original_entries: list[JournalEntry] = JournalEntry.query\
|
||||
.join(offset, be(JournalEntry.id == offset.c.original_entry_id),
|
||||
original_line_items: list[VoucherLineItem] = VoucherLineItem.query\
|
||||
.join(offset, be(VoucherLineItem.id
|
||||
== offset.c.original_line_item_id),
|
||||
isouter=True)\
|
||||
.filter(JournalEntry.id.in_({x.eid.data for x in form.entries
|
||||
if x.eid.data is not None}))\
|
||||
.group_by(JournalEntry.id, JournalEntry.currency_code)\
|
||||
.filter(VoucherLineItem.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)\
|
||||
.having(sa.func.count(offset.c.id) > 0).all()
|
||||
for original_entry in original_entries:
|
||||
if original_entry.currency_code != field.data:
|
||||
for original_line_item in original_line_items:
|
||||
if original_line_item.currency_code != field.data:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"The currency must not be changed when there is offset."))
|
||||
|
||||
|
||||
class NeedSomeJournalEntries:
|
||||
"""The validator to check if there is any journal entry sub-form."""
|
||||
class NeedSomeLineItems:
|
||||
"""The validator to check if there is any line item sub-form."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: FieldList) -> None:
|
||||
if len(field) == 0:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"Please add some journal entries."))
|
||||
"Please add some line items."))
|
||||
|
||||
|
||||
class IsBalanced:
|
||||
"""The validator to check that the total amount of the debit and credit
|
||||
entries are equal."""
|
||||
line items are equal."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: BooleanField) -> None:
|
||||
assert isinstance(form, TransferCurrencyForm)
|
||||
@ -126,20 +129,20 @@ class CurrencyForm(FlaskForm):
|
||||
"""The pseudo field for the whole form validators."""
|
||||
|
||||
@property
|
||||
def entries(self) -> list[JournalEntryForm]:
|
||||
"""Returns the journal entry sub-forms.
|
||||
def line_items(self) -> list[LineItemForm]:
|
||||
"""Returns the line item sub-forms.
|
||||
|
||||
:return: The journal entry sub-forms.
|
||||
:return: The line item sub-forms.
|
||||
"""
|
||||
entry_forms: list[JournalEntryForm] = []
|
||||
line_item_forms: list[LineItemForm] = []
|
||||
if isinstance(self, CashReceiptCurrencyForm):
|
||||
entry_forms.extend([x.form for x in self.credit])
|
||||
line_item_forms.extend([x.form for x in self.credit])
|
||||
elif isinstance(self, CashDisbursementCurrencyForm):
|
||||
entry_forms.extend([x.form for x in self.debit])
|
||||
line_item_forms.extend([x.form for x in self.debit])
|
||||
elif isinstance(self, TransferCurrencyForm):
|
||||
entry_forms.extend([x.form for x in self.debit])
|
||||
entry_forms.extend([x.form for x in self.credit])
|
||||
return entry_forms
|
||||
line_item_forms.extend([x.form for x in self.debit])
|
||||
line_item_forms.extend([x.form for x in self.credit])
|
||||
return line_item_forms
|
||||
|
||||
@property
|
||||
def is_code_locked(self) -> bool:
|
||||
@ -148,16 +151,16 @@ class CurrencyForm(FlaskForm):
|
||||
:return: True if the currency code should not be changed, or False
|
||||
otherwise
|
||||
"""
|
||||
entry_forms: list[JournalEntryForm] = self.entries
|
||||
original_entry_id: set[int] \
|
||||
= {x.original_entry_id.data for x in entry_forms
|
||||
if x.original_entry_id.data is not None}
|
||||
if len(original_entry_id) > 0:
|
||||
line_item_forms: list[LineItemForm] = self.line_items
|
||||
original_line_item_id: set[int] \
|
||||
= {x.original_line_item_id.data for x in line_item_forms
|
||||
if x.original_line_item_id.data is not None}
|
||||
if len(original_line_item_id) > 0:
|
||||
return True
|
||||
entry_id: set[int] = {x.eid.data for x in entry_forms
|
||||
if x.eid.data is not None}
|
||||
select: sa.Select = sa.select(sa.func.count(JournalEntry.id))\
|
||||
.filter(JournalEntry.original_entry_id.in_(entry_id))
|
||||
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))
|
||||
return db.session.scalar(select) > 0
|
||||
|
||||
|
||||
@ -169,27 +172,27 @@ class CashReceiptCurrencyForm(CurrencyForm):
|
||||
filters=[strip_text],
|
||||
validators=[CURRENCY_REQUIRED,
|
||||
CurrencyExists(),
|
||||
SameCurrencyAsOriginalEntries(),
|
||||
SameCurrencyAsOriginalLineItems(),
|
||||
KeepCurrencyWhenHavingOffset()])
|
||||
"""The currency code."""
|
||||
credit = FieldList(FormField(CreditEntryForm),
|
||||
validators=[NeedSomeJournalEntries()])
|
||||
"""The credit entries."""
|
||||
credit = FieldList(FormField(CreditLineItemForm),
|
||||
validators=[NeedSomeLineItems()])
|
||||
"""The credit line items."""
|
||||
whole_form = BooleanField()
|
||||
"""The pseudo field for the whole form validators."""
|
||||
|
||||
@property
|
||||
def credit_total(self) -> Decimal:
|
||||
"""Returns the total amount of the credit journal entries.
|
||||
"""Returns the total amount of the credit line items.
|
||||
|
||||
:return: The total amount of the credit journal entries.
|
||||
:return: The total amount of the credit line items.
|
||||
"""
|
||||
return sum([x.amount.data for x in self.credit
|
||||
if x.amount.data is not None])
|
||||
|
||||
@property
|
||||
def credit_errors(self) -> list[str | LazyString]:
|
||||
"""Returns the credit journal entry errors, without the errors in their
|
||||
"""Returns the credit line item errors, without the errors in their
|
||||
sub-forms.
|
||||
|
||||
:return:
|
||||
@ -206,27 +209,27 @@ class CashDisbursementCurrencyForm(CurrencyForm):
|
||||
filters=[strip_text],
|
||||
validators=[CURRENCY_REQUIRED,
|
||||
CurrencyExists(),
|
||||
SameCurrencyAsOriginalEntries(),
|
||||
SameCurrencyAsOriginalLineItems(),
|
||||
KeepCurrencyWhenHavingOffset()])
|
||||
"""The currency code."""
|
||||
debit = FieldList(FormField(DebitEntryForm),
|
||||
validators=[NeedSomeJournalEntries()])
|
||||
"""The debit entries."""
|
||||
debit = FieldList(FormField(DebitLineItemForm),
|
||||
validators=[NeedSomeLineItems()])
|
||||
"""The debit line items."""
|
||||
whole_form = BooleanField()
|
||||
"""The pseudo field for the whole form validators."""
|
||||
|
||||
@property
|
||||
def debit_total(self) -> Decimal:
|
||||
"""Returns the total amount of the debit journal entries.
|
||||
"""Returns the total amount of the debit line items.
|
||||
|
||||
:return: The total amount of the debit journal entries.
|
||||
:return: The total amount of the debit line items.
|
||||
"""
|
||||
return sum([x.amount.data for x in self.debit
|
||||
if x.amount.data is not None])
|
||||
|
||||
@property
|
||||
def debit_errors(self) -> list[str | LazyString]:
|
||||
"""Returns the debit journal entry errors, without the errors in their
|
||||
"""Returns the debit line item errors, without the errors in their
|
||||
sub-forms.
|
||||
|
||||
:return:
|
||||
@ -243,39 +246,39 @@ class TransferCurrencyForm(CurrencyForm):
|
||||
filters=[strip_text],
|
||||
validators=[CURRENCY_REQUIRED,
|
||||
CurrencyExists(),
|
||||
SameCurrencyAsOriginalEntries(),
|
||||
SameCurrencyAsOriginalLineItems(),
|
||||
KeepCurrencyWhenHavingOffset()])
|
||||
"""The currency code."""
|
||||
debit = FieldList(FormField(DebitEntryForm),
|
||||
validators=[NeedSomeJournalEntries()])
|
||||
"""The debit entries."""
|
||||
credit = FieldList(FormField(CreditEntryForm),
|
||||
validators=[NeedSomeJournalEntries()])
|
||||
"""The credit entries."""
|
||||
debit = FieldList(FormField(DebitLineItemForm),
|
||||
validators=[NeedSomeLineItems()])
|
||||
"""The debit line items."""
|
||||
credit = FieldList(FormField(CreditLineItemForm),
|
||||
validators=[NeedSomeLineItems()])
|
||||
"""The credit line items."""
|
||||
whole_form = BooleanField(validators=[IsBalanced()])
|
||||
"""The pseudo field for the whole form validators."""
|
||||
|
||||
@property
|
||||
def debit_total(self) -> Decimal:
|
||||
"""Returns the total amount of the debit journal entries.
|
||||
"""Returns the total amount of the debit line items.
|
||||
|
||||
:return: The total amount of the debit journal entries.
|
||||
:return: The total amount of the debit line items.
|
||||
"""
|
||||
return sum([x.amount.data for x in self.debit
|
||||
if x.amount.data is not None])
|
||||
|
||||
@property
|
||||
def credit_total(self) -> Decimal:
|
||||
"""Returns the total amount of the credit journal entries.
|
||||
"""Returns the total amount of the credit line items.
|
||||
|
||||
:return: The total amount of the credit journal entries.
|
||||
:return: The total amount of the credit line items.
|
||||
"""
|
||||
return sum([x.amount.data for x in self.credit
|
||||
if x.amount.data is not None])
|
||||
|
||||
@property
|
||||
def debit_errors(self) -> list[str | LazyString]:
|
||||
"""Returns the debit journal entry errors, without the errors in their
|
||||
"""Returns the debit line item errors, without the errors in their
|
||||
sub-forms.
|
||||
|
||||
:return:
|
||||
@ -285,7 +288,7 @@ class TransferCurrencyForm(CurrencyForm):
|
||||
|
||||
@property
|
||||
def credit_errors(self) -> list[str | LazyString]:
|
||||
"""Returns the credit journal entry errors, without the errors in their
|
||||
"""Returns the credit line item errors, without the errors in their
|
||||
sub-forms.
|
||||
|
||||
:return:
|
||||
|
@ -14,7 +14,7 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""The journal entry sub-forms for the voucher management.
|
||||
"""The line item sub-forms for the voucher management.
|
||||
|
||||
"""
|
||||
import re
|
||||
@ -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, JournalEntry
|
||||
from accounting.models import Account, VoucherLineItem
|
||||
from accounting.template_filters import format_amount
|
||||
from accounting.utils.cast import be
|
||||
from accounting.utils.random_id import new_id
|
||||
@ -42,64 +42,67 @@ ACCOUNT_REQUIRED: DataRequired = DataRequired(
|
||||
"""The validator to check if the account code is empty."""
|
||||
|
||||
|
||||
class OriginalEntryExists:
|
||||
"""The validator to check if the original entry exists."""
|
||||
class OriginalLineItemExists:
|
||||
"""The validator to check if the original line item exists."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: IntegerField) -> None:
|
||||
if field.data is None:
|
||||
return
|
||||
if db.session.get(JournalEntry, field.data) is None:
|
||||
if db.session.get(VoucherLineItem, field.data) is None:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"The original entry does not exist."))
|
||||
"The original line item does not exist."))
|
||||
|
||||
|
||||
class OriginalEntryOppositeSide:
|
||||
"""The validator to check if the original entry is on the opposite side."""
|
||||
class OriginalLineItemOppositeSide:
|
||||
"""The validator to check if the original line item is on the opposite
|
||||
side."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: IntegerField) -> None:
|
||||
if field.data is None:
|
||||
return
|
||||
original_entry: JournalEntry | None \
|
||||
= db.session.get(JournalEntry, field.data)
|
||||
if original_entry is None:
|
||||
original_line_item: VoucherLineItem | None \
|
||||
= db.session.get(VoucherLineItem, field.data)
|
||||
if original_line_item is None:
|
||||
return
|
||||
if isinstance(form, CreditEntryForm) and original_entry.is_debit:
|
||||
if isinstance(form, CreditLineItemForm) \
|
||||
and original_line_item.is_debit:
|
||||
return
|
||||
if isinstance(form, DebitEntryForm) and not original_entry.is_debit:
|
||||
if isinstance(form, DebitLineItemForm) \
|
||||
and not original_line_item.is_debit:
|
||||
return
|
||||
raise ValidationError(lazy_gettext(
|
||||
"The original entry is on the same side."))
|
||||
"The original line item is on the same side."))
|
||||
|
||||
|
||||
class OriginalEntryNeedOffset:
|
||||
"""The validator to check if the original entry needs offset."""
|
||||
class OriginalLineItemNeedOffset:
|
||||
"""The validator to check if the original line item needs offset."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: IntegerField) -> None:
|
||||
if field.data is None:
|
||||
return
|
||||
original_entry: JournalEntry | None \
|
||||
= db.session.get(JournalEntry, field.data)
|
||||
if original_entry is None:
|
||||
original_line_item: VoucherLineItem | None \
|
||||
= db.session.get(VoucherLineItem, field.data)
|
||||
if original_line_item is None:
|
||||
return
|
||||
if not original_entry.account.is_need_offset:
|
||||
if not original_line_item.account.is_need_offset:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"The original entry does not need offset."))
|
||||
"The original line item does not need offset."))
|
||||
|
||||
|
||||
class OriginalEntryNotOffset:
|
||||
"""The validator to check if the original entry is not itself an offset
|
||||
entry."""
|
||||
class OriginalLineItemNotOffset:
|
||||
"""The validator to check if the original line item is not itself an
|
||||
offset item."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: IntegerField) -> None:
|
||||
if field.data is None:
|
||||
return
|
||||
original_entry: JournalEntry | None \
|
||||
= db.session.get(JournalEntry, field.data)
|
||||
if original_entry is None:
|
||||
original_line_item: VoucherLineItem | None \
|
||||
= db.session.get(VoucherLineItem, field.data)
|
||||
if original_line_item is None:
|
||||
return
|
||||
if original_entry.original_entry_id is not None:
|
||||
if original_line_item.original_line_item_id is not None:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"The original entry cannot be an offset entry."))
|
||||
"The original line item cannot be an offset item."))
|
||||
|
||||
|
||||
class AccountExists:
|
||||
@ -114,7 +117,7 @@ class AccountExists:
|
||||
|
||||
|
||||
class IsDebitAccount:
|
||||
"""The validator to check if the account is for debit journal entries."""
|
||||
"""The validator to check if the account is for debit line items."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||
if field.data is None:
|
||||
@ -124,11 +127,11 @@ class IsDebitAccount:
|
||||
and not field.data.startswith("3353-"):
|
||||
return
|
||||
raise ValidationError(lazy_gettext(
|
||||
"This account is not for debit entries."))
|
||||
"This account is not for debit line items."))
|
||||
|
||||
|
||||
class IsCreditAccount:
|
||||
"""The validator to check if the account is for credit journal entries."""
|
||||
"""The validator to check if the account is for credit line items."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||
if field.data is None:
|
||||
@ -138,73 +141,73 @@ class IsCreditAccount:
|
||||
and not field.data.startswith("3353-"):
|
||||
return
|
||||
raise ValidationError(lazy_gettext(
|
||||
"This account is not for credit entries."))
|
||||
"This account is not for credit line items."))
|
||||
|
||||
|
||||
class SameAccountAsOriginalEntry:
|
||||
"""The validator to check if the account is the same as the original
|
||||
entry."""
|
||||
class SameAccountAsOriginalLineItem:
|
||||
"""The validator to check if the account is the same as the
|
||||
original line item."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||
assert isinstance(form, JournalEntryForm)
|
||||
if field.data is None or form.original_entry_id.data is None:
|
||||
assert isinstance(form, LineItemForm)
|
||||
if field.data is None or form.original_line_item_id.data is None:
|
||||
return
|
||||
original_entry: JournalEntry | None \
|
||||
= db.session.get(JournalEntry, form.original_entry_id.data)
|
||||
if original_entry is None:
|
||||
original_line_item: VoucherLineItem | None \
|
||||
= db.session.get(VoucherLineItem, form.original_line_item_id.data)
|
||||
if original_line_item is None:
|
||||
return
|
||||
if field.data != original_entry.account_code:
|
||||
if field.data != original_line_item.account_code:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"The account must be the same as the original entry."))
|
||||
"The account must be the same as the original line item."))
|
||||
|
||||
|
||||
class KeepAccountWhenHavingOffset:
|
||||
"""The validator to check if the account is the same when having offset."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||
assert isinstance(form, JournalEntryForm)
|
||||
assert isinstance(form, LineItemForm)
|
||||
if field.data is None or form.eid.data is None:
|
||||
return
|
||||
entry: JournalEntry | None = db.session.query(JournalEntry)\
|
||||
.filter(JournalEntry.id == form.eid.data)\
|
||||
.options(selectinload(JournalEntry.offsets)).first()
|
||||
if entry is None or len(entry.offsets) == 0:
|
||||
line_item: VoucherLineItem | None = db.session.query(VoucherLineItem)\
|
||||
.filter(VoucherLineItem.id == form.eid.data)\
|
||||
.options(selectinload(VoucherLineItem.offsets)).first()
|
||||
if line_item is None or len(line_item.offsets) == 0:
|
||||
return
|
||||
if field.data != entry.account_code:
|
||||
if field.data != line_item.account_code:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"The account must not be changed when there is offset."))
|
||||
|
||||
|
||||
class NotStartPayableFromDebit:
|
||||
"""The validator to check that a payable journal entry does not start from
|
||||
"""The validator to check that a payable line item does not start from
|
||||
the debit side."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||
assert isinstance(form, DebitEntryForm)
|
||||
assert isinstance(form, DebitLineItemForm)
|
||||
if field.data is None \
|
||||
or field.data[0] != "2" \
|
||||
or form.original_entry_id.data is not None:
|
||||
or form.original_line_item_id.data is not None:
|
||||
return
|
||||
account: Account | None = Account.find_by_code(field.data)
|
||||
if account is not None and account.is_need_offset:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"A payable entry cannot start from the debit side."))
|
||||
"A payable line item cannot start from the debit side."))
|
||||
|
||||
|
||||
class NotStartReceivableFromCredit:
|
||||
"""The validator to check that a receivable journal entry does not start
|
||||
"""The validator to check that a receivable line item does not start
|
||||
from the credit side."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||
assert isinstance(form, CreditEntryForm)
|
||||
assert isinstance(form, CreditLineItemForm)
|
||||
if field.data is None \
|
||||
or field.data[0] != "1" \
|
||||
or form.original_entry_id.data is not None:
|
||||
or form.original_line_item_id.data is not None:
|
||||
return
|
||||
account: Account | None = Account.find_by_code(field.data)
|
||||
if account is not None and account.is_need_offset:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"A receivable entry cannot start from the credit side."))
|
||||
"A receivable line item cannot start from the credit side."))
|
||||
|
||||
|
||||
class PositiveAmount:
|
||||
@ -218,55 +221,57 @@ class PositiveAmount:
|
||||
"Please fill in a positive amount."))
|
||||
|
||||
|
||||
class NotExceedingOriginalEntryNetBalance:
|
||||
class NotExceedingOriginalLineItemNetBalance:
|
||||
"""The validator to check if the amount exceeds the net balance of the
|
||||
original entry."""
|
||||
original line item."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: DecimalField) -> None:
|
||||
assert isinstance(form, JournalEntryForm)
|
||||
if field.data is None or form.original_entry_id.data is None:
|
||||
assert isinstance(form, LineItemForm)
|
||||
if field.data is None or form.original_line_item_id.data is None:
|
||||
return
|
||||
original_entry: JournalEntry | None \
|
||||
= db.session.get(JournalEntry, form.original_entry_id.data)
|
||||
if original_entry is None:
|
||||
original_line_item: VoucherLineItem | None \
|
||||
= db.session.get(VoucherLineItem, form.original_line_item_id.data)
|
||||
if original_line_item is None:
|
||||
return
|
||||
is_debit: bool = isinstance(form, DebitEntryForm)
|
||||
existing_entry_id: set[int] = set()
|
||||
is_debit: bool = isinstance(form, DebitLineItemForm)
|
||||
existing_line_item_id: set[int] = set()
|
||||
if form.voucher_form.obj is not None:
|
||||
existing_entry_id = {x.id for x in form.voucher_form.obj.entries}
|
||||
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(JournalEntry.is_debit == is_debit), JournalEntry.amount),
|
||||
else_=-JournalEntry.amount))
|
||||
(be(VoucherLineItem.is_debit == is_debit), VoucherLineItem.amount),
|
||||
else_=-VoucherLineItem.amount))
|
||||
offset_total_but_form: Decimal | None = db.session.scalar(
|
||||
sa.select(offset_total_func)
|
||||
.filter(be(JournalEntry.original_entry_id == original_entry.id),
|
||||
JournalEntry.id.not_in(existing_entry_id)))
|
||||
.filter(be(VoucherLineItem.original_line_item_id
|
||||
== original_line_item.id),
|
||||
VoucherLineItem.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(
|
||||
[x.amount.data for x in form.voucher_form.entries
|
||||
if x.original_entry_id.data == original_entry.id
|
||||
[x.amount.data for x in form.voucher_form.line_items
|
||||
if x.original_line_item_id.data == original_line_item.id
|
||||
and x.amount != field and x.amount.data is not None])
|
||||
net_balance: Decimal = original_entry.amount - offset_total_but_form \
|
||||
- offset_total_on_form
|
||||
net_balance: Decimal = original_line_item.amount \
|
||||
- offset_total_but_form - offset_total_on_form
|
||||
if field.data > net_balance:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"The amount must not exceed the net balance %(balance)s of the"
|
||||
" original entry.", balance=format_amount(net_balance)))
|
||||
" original line item.", balance=format_amount(net_balance)))
|
||||
|
||||
|
||||
class NotLessThanOffsetTotal:
|
||||
"""The validator to check if the amount is less than the offset total."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: DecimalField) -> None:
|
||||
assert isinstance(form, JournalEntryForm)
|
||||
assert isinstance(form, LineItemForm)
|
||||
if field.data is None or form.eid.data is None:
|
||||
return
|
||||
is_debit: bool = isinstance(form, DebitEntryForm)
|
||||
is_debit: bool = isinstance(form, DebitLineItemForm)
|
||||
select_offset_total: sa.Select = sa.select(sa.func.sum(sa.case(
|
||||
(JournalEntry.is_debit != is_debit, JournalEntry.amount),
|
||||
else_=-JournalEntry.amount)))\
|
||||
.filter(be(JournalEntry.original_entry_id == form.eid.data))
|
||||
(VoucherLineItem.is_debit != is_debit, VoucherLineItem.amount),
|
||||
else_=-VoucherLineItem.amount)))\
|
||||
.filter(be(VoucherLineItem.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(
|
||||
@ -274,21 +279,21 @@ class NotLessThanOffsetTotal:
|
||||
total=format_amount(offset_total)))
|
||||
|
||||
|
||||
class JournalEntryForm(FlaskForm):
|
||||
"""The base form to create or edit a journal entry."""
|
||||
class LineItemForm(FlaskForm):
|
||||
"""The base form to create or edit a line item."""
|
||||
eid = IntegerField()
|
||||
"""The existing journal entry ID."""
|
||||
"""The existing line item ID."""
|
||||
no = IntegerField()
|
||||
"""The order in the currency."""
|
||||
original_entry_id = IntegerField()
|
||||
"""The Id of the original entry."""
|
||||
original_line_item_id = IntegerField()
|
||||
"""The Id of the original line item."""
|
||||
account_code = StringField()
|
||||
"""The account code."""
|
||||
amount = DecimalField()
|
||||
"""The amount."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Constructs a base journal entry form.
|
||||
"""Constructs a base line item form.
|
||||
|
||||
:param args: The arguments.
|
||||
:param kwargs: The keyword arguments.
|
||||
@ -312,51 +317,51 @@ class JournalEntryForm(FlaskForm):
|
||||
return str(account)
|
||||
|
||||
@property
|
||||
def __original_entry(self) -> JournalEntry | None:
|
||||
"""Returns the original entry.
|
||||
def __original_line_item(self) -> VoucherLineItem | None:
|
||||
"""Returns the original line item.
|
||||
|
||||
:return: The original entry.
|
||||
:return: The original line item.
|
||||
"""
|
||||
if not hasattr(self, "____original_entry"):
|
||||
def get_entry() -> JournalEntry | None:
|
||||
if self.original_entry_id.data is None:
|
||||
if not hasattr(self, "____original_line_item"):
|
||||
def get_line_item() -> VoucherLineItem | None:
|
||||
if self.original_line_item_id.data is None:
|
||||
return None
|
||||
return db.session.get(JournalEntry,
|
||||
self.original_entry_id.data)
|
||||
setattr(self, "____original_entry", get_entry())
|
||||
return getattr(self, "____original_entry")
|
||||
return db.session.get(VoucherLineItem,
|
||||
self.original_line_item_id.data)
|
||||
setattr(self, "____original_line_item", get_line_item())
|
||||
return getattr(self, "____original_line_item")
|
||||
|
||||
@property
|
||||
def original_entry_date(self) -> date | None:
|
||||
"""Returns the text representation of the original entry.
|
||||
def original_line_item_date(self) -> date | None:
|
||||
"""Returns the text representation of the original line item.
|
||||
|
||||
:return: The text representation of the original entry.
|
||||
:return: The text representation of the original line item.
|
||||
"""
|
||||
return None if self.__original_entry is None \
|
||||
else self.__original_entry.voucher.date
|
||||
return None if self.__original_line_item is None \
|
||||
else self.__original_line_item.voucher.date
|
||||
|
||||
@property
|
||||
def original_entry_text(self) -> str | None:
|
||||
"""Returns the text representation of the original entry.
|
||||
def original_line_item_text(self) -> str | None:
|
||||
"""Returns the text representation of the original line item.
|
||||
|
||||
:return: The text representation of the original entry.
|
||||
:return: The text representation of the original line item.
|
||||
"""
|
||||
return None if self.__original_entry is None \
|
||||
else str(self.__original_entry)
|
||||
return None if self.__original_line_item is None \
|
||||
else str(self.__original_line_item)
|
||||
|
||||
@property
|
||||
def is_need_offset(self) -> bool:
|
||||
"""Returns whether the entry needs offset.
|
||||
"""Returns whether the line item needs offset.
|
||||
|
||||
:return: True if the entry needs offset, or False otherwise.
|
||||
:return: True if the line item needs offset, or False otherwise.
|
||||
"""
|
||||
if self.account_code.data is None:
|
||||
return False
|
||||
if self.account_code.data[0] == "1":
|
||||
if isinstance(self, CreditEntryForm):
|
||||
if isinstance(self, CreditLineItemForm):
|
||||
return False
|
||||
elif self.account_code.data[0] == "2":
|
||||
if isinstance(self, DebitEntryForm):
|
||||
if isinstance(self, DebitLineItemForm):
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
@ -364,21 +369,22 @@ class JournalEntryForm(FlaskForm):
|
||||
return account is not None and account.is_need_offset
|
||||
|
||||
@property
|
||||
def offsets(self) -> list[JournalEntry]:
|
||||
def offsets(self) -> list[VoucherLineItem]:
|
||||
"""Returns the offsets.
|
||||
|
||||
:return: The offsets.
|
||||
"""
|
||||
if not hasattr(self, "__offsets"):
|
||||
def get_offsets() -> list[JournalEntry]:
|
||||
def get_offsets() -> list[VoucherLineItem]:
|
||||
if not self.is_need_offset or self.eid.data is None:
|
||||
return []
|
||||
return JournalEntry.query\
|
||||
.filter(JournalEntry.original_entry_id == self.eid.data)\
|
||||
.options(selectinload(JournalEntry.voucher),
|
||||
selectinload(JournalEntry.account),
|
||||
selectinload(JournalEntry.offsets)
|
||||
.selectinload(JournalEntry.voucher)).all()
|
||||
return VoucherLineItem.query\
|
||||
.filter(VoucherLineItem.original_line_item_id
|
||||
== self.eid.data)\
|
||||
.options(selectinload(VoucherLineItem.voucher),
|
||||
selectinload(VoucherLineItem.account),
|
||||
selectinload(VoucherLineItem.offsets)
|
||||
.selectinload(VoucherLineItem.voucher)).all()
|
||||
setattr(self, "__offsets", get_offsets())
|
||||
return getattr(self, "__offsets")
|
||||
|
||||
@ -392,7 +398,7 @@ class JournalEntryForm(FlaskForm):
|
||||
def get_offset_total():
|
||||
if not self.is_need_offset or self.eid.data is None:
|
||||
return None
|
||||
is_debit: bool = isinstance(self, DebitEntryForm)
|
||||
is_debit: bool = isinstance(self, DebitLineItemForm)
|
||||
return sum([x.amount if x.is_debit != is_debit else -x.amount
|
||||
for x in self.offsets])
|
||||
setattr(self, "__offset_total", get_offset_total())
|
||||
@ -422,48 +428,48 @@ class JournalEntryForm(FlaskForm):
|
||||
return all_errors
|
||||
|
||||
|
||||
class DebitEntryForm(JournalEntryForm):
|
||||
"""The form to create or edit a debit journal entry."""
|
||||
class DebitLineItemForm(LineItemForm):
|
||||
"""The form to create or edit a debit line item."""
|
||||
eid = IntegerField()
|
||||
"""The existing journal entry ID."""
|
||||
"""The existing line item ID."""
|
||||
no = IntegerField()
|
||||
"""The order in the currency."""
|
||||
original_entry_id = IntegerField(
|
||||
original_line_item_id = IntegerField(
|
||||
validators=[Optional(),
|
||||
OriginalEntryExists(),
|
||||
OriginalEntryOppositeSide(),
|
||||
OriginalEntryNeedOffset(),
|
||||
OriginalEntryNotOffset()])
|
||||
"""The Id of the original entry."""
|
||||
OriginalLineItemExists(),
|
||||
OriginalLineItemOppositeSide(),
|
||||
OriginalLineItemNeedOffset(),
|
||||
OriginalLineItemNotOffset()])
|
||||
"""The ID of the original line item."""
|
||||
account_code = StringField(
|
||||
filters=[strip_text],
|
||||
validators=[ACCOUNT_REQUIRED,
|
||||
AccountExists(),
|
||||
IsDebitAccount(),
|
||||
SameAccountAsOriginalEntry(),
|
||||
SameAccountAsOriginalLineItem(),
|
||||
KeepAccountWhenHavingOffset(),
|
||||
NotStartPayableFromDebit()])
|
||||
"""The account code."""
|
||||
offset_original_entry_id = IntegerField()
|
||||
"""The Id of the original entry."""
|
||||
offset_original_line_item_id = IntegerField()
|
||||
"""The ID of the original line item."""
|
||||
summary = StringField(filters=[strip_text])
|
||||
"""The summary."""
|
||||
amount = DecimalField(
|
||||
validators=[PositiveAmount(),
|
||||
NotExceedingOriginalEntryNetBalance(),
|
||||
NotExceedingOriginalLineItemNetBalance(),
|
||||
NotLessThanOffsetTotal()])
|
||||
"""The amount."""
|
||||
|
||||
def populate_obj(self, obj: JournalEntry) -> None:
|
||||
"""Populates the form data into a journal entry object.
|
||||
def populate_obj(self, obj: VoucherLineItem) -> None:
|
||||
"""Populates the form data into a line item object.
|
||||
|
||||
:param obj: The journal entry object.
|
||||
:param obj: The line item object.
|
||||
:return: None.
|
||||
"""
|
||||
is_new: bool = obj.id is None
|
||||
if is_new:
|
||||
obj.id = new_id(JournalEntry)
|
||||
obj.original_entry_id = self.original_entry_id.data
|
||||
obj.id = new_id(VoucherLineItem)
|
||||
obj.original_line_item_id = self.original_line_item_id.data
|
||||
obj.account_id = Account.find_by_code(self.account_code.data).id
|
||||
obj.summary = self.summary.data
|
||||
obj.is_debit = True
|
||||
@ -474,25 +480,25 @@ class DebitEntryForm(JournalEntryForm):
|
||||
obj.updated_by_id = current_user_pk
|
||||
|
||||
|
||||
class CreditEntryForm(JournalEntryForm):
|
||||
"""The form to create or edit a credit journal entry."""
|
||||
class CreditLineItemForm(LineItemForm):
|
||||
"""The form to create or edit a credit line item."""
|
||||
eid = IntegerField()
|
||||
"""The existing journal entry ID."""
|
||||
"""The existing line item ID."""
|
||||
no = IntegerField()
|
||||
"""The order in the currency."""
|
||||
original_entry_id = IntegerField(
|
||||
original_line_item_id = IntegerField(
|
||||
validators=[Optional(),
|
||||
OriginalEntryExists(),
|
||||
OriginalEntryOppositeSide(),
|
||||
OriginalEntryNeedOffset(),
|
||||
OriginalEntryNotOffset()])
|
||||
"""The Id of the original entry."""
|
||||
OriginalLineItemExists(),
|
||||
OriginalLineItemOppositeSide(),
|
||||
OriginalLineItemNeedOffset(),
|
||||
OriginalLineItemNotOffset()])
|
||||
"""The ID of the original line item."""
|
||||
account_code = StringField(
|
||||
filters=[strip_text],
|
||||
validators=[ACCOUNT_REQUIRED,
|
||||
AccountExists(),
|
||||
IsCreditAccount(),
|
||||
SameAccountAsOriginalEntry(),
|
||||
SameAccountAsOriginalLineItem(),
|
||||
KeepAccountWhenHavingOffset(),
|
||||
NotStartReceivableFromCredit()])
|
||||
"""The account code."""
|
||||
@ -500,20 +506,20 @@ class CreditEntryForm(JournalEntryForm):
|
||||
"""The summary."""
|
||||
amount = DecimalField(
|
||||
validators=[PositiveAmount(),
|
||||
NotExceedingOriginalEntryNetBalance(),
|
||||
NotExceedingOriginalLineItemNetBalance(),
|
||||
NotLessThanOffsetTotal()])
|
||||
"""The amount."""
|
||||
|
||||
def populate_obj(self, obj: JournalEntry) -> None:
|
||||
"""Populates the form data into a journal entry object.
|
||||
def populate_obj(self, obj: VoucherLineItem) -> None:
|
||||
"""Populates the form data into a line item object.
|
||||
|
||||
:param obj: The journal entry object.
|
||||
:param obj: The line item object.
|
||||
:return: None.
|
||||
"""
|
||||
is_new: bool = obj.id is None
|
||||
if is_new:
|
||||
obj.id = new_id(JournalEntry)
|
||||
obj.original_entry_id = self.original_entry_id.data
|
||||
obj.id = new_id(VoucherLineItem)
|
||||
obj.original_line_item_id = self.original_line_item_id.data
|
||||
obj.account_id = Account.find_by_code(self.account_code.data).id
|
||||
obj.summary = self.summary.data
|
||||
obj.is_debit = False
|
@ -30,18 +30,18 @@ from wtforms.validators import DataRequired, ValidationError
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import lazy_gettext
|
||||
from accounting.models import Voucher, Account, JournalEntry, \
|
||||
from accounting.models import Voucher, Account, VoucherLineItem, \
|
||||
VoucherCurrency
|
||||
from accounting.voucher.utils.account_option import AccountOption
|
||||
from accounting.voucher.utils.original_entries import \
|
||||
get_selectable_original_entries
|
||||
from accounting.voucher.utils.original_line_items import \
|
||||
get_selectable_original_line_items
|
||||
from accounting.voucher.utils.summary_editor import SummaryEditor
|
||||
from accounting.utils.random_id import new_id
|
||||
from accounting.utils.strip_text import strip_multiline_text
|
||||
from accounting.utils.user import get_current_user_pk
|
||||
from .currency import CurrencyForm, CashReceiptCurrencyForm, \
|
||||
CashDisbursementCurrencyForm, TransferCurrencyForm
|
||||
from .journal_entry import JournalEntryForm, DebitEntryForm, CreditEntryForm
|
||||
from .line_item import LineItemForm, DebitLineItemForm, CreditLineItemForm
|
||||
from .reorder import sort_vouchers_in
|
||||
|
||||
DATE_REQUIRED: DataRequired = DataRequired(
|
||||
@ -49,9 +49,9 @@ DATE_REQUIRED: DataRequired = DataRequired(
|
||||
"""The validator to check if the date is empty."""
|
||||
|
||||
|
||||
class NotBeforeOriginalEntries:
|
||||
"""The validator to check if the date is not before the original
|
||||
entries."""
|
||||
class NotBeforeOriginalLineItems:
|
||||
"""The validator to check if the date is not before the
|
||||
original line items."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: DateField) -> None:
|
||||
assert isinstance(form, VoucherForm)
|
||||
@ -62,11 +62,11 @@ class NotBeforeOriginalEntries:
|
||||
return
|
||||
if field.data < min_date:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"The date cannot be earlier than the original entries."))
|
||||
"The date cannot be earlier than the original line items."))
|
||||
|
||||
|
||||
class NotAfterOffsetEntries:
|
||||
"""The validator to check if the date is not after the offset entries."""
|
||||
class NotAfterOffsetItems:
|
||||
"""The validator to check if the date is not after the offset items."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: DateField) -> None:
|
||||
assert isinstance(form, VoucherForm)
|
||||
@ -77,7 +77,7 @@ class NotAfterOffsetEntries:
|
||||
return
|
||||
if field.data > max_date:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"The date cannot be later than the offset entries."))
|
||||
"The date cannot be later than the offset items."))
|
||||
|
||||
|
||||
class NeedSomeCurrencies:
|
||||
@ -88,21 +88,21 @@ class NeedSomeCurrencies:
|
||||
raise ValidationError(lazy_gettext("Please add some currencies."))
|
||||
|
||||
|
||||
class CannotDeleteOriginalEntriesWithOffset:
|
||||
"""The validator to check the original entries with offset."""
|
||||
class CannotDeleteOriginalLineItemsWithOffset:
|
||||
"""The validator to check the original line items with offset."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: FieldList) -> None:
|
||||
assert isinstance(form, VoucherForm)
|
||||
if form.obj is None:
|
||||
return
|
||||
existing_matched_original_entry_id: set[int] \
|
||||
= {x.id for x in form.obj.entries if len(x.offsets) > 0}
|
||||
entry_id_in_form: set[int] \
|
||||
= {x.eid.data for x in form.entries if x.eid.data is not None}
|
||||
for entry_id in existing_matched_original_entry_id:
|
||||
if entry_id not in entry_id_in_form:
|
||||
existing_matched_original_line_item_id: set[int] \
|
||||
= {x.id for x in form.obj.line_items if len(x.offsets) > 0}
|
||||
line_item_id_in_form: set[int] \
|
||||
= {x.eid.data for x in form.line_items if x.eid.data is not None}
|
||||
for line_item_id in existing_matched_original_line_item_id:
|
||||
if line_item_id not in line_item_id_in_form:
|
||||
raise ValidationError(lazy_gettext(
|
||||
"Journal entries with offset cannot be deleted."))
|
||||
"Line items with offset cannot be deleted."))
|
||||
|
||||
|
||||
class VoucherForm(FlaskForm):
|
||||
@ -110,7 +110,7 @@ class VoucherForm(FlaskForm):
|
||||
date = DateField()
|
||||
"""The date."""
|
||||
currencies = FieldList(FormField(CurrencyForm))
|
||||
"""The journal entries categorized by their currencies."""
|
||||
"""The line items categorized by their currencies."""
|
||||
note = TextAreaField()
|
||||
"""The note."""
|
||||
|
||||
@ -123,23 +123,23 @@ class VoucherForm(FlaskForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_modified: bool = False
|
||||
"""Whether the voucher is modified during populate_obj()."""
|
||||
self.collector: t.Type[JournalEntryCollector] = JournalEntryCollector
|
||||
"""The journal entry collector. The default is the base abstract
|
||||
self.collector: t.Type[LineItemCollector] = LineItemCollector
|
||||
"""The line item collector. The default is the base abstract
|
||||
collector only to provide the correct type. The subclass forms should
|
||||
provide their own collectors."""
|
||||
self.obj: Voucher | None = kwargs.get("obj")
|
||||
"""The voucher, when editing an existing one."""
|
||||
self._is_need_payable: bool = False
|
||||
"""Whether we need the payable original entries."""
|
||||
"""Whether we need the payable original line items."""
|
||||
self._is_need_receivable: bool = False
|
||||
"""Whether we need the receivable original entries."""
|
||||
self.__original_entry_options: list[JournalEntry] | None = None
|
||||
"""The options of the original entries."""
|
||||
"""Whether we need the receivable original line items."""
|
||||
self.__original_line_item_options: list[VoucherLineItem] | None = None
|
||||
"""The options of the original line items."""
|
||||
self.__net_balance_exceeded: dict[int, LazyString] | None = None
|
||||
"""The original entries whose net balances were exceeded by the
|
||||
amounts in the journal entry sub-forms."""
|
||||
for entry in self.entries:
|
||||
entry.voucher_form = self
|
||||
"""The original line items whose net balances were exceeded by the
|
||||
amounts in the line item sub-forms."""
|
||||
for line_item in self.line_items:
|
||||
line_item.voucher_form = self
|
||||
|
||||
def populate_obj(self, obj: Voucher) -> None:
|
||||
"""Populates the form data into a voucher object.
|
||||
@ -154,14 +154,15 @@ class VoucherForm(FlaskForm):
|
||||
self.__set_date(obj, self.date.data)
|
||||
obj.note = self.note.data
|
||||
|
||||
collector_cls: t.Type[JournalEntryCollector] = self.collector
|
||||
collector_cls: t.Type[LineItemCollector] = self.collector
|
||||
collector: collector_cls = collector_cls(self, obj)
|
||||
collector.collect()
|
||||
|
||||
to_delete: set[int] = {x.id for x in obj.entries
|
||||
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:
|
||||
JournalEntry.query.filter(JournalEntry.id.in_(to_delete)).delete()
|
||||
VoucherLineItem.query\
|
||||
.filter(VoucherLineItem.id.in_(to_delete)).delete()
|
||||
self.is_modified = True
|
||||
|
||||
if is_new or db.session.is_modified(obj):
|
||||
@ -173,15 +174,15 @@ class VoucherForm(FlaskForm):
|
||||
obj.updated_by_id = current_user_pk
|
||||
|
||||
@property
|
||||
def entries(self) -> list[JournalEntryForm]:
|
||||
"""Collects and returns the journal entry sub-forms.
|
||||
def line_items(self) -> list[LineItemForm]:
|
||||
"""Collects and returns the line item sub-forms.
|
||||
|
||||
:return: The journal entry sub-forms.
|
||||
:return: The line item sub-forms.
|
||||
"""
|
||||
entries: list[JournalEntryForm] = []
|
||||
line_items: list[LineItemForm] = []
|
||||
for currency in self.currencies:
|
||||
entries.extend(currency.entries)
|
||||
return entries
|
||||
line_items.extend(currency.line_items)
|
||||
return line_items
|
||||
|
||||
def __set_date(self, obj: Voucher, new_date: dt.date) -> None:
|
||||
"""Sets the voucher date and number.
|
||||
@ -221,9 +222,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(JournalEntry.account_id)
|
||||
.filter(JournalEntry.is_debit)
|
||||
.group_by(JournalEntry.account_id)).all())
|
||||
sa.select(VoucherLineItem.account_id)
|
||||
.filter(VoucherLineItem.is_debit)
|
||||
.group_by(VoucherLineItem.account_id)).all())
|
||||
for account in accounts:
|
||||
account.is_in_use = account.id in in_use
|
||||
return accounts
|
||||
@ -238,9 +239,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(JournalEntry.account_id)
|
||||
.filter(sa.not_(JournalEntry.is_debit))
|
||||
.group_by(JournalEntry.account_id)).all())
|
||||
sa.select(VoucherLineItem.account_id)
|
||||
.filter(sa.not_(VoucherLineItem.is_debit))
|
||||
.group_by(VoucherLineItem.account_id)).all())
|
||||
for account in accounts:
|
||||
account.is_in_use = account.id in in_use
|
||||
return accounts
|
||||
@ -263,16 +264,18 @@ class VoucherForm(FlaskForm):
|
||||
return SummaryEditor()
|
||||
|
||||
@property
|
||||
def original_entry_options(self) -> list[JournalEntry]:
|
||||
"""Returns the selectable original entries.
|
||||
def original_line_item_options(self) -> list[VoucherLineItem]:
|
||||
"""Returns the selectable original line items.
|
||||
|
||||
:return: The selectable original entries.
|
||||
:return: The selectable original line items.
|
||||
"""
|
||||
if self.__original_entry_options is None:
|
||||
self.__original_entry_options = get_selectable_original_entries(
|
||||
{x.eid.data for x in self.entries if x.eid.data is not None},
|
||||
self._is_need_payable, self._is_need_receivable)
|
||||
return self.__original_entry_options
|
||||
if self.__original_line_item_options is None:
|
||||
self.__original_line_item_options \
|
||||
= get_selectable_original_line_items(
|
||||
{x.eid.data for x in self.line_items
|
||||
if x.eid.data is not None},
|
||||
self._is_need_payable, self._is_need_receivable)
|
||||
return self.__original_line_item_options
|
||||
|
||||
@property
|
||||
def min_date(self) -> dt.date | None:
|
||||
@ -280,13 +283,14 @@ class VoucherForm(FlaskForm):
|
||||
|
||||
:return: The minimal available date.
|
||||
"""
|
||||
original_entry_id: set[int] \
|
||||
= {x.original_entry_id.data for x in self.entries
|
||||
if x.original_entry_id.data is not None}
|
||||
if len(original_entry_id) == 0:
|
||||
original_line_item_id: set[int] \
|
||||
= {x.original_line_item_id.data for x in self.line_items
|
||||
if x.original_line_item_id.data is not None}
|
||||
if len(original_line_item_id) == 0:
|
||||
return None
|
||||
select: sa.Select = sa.select(sa.func.max(Voucher.date))\
|
||||
.join(JournalEntry).filter(JournalEntry.id.in_(original_entry_id))
|
||||
.join(VoucherLineItem)\
|
||||
.filter(VoucherLineItem.id.in_(original_line_item_id))
|
||||
return db.session.scalar(select)
|
||||
|
||||
@property
|
||||
@ -295,11 +299,11 @@ class VoucherForm(FlaskForm):
|
||||
|
||||
:return: The maximum available date.
|
||||
"""
|
||||
entry_id: set[int] = {x.eid.data for x in self.entries
|
||||
if x.eid.data is not None}
|
||||
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(JournalEntry)\
|
||||
.filter(JournalEntry.original_entry_id.in_(entry_id))
|
||||
.join(VoucherLineItem)\
|
||||
.filter(VoucherLineItem.original_line_item_id.in_(line_item_id))
|
||||
return db.session.scalar(select)
|
||||
|
||||
|
||||
@ -307,11 +311,11 @@ T = t.TypeVar("T", bound=VoucherForm)
|
||||
"""A voucher form variant."""
|
||||
|
||||
|
||||
class JournalEntryCollector(t.Generic[T], ABC):
|
||||
"""The journal entry collector."""
|
||||
class LineItemCollector(t.Generic[T], ABC):
|
||||
"""The line item collector."""
|
||||
|
||||
def __init__(self, form: T, obj: Voucher):
|
||||
"""Constructs the journal entry collector.
|
||||
"""Constructs the line item collector.
|
||||
|
||||
:param form: The voucher form.
|
||||
:param obj: The voucher.
|
||||
@ -320,101 +324,103 @@ class JournalEntryCollector(t.Generic[T], ABC):
|
||||
"""The voucher form."""
|
||||
self.__obj: Voucher = obj
|
||||
"""The voucher object."""
|
||||
self.__entries: list[JournalEntry] = list(obj.entries)
|
||||
"""The existing journal entries."""
|
||||
self.__entries_by_id: dict[int, JournalEntry] \
|
||||
= {x.id: x for x in self.__entries}
|
||||
"""A dictionary from the entry ID to their entries."""
|
||||
self.__no_by_id: dict[int, int] = {x.id: x.no for x in self.__entries}
|
||||
"""A dictionary from the entry number to their entries."""
|
||||
self.__line_items: list[VoucherLineItem] = list(obj.line_items)
|
||||
"""The existing line items."""
|
||||
self.__line_items_by_id: dict[int, VoucherLineItem] \
|
||||
= {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] \
|
||||
= {x.id: x.no for x in self.__line_items}
|
||||
"""A dictionary from the line item number to their line items."""
|
||||
self.__currencies: list[VoucherCurrency] = obj.currencies
|
||||
"""The currencies in the voucher."""
|
||||
self._debit_no: int = 1
|
||||
"""The number index for the debit entries."""
|
||||
"""The number index for the debit line items."""
|
||||
self._credit_no: int = 1
|
||||
"""The number index for the credit entries."""
|
||||
"""The number index for the credit line items."""
|
||||
self.to_keep: set[int] = set()
|
||||
"""The ID of the existing journal entries to keep."""
|
||||
"""The ID of the existing line items to keep."""
|
||||
|
||||
@abstractmethod
|
||||
def collect(self) -> set[int]:
|
||||
"""Collects the journal entries.
|
||||
"""Collects the line items.
|
||||
|
||||
:return: The ID of the journal entries to keep.
|
||||
:return: The ID of the line items to keep.
|
||||
"""
|
||||
|
||||
def _add_entry(self, form: JournalEntryForm, currency_code: str, no: int) \
|
||||
def _add_line_item(self, form: LineItemForm, currency_code: str, no: int) \
|
||||
-> None:
|
||||
"""Composes a journal entry from the form.
|
||||
"""Composes a line item from the form.
|
||||
|
||||
:param form: The journal entry form.
|
||||
:param form: The line item form.
|
||||
:param currency_code: The code of the currency.
|
||||
:param no: The number of the entry.
|
||||
:param no: The number of the line item.
|
||||
:return: None.
|
||||
"""
|
||||
entry: JournalEntry | None = self.__entries_by_id.get(form.eid.data)
|
||||
if entry is not None:
|
||||
entry.currency_code = currency_code
|
||||
form.populate_obj(entry)
|
||||
entry.no = no
|
||||
if db.session.is_modified(entry):
|
||||
line_item: VoucherLineItem | None \
|
||||
= self.__line_items_by_id.get(form.eid.data)
|
||||
if line_item is not None:
|
||||
line_item.currency_code = currency_code
|
||||
form.populate_obj(line_item)
|
||||
line_item.no = no
|
||||
if db.session.is_modified(line_item):
|
||||
self.form.is_modified = True
|
||||
else:
|
||||
entry = JournalEntry()
|
||||
entry.currency_code = currency_code
|
||||
form.populate_obj(entry)
|
||||
entry.no = no
|
||||
self.__obj.entries.append(entry)
|
||||
line_item = VoucherLineItem()
|
||||
line_item.currency_code = currency_code
|
||||
form.populate_obj(line_item)
|
||||
line_item.no = no
|
||||
self.__obj.line_items.append(line_item)
|
||||
self.form.is_modified = True
|
||||
self.to_keep.add(entry.id)
|
||||
self.to_keep.add(line_item.id)
|
||||
|
||||
def _make_cash_entry(self, forms: list[JournalEntryForm], is_debit: bool,
|
||||
currency_code: str, no: int) -> None:
|
||||
"""Composes the cash journal entry at the other side of the cash
|
||||
def _make_cash_line_item(self, forms: list[LineItemForm], is_debit: bool,
|
||||
currency_code: str, no: int) -> None:
|
||||
"""Composes the cash line item at the other side of the cash
|
||||
voucher.
|
||||
|
||||
:param forms: The journal entry forms in the same currency.
|
||||
:param forms: The line item forms in the same currency.
|
||||
:param is_debit: True for a cash receipt voucher, or False for a
|
||||
cash disbursement voucher.
|
||||
:param currency_code: The code of the currency.
|
||||
:param no: The number of the entry.
|
||||
:param no: The number of the line item.
|
||||
:return: None.
|
||||
"""
|
||||
candidates: list[JournalEntry] = [x for x in self.__entries
|
||||
if x.is_debit == is_debit
|
||||
and x.currency_code == currency_code]
|
||||
entry: JournalEntry
|
||||
candidates: list[VoucherLineItem] \
|
||||
= [x for x in self.__line_items
|
||||
if x.is_debit == is_debit and x.currency_code == currency_code]
|
||||
line_item: VoucherLineItem
|
||||
if len(candidates) > 0:
|
||||
candidates.sort(key=lambda x: x.no)
|
||||
entry = candidates[0]
|
||||
entry.account_id = Account.cash().id
|
||||
entry.summary = None
|
||||
entry.amount = sum([x.amount.data for x in forms])
|
||||
entry.no = no
|
||||
if db.session.is_modified(entry):
|
||||
line_item = candidates[0]
|
||||
line_item.account_id = Account.cash().id
|
||||
line_item.summary = None
|
||||
line_item.amount = sum([x.amount.data for x in forms])
|
||||
line_item.no = no
|
||||
if db.session.is_modified(line_item):
|
||||
self.form.is_modified = True
|
||||
else:
|
||||
entry = JournalEntry()
|
||||
entry.id = new_id(JournalEntry)
|
||||
entry.is_debit = is_debit
|
||||
entry.currency_code = currency_code
|
||||
entry.account_id = Account.cash().id
|
||||
entry.summary = None
|
||||
entry.amount = sum([x.amount.data for x in forms])
|
||||
entry.no = no
|
||||
self.__obj.entries.append(entry)
|
||||
line_item = VoucherLineItem()
|
||||
line_item.id = new_id(VoucherLineItem)
|
||||
line_item.is_debit = is_debit
|
||||
line_item.currency_code = currency_code
|
||||
line_item.account_id = Account.cash().id
|
||||
line_item.summary = None
|
||||
line_item.amount = sum([x.amount.data for x in forms])
|
||||
line_item.no = no
|
||||
self.__obj.line_items.append(line_item)
|
||||
self.form.is_modified = True
|
||||
self.to_keep.add(entry.id)
|
||||
self.to_keep.add(line_item.id)
|
||||
|
||||
def _sort_entry_forms(self, forms: list[JournalEntryForm]) -> None:
|
||||
"""Sorts the journal entry forms.
|
||||
def _sort_line_item_forms(self, forms: list[LineItemForm]) -> None:
|
||||
"""Sorts the line item sub-forms.
|
||||
|
||||
:param forms: The journal entry forms.
|
||||
:param forms: The line item sub-forms.
|
||||
:return: None.
|
||||
"""
|
||||
missing_no: int = 100 if len(self.__no_by_id) == 0 \
|
||||
else max(self.__no_by_id.values()) + 100
|
||||
ord_by_form: dict[JournalEntryForm, int] \
|
||||
ord_by_form: dict[LineItemForm, int] \
|
||||
= {forms[i]: i for i in range(len(forms))}
|
||||
recv_no: set[int] = {x.no.data for x in forms if x.no.data is not None}
|
||||
missing_recv_no: int = 100 if len(recv_no) == 0 else max(recv_no) + 100
|
||||
@ -445,42 +451,43 @@ class CashReceiptVoucherForm(VoucherForm):
|
||||
"""The form to create or edit a cash receipt voucher."""
|
||||
date = DateField(
|
||||
validators=[DATE_REQUIRED,
|
||||
NotBeforeOriginalEntries(),
|
||||
NotAfterOffsetEntries()])
|
||||
NotBeforeOriginalLineItems(),
|
||||
NotAfterOffsetItems()])
|
||||
"""The date."""
|
||||
currencies = FieldList(FormField(CashReceiptCurrencyForm), name="currency",
|
||||
validators=[NeedSomeCurrencies()])
|
||||
"""The journal entries categorized by their currencies."""
|
||||
"""The line items categorized by their currencies."""
|
||||
note = TextAreaField(filters=[strip_multiline_text])
|
||||
"""The note."""
|
||||
whole_form = BooleanField(
|
||||
validators=[CannotDeleteOriginalEntriesWithOffset()])
|
||||
validators=[CannotDeleteOriginalLineItemsWithOffset()])
|
||||
"""The pseudo field for the whole form validators."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._is_need_receivable = True
|
||||
|
||||
class Collector(JournalEntryCollector[CashReceiptVoucherForm]):
|
||||
"""The journal entry collector for the cash receipt vouchers."""
|
||||
class Collector(LineItemCollector[CashReceiptVoucherForm]):
|
||||
"""The line item collector for the cash receipt vouchers."""
|
||||
|
||||
def collect(self) -> None:
|
||||
currencies: list[CashReceiptCurrencyForm] \
|
||||
= [x.form for x in self.form.currencies]
|
||||
self._sort_currency_forms(currencies)
|
||||
for currency in currencies:
|
||||
# The debit cash entry
|
||||
self._make_cash_entry(list(currency.credit), True,
|
||||
currency.code.data, self._debit_no)
|
||||
# The debit cash line item
|
||||
self._make_cash_line_item(list(currency.credit), True,
|
||||
currency.code.data,
|
||||
self._debit_no)
|
||||
self._debit_no = self._debit_no + 1
|
||||
|
||||
# The credit forms
|
||||
credit_forms: list[CreditEntryForm] \
|
||||
credit_forms: list[CreditLineItemForm] \
|
||||
= [x.form for x in currency.credit]
|
||||
self._sort_entry_forms(credit_forms)
|
||||
self._sort_line_item_forms(credit_forms)
|
||||
for credit_form in credit_forms:
|
||||
self._add_entry(credit_form, currency.code.data,
|
||||
self._credit_no)
|
||||
self._add_line_item(credit_form, currency.code.data,
|
||||
self._credit_no)
|
||||
self._credit_no = self._credit_no + 1
|
||||
|
||||
self.collector = Collector
|
||||
@ -490,26 +497,25 @@ class CashDisbursementVoucherForm(VoucherForm):
|
||||
"""The form to create or edit a cash disbursement voucher."""
|
||||
date = DateField(
|
||||
validators=[DATE_REQUIRED,
|
||||
NotBeforeOriginalEntries(),
|
||||
NotAfterOffsetEntries()])
|
||||
NotBeforeOriginalLineItems(),
|
||||
NotAfterOffsetItems()])
|
||||
"""The date."""
|
||||
currencies = FieldList(FormField(CashDisbursementCurrencyForm),
|
||||
name="currency",
|
||||
validators=[NeedSomeCurrencies()])
|
||||
"""The journal entries categorized by their currencies."""
|
||||
"""The line items categorized by their currencies."""
|
||||
note = TextAreaField(filters=[strip_multiline_text])
|
||||
"""The note."""
|
||||
whole_form = BooleanField(
|
||||
validators=[CannotDeleteOriginalEntriesWithOffset()])
|
||||
validators=[CannotDeleteOriginalLineItemsWithOffset()])
|
||||
"""The pseudo field for the whole form validators."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._is_need_payable = True
|
||||
|
||||
class Collector(JournalEntryCollector[CashDisbursementVoucherForm]):
|
||||
"""The journal entry collector for the cash disbursement
|
||||
vouchers."""
|
||||
class Collector(LineItemCollector[CashDisbursementVoucherForm]):
|
||||
"""The line item collector for the cash disbursement vouchers."""
|
||||
|
||||
def collect(self) -> None:
|
||||
currencies: list[CashDisbursementCurrencyForm] \
|
||||
@ -517,17 +523,18 @@ class CashDisbursementVoucherForm(VoucherForm):
|
||||
self._sort_currency_forms(currencies)
|
||||
for currency in currencies:
|
||||
# The debit forms
|
||||
debit_forms: list[DebitEntryForm] \
|
||||
debit_forms: list[DebitLineItemForm] \
|
||||
= [x.form for x in currency.debit]
|
||||
self._sort_entry_forms(debit_forms)
|
||||
self._sort_line_item_forms(debit_forms)
|
||||
for debit_form in debit_forms:
|
||||
self._add_entry(debit_form, currency.code.data,
|
||||
self._debit_no)
|
||||
self._add_line_item(debit_form, currency.code.data,
|
||||
self._debit_no)
|
||||
self._debit_no = self._debit_no + 1
|
||||
|
||||
# The credit forms
|
||||
self._make_cash_entry(list(currency.debit), False,
|
||||
currency.code.data, self._credit_no)
|
||||
self._make_cash_line_item(list(currency.debit), False,
|
||||
currency.code.data,
|
||||
self._credit_no)
|
||||
self._credit_no = self._credit_no + 1
|
||||
|
||||
self.collector = Collector
|
||||
@ -537,16 +544,16 @@ class TransferVoucherForm(VoucherForm):
|
||||
"""The form to create or edit a transfer voucher."""
|
||||
date = DateField(
|
||||
validators=[DATE_REQUIRED,
|
||||
NotBeforeOriginalEntries(),
|
||||
NotAfterOffsetEntries()])
|
||||
NotBeforeOriginalLineItems(),
|
||||
NotAfterOffsetItems()])
|
||||
"""The date."""
|
||||
currencies = FieldList(FormField(TransferCurrencyForm), name="currency",
|
||||
validators=[NeedSomeCurrencies()])
|
||||
"""The journal entries categorized by their currencies."""
|
||||
"""The line items categorized by their currencies."""
|
||||
note = TextAreaField(filters=[strip_multiline_text])
|
||||
"""The note."""
|
||||
whole_form = BooleanField(
|
||||
validators=[CannotDeleteOriginalEntriesWithOffset()])
|
||||
validators=[CannotDeleteOriginalLineItemsWithOffset()])
|
||||
"""The pseudo field for the whole form validators."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -554,8 +561,8 @@ class TransferVoucherForm(VoucherForm):
|
||||
self._is_need_payable = True
|
||||
self._is_need_receivable = True
|
||||
|
||||
class Collector(JournalEntryCollector[TransferVoucherForm]):
|
||||
"""The journal entry collector for the transfer vouchers."""
|
||||
class Collector(LineItemCollector[TransferVoucherForm]):
|
||||
"""The line item collector for the transfer vouchers."""
|
||||
|
||||
def collect(self) -> None:
|
||||
currencies: list[TransferCurrencyForm] \
|
||||
@ -563,21 +570,21 @@ class TransferVoucherForm(VoucherForm):
|
||||
self._sort_currency_forms(currencies)
|
||||
for currency in currencies:
|
||||
# The debit forms
|
||||
debit_forms: list[DebitEntryForm] \
|
||||
debit_forms: list[DebitLineItemForm] \
|
||||
= [x.form for x in currency.debit]
|
||||
self._sort_entry_forms(debit_forms)
|
||||
self._sort_line_item_forms(debit_forms)
|
||||
for debit_form in debit_forms:
|
||||
self._add_entry(debit_form, currency.code.data,
|
||||
self._debit_no)
|
||||
self._add_line_item(debit_form, currency.code.data,
|
||||
self._debit_no)
|
||||
self._debit_no = self._debit_no + 1
|
||||
|
||||
# The credit forms
|
||||
credit_forms: list[CreditEntryForm] \
|
||||
credit_forms: list[CreditLineItemForm] \
|
||||
= [x.form for x in currency.credit]
|
||||
self._sort_entry_forms(credit_forms)
|
||||
self._sort_line_item_forms(credit_forms)
|
||||
for credit_form in credit_forms:
|
||||
self._add_entry(credit_form, currency.code.data,
|
||||
self._credit_no)
|
||||
self._add_line_item(credit_form, currency.code.data,
|
||||
self._credit_no)
|
||||
self._credit_no = self._credit_no + 1
|
||||
|
||||
self.collector = Collector
|
||||
|
@ -14,20 +14,20 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""The SQLAlchemy alias for the offset entries.
|
||||
"""The SQLAlchemy alias for the offset items.
|
||||
|
||||
"""
|
||||
import typing as t
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from accounting.models import JournalEntry
|
||||
from accounting.models import VoucherLineItem
|
||||
|
||||
|
||||
def offset_alias() -> sa.Alias:
|
||||
"""Returns the SQLAlchemy alias for the offset entries.
|
||||
"""Returns the SQLAlchemy alias for the offset items.
|
||||
|
||||
:return: The SQLAlchemy alias for the offset entries.
|
||||
:return: The SQLAlchemy alias for the offset items.
|
||||
"""
|
||||
|
||||
def as_from(model_cls: t.Any) -> sa.FromClause:
|
||||
@ -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(JournalEntry), name="offset"))
|
||||
return as_alias(sa.alias(as_from(VoucherLineItem), name="offset"))
|
||||
|
@ -78,16 +78,16 @@ class VoucherOperator(ABC):
|
||||
"""
|
||||
|
||||
@property
|
||||
def _entry_template(self) -> str:
|
||||
"""Renders and returns the template for the journal entry sub-form.
|
||||
def _line_item_template(self) -> str:
|
||||
"""Renders and returns the template for the line item sub-form.
|
||||
|
||||
:return: The template for the journal entry sub-form.
|
||||
:return: The template for the line item sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/voucher/include/form-entry-item.html",
|
||||
"accounting/voucher/include/form-line-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
entry_type="ENTRY_TYPE",
|
||||
entry_index="ENTRY_INDEX")
|
||||
side="SIDE",
|
||||
line_item_index="LINE_ITEM_INDEX")
|
||||
|
||||
|
||||
class CashReceiptVoucher(VoucherOperator):
|
||||
@ -113,7 +113,7 @@ class CashReceiptVoucher(VoucherOperator):
|
||||
form=form,
|
||||
voucher_type=VoucherType.CASH_RECEIPT,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def render_detail_template(self, voucher: Voucher) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
@ -135,7 +135,7 @@ class CashReceiptVoucher(VoucherOperator):
|
||||
return render_template("accounting/voucher/receipt/edit.html",
|
||||
voucher=voucher, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def is_my_type(self, voucher: Voucher) -> bool:
|
||||
"""Checks and returns whether the voucher belongs to the type.
|
||||
@ -182,7 +182,7 @@ class CashDisbursementVoucher(VoucherOperator):
|
||||
form=form,
|
||||
voucher_type=VoucherType.CASH_DISBURSEMENT,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def render_detail_template(self, voucher: Voucher) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
@ -204,7 +204,7 @@ class CashDisbursementVoucher(VoucherOperator):
|
||||
return render_template("accounting/voucher/disbursement/edit.html",
|
||||
voucher=voucher, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def is_my_type(self, voucher: Voucher) -> bool:
|
||||
"""Checks and returns whether the voucher belongs to the type.
|
||||
@ -251,7 +251,7 @@ class TransferVoucher(VoucherOperator):
|
||||
form=form,
|
||||
voucher_type=VoucherType.TRANSFER,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def render_detail_template(self, voucher: Voucher) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
@ -273,7 +273,7 @@ class TransferVoucher(VoucherOperator):
|
||||
return render_template("accounting/voucher/transfer/edit.html",
|
||||
voucher=voucher, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def is_my_type(self, voucher: Voucher) -> bool:
|
||||
"""Checks and returns whether the voucher belongs to the type.
|
||||
|
@ -14,7 +14,7 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""The selectable original entries.
|
||||
"""The selectable original line items.
|
||||
|
||||
"""
|
||||
from decimal import Decimal
|
||||
@ -23,57 +23,59 @@ import sqlalchemy as sa
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from accounting import db
|
||||
from accounting.models import Account, Voucher, JournalEntry
|
||||
from accounting.models import Account, Voucher, VoucherLineItem
|
||||
from accounting.utils.cast import be
|
||||
from .offset_alias import offset_alias
|
||||
|
||||
|
||||
def get_selectable_original_entries(
|
||||
entry_id_on_form: set[int], is_payable: bool, is_receivable: bool) \
|
||||
-> list[JournalEntry]:
|
||||
"""Queries and returns the selectable original entries, with their net
|
||||
def get_selectable_original_line_items(
|
||||
line_item_id_on_form: set[int], is_payable: bool,
|
||||
is_receivable: bool) -> list[VoucherLineItem]:
|
||||
"""Queries and returns the selectable original line items, with their net
|
||||
balances. The offset amounts of the form is excluded.
|
||||
|
||||
:param entry_id_on_form: The ID of the journal entries on the form.
|
||||
:param is_payable: True to check the payable original entries, or False
|
||||
:param line_item_id_on_form: The ID of the line items on the form.
|
||||
:param is_payable: True to check the payable original line items, or False
|
||||
otherwise.
|
||||
:param is_receivable: True to check the receivable original entries, or
|
||||
:param is_receivable: True to check the receivable original line items, or
|
||||
False otherwise.
|
||||
:return: The selectable original entries, with their net balances.
|
||||
:return: The selectable original line items, with their net balances.
|
||||
"""
|
||||
assert is_payable or is_receivable
|
||||
offset: sa.Alias = offset_alias()
|
||||
net_balance: sa.Label = (JournalEntry.amount + sa.func.sum(sa.case(
|
||||
(offset.c.id.in_(entry_id_on_form), 0),
|
||||
(be(offset.c.is_debit == JournalEntry.is_debit), offset.c.amount),
|
||||
net_balance: sa.Label = (VoucherLineItem.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),
|
||||
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_(JournalEntry.is_debit)))
|
||||
sa.not_(VoucherLineItem.is_debit)))
|
||||
if is_receivable:
|
||||
sub_conditions.append(sa.and_(Account.base_code.startswith("1"),
|
||||
JournalEntry.is_debit))
|
||||
VoucherLineItem.is_debit))
|
||||
conditions.append(sa.or_(*sub_conditions))
|
||||
select_net_balances: sa.Select = sa.select(JournalEntry.id, net_balance)\
|
||||
select_net_balances: sa.Select \
|
||||
= sa.select(VoucherLineItem.id, net_balance)\
|
||||
.join(Account)\
|
||||
.join(offset, be(JournalEntry.id == offset.c.original_entry_id),
|
||||
.join(offset, be(VoucherLineItem.id == offset.c.original_line_item_id),
|
||||
isouter=True)\
|
||||
.filter(*conditions)\
|
||||
.group_by(JournalEntry.id)\
|
||||
.group_by(VoucherLineItem.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()}
|
||||
entries: list[JournalEntry] = JournalEntry.query\
|
||||
.filter(JournalEntry.id.in_({x for x in net_balances}))\
|
||||
line_items: list[VoucherLineItem] = VoucherLineItem.query\
|
||||
.filter(VoucherLineItem.id.in_({x for x in net_balances}))\
|
||||
.join(Voucher)\
|
||||
.order_by(Voucher.date, JournalEntry.is_debit, JournalEntry.no)\
|
||||
.options(selectinload(JournalEntry.currency),
|
||||
selectinload(JournalEntry.account),
|
||||
selectinload(JournalEntry.voucher)).all()
|
||||
for entry in entries:
|
||||
entry.net_balance = entry.amount if net_balances[entry.id] is None \
|
||||
else net_balances[entry.id]
|
||||
return entries
|
||||
.order_by(Voucher.date, VoucherLineItem.is_debit, VoucherLineItem.no)\
|
||||
.options(selectinload(VoucherLineItem.currency),
|
||||
selectinload(VoucherLineItem.account),
|
||||
selectinload(VoucherLineItem.voucher)).all()
|
||||
for line_item in line_items:
|
||||
line_item.net_balance = line_item.amount \
|
||||
if net_balances[line_item.id] is None \
|
||||
else net_balances[line_item.id]
|
||||
return line_items
|
@ -22,7 +22,7 @@ import typing as t
|
||||
import sqlalchemy as sa
|
||||
|
||||
from accounting import db
|
||||
from accounting.models import Account, JournalEntry
|
||||
from accounting.models import Account, VoucherLineItem
|
||||
|
||||
|
||||
class SummaryAccount:
|
||||
@ -143,16 +143,16 @@ class SummaryType:
|
||||
return sorted(self.__tag_dict.values(), key=lambda x: -x.freq)
|
||||
|
||||
|
||||
class SummaryEntryType:
|
||||
"""A summary type"""
|
||||
class SummarySide:
|
||||
"""A summary side"""
|
||||
|
||||
def __init__(self, entry_type_id: t.Literal["debit", "credit"]):
|
||||
"""Constructs a summary entry type.
|
||||
def __init__(self, side_id: t.Literal["debit", "credit"]):
|
||||
"""Constructs a summary side.
|
||||
|
||||
:param entry_type_id: The entry type ID, either "debit" or "credit".
|
||||
:param side_id: The side ID, either "debit" or "credit".
|
||||
"""
|
||||
self.type: t.Literal["debit", "credit"] = entry_type_id
|
||||
"""The entry type."""
|
||||
self.side: t.Literal["debit", "credit"] = side_id
|
||||
"""The side."""
|
||||
self.general: SummaryType = SummaryType("general")
|
||||
"""The general tags."""
|
||||
self.travel: SummaryType = SummaryType("travel")
|
||||
@ -179,7 +179,7 @@ class SummaryEntryType:
|
||||
@property
|
||||
def accounts(self) -> list[SummaryAccount]:
|
||||
"""Returns the suggested accounts of all tags in the summary editor in
|
||||
the entry type, in their frequency order.
|
||||
the side, in their frequency order.
|
||||
|
||||
:return: The suggested accounts of all tags, in their frequency order.
|
||||
"""
|
||||
@ -202,33 +202,33 @@ class SummaryEditor:
|
||||
|
||||
def __init__(self):
|
||||
"""Constructs the summary editor."""
|
||||
self.debit: SummaryEntryType = SummaryEntryType("debit")
|
||||
self.debit: SummarySide = SummarySide("debit")
|
||||
"""The debit tags."""
|
||||
self.credit: SummaryEntryType = SummaryEntryType("credit")
|
||||
self.credit: SummarySide = SummarySide("credit")
|
||||
"""The credit tags."""
|
||||
entry_type: sa.Label = sa.case((JournalEntry.is_debit, "debit"),
|
||||
else_="credit").label("entry_type")
|
||||
side: sa.Label = sa.case((VoucherLineItem.is_debit, "debit"),
|
||||
else_="credit").label("side")
|
||||
tag_type: sa.Label = sa.case(
|
||||
(JournalEntry.summary.like("_%—_%—_%→_%"), "bus"),
|
||||
(sa.or_(JournalEntry.summary.like("_%—_%→_%"),
|
||||
JournalEntry.summary.like("_%—_%↔_%")), "travel"),
|
||||
(VoucherLineItem.summary.like("_%—_%—_%→_%"), "bus"),
|
||||
(sa.or_(VoucherLineItem.summary.like("_%—_%→_%"),
|
||||
VoucherLineItem.summary.like("_%—_%↔_%")), "travel"),
|
||||
else_="general").label("tag_type")
|
||||
tag: sa.Label = get_prefix(JournalEntry.summary, "—").label("tag")
|
||||
select: sa.Select = sa.Select(entry_type, tag_type, tag,
|
||||
JournalEntry.account_id,
|
||||
tag: sa.Label = get_prefix(VoucherLineItem.summary, "—").label("tag")
|
||||
select: sa.Select = sa.Select(side, tag_type, tag,
|
||||
VoucherLineItem.account_id,
|
||||
sa.func.count().label("freq"))\
|
||||
.filter(JournalEntry.summary.is_not(None),
|
||||
JournalEntry.summary.like("_%—_%"),
|
||||
JournalEntry.original_entry_id.is_(None))\
|
||||
.group_by(entry_type, tag_type, tag, JournalEntry.account_id)
|
||||
.filter(VoucherLineItem.summary.is_not(None),
|
||||
VoucherLineItem.summary.like("_%—_%"),
|
||||
VoucherLineItem.original_line_item_id.is_(None))\
|
||||
.group_by(side, tag_type, tag, VoucherLineItem.account_id)
|
||||
result: list[sa.Row] = db.session.execute(select).all()
|
||||
accounts: dict[int, Account] \
|
||||
= {x.id: x for x in Account.query
|
||||
.filter(Account.id.in_({x.account_id for x in result})).all()}
|
||||
entry_type_dict: dict[t.Literal["debit", "credit"], SummaryEntryType] \
|
||||
= {x.type: x for x in {self.debit, self.credit}}
|
||||
side_dict: dict[t.Literal["debit", "credit"], SummarySide] \
|
||||
= {x.side: x for x in {self.debit, self.credit}}
|
||||
for row in result:
|
||||
entry_type_dict[row.entry_type].add_tag(
|
||||
side_dict[row.side].add_tag(
|
||||
row.tag_type, row.tag, accounts[row.account_id], row.freq)
|
||||
|
||||
|
||||
|
@ -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, JournalEntryData, VoucherData, \
|
||||
from testlib_offset import TestData, VoucherLineItemData, 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, \
|
||||
JournalEntry
|
||||
VoucherLineItem
|
||||
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()
|
||||
JournalEntry.query.delete()
|
||||
VoucherLineItem.query.delete()
|
||||
|
||||
self.client, self.csrf_token = get_client(self.app, "editor")
|
||||
self.data: TestData = TestData(self.app, self.client, self.csrf_token)
|
||||
@ -84,33 +84,34 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.data.e_r_or3d.voucher.days, [CurrencyData(
|
||||
"USD",
|
||||
[],
|
||||
[JournalEntryData(Accounts.RECEIVABLE,
|
||||
self.data.e_r_or1d.summary, "300",
|
||||
original_entry=self.data.e_r_or1d),
|
||||
JournalEntryData(Accounts.RECEIVABLE,
|
||||
self.data.e_r_or1d.summary, "100",
|
||||
original_entry=self.data.e_r_or1d),
|
||||
JournalEntryData(Accounts.RECEIVABLE,
|
||||
self.data.e_r_or3d.summary, "100",
|
||||
original_entry=self.data.e_r_or3d)])])
|
||||
[VoucherLineItemData(Accounts.RECEIVABLE,
|
||||
self.data.e_r_or1d.summary, "300",
|
||||
original_line_item=self.data.e_r_or1d),
|
||||
VoucherLineItemData(Accounts.RECEIVABLE,
|
||||
self.data.e_r_or1d.summary, "100",
|
||||
original_line_item=self.data.e_r_or1d),
|
||||
VoucherLineItemData(Accounts.RECEIVABLE,
|
||||
self.data.e_r_or3d.summary, "100",
|
||||
original_line_item=self.data.e_r_or3d)])])
|
||||
|
||||
# Non-existing original entry ID
|
||||
# Non-existing original line item ID
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-credit-1-original_entry_id"] = "9999"
|
||||
form["currency-1-credit-1-original_line_item_id"] = "9999"
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# The same side
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-credit-1-original_entry_id"] = self.data.e_p_or1c.id
|
||||
form["currency-1-credit-1-original_line_item_id"] \
|
||||
= self.data.e_p_or1c.id
|
||||
form["currency-1-credit-1-account_code"] = self.data.e_p_or1c.account
|
||||
form["currency-1-credit-1-amount"] = "100"
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# The original entry does not need offset
|
||||
# The original line item does not need offset
|
||||
with self.app.app_context():
|
||||
account = Account.find_by_code(Accounts.RECEIVABLE)
|
||||
account.is_need_offset = False
|
||||
@ -124,9 +125,10 @@ class OffsetTestCase(unittest.TestCase):
|
||||
account.is_need_offset = True
|
||||
db.session.commit()
|
||||
|
||||
# The original entry is also an offset
|
||||
# The original line item is also an offset
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-credit-1-original_entry_id"] = self.data.e_p_of1d.id
|
||||
form["currency-1-credit-1-original_line_item_id"] \
|
||||
= self.data.e_p_of1d.id
|
||||
form["currency-1-credit-1-account_code"] = self.data.e_p_of1d.account
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
@ -164,7 +166,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# Not before the original entries
|
||||
# Not before the original line items
|
||||
old_days = voucher_data.days
|
||||
voucher_data.days = old_days + 1
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
@ -181,7 +183,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
with self.app.app_context():
|
||||
voucher = db.session.get(Voucher, voucher_id)
|
||||
for offset in voucher.currencies[0].credit:
|
||||
self.assertIsNotNone(offset.original_entry_id)
|
||||
self.assertIsNotNone(offset.original_line_item_id)
|
||||
|
||||
def test_edit_receivable_offset(self) -> None:
|
||||
"""Tests to edit the receivable offset.
|
||||
@ -201,16 +203,17 @@ class OffsetTestCase(unittest.TestCase):
|
||||
voucher_data.currencies[0].debit[2].amount = Decimal("600")
|
||||
voucher_data.currencies[0].credit[2].amount = Decimal("600")
|
||||
|
||||
# Non-existing original entry ID
|
||||
# Non-existing original line item ID
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-credit-1-original_entry_id"] = "9999"
|
||||
form["currency-1-credit-1-original_line_item_id"] = "9999"
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# The same side
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-credit-1-original_entry_id"] = self.data.e_p_or1c.id
|
||||
form["currency-1-credit-1-original_line_item_id"] \
|
||||
= self.data.e_p_or1c.id
|
||||
form["currency-1-credit-1-account_code"] = self.data.e_p_or1c.account
|
||||
form["currency-1-debit-1-amount"] = "100"
|
||||
form["currency-1-credit-1-amount"] = "100"
|
||||
@ -218,7 +221,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# The original entry does not need offset
|
||||
# The original line item does not need offset
|
||||
with self.app.app_context():
|
||||
account = Account.find_by_code(Accounts.RECEIVABLE)
|
||||
account.is_need_offset = False
|
||||
@ -232,9 +235,10 @@ class OffsetTestCase(unittest.TestCase):
|
||||
account.is_need_offset = True
|
||||
db.session.commit()
|
||||
|
||||
# The original entry is also an offset
|
||||
# The original line item is also an offset
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-credit-1-original_entry_id"] = self.data.e_p_of1d.id
|
||||
form["currency-1-credit-1-original_line_item_id"] \
|
||||
= self.data.e_p_of1d.id
|
||||
form["currency-1-credit-1-account_code"] = self.data.e_p_of1d.account
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
@ -276,7 +280,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not before the original entries
|
||||
# Not before the original line items
|
||||
old_days: int = voucher_data.days
|
||||
voucher_data.days = old_days + 1
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
@ -292,8 +296,8 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.headers["Location"],
|
||||
f"{PREFIX}/{voucher_data.id}?next=%2F_next")
|
||||
|
||||
def test_edit_receivable_original_entry(self) -> None:
|
||||
"""Tests to edit the receivable original entry.
|
||||
def test_edit_receivable_original_line_item(self) -> None:
|
||||
"""Tests to edit the receivable original line item.
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
@ -346,7 +350,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not after the offset entries
|
||||
# Not after the offset items
|
||||
old_days: int = voucher_data.days
|
||||
voucher_data.days = old_days - 1
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
@ -355,7 +359,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
voucher_data.days = old_days
|
||||
|
||||
# Not deleting matched original entries
|
||||
# Not deleting matched original line items
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
del form["currency-1-debit-1-eid"]
|
||||
response = self.client.post(update_uri, data=form)
|
||||
@ -369,8 +373,8 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.headers["Location"],
|
||||
f"{PREFIX}/{voucher_data.id}?next=%2F_next")
|
||||
|
||||
# The original entry is always before the offset entry, even when they
|
||||
# happen in the same day.
|
||||
# The original line item is always before the offset item, even when
|
||||
# they happen in the same day.
|
||||
with self.app.app_context():
|
||||
voucher_or: Voucher | None = db.session.get(
|
||||
Voucher, voucher_data.id)
|
||||
@ -395,34 +399,35 @@ class OffsetTestCase(unittest.TestCase):
|
||||
voucher_data: VoucherData = VoucherData(
|
||||
self.data.e_p_or3c.voucher.days, [CurrencyData(
|
||||
"USD",
|
||||
[JournalEntryData(Accounts.PAYABLE,
|
||||
self.data.e_p_or1c.summary, "500",
|
||||
original_entry=self.data.e_p_or1c),
|
||||
JournalEntryData(Accounts.PAYABLE,
|
||||
self.data.e_p_or1c.summary, "300",
|
||||
original_entry=self.data.e_p_or1c),
|
||||
JournalEntryData(Accounts.PAYABLE,
|
||||
self.data.e_p_or3c.summary, "120",
|
||||
original_entry=self.data.e_p_or3c)],
|
||||
[VoucherLineItemData(Accounts.PAYABLE,
|
||||
self.data.e_p_or1c.summary, "500",
|
||||
original_line_item=self.data.e_p_or1c),
|
||||
VoucherLineItemData(Accounts.PAYABLE,
|
||||
self.data.e_p_or1c.summary, "300",
|
||||
original_line_item=self.data.e_p_or1c),
|
||||
VoucherLineItemData(Accounts.PAYABLE,
|
||||
self.data.e_p_or3c.summary, "120",
|
||||
original_line_item=self.data.e_p_or3c)],
|
||||
[])])
|
||||
|
||||
# Non-existing original entry ID
|
||||
# Non-existing original line item ID
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-debit-1-original_entry_id"] = "9999"
|
||||
form["currency-1-debit-1-original_line_item_id"] = "9999"
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# The same side
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-debit-1-original_entry_id"] = self.data.e_r_or1d.id
|
||||
form["currency-1-debit-1-original_line_item_id"] \
|
||||
= self.data.e_r_or1d.id
|
||||
form["currency-1-debit-1-account_code"] = self.data.e_r_or1d.account
|
||||
form["currency-1-debit-1-amount"] = "100"
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# The original entry does not need offset
|
||||
# The original line item does not need offset
|
||||
with self.app.app_context():
|
||||
account = Account.find_by_code(Accounts.PAYABLE)
|
||||
account.is_need_offset = False
|
||||
@ -436,9 +441,10 @@ class OffsetTestCase(unittest.TestCase):
|
||||
account.is_need_offset = True
|
||||
db.session.commit()
|
||||
|
||||
# The original entry is also an offset
|
||||
# The original line item is also an offset
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-debit-1-original_entry_id"] = self.data.e_r_of1c.id
|
||||
form["currency-1-debit-1-original_line_item_id"] \
|
||||
= self.data.e_r_of1c.id
|
||||
form["currency-1-debit-1-account_code"] = self.data.e_r_of1c.account
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
@ -474,7 +480,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# Not before the original entries
|
||||
# Not before the original line items
|
||||
old_days: int = voucher_data.days
|
||||
voucher_data.days = old_days + 1
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
@ -491,7 +497,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
with self.app.app_context():
|
||||
voucher = db.session.get(Voucher, voucher_id)
|
||||
for offset in voucher.currencies[0].debit:
|
||||
self.assertIsNotNone(offset.original_entry_id)
|
||||
self.assertIsNotNone(offset.original_line_item_id)
|
||||
|
||||
def test_edit_payable_offset(self) -> None:
|
||||
"""Tests to edit the payable offset.
|
||||
@ -511,16 +517,17 @@ class OffsetTestCase(unittest.TestCase):
|
||||
voucher_data.currencies[0].debit[2].amount = Decimal("900")
|
||||
voucher_data.currencies[0].credit[2].amount = Decimal("900")
|
||||
|
||||
# Non-existing original entry ID
|
||||
# Non-existing original line item ID
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-1-original_entry_id"] = "9999"
|
||||
form["currency-1-debit-1-original_line_item_id"] = "9999"
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# The same side
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-1-original_entry_id"] = self.data.e_r_or1d.id
|
||||
form["currency-1-debit-1-original_line_item_id"] \
|
||||
= self.data.e_r_or1d.id
|
||||
form["currency-1-debit-1-account_code"] = self.data.e_r_or1d.account
|
||||
form["currency-1-debit-1-amount"] = "100"
|
||||
form["currency-1-credit-1-amount"] = "100"
|
||||
@ -528,7 +535,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# The original entry does not need offset
|
||||
# The original line item does not need offset
|
||||
with self.app.app_context():
|
||||
account = Account.find_by_code(Accounts.PAYABLE)
|
||||
account.is_need_offset = False
|
||||
@ -542,9 +549,10 @@ class OffsetTestCase(unittest.TestCase):
|
||||
account.is_need_offset = True
|
||||
db.session.commit()
|
||||
|
||||
# The original entry is also an offset
|
||||
# The original line item is also an offset
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-1-original_entry_id"] = self.data.e_r_of1c.id
|
||||
form["currency-1-debit-1-original_line_item_id"] \
|
||||
= self.data.e_r_of1c.id
|
||||
form["currency-1-debit-1-account_code"] = self.data.e_r_of1c.account
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
@ -586,7 +594,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not before the original entries
|
||||
# Not before the original line items
|
||||
old_days: int = voucher_data.days
|
||||
voucher_data.days = old_days + 1
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
@ -603,10 +611,10 @@ class OffsetTestCase(unittest.TestCase):
|
||||
with self.app.app_context():
|
||||
voucher = db.session.get(Voucher, voucher_id)
|
||||
for offset in voucher.currencies[0].debit:
|
||||
self.assertIsNotNone(offset.original_entry_id)
|
||||
self.assertIsNotNone(offset.original_line_item_id)
|
||||
|
||||
def test_edit_payable_original_entry(self) -> None:
|
||||
"""Tests to edit the payable original entry.
|
||||
def test_edit_payable_original_line_item(self) -> None:
|
||||
"""Tests to edit the payable original line item.
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
@ -659,7 +667,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not after the offset entries
|
||||
# Not after the offset items
|
||||
old_days: int = voucher_data.days
|
||||
voucher_data.days = old_days - 1
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
@ -668,7 +676,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
voucher_data.days = old_days
|
||||
|
||||
# Not deleting matched original entries
|
||||
# Not deleting matched original line items
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
del form["currency-1-credit-1-eid"]
|
||||
response = self.client.post(update_uri, data=form)
|
||||
@ -682,8 +690,8 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.headers["Location"],
|
||||
f"{PREFIX}/{voucher_data.id}?next=%2F_next")
|
||||
|
||||
# The original entry is always before the offset entry, even when they
|
||||
# happen in the same day
|
||||
# The original line item is always before the offset item, even when
|
||||
# they happen in the same day
|
||||
with self.app.app_context():
|
||||
voucher_or: Voucher | None = db.session.get(
|
||||
Voucher, voucher_data.id)
|
||||
|
@ -42,7 +42,7 @@ class SummeryEditorTestCase(unittest.TestCase):
|
||||
runner: FlaskCliRunner = self.app.test_cli_runner()
|
||||
with self.app.app_context():
|
||||
from accounting.models import BaseAccount, Voucher, \
|
||||
JournalEntry
|
||||
VoucherLineItem
|
||||
result: Result
|
||||
result = runner.invoke(args="init-db")
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
@ -56,7 +56,7 @@ class SummeryEditorTestCase(unittest.TestCase):
|
||||
"-u", "editor"])
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
Voucher.query.delete()
|
||||
JournalEntry.query.delete()
|
||||
VoucherLineItem.query.delete()
|
||||
|
||||
self.client, self.csrf_token = get_client(self.app, "editor")
|
||||
|
||||
|
@ -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, \
|
||||
JournalEntry
|
||||
VoucherLineItem
|
||||
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()
|
||||
JournalEntry.query.delete()
|
||||
VoucherLineItem.query.delete()
|
||||
|
||||
self.client, self.csrf_token = get_client(self.app, "editor")
|
||||
|
||||
@ -231,7 +231,7 @@ class CashReceiptVoucherTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# A receivable entry cannot start from the credit side
|
||||
# A receivable line item cannot start from the credit side
|
||||
form = self.__get_add_form()
|
||||
key: str = [x for x in form.keys()
|
||||
if x.endswith("-account_code") and "-credit-" in x][0]
|
||||
@ -391,7 +391,7 @@ class CashReceiptVoucherTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# A receivable entry cannot start from the credit side
|
||||
# A receivable line item cannot start from the credit side
|
||||
form = self.__get_add_form()
|
||||
key: str = [x for x in form.keys()
|
||||
if x.endswith("-account_code") and "-credit-" in x][0]
|
||||
@ -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, \
|
||||
JournalEntry
|
||||
VoucherLineItem
|
||||
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()
|
||||
JournalEntry.query.delete()
|
||||
VoucherLineItem.query.delete()
|
||||
|
||||
self.client, self.csrf_token = get_client(self.app, "editor")
|
||||
|
||||
@ -803,7 +803,7 @@ class CashDisbursementVoucherTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# A payable entry cannot start from the debit side
|
||||
# A payable line item cannot start from the debit side
|
||||
form = self.__get_add_form()
|
||||
key: str = [x for x in form.keys()
|
||||
if x.endswith("-account_code") and "-debit-" in x][0]
|
||||
@ -966,7 +966,7 @@ class CashDisbursementVoucherTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# A payable entry cannot start from the debit side
|
||||
# A payable line item cannot start from the debit side
|
||||
form = self.__get_add_form()
|
||||
key: str = [x for x in form.keys()
|
||||
if x.endswith("-account_code") and "-debit-" in x][0]
|
||||
@ -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, \
|
||||
JournalEntry
|
||||
VoucherLineItem
|
||||
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()
|
||||
JournalEntry.query.delete()
|
||||
VoucherLineItem.query.delete()
|
||||
|
||||
self.client, self.csrf_token = get_client(self.app, "editor")
|
||||
|
||||
@ -1398,7 +1398,7 @@ class TransferVoucherTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# A receivable entry cannot start from the credit side
|
||||
# A receivable line item cannot start from the credit side
|
||||
form = self.__get_add_form()
|
||||
key: str = [x for x in form.keys()
|
||||
if x.endswith("-account_code") and "-credit-" in x][0]
|
||||
@ -1407,7 +1407,7 @@ class TransferVoucherTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# A payable entry cannot start from the debit side
|
||||
# A payable line item cannot start from the debit side
|
||||
form = self.__get_add_form()
|
||||
key: str = [x for x in form.keys()
|
||||
if x.endswith("-account_code") and "-debit-" in x][0]
|
||||
@ -1597,7 +1597,7 @@ class TransferVoucherTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# A receivable entry cannot start from the credit side
|
||||
# A receivable line item cannot start from the credit side
|
||||
form = self.__get_add_form()
|
||||
key: str = [x for x in form.keys()
|
||||
if x.endswith("-account_code") and "-credit-" in x][0]
|
||||
@ -1606,7 +1606,7 @@ class TransferVoucherTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# A payable entry cannot start from the debit side
|
||||
# A payable line item cannot start from the debit side
|
||||
form = self.__get_add_form()
|
||||
key: str = [x for x in form.keys()
|
||||
if x.endswith("-account_code") and "-debit-" in x][0]
|
||||
@ -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, \
|
||||
JournalEntry
|
||||
VoucherLineItem
|
||||
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()
|
||||
JournalEntry.query.delete()
|
||||
VoucherLineItem.query.delete()
|
||||
|
||||
self.client, self.csrf_token = get_client(self.app, "editor")
|
||||
|
||||
|
@ -29,63 +29,65 @@ from test_site import db
|
||||
from testlib_voucher import Accounts, match_voucher_detail, NEXT_URI
|
||||
|
||||
|
||||
class JournalEntryData:
|
||||
"""The journal entry data."""
|
||||
class VoucherLineItemData:
|
||||
"""The voucher line item data."""
|
||||
|
||||
def __init__(self, account: str, summary: str, amount: str,
|
||||
original_entry: JournalEntryData | None = None):
|
||||
"""Constructs the journal entry data.
|
||||
original_line_item: VoucherLineItemData | None = None):
|
||||
"""Constructs the voucher line item data.
|
||||
|
||||
:param account: The account code.
|
||||
:param summary: The summary.
|
||||
:param amount: The amount.
|
||||
:param original_entry: The original entry.
|
||||
:param original_line_item: The original voucher line item.
|
||||
"""
|
||||
self.voucher: VoucherData | None = None
|
||||
self.id: int = -1
|
||||
self.no: int = -1
|
||||
self.original_entry: JournalEntryData | None = original_entry
|
||||
self.original_line_item: VoucherLineItemData | None \
|
||||
= original_line_item
|
||||
self.account: str = account
|
||||
self.summary: str = summary
|
||||
self.amount: Decimal = Decimal(amount)
|
||||
|
||||
def form(self, prefix: str, entry_type: str, index: int, is_update: bool) \
|
||||
def form(self, prefix: str, side: str, index: int, is_update: bool) \
|
||||
-> dict[str, str]:
|
||||
"""Returns the journal entry as form data.
|
||||
"""Returns the line item as form data.
|
||||
|
||||
:param prefix: The prefix of the form fields.
|
||||
:param entry_type: The entry type, either "debit" or "credit".
|
||||
:param index: The entry index.
|
||||
:param side: The side, either "debit" or "credit".
|
||||
:param index: The line item index.
|
||||
:param is_update: True for an update operation, or False otherwise
|
||||
:return: The form data.
|
||||
"""
|
||||
prefix = f"{prefix}-{entry_type}-{index}"
|
||||
prefix = f"{prefix}-{side}-{index}"
|
||||
form: dict[str, str] = {f"{prefix}-account_code": self.account,
|
||||
f"{prefix}-summary": self.summary,
|
||||
f"{prefix}-amount": str(self.amount)}
|
||||
if is_update and self.id != -1:
|
||||
form[f"{prefix}-eid"] = str(self.id)
|
||||
form[f"{prefix}-no"] = str(index) if self.no == -1 else str(self.no)
|
||||
if self.original_entry is not None:
|
||||
assert self.original_entry.id != -1
|
||||
form[f"{prefix}-original_entry_id"] = str(self.original_entry.id)
|
||||
if self.original_line_item is not None:
|
||||
assert self.original_line_item.id != -1
|
||||
form[f"{prefix}-original_line_item_id"] \
|
||||
= str(self.original_line_item.id)
|
||||
return form
|
||||
|
||||
|
||||
class CurrencyData:
|
||||
"""The voucher currency data."""
|
||||
|
||||
def __init__(self, currency: str, debit: list[JournalEntryData],
|
||||
credit: list[JournalEntryData]):
|
||||
def __init__(self, currency: str, debit: list[VoucherLineItemData],
|
||||
credit: list[VoucherLineItemData]):
|
||||
"""Constructs the voucher currency data.
|
||||
|
||||
:param currency: The currency code.
|
||||
:param debit: The debit journal entries.
|
||||
:param credit: The credit journal entries.
|
||||
:param debit: The debit line items.
|
||||
:param credit: The credit line items.
|
||||
"""
|
||||
self.code: str = currency
|
||||
self.debit: list[JournalEntryData] = debit
|
||||
self.credit: list[JournalEntryData] = credit
|
||||
self.debit: list[VoucherLineItemData] = debit
|
||||
self.credit: list[VoucherLineItemData] = credit
|
||||
|
||||
def form(self, index: int, is_update: bool) -> dict[str, str]:
|
||||
"""Returns the currency as form data.
|
||||
@ -118,10 +120,10 @@ class VoucherData:
|
||||
self.currencies: list[CurrencyData] = currencies
|
||||
self.note: str | None = None
|
||||
for currency in self.currencies:
|
||||
for entry in currency.debit:
|
||||
entry.voucher = self
|
||||
for entry in currency.credit:
|
||||
entry.voucher = self
|
||||
for line_item in currency.debit:
|
||||
line_item.voucher = self
|
||||
for line_item in currency.credit:
|
||||
line_item.voucher = self
|
||||
|
||||
def new_form(self, csrf_token: str) -> dict[str, str]:
|
||||
"""Returns the voucher as a creation form.
|
||||
@ -173,19 +175,19 @@ class TestData:
|
||||
self.csrf_token: str = csrf_token
|
||||
|
||||
def couple(summary: str, amount: str, debit: str, credit: str) \
|
||||
-> tuple[JournalEntryData, JournalEntryData]:
|
||||
"""Returns a couple of debit-credit journal entries.
|
||||
-> tuple[VoucherLineItemData, VoucherLineItemData]:
|
||||
"""Returns a couple of debit-credit line items.
|
||||
|
||||
:param summary: The summary.
|
||||
:param amount: The amount.
|
||||
:param debit: The debit account code.
|
||||
:param credit: The credit account code.
|
||||
:return: The debit journal entry and credit journal entry.
|
||||
:return: The debit line item and credit line item.
|
||||
"""
|
||||
return JournalEntryData(debit, summary, amount),\
|
||||
JournalEntryData(credit, summary, amount)
|
||||
return VoucherLineItemData(debit, summary, amount),\
|
||||
VoucherLineItemData(credit, summary, amount)
|
||||
|
||||
# Receivable original entries
|
||||
# Receivable original line items
|
||||
self.e_r_or1d, self.e_r_or1c = couple(
|
||||
"Accountant", "1200", Accounts.RECEIVABLE, Accounts.SERVICE)
|
||||
self.e_r_or2d, self.e_r_or2c = couple(
|
||||
@ -195,7 +197,7 @@ class TestData:
|
||||
self.e_r_or4d, self.e_r_or4c = couple(
|
||||
"Interest", "3.4", Accounts.RECEIVABLE, Accounts.INTEREST)
|
||||
|
||||
# Payable original entries
|
||||
# Payable original line items
|
||||
self.e_p_or1d, self.e_p_or1c = couple(
|
||||
"Airplane", "2000", Accounts.TRAVEL, Accounts.PAYABLE)
|
||||
self.e_p_or2d, self.e_p_or2c = couple(
|
||||
@ -224,39 +226,39 @@ class TestData:
|
||||
self.__add_voucher(self.v_p_or1)
|
||||
self.__add_voucher(self.v_p_or2)
|
||||
|
||||
# Receivable offset entries
|
||||
# Receivable offset items
|
||||
self.e_r_of1d, self.e_r_of1c = couple(
|
||||
"Accountant", "500", Accounts.CASH, Accounts.RECEIVABLE)
|
||||
self.e_r_of1c.original_entry = self.e_r_or1d
|
||||
self.e_r_of1c.original_line_item = self.e_r_or1d
|
||||
self.e_r_of2d, self.e_r_of2c = couple(
|
||||
"Accountant", "200", Accounts.CASH, Accounts.RECEIVABLE)
|
||||
self.e_r_of2c.original_entry = self.e_r_or1d
|
||||
self.e_r_of2c.original_line_item = self.e_r_or1d
|
||||
self.e_r_of3d, self.e_r_of3c = couple(
|
||||
"Accountant", "100", Accounts.CASH, Accounts.RECEIVABLE)
|
||||
self.e_r_of3c.original_entry = self.e_r_or1d
|
||||
self.e_r_of3c.original_line_item = self.e_r_or1d
|
||||
self.e_r_of4d, self.e_r_of4c = couple(
|
||||
"Toy", "240", Accounts.CASH, Accounts.RECEIVABLE)
|
||||
self.e_r_of4c.original_entry = self.e_r_or2d
|
||||
self.e_r_of4c.original_line_item = self.e_r_or2d
|
||||
self.e_r_of5d, self.e_r_of5c = couple(
|
||||
"Interest", "3.4", Accounts.CASH, Accounts.RECEIVABLE)
|
||||
self.e_r_of5c.original_entry = self.e_r_or4d
|
||||
self.e_r_of5c.original_line_item = self.e_r_or4d
|
||||
|
||||
# Payable offset entries
|
||||
# Payable offset items
|
||||
self.e_p_of1d, self.e_p_of1c = couple(
|
||||
"Airplane", "800", Accounts.PAYABLE, Accounts.CASH)
|
||||
self.e_p_of1d.original_entry = self.e_p_or1c
|
||||
self.e_p_of1d.original_line_item = self.e_p_or1c
|
||||
self.e_p_of2d, self.e_p_of2c = couple(
|
||||
"Airplane", "300", Accounts.PAYABLE, Accounts.CASH)
|
||||
self.e_p_of2d.original_entry = self.e_p_or1c
|
||||
self.e_p_of2d.original_line_item = self.e_p_or1c
|
||||
self.e_p_of3d, self.e_p_of3c = couple(
|
||||
"Airplane", "100", Accounts.PAYABLE, Accounts.CASH)
|
||||
self.e_p_of3d.original_entry = self.e_p_or1c
|
||||
self.e_p_of3d.original_line_item = self.e_p_or1c
|
||||
self.e_p_of4d, self.e_p_of4c = couple(
|
||||
"Phone", "400", Accounts.PAYABLE, Accounts.CASH)
|
||||
self.e_p_of4d.original_entry = self.e_p_or2c
|
||||
self.e_p_of4d.original_line_item = self.e_p_or2c
|
||||
self.e_p_of5d, self.e_p_of5c = couple(
|
||||
"Envelop", "0.9", Accounts.PAYABLE, Accounts.CASH)
|
||||
self.e_p_of5d.original_entry = self.e_p_or4c
|
||||
self.e_p_of5d.original_line_item = self.e_p_or4c
|
||||
|
||||
# Offset vouchers
|
||||
self.v_r_of1: VoucherData = VoucherData(
|
||||
|
@ -154,35 +154,36 @@ def get_unchanged_update_form(voucher_id: int, app: Flask, csrf_token: str) \
|
||||
currency_prefix: str = f"currency-{currency_index}"
|
||||
form[f"{currency_prefix}-no"] = str(currency_no)
|
||||
form[f"{currency_prefix}-code"] = currency.code
|
||||
entry_indices_used: set[int]
|
||||
entry_no: int
|
||||
line_item_indices_used: set[int]
|
||||
line_item_no: int
|
||||
prefix: str
|
||||
|
||||
entry_indices_used = set()
|
||||
entry_no = 0
|
||||
for entry in currency.debit:
|
||||
entry_index: int = __get_new_index(entry_indices_used)
|
||||
entry_no = entry_no + 3 + randbelow(3)
|
||||
prefix = f"{currency_prefix}-debit-{entry_index}"
|
||||
form[f"{prefix}-eid"] = str(entry.id)
|
||||
form[f"{prefix}-no"] = str(entry_no)
|
||||
form[f"{prefix}-account_code"] = entry.account.code
|
||||
line_item_indices_used = set()
|
||||
line_item_no = 0
|
||||
for line_item in currency.debit:
|
||||
line_item_index: int = __get_new_index(line_item_indices_used)
|
||||
line_item_no = line_item_no + 3 + randbelow(3)
|
||||
prefix = f"{currency_prefix}-debit-{line_item_index}"
|
||||
form[f"{prefix}-eid"] = str(line_item.id)
|
||||
form[f"{prefix}-no"] = str(line_item_no)
|
||||
form[f"{prefix}-account_code"] = line_item.account.code
|
||||
form[f"{prefix}-summary"] \
|
||||
= " " if entry.summary is None else f" {entry.summary} "
|
||||
form[f"{prefix}-amount"] = str(entry.amount)
|
||||
= " " if line_item.summary is None \
|
||||
else f" {line_item.summary} "
|
||||
form[f"{prefix}-amount"] = str(line_item.amount)
|
||||
|
||||
entry_indices_used = set()
|
||||
entry_no = 0
|
||||
for entry in currency.credit:
|
||||
entry_index: int = __get_new_index(entry_indices_used)
|
||||
entry_no = entry_no + 3 + randbelow(3)
|
||||
prefix = f"{currency_prefix}-credit-{entry_index}"
|
||||
form[f"{prefix}-eid"] = str(entry.id)
|
||||
form[f"{prefix}-no"] = str(entry_no)
|
||||
form[f"{prefix}-account_code"] = entry.account.code
|
||||
line_item_indices_used = set()
|
||||
line_item_no = 0
|
||||
for line_item in currency.credit:
|
||||
line_item_index: int = __get_new_index(line_item_indices_used)
|
||||
line_item_no = line_item_no + 3 + randbelow(3)
|
||||
prefix = f"{currency_prefix}-credit-{line_item_index}"
|
||||
form[f"{prefix}-eid"] = str(line_item.id)
|
||||
form[f"{prefix}-no"] = str(line_item_no)
|
||||
form[f"{prefix}-account_code"] = line_item.account.code
|
||||
form[f"{prefix}-summary"] \
|
||||
= " " if entry.summary is None else f" {entry.summary} "
|
||||
form[f"{prefix}-amount"] = str(entry.amount)
|
||||
= " " if line_item.summary is None else f" {line_item.summary} "
|
||||
form[f"{prefix}-amount"] = str(line_item.amount)
|
||||
|
||||
return form
|
||||
|
||||
@ -216,7 +217,7 @@ def get_update_form(voucher_id: int, app: Flask,
|
||||
form: dict[str, str] = get_unchanged_update_form(
|
||||
voucher_id, app, csrf_token)
|
||||
|
||||
# Mess up the entries in a currency
|
||||
# Mess up the line items in a currency
|
||||
currency_prefix: str = __get_currency_prefix(form, "USD")
|
||||
if is_debit is None or is_debit:
|
||||
form = __mess_up_debit(form, currency_prefix)
|
||||
@ -231,7 +232,7 @@ def get_update_form(voucher_id: int, app: Flask,
|
||||
|
||||
def __mess_up_debit(form: dict[str, str], currency_prefix: str) \
|
||||
-> dict[str, str]:
|
||||
"""Mess up the debit journal entries in the form data.
|
||||
"""Mess up the debit line items in the form data.
|
||||
|
||||
:param form: The form data.
|
||||
:param currency_prefix: The key prefix of the currency sub-form.
|
||||
@ -246,9 +247,9 @@ def __mess_up_debit(form: dict[str, str], currency_prefix: str) \
|
||||
and form[x] == Accounts.OFFICE][0]
|
||||
m = re.match(r"^((.+-)\d+-)account_code$", key)
|
||||
debit_prefix: str = m.group(2)
|
||||
entry_prefix: str = m.group(1)
|
||||
amount: Decimal = Decimal(form[f"{entry_prefix}amount"])
|
||||
form = {x: form[x] for x in form if not x.startswith(entry_prefix)}
|
||||
line_item_prefix: str = m.group(1)
|
||||
amount: Decimal = Decimal(form[f"{line_item_prefix}amount"])
|
||||
form = {x: form[x] for x in form if not x.startswith(line_item_prefix)}
|
||||
# Add a new travel disbursement
|
||||
indices: set[int] = set()
|
||||
for key in form:
|
||||
@ -262,15 +263,15 @@ def __mess_up_debit(form: dict[str, str], currency_prefix: str) \
|
||||
form[f"{debit_prefix}{new_index}-amount"] = str(amount)
|
||||
form[f"{debit_prefix}{new_index}-account_code"] = Accounts.TRAVEL
|
||||
# Swap the cash and the bank order
|
||||
key_cash: str = __get_entry_no_key(form, currency_prefix, Accounts.CASH)
|
||||
key_bank: str = __get_entry_no_key(form, currency_prefix, Accounts.BANK)
|
||||
key_cash: str = __get_line_item_no_key(form, currency_prefix, Accounts.CASH)
|
||||
key_bank: str = __get_line_item_no_key(form, currency_prefix, Accounts.BANK)
|
||||
form[key_cash], form[key_bank] = form[key_bank], form[key_cash]
|
||||
return form
|
||||
|
||||
|
||||
def __mess_up_credit(form: dict[str, str], currency_prefix: str) \
|
||||
-> dict[str, str]:
|
||||
"""Mess up the credit journal entries in the form data.
|
||||
"""Mess up the credit line items in the form data.
|
||||
|
||||
:param form: The form data.
|
||||
:param currency_prefix: The key prefix of the currency sub-form.
|
||||
@ -285,9 +286,9 @@ def __mess_up_credit(form: dict[str, str], currency_prefix: str) \
|
||||
and form[x] == Accounts.SALES][0]
|
||||
m = re.match(r"^((.+-)\d+-)account_code$", key)
|
||||
credit_prefix: str = m.group(2)
|
||||
entry_prefix: str = m.group(1)
|
||||
amount: Decimal = Decimal(form[f"{entry_prefix}amount"])
|
||||
form = {x: form[x] for x in form if not x.startswith(entry_prefix)}
|
||||
line_item_prefix: str = m.group(1)
|
||||
amount: Decimal = Decimal(form[f"{line_item_prefix}amount"])
|
||||
form = {x: form[x] for x in form if not x.startswith(line_item_prefix)}
|
||||
# Add a new agency receipt
|
||||
indices: set[int] = set()
|
||||
for key in form:
|
||||
@ -301,8 +302,8 @@ def __mess_up_credit(form: dict[str, str], currency_prefix: str) \
|
||||
form[f"{credit_prefix}{new_index}-amount"] = str(amount)
|
||||
form[f"{credit_prefix}{new_index}-account_code"] = Accounts.AGENCY
|
||||
# Swap the service and the interest order
|
||||
key_srv: str = __get_entry_no_key(form, currency_prefix, Accounts.SERVICE)
|
||||
key_int: str = __get_entry_no_key(form, currency_prefix, Accounts.INTEREST)
|
||||
key_srv: str = __get_line_item_no_key(form, currency_prefix, Accounts.SERVICE)
|
||||
key_int: str = __get_line_item_no_key(form, currency_prefix, Accounts.INTEREST)
|
||||
form[key_srv], form[key_int] = form[key_int], form[key_srv]
|
||||
return form
|
||||
|
||||
@ -361,14 +362,14 @@ def __mess_up_currencies(form: dict[str, str]) -> dict[str, str]:
|
||||
return form
|
||||
|
||||
|
||||
def __get_entry_no_key(form: dict[str, str], currency_prefix: str,
|
||||
code: str) -> str:
|
||||
"""Returns the key of an entry number in the form data.
|
||||
def __get_line_item_no_key(form: dict[str, str], currency_prefix: str,
|
||||
code: str) -> str:
|
||||
"""Returns the key of a line item number in the form data.
|
||||
|
||||
:param form: The form data.
|
||||
:param currency_prefix: The prefix of the currency.
|
||||
:param code: The code of the account.
|
||||
:return: The key of the entry number in the form data.
|
||||
:return: The key of the line item number in the form data.
|
||||
"""
|
||||
key: str = [x for x in form
|
||||
if x.startswith(currency_prefix)
|
||||
@ -444,7 +445,7 @@ def set_negative_amount(form: dict[str, str]) -> None:
|
||||
|
||||
|
||||
def remove_debit_in_a_currency(form: dict[str, str]) -> None:
|
||||
"""Removes credit entries in a currency sub-form.
|
||||
"""Removes debit line items in a currency sub-form.
|
||||
|
||||
:param form: The form data.
|
||||
:return: None.
|
||||
@ -457,7 +458,7 @@ def remove_debit_in_a_currency(form: dict[str, str]) -> None:
|
||||
|
||||
|
||||
def remove_credit_in_a_currency(form: dict[str, str]) -> None:
|
||||
"""Removes credit entries in a currency sub-form.
|
||||
"""Removes credit line items in a currency sub-form.
|
||||
|
||||
:param form: The form data.
|
||||
:return: None.
|
||||
|
Loading…
Reference in New Issue
Block a user