Added the TransactionTypeEnum in the new "accounting.utils.txn_types" module to remove the dependency from the "accounting.report" module to the "accounting.transaction" module.

This commit is contained in:
依瑪貓 2023-03-04 19:30:04 +08:00
parent 7d412b20d7
commit 9833bac6e4
8 changed files with 94 additions and 79 deletions

View File

@ -27,13 +27,14 @@ from flask_sqlalchemy.query import Query
from accounting import db from accounting import db
from accounting.models import JournalEntry, Transaction, Account, Currency from accounting.models import JournalEntry, Transaction, Account, Currency
from accounting.transaction.dispatcher import TXN_TYPE_OBJ, TransactionTypes
from accounting.utils.pagination import Pagination from accounting.utils.pagination import Pagination
from accounting.utils.txn_types import TransactionTypeEnum
from .period import Period from .period import Period
from .period_choosers import PeriodChooser, \ from .period_choosers import PeriodChooser, \
JournalPeriodChooser JournalPeriodChooser
from .report_chooser import ReportChooser, ReportType from .report_chooser import ReportChooser, ReportType
from .report_rows import ReportRow, JournalRow from .report_rows import ReportRow, JournalRow
import typing as t
class JournalEntryReport(ABC): class JournalEntryReport(ABC):
@ -104,12 +105,12 @@ class JournalEntryReport(ABC):
""" """
@property @property
def txn_types(self) -> TransactionTypes: def txn_types(self) -> t.Type[TransactionTypeEnum]:
"""Returns the transaction types. """Returns the transaction types.
:return: The transaction types. :return: The transaction types.
""" """
return TXN_TYPE_OBJ return TransactionTypeEnum
def as_csv_download(self) -> Response: def as_csv_download(self) -> Response:
"""Returns the journal entries as CSV download. """Returns the journal entries as CSV download.

View File

@ -22,13 +22,13 @@ First written: 2023/2/25
{% if accounting_can_edit() %} {% if accounting_can_edit() %}
<div id="accounting-material-fab-speed-dial" class="d-md-none accounting-material-fab"> <div id="accounting-material-fab-speed-dial" class="d-md-none accounting-material-fab">
<div id="accounting-material-fab-speed-dial-actions" class="d-md-none accounting-material-fab-speed-dial-group"> <div id="accounting-material-fab-speed-dial-actions" class="d-md-none accounting-material-fab-speed-dial-group">
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=types.expense)|accounting_append_next }}"> <a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash expense") }} {{ A_("Cash expense") }}
</a> </a>
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=types.income)|accounting_append_next }}"> <a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash income") }} {{ A_("Cash income") }}
</a> </a>
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=types.transfer)|accounting_append_next }}"> <a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }} {{ A_("Transfer") }}
</a> </a>
</div> </div>

View File

@ -40,16 +40,16 @@ First written: 2023/3/4
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.expense)|accounting_append_next }}"> <a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash Expense") }}</a> {{ A_("Cash Expense") }}</a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.income)|accounting_append_next }}"> <a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash Income") }} {{ A_("Cash Income") }}
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.transfer)|accounting_append_next }}"> <a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }} {{ A_("Transfer") }}
</a> </a>
</li> </li>
@ -69,7 +69,7 @@ First written: 2023/3/4
</a> </a>
</div> </div>
{% with types = report.txn_types %} {% with txn_types = report.txn_types %}
{% include "accounting/include/add-txn-material-fab.html" %} {% include "accounting/include/add-txn-material-fab.html" %}
{% endwith %} {% endwith %}

View File

@ -38,16 +38,16 @@ First written: 2023/2/18
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=types.expense)|accounting_append_next }}"> <a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash Expense") }}</a> {{ A_("Cash Expense") }}</a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=types.income)|accounting_append_next }}"> <a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash Income") }} {{ A_("Cash Income") }}
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=types.transfer)|accounting_append_next }}"> <a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }} {{ A_("Transfer") }}
</a> </a>
</li> </li>

View File

@ -24,8 +24,7 @@ from werkzeug.routing import BaseConverter
from accounting import db from accounting import db
from accounting.models import Transaction from accounting.models import Transaction
from accounting.transaction.dispatcher import TransactionType, \ from accounting.utils.txn_types import TransactionTypeEnum
TXN_TYPE_DICT
class TransactionConverter(BaseConverter): class TransactionConverter(BaseConverter):
@ -56,24 +55,26 @@ class TransactionTypeConverter(BaseConverter):
"""The transaction converter to convert the transaction type ID from and to """The transaction converter to convert the transaction type ID from and to
the corresponding transaction type in the routes.""" the corresponding transaction type in the routes."""
def to_python(self, value: str) -> TransactionType: def to_python(self, value: str) -> TransactionTypeEnum:
"""Converts a transaction ID to a transaction. """Converts a transaction ID to a transaction.
:param value: The transaction ID. :param value: The transaction ID.
:return: The corresponding transaction. :return: The corresponding transaction.
""" """
txn_type: TransactionType | None = TXN_TYPE_DICT.get(value) type_dict: dict[str, TransactionTypeEnum] \
= {x.value: x for x in TransactionTypeEnum}
txn_type: TransactionTypeEnum | None = type_dict.get(value)
if txn_type is None: if txn_type is None:
abort(404) abort(404)
return txn_type return txn_type
def to_url(self, value: TransactionType) -> str: def to_url(self, value: TransactionTypeEnum) -> str:
"""Converts a transaction type to its ID. """Converts a transaction type to its ID.
:param value: The transaction type. :param value: The transaction type.
:return: The ID. :return: The ID.
""" """
return str(value.ID) return str(value.value)
class DateConverter(BaseConverter): class DateConverter(BaseConverter):

View File

@ -24,15 +24,14 @@ from flask import render_template, request, abort
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from accounting.models import Transaction from accounting.models import Transaction
from accounting.template_globals import default_currency_code
from accounting.utils.txn_types import TransactionTypeEnum
from .forms import TransactionForm, IncomeTransactionForm, \ from .forms import TransactionForm, IncomeTransactionForm, \
ExpenseTransactionForm, TransferTransactionForm ExpenseTransactionForm, TransferTransactionForm
from accounting.template_globals import default_currency_code
class TransactionType(ABC): class TransactionType(ABC):
"""An abstract transaction type.""" """An abstract transaction type."""
ID: str = ""
"""The transaction type ID."""
CHECK_ORDER: int = -1 CHECK_ORDER: int = -1
"""The order when checking the transaction type.""" """The order when checking the transaction type."""
@ -93,8 +92,6 @@ class TransactionType(ABC):
class IncomeTransaction(TransactionType): class IncomeTransaction(TransactionType):
"""An income transaction.""" """An income transaction."""
ID: str = "income"
"""The transaction type ID."""
CHECK_ORDER: int = 2 CHECK_ORDER: int = 2
"""The order when checking the transaction type.""" """The order when checking the transaction type."""
@ -113,7 +110,8 @@ class IncomeTransaction(TransactionType):
:return: the form to create a transaction. :return: the form to create a transaction.
""" """
return render_template("accounting/transaction/income/create.html", return render_template("accounting/transaction/income/create.html",
form=form, txn_type=self, form=form,
txn_type=TransactionTypeEnum.CASH_INCOME,
currency_template=self.__currency_template, currency_template=self.__currency_template,
entry_template=self._entry_template) entry_template=self._entry_template)
@ -163,8 +161,6 @@ class IncomeTransaction(TransactionType):
class ExpenseTransaction(TransactionType): class ExpenseTransaction(TransactionType):
"""An expense transaction.""" """An expense transaction."""
ID: str = "expense"
"""The transaction type ID."""
CHECK_ORDER: int = 1 CHECK_ORDER: int = 1
"""The order when checking the transaction type.""" """The order when checking the transaction type."""
@ -183,7 +179,8 @@ class ExpenseTransaction(TransactionType):
:return: the form to create a transaction. :return: the form to create a transaction.
""" """
return render_template("accounting/transaction/expense/create.html", return render_template("accounting/transaction/expense/create.html",
form=form, txn_type=self, form=form,
txn_type=TransactionTypeEnum.CASH_EXPENSE,
currency_template=self.__currency_template, currency_template=self.__currency_template,
entry_template=self._entry_template) entry_template=self._entry_template)
@ -233,8 +230,6 @@ class ExpenseTransaction(TransactionType):
class TransferTransaction(TransactionType): class TransferTransaction(TransactionType):
"""A transfer transaction.""" """A transfer transaction."""
ID: str = "transfer"
"""The transaction type ID."""
CHECK_ORDER: int = 3 CHECK_ORDER: int = 3
"""The order when checking the transaction type.""" """The order when checking the transaction type."""
@ -253,7 +248,8 @@ class TransferTransaction(TransactionType):
:return: the form to create a transaction. :return: the form to create a transaction.
""" """
return render_template("accounting/transaction/transfer/create.html", return render_template("accounting/transaction/transfer/create.html",
form=form, txn_type=self, form=form,
txn_type=TransactionTypeEnum.TRANSFER,
currency_template=self.__currency_template, currency_template=self.__currency_template,
entry_template=self._entry_template) entry_template=self._entry_template)
@ -301,44 +297,28 @@ class TransferTransaction(TransactionType):
debit_total="-", credit_total="-") debit_total="-", credit_total="-")
class TransactionTypes: TXN_ENUM_TO_OP: dict[TransactionTypeEnum, TransactionType] \
"""The transaction types, as object properties.""" = {TransactionTypeEnum.CASH_INCOME: IncomeTransaction(),
TransactionTypeEnum.CASH_EXPENSE: ExpenseTransaction(),
def __init__(self, income: IncomeTransaction, expense: ExpenseTransaction, TransactionTypeEnum.TRANSFER: TransferTransaction()}
transfer: TransferTransaction): """The map from the transaction type enum to its operator."""
"""Constructs the transaction types as object properties.
:param income: The income transaction type.
:param expense: The expense transaction type.
:param transfer: The transfer transaction type.
"""
self.income: IncomeTransaction = income
self.expense: ExpenseTransaction = expense
self.transfer: TransferTransaction = transfer
TXN_TYPE_DICT: dict[str, TransactionType] \ def get_txn_type_op(txn: Transaction) -> TransactionType:
= {x.ID: x() for x in {IncomeTransaction, """Returns the transaction type operator that may be specified in the "as"
ExpenseTransaction, query parameter. If it is not specified, check the transaction type from
TransferTransaction}} the transaction.
"""The transaction types, as a dictionary."""
TXN_TYPE_OBJ: TransactionTypes = TransactionTypes(**TXN_TYPE_DICT)
"""The transaction types, as an object."""
def get_txn_type(txn: Transaction) -> TransactionType:
"""Returns the transaction type 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 txn: The transaction.
:return: None. :return: None.
""" """
if "as" in request.args: if "as" in request.args:
if request.args["as"] not in TXN_TYPE_DICT: type_dict: dict[str, TransactionTypeEnum] \
= {x.value: x for x in TransactionTypeEnum}
if request.args["as"] not in type_dict:
abort(404) abort(404)
return TXN_TYPE_DICT[request.args["as"]] return TXN_ENUM_TO_OP[type_dict[request.args["as"]]]
for txn_type in sorted(TXN_TYPE_DICT.values(), for txn_type in sorted(TXN_ENUM_TO_OP.values(),
key=lambda x: x.CHECK_ORDER): key=lambda x: x.CHECK_ORDER):
if txn_type.is_my_type(txn): if txn_type.is_my_type(txn):
return txn_type return txn_type

View File

@ -32,8 +32,9 @@ from accounting.utils.flash_errors import flash_form_errors
from accounting.utils.next_uri import inherit_next, or_next from accounting.utils.next_uri import inherit_next, or_next
from accounting.utils.pagination import Pagination from accounting.utils.pagination import Pagination
from accounting.utils.permission import has_permission, can_view, can_edit from accounting.utils.permission import has_permission, can_view, can_edit
from accounting.utils.txn_types import TransactionTypeEnum
from accounting.utils.user import get_current_user_pk from accounting.utils.user import get_current_user_pk
from .dispatcher import TransactionType, get_txn_type, TXN_TYPE_OBJ from .dispatcher import TransactionType, TXN_ENUM_TO_OP, get_txn_type_op
from .forms import sort_transactions_in, TransactionReorderForm from .forms import sort_transactions_in, TransactionReorderForm
from .queries import get_transaction_query from .queries import get_transaction_query
from .template_filters import with_type, to_transfer, format_amount_input, \ from .template_filters import with_type, to_transfer, format_amount_input, \
@ -59,37 +60,39 @@ def list_transactions() -> str:
pagination: Pagination = Pagination[Transaction](transactions) pagination: Pagination = Pagination[Transaction](transactions)
return render_template("accounting/transaction/list.html", return render_template("accounting/transaction/list.html",
list=pagination.list, pagination=pagination, list=pagination.list, pagination=pagination,
types=TXN_TYPE_OBJ) txn_types=TransactionTypeEnum)
@bp.get("/create/<transactionType:txn_type>", endpoint="create") @bp.get("/create/<transactionType:txn_type>", endpoint="create")
@has_permission(can_edit) @has_permission(can_edit)
def show_add_transaction_form(txn_type: TransactionType) -> str: def show_add_transaction_form(txn_type: TransactionTypeEnum) -> str:
"""Shows the form to add a transaction. """Shows the form to add a transaction.
:param txn_type: The transaction type. :param txn_type: The transaction type.
:return: The form to add a transaction. :return: The form to add a transaction.
""" """
form: txn_type.form txn_type_op: TransactionType = TXN_ENUM_TO_OP[txn_type]
form: txn_type_op.form
if "form" in session: if "form" in session:
form = txn_type.form(ImmutableMultiDict(parse_qsl(session["form"]))) form = txn_type_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
del session["form"] del session["form"]
form.validate() form.validate()
else: else:
form = txn_type.form() form = txn_type_op.form()
return txn_type.render_create_template(form) return txn_type_op.render_create_template(form)
@bp.post("/store/<transactionType:txn_type>", endpoint="store") @bp.post("/store/<transactionType:txn_type>", endpoint="store")
@has_permission(can_edit) @has_permission(can_edit)
def add_transaction(txn_type: TransactionType) -> redirect: def add_transaction(txn_type: TransactionTypeEnum) -> redirect:
"""Adds a transaction. """Adds a transaction.
:param txn_type: The transaction type. :param txn_type: The transaction type.
:return: The redirection to the transaction detail on success, or the :return: The redirection to the transaction detail on success, or the
transaction creation form on error. transaction creation form on error.
""" """
form: txn_type.form = txn_type.form(request.form) txn_type_op: TransactionType = TXN_ENUM_TO_OP[txn_type]
form: txn_type_op.form = txn_type_op.form(request.form)
if not form.validate(): if not form.validate():
flash_form_errors(form) flash_form_errors(form)
session["form"] = urlencode(list(request.form.items())) session["form"] = urlencode(list(request.form.items()))
@ -111,8 +114,8 @@ def show_transaction_detail(txn: Transaction) -> str:
:param txn: The transaction. :param txn: The transaction.
:return: The detail. :return: The detail.
""" """
txn_type: TransactionType = get_txn_type(txn) txn_type_op: TransactionType = get_txn_type_op(txn)
return txn_type.render_detail_template(txn) return txn_type_op.render_detail_template(txn)
@bp.get("/<transaction:txn>/edit", endpoint="edit") @bp.get("/<transaction:txn>/edit", endpoint="edit")
@ -123,15 +126,15 @@ def show_transaction_edit_form(txn: Transaction) -> str:
:param txn: The transaction. :param txn: The transaction.
:return: The form to edit the transaction. :return: The form to edit the transaction.
""" """
txn_type: TransactionType = get_txn_type(txn) txn_type_op: TransactionType = get_txn_type_op(txn)
form: txn_type.form form: txn_type_op.form
if "form" in session: if "form" in session:
form = txn_type.form(ImmutableMultiDict(parse_qsl(session["form"]))) form = txn_type_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
del session["form"] del session["form"]
form.validate() form.validate()
else: else:
form = txn_type.form(obj=txn) form = txn_type_op.form(obj=txn)
return txn_type.render_edit_template(txn, form) return txn_type_op.render_edit_template(txn, form)
@bp.post("/<transaction:txn>/update", endpoint="update") @bp.post("/<transaction:txn>/update", endpoint="update")
@ -143,8 +146,8 @@ def update_transaction(txn: Transaction) -> redirect:
:return: The redirection to the transaction detail on success, or the :return: The redirection to the transaction detail on success, or the
transaction edit form on error. transaction edit form on error.
""" """
txn_type: TransactionType = get_txn_type(txn) txn_type_op: TransactionType = get_txn_type_op(txn)
form: txn_type.form = txn_type.form(request.form) form: txn_type_op.form = txn_type_op.form(request.form)
if not form.validate(): if not form.validate():
flash_form_errors(form) flash_form_errors(form)
session["form"] = urlencode(list(request.form.items())) session["form"] = urlencode(list(request.form.items()))

View File

@ -0,0 +1,30 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# 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 transaction types.
"""
from enum import Enum
class TransactionTypeEnum(Enum):
"""The transaction types."""
CASH_INCOME: str = "income"
"""The cash income transaction."""
CASH_EXPENSE: str = "expense"
"""The cash expense transaction."""
TRANSFER: str = "transfer"
"""The transfer transaction."""