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
=================================
Subpackages
-----------
.. toctree::
:maxdepth: 4
accounting.report.reports.utils
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::
:maxdepth: 4
accounting.report.period
accounting.report.reports
accounting.report.utils
Submodules
----------
@ -22,6 +20,22 @@ accounting.report.converters module
:undoc-members:
: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
------------------------------------------

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]
name = mia-accounting-flask
version = 0.5.0
version = 0.4.0
author = imacat
author_email = imacat@mail.imacat.idv.tw
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]
"""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,
@ -93,10 +93,10 @@ def init_accounts_command(username: str) -> None:
data: list[AccountData] = []
for base in bases_to_add:
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
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)
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],
no=x[2],
title_l10n=x[3],
is_offset_needed=x[6],
is_pay_off_needed=x[6],
created_by_id=creator_pk,
updated_by_id=creator_pk)
for x in data]

View File

@ -66,8 +66,8 @@ class AccountForm(FlaskForm):
filters=[strip_text],
validators=[DataRequired(lazy_gettext("Please fill in the title"))])
"""The title."""
is_offset_needed = BooleanField()
"""Whether the the entries of this account need offset."""
is_pay_off_needed = BooleanField()
"""Whether the the entries of this account need pay-off."""
def populate_obj(self, obj: Account) -> None:
"""Populates the form data into an account object.
@ -87,7 +87,7 @@ class AccountForm(FlaskForm):
obj.base_code = self.base_code.data
obj.no = count + 1
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:
current_user_pk: int = get_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),
code.contains(k),
Account.id.in_(l10n_matches)]
if k in gettext("Need offset"):
sub_conditions.append(Account.is_offset_needed)
if k in gettext("Pay-off needed"):
sub_conditions.append(Account.is_pay_off_needed)
conditions.append(sa.or_(*sub_conditions))
return Account.query.filter(*conditions)\

View File

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

View File

@ -29,7 +29,7 @@ def init_app(app: Flask, bp: Blueprint) -> None:
"""
from .converters import PeriodConverter, IncomeExpensesAccountConverter
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
bp.register_blueprint(report_bp, url_prefix="/reports")

View File

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

View File

@ -19,8 +19,6 @@
"""
import typing as t
from flask import current_app
from accounting.locale import gettext
from accounting.models import Account
@ -64,23 +62,3 @@ class IncomeExpensesAccount:
account.title = gettext("current assets and liabilities")
account.str = account.title
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.models import Currency, BaseAccount, Account, Transaction, \
JournalEntry
from accounting.report.period import Period, PeriodChooser
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, balance_sheet_url, \
income_statement_url
from accounting.report.period import Period
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.option_link import OptionLink
from .utils.period_choosers import BalanceSheetPeriodChooser
from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType
from .utils.urls import ledger_url, balance_sheet_url, income_statement_url
class ReportAccount:
@ -318,8 +317,8 @@ class PageParams(BasePageParams):
"""The liabilities."""
self.owner_s_equity: Section = owner_s_equity
"""The owner's equity."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: balance_sheet_url(currency, x))
self.period_chooser: BalanceSheetPeriodChooser \
= BalanceSheetPeriodChooser(currency)
"""The period chooser."""
@property

View File

@ -27,17 +27,17 @@ from sqlalchemy.orm import selectinload
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.period import Period, PeriodChooser
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.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.report.income_expense_account import IncomeExpensesAccount
from accounting.report.period import Period
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:
@ -289,8 +289,8 @@ class PageParams(BasePageParams):
"""The report entries."""
self.total: ReportEntry | None = total
"""The total entry."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: income_expenses_url(currency, account, x))
self.period_chooser: IncomeExpensesPeriodChooser \
= IncomeExpensesPeriodChooser(currency, account)
"""The period chooser."""
@property

View File

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

View File

@ -26,15 +26,14 @@ from sqlalchemy.orm import selectinload
from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.period import Period, PeriodChooser
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.report.period import Period
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:
@ -123,8 +122,8 @@ class PageParams(BasePageParams):
"""The pagination."""
self.entries: list[JournalEntry] = entries
"""The entries."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: journal_url(x))
self.period_chooser: JournalPeriodChooser \
= JournalPeriodChooser()
"""The period chooser."""
@property

View File

@ -27,16 +27,16 @@ from sqlalchemy.orm import selectinload
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.period import Period, PeriodChooser
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.report.period import Period
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:
@ -110,8 +110,6 @@ class EntryCollector:
"""
if self.__period.start is 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(
(JournalEntry.is_debit, JournalEntry.amount),
else_=-JournalEntry.amount))
@ -264,8 +262,8 @@ class PageParams(BasePageParams):
"""The entries."""
self.total: ReportEntry | None = total
"""The total entry."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: ledger_url(currency, account, x))
self.period_chooser: LedgerPeriodChooser \
= LedgerPeriodChooser(currency, account)
"""The period chooser."""
@property

View File

@ -27,14 +27,14 @@ from sqlalchemy.orm import selectinload
from accounting.locale import gettext
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
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.query import parse_query_keywords
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:
@ -91,8 +91,8 @@ class EntryCollector:
Account.title_l10n.contains(k),
code.contains(k),
Account.id.in_(select_l10n)]
if k in gettext("Need offset"):
conditions.append(Account.is_offset_needed)
if k in gettext("Pay-off needed"):
conditions.append(Account.is_pay_off_needed)
return sa.select(Account.id).filter(sa.or_(*conditions))
@staticmethod

View File

@ -25,15 +25,15 @@ from flask import Response, render_template
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.period import Period, PeriodChooser
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, trial_balance_url
from accounting.report.period import Period
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, trial_balance_url
from .utils.option_link import OptionLink
from .utils.period_choosers import TrialBalancePeriodChooser
from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType
class ReportAccount:
@ -121,8 +121,8 @@ class PageParams(BasePageParams):
"""The accounts in the trial balance."""
self.total: Total = total
"""The total of the trial balance."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: trial_balance_url(currency, x))
self.period_chooser: TrialBalancePeriodChooser \
= TrialBalancePeriodChooser(currency)
"""The period chooser."""
@property

View File

@ -14,6 +14,6 @@
# 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 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.locale import gettext
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 .ie_account import IncomeExpensesAccount
from .option_link import OptionLink
from .report_type import ReportType
from .urls import journal_url, ledger_url, income_expenses_url, \
@ -53,7 +53,8 @@ class ReportChooser:
"""
self.__active_report: ReportType = 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."""
self.__currency: Currency = db.session.get(
Currency, default_currency_code()) \

View File

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

View File

@ -20,9 +20,8 @@
from flask import url_for
from accounting.models import Currency, Account
from accounting.report.income_expense_account import IncomeExpensesAccount
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) \
@ -63,10 +62,6 @@ def income_expenses_url(currency: Currency, account: IncomeExpensesAccount,
:param period: The period.
: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:
return url_for("accounting.report.income-expenses-default",
currency=currency, account=account)

View File

@ -19,56 +19,41 @@
"""
from flask import Blueprint, request, Response
from accounting import db
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 .income_expense_account import IncomeExpensesAccount
from .period import Period
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
IncomeStatement, BalanceSheet, Search
from .template_filters import format_amount
from .utils.ie_account import IncomeExpensesAccount, default_ie_account
bp: Blueprint = Blueprint("report", __name__)
"""The view blueprint for the reports."""
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")
@has_permission(can_view)
def get_default_journal() -> str | Response:
def get_default_journal_list() -> str | Response:
"""Returns 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")
@has_permission(can_view)
def get_journal(period: Period) -> str | Response:
def get_journal_list(period: Period) -> str | Response:
"""Returns the journal.
:param period: 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.
:param period: The period.
@ -83,20 +68,21 @@ def __get_journal(period: Period) -> str | Response:
@bp.get("ledger/<currency:currency>/<account:account>",
endpoint="ledger-default")
@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.
:param currency: The currency.
:param account: The account.
: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>",
endpoint="ledger")
@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:
"""Returns the ledger.
@ -105,10 +91,10 @@ def get_ledger(currency: Currency, account: Account, period: Period) \
:param period: 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:
"""Returns the ledger.
@ -123,11 +109,11 @@ def __get_ledger(currency: Currency, account: Account, period: Period) \
return report.html()
@bp.get("income-expenses/<currency:currency>/<ieAccount:account>",
@bp.get("income-expenses/<currency:currency>/<ioAccount:account>",
endpoint="income-expenses-default")
@has_permission(can_view)
def get_default_income_expenses(currency: Currency,
account: IncomeExpensesAccount) \
def get_default_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount) \
-> str | Response:
"""Returns the income and expenses log in the default period.
@ -135,15 +121,16 @@ def get_default_income_expenses(currency: Currency,
:param account: The account.
: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(
"income-expenses/<currency:currency>/<ieAccount:account>/<period:period>",
"income-expenses/<currency:currency>/<ioAccount:account>/<period:period>",
endpoint="income-expenses")
@has_permission(can_view)
def get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
period: Period) -> str | Response:
def get_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount,
period: Period) -> str | Response:
"""Returns the income and expenses log.
:param currency: The currency.
@ -151,11 +138,12 @@ def get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
:param period: 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,
period: Period) -> str | Response:
def __get_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount,
period: Period) -> str | Response:
"""Returns the income and expenses log.
:param currency: The currency.
@ -172,29 +160,31 @@ def __get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
@bp.get("trial-balance/<currency:currency>",
endpoint="trial-balance-default")
@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.
:param currency: The currency.
: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>",
endpoint="trial-balance")
@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.
:param currency: The currency.
:param period: 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.
:param currency: The currency.
@ -210,29 +200,30 @@ def __get_trial_balance(currency: Currency, period: Period) -> str | Response:
@bp.get("income-statement/<currency:currency>",
endpoint="income-statement-default")
@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.
:param currency: The currency.
: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>",
endpoint="income-statement")
@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.
:param currency: The currency.
:param period: 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:
"""Returns the income statement.
@ -249,19 +240,19 @@ def __get_income_statement(currency: Currency, period: Period) \
@bp.get("balance-sheet/<currency:currency>",
endpoint="balance-sheet-default")
@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.
:param currency: The currency.
: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>",
endpoint="balance-sheet")
@has_permission(can_view)
def get_balance_sheet(currency: Currency, period: Period) \
def get_balance_sheet_list(currency: Currency, period: Period) \
-> str | Response:
"""Returns the balance sheet.
@ -269,10 +260,10 @@ def get_balance_sheet(currency: Currency, period: Period) \
:param period: 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:
"""Returns the balance sheet.
@ -297,3 +288,4 @@ def search() -> str | Response:
if "as" in request.args and request.args["as"] == "csv":
return report.csv()
return report.html()

View File

@ -171,30 +171,28 @@ class MonthTab extends TabPlane {
constructor(chooser) {
super(chooser);
const monthChooser = document.getElementById(this.prefix + "-chooser");
if (monthChooser !== null) {
let start = monthChooser.dataset.start;
this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, {
restrictions: {
minDate: start,
let start = monthChooser.dataset.start;
this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, {
restrictions: {
minDate: start,
},
display: {
inline: true,
components: {
date: false,
clock: false,
},
display: {
inline: true,
components: {
date: false,
clock: false,
},
},
defaultDate: monthChooser.dataset.default,
});
monthChooser.addEventListener(tempusDominus.Namespace.events.change, (e) => {
const date = e.detail.date;
const year = date.year;
const month = date.month + 1;
const period = month < 10? year + "-0" + month: year + "-" + month;
window.location = chooser.modal.dataset.urlTemplate
.replaceAll("PERIOD", period);
});
}
},
defaultDate: monthChooser.dataset.default,
});
monthChooser.addEventListener(tempusDominus.Namespace.events.change, (e) => {
const date = e.detail.date;
const year = date.year;
const month = date.month + 1;
const period = month < 10? year + "-0" + month: year + "-" + month;
window.location = chooser.modal.dataset.urlTemplate
.replaceAll("PERIOD", period);
});
}
/**
@ -252,14 +250,12 @@ class DayTab extends TabPlane {
super(chooser);
this.#date = document.getElementById(this.prefix + "-date");
this.#dateError = document.getElementById(this.prefix + "-date-error");
if (this.#date !== null) {
this.#date.onchange = () => {
if (this.#validateDate()) {
window.location = chooser.modal.dataset.urlTemplate
.replaceAll("PERIOD", this.#date.value);
}
};
}
this.#date.onchange = () => {
if (this.#validateDate()) {
window.location = chooser.modal.dataset.urlTemplate
.replaceAll("PERIOD", this.#date.value);
}
};
}
/**
@ -342,27 +338,25 @@ class CustomTab extends TabPlane {
this.#end = document.getElementById(this.prefix + "-end");
this.#endError = document.getElementById(this.prefix + "-end-error");
this.#conform = document.getElementById(this.prefix + "-confirm");
if (this.#start !== null) {
this.#start.onchange = () => {
if (this.#validateStart()) {
this.#end.min = this.#start.value;
}
};
this.#end.onchange = () => {
if (this.#validateEnd()) {
this.#start.max = this.#end.value;
}
};
this.#conform.onclick = () => {
let isValid = true;
isValid = this.#validateStart() && isValid;
isValid = this.#validateEnd() && isValid;
if (isValid) {
window.location = chooser.modal.dataset.urlTemplate
.replaceAll("PERIOD", this.#start.value + "-" + this.#end.value);
}
};
}
this.#start.onchange = () => {
if (this.#validateStart()) {
this.#end.min = this.#start.value;
}
};
this.#end.onchange = () => {
if (this.#validateEnd()) {
this.#start.max = this.#end.value;
}
};
this.#conform.onclick = () => {
let isValid = true;
isValid = this.#validateStart() && isValid;
isValid = this.#validateEnd() && isValid;
if (isValid) {
window.location = chooser.modal.dataset.urlTemplate
.replaceAll("PERIOD", this.#start.value + "-" + this.#end.value);
}
};
}
/**

View File

@ -85,9 +85,9 @@ First written: 2023/1/31
<div class="accounting-card col-sm-6">
<div class="accounting-card-title">{{ obj.title }}</div>
<div class="accounting-card-code">{{ obj.code }}</div>
{% if obj.is_offset_needed %}
{% if obj.is_pay_off_needed %}
<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>
{% endif %}
<div class="small text-secondary fst-italic">

View File

@ -63,9 +63,9 @@ First written: 2023/2/1
</div>
<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 %}>
<label class="form-check-label" for="accounting-is-offset-needed">
{{ A_("The entries in the account need offset.") }}
<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-pay-off-needed">
{{ A_("The entries in the account need pay-off.") }}
</label>
</div>

View File

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

View File

@ -28,7 +28,7 @@ First written: 2023/1/26
</span>
<ul class="dropdown-menu">
<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>
{{ A_("Reports") }}
</a>

View File

@ -19,7 +19,7 @@ search-modal.html: The search modal
Author: imacat@mail.imacat.idv.tw (imacat)
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-dialog">
<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>
</button>
{% endif %}
{% if report.has_data %}
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i>
<span class="d-none d-md-inline">{{ A_("Download") }}</span>
</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 %}
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i>
<span class="d-none d-md-inline">{{ A_("Download") }}</span>
</a>
{% 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">
<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 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 %}

View File

@ -26,7 +26,7 @@ First written: 2023/2/26
{% block content %}
<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>
{{ A_("Back") }}
</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 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 %}

View File

@ -31,7 +31,7 @@ First written: 2023/2/26
{% block content %}
<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>
{{ A_("Back") }}
</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 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 %}

View File

@ -126,23 +126,25 @@ class AccountOption:
:param account: The account.
"""
self.__account: Account = account
self.id: str = account.id
"""The account ID."""
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
"""True if this account is in use, or False otherwise."""
def __str__(self) -> str:
"""Returns 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):

View File

@ -304,17 +304,15 @@ TXN_TYPE_TO_OP: dict[TransactionType, TransactionOperator] \
"""The map from the transaction types to their operators."""
def get_txn_op(txn: Transaction, is_check_as: bool = False) \
-> TransactionOperator:
def get_txn_op(txn: Transaction) -> TransactionOperator:
"""Returns the transaction operator that may be specified in the "as" query
parameter. If it is not specified, check the transaction type from the
transaction.
transaction.
:param txn: The transaction.
:param is_check_as: True to check the "as" parameter, or False otherwise.
:return: None.
"""
if is_check_as and "as" in request.args:
if "as" in request.args:
type_dict: dict[str, TransactionType] \
= {x.value: x for x in TransactionType}
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.
: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
if "form" in session:
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
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)
if not form.validate():
flash_form_errors(form)
@ -163,7 +163,7 @@ def delete_transaction(txn: Transaction) -> redirect:
sort_transactions_in(txn.date, txn.id)
db.session.commit()
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")
@ -194,10 +194,10 @@ def sort_transactions(txn_date: date) -> redirect:
form.save_order()
if not form.is_modified:
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()
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:
@ -207,11 +207,3 @@ def __get_detail_uri(txn: Transaction) -> str:
:return: The detail URI of the transaction.
"""
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"
"""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."""