From 5db13393ccf0efe7c88f3320f5a77164e1bde1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BE=9D=E7=91=AA=E8=B2=93?= Date: Sun, 19 Mar 2023 13:44:51 +0800 Subject: [PATCH] Renamed "transaction" to "voucher", "cash expense transaction" to "cash disbursement voucher", and "cash income transaction" to "cash receipt voucher". --- src/accounting/__init__.py | 4 +- src/accounting/models.py | 92 +- src/accounting/report/period/chooser.py | 6 +- .../report/reports/balance_sheet.py | 14 +- .../report/reports/income_expenses.py | 36 +- .../report/reports/income_statement.py | 8 +- src/accounting/report/reports/journal.py | 28 +- src/accounting/report/reports/ledger.py | 32 +- src/accounting/report/reports/search.py | 42 +- .../report/reports/trial_balance.py | 8 +- .../report/utils/base_page_params.py | 10 +- src/accounting/static/css/style.css | 8 +- src/accounting/static/js/account-selector.js | 2 +- .../static/js/journal-entry-editor.js | 6 +- .../static/js/original-entry-selector.js | 2 +- .../{transaction-form.js => voucher-form.js} | 24 +- ...{transaction-order.js => voucher-order.js} | 2 +- src/accounting/template_globals.py | 2 +- .../accounting/report/balance-sheet.html | 2 +- ...fab.html => add-voucher-material-fab.html} | 8 +- .../report/include/toolbar-buttons.html | 6 +- .../accounting/report/income-expenses.html | 2 +- .../accounting/report/income-statement.html | 2 +- .../templates/accounting/report/journal.html | 10 +- .../templates/accounting/report/ledger.html | 2 +- .../templates/accounting/report/search.html | 10 +- .../accounting/report/trial-balance.html | 2 +- .../disbursement}/create.html | 8 +- .../disbursement}/detail.html | 14 +- .../income => voucher/disbursement}/edit.html | 10 +- .../include/form-currency-item.html | 6 +- .../disbursement}/include/form.html | 12 +- .../include/account-selector-modal.html | 0 .../include/detail-entries.html | 10 +- .../include/detail.html | 16 +- .../include/form-entry-item.html | 4 +- .../include/form.html | 8 +- .../include/journal-entry-editor-modal.html | 0 .../original-entry-selector-modal.html | 4 +- .../include/summary-editor-modal.html | 0 .../{transaction => voucher}/order.html | 8 +- .../transfer => voucher/receipt}/create.html | 8 +- .../income => voucher/receipt}/detail.html | 14 +- .../expense => voucher/receipt}/edit.html | 10 +- .../receipt}/include/form-currency-item.html | 6 +- .../receipt}/include/form.html | 12 +- .../income => voucher/transfer}/create.html | 8 +- .../transfer/detail.html | 18 +- .../transfer/edit.html | 10 +- .../transfer/include/form-currency-item.html | 10 +- .../transfer/include/form.html | 16 +- src/accounting/transaction/forms/reorder.py | 92 -- src/accounting/transaction/utils/operators.py | 326 ------- src/accounting/transaction/views.py | 222 ----- .../utils/{txn_types.py => voucher_types.py} | 16 +- .../{transaction => voucher}/__init__.py | 12 +- .../{transaction => voucher}/converters.py | 68 +- .../forms/__init__.py | 8 +- .../forms/currency.py | 28 +- .../forms/journal_entry.py | 22 +- src/accounting/voucher/forms/reorder.py | 92 ++ .../forms/voucher.py} | 131 +-- .../template_filters.py | 10 +- .../utils/__init__.py | 2 +- .../utils/account_option.py | 2 +- .../utils/offset_alias.py | 0 src/accounting/voucher/utils/operators.py | 326 +++++++ .../utils/original_entries.py | 10 +- .../utils/summary_editor.py | 0 src/accounting/voucher/views.py | 221 +++++ tests/test_offset.py | 360 ++++---- tests/test_summary_editor.py | 30 +- .../{test_transaction.py => test_voucher.py} | 847 +++++++++--------- tests/testlib_offset.py | 114 +-- tests/{testlib_txn.py => testlib_voucher.py} | 83 +- 75 files changed, 1812 insertions(+), 1792 deletions(-) rename src/accounting/static/js/{transaction-form.js => voucher-form.js} (98%) rename src/accounting/static/js/{transaction-order.js => voucher-order.js} (95%) rename src/accounting/templates/accounting/report/include/{add-txn-material-fab.html => add-voucher-material-fab.html} (71%) rename src/accounting/templates/accounting/{transaction/expense => voucher/disbursement}/create.html (69%) rename src/accounting/templates/accounting/{transaction/expense => voucher/disbursement}/detail.html (69%) rename src/accounting/templates/accounting/{transaction/income => voucher/disbursement}/edit.html (58%) rename src/accounting/templates/accounting/{transaction/expense => voucher/disbursement}/include/form-currency-item.html (96%) rename src/accounting/templates/accounting/{transaction/expense => voucher/disbursement}/include/form.html (80%) rename src/accounting/templates/accounting/{transaction => voucher}/include/account-selector-modal.html (100%) rename src/accounting/templates/accounting/{transaction => voucher}/include/detail-entries.html (83%) rename src/accounting/templates/accounting/{transaction => voucher}/include/detail.html (85%) rename src/accounting/templates/accounting/{transaction => voucher}/include/form-entry-item.html (96%) rename src/accounting/templates/accounting/{transaction => voucher}/include/form.html (93%) rename src/accounting/templates/accounting/{transaction => voucher}/include/journal-entry-editor-modal.html (100%) rename src/accounting/templates/accounting/{transaction => voucher}/include/original-entry-selector-modal.html (85%) rename src/accounting/templates/accounting/{transaction => voucher}/include/summary-editor-modal.html (100%) rename src/accounting/templates/accounting/{transaction => voucher}/order.html (88%) rename src/accounting/templates/accounting/{transaction/transfer => voucher/receipt}/create.html (70%) rename src/accounting/templates/accounting/{transaction/income => voucher/receipt}/detail.html (69%) rename src/accounting/templates/accounting/{transaction/expense => voucher/receipt}/edit.html (59%) rename src/accounting/templates/accounting/{transaction/income => voucher/receipt}/include/form-currency-item.html (96%) rename src/accounting/templates/accounting/{transaction/income => voucher/receipt}/include/form.html (81%) rename src/accounting/templates/accounting/{transaction/income => voucher/transfer}/create.html (70%) rename src/accounting/templates/accounting/{transaction => voucher}/transfer/detail.html (71%) rename src/accounting/templates/accounting/{transaction => voucher}/transfer/edit.html (59%) rename src/accounting/templates/accounting/{transaction => voucher}/transfer/include/form-currency-item.html (96%) rename src/accounting/templates/accounting/{transaction => voucher}/transfer/include/form.html (79%) delete mode 100644 src/accounting/transaction/forms/reorder.py delete mode 100644 src/accounting/transaction/utils/operators.py delete mode 100644 src/accounting/transaction/views.py rename src/accounting/utils/{txn_types.py => voucher_types.py} (73%) rename src/accounting/{transaction => voucher}/__init__.py (73%) rename src/accounting/{transaction => voucher}/converters.py (50%) rename src/accounting/{transaction => voucher}/forms/__init__.py (74%) rename src/accounting/{transaction => voucher}/forms/currency.py (93%) rename src/accounting/{transaction => voucher}/forms/journal_entry.py (96%) create mode 100644 src/accounting/voucher/forms/reorder.py rename src/accounting/{transaction/forms/transaction.py => voucher/forms/voucher.py} (85%) rename src/accounting/{transaction => voucher}/template_filters.py (87%) rename src/accounting/{transaction => voucher}/utils/__init__.py (93%) rename src/accounting/{transaction => voucher}/utils/account_option.py (96%) rename src/accounting/{transaction => voucher}/utils/offset_alias.py (100%) create mode 100644 src/accounting/voucher/utils/operators.py rename src/accounting/{transaction => voucher}/utils/original_entries.py (91%) rename src/accounting/{transaction => voucher}/utils/summary_editor.py (100%) create mode 100644 src/accounting/voucher/views.py rename tests/{test_transaction.py => test_voucher.py} (75%) rename tests/{testlib_txn.py => testlib_voucher.py} (88%) diff --git a/src/accounting/__init__.py b/src/accounting/__init__.py index e195c96..32b7693 100644 --- a/src/accounting/__init__.py +++ b/src/accounting/__init__.py @@ -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) diff --git a/src/accounting/models.py b/src/accounting/models.py index e37604b..c20096c 100644 --- a/src/accounting/models.py +++ b/src/accounting/models.py @@ -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,18 +551,18 @@ 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, - debit=[y for y in by_currency[x] - if y.is_debit], - credit=[y for y in by_currency[x] - if not y.is_debit]) + return [VoucherCurrency(code=x, + debit=[y for y in by_currency[x] + if y.is_debit], + credit=[y for y in by_currency[x] + if not y.is_debit]) 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", - ondelete="CASCADE"), - nullable=False) - """The transaction ID.""" - transaction = db.relationship(Transaction, back_populates="entries") - """The transaction.""" + voucher_id = db.Column(db.Integer, + db.ForeignKey(Voucher.id, onupdate="CASCADE", + ondelete="CASCADE"), + nullable=False) + """The voucher ID.""" + voucher = db.relationship(Voucher, back_populates="entries") + """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)]) diff --git a/src/accounting/report/period/chooser.py b/src/accounting/report/period/chooser.py index 223c112..0dd684a 100644 --- a/src/accounting/report/period/chooser.py +++ b/src/accounting/report/period/chooser.py @@ -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 diff --git a/src/accounting/report/reports/balance_sheet.py b/src/accounting/report/reports/balance_sheet.py index ced86ce..04d228d 100644 --- a/src/accounting/report/reports/balance_sheet.py +++ b/src/accounting/report/reports/balance_sheet.py @@ -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, diff --git a/src/accounting/report/reports/income_expenses.py b/src/accounting/report/reports/income_expenses.py index 23655fa..c9131f5 100644 --- a/src/accounting/report/reports/income_expenses.py +++ b/src/accounting/report/reports/income_expenses.py @@ -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.""" diff --git a/src/accounting/report/reports/income_statement.py b/src/accounting/report/reports/income_statement.py index 655ae6e..f36e226 100644 --- a/src/accounting/report/reports/income_statement.py +++ b/src/accounting/report/reports/income_statement.py @@ -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) diff --git a/src/accounting/report/reports/journal.py b/src/accounting/report/reports/journal.py index 2cb82dd..ec7e65d 100644 --- a/src/accounting/report/reports/journal.py +++ b/src/accounting/report/reports/journal.py @@ -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. diff --git a/src/accounting/report/reports/ledger.py b/src/accounting/report/reports/ledger.py index b452c67..de455d1 100644 --- a/src/accounting/report/reports/ledger.py +++ b/src/accounting/report/reports/ledger.py @@ -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.""" diff --git a/src/accounting/report/reports/search.py b/src/accounting/report/reports/search.py index b2d14e3..9bbf6cc 100644 --- a/src/accounting/report/reports/search.py +++ b/src/accounting/report/reports/search.py @@ -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): diff --git a/src/accounting/report/reports/trial_balance.py b/src/accounting/report/reports/trial_balance.py index d6a84c2..e98f22d 100644 --- a/src/accounting/report/reports/trial_balance.py +++ b/src/accounting/report/reports/trial_balance.py @@ -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) diff --git a/src/accounting/report/utils/base_page_params.py b/src/accounting/report/utils/base_page_params.py index 436942c..a78def3 100644 --- a/src/accounting/report/utils/base_page_params.py +++ b/src/accounting/report/utils/base_page_params.py @@ -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: diff --git a/src/accounting/static/css/style.css b/src/accounting/static/css/style.css index 4713fbb..b914c4d 100644 --- a/src/accounting/static/css/style.css +++ b/src/accounting/static/css/style.css @@ -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; } diff --git a/src/accounting/static/js/account-selector.js b/src/accounting/static/js/account-selector.js index c4a7f8d..84be852 100644 --- a/src/accounting/static/js/account-selector.js +++ b/src/accounting/static/js/account-selector.js @@ -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. diff --git a/src/accounting/static/js/journal-entry-editor.js b/src/accounting/static/js/journal-entry-editor.js index 63ec09d..ef488b8 100644 --- a/src/accounting/static/js/journal-entry-editor.js +++ b/src/accounting/static/js/journal-entry-editor.js @@ -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; diff --git a/src/accounting/static/js/original-entry-selector.js b/src/accounting/static/js/original-entry-selector.js index 79d091b..3bd0700 100644 --- a/src/accounting/static/js/original-entry-selector.js +++ b/src/accounting/static/js/original-entry-selector.js @@ -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 */ diff --git a/src/accounting/static/js/transaction-form.js b/src/accounting/static/js/voucher-form.js similarity index 98% rename from src/accounting/static/js/transaction-form.js rename to src/accounting/static/js/voucher-form.js index 7b6b325..5b7ac9c 100644 --- a/src/accounting/static/js/transaction-form.js +++ b/src/accounting/static/js/voucher-form.js @@ -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) { diff --git a/src/accounting/static/js/transaction-order.js b/src/accounting/static/js/voucher-order.js similarity index 95% rename from src/accounting/static/js/transaction-order.js rename to src/accounting/static/js/voucher-order.js index be8e29d..6c7d44c 100644 --- a/src/accounting/static/js/transaction-order.js +++ b/src/accounting/static/js/voucher-order.js @@ -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. diff --git a/src/accounting/template_globals.py b/src/accounting/template_globals.py index 07d2c35..ff9169e 100644 --- a/src/accounting/template_globals.py +++ b/src/accounting/template_globals.py @@ -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 diff --git a/src/accounting/templates/accounting/report/balance-sheet.html b/src/accounting/templates/accounting/report/balance-sheet.html index 245d098..9ce54d8 100644 --- a/src/accounting/templates/accounting/report/balance-sheet.html +++ b/src/accounting/templates/accounting/report/balance-sheet.html @@ -37,7 +37,7 @@ First written: 2023/3/7 {% endwith %} -{% 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" %} diff --git a/src/accounting/templates/accounting/report/include/add-txn-material-fab.html b/src/accounting/templates/accounting/report/include/add-voucher-material-fab.html similarity index 71% rename from src/accounting/templates/accounting/report/include/add-txn-material-fab.html rename to src/accounting/templates/accounting/report/include/add-voucher-material-fab.html index bd62aa9..fa00b65 100644 --- a/src/accounting/templates/accounting/report/include/add-txn-material-fab.html +++ b/src/accounting/templates/accounting/report/include/add-voucher-material-fab.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() %}
diff --git a/src/accounting/templates/accounting/report/include/toolbar-buttons.html b/src/accounting/templates/accounting/report/include/toolbar-buttons.html index 7c68ddc..75380b8 100644 --- a/src/accounting/templates/accounting/report/include/toolbar-buttons.html +++ b/src/accounting/templates/accounting/report/include/toolbar-buttons.html @@ -27,17 +27,17 @@ First written: 2023/3/8
-{% 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" %} diff --git a/src/accounting/templates/accounting/report/income-statement.html b/src/accounting/templates/accounting/report/income-statement.html index 8b854db..1941031 100644 --- a/src/accounting/templates/accounting/report/income-statement.html +++ b/src/accounting/templates/accounting/report/income-statement.html @@ -37,7 +37,7 @@ First written: 2023/3/7 {% endwith %} -{% 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" %} diff --git a/src/accounting/templates/accounting/report/journal.html b/src/accounting/templates/accounting/report/journal.html index b49b063..791528b 100644 --- a/src/accounting/templates/accounting/report/journal.html +++ b/src/accounting/templates/accounting/report/journal.html @@ -36,7 +36,7 @@ First written: 2023/3/4 {% endwith %} -{% 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
{% for entry in report.entries %} - -
{{ entry.transaction.date|accounting_format_date }}
+
+
{{ entry.voucher.date|accounting_format_date }}
{{ entry.currency.name }}
{{ entry.account.code }} @@ -77,11 +77,11 @@ First written: 2023/3/4
{% for entry in report.entries %} - +
- {{ entry.transaction.date|accounting_format_date }} + {{ entry.voucher.date|accounting_format_date }} {{ entry.account.title|title }} {% if entry.currency.code != accounting_default_currency_code() %} {{ entry.currency.code }} diff --git a/src/accounting/templates/accounting/report/ledger.html b/src/accounting/templates/accounting/report/ledger.html index ed1a43a..a631ae2 100644 --- a/src/accounting/templates/accounting/report/ledger.html +++ b/src/accounting/templates/accounting/report/ledger.html @@ -38,7 +38,7 @@ First written: 2023/3/5 {% endwith %}
-{% 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" %} diff --git a/src/accounting/templates/accounting/report/search.html b/src/accounting/templates/accounting/report/search.html index cb4f98b..686f1e3 100644 --- a/src/accounting/templates/accounting/report/search.html +++ b/src/accounting/templates/accounting/report/search.html @@ -35,7 +35,7 @@ First written: 2023/3/8 {% endwith %}
-{% 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
{% for entry in report.entries %} - -
{{ entry.transaction.date|accounting_format_date }}
+
+
{{ entry.voucher.date|accounting_format_date }}
{{ entry.currency.name }}
{{ entry.account.code }} @@ -74,11 +74,11 @@ First written: 2023/3/8
{% for entry in report.entries %} - +
- {{ entry.transaction.date|accounting_format_date }} + {{ entry.voucher.date|accounting_format_date }} {{ entry.account.title|title }} {% if entry.currency.code != accounting_default_currency_code() %} {{ entry.currency.code }} diff --git a/src/accounting/templates/accounting/report/trial-balance.html b/src/accounting/templates/accounting/report/trial-balance.html index 8e67aa5..acd112e 100644 --- a/src/accounting/templates/accounting/report/trial-balance.html +++ b/src/accounting/templates/accounting/report/trial-balance.html @@ -37,7 +37,7 @@ First written: 2023/3/5 {% endwith %}
-{% 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" %} diff --git a/src/accounting/templates/accounting/transaction/expense/create.html b/src/accounting/templates/accounting/voucher/disbursement/create.html similarity index 69% rename from src/accounting/templates/accounting/transaction/expense/create.html rename to src/accounting/templates/accounting/voucher/disbursement/create.html index 18be4ab..b513844 100644 --- a/src/accounting/templates/accounting/transaction/expense/create.html +++ b/src/accounting/templates/accounting/voucher/disbursement/create.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 %} diff --git a/src/accounting/templates/accounting/transaction/expense/detail.html b/src/accounting/templates/accounting/voucher/disbursement/detail.html similarity index 69% rename from src/accounting/templates/accounting/transaction/expense/detail.html rename to src/accounting/templates/accounting/voucher/disbursement/detail.html index 009c274..95c4587 100644 --- a/src/accounting/templates/accounting/transaction/expense/detail.html +++ b/src/accounting/templates/accounting/voucher/disbursement/detail.html @@ -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_("To Transfer") }} {% endblock %} -{% block transaction_currencies %} +{% block voucher_currencies %} {% for currency in obj.currencies %}
{{ currency.name }}
    -
  • {{ A_("Content") }}
  • +
  • {{ A_("Content") }}
  • {% with entries = currency.debit %} - {% include "accounting/transaction/include/detail-entries.html" %} + {% include "accounting/voucher/include/detail-entries.html" %} {% endwith %} -
  • +
  • {{ A_("Total") }}
    {{ currency.debit_total|accounting_format_amount }}
    diff --git a/src/accounting/templates/accounting/transaction/income/edit.html b/src/accounting/templates/accounting/voucher/disbursement/edit.html similarity index 58% rename from src/accounting/templates/accounting/transaction/income/edit.html rename to src/accounting/templates/accounting/voucher/disbursement/edit.html index 52f6159..f1f76ce 100644 --- a/src/accounting/templates/accounting/transaction/income/edit.html +++ b/src/accounting/templates/accounting/voucher/disbursement/edit.html @@ -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 %} diff --git a/src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html b/src/accounting/templates/accounting/voucher/disbursement/include/form-currency-item.html similarity index 96% rename from src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html rename to src/accounting/templates/accounting/voucher/disbursement/include/form-currency-item.html index 308a137..233d021 100644 --- a/src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html +++ b/src/accounting/templates/accounting/voucher/disbursement/include/form-currency-item.html @@ -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 %}
diff --git a/src/accounting/templates/accounting/transaction/expense/include/form.html b/src/accounting/templates/accounting/voucher/disbursement/include/form.html similarity index 80% rename from src/accounting/templates/accounting/transaction/expense/include/form.html rename to src/accounting/templates/accounting/voucher/disbursement/include/form.html index bc10a00..05c58b1 100644 --- a/src/accounting/templates/accounting/transaction/expense/include/form.html +++ b/src/accounting/templates/accounting/voucher/disbursement/include/form.html @@ -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 %} diff --git a/src/accounting/templates/accounting/transaction/include/account-selector-modal.html b/src/accounting/templates/accounting/voucher/include/account-selector-modal.html similarity index 100% rename from src/accounting/templates/accounting/transaction/include/account-selector-modal.html rename to src/accounting/templates/accounting/voucher/include/account-selector-modal.html diff --git a/src/accounting/templates/accounting/transaction/include/detail-entries.html b/src/accounting/templates/accounting/voucher/include/detail-entries.html similarity index 83% rename from src/accounting/templates/accounting/transaction/include/detail-entries.html rename to src/accounting/templates/accounting/voucher/include/detail-entries.html index 55c439b..54d59c6 100644 --- a/src/accounting/templates/accounting/transaction/include/detail-entries.html +++ b/src/accounting/templates/accounting/voucher/include/detail-entries.html @@ -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 #} {#
    For SonarQube not to complain about incorrect HTML #} {% for entry in entries %} -
  • +
  • {{ entry.account }}
    @@ -30,7 +30,7 @@ First written: 2023/3/14 {% endif %} {% if entry.original_entry %}
    - + {{ A_("Offset %(entry)s", entry=entry.original_entry) }}
    @@ -43,8 +43,8 @@ First written: 2023/3/14
      {% for offset in entry.offsets %}
    • - - {{ offset.transaction.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }} + + {{ offset.voucher.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}
    • {% endfor %} diff --git a/src/accounting/templates/accounting/transaction/include/detail.html b/src/accounting/templates/accounting/voucher/include/detail.html similarity index 85% rename from src/accounting/templates/accounting/transaction/include/detail.html rename to src/accounting/templates/accounting/voucher/include/detail.html index c2c9609..c0a02b2 100644 --- a/src/accounting/templates/accounting/transaction/include/detail.html +++ b/src/accounting/templates/accounting/voucher/include/detail.html @@ -31,12 +31,12 @@ First written: 2023/2/26 {{ A_("Back") }} {% if accounting_can_edit() %} - + {{ A_("Settings") }} {% endif %} - + {{ A_("Order") }} @@ -58,14 +58,14 @@ First written: 2023/2/26 {% if accounting_can_edit() %} {% endif %} {% if accounting_can_edit() and obj.can_delete %} -
      + {% if request.args.next %} @@ -74,11 +74,11 @@ First written: 2023/2/26
      -{% 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 %} diff --git a/src/accounting/templates/accounting/transaction/include/journal-entry-editor-modal.html b/src/accounting/templates/accounting/voucher/include/journal-entry-editor-modal.html similarity index 100% rename from src/accounting/templates/accounting/transaction/include/journal-entry-editor-modal.html rename to src/accounting/templates/accounting/voucher/include/journal-entry-editor-modal.html diff --git a/src/accounting/templates/accounting/transaction/include/original-entry-selector-modal.html b/src/accounting/templates/accounting/voucher/include/original-entry-selector-modal.html similarity index 85% rename from src/accounting/templates/accounting/transaction/include/original-entry-selector-modal.html rename to src/accounting/templates/accounting/voucher/include/original-entry-selector-modal.html index 74160ea..0fc8ed6 100644 --- a/src/accounting/templates/accounting/transaction/include/original-entry-selector-modal.html +++ b/src/accounting/templates/accounting/voucher/include/original-entry-selector-modal.html @@ -37,8 +37,8 @@ First written: 2023/2/25
        {% for entry in form.original_entry_options %} -
      • -
        {{ entry.transaction.date|accounting_format_date }} {{ entry.summary|accounting_default }}
        +
      • +
        {{ entry.voucher.date|accounting_format_date }} {{ entry.summary|accounting_default }}
        {{ entry.net_balance|accounting_format_amount }} diff --git a/src/accounting/templates/accounting/transaction/include/summary-editor-modal.html b/src/accounting/templates/accounting/voucher/include/summary-editor-modal.html similarity index 100% rename from src/accounting/templates/accounting/transaction/include/summary-editor-modal.html rename to src/accounting/templates/accounting/voucher/include/summary-editor-modal.html diff --git a/src/accounting/templates/accounting/transaction/order.html b/src/accounting/templates/accounting/voucher/order.html similarity index 88% rename from src/accounting/templates/accounting/transaction/order.html rename to src/accounting/templates/accounting/voucher/order.html index f9c551b..7441b57 100644 --- a/src/accounting/templates/accounting/transaction/order.html +++ b/src/accounting/templates/accounting/voucher/order.html @@ -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 %} - + {% 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
        {% if list|length > 1 and accounting_can_edit() %} -
        + {% if request.args.next %} diff --git a/src/accounting/templates/accounting/transaction/transfer/create.html b/src/accounting/templates/accounting/voucher/receipt/create.html similarity index 70% rename from src/accounting/templates/accounting/transaction/transfer/create.html rename to src/accounting/templates/accounting/voucher/receipt/create.html index fad8d15..dff38bd 100644 --- a/src/accounting/templates/accounting/transaction/transfer/create.html +++ b/src/accounting/templates/accounting/voucher/receipt/create.html @@ -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 %} diff --git a/src/accounting/templates/accounting/transaction/income/detail.html b/src/accounting/templates/accounting/voucher/receipt/detail.html similarity index 69% rename from src/accounting/templates/accounting/transaction/income/detail.html rename to src/accounting/templates/accounting/voucher/receipt/detail.html index 2275326..e84a245 100644 --- a/src/accounting/templates/accounting/transaction/income/detail.html +++ b/src/accounting/templates/accounting/voucher/receipt/detail.html @@ -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_("To Transfer") }} {% endblock %} -{% block transaction_currencies %} +{% block voucher_currencies %} {% for currency in obj.currencies %}
        {{ currency.name }}
          -
        • {{ A_("Content") }}
        • +
        • {{ A_("Content") }}
        • {% with entries = currency.credit %} - {% include "accounting/transaction/include/detail-entries.html" %} + {% include "accounting/voucher/include/detail-entries.html" %} {% endwith %} -
        • +
        • {{ A_("Total") }}
          {{ currency.debit_total|accounting_format_amount }}
          diff --git a/src/accounting/templates/accounting/transaction/expense/edit.html b/src/accounting/templates/accounting/voucher/receipt/edit.html similarity index 59% rename from src/accounting/templates/accounting/transaction/expense/edit.html rename to src/accounting/templates/accounting/voucher/receipt/edit.html index 57a1925..d931dfb 100644 --- a/src/accounting/templates/accounting/transaction/expense/edit.html +++ b/src/accounting/templates/accounting/voucher/receipt/edit.html @@ -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 %} diff --git a/src/accounting/templates/accounting/transaction/income/include/form-currency-item.html b/src/accounting/templates/accounting/voucher/receipt/include/form-currency-item.html similarity index 96% rename from src/accounting/templates/accounting/transaction/income/include/form-currency-item.html rename to src/accounting/templates/accounting/voucher/receipt/include/form-currency-item.html index 326631d..efa64da 100644 --- a/src/accounting/templates/accounting/transaction/income/include/form-currency-item.html +++ b/src/accounting/templates/accounting/voucher/receipt/include/form-currency-item.html @@ -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 %}
        diff --git a/src/accounting/templates/accounting/transaction/income/include/form.html b/src/accounting/templates/accounting/voucher/receipt/include/form.html similarity index 81% rename from src/accounting/templates/accounting/transaction/income/include/form.html rename to src/accounting/templates/accounting/voucher/receipt/include/form.html index 9dfaa30..365a5c1 100644 --- a/src/accounting/templates/accounting/transaction/income/include/form.html +++ b/src/accounting/templates/accounting/voucher/receipt/include/form.html @@ -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 %} diff --git a/src/accounting/templates/accounting/transaction/income/create.html b/src/accounting/templates/accounting/voucher/transfer/create.html similarity index 70% rename from src/accounting/templates/accounting/transaction/income/create.html rename to src/accounting/templates/accounting/voucher/transfer/create.html index 22dc9a7..0bbeb22 100644 --- a/src/accounting/templates/accounting/transaction/income/create.html +++ b/src/accounting/templates/accounting/voucher/transfer/create.html @@ -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 %} diff --git a/src/accounting/templates/accounting/transaction/transfer/detail.html b/src/accounting/templates/accounting/voucher/transfer/detail.html similarity index 71% rename from src/accounting/templates/accounting/transaction/transfer/detail.html rename to src/accounting/templates/accounting/voucher/transfer/detail.html index 664d01b..31d2f83 100644 --- a/src/accounting/templates/accounting/transaction/transfer/detail.html +++ b/src/accounting/templates/accounting/voucher/transfer/detail.html @@ -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 %}
        {{ currency.name }}
        @@ -30,11 +30,11 @@ First written: 2023/2/26 {# The debit entries #}
          -
        • {{ A_("Debit") }}
        • +
        • {{ A_("Debit") }}
        • {% with entries = currency.debit %} - {% include "accounting/transaction/include/detail-entries.html" %} + {% include "accounting/voucher/include/detail-entries.html" %} {% endwith %} -
        • +
        • {{ A_("Total") }}
          {{ currency.debit_total|accounting_format_amount }}
          @@ -46,11 +46,11 @@ First written: 2023/2/26 {# The credit entries #}
            -
          • {{ A_("Credit") }}
          • +
          • {{ A_("Credit") }}
          • {% with entries = currency.credit %} - {% include "accounting/transaction/include/detail-entries.html" %} + {% include "accounting/voucher/include/detail-entries.html" %} {% endwith %} -
          • +
          • {{ A_("Total") }}
            {{ currency.debit_total|accounting_format_amount }}
            diff --git a/src/accounting/templates/accounting/transaction/transfer/edit.html b/src/accounting/templates/accounting/voucher/transfer/edit.html similarity index 59% rename from src/accounting/templates/accounting/transaction/transfer/edit.html rename to src/accounting/templates/accounting/voucher/transfer/edit.html index 91fdd77..2c1a994 100644 --- a/src/accounting/templates/accounting/transaction/transfer/edit.html +++ b/src/accounting/templates/accounting/voucher/transfer/edit.html @@ -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 %} diff --git a/src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html b/src/accounting/templates/accounting/voucher/transfer/include/form-currency-item.html similarity index 96% rename from src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html rename to src/accounting/templates/accounting/voucher/transfer/include/form-currency-item.html index 9c089a1..408fbdc 100644 --- a/src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html +++ b/src/accounting/templates/accounting/voucher/transfer/include/form-currency-item.html @@ -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 %}
          @@ -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 %}
        diff --git a/src/accounting/templates/accounting/transaction/transfer/include/form.html b/src/accounting/templates/accounting/voucher/transfer/include/form.html similarity index 79% rename from src/accounting/templates/accounting/transaction/transfer/include/form.html rename to src/accounting/templates/accounting/voucher/transfer/include/form.html index f92541b..1eba831 100644 --- a/src/accounting/templates/accounting/transaction/transfer/include/form.html +++ b/src/accounting/templates/accounting/voucher/transfer/include/form.html @@ -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 %} diff --git a/src/accounting/transaction/forms/reorder.py b/src/accounting/transaction/forms/reorder.py deleted file mode 100644 index 0a6e0dc..0000000 --- a/src/accounting/transaction/forms/reorder.py +++ /dev/null @@ -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 diff --git a/src/accounting/transaction/utils/operators.py b/src/accounting/transaction/utils/operators.py deleted file mode 100644 index 9a2d154..0000000 --- a/src/accounting/transaction/utils/operators.py +++ /dev/null @@ -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 diff --git a/src/accounting/transaction/views.py b/src/accounting/transaction/views.py deleted file mode 100644 index 86c4266..0000000 --- a/src/accounting/transaction/views.py +++ /dev/null @@ -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/", 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/", 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("/", 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("//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("//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("//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/", 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/", 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") diff --git a/src/accounting/utils/txn_types.py b/src/accounting/utils/voucher_types.py similarity index 73% rename from src/accounting/utils/txn_types.py rename to src/accounting/utils/voucher_types.py index 27dd6e4..da365aa 100644 --- a/src/accounting/utils/txn_types.py +++ b/src/accounting/utils/voucher_types.py @@ -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.""" diff --git a/src/accounting/transaction/__init__.py b/src/accounting/voucher/__init__.py similarity index 73% rename from src/accounting/transaction/__init__.py rename to src/accounting/voucher/__init__.py index 445d557..03209b1 100644 --- a/src/accounting/transaction/__init__.py +++ b/src/accounting/voucher/__init__.py @@ -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") diff --git a/src/accounting/transaction/converters.py b/src/accounting/voucher/converters.py similarity index 50% rename from src/accounting/transaction/converters.py rename to src/accounting/voucher/converters.py index 8e6b502..3c09baf 100644 --- a/src/accounting/transaction/converters.py +++ b/src/accounting/voucher/converters.py @@ -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) diff --git a/src/accounting/transaction/forms/__init__.py b/src/accounting/voucher/forms/__init__.py similarity index 74% rename from src/accounting/transaction/forms/__init__.py rename to src/accounting/voucher/forms/__init__.py index 0f46e18..0fcab97 100644 --- a/src/accounting/transaction/forms/__init__.py +++ b/src/accounting/voucher/forms/__init__.py @@ -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 diff --git a/src/accounting/transaction/forms/currency.py b/src/accounting/voucher/forms/currency.py similarity index 93% rename from src/accounting/transaction/forms/currency.py rename to src/accounting/voucher/forms/currency.py index 47d95cc..001b759 100644 --- a/src/accounting/transaction/forms/currency.py +++ b/src/accounting/voucher/forms/currency.py @@ -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, diff --git a/src/accounting/transaction/forms/journal_entry.py b/src/accounting/voucher/forms/journal_entry.py similarity index 96% rename from src/accounting/transaction/forms/journal_entry.py rename to src/accounting/voucher/forms/journal_entry.py index 765f191..a278ae9 100644 --- a/src/accounting/transaction/forms/journal_entry.py +++ b/src/accounting/voucher/forms/journal_entry.py @@ -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") diff --git a/src/accounting/voucher/forms/reorder.py b/src/accounting/voucher/forms/reorder.py new file mode 100644 index 0000000..ffcf833 --- /dev/null +++ b/src/accounting/voucher/forms/reorder.py @@ -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 diff --git a/src/accounting/transaction/forms/transaction.py b/src/accounting/voucher/forms/voucher.py similarity index 85% rename from src/accounting/transaction/forms/transaction.py rename to src/accounting/voucher/forms/voucher.py index 9316487..de9e2d1 100644 --- a/src/accounting/transaction/forms/transaction.py +++ b/src/accounting/voucher/forms/voucher.py @@ -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] \ diff --git a/src/accounting/transaction/template_filters.py b/src/accounting/voucher/template_filters.py similarity index 87% rename from src/accounting/transaction/template_filters.py rename to src/accounting/voucher/template_filters.py index e23d8ac..31df53a 100644 --- a/src/accounting/transaction/template_filters.py +++ b/src/accounting/voucher/template_filters.py @@ -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) diff --git a/src/accounting/transaction/utils/__init__.py b/src/accounting/voucher/utils/__init__.py similarity index 93% rename from src/accounting/transaction/utils/__init__.py rename to src/accounting/voucher/utils/__init__.py index e8f75e2..39d624e 100644 --- a/src/accounting/transaction/utils/__init__.py +++ b/src/accounting/voucher/utils/__init__.py @@ -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. """ diff --git a/src/accounting/transaction/utils/account_option.py b/src/accounting/voucher/utils/account_option.py similarity index 96% rename from src/accounting/transaction/utils/account_option.py rename to src/accounting/voucher/utils/account_option.py index 97d017f..18b655f 100644 --- a/src/accounting/transaction/utils/account_option.py +++ b/src/accounting/voucher/utils/account_option.py @@ -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 diff --git a/src/accounting/transaction/utils/offset_alias.py b/src/accounting/voucher/utils/offset_alias.py similarity index 100% rename from src/accounting/transaction/utils/offset_alias.py rename to src/accounting/voucher/utils/offset_alias.py diff --git a/src/accounting/voucher/utils/operators.py b/src/accounting/voucher/utils/operators.py new file mode 100644 index 0000000..1d93a77 --- /dev/null +++ b/src/accounting/voucher/utils/operators.py @@ -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 diff --git a/src/accounting/transaction/utils/original_entries.py b/src/accounting/voucher/utils/original_entries.py similarity index 91% rename from src/accounting/transaction/utils/original_entries.py rename to src/accounting/voucher/utils/original_entries.py index ca1db36..52c8051 100644 --- a/src/accounting/transaction/utils/original_entries.py +++ b/src/accounting/voucher/utils/original_entries.py @@ -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] diff --git a/src/accounting/transaction/utils/summary_editor.py b/src/accounting/voucher/utils/summary_editor.py similarity index 100% rename from src/accounting/transaction/utils/summary_editor.py rename to src/accounting/voucher/utils/summary_editor.py diff --git a/src/accounting/voucher/views.py b/src/accounting/voucher/views.py new file mode 100644 index 0000000..2cf258c --- /dev/null +++ b/src/accounting/voucher/views.py @@ -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/", 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/", 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("/", 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("//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("//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("//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/", 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/", 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") diff --git a/tests/test_offset.py b/tests/test_offset.py index a782295..9c37dda 100644 --- a/tests/test_offset.py +++ b/tests/test_offset.py @@ -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) diff --git a/tests/test_summary_editor.py b/tests/test_summary_editor.py index d631ea6..738441f 100644 --- a/tests/test_summary_editor.py +++ b/tests/test_summary_editor.py @@ -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 ", diff --git a/tests/test_transaction.py b/tests/test_voucher.py similarity index 75% rename from tests/test_transaction.py rename to tests/test_voucher.py index 3e7e943..87100ec 100644 --- a/tests/test_transaction.py +++ b/tests/test_voucher.py @@ -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 test for the transaction management. +"""The test for the voucher management. """ import unittest @@ -28,19 +28,19 @@ from flask.testing import FlaskCliRunner from test_site import db from testlib import create_test_app, get_client -from testlib_txn import Accounts, get_add_form, get_unchanged_update_form, \ - get_update_form, match_txn_detail, set_negative_amount, \ - remove_debit_in_a_currency, remove_credit_in_a_currency, NEXT_URI, \ - NON_EMPTY_NOTE, EMPTY_NOTE, add_txn +from testlib_voucher import NEXT_URI, NON_EMPTY_NOTE, EMPTY_NOTE, Accounts, \ + get_add_form, get_unchanged_update_form, get_update_form, \ + match_voucher_detail, set_negative_amount, remove_debit_in_a_currency, \ + remove_credit_in_a_currency, add_voucher -PREFIX: str = "/accounting/transactions" -"""The URL prefix for the transaction management.""" +PREFIX: str = "/accounting/vouchers" +"""The URL prefix for the voucher management.""" RETURN_TO_URI: str = "/accounting/reports" """The URL to return to after the operation.""" -class CashIncomeTransactionTestCase(unittest.TestCase): - """The cash income transaction test case.""" +class CashReceiptVoucherTestCase(unittest.TestCase): + """The cash receipt voucher test case.""" def setUp(self) -> None: """Sets up the test. @@ -52,7 +52,7 @@ class CashIncomeTransactionTestCase(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") @@ -66,7 +66,7 @@ class CashIncomeTransactionTestCase(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") @@ -77,29 +77,30 @@ class CashIncomeTransactionTestCase(unittest.TestCase): :return: None. """ client, csrf_token = get_client(self.app, "nobody") - txn_id: int = add_txn(self.client, self.__get_add_form()) + voucher_id: int = add_voucher(self.client, self.__get_add_form()) add_form: dict[str, str] = self.__get_add_form() add_form["csrf_token"] = csrf_token - update_form: dict[str, str] = self.__get_update_form(txn_id) + update_form: dict[str, str] = self.__get_update_form(voucher_id) update_form["csrf_token"] = csrf_token response: httpx.Response - response = client.get(f"{PREFIX}/{txn_id}") + response = client.get(f"{PREFIX}/{voucher_id}") self.assertEqual(response.status_code, 403) - response = client.get(f"{PREFIX}/create/income") + response = client.get(f"{PREFIX}/create/receipt") self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/store/income", data=add_form) + response = client.post(f"{PREFIX}/store/receipt", data=add_form) self.assertEqual(response.status_code, 403) - response = client.get(f"{PREFIX}/{txn_id}/edit") + response = client.get(f"{PREFIX}/{voucher_id}/edit") self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{txn_id}/update", data=update_form) + response = client.post(f"{PREFIX}/{voucher_id}/update", + data=update_form) self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{txn_id}/delete", + response = client.post(f"{PREFIX}/{voucher_id}/delete", data={"csrf_token": csrf_token}) self.assertEqual(response.status_code, 403) @@ -109,29 +110,30 @@ class CashIncomeTransactionTestCase(unittest.TestCase): :return: None. """ client, csrf_token = get_client(self.app, "viewer") - txn_id: int = add_txn(self.client, self.__get_add_form()) + voucher_id: int = add_voucher(self.client, self.__get_add_form()) add_form: dict[str, str] = self.__get_add_form() add_form["csrf_token"] = csrf_token - update_form: dict[str, str] = self.__get_update_form(txn_id) + update_form: dict[str, str] = self.__get_update_form(voucher_id) update_form["csrf_token"] = csrf_token response: httpx.Response - response = client.get(f"{PREFIX}/{txn_id}") + response = client.get(f"{PREFIX}/{voucher_id}") self.assertEqual(response.status_code, 200) - response = client.get(f"{PREFIX}/create/income") + response = client.get(f"{PREFIX}/create/receipt") self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/store/income", data=add_form) + response = client.post(f"{PREFIX}/store/receipt", data=add_form) self.assertEqual(response.status_code, 403) - response = client.get(f"{PREFIX}/{txn_id}/edit") + response = client.get(f"{PREFIX}/{voucher_id}/edit") self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{txn_id}/update", data=update_form) + response = client.post(f"{PREFIX}/{voucher_id}/update", + data=update_form) self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{txn_id}/delete", + response = client.post(f"{PREFIX}/{voucher_id}/delete", data={"csrf_token": csrf_token}) self.assertEqual(response.status_code, 403) @@ -140,47 +142,47 @@ class CashIncomeTransactionTestCase(unittest.TestCase): :return: None. """ - txn_id: int = add_txn(self.client, self.__get_add_form()) + voucher_id: int = add_voucher(self.client, self.__get_add_form()) add_form: dict[str, str] = self.__get_add_form() - update_form: dict[str, str] = self.__get_update_form(txn_id) + update_form: dict[str, str] = self.__get_update_form(voucher_id) response: httpx.Response - response = self.client.get(f"{PREFIX}/{txn_id}") + response = self.client.get(f"{PREFIX}/{voucher_id}") self.assertEqual(response.status_code, 200) - response = self.client.get(f"{PREFIX}/create/income") + response = self.client.get(f"{PREFIX}/create/receipt") self.assertEqual(response.status_code, 200) - response = self.client.post(f"{PREFIX}/store/income", + response = self.client.post(f"{PREFIX}/store/receipt", data=add_form) self.assertEqual(response.status_code, 302) - match_txn_detail(response.headers["Location"]) + match_voucher_detail(response.headers["Location"]) - response = self.client.get(f"{PREFIX}/{txn_id}/edit") + response = self.client.get(f"{PREFIX}/{voucher_id}/edit") self.assertEqual(response.status_code, 200) - response = self.client.post(f"{PREFIX}/{txn_id}/update", + response = self.client.post(f"{PREFIX}/{voucher_id}/update", data=update_form) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], - f"{PREFIX}/{txn_id}?next=%2F_next") + f"{PREFIX}/{voucher_id}?next=%2F_next") - response = self.client.post(f"{PREFIX}/{txn_id}/delete", + response = self.client.post(f"{PREFIX}/{voucher_id}/delete", data={"csrf_token": self.csrf_token}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], RETURN_TO_URI) def test_add(self) -> None: - """Tests to add the transactions. + """Tests to add the vouchers. :return: None. """ - from accounting.models import Transaction, TransactionCurrency - create_uri: str = f"{PREFIX}/create/income?next=%2F_next" - store_uri: str = f"{PREFIX}/store/income" + from accounting.models import Voucher, VoucherCurrency + create_uri: str = f"{PREFIX}/create/receipt?next=%2F_next" + store_uri: str = f"{PREFIX}/store/receipt" response: httpx.Response form: dict[str, str] - txn: Transaction | None + voucher: Voucher | None # No currency content form = self.__get_add_form() @@ -249,12 +251,12 @@ class CashIncomeTransactionTestCase(unittest.TestCase): response = self.client.post(store_uri, data=self.__get_add_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) - self.assertIsNotNone(txn) - currencies: list[TransactionCurrency] = txn.currencies + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + currencies: list[VoucherCurrency] = voucher.currencies self.assertEqual(len(currencies), 3) self.assertEqual(currencies[0].code, "JPY") @@ -308,36 +310,36 @@ class CashIncomeTransactionTestCase(unittest.TestCase): self.assertEqual(currencies[2].credit[1].account.code, Accounts.DONATION) - self.assertEqual(txn.note, NON_EMPTY_NOTE) + self.assertEqual(voucher.note, NON_EMPTY_NOTE) # Success, with empty note form = self.__get_add_form() form["note"] = EMPTY_NOTE 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) - self.assertIsNotNone(txn) - self.assertIsNone(txn.note) + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + self.assertIsNone(voucher.note) def test_basic_update(self) -> None: - """Tests the basic rules to update a transaction. + """Tests the basic rules to update a voucher. :return: None. """ - from accounting.models import Transaction, TransactionCurrency - txn_id: int = add_txn(self.client, self.__get_add_form()) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - edit_uri: str = f"{PREFIX}/{txn_id}/edit?next=%2F_next" - update_uri: str = f"{PREFIX}/{txn_id}/update" - form_0: dict[str, str] = self.__get_update_form(txn_id) + from accounting.models import Voucher, VoucherCurrency + voucher_id: int = add_voucher(self.client, self.__get_add_form()) + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + edit_uri: str = f"{PREFIX}/{voucher_id}/edit?next=%2F_next" + update_uri: str = f"{PREFIX}/{voucher_id}/update" + form_0: dict[str, str] = self.__get_update_form(voucher_id) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - currencies0: list[TransactionCurrency] = txn.currencies + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + currencies0: list[VoucherCurrency] = voucher.currencies old_id: set[int] = set() for currency in currencies0: old_id.update({x.id for x in currency.debit}) @@ -411,9 +413,9 @@ class CashIncomeTransactionTestCase(unittest.TestCase): self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - currencies1: list[TransactionCurrency] = txn.currencies + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + currencies1: list[VoucherCurrency] = voucher.currencies self.assertEqual(len(currencies1), 3) self.assertEqual(currencies1[0].code, "AUD") @@ -482,80 +484,80 @@ class CashIncomeTransactionTestCase(unittest.TestCase): self.assertEqual(currencies1[2].credit[2].account.code, Accounts.SERVICE) - self.assertEqual(txn.note, NON_EMPTY_NOTE) + self.assertEqual(voucher.note, NON_EMPTY_NOTE) def test_update_not_modified(self) -> None: """Tests that the data is not modified. :return: None. """ - from accounting.models import Transaction - txn_id: int = add_txn(self.client, self.__get_add_form()) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - update_uri: str = f"{PREFIX}/{txn_id}/update" - txn: Transaction + from accounting.models import Voucher + voucher_id: int = add_voucher(self.client, self.__get_add_form()) + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + update_uri: str = f"{PREFIX}/{voucher_id}/update" + voucher: Voucher response: httpx.Response response = self.client.post( - update_uri, data=self.__get_unchanged_update_form(txn_id)) + update_uri, data=self.__get_unchanged_update_form(voucher_id)) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - txn.created_at = txn.created_at - timedelta(seconds=5) - txn.updated_at = txn.created_at + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + voucher.created_at = voucher.created_at - timedelta(seconds=5) + voucher.updated_at = voucher.created_at db.session.commit() response = self.client.post( - update_uri, data=self.__get_update_form(txn_id)) + update_uri, data=self.__get_update_form(voucher_id)) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - self.assertLess(txn.created_at, txn.updated_at) + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + self.assertLess(voucher.created_at, voucher.updated_at) def test_created_updated_by(self) -> None: """Tests the created-by and updated-by record. :return: None. """ - from accounting.models import Transaction - txn_id: int = add_txn(self.client, self.__get_add_form()) + from accounting.models import Voucher + voucher_id: int = add_voucher(self.client, self.__get_add_form()) editor_username, editor2_username = "editor", "editor2" client, csrf_token = get_client(self.app, editor2_username) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - update_uri: str = f"{PREFIX}/{txn_id}/update" - txn: Transaction + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + update_uri: str = f"{PREFIX}/{voucher_id}/update" + voucher: Voucher response: httpx.Response with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertEqual(txn.created_by.username, editor_username) - self.assertEqual(txn.updated_by.username, editor_username) + voucher = db.session.get(Voucher, voucher_id) + self.assertEqual(voucher.created_by.username, editor_username) + self.assertEqual(voucher.updated_by.username, editor_username) - form: dict[str, str] = self.__get_update_form(txn_id) + form: dict[str, str] = self.__get_update_form(voucher_id) form["csrf_token"] = csrf_token response = client.post(update_uri, data=form) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertEqual(txn.created_by.username, editor_username) - self.assertEqual(txn.updated_by.username, editor2_username) + voucher = db.session.get(Voucher, voucher_id) + self.assertEqual(voucher.created_by.username, editor_username) + self.assertEqual(voucher.updated_by.username, editor2_username) def test_delete(self) -> None: - """Tests to delete a transaction. + """Tests to delete a voucher. :return: None. """ - txn_id: int = add_txn(self.client, self.__get_add_form()) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - delete_uri: str = f"{PREFIX}/{txn_id}/delete" + voucher_id: int = add_voucher(self.client, self.__get_add_form()) + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + delete_uri: str = f"{PREFIX}/{voucher_id}/delete" response: httpx.Response response = self.client.get(detail_uri) @@ -574,43 +576,43 @@ class CashIncomeTransactionTestCase(unittest.TestCase): self.assertEqual(response.status_code, 404) def __get_add_form(self) -> dict[str, str]: - """Returns the form data to add a new transaction. + """Returns the form data to add a new voucher. - :return: The form data to add a new transaction. + :return: The form data to add a new voucher. """ form: dict[str, str] = get_add_form(self.csrf_token) form = {x: form[x] for x in form if "-debit-" not in x} return form - def __get_unchanged_update_form(self, txn_id: int) -> dict[str, str]: - """Returns the form data to update a transaction, where the data are - not changed. + def __get_unchanged_update_form(self, voucher_id: int) -> dict[str, str]: + """Returns the form data to update a voucher, where the data are not + changed. - :param txn_id: The transaction ID. - :return: The form data to update the transaction, where the data are - not changed. + :param voucher_id: The voucher ID. + :return: The form data to update the voucher, where the data are not + changed. """ form: dict[str, str] = get_unchanged_update_form( - txn_id, self.app, self.csrf_token) + voucher_id, self.app, self.csrf_token) form = {x: form[x] for x in form if "-debit-" not in x} return form - def __get_update_form(self, txn_id: int) -> dict[str, str]: - """Returns the form data to update a transaction, where the data are + def __get_update_form(self, voucher_id: int) -> dict[str, str]: + """Returns the form data to update a voucher, where the data are changed. - :param txn_id: The transaction ID. - :return: The form data to update the transaction, where the data are + :param voucher_id: The voucher ID. + :return: The form data to update the voucher, where the data are changed. """ form: dict[str, str] = get_update_form( - txn_id, self.app, self.csrf_token, False) + voucher_id, self.app, self.csrf_token, False) form = {x: form[x] for x in form if "-debit-" not in x} return form -class CashExpenseTransactionTestCase(unittest.TestCase): - """The cash expense transaction test case.""" +class CashDisbursementVoucherTestCase(unittest.TestCase): + """The cash disbursement voucher test case.""" def setUp(self) -> None: """Sets up the test. @@ -622,7 +624,7 @@ class CashExpenseTransactionTestCase(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") @@ -636,7 +638,7 @@ class CashExpenseTransactionTestCase(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") @@ -647,29 +649,30 @@ class CashExpenseTransactionTestCase(unittest.TestCase): :return: None. """ client, csrf_token = get_client(self.app, "nobody") - txn_id: int = add_txn(self.client, self.__get_add_form()) + voucher_id: int = add_voucher(self.client, self.__get_add_form()) add_form: dict[str, str] = self.__get_add_form() add_form["csrf_token"] = csrf_token - update_form: dict[str, str] = self.__get_update_form(txn_id) + update_form: dict[str, str] = self.__get_update_form(voucher_id) update_form["csrf_token"] = csrf_token response: httpx.Response - response = client.get(f"{PREFIX}/{txn_id}") + response = client.get(f"{PREFIX}/{voucher_id}") self.assertEqual(response.status_code, 403) - response = client.get(f"{PREFIX}/create/expense") + response = client.get(f"{PREFIX}/create/disbursement") self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/store/expense", data=add_form) + response = client.post(f"{PREFIX}/store/disbursement", data=add_form) self.assertEqual(response.status_code, 403) - response = client.get(f"{PREFIX}/{txn_id}/edit") + response = client.get(f"{PREFIX}/{voucher_id}/edit") self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{txn_id}/update", data=update_form) + response = client.post(f"{PREFIX}/{voucher_id}/update", + data=update_form) self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{txn_id}/delete", + response = client.post(f"{PREFIX}/{voucher_id}/delete", data={"csrf_token": csrf_token}) self.assertEqual(response.status_code, 403) @@ -679,29 +682,30 @@ class CashExpenseTransactionTestCase(unittest.TestCase): :return: None. """ client, csrf_token = get_client(self.app, "viewer") - txn_id: int = add_txn(self.client, self.__get_add_form()) + voucher_id: int = add_voucher(self.client, self.__get_add_form()) add_form: dict[str, str] = self.__get_add_form() add_form["csrf_token"] = csrf_token - update_form: dict[str, str] = self.__get_update_form(txn_id) + update_form: dict[str, str] = self.__get_update_form(voucher_id) update_form["csrf_token"] = csrf_token response: httpx.Response - response = client.get(f"{PREFIX}/{txn_id}") + response = client.get(f"{PREFIX}/{voucher_id}") self.assertEqual(response.status_code, 200) - response = client.get(f"{PREFIX}/create/expense") + response = client.get(f"{PREFIX}/create/disbursement") self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/store/expense", data=add_form) + response = client.post(f"{PREFIX}/store/disbursement", data=add_form) self.assertEqual(response.status_code, 403) - response = client.get(f"{PREFIX}/{txn_id}/edit") + response = client.get(f"{PREFIX}/{voucher_id}/edit") self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{txn_id}/update", data=update_form) + response = client.post(f"{PREFIX}/{voucher_id}/update", + data=update_form) self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{txn_id}/delete", + response = client.post(f"{PREFIX}/{voucher_id}/delete", data={"csrf_token": csrf_token}) self.assertEqual(response.status_code, 403) @@ -710,47 +714,47 @@ class CashExpenseTransactionTestCase(unittest.TestCase): :return: None. """ - txn_id: int = add_txn(self.client, self.__get_add_form()) + voucher_id: int = add_voucher(self.client, self.__get_add_form()) add_form: dict[str, str] = self.__get_add_form() - update_form: dict[str, str] = self.__get_update_form(txn_id) + update_form: dict[str, str] = self.__get_update_form(voucher_id) response: httpx.Response - response = self.client.get(f"{PREFIX}/{txn_id}") + response = self.client.get(f"{PREFIX}/{voucher_id}") self.assertEqual(response.status_code, 200) - response = self.client.get(f"{PREFIX}/create/expense") + response = self.client.get(f"{PREFIX}/create/disbursement") self.assertEqual(response.status_code, 200) - response = self.client.post(f"{PREFIX}/store/expense", + response = self.client.post(f"{PREFIX}/store/disbursement", data=add_form) self.assertEqual(response.status_code, 302) - match_txn_detail(response.headers["Location"]) + match_voucher_detail(response.headers["Location"]) - response = self.client.get(f"{PREFIX}/{txn_id}/edit") + response = self.client.get(f"{PREFIX}/{voucher_id}/edit") self.assertEqual(response.status_code, 200) - response = self.client.post(f"{PREFIX}/{txn_id}/update", + response = self.client.post(f"{PREFIX}/{voucher_id}/update", data=update_form) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], - f"{PREFIX}/{txn_id}?next=%2F_next") + f"{PREFIX}/{voucher_id}?next=%2F_next") - response = self.client.post(f"{PREFIX}/{txn_id}/delete", + response = self.client.post(f"{PREFIX}/{voucher_id}/delete", data={"csrf_token": self.csrf_token}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], RETURN_TO_URI) def test_add(self) -> None: - """Tests to add the transactions. + """Tests to add the vouchers. :return: None. """ - from accounting.models import Transaction, TransactionCurrency - create_uri: str = f"{PREFIX}/create/expense?next=%2F_next" - store_uri: str = f"{PREFIX}/store/expense" + from accounting.models import Voucher, VoucherCurrency + create_uri: str = f"{PREFIX}/create/disbursement?next=%2F_next" + store_uri: str = f"{PREFIX}/store/disbursement" response: httpx.Response form: dict[str, str] - txn: Transaction | None + voucher: Voucher | None # No currency content form = self.__get_add_form() @@ -819,12 +823,12 @@ class CashExpenseTransactionTestCase(unittest.TestCase): response = self.client.post(store_uri, data=self.__get_add_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) - self.assertIsNotNone(txn) - currencies: list[TransactionCurrency] = txn.currencies + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + currencies: list[VoucherCurrency] = voucher.currencies self.assertEqual(len(currencies), 3) self.assertEqual(currencies[0].code, "JPY") @@ -881,36 +885,36 @@ class CashExpenseTransactionTestCase(unittest.TestCase): self.assertEqual(currencies[2].credit[0].amount, sum([x.amount for x in currencies[2].debit])) - self.assertEqual(txn.note, NON_EMPTY_NOTE) + self.assertEqual(voucher.note, NON_EMPTY_NOTE) # Success, with empty note form = self.__get_add_form() form["note"] = EMPTY_NOTE 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) - self.assertIsNotNone(txn) - self.assertIsNone(txn.note) + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + self.assertIsNone(voucher.note) def test_basic_update(self) -> None: - """Tests the basic rules to update a transaction. + """Tests the basic rules to update a voucher. :return: None. """ - from accounting.models import Transaction, TransactionCurrency - txn_id: int = add_txn(self.client, self.__get_add_form()) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - edit_uri: str = f"{PREFIX}/{txn_id}/edit?next=%2F_next" - update_uri: str = f"{PREFIX}/{txn_id}/update" - form_0: dict[str, str] = self.__get_update_form(txn_id) + from accounting.models import Voucher, VoucherCurrency + voucher_id: int = add_voucher(self.client, self.__get_add_form()) + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + edit_uri: str = f"{PREFIX}/{voucher_id}/edit?next=%2F_next" + update_uri: str = f"{PREFIX}/{voucher_id}/update" + form_0: dict[str, str] = self.__get_update_form(voucher_id) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - currencies0: list[TransactionCurrency] = txn.currencies + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + currencies0: list[VoucherCurrency] = voucher.currencies old_id: set[int] = set() for currency in currencies0: old_id.update({x.id for x in currency.debit}) @@ -984,9 +988,9 @@ class CashExpenseTransactionTestCase(unittest.TestCase): self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - currencies1: list[TransactionCurrency] = txn.currencies + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + currencies1: list[VoucherCurrency] = voucher.currencies self.assertEqual(len(currencies1), 3) self.assertEqual(currencies1[0].code, "AUD") @@ -1059,80 +1063,80 @@ class CashExpenseTransactionTestCase(unittest.TestCase): self.assertEqual(currencies1[2].credit[0].amount, sum([x.amount for x in currencies1[2].debit])) - self.assertEqual(txn.note, NON_EMPTY_NOTE) + self.assertEqual(voucher.note, NON_EMPTY_NOTE) def test_update_not_modified(self) -> None: """Tests that the data is not modified. :return: None. """ - from accounting.models import Transaction - txn_id: int = add_txn(self.client, self.__get_add_form()) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - update_uri: str = f"{PREFIX}/{txn_id}/update" - txn: Transaction + from accounting.models import Voucher + voucher_id: int = add_voucher(self.client, self.__get_add_form()) + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + update_uri: str = f"{PREFIX}/{voucher_id}/update" + voucher: Voucher response: httpx.Response response = self.client.post( - update_uri, data=self.__get_unchanged_update_form(txn_id)) + update_uri, data=self.__get_unchanged_update_form(voucher_id)) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - txn.created_at = txn.created_at - timedelta(seconds=5) - txn.updated_at = txn.created_at + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + voucher.created_at = voucher.created_at - timedelta(seconds=5) + voucher.updated_at = voucher.created_at db.session.commit() response = self.client.post( - update_uri, data=self.__get_update_form(txn_id)) + update_uri, data=self.__get_update_form(voucher_id)) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - self.assertLess(txn.created_at, txn.updated_at) + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + self.assertLess(voucher.created_at, voucher.updated_at) def test_created_updated_by(self) -> None: """Tests the created-by and updated-by record. :return: None. """ - from accounting.models import Transaction - txn_id: int = add_txn(self.client, self.__get_add_form()) + from accounting.models import Voucher + voucher_id: int = add_voucher(self.client, self.__get_add_form()) editor_username, editor2_username = "editor", "editor2" client, csrf_token = get_client(self.app, editor2_username) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - update_uri: str = f"{PREFIX}/{txn_id}/update" - txn: Transaction + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + update_uri: str = f"{PREFIX}/{voucher_id}/update" + voucher: Voucher response: httpx.Response with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertEqual(txn.created_by.username, editor_username) - self.assertEqual(txn.updated_by.username, editor_username) + voucher = db.session.get(Voucher, voucher_id) + self.assertEqual(voucher.created_by.username, editor_username) + self.assertEqual(voucher.updated_by.username, editor_username) - form: dict[str, str] = self.__get_update_form(txn_id) + form: dict[str, str] = self.__get_update_form(voucher_id) form["csrf_token"] = csrf_token response = client.post(update_uri, data=form) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertEqual(txn.created_by.username, editor_username) - self.assertEqual(txn.updated_by.username, editor2_username) + voucher = db.session.get(Voucher, voucher_id) + self.assertEqual(voucher.created_by.username, editor_username) + self.assertEqual(voucher.updated_by.username, editor2_username) def test_delete(self) -> None: - """Tests to delete a transaction. + """Tests to delete a voucher. :return: None. """ - txn_id: int = add_txn(self.client, self.__get_add_form()) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - delete_uri: str = f"{PREFIX}/{txn_id}/delete" + voucher_id: int = add_voucher(self.client, self.__get_add_form()) + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + delete_uri: str = f"{PREFIX}/{voucher_id}/delete" response: httpx.Response response = self.client.get(detail_uri) @@ -1151,43 +1155,43 @@ class CashExpenseTransactionTestCase(unittest.TestCase): self.assertEqual(response.status_code, 404) def __get_add_form(self) -> dict[str, str]: - """Returns the form data to add a new transaction. + """Returns the form data to add a new voucher. - :return: The form data to add a new transaction. + :return: The form data to add a new voucher. """ form: dict[str, str] = get_add_form(self.csrf_token) form = {x: form[x] for x in form if "-credit-" not in x} return form - def __get_unchanged_update_form(self, txn_id: int) -> dict[str, str]: - """Returns the form data to update a transaction, where the data are + def __get_unchanged_update_form(self, voucher_id: int) -> dict[str, str]: + """Returns the form data to update a voucher, where the data are not changed. - :param txn_id: The transaction ID. - :return: The form data to update the transaction, where the data are + :param voucher_id: The voucher ID. + :return: The form data to update the voucher, where the data are not changed. """ form: dict[str, str] = get_unchanged_update_form( - txn_id, self.app, self.csrf_token) + voucher_id, self.app, self.csrf_token) form = {x: form[x] for x in form if "-credit-" not in x} return form - def __get_update_form(self, txn_id: int) -> dict[str, str]: - """Returns the form data to update a transaction, where the data are + def __get_update_form(self, voucher_id: int) -> dict[str, str]: + """Returns the form data to update a voucher, where the data are changed. - :param txn_id: The transaction ID. - :return: The form data to update the transaction, where the data are + :param voucher_id: The voucher ID. + :return: The form data to update the voucher, where the data are changed. """ form: dict[str, str] = get_update_form( - txn_id, self.app, self.csrf_token, True) + voucher_id, self.app, self.csrf_token, True) form = {x: form[x] for x in form if "-credit-" not in x} return form -class TransferTransactionTestCase(unittest.TestCase): - """The transfer transaction test case.""" +class TransferVoucherTestCase(unittest.TestCase): + """The transfer voucher test case.""" def setUp(self) -> None: """Sets up the test. @@ -1199,7 +1203,7 @@ class TransferTransactionTestCase(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") @@ -1213,7 +1217,7 @@ class TransferTransactionTestCase(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") @@ -1224,14 +1228,14 @@ class TransferTransactionTestCase(unittest.TestCase): :return: None. """ client, csrf_token = get_client(self.app, "nobody") - txn_id: int = add_txn(self.client, self.__get_add_form()) + voucher_id: int = add_voucher(self.client, self.__get_add_form()) add_form: dict[str, str] = self.__get_add_form() add_form["csrf_token"] = csrf_token - update_form: dict[str, str] = self.__get_update_form(txn_id) + update_form: dict[str, str] = self.__get_update_form(voucher_id) update_form["csrf_token"] = csrf_token response: httpx.Response - response = client.get(f"{PREFIX}/{txn_id}") + response = client.get(f"{PREFIX}/{voucher_id}") self.assertEqual(response.status_code, 403) response = client.get(f"{PREFIX}/create/transfer") @@ -1240,13 +1244,14 @@ class TransferTransactionTestCase(unittest.TestCase): response = client.post(f"{PREFIX}/store/transfer", data=add_form) self.assertEqual(response.status_code, 403) - response = client.get(f"{PREFIX}/{txn_id}/edit") + response = client.get(f"{PREFIX}/{voucher_id}/edit") self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{txn_id}/update", data=update_form) + response = client.post(f"{PREFIX}/{voucher_id}/update", + data=update_form) self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{txn_id}/delete", + response = client.post(f"{PREFIX}/{voucher_id}/delete", data={"csrf_token": csrf_token}) self.assertEqual(response.status_code, 403) @@ -1256,14 +1261,14 @@ class TransferTransactionTestCase(unittest.TestCase): :return: None. """ client, csrf_token = get_client(self.app, "viewer") - txn_id: int = add_txn(self.client, self.__get_add_form()) + voucher_id: int = add_voucher(self.client, self.__get_add_form()) add_form: dict[str, str] = self.__get_add_form() add_form["csrf_token"] = csrf_token - update_form: dict[str, str] = self.__get_update_form(txn_id) + update_form: dict[str, str] = self.__get_update_form(voucher_id) update_form["csrf_token"] = csrf_token response: httpx.Response - response = client.get(f"{PREFIX}/{txn_id}") + response = client.get(f"{PREFIX}/{voucher_id}") self.assertEqual(response.status_code, 200) response = client.get(f"{PREFIX}/create/transfer") @@ -1272,13 +1277,14 @@ class TransferTransactionTestCase(unittest.TestCase): response = client.post(f"{PREFIX}/store/transfer", data=add_form) self.assertEqual(response.status_code, 403) - response = client.get(f"{PREFIX}/{txn_id}/edit") + response = client.get(f"{PREFIX}/{voucher_id}/edit") self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{txn_id}/update", data=update_form) + response = client.post(f"{PREFIX}/{voucher_id}/update", + data=update_form) self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{txn_id}/delete", + response = client.post(f"{PREFIX}/{voucher_id}/delete", data={"csrf_token": csrf_token}) self.assertEqual(response.status_code, 403) @@ -1287,12 +1293,12 @@ class TransferTransactionTestCase(unittest.TestCase): :return: None. """ - txn_id: int = add_txn(self.client, self.__get_add_form()) + voucher_id: int = add_voucher(self.client, self.__get_add_form()) add_form: dict[str, str] = self.__get_add_form() - update_form: dict[str, str] = self.__get_update_form(txn_id) + update_form: dict[str, str] = self.__get_update_form(voucher_id) response: httpx.Response - response = self.client.get(f"{PREFIX}/{txn_id}") + response = self.client.get(f"{PREFIX}/{voucher_id}") self.assertEqual(response.status_code, 200) response = self.client.get(f"{PREFIX}/create/transfer") @@ -1301,33 +1307,33 @@ class TransferTransactionTestCase(unittest.TestCase): response = self.client.post(f"{PREFIX}/store/transfer", data=add_form) self.assertEqual(response.status_code, 302) - match_txn_detail(response.headers["Location"]) + match_voucher_detail(response.headers["Location"]) - response = self.client.get(f"{PREFIX}/{txn_id}/edit") + response = self.client.get(f"{PREFIX}/{voucher_id}/edit") self.assertEqual(response.status_code, 200) - response = self.client.post(f"{PREFIX}/{txn_id}/update", + response = self.client.post(f"{PREFIX}/{voucher_id}/update", data=update_form) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], - f"{PREFIX}/{txn_id}?next=%2F_next") + f"{PREFIX}/{voucher_id}?next=%2F_next") - response = self.client.post(f"{PREFIX}/{txn_id}/delete", + response = self.client.post(f"{PREFIX}/{voucher_id}/delete", data={"csrf_token": self.csrf_token}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], RETURN_TO_URI) def test_add(self) -> None: - """Tests to add the transactions. + """Tests to add the vouchers. :return: None. """ - from accounting.models import Transaction, TransactionCurrency + from accounting.models import Voucher, VoucherCurrency create_uri: str = f"{PREFIX}/create/transfer?next=%2F_next" store_uri: str = f"{PREFIX}/store/transfer" response: httpx.Response form: dict[str, str] - txn: Transaction | None + voucher: Voucher | None # No currency content form = self.__get_add_form() @@ -1429,12 +1435,12 @@ class TransferTransactionTestCase(unittest.TestCase): response = self.client.post(store_uri, data=self.__get_add_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) - self.assertIsNotNone(txn) - currencies: list[TransactionCurrency] = txn.currencies + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + currencies: list[VoucherCurrency] = voucher.currencies self.assertEqual(len(currencies), 3) self.assertEqual(currencies[0].code, "JPY") @@ -1494,36 +1500,36 @@ class TransferTransactionTestCase(unittest.TestCase): self.assertEqual(currencies[2].credit[1].account.code, Accounts.DONATION) - self.assertEqual(txn.note, NON_EMPTY_NOTE) + self.assertEqual(voucher.note, NON_EMPTY_NOTE) # Success, with empty note form = self.__get_add_form() form["note"] = EMPTY_NOTE 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) - self.assertIsNotNone(txn) - self.assertIsNone(txn.note) + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + self.assertIsNone(voucher.note) def test_basic_update(self) -> None: - """Tests the basic rules to update a transaction. + """Tests the basic rules to update a voucher. :return: None. """ - from accounting.models import Transaction, TransactionCurrency - txn_id: int = add_txn(self.client, self.__get_add_form()) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - edit_uri: str = f"{PREFIX}/{txn_id}/edit?next=%2F_next" - update_uri: str = f"{PREFIX}/{txn_id}/update" - form_0: dict[str, str] = self.__get_update_form(txn_id) + from accounting.models import Voucher, VoucherCurrency + voucher_id: int = add_voucher(self.client, self.__get_add_form()) + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + edit_uri: str = f"{PREFIX}/{voucher_id}/edit?next=%2F_next" + update_uri: str = f"{PREFIX}/{voucher_id}/update" + form_0: dict[str, str] = self.__get_update_form(voucher_id) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - currencies0: list[TransactionCurrency] = txn.currencies + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + currencies0: list[VoucherCurrency] = voucher.currencies old_id: set[int] = set() for currency in currencies0: old_id.update({x.id for x in currency.debit}) @@ -1630,9 +1636,9 @@ class TransferTransactionTestCase(unittest.TestCase): self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - currencies1: list[TransactionCurrency] = txn.currencies + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + currencies1: list[VoucherCurrency] = voucher.currencies self.assertEqual(len(currencies1), 3) self.assertEqual(currencies1[0].code, "AUD") @@ -1715,88 +1721,88 @@ class TransferTransactionTestCase(unittest.TestCase): self.assertEqual(currencies1[2].credit[2].account.code, Accounts.SERVICE) - self.assertEqual(txn.note, NON_EMPTY_NOTE) + self.assertEqual(voucher.note, NON_EMPTY_NOTE) def test_update_not_modified(self) -> None: """Tests that the data is not modified. :return: None. """ - from accounting.models import Transaction - txn_id: int = add_txn(self.client, self.__get_add_form()) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - update_uri: str = f"{PREFIX}/{txn_id}/update" - txn: Transaction + from accounting.models import Voucher + voucher_id: int = add_voucher(self.client, self.__get_add_form()) + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + update_uri: str = f"{PREFIX}/{voucher_id}/update" + voucher: Voucher response: httpx.Response response = self.client.post( - update_uri, data=self.__get_unchanged_update_form(txn_id)) + update_uri, data=self.__get_unchanged_update_form(voucher_id)) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - txn.created_at = txn.created_at - timedelta(seconds=5) - txn.updated_at = txn.created_at + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + voucher.created_at = voucher.created_at - timedelta(seconds=5) + voucher.updated_at = voucher.created_at db.session.commit() response = self.client.post( - update_uri, data=self.__get_update_form(txn_id)) + update_uri, data=self.__get_update_form(voucher_id)) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - self.assertLess(txn.created_at, txn.updated_at) + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + self.assertLess(voucher.created_at, voucher.updated_at) def test_created_updated_by(self) -> None: """Tests the created-by and updated-by record. :return: None. """ - from accounting.models import Transaction - txn_id: int = add_txn(self.client, self.__get_add_form()) + from accounting.models import Voucher + voucher_id: int = add_voucher(self.client, self.__get_add_form()) editor_username, editor2_username = "editor", "editor2" client, csrf_token = get_client(self.app, editor2_username) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - update_uri: str = f"{PREFIX}/{txn_id}/update" - txn: Transaction + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + update_uri: str = f"{PREFIX}/{voucher_id}/update" + voucher: Voucher response: httpx.Response with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertEqual(txn.created_by.username, editor_username) - self.assertEqual(txn.updated_by.username, editor_username) + voucher = db.session.get(Voucher, voucher_id) + self.assertEqual(voucher.created_by.username, editor_username) + self.assertEqual(voucher.updated_by.username, editor_username) - form: dict[str, str] = self.__get_update_form(txn_id) + form: dict[str, str] = self.__get_update_form(voucher_id) form["csrf_token"] = csrf_token response = client.post(update_uri, data=form) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertEqual(txn.created_by.username, editor_username) - self.assertEqual(txn.updated_by.username, editor2_username) + voucher = db.session.get(Voucher, voucher_id) + self.assertEqual(voucher.created_by.username, editor_username) + self.assertEqual(voucher.updated_by.username, editor2_username) - def test_save_as_income(self) -> None: - """Tests to save a transfer transaction as a cash income transaction. + def test_save_as_receipt(self) -> None: + """Tests to save a transfer voucher as a cash receipt voucher. :return: None. """ - from accounting.models import Transaction, TransactionCurrency - txn_id: int = add_txn(self.client, self.__get_add_form()) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - update_uri: str = f"{PREFIX}/{txn_id}/update?as=income" - form_0: dict[str, str] = self.__get_update_form(txn_id) + from accounting.models import Voucher, VoucherCurrency + voucher_id: int = add_voucher(self.client, self.__get_add_form()) + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + update_uri: str = f"{PREFIX}/{voucher_id}/update?as=receipt" + form_0: dict[str, str] = self.__get_update_form(voucher_id) form_0 = {x: form_0[x] for x in form_0 if "-debit-" not in x} with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - currencies0: list[TransactionCurrency] = txn.currencies + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + currencies0: list[VoucherCurrency] = voucher.currencies old_id: set[int] = set() for currency in currencies0: old_id.update({x.id for x in currency.debit}) @@ -1807,9 +1813,9 @@ class TransferTransactionTestCase(unittest.TestCase): self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - currencies1: list[TransactionCurrency] = txn.currencies + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + currencies1: list[VoucherCurrency] = voucher.currencies self.assertEqual(len(currencies1), 3) self.assertEqual(currencies1[0].code, "AUD") @@ -1878,24 +1884,24 @@ class TransferTransactionTestCase(unittest.TestCase): self.assertEqual(currencies1[2].credit[2].account.code, Accounts.SERVICE) - self.assertEqual(txn.note, NON_EMPTY_NOTE) + self.assertEqual(voucher.note, NON_EMPTY_NOTE) - def test_save_as_expense(self) -> None: - """Tests to save a transfer transaction as a cash expense transaction. + def test_save_as_disbursement(self) -> None: + """Tests to save a transfer voucher as a cash disbursement voucher. :return: None. """ - from accounting.models import Transaction, TransactionCurrency - txn_id: int = add_txn(self.client, self.__get_add_form()) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - update_uri: str = f"{PREFIX}/{txn_id}/update?as=expense" - form_0: dict[str, str] = self.__get_update_form(txn_id) + from accounting.models import Voucher, VoucherCurrency + voucher_id: int = add_voucher(self.client, self.__get_add_form()) + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + update_uri: str = f"{PREFIX}/{voucher_id}/update?as=disbursement" + form_0: dict[str, str] = self.__get_update_form(voucher_id) form_0 = {x: form_0[x] for x in form_0 if "-credit-" not in x} with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - currencies0: list[TransactionCurrency] = txn.currencies + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + currencies0: list[VoucherCurrency] = voucher.currencies old_id: set[int] = set() for currency in currencies0: old_id.update({x.id for x in currency.debit}) @@ -1906,9 +1912,9 @@ class TransferTransactionTestCase(unittest.TestCase): self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - txn = db.session.get(Transaction, txn_id) - self.assertIsNotNone(txn) - currencies1: list[TransactionCurrency] = txn.currencies + voucher = db.session.get(Voucher, voucher_id) + self.assertIsNotNone(voucher) + currencies1: list[VoucherCurrency] = voucher.currencies self.assertEqual(len(currencies1), 3) self.assertEqual(currencies1[0].code, "AUD") @@ -1981,16 +1987,16 @@ class TransferTransactionTestCase(unittest.TestCase): self.assertEqual(currencies1[2].credit[0].amount, sum([x.amount for x in currencies1[2].debit])) - self.assertEqual(txn.note, NON_EMPTY_NOTE) + self.assertEqual(voucher.note, NON_EMPTY_NOTE) def test_delete(self) -> None: - """Tests to delete a transaction. + """Tests to delete a voucher. :return: None. """ - txn_id: int = add_txn(self.client, self.__get_add_form()) - detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" - delete_uri: str = f"{PREFIX}/{txn_id}/delete" + voucher_id: int = add_voucher(self.client, self.__get_add_form()) + detail_uri: str = f"{PREFIX}/{voucher_id}?next=%2F_next" + delete_uri: str = f"{PREFIX}/{voucher_id}/delete" response: httpx.Response response = self.client.get(detail_uri) @@ -2009,35 +2015,35 @@ class TransferTransactionTestCase(unittest.TestCase): self.assertEqual(response.status_code, 404) def __get_add_form(self) -> dict[str, str]: - """Returns the form data to add a new transaction. + """Returns the form data to add a new voucher. - :return: The form data to add a new transaction. + :return: The form data to add a new voucher. """ return get_add_form(self.csrf_token) - def __get_unchanged_update_form(self, txn_id: int) -> dict[str, str]: - """Returns the form data to update a transaction, where the data are - not changed. - - :param txn_id: The transaction ID. - :return: The form data to update the transaction, where the data are - not changed. - """ - return get_unchanged_update_form(txn_id, self.app, self.csrf_token) - - def __get_update_form(self, txn_id: int) -> dict[str, str]: - """Returns the form data to update a transaction, where the data are + def __get_unchanged_update_form(self, voucher_id: int) -> dict[str, str]: + """Returns the form data to update a voucher, where the data are not changed. - :param txn_id: The transaction ID. - :return: The form data to update the transaction, where the data are + :param voucher_id: The voucher ID. + :return: The form data to update the voucher, where the data are not changed. """ - return get_update_form(txn_id, self.app, self.csrf_token, None) + return get_unchanged_update_form(voucher_id, self.app, self.csrf_token) + + def __get_update_form(self, voucher_id: int) -> dict[str, str]: + """Returns the form data to update a voucher, where the data are + changed. + + :param voucher_id: The voucher ID. + :return: The form data to update the voucher, where the data are + changed. + """ + return get_update_form(voucher_id, self.app, self.csrf_token, None) -class TransactionReorderTestCase(unittest.TestCase): - """The transaction reorder test case.""" +class VoucherReorderTestCase(unittest.TestCase): + """The voucher reorder test case.""" def setUp(self) -> None: """Sets up the test. @@ -2049,7 +2055,7 @@ class TransactionReorderTestCase(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") @@ -2063,152 +2069,159 @@ class TransactionReorderTestCase(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") def test_change_date(self) -> None: - """Tests to change the date of a transaction. + """Tests to change the date of a voucher. :return: None. """ - from accounting.models import Transaction + from accounting.models import Voucher response: httpx.Response - id_1: int = add_txn(self.client, self.__get_add_income_form()) - id_2: int = add_txn(self.client, self.__get_add_expense_form()) - id_3: int = add_txn(self.client, self.__get_add_transfer_form()) - id_4: int = add_txn(self.client, self.__get_add_income_form()) - id_5: int = add_txn(self.client, self.__get_add_expense_form()) + id_1: int = add_voucher(self.client, self.__get_add_receipt_form()) + id_2: int = add_voucher(self.client, + self.__get_add_disbursement_form()) + id_3: int = add_voucher(self.client, self.__get_add_transfer_form()) + id_4: int = add_voucher(self.client, self.__get_add_receipt_form()) + id_5: int = add_voucher(self.client, + self.__get_add_disbursement_form()) with self.app.app_context(): - txn_1: Transaction = db.session.get(Transaction, id_1) - txn_date_2: date = txn_1.date - txn_date_1: date = txn_date_2 - timedelta(days=1) - txn_1.date = txn_date_1 - txn_1.no = 3 - txn_2: Transaction = db.session.get(Transaction, id_2) - txn_2.date = txn_date_1 - txn_2.no = 5 - txn_3: Transaction = db.session.get(Transaction, id_3) - txn_3.date = txn_date_1 - txn_3.no = 8 - txn_4: Transaction = db.session.get(Transaction, id_4) - txn_4.no = 2 - txn_5: Transaction = db.session.get(Transaction, id_5) - txn_5.no = 6 + voucher_1: Voucher = db.session.get(Voucher, id_1) + voucher_date_2: date = voucher_1.date + voucher_date_1: date = voucher_date_2 - timedelta(days=1) + voucher_1.date = voucher_date_1 + voucher_1.no = 3 + voucher_2: Voucher = db.session.get(Voucher, id_2) + voucher_2.date = voucher_date_1 + voucher_2.no = 5 + voucher_3: Voucher = db.session.get(Voucher, id_3) + voucher_3.date = voucher_date_1 + voucher_3.no = 8 + voucher_4: Voucher = db.session.get(Voucher, id_4) + voucher_4.no = 2 + voucher_5: Voucher = db.session.get(Voucher, id_5) + voucher_5.no = 6 db.session.commit() - form: dict[str, str] = self.__get_expense_unchanged_update_form(id_2) - form["date"] = txn_date_2.isoformat() + form: dict[str, str] \ + = self.__get_disbursement_unchanged_update_form(id_2) + form["date"] = voucher_date_2.isoformat() response = self.client.post(f"{PREFIX}/{id_2}/update", data=form) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], f"{PREFIX}/{id_2}?next=%2F_next") with self.app.app_context(): - self.assertEqual(db.session.get(Transaction, id_1).no, 1) - self.assertEqual(db.session.get(Transaction, id_2).no, 3) - self.assertEqual(db.session.get(Transaction, id_3).no, 2) - self.assertEqual(db.session.get(Transaction, id_4).no, 1) - self.assertEqual(db.session.get(Transaction, id_5).no, 2) + self.assertEqual(db.session.get(Voucher, id_1).no, 1) + self.assertEqual(db.session.get(Voucher, id_2).no, 3) + self.assertEqual(db.session.get(Voucher, id_3).no, 2) + self.assertEqual(db.session.get(Voucher, id_4).no, 1) + self.assertEqual(db.session.get(Voucher, id_5).no, 2) def test_reorder(self) -> None: - """Tests to reorder the transactions in a same day. + """Tests to reorder the vouchers in a same day. :return: None. """ - from accounting.models import Transaction + from accounting.models import Voucher response: httpx.Response - id_1: int = add_txn(self.client, self.__get_add_income_form()) - id_2: int = add_txn(self.client, self.__get_add_expense_form()) - id_3: int = add_txn(self.client, self.__get_add_transfer_form()) - id_4: int = add_txn(self.client, self.__get_add_income_form()) - id_5: int = add_txn(self.client, self.__get_add_expense_form()) + id_1: int = add_voucher(self.client, self.__get_add_receipt_form()) + id_2: int = add_voucher(self.client, + self.__get_add_disbursement_form()) + id_3: int = add_voucher(self.client, self.__get_add_transfer_form()) + id_4: int = add_voucher(self.client, self.__get_add_receipt_form()) + id_5: int = add_voucher(self.client, + self.__get_add_disbursement_form()) with self.app.app_context(): - txn_date: date = db.session.get(Transaction, id_1).date + voucher_date: date = db.session.get(Voucher, id_1).date - response = self.client.post(f"{PREFIX}/dates/{txn_date.isoformat()}", - data={"csrf_token": self.csrf_token, - "next": "/next", - f"{id_1}-no": "4", - f"{id_2}-no": "1", - f"{id_3}-no": "5", - f"{id_4}-no": "2", - f"{id_5}-no": "3"}) + response = self.client.post( + f"{PREFIX}/dates/{voucher_date.isoformat()}", + data={"csrf_token": self.csrf_token, + "next": "/next", + f"{id_1}-no": "4", + f"{id_2}-no": "1", + f"{id_3}-no": "5", + f"{id_4}-no": "2", + f"{id_5}-no": "3"}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], f"/next") with self.app.app_context(): - self.assertEqual(db.session.get(Transaction, id_1).no, 4) - self.assertEqual(db.session.get(Transaction, id_2).no, 1) - self.assertEqual(db.session.get(Transaction, id_3).no, 5) - self.assertEqual(db.session.get(Transaction, id_4).no, 2) - self.assertEqual(db.session.get(Transaction, id_5).no, 3) + self.assertEqual(db.session.get(Voucher, id_1).no, 4) + self.assertEqual(db.session.get(Voucher, id_2).no, 1) + self.assertEqual(db.session.get(Voucher, id_3).no, 5) + self.assertEqual(db.session.get(Voucher, id_4).no, 2) + self.assertEqual(db.session.get(Voucher, id_5).no, 3) # Malformed orders with self.app.app_context(): - db.session.get(Transaction, id_1).no = 3 - db.session.get(Transaction, id_2).no = 4 - db.session.get(Transaction, id_3).no = 6 - db.session.get(Transaction, id_4).no = 8 - db.session.get(Transaction, id_5).no = 9 + db.session.get(Voucher, id_1).no = 3 + db.session.get(Voucher, id_2).no = 4 + db.session.get(Voucher, id_3).no = 6 + db.session.get(Voucher, id_4).no = 8 + db.session.get(Voucher, id_5).no = 9 db.session.commit() - response = self.client.post(f"{PREFIX}/dates/{txn_date.isoformat()}", - data={"csrf_token": self.csrf_token, - "next": "/next", - f"{id_2}-no": "3a", - f"{id_3}-no": "5", - f"{id_4}-no": "2"}) + response = self.client.post( + f"{PREFIX}/dates/{voucher_date.isoformat()}", + data={"csrf_token": self.csrf_token, + "next": "/next", + f"{id_2}-no": "3a", + f"{id_3}-no": "5", + f"{id_4}-no": "2"}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], f"/next") with self.app.app_context(): - self.assertEqual(db.session.get(Transaction, id_1).no, 3) - self.assertEqual(db.session.get(Transaction, id_2).no, 4) - self.assertEqual(db.session.get(Transaction, id_3).no, 2) - self.assertEqual(db.session.get(Transaction, id_4).no, 1) - self.assertEqual(db.session.get(Transaction, id_5).no, 5) + self.assertEqual(db.session.get(Voucher, id_1).no, 3) + self.assertEqual(db.session.get(Voucher, id_2).no, 4) + self.assertEqual(db.session.get(Voucher, id_3).no, 2) + self.assertEqual(db.session.get(Voucher, id_4).no, 1) + self.assertEqual(db.session.get(Voucher, id_5).no, 5) - def __get_add_income_form(self) -> dict[str, str]: - """Returns the form data to add a new transaction. + def __get_add_receipt_form(self) -> dict[str, str]: + """Returns the form data to add a new cash receipt voucher. - :return: The form data to add a new transaction. + :return: The form data to add a new cash receipt voucher. """ form: dict[str, str] = get_add_form(self.csrf_token) form = {x: form[x] for x in form if "-debit-" not in x} return form - def __get_add_expense_form(self) -> dict[str, str]: - """Returns the form data to add a new transaction. + def __get_add_disbursement_form(self) -> dict[str, str]: + """Returns the form data to add a new cash disbursement voucher. - :return: The form data to add a new transaction. + :return: The form data to add a new cash disbursement voucher. """ form: dict[str, str] = get_add_form(self.csrf_token) form = {x: form[x] for x in form if "-credit-" not in x} return form - def __get_expense_unchanged_update_form(self, txn_id: int) \ + def __get_disbursement_unchanged_update_form(self, voucher_id: int) \ -> dict[str, str]: - """Returns the form data to update a cash expense transaction, where + """Returns the form data to update a cash disbursement voucher, where the data are not changed. - :param txn_id: The transaction ID. - :return: The form data to update the cash expense transaction, where + :param voucher_id: The voucher ID. + :return: The form data to update the cash disbursement voucher, where the data are not changed. """ form: dict[str, str] = get_unchanged_update_form( - txn_id, self.app, self.csrf_token) + voucher_id, self.app, self.csrf_token) form = {x: form[x] for x in form if "-credit-" not in x} return form def __get_add_transfer_form(self) -> dict[str, str]: - """Returns the form data to add a new transaction. + """Returns the form data to add a new voucher. - :return: The form data to add a new transaction. + :return: The form data to add a new voucher. """ return get_add_form(self.csrf_token) diff --git a/tests/testlib_offset.py b/tests/testlib_offset.py index 08fbfc7..8ec6691 100644 --- a/tests/testlib_offset.py +++ b/tests/testlib_offset.py @@ -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 diff --git a/tests/testlib_txn.py b/tests/testlib_voucher.py similarity index 88% rename from tests/testlib_txn.py rename to tests/testlib_voucher.py index 8954db7..1ac1fd6 100644 --- a/tests/testlib_txn.py +++ b/tests/testlib_voucher.py @@ -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,36 +389,35 @@ 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", - location) + m: re.Match = re.match(r"^/accounting/vouchers/(\d+)\?next=%2F_next", + location) assert m is not None return int(m.group(1))