Compare commits

..

No commits in common. "4d11517e211d07b2cd4be00b13c03d8464e78515" and "d7bc01ccb4b8e697ed1228499911fc3145f4377d" have entirely different histories.

55 changed files with 1159 additions and 1304 deletions

View File

@ -1,69 +0,0 @@
accounting.report.period package
================================
Submodules
----------
accounting.report.period.chooser module
---------------------------------------
.. automodule:: accounting.report.period.chooser
:members:
:undoc-members:
:show-inheritance:
accounting.report.period.description module
-------------------------------------------
.. automodule:: accounting.report.period.description
:members:
:undoc-members:
:show-inheritance:
accounting.report.period.month\_end module
------------------------------------------
.. automodule:: accounting.report.period.month_end
:members:
:undoc-members:
:show-inheritance:
accounting.report.period.parser module
--------------------------------------
.. automodule:: accounting.report.period.parser
:members:
:undoc-members:
:show-inheritance:
accounting.report.period.period module
--------------------------------------
.. automodule:: accounting.report.period.period
:members:
:undoc-members:
:show-inheritance:
accounting.report.period.shortcuts module
-----------------------------------------
.. automodule:: accounting.report.period.shortcuts
:members:
:undoc-members:
:show-inheritance:
accounting.report.period.specification module
---------------------------------------------
.. automodule:: accounting.report.period.specification
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.report.period
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,6 +1,14 @@
accounting.report.reports package accounting.report.reports package
================================= =================================
Subpackages
-----------
.. toctree::
:maxdepth: 4
accounting.report.reports.utils
Submodules Submodules
---------- ----------

View File

@ -0,0 +1,77 @@
accounting.report.reports.utils package
=======================================
Submodules
----------
accounting.report.reports.utils.base\_page\_params module
---------------------------------------------------------
.. automodule:: accounting.report.reports.utils.base_page_params
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.utils.base\_report module
---------------------------------------------------
.. automodule:: accounting.report.reports.utils.base_report
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.utils.csv\_export module
--------------------------------------------------
.. automodule:: accounting.report.reports.utils.csv_export
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.utils.option\_link module
---------------------------------------------------
.. automodule:: accounting.report.reports.utils.option_link
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.utils.period\_choosers module
-------------------------------------------------------
.. automodule:: accounting.report.reports.utils.period_choosers
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.utils.report\_chooser module
------------------------------------------------------
.. automodule:: accounting.report.reports.utils.report_chooser
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.utils.report\_type module
---------------------------------------------------
.. automodule:: accounting.report.reports.utils.report_type
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.utils.urls module
-------------------------------------------
.. automodule:: accounting.report.reports.utils.urls
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.report.reports.utils
:members:
:undoc-members:
:show-inheritance:

View File

@ -7,9 +7,7 @@ Subpackages
.. toctree:: .. toctree::
:maxdepth: 4 :maxdepth: 4
accounting.report.period
accounting.report.reports accounting.report.reports
accounting.report.utils
Submodules Submodules
---------- ----------
@ -22,6 +20,22 @@ accounting.report.converters module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
accounting.report.income\_expense\_account module
-------------------------------------------------
.. automodule:: accounting.report.income_expense_account
:members:
:undoc-members:
:show-inheritance:
accounting.report.period module
-------------------------------
.. automodule:: accounting.report.period
:members:
:undoc-members:
:show-inheritance:
accounting.report.template\_filters module accounting.report.template\_filters module
------------------------------------------ ------------------------------------------

View File

@ -1,77 +0,0 @@
accounting.report.utils package
===============================
Submodules
----------
accounting.report.utils.base\_page\_params module
-------------------------------------------------
.. automodule:: accounting.report.utils.base_page_params
:members:
:undoc-members:
:show-inheritance:
accounting.report.utils.base\_report module
-------------------------------------------
.. automodule:: accounting.report.utils.base_report
:members:
:undoc-members:
:show-inheritance:
accounting.report.utils.csv\_export module
------------------------------------------
.. automodule:: accounting.report.utils.csv_export
:members:
:undoc-members:
:show-inheritance:
accounting.report.utils.ie\_account module
------------------------------------------
.. automodule:: accounting.report.utils.ie_account
:members:
:undoc-members:
:show-inheritance:
accounting.report.utils.option\_link module
-------------------------------------------
.. automodule:: accounting.report.utils.option_link
:members:
:undoc-members:
:show-inheritance:
accounting.report.utils.report\_chooser module
----------------------------------------------
.. automodule:: accounting.report.utils.report_chooser
:members:
:undoc-members:
:show-inheritance:
accounting.report.utils.report\_type module
-------------------------------------------
.. automodule:: accounting.report.utils.report_type
:members:
:undoc-members:
:show-inheritance:
accounting.report.utils.urls module
-----------------------------------
.. automodule:: accounting.report.utils.urls
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.report.utils
:members:
:undoc-members:
:show-inheritance:

View File

@ -17,7 +17,7 @@
[metadata] [metadata]
name = mia-accounting-flask name = mia-accounting-flask
version = 0.5.0 version = 0.4.0
author = imacat author = imacat
author_email = imacat@mail.imacat.idv.tw author_email = imacat@mail.imacat.idv.tw
description = The Mia! Accounting Flask project. description = The Mia! Accounting Flask project.

View File

@ -30,7 +30,7 @@ from accounting.utils.user import has_user, get_user_pk
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,
English, Traditional Chinese, Simplified Chinese, is-offset-needed) tuples.""" English, Traditional Chinese, Simplified Chinese, is-pay-off-needed) tuples."""
def __validate_username(ctx: click.core.Context, param: click.core.Option, def __validate_username(ctx: click.core.Context, param: click.core.Option,
@ -93,10 +93,10 @@ def init_accounts_command(username: str) -> None:
data: list[AccountData] = [] data: list[AccountData] = []
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}
is_offset_needed: bool = True if re.match("^[12]1[34]", base.code) \ is_pay_off_needed: bool = True if re.match("^[12]1[34]", base.code) \
else False else False
data.append((get_new_id(), base.code, 1, base.title_l10n, data.append((get_new_id(), base.code, 1, base.title_l10n,
l10n["zh_Hant"], l10n["zh_Hans"], is_offset_needed)) l10n["zh_Hant"], l10n["zh_Hans"], is_pay_off_needed))
__add_accounting_accounts(data, creator_pk) __add_accounting_accounts(data, creator_pk)
click.echo(F"{len(data)} added. Accounting accounts initialized.") click.echo(F"{len(data)} added. Accounting accounts initialized.")
@ -113,7 +113,7 @@ def __add_accounting_accounts(data: list[AccountData], creator_pk: int)\
base_code=x[1], base_code=x[1],
no=x[2], no=x[2],
title_l10n=x[3], title_l10n=x[3],
is_offset_needed=x[6], is_pay_off_needed=x[6],
created_by_id=creator_pk, created_by_id=creator_pk,
updated_by_id=creator_pk) updated_by_id=creator_pk)
for x in data] for x in data]

View File

@ -66,8 +66,8 @@ class AccountForm(FlaskForm):
filters=[strip_text], filters=[strip_text],
validators=[DataRequired(lazy_gettext("Please fill in the title"))]) validators=[DataRequired(lazy_gettext("Please fill in the title"))])
"""The title.""" """The title."""
is_offset_needed = BooleanField() is_pay_off_needed = BooleanField()
"""Whether the the entries of this account need offset.""" """Whether the the entries of this account need pay-off."""
def populate_obj(self, obj: Account) -> None: def populate_obj(self, obj: Account) -> None:
"""Populates the form data into an account object. """Populates the form data into an account object.
@ -87,7 +87,7 @@ class AccountForm(FlaskForm):
obj.base_code = self.base_code.data obj.base_code = self.base_code.data
obj.no = count + 1 obj.no = count + 1
obj.title = self.title.data obj.title = self.title.data
obj.is_offset_needed = self.is_offset_needed.data obj.is_pay_off_needed = self.is_pay_off_needed.data
if is_new: if is_new:
current_user_pk: int = get_current_user_pk() current_user_pk: int = get_current_user_pk()
obj.created_by_id = current_user_pk obj.created_by_id = current_user_pk

View File

@ -47,8 +47,8 @@ def get_account_query() -> list[Account]:
Account.title_l10n.contains(k), Account.title_l10n.contains(k),
code.contains(k), code.contains(k),
Account.id.in_(l10n_matches)] Account.id.in_(l10n_matches)]
if k in gettext("Need offset"): if k in gettext("Pay-off needed"):
sub_conditions.append(Account.is_offset_needed) sub_conditions.append(Account.is_pay_off_needed)
conditions.append(sa.or_(*sub_conditions)) conditions.append(sa.or_(*sub_conditions))
return Account.query.filter(*conditions)\ return Account.query.filter(*conditions)\

View File

@ -113,8 +113,8 @@ class Account(db.Model):
"""The account number under the base account.""" """The account number under the base account."""
title_l10n = db.Column("title", db.String, nullable=False) title_l10n = db.Column("title", db.String, nullable=False)
"""The title.""" """The title."""
is_offset_needed = db.Column(db.Boolean, nullable=False, default=False) is_pay_off_needed = db.Column(db.Boolean, nullable=False, default=False)
"""Whether the entries of this account need offset.""" """Whether the entries of this account need pay-off."""
created_at = db.Column(db.DateTime(timezone=True), nullable=False, created_at = db.Column(db.DateTime(timezone=True), nullable=False,
server_default=db.func.now()) server_default=db.func.now())
"""The time of creation.""" """The time of creation."""
@ -597,15 +597,15 @@ class JournalEntry(db.Model):
"""True for a debit entry, or False for a credit entry.""" """True for a debit entry, or False for a credit entry."""
no = db.Column(db.Integer, nullable=False) no = db.Column(db.Integer, nullable=False)
"""The entry number under the transaction and debit or credit.""" """The entry number under the transaction and debit or credit."""
offset_original_id = db.Column(db.Integer, pay_off_target_id = db.Column(db.Integer,
db.ForeignKey(id, onupdate="CASCADE"), db.ForeignKey(id, onupdate="CASCADE"),
nullable=True) nullable=True)
"""The ID of the original entry to offset.""" """The ID of the pay-off target entry."""
offset_original = db.relationship("JournalEntry", back_populates="offsets", pay_off_target = db.relationship("JournalEntry", back_populates="pay_off",
remote_side=id, passive_deletes=True) remote_side=id, passive_deletes=True)
"""The original entry to offset.""" """The pay-off target entry."""
offsets = db.relationship("JournalEntry", back_populates="offset_original") pay_off = db.relationship("JournalEntry", back_populates="pay_off_target")
"""The offset entries.""" """The pay-off entries."""
currency_code = db.Column(db.String, currency_code = db.Column(db.String,
db.ForeignKey(Currency.code, onupdate="CASCADE"), db.ForeignKey(Currency.code, onupdate="CASCADE"),
nullable=False) nullable=False)

View File

@ -29,7 +29,7 @@ def init_app(app: Flask, bp: Blueprint) -> None:
""" """
from .converters import PeriodConverter, IncomeExpensesAccountConverter from .converters import PeriodConverter, IncomeExpensesAccountConverter
app.url_map.converters["period"] = PeriodConverter app.url_map.converters["period"] = PeriodConverter
app.url_map.converters["ieAccount"] = IncomeExpensesAccountConverter app.url_map.converters["ioAccount"] = IncomeExpensesAccountConverter
from .views import bp as report_bp from .views import bp as report_bp
bp.register_blueprint(report_bp, url_prefix="/reports") bp.register_blueprint(report_bp, url_prefix="/reports")

View File

@ -23,8 +23,8 @@ from flask import abort
from werkzeug.routing import BaseConverter from werkzeug.routing import BaseConverter
from accounting.models import Account from accounting.models import Account
from .period import Period, get_period from .income_expense_account import IncomeExpensesAccount
from .utils.ie_account import IncomeExpensesAccount from .period import Period
class PeriodConverter(BaseConverter): class PeriodConverter(BaseConverter):
@ -38,7 +38,7 @@ class PeriodConverter(BaseConverter):
:return: The corresponding period. :return: The corresponding period.
""" """
try: try:
return get_period(value) return Period.get_instance(value)
except ValueError: except ValueError:
abort(404) abort(404)

View File

@ -19,8 +19,6 @@
""" """
import typing as t import typing as t
from flask import current_app
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Account from accounting.models import Account
@ -64,23 +62,3 @@ class IncomeExpensesAccount:
account.title = gettext("current assets and liabilities") account.title = gettext("current assets and liabilities")
account.str = account.title account.str = account.title
return account return account
def default_ie_account_code() -> str:
"""Returns the default account code for the income and expenses log.
:return: The default account code for the income and expenses log.
"""
with current_app.app_context():
return current_app.config.get("DEFAULT_IE_ACCOUNT", Account.CASH_CODE)
def default_ie_account() -> IncomeExpensesAccount:
"""Returns the default account for the income and expenses log.
:return: The default account for the income and expenses log.
"""
code: str = default_ie_account_code()
if code == IncomeExpensesAccount.CURRENT_AL_CODE:
return IncomeExpensesAccount.current_assets_and_liabilities()
return IncomeExpensesAccount(Account.find_by_code(code))

View File

@ -0,0 +1,633 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The date period.
This file is largely taken from the NanoParma ERP project, first written in
2021/9/16 by imacat (imacat@nanoparma.com).
"""
import calendar
import datetime
import re
import typing as t
from accounting.locale import gettext
class Period:
"""A date period."""
def __init__(self, start: datetime.date | None, end: datetime.date | None):
"""Constructs a new date period.
:param start: The start date, or None from the very beginning.
:param end: The end date, or None till no end.
"""
self.start: datetime.date | None = start
"""The start of the period."""
self.end: datetime.date | None = end
"""The end of the period."""
self.is_default: bool = False
"""Whether the is the default period."""
self.is_this_month: bool = False
"""Whether the period is this month."""
self.is_last_month: bool = False
"""Whether the period is last month."""
self.is_since_last_month: bool = False
"""Whether the period is since last month."""
self.is_this_year: bool = False
"""Whether the period is this year."""
self.is_last_year: bool = False
"""Whether the period is last year."""
self.is_today: bool = False
"""Whether the period is today."""
self.is_yesterday: bool = False
"""Whether the period is yesterday."""
self.is_all: bool = start is None and end is None
"""Whether the period is all time."""
self.spec: str = ""
"""The period specification."""
self.desc: str = ""
"""The text description."""
self.is_a_month: bool = False
"""Whether the period is a whole month."""
self.is_type_month: bool = False
"""Whether the period is for the month chooser."""
self.is_a_year: bool = False
"""Whether the period is a whole year."""
self.is_a_day: bool = False
"""Whether the period is a single day."""
self._set_properties()
def _set_properties(self) -> None:
"""Sets the following properties.
* self.spec
* self.desc
* self.is_a_month
* self.is_type_month
* self.is_a_year
* self.is_a_day
Override this method to set the properties in the subclasses.
:return: None.
"""
self.spec = PeriodSpecification(self).spec
self.desc = PeriodDescription(self).desc
if self.start is None or self.end is None:
return
self.is_a_month = self.start.day == 1 \
and self.end == _month_end(self.start)
self.is_type_month = self.is_a_month
self.is_a_year = self.start == datetime.date(self.start.year, 1, 1) \
and self.end == datetime.date(self.start.year, 12, 31)
self.is_a_day = self.start == self.end
@classmethod
def get_instance(cls, spec: str | None = None) -> t.Self:
"""Returns a period instance.
:param spec: The period specification, or omit for the default.
:return: The period instance.
:raise ValueError: When the period is invalid.
"""
if spec is None:
return ThisMonth()
named_periods: dict[str, t.Type[t.Callable[[], Period]]] = {
"this-month": lambda: ThisMonth(),
"last-month": lambda: LastMonth(),
"since-last-month": lambda: SinceLastMonth(),
"this-year": lambda: ThisYear(),
"last-year": lambda: LastYear(),
"today": lambda: Today(),
"yesterday": lambda: Yesterday(),
}
if spec in named_periods:
return named_periods[spec]()
start: datetime.date
end: datetime.date
start, end = _parse_period_spec(spec)
if start is not None and end is not None and start > end:
raise ValueError
return cls(start, end)
def is_year(self, year: int) -> bool:
"""Returns whether the period is the specific year period.
:param year: The year.
:return: True if the period is the year period, or False otherwise.
"""
if not self.is_a_year:
return False
return self.start.year == year
@property
def is_type_arbitrary(self) -> bool:
"""Returns whether this period is an arbitrary period.
:return: True if this is an arbitrary period, or False otherwise.
"""
return not self.is_type_month and not self.is_a_year \
and not self.is_a_day
@property
def before(self) -> t.Self | None:
"""Returns the period before this period.
:return: The period before this period.
"""
if self.start is None:
return None
return Period(None, self.start - datetime.timedelta(days=1))
class PeriodSpecification:
"""The period specification composer."""
def __init__(self, period: Period):
"""Constructs the period specification composer.
:param period: The period.
"""
self.__start: datetime.date = period.start
self.__end: datetime.date = period.end
self.spec: str = self.__get_spec()
def __get_spec(self) -> str:
"""Returns the period specification.
:return: The period specification.
"""
if self.__start is None and self.__end is None:
return "-"
if self.__end is None:
return self.__get_since_spec()
if self.__start is None:
return self.__get_until_spec()
try:
return self.__get_year_spec()
except ValueError:
pass
try:
return self.__get_month_spec()
except ValueError:
pass
return self.__get_day_spec()
def __get_since_spec(self) -> str:
"""Returns the period specification without the end day.
:return: The period specification without the end day
"""
if self.__start.month == 1 and self.__start.day == 1:
return self.__start.strftime("%Y-")
if self.__start.day == 1:
return self.__start.strftime("%Y-%m-")
return self.__start.strftime("%Y-%m-%d-")
def __get_until_spec(self) -> str:
"""Returns the period specification without the start day.
:return: The period specification without the start day
"""
if self.__end.month == 12 and self.__end.day == 31:
return self.__end.strftime("-%Y")
if (self.__end + datetime.timedelta(days=1)).day == 1:
return self.__end.strftime("-%Y-%m")
return self.__end.strftime("-%Y-%m-%d")
def __get_year_spec(self) -> str:
"""Returns the period specification as a year range.
:return: The period specification as a year range.
:raise ValueError: The period is not a year range.
"""
if self.__start.month != 1 or self.__start.day != 1 \
or self.__end.month != 12 or self.__end.day != 31:
raise ValueError
if self.__start.year == self.__end.year:
return "%04d" % self.__start.year
return "%04d-%04d" % (self.__start.year, self.__end.year)
def __get_month_spec(self) -> str:
"""Returns the period specification as a month range.
:return: The period specification as a month range.
:raise ValueError: The period is not a month range.
"""
if self.__start.day != 1 or self.__end != _month_end(self.__end):
raise ValueError
if self.__start.year == self.__end.year \
and self.__start.month == self.__end.month:
return "%04d-%02d" % (self.__start.year, self.__start.month)
return "%04d-%02d-%04d-%02d" % (
self.__start.year, self.__start.month,
self.__end.year, self.__end.month)
def __get_day_spec(self) -> str:
"""Returns the period specification as a day range.
:return: The period specification as a day range.
:raise ValueError: The period is a month or year range.
"""
if self.__start == self.__end:
return "%04d-%02d-%02d" % (
self.__start.year, self.__start.month, self.__start.day)
return "%04d-%02d-%02d-%04d-%02d-%02d" % (
self.__start.year, self.__start.month, self.__start.day,
self.__end.year, self.__end.month, self.__end.day)
class PeriodDescription:
"""The period description composer."""
def __init__(self, period: Period):
"""Constructs the period description composer.
:param period: The period.
"""
self.__start: datetime.date = period.start
self.__end: datetime.date = period.end
self.desc: str = self.__get_desc()
def __get_desc(self) -> str:
"""Returns the period description.
:return: The period description.
"""
if self.__start is None and self.__end is None:
return gettext("for all time")
if self.__start is None:
return self.__get_until_desc()
if self.__end is None:
return self.__get_since_desc()
try:
return self.__get_year_desc()
except ValueError:
pass
try:
return self.__get_month_desc()
except ValueError:
pass
return self.__get_day_desc()
def __get_since_desc(self) -> str:
"""Returns the description without the end day.
:return: The description without the end day.
"""
def get_start_desc() -> str:
"""Returns the description of the start day.
:return: The description of the start day.
"""
if self.__start.month == 1 and self.__start.day == 1:
return str(self.__start.year)
if self.__start.day == 1:
return self.__format_month(self.__start)
return self.__format_date(self.__start)
return gettext("since %(start)s", start=get_start_desc())
def __get_until_desc(self) -> str:
"""Returns the description without the start day.
:return: The description without the start day.
"""
def get_end_desc() -> str:
"""Returns the description of the end day.
:return: The description of the end day.
"""
if self.__end.month == 12 and self.__end.day == 31:
return str(self.__end.year)
if (self.__end + datetime.timedelta(days=1)).day == 1:
return self.__format_month(self.__end)
return self.__format_date(self.__end)
return gettext("until %(end)s", end=get_end_desc())
def __get_year_desc(self) -> str:
"""Returns the description as a year range.
:return: The description as a year range.
:raise ValueError: The period is not a year range.
"""
if self.__start.month != 1 or self.__start.day != 1 \
or self.__end.month != 12 or self.__end.day != 31:
raise ValueError
start: str = str(self.__start.year)
if self.__start.year == self.__end.year:
return self.__get_in_desc(start)
return self.__get_from_to_desc(start, str(self.__end.year))
def __get_month_desc(self) -> str:
"""Returns the description as a month range.
:return: The description as a month range.
:raise ValueError: The period is not a month range.
"""
if self.__start.day != 1 or self.__end != _month_end(self.__end):
raise ValueError
start: str = self.__format_month(self.__start)
if self.__start.year == self.__end.year \
and self.__start.month == self.__end.month:
return self.__get_in_desc(start)
if self.__start.year == self.__end.year:
return self.__get_from_to_desc(start, str(self.__end.month))
return self.__get_from_to_desc(start, self.__format_month(self.__end))
def __get_day_desc(self) -> str:
"""Returns the description as a day range.
:return: The description as a day range.
:raise ValueError: The period is a month or year range.
"""
start: str = self.__format_day(self.__start)
if self.__start == self.__end:
return self.__get_in_desc(start)
if self.__start.year == self.__end.year \
and self.__start.month == self.__end.month:
return self.__get_from_to_desc(start, str(self.__end.day))
if self.__start.year == self.__end.year:
end_month_day: str = f"{self.__end.month}/{self.__end.day}"
return self.__get_from_to_desc(start, end_month_day)
return self.__get_from_to_desc(start, self.__format_day(self.__end))
@staticmethod
def __format_date(date: datetime.date) -> str:
"""Formats a date.
:param date: The date.
:return: The formatted date.
"""
return f"{date.year}/{date.month}/{date.day}"
@staticmethod
def __format_month(month: datetime.date) -> str:
"""Formats a month.
:param month: The month.
:return: The formatted month.
"""
return f"{month.year}/{month.month}"
@staticmethod
def __format_day(day: datetime.date) -> str:
"""Formats a day.
:param day: The day.
:return: The formatted day.
"""
return f"{day.year}/{day.month}/{day.day}"
@staticmethod
def __get_in_desc(period: str) -> str:
"""Returns the description of a whole year, month, or day.
:param period: The time period.
:return: The description of a whole year, month, or day.
"""
return gettext("in %(period)s", period=period)
@staticmethod
def __get_from_to_desc(start: str, end: str) -> str:
"""Returns the description of a separated start and end.
:param start: The start.
:param end: The end.
:return: The description of the separated start and end.
"""
return gettext("in %(start)s-%(end)s", start=start, end=end)
class ThisMonth(Period):
"""The period of this month."""
def __init__(self):
today: datetime.date = datetime.date.today()
this_month_start: datetime.date \
= datetime.date(today.year, today.month, 1)
super().__init__(this_month_start, _month_end(today))
self.is_default = True
self.is_this_month = True
def _set_properties(self) -> None:
self.spec = "this-month"
self.desc = gettext("This month")
self.is_a_month = True
self.is_type_month = True
class LastMonth(Period):
"""The period of this month."""
def __init__(self):
today: datetime.date = datetime.date.today()
year: int = today.year
month: int = today.month - 1
if month < 1:
year = year - 1
month = 12
start: datetime.date = datetime.date(year, month, 1)
super().__init__(start, _month_end(start))
self.is_last_month = True
def _set_properties(self) -> None:
self.spec = "last-month"
self.desc = gettext("Last month")
self.is_a_month = True
self.is_type_month = True
class SinceLastMonth(Period):
"""The period of this month."""
def __init__(self):
today: datetime.date = datetime.date.today()
year: int = today.year
month: int = today.month - 1
if month < 1:
year = year - 1
month = 12
start: datetime.date = datetime.date(year, month, 1)
super().__init__(start, None)
self.is_since_last_month = True
def _set_properties(self) -> None:
self.spec = "since-last-month"
self.desc = gettext("Since last month")
self.is_type_month = True
class ThisYear(Period):
"""The period of this year."""
def __init__(self):
year: int = datetime.date.today().year
start: datetime.date = datetime.date(year, 1, 1)
end: datetime.date = datetime.date(year, 12, 31)
super().__init__(start, end)
self.is_this_year = True
def _set_properties(self) -> None:
self.spec = "this-year"
self.desc = gettext("This year")
self.is_a_year = True
class LastYear(Period):
"""The period of last year."""
def __init__(self):
year: int = datetime.date.today().year
start: datetime.date = datetime.date(year - 1, 1, 1)
end: datetime.date = datetime.date(year - 1, 12, 31)
super().__init__(start, end)
self.is_last_year = True
def _set_properties(self) -> None:
self.spec = "last-year"
self.desc = gettext("Last year")
self.is_a_year = True
class Today(Period):
"""The period of today."""
def __init__(self):
today: datetime.date = datetime.date.today()
super().__init__(today, today)
self.is_this_year = True
def _set_properties(self) -> None:
self.spec = "today"
self.desc = gettext("Today")
self.is_a_day = True
self.is_today = True
class Yesterday(Period):
"""The period of yesterday."""
def __init__(self):
yesterday: datetime.date \
= datetime.date.today() - datetime.timedelta(days=1)
super().__init__(yesterday, yesterday)
self.is_this_year = True
def _set_properties(self) -> None:
self.spec = "yesterday"
self.desc = gettext("Yesterday")
self.is_a_day = True
self.is_yesterday = True
class TemplatePeriod(Period):
"""The period template."""
def __init__(self):
super().__init__(None, None)
def _set_properties(self) -> None:
self.spec = "PERIOD"
class YearPeriod(Period):
"""A year period."""
def __init__(self, year: int):
"""Constructs a year period.
:param year: The year.
"""
start: datetime.date = datetime.date(year, 1, 1)
end: datetime.date = datetime.date(year, 12, 31)
super().__init__(start, end)
self.spec = str(year)
self.is_a_year = True
DATE_SPEC_RE: str = r"(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?"
"""The regular expression of a date specification."""
def _parse_period_spec(text: str) \
-> tuple[datetime.date | None, datetime.date | None]:
"""Parses the period specification.
:param text: The period specification.
:return: The start and end day of the period. The start and end day
may be None.
:raise ValueError: When the date is invalid.
"""
if text == "-":
return None, None
m = re.match(f"^{DATE_SPEC_RE}$", text)
if m is not None:
return __get_start(m[1], m[2], m[3]), \
__get_end(m[1], m[2], m[3])
m = re.match(f"^{DATE_SPEC_RE}-$", text)
if m is not None:
return __get_start(m[1], m[2], m[3]), None
m = re.match(f"-{DATE_SPEC_RE}$", text)
if m is not None:
return None, __get_end(m[1], m[2], m[3])
m = re.match(f"^{DATE_SPEC_RE}-{DATE_SPEC_RE}$", text)
if m is not None:
return __get_start(m[1], m[2], m[3]), \
__get_end(m[4], m[5], m[6])
raise ValueError
def __get_start(year: str, month: str | None, day: str | None)\
-> datetime.date:
"""Returns the start of the period from the date representation.
:param year: The year.
:param month: The month, if any.
:param day: The day, if any.
:return: The start of the period.
:raise ValueError: When the date is invalid.
"""
if day is not None:
return datetime.date(int(year), int(month), int(day))
if month is not None:
return datetime.date(int(year), int(month), 1)
return datetime.date(int(year), 1, 1)
def __get_end(year: str, month: str | None, day: str | None)\
-> datetime.date:
"""Returns the end of the period from the date representation.
:param year: The year.
:param month: The month, if any.
:param day: The day, if any.
:return: The end of the period.
:raise ValueError: When the date is invalid.
"""
if day is not None:
return datetime.date(int(year), int(month), int(day))
if month is not None:
year_n: int = int(year)
month_n: int = int(month)
day_n: int = calendar.monthrange(year_n, month_n)[1]
return datetime.date(year_n, month_n, day_n)
return datetime.date(int(year), 12, 31)
def _month_end(date: datetime.date) -> datetime.date:
"""Returns the end day of month for a date.
:param date: The date.
:return: The end day of the month of that day.
"""
day: int = calendar.monthrange(date.year, date.month)[1]
return datetime.date(date.year, date.month, day)

View File

@ -1,22 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The period utility.
"""
from .chooser import PeriodChooser
from .parser import get_period
from .period import Period

View File

@ -1,97 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The period chooser.
This file is largely taken from the NanoParma ERP project, first written in
2021/9/16 by imacat (imacat@nanoparma.com).
"""
import typing as t
from datetime import date
from accounting.models import Transaction
from .period import Period
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
LastYear, Today, Yesterday, AllTime, TemplatePeriod, YearPeriod
class PeriodChooser:
"""The period chooser."""
def __init__(self, get_url: t.Callable[[Period], str]):
"""Constructs a period chooser.
:param get_url: The callback to return the URL of the current report in
a period.
"""
self.__get_url: t.Callable[[Period], str] = get_url
"""The callback to return the URL of the current report in a period."""
# Shortcut periods
self.this_month_url: str = get_url(ThisMonth())
"""The URL for this month."""
self.last_month_url: str = get_url(LastMonth())
"""The URL for last month."""
self.since_last_month_url: str = get_url(SinceLastMonth())
"""The URL since last mint."""
self.this_year_url: str = get_url(ThisYear())
"""The URL for this year."""
self.last_year_url: str = get_url(LastYear())
"""The URL for last year."""
self.today_url: str = get_url(Today())
"""The URL for today."""
self.yesterday_url: str = get_url(Yesterday())
"""The URL for yesterday."""
self.all_url: str = get_url(AllTime())
"""The URL for all period."""
self.url_template: str = get_url(TemplatePeriod())
"""The URL template."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
start: date | None = None if first is None else first.date
# Attributes
self.data_start: date | None = start
"""The start of the data."""
self.has_data: bool = start is not None
"""Whether there is any data."""
self.has_last_month: bool = False
"""Where there is data in last month."""
self.has_last_year: bool = False
"""Whether there is data in last year."""
self.has_yesterday: bool = False
"""Whether there is data in yesterday."""
self.available_years: list[int] = []
"""The available years."""
if self.has_data:
today: date = date.today()
self.has_last_month = start < date(today.year, today.month, 1)
self.has_last_year = start.year < today.year
self.has_yesterday = start < today
if start.year < today.year - 1:
self.available_years \
= reversed(range(start.year, today.year - 1))
def year_url(self, year: int) -> str:
"""Returns the period URL of a year.
:param year: The year
:return: The period URL of the year.
"""
return self.__get_url(YearPeriod(year))

View File

@ -1,179 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The period description composer.
"""
from datetime import date, timedelta
from accounting.locale import gettext
def get_desc(start: date | None, end: date | None) -> str:
"""Returns the period description.
:param start: The start of the period.
:param end: The end of the period.
:return: The period description.
"""
if start is None and end is None:
return gettext("for all time")
if start is None:
return __get_until_desc(end)
if end is None:
return __get_since_desc(start)
try:
return __get_year_desc(start, end)
except ValueError:
pass
try:
return __get_month_desc(start, end)
except ValueError:
pass
return __get_day_desc(start, end)
def __get_since_desc(start: date) -> str:
"""Returns the description without the end day.
:param start: The start of the period.
:return: The description without the end day.
"""
def get_start_desc() -> str:
"""Returns the description of the start day.
:return: The description of the start day.
"""
if start.month == 1 and start.day == 1:
return str(start.year)
if start.day == 1:
return __format_month(start)
return __format_day(start)
return gettext("since %(start)s", start=get_start_desc())
def __get_until_desc(end: date) -> str:
"""Returns the description without the start day.
:param end: The end of the period.
:return: The description without the start day.
"""
def get_end_desc() -> str:
"""Returns the description of the end day.
:return: The description of the end day.
"""
if end.month == 12 and end.day == 31:
return str(end.year)
if (end + timedelta(days=1)).day == 1:
return __format_month(end)
return __format_day(end)
return gettext("until %(end)s", end=get_end_desc())
def __get_year_desc(start: date, end: date) -> str:
"""Returns the description as a year range.
:param start: The start of the period.
:param end: The end of the period.
:return: The description as a year range.
:raise ValueError: The period is not a year range.
"""
if start.month != 1 or start.day != 1 \
or end.month != 12 or end.day != 31:
raise ValueError
start_text: str = str(start.year)
if start.year == end.year:
return __get_in_desc(start_text)
return __get_from_to_desc(start_text, str(end.year))
def __get_month_desc(start: date, end: date) -> str:
"""Returns the description as a month range.
:param start: The start of the period.
:param end: The end of the period.
:return: The description as a month range.
:raise ValueError: The period is not a month range.
"""
if start.day != 1 or (end + timedelta(days=1)).day != 1:
raise ValueError
start_text: str = __format_month(start)
if start.year == end.year and start.month == end.month:
return __get_in_desc(start_text)
if start.year == end.year:
return __get_from_to_desc(start_text, str(end.month))
return __get_from_to_desc(start_text, __format_month(end))
def __get_day_desc(start: date, end: date) -> str:
"""Returns the description as a day range.
:param start: The start of the period.
:param end: The end of the period.
:return: The description as a day range.
:raise ValueError: The period is a month or year range.
"""
start_text: str = __format_day(start)
if start == end:
return __get_in_desc(start_text)
if start.year == end.year and start.month == end.month:
return __get_from_to_desc(start_text, str(end.day))
if start.year == end.year:
end_month_day: str = f"{end.month}/{end.day}"
return __get_from_to_desc(start_text, end_month_day)
return __get_from_to_desc(start_text, __format_day(end))
def __format_month(month: date) -> str:
"""Formats a month.
:param month: The month.
:return: The formatted month.
"""
return f"{month.year}/{month.month}"
def __format_day(day: date) -> str:
"""Formats a day.
:param day: The day.
:return: The formatted day.
"""
return f"{day.year}/{day.month}/{day.day}"
def __get_in_desc(period: str) -> str:
"""Returns the description of a whole year, month, or day.
:param period: The time period.
:return: The description of a whole year, month, or day.
"""
return gettext("in %(period)s", period=period)
def __get_from_to_desc(start: str, end: str) -> str:
"""Returns the description of a separated start and end.
:param start: The start.
:param end: The end.
:return: The description of the separated start and end.
"""
return gettext("in %(start)s-%(end)s", start=start, end=end)

View File

@ -1,31 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The utility to return the end of a month.
"""
import calendar
from datetime import date
def month_end(day: date) -> date:
"""Returns the end day of month for a date.
:param day: The date.
:return: The end day of the month of that day.
"""
last_day: int = calendar.monthrange(day.year, day.month)[1]
return date(day.year, day.month, last_day)

View File

@ -1,119 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The period specification parser.
"""
import calendar
import re
import typing as t
from datetime import date
from .period import Period
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
LastYear, Today, Yesterday, AllTime
DATE_SPEC_RE: str = r"(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?"
"""The regular expression of a date specification."""
def get_period(spec: str | None = None) -> Period:
"""Returns a period instance.
:param spec: The period specification, or omit for the default.
:return: The period instance.
:raise ValueError: When the period specification is invalid.
"""
if spec is None:
return ThisMonth()
named_periods: dict[str, t.Type[t.Callable[[], Period]]] = {
"this-month": lambda: ThisMonth(),
"last-month": lambda: LastMonth(),
"since-last-month": lambda: SinceLastMonth(),
"this-year": lambda: ThisYear(),
"last-year": lambda: LastYear(),
"today": lambda: Today(),
"yesterday": lambda: Yesterday(),
"all-time": lambda: AllTime(),
}
if spec in named_periods:
return named_periods[spec]()
start, end = __parse_spec(spec)
if start is not None and end is not None and start > end:
raise ValueError
return Period(start, end)
def __parse_spec(text: str) -> tuple[date | None, date | None]:
"""Parses the period specification.
:param text: The period specification.
:return: The start and end day of the period. The start and end day
may be None.
:raise ValueError: When the date is invalid.
"""
if text == "-":
return None, None
m = re.match(f"^{DATE_SPEC_RE}$", text)
if m is not None:
return __get_start(m[1], m[2], m[3]), \
__get_end(m[1], m[2], m[3])
m = re.match(f"^{DATE_SPEC_RE}-$", text)
if m is not None:
return __get_start(m[1], m[2], m[3]), None
m = re.match(f"-{DATE_SPEC_RE}$", text)
if m is not None:
return None, __get_end(m[1], m[2], m[3])
m = re.match(f"^{DATE_SPEC_RE}-{DATE_SPEC_RE}$", text)
if m is not None:
return __get_start(m[1], m[2], m[3]), \
__get_end(m[4], m[5], m[6])
raise ValueError
def __get_start(year: str, month: str | None, day: str | None) -> date:
"""Returns the start of the period from the date representation.
:param year: The year.
:param month: The month, if any.
:param day: The day, if any.
:return: The start of the period.
:raise ValueError: When the date is invalid.
"""
if day is not None:
return date(int(year), int(month), int(day))
if month is not None:
return date(int(year), int(month), 1)
return date(int(year), 1, 1)
def __get_end(year: str, month: str | None, day: str | None) -> date:
"""Returns the end of the period from the date representation.
:param year: The year.
:param month: The month, if any.
:param day: The day, if any.
:return: The end of the period.
:raise ValueError: When the date is invalid.
"""
if day is not None:
return date(int(year), int(month), int(day))
if month is not None:
year_n: int = int(year)
month_n: int = int(month)
day_n: int = calendar.monthrange(year_n, month_n)[1]
return date(year_n, month_n, day_n)
return date(int(year), 12, 31)

View File

@ -1,129 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The date period.
This file is largely taken from the NanoParma ERP project, first written in
2021/9/16 by imacat (imacat@nanoparma.com).
"""
import typing as t
from datetime import date, timedelta
from .description import get_desc
from .month_end import month_end
from .specification import get_spec
class Period:
"""A date period."""
def __init__(self, start: date | None, end: date | None):
"""Constructs a new date period.
:param start: The start date, or None from the very beginning.
:param end: The end date, or None till no end.
"""
self.start: date | None = start
"""The start of the period."""
self.end: date | None = end
"""The end of the period."""
self.is_default: bool = False
"""Whether the is the default period."""
self.is_this_month: bool = False
"""Whether the period is this month."""
self.is_last_month: bool = False
"""Whether the period is last month."""
self.is_since_last_month: bool = False
"""Whether the period is since last month."""
self.is_this_year: bool = False
"""Whether the period is this year."""
self.is_last_year: bool = False
"""Whether the period is last year."""
self.is_today: bool = False
"""Whether the period is today."""
self.is_yesterday: bool = False
"""Whether the period is yesterday."""
self.is_all: bool = start is None and end is None
"""Whether the period is all time."""
self.spec: str = ""
"""The period specification."""
self.desc: str = ""
"""The text description."""
self.is_a_month: bool = False
"""Whether the period is a whole month."""
self.is_type_month: bool = False
"""Whether the period is for the month chooser."""
self.is_a_year: bool = False
"""Whether the period is a whole year."""
self.is_a_day: bool = False
"""Whether the period is a single day."""
self._set_properties()
def _set_properties(self) -> None:
"""Sets the following properties.
* self.spec
* self.desc
* self.is_a_month
* self.is_type_month
* self.is_a_year
* self.is_a_day
Override this method to set the properties in the subclasses, to skip
the calculation.
:return: None.
"""
self.spec = get_spec(self.start, self.end)
self.desc = get_desc(self.start, self.end)
if self.start is None or self.end is None:
return
self.is_a_month = self.start.day == 1 \
and self.end == month_end(self.start)
self.is_type_month = self.is_a_month
self.is_a_year = self.start == date(self.start.year, 1, 1) \
and self.end == date(self.start.year, 12, 31)
self.is_a_day = self.start == self.end
def is_year(self, year: int) -> bool:
"""Returns whether the period is the specific year period.
:param year: The year.
:return: True if the period is the year period, or False otherwise.
"""
if not self.is_a_year:
return False
return self.start.year == year
@property
def is_type_arbitrary(self) -> bool:
"""Returns whether this period is an arbitrary period.
:return: True if this is an arbitrary period, or False otherwise.
"""
return not self.is_type_month and not self.is_a_year \
and not self.is_a_day
@property
def before(self) -> t.Self | None:
"""Returns the period before this period.
:return: The period before this period.
"""
if self.start is None:
return None
return Period(None, self.start - timedelta(days=1))

View File

@ -1,168 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The named shortcut periods.
"""
from datetime import date, timedelta
from accounting.locale import gettext
from .month_end import month_end
from .period import Period
class ThisMonth(Period):
"""The period of this month."""
def __init__(self):
today: date = date.today()
this_month_start: date = date(today.year, today.month, 1)
super().__init__(this_month_start, month_end(today))
self.is_default = True
self.is_this_month = True
def _set_properties(self) -> None:
self.spec = "this-month"
self.desc = gettext("This month")
self.is_a_month = True
self.is_type_month = True
class LastMonth(Period):
"""The period of this month."""
def __init__(self):
today: date = date.today()
year: int = today.year
month: int = today.month - 1
if month < 1:
year = year - 1
month = 12
start: date = date(year, month, 1)
super().__init__(start, month_end(start))
self.is_last_month = True
def _set_properties(self) -> None:
self.spec = "last-month"
self.desc = gettext("Last month")
self.is_a_month = True
self.is_type_month = True
class SinceLastMonth(Period):
"""The period of this month."""
def __init__(self):
today: date = date.today()
year: int = today.year
month: int = today.month - 1
if month < 1:
year = year - 1
month = 12
start: date = date(year, month, 1)
super().__init__(start, None)
self.is_since_last_month = True
def _set_properties(self) -> None:
self.spec = "since-last-month"
self.desc = gettext("Since last month")
self.is_type_month = True
class ThisYear(Period):
"""The period of this year."""
def __init__(self):
year: int = date.today().year
start: date = date(year, 1, 1)
end: date = date(year, 12, 31)
super().__init__(start, end)
self.is_this_year = True
def _set_properties(self) -> None:
self.spec = "this-year"
self.desc = gettext("This year")
self.is_a_year = True
class LastYear(Period):
"""The period of last year."""
def __init__(self):
year: int = date.today().year
start: date = date(year - 1, 1, 1)
end: date = date(year - 1, 12, 31)
super().__init__(start, end)
self.is_last_year = True
def _set_properties(self) -> None:
self.spec = "last-year"
self.desc = gettext("Last year")
self.is_a_year = True
class Today(Period):
"""The period of today."""
def __init__(self):
today: date = date.today()
super().__init__(today, today)
self.is_today = True
def _set_properties(self) -> None:
self.spec = "today"
self.desc = gettext("Today")
self.is_a_day = True
class Yesterday(Period):
"""The period of yesterday."""
def __init__(self):
yesterday: date = date.today() - timedelta(days=1)
super().__init__(yesterday, yesterday)
self.is_yesterday = True
def _set_properties(self) -> None:
self.spec = "yesterday"
self.desc = gettext("Yesterday")
self.is_a_day = True
class AllTime(Period):
"""The period of all time."""
def __init__(self):
super().__init__(None, None)
self.is_all = True
def _set_properties(self) -> None:
self.spec = "all-time"
self.desc = gettext("All")
class TemplatePeriod(Period):
"""The period template."""
def __init__(self):
super().__init__(None, None)
def _set_properties(self) -> None:
self.spec = "PERIOD"
class YearPeriod(Period):
"""A year period."""
def __init__(self, year: int):
"""Constructs a year period.
:param year: The year.
"""
start: date = date(year, 1, 1)
end: date = date(year, 12, 31)
super().__init__(start, end)

View File

@ -1,120 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The period specification composer.
"""
from datetime import date, timedelta
def get_spec(start: date | None, end: date | None) -> str:
"""Returns the period specification.
:param start: The start of the period.
:param end: The end of the period.
:return: The period specification.
"""
if start is None and end is None:
return "-"
if end is None:
return __get_since_spec(start)
if start is None:
return __get_until_spec(end)
try:
return __get_year_spec(start, end)
except ValueError:
pass
try:
return __get_month_spec(start, end)
except ValueError:
pass
return __get_day_spec(start, end)
def __get_since_spec(start: date) -> str:
"""Returns the period specification without the end day.
:param start: The start of the period.
:return: The period specification without the end day
"""
if start.month == 1 and start.day == 1:
return start.strftime("%Y-")
if start.day == 1:
return start.strftime("%Y-%m-")
return start.strftime("%Y-%m-%d-")
def __get_until_spec(end: date) -> str:
"""Returns the period specification without the start day.
:param end: The end of the period.
:return: The period specification without the start day
"""
if end.month == 12 and end.day == 31:
return end.strftime("-%Y")
if (end + timedelta(days=1)).day == 1:
return end.strftime("-%Y-%m")
return end.strftime("-%Y-%m-%d")
def __get_year_spec(start: date, end: date) -> str:
"""Returns the period specification as a year range.
:param start: The start of the period.
:param end: The end of the period.
:return: The period specification as a year range.
:raise ValueError: The period is not a year range.
"""
if start.month != 1 or start.day != 1 \
or end.month != 12 or end.day != 31:
raise ValueError
start_spec: str = start.strftime("%Y")
if start.year == end.year:
return start_spec
end_spec: str = end.strftime("%Y")
return f"{start_spec}-{end_spec}"
def __get_month_spec(start: date, end: date) -> str:
"""Returns the period specification as a month range.
:param start: The start of the period.
:param end: The end of the period.
:return: The period specification as a month range.
:raise ValueError: The period is not a month range.
"""
if start.day != 1 or (end + timedelta(days=1)).day != 1:
raise ValueError
start_spec: str = start.strftime("%Y-%m")
if start.year == end.year and start.month == end.month:
return start_spec
end_spec: str = end.strftime("%Y-%m")
return f"{start_spec}-{end_spec}"
def __get_day_spec(start: date, end: date) -> str:
"""Returns the period specification as a day range.
:param start: The start of the period.
:param end: The end of the period.
:return: The period specification as a day range.
:raise ValueError: The period is a month or year range.
"""
start_spec: str = start.strftime("%Y-%m-%d")
if start == end:
return start_spec
end_spec: str = end.strftime("%Y-%m-%d")
return f"{start_spec}-{end_spec}"

View File

@ -26,16 +26,15 @@ from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, BaseAccount, Account, Transaction, \ from accounting.models import Currency, BaseAccount, Account, Transaction, \
JournalEntry JournalEntry
from accounting.report.period import Period, PeriodChooser from accounting.report.period import Period
from accounting.report.utils.base_page_params import BasePageParams from .utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport from .utils.base_report import BaseReport
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \ from .utils.csv_export import BaseCSVRow, csv_download, period_spec
period_spec from .utils.option_link import OptionLink
from accounting.report.utils.option_link import OptionLink from .utils.period_choosers import BalanceSheetPeriodChooser
from accounting.report.utils.report_chooser import ReportChooser from .utils.report_chooser import ReportChooser
from accounting.report.utils.report_type import ReportType from .utils.report_type import ReportType
from accounting.report.utils.urls import ledger_url, balance_sheet_url, \ from .utils.urls import ledger_url, balance_sheet_url, income_statement_url
income_statement_url
class ReportAccount: class ReportAccount:
@ -318,8 +317,8 @@ class PageParams(BasePageParams):
"""The liabilities.""" """The liabilities."""
self.owner_s_equity: Section = owner_s_equity self.owner_s_equity: Section = owner_s_equity
"""The owner's equity.""" """The owner's equity."""
self.period_chooser: PeriodChooser = PeriodChooser( self.period_chooser: BalanceSheetPeriodChooser \
lambda x: balance_sheet_url(currency, x)) = BalanceSheetPeriodChooser(currency)
"""The period chooser.""" """The period chooser."""
@property @property

View File

@ -27,17 +27,17 @@ from sqlalchemy.orm import selectinload
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.period import Period, PeriodChooser from accounting.report.income_expense_account import IncomeExpensesAccount
from accounting.report.utils.base_page_params import BasePageParams from accounting.report.period import Period
from accounting.report.utils.base_report import BaseReport
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
period_spec
from accounting.report.utils.ie_account import IncomeExpensesAccount
from accounting.report.utils.option_link import OptionLink
from accounting.report.utils.report_chooser import ReportChooser
from accounting.report.utils.report_type import ReportType
from accounting.report.utils.urls import income_expenses_url
from accounting.utils.pagination import Pagination from accounting.utils.pagination import Pagination
from .utils.base_page_params import BasePageParams
from .utils.base_report import BaseReport
from .utils.csv_export import BaseCSVRow, csv_download, period_spec
from .utils.urls import income_expenses_url
from .utils.option_link import OptionLink
from .utils.period_choosers import IncomeExpensesPeriodChooser
from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType
class ReportEntry: class ReportEntry:
@ -289,8 +289,8 @@ class PageParams(BasePageParams):
"""The report entries.""" """The report entries."""
self.total: ReportEntry | None = total self.total: ReportEntry | None = total
"""The total entry.""" """The total entry."""
self.period_chooser: PeriodChooser = PeriodChooser( self.period_chooser: IncomeExpensesPeriodChooser \
lambda x: income_expenses_url(currency, account, x)) = IncomeExpensesPeriodChooser(currency, account)
"""The period chooser.""" """The period chooser."""
@property @property

View File

@ -26,15 +26,15 @@ from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, BaseAccount, Account, Transaction, \ from accounting.models import Currency, BaseAccount, Account, Transaction, \
JournalEntry JournalEntry
from accounting.report.period import Period, PeriodChooser from accounting.report.period import Period
from accounting.report.utils.base_page_params import BasePageParams from .utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport from .utils.base_report import BaseReport
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \ from .utils.csv_export import BaseCSVRow, csv_download, period_spec
period_spec from .utils.urls import ledger_url, income_statement_url
from accounting.report.utils.option_link import OptionLink from .utils.option_link import OptionLink
from accounting.report.utils.report_chooser import ReportChooser from .utils.period_choosers import IncomeStatementPeriodChooser
from accounting.report.utils.report_type import ReportType from .utils.report_chooser import ReportChooser
from accounting.report.utils.urls import ledger_url, income_statement_url from .utils.report_type import ReportType
class ReportAccount: class ReportAccount:
@ -159,9 +159,8 @@ class PageParams(BasePageParams):
self.__has_data: bool = has_data self.__has_data: bool = has_data
"""True if there is any data, or False otherwise.""" """True if there is any data, or False otherwise."""
self.sections: list[Section] = sections self.sections: list[Section] = sections
"""The sections in the income statement.""" self.period_chooser: IncomeStatementPeriodChooser \
self.period_chooser: PeriodChooser = PeriodChooser( = IncomeStatementPeriodChooser(currency)
lambda x: income_statement_url(currency, x))
"""The period chooser.""" """The period chooser."""
@property @property

View File

@ -26,15 +26,14 @@ from sqlalchemy.orm import selectinload
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.period import Period, PeriodChooser from accounting.report.period import Period
from accounting.report.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
period_spec
from accounting.report.utils.report_chooser import ReportChooser
from accounting.report.utils.report_type import ReportType
from accounting.report.utils.urls import journal_url
from accounting.utils.pagination import Pagination from accounting.utils.pagination import Pagination
from .utils.base_page_params import BasePageParams
from .utils.base_report import BaseReport
from .utils.csv_export import BaseCSVRow, csv_download, period_spec
from .utils.period_choosers import JournalPeriodChooser
from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType
class ReportEntry: class ReportEntry:
@ -123,8 +122,8 @@ class PageParams(BasePageParams):
"""The pagination.""" """The pagination."""
self.entries: list[JournalEntry] = entries self.entries: list[JournalEntry] = entries
"""The entries.""" """The entries."""
self.period_chooser: PeriodChooser = PeriodChooser( self.period_chooser: JournalPeriodChooser \
lambda x: journal_url(x)) = JournalPeriodChooser()
"""The period chooser.""" """The period chooser."""
@property @property

View File

@ -27,16 +27,16 @@ from sqlalchemy.orm import selectinload
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.period import Period, PeriodChooser from accounting.report.period import Period
from accounting.report.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
period_spec
from accounting.report.utils.option_link import OptionLink
from accounting.report.utils.report_chooser import ReportChooser
from accounting.report.utils.report_type import ReportType
from accounting.report.utils.urls import ledger_url
from accounting.utils.pagination import Pagination from accounting.utils.pagination import Pagination
from .utils.base_page_params import BasePageParams
from .utils.base_report import BaseReport
from .utils.csv_export import BaseCSVRow, csv_download, period_spec
from .utils.urls import ledger_url
from .utils.option_link import OptionLink
from .utils.period_choosers import LedgerPeriodChooser
from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType
class ReportEntry: class ReportEntry:
@ -110,8 +110,6 @@ class EntryCollector:
""" """
if self.__period.start is None: if self.__period.start is None:
return None return None
if self.__account.base_code[0] not in {"1", "2", "3"}:
return None
balance_func: sa.Function = sa.func.sum(sa.case( balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntry.is_debit, JournalEntry.amount), (JournalEntry.is_debit, JournalEntry.amount),
else_=-JournalEntry.amount)) else_=-JournalEntry.amount))
@ -264,8 +262,8 @@ class PageParams(BasePageParams):
"""The entries.""" """The entries."""
self.total: ReportEntry | None = total self.total: ReportEntry | None = total
"""The total entry.""" """The total entry."""
self.period_chooser: PeriodChooser = PeriodChooser( self.period_chooser: LedgerPeriodChooser \
lambda x: ledger_url(currency, account, x)) = LedgerPeriodChooser(currency, account)
"""The period chooser.""" """The period chooser."""
@property @property

View File

@ -27,14 +27,14 @@ from sqlalchemy.orm import selectinload
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \ from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
Transaction, JournalEntry Transaction, JournalEntry
from accounting.report.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport
from accounting.report.utils.csv_export import csv_download
from accounting.report.utils.report_chooser import ReportChooser
from accounting.report.utils.report_type import ReportType
from accounting.utils.pagination import Pagination from accounting.utils.pagination import Pagination
from accounting.utils.query import parse_query_keywords from accounting.utils.query import parse_query_keywords
from .journal import get_csv_rows from .journal import get_csv_rows
from .utils.base_page_params import BasePageParams
from .utils.base_report import BaseReport
from .utils.csv_export import csv_download
from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType
class EntryCollector: class EntryCollector:
@ -91,8 +91,8 @@ class EntryCollector:
Account.title_l10n.contains(k), Account.title_l10n.contains(k),
code.contains(k), code.contains(k),
Account.id.in_(select_l10n)] Account.id.in_(select_l10n)]
if k in gettext("Need offset"): if k in gettext("Pay-off needed"):
conditions.append(Account.is_offset_needed) conditions.append(Account.is_pay_off_needed)
return sa.select(Account.id).filter(sa.or_(*conditions)) return sa.select(Account.id).filter(sa.or_(*conditions))
@staticmethod @staticmethod

View File

@ -25,15 +25,15 @@ from flask import Response, render_template
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.period import Period, PeriodChooser from accounting.report.period import Period
from accounting.report.utils.base_page_params import BasePageParams from .utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport from .utils.base_report import BaseReport
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \ from .utils.csv_export import BaseCSVRow, csv_download, period_spec
period_spec from .utils.urls import ledger_url, trial_balance_url
from accounting.report.utils.option_link import OptionLink from .utils.option_link import OptionLink
from accounting.report.utils.report_chooser import ReportChooser from .utils.period_choosers import TrialBalancePeriodChooser
from accounting.report.utils.report_type import ReportType from .utils.report_chooser import ReportChooser
from accounting.report.utils.urls import ledger_url, trial_balance_url from .utils.report_type import ReportType
class ReportAccount: class ReportAccount:
@ -121,8 +121,8 @@ class PageParams(BasePageParams):
"""The accounts in the trial balance.""" """The accounts in the trial balance."""
self.total: Total = total self.total: Total = total
"""The total of the trial balance.""" """The total of the trial balance."""
self.period_chooser: PeriodChooser = PeriodChooser( self.period_chooser: TrialBalancePeriodChooser \
lambda x: trial_balance_url(currency, x)) = TrialBalancePeriodChooser(currency)
"""The period chooser.""" """The period chooser."""
@property @property

View File

@ -14,6 +14,6 @@
# 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 utilities for the reports. """The utilities to generate reports.
""" """

View File

@ -0,0 +1,194 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The period choosers.
This file is largely taken from the NanoParma ERP project, first written in
2021/9/16 by imacat (imacat@nanoparma.com).
"""
from abc import ABC, abstractmethod
from datetime import date
from accounting.models import Currency, Account, Transaction
from accounting.report.income_expense_account import IncomeExpensesAccount
from accounting.report.period import YearPeriod, Period, ThisMonth, \
LastMonth, SinceLastMonth, ThisYear, LastYear, Today, Yesterday, \
TemplatePeriod
from .urls import journal_url, ledger_url, income_expenses_url, \
trial_balance_url, income_statement_url, balance_sheet_url
class PeriodChooser(ABC):
"""The period chooser."""
def __init__(self, start: date | None):
"""Constructs a period chooser.
:param start: The start of the period.
"""
# Shortcut periods
self.this_month_url: str = self._url_for(ThisMonth())
"""The URL for this month."""
self.last_month_url: str = self._url_for(LastMonth())
"""The URL for last month."""
self.since_last_month_url: str = self._url_for(SinceLastMonth())
"""The URL since last mint."""
self.this_year_url: str = self._url_for(ThisYear())
"""The URL for this year."""
self.last_year_url: str = self._url_for(LastYear())
"""The URL for last year."""
self.today_url: str = self._url_for(Today())
"""The URL for today."""
self.yesterday_url: str = self._url_for(Yesterday())
"""The URL for yesterday."""
self.all_url: str = self._url_for(Period(None, None))
"""The URL for all period."""
self.url_template: str = self._url_for(TemplatePeriod())
"""The URL template."""
# Attributes
self.data_start: date | None = start
"""The start of the data."""
self.has_data: bool = start is not None
"""Whether there is any data."""
self.has_last_month: bool = False
"""Where there is data in last month."""
self.has_last_year: bool = False
"""Whether there is data in last year."""
self.has_yesterday: bool = False
"""Whether there is data in yesterday."""
self.available_years: list[int] = []
"""The available years."""
if self.has_data is not None:
today: date = date.today()
self.has_last_month = start < date(today.year, today.month, 1)
self.has_last_year = start.year < today.year
self.has_yesterday = start < today
if start.year < today.year - 1:
self.available_years \
= reversed(range(start.year, today.year - 1))
@abstractmethod
def _url_for(self, period: Period) -> str:
"""Returns the URL for a period.
:param period: The period.
:return: The URL for the period.
"""
pass
def year_url(self, year: int) -> str:
"""Returns the period URL of a year.
:param year: The year
:return: The period URL of the year.
"""
return self._url_for(YearPeriod(year))
class JournalPeriodChooser(PeriodChooser):
"""The journal period chooser."""
def __init__(self):
"""Constructs the journal period chooser."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
return journal_url(period)
class LedgerPeriodChooser(PeriodChooser):
"""The ledger period chooser."""
def __init__(self, currency: Currency, account: Account):
"""Constructs the ledger period chooser."""
self.currency: Currency = currency
"""The currency."""
self.account: Account = account
"""The account."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
return ledger_url(self.currency, self.account, period)
class IncomeExpensesPeriodChooser(PeriodChooser):
"""The income and expenses log period chooser."""
def __init__(self, currency: Currency, account: IncomeExpensesAccount):
"""Constructs the income and expenses log period chooser."""
self.currency: Currency = currency
"""The currency."""
self.account: IncomeExpensesAccount = account
"""The account."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
return income_expenses_url(self.currency, self.account, period)
class TrialBalancePeriodChooser(PeriodChooser):
"""The trial balance period chooser."""
def __init__(self, currency: Currency):
"""Constructs the trial balance period chooser."""
self.currency: Currency = currency
"""The currency."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
return trial_balance_url(self.currency, period)
class IncomeStatementPeriodChooser(PeriodChooser):
"""The income statement period chooser."""
def __init__(self, currency: Currency):
"""Constructs the income statement period chooser."""
self.currency: Currency = currency
"""The currency."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
return income_statement_url(self.currency, period)
class BalanceSheetPeriodChooser(PeriodChooser):
"""The balance sheet period chooser."""
def __init__(self, currency: Currency):
"""Constructs the balance sheet period chooser."""
self.currency: Currency = currency
"""The currency."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
return balance_sheet_url(self.currency, period)

View File

@ -28,9 +28,9 @@ from flask_babel import LazyString
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account from accounting.models import Currency, Account
from accounting.report.period import Period, get_period from accounting.report.income_expense_account import IncomeExpensesAccount
from accounting.report.period import Period
from accounting.template_globals import default_currency_code from accounting.template_globals import default_currency_code
from .ie_account import IncomeExpensesAccount
from .option_link import OptionLink from .option_link import OptionLink
from .report_type import ReportType from .report_type import ReportType
from .urls import journal_url, ledger_url, income_expenses_url, \ from .urls import journal_url, ledger_url, income_expenses_url, \
@ -53,7 +53,8 @@ class ReportChooser:
""" """
self.__active_report: ReportType = active_report self.__active_report: ReportType = active_report
"""The currently active report.""" """The currently active report."""
self.__period: Period = get_period() if period is None else period self.__period: Period = Period.get_instance() if period is None \
else period
"""The period.""" """The period."""
self.__currency: Currency = db.session.get( self.__currency: Currency = db.session.get(
Currency, default_currency_code()) \ Currency, default_currency_code()) \

View File

@ -35,4 +35,4 @@ class ReportType(Enum):
BALANCE_SHEET: str = "balance-sheet" BALANCE_SHEET: str = "balance-sheet"
"""The balance sheet.""" """The balance sheet."""
SEARCH: str = "search" SEARCH: str = "search"
"""The search.""" """The balance sheet."""

View File

@ -20,9 +20,8 @@
from flask import url_for from flask import url_for
from accounting.models import Currency, Account from accounting.models import Currency, Account
from accounting.report.income_expense_account import IncomeExpensesAccount
from accounting.report.period import Period from accounting.report.period import Period
from accounting.template_globals import default_currency_code
from .ie_account import IncomeExpensesAccount, default_ie_account_code
def journal_url(period: Period) \ def journal_url(period: Period) \
@ -63,10 +62,6 @@ def income_expenses_url(currency: Currency, account: IncomeExpensesAccount,
:param period: The period. :param period: The period.
:return: The URL of the income and expenses log. :return: The URL of the income and expenses log.
""" """
if currency.code == default_currency_code() \
and account.code == default_ie_account_code() \
and period.is_default:
return url_for("accounting.report.default")
if period.is_default: if period.is_default:
return url_for("accounting.report.income-expenses-default", return url_for("accounting.report.income-expenses-default",
currency=currency, account=account) currency=currency, account=account)

View File

@ -19,56 +19,41 @@
""" """
from flask import Blueprint, request, Response from flask import Blueprint, request, Response
from accounting import db
from accounting.models import Currency, Account from accounting.models import Currency, Account
from accounting.report.period import Period, get_period
from accounting.template_globals import default_currency_code
from accounting.utils.permission import has_permission, can_view from accounting.utils.permission import has_permission, can_view
from .income_expense_account import IncomeExpensesAccount
from .period import Period
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \ from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
IncomeStatement, BalanceSheet, Search IncomeStatement, BalanceSheet, Search
from .template_filters import format_amount from .template_filters import format_amount
from .utils.ie_account import IncomeExpensesAccount, default_ie_account
bp: Blueprint = Blueprint("report", __name__) bp: Blueprint = Blueprint("report", __name__)
"""The view blueprint for the reports.""" """The view blueprint for the reports."""
bp.add_app_template_filter(format_amount, "accounting_report_format_amount") bp.add_app_template_filter(format_amount, "accounting_report_format_amount")
@bp.get("", endpoint="default")
@has_permission(can_view)
def get_default_report() -> str | Response:
"""Returns the income and expenses log in the default period.
:return: The income and expenses log in the default period.
"""
return __get_income_expenses(
db.session.get(Currency, default_currency_code()),
default_ie_account(),
get_period())
@bp.get("journal", endpoint="journal-default") @bp.get("journal", endpoint="journal-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_journal() -> str | Response: def get_default_journal_list() -> str | Response:
"""Returns the journal in the default period. """Returns the journal in the default period.
:return: The journal in the default period. :return: The journal in the default period.
""" """
return __get_journal(get_period()) return __get_journal_list(Period.get_instance())
@bp.get("journal/<period:period>", endpoint="journal") @bp.get("journal/<period:period>", endpoint="journal")
@has_permission(can_view) @has_permission(can_view)
def get_journal(period: Period) -> str | Response: def get_journal_list(period: Period) -> str | Response:
"""Returns the journal. """Returns the journal.
:param period: The period. :param period: The period.
:return: The journal in the period. :return: The journal in the period.
""" """
return __get_journal(period) return __get_journal_list(period)
def __get_journal(period: Period) -> str | Response: def __get_journal_list(period: Period) -> str | Response:
"""Returns the journal. """Returns the journal.
:param period: The period. :param period: The period.
@ -83,20 +68,21 @@ def __get_journal(period: Period) -> str | Response:
@bp.get("ledger/<currency:currency>/<account:account>", @bp.get("ledger/<currency:currency>/<account:account>",
endpoint="ledger-default") endpoint="ledger-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_ledger(currency: Currency, account: Account) -> str | Response: def get_default_ledger_list(currency: Currency, account: Account) \
-> str | Response:
"""Returns the ledger in the default period. """Returns the ledger in the default period.
:param currency: The currency. :param currency: The currency.
:param account: The account. :param account: The account.
:return: The ledger in the default period. :return: The ledger in the default period.
""" """
return __get_ledger(currency, account, get_period()) return __get_ledger_list(currency, account, Period.get_instance())
@bp.get("ledger/<currency:currency>/<account:account>/<period:period>", @bp.get("ledger/<currency:currency>/<account:account>/<period:period>",
endpoint="ledger") endpoint="ledger")
@has_permission(can_view) @has_permission(can_view)
def get_ledger(currency: Currency, account: Account, period: Period) \ def get_ledger_list(currency: Currency, account: Account, period: Period) \
-> str | Response: -> str | Response:
"""Returns the ledger. """Returns the ledger.
@ -105,10 +91,10 @@ def get_ledger(currency: Currency, account: Account, period: Period) \
:param period: The period. :param period: The period.
:return: The ledger in the period. :return: The ledger in the period.
""" """
return __get_ledger(currency, account, period) return __get_ledger_list(currency, account, period)
def __get_ledger(currency: Currency, account: Account, period: Period) \ def __get_ledger_list(currency: Currency, account: Account, period: Period) \
-> str | Response: -> str | Response:
"""Returns the ledger. """Returns the ledger.
@ -123,10 +109,10 @@ def __get_ledger(currency: Currency, account: Account, period: Period) \
return report.html() return report.html()
@bp.get("income-expenses/<currency:currency>/<ieAccount:account>", @bp.get("income-expenses/<currency:currency>/<ioAccount:account>",
endpoint="income-expenses-default") endpoint="income-expenses-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_income_expenses(currency: Currency, def get_default_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount) \ account: IncomeExpensesAccount) \
-> str | Response: -> str | Response:
"""Returns the income and expenses log in the default period. """Returns the income and expenses log in the default period.
@ -135,14 +121,15 @@ def get_default_income_expenses(currency: Currency,
:param account: The account. :param account: The account.
:return: The income and expenses log in the default period. :return: The income and expenses log in the default period.
""" """
return __get_income_expenses(currency, account, get_period()) return __get_income_expenses_list(currency, account, Period.get_instance())
@bp.get( @bp.get(
"income-expenses/<currency:currency>/<ieAccount:account>/<period:period>", "income-expenses/<currency:currency>/<ioAccount:account>/<period:period>",
endpoint="income-expenses") endpoint="income-expenses")
@has_permission(can_view) @has_permission(can_view)
def get_income_expenses(currency: Currency, account: IncomeExpensesAccount, def get_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount,
period: Period) -> str | Response: period: Period) -> str | Response:
"""Returns the income and expenses log. """Returns the income and expenses log.
@ -151,10 +138,11 @@ def get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
:param period: The period. :param period: The period.
:return: The income and expenses log in the period. :return: The income and expenses log in the period.
""" """
return __get_income_expenses(currency, account, period) return __get_income_expenses_list(currency, account, period)
def __get_income_expenses(currency: Currency, account: IncomeExpensesAccount, def __get_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount,
period: Period) -> str | Response: period: Period) -> str | Response:
"""Returns the income and expenses log. """Returns the income and expenses log.
@ -172,29 +160,31 @@ def __get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
@bp.get("trial-balance/<currency:currency>", @bp.get("trial-balance/<currency:currency>",
endpoint="trial-balance-default") endpoint="trial-balance-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_trial_balance(currency: Currency) -> str | Response: def get_default_trial_balance_list(currency: Currency) -> str | Response:
"""Returns the trial balance in the default period. """Returns the trial balance in the default period.
:param currency: The currency. :param currency: The currency.
:return: The trial balance in the default period. :return: The trial balance in the default period.
""" """
return __get_trial_balance(currency, get_period()) return __get_trial_balance_list(currency, Period.get_instance())
@bp.get("trial-balance/<currency:currency>/<period:period>", @bp.get("trial-balance/<currency:currency>/<period:period>",
endpoint="trial-balance") endpoint="trial-balance")
@has_permission(can_view) @has_permission(can_view)
def get_trial_balance(currency: Currency, period: Period) -> str | Response: def get_trial_balance_list(currency: Currency, period: Period) \
-> str | Response:
"""Returns the trial balance. """Returns the trial balance.
:param currency: The currency. :param currency: The currency.
:param period: The period. :param period: The period.
:return: The trial balance in the period. :return: The trial balance in the period.
""" """
return __get_trial_balance(currency, period) return __get_trial_balance_list(currency, period)
def __get_trial_balance(currency: Currency, period: Period) -> str | Response: def __get_trial_balance_list(currency: Currency, period: Period) \
-> str | Response:
"""Returns the trial balance. """Returns the trial balance.
:param currency: The currency. :param currency: The currency.
@ -210,29 +200,30 @@ def __get_trial_balance(currency: Currency, period: Period) -> str | Response:
@bp.get("income-statement/<currency:currency>", @bp.get("income-statement/<currency:currency>",
endpoint="income-statement-default") endpoint="income-statement-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_income_statement(currency: Currency) -> str | Response: def get_default_income_statement_list(currency: Currency) -> str | Response:
"""Returns the income statement in the default period. """Returns the income statement in the default period.
:param currency: The currency. :param currency: The currency.
:return: The income statement in the default period. :return: The income statement in the default period.
""" """
return __get_income_statement(currency, get_period()) return __get_income_statement_list(currency, Period.get_instance())
@bp.get("income-statement/<currency:currency>/<period:period>", @bp.get("income-statement/<currency:currency>/<period:period>",
endpoint="income-statement") endpoint="income-statement")
@has_permission(can_view) @has_permission(can_view)
def get_income_statement(currency: Currency, period: Period) -> str | Response: def get_income_statement_list(currency: Currency, period: Period) \
-> str | Response:
"""Returns the income statement. """Returns the income statement.
:param currency: The currency. :param currency: The currency.
:param period: The period. :param period: The period.
:return: The income statement in the period. :return: The income statement in the period.
""" """
return __get_income_statement(currency, period) return __get_income_statement_list(currency, period)
def __get_income_statement(currency: Currency, period: Period) \ def __get_income_statement_list(currency: Currency, period: Period) \
-> str | Response: -> str | Response:
"""Returns the income statement. """Returns the income statement.
@ -249,19 +240,19 @@ def __get_income_statement(currency: Currency, period: Period) \
@bp.get("balance-sheet/<currency:currency>", @bp.get("balance-sheet/<currency:currency>",
endpoint="balance-sheet-default") endpoint="balance-sheet-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_balance_sheet(currency: Currency) -> str | Response: def get_default_balance_sheet_list(currency: Currency) -> str | Response:
"""Returns the balance sheet in the default period. """Returns the balance sheet in the default period.
:param currency: The currency. :param currency: The currency.
:return: The balance sheet in the default period. :return: The balance sheet in the default period.
""" """
return __get_balance_sheet(currency, get_period()) return __get_balance_sheet_list(currency, Period.get_instance())
@bp.get("balance-sheet/<currency:currency>/<period:period>", @bp.get("balance-sheet/<currency:currency>/<period:period>",
endpoint="balance-sheet") endpoint="balance-sheet")
@has_permission(can_view) @has_permission(can_view)
def get_balance_sheet(currency: Currency, period: Period) \ def get_balance_sheet_list(currency: Currency, period: Period) \
-> str | Response: -> str | Response:
"""Returns the balance sheet. """Returns the balance sheet.
@ -269,10 +260,10 @@ def get_balance_sheet(currency: Currency, period: Period) \
:param period: The period. :param period: The period.
:return: The balance sheet in the period. :return: The balance sheet in the period.
""" """
return __get_balance_sheet(currency, period) return __get_balance_sheet_list(currency, period)
def __get_balance_sheet(currency: Currency, period: Period) \ def __get_balance_sheet_list(currency: Currency, period: Period) \
-> str | Response: -> str | Response:
"""Returns the balance sheet. """Returns the balance sheet.
@ -297,3 +288,4 @@ def search() -> str | Response:
if "as" in request.args and request.args["as"] == "csv": if "as" in request.args and request.args["as"] == "csv":
return report.csv() return report.csv()
return report.html() return report.html()

View File

@ -171,7 +171,6 @@ class MonthTab extends TabPlane {
constructor(chooser) { constructor(chooser) {
super(chooser); super(chooser);
const monthChooser = document.getElementById(this.prefix + "-chooser"); const monthChooser = document.getElementById(this.prefix + "-chooser");
if (monthChooser !== null) {
let start = monthChooser.dataset.start; let start = monthChooser.dataset.start;
this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, { this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, {
restrictions: { restrictions: {
@ -195,7 +194,6 @@ class MonthTab extends TabPlane {
.replaceAll("PERIOD", period); .replaceAll("PERIOD", period);
}); });
} }
}
/** /**
* The tab ID * The tab ID
@ -252,7 +250,6 @@ class DayTab extends TabPlane {
super(chooser); super(chooser);
this.#date = document.getElementById(this.prefix + "-date"); this.#date = document.getElementById(this.prefix + "-date");
this.#dateError = document.getElementById(this.prefix + "-date-error"); this.#dateError = document.getElementById(this.prefix + "-date-error");
if (this.#date !== null) {
this.#date.onchange = () => { this.#date.onchange = () => {
if (this.#validateDate()) { if (this.#validateDate()) {
window.location = chooser.modal.dataset.urlTemplate window.location = chooser.modal.dataset.urlTemplate
@ -260,7 +257,6 @@ class DayTab extends TabPlane {
} }
}; };
} }
}
/** /**
* Validates the date. * Validates the date.
@ -342,7 +338,6 @@ class CustomTab extends TabPlane {
this.#end = document.getElementById(this.prefix + "-end"); this.#end = document.getElementById(this.prefix + "-end");
this.#endError = document.getElementById(this.prefix + "-end-error"); this.#endError = document.getElementById(this.prefix + "-end-error");
this.#conform = document.getElementById(this.prefix + "-confirm"); this.#conform = document.getElementById(this.prefix + "-confirm");
if (this.#start !== null) {
this.#start.onchange = () => { this.#start.onchange = () => {
if (this.#validateStart()) { if (this.#validateStart()) {
this.#end.min = this.#start.value; this.#end.min = this.#start.value;
@ -363,7 +358,6 @@ class CustomTab extends TabPlane {
} }
}; };
} }
}
/** /**
* Validates the start of the period. * Validates the start of the period.

View File

@ -85,9 +85,9 @@ First written: 2023/1/31
<div class="accounting-card col-sm-6"> <div class="accounting-card col-sm-6">
<div class="accounting-card-title">{{ obj.title }}</div> <div class="accounting-card-title">{{ obj.title }}</div>
<div class="accounting-card-code">{{ obj.code }}</div> <div class="accounting-card-code">{{ obj.code }}</div>
{% if obj.is_offset_needed %} {% if obj.is_pay_off_needed %}
<div> <div>
<span class="badge rounded-pill bg-info">{{ A_("Need offset") }}</span> <span class="badge rounded-pill bg-info">{{ A_("Pay-off needed") }}</span>
</div> </div>
{% endif %} {% endif %}
<div class="small text-secondary fst-italic"> <div class="small text-secondary fst-italic">

View File

@ -63,9 +63,9 @@ First written: 2023/2/1
</div> </div>
<div class="form-check form-switch mb-3"> <div class="form-check form-switch mb-3">
<input id="accounting-is-offset-needed" class="form-check-input" type="checkbox" name="is_offset_needed" value="1" {% if form.is_offset_needed.data %} checked="checked" {% endif %}> <input id="accounting-is-pay-off-needed" class="form-check-input" type="checkbox" name="is_pay_off_needed" value="1" {% if form.is_pay_off_needed.data %} checked="checked" {% endif %}>
<label class="form-check-label" for="accounting-is-offset-needed"> <label class="form-check-label" for="accounting-is-pay-off-needed">
{{ A_("The entries in the account need offset.") }} {{ A_("The entries in the account need pay-off.") }}
</label> </label>
</div> </div>

View File

@ -58,8 +58,8 @@ First written: 2023/1/30
{% for item in list %} {% for item in list %}
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.account.detail", account=item)|accounting_append_next }}"> <a class="list-group-item list-group-item-action" href="{{ url_for("accounting.account.detail", account=item)|accounting_append_next }}">
{{ item }} {{ item }}
{% if item.is_offset_needed %} {% if item.is_pay_off_needed %}
<span class="badge rounded-pill bg-info">{{ A_("Need offset") }}</span> <span class="badge rounded-pill bg-info">{{ A_("Pay-off needed") }}</span>
{% endif %} {% endif %}
</a> </a>
{% endfor %} {% endfor %}

View File

@ -28,7 +28,7 @@ First written: 2023/1/26
</span> </span>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<a class="dropdown-item {% if request.endpoint.startswith("accounting.report.") %} active {% endif %}" href="{{ url_for("accounting.report.default") }}"> <a class="dropdown-item {% if request.endpoint.startswith("accounting.report.") %} active {% endif %}" href="{{ url_for("accounting.report.journal-default") }}">
<i class="fa-solid fa-book"></i> <i class="fa-solid fa-book"></i>
{{ A_("Reports") }} {{ A_("Reports") }}
</a> </a>

View File

@ -19,7 +19,7 @@ search-modal.html: The search modal
Author: imacat@mail.imacat.idv.tw (imacat) Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/8 First written: 2023/3/8
#} #}
<form action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-labelledby="accounting-search-modal-label"> <form action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-label="{{ A_("Search the Accounting Data") }}">
<div class="modal fade" id="accounting-search-modal" tabindex="-1" aria-labelledby="accounting-search-modal-label" aria-hidden="true"> <div class="modal fade" id="accounting-search-modal" tabindex="-1" aria-labelledby="accounting-search-modal-label" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">

View File

@ -106,17 +106,10 @@ First written: 2023/3/8
<span class="d-none d-md-inline">{{ report.period.desc|title }}</span> <span class="d-none d-md-inline">{{ report.period.desc|title }}</span>
</button> </button>
{% endif %} {% endif %}
{% if report.has_data %}
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}"> <a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i> <i class="fa-solid fa-download"></i>
<span class="d-none d-md-inline">{{ A_("Download") }}</span> <span class="d-none d-md-inline">{{ A_("Download") }}</span>
</a> </a>
{% else %}
<button class="btn btn-secondary" type="button" disabled="disabled">
<i class="fa-solid fa-download"></i>
<span class="d-none d-md-inline">{{ A_("Download") }}</span>
</button>
{% endif %}
{% if use_search %} {% if use_search %}
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label"> <form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required"> <input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">

View File

@ -23,6 +23,6 @@ First written: 2023/2/25
{% block header %}{% block title %}{{ A_("Add a New Cash Expense Transaction") }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ A_("Add a New Cash Expense Transaction") }}{% endblock %}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %} {% block back_url %}{{ request.args.get("next") or url_for("accounting.report.journal-default") }}{% endblock %}
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %} {% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}

View File

@ -26,7 +26,7 @@ First written: 2023/2/26
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="btn-group mb-3">
<a class="btn btn-primary" href="{{ url_for("accounting.report.default")|accounting_or_next }}"> <a class="btn btn-primary" href="{{ url_for("accounting.report.journal-default")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}
</a> </a>

View File

@ -23,6 +23,6 @@ First written: 2023/2/25
{% block header %}{% block title %}{{ A_("Add a New Cash Income Transaction") }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ A_("Add a New Cash Income Transaction") }}{% endblock %}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %} {% block back_url %}{{ request.args.get("next") or url_for("accounting.report.journal-default") }}{% endblock %}
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %} {% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}

View File

@ -31,7 +31,7 @@ First written: 2023/2/26
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="btn-group mb-3">
<a class="btn btn-primary" href="{{ url_for("accounting.report.default")|accounting_or_next }}"> <a class="btn btn-primary" href="{{ url_for("accounting.report.journal-default")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}
</a> </a>

View File

@ -23,6 +23,6 @@ First written: 2023/2/25
{% block header %}{% block title %}{{ A_("Add a New Transfer Transaction") }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ A_("Add a New Transfer Transaction") }}{% endblock %}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %} {% block back_url %}{{ request.args.get("next") or url_for("accounting.report.journal-default") }}{% endblock %}
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %} {% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}

View File

@ -126,23 +126,25 @@ class AccountOption:
:param account: The account. :param account: The account.
""" """
self.__account: Account = account
self.id: str = account.id self.id: str = account.id
"""The account ID."""
self.code: str = account.code self.code: str = account.code
"""The account code."""
self.query_values: list[str] = account.query_values
"""The values to be queried."""
self.__str: str = str(account)
"""The string representation of the account option."""
self.is_in_use: bool = False self.is_in_use: bool = False
"""True if this account is in use, or False otherwise."""
def __str__(self) -> str: def __str__(self) -> str:
"""Returns the string representation of the account option. """Returns the string representation of the account option.
:return: The string representation of the account option. :return: The string representation of the account option.
""" """
return self.__str return str(self.__account)
@property
def query_values(self) -> list[str]:
"""Returns the values to be queried.
:return: The values to be queried.
"""
return self.__account.query_values
class JournalEntryForm(FlaskForm): class JournalEntryForm(FlaskForm):

View File

@ -304,17 +304,15 @@ TXN_TYPE_TO_OP: dict[TransactionType, TransactionOperator] \
"""The map from the transaction types to their operators.""" """The map from the transaction types to their operators."""
def get_txn_op(txn: Transaction, is_check_as: bool = False) \ def get_txn_op(txn: Transaction) -> TransactionOperator:
-> TransactionOperator:
"""Returns the transaction operator that may be specified in the "as" query """Returns the transaction operator that may be specified in the "as" query
parameter. If it is not specified, check the transaction type from the parameter. If it is not specified, check the transaction type from the
transaction. transaction.
:param txn: The transaction. :param txn: The transaction.
:param is_check_as: True to check the "as" parameter, or False otherwise.
:return: None. :return: None.
""" """
if is_check_as and "as" in request.args: if "as" in request.args:
type_dict: dict[str, TransactionType] \ type_dict: dict[str, TransactionType] \
= {x.value: x for x in TransactionType} = {x.value: x for x in TransactionType}
if request.args["as"] not in type_dict: if request.args["as"] not in type_dict:

View File

@ -111,7 +111,7 @@ def show_transaction_edit_form(txn: Transaction) -> str:
:param txn: The transaction. :param txn: The transaction.
:return: The form to edit the transaction. :return: The form to edit the transaction.
""" """
txn_op: TransactionOperator = get_txn_op(txn, is_check_as=True) txn_op: TransactionOperator = get_txn_op(txn)
form: txn_op.form form: txn_op.form
if "form" in session: if "form" in session:
form = txn_op.form(ImmutableMultiDict(parse_qsl(session["form"]))) form = txn_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
@ -131,7 +131,7 @@ def update_transaction(txn: Transaction) -> redirect:
:return: The redirection to the transaction detail on success, or the :return: The redirection to the transaction detail on success, or the
transaction edit form on error. transaction edit form on error.
""" """
txn_op: TransactionOperator = get_txn_op(txn, is_check_as=True) txn_op: TransactionOperator = get_txn_op(txn)
form: txn_op.form = txn_op.form(request.form) form: txn_op.form = txn_op.form(request.form)
if not form.validate(): if not form.validate():
flash_form_errors(form) flash_form_errors(form)
@ -163,7 +163,7 @@ def delete_transaction(txn: Transaction) -> redirect:
sort_transactions_in(txn.date, txn.id) sort_transactions_in(txn.date, txn.id)
db.session.commit() db.session.commit()
flash(lazy_gettext("The transaction is deleted successfully."), "success") flash(lazy_gettext("The transaction is deleted successfully."), "success")
return redirect(or_next(__get_default_page_uri())) return redirect(or_next(url_for("accounting.report.journal-default")))
@bp.get("/dates/<date:txn_date>", endpoint="order") @bp.get("/dates/<date:txn_date>", endpoint="order")
@ -194,10 +194,10 @@ def sort_transactions(txn_date: date) -> redirect:
form.save_order() form.save_order()
if not form.is_modified: if not form.is_modified:
flash(lazy_gettext("The order was not modified."), "success") flash(lazy_gettext("The order was not modified."), "success")
return redirect(or_next(__get_default_page_uri())) return redirect(or_next(url_for("accounting.report.journal-default")))
db.session.commit() db.session.commit()
flash(lazy_gettext("The order is updated successfully."), "success") flash(lazy_gettext("The order is updated successfully."), "success")
return redirect(or_next(__get_default_page_uri())) return redirect(or_next(url_for("accounting.report.journal-default")))
def __get_detail_uri(txn: Transaction) -> str: def __get_detail_uri(txn: Transaction) -> str:
@ -207,11 +207,3 @@ def __get_detail_uri(txn: Transaction) -> str:
:return: The detail URI of the transaction. :return: The detail URI of the transaction.
""" """
return url_for("accounting.transaction.detail", txn=txn) return url_for("accounting.transaction.detail", txn=txn)
def __get_default_page_uri() -> str:
"""Returns the URI for the default page.
:return: The URI for the default page.
"""
return url_for("accounting.report.default")

View File

@ -35,7 +35,7 @@ from testlib_txn import Accounts, get_add_form, get_unchanged_update_form, \
PREFIX: str = "/accounting/transactions" PREFIX: str = "/accounting/transactions"
"""The URL prefix for the transaction management.""" """The URL prefix for the transaction management."""
RETURN_TO_URI: str = "/accounting/reports" RETURN_TO_URI: str = "/accounting/reports/journal"
"""The URL to return to after the operation.""" """The URL to return to after the operation."""