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