Compare commits
43 Commits
d7bc01ccb4
...
4d11517e21
Author | SHA1 | Date | |
---|---|---|---|
4d11517e21 | |||
308e4ac69d | |||
de09e1498b | |||
c26c4686c5 | |||
c95f4fcc47 | |||
71af74fc8a | |||
56e972c371 | |||
7feb6da062 | |||
af71874f9d | |||
3fa8818a27 | |||
be46d8aa14 | |||
20f55058ac | |||
e9d1a53e03 | |||
38141759fd | |||
7fb3e3bc2c | |||
05ac5158f8 | |||
ec257a4b57 | |||
5ebb89a6d5 | |||
900d60d1ae | |||
bc792c145f | |||
4432484acd | |||
7ad3f9e0cb | |||
060a52f7a2 | |||
c17430d211 | |||
8fd99bb617 | |||
ce388eb6c8 | |||
1850f9787e | |||
c6d55fad1c | |||
0c647d8f21 | |||
5d1f87582e | |||
ef086b3f81 | |||
b4be1db712 | |||
5d44ebdfd8 | |||
9859604c81 | |||
d31e495f6b | |||
7c4102be44 | |||
1fd50e23d9 | |||
9635448f18 | |||
e7f1ca332e | |||
3d2e40865e | |||
5132141c68 | |||
e37f6792c9 | |||
e6b1136a14 |
69
docs/source/accounting.report.period.rst
Normal file
69
docs/source/accounting.report.period.rst
Normal 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:
|
@ -1,14 +1,6 @@
|
||||
accounting.report.reports package
|
||||
=================================
|
||||
|
||||
Subpackages
|
||||
-----------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
accounting.report.reports.utils
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
|
@ -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:
|
@ -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
|
||||
------------------------------------------
|
||||
|
||||
|
77
docs/source/accounting.report.utils.rst
Normal file
77
docs/source/accounting.report.utils.rst
Normal 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:
|
@ -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.
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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)\
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
22
src/accounting/report/period/__init__.py
Normal file
22
src/accounting/report/period/__init__.py
Normal 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
|
97
src/accounting/report/period/chooser.py
Normal file
97
src/accounting/report/period/chooser.py
Normal 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))
|
179
src/accounting/report/period/description.py
Normal file
179
src/accounting/report/period/description.py
Normal 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)
|
31
src/accounting/report/period/month_end.py
Normal file
31
src/accounting/report/period/month_end.py
Normal 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)
|
119
src/accounting/report/period/parser.py
Normal file
119
src/accounting/report/period/parser.py
Normal 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)
|
129
src/accounting/report/period/period.py
Normal file
129
src/accounting/report/period/period.py
Normal 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))
|
168
src/accounting/report/period/shortcuts.py
Normal file
168
src/accounting/report/period/shortcuts.py
Normal 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)
|
120
src/accounting/report/period/specification.py
Normal file
120
src/accounting/report/period/specification.py
Normal 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}"
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
@ -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.
|
||||
|
||||
"""
|
@ -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))
|
@ -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()) \
|
@ -35,4 +35,4 @@ class ReportType(Enum):
|
||||
BALANCE_SHEET: str = "balance-sheet"
|
||||
"""The balance sheet."""
|
||||
SEARCH: str = "search"
|
||||
"""The balance sheet."""
|
||||
"""The search."""
|
@ -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)
|
@ -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()
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
@ -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."""
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user