Fix various type hints

This commit is contained in:
2026-04-05 08:27:40 +08:00
parent 29dfc6c5a4
commit 674b0de3b2
36 changed files with 157 additions and 121 deletions
+4 -4
View File
@@ -33,16 +33,16 @@ def get_account_query() -> list[Account]:
keywords: list[str] = parse_query_keywords(request.args.get("q"))
if len(keywords) == 0:
return Account.query.order_by(Account.base_code, Account.no).all()
code: sa.BinaryExpression = Account.base_code + "-" \
code: sa.ColumnElement[str] = Account.base_code + "-" \
+ sa.func.substr("000" + sa.cast(Account.no, sa.String),
sa.func.char_length(sa.cast(Account.no,
sa.String)) + 1)
conditions: list[sa.BinaryExpression] = []
conditions: list[sa.ColumnElement[bool]] = []
for k in keywords:
l10n: list[AccountL10n] = AccountL10n.query\
.filter(AccountL10n.title.icontains(k)).all()
l10n_matches: set[str] = {x.account_id for x in l10n}
sub_conditions: list[sa.BinaryExpression] \
l10n_matches: set[int] = {x.account_id for x in l10n}
sub_conditions: list[sa.ColumnElement[bool]] \
= [Account.base_code.contains(k),
Account.title_l10n.icontains(k),
code.contains(k),
+8 -8
View File
@@ -1,7 +1,7 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
# Copyright (c) 2023 imacat.
# Copyright (c) 2023-2026 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -21,7 +21,7 @@ from urllib.parse import parse_qsl, urlencode
import sqlalchemy as sa
from flask import Blueprint, render_template, session, redirect, flash, \
url_for, request
url_for, request, Response
from werkzeug.datastructures import ImmutableMultiDict
from accounting import db
@@ -47,8 +47,8 @@ def list_accounts() -> str:
:return: The account list.
"""
accounts: list[BaseAccount] = get_account_query()
pagination: Pagination = Pagination[BaseAccount](accounts)
accounts: list[Account] = get_account_query()
pagination: Pagination = Pagination[Account](accounts)
return render_template("accounting/account/list.html",
list=pagination.list, pagination=pagination)
@@ -72,7 +72,7 @@ def show_add_account_form() -> str:
@bp.post("store", endpoint="store")
@has_permission(can_edit)
def add_account() -> redirect:
def add_account() -> Response:
"""Adds an account.
:return: The redirection to the account detail on success, or the account
@@ -123,7 +123,7 @@ def show_account_edit_form(account: Account) -> str:
@bp.post("<account:account>/update", endpoint="update")
@has_permission(can_edit)
def update_account(account: Account) -> redirect:
def update_account(account: Account) -> Response:
"""Updates an account.
:param account: The account.
@@ -150,7 +150,7 @@ def update_account(account: Account) -> redirect:
@bp.post("<account:account>/delete", endpoint="delete")
@has_permission(can_edit)
def delete_account(account: Account) -> redirect:
def delete_account(account: Account) -> Response:
"""Deletes an account.
:param account: The account.
@@ -180,7 +180,7 @@ def show_account_order(base: BaseAccount) -> str:
@bp.post("bases/<baseAccount:base>", endpoint="sort")
@has_permission(can_edit)
def sort_accounts(base: BaseAccount) -> redirect:
def sort_accounts(base: BaseAccount) -> Response:
"""Reorders the accounts under a base account.
:param base: The base account.
+1 -1
View File
@@ -32,7 +32,7 @@ def get_base_account_query() -> list[BaseAccount]:
keywords: list[str] = parse_query_keywords(request.args.get("q"))
if len(keywords) == 0:
return BaseAccount.query.order_by(BaseAccount.code).all()
conditions: list[sa.BinaryExpression] = []
conditions: list[sa.ColumnElement[bool]] = []
for k in keywords:
l10n: list[BaseAccountL10n] = BaseAccountL10n.query\
.filter(BaseAccountL10n.title.icontains(k)).all()
+1 -1
View File
@@ -32,7 +32,7 @@ def get_currency_query() -> list[Currency]:
keywords: list[str] = parse_query_keywords(request.args.get("q"))
if len(keywords) == 0:
return Currency.query.order_by(Currency.code).all()
conditions: list[sa.BinaryExpression] = []
conditions: list[sa.ColumnElement[bool]] = []
for k in keywords:
l10n: list[CurrencyL10n] = CurrencyL10n.query\
.filter(CurrencyL10n.name.icontains(k)).all()
+5 -5
View File
@@ -1,7 +1,7 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
# Copyright (c) 2023 imacat.
# Copyright (c) 2023-2026 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -21,7 +21,7 @@ from urllib.parse import urlencode, parse_qsl
import sqlalchemy as sa
from flask import Blueprint, render_template, redirect, session, request, \
flash, url_for
flash, url_for, Response
from werkzeug.datastructures import ImmutableMultiDict
from accounting import db
@@ -74,7 +74,7 @@ def show_add_currency_form() -> str:
@bp.post("store", endpoint="store")
@has_permission(can_edit)
def add_currency() -> redirect:
def add_currency() -> Response:
"""Adds a currency.
:return: The redirection to the currency detail on success, or the currency
@@ -125,7 +125,7 @@ def show_currency_edit_form(currency: Currency) -> str:
@bp.post("<currency:currency>/update", endpoint="update")
@has_permission(can_edit)
def update_currency(currency: Currency) -> redirect:
def update_currency(currency: Currency) -> Response:
"""Updates a currency.
:param currency: The currency.
@@ -153,7 +153,7 @@ def update_currency(currency: Currency) -> redirect:
@bp.post("<currency:currency>/delete", endpoint="delete")
@has_permission(can_edit)
def delete_currency(currency: Currency) -> redirect:
def delete_currency(currency: Currency) -> Response:
"""Deletes a currency.
:param currency: The currency.
@@ -34,7 +34,7 @@ def sort_journal_entries_in(date: dt.date, exclude: int | None = None) -> None:
:param exclude: The journal entry ID to exclude.
:return: None.
"""
conditions: list[sa.BinaryExpression] = [JournalEntry.date == date]
conditions: list[sa.ColumnElement[bool]] = [JournalEntry.date == date]
if exclude is not None:
conditions.append(JournalEntry.id != exclude)
journal_entries: list[JournalEntry] = JournalEntry.query\
@@ -28,7 +28,7 @@ class AccountOption:
:param account: The account.
"""
self.id: str = account.id
self.id: int = account.id
"""The account ID."""
self.code: str = account.code
"""The account code."""
@@ -315,14 +315,14 @@ class DescriptionEditor:
if len(codes) == 0:
return {}
def get_condition(code0: str) -> sa.BinaryExpression:
m: re.Match = re.match(r"^(\d{4})-(\d{3})$", code0)
def get_condition(code0: str) -> sa.ColumnElement[bool]:
m: re.Match[str] | None = re.match(r"^(\d{4})-(\d{3})$", code0)
assert m is not None, \
f"Malformed account code \"{code0}\" for regular transactions."
return sa.and_(Account.base_code == m.group(1),
Account.no == int(m.group(2)))
conditions: list[sa.BinaryExpression] \
conditions: list[sa.ColumnElement[bool]] \
= [get_condition(x) for x in codes]
accounts: dict[str, Account] \
= {x.code: x for x in
@@ -334,3 +334,4 @@ def get_journal_entry_op(journal_entry: JournalEntry,
key=lambda x: x.CHECK_ORDER):
if journal_entry_type.is_my_type(journal_entry):
return journal_entry_type
assert False
@@ -46,8 +46,8 @@ def get_selectable_original_line_items(
(offset.c.id.in_(line_item_id_on_form), 0),
(offset.c.is_debit == JournalEntryLineItem.is_debit, offset.c.amount),
else_=-offset.c.amount))).label("net_balance")
conditions: list[sa.BinaryExpression] = [Account.is_need_offset]
sub_conditions: list[sa.BinaryExpression] = []
conditions: list[sa.ColumnElement[bool]] = [Account.is_need_offset]
sub_conditions: list[sa.ColumnElement[bool]] = []
if is_payable:
sub_conditions.append(sa.and_(Account.base_code.startswith("2"),
sa.not_(JournalEntryLineItem.is_debit)))
+6 -6
View File
@@ -1,7 +1,7 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
# Copyright (c) 2023-2024 imacat.
# Copyright (c) 2023-2026 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -22,7 +22,7 @@ from urllib.parse import parse_qsl, urlencode
import sqlalchemy as sa
from flask import Blueprint, render_template, session, redirect, request, \
flash, url_for
flash, url_for, Response
from werkzeug.datastructures import ImmutableMultiDict
from accounting import db
@@ -74,7 +74,7 @@ def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
@bp.post("store/<journalEntryType:journal_entry_type>", endpoint="store")
@has_permission(can_edit)
def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
def add_journal_entry(journal_entry_type: JournalEntryType) -> Response:
"""Adds a journal entry.
:param journal_entry_type: The journal entry type.
@@ -136,7 +136,7 @@ def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
@bp.post("<journalEntry:journal_entry>/update", endpoint="update")
@has_permission(can_edit)
def update_journal_entry(journal_entry: JournalEntry) -> redirect:
def update_journal_entry(journal_entry: JournalEntry) -> Response:
"""Updates a journal entry.
:param journal_entry: The journal entry.
@@ -169,7 +169,7 @@ def update_journal_entry(journal_entry: JournalEntry) -> redirect:
@bp.post("<journalEntry:journal_entry>/delete", endpoint="delete")
@has_permission(can_edit)
def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
def delete_journal_entry(journal_entry: JournalEntry) -> Response:
"""Deletes a journal entry.
:param journal_entry: The journal entry.
@@ -204,7 +204,7 @@ def show_journal_entry_order(date: dt.date) -> str:
@bp.post("dates/<date:date>", endpoint="sort")
@has_permission(can_edit)
def sort_journal_entries(date: dt.date) -> redirect:
def sort_journal_entries(date: dt.date) -> Response:
"""Reorders the journal entries in a date.
:param date: The date.
+7 -3
View File
@@ -279,7 +279,7 @@ class Account(db.Model):
:param code: The code.
:return: The account, or None if this account does not exist.
"""
m = re.match(r"^([1-9]{4})-(\d{3})$", code)
m: re.Match[str] | None = re.match(r"^([1-9]{4})-(\d{3})$", code)
if m is None:
return None
return cls.query.filter(cls.base_code == m.group(1),
@@ -334,7 +334,9 @@ class Account(db.Model):
:return: The cash account
"""
return cls.find_by_code(cls.CASH_CODE)
account: Self | None = cls.find_by_code(cls.CASH_CODE)
assert account is not None
return account
@classmethod
def accumulated_change(cls) -> Self:
@@ -342,7 +344,9 @@ class Account(db.Model):
:return: The accumulated-change account
"""
return cls.find_by_code(cls.ACCUMULATED_CHANGE_CODE)
account: Self | None = cls.find_by_code(cls.ACCUMULATED_CHANGE_CODE)
assert account is not None
return account
class AccountL10n(db.Model):
+3 -3
View File
@@ -1,7 +1,7 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
# Copyright (c) 2023 imacat.
# Copyright (c) 2023-2026 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
from urllib.parse import parse_qsl, urlencode
from flask import Blueprint, render_template, redirect, session, request, \
flash, url_for
flash, url_for, Response
from werkzeug.datastructures import ImmutableMultiDict
from accounting.locale import lazy_gettext
@@ -64,7 +64,7 @@ def show_option_form() -> str:
@bp.post("update", endpoint="update")
@has_permission(can_admin)
def update_options() -> redirect:
def update_options() -> Response:
"""Updates the options.
:return: The redirection to the option form.
+1
View File
@@ -68,6 +68,7 @@ def __parse_spec(text: str) -> tuple[dt.date | None, dt.date | None]:
"""
if text == "-":
return None, None
m: re.Match[str] | None
m = re.match(f"^{DATE_SPEC_RE}$", text)
if m is not None:
return __get_start(m[1], m[2], m[3]), \
+11 -5
View File
@@ -27,12 +27,18 @@ def get_spec(start: dt.date | None, end: dt.date | None) -> str:
:param end: The end of the period.
:return: The period specification.
"""
if start is None and end is None:
return "-"
if end is None:
return __get_since_spec(start)
if start is None:
return __get_until_spec(end)
return "-" if end is None else __get_until_spec(end)
return __get_since_spec(start) if end is None else __get_spec(start, end)
def __get_spec(start: dt.date, end: dt.date) -> str:
"""Returns the period specification with both start and end.
:param start: The start of the period.
:param end: The end of the period.
:return: The period specification.
"""
try:
return __get_year_spec(start, end)
except ValueError:
@@ -121,9 +121,9 @@ class AccountCollector:
:return: The balances.
"""
sub_conditions: list[sa.BinaryExpression] \
sub_conditions: list[sa.ColumnElement[bool]] \
= [Account.base_code.startswith(x) for x in {"1", "2", "3"}]
conditions: list[sa.BinaryExpression] \
conditions: list[sa.ColumnElement[bool]] \
= [JournalEntryLineItem.currency_code == self.__currency.code,
sa.or_(*sub_conditions)]
if self.__period.end is not None:
@@ -180,7 +180,7 @@ class AccountCollector:
"""
if self.__period.start is None:
return None
conditions: list[sa.BinaryExpression] \
conditions: list[sa.ColumnElement[bool]] \
= [JournalEntryLineItem.currency_code == self.__currency.code,
JournalEntry.date < self.__period.start]
return self.__query_balance(conditions)
@@ -199,7 +199,7 @@ class AccountCollector:
:return: The net income or loss for current period.
"""
conditions: list[sa.BinaryExpression] \
conditions: list[sa.ColumnElement[bool]] \
= [JournalEntryLineItem.currency_code == self.__currency.code]
if self.__period.start is not None:
conditions.append(JournalEntry.date >= self.__period.start)
@@ -208,7 +208,7 @@ class AccountCollector:
return self.__query_balance(conditions)
@staticmethod
def __query_balance(conditions: list[sa.BinaryExpression])\
def __query_balance(conditions: list[sa.ColumnElement[bool]])\
-> Decimal:
"""Queries the balance.
@@ -119,12 +119,12 @@ class LineItemCollector:
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount))
select: sa.Select = sa.Select(balance_func)\
select: sa.Select[tuple[Decimal]] = sa.Select(balance_func)\
.join(JournalEntry).join(Account)\
.filter(JournalEntryLineItem.currency_code == self.__currency.code,
self.__account_condition,
JournalEntry.date < self.__period.start)
balance: int | None = db.session.scalar(select)
balance: Decimal | None = db.session.scalar(select)
if balance is None:
return None
line_item: ReportLineItem = ReportLineItem()
@@ -144,7 +144,7 @@ class LineItemCollector:
:return: The line items.
"""
conditions: list[sa.BinaryExpression] \
conditions: list[sa.ColumnElement[bool]] \
= [JournalEntryLineItem.currency_code == self.__currency.code,
self.__account_condition]
if self.__period.start is not None:
@@ -170,7 +170,7 @@ class LineItemCollector:
selectinload(JournalEntryLineItem.journal_entry))]
@property
def __account_condition(self) -> sa.BinaryExpression:
def __account_condition(self) -> sa.ColumnElement[bool]:
if self.__account.code == CurrentAccount.CURRENT_AL_CODE:
return CurrentAccount.sql_condition()
return Account.id == self.__account.id
@@ -254,9 +254,9 @@ class IncomeStatement(BaseReport):
:return: The balances.
"""
sub_conditions: list[sa.BinaryExpression] \
sub_conditions: list[sa.ColumnElement[bool]] \
= [Account.base_code.startswith(str(x)) for x in range(4, 10)]
conditions: list[sa.BinaryExpression] \
conditions: list[sa.ColumnElement[bool]] \
= [JournalEntryLineItem.currency_code == self.__currency.code,
sa.or_(*sub_conditions)]
if self.__period.start is not None:
+1 -1
View File
@@ -185,7 +185,7 @@ class Journal(BaseReport):
:return: The line items.
"""
conditions: list[sa.BinaryExpression] = []
conditions: list[sa.ColumnElement[bool]] = []
if self.__period.start is not None:
conditions.append(JournalEntry.date >= self.__period.start)
if self.__period.end is not None:
+1 -1
View File
@@ -139,7 +139,7 @@ class LineItemCollector:
:return: The line items.
"""
conditions: list[sa.BinaryExpression] \
conditions: list[sa.ColumnElement[bool]] \
= [JournalEntryLineItem.currency_code == self.__currency.code,
JournalEntryLineItem.account_id == self.__account.id]
if self.__period.start is not None:
+5 -5
View File
@@ -53,9 +53,9 @@ class LineItemCollector:
keywords: list[str] = parse_query_keywords(request.args.get("q"))
if len(keywords) == 0:
return []
conditions: list[sa.BinaryExpression] = []
conditions: list[sa.ColumnElement[bool]] = []
for k in keywords:
sub_conditions: list[sa.BinaryExpression] \
sub_conditions: list[sa.ColumnElement[bool]] \
= [JournalEntryLineItem.description.icontains(k),
JournalEntryLineItem.account_id.in_(
self.__get_account_condition(k)),
@@ -86,13 +86,13 @@ class LineItemCollector:
:param k: The keyword.
:return: The condition to filter the account.
"""
code: sa.BinaryExpression = Account.base_code + "-" \
code: sa.ColumnElement[str] = Account.base_code + "-" \
+ sa.func.substr("000" + sa.cast(Account.no, sa.String),
sa.func.char_length(sa.cast(Account.no,
sa.String)) + 1)
select_l10n: sa.Select = sa.select(AccountL10n.account_id)\
.filter(AccountL10n.title.icontains(k))
conditions: list[sa.BinaryExpression] \
conditions: list[sa.ColumnElement[bool]] \
= [Account.base_code.contains(k),
Account.title_l10n.icontains(k),
code.contains(k),
@@ -122,7 +122,7 @@ class LineItemCollector:
:param k: The keyword.
:return: The condition to filter the journal entry.
"""
conditions: list[sa.BinaryExpression] \
conditions: list[sa.ColumnElement[bool]] \
= [JournalEntry.note.icontains(k)]
date: dt.datetime
try:
@@ -178,7 +178,7 @@ class TrialBalance(BaseReport):
:return: None.
"""
conditions: list[sa.BinaryExpression] \
conditions: list[sa.ColumnElement[bool]] \
= [JournalEntryLineItem.currency_code == self.__currency.code]
if self.__period.start is not None:
conditions.append(JournalEntry.date >= self.__period.start)
+13 -9
View File
@@ -66,15 +66,19 @@ def period_spec(period: Period) -> str:
"""
start: str | None = __get_start_str(period.start)
end: str | None = __get_end_str(period.end)
if period.start is None and period.end is None:
return "all-time"
if start == end:
return start
if period.start is None:
return f"until-{end}"
if period.end is None:
return f"since-{start}"
return f"{start}-{end}"
if start is None:
return "all-time" if end is None else f"until-{end}"
return f"since-{start}" if end is None else __get_spec(start, end)
def __get_spec(start: str, end: str) -> str:
"""Constructs the period specification with both start and end
:param start: The start date.
:param end: The end date.
:return: The period specification.
"""
return start if start == end else f"{start}-{end}"
def __get_start_str(start: dt.date | None) -> str | None:
@@ -54,7 +54,7 @@ class OffsetMatcher:
:param currency: The currency.
:param account: The account.
"""
self.__currency: Account = currency
self.__currency: Currency = currency
"""The currency."""
self.__account: Account = account
"""The account."""
@@ -105,7 +105,7 @@ class OffsetMatcher:
"""
net_balances: dict[int, Decimal | None] \
= get_net_balances(self.__currency, self.__account)
unmatched_offset_condition: sa.BinaryExpression \
unmatched_offset_condition: sa.ColumnElement[bool] \
= sa.and_(Account.id == self.__account.id,
JournalEntryLineItem.currency_code
== self.__currency.code,
+9 -9
View File
@@ -22,21 +22,21 @@ from enum import Enum
class ReportType(Enum):
"""The report types."""
JOURNAL: str = "journal"
JOURNAL = "journal"
"""The journal."""
LEDGER: str = "ledger"
LEDGER = "ledger"
"""The ledger."""
INCOME_EXPENSES: str = "income-expenses"
INCOME_EXPENSES = "income-expenses"
"""The income and expenses log."""
TRIAL_BALANCE: str = "trial-balance"
TRIAL_BALANCE = "trial-balance"
"""The trial balance."""
INCOME_STATEMENT: str = "income-statement"
INCOME_STATEMENT = "income-statement"
"""The income statement."""
BALANCE_SHEET: str = "balance-sheet"
BALANCE_SHEET = "balance-sheet"
"""The balance sheet."""
UNAPPLIED: str = "unapplied"
UNAPPLIED = "unapplied"
"""The unapplied original line items."""
UNMATCHED: str = "unmatched"
UNMATCHED = "unmatched"
"""The unmatched offsets."""
SEARCH: str = "search"
SEARCH = "search"
"""The search."""
+2 -2
View File
@@ -1,7 +1,7 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
# Copyright (c) 2023 imacat.
# Copyright (c) 2023-2026 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -391,7 +391,7 @@ def get_unmatched(currency: Currency, account: Account) -> str | Response:
@bp.post("match-offsets/<currency:currency>/<needOffsetAccount:account>",
endpoint="match-offsets")
@has_permission(can_edit)
def match_offsets(currency: Currency, account: Account) -> redirect:
def match_offsets(currency: Currency, account: Account) -> Response:
"""Matches the original line items with their offsets.
:return: Redirection to the view of the unmatched offsets.
+3 -3
View File
@@ -59,7 +59,7 @@ class CurrentAccount:
:return: The pseudo account for all current assets and liabilities.
"""
account: cls = cls()
account: Self = cls()
account.id = 0
account.code = cls.CURRENT_AL_CODE
account.title = gettext("current assets and liabilities")
@@ -73,14 +73,14 @@ class CurrentAccount:
:return: The current assets and liabilities accounts.
"""
accounts: list[cls] = [cls.current_assets_and_liabilities()]
accounts.extend([CurrentAccount(x)
accounts.extend([cls(x)
for x in Account.query
.filter(cls.sql_condition())
.order_by(Account.base_code, Account.no)])
return accounts
@classmethod
def sql_condition(cls) -> sa.BinaryExpression:
def sql_condition(cls) -> sa.ColumnElement[bool]:
"""Returns the SQL condition for the current assets and liabilities
accounts.
+3 -3
View File
@@ -22,9 +22,9 @@ from enum import Enum
class JournalEntryType(Enum):
"""The journal entry types."""
CASH_RECEIPT: str = "receipt"
CASH_RECEIPT = "receipt"
"""The cash receipt journal entry."""
CASH_DISBURSEMENT: str = "disbursement"
CASH_DISBURSEMENT = "disbursement"
"""The cash disbursement journal entry."""
TRANSFER: str = "transfer"
TRANSFER = "transfer"
"""The transfer journal entry."""
+4 -1
View File
@@ -21,6 +21,8 @@ This module should not import any other module from the application.
"""
import re
from typing_extensions import assert_type
def parse_query_keywords(q: str | None) -> list[str]:
"""Returns the query keywords by the query parameter.
@@ -35,7 +37,8 @@ def parse_query_keywords(q: str | None) -> list[str]:
return []
keywords: list[str] = []
while True:
m: re.Match
m: re.Match[str] | None
assert q is not None
m = re.match(r"\"([^\"]+)\"\s+(.+)$", q)
if m is not None:
keywords.append(m.group(1))
+1 -1
View File
@@ -48,7 +48,7 @@ def title_case(s: str) -> str:
return re.sub(r"\w+", __cap_word, s)
def __cap_word(m: re.Match) -> str:
def __cap_word(m: re.Match[str]) -> str:
"""Capitalize a matched title word.
:param m: The matched title word.
+23 -11
View File
@@ -352,7 +352,9 @@ class AccountTestCase(unittest.TestCase):
# Success under the same base, with order in a mess.
with self.__app.app_context():
stock_2: Account = Account.find_by_code(f"{STOCK.base_code}-002")
stock_2: Account | None = \
Account.find_by_code(f"{STOCK.base_code}-002")
self.assertIsNotNone(stock_2)
stock_2.no = 66
db.session.commit()
@@ -370,7 +372,8 @@ class AccountTestCase(unittest.TestCase):
f"{STOCK.base_code}-002",
f"{STOCK.base_code}-003"})
account: Account = Account.find_by_code(STOCK.code)
account: Account | None = Account.find_by_code(STOCK.code)
self.assertIsNotNone(account)
self.assertEqual(account.base_code, STOCK.base_code)
self.assertEqual(account.title_l10n, STOCK.title)
@@ -395,7 +398,8 @@ class AccountTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], detail_uri)
with self.__app.app_context():
account: Account = Account.find_by_code(CASH.code)
account: Account | None = Account.find_by_code(CASH.code)
self.assertIsNotNone(account)
self.assertEqual(account.base_code, CASH.base_code)
self.assertEqual(account.title_l10n, f"{CASH.title}-1")
@@ -462,7 +466,7 @@ class AccountTestCase(unittest.TestCase):
from accounting.models import Account
detail_uri: str = f"{PREFIX}/{CASH.code}"
update_uri: str = f"{PREFIX}/{CASH.code}/update"
account: Account
account: Account | None
response: httpx.Response
response = self.__client.post(update_uri,
@@ -504,11 +508,12 @@ class AccountTestCase(unittest.TestCase):
csrf_token: str = get_csrf_token(client)
detail_uri: str = f"{PREFIX}/{CASH.code}"
update_uri: str = f"{PREFIX}/{CASH.code}/update"
account: Account
account: Account | None
response: httpx.Response
with self.__app.app_context():
account = Account.find_by_code(CASH.code)
self.assertIsNotNone(account)
self.assertEqual(account.created_by.username, editor_username)
self.assertEqual(account.updated_by.username, editor_username)
@@ -534,11 +539,12 @@ class AccountTestCase(unittest.TestCase):
from accounting.models import Account
detail_uri: str = f"{PREFIX}/{CASH.code}"
update_uri: str = f"{PREFIX}/{CASH.code}/update"
account: Account
account: Account | None
response: httpx.Response
with self.__app.app_context():
account = Account.find_by_code(CASH.code)
self.assertIsNotNone(account)
self.assertEqual(account.title_l10n, CASH.title)
self.assertEqual(account.l10n, [])
@@ -553,6 +559,7 @@ class AccountTestCase(unittest.TestCase):
with self.__app.app_context():
account = Account.find_by_code(CASH.code)
self.assertIsNotNone(account)
self.assertEqual(account.title_l10n, CASH.title)
self.assertEqual({(x.locale, x.title) for x in account.l10n},
{("zh_Hant", f"{CASH.title}-zh_Hant")})
@@ -665,15 +672,20 @@ class AccountTestCase(unittest.TestCase):
f"{PREFIX}/1111-00{i}")
with self.__app.app_context():
account_1: Account = Account.find_by_code("1111-001")
account_1: Account | None = Account.find_by_code("1111-001")
self.assertIsNotNone(account_1)
id_1: int = account_1.id
account_2: Account = Account.find_by_code("1111-002")
account_2: Account | None = Account.find_by_code("1111-002")
self.assertIsNotNone(account_2)
id_2: int = account_2.id
account_3: Account = Account.find_by_code("1111-003")
account_3: Account | None = Account.find_by_code("1111-003")
self.assertIsNotNone(account_3)
id_3: int = account_3.id
account_4: Account = Account.find_by_code("1111-004")
account_4: Account | None = Account.find_by_code("1111-004")
self.assertIsNotNone(account_4)
id_4: int = account_4.id
account_5: Account = Account.find_by_code("1111-005")
account_5: Account | None = Account.find_by_code("1111-005")
self.assertIsNotNone(account_5)
id_5: int = account_5.id
account_1.no = 3
account_2.no = 5
+1 -1
View File
@@ -230,7 +230,7 @@ class ConsoleCommandTestCase(unittest.TestCase):
new_account: Account = Account(
id=new_id(Account),
base_code="1112",
no="2",
no=2,
title_l10n=custom_title,
is_need_offset=False,
created_by_id=creator_pk,
+3 -3
View File
@@ -1,7 +1,7 @@
# The Mia! Accounting Demonstration Website.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/27
# Copyright (c) 2023 imacat.
# Copyright (c) 2023-2026 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -58,7 +58,7 @@ def show_login_form() -> str | Response:
@bp.post("login", endpoint="login")
def login() -> redirect:
def login() -> Response:
"""Logs in the user.
:return: The redirection to the home page.
@@ -72,7 +72,7 @@ def login() -> redirect:
@bp.post("logout", endpoint="logout")
def logout() -> redirect:
def logout() -> Response:
"""Logs out the user.
:return: The redirection to the home page.
+4 -4
View File
@@ -1,7 +1,7 @@
# The Mia! Accounting Demonstration Website.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/12
# Copyright (c) 2023-2024 imacat.
# Copyright (c) 2023-2026 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
import datetime as dt
from flask import Flask, Blueprint, url_for, flash, redirect, session, \
render_template, current_app
render_template, current_app, Response
from flask_babel import lazy_gettext
from accounting.utils.timezone import get_tz_today
@@ -45,7 +45,7 @@ def reset() -> str:
@bp.post("sample", endpoint="sample")
@admin_required
def reset_sample() -> redirect:
def reset_sample() -> Response:
"""Resets the sample data.
:return: Redirection to the accounting application.
@@ -60,7 +60,7 @@ def reset_sample() -> redirect:
@bp.post("reset", endpoint="clean-up")
@admin_required
def clean_up() -> redirect:
def clean_up() -> Response:
"""Clean-up the database data.
:return: Redirection to the accounting application.
+1 -1
View File
@@ -165,7 +165,7 @@ def match_journal_entry_detail(location: str) -> int:
:return: The journal entry ID.
:raise AssertionError: When the location is not the journal entry detail.
"""
m: re.Match = re.match(
m: re.Match[str] | None = re.match(
r"^/accounting/journal-entries/(\d+)\?next=", location)
assert m is not None
return int(m.group(1))
+14 -9
View File
@@ -224,13 +224,14 @@ def __mess_up_debit(form: dict[str, str], currency_prefix: str) \
:return: The messed-up form.
"""
key: str
m: re.Match
m: re.Match[str] | None
# Remove the office disbursement
key = [x for x in form
if x.startswith(currency_prefix)
and form[x] == Accounts.OFFICE][0]
m = re.match(r"^((.+-)\d+-)account_code$", key)
assert m is not None
debit_prefix: str = m.group(2)
line_item_prefix: str = m.group(1)
amount: Decimal = Decimal(form[f"{line_item_prefix}amount"])
@@ -265,13 +266,14 @@ def __mess_up_credit(form: dict[str, str], currency_prefix: str) \
:return: The messed-up form.
"""
key: str
m: re.Match
m: re.Match[str] | None
# Remove the sales receipt
key = [x for x in form
if x.startswith(currency_prefix)
and form[x] == Accounts.SALES][0]
m = re.match(r"^((.+-)\d+-)account_code$", key)
assert m is not None
credit_prefix: str = m.group(2)
line_item_prefix: str = m.group(1)
amount: Decimal = Decimal(form[f"{line_item_prefix}amount"])
@@ -304,7 +306,6 @@ def __mess_up_currencies(form: dict[str, str]) -> dict[str, str]:
:return: The messed-up form.
"""
key: str
m: re.Match
# Remove JPY
currency_prefix: str = __get_currency_prefix(form, "JPY")
@@ -312,7 +313,7 @@ def __mess_up_currencies(form: dict[str, str]) -> dict[str, str]:
# Add AUD
indices: set[int] = set()
for key in form:
m = re.match(r"^currency-(\d+)-code$", key)
m: re.Match[str] | None = re.match(r"^currency-(\d+)-code$", key)
if m is not None:
indices.add(int(m.group(1)))
new_index: int = max(indices) + 5 + randbelow(20)
@@ -363,7 +364,8 @@ def __get_line_item_no_key(form: dict[str, str], currency_prefix: str,
key: str = [x for x in form
if x.startswith(currency_prefix)
and form[x] == code][0]
m: re.Match = re.match(r"^(.+-\d+-)account_code$", key)
m: re.Match[str] | None = re.match(r"^(.+-\d+-)account_code$", key)
assert m is not None
return f"{m.group(1)}no"
@@ -375,7 +377,8 @@ def __get_currency_prefix(form: dict[str, str], code: str) -> str:
:return: The prefix of the currency.
"""
key: str = [x for x in form if form[x] == code][0]
m: re.Match = re.match(r"^(.+-)code$", key)
m: re.Match[str] | None = re.match(r"^(.+-)code$", key)
assert m is not None
return m.group(1)
@@ -388,7 +391,7 @@ def set_negative_amount(form: dict[str, str]) -> None:
amount_keys: list[str] = []
prefix: str = ""
for key in form.keys():
m: re.Match = re.match(r"^(.+)-\d+-amount$", key)
m: re.Match[str] | None = re.match(r"^(.+)-\d+-amount$", key)
if m is None:
continue
if prefix != "" and prefix != m.group(1):
@@ -407,7 +410,8 @@ def remove_debit_in_a_currency(form: dict[str, str]) -> None:
:return: None.
"""
key: str = [x for x in form if "-debit-" in x][0]
m: re.Match = re.match(r"^(.+-debit-)", key)
m: re.Match[str] | None = re.match(r"^(.+-debit-)", key)
assert m is not None
keys: set[str] = {x for x in form if x.startswith(m.group(1))}
for key in keys:
del form[key]
@@ -420,7 +424,8 @@ def remove_credit_in_a_currency(form: dict[str, str]) -> None:
:return: None.
"""
key: str = [x for x in form if "-credit-" in x][0]
m: re.Match = re.match(r"^(.+-credit-)", key)
m: re.Match[str] | None = re.match(r"^(.+-credit-)", key)
assert m is not None
keys: set[str] = {x for x in form if x.startswith(m.group(1))}
for key in keys:
del form[key]