Fix various type hints
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]), \
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user