Compare commits

...

43 Commits

Author SHA1 Message Date
4d11517e21 Advanced to version 0.5.0. 2023-03-10 08:36:22 +08:00
308e4ac69d Updated the Sphinx documentation. 2023-03-10 08:36:08 +08:00
de09e1498b Added the __get_default_page_uri() function to the "accounting.transaction.views" module to simplify the code. 2023-03-10 08:34:44 +08:00
c26c4686c5 Renamed the "original_id" column to "offset_original_id", and the "original" and "offset" relationships to "offset_original" and "offsets", respectively, in the JournalEntry data model. 2023-03-10 08:25:38 +08:00
c95f4fcc47 Added the __str property and changed the query_values property from a pseudo property to a real property in the AccountOption data model, so that it does not need to hold the account object forever. 2023-03-09 22:50:18 +08:00
71af74fc8a Added documentation to the properties of the AccountOption data model. 2023-03-09 22:47:46 +08:00
56e972c371 Fixed so that the download buttons on the report pages are disabled when there is no data. 2023-03-09 22:29:44 +08:00
7feb6da062 Fixed the JavaScript period chooser error when there is no data. 2023-03-09 22:25:26 +08:00
af71874f9d Fixed an error checking if there is any data in the PeriodChooser utility. 2023-03-09 22:20:24 +08:00
3fa8818a27 Added the is_check_as parameter to the get_txn_op function so that the "as" query parameter is not checked when showing the transaction detail. 2023-03-09 22:14:22 +08:00
be46d8aa14 Renamed the default_io_account_code and default_io_account functions to default_ie_account_code and default_ie_account, respectively. That was a mistake. 2023-03-09 20:59:21 +08:00
20f55058ac Shortened the name of the "accounting.report.utils.income_expenses_account" module to "accounting.report.utils.ie_account". 2023-03-09 20:59:21 +08:00
e9d1a53e03 Shortened the name of the "accounting.report.utils.income_expenses_account" module to "accounting.report.utils.ie_account". 2023-03-09 20:59:21 +08:00
38141759fd Removed an excess blank line in the "accounting.report.view" module. 2023-03-09 20:59:20 +08:00
7fb3e3bc2c Shortened the names of the views of the reports. 2023-03-09 20:59:20 +08:00
05ac5158f8 Added the default report view as the income and expenses log with the default currency, default account and default period. Changed the previous default journal links to the current default. 2023-03-09 20:59:09 +08:00
ec257a4b57 Renamed the "accounting.report.period.periods" module to "accounting.report.period.shortcuts", to be clear. 2023-03-09 20:13:15 +08:00
5ebb89a6d5 Moved the month_end utility from the "accounting.report.period.period" module to the new "accounting.report.period.month_end" module. 2023-03-09 19:56:06 +08:00
900d60d1ae Moved the shortcut named periods from the "accounting.report.period.period" module to the "accounting.report.period.periods" module. 2023-03-09 19:44:53 +08:00
bc792c145f Replaced the Period.get_instance method with the get_period function in the "accounting.report.period.parser" module. Changed the parse_spec function in the "accounting.report.period.parser" to private. 2023-03-09 19:40:34 +08:00
4432484acd Replaced the PeriodSpecification object-based utility with the get_spec function-based utility, for simplicity. 2023-03-09 19:30:36 +08:00
7ad3f9e0cb Replaced the PeriodDescription object-based utility with the get_desc function-based utility, for simplicity. 2023-03-09 19:25:43 +08:00
060a52f7a2 Moved the period specification parser from the "accounting.report.period.period" module to the "accounting.report.period.parser" module. 2023-03-09 19:10:21 +08:00
c17430d211 Renamed the "accounting.report.period.period_chooser" module to "accounting.report.period.chooser", for simplicity. 2023-03-09 19:07:58 +08:00
8fd99bb617 Simplified the import of the datetime module in the "accounting.report.period.period" module. 2023-03-09 19:05:27 +08:00
ce388eb6c8 Moved the PeriodSpecification and PeriodDescription utilities from the "accounting.report.period.period" module to the "accounting.report.period.specification" and "accounting.report.period.description" modules, respectively. 2023-03-09 18:57:29 +08:00
1850f9787e Moved the period and period chooser to the "accounting.report.period" module. 2023-03-09 18:30:41 +08:00
c6d55fad1c Renamed the "accounting.report.utils.period_choosers" module to "accounting.report.utils.period_chooser", because there is only period chooser now. 2023-03-09 18:14:20 +08:00
0c647d8f21 Moved the "accounting.reports.period" and "accounting.reports.income_expense_account" utility modules into the "accounting.reports.utils" module. 2023-03-09 18:13:18 +08:00
5d1f87582e Moved the "accounting.report.reports.utils" module to "accounting.report.utils". It does not make sense to have a wierd and long module name just to make the import pretty. 2023-03-09 18:09:08 +08:00
ef086b3f81 Revised to simplify the YearPeriod period. 2023-03-09 18:03:02 +08:00
b4be1db712 Revised the imports in the "accounting.report.reports.utils.period_chooser" module. 2023-03-09 18:00:38 +08:00
5d44ebdfd8 Revised the properties of the Today, Yesterday, and AllTime periods. 2023-03-09 17:58:49 +08:00
9859604c81 Revised the documentation of the _set_properties method of the Period utility. 2023-03-09 17:56:27 +08:00
d31e495f6b Added the AllTime class as a named period. 2023-03-09 17:49:55 +08:00
7c4102be44 Fixed the documentation of the "ReportType.SEARCH" enum item. 2023-03-09 17:49:05 +08:00
1fd50e23d9 Changed the PeriodChooser utility from abstract to real, and replaced the various trivial subclasses with the get_url callable as the parameter. 2023-03-09 17:43:21 +08:00
9635448f18 Added the missing documentation to the sections property of the PageParams data model in the income statement report. 2023-03-09 17:36:33 +08:00
e7f1ca332e Revised the imports in the modules of ledger, income and expenses log, trial balance, and income statement. 2023-03-09 17:32:22 +08:00
3d2e40865e Revised the PeriodChooser utility to find the start of the data by itself. It can do that. It's child classes are all doing the same thing. There is no need to do that in its child classes. 2023-03-09 17:20:52 +08:00
5132141c68 Renamed the "is_pay_off_needed" column of the Account data model to "is_offset_needed", and the "pay_off_target_id" column of the JournalEntry data model to "original_id". 2023-03-09 17:16:05 +08:00
e37f6792c9 Replaced aria-label with aria-labelled-by in the search modal of the report, for simplicity. 2023-03-09 16:42:13 +08:00
e6b1136a14 Fixed so that the brought-forward row is not added for norminal accounts in the ledger. 2023-03-09 16:25:59 +08:00
55 changed files with 1304 additions and 1159 deletions

View File

@ -0,0 +1,69 @@
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,14 +1,6 @@
accounting.report.reports package
=================================
Subpackages
-----------
.. toctree::
:maxdepth: 4
accounting.report.reports.utils
Submodules
----------

View File

@ -1,77 +0,0 @@
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,7 +7,9 @@ Subpackages
.. toctree::
:maxdepth: 4
accounting.report.period
accounting.report.reports
accounting.report.utils
Submodules
----------
@ -20,22 +22,6 @@ 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

@ -0,0 +1,77 @@
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.4.0
version = 0.5.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-pay-off-needed) tuples."""
English, Traditional Chinese, Simplified Chinese, is-offset-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_pay_off_needed: bool = True if re.match("^[12]1[34]", base.code) \
is_offset_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_pay_off_needed))
l10n["zh_Hant"], l10n["zh_Hans"], is_offset_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_pay_off_needed=x[6],
is_offset_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_pay_off_needed = BooleanField()
"""Whether the the entries of this account need pay-off."""
is_offset_needed = BooleanField()
"""Whether the the entries of this account need offset."""
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_pay_off_needed = self.is_pay_off_needed.data
obj.is_offset_needed = self.is_offset_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("Pay-off needed"):
sub_conditions.append(Account.is_pay_off_needed)
if k in gettext("Need offset"):
sub_conditions.append(Account.is_offset_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_pay_off_needed = db.Column(db.Boolean, nullable=False, default=False)
"""Whether the entries of this account need pay-off."""
is_offset_needed = db.Column(db.Boolean, nullable=False, default=False)
"""Whether the entries of this account need offset."""
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."""
pay_off_target_id = db.Column(db.Integer,
offset_original_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",
"""The ID of the original entry to offset."""
offset_original = db.relationship("JournalEntry", back_populates="offsets",
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."""
"""The original entry to offset."""
offsets = db.relationship("JournalEntry", back_populates="offset_original")
"""The offset 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["ioAccount"] = IncomeExpensesAccountConverter
app.url_map.converters["ieAccount"] = 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 .income_expense_account import IncomeExpensesAccount
from .period import Period
from .period import Period, get_period
from .utils.ie_account import IncomeExpensesAccount
class PeriodConverter(BaseConverter):
@ -38,7 +38,7 @@ class PeriodConverter(BaseConverter):
:return: The corresponding period.
"""
try:
return Period.get_instance(value)
return get_period(value)
except ValueError:
abort(404)

View File

@ -1,633 +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 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

@ -0,0 +1,22 @@
# 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

@ -0,0 +1,97 @@
# 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

@ -0,0 +1,179 @@
# 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

@ -0,0 +1,31 @@
# 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

@ -0,0 +1,119 @@
# 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

@ -0,0 +1,129 @@
# 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

@ -0,0 +1,168 @@
# 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

@ -0,0 +1,120 @@
# 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,15 +26,16 @@ from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, BaseAccount, Account, Transaction, \
JournalEntry
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
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
class ReportAccount:
@ -317,8 +318,8 @@ class PageParams(BasePageParams):
"""The liabilities."""
self.owner_s_equity: Section = owner_s_equity
"""The owner's equity."""
self.period_chooser: BalanceSheetPeriodChooser \
= BalanceSheetPeriodChooser(currency)
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: balance_sheet_url(currency, x))
"""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.income_expense_account import IncomeExpensesAccount
from accounting.report.period import Period
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.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: IncomeExpensesPeriodChooser \
= IncomeExpensesPeriodChooser(currency, account)
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: income_expenses_url(currency, account, x))
"""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
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
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
class ReportAccount:
@ -159,8 +159,9 @@ class PageParams(BasePageParams):
self.__has_data: bool = has_data
"""True if there is any data, or False otherwise."""
self.sections: list[Section] = sections
self.period_chooser: IncomeStatementPeriodChooser \
= IncomeStatementPeriodChooser(currency)
"""The sections in the income statement."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: income_statement_url(currency, x))
"""The period chooser."""
@property

View File

@ -26,14 +26,15 @@ from sqlalchemy.orm import selectinload
from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.period import Period
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.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:
@ -122,8 +123,8 @@ class PageParams(BasePageParams):
"""The pagination."""
self.entries: list[JournalEntry] = entries
"""The entries."""
self.period_chooser: JournalPeriodChooser \
= JournalPeriodChooser()
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: journal_url(x))
"""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
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.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,6 +110,8 @@ 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))
@ -262,8 +264,8 @@ class PageParams(BasePageParams):
"""The entries."""
self.total: ReportEntry | None = total
"""The total entry."""
self.period_chooser: LedgerPeriodChooser \
= LedgerPeriodChooser(currency, account)
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: ledger_url(currency, account, x))
"""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("Pay-off needed"):
conditions.append(Account.is_pay_off_needed)
if k in gettext("Need offset"):
conditions.append(Account.is_offset_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
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
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
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: TrialBalancePeriodChooser \
= TrialBalancePeriodChooser(currency)
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: trial_balance_url(currency, x))
"""The period chooser."""
@property

View File

@ -1,194 +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 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

@ -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 to generate reports.
"""The utilities for the reports.
"""

View File

@ -19,6 +19,8 @@
"""
import typing as t
from flask import current_app
from accounting.locale import gettext
from accounting.models import Account
@ -62,3 +64,23 @@ 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

@ -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.income_expense_account import IncomeExpensesAccount
from accounting.report.period import Period
from accounting.report.period import Period, get_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,8 +53,7 @@ class ReportChooser:
"""
self.__active_report: ReportType = active_report
"""The currently active report."""
self.__period: Period = Period.get_instance() if period is None \
else period
self.__period: Period = get_period() 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 balance sheet."""
"""The search."""

View File

@ -20,8 +20,9 @@
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) \
@ -62,6 +63,10 @@ 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,41 +19,56 @@
"""
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_list() -> str | Response:
def get_default_journal() -> str | Response:
"""Returns the journal in the default period.
:return: The journal in the default period.
"""
return __get_journal_list(Period.get_instance())
return __get_journal(get_period())
@bp.get("journal/<period:period>", endpoint="journal")
@has_permission(can_view)
def get_journal_list(period: Period) -> str | Response:
def get_journal(period: Period) -> str | Response:
"""Returns the journal.
:param period: The period.
:return: The journal in the period.
"""
return __get_journal_list(period)
return __get_journal(period)
def __get_journal_list(period: Period) -> str | Response:
def __get_journal(period: Period) -> str | Response:
"""Returns the journal.
:param period: The period.
@ -68,21 +83,20 @@ def __get_journal_list(period: Period) -> str | Response:
@bp.get("ledger/<currency:currency>/<account:account>",
endpoint="ledger-default")
@has_permission(can_view)
def get_default_ledger_list(currency: Currency, account: Account) \
-> str | Response:
def get_default_ledger(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_list(currency, account, Period.get_instance())
return __get_ledger(currency, account, get_period())
@bp.get("ledger/<currency:currency>/<account:account>/<period:period>",
endpoint="ledger")
@has_permission(can_view)
def get_ledger_list(currency: Currency, account: Account, period: Period) \
def get_ledger(currency: Currency, account: Account, period: Period) \
-> str | Response:
"""Returns the ledger.
@ -91,10 +105,10 @@ def get_ledger_list(currency: Currency, account: Account, period: Period) \
:param period: The period.
:return: The ledger in the period.
"""
return __get_ledger_list(currency, account, period)
return __get_ledger(currency, account, period)
def __get_ledger_list(currency: Currency, account: Account, period: Period) \
def __get_ledger(currency: Currency, account: Account, period: Period) \
-> str | Response:
"""Returns the ledger.
@ -109,10 +123,10 @@ def __get_ledger_list(currency: Currency, account: Account, period: Period) \
return report.html()
@bp.get("income-expenses/<currency:currency>/<ioAccount:account>",
@bp.get("income-expenses/<currency:currency>/<ieAccount:account>",
endpoint="income-expenses-default")
@has_permission(can_view)
def get_default_income_expenses_list(currency: Currency,
def get_default_income_expenses(currency: Currency,
account: IncomeExpensesAccount) \
-> str | Response:
"""Returns the income and expenses log in the default period.
@ -121,15 +135,14 @@ def get_default_income_expenses_list(currency: Currency,
:param account: The account.
:return: The income and expenses log in the default period.
"""
return __get_income_expenses_list(currency, account, Period.get_instance())
return __get_income_expenses(currency, account, get_period())
@bp.get(
"income-expenses/<currency:currency>/<ioAccount:account>/<period:period>",
"income-expenses/<currency:currency>/<ieAccount:account>/<period:period>",
endpoint="income-expenses")
@has_permission(can_view)
def get_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount,
def get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
period: Period) -> str | Response:
"""Returns the income and expenses log.
@ -138,11 +151,10 @@ def get_income_expenses_list(currency: Currency,
:param period: The period.
:return: The income and expenses log in the period.
"""
return __get_income_expenses_list(currency, account, period)
return __get_income_expenses(currency, account, period)
def __get_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount,
def __get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
period: Period) -> str | Response:
"""Returns the income and expenses log.
@ -160,31 +172,29 @@ def __get_income_expenses_list(currency: Currency,
@bp.get("trial-balance/<currency:currency>",
endpoint="trial-balance-default")
@has_permission(can_view)
def get_default_trial_balance_list(currency: Currency) -> str | Response:
def get_default_trial_balance(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_list(currency, Period.get_instance())
return __get_trial_balance(currency, get_period())
@bp.get("trial-balance/<currency:currency>/<period:period>",
endpoint="trial-balance")
@has_permission(can_view)
def get_trial_balance_list(currency: Currency, period: Period) \
-> str | Response:
def get_trial_balance(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_list(currency, period)
return __get_trial_balance(currency, period)
def __get_trial_balance_list(currency: Currency, period: Period) \
-> str | Response:
def __get_trial_balance(currency: Currency, period: Period) -> str | Response:
"""Returns the trial balance.
:param currency: The currency.
@ -200,30 +210,29 @@ def __get_trial_balance_list(currency: Currency, period: Period) \
@bp.get("income-statement/<currency:currency>",
endpoint="income-statement-default")
@has_permission(can_view)
def get_default_income_statement_list(currency: Currency) -> str | Response:
def get_default_income_statement(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_list(currency, Period.get_instance())
return __get_income_statement(currency, get_period())
@bp.get("income-statement/<currency:currency>/<period:period>",
endpoint="income-statement")
@has_permission(can_view)
def get_income_statement_list(currency: Currency, period: Period) \
-> str | Response:
def get_income_statement(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_list(currency, period)
return __get_income_statement(currency, period)
def __get_income_statement_list(currency: Currency, period: Period) \
def __get_income_statement(currency: Currency, period: Period) \
-> str | Response:
"""Returns the income statement.
@ -240,19 +249,19 @@ def __get_income_statement_list(currency: Currency, period: Period) \
@bp.get("balance-sheet/<currency:currency>",
endpoint="balance-sheet-default")
@has_permission(can_view)
def get_default_balance_sheet_list(currency: Currency) -> str | Response:
def get_default_balance_sheet(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_list(currency, Period.get_instance())
return __get_balance_sheet(currency, get_period())
@bp.get("balance-sheet/<currency:currency>/<period:period>",
endpoint="balance-sheet")
@has_permission(can_view)
def get_balance_sheet_list(currency: Currency, period: Period) \
def get_balance_sheet(currency: Currency, period: Period) \
-> str | Response:
"""Returns the balance sheet.
@ -260,10 +269,10 @@ def get_balance_sheet_list(currency: Currency, period: Period) \
:param period: The period.
:return: The balance sheet in the period.
"""
return __get_balance_sheet_list(currency, period)
return __get_balance_sheet(currency, period)
def __get_balance_sheet_list(currency: Currency, period: Period) \
def __get_balance_sheet(currency: Currency, period: Period) \
-> str | Response:
"""Returns the balance sheet.
@ -288,4 +297,3 @@ def search() -> str | Response:
if "as" in request.args and request.args["as"] == "csv":
return report.csv()
return report.html()

View File

@ -171,6 +171,7 @@ 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: {
@ -194,6 +195,7 @@ class MonthTab extends TabPlane {
.replaceAll("PERIOD", period);
});
}
}
/**
* The tab ID
@ -250,6 +252,7 @@ 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
@ -257,6 +260,7 @@ class DayTab extends TabPlane {
}
};
}
}
/**
* Validates the date.
@ -338,6 +342,7 @@ 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;
@ -358,6 +363,7 @@ class CustomTab extends TabPlane {
}
};
}
}
/**
* Validates the start of the period.

View File

@ -85,9 +85,9 @@ First written: 2023/1/31
<div class="accounting-card col-sm-6">
<div class="accounting-card-title">{{ obj.title }}</div>
<div class="accounting-card-code">{{ obj.code }}</div>
{% if obj.is_pay_off_needed %}
{% if obj.is_offset_needed %}
<div>
<span class="badge rounded-pill bg-info">{{ A_("Pay-off needed") }}</span>
<span class="badge rounded-pill bg-info">{{ A_("Need offset") }}</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-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.") }}
<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.") }}
</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_pay_off_needed %}
<span class="badge rounded-pill bg-info">{{ A_("Pay-off needed") }}</span>
{% if item.is_offset_needed %}
<span class="badge rounded-pill bg-info">{{ A_("Need offset") }}</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.journal-default") }}">
<a class="dropdown-item {% if request.endpoint.startswith("accounting.report.") %} active {% endif %}" href="{{ url_for("accounting.report.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-label="{{ A_("Search the Accounting Data") }}">
<form action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-labelledby="accounting-search-modal-label">
<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,10 +106,17 @@ 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 %}
{% 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.journal-default") }}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.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.journal-default")|accounting_or_next }}">
<a class="btn btn-primary" href="{{ url_for("accounting.report.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.journal-default") }}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.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.journal-default")|accounting_or_next }}">
<a class="btn btn-primary" href="{{ url_for("accounting.report.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.journal-default") }}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}

View File

@ -126,25 +126,23 @@ 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 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
return self.__str
class JournalEntryForm(FlaskForm):

View File

@ -304,15 +304,17 @@ TXN_TYPE_TO_OP: dict[TransactionType, TransactionOperator] \
"""The map from the transaction types to their operators."""
def get_txn_op(txn: Transaction) -> TransactionOperator:
def get_txn_op(txn: Transaction, is_check_as: bool = False) \
-> 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.
:param txn: The transaction.
:param is_check_as: True to check the "as" parameter, or False otherwise.
:return: None.
"""
if "as" in request.args:
if is_check_as and "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)
txn_op: TransactionOperator = get_txn_op(txn, is_check_as=True)
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)
txn_op: TransactionOperator = get_txn_op(txn, is_check_as=True)
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(url_for("accounting.report.journal-default")))
return redirect(or_next(__get_default_page_uri()))
@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(url_for("accounting.report.journal-default")))
return redirect(or_next(__get_default_page_uri()))
db.session.commit()
flash(lazy_gettext("The order is updated successfully."), "success")
return redirect(or_next(url_for("accounting.report.journal-default")))
return redirect(or_next(__get_default_page_uri()))
def __get_detail_uri(txn: Transaction) -> str:
@ -207,3 +207,11 @@ 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/journal"
RETURN_TO_URI: str = "/accounting/reports"
"""The URL to return to after the operation."""