From 674b0de3b26bd17ea08e1cfcbe65aff5a316c431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BE=9D=E7=91=AA=E8=B2=93?= Date: Sun, 5 Apr 2026 08:27:40 +0800 Subject: [PATCH] Fix various type hints --- src/accounting/account/queries.py | 8 ++--- src/accounting/account/views.py | 16 ++++----- src/accounting/base_account/queries.py | 2 +- src/accounting/currency/queries.py | 2 +- src/accounting/currency/views.py | 10 +++--- src/accounting/journal_entry/forms/reorder.py | 2 +- .../journal_entry/utils/account_option.py | 2 +- .../journal_entry/utils/description_editor.py | 6 ++-- .../journal_entry/utils/operators.py | 1 + .../utils/original_line_items.py | 4 +-- src/accounting/journal_entry/views.py | 12 +++---- src/accounting/models.py | 10 ++++-- src/accounting/option/views.py | 6 ++-- src/accounting/report/period/parser.py | 1 + src/accounting/report/period/specification.py | 16 ++++++--- .../report/reports/balance_sheet.py | 10 +++--- .../report/reports/income_expenses.py | 8 ++--- .../report/reports/income_statement.py | 4 +-- src/accounting/report/reports/journal.py | 2 +- src/accounting/report/reports/ledger.py | 2 +- src/accounting/report/reports/search.py | 10 +++--- .../report/reports/trial_balance.py | 2 +- src/accounting/report/utils/csv_export.py | 22 +++++++----- src/accounting/report/utils/offset_matcher.py | 4 +-- src/accounting/report/utils/report_type.py | 18 +++++----- src/accounting/report/views.py | 4 +-- src/accounting/utils/current_account.py | 6 ++-- src/accounting/utils/journal_entry_types.py | 6 ++-- src/accounting/utils/query.py | 5 ++- src/accounting/utils/title_case.py | 2 +- tests/test_account.py | 34 +++++++++++++------ tests/test_commands.py | 2 +- tests/test_site/auth.py | 6 ++-- tests/test_site/reset.py | 8 ++--- tests/testlib.py | 2 +- tests/testlib_journal_entry.py | 23 ++++++++----- 36 files changed, 157 insertions(+), 121 deletions(-) diff --git a/src/accounting/account/queries.py b/src/accounting/account/queries.py index d8ac09c..d6d6373 100644 --- a/src/accounting/account/queries.py +++ b/src/accounting/account/queries.py @@ -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), diff --git a/src/accounting/account/views.py b/src/accounting/account/views.py index 04a9b7b..b9a8cf5 100644 --- a/src/accounting/account/views.py +++ b/src/accounting/account/views.py @@ -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("/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("/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/", 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. diff --git a/src/accounting/base_account/queries.py b/src/accounting/base_account/queries.py index 4aae228..f8091ee 100644 --- a/src/accounting/base_account/queries.py +++ b/src/accounting/base_account/queries.py @@ -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() diff --git a/src/accounting/currency/queries.py b/src/accounting/currency/queries.py index 98acc70..29137c4 100644 --- a/src/accounting/currency/queries.py +++ b/src/accounting/currency/queries.py @@ -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() diff --git a/src/accounting/currency/views.py b/src/accounting/currency/views.py index 685ac7c..bf956e3 100644 --- a/src/accounting/currency/views.py +++ b/src/accounting/currency/views.py @@ -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("/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("/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. diff --git a/src/accounting/journal_entry/forms/reorder.py b/src/accounting/journal_entry/forms/reorder.py index 2c0932d..1284fbc 100644 --- a/src/accounting/journal_entry/forms/reorder.py +++ b/src/accounting/journal_entry/forms/reorder.py @@ -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\ diff --git a/src/accounting/journal_entry/utils/account_option.py b/src/accounting/journal_entry/utils/account_option.py index 2d7e718..3a4bc53 100644 --- a/src/accounting/journal_entry/utils/account_option.py +++ b/src/accounting/journal_entry/utils/account_option.py @@ -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.""" diff --git a/src/accounting/journal_entry/utils/description_editor.py b/src/accounting/journal_entry/utils/description_editor.py index f894893..269469e 100644 --- a/src/accounting/journal_entry/utils/description_editor.py +++ b/src/accounting/journal_entry/utils/description_editor.py @@ -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 diff --git a/src/accounting/journal_entry/utils/operators.py b/src/accounting/journal_entry/utils/operators.py index 8915e36..79455b5 100644 --- a/src/accounting/journal_entry/utils/operators.py +++ b/src/accounting/journal_entry/utils/operators.py @@ -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 diff --git a/src/accounting/journal_entry/utils/original_line_items.py b/src/accounting/journal_entry/utils/original_line_items.py index d250013..2321ef3 100644 --- a/src/accounting/journal_entry/utils/original_line_items.py +++ b/src/accounting/journal_entry/utils/original_line_items.py @@ -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))) diff --git a/src/accounting/journal_entry/views.py b/src/accounting/journal_entry/views.py index 6795152..7560fa0 100644 --- a/src/accounting/journal_entry/views.py +++ b/src/accounting/journal_entry/views.py @@ -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/", 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("/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("/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/", 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. diff --git a/src/accounting/models.py b/src/accounting/models.py index 4524b3d..1e5a849 100644 --- a/src/accounting/models.py +++ b/src/accounting/models.py @@ -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): diff --git a/src/accounting/option/views.py b/src/accounting/option/views.py index 6dd604f..57b6c5c 100644 --- a/src/accounting/option/views.py +++ b/src/accounting/option/views.py @@ -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. diff --git a/src/accounting/report/period/parser.py b/src/accounting/report/period/parser.py index 2bfe028..cbd9c49 100644 --- a/src/accounting/report/period/parser.py +++ b/src/accounting/report/period/parser.py @@ -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]), \ diff --git a/src/accounting/report/period/specification.py b/src/accounting/report/period/specification.py index 95f4623..c6fe67f 100644 --- a/src/accounting/report/period/specification.py +++ b/src/accounting/report/period/specification.py @@ -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: diff --git a/src/accounting/report/reports/balance_sheet.py b/src/accounting/report/reports/balance_sheet.py index 65969b7..39ca2a8 100644 --- a/src/accounting/report/reports/balance_sheet.py +++ b/src/accounting/report/reports/balance_sheet.py @@ -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. diff --git a/src/accounting/report/reports/income_expenses.py b/src/accounting/report/reports/income_expenses.py index ae5b355..6412103 100644 --- a/src/accounting/report/reports/income_expenses.py +++ b/src/accounting/report/reports/income_expenses.py @@ -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 diff --git a/src/accounting/report/reports/income_statement.py b/src/accounting/report/reports/income_statement.py index c45349a..8af89d7 100644 --- a/src/accounting/report/reports/income_statement.py +++ b/src/accounting/report/reports/income_statement.py @@ -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: diff --git a/src/accounting/report/reports/journal.py b/src/accounting/report/reports/journal.py index 67bdeb3..470b2d3 100644 --- a/src/accounting/report/reports/journal.py +++ b/src/accounting/report/reports/journal.py @@ -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: diff --git a/src/accounting/report/reports/ledger.py b/src/accounting/report/reports/ledger.py index 428b957..1175145 100644 --- a/src/accounting/report/reports/ledger.py +++ b/src/accounting/report/reports/ledger.py @@ -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: diff --git a/src/accounting/report/reports/search.py b/src/accounting/report/reports/search.py index 4446cf3..e326b44 100644 --- a/src/accounting/report/reports/search.py +++ b/src/accounting/report/reports/search.py @@ -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: diff --git a/src/accounting/report/reports/trial_balance.py b/src/accounting/report/reports/trial_balance.py index 50be407..82cd7b1 100644 --- a/src/accounting/report/reports/trial_balance.py +++ b/src/accounting/report/reports/trial_balance.py @@ -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) diff --git a/src/accounting/report/utils/csv_export.py b/src/accounting/report/utils/csv_export.py index 7e3cede..81df94b 100644 --- a/src/accounting/report/utils/csv_export.py +++ b/src/accounting/report/utils/csv_export.py @@ -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: diff --git a/src/accounting/report/utils/offset_matcher.py b/src/accounting/report/utils/offset_matcher.py index 905ecab..e244f85 100644 --- a/src/accounting/report/utils/offset_matcher.py +++ b/src/accounting/report/utils/offset_matcher.py @@ -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, diff --git a/src/accounting/report/utils/report_type.py b/src/accounting/report/utils/report_type.py index e4a68ea..046239f 100644 --- a/src/accounting/report/utils/report_type.py +++ b/src/accounting/report/utils/report_type.py @@ -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.""" diff --git a/src/accounting/report/views.py b/src/accounting/report/views.py index b12db8f..96a557e 100644 --- a/src/accounting/report/views.py +++ b/src/accounting/report/views.py @@ -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//", 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. diff --git a/src/accounting/utils/current_account.py b/src/accounting/utils/current_account.py index 5be0151..71d41f1 100644 --- a/src/accounting/utils/current_account.py +++ b/src/accounting/utils/current_account.py @@ -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. diff --git a/src/accounting/utils/journal_entry_types.py b/src/accounting/utils/journal_entry_types.py index 64f621c..bb08619 100644 --- a/src/accounting/utils/journal_entry_types.py +++ b/src/accounting/utils/journal_entry_types.py @@ -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.""" diff --git a/src/accounting/utils/query.py b/src/accounting/utils/query.py index 166c8bf..c40c965 100644 --- a/src/accounting/utils/query.py +++ b/src/accounting/utils/query.py @@ -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)) diff --git a/src/accounting/utils/title_case.py b/src/accounting/utils/title_case.py index 920f2e3..e331ee0 100644 --- a/src/accounting/utils/title_case.py +++ b/src/accounting/utils/title_case.py @@ -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. diff --git a/tests/test_account.py b/tests/test_account.py index 2ecfc6d..1b67bbb 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -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 diff --git a/tests/test_commands.py b/tests/test_commands.py index 7b1c4c7..4f6edd3 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -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, diff --git a/tests/test_site/auth.py b/tests/test_site/auth.py index d661066..6572d1a 100644 --- a/tests/test_site/auth.py +++ b/tests/test_site/auth.py @@ -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. diff --git a/tests/test_site/reset.py b/tests/test_site/reset.py index 85ec4c3..b7fc023 100644 --- a/tests/test_site/reset.py +++ b/tests/test_site/reset.py @@ -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. diff --git a/tests/testlib.py b/tests/testlib.py index 0689079..f48d471 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -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)) diff --git a/tests/testlib_journal_entry.py b/tests/testlib_journal_entry.py index 9f4ec9b..bb2bee0 100644 --- a/tests/testlib_journal_entry.py +++ b/tests/testlib_journal_entry.py @@ -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]