Renamed "transaction" to "voucher", "cash expense transaction" to "cash disbursement voucher", and "cash income transaction" to "cash receipt voucher".
This commit is contained in:
		@@ -80,8 +80,8 @@ def init_app(app: Flask, user_utils: UserUtilityInterface,
 | 
			
		||||
    from . import currency
 | 
			
		||||
    currency.init_app(app, bp)
 | 
			
		||||
 | 
			
		||||
    from . import transaction
 | 
			
		||||
    transaction.init_app(app, bp)
 | 
			
		||||
    from . import voucher
 | 
			
		||||
    voucher.init_app(app, bp)
 | 
			
		||||
 | 
			
		||||
    from . import report
 | 
			
		||||
    report.init_app(app, bp)
 | 
			
		||||
 
 | 
			
		||||
@@ -447,12 +447,12 @@ class CurrencyL10n(db.Model):
 | 
			
		||||
    """The localized name."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionCurrency:
 | 
			
		||||
    """A currency in a transaction."""
 | 
			
		||||
class VoucherCurrency:
 | 
			
		||||
    """A currency in a voucher."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, code: str, debit: list[JournalEntry],
 | 
			
		||||
                 credit: list[JournalEntry]):
 | 
			
		||||
        """Constructs the currency in the transaction.
 | 
			
		||||
        """Constructs the currency in the voucher.
 | 
			
		||||
 | 
			
		||||
        :param code: The currency code.
 | 
			
		||||
        :param debit: The debit entries.
 | 
			
		||||
@@ -490,13 +490,13 @@ class TransactionCurrency:
 | 
			
		||||
        return sum([x.amount for x in self.credit])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Transaction(db.Model):
 | 
			
		||||
    """A transaction."""
 | 
			
		||||
    __tablename__ = "accounting_transactions"
 | 
			
		||||
class Voucher(db.Model):
 | 
			
		||||
    """A voucher."""
 | 
			
		||||
    __tablename__ = "accounting_vouchers"
 | 
			
		||||
    """The table name."""
 | 
			
		||||
    id = db.Column(db.Integer, nullable=False, primary_key=True,
 | 
			
		||||
                   autoincrement=False)
 | 
			
		||||
    """The transaction ID."""
 | 
			
		||||
    """The voucher ID."""
 | 
			
		||||
    date = db.Column(db.Date, nullable=False)
 | 
			
		||||
    """The date."""
 | 
			
		||||
    no = db.Column(db.Integer, nullable=False, default=text("1"))
 | 
			
		||||
@@ -523,22 +523,22 @@ class Transaction(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="transaction")
 | 
			
		||||
    entries = db.relationship("JournalEntry", back_populates="voucher")
 | 
			
		||||
    """The journal entries."""
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        """Returns the string representation of this transaction.
 | 
			
		||||
        """Returns the string representation of this voucher.
 | 
			
		||||
 | 
			
		||||
        :return: The string representation of this transaction.
 | 
			
		||||
        :return: The string representation of this voucher.
 | 
			
		||||
        """
 | 
			
		||||
        if self.is_cash_expense:
 | 
			
		||||
            return gettext("Cash Expense Transaction#%(id)s", id=self.id)
 | 
			
		||||
        if self.is_cash_income:
 | 
			
		||||
            return gettext("Cash Income Transaction#%(id)s", id=self.id)
 | 
			
		||||
        return gettext("Transfer Transaction#%(id)s", id=self.id)
 | 
			
		||||
        if self.is_cash_disbursement:
 | 
			
		||||
            return gettext("Cash Disbursement Voucher#%(id)s", id=self.id)
 | 
			
		||||
        if self.is_cash_receipt:
 | 
			
		||||
            return gettext("Cash Receipt Voucher#%(id)s", id=self.id)
 | 
			
		||||
        return gettext("Transfer Voucher#%(id)s", id=self.id)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def currencies(self) -> list[TransactionCurrency]:
 | 
			
		||||
    def currencies(self) -> list[VoucherCurrency]:
 | 
			
		||||
        """Returns the journal entries categorized by their currencies.
 | 
			
		||||
 | 
			
		||||
        :return: The currency categories.
 | 
			
		||||
@@ -551,7 +551,7 @@ class Transaction(db.Model):
 | 
			
		||||
                codes.append(entry.currency_code)
 | 
			
		||||
                by_currency[entry.currency_code] = []
 | 
			
		||||
            by_currency[entry.currency_code].append(entry)
 | 
			
		||||
        return [TransactionCurrency(code=x,
 | 
			
		||||
        return [VoucherCurrency(code=x,
 | 
			
		||||
                                debit=[y for y in by_currency[x]
 | 
			
		||||
                                       if y.is_debit],
 | 
			
		||||
                                credit=[y for y in by_currency[x]
 | 
			
		||||
@@ -559,10 +559,10 @@ class Transaction(db.Model):
 | 
			
		||||
                for x in codes]
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_cash_income(self) -> bool:
 | 
			
		||||
        """Returns whether this is a cash income transaction.
 | 
			
		||||
    def is_cash_receipt(self) -> bool:
 | 
			
		||||
        """Returns whether this is a cash receipt voucher.
 | 
			
		||||
 | 
			
		||||
        :return: True if this is a cash income transaction, or False otherwise.
 | 
			
		||||
        :return: True if this is a cash receipt voucher, or False otherwise.
 | 
			
		||||
        """
 | 
			
		||||
        for currency in self.currencies:
 | 
			
		||||
            if len(currency.debit) > 1:
 | 
			
		||||
@@ -572,10 +572,10 @@ class Transaction(db.Model):
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_cash_expense(self) -> bool:
 | 
			
		||||
        """Returns whether this is a cash expense transaction.
 | 
			
		||||
    def is_cash_disbursement(self) -> bool:
 | 
			
		||||
        """Returns whether this is a cash disbursement voucher.
 | 
			
		||||
 | 
			
		||||
        :return: True if this is a cash expense transaction, or False
 | 
			
		||||
        :return: True if this is a cash disbursement voucher, or False
 | 
			
		||||
            otherwise.
 | 
			
		||||
        """
 | 
			
		||||
        for currency in self.currencies:
 | 
			
		||||
@@ -587,9 +587,9 @@ class Transaction(db.Model):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def can_delete(self) -> bool:
 | 
			
		||||
        """Returns whether the transaction can be deleted.
 | 
			
		||||
        """Returns whether the voucher can be deleted.
 | 
			
		||||
 | 
			
		||||
        :return: True if the transaction can be deleted, or False otherwise.
 | 
			
		||||
        :return: True if the voucher can be deleted, or False otherwise.
 | 
			
		||||
        """
 | 
			
		||||
        if not hasattr(self, "__can_delete"):
 | 
			
		||||
            def has_offset() -> bool:
 | 
			
		||||
@@ -601,12 +601,12 @@ class Transaction(db.Model):
 | 
			
		||||
        return getattr(self, "__can_delete")
 | 
			
		||||
 | 
			
		||||
    def delete(self) -> None:
 | 
			
		||||
        """Deletes the transaction.
 | 
			
		||||
        """Deletes the voucher.
 | 
			
		||||
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        JournalEntry.query\
 | 
			
		||||
            .filter(JournalEntry.transaction_id == self.id).delete()
 | 
			
		||||
            .filter(JournalEntry.voucher_id == self.id).delete()
 | 
			
		||||
        db.session.delete(self)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -617,18 +617,17 @@ class JournalEntry(db.Model):
 | 
			
		||||
    id = db.Column(db.Integer, nullable=False, primary_key=True,
 | 
			
		||||
                   autoincrement=False)
 | 
			
		||||
    """The entry ID."""
 | 
			
		||||
    transaction_id = db.Column(db.Integer,
 | 
			
		||||
                               db.ForeignKey(Transaction.id,
 | 
			
		||||
                                             onupdate="CASCADE",
 | 
			
		||||
    voucher_id = db.Column(db.Integer,
 | 
			
		||||
                           db.ForeignKey(Voucher.id, onupdate="CASCADE",
 | 
			
		||||
                                         ondelete="CASCADE"),
 | 
			
		||||
                           nullable=False)
 | 
			
		||||
    """The transaction ID."""
 | 
			
		||||
    transaction = db.relationship(Transaction, back_populates="entries")
 | 
			
		||||
    """The transaction."""
 | 
			
		||||
    """The voucher ID."""
 | 
			
		||||
    voucher = db.relationship(Voucher, back_populates="entries")
 | 
			
		||||
    """The voucher."""
 | 
			
		||||
    is_debit = db.Column(db.Boolean, nullable=False)
 | 
			
		||||
    """True for a debit entry, or False for a credit entry."""
 | 
			
		||||
    no = db.Column(db.Integer, nullable=False)
 | 
			
		||||
    """The entry number under the transaction and debit or credit."""
 | 
			
		||||
    """The entry number under the voucher and debit or credit."""
 | 
			
		||||
    original_entry_id = db.Column(db.Integer,
 | 
			
		||||
                                  db.ForeignKey(id, onupdate="CASCADE"),
 | 
			
		||||
                                  nullable=True)
 | 
			
		||||
@@ -665,7 +664,7 @@ class JournalEntry(db.Model):
 | 
			
		||||
            from accounting.template_filters import format_date, format_amount
 | 
			
		||||
            setattr(self, "__str",
 | 
			
		||||
                    gettext("%(date)s %(summary)s %(amount)s",
 | 
			
		||||
                            date=format_date(self.transaction.date),
 | 
			
		||||
                            date=format_date(self.voucher.date),
 | 
			
		||||
                            summary="" if self.summary is None
 | 
			
		||||
                            else self.summary,
 | 
			
		||||
                            amount=format_amount(self.amount)))
 | 
			
		||||
@@ -750,12 +749,13 @@ class JournalEntry(db.Model):
 | 
			
		||||
            frac: Decimal = (value - whole).normalize()
 | 
			
		||||
            return str(whole) + str(abs(frac))[1:]
 | 
			
		||||
 | 
			
		||||
        txn_day: date = self.transaction.date
 | 
			
		||||
        voucher_day: date = self.voucher.date
 | 
			
		||||
        summary: str = "" if self.summary is None else self.summary
 | 
			
		||||
        return ([summary],
 | 
			
		||||
                [str(txn_day.year),
 | 
			
		||||
                 "{}/{}".format(txn_day.year, txn_day.month),
 | 
			
		||||
                 "{}/{}".format(txn_day.month, txn_day.day),
 | 
			
		||||
                 "{}/{}/{}".format(txn_day.year, txn_day.month, txn_day.day),
 | 
			
		||||
                [str(voucher_day.year),
 | 
			
		||||
                 "{}/{}".format(voucher_day.year, voucher_day.month),
 | 
			
		||||
                 "{}/{}".format(voucher_day.month, voucher_day.day),
 | 
			
		||||
                 "{}/{}/{}".format(voucher_day.year, voucher_day.month,
 | 
			
		||||
                                   voucher_day.day),
 | 
			
		||||
                 format_amount(self.amount),
 | 
			
		||||
                 format_amount(self.net_balance)])
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ This file is largely taken from the NanoParma ERP project, first written in
 | 
			
		||||
import typing as t
 | 
			
		||||
from datetime import date
 | 
			
		||||
 | 
			
		||||
from accounting.models import Transaction
 | 
			
		||||
from accounting.models import Voucher
 | 
			
		||||
from .period import Period
 | 
			
		||||
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
 | 
			
		||||
    LastYear, Today, Yesterday, AllTime, TemplatePeriod, YearPeriod
 | 
			
		||||
@@ -61,8 +61,8 @@ class PeriodChooser:
 | 
			
		||||
        self.url_template: str = get_url(TemplatePeriod())
 | 
			
		||||
        """The URL template."""
 | 
			
		||||
 | 
			
		||||
        first: Transaction | None \
 | 
			
		||||
            = Transaction.query.order_by(Transaction.date).first()
 | 
			
		||||
        first: Voucher | None \
 | 
			
		||||
            = Voucher.query.order_by(Voucher.date).first()
 | 
			
		||||
        start: date | None = None if first is None else first.date
 | 
			
		||||
 | 
			
		||||
        # Attributes
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ from flask import render_template, Response
 | 
			
		||||
 | 
			
		||||
from accounting import db
 | 
			
		||||
from accounting.locale import gettext
 | 
			
		||||
from accounting.models import Currency, BaseAccount, Account, Transaction, \
 | 
			
		||||
from accounting.models import Currency, BaseAccount, Account, Voucher, \
 | 
			
		||||
    JournalEntry
 | 
			
		||||
from accounting.report.period import Period, PeriodChooser
 | 
			
		||||
from accounting.report.utils.base_page_params import BasePageParams
 | 
			
		||||
@@ -127,14 +127,14 @@ class AccountCollector:
 | 
			
		||||
            = [JournalEntry.currency_code == self.__currency.code,
 | 
			
		||||
               sa.or_(*sub_conditions)]
 | 
			
		||||
        if self.__period.end is not None:
 | 
			
		||||
            conditions.append(Transaction.date <= self.__period.end)
 | 
			
		||||
            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")
 | 
			
		||||
        select_balance: sa.Select \
 | 
			
		||||
            = sa.select(Account.id, Account.base_code, Account.no,
 | 
			
		||||
                        balance_func)\
 | 
			
		||||
            .join(Transaction).join(Account)\
 | 
			
		||||
            .join(Voucher).join(Account)\
 | 
			
		||||
            .filter(*conditions)\
 | 
			
		||||
            .group_by(Account.id, Account.base_code, Account.no)\
 | 
			
		||||
            .order_by(Account.base_code, Account.no)
 | 
			
		||||
@@ -179,7 +179,7 @@ class AccountCollector:
 | 
			
		||||
            return None
 | 
			
		||||
        conditions: list[sa.BinaryExpression] \
 | 
			
		||||
            = [JournalEntry.currency_code == self.__currency.code,
 | 
			
		||||
               Transaction.date < self.__period.start]
 | 
			
		||||
               Voucher.date < self.__period.start]
 | 
			
		||||
        return self.__query_balance(conditions)
 | 
			
		||||
 | 
			
		||||
    def __add_current_period(self) -> None:
 | 
			
		||||
@@ -199,9 +199,9 @@ class AccountCollector:
 | 
			
		||||
        conditions: list[sa.BinaryExpression] \
 | 
			
		||||
            = [JournalEntry.currency_code == self.__currency.code]
 | 
			
		||||
        if self.__period.start is not None:
 | 
			
		||||
            conditions.append(Transaction.date >= self.__period.start)
 | 
			
		||||
            conditions.append(Voucher.date >= self.__period.start)
 | 
			
		||||
        if self.__period.end is not None:
 | 
			
		||||
            conditions.append(Transaction.date <= self.__period.end)
 | 
			
		||||
            conditions.append(Voucher.date <= self.__period.end)
 | 
			
		||||
        return self.__query_balance(conditions)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
@@ -218,7 +218,7 @@ class AccountCollector:
 | 
			
		||||
            (JournalEntry.is_debit, JournalEntry.amount),
 | 
			
		||||
            else_=-JournalEntry.amount))
 | 
			
		||||
        select_balance: sa.Select = sa.select(balance_func)\
 | 
			
		||||
            .join(Transaction).join(Account).filter(*conditions)
 | 
			
		||||
            .join(Voucher).join(Account).filter(*conditions)
 | 
			
		||||
        return db.session.scalar(select_balance)
 | 
			
		||||
 | 
			
		||||
    def __add_owner_s_equity(self, code: str, amount: Decimal | None,
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
 | 
			
		||||
 | 
			
		||||
from accounting import db
 | 
			
		||||
from accounting.locale import gettext
 | 
			
		||||
from accounting.models import Currency, Account, Transaction, JournalEntry
 | 
			
		||||
from accounting.models import Currency, Account, Voucher, JournalEntry
 | 
			
		||||
from accounting.report.period import Period, PeriodChooser
 | 
			
		||||
from accounting.report.utils.base_page_params import BasePageParams
 | 
			
		||||
from accounting.report.utils.base_report import BaseReport
 | 
			
		||||
@@ -70,14 +70,14 @@ class ReportEntry:
 | 
			
		||||
        self.url: str | None = None
 | 
			
		||||
        """The URL to the journal entry."""
 | 
			
		||||
        if entry is not None:
 | 
			
		||||
            self.date = entry.transaction.date
 | 
			
		||||
            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.transaction.note
 | 
			
		||||
            self.url = url_for("accounting.transaction.detail",
 | 
			
		||||
                               txn=entry.transaction)
 | 
			
		||||
            self.note = entry.voucher.note
 | 
			
		||||
            self.url = url_for("accounting.voucher.detail",
 | 
			
		||||
                               voucher=entry.voucher)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EntryCollector:
 | 
			
		||||
@@ -120,10 +120,10 @@ class EntryCollector:
 | 
			
		||||
            (JournalEntry.is_debit, JournalEntry.amount),
 | 
			
		||||
            else_=-JournalEntry.amount))
 | 
			
		||||
        select: sa.Select = sa.Select(balance_func)\
 | 
			
		||||
            .join(Transaction).join(Account)\
 | 
			
		||||
            .join(Voucher).join(Account)\
 | 
			
		||||
            .filter(be(JournalEntry.currency_code == self.__currency.code),
 | 
			
		||||
                    self.__account_condition,
 | 
			
		||||
                    Transaction.date < self.__period.start)
 | 
			
		||||
                    Voucher.date < self.__period.start)
 | 
			
		||||
        balance: int | None = db.session.scalar(select)
 | 
			
		||||
        if balance is None:
 | 
			
		||||
            return None
 | 
			
		||||
@@ -148,23 +148,23 @@ class EntryCollector:
 | 
			
		||||
            = [JournalEntry.currency_code == self.__currency.code,
 | 
			
		||||
               self.__account_condition]
 | 
			
		||||
        if self.__period.start is not None:
 | 
			
		||||
            conditions.append(Transaction.date >= self.__period.start)
 | 
			
		||||
            conditions.append(Voucher.date >= self.__period.start)
 | 
			
		||||
        if self.__period.end is not None:
 | 
			
		||||
            conditions.append(Transaction.date <= self.__period.end)
 | 
			
		||||
        txn_with_account: sa.Select = sa.Select(Transaction.id).\
 | 
			
		||||
            conditions.append(Voucher.date <= self.__period.end)
 | 
			
		||||
        voucher_with_account: sa.Select = sa.Select(Voucher.id).\
 | 
			
		||||
            join(JournalEntry).join(Account).filter(*conditions)
 | 
			
		||||
 | 
			
		||||
        return [ReportEntry(x)
 | 
			
		||||
                for x in JournalEntry.query.join(Transaction).join(Account)
 | 
			
		||||
                .filter(JournalEntry.transaction_id.in_(txn_with_account),
 | 
			
		||||
                for x in JournalEntry.query.join(Voucher).join(Account)
 | 
			
		||||
                .filter(JournalEntry.voucher_id.in_(voucher_with_account),
 | 
			
		||||
                        JournalEntry.currency_code == self.__currency.code,
 | 
			
		||||
                        sa.not_(self.__account_condition))
 | 
			
		||||
                .order_by(Transaction.date,
 | 
			
		||||
                          Transaction.no,
 | 
			
		||||
                .order_by(Voucher.date,
 | 
			
		||||
                          Voucher.no,
 | 
			
		||||
                          JournalEntry.is_debit,
 | 
			
		||||
                          JournalEntry.no)
 | 
			
		||||
                .options(selectinload(JournalEntry.account),
 | 
			
		||||
                         selectinload(JournalEntry.transaction))]
 | 
			
		||||
                         selectinload(JournalEntry.voucher))]
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def __account_condition(self) -> sa.BinaryExpression:
 | 
			
		||||
@@ -212,7 +212,7 @@ class EntryCollector:
 | 
			
		||||
class CSVRow(BaseCSVRow):
 | 
			
		||||
    """A row in the CSV."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, txn_date: date | str | None,
 | 
			
		||||
    def __init__(self, voucher_date: date | str | None,
 | 
			
		||||
                 account: str | None,
 | 
			
		||||
                 summary: str | None,
 | 
			
		||||
                 income: str | Decimal | None,
 | 
			
		||||
@@ -221,7 +221,7 @@ class CSVRow(BaseCSVRow):
 | 
			
		||||
                 note: str | None):
 | 
			
		||||
        """Constructs a row in the CSV.
 | 
			
		||||
 | 
			
		||||
        :param txn_date: The transaction date.
 | 
			
		||||
        :param voucher_date: The voucher date.
 | 
			
		||||
        :param account: The account.
 | 
			
		||||
        :param summary: The summary.
 | 
			
		||||
        :param income: The income.
 | 
			
		||||
@@ -229,7 +229,7 @@ class CSVRow(BaseCSVRow):
 | 
			
		||||
        :param balance: The balance.
 | 
			
		||||
        :param note: The note.
 | 
			
		||||
        """
 | 
			
		||||
        self.date: date | str | None = txn_date
 | 
			
		||||
        self.date: date | str | None = voucher_date
 | 
			
		||||
        """The date."""
 | 
			
		||||
        self.account: str | None = account
 | 
			
		||||
        """The account."""
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ from flask import render_template, Response
 | 
			
		||||
 | 
			
		||||
from accounting import db
 | 
			
		||||
from accounting.locale import gettext
 | 
			
		||||
from accounting.models import Currency, BaseAccount, Account, Transaction, \
 | 
			
		||||
from accounting.models import Currency, BaseAccount, Account, Voucher, \
 | 
			
		||||
    JournalEntry
 | 
			
		||||
from accounting.report.period import Period, PeriodChooser
 | 
			
		||||
from accounting.report.utils.base_page_params import BasePageParams
 | 
			
		||||
@@ -259,14 +259,14 @@ class IncomeStatement(BaseReport):
 | 
			
		||||
            = [JournalEntry.currency_code == self.__currency.code,
 | 
			
		||||
               sa.or_(*sub_conditions)]
 | 
			
		||||
        if self.__period.start is not None:
 | 
			
		||||
            conditions.append(Transaction.date >= self.__period.start)
 | 
			
		||||
            conditions.append(Voucher.date >= self.__period.start)
 | 
			
		||||
        if self.__period.end is not None:
 | 
			
		||||
            conditions.append(Transaction.date <= self.__period.end)
 | 
			
		||||
            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")
 | 
			
		||||
        select_balances: sa.Select = sa.select(Account.id, balance_func)\
 | 
			
		||||
            .join(Transaction).join(Account)\
 | 
			
		||||
            .join(Voucher).join(Account)\
 | 
			
		||||
            .filter(*conditions)\
 | 
			
		||||
            .group_by(Account.id)\
 | 
			
		||||
            .order_by(Account.base_code, Account.no)
 | 
			
		||||
 
 | 
			
		||||
@@ -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, Transaction, JournalEntry
 | 
			
		||||
from accounting.models import Currency, Account, Voucher, JournalEntry
 | 
			
		||||
from accounting.report.period import Period, PeriodChooser
 | 
			
		||||
from accounting.report.utils.base_page_params import BasePageParams
 | 
			
		||||
from accounting.report.utils.base_report import BaseReport
 | 
			
		||||
@@ -47,8 +47,8 @@ class ReportEntry:
 | 
			
		||||
        """
 | 
			
		||||
        self.entry: JournalEntry = entry
 | 
			
		||||
        """The journal entry."""
 | 
			
		||||
        self.transaction: Transaction = entry.transaction
 | 
			
		||||
        """The transaction."""
 | 
			
		||||
        self.voucher: Voucher = entry.voucher
 | 
			
		||||
        """The voucher."""
 | 
			
		||||
        self.currency: Currency = entry.currency
 | 
			
		||||
        """The account."""
 | 
			
		||||
        self.account: Account = entry.account
 | 
			
		||||
@@ -66,7 +66,7 @@ class ReportEntry:
 | 
			
		||||
class CSVRow(BaseCSVRow):
 | 
			
		||||
    """A row in the CSV."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, txn_date: str | date,
 | 
			
		||||
    def __init__(self, voucher_date: str | date,
 | 
			
		||||
                 currency: str,
 | 
			
		||||
                 account: str,
 | 
			
		||||
                 summary: str | None,
 | 
			
		||||
@@ -75,13 +75,13 @@ class CSVRow(BaseCSVRow):
 | 
			
		||||
                 note: str | None):
 | 
			
		||||
        """Constructs a row in the CSV.
 | 
			
		||||
 | 
			
		||||
        :param txn_date: The transaction date.
 | 
			
		||||
        :param voucher_date: The voucher date.
 | 
			
		||||
        :param summary: The summary.
 | 
			
		||||
        :param debit: The debit amount.
 | 
			
		||||
        :param credit: The credit amount.
 | 
			
		||||
        :param note: The note.
 | 
			
		||||
        """
 | 
			
		||||
        self.date: str | date = txn_date
 | 
			
		||||
        self.date: str | date = voucher_date
 | 
			
		||||
        """The date."""
 | 
			
		||||
        self.currency: str = currency
 | 
			
		||||
        """The currency."""
 | 
			
		||||
@@ -155,9 +155,9 @@ def get_csv_rows(entries: list[JournalEntry]) -> list[CSVRow]:
 | 
			
		||||
                                 gettext("Account"), gettext("Summary"),
 | 
			
		||||
                                 gettext("Debit"), gettext("Credit"),
 | 
			
		||||
                                 gettext("Note"))]
 | 
			
		||||
    rows.extend([CSVRow(x.transaction.date, x.currency.code,
 | 
			
		||||
    rows.extend([CSVRow(x.voucher.date, x.currency.code,
 | 
			
		||||
                        str(x.account).title(), x.summary,
 | 
			
		||||
                        x.debit, x.credit, x.transaction.note)
 | 
			
		||||
                        x.debit, x.credit, x.voucher.note)
 | 
			
		||||
                 for x in entries])
 | 
			
		||||
    return rows
 | 
			
		||||
 | 
			
		||||
@@ -182,18 +182,18 @@ class Journal(BaseReport):
 | 
			
		||||
        """
 | 
			
		||||
        conditions: list[sa.BinaryExpression] = []
 | 
			
		||||
        if self.__period.start is not None:
 | 
			
		||||
            conditions.append(Transaction.date >= self.__period.start)
 | 
			
		||||
            conditions.append(Voucher.date >= self.__period.start)
 | 
			
		||||
        if self.__period.end is not None:
 | 
			
		||||
            conditions.append(Transaction.date <= self.__period.end)
 | 
			
		||||
        return JournalEntry.query.join(Transaction)\
 | 
			
		||||
            conditions.append(Voucher.date <= self.__period.end)
 | 
			
		||||
        return JournalEntry.query.join(Voucher)\
 | 
			
		||||
            .filter(*conditions)\
 | 
			
		||||
            .order_by(Transaction.date,
 | 
			
		||||
                      Transaction.no,
 | 
			
		||||
            .order_by(Voucher.date,
 | 
			
		||||
                      Voucher.no,
 | 
			
		||||
                      JournalEntry.is_debit.desc(),
 | 
			
		||||
                      JournalEntry.no)\
 | 
			
		||||
            .options(selectinload(JournalEntry.account),
 | 
			
		||||
                     selectinload(JournalEntry.currency),
 | 
			
		||||
                     selectinload(JournalEntry.transaction)).all()
 | 
			
		||||
                     selectinload(JournalEntry.voucher)).all()
 | 
			
		||||
 | 
			
		||||
    def csv(self) -> Response:
 | 
			
		||||
        """Returns the report as CSV for download.
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
 | 
			
		||||
 | 
			
		||||
from accounting import db
 | 
			
		||||
from accounting.locale import gettext
 | 
			
		||||
from accounting.models import Currency, Account, Transaction, JournalEntry
 | 
			
		||||
from accounting.models import Currency, Account, Voucher, JournalEntry
 | 
			
		||||
from accounting.report.period import Period, PeriodChooser
 | 
			
		||||
from accounting.report.utils.base_page_params import BasePageParams
 | 
			
		||||
from accounting.report.utils.base_report import BaseReport
 | 
			
		||||
@@ -67,13 +67,13 @@ class ReportEntry:
 | 
			
		||||
        self.url: str | None = None
 | 
			
		||||
        """The URL to the journal entry."""
 | 
			
		||||
        if entry is not None:
 | 
			
		||||
            self.date = entry.transaction.date
 | 
			
		||||
            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.transaction.note
 | 
			
		||||
            self.url = url_for("accounting.transaction.detail",
 | 
			
		||||
                               txn=entry.transaction)
 | 
			
		||||
            self.note = entry.voucher.note
 | 
			
		||||
            self.url = url_for("accounting.voucher.detail",
 | 
			
		||||
                               voucher=entry.voucher)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EntryCollector:
 | 
			
		||||
@@ -116,10 +116,10 @@ class EntryCollector:
 | 
			
		||||
        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
			
		||||
            (JournalEntry.is_debit, JournalEntry.amount),
 | 
			
		||||
            else_=-JournalEntry.amount))
 | 
			
		||||
        select: sa.Select = sa.Select(balance_func).join(Transaction)\
 | 
			
		||||
        select: sa.Select = sa.Select(balance_func).join(Voucher)\
 | 
			
		||||
            .filter(be(JournalEntry.currency_code == self.__currency.code),
 | 
			
		||||
                    be(JournalEntry.account_id == self.__account.id),
 | 
			
		||||
                    Transaction.date < self.__period.start)
 | 
			
		||||
                    Voucher.date < self.__period.start)
 | 
			
		||||
        balance: int | None = db.session.scalar(select)
 | 
			
		||||
        if balance is None:
 | 
			
		||||
            return None
 | 
			
		||||
@@ -143,16 +143,16 @@ class EntryCollector:
 | 
			
		||||
            = [JournalEntry.currency_code == self.__currency.code,
 | 
			
		||||
               JournalEntry.account_id == self.__account.id]
 | 
			
		||||
        if self.__period.start is not None:
 | 
			
		||||
            conditions.append(Transaction.date >= self.__period.start)
 | 
			
		||||
            conditions.append(Voucher.date >= self.__period.start)
 | 
			
		||||
        if self.__period.end is not None:
 | 
			
		||||
            conditions.append(Transaction.date <= self.__period.end)
 | 
			
		||||
        return [ReportEntry(x) for x in JournalEntry.query.join(Transaction)
 | 
			
		||||
            conditions.append(Voucher.date <= self.__period.end)
 | 
			
		||||
        return [ReportEntry(x) for x in JournalEntry.query.join(Voucher)
 | 
			
		||||
                .filter(*conditions)
 | 
			
		||||
                .order_by(Transaction.date,
 | 
			
		||||
                          Transaction.no,
 | 
			
		||||
                .order_by(Voucher.date,
 | 
			
		||||
                          Voucher.no,
 | 
			
		||||
                          JournalEntry.is_debit.desc(),
 | 
			
		||||
                          JournalEntry.no)
 | 
			
		||||
                .options(selectinload(JournalEntry.transaction)).all()]
 | 
			
		||||
                .options(selectinload(JournalEntry.voucher)).all()]
 | 
			
		||||
 | 
			
		||||
    def __get_total_entry(self) -> ReportEntry | None:
 | 
			
		||||
        """Composes the total entry.
 | 
			
		||||
@@ -193,7 +193,7 @@ class EntryCollector:
 | 
			
		||||
class CSVRow(BaseCSVRow):
 | 
			
		||||
    """A row in the CSV."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, txn_date: date | str | None,
 | 
			
		||||
    def __init__(self, voucher_date: date | str | None,
 | 
			
		||||
                 summary: str | None,
 | 
			
		||||
                 debit: str | Decimal | None,
 | 
			
		||||
                 credit: str | Decimal | None,
 | 
			
		||||
@@ -201,14 +201,14 @@ class CSVRow(BaseCSVRow):
 | 
			
		||||
                 note: str | None):
 | 
			
		||||
        """Constructs a row in the CSV.
 | 
			
		||||
 | 
			
		||||
        :param txn_date: The transaction date.
 | 
			
		||||
        :param voucher_date: The voucher date.
 | 
			
		||||
        :param summary: The summary.
 | 
			
		||||
        :param debit: The debit amount.
 | 
			
		||||
        :param credit: The credit amount.
 | 
			
		||||
        :param balance: The balance.
 | 
			
		||||
        :param note: The note.
 | 
			
		||||
        """
 | 
			
		||||
        self.date: date | str | None = txn_date
 | 
			
		||||
        self.date: date | str | None = voucher_date
 | 
			
		||||
        """The date."""
 | 
			
		||||
        self.summary: str | None = summary
 | 
			
		||||
        """The summary."""
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
 | 
			
		||||
 | 
			
		||||
from accounting.locale import gettext
 | 
			
		||||
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
 | 
			
		||||
    Transaction, JournalEntry
 | 
			
		||||
    Voucher, JournalEntry
 | 
			
		||||
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
 | 
			
		||||
@@ -62,21 +62,21 @@ class EntryCollector:
 | 
			
		||||
                       self.__get_account_condition(k)),
 | 
			
		||||
                   JournalEntry.currency_code.in_(
 | 
			
		||||
                       self.__get_currency_condition(k)),
 | 
			
		||||
                   JournalEntry.transaction_id.in_(
 | 
			
		||||
                       self.__get_transaction_condition(k))]
 | 
			
		||||
                   JournalEntry.voucher_id.in_(
 | 
			
		||||
                       self.__get_voucher_condition(k))]
 | 
			
		||||
            try:
 | 
			
		||||
                sub_conditions.append(JournalEntry.amount == Decimal(k))
 | 
			
		||||
            except ArithmeticError:
 | 
			
		||||
                pass
 | 
			
		||||
            conditions.append(sa.or_(*sub_conditions))
 | 
			
		||||
        return JournalEntry.query.join(Transaction).filter(*conditions)\
 | 
			
		||||
            .order_by(Transaction.date,
 | 
			
		||||
                      Transaction.no,
 | 
			
		||||
        return JournalEntry.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.transaction)).all()
 | 
			
		||||
                     selectinload(JournalEntry.voucher)).all()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def __get_account_condition(k: str) -> sa.Select:
 | 
			
		||||
@@ -115,35 +115,35 @@ class EntryCollector:
 | 
			
		||||
                   Currency.code.in_(select_l10n)))
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def __get_transaction_condition(k: str) -> sa.Select:
 | 
			
		||||
        """Composes and returns the condition to filter the transaction.
 | 
			
		||||
    def __get_voucher_condition(k: str) -> sa.Select:
 | 
			
		||||
        """Composes and returns the condition to filter the voucher.
 | 
			
		||||
 | 
			
		||||
        :param k: The keyword.
 | 
			
		||||
        :return: The condition to filter the transaction.
 | 
			
		||||
        :return: The condition to filter the voucher.
 | 
			
		||||
        """
 | 
			
		||||
        conditions: list[sa.BinaryExpression] = [Transaction.note.contains(k)]
 | 
			
		||||
        txn_date: datetime
 | 
			
		||||
        conditions: list[sa.BinaryExpression] = [Voucher.note.contains(k)]
 | 
			
		||||
        voucher_date: datetime
 | 
			
		||||
        try:
 | 
			
		||||
            txn_date = datetime.strptime(k, "%Y")
 | 
			
		||||
            voucher_date = datetime.strptime(k, "%Y")
 | 
			
		||||
            conditions.append(
 | 
			
		||||
                be(sa.extract("year", Transaction.date) == txn_date.year))
 | 
			
		||||
                be(sa.extract("year", Voucher.date) == voucher_date.year))
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            pass
 | 
			
		||||
        try:
 | 
			
		||||
            txn_date = datetime.strptime(k, "%Y/%m")
 | 
			
		||||
            voucher_date = datetime.strptime(k, "%Y/%m")
 | 
			
		||||
            conditions.append(sa.and_(
 | 
			
		||||
                sa.extract("year", Transaction.date) == txn_date.year,
 | 
			
		||||
                sa.extract("month", Transaction.date) == txn_date.month))
 | 
			
		||||
                sa.extract("year", Voucher.date) == voucher_date.year,
 | 
			
		||||
                sa.extract("month", Voucher.date) == voucher_date.month))
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            pass
 | 
			
		||||
        try:
 | 
			
		||||
            txn_date = datetime.strptime(f"2000/{k}", "%Y/%m/%d")
 | 
			
		||||
            voucher_date = datetime.strptime(f"2000/{k}", "%Y/%m/%d")
 | 
			
		||||
            conditions.append(sa.and_(
 | 
			
		||||
                sa.extract("month", Transaction.date) == txn_date.month,
 | 
			
		||||
                sa.extract("day", Transaction.date) == txn_date.day))
 | 
			
		||||
                sa.extract("month", Voucher.date) == voucher_date.month,
 | 
			
		||||
                sa.extract("day", Voucher.date) == voucher_date.day))
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            pass
 | 
			
		||||
        return sa.select(Transaction.id).filter(sa.or_(*conditions))
 | 
			
		||||
        return sa.select(Voucher.id).filter(sa.or_(*conditions))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PageParams(BasePageParams):
 | 
			
		||||
 
 | 
			
		||||
@@ -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, Transaction, JournalEntry
 | 
			
		||||
from accounting.models import Currency, Account, Voucher, JournalEntry
 | 
			
		||||
from accounting.report.period import Period, PeriodChooser
 | 
			
		||||
from accounting.report.utils.base_page_params import BasePageParams
 | 
			
		||||
from accounting.report.utils.base_report import BaseReport
 | 
			
		||||
@@ -180,14 +180,14 @@ class TrialBalance(BaseReport):
 | 
			
		||||
        conditions: list[sa.BinaryExpression] \
 | 
			
		||||
            = [JournalEntry.currency_code == self.__currency.code]
 | 
			
		||||
        if self.__period.start is not None:
 | 
			
		||||
            conditions.append(Transaction.date >= self.__period.start)
 | 
			
		||||
            conditions.append(Voucher.date >= self.__period.start)
 | 
			
		||||
        if self.__period.end is not None:
 | 
			
		||||
            conditions.append(Transaction.date <= self.__period.end)
 | 
			
		||||
            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")
 | 
			
		||||
        select_balances: sa.Select = sa.select(Account.id, balance_func)\
 | 
			
		||||
            .join(Transaction).join(Account)\
 | 
			
		||||
            .join(Voucher).join(Account)\
 | 
			
		||||
            .filter(*conditions)\
 | 
			
		||||
            .group_by(Account.id)\
 | 
			
		||||
            .order_by(Account.base_code, Account.no)
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ from flask import request
 | 
			
		||||
 | 
			
		||||
from accounting import db
 | 
			
		||||
from accounting.models import Currency, JournalEntry
 | 
			
		||||
from accounting.utils.txn_types import TransactionType
 | 
			
		||||
from accounting.utils.voucher_types import VoucherType
 | 
			
		||||
from .option_link import OptionLink
 | 
			
		||||
from .report_chooser import ReportChooser
 | 
			
		||||
 | 
			
		||||
@@ -52,12 +52,12 @@ class BasePageParams(ABC):
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def txn_types(self) -> t.Type[TransactionType]:
 | 
			
		||||
        """Returns the transaction types.
 | 
			
		||||
    def voucher_types(self) -> t.Type[VoucherType]:
 | 
			
		||||
        """Returns the voucher types.
 | 
			
		||||
 | 
			
		||||
        :return: The transaction types.
 | 
			
		||||
        :return: The voucher types.
 | 
			
		||||
        """
 | 
			
		||||
        return TransactionType
 | 
			
		||||
        return VoucherType
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def csv_uri(self) -> str:
 | 
			
		||||
 
 | 
			
		||||
@@ -149,7 +149,7 @@
 | 
			
		||||
    overflow-y: scroll;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** The transaction management */
 | 
			
		||||
/** The voucher management */
 | 
			
		||||
.accounting-currency-control {
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
@@ -169,14 +169,14 @@
 | 
			
		||||
.accounting-list-group-hover .list-group-item:hover {
 | 
			
		||||
    background-color: #ececec;
 | 
			
		||||
}
 | 
			
		||||
.accounting-transaction-entry {
 | 
			
		||||
.accounting-voucher-entry {
 | 
			
		||||
    border: none;
 | 
			
		||||
}
 | 
			
		||||
.accounting-transaction-entry-header {
 | 
			
		||||
.accounting-voucher-entry-header {
 | 
			
		||||
    font-weight: bolder;
 | 
			
		||||
    border-bottom: thick double slategray;
 | 
			
		||||
}
 | 
			
		||||
.list-group-item.accounting-transaction-entry-total {
 | 
			
		||||
.list-group-item.accounting-voucher-entry-total {
 | 
			
		||||
    font-weight: bolder;
 | 
			
		||||
    border-top: thick double slategray;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
/* The Mia! Accounting Flask Project
 | 
			
		||||
 * transaction-transfer-form.js: The JavaScript for the transfer transaction form
 | 
			
		||||
 * account-selector.js: The JavaScript for the account selector
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/*  Copyright (c) 2023 imacat.
 | 
			
		||||
 
 | 
			
		||||
@@ -29,8 +29,8 @@
 | 
			
		||||
class JournalEntryEditor {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The transaction form
 | 
			
		||||
     * @type {TransactionForm}
 | 
			
		||||
     * The voucher form
 | 
			
		||||
     * @type {VoucherForm}
 | 
			
		||||
     */
 | 
			
		||||
    form;
 | 
			
		||||
 | 
			
		||||
@@ -217,7 +217,7 @@ class JournalEntryEditor {
 | 
			
		||||
    /**
 | 
			
		||||
     * Constructs a new journal entry editor.
 | 
			
		||||
     *
 | 
			
		||||
     * @param form {TransactionForm} the transaction form
 | 
			
		||||
     * @param form {VoucherForm} the voucher form
 | 
			
		||||
     */
 | 
			
		||||
    constructor(form) {
 | 
			
		||||
        this.form = form;
 | 
			
		||||
 
 | 
			
		||||
@@ -105,7 +105,7 @@ class OriginalEntrySelector {
 | 
			
		||||
     * Returns the net balance for an original entry.
 | 
			
		||||
     *
 | 
			
		||||
     * @param currentEntry {JournalEntrySubForm} the journal entry sub-form that is currently editing
 | 
			
		||||
     * @param form {TransactionForm} the transaction form
 | 
			
		||||
     * @param form {VoucherForm} the voucher form
 | 
			
		||||
     * @param originalEntryId {string} the ID of the original entry
 | 
			
		||||
     * @return {Decimal} the net balance of the original entry
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
/* The Mia! Accounting Flask Project
 | 
			
		||||
 * transaction-form.js: The JavaScript for the transaction form
 | 
			
		||||
 * voucher-form.js: The JavaScript for the voucher form
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/*  Copyright (c) 2023 imacat.
 | 
			
		||||
@@ -23,14 +23,14 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
    TransactionForm.initialize();
 | 
			
		||||
    VoucherForm.initialize();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The transaction form
 | 
			
		||||
 * The voucher form
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
class TransactionForm {
 | 
			
		||||
class VoucherForm {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The form element
 | 
			
		||||
@@ -105,7 +105,7 @@ class TransactionForm {
 | 
			
		||||
    entryEditor;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Constructs the transaction form.
 | 
			
		||||
     * Constructs the voucher form.
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    constructor() {
 | 
			
		||||
@@ -325,17 +325,17 @@ class TransactionForm {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The transaction form
 | 
			
		||||
     * @type {TransactionForm}
 | 
			
		||||
     * The voucher form
 | 
			
		||||
     * @type {VoucherForm}
 | 
			
		||||
     */
 | 
			
		||||
    static #form;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initializes the transaction form.
 | 
			
		||||
     * Initializes the voucher form.
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    static initialize() {
 | 
			
		||||
        this.#form = new TransactionForm()
 | 
			
		||||
        this.#form = new VoucherForm()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -352,8 +352,8 @@ class CurrencySubForm {
 | 
			
		||||
    element;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The transaction form
 | 
			
		||||
     * @type {TransactionForm}
 | 
			
		||||
     * The voucher form
 | 
			
		||||
     * @type {VoucherForm}
 | 
			
		||||
     */
 | 
			
		||||
    form;
 | 
			
		||||
 | 
			
		||||
@@ -420,7 +420,7 @@ class CurrencySubForm {
 | 
			
		||||
    /**
 | 
			
		||||
     * Constructs a currency sub-form
 | 
			
		||||
     *
 | 
			
		||||
     * @param form {TransactionForm} the transaction form
 | 
			
		||||
     * @param form {VoucherForm} the voucher form
 | 
			
		||||
     * @param element {HTMLDivElement} the currency sub-form element
 | 
			
		||||
     */
 | 
			
		||||
    constructor(form, element) {
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
/* The Mia! Accounting Flask Project
 | 
			
		||||
 * transaction-order.js: The JavaScript for the transaction order
 | 
			
		||||
 * voucher-order.js: The JavaScript for the voucher order
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/*  Copyright (c) 2023 imacat.
 | 
			
		||||
@@ -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 template globals for the transaction management.
 | 
			
		||||
"""The template globals.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from flask import current_app
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ First written: 2023/3/7
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
 | 
			
		||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/period-chooser.html" %}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
add-txn-material-fab.html: The material floating action buttons to add a new transaction
 | 
			
		||||
add-voucher-material-fab.html: The material floating action buttons to add a new voucher
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -22,13 +22,13 @@ First written: 2023/2/25
 | 
			
		||||
{% if accounting_can_edit() %}
 | 
			
		||||
  <div id="accounting-material-fab-speed-dial" class="d-md-none accounting-material-fab">
 | 
			
		||||
    <div id="accounting-material-fab-speed-dial-actions" class="d-md-none accounting-material-fab-speed-dial-group">
 | 
			
		||||
      <a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
 | 
			
		||||
      <a class="btn rounded-pill" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.CASH_DISBURSEMENT)|accounting_append_next }}">
 | 
			
		||||
        {{ A_("Cash expense") }}
 | 
			
		||||
      </a>
 | 
			
		||||
      <a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
 | 
			
		||||
      <a class="btn rounded-pill" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.CASH_RECEIPT)|accounting_append_next }}">
 | 
			
		||||
        {{ A_("Cash income") }}
 | 
			
		||||
      </a>
 | 
			
		||||
      <a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
 | 
			
		||||
      <a class="btn rounded-pill" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.TRANSFER)|accounting_append_next }}">
 | 
			
		||||
        {{ A_("Transfer") }}
 | 
			
		||||
      </a>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -27,17 +27,17 @@ First written: 2023/3/8
 | 
			
		||||
    </button>
 | 
			
		||||
    <ul class="dropdown-menu">
 | 
			
		||||
      <li>
 | 
			
		||||
        <a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
 | 
			
		||||
        <a class="dropdown-item" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.CASH_DISBURSEMENT)|accounting_append_next }}">
 | 
			
		||||
          {{ A_("Cash Expense") }}
 | 
			
		||||
        </a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
 | 
			
		||||
        <a class="dropdown-item" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.CASH_RECEIPT)|accounting_append_next }}">
 | 
			
		||||
          {{ A_("Cash Income") }}
 | 
			
		||||
        </a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
 | 
			
		||||
        <a class="dropdown-item" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.TRANSFER)|accounting_append_next }}">
 | 
			
		||||
          {{ A_("Transfer") }}
 | 
			
		||||
        </a>
 | 
			
		||||
      </li>
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ First written: 2023/3/5
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
 | 
			
		||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/period-chooser.html" %}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ First written: 2023/3/7
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
 | 
			
		||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/period-chooser.html" %}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@ First written: 2023/3/4
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
 | 
			
		||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/period-chooser.html" %}
 | 
			
		||||
 | 
			
		||||
@@ -60,8 +60,8 @@ First written: 2023/3/4
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="accounting-report-table-body">
 | 
			
		||||
      {% for entry in report.entries %}
 | 
			
		||||
        <a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
 | 
			
		||||
          <div>{{ entry.transaction.date|accounting_format_date }}</div>
 | 
			
		||||
        <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>
 | 
			
		||||
          <div>
 | 
			
		||||
            <span class="d-none d-md-inline">{{ entry.account.code }}</span>
 | 
			
		||||
@@ -77,11 +77,11 @@ First written: 2023/3/4
 | 
			
		||||
 | 
			
		||||
  <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.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
 | 
			
		||||
    <a class="list-group-item list-group-item-action" href="{{ url_for("accounting.voucher.detail", voucher=entry.voucher)|accounting_append_next }}">
 | 
			
		||||
      <div class="d-flex justify-content-between">
 | 
			
		||||
        <div {% if not entry.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
 | 
			
		||||
          <div class="text-muted small">
 | 
			
		||||
            {{ entry.transaction.date|accounting_format_date }}
 | 
			
		||||
            {{ 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>
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ First written: 2023/3/5
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
 | 
			
		||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/period-chooser.html" %}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ First written: 2023/3/8
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
 | 
			
		||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/search-modal.html" %}
 | 
			
		||||
 | 
			
		||||
@@ -57,8 +57,8 @@ First written: 2023/3/8
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="accounting-report-table-body">
 | 
			
		||||
      {% for entry in report.entries %}
 | 
			
		||||
        <a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
 | 
			
		||||
          <div>{{ entry.transaction.date|accounting_format_date }}</div>
 | 
			
		||||
        <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>
 | 
			
		||||
          <div>
 | 
			
		||||
            <span class="d-none d-md-inline">{{ entry.account.code }}</span>
 | 
			
		||||
@@ -74,11 +74,11 @@ First written: 2023/3/8
 | 
			
		||||
 | 
			
		||||
  <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.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
 | 
			
		||||
    <a class="list-group-item list-group-item-action" href="{{ url_for("accounting.voucher.detail", voucher=entry.voucher)|accounting_append_next }}">
 | 
			
		||||
      <div class="d-flex justify-content-between">
 | 
			
		||||
        <div {% if not entry.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
 | 
			
		||||
          <div class="text-muted small">
 | 
			
		||||
            {{ entry.transaction.date|accounting_format_date }}
 | 
			
		||||
            {{ 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>
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ First written: 2023/3/5
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
 | 
			
		||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
			
		||||
 | 
			
		||||
{% include "accounting/report/include/period-chooser.html" %}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
create.html: The cash expense transaction creation form
 | 
			
		||||
create.html: The cash disbursement voucher creation form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -19,10 +19,10 @@ create.html: The cash expense transaction creation form
 | 
			
		||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
			
		||||
First written: 2023/2/25
 | 
			
		||||
#}
 | 
			
		||||
{% extends "accounting/transaction/expense/include/form.html" %}
 | 
			
		||||
{% extends "accounting/voucher/disbursement/include/form.html" %}
 | 
			
		||||
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Add a New Cash Expense Transaction") }}{% endblock %}{% endblock %}
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Add a New Cash Disbursement Voucher") }}{% endblock %}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}
 | 
			
		||||
{% block action_url %}{{ url_for("accounting.voucher.store", voucher_type=voucher_type) }}{% endblock %}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
detail.html: The account detail
 | 
			
		||||
detail.html: The cash disbursement voucher detail
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -19,26 +19,26 @@ detail.html: The account detail
 | 
			
		||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
			
		||||
First written: 2023/2/26
 | 
			
		||||
#}
 | 
			
		||||
{% extends "accounting/transaction/include/detail.html" %}
 | 
			
		||||
{% extends "accounting/voucher/include/detail.html" %}
 | 
			
		||||
 | 
			
		||||
{% block to_transfer %}
 | 
			
		||||
  <a class="btn btn-primary" href="{{ url_for("accounting.transaction.edit", txn=obj)|accounting_txn_to_transfer|accounting_inherit_next }}">
 | 
			
		||||
  <a class="btn btn-primary" href="{{ url_for("accounting.voucher.edit", voucher=obj)|accounting_voucher_to_transfer|accounting_inherit_next }}">
 | 
			
		||||
    <i class="fa-solid fa-bars-staggered"></i>
 | 
			
		||||
    {{ A_("To Transfer") }}
 | 
			
		||||
  </a>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block transaction_currencies %}
 | 
			
		||||
{% block voucher_currencies %}
 | 
			
		||||
  {% for currency in obj.currencies %}
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
      <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-transaction-entry accounting-transaction-entry-header">{{ A_("Content") }}</li>
 | 
			
		||||
        <li class="list-group-item accounting-voucher-entry accounting-voucher-entry-header">{{ A_("Content") }}</li>
 | 
			
		||||
        {% with entries = currency.debit %}
 | 
			
		||||
          {% include "accounting/transaction/include/detail-entries.html" %}
 | 
			
		||||
          {% include "accounting/voucher/include/detail-entries.html" %}
 | 
			
		||||
        {% endwith %}
 | 
			
		||||
        <li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
 | 
			
		||||
        <li class="list-group-item accounting-voucher-entry accounting-voucher-entry-total">
 | 
			
		||||
          <div class="d-flex justify-content-between">
 | 
			
		||||
            <div>{{ A_("Total") }}</div>
 | 
			
		||||
            <div>{{ currency.debit_total|accounting_format_amount }}</div>
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
edit.html: The cash income transaction edit form
 | 
			
		||||
edit.html: The cash disbursement voucher edit form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -19,10 +19,10 @@ edit.html: The cash income transaction edit form
 | 
			
		||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
			
		||||
First written: 2023/2/25
 | 
			
		||||
#}
 | 
			
		||||
{% extends "accounting/transaction/income/include/form.html" %}
 | 
			
		||||
{% extends "accounting/voucher/disbursement/include/form.html" %}
 | 
			
		||||
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Editing %(txn)s", txn=txn) }}{% endblock %}{% endblock %}
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Editing %(voucher)s", voucher=voucher) }}{% endblock %}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block back_url %}{{ url_for("accounting.transaction.detail", txn=txn)|accounting_inherit_next }}{% endblock %}
 | 
			
		||||
{% block back_url %}{{ url_for("accounting.voucher.detail", voucher=voucher)|accounting_inherit_next }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block action_url %}{{ url_for("accounting.transaction.update", txn=txn)|accounting_txn_with_type }}{% endblock %}
 | 
			
		||||
{% block action_url %}{{ url_for("accounting.voucher.update", voucher=voucher)|accounting_voucher_with_type }}{% endblock %}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
currency-sub-form.html: The currency sub-form in the cash expense transaction form
 | 
			
		||||
currency-sub-form.html: The currency sub-form in the cash disbursement voucher form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -64,11 +64,11 @@ First written: 2023/2/25
 | 
			
		||||
                    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_txn_format_amount_input,
 | 
			
		||||
                    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/transaction/include/form-entry-item.html" %}
 | 
			
		||||
              {% include "accounting/voucher/include/form-entry-item.html" %}
 | 
			
		||||
            {% endwith %}
 | 
			
		||||
          {% endfor %}
 | 
			
		||||
        </ul>
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
form.html: The cash expense transaction form
 | 
			
		||||
form.html: The cash disbursement voucher form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -19,7 +19,7 @@ form.html: The cash expense transaction form
 | 
			
		||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
			
		||||
First written: 2023/2/25
 | 
			
		||||
#}
 | 
			
		||||
{% extends "accounting/transaction/include/form.html" %}
 | 
			
		||||
{% extends "accounting/voucher/include/form.html" %}
 | 
			
		||||
 | 
			
		||||
{% block currency_sub_forms %}
 | 
			
		||||
  {% if form.currencies %}
 | 
			
		||||
@@ -33,7 +33,7 @@ First written: 2023/2/25
 | 
			
		||||
              debit_forms = currency_form.debit,
 | 
			
		||||
              debit_errors = currency_form.debit_errors,
 | 
			
		||||
              debit_total = currency_form.form.debit_total|accounting_format_amount %}
 | 
			
		||||
        {% include "accounting/transaction/expense/include/form-currency-item.html" %}
 | 
			
		||||
        {% include "accounting/voucher/disbursement/include/form-currency-item.html" %}
 | 
			
		||||
      {% endwith %}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
  {% else %}
 | 
			
		||||
@@ -41,17 +41,17 @@ First written: 2023/2/25
 | 
			
		||||
            only_one_currency_form = True,
 | 
			
		||||
            currency_code_data = accounting_default_currency_code(),
 | 
			
		||||
            debit_total = "-" %}
 | 
			
		||||
      {% include "accounting/transaction/expense/include/form-currency-item.html" %}
 | 
			
		||||
      {% include "accounting/voucher/disbursement/include/form-currency-item.html" %}
 | 
			
		||||
    {% endwith %}
 | 
			
		||||
  {% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block form_modals %}
 | 
			
		||||
  {% with summary_editor = form.summary_editor.debit %}
 | 
			
		||||
    {% include "accounting/transaction/include/summary-editor-modal.html" %}
 | 
			
		||||
    {% include "accounting/voucher/include/summary-editor-modal.html" %}
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
  {% with entry_type = "debit",
 | 
			
		||||
          account_options = form.debit_account_options %}
 | 
			
		||||
    {% include "accounting/transaction/include/account-selector-modal.html" %}
 | 
			
		||||
    {% include "accounting/voucher/include/account-selector-modal.html" %}
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
detail-entries-item: The journal entries in the transaction detail
 | 
			
		||||
detail-entries-item: The journal entries in the voucher detail
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -21,7 +21,7 @@ First written: 2023/3/14
 | 
			
		||||
#}
 | 
			
		||||
{# <ul> For SonarQube not to complain about incorrect HTML #}
 | 
			
		||||
{% for entry in entries %}
 | 
			
		||||
  <li class="list-group-item accounting-transaction-entry">
 | 
			
		||||
  <li class="list-group-item accounting-voucher-entry">
 | 
			
		||||
    <div class="d-flex justify-content-between">
 | 
			
		||||
      <div>
 | 
			
		||||
        <div class="small">{{ entry.account }}</div>
 | 
			
		||||
@@ -30,7 +30,7 @@ First written: 2023/3/14
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if entry.original_entry %}
 | 
			
		||||
          <div class="fst-italic small accounting-original-entry">
 | 
			
		||||
            <a href="{{ url_for("accounting.transaction.detail", txn=entry.original_entry.transaction)|accounting_append_next }}">
 | 
			
		||||
            <a href="{{ url_for("accounting.voucher.detail", voucher=entry.original_entry.voucher)|accounting_append_next }}">
 | 
			
		||||
              {{ A_("Offset %(entry)s", entry=entry.original_entry) }}
 | 
			
		||||
            </a>
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -43,8 +43,8 @@ First written: 2023/3/14
 | 
			
		||||
                <ul class="ms-2 ps-0">
 | 
			
		||||
                  {% for offset in entry.offsets %}
 | 
			
		||||
                    <li>
 | 
			
		||||
                      <a href="{{ url_for("accounting.transaction.detail", txn=offset.transaction)|accounting_append_next }}">
 | 
			
		||||
                        {{ offset.transaction.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}
 | 
			
		||||
                      <a href="{{ url_for("accounting.voucher.detail", voucher=offset.voucher)|accounting_append_next }}">
 | 
			
		||||
                        {{ offset.voucher.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}
 | 
			
		||||
                      </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                  {% endfor %}
 | 
			
		||||
@@ -31,12 +31,12 @@ First written: 2023/2/26
 | 
			
		||||
    {{ A_("Back") }}
 | 
			
		||||
  </a>
 | 
			
		||||
  {% if accounting_can_edit() %}
 | 
			
		||||
    <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.transaction.edit", txn=obj)|accounting_inherit_next }}">
 | 
			
		||||
    <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.voucher.edit", voucher=obj)|accounting_inherit_next }}">
 | 
			
		||||
      <i class="fa-solid fa-gear"></i>
 | 
			
		||||
      {{ A_("Settings") }}
 | 
			
		||||
    </a>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
  <a class="btn btn-primary" href="{{ url_for("accounting.transaction.order", txn_date=obj.date)|accounting_append_next }}">
 | 
			
		||||
  <a class="btn btn-primary" href="{{ url_for("accounting.voucher.order", voucher_date=obj.date)|accounting_append_next }}">
 | 
			
		||||
    <i class="fa-solid fa-bars-staggered"></i>
 | 
			
		||||
    {{ A_("Order") }}
 | 
			
		||||
  </a>
 | 
			
		||||
@@ -58,14 +58,14 @@ First written: 2023/2/26
 | 
			
		||||
 | 
			
		||||
{% if accounting_can_edit() %}
 | 
			
		||||
  <div class="d-md-none accounting-material-fab">
 | 
			
		||||
    <a class="btn btn-primary" href="{{ url_for("accounting.transaction.edit", txn=obj)|accounting_inherit_next }}">
 | 
			
		||||
    <a class="btn btn-primary" href="{{ url_for("accounting.voucher.edit", voucher=obj)|accounting_inherit_next }}">
 | 
			
		||||
      <i class="fa-solid fa-pen-to-square"></i>
 | 
			
		||||
    </a>
 | 
			
		||||
  </div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% if accounting_can_edit() and obj.can_delete %}
 | 
			
		||||
  <form action="{{ url_for("accounting.transaction.delete", txn=obj) }}" method="post">
 | 
			
		||||
  <form action="{{ url_for("accounting.voucher.delete", voucher=obj) }}" method="post">
 | 
			
		||||
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
			
		||||
    {% if request.args.next %}
 | 
			
		||||
      <input type="hidden" name="next" value="{{ request.args.next }}">
 | 
			
		||||
@@ -74,11 +74,11 @@ First written: 2023/2/26
 | 
			
		||||
      <div class="modal-dialog">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
          <div class="modal-header">
 | 
			
		||||
            <h1 class="modal-title fs-5" id="accounting-delete-modal-label">{{ A_("Delete Transaction Confirmation") }}</h1>
 | 
			
		||||
            <h1 class="modal-title fs-5" id="accounting-delete-modal-label">{{ A_("Delete Voucher Confirmation") }}</h1>
 | 
			
		||||
            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ A_("Close") }}"></button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="modal-body">
 | 
			
		||||
            {{ A_("Do you really want to delete this transaction?") }}
 | 
			
		||||
            {{ A_("Do you really want to delete this voucher?") }}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="modal-footer">
 | 
			
		||||
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
 | 
			
		||||
@@ -99,13 +99,13 @@ First written: 2023/2/26
 | 
			
		||||
    {{ obj.date|accounting_format_date }}
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  {% block transaction_currencies %}{% endblock %}
 | 
			
		||||
  {% block voucher_currencies %}{% endblock %}
 | 
			
		||||
 | 
			
		||||
  {% if obj.note %}
 | 
			
		||||
    <div class="card mb-3">
 | 
			
		||||
      <div class="card-body">
 | 
			
		||||
        <i class="far fa-comment-dots"></i>
 | 
			
		||||
        {{ obj.note|accounting_txn_text2html|safe }}
 | 
			
		||||
        {{ obj.note|accounting_voucher_text2html|safe }}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
entry-sub-form.html: The journal entry sub-form in the transaction form
 | 
			
		||||
entry-sub-form.html: The journal entry sub-form in the voucher form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -43,7 +43,7 @@ First written: 2023/2/25
 | 
			
		||||
              <div>{{ A_("Offsets") }}</div>
 | 
			
		||||
              <ul class="ms-2 ps-0">
 | 
			
		||||
                {% for offset in offset_entries %}
 | 
			
		||||
                  <li>{{ offset.transaction.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}</li>
 | 
			
		||||
                  <li>{{ offset.voucher.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}</li>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
              </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
form.html: The transfer transaction form
 | 
			
		||||
form.html: The base voucher form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -23,7 +23,7 @@ First written: 2023/2/26
 | 
			
		||||
 | 
			
		||||
{% block accounting_scripts %}
 | 
			
		||||
  <script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
 | 
			
		||||
  <script src="{{ url_for("accounting.static", filename="js/transaction-form.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/account-selector.js") }}"></script>
 | 
			
		||||
  <script src="{{ url_for("accounting.static", filename="js/original-entry-selector.js") }}"></script>
 | 
			
		||||
@@ -88,8 +88,8 @@ First written: 2023/2/26
 | 
			
		||||
  </div>
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
{% include "accounting/transaction/include/journal-entry-editor-modal.html" %}
 | 
			
		||||
{% include "accounting/voucher/include/journal-entry-editor-modal.html" %}
 | 
			
		||||
{% block form_modals %}{% endblock %}
 | 
			
		||||
{% include "accounting/transaction/include/original-entry-selector-modal.html" %}
 | 
			
		||||
{% include "accounting/voucher/include/original-entry-selector-modal.html" %}
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -37,8 +37,8 @@ First written: 2023/2/25
 | 
			
		||||
 | 
			
		||||
        <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.transaction.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_txn_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.transaction.date|accounting_format_date }} {{ entry.summary|accounting_default }}</div>
 | 
			
		||||
            <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>
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
order.html: The order of the transactions in a same day
 | 
			
		||||
order.html: The order of the vouchers in a same day
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -23,10 +23,10 @@ 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/transaction-order.js") }}"></script>
 | 
			
		||||
  <script src="{{ url_for("accounting.static", filename="js/voucher-order.js") }}"></script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Transactions on %(date)s", date=date) }}{% endblock %}{% endblock %}
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Vouchers on %(date)s", date=date) }}{% endblock %}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
@@ -38,7 +38,7 @@ First written: 2023/2/26
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% if list|length > 1 and accounting_can_edit() %}
 | 
			
		||||
  <form action="{{ url_for("accounting.transaction.sort", txn_date=date) }}" method="post">
 | 
			
		||||
  <form action="{{ url_for("accounting.voucher.sort", voucher_date=date) }}" method="post">
 | 
			
		||||
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
			
		||||
    {% if request.args.next %}
 | 
			
		||||
      <input type="hidden" name="next" value="{{ request.args.next }}">
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
create.html: The transfer transaction creation form
 | 
			
		||||
create.html: The cash receipt voucher creation form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -19,10 +19,10 @@ create.html: The transfer transaction creation form
 | 
			
		||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
			
		||||
First written: 2023/2/25
 | 
			
		||||
#}
 | 
			
		||||
{% extends "accounting/transaction/transfer/include/form.html" %}
 | 
			
		||||
{% extends "accounting/voucher/receipt/include/form.html" %}
 | 
			
		||||
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Add a New Transfer Transaction") }}{% endblock %}{% endblock %}
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Add a New Cash Receipt Voucher") }}{% endblock %}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}
 | 
			
		||||
{% block action_url %}{{ url_for("accounting.voucher.store", voucher_type=voucher_type) }}{% endblock %}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
detail.html: The account detail
 | 
			
		||||
detail.html: The cash receipt voucher detail
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -19,26 +19,26 @@ detail.html: The account detail
 | 
			
		||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
			
		||||
First written: 2023/2/26
 | 
			
		||||
#}
 | 
			
		||||
{% extends "accounting/transaction/include/detail.html" %}
 | 
			
		||||
{% extends "accounting/voucher/include/detail.html" %}
 | 
			
		||||
 | 
			
		||||
{% block to_transfer %}
 | 
			
		||||
  <a class="btn btn-primary" href="{{ url_for("accounting.transaction.edit", txn=obj)|accounting_txn_to_transfer|accounting_inherit_next }}">
 | 
			
		||||
  <a class="btn btn-primary" href="{{ url_for("accounting.voucher.edit", voucher=obj)|accounting_voucher_to_transfer|accounting_inherit_next }}">
 | 
			
		||||
    <i class="fa-solid fa-bars-staggered"></i>
 | 
			
		||||
    {{ A_("To Transfer") }}
 | 
			
		||||
  </a>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block transaction_currencies %}
 | 
			
		||||
{% block voucher_currencies %}
 | 
			
		||||
  {% for currency in obj.currencies %}
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
      <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-transaction-entry accounting-transaction-entry-header">{{ A_("Content") }}</li>
 | 
			
		||||
        <li class="list-group-item accounting-voucher-entry accounting-voucher-entry-header">{{ A_("Content") }}</li>
 | 
			
		||||
        {% with entries = currency.credit %}
 | 
			
		||||
          {% include "accounting/transaction/include/detail-entries.html" %}
 | 
			
		||||
          {% include "accounting/voucher/include/detail-entries.html" %}
 | 
			
		||||
        {% endwith %}
 | 
			
		||||
        <li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
 | 
			
		||||
        <li class="list-group-item accounting-voucher-entry accounting-voucher-entry-total">
 | 
			
		||||
          <div class="d-flex justify-content-between">
 | 
			
		||||
            <div>{{ A_("Total") }}</div>
 | 
			
		||||
            <div>{{ currency.debit_total|accounting_format_amount }}</div>
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
edit.html: The cash expense transaction edit form
 | 
			
		||||
edit.html: The cash receipt voucher edit form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -19,10 +19,10 @@ edit.html: The cash expense transaction edit form
 | 
			
		||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
			
		||||
First written: 2023/2/25
 | 
			
		||||
#}
 | 
			
		||||
{% extends "accounting/transaction/expense/include/form.html" %}
 | 
			
		||||
{% extends "accounting/voucher/receipt/include/form.html" %}
 | 
			
		||||
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Editing %(txn)s", txn=txn) }}{% endblock %}{% endblock %}
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Editing %(voucher)s", voucher=voucher) }}{% endblock %}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block back_url %}{{ url_for("accounting.transaction.detail", txn=txn)|accounting_inherit_next }}{% endblock %}
 | 
			
		||||
{% block back_url %}{{ url_for("accounting.voucher.detail", voucher=voucher)|accounting_inherit_next }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block action_url %}{{ url_for("accounting.transaction.update", txn=txn)|accounting_txn_with_type }}{% endblock %}
 | 
			
		||||
{% block action_url %}{{ url_for("accounting.voucher.update", voucher=voucher)|accounting_voucher_with_type }}{% endblock %}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
currency-sub-form.html: The currency sub-form in the cash income transaction form
 | 
			
		||||
currency-sub-form.html: The currency sub-form in the cash receipt voucher form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -64,11 +64,11 @@ First written: 2023/2/25
 | 
			
		||||
                    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_txn_format_amount_input,
 | 
			
		||||
                    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/transaction/include/form-entry-item.html" %}
 | 
			
		||||
              {% include "accounting/voucher/include/form-entry-item.html" %}
 | 
			
		||||
            {% endwith %}
 | 
			
		||||
          {% endfor %}
 | 
			
		||||
        </ul>
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
form.html: The cash income transaction form
 | 
			
		||||
form.html: The cash receipt voucher form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -19,7 +19,7 @@ form.html: The cash income transaction form
 | 
			
		||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
			
		||||
First written: 2023/2/25
 | 
			
		||||
#}
 | 
			
		||||
{% extends "accounting/transaction/include/form.html" %}
 | 
			
		||||
{% extends "accounting/voucher/include/form.html" %}
 | 
			
		||||
 | 
			
		||||
{% block currency_sub_forms %}
 | 
			
		||||
  {% if form.currencies %}
 | 
			
		||||
@@ -33,7 +33,7 @@ First written: 2023/2/25
 | 
			
		||||
              credit_forms = currency_form.credit,
 | 
			
		||||
              credit_errors = currency_form.credit_errors,
 | 
			
		||||
              credit_total = currency_form.form.credit_total|accounting_format_amount %}
 | 
			
		||||
        {% include "accounting/transaction/income/include/form-currency-item.html" %}
 | 
			
		||||
        {% include "accounting/voucher/receipt/include/form-currency-item.html" %}
 | 
			
		||||
      {% endwith %}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
  {% else %}
 | 
			
		||||
@@ -41,17 +41,17 @@ First written: 2023/2/25
 | 
			
		||||
            only_one_currency_form = True,
 | 
			
		||||
            currency_code_data = accounting_default_currency_code(),
 | 
			
		||||
            credit_total = "-" %}
 | 
			
		||||
      {% include "accounting/transaction/income/include/form-currency-item.html" %}
 | 
			
		||||
      {% include "accounting/voucher/receipt/include/form-currency-item.html" %}
 | 
			
		||||
    {% endwith %}
 | 
			
		||||
  {% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block form_modals %}
 | 
			
		||||
  {% with summary_editor = form.summary_editor.credit %}
 | 
			
		||||
    {% include "accounting/transaction/include/summary-editor-modal.html" %}
 | 
			
		||||
    {% include "accounting/voucher/include/summary-editor-modal.html" %}
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
  {% with entry_type = "credit",
 | 
			
		||||
          account_options = form.credit_account_options %}
 | 
			
		||||
    {% include "accounting/transaction/include/account-selector-modal.html" %}
 | 
			
		||||
    {% include "accounting/voucher/include/account-selector-modal.html" %}
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
create.html: The cash income transaction creation form
 | 
			
		||||
create.html: The transfer voucher creation form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -19,10 +19,10 @@ create.html: The cash income transaction creation form
 | 
			
		||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
			
		||||
First written: 2023/2/25
 | 
			
		||||
#}
 | 
			
		||||
{% extends "accounting/transaction/income/include/form.html" %}
 | 
			
		||||
{% extends "accounting/voucher/transfer/include/form.html" %}
 | 
			
		||||
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Add a New Cash Income Transaction") }}{% endblock %}{% endblock %}
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Add a New Transfer Voucher") }}{% endblock %}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}
 | 
			
		||||
{% block action_url %}{{ url_for("accounting.voucher.store", voucher_type=voucher_type) }}{% endblock %}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
detail.html: The account detail
 | 
			
		||||
detail.html: The transfer voucher detail
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -19,9 +19,9 @@ detail.html: The account detail
 | 
			
		||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
			
		||||
First written: 2023/2/26
 | 
			
		||||
#}
 | 
			
		||||
{% extends "accounting/transaction/include/detail.html" %}
 | 
			
		||||
{% extends "accounting/voucher/include/detail.html" %}
 | 
			
		||||
 | 
			
		||||
{% block transaction_currencies %}
 | 
			
		||||
{% block voucher_currencies %}
 | 
			
		||||
  {% for currency in obj.currencies %}
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
      <div class="mb-2 fw-bolder">{{ currency.name }}</div>
 | 
			
		||||
@@ -30,11 +30,11 @@ First written: 2023/2/26
 | 
			
		||||
        {# The debit entries #}
 | 
			
		||||
        <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-transaction-entry accounting-transaction-entry-header">{{ A_("Debit") }}</li>
 | 
			
		||||
            <li class="list-group-item accounting-voucher-entry accounting-voucher-entry-header">{{ A_("Debit") }}</li>
 | 
			
		||||
            {% with entries = currency.debit %}
 | 
			
		||||
              {% include "accounting/transaction/include/detail-entries.html" %}
 | 
			
		||||
              {% include "accounting/voucher/include/detail-entries.html" %}
 | 
			
		||||
            {% endwith %}
 | 
			
		||||
            <li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
 | 
			
		||||
            <li class="list-group-item accounting-voucher-entry accounting-voucher-entry-total">
 | 
			
		||||
              <div class="d-flex justify-content-between">
 | 
			
		||||
                <div>{{ A_("Total") }}</div>
 | 
			
		||||
                <div>{{ currency.debit_total|accounting_format_amount }}</div>
 | 
			
		||||
@@ -46,11 +46,11 @@ First written: 2023/2/26
 | 
			
		||||
        {# The credit entries #}
 | 
			
		||||
        <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-transaction-entry accounting-transaction-entry-header">{{ A_("Credit") }}</li>
 | 
			
		||||
            <li class="list-group-item accounting-voucher-entry accounting-voucher-entry-header">{{ A_("Credit") }}</li>
 | 
			
		||||
            {% with entries = currency.credit %}
 | 
			
		||||
              {% include "accounting/transaction/include/detail-entries.html" %}
 | 
			
		||||
              {% include "accounting/voucher/include/detail-entries.html" %}
 | 
			
		||||
            {% endwith %}
 | 
			
		||||
            <li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
 | 
			
		||||
            <li class="list-group-item accounting-voucher-entry accounting-voucher-entry-total">
 | 
			
		||||
              <div class="d-flex justify-content-between">
 | 
			
		||||
                <div>{{ A_("Total") }}</div>
 | 
			
		||||
                <div>{{ currency.debit_total|accounting_format_amount }}</div>
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
edit.html: The transfer transaction edit form
 | 
			
		||||
edit.html: The transfer voucher edit form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -19,10 +19,10 @@ edit.html: The transfer transaction edit form
 | 
			
		||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
			
		||||
First written: 2023/2/25
 | 
			
		||||
#}
 | 
			
		||||
{% extends "accounting/transaction/transfer/include/form.html" %}
 | 
			
		||||
{% extends "accounting/voucher/transfer/include/form.html" %}
 | 
			
		||||
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Editing %(txn)s", txn=txn) }}{% endblock %}{% endblock %}
 | 
			
		||||
{% block header %}{% block title %}{{ A_("Editing %(voucher)s", voucher=voucher) }}{% endblock %}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block back_url %}{{ url_for("accounting.transaction.detail", txn=txn)|accounting_inherit_next }}{% endblock %}
 | 
			
		||||
{% block back_url %}{{ url_for("accounting.voucher.detail", voucher=voucher)|accounting_inherit_next }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block action_url %}{{ url_for("accounting.transaction.update", txn=txn)|accounting_txn_with_type }}{% endblock %}
 | 
			
		||||
{% block action_url %}{{ url_for("accounting.voucher.update", voucher=voucher)|accounting_voucher_with_type }}{% endblock %}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
currency-sub-form.html: The currency sub-form in the transfer transaction form
 | 
			
		||||
currency-sub-form.html: The currency sub-form in the transfer voucher form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -66,11 +66,11 @@ First written: 2023/2/25
 | 
			
		||||
                      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_txn_format_amount_input,
 | 
			
		||||
                      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/transaction/include/form-entry-item.html" %}
 | 
			
		||||
                {% include "accounting/voucher/include/form-entry-item.html" %}
 | 
			
		||||
              {% endwith %}
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
          </ul>
 | 
			
		||||
@@ -114,11 +114,11 @@ First written: 2023/2/25
 | 
			
		||||
                      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_txn_format_amount_input,
 | 
			
		||||
                      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/transaction/include/form-entry-item.html" %}
 | 
			
		||||
                {% include "accounting/voucher/include/form-entry-item.html" %}
 | 
			
		||||
              {% endwith %}
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
          </ul>
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{#
 | 
			
		||||
The Mia! Accounting Flask Project
 | 
			
		||||
form.html: The transfer transaction form
 | 
			
		||||
form.html: The transfer voucher form
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2023 imacat.
 | 
			
		||||
 | 
			
		||||
@@ -19,7 +19,7 @@ form.html: The transfer transaction form
 | 
			
		||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
			
		||||
First written: 2023/2/25
 | 
			
		||||
#}
 | 
			
		||||
{% extends "accounting/transaction/include/form.html" %}
 | 
			
		||||
{% extends "accounting/voucher/include/form.html" %}
 | 
			
		||||
 | 
			
		||||
{% block currency_sub_forms %}
 | 
			
		||||
  {% if form.currencies %}
 | 
			
		||||
@@ -36,7 +36,7 @@ First written: 2023/2/25
 | 
			
		||||
              credit_forms = currency_form.credit,
 | 
			
		||||
              credit_errors = currency_form.credit_errors,
 | 
			
		||||
              credit_total = currency_form.form.credit_total|accounting_format_amount %}
 | 
			
		||||
        {% include "accounting/transaction/transfer/include/form-currency-item.html" %}
 | 
			
		||||
        {% include "accounting/voucher/transfer/include/form-currency-item.html" %}
 | 
			
		||||
      {% endwith %}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
  {% else %}
 | 
			
		||||
@@ -45,24 +45,24 @@ First written: 2023/2/25
 | 
			
		||||
            currency_code_data = accounting_default_currency_code(),
 | 
			
		||||
            debit_total = "-",
 | 
			
		||||
            credit_total = "-" %}
 | 
			
		||||
      {% include "accounting/transaction/transfer/include/form-currency-item.html" %}
 | 
			
		||||
      {% include "accounting/voucher/transfer/include/form-currency-item.html" %}
 | 
			
		||||
    {% endwith %}
 | 
			
		||||
  {% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block form_modals %}
 | 
			
		||||
  {% with summary_editor = form.summary_editor.debit %}
 | 
			
		||||
    {% include "accounting/transaction/include/summary-editor-modal.html" %}
 | 
			
		||||
    {% include "accounting/voucher/include/summary-editor-modal.html" %}
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
  {% with summary_editor = form.summary_editor.credit %}
 | 
			
		||||
    {% include "accounting/transaction/include/summary-editor-modal.html" %}
 | 
			
		||||
    {% include "accounting/voucher/include/summary-editor-modal.html" %}
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
  {% with entry_type = "debit",
 | 
			
		||||
          account_options = form.debit_account_options %}
 | 
			
		||||
    {% include "accounting/transaction/include/account-selector-modal.html" %}
 | 
			
		||||
    {% include "accounting/voucher/include/account-selector-modal.html" %}
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
  {% with entry_type = "credit",
 | 
			
		||||
          account_options = form.credit_account_options %}
 | 
			
		||||
    {% include "accounting/transaction/include/account-selector-modal.html" %}
 | 
			
		||||
    {% include "accounting/voucher/include/account-selector-modal.html" %}
 | 
			
		||||
  {% endwith %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -1,92 +0,0 @@
 | 
			
		||||
# The Mia! Accounting Flask Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
			
		||||
 | 
			
		||||
#  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.
 | 
			
		||||
"""The reorder forms for the transaction management.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from datetime import date
 | 
			
		||||
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from flask import request
 | 
			
		||||
 | 
			
		||||
from accounting import db
 | 
			
		||||
from accounting.models import Transaction
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def sort_transactions_in(txn_date: date, exclude: int | None = None) -> None:
 | 
			
		||||
    """Sorts the transactions under a date after changing the date or deleting
 | 
			
		||||
    a transaction.
 | 
			
		||||
 | 
			
		||||
    :param txn_date: The date of the transaction.
 | 
			
		||||
    :param exclude: The transaction ID to exclude.
 | 
			
		||||
    :return: None.
 | 
			
		||||
    """
 | 
			
		||||
    conditions: list[sa.BinaryExpression] = [Transaction.date == txn_date]
 | 
			
		||||
    if exclude is not None:
 | 
			
		||||
        conditions.append(Transaction.id != exclude)
 | 
			
		||||
    transactions: list[Transaction] = Transaction.query\
 | 
			
		||||
        .filter(*conditions)\
 | 
			
		||||
        .order_by(Transaction.no).all()
 | 
			
		||||
    for i in range(len(transactions)):
 | 
			
		||||
        if transactions[i].no != i + 1:
 | 
			
		||||
            transactions[i].no = i + 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionReorderForm:
 | 
			
		||||
    """The form to reorder the transactions."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, txn_date: date):
 | 
			
		||||
        """Constructs the form to reorder the transactions in a day.
 | 
			
		||||
 | 
			
		||||
        :param txn_date: The date.
 | 
			
		||||
        """
 | 
			
		||||
        self.date: date = txn_date
 | 
			
		||||
        self.is_modified: bool = False
 | 
			
		||||
 | 
			
		||||
    def save_order(self) -> None:
 | 
			
		||||
        """Saves the order of the account.
 | 
			
		||||
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        transactions: list[Transaction] = Transaction.query\
 | 
			
		||||
            .filter(Transaction.date == self.date).all()
 | 
			
		||||
 | 
			
		||||
        # Collects the specified order.
 | 
			
		||||
        orders: dict[Transaction, int] = {}
 | 
			
		||||
        for txn in transactions:
 | 
			
		||||
            if f"{txn.id}-no" in request.form:
 | 
			
		||||
                try:
 | 
			
		||||
                    orders[txn] = int(request.form[f"{txn.id}-no"])
 | 
			
		||||
                except ValueError:
 | 
			
		||||
                    pass
 | 
			
		||||
 | 
			
		||||
        # Missing and invalid orders are appended to the end.
 | 
			
		||||
        missing: list[Transaction] \
 | 
			
		||||
            = [x for x in transactions if x not in orders]
 | 
			
		||||
        if len(missing) > 0:
 | 
			
		||||
            next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
 | 
			
		||||
            for txn in missing:
 | 
			
		||||
                orders[txn] = next_no
 | 
			
		||||
 | 
			
		||||
        # Sort by the specified order first, and their original order.
 | 
			
		||||
        transactions.sort(key=lambda x: (orders[x], x.no))
 | 
			
		||||
 | 
			
		||||
        # Update the orders.
 | 
			
		||||
        with db.session.no_autoflush:
 | 
			
		||||
            for i in range(len(transactions)):
 | 
			
		||||
                if transactions[i].no != i + 1:
 | 
			
		||||
                    transactions[i].no = i + 1
 | 
			
		||||
                    self.is_modified = True
 | 
			
		||||
@@ -1,326 +0,0 @@
 | 
			
		||||
# The Mia! Accounting Flask Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
 | 
			
		||||
 | 
			
		||||
#  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.
 | 
			
		||||
"""The operators for different transaction types.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import typing as t
 | 
			
		||||
from abc import ABC, abstractmethod
 | 
			
		||||
 | 
			
		||||
from flask import render_template, request, abort
 | 
			
		||||
from flask_wtf import FlaskForm
 | 
			
		||||
 | 
			
		||||
from accounting.models import Transaction
 | 
			
		||||
from accounting.template_globals import default_currency_code
 | 
			
		||||
from accounting.utils.txn_types import TransactionType
 | 
			
		||||
from accounting.transaction.forms import TransactionForm, \
 | 
			
		||||
    IncomeTransactionForm, ExpenseTransactionForm, TransferTransactionForm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionOperator(ABC):
 | 
			
		||||
    """The base transaction operator."""
 | 
			
		||||
    CHECK_ORDER: int = -1
 | 
			
		||||
    """The order when checking the transaction operator."""
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def form(self) -> t.Type[TransactionForm]:
 | 
			
		||||
        """Returns the form class.
 | 
			
		||||
 | 
			
		||||
        :return: The form class.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def render_create_template(self, form: FlaskForm) -> str:
 | 
			
		||||
        """Renders the template for the form to create a transaction.
 | 
			
		||||
 | 
			
		||||
        :param form: The transaction form.
 | 
			
		||||
        :return: the form to create a transaction.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def render_detail_template(self, txn: Transaction) -> str:
 | 
			
		||||
        """Renders the template for the detail page.
 | 
			
		||||
 | 
			
		||||
        :param txn: The transaction.
 | 
			
		||||
        :return: the detail page.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def render_edit_template(self, txn: Transaction, form: FlaskForm) -> str:
 | 
			
		||||
        """Renders the template for the form to edit a transaction.
 | 
			
		||||
 | 
			
		||||
        :param txn: The transaction.
 | 
			
		||||
        :param form: The form.
 | 
			
		||||
        :return: the form to edit a transaction.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def is_my_type(self, txn: Transaction) -> bool:
 | 
			
		||||
        """Checks and returns whether the transaction belongs to the type.
 | 
			
		||||
 | 
			
		||||
        :param txn: The transaction.
 | 
			
		||||
        :return: True if the transaction belongs to the type, or False
 | 
			
		||||
            otherwise.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def _entry_template(self) -> str:
 | 
			
		||||
        """Renders and returns the template for the journal entry sub-form.
 | 
			
		||||
 | 
			
		||||
        :return: The template for the journal entry sub-form.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template(
 | 
			
		||||
            "accounting/transaction/include/form-entry-item.html",
 | 
			
		||||
            currency_index="CURRENCY_INDEX",
 | 
			
		||||
            entry_type="ENTRY_TYPE",
 | 
			
		||||
            entry_index="ENTRY_INDEX")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IncomeTransaction(TransactionOperator):
 | 
			
		||||
    """An income transaction."""
 | 
			
		||||
    CHECK_ORDER: int = 2
 | 
			
		||||
    """The order when checking the transaction operator."""
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def form(self) -> t.Type[TransactionForm]:
 | 
			
		||||
        """Returns the form class.
 | 
			
		||||
 | 
			
		||||
        :return: The form class.
 | 
			
		||||
        """
 | 
			
		||||
        return IncomeTransactionForm
 | 
			
		||||
 | 
			
		||||
    def render_create_template(self, form: IncomeTransactionForm) -> str:
 | 
			
		||||
        """Renders the template for the form to create a transaction.
 | 
			
		||||
 | 
			
		||||
        :param form: The transaction form.
 | 
			
		||||
        :return: the form to create a transaction.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/transaction/income/create.html",
 | 
			
		||||
                               form=form,
 | 
			
		||||
                               txn_type=TransactionType.CASH_INCOME,
 | 
			
		||||
                               currency_template=self.__currency_template,
 | 
			
		||||
                               entry_template=self._entry_template)
 | 
			
		||||
 | 
			
		||||
    def render_detail_template(self, txn: Transaction) -> str:
 | 
			
		||||
        """Renders the template for the detail page.
 | 
			
		||||
 | 
			
		||||
        :param txn: The transaction.
 | 
			
		||||
        :return: the detail page.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/transaction/income/detail.html",
 | 
			
		||||
                               obj=txn)
 | 
			
		||||
 | 
			
		||||
    def render_edit_template(self, txn: Transaction,
 | 
			
		||||
                             form: IncomeTransactionForm) -> str:
 | 
			
		||||
        """Renders the template for the form to edit a transaction.
 | 
			
		||||
 | 
			
		||||
        :param txn: The transaction.
 | 
			
		||||
        :param form: The form.
 | 
			
		||||
        :return: the form to edit a transaction.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/transaction/income/edit.html",
 | 
			
		||||
                               txn=txn, form=form,
 | 
			
		||||
                               currency_template=self.__currency_template,
 | 
			
		||||
                               entry_template=self._entry_template)
 | 
			
		||||
 | 
			
		||||
    def is_my_type(self, txn: Transaction) -> bool:
 | 
			
		||||
        """Checks and returns whether the transaction belongs to the type.
 | 
			
		||||
 | 
			
		||||
        :param txn: The transaction.
 | 
			
		||||
        :return: True if the transaction belongs to the type, or False
 | 
			
		||||
            otherwise.
 | 
			
		||||
        """
 | 
			
		||||
        return txn.is_cash_income
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def __currency_template(self) -> str:
 | 
			
		||||
        """Renders and returns the template for the currency sub-form.
 | 
			
		||||
 | 
			
		||||
        :return: The template for the currency sub-form.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template(
 | 
			
		||||
            "accounting/transaction/income/include/form-currency-item.html",
 | 
			
		||||
            currency_index="CURRENCY_INDEX",
 | 
			
		||||
            currency_code_data=default_currency_code(),
 | 
			
		||||
            credit_total="-")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExpenseTransaction(TransactionOperator):
 | 
			
		||||
    """An expense transaction."""
 | 
			
		||||
    CHECK_ORDER: int = 1
 | 
			
		||||
    """The order when checking the transaction operator."""
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def form(self) -> t.Type[TransactionForm]:
 | 
			
		||||
        """Returns the form class.
 | 
			
		||||
 | 
			
		||||
        :return: The form class.
 | 
			
		||||
        """
 | 
			
		||||
        return ExpenseTransactionForm
 | 
			
		||||
 | 
			
		||||
    def render_create_template(self, form: ExpenseTransactionForm) -> str:
 | 
			
		||||
        """Renders the template for the form to create a transaction.
 | 
			
		||||
 | 
			
		||||
        :param form: The transaction form.
 | 
			
		||||
        :return: the form to create a transaction.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/transaction/expense/create.html",
 | 
			
		||||
                               form=form,
 | 
			
		||||
                               txn_type=TransactionType.CASH_EXPENSE,
 | 
			
		||||
                               currency_template=self.__currency_template,
 | 
			
		||||
                               entry_template=self._entry_template)
 | 
			
		||||
 | 
			
		||||
    def render_detail_template(self, txn: Transaction) -> str:
 | 
			
		||||
        """Renders the template for the detail page.
 | 
			
		||||
 | 
			
		||||
        :param txn: The transaction.
 | 
			
		||||
        :return: the detail page.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/transaction/expense/detail.html",
 | 
			
		||||
                               obj=txn)
 | 
			
		||||
 | 
			
		||||
    def render_edit_template(self, txn: Transaction,
 | 
			
		||||
                             form: ExpenseTransactionForm) -> str:
 | 
			
		||||
        """Renders the template for the form to edit a transaction.
 | 
			
		||||
 | 
			
		||||
        :param txn: The transaction.
 | 
			
		||||
        :param form: The form.
 | 
			
		||||
        :return: the form to edit a transaction.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/transaction/expense/edit.html",
 | 
			
		||||
                               txn=txn, form=form,
 | 
			
		||||
                               currency_template=self.__currency_template,
 | 
			
		||||
                               entry_template=self._entry_template)
 | 
			
		||||
 | 
			
		||||
    def is_my_type(self, txn: Transaction) -> bool:
 | 
			
		||||
        """Checks and returns whether the transaction belongs to the type.
 | 
			
		||||
 | 
			
		||||
        :param txn: The transaction.
 | 
			
		||||
        :return: True if the transaction belongs to the type, or False
 | 
			
		||||
            otherwise.
 | 
			
		||||
        """
 | 
			
		||||
        return txn.is_cash_expense
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def __currency_template(self) -> str:
 | 
			
		||||
        """Renders and returns the template for the currency sub-form.
 | 
			
		||||
 | 
			
		||||
        :return: The template for the currency sub-form.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template(
 | 
			
		||||
            "accounting/transaction/expense/include/form-currency-item.html",
 | 
			
		||||
            currency_index="CURRENCY_INDEX",
 | 
			
		||||
            currency_code_data=default_currency_code(),
 | 
			
		||||
            debit_total="-")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransferTransaction(TransactionOperator):
 | 
			
		||||
    """A transfer transaction."""
 | 
			
		||||
    CHECK_ORDER: int = 3
 | 
			
		||||
    """The order when checking the transaction operator."""
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def form(self) -> t.Type[TransactionForm]:
 | 
			
		||||
        """Returns the form class.
 | 
			
		||||
 | 
			
		||||
        :return: The form class.
 | 
			
		||||
        """
 | 
			
		||||
        return TransferTransactionForm
 | 
			
		||||
 | 
			
		||||
    def render_create_template(self, form: TransferTransactionForm) -> str:
 | 
			
		||||
        """Renders the template for the form to create a transaction.
 | 
			
		||||
 | 
			
		||||
        :param form: The transaction form.
 | 
			
		||||
        :return: the form to create a transaction.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/transaction/transfer/create.html",
 | 
			
		||||
                               form=form,
 | 
			
		||||
                               txn_type=TransactionType.TRANSFER,
 | 
			
		||||
                               currency_template=self.__currency_template,
 | 
			
		||||
                               entry_template=self._entry_template)
 | 
			
		||||
 | 
			
		||||
    def render_detail_template(self, txn: Transaction) -> str:
 | 
			
		||||
        """Renders the template for the detail page.
 | 
			
		||||
 | 
			
		||||
        :param txn: The transaction.
 | 
			
		||||
        :return: the detail page.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/transaction/transfer/detail.html",
 | 
			
		||||
                               obj=txn)
 | 
			
		||||
 | 
			
		||||
    def render_edit_template(self, txn: Transaction,
 | 
			
		||||
                             form: TransferTransactionForm) -> str:
 | 
			
		||||
        """Renders the template for the form to edit a transaction.
 | 
			
		||||
 | 
			
		||||
        :param txn: The transaction.
 | 
			
		||||
        :param form: The form.
 | 
			
		||||
        :return: the form to edit a transaction.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/transaction/transfer/edit.html",
 | 
			
		||||
                               txn=txn, form=form,
 | 
			
		||||
                               currency_template=self.__currency_template,
 | 
			
		||||
                               entry_template=self._entry_template)
 | 
			
		||||
 | 
			
		||||
    def is_my_type(self, txn: Transaction) -> bool:
 | 
			
		||||
        """Checks and returns whether the transaction belongs to the type.
 | 
			
		||||
 | 
			
		||||
        :param txn: The transaction.
 | 
			
		||||
        :return: True if the transaction belongs to the type, or False
 | 
			
		||||
            otherwise.
 | 
			
		||||
        """
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def __currency_template(self) -> str:
 | 
			
		||||
        """Renders and returns the template for the currency sub-form.
 | 
			
		||||
 | 
			
		||||
        :return: The template for the currency sub-form.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template(
 | 
			
		||||
            "accounting/transaction/transfer/include/form-currency-item.html",
 | 
			
		||||
            currency_index="CURRENCY_INDEX",
 | 
			
		||||
            currency_code_data=default_currency_code(),
 | 
			
		||||
            debit_total="-", credit_total="-")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
TXN_TYPE_TO_OP: dict[TransactionType, TransactionOperator] \
 | 
			
		||||
    = {TransactionType.CASH_INCOME: IncomeTransaction(),
 | 
			
		||||
       TransactionType.CASH_EXPENSE: ExpenseTransaction(),
 | 
			
		||||
       TransactionType.TRANSFER: TransferTransaction()}
 | 
			
		||||
"""The map from the transaction types to their operators."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_txn_op(txn: Transaction, is_check_as: bool = False) \
 | 
			
		||||
        -> TransactionOperator:
 | 
			
		||||
    """Returns the transaction operator that may be specified in the "as" query
 | 
			
		||||
    parameter.  If it is not specified, check the transaction type from the
 | 
			
		||||
    transaction.
 | 
			
		||||
 | 
			
		||||
    :param txn: The transaction.
 | 
			
		||||
    :param is_check_as: True to check the "as" parameter, or False otherwise.
 | 
			
		||||
    :return: None.
 | 
			
		||||
    """
 | 
			
		||||
    if is_check_as and "as" in request.args:
 | 
			
		||||
        type_dict: dict[str, TransactionType] \
 | 
			
		||||
            = {x.value: x for x in TransactionType}
 | 
			
		||||
        if request.args["as"] not in type_dict:
 | 
			
		||||
            abort(404)
 | 
			
		||||
        return TXN_TYPE_TO_OP[type_dict[request.args["as"]]]
 | 
			
		||||
    for txn_type in sorted(TXN_TYPE_TO_OP.values(),
 | 
			
		||||
                           key=lambda x: x.CHECK_ORDER):
 | 
			
		||||
        if txn_type.is_my_type(txn):
 | 
			
		||||
            return txn_type
 | 
			
		||||
@@ -1,222 +0,0 @@
 | 
			
		||||
# The Mia! Accounting Flask Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
 | 
			
		||||
 | 
			
		||||
#  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.
 | 
			
		||||
"""The views for the transaction management.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from datetime import date
 | 
			
		||||
from urllib.parse import parse_qsl, urlencode
 | 
			
		||||
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from flask import Blueprint, render_template, session, redirect, request, \
 | 
			
		||||
    flash, url_for
 | 
			
		||||
from werkzeug.datastructures import ImmutableMultiDict
 | 
			
		||||
 | 
			
		||||
from accounting import db
 | 
			
		||||
from accounting.locale import lazy_gettext
 | 
			
		||||
from accounting.models import Transaction
 | 
			
		||||
from accounting.utils.cast import s
 | 
			
		||||
from accounting.utils.flash_errors import flash_form_errors
 | 
			
		||||
from accounting.utils.next_uri import inherit_next, or_next
 | 
			
		||||
from accounting.utils.permission import has_permission, can_view, can_edit
 | 
			
		||||
from accounting.utils.txn_types import TransactionType
 | 
			
		||||
from accounting.utils.user import get_current_user_pk
 | 
			
		||||
from .forms import sort_transactions_in, TransactionReorderForm
 | 
			
		||||
from .template_filters import with_type, to_transfer, format_amount_input, \
 | 
			
		||||
    text2html
 | 
			
		||||
from .utils.operators import TransactionOperator, TXN_TYPE_TO_OP, get_txn_op
 | 
			
		||||
 | 
			
		||||
bp: Blueprint = Blueprint("transaction", __name__)
 | 
			
		||||
"""The view blueprint for the transaction management."""
 | 
			
		||||
bp.add_app_template_filter(with_type, "accounting_txn_with_type")
 | 
			
		||||
bp.add_app_template_filter(to_transfer, "accounting_txn_to_transfer")
 | 
			
		||||
bp.add_app_template_filter(format_amount_input,
 | 
			
		||||
                           "accounting_txn_format_amount_input")
 | 
			
		||||
bp.add_app_template_filter(text2html, "accounting_txn_text2html")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.get("/create/<transactionType:txn_type>", endpoint="create")
 | 
			
		||||
@has_permission(can_edit)
 | 
			
		||||
def show_add_transaction_form(txn_type: TransactionType) -> str:
 | 
			
		||||
    """Shows the form to add a transaction.
 | 
			
		||||
 | 
			
		||||
    :param txn_type: The transaction type.
 | 
			
		||||
    :return: The form to add a transaction.
 | 
			
		||||
    """
 | 
			
		||||
    txn_op: TransactionOperator = TXN_TYPE_TO_OP[txn_type]
 | 
			
		||||
    form: txn_op.form
 | 
			
		||||
    if "form" in session:
 | 
			
		||||
        form = txn_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
 | 
			
		||||
        del session["form"]
 | 
			
		||||
        form.validate()
 | 
			
		||||
    else:
 | 
			
		||||
        form = txn_op.form()
 | 
			
		||||
        form.date.data = date.today()
 | 
			
		||||
    return txn_op.render_create_template(form)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.post("/store/<transactionType:txn_type>", endpoint="store")
 | 
			
		||||
@has_permission(can_edit)
 | 
			
		||||
def add_transaction(txn_type: TransactionType) -> redirect:
 | 
			
		||||
    """Adds a transaction.
 | 
			
		||||
 | 
			
		||||
    :param txn_type: The transaction type.
 | 
			
		||||
    :return: The redirection to the transaction detail on success, or the
 | 
			
		||||
        transaction creation form on error.
 | 
			
		||||
    """
 | 
			
		||||
    txn_op: TransactionOperator = TXN_TYPE_TO_OP[txn_type]
 | 
			
		||||
    form: txn_op.form = txn_op.form(request.form)
 | 
			
		||||
    if not form.validate():
 | 
			
		||||
        flash_form_errors(form)
 | 
			
		||||
        session["form"] = urlencode(list(request.form.items()))
 | 
			
		||||
        return redirect(inherit_next(with_type(
 | 
			
		||||
            url_for("accounting.transaction.create", txn_type=txn_type))))
 | 
			
		||||
    txn: Transaction = Transaction()
 | 
			
		||||
    form.populate_obj(txn)
 | 
			
		||||
    db.session.add(txn)
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
    flash(s(lazy_gettext("The transaction is added successfully")), "success")
 | 
			
		||||
    return redirect(inherit_next(__get_detail_uri(txn)))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.get("/<transaction:txn>", endpoint="detail")
 | 
			
		||||
@has_permission(can_view)
 | 
			
		||||
def show_transaction_detail(txn: Transaction) -> str:
 | 
			
		||||
    """Shows the transaction detail.
 | 
			
		||||
 | 
			
		||||
    :param txn: The transaction.
 | 
			
		||||
    :return: The detail.
 | 
			
		||||
    """
 | 
			
		||||
    txn_op: TransactionOperator = get_txn_op(txn)
 | 
			
		||||
    return txn_op.render_detail_template(txn)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.get("/<transaction:txn>/edit", endpoint="edit")
 | 
			
		||||
@has_permission(can_edit)
 | 
			
		||||
def show_transaction_edit_form(txn: Transaction) -> str:
 | 
			
		||||
    """Shows the form to edit a transaction.
 | 
			
		||||
 | 
			
		||||
    :param txn: The transaction.
 | 
			
		||||
    :return: The form to edit the transaction.
 | 
			
		||||
    """
 | 
			
		||||
    txn_op: TransactionOperator = get_txn_op(txn, is_check_as=True)
 | 
			
		||||
    form: txn_op.form
 | 
			
		||||
    if "form" in session:
 | 
			
		||||
        form = txn_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
 | 
			
		||||
        del session["form"]
 | 
			
		||||
        form.obj = txn
 | 
			
		||||
        form.validate()
 | 
			
		||||
    else:
 | 
			
		||||
        form = txn_op.form(obj=txn)
 | 
			
		||||
    return txn_op.render_edit_template(txn, form)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.post("/<transaction:txn>/update", endpoint="update")
 | 
			
		||||
@has_permission(can_edit)
 | 
			
		||||
def update_transaction(txn: Transaction) -> redirect:
 | 
			
		||||
    """Updates a transaction.
 | 
			
		||||
 | 
			
		||||
    :param txn: The transaction.
 | 
			
		||||
    :return: The redirection to the transaction detail on success, or the
 | 
			
		||||
        transaction edit form on error.
 | 
			
		||||
    """
 | 
			
		||||
    txn_op: TransactionOperator = get_txn_op(txn, is_check_as=True)
 | 
			
		||||
    form: txn_op.form = txn_op.form(request.form)
 | 
			
		||||
    form.obj = txn
 | 
			
		||||
    if not form.validate():
 | 
			
		||||
        flash_form_errors(form)
 | 
			
		||||
        session["form"] = urlencode(list(request.form.items()))
 | 
			
		||||
        return redirect(inherit_next(with_type(
 | 
			
		||||
            url_for("accounting.transaction.edit", txn=txn))))
 | 
			
		||||
    with db.session.no_autoflush:
 | 
			
		||||
        form.populate_obj(txn)
 | 
			
		||||
    if not form.is_modified:
 | 
			
		||||
        flash(s(lazy_gettext("The transaction was not modified.")), "success")
 | 
			
		||||
        return redirect(inherit_next(__get_detail_uri(txn)))
 | 
			
		||||
    txn.updated_by_id = get_current_user_pk()
 | 
			
		||||
    txn.updated_at = sa.func.now()
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
    flash(s(lazy_gettext("The transaction is updated successfully.")),
 | 
			
		||||
          "success")
 | 
			
		||||
    return redirect(inherit_next(__get_detail_uri(txn)))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.post("/<transaction:txn>/delete", endpoint="delete")
 | 
			
		||||
@has_permission(can_edit)
 | 
			
		||||
def delete_transaction(txn: Transaction) -> redirect:
 | 
			
		||||
    """Deletes a transaction.
 | 
			
		||||
 | 
			
		||||
    :param txn: The transaction.
 | 
			
		||||
    :return: The redirection to the transaction list on success, or the
 | 
			
		||||
        transaction detail on error.
 | 
			
		||||
    """
 | 
			
		||||
    txn.delete()
 | 
			
		||||
    sort_transactions_in(txn.date, txn.id)
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
    flash(s(lazy_gettext("The transaction is deleted successfully.")),
 | 
			
		||||
          "success")
 | 
			
		||||
    return redirect(or_next(__get_default_page_uri()))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.get("/dates/<date:txn_date>", endpoint="order")
 | 
			
		||||
@has_permission(can_view)
 | 
			
		||||
def show_transaction_order(txn_date: date) -> str:
 | 
			
		||||
    """Shows the order of the transactions in a same date.
 | 
			
		||||
 | 
			
		||||
    :param txn_date: The date.
 | 
			
		||||
    :return: The order of the transactions in the date.
 | 
			
		||||
    """
 | 
			
		||||
    transactions: list[Transaction] = Transaction.query \
 | 
			
		||||
        .filter(Transaction.date == txn_date) \
 | 
			
		||||
        .order_by(Transaction.no).all()
 | 
			
		||||
    return render_template("accounting/transaction/order.html",
 | 
			
		||||
                           date=txn_date, list=transactions)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.post("/dates/<date:txn_date>", endpoint="sort")
 | 
			
		||||
@has_permission(can_edit)
 | 
			
		||||
def sort_transactions(txn_date: date) -> redirect:
 | 
			
		||||
    """Reorders the transactions in a date.
 | 
			
		||||
 | 
			
		||||
    :param txn_date: The date.
 | 
			
		||||
    :return: The redirection to the incoming account or the account list.  The
 | 
			
		||||
        reordering operation does not fail.
 | 
			
		||||
    """
 | 
			
		||||
    form: TransactionReorderForm = TransactionReorderForm(txn_date)
 | 
			
		||||
    form.save_order()
 | 
			
		||||
    if not form.is_modified:
 | 
			
		||||
        flash(s(lazy_gettext("The order was not modified.")), "success")
 | 
			
		||||
        return redirect(or_next(__get_default_page_uri()))
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
    flash(s(lazy_gettext("The order is updated successfully.")), "success")
 | 
			
		||||
    return redirect(or_next(__get_default_page_uri()))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def __get_detail_uri(txn: Transaction) -> str:
 | 
			
		||||
    """Returns the detail URI of a transaction.
 | 
			
		||||
 | 
			
		||||
    :param txn: The transaction.
 | 
			
		||||
    :return: The detail URI of the transaction.
 | 
			
		||||
    """
 | 
			
		||||
    return url_for("accounting.transaction.detail", txn=txn)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def __get_default_page_uri() -> str:
 | 
			
		||||
    """Returns the URI for the default page.
 | 
			
		||||
 | 
			
		||||
    :return: The URI for the default page.
 | 
			
		||||
    """
 | 
			
		||||
    return url_for("accounting.report.default")
 | 
			
		||||
@@ -14,17 +14,17 @@
 | 
			
		||||
#  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 transaction types.
 | 
			
		||||
"""The voucher types.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from enum import Enum
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionType(Enum):
 | 
			
		||||
    """The transaction types."""
 | 
			
		||||
    CASH_INCOME: str = "income"
 | 
			
		||||
    """The cash income transaction."""
 | 
			
		||||
    CASH_EXPENSE: str = "expense"
 | 
			
		||||
    """The cash expense transaction."""
 | 
			
		||||
class VoucherType(Enum):
 | 
			
		||||
    """The voucher types."""
 | 
			
		||||
    CASH_RECEIPT: str = "receipt"
 | 
			
		||||
    """The cash receipt voucher."""
 | 
			
		||||
    CASH_DISBURSEMENT: str = "disbursement"
 | 
			
		||||
    """The cash disbursement voucher."""
 | 
			
		||||
    TRANSFER: str = "transfer"
 | 
			
		||||
    """The transfer transaction."""
 | 
			
		||||
    """The transfer voucher."""
 | 
			
		||||
@@ -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 transaction management.
 | 
			
		||||
"""The voucher management.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from flask import Flask, Blueprint
 | 
			
		||||
@@ -27,11 +27,11 @@ def init_app(app: Flask, bp: Blueprint) -> None:
 | 
			
		||||
    :param bp: The blueprint of the accounting application.
 | 
			
		||||
    :return: None.
 | 
			
		||||
    """
 | 
			
		||||
    from .converters import TransactionConverter, TransactionTypeConverter, \
 | 
			
		||||
    from .converters import VoucherConverter, VoucherTypeConverter, \
 | 
			
		||||
        DateConverter
 | 
			
		||||
    app.url_map.converters["transaction"] = TransactionConverter
 | 
			
		||||
    app.url_map.converters["transactionType"] = TransactionTypeConverter
 | 
			
		||||
    app.url_map.converters["voucher"] = VoucherConverter
 | 
			
		||||
    app.url_map.converters["voucherType"] = VoucherTypeConverter
 | 
			
		||||
    app.url_map.converters["date"] = DateConverter
 | 
			
		||||
 | 
			
		||||
    from .views import bp as transaction_bp
 | 
			
		||||
    bp.register_blueprint(transaction_bp, url_prefix="/transactions")
 | 
			
		||||
    from .views import bp as voucher_bp
 | 
			
		||||
    bp.register_blueprint(voucher_bp, url_prefix="/vouchers")
 | 
			
		||||
@@ -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 path converters for the transaction management.
 | 
			
		||||
"""The path converters for the voucher management.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from datetime import date
 | 
			
		||||
@@ -23,61 +23,59 @@ from flask import abort
 | 
			
		||||
from sqlalchemy.orm import selectinload
 | 
			
		||||
from werkzeug.routing import BaseConverter
 | 
			
		||||
 | 
			
		||||
from accounting.models import Transaction, JournalEntry
 | 
			
		||||
from accounting.utils.txn_types import TransactionType
 | 
			
		||||
from accounting.models import Voucher, JournalEntry
 | 
			
		||||
from accounting.utils.voucher_types import VoucherType
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionConverter(BaseConverter):
 | 
			
		||||
    """The transaction converter to convert the transaction ID from and to the
 | 
			
		||||
    corresponding transaction in the routes."""
 | 
			
		||||
class VoucherConverter(BaseConverter):
 | 
			
		||||
    """The voucher converter to convert the voucher ID from and to the
 | 
			
		||||
    corresponding voucher in the routes."""
 | 
			
		||||
 | 
			
		||||
    def to_python(self, value: str) -> Transaction:
 | 
			
		||||
        """Converts a transaction ID to a transaction.
 | 
			
		||||
    def to_python(self, value: str) -> Voucher:
 | 
			
		||||
        """Converts a voucher ID to a voucher.
 | 
			
		||||
 | 
			
		||||
        :param value: The transaction ID.
 | 
			
		||||
        :return: The corresponding transaction.
 | 
			
		||||
        :param value: The voucher ID.
 | 
			
		||||
        :return: The corresponding voucher.
 | 
			
		||||
        """
 | 
			
		||||
        transaction: Transaction | None = Transaction.query\
 | 
			
		||||
            .join(JournalEntry)\
 | 
			
		||||
            .filter(Transaction.id == value)\
 | 
			
		||||
            .options(selectinload(Transaction.entries)
 | 
			
		||||
        voucher: Voucher | None = Voucher.query.join(JournalEntry)\
 | 
			
		||||
            .filter(Voucher.id == value)\
 | 
			
		||||
            .options(selectinload(Voucher.entries)
 | 
			
		||||
                     .selectinload(JournalEntry.offsets)
 | 
			
		||||
                     .selectinload(JournalEntry.transaction))\
 | 
			
		||||
                     .selectinload(JournalEntry.voucher))\
 | 
			
		||||
            .first()
 | 
			
		||||
        if transaction is None:
 | 
			
		||||
        if voucher is None:
 | 
			
		||||
            abort(404)
 | 
			
		||||
        return transaction
 | 
			
		||||
        return voucher
 | 
			
		||||
 | 
			
		||||
    def to_url(self, value: Transaction) -> str:
 | 
			
		||||
        """Converts a transaction to its ID.
 | 
			
		||||
    def to_url(self, value: Voucher) -> str:
 | 
			
		||||
        """Converts a voucher to its ID.
 | 
			
		||||
 | 
			
		||||
        :param value: The transaction.
 | 
			
		||||
        :param value: The voucher.
 | 
			
		||||
        :return: The ID.
 | 
			
		||||
        """
 | 
			
		||||
        return str(value.id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionTypeConverter(BaseConverter):
 | 
			
		||||
    """The transaction converter to convert the transaction type ID from and to
 | 
			
		||||
    the corresponding transaction type in the routes."""
 | 
			
		||||
class VoucherTypeConverter(BaseConverter):
 | 
			
		||||
    """The voucher converter to convert the voucher type ID from and to the
 | 
			
		||||
    corresponding voucher type in the routes."""
 | 
			
		||||
 | 
			
		||||
    def to_python(self, value: str) -> TransactionType:
 | 
			
		||||
        """Converts a transaction ID to a transaction.
 | 
			
		||||
    def to_python(self, value: str) -> VoucherType:
 | 
			
		||||
        """Converts a voucher ID to a voucher.
 | 
			
		||||
 | 
			
		||||
        :param value: The transaction ID.
 | 
			
		||||
        :return: The corresponding transaction.
 | 
			
		||||
        :param value: The voucher ID.
 | 
			
		||||
        :return: The corresponding voucher type.
 | 
			
		||||
        """
 | 
			
		||||
        type_dict: dict[str, TransactionType] \
 | 
			
		||||
            = {x.value: x for x in TransactionType}
 | 
			
		||||
        txn_type: TransactionType | None = type_dict.get(value)
 | 
			
		||||
        if txn_type is None:
 | 
			
		||||
        type_dict: dict[str, VoucherType] = {x.value: x for x in VoucherType}
 | 
			
		||||
        voucher_type: VoucherType | None = type_dict.get(value)
 | 
			
		||||
        if voucher_type is None:
 | 
			
		||||
            abort(404)
 | 
			
		||||
        return txn_type
 | 
			
		||||
        return voucher_type
 | 
			
		||||
 | 
			
		||||
    def to_url(self, value: TransactionType) -> str:
 | 
			
		||||
        """Converts a transaction type to its ID.
 | 
			
		||||
    def to_url(self, value: VoucherType) -> str:
 | 
			
		||||
        """Converts a voucher type to its ID.
 | 
			
		||||
 | 
			
		||||
        :param value: The transaction type.
 | 
			
		||||
        :param value: The voucher type.
 | 
			
		||||
        :return: The ID.
 | 
			
		||||
        """
 | 
			
		||||
        return str(value.value)
 | 
			
		||||
@@ -14,9 +14,9 @@
 | 
			
		||||
#  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 forms for the transaction management.
 | 
			
		||||
"""The forms for the voucher management.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from .reorder import sort_transactions_in, TransactionReorderForm
 | 
			
		||||
from .transaction import TransactionForm, IncomeTransactionForm, \
 | 
			
		||||
    ExpenseTransactionForm, TransferTransactionForm
 | 
			
		||||
from .reorder import sort_vouchers_in, VoucherReorderForm
 | 
			
		||||
from .voucher import VoucherForm, CashReceiptVoucherForm, \
 | 
			
		||||
    CashDisbursementVoucherForm, TransferVoucherForm
 | 
			
		||||
@@ -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 currency sub-forms for the transaction management.
 | 
			
		||||
"""The currency sub-forms for the voucher management.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from decimal import Decimal
 | 
			
		||||
@@ -29,7 +29,7 @@ from wtforms.validators import DataRequired
 | 
			
		||||
from accounting import db
 | 
			
		||||
from accounting.locale import lazy_gettext
 | 
			
		||||
from accounting.models import Currency, JournalEntry
 | 
			
		||||
from accounting.transaction.utils.offset_alias import offset_alias
 | 
			
		||||
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
 | 
			
		||||
@@ -117,9 +117,9 @@ class IsBalanced:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CurrencyForm(FlaskForm):
 | 
			
		||||
    """The form to create or edit a currency in a transaction."""
 | 
			
		||||
    """The form to create or edit a currency in a voucher."""
 | 
			
		||||
    no = IntegerField()
 | 
			
		||||
    """The order in the transaction."""
 | 
			
		||||
    """The order in the voucher."""
 | 
			
		||||
    code = StringField()
 | 
			
		||||
    """The currency code."""
 | 
			
		||||
    whole_form = BooleanField()
 | 
			
		||||
@@ -132,9 +132,9 @@ class CurrencyForm(FlaskForm):
 | 
			
		||||
        :return: The journal entry sub-forms.
 | 
			
		||||
        """
 | 
			
		||||
        entry_forms: list[JournalEntryForm] = []
 | 
			
		||||
        if isinstance(self, IncomeCurrencyForm):
 | 
			
		||||
        if isinstance(self, CashReceiptCurrencyForm):
 | 
			
		||||
            entry_forms.extend([x.form for x in self.credit])
 | 
			
		||||
        elif isinstance(self, ExpenseCurrencyForm):
 | 
			
		||||
        elif isinstance(self, CashDisbursementCurrencyForm):
 | 
			
		||||
            entry_forms.extend([x.form for x in self.debit])
 | 
			
		||||
        elif isinstance(self, TransferCurrencyForm):
 | 
			
		||||
            entry_forms.extend([x.form for x in self.debit])
 | 
			
		||||
@@ -161,10 +161,10 @@ class CurrencyForm(FlaskForm):
 | 
			
		||||
        return db.session.scalar(select) > 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IncomeCurrencyForm(CurrencyForm):
 | 
			
		||||
    """The form to create or edit a currency in a cash income transaction."""
 | 
			
		||||
class CashReceiptCurrencyForm(CurrencyForm):
 | 
			
		||||
    """The form to create or edit a currency in a cash receipt voucher."""
 | 
			
		||||
    no = IntegerField()
 | 
			
		||||
    """The order in the transaction."""
 | 
			
		||||
    """The order in the voucher."""
 | 
			
		||||
    code = StringField(
 | 
			
		||||
        filters=[strip_text],
 | 
			
		||||
        validators=[CURRENCY_REQUIRED,
 | 
			
		||||
@@ -198,10 +198,10 @@ class IncomeCurrencyForm(CurrencyForm):
 | 
			
		||||
                if isinstance(x, str) or isinstance(x, LazyString)]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExpenseCurrencyForm(CurrencyForm):
 | 
			
		||||
    """The form to create or edit a currency in a cash expense transaction."""
 | 
			
		||||
class CashDisbursementCurrencyForm(CurrencyForm):
 | 
			
		||||
    """The form to create or edit a currency in a cash disbursement voucher."""
 | 
			
		||||
    no = IntegerField()
 | 
			
		||||
    """The order in the transaction."""
 | 
			
		||||
    """The order in the voucher."""
 | 
			
		||||
    code = StringField(
 | 
			
		||||
        filters=[strip_text],
 | 
			
		||||
        validators=[CURRENCY_REQUIRED,
 | 
			
		||||
@@ -236,9 +236,9 @@ class ExpenseCurrencyForm(CurrencyForm):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransferCurrencyForm(CurrencyForm):
 | 
			
		||||
    """The form to create or edit a currency in a transfer transaction."""
 | 
			
		||||
    """The form to create or edit a currency in a transfer voucher."""
 | 
			
		||||
    no = IntegerField()
 | 
			
		||||
    """The order in the transaction."""
 | 
			
		||||
    """The order in the voucher."""
 | 
			
		||||
    code = StringField(
 | 
			
		||||
        filters=[strip_text],
 | 
			
		||||
        validators=[CURRENCY_REQUIRED,
 | 
			
		||||
@@ -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 transaction management.
 | 
			
		||||
"""The journal entry sub-forms for the voucher management.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import re
 | 
			
		||||
@@ -232,8 +232,8 @@ class NotExceedingOriginalEntryNetBalance:
 | 
			
		||||
            return
 | 
			
		||||
        is_debit: bool = isinstance(form, DebitEntryForm)
 | 
			
		||||
        existing_entry_id: set[int] = set()
 | 
			
		||||
        if form.txn_form.obj is not None:
 | 
			
		||||
            existing_entry_id = {x.id for x in form.txn_form.obj.entries}
 | 
			
		||||
        if form.voucher_form.obj is not None:
 | 
			
		||||
            existing_entry_id = {x.id for x in form.voucher_form.obj.entries}
 | 
			
		||||
        offset_total_func: sa.Function = sa.func.sum(sa.case(
 | 
			
		||||
            (be(JournalEntry.is_debit == is_debit), JournalEntry.amount),
 | 
			
		||||
            else_=-JournalEntry.amount))
 | 
			
		||||
@@ -244,7 +244,7 @@ class NotExceedingOriginalEntryNetBalance:
 | 
			
		||||
        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.txn_form.entries
 | 
			
		||||
            [x.amount.data for x in form.voucher_form.entries
 | 
			
		||||
             if x.original_entry_id.data == original_entry.id
 | 
			
		||||
             and x.amount != field and x.amount.data is not None])
 | 
			
		||||
        net_balance: Decimal = original_entry.amount - offset_total_but_form \
 | 
			
		||||
@@ -288,15 +288,15 @@ class JournalEntryForm(FlaskForm):
 | 
			
		||||
    """The amount."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        """Constructs a base transaction form.
 | 
			
		||||
        """Constructs a base journal entry form.
 | 
			
		||||
 | 
			
		||||
        :param args: The arguments.
 | 
			
		||||
        :param kwargs: The keyword arguments.
 | 
			
		||||
        """
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        from .transaction import TransactionForm
 | 
			
		||||
        self.txn_form: TransactionForm | None = None
 | 
			
		||||
        """The source transaction form."""
 | 
			
		||||
        from .voucher import VoucherForm
 | 
			
		||||
        self.voucher_form: VoucherForm | None = None
 | 
			
		||||
        """The source voucher form."""
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def account_text(self) -> str:
 | 
			
		||||
@@ -333,7 +333,7 @@ class JournalEntryForm(FlaskForm):
 | 
			
		||||
        :return: The text representation of the original entry.
 | 
			
		||||
        """
 | 
			
		||||
        return None if self.__original_entry is None \
 | 
			
		||||
            else self.__original_entry.transaction.date
 | 
			
		||||
            else self.__original_entry.voucher.date
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def original_entry_text(self) -> str | None:
 | 
			
		||||
@@ -375,10 +375,10 @@ class JournalEntryForm(FlaskForm):
 | 
			
		||||
                    return []
 | 
			
		||||
                return JournalEntry.query\
 | 
			
		||||
                    .filter(JournalEntry.original_entry_id == self.eid.data)\
 | 
			
		||||
                    .options(selectinload(JournalEntry.transaction),
 | 
			
		||||
                    .options(selectinload(JournalEntry.voucher),
 | 
			
		||||
                             selectinload(JournalEntry.account),
 | 
			
		||||
                             selectinload(JournalEntry.offsets)
 | 
			
		||||
                             .selectinload(JournalEntry.transaction)).all()
 | 
			
		||||
                             .selectinload(JournalEntry.voucher)).all()
 | 
			
		||||
            setattr(self, "__offsets", get_offsets())
 | 
			
		||||
        return getattr(self, "__offsets")
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										92
									
								
								src/accounting/voucher/forms/reorder.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/accounting/voucher/forms/reorder.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
# The Mia! Accounting Flask Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
			
		||||
 | 
			
		||||
#  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.
 | 
			
		||||
"""The reorder forms for the voucher management.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from datetime import date
 | 
			
		||||
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from flask import request
 | 
			
		||||
 | 
			
		||||
from accounting import db
 | 
			
		||||
from accounting.models import Voucher
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def sort_vouchers_in(voucher_date: date, exclude: int | None = None) -> None:
 | 
			
		||||
    """Sorts the vouchers under a date after changing the date or deleting
 | 
			
		||||
    a voucher.
 | 
			
		||||
 | 
			
		||||
    :param voucher_date: The date of the voucher.
 | 
			
		||||
    :param exclude: The voucher ID to exclude.
 | 
			
		||||
    :return: None.
 | 
			
		||||
    """
 | 
			
		||||
    conditions: list[sa.BinaryExpression] = [Voucher.date == voucher_date]
 | 
			
		||||
    if exclude is not None:
 | 
			
		||||
        conditions.append(Voucher.id != exclude)
 | 
			
		||||
    vouchers: list[Voucher] = Voucher.query\
 | 
			
		||||
        .filter(*conditions)\
 | 
			
		||||
        .order_by(Voucher.no).all()
 | 
			
		||||
    for i in range(len(vouchers)):
 | 
			
		||||
        if vouchers[i].no != i + 1:
 | 
			
		||||
            vouchers[i].no = i + 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VoucherReorderForm:
 | 
			
		||||
    """The form to reorder the vouchers."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, voucher_date: date):
 | 
			
		||||
        """Constructs the form to reorder the vouchers in a day.
 | 
			
		||||
 | 
			
		||||
        :param voucher_date: The date.
 | 
			
		||||
        """
 | 
			
		||||
        self.date: date = voucher_date
 | 
			
		||||
        self.is_modified: bool = False
 | 
			
		||||
 | 
			
		||||
    def save_order(self) -> None:
 | 
			
		||||
        """Saves the order of the account.
 | 
			
		||||
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        vouchers: list[Voucher] = Voucher.query\
 | 
			
		||||
            .filter(Voucher.date == self.date).all()
 | 
			
		||||
 | 
			
		||||
        # Collects the specified order.
 | 
			
		||||
        orders: dict[Voucher, int] = {}
 | 
			
		||||
        for voucher in vouchers:
 | 
			
		||||
            if f"{voucher.id}-no" in request.form:
 | 
			
		||||
                try:
 | 
			
		||||
                    orders[voucher] = int(request.form[f"{voucher.id}-no"])
 | 
			
		||||
                except ValueError:
 | 
			
		||||
                    pass
 | 
			
		||||
 | 
			
		||||
        # Missing and invalid orders are appended to the end.
 | 
			
		||||
        missing: list[Voucher] \
 | 
			
		||||
            = [x for x in vouchers if x not in orders]
 | 
			
		||||
        if len(missing) > 0:
 | 
			
		||||
            next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
 | 
			
		||||
            for voucher in missing:
 | 
			
		||||
                orders[voucher] = next_no
 | 
			
		||||
 | 
			
		||||
        # Sort by the specified order first, and their original order.
 | 
			
		||||
        vouchers.sort(key=lambda x: (orders[x], x.no))
 | 
			
		||||
 | 
			
		||||
        # Update the orders.
 | 
			
		||||
        with db.session.no_autoflush:
 | 
			
		||||
            for i in range(len(vouchers)):
 | 
			
		||||
                if vouchers[i].no != i + 1:
 | 
			
		||||
                    vouchers[i].no = i + 1
 | 
			
		||||
                    self.is_modified = True
 | 
			
		||||
@@ -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 transaction forms for the transaction management.
 | 
			
		||||
"""The voucher forms for the voucher management.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import datetime as dt
 | 
			
		||||
@@ -30,19 +30,19 @@ from wtforms.validators import DataRequired, ValidationError
 | 
			
		||||
 | 
			
		||||
from accounting import db
 | 
			
		||||
from accounting.locale import lazy_gettext
 | 
			
		||||
from accounting.models import Transaction, Account, JournalEntry, \
 | 
			
		||||
    TransactionCurrency
 | 
			
		||||
from accounting.transaction.utils.account_option import AccountOption
 | 
			
		||||
from accounting.transaction.utils.original_entries import \
 | 
			
		||||
from accounting.models import Voucher, Account, JournalEntry, \
 | 
			
		||||
    VoucherCurrency
 | 
			
		||||
from accounting.voucher.utils.account_option import AccountOption
 | 
			
		||||
from accounting.voucher.utils.original_entries import \
 | 
			
		||||
    get_selectable_original_entries
 | 
			
		||||
from accounting.transaction.utils.summary_editor import SummaryEditor
 | 
			
		||||
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, IncomeCurrencyForm, ExpenseCurrencyForm, \
 | 
			
		||||
    TransferCurrencyForm
 | 
			
		||||
from .currency import CurrencyForm, CashReceiptCurrencyForm, \
 | 
			
		||||
    CashDisbursementCurrencyForm, TransferCurrencyForm
 | 
			
		||||
from .journal_entry import JournalEntryForm, DebitEntryForm, CreditEntryForm
 | 
			
		||||
from .reorder import sort_transactions_in
 | 
			
		||||
from .reorder import sort_vouchers_in
 | 
			
		||||
 | 
			
		||||
DATE_REQUIRED: DataRequired = DataRequired(
 | 
			
		||||
    lazy_gettext("Please fill in the date."))
 | 
			
		||||
@@ -54,7 +54,7 @@ class NotBeforeOriginalEntries:
 | 
			
		||||
    entries."""
 | 
			
		||||
 | 
			
		||||
    def __call__(self, form: FlaskForm, field: DateField) -> None:
 | 
			
		||||
        assert isinstance(form, TransactionForm)
 | 
			
		||||
        assert isinstance(form, VoucherForm)
 | 
			
		||||
        if field.data is None:
 | 
			
		||||
            return
 | 
			
		||||
        min_date: dt.date | None = form.min_date
 | 
			
		||||
@@ -69,7 +69,7 @@ class NotAfterOffsetEntries:
 | 
			
		||||
    """The validator to check if the date is not after the offset entries."""
 | 
			
		||||
 | 
			
		||||
    def __call__(self, form: FlaskForm, field: DateField) -> None:
 | 
			
		||||
        assert isinstance(form, TransactionForm)
 | 
			
		||||
        assert isinstance(form, VoucherForm)
 | 
			
		||||
        if field.data is None:
 | 
			
		||||
            return
 | 
			
		||||
        max_date: dt.date | None = form.max_date
 | 
			
		||||
@@ -92,7 +92,7 @@ class CannotDeleteOriginalEntriesWithOffset:
 | 
			
		||||
    """The validator to check the original entries with offset."""
 | 
			
		||||
 | 
			
		||||
    def __call__(self, form: FlaskForm, field: FieldList) -> None:
 | 
			
		||||
        assert isinstance(form, TransactionForm)
 | 
			
		||||
        assert isinstance(form, VoucherForm)
 | 
			
		||||
        if form.obj is None:
 | 
			
		||||
            return
 | 
			
		||||
        existing_matched_original_entry_id: set[int] \
 | 
			
		||||
@@ -105,8 +105,8 @@ class CannotDeleteOriginalEntriesWithOffset:
 | 
			
		||||
                    "Journal entries with offset cannot be deleted."))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionForm(FlaskForm):
 | 
			
		||||
    """The base form to create or edit a transaction."""
 | 
			
		||||
class VoucherForm(FlaskForm):
 | 
			
		||||
    """The base form to create or edit a voucher."""
 | 
			
		||||
    date = DateField()
 | 
			
		||||
    """The date."""
 | 
			
		||||
    currencies = FieldList(FormField(CurrencyForm))
 | 
			
		||||
@@ -115,20 +115,20 @@ class TransactionForm(FlaskForm):
 | 
			
		||||
    """The note."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        """Constructs a base transaction form.
 | 
			
		||||
        """Constructs a base voucher form.
 | 
			
		||||
 | 
			
		||||
        :param args: The arguments.
 | 
			
		||||
        :param kwargs: The keyword arguments.
 | 
			
		||||
        """
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.is_modified: bool = False
 | 
			
		||||
        """Whether the transaction is modified during populate_obj()."""
 | 
			
		||||
        """Whether the voucher is modified during populate_obj()."""
 | 
			
		||||
        self.collector: t.Type[JournalEntryCollector] = JournalEntryCollector
 | 
			
		||||
        """The journal entry collector.  The default is the base abstract
 | 
			
		||||
        collector only to provide the correct type.  The subclass forms should
 | 
			
		||||
        provide their own collectors."""
 | 
			
		||||
        self.obj: Transaction | None = kwargs.get("obj")
 | 
			
		||||
        """The transaction, when editing an existing one."""
 | 
			
		||||
        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."""
 | 
			
		||||
        self._is_need_receivable: bool = False
 | 
			
		||||
@@ -139,17 +139,17 @@ class TransactionForm(FlaskForm):
 | 
			
		||||
        """The original entries whose net balances were exceeded by the
 | 
			
		||||
        amounts in the journal entry sub-forms."""
 | 
			
		||||
        for entry in self.entries:
 | 
			
		||||
            entry.txn_form = self
 | 
			
		||||
            entry.voucher_form = self
 | 
			
		||||
 | 
			
		||||
    def populate_obj(self, obj: Transaction) -> None:
 | 
			
		||||
        """Populates the form data into a transaction object.
 | 
			
		||||
    def populate_obj(self, obj: Voucher) -> None:
 | 
			
		||||
        """Populates the form data into a voucher object.
 | 
			
		||||
 | 
			
		||||
        :param obj: The transaction object.
 | 
			
		||||
        :param obj: The voucher object.
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        is_new: bool = obj.id is None
 | 
			
		||||
        if is_new:
 | 
			
		||||
            obj.id = new_id(Transaction)
 | 
			
		||||
            obj.id = new_id(Voucher)
 | 
			
		||||
        self.date: DateField
 | 
			
		||||
        self.__set_date(obj, self.date.data)
 | 
			
		||||
        obj.note = self.note.data
 | 
			
		||||
@@ -183,31 +183,31 @@ class TransactionForm(FlaskForm):
 | 
			
		||||
            entries.extend(currency.entries)
 | 
			
		||||
        return entries
 | 
			
		||||
 | 
			
		||||
    def __set_date(self, obj: Transaction, new_date: dt.date) -> None:
 | 
			
		||||
        """Sets the transaction date and number.
 | 
			
		||||
    def __set_date(self, obj: Voucher, new_date: dt.date) -> None:
 | 
			
		||||
        """Sets the voucher date and number.
 | 
			
		||||
 | 
			
		||||
        :param obj: The transaction object.
 | 
			
		||||
        :param obj: The voucher object.
 | 
			
		||||
        :param new_date: The new date.
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        if obj.date is None or obj.date != new_date:
 | 
			
		||||
            if obj.date is not None:
 | 
			
		||||
                sort_transactions_in(obj.date, obj.id)
 | 
			
		||||
                sort_vouchers_in(obj.date, obj.id)
 | 
			
		||||
            if self.max_date is not None and new_date == self.max_date:
 | 
			
		||||
                db_min_no: int | None = db.session.scalar(
 | 
			
		||||
                    sa.select(sa.func.min(Transaction.no))
 | 
			
		||||
                    .filter(Transaction.date == new_date))
 | 
			
		||||
                    sa.select(sa.func.min(Voucher.no))
 | 
			
		||||
                    .filter(Voucher.date == new_date))
 | 
			
		||||
                if db_min_no is None:
 | 
			
		||||
                    obj.date = new_date
 | 
			
		||||
                    obj.no = 1
 | 
			
		||||
                else:
 | 
			
		||||
                    obj.date = new_date
 | 
			
		||||
                    obj.no = db_min_no - 1
 | 
			
		||||
                    sort_transactions_in(new_date)
 | 
			
		||||
                    sort_vouchers_in(new_date)
 | 
			
		||||
            else:
 | 
			
		||||
                sort_transactions_in(new_date, obj.id)
 | 
			
		||||
                count: int = Transaction.query\
 | 
			
		||||
                    .filter(Transaction.date == new_date).count()
 | 
			
		||||
                sort_vouchers_in(new_date, obj.id)
 | 
			
		||||
                count: int = Voucher.query\
 | 
			
		||||
                    .filter(Voucher.date == new_date).count()
 | 
			
		||||
                obj.date = new_date
 | 
			
		||||
                obj.no = count + 1
 | 
			
		||||
 | 
			
		||||
@@ -285,7 +285,7 @@ class TransactionForm(FlaskForm):
 | 
			
		||||
               if x.original_entry_id.data is not None}
 | 
			
		||||
        if len(original_entry_id) == 0:
 | 
			
		||||
            return None
 | 
			
		||||
        select: sa.Select = sa.select(sa.func.max(Transaction.date))\
 | 
			
		||||
        select: sa.Select = sa.select(sa.func.max(Voucher.date))\
 | 
			
		||||
            .join(JournalEntry).filter(JournalEntry.id.in_(original_entry_id))
 | 
			
		||||
        return db.session.scalar(select)
 | 
			
		||||
 | 
			
		||||
@@ -297,29 +297,29 @@ class TransactionForm(FlaskForm):
 | 
			
		||||
        """
 | 
			
		||||
        entry_id: set[int] = {x.eid.data for x in self.entries
 | 
			
		||||
                              if x.eid.data is not None}
 | 
			
		||||
        select: sa.Select = sa.select(sa.func.min(Transaction.date))\
 | 
			
		||||
        select: sa.Select = sa.select(sa.func.min(Voucher.date))\
 | 
			
		||||
            .join(JournalEntry)\
 | 
			
		||||
            .filter(JournalEntry.original_entry_id.in_(entry_id))
 | 
			
		||||
        return db.session.scalar(select)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
T = t.TypeVar("T", bound=TransactionForm)
 | 
			
		||||
"""A transaction form variant."""
 | 
			
		||||
T = t.TypeVar("T", bound=VoucherForm)
 | 
			
		||||
"""A voucher form variant."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class JournalEntryCollector(t.Generic[T], ABC):
 | 
			
		||||
    """The journal entry collector."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, form: T, obj: Transaction):
 | 
			
		||||
    def __init__(self, form: T, obj: Voucher):
 | 
			
		||||
        """Constructs the journal entry collector.
 | 
			
		||||
 | 
			
		||||
        :param form: The transaction form.
 | 
			
		||||
        :param obj: The transaction.
 | 
			
		||||
        :param form: The voucher form.
 | 
			
		||||
        :param obj: The voucher.
 | 
			
		||||
        """
 | 
			
		||||
        self.form: T = form
 | 
			
		||||
        """The transaction form."""
 | 
			
		||||
        self.__obj: Transaction = obj
 | 
			
		||||
        """The transaction object."""
 | 
			
		||||
        """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] \
 | 
			
		||||
@@ -327,8 +327,8 @@ class JournalEntryCollector(t.Generic[T], ABC):
 | 
			
		||||
        """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.__currencies: list[TransactionCurrency] = obj.currencies
 | 
			
		||||
        """The currencies in the transaction."""
 | 
			
		||||
        self.__currencies: list[VoucherCurrency] = obj.currencies
 | 
			
		||||
        """The currencies in the voucher."""
 | 
			
		||||
        self._debit_no: int = 1
 | 
			
		||||
        """The number index for the debit entries."""
 | 
			
		||||
        self._credit_no: int = 1
 | 
			
		||||
@@ -371,11 +371,11 @@ class JournalEntryCollector(t.Generic[T], ABC):
 | 
			
		||||
    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
 | 
			
		||||
        transaction.
 | 
			
		||||
        voucher.
 | 
			
		||||
 | 
			
		||||
        :param forms: The journal entry forms in the same currency.
 | 
			
		||||
        :param is_debit: True for a cash income transaction, or False for a
 | 
			
		||||
            cash expense transaction.
 | 
			
		||||
        :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.
 | 
			
		||||
        :return: None.
 | 
			
		||||
@@ -441,14 +441,14 @@ class JournalEntryCollector(t.Generic[T], ABC):
 | 
			
		||||
                                  ord_by_form.get(x)))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IncomeTransactionForm(TransactionForm):
 | 
			
		||||
    """The form to create or edit a cash income transaction."""
 | 
			
		||||
class CashReceiptVoucherForm(VoucherForm):
 | 
			
		||||
    """The form to create or edit a cash receipt voucher."""
 | 
			
		||||
    date = DateField(
 | 
			
		||||
        validators=[DATE_REQUIRED,
 | 
			
		||||
                    NotBeforeOriginalEntries(),
 | 
			
		||||
                    NotAfterOffsetEntries()])
 | 
			
		||||
    """The date."""
 | 
			
		||||
    currencies = FieldList(FormField(IncomeCurrencyForm), name="currency",
 | 
			
		||||
    currencies = FieldList(FormField(CashReceiptCurrencyForm), name="currency",
 | 
			
		||||
                           validators=[NeedSomeCurrencies()])
 | 
			
		||||
    """The journal entries categorized by their currencies."""
 | 
			
		||||
    note = TextAreaField(filters=[strip_multiline_text])
 | 
			
		||||
@@ -461,11 +461,11 @@ class IncomeTransactionForm(TransactionForm):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self._is_need_receivable = True
 | 
			
		||||
 | 
			
		||||
        class Collector(JournalEntryCollector[IncomeTransactionForm]):
 | 
			
		||||
            """The journal entry collector for the cash income transactions."""
 | 
			
		||||
        class Collector(JournalEntryCollector[CashReceiptVoucherForm]):
 | 
			
		||||
            """The journal entry collector for the cash receipt vouchers."""
 | 
			
		||||
 | 
			
		||||
            def collect(self) -> None:
 | 
			
		||||
                currencies: list[IncomeCurrencyForm] \
 | 
			
		||||
                currencies: list[CashReceiptCurrencyForm] \
 | 
			
		||||
                    = [x.form for x in self.form.currencies]
 | 
			
		||||
                self._sort_currency_forms(currencies)
 | 
			
		||||
                for currency in currencies:
 | 
			
		||||
@@ -486,14 +486,15 @@ class IncomeTransactionForm(TransactionForm):
 | 
			
		||||
        self.collector = Collector
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExpenseTransactionForm(TransactionForm):
 | 
			
		||||
    """The form to create or edit a cash expense transaction."""
 | 
			
		||||
class CashDisbursementVoucherForm(VoucherForm):
 | 
			
		||||
    """The form to create or edit a cash disbursement voucher."""
 | 
			
		||||
    date = DateField(
 | 
			
		||||
        validators=[DATE_REQUIRED,
 | 
			
		||||
                    NotBeforeOriginalEntries(),
 | 
			
		||||
                    NotAfterOffsetEntries()])
 | 
			
		||||
    """The date."""
 | 
			
		||||
    currencies = FieldList(FormField(ExpenseCurrencyForm), name="currency",
 | 
			
		||||
    currencies = FieldList(FormField(CashDisbursementCurrencyForm),
 | 
			
		||||
                           name="currency",
 | 
			
		||||
                           validators=[NeedSomeCurrencies()])
 | 
			
		||||
    """The journal entries categorized by their currencies."""
 | 
			
		||||
    note = TextAreaField(filters=[strip_multiline_text])
 | 
			
		||||
@@ -506,12 +507,12 @@ class ExpenseTransactionForm(TransactionForm):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self._is_need_payable = True
 | 
			
		||||
 | 
			
		||||
        class Collector(JournalEntryCollector[ExpenseTransactionForm]):
 | 
			
		||||
            """The journal entry collector for the cash expense
 | 
			
		||||
            transactions."""
 | 
			
		||||
        class Collector(JournalEntryCollector[CashDisbursementVoucherForm]):
 | 
			
		||||
            """The journal entry collector for the cash disbursement
 | 
			
		||||
            vouchers."""
 | 
			
		||||
 | 
			
		||||
            def collect(self) -> None:
 | 
			
		||||
                currencies: list[ExpenseCurrencyForm] \
 | 
			
		||||
                currencies: list[CashDisbursementCurrencyForm] \
 | 
			
		||||
                    = [x.form for x in self.form.currencies]
 | 
			
		||||
                self._sort_currency_forms(currencies)
 | 
			
		||||
                for currency in currencies:
 | 
			
		||||
@@ -532,8 +533,8 @@ class ExpenseTransactionForm(TransactionForm):
 | 
			
		||||
        self.collector = Collector
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransferTransactionForm(TransactionForm):
 | 
			
		||||
    """The form to create or edit a transfer transaction."""
 | 
			
		||||
class TransferVoucherForm(VoucherForm):
 | 
			
		||||
    """The form to create or edit a transfer voucher."""
 | 
			
		||||
    date = DateField(
 | 
			
		||||
        validators=[DATE_REQUIRED,
 | 
			
		||||
                    NotBeforeOriginalEntries(),
 | 
			
		||||
@@ -553,8 +554,8 @@ class TransferTransactionForm(TransactionForm):
 | 
			
		||||
        self._is_need_payable = True
 | 
			
		||||
        self._is_need_receivable = True
 | 
			
		||||
 | 
			
		||||
        class Collector(JournalEntryCollector[TransferTransactionForm]):
 | 
			
		||||
            """The journal entry collector for the transfer transactions."""
 | 
			
		||||
        class Collector(JournalEntryCollector[TransferVoucherForm]):
 | 
			
		||||
            """The journal entry collector for the transfer vouchers."""
 | 
			
		||||
 | 
			
		||||
            def collect(self) -> None:
 | 
			
		||||
                currencies: list[TransferCurrencyForm] \
 | 
			
		||||
@@ -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 template filters for the transaction management.
 | 
			
		||||
"""The template filters for the voucher management.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from decimal import Decimal
 | 
			
		||||
@@ -26,10 +26,10 @@ from flask import request
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def with_type(uri: str) -> str:
 | 
			
		||||
    """Adds the transaction type to the URI, if it is specified.
 | 
			
		||||
    """Adds the voucher type to the URI, if it is specified.
 | 
			
		||||
 | 
			
		||||
    :param uri: The URI.
 | 
			
		||||
    :return: The result URL, optionally with the transaction type added.
 | 
			
		||||
    :return: The result URL, optionally with the voucher type added.
 | 
			
		||||
    """
 | 
			
		||||
    if "as" not in request.args:
 | 
			
		||||
        return uri
 | 
			
		||||
@@ -43,10 +43,10 @@ def with_type(uri: str) -> str:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_transfer(uri: str) -> str:
 | 
			
		||||
    """Adds the transfer transaction type to the URI.
 | 
			
		||||
    """Adds the transfer voucher type to the URI.
 | 
			
		||||
 | 
			
		||||
    :param uri: The URI.
 | 
			
		||||
    :return: The result URL, with the transfer transaction type added.
 | 
			
		||||
    :return: The result URL, with the transfer voucher type added.
 | 
			
		||||
    """
 | 
			
		||||
    uri_p: ParseResult = urlparse(uri)
 | 
			
		||||
    params: list[tuple[str, str]] = parse_qsl(uri_p.query)
 | 
			
		||||
@@ -14,6 +14,6 @@
 | 
			
		||||
#  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 utilities for the transaction management.
 | 
			
		||||
"""The utilities for the voucher management.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
@@ -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 account option for the transaction management.
 | 
			
		||||
"""The account option for the voucher management.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from accounting.models import Account
 | 
			
		||||
							
								
								
									
										326
									
								
								src/accounting/voucher/utils/operators.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										326
									
								
								src/accounting/voucher/utils/operators.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,326 @@
 | 
			
		||||
# The Mia! Accounting Flask Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
 | 
			
		||||
 | 
			
		||||
#  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.
 | 
			
		||||
"""The operators for different voucher types.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import typing as t
 | 
			
		||||
from abc import ABC, abstractmethod
 | 
			
		||||
 | 
			
		||||
from flask import render_template, request, abort
 | 
			
		||||
from flask_wtf import FlaskForm
 | 
			
		||||
 | 
			
		||||
from accounting.models import Voucher
 | 
			
		||||
from accounting.template_globals import default_currency_code
 | 
			
		||||
from accounting.utils.voucher_types import VoucherType
 | 
			
		||||
from accounting.voucher.forms import VoucherForm, CashReceiptVoucherForm, \
 | 
			
		||||
    CashDisbursementVoucherForm, TransferVoucherForm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VoucherOperator(ABC):
 | 
			
		||||
    """The base voucher operator."""
 | 
			
		||||
    CHECK_ORDER: int = -1
 | 
			
		||||
    """The order when checking the voucher operator."""
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def form(self) -> t.Type[VoucherForm]:
 | 
			
		||||
        """Returns the form class.
 | 
			
		||||
 | 
			
		||||
        :return: The form class.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def render_create_template(self, form: FlaskForm) -> str:
 | 
			
		||||
        """Renders the template for the form to create a voucher.
 | 
			
		||||
 | 
			
		||||
        :param form: The voucher form.
 | 
			
		||||
        :return: the form to create a voucher.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def render_detail_template(self, voucher: Voucher) -> str:
 | 
			
		||||
        """Renders the template for the detail page.
 | 
			
		||||
 | 
			
		||||
        :param voucher: The voucher.
 | 
			
		||||
        :return: the detail page.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def render_edit_template(self, voucher: Voucher, form: FlaskForm) -> str:
 | 
			
		||||
        """Renders the template for the form to edit a voucher.
 | 
			
		||||
 | 
			
		||||
        :param voucher: The voucher.
 | 
			
		||||
        :param form: The form.
 | 
			
		||||
        :return: the form to edit a voucher.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def is_my_type(self, voucher: Voucher) -> bool:
 | 
			
		||||
        """Checks and returns whether the voucher belongs to the type.
 | 
			
		||||
 | 
			
		||||
        :param voucher: The voucher.
 | 
			
		||||
        :return: True if the voucher belongs to the type, or False
 | 
			
		||||
            otherwise.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def _entry_template(self) -> str:
 | 
			
		||||
        """Renders and returns the template for the journal entry sub-form.
 | 
			
		||||
 | 
			
		||||
        :return: The template for the journal entry sub-form.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template(
 | 
			
		||||
            "accounting/voucher/include/form-entry-item.html",
 | 
			
		||||
            currency_index="CURRENCY_INDEX",
 | 
			
		||||
            entry_type="ENTRY_TYPE",
 | 
			
		||||
            entry_index="ENTRY_INDEX")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CashReceiptVoucher(VoucherOperator):
 | 
			
		||||
    """A cash receipt voucher."""
 | 
			
		||||
    CHECK_ORDER: int = 2
 | 
			
		||||
    """The order when checking the voucher operator."""
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def form(self) -> t.Type[VoucherForm]:
 | 
			
		||||
        """Returns the form class.
 | 
			
		||||
 | 
			
		||||
        :return: The form class.
 | 
			
		||||
        """
 | 
			
		||||
        return CashReceiptVoucherForm
 | 
			
		||||
 | 
			
		||||
    def render_create_template(self, form: CashReceiptVoucherForm) -> str:
 | 
			
		||||
        """Renders the template for the form to create a voucher.
 | 
			
		||||
 | 
			
		||||
        :param form: The voucher form.
 | 
			
		||||
        :return: the form to create a voucher.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/voucher/receipt/create.html",
 | 
			
		||||
                               form=form,
 | 
			
		||||
                               voucher_type=VoucherType.CASH_RECEIPT,
 | 
			
		||||
                               currency_template=self.__currency_template,
 | 
			
		||||
                               entry_template=self._entry_template)
 | 
			
		||||
 | 
			
		||||
    def render_detail_template(self, voucher: Voucher) -> str:
 | 
			
		||||
        """Renders the template for the detail page.
 | 
			
		||||
 | 
			
		||||
        :param voucher: The voucher.
 | 
			
		||||
        :return: the detail page.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/voucher/receipt/detail.html",
 | 
			
		||||
                               obj=voucher)
 | 
			
		||||
 | 
			
		||||
    def render_edit_template(self, voucher: Voucher,
 | 
			
		||||
                             form: CashReceiptVoucherForm) -> str:
 | 
			
		||||
        """Renders the template for the form to edit a voucher.
 | 
			
		||||
 | 
			
		||||
        :param voucher: The voucher.
 | 
			
		||||
        :param form: The form.
 | 
			
		||||
        :return: the form to edit a voucher.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/voucher/receipt/edit.html",
 | 
			
		||||
                               voucher=voucher, form=form,
 | 
			
		||||
                               currency_template=self.__currency_template,
 | 
			
		||||
                               entry_template=self._entry_template)
 | 
			
		||||
 | 
			
		||||
    def is_my_type(self, voucher: Voucher) -> bool:
 | 
			
		||||
        """Checks and returns whether the voucher belongs to the type.
 | 
			
		||||
 | 
			
		||||
        :param voucher: The voucher.
 | 
			
		||||
        :return: True if the voucher belongs to the type, or False
 | 
			
		||||
            otherwise.
 | 
			
		||||
        """
 | 
			
		||||
        return voucher.is_cash_receipt
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def __currency_template(self) -> str:
 | 
			
		||||
        """Renders and returns the template for the currency sub-form.
 | 
			
		||||
 | 
			
		||||
        :return: The template for the currency sub-form.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template(
 | 
			
		||||
            "accounting/voucher/receipt/include/form-currency-item.html",
 | 
			
		||||
            currency_index="CURRENCY_INDEX",
 | 
			
		||||
            currency_code_data=default_currency_code(),
 | 
			
		||||
            credit_total="-")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CashDisbursementVoucher(VoucherOperator):
 | 
			
		||||
    """A cash disbursement voucher."""
 | 
			
		||||
    CHECK_ORDER: int = 1
 | 
			
		||||
    """The order when checking the voucher operator."""
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def form(self) -> t.Type[VoucherForm]:
 | 
			
		||||
        """Returns the form class.
 | 
			
		||||
 | 
			
		||||
        :return: The form class.
 | 
			
		||||
        """
 | 
			
		||||
        return CashDisbursementVoucherForm
 | 
			
		||||
 | 
			
		||||
    def render_create_template(self, form: CashDisbursementVoucherForm) -> str:
 | 
			
		||||
        """Renders the template for the form to create a voucher.
 | 
			
		||||
 | 
			
		||||
        :param form: The voucher form.
 | 
			
		||||
        :return: the form to create a voucher.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/voucher/disbursement/create.html",
 | 
			
		||||
                               form=form,
 | 
			
		||||
                               voucher_type=VoucherType.CASH_DISBURSEMENT,
 | 
			
		||||
                               currency_template=self.__currency_template,
 | 
			
		||||
                               entry_template=self._entry_template)
 | 
			
		||||
 | 
			
		||||
    def render_detail_template(self, voucher: Voucher) -> str:
 | 
			
		||||
        """Renders the template for the detail page.
 | 
			
		||||
 | 
			
		||||
        :param voucher: The voucher.
 | 
			
		||||
        :return: the detail page.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/voucher/disbursement/detail.html",
 | 
			
		||||
                               obj=voucher)
 | 
			
		||||
 | 
			
		||||
    def render_edit_template(self, voucher: Voucher,
 | 
			
		||||
                             form: CashDisbursementVoucherForm) -> str:
 | 
			
		||||
        """Renders the template for the form to edit a voucher.
 | 
			
		||||
 | 
			
		||||
        :param voucher: The voucher.
 | 
			
		||||
        :param form: The form.
 | 
			
		||||
        :return: the form to edit a voucher.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/voucher/disbursement/edit.html",
 | 
			
		||||
                               voucher=voucher, form=form,
 | 
			
		||||
                               currency_template=self.__currency_template,
 | 
			
		||||
                               entry_template=self._entry_template)
 | 
			
		||||
 | 
			
		||||
    def is_my_type(self, voucher: Voucher) -> bool:
 | 
			
		||||
        """Checks and returns whether the voucher belongs to the type.
 | 
			
		||||
 | 
			
		||||
        :param voucher: The voucher.
 | 
			
		||||
        :return: True if the voucher belongs to the type, or False
 | 
			
		||||
            otherwise.
 | 
			
		||||
        """
 | 
			
		||||
        return voucher.is_cash_disbursement
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def __currency_template(self) -> str:
 | 
			
		||||
        """Renders and returns the template for the currency sub-form.
 | 
			
		||||
 | 
			
		||||
        :return: The template for the currency sub-form.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template(
 | 
			
		||||
            "accounting/voucher/disbursement/include/form-currency-item.html",
 | 
			
		||||
            currency_index="CURRENCY_INDEX",
 | 
			
		||||
            currency_code_data=default_currency_code(),
 | 
			
		||||
            debit_total="-")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransferVoucher(VoucherOperator):
 | 
			
		||||
    """A transfer voucher."""
 | 
			
		||||
    CHECK_ORDER: int = 3
 | 
			
		||||
    """The order when checking the voucher operator."""
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def form(self) -> t.Type[VoucherForm]:
 | 
			
		||||
        """Returns the form class.
 | 
			
		||||
 | 
			
		||||
        :return: The form class.
 | 
			
		||||
        """
 | 
			
		||||
        return TransferVoucherForm
 | 
			
		||||
 | 
			
		||||
    def render_create_template(self, form: TransferVoucherForm) -> str:
 | 
			
		||||
        """Renders the template for the form to create a voucher.
 | 
			
		||||
 | 
			
		||||
        :param form: The voucher form.
 | 
			
		||||
        :return: the form to create a voucher.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/voucher/transfer/create.html",
 | 
			
		||||
                               form=form,
 | 
			
		||||
                               voucher_type=VoucherType.TRANSFER,
 | 
			
		||||
                               currency_template=self.__currency_template,
 | 
			
		||||
                               entry_template=self._entry_template)
 | 
			
		||||
 | 
			
		||||
    def render_detail_template(self, voucher: Voucher) -> str:
 | 
			
		||||
        """Renders the template for the detail page.
 | 
			
		||||
 | 
			
		||||
        :param voucher: The voucher.
 | 
			
		||||
        :return: the detail page.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/voucher/transfer/detail.html",
 | 
			
		||||
                               obj=voucher)
 | 
			
		||||
 | 
			
		||||
    def render_edit_template(self, voucher: Voucher,
 | 
			
		||||
                             form: TransferVoucherForm) -> str:
 | 
			
		||||
        """Renders the template for the form to edit a voucher.
 | 
			
		||||
 | 
			
		||||
        :param voucher: The voucher.
 | 
			
		||||
        :param form: The form.
 | 
			
		||||
        :return: the form to edit a voucher.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template("accounting/voucher/transfer/edit.html",
 | 
			
		||||
                               voucher=voucher, form=form,
 | 
			
		||||
                               currency_template=self.__currency_template,
 | 
			
		||||
                               entry_template=self._entry_template)
 | 
			
		||||
 | 
			
		||||
    def is_my_type(self, voucher: Voucher) -> bool:
 | 
			
		||||
        """Checks and returns whether the voucher belongs to the type.
 | 
			
		||||
 | 
			
		||||
        :param voucher: The voucher.
 | 
			
		||||
        :return: True if the voucher belongs to the type, or False
 | 
			
		||||
            otherwise.
 | 
			
		||||
        """
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def __currency_template(self) -> str:
 | 
			
		||||
        """Renders and returns the template for the currency sub-form.
 | 
			
		||||
 | 
			
		||||
        :return: The template for the currency sub-form.
 | 
			
		||||
        """
 | 
			
		||||
        return render_template(
 | 
			
		||||
            "accounting/voucher/transfer/include/form-currency-item.html",
 | 
			
		||||
            currency_index="CURRENCY_INDEX",
 | 
			
		||||
            currency_code_data=default_currency_code(),
 | 
			
		||||
            debit_total="-", credit_total="-")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
VOUCHER_TYPE_TO_OP: dict[VoucherType, VoucherOperator] \
 | 
			
		||||
    = {VoucherType.CASH_RECEIPT: CashReceiptVoucher(),
 | 
			
		||||
       VoucherType.CASH_DISBURSEMENT: CashDisbursementVoucher(),
 | 
			
		||||
       VoucherType.TRANSFER: TransferVoucher()}
 | 
			
		||||
"""The map from the voucher types to their operators."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_voucher_op(voucher: Voucher, is_check_as: bool = False) \
 | 
			
		||||
        -> VoucherOperator:
 | 
			
		||||
    """Returns the voucher operator that may be specified in the "as" query
 | 
			
		||||
    parameter.  If it is not specified, check the voucher type from the
 | 
			
		||||
    voucher.
 | 
			
		||||
 | 
			
		||||
    :param voucher: The voucher.
 | 
			
		||||
    :param is_check_as: True to check the "as" parameter, or False otherwise.
 | 
			
		||||
    :return: None.
 | 
			
		||||
    """
 | 
			
		||||
    if is_check_as and "as" in request.args:
 | 
			
		||||
        type_dict: dict[str, VoucherType] \
 | 
			
		||||
            = {x.value: x for x in VoucherType}
 | 
			
		||||
        if request.args["as"] not in type_dict:
 | 
			
		||||
            abort(404)
 | 
			
		||||
        return VOUCHER_TYPE_TO_OP[type_dict[request.args["as"]]]
 | 
			
		||||
    for voucher_type in sorted(VOUCHER_TYPE_TO_OP.values(),
 | 
			
		||||
                               key=lambda x: x.CHECK_ORDER):
 | 
			
		||||
        if voucher_type.is_my_type(voucher):
 | 
			
		||||
            return voucher_type
 | 
			
		||||
@@ -25,8 +25,8 @@ from sqlalchemy.orm import selectinload
 | 
			
		||||
 | 
			
		||||
from accounting import db
 | 
			
		||||
from accounting.locale import lazy_gettext
 | 
			
		||||
from accounting.models import Account, Transaction, JournalEntry
 | 
			
		||||
from accounting.transaction.forms.journal_entry import JournalEntryForm
 | 
			
		||||
from accounting.models import Account, Voucher, JournalEntry
 | 
			
		||||
from accounting.voucher.forms.journal_entry import JournalEntryForm
 | 
			
		||||
from accounting.utils.cast import be
 | 
			
		||||
from .offset_alias import offset_alias
 | 
			
		||||
 | 
			
		||||
@@ -71,11 +71,11 @@ def get_selectable_original_entries(
 | 
			
		||||
           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}))\
 | 
			
		||||
        .join(Transaction)\
 | 
			
		||||
        .order_by(Transaction.date, JournalEntry.is_debit, JournalEntry.no)\
 | 
			
		||||
        .join(Voucher)\
 | 
			
		||||
        .order_by(Voucher.date, JournalEntry.is_debit, JournalEntry.no)\
 | 
			
		||||
        .options(selectinload(JournalEntry.currency),
 | 
			
		||||
                 selectinload(JournalEntry.account),
 | 
			
		||||
                 selectinload(JournalEntry.transaction)).all()
 | 
			
		||||
                 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]
 | 
			
		||||
							
								
								
									
										221
									
								
								src/accounting/voucher/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								src/accounting/voucher/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,221 @@
 | 
			
		||||
# The Mia! Accounting Flask Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
 | 
			
		||||
 | 
			
		||||
#  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.
 | 
			
		||||
"""The views for the voucher management.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from datetime import date
 | 
			
		||||
from urllib.parse import parse_qsl, urlencode
 | 
			
		||||
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from flask import Blueprint, render_template, session, redirect, request, \
 | 
			
		||||
    flash, url_for
 | 
			
		||||
from werkzeug.datastructures import ImmutableMultiDict
 | 
			
		||||
 | 
			
		||||
from accounting import db
 | 
			
		||||
from accounting.locale import lazy_gettext
 | 
			
		||||
from accounting.models import Voucher
 | 
			
		||||
from accounting.utils.cast import s
 | 
			
		||||
from accounting.utils.flash_errors import flash_form_errors
 | 
			
		||||
from accounting.utils.next_uri import inherit_next, or_next
 | 
			
		||||
from accounting.utils.permission import has_permission, can_view, can_edit
 | 
			
		||||
from accounting.utils.voucher_types import VoucherType
 | 
			
		||||
from accounting.utils.user import get_current_user_pk
 | 
			
		||||
from .forms import sort_vouchers_in, VoucherReorderForm
 | 
			
		||||
from .template_filters import with_type, to_transfer, format_amount_input, \
 | 
			
		||||
    text2html
 | 
			
		||||
from .utils.operators import VoucherOperator, VOUCHER_TYPE_TO_OP, \
 | 
			
		||||
    get_voucher_op
 | 
			
		||||
 | 
			
		||||
bp: Blueprint = Blueprint("voucher", __name__)
 | 
			
		||||
"""The view blueprint for the voucher management."""
 | 
			
		||||
bp.add_app_template_filter(with_type, "accounting_voucher_with_type")
 | 
			
		||||
bp.add_app_template_filter(to_transfer, "accounting_voucher_to_transfer")
 | 
			
		||||
bp.add_app_template_filter(format_amount_input,
 | 
			
		||||
                           "accounting_voucher_format_amount_input")
 | 
			
		||||
bp.add_app_template_filter(text2html, "accounting_voucher_text2html")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.get("/create/<voucherType:voucher_type>", endpoint="create")
 | 
			
		||||
@has_permission(can_edit)
 | 
			
		||||
def show_add_voucher_form(voucher_type: VoucherType) -> str:
 | 
			
		||||
    """Shows the form to add a voucher.
 | 
			
		||||
 | 
			
		||||
    :param voucher_type: The voucher type.
 | 
			
		||||
    :return: The form to add a voucher.
 | 
			
		||||
    """
 | 
			
		||||
    voucher_op: VoucherOperator = VOUCHER_TYPE_TO_OP[voucher_type]
 | 
			
		||||
    form: voucher_op.form
 | 
			
		||||
    if "form" in session:
 | 
			
		||||
        form = voucher_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
 | 
			
		||||
        del session["form"]
 | 
			
		||||
        form.validate()
 | 
			
		||||
    else:
 | 
			
		||||
        form = voucher_op.form()
 | 
			
		||||
        form.date.data = date.today()
 | 
			
		||||
    return voucher_op.render_create_template(form)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.post("/store/<voucherType:voucher_type>", endpoint="store")
 | 
			
		||||
@has_permission(can_edit)
 | 
			
		||||
def add_voucher(voucher_type: VoucherType) -> redirect:
 | 
			
		||||
    """Adds a voucher.
 | 
			
		||||
 | 
			
		||||
    :param voucher_type: The voucher type.
 | 
			
		||||
    :return: The redirection to the voucher detail on success, or the
 | 
			
		||||
        voucher creation form on error.
 | 
			
		||||
    """
 | 
			
		||||
    voucher_op: VoucherOperator = VOUCHER_TYPE_TO_OP[voucher_type]
 | 
			
		||||
    form: voucher_op.form = voucher_op.form(request.form)
 | 
			
		||||
    if not form.validate():
 | 
			
		||||
        flash_form_errors(form)
 | 
			
		||||
        session["form"] = urlencode(list(request.form.items()))
 | 
			
		||||
        return redirect(inherit_next(with_type(
 | 
			
		||||
            url_for("accounting.voucher.create", voucher_type=voucher_type))))
 | 
			
		||||
    voucher: Voucher = Voucher()
 | 
			
		||||
    form.populate_obj(voucher)
 | 
			
		||||
    db.session.add(voucher)
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
    flash(s(lazy_gettext("The voucher is added successfully")), "success")
 | 
			
		||||
    return redirect(inherit_next(__get_detail_uri(voucher)))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.get("/<voucher:voucher>", endpoint="detail")
 | 
			
		||||
@has_permission(can_view)
 | 
			
		||||
def show_voucher_detail(voucher: Voucher) -> str:
 | 
			
		||||
    """Shows the voucher detail.
 | 
			
		||||
 | 
			
		||||
    :param voucher: The voucher.
 | 
			
		||||
    :return: The detail.
 | 
			
		||||
    """
 | 
			
		||||
    voucher_op: VoucherOperator = get_voucher_op(voucher)
 | 
			
		||||
    return voucher_op.render_detail_template(voucher)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.get("/<voucher:voucher>/edit", endpoint="edit")
 | 
			
		||||
@has_permission(can_edit)
 | 
			
		||||
def show_voucher_edit_form(voucher: Voucher) -> str:
 | 
			
		||||
    """Shows the form to edit a voucher.
 | 
			
		||||
 | 
			
		||||
    :param voucher: The voucher.
 | 
			
		||||
    :return: The form to edit the voucher.
 | 
			
		||||
    """
 | 
			
		||||
    voucher_op: VoucherOperator = get_voucher_op(voucher, is_check_as=True)
 | 
			
		||||
    form: voucher_op.form
 | 
			
		||||
    if "form" in session:
 | 
			
		||||
        form = voucher_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
 | 
			
		||||
        del session["form"]
 | 
			
		||||
        form.obj = voucher
 | 
			
		||||
        form.validate()
 | 
			
		||||
    else:
 | 
			
		||||
        form = voucher_op.form(obj=voucher)
 | 
			
		||||
    return voucher_op.render_edit_template(voucher, form)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.post("/<voucher:voucher>/update", endpoint="update")
 | 
			
		||||
@has_permission(can_edit)
 | 
			
		||||
def update_voucher(voucher: Voucher) -> redirect:
 | 
			
		||||
    """Updates a voucher.
 | 
			
		||||
 | 
			
		||||
    :param voucher: The voucher.
 | 
			
		||||
    :return: The redirection to the voucher detail on success, or the voucher
 | 
			
		||||
        edit form on error.
 | 
			
		||||
    """
 | 
			
		||||
    voucher_op: VoucherOperator = get_voucher_op(voucher, is_check_as=True)
 | 
			
		||||
    form: voucher_op.form = voucher_op.form(request.form)
 | 
			
		||||
    form.obj = voucher
 | 
			
		||||
    if not form.validate():
 | 
			
		||||
        flash_form_errors(form)
 | 
			
		||||
        session["form"] = urlencode(list(request.form.items()))
 | 
			
		||||
        return redirect(inherit_next(with_type(
 | 
			
		||||
            url_for("accounting.voucher.edit", voucher=voucher))))
 | 
			
		||||
    with db.session.no_autoflush:
 | 
			
		||||
        form.populate_obj(voucher)
 | 
			
		||||
    if not form.is_modified:
 | 
			
		||||
        flash(s(lazy_gettext("The voucher was not modified.")), "success")
 | 
			
		||||
        return redirect(inherit_next(__get_detail_uri(voucher)))
 | 
			
		||||
    voucher.updated_by_id = get_current_user_pk()
 | 
			
		||||
    voucher.updated_at = sa.func.now()
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
    flash(s(lazy_gettext("The voucher is updated successfully.")), "success")
 | 
			
		||||
    return redirect(inherit_next(__get_detail_uri(voucher)))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.post("/<voucher:voucher>/delete", endpoint="delete")
 | 
			
		||||
@has_permission(can_edit)
 | 
			
		||||
def delete_voucher(voucher: Voucher) -> redirect:
 | 
			
		||||
    """Deletes a voucher.
 | 
			
		||||
 | 
			
		||||
    :param voucher: The voucher.
 | 
			
		||||
    :return: The redirection to the voucher list on success, or the voucher
 | 
			
		||||
        detail on error.
 | 
			
		||||
    """
 | 
			
		||||
    voucher.delete()
 | 
			
		||||
    sort_vouchers_in(voucher.date, voucher.id)
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
    flash(s(lazy_gettext("The voucher is deleted successfully.")), "success")
 | 
			
		||||
    return redirect(or_next(__get_default_page_uri()))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.get("/dates/<date:voucher_date>", endpoint="order")
 | 
			
		||||
@has_permission(can_view)
 | 
			
		||||
def show_voucher_order(voucher_date: date) -> str:
 | 
			
		||||
    """Shows the order of the vouchers in a same date.
 | 
			
		||||
 | 
			
		||||
    :param voucher_date: The date.
 | 
			
		||||
    :return: The order of the vouchers in the date.
 | 
			
		||||
    """
 | 
			
		||||
    vouchers: list[Voucher] = Voucher.query \
 | 
			
		||||
        .filter(Voucher.date == voucher_date) \
 | 
			
		||||
        .order_by(Voucher.no).all()
 | 
			
		||||
    return render_template("accounting/voucher/order.html",
 | 
			
		||||
                           date=voucher_date, list=vouchers)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.post("/dates/<date:voucher_date>", endpoint="sort")
 | 
			
		||||
@has_permission(can_edit)
 | 
			
		||||
def sort_vouchers(voucher_date: date) -> redirect:
 | 
			
		||||
    """Reorders the vouchers in a date.
 | 
			
		||||
 | 
			
		||||
    :param voucher_date: The date.
 | 
			
		||||
    :return: The redirection to the incoming account or the account list.  The
 | 
			
		||||
        reordering operation does not fail.
 | 
			
		||||
    """
 | 
			
		||||
    form: VoucherReorderForm = VoucherReorderForm(voucher_date)
 | 
			
		||||
    form.save_order()
 | 
			
		||||
    if not form.is_modified:
 | 
			
		||||
        flash(s(lazy_gettext("The order was not modified.")), "success")
 | 
			
		||||
        return redirect(or_next(__get_default_page_uri()))
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
    flash(s(lazy_gettext("The order is updated successfully.")), "success")
 | 
			
		||||
    return redirect(or_next(__get_default_page_uri()))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def __get_detail_uri(voucher: Voucher) -> str:
 | 
			
		||||
    """Returns the detail URI of a voucher.
 | 
			
		||||
 | 
			
		||||
    :param voucher: The voucher.
 | 
			
		||||
    :return: The detail URI of the voucher.
 | 
			
		||||
    """
 | 
			
		||||
    return url_for("accounting.voucher.detail", voucher=voucher)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def __get_default_page_uri() -> str:
 | 
			
		||||
    """Returns the URI for the default page.
 | 
			
		||||
 | 
			
		||||
    :return: The URI for the default page.
 | 
			
		||||
    """
 | 
			
		||||
    return url_for("accounting.report.default")
 | 
			
		||||
@@ -27,12 +27,12 @@ from flask.testing import FlaskCliRunner
 | 
			
		||||
 | 
			
		||||
from test_site import db
 | 
			
		||||
from testlib import create_test_app, get_client
 | 
			
		||||
from testlib_offset import TestData, JournalEntryData, TransactionData, \
 | 
			
		||||
from testlib_offset import TestData, JournalEntryData, VoucherData, \
 | 
			
		||||
    CurrencyData
 | 
			
		||||
from testlib_txn import Accounts, match_txn_detail
 | 
			
		||||
from testlib_voucher import Accounts, match_voucher_detail
 | 
			
		||||
 | 
			
		||||
PREFIX: str = "/accounting/transactions"
 | 
			
		||||
"""The URL prefix for the transaction management."""
 | 
			
		||||
PREFIX: str = "/accounting/vouchers"
 | 
			
		||||
"""The URL prefix for the voucher management."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
@@ -48,7 +48,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        runner: FlaskCliRunner = self.app.test_cli_runner()
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
            from accounting.models import BaseAccount, Transaction, \
 | 
			
		||||
            from accounting.models import BaseAccount, Voucher, \
 | 
			
		||||
                JournalEntry
 | 
			
		||||
            result: Result
 | 
			
		||||
            result = runner.invoke(args="init-db")
 | 
			
		||||
@@ -62,7 +62,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
            result = runner.invoke(args=["accounting-init-accounts",
 | 
			
		||||
                                         "-u", "editor"])
 | 
			
		||||
            self.assertEqual(result.exit_code, 0)
 | 
			
		||||
            Transaction.query.delete()
 | 
			
		||||
            Voucher.query.delete()
 | 
			
		||||
            JournalEntry.query.delete()
 | 
			
		||||
 | 
			
		||||
        self.client, self.csrf_token = get_client(self.app, "editor")
 | 
			
		||||
@@ -73,15 +73,15 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        from accounting.models import Account, Transaction
 | 
			
		||||
        create_uri: str = f"{PREFIX}/create/income?next=%2F_next"
 | 
			
		||||
        store_uri: str = f"{PREFIX}/store/income"
 | 
			
		||||
        from accounting.models import Account, Voucher
 | 
			
		||||
        create_uri: str = f"{PREFIX}/create/receipt?next=%2F_next"
 | 
			
		||||
        store_uri: str = f"{PREFIX}/store/receipt"
 | 
			
		||||
        form: dict[str, str]
 | 
			
		||||
        old_amount: Decimal
 | 
			
		||||
        response: httpx.Response
 | 
			
		||||
 | 
			
		||||
        txn_data: TransactionData = TransactionData(
 | 
			
		||||
            self.data.e_r_or3d.txn.days, [CurrencyData(
 | 
			
		||||
        voucher_data: VoucherData = VoucherData(
 | 
			
		||||
            self.data.e_r_or3d.voucher.days, [CurrencyData(
 | 
			
		||||
                "USD",
 | 
			
		||||
                [],
 | 
			
		||||
                [JournalEntryData(Accounts.RECEIVABLE,
 | 
			
		||||
@@ -95,14 +95,14 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
                                  original_entry=self.data.e_r_or3d)])])
 | 
			
		||||
 | 
			
		||||
        # Non-existing original entry ID
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-credit-1-original_entry_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 = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        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-account_code"] = self.data.e_p_or1c.account
 | 
			
		||||
        form["currency-1-credit-1-amount"] = "100"
 | 
			
		||||
@@ -115,8 +115,8 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
            account = Account.find_by_code(Accounts.RECEIVABLE)
 | 
			
		||||
            account.is_need_offset = False
 | 
			
		||||
            db.session.commit()
 | 
			
		||||
        response = self.client.post(store_uri,
 | 
			
		||||
                                    data=txn_data.new_form(self.csrf_token))
 | 
			
		||||
        response = self.client.post(
 | 
			
		||||
            store_uri, data=voucher_data.new_form(self.csrf_token))
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
@@ -125,7 +125,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
            db.session.commit()
 | 
			
		||||
 | 
			
		||||
        # The original entry is also an offset
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        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-account_code"] = self.data.e_p_of1d.account
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
@@ -133,52 +133,54 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
 | 
			
		||||
        # Not the same currency
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-code"] = "EUR"
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
 | 
			
		||||
        # Not the same account
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
 | 
			
		||||
        # Not exceeding net balance - partially offset
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-credit-1-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].credit[0].amount + Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].credit[0].amount
 | 
			
		||||
                  + Decimal("0.01"))
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
 | 
			
		||||
        # Not exceeding net balance - unmatched
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-credit-3-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].credit[2].amount + Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].credit[2].amount
 | 
			
		||||
                  + Decimal("0.01"))
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
 | 
			
		||||
        # Not before the original entries
 | 
			
		||||
        old_days = txn_data.days
 | 
			
		||||
        txn_data.days = old_days + 1
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        old_days = voucher_data.days
 | 
			
		||||
        voucher_data.days = old_days + 1
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
        txn_data.days = old_days
 | 
			
		||||
        voucher_data.days = old_days
 | 
			
		||||
 | 
			
		||||
        # Success
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        txn_id: int = match_txn_detail(response.headers["Location"])
 | 
			
		||||
        voucher_id: int = match_voucher_detail(response.headers["Location"])
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
            txn = db.session.get(Transaction, txn_id)
 | 
			
		||||
            for offset in txn.currencies[0].credit:
 | 
			
		||||
            voucher = db.session.get(Voucher, voucher_id)
 | 
			
		||||
            for offset in voucher.currencies[0].credit:
 | 
			
		||||
                self.assertIsNotNone(offset.original_entry_id)
 | 
			
		||||
 | 
			
		||||
    def test_edit_receivable_offset(self) -> None:
 | 
			
		||||
@@ -187,27 +189,27 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        from accounting.models import Account
 | 
			
		||||
        txn_data: TransactionData = self.data.t_r_of2
 | 
			
		||||
        edit_uri: str = f"{PREFIX}/{txn_data.id}/edit?next=%2F_next"
 | 
			
		||||
        update_uri: str = f"{PREFIX}/{txn_data.id}/update"
 | 
			
		||||
        voucher_data: VoucherData = self.data.v_r_of2
 | 
			
		||||
        edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
 | 
			
		||||
        update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
 | 
			
		||||
        form: dict[str, str]
 | 
			
		||||
        response: httpx.Response
 | 
			
		||||
 | 
			
		||||
        txn_data.days = self.data.t_r_or2.days
 | 
			
		||||
        txn_data.currencies[0].debit[0].amount = Decimal("600")
 | 
			
		||||
        txn_data.currencies[0].credit[0].amount = Decimal("600")
 | 
			
		||||
        txn_data.currencies[0].debit[2].amount = Decimal("600")
 | 
			
		||||
        txn_data.currencies[0].credit[2].amount = Decimal("600")
 | 
			
		||||
        voucher_data.days = self.data.v_r_or2.days
 | 
			
		||||
        voucher_data.currencies[0].debit[0].amount = Decimal("600")
 | 
			
		||||
        voucher_data.currencies[0].credit[0].amount = Decimal("600")
 | 
			
		||||
        voucher_data.currencies[0].debit[2].amount = Decimal("600")
 | 
			
		||||
        voucher_data.currencies[0].credit[2].amount = Decimal("600")
 | 
			
		||||
 | 
			
		||||
        # Non-existing original entry ID
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-credit-1-original_entry_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 = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        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-account_code"] = self.data.e_p_or1c.account
 | 
			
		||||
        form["currency-1-debit-1-amount"] = "100"
 | 
			
		||||
@@ -221,8 +223,8 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
            account = Account.find_by_code(Accounts.RECEIVABLE)
 | 
			
		||||
            account.is_need_offset = False
 | 
			
		||||
            db.session.commit()
 | 
			
		||||
        response = self.client.post(update_uri,
 | 
			
		||||
                                    data=txn_data.update_form(self.csrf_token))
 | 
			
		||||
        response = self.client.post(
 | 
			
		||||
            update_uri, data=voucher_data.update_form(self.csrf_token))
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
@@ -231,7 +233,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
            db.session.commit()
 | 
			
		||||
 | 
			
		||||
        # The original entry is also an offset
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        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-account_code"] = self.data.e_p_of1d.account
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
@@ -239,155 +241,159 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not the same currency
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-code"] = "EUR"
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not the same account
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not exceeding net balance - partially offset
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-1-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].debit[0].amount + Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].debit[0].amount + Decimal("0.01"))
 | 
			
		||||
        form["currency-1-credit-1-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].credit[0].amount + Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].credit[0].amount
 | 
			
		||||
                  + Decimal("0.01"))
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not exceeding net balance - unmatched
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-3-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].debit[2].amount + Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].debit[2].amount + Decimal("0.01"))
 | 
			
		||||
        form["currency-1-credit-3-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].credit[2].amount + Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].credit[2].amount
 | 
			
		||||
                  + Decimal("0.01"))
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not before the original entries
 | 
			
		||||
        old_days: int = txn_data.days
 | 
			
		||||
        txn_data.days = old_days + 1
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        old_days: int = voucher_data.days
 | 
			
		||||
        voucher_data.days = old_days + 1
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
        txn_data.days = old_days
 | 
			
		||||
        voucher_data.days = old_days
 | 
			
		||||
 | 
			
		||||
        # Success
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"],
 | 
			
		||||
                         f"{PREFIX}/{txn_data.id}?next=%2F_next")
 | 
			
		||||
                         f"{PREFIX}/{voucher_data.id}?next=%2F_next")
 | 
			
		||||
 | 
			
		||||
    def test_edit_receivable_original_entry(self) -> None:
 | 
			
		||||
        """Tests to edit the receivable original entry.
 | 
			
		||||
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        from accounting.models import Transaction
 | 
			
		||||
        txn_data: TransactionData = self.data.t_r_or1
 | 
			
		||||
        edit_uri: str = f"{PREFIX}/{txn_data.id}/edit?next=%2F_next"
 | 
			
		||||
        update_uri: str = f"{PREFIX}/{txn_data.id}/update"
 | 
			
		||||
        from accounting.models import Voucher
 | 
			
		||||
        voucher_data: VoucherData = self.data.v_r_or1
 | 
			
		||||
        edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
 | 
			
		||||
        update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
 | 
			
		||||
        form: dict[str, str]
 | 
			
		||||
        response: httpx.Response
 | 
			
		||||
 | 
			
		||||
        txn_data.days = self.data.t_r_of1.days
 | 
			
		||||
        txn_data.currencies[0].debit[0].amount = Decimal("800")
 | 
			
		||||
        txn_data.currencies[0].credit[0].amount = Decimal("800")
 | 
			
		||||
        txn_data.currencies[0].debit[1].amount = Decimal("3.4")
 | 
			
		||||
        txn_data.currencies[0].credit[1].amount = Decimal("3.4")
 | 
			
		||||
        voucher_data.days = self.data.v_r_of1.days
 | 
			
		||||
        voucher_data.currencies[0].debit[0].amount = Decimal("800")
 | 
			
		||||
        voucher_data.currencies[0].credit[0].amount = Decimal("800")
 | 
			
		||||
        voucher_data.currencies[0].debit[1].amount = Decimal("3.4")
 | 
			
		||||
        voucher_data.currencies[0].credit[1].amount = Decimal("3.4")
 | 
			
		||||
 | 
			
		||||
        # Not the same currency
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-code"] = "EUR"
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not the same account
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-1-account_code"] = Accounts.NOTES_RECEIVABLE
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not less than offset total - partially offset
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-1-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].debit[0].amount - Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].debit[0].amount - Decimal("0.01"))
 | 
			
		||||
        form["currency-1-credit-1-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].credit[0].amount - Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].credit[0].amount
 | 
			
		||||
                  - Decimal("0.01"))
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not less than offset total - fully offset
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-2-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].debit[1].amount - Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].debit[1].amount - Decimal("0.01"))
 | 
			
		||||
        form["currency-1-credit-2-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].credit[1].amount - Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].credit[1].amount
 | 
			
		||||
                  - Decimal("0.01"))
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not after the offset entries
 | 
			
		||||
        old_days: int = txn_data.days
 | 
			
		||||
        txn_data.days = old_days - 1
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        old_days: int = voucher_data.days
 | 
			
		||||
        voucher_data.days = old_days - 1
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
        txn_data.days = old_days
 | 
			
		||||
        voucher_data.days = old_days
 | 
			
		||||
 | 
			
		||||
        # Not deleting matched original entries
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        del form["currency-1-debit-1-eid"]
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Success
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"],
 | 
			
		||||
                         f"{PREFIX}/{txn_data.id}?next=%2F_next")
 | 
			
		||||
                         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.
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
            txn_or: Transaction | None = db.session.get(
 | 
			
		||||
                Transaction, txn_data.id)
 | 
			
		||||
            self.assertIsNotNone(txn_or)
 | 
			
		||||
            txn_of: Transaction | None = db.session.get(
 | 
			
		||||
                Transaction, self.data.t_r_of1.id)
 | 
			
		||||
            self.assertIsNotNone(txn_of)
 | 
			
		||||
            self.assertEqual(txn_or.date, txn_of.date)
 | 
			
		||||
            self.assertLess(txn_or.no, txn_of.no)
 | 
			
		||||
            voucher_or: Voucher | None = db.session.get(
 | 
			
		||||
                Voucher, voucher_data.id)
 | 
			
		||||
            self.assertIsNotNone(voucher_or)
 | 
			
		||||
            voucher_of: Voucher | None = db.session.get(
 | 
			
		||||
                Voucher, self.data.v_r_of1.id)
 | 
			
		||||
            self.assertIsNotNone(voucher_of)
 | 
			
		||||
            self.assertEqual(voucher_or.date, voucher_of.date)
 | 
			
		||||
            self.assertLess(voucher_or.no, voucher_of.no)
 | 
			
		||||
 | 
			
		||||
    def test_add_payable_offset(self) -> None:
 | 
			
		||||
        """Tests to add the payable offset.
 | 
			
		||||
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        from accounting.models import Account, Transaction
 | 
			
		||||
        create_uri: str = f"{PREFIX}/create/expense?next=%2F_next"
 | 
			
		||||
        store_uri: str = f"{PREFIX}/store/expense"
 | 
			
		||||
        from accounting.models import Account, Voucher
 | 
			
		||||
        create_uri: str = f"{PREFIX}/create/disbursement?next=%2F_next"
 | 
			
		||||
        store_uri: str = f"{PREFIX}/store/disbursement"
 | 
			
		||||
        form: dict[str, str]
 | 
			
		||||
        response: httpx.Response
 | 
			
		||||
 | 
			
		||||
        txn_data: TransactionData = TransactionData(
 | 
			
		||||
            self.data.e_p_or3c.txn.days, [CurrencyData(
 | 
			
		||||
        voucher_data: VoucherData = VoucherData(
 | 
			
		||||
            self.data.e_p_or3c.voucher.days, [CurrencyData(
 | 
			
		||||
                "USD",
 | 
			
		||||
                [JournalEntryData(Accounts.PAYABLE,
 | 
			
		||||
                                  self.data.e_p_or1c.summary, "500",
 | 
			
		||||
@@ -401,14 +407,14 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
                [])])
 | 
			
		||||
 | 
			
		||||
        # Non-existing original entry ID
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-1-original_entry_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 = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        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-account_code"] = self.data.e_r_or1d.account
 | 
			
		||||
        form["currency-1-debit-1-amount"] = "100"
 | 
			
		||||
@@ -421,8 +427,8 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
            account = Account.find_by_code(Accounts.PAYABLE)
 | 
			
		||||
            account.is_need_offset = False
 | 
			
		||||
            db.session.commit()
 | 
			
		||||
        response = self.client.post(store_uri,
 | 
			
		||||
                                    data=txn_data.new_form(self.csrf_token))
 | 
			
		||||
        response = self.client.post(
 | 
			
		||||
            store_uri, data=voucher_data.new_form(self.csrf_token))
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
@@ -431,7 +437,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
            db.session.commit()
 | 
			
		||||
 | 
			
		||||
        # The original entry is also an offset
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        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-account_code"] = self.data.e_r_of1c.account
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
@@ -439,52 +445,52 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
 | 
			
		||||
        # Not the same currency
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-code"] = "EUR"
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
 | 
			
		||||
        # Not the same account
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
 | 
			
		||||
        # Not exceeding net balance - partially offset
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-1-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].debit[0].amount + Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].debit[0].amount + Decimal("0.01"))
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
 | 
			
		||||
        # Not exceeding net balance - unmatched
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-3-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].debit[2].amount + Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].debit[2].amount + Decimal("0.01"))
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
 | 
			
		||||
        # Not before the original entries
 | 
			
		||||
        old_days: int = txn_data.days
 | 
			
		||||
        txn_data.days = old_days + 1
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        old_days: int = voucher_data.days
 | 
			
		||||
        voucher_data.days = old_days + 1
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
			
		||||
        txn_data.days = old_days
 | 
			
		||||
        voucher_data.days = old_days
 | 
			
		||||
 | 
			
		||||
        # Success
 | 
			
		||||
        form = txn_data.new_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.new_form(self.csrf_token)
 | 
			
		||||
        response = self.client.post(store_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        txn_id: int = match_txn_detail(response.headers["Location"])
 | 
			
		||||
        voucher_id: int = match_voucher_detail(response.headers["Location"])
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
            txn = db.session.get(Transaction, txn_id)
 | 
			
		||||
            for offset in txn.currencies[0].debit:
 | 
			
		||||
            voucher = db.session.get(Voucher, voucher_id)
 | 
			
		||||
            for offset in voucher.currencies[0].debit:
 | 
			
		||||
                self.assertIsNotNone(offset.original_entry_id)
 | 
			
		||||
 | 
			
		||||
    def test_edit_payable_offset(self) -> None:
 | 
			
		||||
@@ -492,28 +498,28 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        from accounting.models import Account, Transaction
 | 
			
		||||
        txn_data: TransactionData = self.data.t_p_of2
 | 
			
		||||
        edit_uri: str = f"{PREFIX}/{txn_data.id}/edit?next=%2F_next"
 | 
			
		||||
        update_uri: str = f"{PREFIX}/{txn_data.id}/update"
 | 
			
		||||
        from accounting.models import Account, Voucher
 | 
			
		||||
        voucher_data: VoucherData = self.data.v_p_of2
 | 
			
		||||
        edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
 | 
			
		||||
        update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
 | 
			
		||||
        form: dict[str, str]
 | 
			
		||||
        response: httpx.Response
 | 
			
		||||
 | 
			
		||||
        txn_data.days = self.data.t_p_or2.days
 | 
			
		||||
        txn_data.currencies[0].debit[0].amount = Decimal("1100")
 | 
			
		||||
        txn_data.currencies[0].credit[0].amount = Decimal("1100")
 | 
			
		||||
        txn_data.currencies[0].debit[2].amount = Decimal("900")
 | 
			
		||||
        txn_data.currencies[0].credit[2].amount = Decimal("900")
 | 
			
		||||
        voucher_data.days = self.data.v_p_or2.days
 | 
			
		||||
        voucher_data.currencies[0].debit[0].amount = Decimal("1100")
 | 
			
		||||
        voucher_data.currencies[0].credit[0].amount = Decimal("1100")
 | 
			
		||||
        voucher_data.currencies[0].debit[2].amount = Decimal("900")
 | 
			
		||||
        voucher_data.currencies[0].credit[2].amount = Decimal("900")
 | 
			
		||||
 | 
			
		||||
        # Non-existing original entry ID
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-1-original_entry_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 = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        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-account_code"] = self.data.e_r_or1d.account
 | 
			
		||||
        form["currency-1-debit-1-amount"] = "100"
 | 
			
		||||
@@ -527,8 +533,8 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
            account = Account.find_by_code(Accounts.PAYABLE)
 | 
			
		||||
            account.is_need_offset = False
 | 
			
		||||
            db.session.commit()
 | 
			
		||||
        response = self.client.post(update_uri,
 | 
			
		||||
                                    data=txn_data.update_form(self.csrf_token))
 | 
			
		||||
        response = self.client.post(
 | 
			
		||||
            update_uri, data=voucher_data.update_form(self.csrf_token))
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
@@ -537,7 +543,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
            db.session.commit()
 | 
			
		||||
 | 
			
		||||
        # The original entry is also an offset
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        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-account_code"] = self.data.e_r_of1c.account
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
@@ -545,56 +551,58 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not the same currency
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-code"] = "EUR"
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not the same account
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not exceeding net balance - partially offset
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-1-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].debit[0].amount + Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].debit[0].amount + Decimal("0.01"))
 | 
			
		||||
        form["currency-1-credit-1-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].credit[0].amount + Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].credit[0].amount
 | 
			
		||||
                  + Decimal("0.01"))
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not exceeding net balance - unmatched
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-3-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].debit[2].amount + Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].debit[2].amount + Decimal("0.01"))
 | 
			
		||||
        form["currency-1-credit-3-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].credit[2].amount + Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].credit[2].amount
 | 
			
		||||
                  + Decimal("0.01"))
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not before the original entries
 | 
			
		||||
        old_days: int = txn_data.days
 | 
			
		||||
        txn_data.days = old_days + 1
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        old_days: int = voucher_data.days
 | 
			
		||||
        voucher_data.days = old_days + 1
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
        txn_data.days = old_days
 | 
			
		||||
        voucher_data.days = old_days
 | 
			
		||||
 | 
			
		||||
        # Success
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        txn_id: int = match_txn_detail(response.headers["Location"])
 | 
			
		||||
        voucher_id: int = match_voucher_detail(response.headers["Location"])
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
            txn = db.session.get(Transaction, txn_id)
 | 
			
		||||
            for offset in txn.currencies[0].debit:
 | 
			
		||||
            voucher = db.session.get(Voucher, voucher_id)
 | 
			
		||||
            for offset in voucher.currencies[0].debit:
 | 
			
		||||
                self.assertIsNotNone(offset.original_entry_id)
 | 
			
		||||
 | 
			
		||||
    def test_edit_payable_original_entry(self) -> None:
 | 
			
		||||
@@ -602,84 +610,86 @@ class OffsetTestCase(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        from accounting.models import Transaction
 | 
			
		||||
        txn_data: TransactionData = self.data.t_p_or1
 | 
			
		||||
        edit_uri: str = f"{PREFIX}/{txn_data.id}/edit?next=%2F_next"
 | 
			
		||||
        update_uri: str = f"{PREFIX}/{txn_data.id}/update"
 | 
			
		||||
        from accounting.models import Voucher
 | 
			
		||||
        voucher_data: VoucherData = self.data.v_p_or1
 | 
			
		||||
        edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
 | 
			
		||||
        update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
 | 
			
		||||
        form: dict[str, str]
 | 
			
		||||
        response: httpx.Response
 | 
			
		||||
 | 
			
		||||
        txn_data.days = self.data.t_p_of1.days
 | 
			
		||||
        txn_data.currencies[0].debit[0].amount = Decimal("1200")
 | 
			
		||||
        txn_data.currencies[0].credit[0].amount = Decimal("1200")
 | 
			
		||||
        txn_data.currencies[0].debit[1].amount = Decimal("0.9")
 | 
			
		||||
        txn_data.currencies[0].credit[1].amount = Decimal("0.9")
 | 
			
		||||
        voucher_data.days = self.data.v_p_of1.days
 | 
			
		||||
        voucher_data.currencies[0].debit[0].amount = Decimal("1200")
 | 
			
		||||
        voucher_data.currencies[0].credit[0].amount = Decimal("1200")
 | 
			
		||||
        voucher_data.currencies[0].debit[1].amount = Decimal("0.9")
 | 
			
		||||
        voucher_data.currencies[0].credit[1].amount = Decimal("0.9")
 | 
			
		||||
 | 
			
		||||
        # Not the same currency
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-code"] = "EUR"
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not the same account
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-credit-1-account_code"] = Accounts.NOTES_PAYABLE
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not less than offset total - partially offset
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-1-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].debit[0].amount - Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].debit[0].amount - Decimal("0.01"))
 | 
			
		||||
        form["currency-1-credit-1-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].credit[0].amount - Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].credit[0].amount
 | 
			
		||||
                  - Decimal("0.01"))
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not less than offset total - fully offset
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        form["currency-1-debit-2-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].debit[1].amount - Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].debit[1].amount - Decimal("0.01"))
 | 
			
		||||
        form["currency-1-credit-2-amount"] \
 | 
			
		||||
            = str(txn_data.currencies[0].credit[1].amount - Decimal("0.01"))
 | 
			
		||||
            = str(voucher_data.currencies[0].credit[1].amount
 | 
			
		||||
                  - Decimal("0.01"))
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Not after the offset entries
 | 
			
		||||
        old_days: int = txn_data.days
 | 
			
		||||
        txn_data.days = old_days - 1
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        old_days: int = voucher_data.days
 | 
			
		||||
        voucher_data.days = old_days - 1
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
        txn_data.days = old_days
 | 
			
		||||
        voucher_data.days = old_days
 | 
			
		||||
 | 
			
		||||
        # Not deleting matched original entries
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        del form["currency-1-credit-1-eid"]
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
			
		||||
 | 
			
		||||
        # Success
 | 
			
		||||
        form = txn_data.update_form(self.csrf_token)
 | 
			
		||||
        form = voucher_data.update_form(self.csrf_token)
 | 
			
		||||
        response = self.client.post(update_uri, data=form)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.headers["Location"],
 | 
			
		||||
                         f"{PREFIX}/{txn_data.id}?next=%2F_next")
 | 
			
		||||
                         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
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
            txn_or: Transaction | None = db.session.get(
 | 
			
		||||
                Transaction, txn_data.id)
 | 
			
		||||
            self.assertIsNotNone(txn_or)
 | 
			
		||||
            txn_of: Transaction | None = db.session.get(
 | 
			
		||||
                Transaction, self.data.t_p_of1.id)
 | 
			
		||||
            self.assertIsNotNone(txn_of)
 | 
			
		||||
            self.assertEqual(txn_or.date, txn_of.date)
 | 
			
		||||
            self.assertLess(txn_or.no, txn_of.no)
 | 
			
		||||
            voucher_or: Voucher | None = db.session.get(
 | 
			
		||||
                Voucher, voucher_data.id)
 | 
			
		||||
            self.assertIsNotNone(voucher_or)
 | 
			
		||||
            voucher_of: Voucher | None = db.session.get(
 | 
			
		||||
                Voucher, self.data.v_p_of1.id)
 | 
			
		||||
            self.assertIsNotNone(voucher_of)
 | 
			
		||||
            self.assertEqual(voucher_or.date, voucher_of.date)
 | 
			
		||||
            self.assertLess(voucher_or.no, voucher_of.no)
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ from flask import Flask
 | 
			
		||||
from flask.testing import FlaskCliRunner
 | 
			
		||||
 | 
			
		||||
from testlib import create_test_app, get_client
 | 
			
		||||
from testlib_txn import Accounts, NEXT_URI, add_txn
 | 
			
		||||
from testlib_voucher import Accounts, NEXT_URI, add_voucher
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SummeryEditorTestCase(unittest.TestCase):
 | 
			
		||||
@@ -41,7 +41,7 @@ class SummeryEditorTestCase(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        runner: FlaskCliRunner = self.app.test_cli_runner()
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
            from accounting.models import BaseAccount, Transaction, \
 | 
			
		||||
            from accounting.models import BaseAccount, Voucher, \
 | 
			
		||||
                JournalEntry
 | 
			
		||||
            result: Result
 | 
			
		||||
            result = runner.invoke(args="init-db")
 | 
			
		||||
@@ -55,7 +55,7 @@ class SummeryEditorTestCase(unittest.TestCase):
 | 
			
		||||
            result = runner.invoke(args=["accounting-init-accounts",
 | 
			
		||||
                                         "-u", "editor"])
 | 
			
		||||
            self.assertEqual(result.exit_code, 0)
 | 
			
		||||
            Transaction.query.delete()
 | 
			
		||||
            Voucher.query.delete()
 | 
			
		||||
            JournalEntry.query.delete()
 | 
			
		||||
 | 
			
		||||
        self.client, self.csrf_token = get_client(self.app, "editor")
 | 
			
		||||
@@ -65,9 +65,9 @@ class SummeryEditorTestCase(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        from accounting.transaction.utils.summary_editor import SummaryEditor
 | 
			
		||||
        from accounting.voucher.utils.summary_editor import SummaryEditor
 | 
			
		||||
        for form in get_form_data(self.csrf_token):
 | 
			
		||||
            add_txn(self.client, form)
 | 
			
		||||
            add_voucher(self.client, form)
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
            editor: SummaryEditor = SummaryEditor()
 | 
			
		||||
 | 
			
		||||
@@ -159,22 +159,22 @@ class SummeryEditorTestCase(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_form_data(csrf_token: str) -> list[dict[str, str]]:
 | 
			
		||||
    """Returns the form data for multiple transaction forms.
 | 
			
		||||
    """Returns the form data for multiple voucher forms.
 | 
			
		||||
 | 
			
		||||
    :param csrf_token: The CSRF token.
 | 
			
		||||
    :return: A list of the form data.
 | 
			
		||||
    """
 | 
			
		||||
    txn_date: str = date.today().isoformat()
 | 
			
		||||
    voucher_date: str = date.today().isoformat()
 | 
			
		||||
    return [{"csrf_token": csrf_token,
 | 
			
		||||
             "next": NEXT_URI,
 | 
			
		||||
             "date": txn_date,
 | 
			
		||||
             "date": voucher_date,
 | 
			
		||||
             "currency-0-code": "USD",
 | 
			
		||||
             "currency-0-credit-0-account_code": Accounts.SERVICE,
 | 
			
		||||
             "currency-0-credit-0-summary": " Salary ",
 | 
			
		||||
             "currency-0-credit-0-amount": "2500"},
 | 
			
		||||
            {"csrf_token": csrf_token,
 | 
			
		||||
             "next": NEXT_URI,
 | 
			
		||||
             "date": txn_date,
 | 
			
		||||
             "date": voucher_date,
 | 
			
		||||
             "currency-0-code": "USD",
 | 
			
		||||
             "currency-0-debit-0-account_code": Accounts.MEAL,
 | 
			
		||||
             "currency-0-debit-0-summary": " Lunch—Fish ",
 | 
			
		||||
@@ -196,7 +196,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
 | 
			
		||||
             "currency-0-credit-2-amount": "4.25"},
 | 
			
		||||
            {"csrf_token": csrf_token,
 | 
			
		||||
             "next": NEXT_URI,
 | 
			
		||||
             "date": txn_date,
 | 
			
		||||
             "date": voucher_date,
 | 
			
		||||
             "currency-0-code": "USD",
 | 
			
		||||
             "currency-0-debit-0-account_code": Accounts.MEAL,
 | 
			
		||||
             "currency-0-debit-0-summary": " Lunch—Salad ",
 | 
			
		||||
@@ -212,7 +212,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
 | 
			
		||||
             "currency-0-credit-1-amount": "8.28"},
 | 
			
		||||
            {"csrf_token": csrf_token,
 | 
			
		||||
             "next": NEXT_URI,
 | 
			
		||||
             "date": txn_date,
 | 
			
		||||
             "date": voucher_date,
 | 
			
		||||
             "currency-0-code": "USD",
 | 
			
		||||
             "currency-0-debit-0-account_code": Accounts.MEAL,
 | 
			
		||||
             "currency-0-debit-0-summary": " Lunch—Pizza  ",
 | 
			
		||||
@@ -228,14 +228,14 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
 | 
			
		||||
             "currency-0-credit-1-amount": "7.47"},
 | 
			
		||||
            {"csrf_token": csrf_token,
 | 
			
		||||
             "next": NEXT_URI,
 | 
			
		||||
             "date": txn_date,
 | 
			
		||||
             "date": voucher_date,
 | 
			
		||||
             "currency-0-code": "USD",
 | 
			
		||||
             "currency-0-debit-0-account_code": Accounts.TRAVEL,
 | 
			
		||||
             "currency-0-debit-0-summary": " Airplane—Lake City↔Hill Town ",
 | 
			
		||||
             "currency-0-debit-0-amount": "800"},
 | 
			
		||||
            {"csrf_token": csrf_token,
 | 
			
		||||
             "next": NEXT_URI,
 | 
			
		||||
             "date": txn_date,
 | 
			
		||||
             "date": voucher_date,
 | 
			
		||||
             "currency-0-code": "USD",
 | 
			
		||||
             "currency-0-debit-0-account_code": Accounts.TRAVEL,
 | 
			
		||||
             "currency-0-debit-0-summary": " Bus—323—Downtown→Museum ",
 | 
			
		||||
@@ -263,7 +263,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
 | 
			
		||||
             "currency-0-credit-3-amount": "4.4"},
 | 
			
		||||
            {"csrf_token": csrf_token,
 | 
			
		||||
             "next": NEXT_URI,
 | 
			
		||||
             "date": txn_date,
 | 
			
		||||
             "date": voucher_date,
 | 
			
		||||
             "currency-0-code": "USD",
 | 
			
		||||
             "currency-0-debit-0-account_code": Accounts.TRAVEL,
 | 
			
		||||
             "currency-0-debit-0-summary": " Taxi—Museum→Office ",
 | 
			
		||||
@@ -309,7 +309,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
 | 
			
		||||
             "currency-0-credit-6-amount": "5.5"},
 | 
			
		||||
            {"csrf_token": csrf_token,
 | 
			
		||||
             "next": NEXT_URI,
 | 
			
		||||
             "date": txn_date,
 | 
			
		||||
             "date": voucher_date,
 | 
			
		||||
             "currency-0-code": "USD",
 | 
			
		||||
             "currency-0-debit-0-account_code": Accounts.PETTY_CASH,
 | 
			
		||||
             "currency-0-debit-0-summary": " Dinner—Steak  ",
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -26,7 +26,7 @@ import httpx
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
from test_site import db
 | 
			
		||||
from testlib_txn import Accounts, match_txn_detail, NEXT_URI
 | 
			
		||||
from testlib_voucher import Accounts, match_voucher_detail, NEXT_URI
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class JournalEntryData:
 | 
			
		||||
@@ -41,7 +41,7 @@ class JournalEntryData:
 | 
			
		||||
        :param amount: The amount.
 | 
			
		||||
        :param original_entry: The original entry.
 | 
			
		||||
        """
 | 
			
		||||
        self.txn: TransactionData | None = None
 | 
			
		||||
        self.voucher: VoucherData | None = None
 | 
			
		||||
        self.id: int = -1
 | 
			
		||||
        self.no: int = -1
 | 
			
		||||
        self.original_entry: JournalEntryData | None = original_entry
 | 
			
		||||
@@ -73,11 +73,11 @@ class JournalEntryData:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CurrencyData:
 | 
			
		||||
    """The transaction currency data."""
 | 
			
		||||
    """The voucher currency data."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, currency: str, debit: list[JournalEntryData],
 | 
			
		||||
                 credit: list[JournalEntryData]):
 | 
			
		||||
        """Constructs the transaction currency data.
 | 
			
		||||
        """Constructs the voucher currency data.
 | 
			
		||||
 | 
			
		||||
        :param currency: The currency code.
 | 
			
		||||
        :param debit: The debit journal entries.
 | 
			
		||||
@@ -104,14 +104,14 @@ class CurrencyData:
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionData:
 | 
			
		||||
    """The transaction data."""
 | 
			
		||||
class VoucherData:
 | 
			
		||||
    """The voucher data."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, days: int, currencies: list[CurrencyData]):
 | 
			
		||||
        """Constructs a transaction.
 | 
			
		||||
        """Constructs a voucher.
 | 
			
		||||
 | 
			
		||||
        :param days: The number of days before today.
 | 
			
		||||
        :param currencies: The transaction currency data.
 | 
			
		||||
        :param currencies: The voucher currency data.
 | 
			
		||||
        """
 | 
			
		||||
        self.id: int = -1
 | 
			
		||||
        self.days: int = days
 | 
			
		||||
@@ -119,38 +119,38 @@ class TransactionData:
 | 
			
		||||
        self.note: str | None = None
 | 
			
		||||
        for currency in self.currencies:
 | 
			
		||||
            for entry in currency.debit:
 | 
			
		||||
                entry.txn = self
 | 
			
		||||
                entry.voucher = self
 | 
			
		||||
            for entry in currency.credit:
 | 
			
		||||
                entry.txn = self
 | 
			
		||||
                entry.voucher = self
 | 
			
		||||
 | 
			
		||||
    def new_form(self, csrf_token: str) -> dict[str, str]:
 | 
			
		||||
        """Returns the transaction as a form.
 | 
			
		||||
        """Returns the voucher as a creation form.
 | 
			
		||||
 | 
			
		||||
        :param csrf_token: The CSRF token.
 | 
			
		||||
        :return: The transaction as a form.
 | 
			
		||||
        :return: The voucher as a creation form.
 | 
			
		||||
        """
 | 
			
		||||
        return self.__form(csrf_token, is_update=False)
 | 
			
		||||
 | 
			
		||||
    def update_form(self, csrf_token: str) -> dict[str, str]:
 | 
			
		||||
        """Returns the transaction as a form.
 | 
			
		||||
        """Returns the voucher as a update form.
 | 
			
		||||
 | 
			
		||||
        :param csrf_token: The CSRF token.
 | 
			
		||||
        :return: The transaction as a form.
 | 
			
		||||
        :return: The voucher as a update form.
 | 
			
		||||
        """
 | 
			
		||||
        return self.__form(csrf_token, is_update=True)
 | 
			
		||||
 | 
			
		||||
    def __form(self, csrf_token: str, is_update: bool = False) \
 | 
			
		||||
            -> dict[str, str]:
 | 
			
		||||
        """Returns the transaction as a form.
 | 
			
		||||
        """Returns the voucher as a form.
 | 
			
		||||
 | 
			
		||||
        :param csrf_token: The CSRF token.
 | 
			
		||||
        :param is_update: True for an update operation, or False otherwise
 | 
			
		||||
        :return: The transaction as a form.
 | 
			
		||||
        :return: The voucher as a form.
 | 
			
		||||
        """
 | 
			
		||||
        txn_date: date = date.today() - timedelta(days=self.days)
 | 
			
		||||
        voucher_date: date = date.today() - timedelta(days=self.days)
 | 
			
		||||
        form: dict[str, str] = {"csrf_token": csrf_token,
 | 
			
		||||
                                "next": NEXT_URI,
 | 
			
		||||
                                "date": txn_date.isoformat()}
 | 
			
		||||
                                "date": voucher_date.isoformat()}
 | 
			
		||||
        for i in range(len(self.currencies)):
 | 
			
		||||
            form.update(self.currencies[i].form(i + 1, is_update))
 | 
			
		||||
        if self.note is not None:
 | 
			
		||||
@@ -205,24 +205,24 @@ class TestData:
 | 
			
		||||
        self.e_p_or4d, self.e_p_or4c = couple(
 | 
			
		||||
            "Envelop", "0.9", Accounts.OFFICE, Accounts.PAYABLE)
 | 
			
		||||
 | 
			
		||||
        # Original transactions
 | 
			
		||||
        self.t_r_or1: TransactionData = TransactionData(
 | 
			
		||||
        # Original vouchers
 | 
			
		||||
        self.v_r_or1: VoucherData = VoucherData(
 | 
			
		||||
            50, [CurrencyData("USD", [self.e_r_or1d, self.e_r_or4d],
 | 
			
		||||
                              [self.e_r_or1c, self.e_r_or4c])])
 | 
			
		||||
        self.t_r_or2: TransactionData = TransactionData(
 | 
			
		||||
        self.v_r_or2: VoucherData = VoucherData(
 | 
			
		||||
            30, [CurrencyData("USD", [self.e_r_or2d, self.e_r_or3d],
 | 
			
		||||
                              [self.e_r_or2c, self.e_r_or3c])])
 | 
			
		||||
        self.t_p_or1: TransactionData = TransactionData(
 | 
			
		||||
        self.v_p_or1: VoucherData = VoucherData(
 | 
			
		||||
            40, [CurrencyData("USD", [self.e_p_or1d, self.e_p_or4d],
 | 
			
		||||
                              [self.e_p_or1c, self.e_p_or4c])])
 | 
			
		||||
        self.t_p_or2: TransactionData = TransactionData(
 | 
			
		||||
        self.v_p_or2: VoucherData = VoucherData(
 | 
			
		||||
            20, [CurrencyData("USD", [self.e_p_or2d, self.e_p_or3d],
 | 
			
		||||
                              [self.e_p_or2c, self.e_p_or3c])])
 | 
			
		||||
 | 
			
		||||
        self.__add_txn(self.t_r_or1)
 | 
			
		||||
        self.__add_txn(self.t_r_or2)
 | 
			
		||||
        self.__add_txn(self.t_p_or1)
 | 
			
		||||
        self.__add_txn(self.t_p_or2)
 | 
			
		||||
        self.__add_voucher(self.v_r_or1)
 | 
			
		||||
        self.__add_voucher(self.v_r_or2)
 | 
			
		||||
        self.__add_voucher(self.v_p_or1)
 | 
			
		||||
        self.__add_voucher(self.v_p_or2)
 | 
			
		||||
 | 
			
		||||
        # Receivable offset entries
 | 
			
		||||
        self.e_r_of1d, self.e_r_of1c = couple(
 | 
			
		||||
@@ -258,52 +258,52 @@ class TestData:
 | 
			
		||||
            "Envelop", "0.9", Accounts.PAYABLE, Accounts.CASH)
 | 
			
		||||
        self.e_p_of5d.original_entry = self.e_p_or4c
 | 
			
		||||
 | 
			
		||||
        # Offset transactions
 | 
			
		||||
        self.t_r_of1: TransactionData = TransactionData(
 | 
			
		||||
        # Offset vouchers
 | 
			
		||||
        self.v_r_of1: VoucherData = VoucherData(
 | 
			
		||||
            25, [CurrencyData("USD", [self.e_r_of1d], [self.e_r_of1c])])
 | 
			
		||||
        self.t_r_of2: TransactionData = TransactionData(
 | 
			
		||||
        self.v_r_of2: VoucherData = VoucherData(
 | 
			
		||||
            20, [CurrencyData("USD",
 | 
			
		||||
                              [self.e_r_of2d, self.e_r_of3d, self.e_r_of4d],
 | 
			
		||||
                              [self.e_r_of2c, self.e_r_of3c, self.e_r_of4c])])
 | 
			
		||||
        self.t_r_of3: TransactionData = TransactionData(
 | 
			
		||||
        self.v_r_of3: VoucherData = VoucherData(
 | 
			
		||||
            15, [CurrencyData("USD", [self.e_r_of5d], [self.e_r_of5c])])
 | 
			
		||||
        self.t_p_of1: TransactionData = TransactionData(
 | 
			
		||||
        self.v_p_of1: VoucherData = VoucherData(
 | 
			
		||||
            15, [CurrencyData("USD", [self.e_p_of1d], [self.e_p_of1c])])
 | 
			
		||||
        self.t_p_of2: TransactionData = TransactionData(
 | 
			
		||||
        self.v_p_of2: VoucherData = VoucherData(
 | 
			
		||||
            10, [CurrencyData("USD",
 | 
			
		||||
                              [self.e_p_of2d, self.e_p_of3d, self.e_p_of4d],
 | 
			
		||||
                              [self.e_p_of2c, self.e_p_of3c, self.e_p_of4c])])
 | 
			
		||||
        self.t_p_of3: TransactionData = TransactionData(
 | 
			
		||||
        self.v_p_of3: VoucherData = VoucherData(
 | 
			
		||||
            5, [CurrencyData("USD", [self.e_p_of5d], [self.e_p_of5c])])
 | 
			
		||||
 | 
			
		||||
        self.__add_txn(self.t_r_of1)
 | 
			
		||||
        self.__add_txn(self.t_r_of2)
 | 
			
		||||
        self.__add_txn(self.t_r_of3)
 | 
			
		||||
        self.__add_txn(self.t_p_of1)
 | 
			
		||||
        self.__add_txn(self.t_p_of2)
 | 
			
		||||
        self.__add_txn(self.t_p_of3)
 | 
			
		||||
        self.__add_voucher(self.v_r_of1)
 | 
			
		||||
        self.__add_voucher(self.v_r_of2)
 | 
			
		||||
        self.__add_voucher(self.v_r_of3)
 | 
			
		||||
        self.__add_voucher(self.v_p_of1)
 | 
			
		||||
        self.__add_voucher(self.v_p_of2)
 | 
			
		||||
        self.__add_voucher(self.v_p_of3)
 | 
			
		||||
 | 
			
		||||
    def __add_txn(self, txn_data: TransactionData) -> None:
 | 
			
		||||
        """Adds a transaction.
 | 
			
		||||
    def __add_voucher(self, voucher_data: VoucherData) -> None:
 | 
			
		||||
        """Adds a voucher.
 | 
			
		||||
 | 
			
		||||
        :param txn_data: The transaction data.
 | 
			
		||||
        :param voucher_data: The voucher data.
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        from accounting.models import Transaction
 | 
			
		||||
        store_uri: str = "/accounting/transactions/store/transfer"
 | 
			
		||||
        from accounting.models import Voucher
 | 
			
		||||
        store_uri: str = "/accounting/vouchers/store/transfer"
 | 
			
		||||
 | 
			
		||||
        response: httpx.Response = self.client.post(
 | 
			
		||||
            store_uri, data=txn_data.new_form(self.csrf_token))
 | 
			
		||||
            store_uri, data=voucher_data.new_form(self.csrf_token))
 | 
			
		||||
        assert response.status_code == 302
 | 
			
		||||
        txn_id: int = match_txn_detail(response.headers["Location"])
 | 
			
		||||
        txn_data.id = txn_id
 | 
			
		||||
        voucher_id: int = match_voucher_detail(response.headers["Location"])
 | 
			
		||||
        voucher_data.id = voucher_id
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
            txn: Transaction | None = db.session.get(Transaction, txn_id)
 | 
			
		||||
            assert txn is not None
 | 
			
		||||
            for i in range(len(txn.currencies)):
 | 
			
		||||
                for j in range(len(txn.currencies[i].debit)):
 | 
			
		||||
                    txn_data.currencies[i].debit[j].id \
 | 
			
		||||
                        = txn.currencies[i].debit[j].id
 | 
			
		||||
                for j in range(len(txn.currencies[i].credit)):
 | 
			
		||||
                    txn_data.currencies[i].credit[j].id \
 | 
			
		||||
                        = txn.currencies[i].credit[j].id
 | 
			
		||||
            voucher: Voucher | None = db.session.get(Voucher, voucher_id)
 | 
			
		||||
            assert voucher is not None
 | 
			
		||||
            for i in range(len(voucher.currencies)):
 | 
			
		||||
                for j in range(len(voucher.currencies[i].debit)):
 | 
			
		||||
                    voucher_data.currencies[i].debit[j].id \
 | 
			
		||||
                        = voucher.currencies[i].debit[j].id
 | 
			
		||||
                for j in range(len(voucher.currencies[i].credit)):
 | 
			
		||||
                    voucher_data.currencies[i].credit[j].id \
 | 
			
		||||
                        = voucher.currencies[i].credit[j].id
 | 
			
		||||
 
 | 
			
		||||
@@ -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 common test libraries for the transaction test cases.
 | 
			
		||||
"""The common test libraries for the voucher test cases.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import re
 | 
			
		||||
@@ -57,10 +57,10 @@ class Accounts:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_add_form(csrf_token: str) -> dict[str, str]:
 | 
			
		||||
    """Returns the form data to add a new transaction.
 | 
			
		||||
    """Returns the form data to add a new voucher.
 | 
			
		||||
 | 
			
		||||
    :param csrf_token: The CSRF token.
 | 
			
		||||
    :return: The form data to add a new transaction.
 | 
			
		||||
    :return: The form data to add a new voucher.
 | 
			
		||||
    """
 | 
			
		||||
    return {"csrf_token": csrf_token,
 | 
			
		||||
            "next": NEXT_URI,
 | 
			
		||||
@@ -124,28 +124,28 @@ def get_add_form(csrf_token: str) -> dict[str, str]:
 | 
			
		||||
            "note": f"\n \n\n  \n{NON_EMPTY_NOTE}  \n  \n\n  "}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_unchanged_update_form(txn_id: int, app: Flask, csrf_token: str) \
 | 
			
		||||
def get_unchanged_update_form(voucher_id: int, app: Flask, csrf_token: str) \
 | 
			
		||||
        -> dict[str, str]:
 | 
			
		||||
    """Returns the form data to update a transaction, where the data are not
 | 
			
		||||
    """Returns the form data to update a voucher, where the data are not
 | 
			
		||||
    changed.
 | 
			
		||||
 | 
			
		||||
    :param txn_id: The transaction ID.
 | 
			
		||||
    :param voucher_id: The voucher ID.
 | 
			
		||||
    :param app: The Flask application.
 | 
			
		||||
    :param csrf_token: The CSRF token.
 | 
			
		||||
    :return: The form data to update the transaction, where the data are not
 | 
			
		||||
    :return: The form data to update the voucher, where the data are not
 | 
			
		||||
        changed.
 | 
			
		||||
    """
 | 
			
		||||
    from accounting.models import Transaction, TransactionCurrency
 | 
			
		||||
    from accounting.models import Voucher, VoucherCurrency
 | 
			
		||||
    with app.app_context():
 | 
			
		||||
        txn: Transaction | None = db.session.get(Transaction, txn_id)
 | 
			
		||||
        assert txn is not None
 | 
			
		||||
        currencies: list[TransactionCurrency] = txn.currencies
 | 
			
		||||
        voucher: Voucher | None = db.session.get(Voucher, voucher_id)
 | 
			
		||||
        assert voucher is not None
 | 
			
		||||
        currencies: list[VoucherCurrency] = voucher.currencies
 | 
			
		||||
 | 
			
		||||
    form: dict[str, str] = {"csrf_token": csrf_token,
 | 
			
		||||
                            "next": NEXT_URI,
 | 
			
		||||
                            "date": txn.date,
 | 
			
		||||
                            "note": "  \n \n\n  " if txn.note is None
 | 
			
		||||
                            else f"\n    \n\n \n  \n{txn.note}  \n\n   "}
 | 
			
		||||
                            "date": voucher.date,
 | 
			
		||||
                            "note": "  \n \n\n  " if voucher.note is None
 | 
			
		||||
                            else f"\n    \n\n \n  \n{voucher.note}  \n\n   "}
 | 
			
		||||
    currency_indices_used: set[int] = set()
 | 
			
		||||
    currency_no: int = 0
 | 
			
		||||
    for currency in currencies:
 | 
			
		||||
@@ -200,21 +200,21 @@ def __get_new_index(indices_used: set[int]) -> int:
 | 
			
		||||
            return index
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_update_form(txn_id: int, app: Flask,
 | 
			
		||||
def get_update_form(voucher_id: int, app: Flask,
 | 
			
		||||
                    csrf_token: str, is_debit: bool | None) -> dict[str, str]:
 | 
			
		||||
    """Returns the form data to update a transaction, where the data are
 | 
			
		||||
    """Returns the form data to update a voucher, where the data are
 | 
			
		||||
    changed.
 | 
			
		||||
 | 
			
		||||
    :param txn_id: The transaction ID.
 | 
			
		||||
    :param voucher_id: The voucher ID.
 | 
			
		||||
    :param app: The Flask application.
 | 
			
		||||
    :param csrf_token: The CSRF token.
 | 
			
		||||
    :param is_debit: True for a cash expense transaction, False for a cash
 | 
			
		||||
        income transaction, or None for a transfer transaction
 | 
			
		||||
    :return: The form data to update the transaction, where the data are
 | 
			
		||||
    :param is_debit: True for a cash disbursement voucher, False for a cash
 | 
			
		||||
        receipt voucher, or None for a transfer voucher
 | 
			
		||||
    :return: The form data to update the voucher, where the data are
 | 
			
		||||
        changed.
 | 
			
		||||
    """
 | 
			
		||||
    form: dict[str, str] = get_unchanged_update_form(
 | 
			
		||||
        txn_id, app, csrf_token)
 | 
			
		||||
        voucher_id, app, csrf_token)
 | 
			
		||||
 | 
			
		||||
    # Mess up the entries in a currency
 | 
			
		||||
    currency_prefix: str = __get_currency_prefix(form, "USD")
 | 
			
		||||
@@ -240,7 +240,7 @@ def __mess_up_debit(form: dict[str, str], currency_prefix: str) \
 | 
			
		||||
    key: str
 | 
			
		||||
    m: re.Match
 | 
			
		||||
 | 
			
		||||
    # Remove the office expense
 | 
			
		||||
    # Remove the office disbursement
 | 
			
		||||
    key = [x for x in form
 | 
			
		||||
           if x.startswith(currency_prefix)
 | 
			
		||||
           and form[x] == Accounts.OFFICE][0]
 | 
			
		||||
@@ -249,7 +249,7 @@ def __mess_up_debit(form: dict[str, str], currency_prefix: str) \
 | 
			
		||||
    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)}
 | 
			
		||||
    # Add a new travel expense
 | 
			
		||||
    # Add a new travel disbursement
 | 
			
		||||
    indices: set[int] = set()
 | 
			
		||||
    for key in form:
 | 
			
		||||
        m = re.match(r"^.+-(\d+)-amount$", key)
 | 
			
		||||
@@ -279,7 +279,7 @@ def __mess_up_credit(form: dict[str, str], currency_prefix: str) \
 | 
			
		||||
    key: str
 | 
			
		||||
    m: re.Match
 | 
			
		||||
 | 
			
		||||
    # Remove the sales income
 | 
			
		||||
    # Remove the sales receipt
 | 
			
		||||
    key = [x for x in form
 | 
			
		||||
           if x.startswith(currency_prefix)
 | 
			
		||||
           and form[x] == Accounts.SALES][0]
 | 
			
		||||
@@ -288,7 +288,7 @@ def __mess_up_credit(form: dict[str, str], currency_prefix: str) \
 | 
			
		||||
    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)}
 | 
			
		||||
    # Add a new agency income
 | 
			
		||||
    # Add a new agency receipt
 | 
			
		||||
    indices: set[int] = set()
 | 
			
		||||
    for key in form:
 | 
			
		||||
        m = re.match(r"^.+-(\d+)-amount$", key)
 | 
			
		||||
@@ -389,35 +389,34 @@ def __get_currency_prefix(form: dict[str, str], code: str) -> str:
 | 
			
		||||
    return m.group(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_txn(client: httpx.Client, form: dict[str, str]) -> int:
 | 
			
		||||
    """Adds a transfer transaction.
 | 
			
		||||
def add_voucher(client: httpx.Client, form: dict[str, str]) -> int:
 | 
			
		||||
    """Adds a transfer voucher.
 | 
			
		||||
 | 
			
		||||
    :param client: The client.
 | 
			
		||||
    :param form: The form data.
 | 
			
		||||
    :return: The newly-added transaction ID.
 | 
			
		||||
    :return: The newly-added voucher ID.
 | 
			
		||||
    """
 | 
			
		||||
    prefix: str = "/accounting/transactions"
 | 
			
		||||
    txn_type: str = "transfer"
 | 
			
		||||
    prefix: str = "/accounting/vouchers"
 | 
			
		||||
    voucher_type: str = "transfer"
 | 
			
		||||
    if len({x for x in form if "-debit-" in x}) == 0:
 | 
			
		||||
        txn_type = "income"
 | 
			
		||||
        voucher_type = "receipt"
 | 
			
		||||
    elif len({x for x in form if "-credit-" in x}) == 0:
 | 
			
		||||
        txn_type = "expense"
 | 
			
		||||
    store_uri = f"{prefix}/store/{txn_type}"
 | 
			
		||||
        voucher_type = "disbursement"
 | 
			
		||||
    store_uri = f"{prefix}/store/{voucher_type}"
 | 
			
		||||
    response: httpx.Response = client.post(store_uri, data=form)
 | 
			
		||||
    assert response.status_code == 302
 | 
			
		||||
    return match_txn_detail(response.headers["Location"])
 | 
			
		||||
    return match_voucher_detail(response.headers["Location"])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def match_txn_detail(location: str) -> int:
 | 
			
		||||
    """Validates if the redirect location is the transaction detail, and
 | 
			
		||||
    returns the transaction ID on success.
 | 
			
		||||
def match_voucher_detail(location: str) -> int:
 | 
			
		||||
    """Validates if the redirect location is the voucher detail, and
 | 
			
		||||
    returns the voucher ID on success.
 | 
			
		||||
 | 
			
		||||
    :param location: The redirect location.
 | 
			
		||||
    :return: The transaction ID.
 | 
			
		||||
    :raise AssertionError: When the location is not the transaction detail.
 | 
			
		||||
    :return: The voucher ID.
 | 
			
		||||
    :raise AssertionError: When the location is not the voucher detail.
 | 
			
		||||
    """
 | 
			
		||||
    m: re.Match = re.match(
 | 
			
		||||
        r"^/accounting/transactions/(\d+)\?next=%2F_next",
 | 
			
		||||
    m: re.Match = re.match(r"^/accounting/vouchers/(\d+)\?next=%2F_next",
 | 
			
		||||
                           location)
 | 
			
		||||
    assert m is not None
 | 
			
		||||
    return int(m.group(1))
 | 
			
		||||
		Reference in New Issue
	
	Block a user