Compare commits

..

3 Commits

25 changed files with 117 additions and 114 deletions

View File

@ -17,15 +17,15 @@
"""The console commands for the account management. """The console commands for the account management.
""" """
import typing as t
from secrets import randbelow from secrets import randbelow
from typing import Any
import click import click
import sqlalchemy as sa
from accounting import db from accounting import db
from accounting.models import BaseAccount, Account, AccountL10n from accounting.models import BaseAccount, Account, AccountL10n
from accounting.utils.user import get_user_pk from accounting.utils.user import get_user_pk
import sqlalchemy as sa
AccountData = tuple[int, str, int, str, str, str, bool] AccountData = tuple[int, str, int, str, str, str, bool]
"""The format of the account data, as a list of (ID, base account code, number, """The format of the account data, as a list of (ID, base account code, number,
@ -63,8 +63,8 @@ def init_accounts_command(username: str) -> None:
existing_id.add(new_id) existing_id.add(new_id)
return new_id return new_id
data: list[dict[str, t.Any]] = [] data: list[dict[str, Any]] = []
l10n_data: list[dict[str, t.Any]] = [] l10n_data: list[dict[str, Any]] = []
for base in bases_to_add: for base in bases_to_add:
l10n: dict[str, str] = {x.locale: x.title for x in base.l10n} l10n: dict[str, str] = {x.locale: x.title for x in base.l10n}
account_id: int = get_new_id() account_id: int = get_new_id()

View File

@ -18,7 +18,7 @@
""" """
import csv import csv
import typing as t from typing import Any
import sqlalchemy as sa import sqlalchemy as sa
@ -39,11 +39,11 @@ def init_currencies_command(username: str) -> None:
return return
creator_pk: int = get_user_pk(username) creator_pk: int = get_user_pk(username)
currency_data: list[dict[str, t.Any]] = [{"code": x["code"], currency_data: list[dict[str, Any]] = [{"code": x["code"],
"name_l10n": x["name"], "name_l10n": x["name"],
"created_by_id": creator_pk, "created_by_id": creator_pk,
"updated_by_id": creator_pk} "updated_by_id": creator_pk}
for x in to_add] for x in to_add]
locales: list[str] = [x[5:] for x in to_add[0] if x.startswith("l10n-")] locales: list[str] = [x[5:] for x in to_add[0] if x.startswith("l10n-")]
l10n_data: list[dict[str, str]] = [{"currency_code": x["code"], l10n_data: list[dict[str, str]] = [{"currency_code": x["code"],
"locale": y, "locale": y,

View File

@ -18,8 +18,8 @@
""" """
import datetime as dt import datetime as dt
import typing as t
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import TypeVar, Generic, Type
import sqlalchemy as sa import sqlalchemy as sa
from flask_babel import LazyString from flask_babel import LazyString
@ -29,13 +29,13 @@ from wtforms import DateField, FieldList, FormField, TextAreaField, \
from wtforms.validators import DataRequired, ValidationError from wtforms.validators import DataRequired, ValidationError
from accounting import db from accounting import db
from accounting.journal_entry.utils.account_option import AccountOption
from accounting.journal_entry.utils.description_editor import DescriptionEditor
from accounting.journal_entry.utils.original_line_items import \
get_selectable_original_line_items
from accounting.locale import lazy_gettext from accounting.locale import lazy_gettext
from accounting.models import JournalEntry, Account, JournalEntryLineItem, \ from accounting.models import JournalEntry, Account, JournalEntryLineItem, \
JournalEntryCurrency JournalEntryCurrency
from accounting.journal_entry.utils.account_option import AccountOption
from accounting.journal_entry.utils.original_line_items import \
get_selectable_original_line_items
from accounting.journal_entry.utils.description_editor import DescriptionEditor
from accounting.utils.random_id import new_id from accounting.utils.random_id import new_id
from accounting.utils.strip_text import strip_multiline_text from accounting.utils.strip_text import strip_multiline_text
from accounting.utils.user import get_current_user_pk from accounting.utils.user import get_current_user_pk
@ -123,7 +123,7 @@ class JournalEntryForm(FlaskForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.is_modified: bool = False self.is_modified: bool = False
"""Whether the journal entry is modified during populate_obj().""" """Whether the journal entry is modified during populate_obj()."""
self.collector: t.Type[LineItemCollector] = LineItemCollector self.collector: Type[LineItemCollector] = LineItemCollector
"""The line item collector. The default is the base abstract """The line item collector. The default is the base abstract
collector only to provide the correct type. The subclass forms should collector only to provide the correct type. The subclass forms should
provide their own collectors.""" provide their own collectors."""
@ -155,7 +155,7 @@ class JournalEntryForm(FlaskForm):
self.__set_date(obj, self.date.data) self.__set_date(obj, self.date.data)
obj.note = self.note.data obj.note = self.note.data
collector_cls: t.Type[LineItemCollector] = self.collector collector_cls: Type[LineItemCollector] = self.collector
collector: collector_cls = collector_cls(self, obj) collector: collector_cls = collector_cls(self, obj)
collector.collect() collector.collect()
@ -309,11 +309,11 @@ class JournalEntryForm(FlaskForm):
return db.session.scalar(select) return db.session.scalar(select)
T = t.TypeVar("T", bound=JournalEntryForm) T = TypeVar("T", bound=JournalEntryForm)
"""A journal entry form variant.""" """A journal entry form variant."""
class LineItemCollector(t.Generic[T], ABC): class LineItemCollector(Generic[T], ABC):
"""The line item collector.""" """The line item collector."""
def __init__(self, form: T, obj: JournalEntry): def __init__(self, form: T, obj: JournalEntry):

View File

@ -18,7 +18,7 @@
""" """
import re import re
import typing as t from typing import Literal
import sqlalchemy as sa import sqlalchemy as sa
@ -124,12 +124,12 @@ class DescriptionTag:
class DescriptionType: class DescriptionType:
"""A description type""" """A description type"""
def __init__(self, type_id: t.Literal["general", "travel", "bus"]): def __init__(self, type_id: Literal["general", "travel", "bus"]):
"""Constructs a description type. """Constructs a description type.
:param type_id: The type ID, either "general", "travel", or "bus". :param type_id: The type ID, either "general", "travel", or "bus".
""" """
self.id: t.Literal["general", "travel", "bus"] = type_id self.id: Literal["general", "travel", "bus"] = type_id
"""The type ID.""" """The type ID."""
self.__tag_dict: dict[str, DescriptionTag] = {} self.__tag_dict: dict[str, DescriptionTag] = {}
"""A dictionary from the tag name to their corresponding tag.""" """A dictionary from the tag name to their corresponding tag."""
@ -181,12 +181,12 @@ class DescriptionRecurring:
class DescriptionDebitCredit: class DescriptionDebitCredit:
"""The description on debit or credit.""" """The description on debit or credit."""
def __init__(self, debit_credit: t.Literal["debit", "credit"]): def __init__(self, debit_credit: Literal["debit", "credit"]):
"""Constructs the description on debit or credit. """Constructs the description on debit or credit.
:param debit_credit: Either "debit" or "credit". :param debit_credit: Either "debit" or "credit".
""" """
self.debit_credit: t.Literal["debit", "credit"] = debit_credit self.debit_credit: Literal["debit", "credit"] = debit_credit
"""Either debit or credit.""" """Either debit or credit."""
self.general: DescriptionType = DescriptionType("general") self.general: DescriptionType = DescriptionType("general")
"""The general tags.""" """The general tags."""
@ -194,14 +194,14 @@ class DescriptionDebitCredit:
"""The travel tags.""" """The travel tags."""
self.bus: DescriptionType = DescriptionType("bus") self.bus: DescriptionType = DescriptionType("bus")
"""The bus tags.""" """The bus tags."""
self.__type_dict: dict[t.Literal["general", "travel", "bus"], self.__type_dict: dict[Literal["general", "travel", "bus"],
DescriptionType] \ DescriptionType] \
= {x.id: x for x in {self.general, self.travel, self.bus}} = {x.id: x for x in {self.general, self.travel, self.bus}}
"""A dictionary from the type ID to the corresponding tags.""" """A dictionary from the type ID to the corresponding tags."""
self.recurring: list[DescriptionRecurring] = [] self.recurring: list[DescriptionRecurring] = []
"""The recurring transactions.""" """The recurring transactions."""
def add_tag(self, tag_type: t.Literal["general", "travel", "bus"], def add_tag(self, tag_type: Literal["general", "travel", "bus"],
name: str, account: Account, freq: int) -> None: name: str, account: Account, freq: int) -> None:
"""Adds a tag. """Adds a tag.
@ -278,7 +278,7 @@ class DescriptionEditor:
accounts: dict[int, Account] \ accounts: dict[int, Account] \
= {x.id: x for x in Account.query = {x.id: x for x in Account.query
.filter(Account.id.in_({x.account_id for x in result})).all()} .filter(Account.id.in_({x.account_id for x in result})).all()}
debit_credit_dict: dict[t.Literal["debit", "credit"], debit_credit_dict: dict[Literal["debit", "credit"],
DescriptionDebitCredit] \ DescriptionDebitCredit] \
= {x.debit_credit: x for x in {self.debit, self.credit}} = {x.debit_credit: x for x in {self.debit, self.credit}}
for row in result: for row in result:

View File

@ -17,19 +17,19 @@
"""The operators for different journal entry types. """The operators for different journal entry types.
""" """
import typing as t
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Type
from flask import render_template, request, abort from flask import render_template, request, abort
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from accounting.models import JournalEntry
from accounting.template_globals import default_currency_code
from accounting.utils.journal_entry_types import JournalEntryType
from accounting.journal_entry.forms import JournalEntryForm, \ from accounting.journal_entry.forms import JournalEntryForm, \
CashReceiptJournalEntryForm, CashDisbursementJournalEntryForm, \ CashReceiptJournalEntryForm, CashDisbursementJournalEntryForm, \
TransferJournalEntryForm TransferJournalEntryForm
from accounting.journal_entry.forms.line_item import LineItemForm from accounting.journal_entry.forms.line_item import LineItemForm
from accounting.models import JournalEntry
from accounting.template_globals import default_currency_code
from accounting.utils.journal_entry_types import JournalEntryType
class JournalEntryOperator(ABC): class JournalEntryOperator(ABC):
@ -39,7 +39,7 @@ class JournalEntryOperator(ABC):
@property @property
@abstractmethod @abstractmethod
def form(self) -> t.Type[JournalEntryForm]: def form(self) -> Type[JournalEntryForm]:
"""Returns the form class. """Returns the form class.
:return: The form class. :return: The form class.
@ -100,7 +100,7 @@ class CashReceiptJournalEntry(JournalEntryOperator):
"""The order when checking the journal entry operator.""" """The order when checking the journal entry operator."""
@property @property
def form(self) -> t.Type[JournalEntryForm]: def form(self) -> Type[JournalEntryForm]:
"""Returns the form class. """Returns the form class.
:return: The form class. :return: The form class.
@ -170,7 +170,7 @@ class CashDisbursementJournalEntry(JournalEntryOperator):
"""The order when checking the journal entry operator.""" """The order when checking the journal entry operator."""
@property @property
def form(self) -> t.Type[JournalEntryForm]: def form(self) -> Type[JournalEntryForm]:
"""Returns the form class. """Returns the form class.
:return: The form class. :return: The form class.
@ -243,7 +243,7 @@ class TransferJournalEntry(JournalEntryOperator):
"""The order when checking the journal entry operator.""" """The order when checking the journal entry operator."""
@property @property
def form(self) -> t.Type[JournalEntryForm]: def form(self) -> Type[JournalEntryForm]:
"""Returns the form class. """Returns the form class.
:return: The form class. :return: The form class.

View File

@ -21,8 +21,8 @@ from __future__ import annotations
import datetime as dt import datetime as dt
import re import re
import typing as t
from decimal import Decimal from decimal import Decimal
from typing import Type, Annotated, Self
import sqlalchemy as sa import sqlalchemy as sa
from babel import Locale from babel import Locale
@ -34,16 +34,16 @@ from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.utils.user import user_cls, user_pk_column from accounting.utils.user import user_cls, user_pk_column
timestamp: t.Type[dt.datetime] \ timestamp: Type[dt.datetime] \
= t.Annotated[dt.datetime, mapped_column(db.DateTime(timezone=True), = Annotated[dt.datetime, mapped_column(db.DateTime(timezone=True),
server_default=db.func.now())] server_default=db.func.now())]
"""The timestamp.""" """The timestamp."""
user_pk: t.Type[int] \ user_pk: Type[int] \
= t.Annotated[int, mapped_column(db.ForeignKey(user_pk_column, = Annotated[int, mapped_column(db.ForeignKey(user_pk_column,
onupdate="CASCADE"))] onupdate="CASCADE"))]
"""The user primary key.""" """The user primary key."""
random_pk: t.Type[int] \ random_pk: Type[int] \
= t.Annotated[int, mapped_column(primary_key=True, autoincrement=False)] = Annotated[int, mapped_column(primary_key=True, autoincrement=False)]
"""The random primary key.""" """The random primary key."""
@ -273,11 +273,11 @@ class Account(db.Model):
:return: None. :return: None.
""" """
AccountL10n.query.filter(AccountL10n.account == self).delete() AccountL10n.query.filter(AccountL10n.account == self).delete()
cls: t.Type[t.Self] = self.__class__ cls: Type[Self] = self.__class__
cls.query.filter(cls.id == self.id).delete() cls.query.filter(cls.id == self.id).delete()
@classmethod @classmethod
def find_by_code(cls, code: str) -> t.Self | None: def find_by_code(cls, code: str) -> Self | None:
"""Finds an account by its code. """Finds an account by its code.
:param code: The code. :param code: The code.
@ -290,7 +290,7 @@ class Account(db.Model):
cls.no == int(m.group(2))).first() cls.no == int(m.group(2))).first()
@classmethod @classmethod
def selectable_debit(cls) -> list[t.Self]: def selectable_debit(cls) -> list[Self]:
"""Returns the selectable debit accounts. """Returns the selectable debit accounts.
Payable line items can not start from debit. Payable line items can not start from debit.
@ -313,7 +313,7 @@ class Account(db.Model):
.order_by(cls.base_code, cls.no).all() .order_by(cls.base_code, cls.no).all()
@classmethod @classmethod
def selectable_credit(cls) -> list[t.Self]: def selectable_credit(cls) -> list[Self]:
"""Returns the selectable debit accounts. """Returns the selectable debit accounts.
Receivable line items can not start from credit. Receivable line items can not start from credit.
@ -335,7 +335,7 @@ class Account(db.Model):
.order_by(cls.base_code, cls.no).all() .order_by(cls.base_code, cls.no).all()
@classmethod @classmethod
def cash(cls) -> t.Self: def cash(cls) -> Self:
"""Returns the cash account. """Returns the cash account.
:return: The cash account :return: The cash account
@ -343,7 +343,7 @@ class Account(db.Model):
return cls.find_by_code(cls.CASH_CODE) return cls.find_by_code(cls.CASH_CODE)
@classmethod @classmethod
def accumulated_change(cls) -> t.Self: def accumulated_change(cls) -> Self:
"""Returns the accumulated-change account. """Returns the accumulated-change account.
:return: The accumulated-change account :return: The accumulated-change account
@ -467,7 +467,7 @@ class Currency(db.Model):
:return: None. :return: None.
""" """
CurrencyL10n.query.filter(CurrencyL10n.currency == self).delete() CurrencyL10n.query.filter(CurrencyL10n.currency == self).delete()
cls: t.Type[t.Self] = self.__class__ cls: Type[Self] = self.__class__
cls.query.filter(cls.code == self.code).delete() cls.query.filter(cls.code == self.code).delete()
@ -797,14 +797,14 @@ class JournalEntryLineItem(db.Model):
setattr(self, "__balance", value) setattr(self, "__balance", value)
@property @property
def offsets(self) -> list[t.Self]: def offsets(self) -> list[Self]:
"""Returns the offset items. """Returns the offset items.
:return: The offset items. :return: The offset items.
""" """
if not hasattr(self, "__offsets"): if not hasattr(self, "__offsets"):
cls: t.Type[t.Self] = self.__class__ cls: Type[Self] = self.__class__
offsets: list[t.Self] = cls.query.join(JournalEntry)\ offsets: list[Self] = cls.query.join(JournalEntry)\
.filter(JournalEntryLineItem.original_line_item_id == self.id)\ .filter(JournalEntryLineItem.original_line_item_id == self.id)\
.order_by(JournalEntry.date, JournalEntry.no, .order_by(JournalEntry.date, JournalEntry.no,
cls.is_debit, cls.no).all() cls.is_debit, cls.no).all()
@ -831,7 +831,7 @@ class JournalEntryLineItem(db.Model):
setattr(self, "__is_offset", value) setattr(self, "__is_offset", value)
@property @property
def match(self) -> t.Self | None: def match(self) -> Self | None:
"""Returns the match of the line item. """Returns the match of the line item.
:return: The match of the line item. :return: The match of the line item.
@ -841,7 +841,7 @@ class JournalEntryLineItem(db.Model):
return getattr(self, "__match") return getattr(self, "__match")
@match.setter @match.setter
def match(self, value: t.Self) -> None: def match(self, value: Self) -> None:
"""Sets the match of the line item. """Sets the match of the line item.
:param value: The matcho of the line item. :param value: The matcho of the line item.

View File

@ -21,7 +21,7 @@ This file is largely taken from the NanoParma ERP project, first written in
""" """
import datetime as dt import datetime as dt
import typing as t from collections.abc import Callable
from accounting.models import JournalEntry from accounting.models import JournalEntry
from .period import Period from .period import Period
@ -32,13 +32,13 @@ from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
class PeriodChooser: class PeriodChooser:
"""The period chooser.""" """The period chooser."""
def __init__(self, get_url: t.Callable[[Period], str]): def __init__(self, get_url: Callable[[Period], str]):
"""Constructs a period chooser. """Constructs a period chooser.
:param get_url: The callback to return the URL of the current report in :param get_url: The callback to return the URL of the current report in
a period. a period.
""" """
self.__get_url: t.Callable[[Period], str] = get_url self.__get_url: Callable[[Period], str] = get_url
"""The callback to return the URL of the current report in a period.""" """The callback to return the URL of the current report in a period."""
# Shortcut periods # Shortcut periods

View File

@ -20,7 +20,8 @@
import calendar import calendar
import datetime as dt import datetime as dt
import re import re
import typing as t from collections.abc import Callable
from typing import Type
from .period import Period from .period import Period
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \ from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
@ -39,7 +40,7 @@ def get_period(spec: str | None = None) -> Period:
""" """
if spec is None: if spec is None:
return ThisMonth() return ThisMonth()
named_periods: dict[str, t.Type[t.Callable[[], Period]]] = { named_periods: dict[str, Type[Callable[[], Period]]] = {
"this-month": lambda: ThisMonth(), "this-month": lambda: ThisMonth(),
"last-month": lambda: LastMonth(), "last-month": lambda: LastMonth(),
"since-last-month": lambda: SinceLastMonth(), "since-last-month": lambda: SinceLastMonth(),

View File

@ -21,7 +21,7 @@ This file is largely taken from the NanoParma ERP project, first written in
""" """
import datetime as dt import datetime as dt
import typing as t from typing import Self
from .description import get_desc from .description import get_desc
from .month_end import month_end from .month_end import month_end
@ -119,7 +119,7 @@ class Period:
and not self.is_a_day and not self.is_a_day
@property @property
def before(self) -> t.Self | None: def before(self) -> Self | None:
"""Returns the period before this period. """Returns the period before this period.
:return: The period before this period. :return: The period before this period.

View File

@ -17,8 +17,9 @@
"""The page parameters of a report. """The page parameters of a report.
""" """
import typing as t
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Callable
from typing import Type
from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \ from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \
urlunparse urlunparse
@ -52,7 +53,7 @@ class BasePageParams(ABC):
""" """
@property @property
def journal_entry_types(self) -> t.Type[JournalEntryType]: def journal_entry_types(self) -> Type[JournalEntryType]:
"""Returns the journal entry types. """Returns the journal entry types.
:return: The journal entry types. :return: The journal entry types.
@ -72,7 +73,7 @@ class BasePageParams(ABC):
return urlunparse(parts) return urlunparse(parts)
@staticmethod @staticmethod
def _get_currency_options(get_url: t.Callable[[Currency], str], def _get_currency_options(get_url: Callable[[Currency], str],
active_currency: Currency) -> list[OptionLink]: active_currency: Currency) -> list[OptionLink]:
"""Returns the currency options. """Returns the currency options.

View File

@ -21,7 +21,7 @@ This file is largely taken from the NanoParma ERP project, first written in
""" """
import re import re
import typing as t from collections.abc import Iterator
from flask_babel import LazyString from flask_babel import LazyString
@ -190,7 +190,7 @@ class ReportChooser:
self.__active_report == ReportType.UNMATCHED, self.__active_report == ReportType.UNMATCHED,
fa_icon="fa-solid fa-file-circle-question") fa_icon="fa-solid fa-file-circle-question")
def __iter__(self) -> t.Iterator[OptionLink]: def __iter__(self) -> Iterator[OptionLink]:
"""Returns the iteration of the reports. """Returns the iteration of the reports.
:return: The iteration of the reports. :return: The iteration of the reports.

View File

@ -18,8 +18,8 @@
""" """
import datetime as dt import datetime as dt
import typing as t
from decimal import Decimal from decimal import Decimal
from typing import Any
from flask_babel import get_locale from flask_babel import get_locale
@ -71,7 +71,7 @@ def format_date(value: dt.date) -> str:
return "{}/{}({})".format(value.month, value.day, weekday) return "{}/{}({})".format(value.month, value.day, weekday)
def default(value: t.Any, default_value: t.Any = "") -> t.Any: def default(value: Any, default_value: Any = "") -> Any:
"""Returns the default value if the given value is None. """Returns the default value if the given value is None.
:param value: The value. :param value: The value.

View File

@ -20,10 +20,10 @@ warnings from the IDE.
This module should not import any other module from the application. This module should not import any other module from the application.
""" """
import typing as t from typing import Any
def s(message: t.Any) -> str: def s(message: Any) -> str:
"""Casts the LazyString message to the string type. """Casts the LazyString message to the string type.
:param message: The message. :param message: The message.

View File

@ -17,12 +17,13 @@
"""The current assets and liabilities account. """The current assets and liabilities account.
""" """
import typing as t from typing import Self
import sqlalchemy as sa
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Account from accounting.models import Account
import sqlalchemy as sa
class CurrentAccount: class CurrentAccount:
@ -54,7 +55,7 @@ class CurrentAccount:
return self.str return self.str
@classmethod @classmethod
def current_assets_and_liabilities(cls) -> t.Self: def current_assets_and_liabilities(cls) -> Self:
"""Returns the pseudo account for all current assets and liabilities. """Returns the pseudo account for all current assets and liabilities.
:return: The pseudo account for all current assets and liabilities. :return: The pseudo account for all current assets and liabilities.
@ -67,7 +68,7 @@ class CurrentAccount:
return account return account
@classmethod @classmethod
def accounts(cls) -> list[t.Self]: def accounts(cls) -> list[Self]:
"""Returns the current assets and liabilities accounts. """Returns the current assets and liabilities accounts.
:return: The current assets and liabilities accounts. :return: The current assets and liabilities accounts.

View File

@ -19,7 +19,7 @@
This module should not import any other module from the application. This module should not import any other module from the application.
""" """
import typing as t from typing import Any
from flask import flash from flask import flash
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
@ -34,7 +34,7 @@ def flash_form_errors(form: FlaskForm) -> None:
__flash_errors(form.errors) __flash_errors(form.errors)
def __flash_errors(error: t.Any) -> None: def __flash_errors(error: Any) -> None:
"""Flash all errors recursively. """Flash all errors recursively.
:param error: The errors. :param error: The errors.

View File

@ -17,7 +17,7 @@
"""The SQLAlchemy alias for the offset items. """The SQLAlchemy alias for the offset items.
""" """
import typing as t from typing import Any
import sqlalchemy as sa import sqlalchemy as sa
@ -30,10 +30,10 @@ def offset_alias() -> sa.Alias:
:return: The SQLAlchemy alias for the offset items. :return: The SQLAlchemy alias for the offset items.
""" """
def as_from(model_cls: t.Any) -> sa.FromClause: def as_from(model_cls: Any) -> sa.FromClause:
return model_cls return model_cls
def as_alias(alias: t.Any) -> sa.Alias: def as_alias(alias: Any) -> sa.Alias:
return alias return alias
return as_alias(sa.alias(as_from(JournalEntryLineItem), name="offset")) return as_alias(sa.alias(as_from(JournalEntryLineItem), name="offset"))

View File

@ -19,7 +19,7 @@
This module should not import any other module from the application. This module should not import any other module from the application.
""" """
import typing as t from typing import TypeVar, Generic
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse, \ from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse, \
ParseResult ParseResult
@ -62,10 +62,10 @@ class Redirection(RequestRedirect):
DEFAULT_PAGE_SIZE: int = 10 DEFAULT_PAGE_SIZE: int = 10
"""The default page size.""" """The default page size."""
T = t.TypeVar("T") T = TypeVar("T")
class Pagination(t.Generic[T]): class Pagination(Generic[T]):
"""The pagination utility.""" """The pagination utility."""
def __init__(self, items: list[T], is_reversed: bool = False): def __init__(self, items: list[T], is_reversed: bool = False):
@ -91,7 +91,7 @@ class Pagination(t.Generic[T]):
"""The options to the number of items in a page.""" """The options to the number of items in a page."""
class AbstractPagination(t.Generic[T]): class AbstractPagination(Generic[T]):
"""An abstract pagination.""" """An abstract pagination."""
def __init__(self): def __init__(self):

View File

@ -19,21 +19,21 @@
This module should not import any other module from the application. This module should not import any other module from the application.
""" """
import typing as t from collections.abc import Callable
from flask import abort, Blueprint, Response from flask import abort, Blueprint, Response
from accounting.utils.user import get_current_user, UserUtilityInterface from accounting.utils.user import get_current_user, UserUtilityInterface
def has_permission(rule: t.Callable[[], bool]) -> t.Callable: def has_permission(rule: Callable[[], bool]) -> Callable:
"""The permission decorator to check whether the current user is allowed. """The permission decorator to check whether the current user is allowed.
:param rule: The permission rule. :param rule: The permission rule.
:return: The view decorator. :return: The view decorator.
""" """
def decorator(view: t.Callable) -> t.Callable: def decorator(view: Callable) -> Callable:
"""The view decorator to decorate a view with permission tests. """The view decorator to decorate a view with permission tests.
:param view: The view. :param view: The view.
@ -61,16 +61,16 @@ def has_permission(rule: t.Callable[[], bool]) -> t.Callable:
return decorator return decorator
__can_view_func: t.Callable[[], bool] = lambda: True __can_view_func: Callable[[], bool] = lambda: True
"""The callback that returns whether the current user can view the accounting """The callback that returns whether the current user can view the accounting
data.""" data."""
__can_edit_func: t.Callable[[], bool] = lambda: True __can_edit_func: Callable[[], bool] = lambda: True
"""The callback that returns whether the current user can edit the accounting """The callback that returns whether the current user can edit the accounting
data.""" data."""
__can_admin_func: t.Callable[[], bool] = lambda: True __can_admin_func: Callable[[], bool] = lambda: True
"""The callback that returns whether the current user can administrate the """The callback that returns whether the current user can administrate the
accounting settings.""" accounting settings."""
_unauthorized_func: t.Callable[[], Response | None] \ _unauthorized_func: Callable[[], Response | None] \
= lambda: Response(status=403) = lambda: Response(status=403)
"""The callback that returns the response to require the user to log in.""" """The callback that returns the response to require the user to log in."""

View File

@ -14,18 +14,18 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""The random ID mixin for the data models. """The random ID utility for the data models.
This module should not import any other module from the application. This module should not import any other module from the application.
""" """
import typing as t
from secrets import randbelow from secrets import randbelow
from typing import Type
from accounting import db from accounting import db
def new_id(cls: t.Type): def new_id(cls: Type[db.Model]):
"""Returns a new random ID for the data model. """Returns a new random ID for the data model.
:param cls: The data model. :param cls: The data model.

View File

@ -19,17 +19,17 @@
This module should not import any other module from the application. This module should not import any other module from the application.
""" """
import typing as t
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import TypeVar, Generic, Type
import sqlalchemy as sa import sqlalchemy as sa
from flask import g, Response from flask import g, Response
from flask_sqlalchemy.model import Model from flask_sqlalchemy.model import Model
T = t.TypeVar("T", bound=Model) T = TypeVar("T", bound=Model)
class UserUtilityInterface(t.Generic[T], ABC): class UserUtilityInterface(Generic[T], ABC):
"""The interface for the user utilities.""" """The interface for the user utilities."""
@abstractmethod @abstractmethod
@ -72,7 +72,7 @@ class UserUtilityInterface(t.Generic[T], ABC):
@property @property
@abstractmethod @abstractmethod
def cls(self) -> t.Type[T]: def cls(self) -> Type[T]:
"""Returns the class of the user data model. """Returns the class of the user data model.
:return: The class of the user data model. :return: The class of the user data model.
@ -112,7 +112,7 @@ class UserUtilityInterface(t.Generic[T], ABC):
__user_utils: UserUtilityInterface __user_utils: UserUtilityInterface
"""The user utilities.""" """The user utilities."""
user_cls: t.Type[Model] = Model user_cls: Type[Model] = Model
"""The user class.""" """The user class."""
user_pk_column: sa.Column = sa.Column(sa.Integer) user_pk_column: sa.Column = sa.Column(sa.Integer)
"""The primary key column of the user class.""" """The primary key column of the user class."""

View File

@ -18,8 +18,8 @@
""" """
import csv import csv
import typing as t
import unittest import unittest
from typing import Any
import sqlalchemy as sa import sqlalchemy as sa
from click.testing import Result from click.testing import Result
@ -80,7 +80,7 @@ class ConsoleCommandTestCase(unittest.TestCase):
from accounting.models import BaseAccount from accounting.models import BaseAccount
with open(data_dir / "base_accounts.csv") as fp: with open(data_dir / "base_accounts.csv") as fp:
data: dict[dict[str, t.Any]] \ data: dict[dict[str, Any]] \
= {x["code"]: {"code": x["code"], = {x["code"]: {"code": x["code"],
"title": x["title"], "title": x["title"],
"l10n": {y[5:]: x[y] "l10n": {y[5:]: x[y]
@ -135,7 +135,7 @@ class ConsoleCommandTestCase(unittest.TestCase):
from accounting.models import Currency from accounting.models import Currency
with open(data_dir / "currencies.csv") as fp: with open(data_dir / "currencies.csv") as fp:
data: dict[dict[str, t.Any]] \ data: dict[dict[str, Any]] \
= {x["code"]: {"code": x["code"], = {x["code"]: {"code": x["code"],
"name": x["name"], "name": x["name"],
"l10n": {y[5:]: x[y] "l10n": {y[5:]: x[y]

View File

@ -18,8 +18,8 @@
""" """
import os import os
import typing as t
from secrets import token_urlsafe from secrets import token_urlsafe
from typing import Type
from click.testing import Result from click.testing import Result
from flask import Flask, Blueprint, render_template, redirect, Response, \ from flask import Flask, Blueprint, render_template, redirect, Response, \
@ -94,7 +94,7 @@ def create_app(is_testing: bool = False) -> Flask:
return redirect(append_next(url_for("auth.login-form"))) return redirect(append_next(url_for("auth.login-form")))
@property @property
def cls(self) -> t.Type[auth.User]: def cls(self) -> Type[auth.User]:
return auth.User return auth.User
@property @property

View File

@ -17,7 +17,7 @@
"""The authentication for the Mia! Accounting demonstration website. """The authentication for the Mia! Accounting demonstration website.
""" """
import typing as t from collections.abc import Callable
from flask import Blueprint, render_template, Flask, redirect, url_for, \ from flask import Blueprint, render_template, Flask, redirect, url_for, \
session, request, g, Response, abort session, request, g, Response, abort
@ -96,7 +96,7 @@ def current_user() -> User | None:
return g.user return g.user
def admin_required(view: t.Callable) -> t.Callable: def admin_required(view: Callable) -> Callable:
"""The view decorator to require the user to be an administrator. """The view decorator to require the user to be an administrator.
:param view: The view. :param view: The view.

View File

@ -20,10 +20,10 @@
from __future__ import annotations from __future__ import annotations
import datetime as dt import datetime as dt
import typing as t
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from decimal import Decimal from decimal import Decimal
from secrets import randbelow from secrets import randbelow
from typing import Any
import sqlalchemy as sa import sqlalchemy as sa
from flask import Flask from flask import Flask
@ -193,8 +193,8 @@ class BaseTestData(ABC):
.filter(User.username == username).first() .filter(User.username == username).first()
assert current_user is not None assert current_user is not None
self.__current_user_id: int = current_user.id self.__current_user_id: int = current_user.id
self.__journal_entries: list[dict[str, t.Any]] = [] self.__journal_entries: list[dict[str, Any]] = []
self.__line_items: list[dict[str, t.Any]] = [] self.__line_items: list[dict[str, Any]] = []
self._init_data() self._init_data()
@abstractmethod @abstractmethod
@ -258,7 +258,7 @@ class BaseTestData(ABC):
assert account is not None assert account is not None
debit_no = debit_no + 1 debit_no = debit_no + 1
line_item.id = self.__new_id(existing_l_id) line_item.id = self.__new_id(existing_l_id)
data: dict[str, t.Any] \ data: dict[str, Any] \
= {"id": line_item.id, = {"id": line_item.id,
"journal_entry_id": journal_entry_data.id, "journal_entry_id": journal_entry_data.id,
"is_debit": True, "is_debit": True,
@ -277,7 +277,7 @@ class BaseTestData(ABC):
assert account is not None assert account is not None
credit_no = credit_no + 1 credit_no = credit_no + 1
line_item.id = self.__new_id(existing_l_id) line_item.id = self.__new_id(existing_l_id)
data: dict[str, t.Any] \ data: dict[str, Any] \
= {"id": line_item.id, = {"id": line_item.id,
"journal_entry_id": journal_entry_data.id, "journal_entry_id": journal_entry_data.id,
"is_debit": False, "is_debit": False,

View File

@ -20,7 +20,7 @@
from __future__ import annotations from __future__ import annotations
import re import re
import typing as t from typing import Literal
import httpx import httpx
from flask import Flask, render_template_string from flask import Flask, render_template_string
@ -108,7 +108,7 @@ def get_client(app: Flask, username: str) -> tuple[httpx.Client, str]:
def set_locale(client: httpx.Client, csrf_token: str, def set_locale(client: httpx.Client, csrf_token: str,
locale: t.Literal["en", "zh_Hant", "zh_Hans"]) -> None: locale: Literal["en", "zh_Hant", "zh_Hans"]) -> None:
"""Sets the current locale. """Sets the current locale.
:param client: The test client. :param client: The test client.