Compare commits

..

No commits in common. "d7bc01ccb4b8e697ed1228499911fc3145f4377d" and "9993f6562772c119322d808d7d4f30f89a98b8fc" have entirely different histories.

75 changed files with 2364 additions and 2237 deletions

View File

@ -28,10 +28,10 @@ accounting.account.forms module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
accounting.account.queries module accounting.account.query module
--------------------------------- -------------------------------
.. automodule:: accounting.account.queries .. automodule:: accounting.account.query
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View File

@ -20,10 +20,10 @@ accounting.base\_account.converters module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
accounting.base\_account.queries module accounting.base\_account.query module
--------------------------------------- -------------------------------------
.. automodule:: accounting.base_account.queries .. automodule:: accounting.base_account.query
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View File

@ -28,10 +28,10 @@ accounting.currency.forms module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
accounting.currency.queries module accounting.currency.query module
---------------------------------- --------------------------------
.. automodule:: accounting.currency.queries .. automodule:: accounting.currency.query
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View File

@ -1,77 +0,0 @@
accounting.report.reports package
=================================
Subpackages
-----------
.. toctree::
:maxdepth: 4
accounting.report.reports.utils
Submodules
----------
accounting.report.reports.balance\_sheet module
-----------------------------------------------
.. automodule:: accounting.report.reports.balance_sheet
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.income\_expenses module
-------------------------------------------------
.. automodule:: accounting.report.reports.income_expenses
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.income\_statement module
--------------------------------------------------
.. automodule:: accounting.report.reports.income_statement
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.journal module
----------------------------------------
.. automodule:: accounting.report.reports.journal
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.ledger module
---------------------------------------
.. automodule:: accounting.report.reports.ledger
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.search module
---------------------------------------
.. automodule:: accounting.report.reports.search
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.trial\_balance module
-----------------------------------------------
.. automodule:: accounting.report.reports.trial_balance
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.report.reports
:members:
:undoc-members:
:show-inheritance:

View File

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

View File

@ -1,61 +0,0 @@
accounting.report package
=========================
Subpackages
-----------
.. toctree::
:maxdepth: 4
accounting.report.reports
Submodules
----------
accounting.report.converters module
-----------------------------------
.. automodule:: accounting.report.converters
:members:
: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
------------------------------------------
.. automodule:: accounting.report.template_filters
:members:
:undoc-members:
:show-inheritance:
accounting.report.views module
------------------------------
.. automodule:: accounting.report.views
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.report
:members:
:undoc-members:
:show-inheritance:

View File

@ -10,7 +10,6 @@ Subpackages
accounting.account accounting.account
accounting.base_account accounting.base_account
accounting.currency accounting.currency
accounting.report
accounting.transaction accounting.transaction
accounting.utils accounting.utils
@ -33,22 +32,6 @@ accounting.models module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
accounting.template\_filters module
-----------------------------------
.. automodule:: accounting.template_filters
:members:
:undoc-members:
:show-inheritance:
accounting.template\_globals module
-----------------------------------
.. automodule:: accounting.template_globals
:members:
:undoc-members:
:show-inheritance:
Module contents Module contents
--------------- ---------------

View File

@ -12,6 +12,14 @@ accounting.transaction.converters module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
accounting.transaction.dispatcher module
----------------------------------------
.. automodule:: accounting.transaction.dispatcher
:members:
:undoc-members:
:show-inheritance:
accounting.transaction.forms module accounting.transaction.forms module
----------------------------------- -----------------------------------
@ -20,26 +28,26 @@ accounting.transaction.forms module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
accounting.transaction.operators module accounting.transaction.query module
--------------------------------------- -----------------------------------
.. automodule:: accounting.transaction.operators .. automodule:: accounting.transaction.query
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
accounting.transaction.summary\_editor module accounting.transaction.summary\_helper module
--------------------------------------------- ---------------------------------------------
.. automodule:: accounting.transaction.summary_editor .. automodule:: accounting.transaction.summary_helper
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
accounting.transaction.template\_filters module accounting.transaction.template module
----------------------------------------------- --------------------------------------
.. automodule:: accounting.transaction.template_filters .. automodule:: accounting.transaction.template
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View File

@ -60,14 +60,6 @@ accounting.utils.strip\_text module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
accounting.utils.txn\_types module
----------------------------------
.. automodule:: accounting.utils.txn_types
:members:
:undoc-members:
:show-inheritance:
accounting.utils.user module accounting.utils.user module
---------------------------- ----------------------------

View File

@ -52,7 +52,7 @@ class BaseAccount(db.Model):
:return: The string representation of the base account. :return: The string representation of the base account.
""" """
return f"{self.code} {self.title}" return F"{self.code} {self.title}"
@property @property
def title(self) -> str: def title(self) -> str:
@ -141,11 +141,17 @@ class Account(db.Model):
entries = db.relationship("JournalEntry", back_populates="account") entries = db.relationship("JournalEntry", back_populates="account")
"""The journal entries.""" """The journal entries."""
CASH_CODE: str = "1111-001" __CASH = "1111-001"
"""The code of the cash account,""" """The code of the cash account,"""
ACCUMULATED_CHANGE_CODE: str = "3351-001" __RECEIVABLE = "1141-001"
"""The code of the receivable account,"""
__PAYABLE = "2141-001"
"""The code of the payable account,"""
__ACCUMULATED_CHANGE = "3351-001"
"""The code of the accumulated-change account,""" """The code of the accumulated-change account,"""
NET_CHANGE_CODE: str = "3353-001" __BROUGHT_FORWARD = "3352-001"
"""The code of the brought-forward account,"""
__NET_CHANGE = "3353-001"
"""The code of the net-change account,""" """The code of the net-change account,"""
def __str__(self) -> str: def __str__(self) -> str:
@ -153,7 +159,7 @@ class Account(db.Model):
:return: The string representation of this account. :return: The string representation of this account.
""" """
return f"{self.base_code}-{self.no:03d} {self.title}" return F"{self.base_code}-{self.no:03d} {self.title}"
@property @property
def code(self) -> str: def code(self) -> str:
@ -161,7 +167,7 @@ class Account(db.Model):
:return: The code. :return: The code.
""" """
return f"{self.base_code}-{self.no:03d}" return F"{self.base_code}-{self.no:03d}"
@property @property
def title(self) -> str: def title(self) -> str:
@ -265,7 +271,23 @@ class Account(db.Model):
:return: The cash account :return: The cash account
""" """
return cls.find_by_code(cls.CASH_CODE) return cls.find_by_code(cls.__CASH)
@classmethod
def receivable(cls) -> t.Self:
"""Returns the receivable account.
:return: The receivable account
"""
return cls.find_by_code(cls.__RECEIVABLE)
@classmethod
def payable(cls) -> t.Self:
"""Returns the payable account.
:return: The payable account
"""
return cls.find_by_code(cls.__PAYABLE)
@classmethod @classmethod
def accumulated_change(cls) -> t.Self: def accumulated_change(cls) -> t.Self:
@ -273,7 +295,23 @@ class Account(db.Model):
:return: The accumulated-change account :return: The accumulated-change account
""" """
return cls.find_by_code(cls.ACCUMULATED_CHANGE_CODE) return cls.find_by_code(cls.__ACCUMULATED_CHANGE)
@classmethod
def brought_forward(cls) -> t.Self:
"""Returns the brought-forward account.
:return: The brought-forward account
"""
return cls.find_by_code(cls.__BROUGHT_FORWARD)
@classmethod
def net_change(cls) -> t.Self:
"""Returns the net-change account.
:return: The net-change account
"""
return cls.find_by_code(cls.__NET_CHANGE)
@property @property
def is_modified(self) -> bool: def is_modified(self) -> bool:
@ -354,7 +392,7 @@ class Currency(db.Model):
:return: The string representation of the currency. :return: The string representation of the currency.
""" """
return f"{self.name} ({self.code})" return F"{self.name} ({self.code})"
@property @property
def name(self) -> str: def name(self) -> str:
@ -550,7 +588,7 @@ class Transaction(db.Model):
for currency in self.currencies: for currency in self.currencies:
if len(currency.debit) > 1: if len(currency.debit) > 1:
return False return False
if currency.debit[0].account.code != Account.CASH_CODE: if currency.debit[0].account.code != "1111-001":
return False return False
return True return True
@ -564,7 +602,7 @@ class Transaction(db.Model):
for currency in self.currencies: for currency in self.currencies:
if len(currency.credit) > 1: if len(currency.credit) > 1:
return False return False
if currency.credit[0].account.code != Account.CASH_CODE: if currency.credit[0].account.code != "1111-001":
return False return False
return True return True
@ -617,7 +655,7 @@ class JournalEntry(db.Model):
onupdate="CASCADE"), onupdate="CASCADE"),
nullable=False) nullable=False)
"""The account ID.""" """The account ID."""
account = db.relationship(Account, back_populates="entries", lazy=False) account = db.relationship(Account, back_populates="entries")
"""The account.""" """The account."""
summary = db.Column(db.String, nullable=True) summary = db.Column(db.String, nullable=True)
"""The summary.""" """The summary."""
@ -640,19 +678,3 @@ class JournalEntry(db.Model):
:return: The account code. :return: The account code.
""" """
return self.account.code return self.account.code
@property
def debit(self) -> Decimal | None:
"""Returns the debit amount.
:return: The debit amount, or None if this is not a debit entry.
"""
return self.amount if self.is_debit else None
@property
def credit(self) -> Decimal | None:
"""Returns the credit amount.
:return: The credit amount, or None if this is not a credit entry.
"""
return None if self.is_debit else self.amount

View File

@ -52,8 +52,8 @@ class PeriodConverter(BaseConverter):
class IncomeExpensesAccountConverter(BaseConverter): class IncomeExpensesAccountConverter(BaseConverter):
"""The supplier converter to convert the income and expenses log pseudo """The supplier converter to convert the income and expenses pseudo account
account code from and to the corresponding pseudo account in the routes.""" code from and to the corresponding pseudo account in the routes."""
def to_python(self, value: str) -> IncomeExpensesAccount: def to_python(self, value: str) -> IncomeExpensesAccount:
"""Converts an account code to an account. """Converts an account code to an account.

View File

@ -33,15 +33,21 @@ class IncomeExpensesAccount:
:param account: The actual account. :param account: The actual account.
""" """
self.account: Account | None = account self.account: Account | None = None
self.id: int = -1 if account is None else account.id self.id: int | None = None
"""The ID.""" """The ID."""
self.code: str = "" if account is None else account.code self.code: str | None = None
"""The code.""" """The code."""
self.title: str = "" if account is None else account.title self.title: str | None = None
"""The title.""" """The title."""
self.str: str = "" if account is None else str(account) self.str: str = ""
"""The string representation of the account.""" """The string representation of the account."""
if account is not None:
self.account = account
self.id = account.id
self.code = account.code
self.title = account.title
self.str = str(account)
def __str__(self) -> str: def __str__(self) -> str:
"""Returns the string representation of the account. """Returns the string representation of the account.

View File

@ -63,8 +63,6 @@ class Period:
"""The period specification.""" """The period specification."""
self.desc: str = "" self.desc: str = ""
"""The text description.""" """The text description."""
self.is_a_month: bool = False
"""Whether the period is a whole month."""
self.is_type_month: bool = False self.is_type_month: bool = False
"""Whether the period is for the month chooser.""" """Whether the period is for the month chooser."""
self.is_a_year: bool = False self.is_a_year: bool = False
@ -87,13 +85,12 @@ class Period:
:return: None. :return: None.
""" """
self.spec = PeriodSpecification(self).spec self.spec = self.__get_spec()
self.desc = PeriodDescription(self).desc self.desc = self.__get_desc()
if self.start is None or self.end is None: if self.start is None or self.end is None:
return return
self.is_a_month = self.start.day == 1 \ self.is_type_month \
and self.end == _month_end(self.start) = 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) \ self.is_a_year = self.start == datetime.date(self.start.year, 1, 1) \
and self.end == datetime.date(self.start.year, 12, 31) and self.end == datetime.date(self.start.year, 12, 31)
self.is_a_day = self.start == self.end self.is_a_day = self.start == self.end
@ -126,6 +123,189 @@ class Period:
raise ValueError raise ValueError
return cls(start, end) return cls(start, end)
def __get_spec(self) -> str:
"""Returns the period specification.
:return: The period specification.
"""
if self.start is None:
if self.end is None:
return "-"
else:
if self.end.day != _month_end(self.end).day:
return "-%04d-%02d-%02d" % (
self.end.year, self.end.month, self.end.day)
if self.end.month != 12:
return "-%04d-%02d" % (self.end.year, self.end.month)
return "-%04d" % self.end.year
else:
if self.end is None:
if self.start.day != 1:
return "%04d-%02d-%02d-" % (
self.start.year, self.start.month, self.start.day)
if self.start.month != 1:
return "%04d-%02d-" % (self.start.year, self.start.month)
return "%04d-" % self.start.year
else:
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_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)
def __get_desc(self) -> str:
"""Returns the period description.
:return: The period description.
"""
cls: t.Type[t.Self] = self.__class__
if self.start is None:
if self.end is None:
return gettext("for all time")
else:
if self.end != _month_end(self.end):
return gettext("until %(end)s",
end=cls.__format_date(self.end))
if self.end.month != 12:
return gettext("until %(end)s",
end=cls.__format_month(self.end))
return gettext("until %(end)s", end=str(self.end.year))
else:
if self.end is None:
if self.start.day != 1:
return gettext("since %(start)s",
start=cls.__format_date(self.start))
if self.start.month != 1:
return gettext("since %(start)s",
start=cls.__format_month(self.start))
return gettext("since %(start)s", start=str(self.start.year))
else:
try:
return self.__get_year_desc()
except ValueError:
pass
try:
return self.__get_month_desc()
except ValueError:
pass
return self.__get_day_desc()
@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}"
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 gettext("in %(period)s", period=start)
end: str = str(self.end.year)
return gettext("in %(start)s-%(end)s", start=start, end=end)
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 = F"{self.start.year}/{self.start.month}"
if self.start.year == self.end.year \
and self.start.month == self.end.month:
return gettext("in %(period)s", period=start)
if self.start.year == self.end.year:
end_month: str = str(self.end.month)
return gettext("in %(start)s-%(end)s", start=start, end=end_month)
end: str = F"{self.end.year}/{self.end.month}"
return gettext("in %(start)s-%(end)s", start=start, end=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 = F"{self.start.year}/{self.start.month}/{self.start.day}"
if self.start == self.end:
return gettext("in %(period)s", period=start)
if self.start.year == self.end.year \
and self.start.month == self.end.month:
end_day: str = str(self.end.day)
return gettext("in %(start)s-%(end)s", start=start, end=end_day)
if self.start.year == self.end.year:
end_month_day: str = F"{self.end.month}/{self.end.day}"
return gettext("in %(start)s-%(end)s",
start=start, end=end_month_day)
end: str = F"{self.end.year}/{self.end.month}/{self.end.day}"
return gettext("in %(start)s-%(end)s", start=start, end=end)
def is_year(self, year: int) -> bool: def is_year(self, year: int) -> bool:
"""Returns whether the period is the specific year period. """Returns whether the period is the specific year period.
@ -156,266 +336,6 @@ class Period:
return Period(None, self.start - datetime.timedelta(days=1)) 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): class ThisMonth(Period):
"""The period of this month.""" """The period of this month."""
def __init__(self): def __init__(self):
@ -553,9 +473,8 @@ class YearPeriod(Period):
self.spec = str(year) self.spec = str(year)
self.is_a_year = True self.is_a_year = True
def _set_properties(self) -> None:
DATE_SPEC_RE: str = r"(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?" pass
"""The regular expression of a date specification."""
def _parse_period_spec(text: str) \ def _parse_period_spec(text: str) \
@ -567,19 +486,22 @@ def _parse_period_spec(text: str) \
may be None. may be None.
:raise ValueError: When the date is invalid. :raise ValueError: When the date is invalid.
""" """
if text == "this-month":
today: datetime.date = datetime.date.today()
return datetime.date(today.year, today.month, 1), _month_end(today)
if text == "-": if text == "-":
return None, None return None, None
m = re.match(f"^{DATE_SPEC_RE}$", text) m = re.match(r"^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$", text)
if m is not None: if m is not None:
return __get_start(m[1], m[2], m[3]), \ return __get_start(m[1], m[2], m[3]), \
__get_end(m[1], m[2], m[3]) __get_end(m[1], m[2], m[3])
m = re.match(f"^{DATE_SPEC_RE}-$", text) m = re.match(r"^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?-$", text)
if m is not None: if m is not None:
return __get_start(m[1], m[2], m[3]), None return __get_start(m[1], m[2], m[3]), None
m = re.match(f"-{DATE_SPEC_RE}$", text) m = re.match(r"-(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$", text)
if m is not None: if m is not None:
return None, __get_end(m[1], m[2], m[3]) return None, __get_end(m[1], m[2], m[3])
m = re.match(f"^{DATE_SPEC_RE}-{DATE_SPEC_RE}$", text) m = re.match(r"^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?-(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$", text)
if m is not None: if m is not None:
return __get_start(m[1], m[2], m[3]), \ return __get_start(m[1], m[2], m[3]), \
__get_end(m[4], m[5], m[6]) __get_end(m[4], m[5], m[6])

View File

@ -20,28 +20,27 @@
from decimal import Decimal from decimal import Decimal
import sqlalchemy as sa import sqlalchemy as sa
from flask import render_template, Response from flask import url_for, render_template, Response
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, BaseAccount, Account, Transaction, \ from accounting.models import Currency, BaseAccount, Account, Transaction, \
JournalEntry JournalEntry
from accounting.report.period import Period from accounting.report.period import Period
from .utils.base_page_params import BasePageParams
from .utils.base_report import BaseReport from .utils.base_report import BaseReport
from .utils.csv_export import BaseCSVRow, csv_download, period_spec from .utils.csv_export import BaseCSVRow, csv_download
from .utils.option_link import OptionLink from .utils.option_link import OptionLink
from .utils.page_params import PageParams
from .utils.period_choosers import BalanceSheetPeriodChooser from .utils.period_choosers import BalanceSheetPeriodChooser
from .utils.report_chooser import ReportChooser from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType from .utils.report_type import ReportType
from .utils.urls import ledger_url, balance_sheet_url, income_statement_url
class ReportAccount: class BalanceSheetAccount:
"""An account in the report.""" """An account in the balance sheet."""
def __init__(self, account: Account, amount: Decimal, url: str): def __init__(self, account: Account, amount: Decimal, url: str):
"""Constructs an account in the report. """Constructs an account in the balance sheet.
:param account: The account. :param account: The account.
:param amount: The amount. :param amount: The amount.
@ -55,17 +54,17 @@ class ReportAccount:
"""The URL to the ledger of the account.""" """The URL to the ledger of the account."""
class Subsection: class BalanceSheetSubsection:
"""A subsection.""" """A subsection in the balance sheet."""
def __init__(self, title: BaseAccount): def __init__(self, title: BaseAccount):
"""Constructs a subsection. """Constructs a subsection in the balance sheet.
:param title: The title account. :param title: The title account.
""" """
self.title: BaseAccount = title self.title: BaseAccount = title
"""The title account.""" """The title account."""
self.accounts: list[ReportAccount] = [] self.accounts: list[BalanceSheetAccount] = []
"""The accounts in the subsection.""" """The accounts in the subsection."""
@property @property
@ -77,17 +76,17 @@ class Subsection:
return sum([x.amount for x in self.accounts]) return sum([x.amount for x in self.accounts])
class Section: class BalanceSheetSection:
"""A section.""" """A section in the balance sheet."""
def __init__(self, title: BaseAccount): def __init__(self, title: BaseAccount):
"""Constructs a section. """Constructs a section in the balance sheet.
:param title: The title account. :param title: The title account.
""" """
self.title: BaseAccount = title self.title: BaseAccount = title
"""The title account.""" """The title account."""
self.subsections: list[Subsection] = [] self.subsections: list[BalanceSheetSubsection] = []
"""The subsections in the section.""" """The subsections in the section."""
@property @property
@ -112,10 +111,10 @@ class AccountCollector:
"""The currency.""" """The currency."""
self.__period: Period = period self.__period: Period = period
"""The period.""" """The period."""
self.accounts: list[ReportAccount] = self.__query_balances() self.accounts: list[BalanceSheetAccount] = self.__query_balances()
"""The balance sheet accounts.""" """The balance sheet accounts."""
def __query_balances(self) -> list[ReportAccount]: def __query_balances(self) -> list[BalanceSheetAccount]:
"""Queries and returns the balances. """Queries and returns the balances.
:return: The balances. :return: The balances.
@ -145,12 +144,24 @@ class AccountCollector:
Account.base_code == "3353")).all() Account.base_code == "3353")).all()
account_by_id: dict[int, Account] \ account_by_id: dict[int, Account] \
= {x.id: x for x in self.__all_accounts} = {x.id: x for x in self.__all_accounts}
self.accounts: list[ReportAccount] \
= [ReportAccount(account=account_by_id[x.id], def get_url(account: Account) -> str:
amount=x.balance, """Returns the ledger URL of an account.
url=ledger_url(self.__currency,
account_by_id[x.id], :param account: The account.
self.__period)) :return: The ledger URL of the account.
"""
if self.__period.is_default:
return url_for("accounting.report.ledger-default",
currency=self.__currency, account=account)
return url_for("accounting.report.ledger",
currency=self.__currency, account=account,
period=self.__period)
self.accounts: list[BalanceSheetAccount] \
= [BalanceSheetAccount(account=account_by_id[x.id],
amount=x.balance,
url=get_url(account_by_id[x.id]))
for x in account_balances] for x in account_balances]
self.__add_accumulated() self.__add_accumulated()
self.__add_current_period() self.__add_current_period()
@ -165,9 +176,12 @@ class AccountCollector:
:return: None. :return: None.
""" """
self.__add_owner_s_equity(Account.ACCUMULATED_CHANGE_CODE, code: str = "3351-001"
self.__query_accumulated(), amount: Decimal | None = self.__query_accumulated()
self.__period) url: str = url_for("accounting.report.income-statement",
currency=self.__currency,
period=self.__period.before)
self.__add_owner_s_equity(code, amount, url)
def __query_accumulated(self) -> Decimal | None: def __query_accumulated(self) -> Decimal | None:
"""Queries and returns the accumulated profit or loss. """Queries and returns the accumulated profit or loss.
@ -179,16 +193,25 @@ class AccountCollector:
conditions: list[sa.BinaryExpression] \ conditions: list[sa.BinaryExpression] \
= [JournalEntry.currency_code == self.__currency.code, = [JournalEntry.currency_code == self.__currency.code,
Transaction.date < self.__period.start] Transaction.date < self.__period.start]
return self.__query_balance(conditions) conditions.extend([sa.not_(Account.base_code.startswith(x))
for x in {"1", "2"}])
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntry.is_debit, JournalEntry.amount),
else_=-JournalEntry.amount)).label("balance")
select_balance: sa.Select = sa.select(balance_func)\
.join(Transaction).join(Account).filter(*conditions)
return db.session.scalar(select_balance)
def __add_current_period(self) -> None: def __add_current_period(self) -> None:
"""Adds the accumulated profit or loss to the balances. """Adds the accumulated profit or loss to the balances.
:return: None. :return: None.
""" """
self.__add_owner_s_equity(Account.NET_CHANGE_CODE, code: str = "3353-001"
self.__query_currency_period(), amount: Decimal | None = self.__query_currency_period()
self.__period) url: str = url_for("accounting.report.income-statement",
currency=self.__currency, period=self.__period)
self.__add_owner_s_equity(code, amount, url)
def __query_currency_period(self) -> Decimal | None: def __query_currency_period(self) -> Decimal | None:
"""Queries and returns the net income or loss for current period. """Queries and returns the net income or loss for current period.
@ -201,58 +224,47 @@ class AccountCollector:
conditions.append(Transaction.date >= self.__period.start) conditions.append(Transaction.date >= self.__period.start)
if self.__period.end is not None: if self.__period.end is not None:
conditions.append(Transaction.date <= self.__period.end) conditions.append(Transaction.date <= self.__period.end)
return self.__query_balance(conditions)
@staticmethod
def __query_balance(conditions: list[sa.BinaryExpression])\
-> Decimal:
"""Queries the balance.
:param conditions: The SQL conditions for the balance.
:return: The balance.
"""
conditions.extend([sa.not_(Account.base_code.startswith(x)) conditions.extend([sa.not_(Account.base_code.startswith(x))
for x in {"1", "2"}]) for x in {"1", "2"}])
balance_func: sa.Function = sa.func.sum(sa.case( balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntry.is_debit, JournalEntry.amount), (JournalEntry.is_debit, JournalEntry.amount),
else_=-JournalEntry.amount)) else_=-JournalEntry.amount)).label("balance")
select_balance: sa.Select = sa.select(balance_func)\ select_balance: sa.Select = sa.select(balance_func)\
.join(Transaction).join(Account).filter(*conditions) .join(Transaction).join(Account).filter(*conditions)
return db.session.scalar(select_balance) return db.session.scalar(select_balance)
def __add_owner_s_equity(self, code: str, amount: Decimal | None, def __add_owner_s_equity(self, code: str, amount: Decimal | None,
period: Period) -> None: url: str) -> None:
"""Adds an owner's equity balance. """Adds an owner's equity balance.
:param code: The code of the account to add. :param code: The code of the account to add.
:param amount: The amount. :param amount: The amount.
:param period: The period.
:return: None. :return: None.
""" """
if amount is None:
return
url: str = income_statement_url(self.__currency, period)
# There is an existing balance. # There is an existing balance.
account_balance_by_code: dict[str, ReportAccount] \ account_balance_by_code: dict[str, BalanceSheetAccount] \
= {x.account.code: x for x in self.accounts} = {x.account.code: x for x in self.accounts}
if code in account_balance_by_code: if code in account_balance_by_code:
balance: ReportAccount = account_balance_by_code[code] balance: BalanceSheetAccount = account_balance_by_code[code]
balance.amount = balance.amount + amount
balance.url = url balance.url = url
if amount is not None:
balance.amount = balance.amount + amount
return return
# Add a new balance # Add a new balance
if amount is None:
return
account_by_code: dict[str, Account] \ account_by_code: dict[str, Account] \
= {x.code: x for x in self.__all_accounts} = {x.code: x for x in self.__all_accounts}
self.accounts.append(ReportAccount(account=account_by_code[code], self.accounts.append(BalanceSheetAccount(account=account_by_code[code],
amount=amount, amount=amount,
url=url)) url=url))
class CSVHalfRow: class CSVHalfRow:
"""A half row in the CSV.""" """A half row in the CSV balance sheet."""
def __init__(self, title: str | None, amount: Decimal | None): def __init__(self, title: str | None, amount: Decimal | None):
"""The constructs a half row in the CSV. """The constructs a half row in the CSV balance sheet.
:param title: The title. :param title: The title.
:param amount: The amount. :param amount: The amount.
@ -264,10 +276,10 @@ class CSVHalfRow:
class CSVRow(BaseCSVRow): class CSVRow(BaseCSVRow):
"""A row in the CSV.""" """A row in the CSV balance sheet."""
def __init__(self): def __init__(self):
"""Constructs a row in the CSV.""" """Constructs a row in the CSV balance sheet."""
self.asset_title: str | None = None self.asset_title: str | None = None
"""The title of the asset.""" """The title of the asset."""
self.asset_amount: Decimal | None = None self.asset_amount: Decimal | None = None
@ -287,16 +299,16 @@ class CSVRow(BaseCSVRow):
self.liability_title, self.liability_amount] self.liability_title, self.liability_amount]
class PageParams(BasePageParams): class BalanceSheetPageParams(PageParams):
"""The HTML page parameters.""" """The HTML parameters of the balance sheet."""
def __init__(self, currency: Currency, def __init__(self, currency: Currency,
period: Period, period: Period,
has_data: bool, has_data: bool,
assets: Section, assets: BalanceSheetSection,
liabilities: Section, liabilities: BalanceSheetSection,
owner_s_equity: Section): owner_s_equity: BalanceSheetSection):
"""Constructs the HTML page parameters. """Constructs the HTML parameters of the balance sheet.
:param currency: The currency. :param currency: The currency.
:param period: The period. :param period: The period.
@ -311,11 +323,11 @@ class PageParams(BasePageParams):
"""The period.""" """The period."""
self.__has_data: bool = has_data self.__has_data: bool = has_data
"""True if there is any data, or False otherwise.""" """True if there is any data, or False otherwise."""
self.assets: Section = assets self.assets: BalanceSheetSection = assets
"""The assets.""" """The assets."""
self.liabilities: Section = liabilities self.liabilities: BalanceSheetSection = liabilities
"""The liabilities.""" """The liabilities."""
self.owner_s_equity: Section = owner_s_equity self.owner_s_equity: BalanceSheetSection = owner_s_equity
"""The owner's equity.""" """The owner's equity."""
self.period_chooser: BalanceSheetPeriodChooser \ self.period_chooser: BalanceSheetPeriodChooser \
= BalanceSheetPeriodChooser(currency) = BalanceSheetPeriodChooser(currency)
@ -345,8 +357,19 @@ class PageParams(BasePageParams):
:return: The currency options. :return: The currency options.
""" """
return self._get_currency_options( def get_url(currency: Currency):
lambda x: balance_sheet_url(x, self.period), self.currency) if self.period.is_default:
return url_for("accounting.report.balance-sheet-default",
currency=currency)
return url_for("accounting.report.balance-sheet",
currency=currency, period=self.period)
in_use: set[str] = set(db.session.scalars(
sa.select(JournalEntry.currency_code)
.group_by(JournalEntry.currency_code)).all())
return [OptionLink(str(x), get_url(x), x.code == self.currency.code)
for x in Currency.query.filter(Currency.code.in_(in_use))
.order_by(Currency.code).all()]
class BalanceSheet(BaseReport): class BalanceSheet(BaseReport):
@ -364,11 +387,11 @@ class BalanceSheet(BaseReport):
"""The period.""" """The period."""
self.__has_data: bool self.__has_data: bool
"""True if there is any data, or False otherwise.""" """True if there is any data, or False otherwise."""
self.__assets: Section self.__assets: BalanceSheetSection
"""The assets.""" """The assets."""
self.__liabilities: Section self.__liabilities: BalanceSheetSection
"""The liabilities.""" """The liabilities."""
self.__owner_s_equity: Section self.__owner_s_equity: BalanceSheetSection
"""The owner's equity.""" """The owner's equity."""
self.__set_data() self.__set_data()
@ -378,7 +401,7 @@ class BalanceSheet(BaseReport):
:return: None. :return: None.
""" """
balances: list[ReportAccount] = AccountCollector( balances: list[BalanceSheetAccount] = AccountCollector(
self.__currency, self.__period).accounts self.__currency, self.__period).accounts
titles: list[BaseAccount] = BaseAccount.query\ titles: list[BaseAccount] = BaseAccount.query\
@ -387,9 +410,10 @@ class BalanceSheet(BaseReport):
.filter(BaseAccount.code.in_({x.account.base_code[:2] .filter(BaseAccount.code.in_({x.account.base_code[:2]
for x in balances})).all() for x in balances})).all()
sections: dict[str, Section] = {x.code: Section(x) for x in titles} sections: dict[str, BalanceSheetSection] \
subsections: dict[str, Subsection] = {x.code: Subsection(x) = {x.code: BalanceSheetSection(x) for x in titles}
for x in subtitles} subsections: dict[str, BalanceSheetSubsection] \
= {x.code: BalanceSheetSubsection(x) for x in subtitles}
for subsection in subsections.values(): for subsection in subsections.values():
sections[subsection.title.code[0]].subsections.append(subsection) sections[subsection.title.code[0]].subsections.append(subsection)
for balance in balances: for balance in balances:
@ -406,8 +430,7 @@ class BalanceSheet(BaseReport):
:return: The response of the report for download. :return: The response of the report for download.
""" """
filename: str = "balance-sheet-{currency}-{period}.csv"\ filename: str = "balance-sheet-{currency}-{period}.csv"\
.format(currency=self.__currency.code, .format(currency=self.__currency.code, period=self.__period.spec)
period=period_spec(self.__period))
return csv_download(filename, self.__get_csv_rows()) return csv_download(filename, self.__get_csv_rows())
def __get_csv_rows(self) -> list[CSVRow]: def __get_csv_rows(self) -> list[CSVRow]:
@ -443,7 +466,7 @@ class BalanceSheet(BaseReport):
return rows return rows
@staticmethod @staticmethod
def __section_csv_rows(section: Section) -> list[CSVHalfRow]: def __section_csv_rows(section: BalanceSheetSection) -> list[CSVHalfRow]:
"""Gathers the CSV rows for a section. """Gathers the CSV rows for a section.
:param section: The section. :param section: The section.
@ -463,11 +486,12 @@ class BalanceSheet(BaseReport):
:return: The report as HTML. :return: The report as HTML.
""" """
params: PageParams = PageParams(currency=self.__currency, params: BalanceSheetPageParams = BalanceSheetPageParams(
period=self.__period, currency=self.__currency,
has_data=self.__has_data, period=self.__period,
assets=self.__assets, has_data=self.__has_data,
liabilities=self.__liabilities, assets=self.__assets,
owner_s_equity=self.__owner_s_equity) liabilities=self.__liabilities,
owner_s_equity=self.__owner_s_equity)
return render_template("accounting/report/balance-sheet.html", return render_template("accounting/report/balance-sheet.html",
report=params) report=params)

View File

@ -22,7 +22,6 @@ from decimal import Decimal
import sqlalchemy as sa import sqlalchemy as sa
from flask import url_for, render_template, Response from flask import url_for, render_template, Response
from sqlalchemy.orm import selectinload
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
@ -30,24 +29,27 @@ from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.income_expense_account import IncomeExpensesAccount from accounting.report.income_expense_account import IncomeExpensesAccount
from accounting.report.period import Period from accounting.report.period import Period
from accounting.utils.pagination import Pagination from accounting.utils.pagination import Pagination
from .utils.base_page_params import BasePageParams
from .utils.base_report import BaseReport from .utils.base_report import BaseReport
from .utils.csv_export import BaseCSVRow, csv_download, period_spec from .utils.csv_export import BaseCSVRow, csv_download
from .utils.urls import income_expenses_url
from .utils.option_link import OptionLink from .utils.option_link import OptionLink
from .utils.page_params import PageParams
from .utils.period_choosers import IncomeExpensesPeriodChooser from .utils.period_choosers import IncomeExpensesPeriodChooser
from .utils.report_chooser import ReportChooser from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType from .utils.report_type import ReportType
class ReportEntry: class Entry:
"""An entry in the report.""" """An entry in the income and expenses log."""
def __init__(self, entry: JournalEntry | None = None): def __init__(self, entry: JournalEntry | None = None):
"""Constructs the entry in the report. """Constructs the entry in the income and expenses log.
:param entry: The journal entry. :param entry: The journal entry.
""" """
self.entry: JournalEntry | None = None
"""The journal entry."""
self.transaction: Transaction | None = None
"""The transaction."""
self.is_brought_forward: bool = False self.is_brought_forward: bool = False
"""Whether this is the brought-forward entry.""" """Whether this is the brought-forward entry."""
self.is_total: bool = False self.is_total: bool = False
@ -66,25 +68,19 @@ class ReportEntry:
"""The balance.""" """The balance."""
self.note: str | None = None self.note: str | None = None
"""The note.""" """The note."""
self.url: str | None = None
"""The URL to the journal entry."""
if entry is not None: if entry is not None:
self.date = entry.transaction.date self.entry = entry
self.account = entry.account
self.summary = entry.summary self.summary = entry.summary
self.income = None if entry.is_debit else entry.amount self.income = None if entry.is_debit else entry.amount
self.expense = entry.amount if entry.is_debit else None self.expense = entry.amount if entry.is_debit else None
self.note = entry.transaction.note
self.url = url_for("accounting.transaction.detail",
txn=entry.transaction)
class EntryCollector: class EntryCollector:
"""The report entry collector.""" """The income and expenses log entry collector."""
def __init__(self, currency: Currency, account: IncomeExpensesAccount, def __init__(self, currency: Currency, account: IncomeExpensesAccount,
period: Period): period: Period):
"""Constructs the report entry collector. """Constructs the income and expenses log entry collector.
:param currency: The currency. :param currency: The currency.
:param account: The account. :param account: The account.
@ -96,18 +92,18 @@ class EntryCollector:
"""The account.""" """The account."""
self.__period: Period = period self.__period: Period = period
"""The period""" """The period"""
self.brought_forward: ReportEntry | None self.brought_forward: Entry | None
"""The brought-forward entry.""" """The brought-forward entry."""
self.entries: list[ReportEntry] self.entries: list[Entry]
"""The log entries.""" """The log entries."""
self.total: ReportEntry | None self.total: Entry | None
"""The total entry.""" """The total entry."""
self.brought_forward = self.__get_brought_forward_entry() self.brought_forward = self.__get_brought_forward_entry()
self.entries = self.__query_entries() self.entries = self.__query_entries()
self.total = self.__get_total_entry() self.total = self.__get_total_entry()
self.__populate_balance() self.__populate_balance()
def __get_brought_forward_entry(self) -> ReportEntry | None: def __get_brought_forward_entry(self) -> Entry | None:
"""Queries, composes and returns the brought-forward entry. """Queries, composes and returns the brought-forward entry.
:return: The brought-forward entry, or None if the period starts from :return: The brought-forward entry, or None if the period starts from
@ -126,10 +122,10 @@ class EntryCollector:
balance: int | None = db.session.scalar(select) balance: int | None = db.session.scalar(select)
if balance is None: if balance is None:
return None return None
entry: ReportEntry = ReportEntry() entry: Entry = Entry()
entry.is_brought_forward = True entry.is_brought_forward = True
entry.date = self.__period.start entry.date = self.__period.start
entry.account = Account.accumulated_change() entry.account = Account.find_by_code("3351-001")
entry.summary = gettext("Brought forward") entry.summary = gettext("Brought forward")
if balance > 0: if balance > 0:
entry.income = balance entry.income = balance
@ -138,7 +134,7 @@ class EntryCollector:
entry.balance = balance entry.balance = balance
return entry return entry
def __query_entries(self) -> list[ReportEntry]: def __query_entries(self) -> list[Entry]:
"""Queries and returns the log entries. """Queries and returns the log entries.
:return: The log entries. :return: The log entries.
@ -153,16 +149,14 @@ class EntryCollector:
txn_with_account: sa.Select = sa.Select(Transaction.id).\ txn_with_account: sa.Select = sa.Select(Transaction.id).\
join(JournalEntry).join(Account).filter(*conditions) join(JournalEntry).join(Account).filter(*conditions)
return [ReportEntry(x) return [Entry(x)
for x in JournalEntry.query.join(Transaction).join(Account) for x in JournalEntry.query.join(Transaction).join(Account)
.filter(JournalEntry.transaction_id.in_(txn_with_account), .filter(JournalEntry.transaction_id.in_(txn_with_account),
JournalEntry.currency_code == self.__currency.code, JournalEntry.currency_code == self.__currency.code,
sa.not_(self.__account_condition)) sa.not_(self.__account_condition))
.order_by(Transaction.date, .order_by(Transaction.date,
JournalEntry.is_debit, JournalEntry.is_debit,
JournalEntry.no) JournalEntry.no)]
.options(selectinload(JournalEntry.account),
selectinload(JournalEntry.transaction))]
@property @property
def __account_condition(self) -> sa.BinaryExpression: def __account_condition(self) -> sa.BinaryExpression:
@ -173,14 +167,14 @@ class EntryCollector:
Account.base_code.startswith("22")) Account.base_code.startswith("22"))
return Account.id == self.__account.id return Account.id == self.__account.id
def __get_total_entry(self) -> ReportEntry | None: def __get_total_entry(self) -> Entry | None:
"""Composes the total entry. """Composes the total entry.
:return: The total entry, or None if there is no data. :return: The total entry, or None if there is no data.
""" """
if self.brought_forward is None and len(self.entries) == 0: if self.brought_forward is None and len(self.entries) == 0:
return None return None
entry: ReportEntry = ReportEntry() entry: Entry = Entry()
entry.is_total = True entry.is_total = True
entry.summary = gettext("Total") entry.summary = gettext("Total")
entry.income = sum([x.income for x in self.entries entry.income = sum([x.income for x in self.entries
@ -208,7 +202,7 @@ class EntryCollector:
class CSVRow(BaseCSVRow): class CSVRow(BaseCSVRow):
"""A row in the CSV.""" """A row in the CSV income and expenses log."""
def __init__(self, txn_date: date | str | None, def __init__(self, txn_date: date | str | None,
account: str | None, account: str | None,
@ -217,7 +211,7 @@ class CSVRow(BaseCSVRow):
expense: str | Decimal | None, expense: str | Decimal | None,
balance: str | Decimal | None, balance: str | Decimal | None,
note: str | None): note: str | None):
"""Constructs a row in the CSV. """Constructs a row in the CSV income and expenses log.
:param txn_date: The transaction date. :param txn_date: The transaction date.
:param account: The account. :param account: The account.
@ -252,18 +246,18 @@ class CSVRow(BaseCSVRow):
self.income, self.expense, self.balance, self.note] self.income, self.expense, self.balance, self.note]
class PageParams(BasePageParams): class IncomeExpensesPageParams(PageParams):
"""The HTML page parameters.""" """The HTML parameters of the income and expenses log."""
def __init__(self, currency: Currency, def __init__(self, currency: Currency,
account: IncomeExpensesAccount, account: IncomeExpensesAccount,
period: Period, period: Period,
has_data: bool, has_data: bool,
pagination: Pagination[ReportEntry], pagination: Pagination[Entry],
brought_forward: ReportEntry | None, brought_forward: Entry | None,
entries: list[ReportEntry], entries: list[Entry],
total: ReportEntry | None): total: Entry | None):
"""Constructs the HTML page parameters. """Constructs the HTML parameters of the income and expenses log.
:param currency: The currency. :param currency: The currency.
:param account: The account. :param account: The account.
@ -281,13 +275,13 @@ class PageParams(BasePageParams):
"""The period.""" """The period."""
self.__has_data: bool = has_data self.__has_data: bool = has_data
"""True if there is any data, or False otherwise.""" """True if there is any data, or False otherwise."""
self.pagination: Pagination[ReportEntry] = pagination self.pagination: Pagination[Entry] = pagination
"""The pagination.""" """The pagination."""
self.brought_forward: ReportEntry | None = brought_forward self.brought_forward: Entry | None = brought_forward
"""The brought-forward entry.""" """The brought-forward entry."""
self.entries: list[ReportEntry] = entries self.entries: list[Entry] = entries
"""The report entries.""" """The entries."""
self.total: ReportEntry | None = total self.total: Entry | None = total
"""The total entry.""" """The total entry."""
self.period_chooser: IncomeExpensesPeriodChooser \ self.period_chooser: IncomeExpensesPeriodChooser \
= IncomeExpensesPeriodChooser(currency, account) = IncomeExpensesPeriodChooser(currency, account)
@ -323,9 +317,20 @@ class PageParams(BasePageParams):
:return: The currency options. :return: The currency options.
""" """
return self._get_currency_options( def get_url(currency: Currency):
lambda x: income_expenses_url(x, self.account, self.period), if self.period.is_default:
self.currency) return url_for("accounting.report.income-expenses-default",
currency=currency, account=self.account)
return url_for("accounting.report.income-expenses",
currency=currency, account=self.account,
period=self.period)
in_use: set[str] = set(db.session.scalars(
sa.select(JournalEntry.currency_code)
.group_by(JournalEntry.currency_code)).all())
return [OptionLink(str(x), get_url(x), x.code == self.currency.code)
for x in Currency.query.filter(Currency.code.in_(in_use))
.order_by(Currency.code).all()]
@property @property
def account_options(self) -> list[OptionLink]: def account_options(self) -> list[OptionLink]:
@ -333,12 +338,18 @@ class PageParams(BasePageParams):
:return: The account options. :return: The account options.
""" """
def get_url(account: IncomeExpensesAccount):
if self.period.is_default:
return url_for("accounting.report.income-expenses-default",
currency=self.currency, account=account)
return url_for("accounting.report.income-expenses",
currency=self.currency, account=account,
period=self.period)
current_al: IncomeExpensesAccount \ current_al: IncomeExpensesAccount \
= IncomeExpensesAccount.current_assets_and_liabilities() = IncomeExpensesAccount.current_assets_and_liabilities()
options: list[OptionLink] \ options: list[OptionLink] \
= [OptionLink(str(current_al), = [OptionLink(str(current_al), get_url(current_al),
income_expenses_url(self.currency, current_al,
self.period),
self.account.id == 0)] self.account.id == 0)]
in_use: sa.Select = sa.Select(JournalEntry.account_id)\ in_use: sa.Select = sa.Select(JournalEntry.account_id)\
.join(Account)\ .join(Account)\
@ -348,17 +359,35 @@ class PageParams(BasePageParams):
Account.base_code.startswith("21"), Account.base_code.startswith("21"),
Account.base_code.startswith("22")))\ Account.base_code.startswith("22")))\
.group_by(JournalEntry.account_id) .group_by(JournalEntry.account_id)
options.extend([OptionLink(str(x), options.extend([OptionLink(str(x), get_url(IncomeExpensesAccount(x)),
income_expenses_url(
self.currency,
IncomeExpensesAccount(x),
self.period),
x.id == self.account.id) x.id == self.account.id)
for x in Account.query.filter(Account.id.in_(in_use)) for x in Account.query.filter(Account.id.in_(in_use))
.order_by(Account.base_code, Account.no).all()]) .order_by(Account.base_code, Account.no).all()])
return options return options
def _populate_entries(entries: list[Entry]) -> None:
"""Populates the income and expenses entries with relative data.
:param entries: The income and expenses entries.
:return: None.
"""
transactions: dict[int, Transaction] \
= {x.id: x for x in Transaction.query.filter(
Transaction.id.in_({x.entry.transaction_id for x in entries
if x.entry is not None}))}
accounts: dict[int, Account] \
= {x.id: x for x in Account.query.filter(
Account.id.in_({x.entry.account_id for x in entries
if x.entry is not None}))}
for entry in entries:
if entry.entry is not None:
entry.transaction = transactions[entry.entry.transaction_id]
entry.date = entry.transaction.date
entry.note = entry.transaction.note
entry.account = accounts[entry.entry.account_id]
class IncomeExpenses(BaseReport): class IncomeExpenses(BaseReport):
"""The income and expenses log.""" """The income and expenses log."""
@ -378,11 +407,11 @@ class IncomeExpenses(BaseReport):
"""The period.""" """The period."""
collector: EntryCollector = EntryCollector( collector: EntryCollector = EntryCollector(
self.__currency, self.__account, self.__period) self.__currency, self.__account, self.__period)
self.__brought_forward: ReportEntry | None = collector.brought_forward self.__brought_forward: Entry | None = collector.brought_forward
"""The brought-forward entry.""" """The brought-forward entry."""
self.__entries: list[ReportEntry] = collector.entries self.__entries: list[Entry] = collector.entries
"""The report entries.""" """The log entries."""
self.__total: ReportEntry | None = collector.total self.__total: Entry | None = collector.total
"""The total entry.""" """The total entry."""
def csv(self) -> Response: def csv(self) -> Response:
@ -392,7 +421,7 @@ class IncomeExpenses(BaseReport):
""" """
filename: str = "income-expenses-{currency}-{account}-{period}.csv"\ filename: str = "income-expenses-{currency}-{account}-{period}.csv"\
.format(currency=self.__currency.code, account=self.__account.code, .format(currency=self.__currency.code, account=self.__account.code,
period=period_spec(self.__period)) period=self.__period.spec)
return csv_download(filename, self.__get_csv_rows()) return csv_download(filename, self.__get_csv_rows())
def __get_csv_rows(self) -> list[CSVRow]: def __get_csv_rows(self) -> list[CSVRow]:
@ -400,6 +429,7 @@ class IncomeExpenses(BaseReport):
:return: The CSV rows. :return: The CSV rows.
""" """
_populate_entries(self.__entries)
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Account"), rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Account"),
gettext("Summary"), gettext("Income"), gettext("Summary"), gettext("Income"),
gettext("Expense"), gettext("Balance"), gettext("Expense"), gettext("Balance"),
@ -426,31 +456,32 @@ class IncomeExpenses(BaseReport):
:return: The report as HTML. :return: The report as HTML.
""" """
all_entries: list[ReportEntry] = [] all_entries: list[Entry] = []
if self.__brought_forward is not None: if self.__brought_forward is not None:
all_entries.append(self.__brought_forward) all_entries.append(self.__brought_forward)
all_entries.extend(self.__entries) all_entries.extend(self.__entries)
if self.__total is not None: if self.__total is not None:
all_entries.append(self.__total) all_entries.append(self.__total)
pagination: Pagination[ReportEntry] \ pagination: Pagination[Entry] = Pagination[Entry](all_entries)
= Pagination[ReportEntry](all_entries) page_entries: list[Entry] = pagination.list
page_entries: list[ReportEntry] = pagination.list
has_data: bool = len(page_entries) > 0 has_data: bool = len(page_entries) > 0
brought_forward: ReportEntry | None = None _populate_entries(page_entries)
brought_forward: Entry | None = None
if len(page_entries) > 0 and page_entries[0].is_brought_forward: if len(page_entries) > 0 and page_entries[0].is_brought_forward:
brought_forward = page_entries[0] brought_forward = page_entries[0]
page_entries = page_entries[1:] page_entries = page_entries[1:]
total: ReportEntry | None = None total: Entry | None = None
if len(page_entries) > 0 and page_entries[-1].is_total: if len(page_entries) > 0 and page_entries[-1].is_total:
total = page_entries[-1] total = page_entries[-1]
page_entries = page_entries[:-1] page_entries = page_entries[:-1]
params: PageParams = PageParams(currency=self.__currency, params: IncomeExpensesPageParams = IncomeExpensesPageParams(
account=self.__account, currency=self.__currency,
period=self.__period, account=self.__account,
has_data=has_data, period=self.__period,
pagination=pagination, has_data=has_data,
brought_forward=brought_forward, pagination=pagination,
entries=page_entries, brought_forward=brought_forward,
total=total) entries=page_entries,
total=total)
return render_template("accounting/report/income-expenses.html", return render_template("accounting/report/income-expenses.html",
report=params) report=params)

View File

@ -20,28 +20,27 @@
from decimal import Decimal from decimal import Decimal
import sqlalchemy as sa import sqlalchemy as sa
from flask import render_template, Response from flask import url_for, render_template, Response
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, BaseAccount, Account, Transaction, \ from accounting.models import Currency, BaseAccount, Account, Transaction, \
JournalEntry JournalEntry
from accounting.report.period import Period from accounting.report.period import Period
from .utils.base_page_params import BasePageParams
from .utils.base_report import BaseReport from .utils.base_report import BaseReport
from .utils.csv_export import BaseCSVRow, csv_download, period_spec from .utils.csv_export import BaseCSVRow, csv_download
from .utils.urls import ledger_url, income_statement_url
from .utils.option_link import OptionLink from .utils.option_link import OptionLink
from .utils.page_params import PageParams
from .utils.period_choosers import IncomeStatementPeriodChooser from .utils.period_choosers import IncomeStatementPeriodChooser
from .utils.report_chooser import ReportChooser from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType from .utils.report_type import ReportType
class ReportAccount: class IncomeStatementAccount:
"""An account in the report.""" """An account in the income statement."""
def __init__(self, account: Account, amount: Decimal, url: str): def __init__(self, account: Account, amount: Decimal, url: str):
"""Constructs an account in the report. """Constructs an account in the income statement.
:param account: The account. :param account: The account.
:param amount: The amount. :param amount: The amount.
@ -55,11 +54,11 @@ class ReportAccount:
"""The URL to the ledger of the account.""" """The URL to the ledger of the account."""
class AccumulatedTotal: class IncomeStatementAccumulatedTotal:
"""An accumulated total.""" """An accumulated total in the income statement."""
def __init__(self, title: str): def __init__(self, title: str):
"""Constructs an accumulated total. """Constructs an accumulated total in the income statement.
:param title: The title. :param title: The title.
""" """
@ -69,17 +68,17 @@ class AccumulatedTotal:
"""The amount of the account.""" """The amount of the account."""
class Subsection: class IncomeStatementSubsection:
"""A subsection.""" """A subsection in the income statement."""
def __init__(self, title: BaseAccount): def __init__(self, title: BaseAccount):
"""Constructs a subsection. """Constructs a subsection in the income statement.
:param title: The title account. :param title: The title account.
""" """
self.title: BaseAccount = title self.title: BaseAccount = title
"""The title account.""" """The title account."""
self.accounts: list[ReportAccount] = [] self.accounts: list[IncomeStatementAccount] = []
"""The accounts in the subsection.""" """The accounts in the subsection."""
@property @property
@ -91,21 +90,21 @@ class Subsection:
return sum([x.amount for x in self.accounts]) return sum([x.amount for x in self.accounts])
class Section: class IncomeStatementSection:
"""A section.""" """A section in the income statement."""
def __init__(self, title: BaseAccount, accumulated_title: str): def __init__(self, title: BaseAccount, accumulated_title: str):
"""Constructs a section. """Constructs a section in the income statement.
:param title: The title account. :param title: The title account.
:param accumulated_title: The title for the accumulated total. :param accumulated_title: The title for the accumulated total.
""" """
self.title: BaseAccount = title self.title: BaseAccount = title
"""The title account.""" """The title account."""
self.subsections: list[Subsection] = [] self.subsections: list[IncomeStatementSubsection] = []
"""The subsections in the section.""" """The subsections in the section."""
self.accumulated: AccumulatedTotal \ self.accumulated: IncomeStatementAccumulatedTotal \
= AccumulatedTotal(accumulated_title) = IncomeStatementAccumulatedTotal(accumulated_title)
@property @property
def total(self) -> Decimal: def total(self) -> Decimal:
@ -117,10 +116,10 @@ class Section:
class CSVRow(BaseCSVRow): class CSVRow(BaseCSVRow):
"""A row in the CSV.""" """A row in the CSV income statement."""
def __init__(self, text: str | None, amount: str | Decimal | None): def __init__(self, text: str | None, amount: str | Decimal | None):
"""Constructs a row in the CSV. """Constructs a row in the CSV income statement.
:param text: The text. :param text: The text.
:param amount: The amount. :param amount: The amount.
@ -139,14 +138,14 @@ class CSVRow(BaseCSVRow):
return [self.text, self.amount] return [self.text, self.amount]
class PageParams(BasePageParams): class IncomeStatementPageParams(PageParams):
"""The HTML page parameters.""" """The HTML parameters of the income statement."""
def __init__(self, currency: Currency, def __init__(self, currency: Currency,
period: Period, period: Period,
has_data: bool, has_data: bool,
sections: list[Section], ): sections: list[IncomeStatementSection],):
"""Constructs the HTML page parameters. """Constructs the HTML parameters of the income statement.
:param currency: The currency. :param currency: The currency.
:param period: The period. :param period: The period.
@ -158,7 +157,7 @@ class PageParams(BasePageParams):
"""The period.""" """The period."""
self.__has_data: bool = has_data self.__has_data: bool = has_data
"""True if there is any data, or False otherwise.""" """True if there is any data, or False otherwise."""
self.sections: list[Section] = sections self.sections: list[IncomeStatementSection] = sections
self.period_chooser: IncomeStatementPeriodChooser \ self.period_chooser: IncomeStatementPeriodChooser \
= IncomeStatementPeriodChooser(currency) = IncomeStatementPeriodChooser(currency)
"""The period chooser.""" """The period chooser."""
@ -187,8 +186,19 @@ class PageParams(BasePageParams):
:return: The currency options. :return: The currency options.
""" """
return self._get_currency_options( def get_url(currency: Currency):
lambda x: income_statement_url(x, self.period), self.currency) if self.period.is_default:
return url_for("accounting.report.income-statement-default",
currency=currency)
return url_for("accounting.report.income-statement",
currency=currency, period=self.period)
in_use: set[str] = set(db.session.scalars(
sa.select(JournalEntry.currency_code)
.group_by(JournalEntry.currency_code)).all())
return [OptionLink(str(x), get_url(x), x.code == self.currency.code)
for x in Currency.query.filter(Currency.code.in_(in_use))
.order_by(Currency.code).all()]
class IncomeStatement(BaseReport): class IncomeStatement(BaseReport):
@ -206,7 +216,7 @@ class IncomeStatement(BaseReport):
"""The period.""" """The period."""
self.__has_data: bool self.__has_data: bool
"""True if there is any data, or False otherwise.""" """True if there is any data, or False otherwise."""
self.__sections: list[Section] self.__sections: list[IncomeStatementSection]
"""The sections.""" """The sections."""
self.__set_data() self.__set_data()
@ -215,7 +225,7 @@ class IncomeStatement(BaseReport):
:return: None. :return: None.
""" """
balances: list[ReportAccount] = self.__query_balances() balances: list[IncomeStatementAccount] = self.__query_balances()
titles: list[BaseAccount] = BaseAccount.query\ titles: list[BaseAccount] = BaseAccount.query\
.filter(BaseAccount.code.in_({"4", "5", "6", "7", "8", "9"})).all() .filter(BaseAccount.code.in_({"4", "5", "6", "7", "8", "9"})).all()
@ -224,17 +234,18 @@ class IncomeStatement(BaseReport):
for x in balances})).all() for x in balances})).all()
total_titles: dict[str, str] \ total_titles: dict[str, str] \
= {"4": gettext("total operating revenue"), = {"4": gettext("total revenue"),
"5": gettext("gross income"), "5": gettext("gross income"),
"6": gettext("operating income"), "6": gettext("operating income"),
"7": gettext("before tax income"), "7": gettext("before tax income"),
"8": gettext("after tax income"), "8": gettext("after tax income"),
"9": gettext("net income or loss for current period")} "9": gettext("net income or loss for current period")}
sections: dict[str, Section] \ sections: dict[str, IncomeStatementSection] \
= {x.code: Section(x, total_titles[x.code]) for x in titles} = {x.code: IncomeStatementSection(x, total_titles[x.code])
subsections: dict[str, Subsection] \ for x in titles}
= {x.code: Subsection(x) for x in subtitles} subsections: dict[str, IncomeStatementSubsection] \
= {x.code: IncomeStatementSubsection(x) for x in subtitles}
for subsection in subsections.values(): for subsection in subsections.values():
sections[subsection.title.code[0]].subsections.append(subsection) sections[subsection.title.code[0]].subsections.append(subsection)
for balance in balances: for balance in balances:
@ -247,7 +258,7 @@ class IncomeStatement(BaseReport):
total = total + section.total total = total + section.total
section.accumulated.amount = total section.accumulated.amount = total
def __query_balances(self) -> list[ReportAccount]: def __query_balances(self) -> list[IncomeStatementAccount]:
"""Queries and returns the balances. """Queries and returns the balances.
:return: The balances. :return: The balances.
@ -264,20 +275,33 @@ class IncomeStatement(BaseReport):
balance_func: sa.Function = sa.func.sum(sa.case( balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntry.is_debit, -JournalEntry.amount), (JournalEntry.is_debit, -JournalEntry.amount),
else_=JournalEntry.amount)).label("balance") else_=JournalEntry.amount)).label("balance")
select_balances: sa.Select = sa.select(Account.id, balance_func)\ select_balance: sa.Select \
= sa.select(JournalEntry.account_id, balance_func)\
.join(Transaction).join(Account)\ .join(Transaction).join(Account)\
.filter(*conditions)\ .filter(*conditions)\
.group_by(Account.id)\ .group_by(JournalEntry.account_id)\
.order_by(Account.base_code, Account.no) .order_by(Account.base_code, Account.no)
balances: list[sa.Row] = db.session.execute(select_balances).all() balances: list[sa.Row] = db.session.execute(select_balance).all()
accounts: dict[int, Account] \ accounts: dict[int, Account] \
= {x.id: x for x in Account.query = {x.id: x for x in Account.query
.filter(Account.id.in_([x.id for x in balances])).all()} .filter(Account.id.in_([x.account_id for x in balances])).all()}
return [ReportAccount(account=accounts[x.id],
amount=x.balance, def get_url(account: Account) -> str:
url=ledger_url(self.__currency, """Returns the ledger URL of an account.
accounts[x.id],
self.__period)) :param account: The account.
:return: The ledger URL of the account.
"""
if self.__period.is_default:
return url_for("accounting.report.ledger-default",
currency=self.__currency, account=account)
return url_for("accounting.report.ledger",
currency=self.__currency, account=account,
period=self.__period)
return [IncomeStatementAccount(account=accounts[x.account_id],
amount=x.balance,
url=get_url(accounts[x.account_id]))
for x in balances] for x in balances]
def csv(self) -> Response: def csv(self) -> Response:
@ -286,8 +310,7 @@ class IncomeStatement(BaseReport):
:return: The response of the report for download. :return: The response of the report for download.
""" """
filename: str = "income-statement-{currency}-{period}.csv"\ filename: str = "income-statement-{currency}-{period}.csv"\
.format(currency=self.__currency.code, .format(currency=self.__currency.code, period=self.__period.spec)
period=period_spec(self.__period))
return csv_download(filename, self.__get_csv_rows()) return csv_download(filename, self.__get_csv_rows())
def __get_csv_rows(self) -> list[CSVRow]: def __get_csv_rows(self) -> list[CSVRow]:
@ -316,9 +339,10 @@ class IncomeStatement(BaseReport):
:return: The report as HTML. :return: The report as HTML.
""" """
params: PageParams = PageParams(currency=self.__currency, params: IncomeStatementPageParams = IncomeStatementPageParams(
period=self.__period, currency=self.__currency,
has_data=self.__has_data, period=self.__period,
sections=self.__sections) has_data=self.__has_data,
sections=self.__sections)
return render_template("accounting/report/income-statement.html", return render_template("accounting/report/income-statement.html",
report=params) report=params)

View File

@ -22,48 +22,56 @@ from decimal import Decimal
import sqlalchemy as sa import sqlalchemy as sa
from flask import render_template, Response from flask import render_template, Response
from sqlalchemy.orm import selectinload
from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.period import Period from accounting.report.period import Period
from accounting.utils.pagination import Pagination from accounting.utils.pagination import Pagination
from .utils.base_page_params import BasePageParams
from .utils.base_report import BaseReport from .utils.base_report import BaseReport
from .utils.csv_export import BaseCSVRow, csv_download, period_spec from .utils.csv_export import BaseCSVRow, csv_download
from .utils.page_params import PageParams
from .utils.period_choosers import JournalPeriodChooser from .utils.period_choosers import JournalPeriodChooser
from .utils.report_chooser import ReportChooser from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType from .utils.report_type import ReportType
class ReportEntry: class Entry:
"""An entry in the report.""" """An entry in the journal."""
def __init__(self, entry: JournalEntry): def __init__(self, entry: JournalEntry | None = None):
"""Constructs the entry in the report. """Constructs the entry in the journal.
:param entry: The journal entry. :param entry: The journal entry.
""" """
self.entry: JournalEntry = entry self.entry: JournalEntry | None = None
"""The journal entry.""" """The journal entry."""
self.transaction: Transaction = entry.transaction self.transaction: Transaction | None = None
"""The transaction.""" """The transaction."""
self.currency: Currency = entry.currency self.is_total: bool = False
"""Whether this is the total entry."""
self.currency: Currency | None = None
"""The account.""" """The account."""
self.account: Account = entry.account self.account: Account | None = None
"""The account.""" """The account."""
self.summary: str | None = entry.summary self.summary: str | None = None
"""The summary.""" """The summary."""
self.debit: Decimal | None = entry.debit self.debit: Decimal | None = None
"""The debit amount.""" """The debit amount."""
self.credit: Decimal | None = entry.credit self.credit: Decimal | None = None
"""The credit amount.""" """The credit amount."""
self.amount: Decimal = entry.amount self.amount: Decimal | None = None
"""The amount.""" """The amount."""
if entry is not None:
self.entry = entry
self.summary = entry.summary
self.debit = entry.amount if entry.is_debit else None
self.credit = None if entry.is_debit else entry.amount
self.amount = entry.amount
class CSVRow(BaseCSVRow): class CSVRow(BaseCSVRow):
"""A row in the CSV.""" """A row in the CSV journal."""
def __init__(self, txn_date: str | date, def __init__(self, txn_date: str | date,
currency: str, currency: str,
@ -72,7 +80,7 @@ class CSVRow(BaseCSVRow):
debit: str | Decimal | None, debit: str | Decimal | None,
credit: str | Decimal | None, credit: str | Decimal | None,
note: str | None): note: str | None):
"""Constructs a row in the CSV. """Constructs a row in the CSV journal.
:param txn_date: The transaction date. :param txn_date: The transaction date.
:param summary: The summary. :param summary: The summary.
@ -105,22 +113,22 @@ class CSVRow(BaseCSVRow):
self.debit, self.credit, self.note] self.debit, self.credit, self.note]
class PageParams(BasePageParams): class JournalPageParams(PageParams):
"""The HTML page parameters.""" """The HTML parameters of the journal."""
def __init__(self, period: Period, def __init__(self, period: Period,
pagination: Pagination[JournalEntry], pagination: Pagination[Entry],
entries: list[JournalEntry]): entries: list[Entry]):
"""Constructs the HTML page parameters. """Constructs the HTML parameters of the journal.
:param period: The period. :param period: The period.
:param entries: The journal entries. :param entries: The journal entries.
""" """
self.period: Period = period self.period: Period = period
"""The period.""" """The period."""
self.pagination: Pagination[JournalEntry] = pagination self.pagination: Pagination[Entry] = pagination
"""The pagination.""" """The pagination."""
self.entries: list[JournalEntry] = entries self.entries: list[Entry] = entries
"""The entries.""" """The entries."""
self.period_chooser: JournalPeriodChooser \ self.period_chooser: JournalPeriodChooser \
= JournalPeriodChooser() = JournalPeriodChooser()
@ -144,21 +152,25 @@ class PageParams(BasePageParams):
period=self.period) period=self.period)
def get_csv_rows(entries: list[JournalEntry]) -> list[CSVRow]: def _populate_entries(entries: list[Entry]) -> None:
"""Composes and returns the CSV rows from the report entries. """Populates the journal entries with relative data.
:param entries: The report entries. :param entries: The journal entries.
:return: The CSV rows. :return: None.
""" """
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"), transactions: dict[int, Transaction] \
gettext("Account"), gettext("Summary"), = {x.id: x for x in Transaction.query.filter(
gettext("Debit"), gettext("Credit"), Transaction.id.in_({x.entry.transaction_id for x in entries}))}
gettext("Note"))] accounts: dict[int, Account] \
rows.extend([CSVRow(x.transaction.date, x.currency.code, = {x.id: x for x in Account.query.filter(
str(x.account).title(), x.summary, Account.id.in_({x.entry.account_id for x in entries}))}
x.debit, x.credit, x.transaction.note) currencies: dict[int, Currency] \
for x in entries]) = {x.code: x for x in Currency.query.filter(
return rows Currency.code.in_({x.entry.currency_code for x in entries}))}
for entry in entries:
entry.transaction = transactions[entry.entry.transaction_id]
entry.account = accounts[entry.entry.account_id]
entry.currency = currencies[entry.entry.currency_code]
class Journal(BaseReport): class Journal(BaseReport):
@ -169,12 +181,13 @@ class Journal(BaseReport):
:param period: The period. :param period: The period.
""" """
"""The account."""
self.__period: Period = period self.__period: Period = period
"""The period.""" """The period."""
self.__entries: list[JournalEntry] = self.__query_entries() self.__entries: list[Entry] = self.__query_entries()
"""The journal entries.""" """The journal entries."""
def __query_entries(self) -> list[JournalEntry]: def __query_entries(self) -> list[Entry]:
"""Queries and returns the journal entries. """Queries and returns the journal entries.
:return: The journal entries. :return: The journal entries.
@ -184,32 +197,47 @@ class Journal(BaseReport):
conditions.append(Transaction.date >= self.__period.start) conditions.append(Transaction.date >= self.__period.start)
if self.__period.end is not None: if self.__period.end is not None:
conditions.append(Transaction.date <= self.__period.end) conditions.append(Transaction.date <= self.__period.end)
return JournalEntry.query.join(Transaction)\ return [Entry(x) for x in db.session
.filter(*conditions)\ .query(JournalEntry).join(Transaction).filter(*conditions)
.order_by(Transaction.date, .order_by(Transaction.date,
JournalEntry.is_debit.desc(), JournalEntry.is_debit.desc(),
JournalEntry.no)\ JournalEntry.no).all()]
.options(selectinload(JournalEntry.account),
selectinload(JournalEntry.currency),
selectinload(JournalEntry.transaction)).all()
def csv(self) -> Response: def csv(self) -> Response:
"""Returns the report as CSV for download. """Returns the report as CSV for download.
:return: The response of the report for download. :return: The response of the report for download.
""" """
filename: str = f"journal-{period_spec(self.__period)}.csv" filename: str = f"journal-{self.__period.spec}.csv"
return csv_download(filename, get_csv_rows(self.__entries)) return csv_download(filename, self.__get_csv_rows())
def __get_csv_rows(self) -> list[CSVRow]:
"""Composes and returns the CSV rows.
:return: The CSV rows.
"""
_populate_entries(self.__entries)
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
gettext("Account"), gettext("Summary"),
gettext("Debit"), gettext("Credit"),
gettext("Note"))]
rows.extend([CSVRow(x.transaction.date, x.currency.code,
str(x.account).title(), x.summary,
x.debit, x.credit, x.transaction.note)
for x in self.__entries])
return rows
def html(self) -> str: def html(self) -> str:
"""Composes and returns the report as HTML. """Composes and returns the report as HTML.
:return: The report as HTML. :return: The report as HTML.
""" """
pagination: Pagination[JournalEntry] \ pagination: Pagination[Entry] = Pagination[Entry](self.__entries)
= Pagination[JournalEntry](self.__entries) page_entries: list[Entry] = pagination.list
params: PageParams = PageParams(period=self.__period, _populate_entries(page_entries)
pagination=pagination, params: JournalPageParams = JournalPageParams(
entries=pagination.list) period=self.__period,
pagination=pagination,
entries=page_entries)
return render_template("accounting/report/journal.html", return render_template("accounting/report/journal.html",
report=params) report=params)

View File

@ -22,37 +22,41 @@ from decimal import Decimal
import sqlalchemy as sa import sqlalchemy as sa
from flask import url_for, render_template, Response from flask import url_for, render_template, Response
from sqlalchemy.orm import selectinload
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.period import Period from accounting.report.period import Period
from accounting.utils.pagination import Pagination from accounting.utils.pagination import Pagination
from .utils.base_page_params import BasePageParams
from .utils.base_report import BaseReport from .utils.base_report import BaseReport
from .utils.csv_export import BaseCSVRow, csv_download, period_spec from .utils.csv_export import BaseCSVRow, csv_download
from .utils.urls import ledger_url
from .utils.option_link import OptionLink from .utils.option_link import OptionLink
from .utils.page_params import PageParams
from .utils.period_choosers import LedgerPeriodChooser from .utils.period_choosers import LedgerPeriodChooser
from .utils.report_chooser import ReportChooser from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType from .utils.report_type import ReportType
class ReportEntry: class Entry:
"""An entry in the report.""" """An entry in the ledger."""
def __init__(self, entry: JournalEntry | None = None): def __init__(self, entry: JournalEntry | None = None):
"""Constructs the entry in the report. """Constructs the entry in the ledger.
:param entry: The journal entry. :param entry: The journal entry.
""" """
self.entry: JournalEntry | None = None
"""The journal entry."""
self.transaction: Transaction | None = None
"""The transaction."""
self.is_brought_forward: bool = False self.is_brought_forward: bool = False
"""Whether this is the brought-forward entry.""" """Whether this is the brought-forward entry."""
self.is_total: bool = False self.is_total: bool = False
"""Whether this is the total entry.""" """Whether this is the total entry."""
self.date: date | None = None self.date: date | None = None
"""The date.""" """The date."""
self.account: Account | None = None
"""The account."""
self.summary: str | None = None self.summary: str | None = None
"""The summary.""" """The summary."""
self.debit: Decimal | None = None self.debit: Decimal | None = None
@ -63,23 +67,18 @@ class ReportEntry:
"""The balance.""" """The balance."""
self.note: str | None = None self.note: str | None = None
"""The note.""" """The note."""
self.url: str | None = None
"""The URL to the journal entry."""
if entry is not None: if entry is not None:
self.date = entry.transaction.date self.entry = entry
self.summary = entry.summary self.summary = entry.summary
self.debit = entry.amount if entry.is_debit else None self.debit = entry.amount if entry.is_debit else None
self.credit = None if entry.is_debit else entry.amount self.credit = None if entry.is_debit else entry.amount
self.note = entry.transaction.note
self.url = url_for("accounting.transaction.detail",
txn=entry.transaction)
class EntryCollector: class EntryCollector:
"""The report entry collector.""" """The ledger entry collector."""
def __init__(self, currency: Currency, account: Account, period: Period): def __init__(self, currency: Currency, account: Account, period: Period):
"""Constructs the report entry collector. """Constructs the ledger entry collector.
:param currency: The currency. :param currency: The currency.
:param account: The account. :param account: The account.
@ -91,21 +90,21 @@ class EntryCollector:
"""The account.""" """The account."""
self.__period: Period = period self.__period: Period = period
"""The period""" """The period"""
self.brought_forward: ReportEntry | None self.brought_forward: Entry | None
"""The brought-forward entry.""" """The brought-forward entry."""
self.entries: list[ReportEntry] self.entries: list[Entry]
"""The report entries.""" """The ledger entries."""
self.total: ReportEntry | None self.total: Entry | None
"""The total entry.""" """The total entry."""
self.brought_forward = self.__get_brought_forward_entry() self.brought_forward = self.__get_brought_forward_entry()
self.entries = self.__query_entries() self.entries = self.__query_entries()
self.total = self.__get_total_entry() self.total = self.__get_total_entry()
self.__populate_balance() self.__populate_balance()
def __get_brought_forward_entry(self) -> ReportEntry | None: def __get_brought_forward_entry(self) -> Entry | None:
"""Queries, composes and returns the brought-forward entry. """Queries, composes and returns the brought-forward entry.
:return: The brought-forward entry, or None if the report starts from :return: The brought-forward entry, or None if the ledger starts from
the beginning. the beginning.
""" """
if self.__period.start is None: if self.__period.start is None:
@ -120,7 +119,7 @@ class EntryCollector:
balance: int | None = db.session.scalar(select) balance: int | None = db.session.scalar(select)
if balance is None: if balance is None:
return None return None
entry: ReportEntry = ReportEntry() entry: Entry = Entry()
entry.is_brought_forward = True entry.is_brought_forward = True
entry.date = self.__period.start entry.date = self.__period.start
entry.summary = gettext("Brought forward") entry.summary = gettext("Brought forward")
@ -131,10 +130,10 @@ class EntryCollector:
entry.balance = balance entry.balance = balance
return entry return entry
def __query_entries(self) -> list[ReportEntry]: def __query_entries(self) -> list[Entry]:
"""Queries and returns the report entries. """Queries and returns the ledger entries.
:return: The report entries. :return: The ledger entries.
""" """
conditions: list[sa.BinaryExpression] \ conditions: list[sa.BinaryExpression] \
= [JournalEntry.currency_code == self.__currency.code, = [JournalEntry.currency_code == self.__currency.code,
@ -143,21 +142,20 @@ class EntryCollector:
conditions.append(Transaction.date >= self.__period.start) conditions.append(Transaction.date >= self.__period.start)
if self.__period.end is not None: if self.__period.end is not None:
conditions.append(Transaction.date <= self.__period.end) conditions.append(Transaction.date <= self.__period.end)
return [ReportEntry(x) for x in JournalEntry.query.join(Transaction) return [Entry(x) for x in JournalEntry.query.join(Transaction)
.filter(*conditions) .filter(*conditions)
.order_by(Transaction.date, .order_by(Transaction.date,
JournalEntry.is_debit.desc(), JournalEntry.is_debit.desc(),
JournalEntry.no) JournalEntry.no).all()]
.options(selectinload(JournalEntry.transaction)).all()]
def __get_total_entry(self) -> ReportEntry | None: def __get_total_entry(self) -> Entry | None:
"""Composes the total entry. """Composes the total entry.
:return: The total entry, or None if there is no data. :return: The total entry, or None if there is no data.
""" """
if self.brought_forward is None and len(self.entries) == 0: if self.brought_forward is None and len(self.entries) == 0:
return None return None
entry: ReportEntry = ReportEntry() entry: Entry = Entry()
entry.is_total = True entry.is_total = True
entry.summary = gettext("Total") entry.summary = gettext("Total")
entry.debit = sum([x.debit for x in self.entries entry.debit = sum([x.debit for x in self.entries
@ -185,7 +183,7 @@ class EntryCollector:
class CSVRow(BaseCSVRow): class CSVRow(BaseCSVRow):
"""A row in the CSV.""" """A row in the CSV ledger."""
def __init__(self, txn_date: date | str | None, def __init__(self, txn_date: date | str | None,
summary: str | None, summary: str | None,
@ -193,7 +191,7 @@ class CSVRow(BaseCSVRow):
credit: str | Decimal | None, credit: str | Decimal | None,
balance: str | Decimal | None, balance: str | Decimal | None,
note: str | None): note: str | None):
"""Constructs a row in the CSV. """Constructs a row in the CSV ledger.
:param txn_date: The transaction date. :param txn_date: The transaction date.
:param summary: The summary. :param summary: The summary.
@ -225,25 +223,25 @@ class CSVRow(BaseCSVRow):
self.debit, self.credit, self.balance, self.note] self.debit, self.credit, self.balance, self.note]
class PageParams(BasePageParams): class LedgerPageParams(PageParams):
"""The HTML page parameters.""" """The HTML parameters of the ledger."""
def __init__(self, currency: Currency, def __init__(self, currency: Currency,
account: Account, account: Account,
period: Period, period: Period,
has_data: bool, has_data: bool,
pagination: Pagination[ReportEntry], pagination: Pagination[Entry],
brought_forward: ReportEntry | None, brought_forward: Entry | None,
entries: list[ReportEntry], entries: list[Entry],
total: ReportEntry | None): total: Entry | None):
"""Constructs the HTML page parameters. """Constructs the HTML parameters of the ledger.
:param currency: The currency. :param currency: The currency.
:param account: The account. :param account: The account.
:param period: The period. :param period: The period.
:param has_data: True if there is any data, or False otherwise. :param has_data: True if there is any data, or False otherwise.
:param brought_forward: The brought-forward entry. :param brought_forward: The brought-forward entry.
:param entries: The report entries. :param entries: The ledger entries.
:param total: The total entry. :param total: The total entry.
""" """
self.currency: Currency = currency self.currency: Currency = currency
@ -254,13 +252,13 @@ class PageParams(BasePageParams):
"""The period.""" """The period."""
self.__has_data: bool = has_data self.__has_data: bool = has_data
"""True if there is any data, or False otherwise.""" """True if there is any data, or False otherwise."""
self.pagination: Pagination[ReportEntry] = pagination self.pagination: Pagination[Entry] = pagination
"""The pagination.""" """The pagination."""
self.brought_forward: ReportEntry | None = brought_forward self.brought_forward: Entry | None = brought_forward
"""The brought-forward entry.""" """The brought-forward entry."""
self.entries: list[ReportEntry] = entries self.entries: list[Entry] = entries
"""The entries.""" """The entries."""
self.total: ReportEntry | None = total self.total: Entry | None = total
"""The total entry.""" """The total entry."""
self.period_chooser: LedgerPeriodChooser \ self.period_chooser: LedgerPeriodChooser \
= LedgerPeriodChooser(currency, account) = LedgerPeriodChooser(currency, account)
@ -291,8 +289,20 @@ class PageParams(BasePageParams):
:return: The currency options. :return: The currency options.
""" """
return self._get_currency_options( def get_url(currency: Currency):
lambda x: ledger_url(x, self.account, self.period), self.currency) if self.period.is_default:
return url_for("accounting.report.ledger-default",
currency=currency, account=self.account)
return url_for("accounting.report.ledger",
currency=currency, account=self.account,
period=self.period)
in_use: set[str] = set(db.session.scalars(
sa.select(JournalEntry.currency_code)
.group_by(JournalEntry.currency_code)).all())
return [OptionLink(str(x), get_url(x), x.code == self.currency.code)
for x in Currency.query.filter(Currency.code.in_(in_use))
.order_by(Currency.code).all()]
@property @property
def account_options(self) -> list[OptionLink]: def account_options(self) -> list[OptionLink]:
@ -300,15 +310,39 @@ class PageParams(BasePageParams):
:return: The account options. :return: The account options.
""" """
def get_url(account: Account):
if self.period.is_default:
return url_for("accounting.report.ledger-default",
currency=self.currency, account=account)
return url_for("accounting.report.ledger",
currency=self.currency, account=account,
period=self.period)
in_use: sa.Select = sa.Select(JournalEntry.account_id)\ in_use: sa.Select = sa.Select(JournalEntry.account_id)\
.filter(JournalEntry.currency_code == self.currency.code)\ .filter(JournalEntry.currency_code == self.currency.code)\
.group_by(JournalEntry.account_id) .group_by(JournalEntry.account_id)
return [OptionLink(str(x), ledger_url(self.currency, x, self.period), return [OptionLink(str(x), get_url(x), x.id == self.account.id)
x.id == self.account.id)
for x in Account.query.filter(Account.id.in_(in_use)) for x in Account.query.filter(Account.id.in_(in_use))
.order_by(Account.base_code, Account.no).all()] .order_by(Account.base_code, Account.no).all()]
def _populate_entries(entries: list[Entry]) -> None:
"""Populates the ledger entries with relative data.
:param entries: The ledger entries.
:return: None.
"""
transactions: dict[int, Transaction] \
= {x.id: x for x in Transaction.query.filter(
Transaction.id.in_({x.entry.transaction_id for x in entries
if x.entry is not None}))}
for entry in entries:
if entry.entry is not None:
entry.transaction = transactions[entry.entry.transaction_id]
entry.date = entry.transaction.date
entry.note = entry.transaction.note
class Ledger(BaseReport): class Ledger(BaseReport):
"""The ledger.""" """The ledger."""
@ -327,11 +361,11 @@ class Ledger(BaseReport):
"""The period.""" """The period."""
collector: EntryCollector = EntryCollector( collector: EntryCollector = EntryCollector(
self.__currency, self.__account, self.__period) self.__currency, self.__account, self.__period)
self.__brought_forward: ReportEntry | None = collector.brought_forward self.__brought_forward: Entry | None = collector.brought_forward
"""The brought-forward entry.""" """The brought-forward entry."""
self.__entries: list[ReportEntry] = collector.entries self.__entries: list[Entry] = collector.entries
"""The report entries.""" """The ledger entries."""
self.__total: ReportEntry | None = collector.total self.__total: Entry | None = collector.total
"""The total entry.""" """The total entry."""
def csv(self) -> Response: def csv(self) -> Response:
@ -341,7 +375,7 @@ class Ledger(BaseReport):
""" """
filename: str = "ledger-{currency}-{account}-{period}.csv"\ filename: str = "ledger-{currency}-{account}-{period}.csv"\
.format(currency=self.__currency.code, account=self.__account.code, .format(currency=self.__currency.code, account=self.__account.code,
period=period_spec(self.__period)) period=self.__period.spec)
return csv_download(filename, self.__get_csv_rows()) return csv_download(filename, self.__get_csv_rows())
def __get_csv_rows(self) -> list[CSVRow]: def __get_csv_rows(self) -> list[CSVRow]:
@ -349,6 +383,7 @@ class Ledger(BaseReport):
:return: The CSV rows. :return: The CSV rows.
""" """
_populate_entries(self.__entries)
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Summary"), rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Summary"),
gettext("Debit"), gettext("Credit"), gettext("Debit"), gettext("Credit"),
gettext("Balance"), gettext("Note"))] gettext("Balance"), gettext("Note"))]
@ -373,31 +408,32 @@ class Ledger(BaseReport):
:return: The report as HTML. :return: The report as HTML.
""" """
all_entries: list[ReportEntry] = [] all_entries: list[Entry] = []
if self.__brought_forward is not None: if self.__brought_forward is not None:
all_entries.append(self.__brought_forward) all_entries.append(self.__brought_forward)
all_entries.extend(self.__entries) all_entries.extend(self.__entries)
if self.__total is not None: if self.__total is not None:
all_entries.append(self.__total) all_entries.append(self.__total)
pagination: Pagination[ReportEntry] \ pagination: Pagination[Entry] = Pagination[Entry](all_entries)
= Pagination[ReportEntry](all_entries) page_entries: list[Entry] = pagination.list
page_entries: list[ReportEntry] = pagination.list
has_data: bool = len(page_entries) > 0 has_data: bool = len(page_entries) > 0
brought_forward: ReportEntry | None = None _populate_entries(page_entries)
brought_forward: Entry | None = None
if len(page_entries) > 0 and page_entries[0].is_brought_forward: if len(page_entries) > 0 and page_entries[0].is_brought_forward:
brought_forward = page_entries[0] brought_forward = page_entries[0]
page_entries = page_entries[1:] page_entries = page_entries[1:]
total: ReportEntry | None = None total: Entry | None = None
if len(page_entries) > 0 and page_entries[-1].is_total: if len(page_entries) > 0 and page_entries[-1].is_total:
total = page_entries[-1] total = page_entries[-1]
page_entries = page_entries[:-1] page_entries = page_entries[:-1]
params: PageParams = PageParams(currency=self.__currency, params: LedgerPageParams = LedgerPageParams(
account=self.__account, currency=self.__currency,
period=self.__period, account=self.__account,
has_data=has_data, period=self.__period,
pagination=pagination, has_data=has_data,
brought_forward=brought_forward, pagination=pagination,
entries=page_entries, brought_forward=brought_forward,
total=total) entries=page_entries,
total=total)
return render_template("accounting/report/ledger.html", return render_template("accounting/report/ledger.html",
report=params) report=params)

View File

@ -17,35 +17,163 @@
"""The search. """The search.
""" """
from datetime import datetime from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
import sqlalchemy as sa import sqlalchemy as sa
from flask import Response, render_template, request from flask import Response, render_template, request
from sqlalchemy.orm import selectinload
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \ from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
Transaction, JournalEntry Transaction, JournalEntry
from accounting.utils.pagination import Pagination from accounting.utils.pagination import Pagination
from accounting.utils.query import parse_query_keywords from accounting.utils.query import parse_query_keywords
from .journal import get_csv_rows
from .utils.base_page_params import BasePageParams
from .utils.base_report import BaseReport from .utils.base_report import BaseReport
from .utils.csv_export import csv_download from .utils.csv_export import BaseCSVRow, csv_download
from .utils.page_params import PageParams
from .utils.report_chooser import ReportChooser from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType from .utils.report_type import ReportType
class EntryCollector: class Entry:
"""The report entry collector.""" """An entry in the search result."""
def __init__(self, entry: JournalEntry | None = None):
"""Constructs the entry in the search result.
:param entry: The journal entry.
"""
self.entry: JournalEntry | None = None
"""The journal entry."""
self.transaction: Transaction | None = None
"""The transaction."""
self.is_total: bool = False
"""Whether this is the total entry."""
self.currency: Currency | None = None
"""The account."""
self.account: Account | None = None
"""The account."""
self.summary: str | None = None
"""The summary."""
self.debit: Decimal | None = None
"""The debit amount."""
self.credit: Decimal | None = None
"""The credit amount."""
self.amount: Decimal | None = None
"""The amount."""
if entry is not None:
self.entry = entry
self.summary = entry.summary
self.debit = entry.amount if entry.is_debit else None
self.credit = None if entry.is_debit else entry.amount
self.amount = entry.amount
class CSVRow(BaseCSVRow):
"""A row in the CSV search result."""
def __init__(self, txn_date: str | date,
currency: str,
account: str,
summary: str | None,
debit: str | Decimal | None,
credit: str | Decimal | None,
note: str | None):
"""Constructs a row in the CSV search result.
:param txn_date: The transaction date.
:param summary: The summary.
:param debit: The debit amount.
:param credit: The credit amount.
:param note: The note.
"""
self.date: str | date = txn_date
"""The date."""
self.currency: str = currency
"""The currency."""
self.account: str = account
"""The account."""
self.summary: str | None = summary
"""The summary."""
self.debit: str | Decimal | None = debit
"""The debit amount."""
self.credit: str | Decimal | None = credit
"""The credit amount."""
self.note: str | None = note
"""The note."""
@property
def values(self) -> list[str | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.
"""
return [self.date, self.currency, self.account, self.summary,
self.debit, self.credit, self.note]
class SearchPageParams(PageParams):
"""The HTML parameters of the search result."""
def __init__(self, pagination: Pagination[Entry],
entries: list[Entry]):
"""Constructs the HTML parameters of the search result.
:param entries: The search result entries.
"""
self.pagination: Pagination[Entry] = pagination
"""The pagination."""
self.entries: list[Entry] = entries
"""The entries."""
@property
def has_data(self) -> bool:
"""Returns whether there is any data on the page.
:return: True if there is any data, or False otherwise.
"""
return len(self.entries) > 0
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
return ReportChooser(ReportType.SEARCH)
def _populate_entries(entries: list[Entry]) -> None:
"""Populates the search result entries with relative data.
:param entries: The search result entries.
:return: None.
"""
transactions: dict[int, Transaction] \
= {x.id: x for x in Transaction.query.filter(
Transaction.id.in_({x.entry.transaction_id for x in entries}))}
accounts: dict[int, Account] \
= {x.id: x for x in Account.query.filter(
Account.id.in_({x.entry.account_id for x in entries}))}
currencies: dict[int, Currency] \
= {x.code: x for x in Currency.query.filter(
Currency.code.in_({x.entry.currency_code for x in entries}))}
for entry in entries:
entry.transaction = transactions[entry.entry.transaction_id]
entry.account = accounts[entry.entry.account_id]
entry.currency = currencies[entry.entry.currency_code]
class Search(BaseReport):
"""The search."""
def __init__(self): def __init__(self):
"""Constructs the report entry collector.""" """Constructs a search."""
self.entries: list[JournalEntry] = self.__query_entries() """The account."""
"""The report entries.""" self.__entries: list[Entry] = self.__query_entries()
"""The journal entries."""
def __query_entries(self) -> list[JournalEntry]: def __query_entries(self) -> list[Entry]:
"""Queries and returns the journal entries. """Queries and returns the journal entries.
:return: The journal entries. :return: The journal entries.
@ -55,23 +183,15 @@ class EntryCollector:
return [] return []
conditions: list[sa.BinaryExpression] = [] conditions: list[sa.BinaryExpression] = []
for k in keywords: for k in keywords:
sub_conditions: list[sa.BinaryExpression] \ conditions.append(sa.or_(
= [JournalEntry.summary.contains(k), JournalEntry.summary.contains(k),
JournalEntry.account_id.in_( sa.cast(JournalEntry.amount, sa.String).contains(k),
self.__get_account_condition(k)), JournalEntry.account_id.in_(self.__get_account_condition(k)),
JournalEntry.currency_code.in_( JournalEntry.currency_code.in_(
self.__get_currency_condition(k)), self.__get_currency_condition(k)),
JournalEntry.transaction_id.in_( JournalEntry.transaction_id.in_(
self.__get_transaction_condition(k))] self.__get_transaction_condition(k))))
try: return [Entry(x) for x in JournalEntry.query.filter(*conditions)]
sub_conditions.append(JournalEntry.amount == Decimal(k))
except ArithmeticError:
pass
conditions.append(sa.or_(*sub_conditions))
return JournalEntry.query.filter(*conditions)\
.options(selectinload(JournalEntry.account),
selectinload(JournalEntry.currency),
selectinload(JournalEntry.transaction)).all()
@staticmethod @staticmethod
def __get_account_condition(k: str) -> sa.Select: def __get_account_condition(k: str) -> sa.Select:
@ -116,7 +236,8 @@ class EntryCollector:
:param k: The keyword. :param k: The keyword.
:return: The condition to filter the transaction. :return: The condition to filter the transaction.
""" """
conditions: list[sa.BinaryExpression] = [Transaction.note.contains(k)] conditions: list[sa.BinaryExpression] \
= [Transaction.note.contains(k)]
txn_date: datetime txn_date: datetime
try: try:
txn_date = datetime.strptime(k, "%Y") txn_date = datetime.strptime(k, "%Y")
@ -140,62 +261,39 @@ class EntryCollector:
pass pass
return sa.select(Transaction.id).filter(sa.or_(*conditions)) return sa.select(Transaction.id).filter(sa.or_(*conditions))
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, pagination: Pagination[JournalEntry],
entries: list[JournalEntry]):
"""Constructs the HTML page parameters.
:param entries: The search result entries.
"""
self.pagination: Pagination[JournalEntry] = pagination
"""The pagination."""
self.entries: list[JournalEntry] = entries
"""The entries."""
@property
def has_data(self) -> bool:
"""Returns whether there is any data on the page.
:return: True if there is any data, or False otherwise.
"""
return len(self.entries) > 0
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
return ReportChooser(ReportType.SEARCH)
class Search(BaseReport):
"""The search."""
def __init__(self):
"""Constructs a search."""
self.__entries: list[JournalEntry] = EntryCollector().entries
"""The journal entries."""
def csv(self) -> Response: def csv(self) -> Response:
"""Returns the report as CSV for download. """Returns the report as CSV for download.
:return: The response of the report for download. :return: The response of the report for download.
""" """
filename: str = "search-{q}.csv".format(q=request.args["q"]) filename: str = "search-{q}.csv".format(q=request.args["q"])
return csv_download(filename, get_csv_rows(self.__entries)) return csv_download(filename, self.__get_csv_rows())
def __get_csv_rows(self) -> list[CSVRow]:
"""Composes and returns the CSV rows.
:return: The CSV rows.
"""
_populate_entries(self.__entries)
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
gettext("Account"), gettext("Summary"),
gettext("Debit"), gettext("Credit"),
gettext("Note"))]
rows.extend([CSVRow(x.transaction.date, x.currency.code,
str(x.account).title(), x.summary,
x.debit, x.credit, x.transaction.note)
for x in self.__entries])
return rows
def html(self) -> str: def html(self) -> str:
"""Composes and returns the report as HTML. """Composes and returns the report as HTML.
:return: The report as HTML. :return: The report as HTML.
""" """
pagination: Pagination[JournalEntry] \ pagination: Pagination[Entry] = Pagination[Entry](self.__entries)
= Pagination[JournalEntry](self.__entries) page_entries: list[Entry] = pagination.list
params: PageParams = PageParams(pagination=pagination, _populate_entries(page_entries)
entries=pagination.list) params: SearchPageParams = SearchPageParams(pagination=pagination,
entries=page_entries)
return render_template("accounting/report/search.html", return render_template("accounting/report/search.html",
report=params) report=params)

View File

@ -20,27 +20,26 @@
from decimal import Decimal from decimal import Decimal
import sqlalchemy as sa import sqlalchemy as sa
from flask import Response, render_template from flask import url_for, Response, render_template
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.period import Period from accounting.report.period import Period
from .utils.base_page_params import BasePageParams
from .utils.base_report import BaseReport from .utils.base_report import BaseReport
from .utils.csv_export import BaseCSVRow, csv_download, period_spec from .utils.csv_export import BaseCSVRow, csv_download
from .utils.urls import ledger_url, trial_balance_url
from .utils.option_link import OptionLink from .utils.option_link import OptionLink
from .utils.page_params import PageParams
from .utils.period_choosers import TrialBalancePeriodChooser from .utils.period_choosers import TrialBalancePeriodChooser
from .utils.report_chooser import ReportChooser from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType from .utils.report_type import ReportType
class ReportAccount: class TrialBalanceAccount:
"""An account in the report.""" """An account in the trial balance."""
def __init__(self, account: Account, amount: Decimal, url: str): def __init__(self, account: Account, amount: Decimal, url: str):
"""Constructs an account in the report. """Constructs an account in the trial balance.
:param account: The account. :param account: The account.
:param amount: The amount. :param amount: The amount.
@ -56,8 +55,8 @@ class ReportAccount:
"""The URL to the ledger of the account.""" """The URL to the ledger of the account."""
class Total: class TrialBalanceTotal:
"""The totals.""" """The total in the trial balance."""
def __init__(self, debit: Decimal, credit: Decimal): def __init__(self, debit: Decimal, credit: Decimal):
"""Constructs the total in the trial balance. """Constructs the total in the trial balance.
@ -72,12 +71,12 @@ class Total:
class CSVRow(BaseCSVRow): class CSVRow(BaseCSVRow):
"""A row in the CSV.""" """A row in the CSV trial balance."""
def __init__(self, text: str | None, def __init__(self, text: str | None,
debit: str | Decimal | None, debit: str | Decimal | None,
credit: str | Decimal | None): credit: str | Decimal | None):
"""Constructs a row in the CSV. """Constructs a row in the CSV trial balance.
:param text: The text. :param text: The text.
:param debit: The debit amount. :param debit: The debit amount.
@ -99,14 +98,14 @@ class CSVRow(BaseCSVRow):
return [self.text, self.debit, self.credit] return [self.text, self.debit, self.credit]
class PageParams(BasePageParams): class TrialBalancePageParams(PageParams):
"""The HTML page parameters.""" """The HTML parameters of the trial balance."""
def __init__(self, currency: Currency, def __init__(self, currency: Currency,
period: Period, period: Period,
accounts: list[ReportAccount], accounts: list[TrialBalanceAccount],
total: Total): total: TrialBalanceTotal):
"""Constructs the HTML page parameters. """Constructs the HTML parameters of the trial balance.
:param currency: The currency. :param currency: The currency.
:param period: The period. :param period: The period.
@ -117,9 +116,9 @@ class PageParams(BasePageParams):
"""The currency.""" """The currency."""
self.period: Period = period self.period: Period = period
"""The period.""" """The period."""
self.accounts: list[ReportAccount] = accounts self.accounts: list[TrialBalanceAccount] = accounts
"""The accounts in the trial balance.""" """The accounts in the trial balance."""
self.total: Total = total self.total: TrialBalanceTotal = total
"""The total of the trial balance.""" """The total of the trial balance."""
self.period_chooser: TrialBalancePeriodChooser \ self.period_chooser: TrialBalancePeriodChooser \
= TrialBalancePeriodChooser(currency) = TrialBalancePeriodChooser(currency)
@ -149,8 +148,19 @@ class PageParams(BasePageParams):
:return: The currency options. :return: The currency options.
""" """
return self._get_currency_options( def get_url(currency: Currency):
lambda x: trial_balance_url(x, self.period), self.currency) if self.period.is_default:
return url_for("accounting.report.trial-balance-default",
currency=currency)
return url_for("accounting.report.trial-balance",
currency=currency, period=self.period)
in_use: set[str] = set(db.session.scalars(
sa.select(JournalEntry.currency_code)
.group_by(JournalEntry.currency_code)).all())
return [OptionLink(str(x), get_url(x), x.code == self.currency.code)
for x in Currency.query.filter(Currency.code.in_(in_use))
.order_by(Currency.code).all()]
class TrialBalance(BaseReport): class TrialBalance(BaseReport):
@ -166,9 +176,9 @@ class TrialBalance(BaseReport):
"""The currency.""" """The currency."""
self.__period: Period = period self.__period: Period = period
"""The period.""" """The period."""
self.__accounts: list[ReportAccount] self.__accounts: list[TrialBalanceAccount]
"""The accounts in the trial balance.""" """The accounts in the trial balance."""
self.__total: Total self.__total: TrialBalanceTotal
"""The total of the trial balance.""" """The total of the trial balance."""
self.__set_data() self.__set_data()
@ -186,22 +196,35 @@ class TrialBalance(BaseReport):
balance_func: sa.Function = sa.func.sum(sa.case( balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntry.is_debit, JournalEntry.amount), (JournalEntry.is_debit, JournalEntry.amount),
else_=-JournalEntry.amount)).label("balance") else_=-JournalEntry.amount)).label("balance")
select_balances: sa.Select = sa.select(Account.id, balance_func)\ select_balances: sa.Select \
= sa.select(Account.id, balance_func)\
.join(Transaction).join(Account)\ .join(Transaction).join(Account)\
.filter(*conditions)\ .filter(*conditions)\
.group_by(Account.id)\ .group_by(JournalEntry.account_id)\
.order_by(Account.base_code, Account.no) .order_by(Account.base_code, Account.no)
balances: list[sa.Row] = db.session.execute(select_balances).all() balances: list[sa.Row] = db.session.execute(select_balances).all()
accounts: dict[int, Account] \ accounts: dict[int, Account] \
= {x.id: x for x in Account.query = {x.id: x for x in Account.query
.filter(Account.id.in_([x.id for x in balances])).all()} .filter(Account.id.in_([x.id for x in balances])).all()}
self.__accounts = [ReportAccount(account=accounts[x.id],
amount=x.balance, def get_url(account: Account) -> str:
url=ledger_url(self.__currency, """Returns the ledger URL of an account.
accounts[x.id],
self.__period)) :param account: The account.
:return: The ledger URL of the account.
"""
if self.__period.is_default:
return url_for("accounting.report.ledger-default",
currency=self.__currency, account=account)
return url_for("accounting.report.ledger",
currency=self.__currency, account=account,
period=self.__period)
self.__accounts = [TrialBalanceAccount(account=accounts[x.id],
amount=x.balance,
url=get_url(accounts[x.id]))
for x in balances] for x in balances]
self.__total = Total( self.__total = TrialBalanceTotal(
sum([x.debit for x in self.__accounts if x.debit is not None]), sum([x.debit for x in self.__accounts if x.debit is not None]),
sum([x.credit for x in self.__accounts if x.credit is not None])) sum([x.credit for x in self.__accounts if x.credit is not None]))
@ -211,8 +234,7 @@ class TrialBalance(BaseReport):
:return: The response of the report for download. :return: The response of the report for download.
""" """
filename: str = "trial-balance-{currency}-{period}.csv"\ filename: str = "trial-balance-{currency}-{period}.csv"\
.format(currency=self.__currency.code, .format(currency=self.__currency.code, period=self.__period.spec)
period=period_spec(self.__period))
return csv_download(filename, self.__get_csv_rows()) return csv_download(filename, self.__get_csv_rows())
def __get_csv_rows(self) -> list[CSVRow]: def __get_csv_rows(self) -> list[CSVRow]:
@ -233,9 +255,10 @@ class TrialBalance(BaseReport):
:return: The report as HTML. :return: The report as HTML.
""" """
params: PageParams = PageParams(currency=self.__currency, params: TrialBalancePageParams = TrialBalancePageParams(
period=self.__period, currency=self.__currency,
accounts=self.__accounts, period=self.__period,
total=self.__total) accounts=self.__accounts,
total=self.__total)
return render_template("accounting/report/trial-balance.html", return render_template("accounting/report/trial-balance.html",
report=params) report=params)

View File

@ -14,19 +14,16 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""The utilities to export the report as CSV for download. """The utility to export the report as CSV for download.
""" """
import csv import csv
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import timedelta, date
from decimal import Decimal from decimal import Decimal
from io import StringIO from io import StringIO
from flask import Response from flask import Response
from accounting.report.period import Period
class BaseCSVRow(ABC): class BaseCSVRow(ABC):
"""The base CSV row.""" """The base CSV row."""
@ -55,54 +52,3 @@ def csv_download(filename: str, rows: list[BaseCSVRow]) -> Response:
response.headers["Content-Disposition"] \ response.headers["Content-Disposition"] \
= f"attachment; filename={filename}" = f"attachment; filename={filename}"
return response return response
def period_spec(period: Period) -> str:
"""Constructs the period specification to be used in the filename.
:param period: The period.
:return: The period specification to be used in the filename.
"""
start: str | None = __get_start_str(period.start)
end: str | None = __get_end_str(period.end)
if period.start is None and period.end is None:
return "all-time"
if start == end:
return start
if period.start is None:
return f"until-{end}"
if period.end is None:
return f"since-{start}"
return f"{start}-{end}"
def __get_start_str(start: date | None) -> str | None:
"""Returns the string representation of the start date.
:param start: The start date.
:return: The string representation of the start date, or None if the start
date is None.
"""
if start is None:
return None
if start.month == 1 and start.day == 1:
return str(start.year)
if start.day == 1:
return start.strftime("%Y%m")
return start.strftime("%Y%m%d")
def __get_end_str(end: date | None) -> str | None:
"""Returns the string representation of the end date.
:param end: The end date.
:return: The string representation of the end date, or None if the end
date is None.
"""
if end is None:
return None
if end.month == 12 and end.day == 31:
return str(end.year)
if (end + timedelta(days=1)).day == 1:
return end.strftime("%Y%m")
return end.strftime("%Y%m%d")

View File

@ -22,8 +22,7 @@
class OptionLink: class OptionLink:
"""An option link.""" """An option link."""
def __init__(self, title: str, url: str, is_active: bool, def __init__(self, title: str, url: str, is_active: bool):
fa_icon: str | None = None):
"""Constructs an option link. """Constructs an option link.
:param title: The title. :param title: The title.
@ -33,4 +32,3 @@ class OptionLink:
self.title: str = title self.title: str = title
self.url: str = url self.url: str = url
self.is_active: bool = is_active self.is_active: bool = is_active
self.fa_icon: str | None = fa_icon

View File

@ -22,18 +22,14 @@ from abc import ABC, abstractmethod
from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \ from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \
urlunparse urlunparse
import sqlalchemy as sa
from flask import request from flask import request
from accounting import db
from accounting.models import Currency, JournalEntry
from accounting.utils.txn_types import TransactionType from accounting.utils.txn_types import TransactionType
from .option_link import OptionLink
from .report_chooser import ReportChooser from .report_chooser import ReportChooser
class BasePageParams(ABC): class PageParams(ABC):
"""The base HTML page parameters class.""" """The page parameters of a report."""
@property @property
@abstractmethod @abstractmethod
@ -70,19 +66,3 @@ class BasePageParams(ABC):
parts: list[str] = list(uri_p) parts: list[str] = list(uri_p)
parts[4] = urlencode(params) parts[4] = urlencode(params)
return urlunparse(parts) return urlunparse(parts)
@staticmethod
def _get_currency_options(get_url: t.Callable[[Currency], str],
active_currency: Currency) -> list[OptionLink]:
"""Returns the currency options.
:param get_url: The callback to return the URL of a currency.
:param active_currency: The active currency.
:return: The currency options.
"""
in_use: set[str] = set(db.session.scalars(
sa.select(JournalEntry.currency_code)
.group_by(JournalEntry.currency_code)).all())
return [OptionLink(str(x), get_url(x), x.code == active_currency.code)
for x in Currency.query.filter(Currency.code.in_(in_use))
.order_by(Currency.code).all()]

View File

@ -20,16 +20,17 @@ This file is largely taken from the NanoParma ERP project, first written in
2021/9/16 by imacat (imacat@nanoparma.com). 2021/9/16 by imacat (imacat@nanoparma.com).
""" """
import typing as t
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import date from datetime import date
from flask import url_for
from accounting.models import Currency, Account, Transaction from accounting.models import Currency, Account, Transaction
from accounting.report.income_expense_account import IncomeExpensesAccount from accounting.report.income_expense_account import IncomeExpensesAccount
from accounting.report.period import YearPeriod, Period, ThisMonth, \ from accounting.report.period import YearPeriod, Period, ThisMonth, \
LastMonth, SinceLastMonth, ThisYear, LastYear, Today, Yesterday, \ LastMonth, SinceLastMonth, ThisYear, LastYear, Today, Yesterday, \
TemplatePeriod TemplatePeriod
from .urls import journal_url, ledger_url, income_expenses_url, \
trial_balance_url, income_statement_url, balance_sheet_url
class PeriodChooser(ABC): class PeriodChooser(ABC):
@ -72,7 +73,7 @@ class PeriodChooser(ABC):
"""Whether there is data in last year.""" """Whether there is data in last year."""
self.has_yesterday: bool = False self.has_yesterday: bool = False
"""Whether there is data in yesterday.""" """Whether there is data in yesterday."""
self.available_years: list[int] = [] self.available_years: t.Iterator[int] = []
"""The available years.""" """The available years."""
if self.has_data is not None: if self.has_data is not None:
@ -80,6 +81,7 @@ class PeriodChooser(ABC):
self.has_last_month = start < date(today.year, today.month, 1) self.has_last_month = start < date(today.year, today.month, 1)
self.has_last_year = start.year < today.year self.has_last_year = start.year < today.year
self.has_yesterday = start < today self.has_yesterday = start < today
self.available_years: t.Iterator[int] = []
if start.year < today.year - 1: if start.year < today.year - 1:
self.available_years \ self.available_years \
= reversed(range(start.year, today.year - 1)) = reversed(range(start.year, today.year - 1))
@ -112,7 +114,9 @@ class JournalPeriodChooser(PeriodChooser):
super().__init__(None if first is None else first.date) super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str: def _url_for(self, period: Period) -> str:
return journal_url(period) if period.is_default:
return url_for("accounting.report.journal-default")
return url_for("accounting.report.journal", period=period)
class LedgerPeriodChooser(PeriodChooser): class LedgerPeriodChooser(PeriodChooser):
@ -129,14 +133,19 @@ class LedgerPeriodChooser(PeriodChooser):
super().__init__(None if first is None else first.date) super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str: def _url_for(self, period: Period) -> str:
return ledger_url(self.currency, self.account, period) if period.is_default:
return url_for("accounting.report.ledger-default",
currency=self.currency, account=self.account)
return url_for("accounting.report.ledger",
currency=self.currency, account=self.account,
period=period)
class IncomeExpensesPeriodChooser(PeriodChooser): class IncomeExpensesPeriodChooser(PeriodChooser):
"""The income and expenses log period chooser.""" """The income and expenses period chooser."""
def __init__(self, currency: Currency, account: IncomeExpensesAccount): def __init__(self, currency: Currency, account: IncomeExpensesAccount):
"""Constructs the income and expenses log period chooser.""" """Constructs the income and expenses period chooser."""
self.currency: Currency = currency self.currency: Currency = currency
"""The currency.""" """The currency."""
self.account: IncomeExpensesAccount = account self.account: IncomeExpensesAccount = account
@ -146,7 +155,12 @@ class IncomeExpensesPeriodChooser(PeriodChooser):
super().__init__(None if first is None else first.date) super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str: def _url_for(self, period: Period) -> str:
return income_expenses_url(self.currency, self.account, period) if period.is_default:
return url_for("accounting.report.income-expenses-default",
currency=self.currency, account=self.account)
return url_for("accounting.report.income-expenses",
currency=self.currency, account=self.account,
period=period)
class TrialBalancePeriodChooser(PeriodChooser): class TrialBalancePeriodChooser(PeriodChooser):
@ -161,7 +175,11 @@ class TrialBalancePeriodChooser(PeriodChooser):
super().__init__(None if first is None else first.date) super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str: def _url_for(self, period: Period) -> str:
return trial_balance_url(self.currency, period) if period.is_default:
return url_for("accounting.report.trial-balance-default",
currency=self.currency)
return url_for("accounting.report.trial-balance",
currency=self.currency, period=period)
class IncomeStatementPeriodChooser(PeriodChooser): class IncomeStatementPeriodChooser(PeriodChooser):
@ -176,7 +194,11 @@ class IncomeStatementPeriodChooser(PeriodChooser):
super().__init__(None if first is None else first.date) super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str: def _url_for(self, period: Period) -> str:
return income_statement_url(self.currency, period) if period.is_default:
return url_for("accounting.report.income-statement-default",
currency=self.currency)
return url_for("accounting.report.income-statement",
currency=self.currency, period=period)
class BalanceSheetPeriodChooser(PeriodChooser): class BalanceSheetPeriodChooser(PeriodChooser):
@ -191,4 +213,8 @@ class BalanceSheetPeriodChooser(PeriodChooser):
super().__init__(None if first is None else first.date) super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str: def _url_for(self, period: Period) -> str:
return balance_sheet_url(self.currency, period) if period.is_default:
return url_for("accounting.report.balance-sheet-default",
currency=self.currency)
return url_for("accounting.report.balance-sheet",
currency=self.currency, period=period)

View File

@ -23,18 +23,16 @@ This file is largely taken from the NanoParma ERP project, first written in
import re import re
import typing as t import typing as t
from flask import url_for
from flask_babel import LazyString from flask_babel import LazyString
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account from accounting.models import Currency, Account
from accounting.report.income_expense_account import IncomeExpensesAccount
from accounting.report.period import Period from accounting.report.period import Period
from accounting.template_globals import default_currency_code from accounting.template_globals import default_currency_code
from .option_link import OptionLink from .option_link import OptionLink
from .report_type import ReportType from .report_type import ReportType
from .urls import journal_url, ledger_url, income_expenses_url, \
trial_balance_url, income_statement_url, balance_sheet_url
class ReportChooser: class ReportChooser:
@ -60,15 +58,13 @@ class ReportChooser:
Currency, default_currency_code()) \ Currency, default_currency_code()) \
if currency is None else currency if currency is None else currency
"""The currency.""" """The currency."""
self.__account: Account = Account.cash() if account is None \ self.__account: Account = Account.find_by_code("1111-001") \
else account if account is None else account
"""The currency.""" """The currency."""
self.__reports: list[OptionLink] = [] self.__reports: list[OptionLink] = []
"""The links to the reports.""" """The links to the reports."""
self.current_report: str | LazyString = "" self.current_report: str | LazyString = ""
"""The title of the current report.""" """The title of the current report."""
self.is_search: bool = active_report == ReportType.SEARCH
"""Whether the current report is the search page."""
self.__reports.append(self.__journal) self.__reports.append(self.__journal)
self.__reports.append(self.__ledger) self.__reports.append(self.__ledger)
self.__reports.append(self.__income_expenses) self.__reports.append(self.__income_expenses)
@ -78,8 +74,6 @@ class ReportChooser:
for report in self.__reports: for report in self.__reports:
if report.is_active: if report.is_active:
self.current_report = report.title self.current_report = report.title
if self.is_search:
self.current_report = gettext("Search")
@property @property
def __journal(self) -> OptionLink: def __journal(self) -> OptionLink:
@ -87,9 +81,11 @@ class ReportChooser:
:return: The journal. :return: The journal.
""" """
return OptionLink(gettext("Journal"), journal_url(self.__period), url: str = url_for("accounting.report.journal-default") \
self.__active_report == ReportType.JOURNAL, if self.__period.is_default \
fa_icon="fa-solid fa-book") else url_for("accounting.report.journal", period=self.__period)
return OptionLink(gettext("Journal"), url,
self.__active_report == ReportType.JOURNAL)
@property @property
def __ledger(self) -> OptionLink: def __ledger(self) -> OptionLink:
@ -97,27 +93,32 @@ class ReportChooser:
:return: The ledger. :return: The ledger.
""" """
return OptionLink(gettext("Ledger"), url: str = url_for("accounting.report.ledger-default",
ledger_url(self.__currency, self.__account, currency=self.__currency, account=self.__account) \
self.__period), if self.__period.is_default \
self.__active_report == ReportType.LEDGER, else url_for("accounting.report.ledger",
fa_icon="fa-solid fa-clipboard") currency=self.__currency, account=self.__account,
period=self.__period)
return OptionLink(gettext("Ledger"), url,
self.__active_report == ReportType.LEDGER)
@property @property
def __income_expenses(self) -> OptionLink: def __income_expenses(self) -> OptionLink:
"""Returns the income and expenses log. """Returns the income and expenses.
:return: The income and expenses log. :return: The income and expenses.
""" """
account: Account = self.__account account: Account = self.__account
if not re.match(r"[12][12]", account.base_code): if not re.match(r"[12][12]", account.base_code):
account: Account = Account.cash() account: Account = Account.find_by_code("1111-001")
return OptionLink(gettext("Income and Expenses Log"), url: str = url_for("accounting.report.income-expenses-default",
income_expenses_url(self.__currency, currency=self.__currency, account=account) \
IncomeExpensesAccount(account), if self.__period.is_default \
self.__period), else url_for("accounting.report.income-expenses",
self.__active_report == ReportType.INCOME_EXPENSES, currency=self.__currency, account=account,
fa_icon="fa-solid fa-money-bill-wave") period=self.__period)
return OptionLink(gettext("Income and Expenses"), url,
self.__active_report == ReportType.INCOME_EXPENSES)
@property @property
def __trial_balance(self) -> OptionLink: def __trial_balance(self) -> OptionLink:
@ -125,10 +126,13 @@ class ReportChooser:
:return: The trial balance. :return: The trial balance.
""" """
return OptionLink(gettext("Trial Balance"), url: str = url_for("accounting.report.trial-balance-default",
trial_balance_url(self.__currency, self.__period), currency=self.__currency) \
self.__active_report == ReportType.TRIAL_BALANCE, if self.__period.is_default \
fa_icon="fa-solid fa-scale-unbalanced") else url_for("accounting.report.trial-balance",
currency=self.__currency, period=self.__period)
return OptionLink(gettext("Trial Balance"), url,
self.__active_report == ReportType.TRIAL_BALANCE)
@property @property
def __income_statement(self) -> OptionLink: def __income_statement(self) -> OptionLink:
@ -136,10 +140,13 @@ class ReportChooser:
:return: The income statement. :return: The income statement.
""" """
return OptionLink(gettext("Income Statement"), url: str = url_for("accounting.report.income-statement-default",
income_statement_url(self.__currency, self.__period), currency=self.__currency) \
self.__active_report == ReportType.INCOME_STATEMENT, if self.__period.is_default \
fa_icon="fa-solid fa-file-invoice-dollar") else url_for("accounting.report.income-statement",
currency=self.__currency, period=self.__period)
return OptionLink(gettext("Income Statement"), url,
self.__active_report == ReportType.INCOME_STATEMENT)
@property @property
def __balance_sheet(self) -> OptionLink: def __balance_sheet(self) -> OptionLink:
@ -147,10 +154,13 @@ class ReportChooser:
:return: The balance sheet. :return: The balance sheet.
""" """
return OptionLink(gettext("Balance Sheet"), url: str = url_for("accounting.report.balance-sheet-default",
balance_sheet_url(self.__currency, self.__period), currency=self.__currency) \
self.__active_report == ReportType.BALANCE_SHEET, if self.__period.is_default \
fa_icon="fa-solid fa-scale-balanced") else url_for("accounting.report.balance-sheet",
currency=self.__currency, period=self.__period)
return OptionLink(gettext("Balance Sheet"), url,
self.__active_report == ReportType.BALANCE_SHEET)
def __iter__(self) -> t.Iterator[OptionLink]: def __iter__(self) -> t.Iterator[OptionLink]:
"""Returns the iteration of the reports. """Returns the iteration of the reports.

View File

@ -27,7 +27,7 @@ class ReportType(Enum):
LEDGER: str = "ledger" LEDGER: str = "ledger"
"""The ledger.""" """The ledger."""
INCOME_EXPENSES: str = "income-expenses" INCOME_EXPENSES: str = "income-expenses"
"""The income and expenses log.""" """The income and expenses."""
TRIAL_BALANCE: str = "trial-balance" TRIAL_BALANCE: str = "trial-balance"
"""The trial balance.""" """The trial balance."""
INCOME_STATEMENT: str = "income-statement" INCOME_STATEMENT: str = "income-statement"

View File

@ -1,112 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The utilities to get the ledger URL.
"""
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
def journal_url(period: Period) \
-> str:
"""Returns the URL of a journal.
:param period: The period.
:return: The URL of the journal.
"""
if period.is_default:
return url_for("accounting.report.journal-default")
return url_for("accounting.report.journal", period=period)
def ledger_url(currency: Currency, account: Account, period: Period) \
-> str:
"""Returns the URL of a ledger.
:param currency: The currency.
:param account: The account.
:param period: The period.
:return: The URL of the ledger.
"""
if period.is_default:
return url_for("accounting.report.ledger-default",
currency=currency, account=account)
return url_for("accounting.report.ledger",
currency=currency, account=account,
period=period)
def income_expenses_url(currency: Currency, account: IncomeExpensesAccount,
period: Period) -> str:
"""Returns the URL of an income and expenses log.
:param currency: The currency.
:param account: The account.
:param period: The period.
:return: The URL of the income and expenses log.
"""
if period.is_default:
return url_for("accounting.report.income-expenses-default",
currency=currency, account=account)
return url_for("accounting.report.income-expenses",
currency=currency, account=account,
period=period)
def trial_balance_url(currency: Currency, period: Period) -> str:
"""Returns the URL of a trial balance.
:param currency: The currency.
:param period: The period.
:return: The URL of the trial balance.
"""
if period.is_default:
return url_for("accounting.report.trial-balance-default",
currency=currency)
return url_for("accounting.report.trial-balance",
currency=currency, period=period)
def income_statement_url(currency: Currency, period: Period) -> str:
"""Returns the URL of an income statement.
:param currency: The currency.
:param period: The period.
:return: The URL of the income statement.
"""
if period.is_default:
return url_for("accounting.report.income-statement-default",
currency=currency)
return url_for("accounting.report.income-statement",
currency=currency, period=period)
def balance_sheet_url(currency: Currency, period: Period) -> str:
"""Returns the URL of a balance sheet.
:param currency: The currency.
:param period: The period.
:return: The URL of the balance sheet.
"""
if period.is_default:
return url_for("accounting.report.balance-sheet-default",
currency=currency)
return url_for("accounting.report.balance-sheet",
currency=currency, period=period)

View File

@ -115,11 +115,11 @@ def __get_ledger_list(currency: Currency, account: Account, period: Period) \
def get_default_income_expenses_list(currency: Currency, def get_default_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount) \ account: IncomeExpensesAccount) \
-> str | Response: -> str | Response:
"""Returns the income and expenses log in the default period. """Returns the income and expenses in the default period.
:param currency: The currency. :param currency: The currency.
:param account: The account. :param account: The account.
:return: The income and expenses log in the default period. :return: The income and expenses in the default period.
""" """
return __get_income_expenses_list(currency, account, Period.get_instance()) return __get_income_expenses_list(currency, account, Period.get_instance())
@ -131,12 +131,12 @@ def get_default_income_expenses_list(currency: Currency,
def get_income_expenses_list(currency: Currency, def get_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount, account: IncomeExpensesAccount,
period: Period) -> str | Response: period: Period) -> str | Response:
"""Returns the income and expenses log. """Returns the income and expenses.
:param currency: The currency. :param currency: The currency.
:param account: The account. :param account: The account.
:param period: The period. :param period: The period.
:return: The income and expenses log in the period. :return: The income and expenses in the period.
""" """
return __get_income_expenses_list(currency, account, period) return __get_income_expenses_list(currency, account, period)
@ -144,12 +144,12 @@ def get_income_expenses_list(currency: Currency,
def __get_income_expenses_list(currency: Currency, def __get_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount, account: IncomeExpensesAccount,
period: Period) -> str | Response: period: Period) -> str | Response:
"""Returns the income and expenses log. """Returns the income and expenses.
:param currency: The currency. :param currency: The currency.
:param account: The account. :param account: The account.
:param period: The period. :param period: The period.
:return: The income and expenses log in the period. :return: The income and expenses in the period.
""" """
report: IncomeExpenses = IncomeExpenses(currency, account, period) report: IncomeExpenses = IncomeExpenses(currency, account, period)
if "as" in request.args and request.args["as"] == "csv": if "as" in request.args and request.args["as"] == "csv":

View File

@ -24,6 +24,19 @@
.accounting-clickable { .accounting-clickable {
cursor: pointer; cursor: pointer;
} }
.accounting-search-desktop-form {
max-width: 16rem;
}
.btn-group .btn .accounting-search-input {
min-height: calc(1em + .5rem + 2px);
padding: 0 0.5rem;
}
.btn-group .btn .accounting-search-label button {
border: none;
background-color: transparent;
color: inherit;
padding-right: 0;
}
.form-floating > textarea.form-control { .form-floating > textarea.form-control {
height: 6rem; height: 6rem;
} }
@ -32,61 +45,6 @@
background-color: #D3D3D4; background-color: #D3D3D4;
} }
/** The toolbar */
.accounting-toolbar {
display: flex;
}
.accounting-toolbar .input-group > .input-group-text {
padding: 0;
background-color: transparent;
color: inherit;
border: 0;
}
.accounting-toolbar .input-group > .input-group-text > button {
background-color: transparent;
color: inherit;
border: 0;
}
.accounting-toolbar form.btn > .form-control {
min-height: calc(1.5em + 2px);
padding-top: 0.1rem;
padding-bottom: 0.1rem;
}
@media(min-width: 768px) {
.accounting-toolbar > .btn, .accounting-toolbar > .btn-group > .btn {
border-radius: 0;
}
.accounting-toolbar > .btn:first-child, .accounting-toolbar > .btn-group:first-child > .btn {
border-top-left-radius: 0.375rem;
border-bottom-left-radius: 0.375rem;
}
.accounting-toolbar > .btn:last-child, .accounting-toolbar > .btn-group:last-child > .btn {
border-top-right-radius: 0.375rem;
border-bottom-right-radius: 0.375rem;
}
.accounting-toolbar .btn.input-group {
width: 16rem;
}
}
@media(max-width:767px) {
.accounting-toolbar > .btn:not(form), .accounting-toolbar > .btn-group > .btn {
height: 3.2rem;
width: 3.2rem;
border-radius: 50%;
margin-left: 1rem;
}
.accounting-toolbar > a.btn, .accounting-toolbar > .btn-group > a.btn {
padding-top: 0.7rem;
}
.accounting-toolbar > form.btn {
width: 12rem;
height: 2.6rem;
border-radius: 0.375rem;
margin-top: 0.3rem;
margin-left: 1rem;
}
}
/** The card layout */ /** The card layout */
.accounting-card { .accounting-card {
padding: 2em 1.5em; padding: 2em 1.5em;

View File

@ -20,7 +20,6 @@
/* Author: imacat@mail.imacat.idv.tw (imacat) /* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/2/1 * First written: 2023/2/1
*/ */
"use strict";
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {

View File

@ -20,7 +20,6 @@
/* Author: imacat@mail.imacat.idv.tw (imacat) /* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/2/2 * First written: 2023/2/2
*/ */
"use strict";
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {

View File

@ -20,7 +20,6 @@
/* Author: imacat@mail.imacat.idv.tw (imacat) /* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/2/28 * First written: 2023/2/28
*/ */
"use strict";
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {

View File

@ -20,7 +20,6 @@
/* Author: imacat@mail.imacat.idv.tw (imacat) /* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/2/6 * First written: 2023/2/6
*/ */
"use strict";
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {

View File

@ -20,7 +20,6 @@
/* Author: imacat@mail.imacat.idv.tw (imacat) /* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/2/3 * First written: 2023/2/3
*/ */
"use strict";
/** /**
* Initializes the drag-and-drop reordering on a list. * Initializes the drag-and-drop reordering on a list.

View File

@ -20,7 +20,6 @@
/* Author: imacat@mail.imacat.idv.tw (imacat) /* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/2/25 * First written: 2023/2/25
*/ */
"use strict";
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {

View File

@ -20,11 +20,10 @@
/* Author: imacat@mail.imacat.idv.tw (imacat) /* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/3/4 * First written: 2023/3/4
*/ */
"use strict";
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
PeriodChooser.initialize(); new PeriodChooser();
}); });
/** /**
@ -63,20 +62,6 @@ class PeriodChooser {
this.tabPlanes[tab.tabId()] = tab; this.tabPlanes[tab.tabId()] = tab;
} }
} }
/**
* The period chooser.
* @type {PeriodChooser}
*/
static #chooser;
/**
* Initializes the period chooser.
*
*/
static initialize() {
this.#chooser = new PeriodChooser();
}
} }
/** /**
@ -157,12 +142,6 @@ class TabPlane {
*/ */
class MonthTab extends TabPlane { class MonthTab extends TabPlane {
/**
* The month chooser.
* @type {tempusDominus.TempusDominus}
*/
#monthChooser
/** /**
* Constructs a tab plane. * Constructs a tab plane.
* *
@ -172,7 +151,7 @@ class MonthTab extends TabPlane {
super(chooser); super(chooser);
const monthChooser = document.getElementById(this.prefix + "-chooser"); const monthChooser = document.getElementById(this.prefix + "-chooser");
let start = monthChooser.dataset.start; let start = monthChooser.dataset.start;
this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, { new tempusDominus.TempusDominus(monthChooser, {
restrictions: { restrictions: {
minDate: start, minDate: start,
}, },

View File

@ -20,7 +20,6 @@
/* Author: imacat@mail.imacat.idv.tw (imacat) /* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/2/28 * First written: 2023/2/28
*/ */
"use strict";
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
@ -763,7 +762,7 @@ class GeneralTripTab extends TagTabPlane {
* @override * @override
*/ */
populate() { populate() {
const found = this.editor.summary.value.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/); const found = this.editor.summary.value.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:[*×]\d+)?(?:\([^)]+\))?$/);
if (found === null) { if (found === null) {
return false; return false;
} }
@ -956,7 +955,7 @@ class BusTripTab extends TagTabPlane {
* @override * @override
*/ */
populate() { populate() {
const found = this.editor.summary.value.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/); const found = this.editor.summary.value.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:[*×]\d+)?(?:\([^)]+\))?$/);
if (found === null) { if (found === null) {
return false; return false;
} }
@ -1141,7 +1140,7 @@ class AnnotationTab extends TabPlane {
* @override * @override
*/ */
updateSummary() { updateSummary() {
const found = this.editor.summary.value.match(/^(.*?)(?:[*×]\d+)?(?:\([^()]+\))?$/); const found = this.editor.summary.value.match(/^(.*?)(?:[*×]\d+)?(?:\([^)]+\))?$/);
if (found !== null) { if (found !== null) {
this.editor.summary.value = found[1]; this.editor.summary.value = found[1];
} }
@ -1170,7 +1169,7 @@ class AnnotationTab extends TabPlane {
* @override * @override
*/ */
populate() { populate() {
const found = this.editor.summary.value.match(/^(.*?)(?:[*×](\d+))?(?:\(([^()]+)\))?$/); const found = this.editor.summary.value.match(/^(.*?)(?:[*×](\d+))?(?:\(([^)]+)\))?$/);
this.editor.summary.value = found[1]; this.editor.summary.value = found[1];
if (found[2] === undefined || parseInt(found[2]) === 1) { if (found[2] === undefined || parseInt(found[2]) === 1) {
this.editor.number.value = ""; this.editor.number.value = "";

View File

@ -0,0 +1,41 @@
/* The Mia! Accounting Flask Project
* table-row-link.js: The JavaScript for table rows as links.
*/
/* 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.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/3/4
*/
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => {
initializeTableRowLinks();
});
/**
* Initializes the table rows as links.
*
* @private
*/
function initializeTableRowLinks() {
const rows = Array.from(document.getElementsByClassName("accounting-clickable accounting-table-row-link"));
for (const row of rows) {
row.onclick = () => {
window.location = row.dataset.href;
};
}
}

View File

@ -20,7 +20,6 @@
/* Author: imacat@mail.imacat.idv.tw (imacat) /* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/2/25 * First written: 2023/2/25
*/ */
"use strict";
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {

View File

@ -20,7 +20,6 @@
/* Author: imacat@mail.imacat.idv.tw (imacat) /* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/2/26 * First written: 2023/2/26
*/ */
"use strict";
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {

View File

@ -27,7 +27,7 @@ First written: 2023/2/1
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="btn-group btn-actions mb-3">
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}"> <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}

View File

@ -25,19 +25,31 @@ First written: 2023/1/30
{% block content %} {% block content %}
<div class="mb-2 accounting-toolbar"> <div class="btn-group mb-2 d-none d-md-inline-flex">
{% if accounting_can_edit() %} {% if accounting_can_edit() %}
<a class="btn btn-primary text-nowrap d-none d-md-block" href="{{ url_for("accounting.account.create")|accounting_append_next }}"> <a class="btn btn-primary text-nowrap" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
<i class="fa-solid fa-plus"></i> <i class="fa-solid fa-plus"></i>
{{ A_("New") }} {{ A_("New") }}
</a> </a>
{% endif %} {% endif %}
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label"> <form class="btn btn-primary d-flex input-group accounting-search-desktop-form" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-label="{{ A_("Search for Desktop") }}">
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required"> <input id="accounting-search-desktop" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text"> <label for="accounting-search-desktop" class="accounting-search-label">
<button type="submit"> <button type="submit">
<i class="fa-solid fa-magnifying-glass"></i> <i class="fa-solid fa-magnifying-glass"></i>
<span class="d-none d-md-inline">{{ A_("Search") }}</span> {{ A_("Search") }}
</button>
</label>
</form>
</div>
<div class="btn-group mb-2 d-md-none">
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-label="{{ A_("Search for Mobile") }}">
<input id="accounting-search-mobile" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label for="accounting-search-mobile" class="accounting-search-label">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
</button> </button>
</label> </label>
</form> </form>

View File

@ -25,13 +25,13 @@ First written: 2023/1/26
{% block content %} {% block content %}
<div class="mb-2 accounting-toolbar"> <div class="btn-group mb-2">
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.base-account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label"> <form class="btn btn-primary d-flex input-group accounting-search-desktop-form" action="{{ url_for("accounting.base-account.list") }}" method="get" role="search" aria-label="{{ A_("Search") }}">
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required"> <input id="accounting-search" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text"> <label for="accounting-search" class="accounting-search-label">
<button type="submit"> <button type="submit">
<i class="fa-solid fa-magnifying-glass"></i> <i class="fa-solid fa-magnifying-glass"></i>
<span class="d-none d-md-inline">{{ A_("Search") }}</span> {{ A_("Search") }}
</button> </button>
</label> </label>
</form> </form>

View File

@ -27,7 +27,7 @@ First written: 2023/2/6
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="btn-group btn-actions mb-3">
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}"> <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}

View File

@ -25,19 +25,31 @@ First written: 2023/2/6
{% block content %} {% block content %}
<div class="mb-2 accounting-toolbar"> <div class="btn-group mb-2 d-none d-md-inline-flex">
{% if accounting_can_edit() %} {% if accounting_can_edit() %}
<a class="btn btn-primary text-nowrap d-none d-md-block" href="{{ url_for("accounting.currency.create")|accounting_append_next }}"> <a class="btn btn-primary text-nowrap" href="{{ url_for("accounting.currency.create")|accounting_append_next }}">
<i class="fa-solid fa-plus"></i> <i class="fa-solid fa-plus"></i>
{{ A_("New") }} {{ A_("New") }}
</a> </a>
{% endif %} {% endif %}
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label"> <form class="btn btn-primary d-flex input-group accounting-search-desktop-form" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-label="{{ A_("Search for Desktop") }}">
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required"> <input id="accounting-search-desktop" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text"> <label for="accounting-search-desktop" class="accounting-search-label">
<button type="submit"> <button type="submit">
<i class="fa-solid fa-magnifying-glass"></i> <i class="fa-solid fa-magnifying-glass"></i>
<span class="d-none d-md-inline">{{ A_("Search") }}</span> {{ A_("Search") }}
</button>
</label>
</form>
</div>
<div class="btn-group mb-2 d-md-none">
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-label="{{ A_("Search for Mobile") }}">
<input id="accounting-search-mobile" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label for="accounting-search-mobile" class="accounting-search-label">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
</button> </button>
</label> </label>
</form> </form>

View File

@ -22,13 +22,13 @@ First written: 2023/2/25
{% if accounting_can_edit() %} {% if accounting_can_edit() %}
<div id="accounting-material-fab-speed-dial" class="d-md-none accounting-material-fab"> <div id="accounting-material-fab-speed-dial" class="d-md-none accounting-material-fab">
<div id="accounting-material-fab-speed-dial-actions" class="d-md-none accounting-material-fab-speed-dial-group"> <div id="accounting-material-fab-speed-dial-actions" class="d-md-none accounting-material-fab-speed-dial-group">
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}"> <a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash expense") }} {{ A_("Cash expense") }}
</a> </a>
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}"> <a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash income") }} {{ A_("Cash income") }}
</a> </a>
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}"> <a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }} {{ A_("Transfer") }}
</a> </a>
</div> </div>

View File

@ -26,35 +26,129 @@ First written: 2023/3/7
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %} {% endblock %}
{% block header %}{% block title %}{{ A_("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ _("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
{% block content %} {% block content %}
<div class="mb-3 accounting-toolbar"> <div class="btn-group mb-2 d-none d-md-inline-flex">
{% with use_currency_chooser = true, {% if accounting_can_edit() %}
use_period_chooser = true %} <div class="btn-group" role="group">
{% include "accounting/report/include/toolbar-buttons.html" %} <button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-plus"></i>
{{ A_("New") }}
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash Expense") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash Income") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }}
</a>
</li>
</ul>
</div>
{% endif %}
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %} {% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-money-bill-wave"></i>
{{ report.currency.name|title }}
</button>
<ul class="dropdown-menu">
{% for currency in report.currency_options %}
<li>
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
{{ currency.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
{{ report.period.desc|title }}
</button>
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i>
{{ A_("Download") }}
</a>
</div> </div>
{% include "accounting/report/include/add-txn-material-fab.html" %} {% with txn_types = report.txn_types %}
{% include "accounting/include/add-txn-material-fab.html" %}
{% endwith %}
{% include "accounting/report/include/period-chooser.html" %} <div class="btn-group btn-actions mb-3 d-md-none">
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-money-bill-wave"></i>
</button>
<ul class="dropdown-menu">
{% for currency in report.currency_options %}
<li>
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
{{ currency.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
{{ A_("Period") }}
</button>
</div>
{% with period = report.period, period_chooser = report.period_chooser %}
{% include "accounting/report/include/period-chooser.html" %}
{% endwith %}
{% include "accounting/report/include/search-modal.html" %} {% include "accounting/report/include/search-modal.html" %}
{% if report.has_data %} {% if report.has_data %}
<div class="accounting-sheet"> <div class="accounting-sheet">
<div class="d-none d-sm-flex justify-content-center mb-3"> <div class="d-none d-sm-flex justify-content-center mb-3">
<h2 class="text-center">{{ A_("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2> <h2 class="text-center">{{ _("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
</div> </div>
<div class="row accounting-report-table accounting-balance-sheet-table"> <div class="row accounting-report-table accounting-balance-sheet-table">
<div class="col-sm-6"> <div class="col-sm-6">
{% if report.assets.subsections %} {% if report.assets.subsections %}
{% with section = report.assets %} <div class="accounting-report-table-row accounting-balance-sheet-section">
{% include "accounting/report/include/balance-sheet-section.html" %} <div>{{ report.assets.title.title|title }}</div>
{% endwith %} </div>
<div class="accounting-report-table-body">
{% for subsection in report.assets.subsections %}
<div class="accounting-report-table-row accounting-balance-sheet-subsection">
<div>
<span class="d-none d-md-inline">{{ subsection.title.code }}</span>
{{ subsection.title.title|title }}
</div>
</div>
{% for account in subsection.accounts %}
<a class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-account" href="{{ account.url }}">
<div>
<span class="d-none d-md-inline">{{ account.account.code }}</span>
{{ account.account.title|title }}
</div>
<div class="accounting-amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_report_format_amount }}</div>
</a>
{% endfor %}
{% endfor %}
</div>
<div class="d-md-none d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-total"> <div class="d-md-none d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-total">
<div>{{ A_("Total") }}</div> <div>{{ A_("Total") }}</div>
<div class="accounting-amount {% if report.assets.total < 0 %} text-danger {% endif %}">{{ report.assets.total|accounting_report_format_amount }}</div> <div class="accounting-amount {% if report.assets.total < 0 %} text-danger {% endif %}">{{ report.assets.total|accounting_report_format_amount }}</div>
@ -64,9 +158,28 @@ First written: 2023/3/7
<div class="col-sm-6"> <div class="col-sm-6">
{% if report.liabilities.subsections %} {% if report.liabilities.subsections %}
{% with section = report.liabilities %} <div class="accounting-report-table-row accounting-balance-sheet-section">
{% include "accounting/report/include/balance-sheet-section.html" %} <div>{{ report.liabilities.title.title|title }}</div>
{% endwith %} </div>
<div class="accounting-report-table-body">
{% for subsection in report.liabilities.subsections %}
<div class="accounting-report-table-row accounting-balance-sheet-subsection">
<div>
<span class="d-none d-md-inline">{{ subsection.title.code }}</span>
{{ subsection.title.title|title }}
</div>
</div>
{% for account in subsection.accounts %}
<a class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-account" href="{{ account.url }}">
<div>
<span class="d-none d-md-inline">{{ account.account.code }}</span>
{{ account.account.title|title }}
</div>
<div class="accounting-amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_report_format_amount }}</div>
</a>
{% endfor %}
{% endfor %}
</div>
<div class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-subtotal"> <div class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-subtotal">
<div>{{ A_("Total") }}</div> <div>{{ A_("Total") }}</div>
<div class="accounting-amount {% if report.liabilities.total < 0 %} text-danger {% endif %}">{{ report.liabilities.total|accounting_report_format_amount }}</div> <div class="accounting-amount {% if report.liabilities.total < 0 %} text-danger {% endif %}">{{ report.liabilities.total|accounting_report_format_amount }}</div>
@ -74,9 +187,28 @@ First written: 2023/3/7
{% endif %} {% endif %}
{% if report.owner_s_equity.subsections %} {% if report.owner_s_equity.subsections %}
{% with section = report.owner_s_equity %} <div class="accounting-report-table-row accounting-balance-sheet-section">
{% include "accounting/report/include/balance-sheet-section.html" %} <div>{{ report.owner_s_equity.title.title|title }}</div>
{% endwith %} </div>
<div class="accounting-report-table-body">
{% for subsection in report.owner_s_equity.subsections %}
<div class="accounting-report-table-row accounting-balance-sheet-subsection">
<div>
<span class="d-none d-md-inline">{{ subsection.title.code }}</span>
{{ subsection.title.title|title }}
</div>
</div>
{% for account in subsection.accounts %}
<a class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-account" href="{{ account.url }}">
<div>
<span class="d-none d-md-inline">{{ account.account.code }}</span>
{{ account.account.title|title }}
</div>
<div class="accounting-amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_report_format_amount }}</div>
</a>
{% endfor %}
{% endfor %}
</div>
<div class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-subtotal"> <div class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-subtotal">
<div>{{ A_("Total") }}</div> <div>{{ A_("Total") }}</div>
<div class="accounting-amount {% if report.owner_s_equity.total < 0 %} text-danger {% endif %}">{{ report.owner_s_equity.total|accounting_report_format_amount }}</div> <div class="accounting-amount {% if report.owner_s_equity.total < 0 %} text-danger {% endif %}">{{ report.owner_s_equity.total|accounting_report_format_amount }}</div>

View File

@ -1,43 +0,0 @@
{#
The Mia! Accounting Flask Project
balance-sheet-section.html: A section in the balance sheet.
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.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/8
#}
<div class="accounting-report-table-row accounting-balance-sheet-section">
<div>{{ section.title.title|title }}</div>
</div>
<div class="accounting-report-table-body">
{% for subsection in section.subsections %}
<div class="accounting-report-table-row accounting-balance-sheet-subsection">
<div>
<span class="d-none d-md-inline">{{ subsection.title.code }}</span>
{{ subsection.title.title|title }}
</div>
</div>
{% for account in subsection.accounts %}
<a class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-account" href="{{ account.url }}">
<div>
<span class="d-none d-md-inline">{{ account.account.code }}</span>
{{ account.account.title|title }}
</div>
<div class="accounting-amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_report_format_amount }}</div>
</a>
{% endfor %}
{% endfor %}
</div>

View File

@ -1,6 +1,6 @@
{# {#
The Mia! Accounting Flask Project The Mia! Accounting Flask Project
income-expenses-row-mobile.html: The row in the income and expenses log for the mobile devices income-expenses-mobile-row.html: The row in the income and expenses for the mobile devices
Copyright (c) 2023 imacat. Copyright (c) 2023 imacat.

View File

@ -1,27 +0,0 @@
{#
The Mia! Accounting Flask Project
income-expenses-row-desktop.html: The row in the income and expenses log for the desktop computers
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.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/8
#}
<div>{{ entry.date|accounting_format_date }}</div>
<div>{{ entry.account.title|title }}</div>
<div>{{ entry.summary|accounting_default }}</div>
<div class="accounting-amount">{{ entry.income|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ entry.expense|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>

View File

@ -1,6 +1,6 @@
{# {#
The Mia! Accounting Flask Project The Mia! Accounting Flask Project
ledger-row-mobile.html: The row in the ledger for the mobile devices ledger-mobile-row.html: The row in the ledger for the mobile devices
Copyright (c) 2023 imacat. Copyright (c) 2023 imacat.

View File

@ -1,26 +0,0 @@
{#
The Mia! Accounting Flask Project
ledger-row-desktop.html: The row in the ledger for the desktop computers
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.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/8
#}
<div>{{ entry.date|accounting_format_date }}</div>
<div>{{ entry.summary|accounting_default }}</div>
<div class="accounting-amount">{{ entry.debit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ entry.credit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>

View File

@ -19,7 +19,7 @@ period-chooser.html: The period chooser
Author: imacat@mail.imacat.idv.tw (imacat) Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/4 First written: 2023/3/4
#} #}
<div id="accounting-period-chooser-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-period-chooser-modal-label" aria-hidden="true" data-url-template="{{ report.period_chooser.url_template }}"> <div id="accounting-period-chooser-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-period-chooser-modal-label" aria-hidden="true" data-url-template="{{ period_chooser.url_template }}">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@ -30,62 +30,62 @@ First written: 2023/3/4
{# Tab navigation #} {# Tab navigation #}
<ul class="nav nav-tabs mb-2"> <ul class="nav nav-tabs mb-2">
<li class="nav-item"> <li class="nav-item">
<span id="accounting-period-chooser-month-tab" class="nav-link {% if report.period.is_type_month %} active {% endif %} accounting-clickable" aria-current="{% if report.period.is_type_month %} page {% else %} false {% endif %}" data-tab-id="month"> <span id="accounting-period-chooser-month-tab" class="nav-link {% if period.is_type_month %} active {% endif %} accounting-clickable" aria-current="{% if period.is_type_month %} page {% else %} false {% endif %}" data-tab-id="month">
{{ A_("Month") }} {{ A_("Month") }}
</span> </span>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<span id="accounting-period-chooser-year-tab" class="nav-link {% if report.period.is_a_year %} active {% endif %} accounting-clickable" aria-current="{% if report.period.is_a_year %} page {% else %} false {% endif %}" data-tab-id="year"> <span id="accounting-period-chooser-year-tab" class="nav-link {% if period.is_a_year %} active {% endif %} accounting-clickable" aria-current="{% if period.is_a_year %} page {% else %} false {% endif %}" data-tab-id="year">
{{ A_("Year") }} {{ A_("Year") }}
</span> </span>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<span id="accounting-period-chooser-day-tab" class="nav-link {% if report.period.is_a_day %} active {% endif %} accounting-clickable" aria-current="{% if report.period.is_a_day %} page {% else %} false {% endif %}" data-tab-id="day"> <span id="accounting-period-chooser-day-tab" class="nav-link {% if period.is_a_day %} active {% endif %} accounting-clickable" aria-current="{% if period.is_a_day %} page {% else %} false {% endif %}" data-tab-id="day">
{{ A_("Day") }} {{ A_("Day") }}
</span> </span>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<span id="accounting-period-chooser-custom-tab" class="nav-link {% if report.period.is_type_arbitrary %} active {% endif %} accounting-clickable" aria-current="{% if report.period.is_type_arbitrary %} page {% else %} false {% endif %}" data-tab-id="custom"> <span id="accounting-period-chooser-custom-tab" class="nav-link {% if period.is_type_arbitrary %} active {% endif %} accounting-clickable" aria-current="{% if period.is_type_arbitrary %} page {% else %} false {% endif %}" data-tab-id="custom">
{{ A_("Custom") }} {{ A_("Custom") }}
</span> </span>
</li> </li>
</ul> </ul>
{# The month periods #} {# The month periods #}
<div id="accounting-period-chooser-month-page" {% if report.period.is_type_month %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-month-tab"> <div id="accounting-period-chooser-month-page" {% if period.is_type_month %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-month-tab">
<div> <div>
<a class="btn {% if report.period.is_this_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.this_month_url }}"> <a class="btn {% if period.is_this_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.this_month_url }}">
{{ A_("This month") }} {{ A_("This month") }}
</a> </a>
{% if report.period_chooser.has_last_month %} {% if period_chooser.has_last_month %}
<a class="btn {% if report.period.is_last_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.last_month_url }}"> <a class="btn {% if period.is_last_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.last_month_url }}">
{{ A_("Last month") }} {{ A_("Last month") }}
</a> </a>
<a class="btn {% if report.period.is_since_last_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.since_last_month_url }}"> <a class="btn {% if period.is_since_last_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.since_last_month_url }}">
{{ A_("Since last month") }} {{ A_("Since last month") }}
</a> </a>
{% endif %} {% endif %}
</div> </div>
{% if report.period_chooser.has_data %} {% if period_chooser.has_data %}
<div id="accounting-period-chooser-month-chooser" class="mt-3" data-start="{{ report.period_chooser.data_start }}" data-default="{{ report.period.start|accounting_default(report.period_chooser.data_start) }}"></div> <div id="accounting-period-chooser-month-chooser" class="mt-3" data-start="{{ period_chooser.data_start }}" data-default="{{ period.start }}"></div>
{% endif %} {% endif %}
</div> </div>
{# The year periods #} {# The year periods #}
<div id="accounting-period-chooser-year-page" {% if report.period.is_a_year %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-year-tab"> <div id="accounting-period-chooser-year-page" {% if period.is_a_year %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-year-tab">
<a class="btn {% if report.period.is_this_year %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.this_year_url }}"> <a class="btn {% if period.is_this_year %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.this_year_url }}">
{{ A_("This year") }} {{ A_("This year") }}
</a> </a>
{% if report.period_chooser.has_last_year %} {% if period_chooser.has_last_year %}
<a class="btn {% if report.period.is_last_year %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.last_year_url }}"> <a class="btn {% if period.is_last_year %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.last_year_url }}">
{{ A_("Last year") }} {{ A_("Last year") }}
</a> </a>
{% endif %} {% endif %}
{% if report.period_chooser.available_years %} {% if period_chooser.available_years %}
<ul class="nav nav-pills mt-3"> <ul class="nav nav-pills mt-3">
{% for year in report.period_chooser.available_years %} {% for year in period_chooser.available_years %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if report.period.is_year(year) %} active {% endif %}" href="{{ report.period_chooser.year_url(year) }}">{{ year }}</a> <a class="nav-link {% if period.is_year(year) %} active {% endif %}" href="{{ period_chooser.year_url(year) }}">{{ year }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -93,21 +93,21 @@ First written: 2023/3/4
</div> </div>
{# The day periods #} {# The day periods #}
<div id="accounting-period-chooser-day-page" {% if report.period.is_a_day %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-day-tab"> <div id="accounting-period-chooser-day-page" {% if period.is_a_day %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-day-tab">
<div> <div>
<a class="btn {% if report.period.is_today %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.today_url }}"> <a class="btn {% if period.is_today %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.today_url }}">
{{ A_("Today") }} {{ A_("Today") }}
</a> </a>
{% if report.period_chooser.has_yesterday %} {% if period_chooser.has_yesterday %}
<a class="btn {% if report.period.is_yesterday %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.yesterday_url }}"> <a class="btn {% if period.is_yesterday %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.yesterday_url }}">
{{ A_("Yesterday") }} {{ A_("Yesterday") }}
</a> </a>
{% endif %} {% endif %}
</div> </div>
{% if report.period_chooser.has_data %} {% if period_chooser.has_data %}
<div class="mt-3"> <div class="mt-3">
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input id="accounting-period-chooser-day-date" class="form-control" type="date" value="{{ report.period.start|accounting_default }}" min="{{ report.period_chooser.data_start }}" required="required"> <input id="accounting-period-chooser-day-date" class="form-control" type="date" value="{{ period.start|accounting_default }}" min="{{ period_chooser.data_start }}" required="required">
<label for="accounting-period-chooser-day-date" class="form-label">{{ A_("Date") }}</label> <label for="accounting-period-chooser-day-date" class="form-label">{{ A_("Date") }}</label>
<div id="accounting-period-chooser-day-date-error" class="invalid-feedback"></div> <div id="accounting-period-chooser-day-date-error" class="invalid-feedback"></div>
</div> </div>
@ -116,22 +116,22 @@ First written: 2023/3/4
</div> </div>
{# The custom periods #} {# The custom periods #}
<div id="accounting-period-chooser-custom-page" {% if report.period.is_type_arbitrary %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-custom-tab"> <div id="accounting-period-chooser-custom-page" {% if period.is_type_arbitrary %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-custom-tab">
<div> <div>
<a class="btn {% if report.period.is_all %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.all_url }}"> <a class="btn {% if period.is_all %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.all_url }}">
{{ A_("All") }} {{ A_("All") }}
</a> </a>
</div> </div>
{% if report.period_chooser.has_data %} {% if period_chooser.has_data %}
<div class="mt-3"> <div class="mt-3">
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input id="accounting-period-chooser-custom-start" class="form-control" type="date" value="{{ report.period.start|accounting_default }}" min="{{ report.period_chooser.data_start }}" max="{{ report.period.end }}" required="required"> <input id="accounting-period-chooser-custom-start" class="form-control" type="date" value="{{ period.start|accounting_default }}" min="{{ period_chooser.data_start }}" max="{{ period.end }}" required="required">
<label for="accounting-period-chooser-custom-start" class="form-label">{{ A_("From") }}</label> <label for="accounting-period-chooser-custom-start" class="form-label">{{ A_("From") }}</label>
<div id="accounting-period-chooser-custom-start-error" class="invalid-feedback"></div> <div id="accounting-period-chooser-custom-start-error" class="invalid-feedback"></div>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input id="accounting-period-chooser-custom-end" class="form-control" type="date" value="{{ report.period.end|accounting_default }}" min="{{ report.period.start }}" required="required"> <input id="accounting-period-chooser-custom-end" class="form-control" type="date" value="{{ period.end|accounting_default }}" min="{{ period.start }}" required="required">
<label for="accounting-period-chooser-custom-end" class="form-label">{{ A_("To") }}</label> <label for="accounting-period-chooser-custom-end" class="form-label">{{ A_("To") }}</label>
<div id="accounting-period-chooser-custom-end-error" class="invalid-feedback"></div> <div id="accounting-period-chooser-custom-end-error" class="invalid-feedback"></div>
</div> </div>

View File

@ -0,0 +1,38 @@
{#
The Mia! Accounting Flask Project
report-chooser.html: The report chooser
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.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/4
#}
<div class="btn-group" role="group">
<button id="accounting-report-chooser" class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-book"></i>
<span class="d-none d-md-inline">{{ report_chooser.current_report }}</span>
<span class="d-md-none">{{ A_("Report") }}</span>
</button>
<ul class="dropdown-menu" aria-labelledby="accounting-report-chooser">
{% for report in report_chooser %}
<li><a class="dropdown-item {% if report.is_active %} active {% endif %}" href="{{ report.url }}">{{ report.title }}</a></li>
{% endfor %}
<li>
<span class="dropdown-item accounting-clickable" data-bs-toggle="modal" data-bs-target="#accounting-search-modal">
{{ A_("Search") }}
</span>
</li>
</ul>
</div>

View File

@ -1,123 +0,0 @@
{#
The Mia! Accounting Flask Project
toolbar-buttons.html: The toolbar buttons on the report
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.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/8
#}
{% if accounting_can_edit() %}
<div class="btn-group d-none d-md-flex" role="group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-plus"></i>
<span class="d-none d-md-inline">{{ A_("New") }}</span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash Expense") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash Income") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }}
</a>
</li>
</ul>
</div>
{% endif %}
<div class="btn-group" role="group">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-book"></i>
<span class="d-none d-md-inline">{{ report.report_chooser.current_report }}</span>
</button>
<ul class="dropdown-menu" aria-label="{{ A_("Report") }}">
{% for report in report.report_chooser %}
<li>
<a class="dropdown-item {% if report.is_active %} active {% endif %}" href="{{ report.url }}">
<i class="{{ report.fa_icon }}"></i>
{{ report.title }}
</a>
</li>
{% endfor %}
<li>
<span class="dropdown-item {% if report.report_chooser.is_search %} active {% endif %} accounting-clickable" data-bs-toggle="modal" data-bs-target="#accounting-search-modal">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
</span>
</li>
</ul>
</div>
{% if use_currency_chooser %}
<div class="btn-group" role="group">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-money-bill-wave"></i>
<span class="d-none d-md-inline">{{ report.currency.name|title }}</span>
</button>
<ul class="dropdown-menu" aria-label="{{ A_("Currency") }}">
{% for currency in report.currency_options %}
<li>
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
{{ currency.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if use_account_chooser %}
<div class="btn-group" role="group">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-clipboard"></i>
<span class="d-none d-md-inline">{{ report.account.title|title }}</span>
</button>
<ul class="dropdown-menu" aria-label="{{ A_("Account") }}">
{% for account in report.account_options %}
<li>
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
{{ account.title|title }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if use_period_chooser %}
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
<span class="d-none d-md-inline">{{ report.period.desc|title }}</span>
</button>
{% endif %}
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i>
<span class="d-none d-md-inline">{{ A_("Download") }}</span>
</a>
{% if use_search %}
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
<span class="d-none d-md-inline">{{ A_("Search") }}</span>
</button>
</label>
</form>
{% endif %}

View File

@ -1,6 +1,6 @@
{# {#
The Mia! Accounting Flask Project The Mia! Accounting Flask Project
income-expenses.html: The income and expenses log income-expenses.html: The income and expenses
Copyright (c) 2023 imacat. Copyright (c) 2023 imacat.
@ -24,23 +24,127 @@ First written: 2023/3/5
{% block accounting_scripts %} {% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script>
{% endblock %} {% endblock %}
{% block header %}{% block title %}{{ A_("Income and Expenses Log of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account.title|title, period=report.period.desc|title) }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ _("Income and Expenses of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
{% block content %} {% block content %}
<div class="mb-3 accounting-toolbar"> <div class="btn-group mb-2 d-none d-md-inline-flex">
{% with use_currency_chooser = true, {% if accounting_can_edit() %}
use_account_chooser = true, <div class="btn-group" role="group">
use_period_chooser = true %} <button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
{% include "accounting/report/include/toolbar-buttons.html" %} <i class="fa-solid fa-plus"></i>
{{ A_("New") }}
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash Expense") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash Income") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }}
</a>
</li>
</ul>
</div>
{% endif %}
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %} {% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-money-bill-wave"></i>
{{ report.currency.name|title }}
</button>
<ul class="dropdown-menu">
{% for currency in report.currency_options %}
<li>
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
{{ currency.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-clipboard"></i>
{{ report.account.title|title }}
</button>
<ul class="dropdown-menu">
{% for account in report.account_options %}
<li>
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
{{ account.title|title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
{{ report.period.desc|title }}
</button>
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i>
{{ A_("Download") }}
</a>
</div> </div>
{% include "accounting/report/include/add-txn-material-fab.html" %} {% with txn_types = report.txn_types %}
{% include "accounting/include/add-txn-material-fab.html" %}
{% endwith %}
{% include "accounting/report/include/period-chooser.html" %} <div class="btn-group btn-actions mb-3 d-md-none">
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-money-bill-wave"></i>
</button>
<ul class="dropdown-menu">
{% for currency in report.currency_options %}
<li>
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
{{ currency.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-clipboard"></i>
</button>
<ul class="dropdown-menu">
{% for account in report.account_options %}
<li>
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
{{ account.title|title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
{{ A_("Period") }}
</button>
</div>
{% with period = report.period, period_chooser = report.period_chooser %}
{% include "accounting/report/include/period-chooser.html" %}
{% endwith %}
{% include "accounting/report/include/search-modal.html" %} {% include "accounting/report/include/search-modal.html" %}
@ -64,13 +168,23 @@ First written: 2023/3/5
{% if report.brought_forward %} {% if report.brought_forward %}
{% with entry = report.brought_forward %} {% with entry = report.brought_forward %}
<div class="accounting-report-table-row"> <div class="accounting-report-table-row">
{% include "accounting/report/include/income-expenses-row-desktop.html" %} <div>{{ entry.date|accounting_format_date }}</div>
<div>{{ entry.account.title|title }}</div>
<div>{{ entry.summary|accounting_default }}</div>
<div class="accounting-amount">{{ entry.income|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ entry.expense|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
</div> </div>
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% for entry in report.entries %} {% for entry in report.entries %}
<a class="accounting-report-table-row" href="{{ entry.url|accounting_append_next }}"> <a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
{% include "accounting/report/include/income-expenses-row-desktop.html" %} <div>{{ entry.date|accounting_format_date }}</div>
<div>{{ entry.account.title|title }}</div>
<div>{{ entry.summary|accounting_default }}</div>
<div class="accounting-amount">{{ entry.income|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ entry.expense|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
@ -92,19 +206,19 @@ First written: 2023/3/5
{% if report.brought_forward %} {% if report.brought_forward %}
{% with entry = report.brought_forward %} {% with entry = report.brought_forward %}
<div class="list-group-item list-group-item-action d-flex justify-content-between"> <div class="list-group-item list-group-item-action d-flex justify-content-between">
{% include "accounting/report/include/income-expenses-row-mobile.html" %} {% include "accounting/report/include/income-expenses-mobile-row.html" %}
</div> </div>
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% for entry in report.entries %} {% for entry in report.entries %}
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ entry.url|accounting_append_next }}"> <a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
{% include "accounting/report/include/income-expenses-row-mobile.html" %} {% include "accounting/report/include/income-expenses-mobile-row.html" %}
</a> </a>
{% endfor %} {% endfor %}
{% if report.total %} {% if report.total %}
{% with entry = report.total %} {% with entry = report.total %}
<div class="list-group-item list-group-item-action d-flex justify-content-between"> <div class="list-group-item list-group-item-action d-flex justify-content-between">
{% include "accounting/report/include/income-expenses-row-mobile.html" %} {% include "accounting/report/include/income-expenses-mobile-row.html" %}
</div> </div>
{% endwith %} {% endwith %}
{% endif %} {% endif %}

View File

@ -26,27 +26,102 @@ First written: 2023/3/7
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %} {% endblock %}
{% block header %}{% block title %}{{ A_("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ _("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
{% block content %} {% block content %}
<div class="mb-3 accounting-toolbar"> <div class="btn-group mb-2 d-none d-md-inline-flex">
{% with use_currency_chooser = true, {% if accounting_can_edit() %}
use_period_chooser = true %} <div class="btn-group" role="group">
{% include "accounting/report/include/toolbar-buttons.html" %} <button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-plus"></i>
{{ A_("New") }}
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash Expense") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash Income") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }}
</a>
</li>
</ul>
</div>
{% endif %}
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %} {% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-money-bill-wave"></i>
{{ report.currency.name|title }}
</button>
<ul class="dropdown-menu">
{% for currency in report.currency_options %}
<li>
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
{{ currency.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
{{ report.period.desc|title }}
</button>
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i>
{{ A_("Download") }}
</a>
</div> </div>
{% include "accounting/report/include/add-txn-material-fab.html" %} {% with txn_types = report.txn_types %}
{% include "accounting/include/add-txn-material-fab.html" %}
{% endwith %}
{% include "accounting/report/include/period-chooser.html" %} <div class="btn-group btn-actions mb-3 d-md-none">
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-money-bill-wave"></i>
</button>
<ul class="dropdown-menu">
{% for currency in report.currency_options %}
<li>
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
{{ currency.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
{{ A_("Period") }}
</button>
</div>
{% with period = report.period, period_chooser = report.period_chooser %}
{% include "accounting/report/include/period-chooser.html" %}
{% endwith %}
{% include "accounting/report/include/search-modal.html" %} {% include "accounting/report/include/search-modal.html" %}
{% if report.has_data %} {% if report.has_data %}
<div class="accounting-sheet"> <div class="accounting-sheet">
<div class="d-none d-sm-flex justify-content-center mb-3"> <div class="d-none d-sm-flex justify-content-center mb-3">
<h2 class="text-center">{{ A_("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2> <h2 class="text-center">{{ _("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
</div> </div>
<div class="accounting-report-table accounting-income-statement-table"> <div class="accounting-report-table accounting-income-statement-table">

View File

@ -24,21 +24,69 @@ First written: 2023/3/4
{% block accounting_scripts %} {% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{# <script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script> #}
{% endblock %} {% endblock %}
{% block header %}{% block title %}{{ A_("Journal %(period)s", period=report.period.desc|title) }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ _("Journal %(period)s", period=report.period.desc|title) }}{% endblock %}{% endblock %}
{% block content %} {% block content %}
<div class="mb-3 accounting-toolbar"> <div class="btn-group mb-2 d-none d-md-inline-flex">
{% with use_period_chooser = true %} {% if accounting_can_edit() %}
{% include "accounting/report/include/toolbar-buttons.html" %} <div class="btn-group" role="group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-plus"></i>
{{ A_("New") }}
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash Expense") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash Income") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }}
</a>
</li>
</ul>
</div>
{% endif %}
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %} {% endwith %}
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
{{ report.period.desc|title }}
</button>
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i>
{{ A_("Download") }}
</a>
</div> </div>
{% include "accounting/report/include/add-txn-material-fab.html" %} {% with txn_types = report.txn_types %}
{% include "accounting/include/add-txn-material-fab.html" %}
{% endwith %}
{% include "accounting/report/include/period-chooser.html" %} <div class="btn-group btn-actions mb-3 d-md-none">
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %}
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
{{ A_("Period") }}
</button>
</div>
{% with period = report.period, period_chooser = report.period_chooser %}
{% include "accounting/report/include/period-chooser.html" %}
{% endwith %}
{% include "accounting/report/include/search-modal.html" %} {% include "accounting/report/include/search-modal.html" %}

View File

@ -24,23 +24,127 @@ First written: 2023/3/5
{% block accounting_scripts %} {% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script>
{% endblock %} {% endblock %}
{% block header %}{% block title %}{{ A_("Ledger of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account.title|title, period=report.period.desc|title) }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ _("Ledger of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
{% block content %} {% block content %}
<div class="mb-3 accounting-toolbar"> <div class="btn-group mb-2 d-none d-md-inline-flex">
{% with use_currency_chooser = true, {% if accounting_can_edit() %}
use_account_chooser = true, <div class="btn-group" role="group">
use_period_chooser = true %} <button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
{% include "accounting/report/include/toolbar-buttons.html" %} <i class="fa-solid fa-plus"></i>
{{ A_("New") }}
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash Expense") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash Income") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }}
</a>
</li>
</ul>
</div>
{% endif %}
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %} {% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-money-bill-wave"></i>
{{ report.currency.name|title }}
</button>
<ul class="dropdown-menu">
{% for currency in report.currency_options %}
<li>
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
{{ currency.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-clipboard"></i>
{{ report.account.title|title }}
</button>
<ul class="dropdown-menu">
{% for account in report.account_options %}
<li>
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
{{ account.title|title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
{{ report.period.desc|title }}
</button>
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i>
{{ A_("Download") }}
</a>
</div> </div>
{% include "accounting/report/include/add-txn-material-fab.html" %} {% with txn_types = report.txn_types %}
{% include "accounting/include/add-txn-material-fab.html" %}
{% endwith %}
{% include "accounting/report/include/period-chooser.html" %} <div class="btn-group btn-actions mb-3 d-md-none">
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-money-bill-wave"></i>
</button>
<ul class="dropdown-menu">
{% for currency in report.currency_options %}
<li>
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
{{ currency.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-clipboard"></i>
</button>
<ul class="dropdown-menu">
{% for account in report.account_options %}
<li>
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
{{ account.title|title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
{{ A_("Period") }}
</button>
</div>
{% with period = report.period, period_chooser = report.period_chooser %}
{% include "accounting/report/include/period-chooser.html" %}
{% endwith %}
{% include "accounting/report/include/search-modal.html" %} {% include "accounting/report/include/search-modal.html" %}
@ -63,13 +167,21 @@ First written: 2023/3/5
{% if report.brought_forward %} {% if report.brought_forward %}
{% with entry = report.brought_forward %} {% with entry = report.brought_forward %}
<div class="accounting-report-table-row"> <div class="accounting-report-table-row">
{% include "accounting/report/include/ledger-row-desktop.html" %} <div>{{ entry.date|accounting_format_date }}</div>
<div>{{ entry.summary|accounting_default }}</div>
<div class="accounting-amount">{{ entry.debit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ entry.credit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
</div> </div>
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% for entry in report.entries %} {% for entry in report.entries %}
<a class="accounting-report-table-row" href="{{ entry.url|accounting_append_next }}"> <a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
{% include "accounting/report/include/ledger-row-desktop.html" %} <div>{{ entry.date|accounting_format_date }}</div>
<div>{{ entry.summary|accounting_default }}</div>
<div class="accounting-amount">{{ entry.debit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ entry.credit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
@ -91,19 +203,19 @@ First written: 2023/3/5
{% if report.brought_forward %} {% if report.brought_forward %}
{% with entry = report.brought_forward %} {% with entry = report.brought_forward %}
<div class="list-group-item list-group-item-action d-flex justify-content-between"> <div class="list-group-item list-group-item-action d-flex justify-content-between">
{% include "accounting/report/include/ledger-row-mobile.html" %} {% include "accounting/report/include/ledger-mobile-row.html" %}
</div> </div>
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% for entry in report.entries %} {% for entry in report.entries %}
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ entry.url|accounting_append_next }}"> <a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
{% include "accounting/report/include/ledger-row-mobile.html" %} {% include "accounting/report/include/ledger-mobile-row.html" %}
</a> </a>
{% endfor %} {% endfor %}
{% if report.total %} {% if report.total %}
{% with entry = report.total %} {% with entry = report.total %}
<div class="list-group-item list-group-item-action d-flex justify-content-between"> <div class="list-group-item list-group-item-action d-flex justify-content-between">
{% include "accounting/report/include/ledger-row-mobile.html" %} {% include "accounting/report/include/ledger-mobile-row.html" %}
</div> </div>
{% endwith %} {% endwith %}
{% endif %} {% endif %}

View File

@ -23,19 +23,75 @@ First written: 2023/3/8
{% block accounting_scripts %} {% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %} {% endblock %}
{% block header %}{% block title %}{{ A_("Search Result for \"%(query)s\"", query=request.args.q) }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ A_("Search Result for \"%(query)s\"", query=request.args.q) }}{% endblock %}{% endblock %}
{% block content %} {% block content %}
<div class="mb-3 accounting-toolbar"> <div class="btn-group mb-2 d-none d-md-inline-flex">
{% with use_search = true %} {% if accounting_can_edit() %}
{% include "accounting/report/include/toolbar-buttons.html" %} <div class="btn-group" role="group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-plus"></i>
{{ A_("New") }}
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash Expense") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash Income") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }}
</a>
</li>
</ul>
</div>
{% endif %}
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %} {% endwith %}
<form class="btn btn-primary d-flex input-group accounting-search-desktop-form" action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-label="{{ A_("Search for Desktop") }}">
<input id="accounting-search-desktop" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label for="accounting-search-desktop" class="accounting-search-label">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
</button>
</label>
</form>
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i>
{{ A_("Download") }}
</a>
</div> </div>
{% include "accounting/report/include/add-txn-material-fab.html" %} {% with txn_types = report.txn_types %}
{% include "accounting/include/add-txn-material-fab.html" %}
{% endwith %}
<div class="btn-group btn-actions mb-3 d-md-none">
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %}
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-label="{{ A_("Search for Mobile") }}">
<input id="accounting-search-mobile" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label for="accounting-search-mobile" class="accounting-search-label">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
</button>
</label>
</form>
</div>
{% include "accounting/report/include/search-modal.html" %} {% include "accounting/report/include/search-modal.html" %}

View File

@ -26,27 +26,102 @@ First written: 2023/3/5
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %} {% endblock %}
{% block header %}{% block title %}{{ A_("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ _("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
{% block content %} {% block content %}
<div class="mb-3 accounting-toolbar"> <div class="btn-group mb-2 d-none d-md-inline-flex">
{% with use_currency_chooser = true, {% if accounting_can_edit() %}
use_period_chooser = true %} <div class="btn-group" role="group">
{% include "accounting/report/include/toolbar-buttons.html" %} <button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-plus"></i>
{{ A_("New") }}
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash Expense") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash Income") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }}
</a>
</li>
</ul>
</div>
{% endif %}
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %} {% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-money-bill-wave"></i>
{{ report.currency.name|title }}
</button>
<ul class="dropdown-menu">
{% for currency in report.currency_options %}
<li>
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
{{ currency.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
{{ report.period.desc|title }}
</button>
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i>
{{ A_("Download") }}
</a>
</div> </div>
{% include "accounting/report/include/add-txn-material-fab.html" %} {% with txn_types = report.txn_types %}
{% include "accounting/include/add-txn-material-fab.html" %}
{% endwith %}
{% include "accounting/report/include/period-chooser.html" %} <div class="btn-group btn-actions mb-3 d-md-none">
{% with report_chooser = report.report_chooser %}
{% include "accounting/report/include/report-chooser.html" %}
{% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-money-bill-wave"></i>
</button>
<ul class="dropdown-menu">
{% for currency in report.currency_options %}
<li>
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
{{ currency.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
{{ A_("Period") }}
</button>
</div>
{% with period = report.period, period_chooser = report.period_chooser %}
{% include "accounting/report/include/period-chooser.html" %}
{% endwith %}
{% include "accounting/report/include/search-modal.html" %} {% include "accounting/report/include/search-modal.html" %}
{% if report.has_data %} {% if report.has_data %}
<div class="accounting-sheet"> <div class="accounting-sheet">
<div class="d-none d-sm-flex justify-content-center mb-3"> <div class="d-none d-sm-flex justify-content-center mb-3">
<h2 class="text-center">{{ A_("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2> <h2 class="text-center">{{ _("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
</div> </div>
<div class="accounting-report-table accounting-trial-balance-table"> <div class="accounting-report-table accounting-trial-balance-table">
@ -74,7 +149,6 @@ First written: 2023/3/5
<div>{{ A_("Total") }}</div> <div>{{ A_("Total") }}</div>
<div class="accounting-amount">{{ report.total.debit|accounting_format_amount }}</div> <div class="accounting-amount">{{ report.total.debit|accounting_format_amount }}</div>
<div class="accounting-amount">{{ report.total.credit|accounting_format_amount }}</div> <div class="accounting-amount">{{ report.total.credit|accounting_format_amount }}</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@ -30,7 +30,7 @@ First written: 2023/2/26
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="btn-group btn-actions mb-3">
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}"> <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}

View File

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

View File

@ -0,0 +1,96 @@
{#
The Mia! Accounting Flask Project
list.html: The transaction list
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.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/18
#}
{% extends "accounting/base.html" %}
{% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
{% endblock %}
{% block header %}{% block title %}{% if request.args.q %}{{ A_("Search Result for \"%(query)s\"", query=request.args.q) }}{% else %}{{ A_("Transaction Management") }}{% endif %}{% endblock %}{% endblock %}
{% block content %}
<div class="btn-group mb-2 d-none d-md-inline-flex">
{% if accounting_can_edit() %}
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-plus"></i>
{{ A_("New") }}
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash Expense") }}</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash Income") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }}
</a>
</li>
</ul>
</div>
{% endif %}
<form class="btn btn-primary d-flex input-group accounting-search-desktop-form" action="{{ url_for("accounting.transaction.list") }}" method="get" role="search" aria-label="{{ A_("Search for Desktop") }}">
<input id="accounting-search-desktop" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label for="accounting-search-desktop" class="accounting-search-label">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
</button>
</label>
</form>
</div>
<div class="btn-group mb-2 d-md-none">
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.transaction.list") }}" method="get" role="search" aria-label="{{ A_("Search for Mobile") }}">
<input id="accounting-search-mobile" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label for="accounting-search-mobile" class="accounting-search-label">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
</button>
</label>
</form>
</div>
{% include "accounting/include/add-txn-material-fab.html" %}
{% if list %}
{% include "accounting/include/pagination.html" %}
<div class="list-group">
{% for item in list %}
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.transaction.detail", txn=item)|accounting_append_next }}">
{{ item.date|accounting_format_date }} {{ item }}
</a>
{% endfor %}
</div>
{% else %}
<p>{{ A_("There is no data.") }}</p>
{% endif %}
{% endblock %}

View File

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

View File

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

View File

@ -46,9 +46,6 @@ MISSING_CURRENCY: LazyString = lazy_gettext("Please select the currency.")
"""The error message when the currency code is empty.""" """The error message when the currency code is empty."""
MISSING_ACCOUNT: LazyString = lazy_gettext("Please select the account.") MISSING_ACCOUNT: LazyString = lazy_gettext("Please select the account.")
"""The error message when the account code is empty.""" """The error message when the account code is empty."""
DATE_REQUIRED: DataRequired = DataRequired(
lazy_gettext("Please fill in the date."))
"""The validator to check if the date is empty."""
class NeedSomeCurrencies: class NeedSomeCurrencies:
@ -577,7 +574,8 @@ class IncomeCurrencyForm(CurrencyForm):
class IncomeTransactionForm(TransactionForm): class IncomeTransactionForm(TransactionForm):
"""The form to create or edit a cash income transaction.""" """The form to create or edit a cash income transaction."""
date = DateField(validators=[DATE_REQUIRED]) date = DateField(
validators=[DataRequired(lazy_gettext("Please fill in the date."))])
"""The date.""" """The date."""
currencies = FieldList(FormField(IncomeCurrencyForm), name="currency", currencies = FieldList(FormField(IncomeCurrencyForm), name="currency",
validators=[NeedSomeCurrencies()]) validators=[NeedSomeCurrencies()])
@ -650,7 +648,8 @@ class ExpenseCurrencyForm(CurrencyForm):
class ExpenseTransactionForm(TransactionForm): class ExpenseTransactionForm(TransactionForm):
"""The form to create or edit a cash expense transaction.""" """The form to create or edit a cash expense transaction."""
date = DateField(validators=[DATE_REQUIRED]) date = DateField(
validators=[DataRequired(lazy_gettext("Please fill in the date."))])
"""The date.""" """The date."""
currencies = FieldList(FormField(ExpenseCurrencyForm), name="currency", currencies = FieldList(FormField(ExpenseCurrencyForm), name="currency",
validators=[NeedSomeCurrencies()]) validators=[NeedSomeCurrencies()])
@ -759,7 +758,8 @@ class TransferCurrencyForm(CurrencyForm):
class TransferTransactionForm(TransactionForm): class TransferTransactionForm(TransactionForm):
"""The form to create or edit a transfer transaction.""" """The form to create or edit a transfer transaction."""
date = DateField(validators=[DATE_REQUIRED]) date = DateField(
validators=[DataRequired(lazy_gettext("Please fill in the date."))])
"""The date.""" """The date."""
currencies = FieldList(FormField(TransferCurrencyForm), name="currency", currencies = FieldList(FormField(TransferCurrencyForm), name="currency",
validators=[NeedSomeCurrencies()]) validators=[NeedSomeCurrencies()])

View File

@ -0,0 +1,65 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
# 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 queries for the transaction management.
"""
from datetime import datetime
import sqlalchemy as sa
from flask import request
from accounting.models import Transaction
from accounting.utils.query import parse_query_keywords
def get_transaction_query() -> list[Transaction]:
"""Returns the transactions, optionally filtered by the query.
:return: The transactions.
"""
keywords: list[str] = parse_query_keywords(request.args.get("q"))
if len(keywords) == 0:
return Transaction.query\
.order_by(Transaction.date, Transaction.no).all()
conditions: list[sa.BinaryExpression] = []
for k in keywords:
sub_conditions: list[sa.BinaryExpression] \
= [Transaction.note.contains(k)]
date: datetime
try:
date = datetime.strptime(k, "%Y")
sub_conditions.append(
sa.extract("year", Transaction.date) == date.year)
except ValueError:
pass
try:
date = datetime.strptime(k, "%Y/%m")
sub_conditions.append(sa.and_(
sa.extract("year", Transaction.date) == date.year,
sa.extract("month", Transaction.date) == date.month))
except ValueError:
pass
try:
date = datetime.strptime(f"2000/{k}", "%Y/%m/%d")
sub_conditions.append(sa.and_(
sa.extract("month", Transaction.date) == date.month,
sa.extract("day", Transaction.date) == date.day))
except ValueError:
pass
conditions.append(sa.or_(*sub_conditions))
return Transaction.query.filter(*conditions)\
.order_by(Transaction.date, Transaction.no).all()

View File

@ -30,11 +30,13 @@ from accounting.locale import lazy_gettext
from accounting.models import Transaction from accounting.models import Transaction
from accounting.utils.flash_errors import flash_form_errors from accounting.utils.flash_errors import flash_form_errors
from accounting.utils.next_uri import inherit_next, or_next from accounting.utils.next_uri import inherit_next, or_next
from accounting.utils.pagination import Pagination
from accounting.utils.permission import has_permission, can_view, can_edit from accounting.utils.permission import has_permission, can_view, can_edit
from accounting.utils.txn_types import TransactionType from accounting.utils.txn_types import TransactionType
from accounting.utils.user import get_current_user_pk from accounting.utils.user import get_current_user_pk
from .forms import sort_transactions_in, TransactionReorderForm
from .operators import TransactionOperator, TXN_TYPE_TO_OP, get_txn_op from .operators import TransactionOperator, TXN_TYPE_TO_OP, get_txn_op
from .forms import sort_transactions_in, TransactionReorderForm
from .queries import get_transaction_query
from .template_filters import with_type, to_transfer, format_amount_input, \ from .template_filters import with_type, to_transfer, format_amount_input, \
text2html text2html
@ -47,6 +49,20 @@ bp.add_app_template_filter(format_amount_input,
bp.add_app_template_filter(text2html, "accounting_txn_text2html") bp.add_app_template_filter(text2html, "accounting_txn_text2html")
@bp.get("", endpoint="list")
@has_permission(can_view)
def list_transactions() -> str:
"""Lists the transactions.
:return: The transaction list.
"""
transactions: list[Transaction] = get_transaction_query()
pagination: Pagination = Pagination[Transaction](transactions)
return render_template("accounting/transaction/list.html",
list=pagination.list, pagination=pagination,
txn_types=TransactionType)
@bp.get("/create/<transactionType:txn_type>", endpoint="create") @bp.get("/create/<transactionType:txn_type>", endpoint="create")
@has_permission(can_edit) @has_permission(can_edit)
def show_add_transaction_form(txn_type: TransactionType) -> str: def show_add_transaction_form(txn_type: TransactionType) -> str:
@ -142,12 +158,12 @@ def update_transaction(txn: Transaction) -> redirect:
form.populate_obj(txn) form.populate_obj(txn)
if not form.is_modified: if not form.is_modified:
flash(lazy_gettext("The transaction was not modified."), "success") flash(lazy_gettext("The transaction was not modified."), "success")
return redirect(inherit_next(__get_detail_uri(txn))) return redirect(inherit_next(with_type(__get_detail_uri(txn))))
txn.updated_by_id = get_current_user_pk() txn.updated_by_id = get_current_user_pk()
txn.updated_at = sa.func.now() txn.updated_at = sa.func.now()
db.session.commit() db.session.commit()
flash(lazy_gettext("The transaction is updated successfully."), "success") flash(lazy_gettext("The transaction is updated successfully."), "success")
return redirect(inherit_next(__get_detail_uri(txn))) return redirect(inherit_next(with_type(__get_detail_uri(txn))))
@bp.post("/<transaction:txn>/delete", endpoint="delete") @bp.post("/<transaction:txn>/delete", endpoint="delete")
@ -163,7 +179,7 @@ def delete_transaction(txn: Transaction) -> redirect:
sort_transactions_in(txn.date, txn.id) sort_transactions_in(txn.date, txn.id)
db.session.commit() db.session.commit()
flash(lazy_gettext("The transaction is deleted successfully."), "success") flash(lazy_gettext("The transaction is deleted successfully."), "success")
return redirect(or_next(url_for("accounting.report.journal-default"))) return redirect(or_next(with_type(url_for("accounting.transaction.list"))))
@bp.get("/dates/<date:txn_date>", endpoint="order") @bp.get("/dates/<date:txn_date>", endpoint="order")
@ -183,7 +199,7 @@ def show_transaction_order(txn_date: date) -> str:
@bp.post("/dates/<date:txn_date>", endpoint="sort") @bp.post("/dates/<date:txn_date>", endpoint="sort")
@has_permission(can_edit) @has_permission(can_edit)
def sort_transactions(txn_date: date) -> redirect: def sort_accounts(txn_date: date) -> redirect:
"""Reorders the transactions in a date. """Reorders the transactions in a date.
:param txn_date: The date. :param txn_date: The date.
@ -194,10 +210,10 @@ def sort_transactions(txn_date: date) -> redirect:
form.save_order() form.save_order()
if not form.is_modified: if not form.is_modified:
flash(lazy_gettext("The order was not modified."), "success") flash(lazy_gettext("The order was not modified."), "success")
return redirect(or_next(url_for("accounting.report.journal-default"))) return redirect(or_next(url_for("accounting.account.list")))
db.session.commit() db.session.commit()
flash(lazy_gettext("The order is updated successfully."), "success") flash(lazy_gettext("The order is updated successfully."), "success")
return redirect(or_next(url_for("accounting.report.journal-default"))) return redirect(or_next(url_for("accounting.account.list")))
def __get_detail_uri(txn: Transaction) -> str: def __get_detail_uri(txn: Transaction) -> str:

View File

@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Mia! Accounting Flask 0.0.0\n" "Project-Id-Version: Mia! Accounting Flask 0.0.0\n"
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n" "Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
"POT-Creation-Date: 2023-03-08 19:11+0800\n" "POT-Creation-Date: 2023-03-01 00:51+0800\n"
"PO-Revision-Date: 2023-03-08 19:11+0800\n" "PO-Revision-Date: 2023-03-01 00:51+0800\n"
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n" "Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
"Language: zh_Hant\n" "Language: zh_Hant\n"
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n" "Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
@ -17,45 +17,23 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.12.1\n" "Generated-By: Babel 2.11.0\n"
#: src/accounting/models.py:518 #: src/accounting/models.py:575
#, python-format #, python-format
msgid "Cash Expense Transaction#%(id)s" msgid "Cash Expense Transaction#%(id)s"
msgstr "現金支出傳票#%(id)s" msgstr "現金支出傳票#%(id)s"
#: src/accounting/models.py:520 #: src/accounting/models.py:577
#, python-format #, python-format
msgid "Cash Income Transaction#%(id)s" msgid "Cash Income Transaction#%(id)s"
msgstr "現金收入傳票#%(id)s" msgstr "現金收入傳票#%(id)s"
#: src/accounting/models.py:521 #: src/accounting/models.py:578
#, python-format #, python-format
msgid "Transfer Transaction#%(id)s" msgid "Transfer Transaction#%(id)s"
msgstr "轉帳傳票#%(id)s" msgstr "轉帳傳票#%(id)s"
#: src/accounting/report/period.py:493 src/accounting/template_filters.py:52
#: src/accounting/templates/accounting/report/include/period-chooser.html:99
msgid "Today"
msgstr "今天"
#: src/accounting/report/period.py:508 src/accounting/template_filters.py:54
#: src/accounting/templates/accounting/report/include/period-chooser.html:103
msgid "Yesterday"
msgstr "昨天"
#: src/accounting/template_filters.py:56
msgid "Tomorrow"
msgstr "明天"
#: src/accounting/template_filters.py:60
msgid "The day before yesterday"
msgstr "前天"
#: src/accounting/template_filters.py:62
msgid "The day after tomorrow"
msgstr "後天"
#: src/accounting/account/forms.py:41 #: src/accounting/account/forms.py:41
msgid "The base account does not exist." msgid "The base account does not exist."
msgstr "沒有這個基本科目。" msgstr "沒有這個基本科目。"
@ -65,7 +43,7 @@ msgid "The base account is not available."
msgstr "不能選這個基本科目。" msgstr "不能選這個基本科目。"
#: src/accounting/account/forms.py:61 #: src/accounting/account/forms.py:61
#: src/accounting/static/js/account-form.js:158 #: src/accounting/static/js/account-form.js:157
msgid "Please select the base account." msgid "Please select the base account."
msgstr "請選擇基本科目。" msgstr "請選擇基本科目。"
@ -73,8 +51,7 @@ msgstr "請選擇基本科目。"
msgid "Please fill in the title" msgid "Please fill in the title"
msgstr "請填上標題。" msgstr "請填上標題。"
#: src/accounting/account/queries.py:50 #: src/accounting/account/query.py:50
#: src/accounting/report/reports/search.py:90
#: src/accounting/templates/accounting/account/detail.html:90 #: src/accounting/templates/accounting/account/detail.html:90
#: src/accounting/templates/accounting/account/list.html:74 #: src/accounting/templates/accounting/account/list.html:74
msgid "Pay-off needed" msgid "Pay-off needed"
@ -96,36 +73,36 @@ msgstr "科目存好了。"
msgid "The account is deleted successfully." msgid "The account is deleted successfully."
msgstr "科目刪掉了" msgstr "科目刪掉了"
#: src/accounting/account/views.py:189 src/accounting/transaction/views.py:212 #: src/accounting/account/views.py:189 src/accounting/transaction/views.py:214
msgid "The order was not modified." msgid "The order was not modified."
msgstr "順序未異動。" msgstr "順序未異動。"
#: src/accounting/account/views.py:192 src/accounting/transaction/views.py:215 #: src/accounting/account/views.py:192 src/accounting/transaction/views.py:217
msgid "The order is updated successfully." msgid "The order is updated successfully."
msgstr "順序存好了。" msgstr "順序存好了。"
#: src/accounting/currency/forms.py:46 #: src/accounting/currency/forms.py:46
#: src/accounting/static/js/currency-form.js:137 #: src/accounting/static/js/currency-form.js:136
msgid "Code conflicts with another currency." msgid "Code conflicts with another currency."
msgstr "代碼與其它貨幣重複。" msgstr "代碼與其它貨幣重複。"
#: src/accounting/currency/forms.py:51 #: src/accounting/currency/forms.py:51
#: src/accounting/static/js/currency-form.js:93 #: src/accounting/static/js/currency-form.js:92
msgid "Please fill in the code." msgid "Please fill in the code."
msgstr "請填上代碼。" msgstr "請填上代碼。"
#: src/accounting/currency/forms.py:53 #: src/accounting/currency/forms.py:53
#: src/accounting/static/js/currency-form.js:104 #: src/accounting/static/js/currency-form.js:103
msgid "Code can only be composed of 3 upper-cased letters." msgid "Code can only be composed of 3 upper-cased letters."
msgstr "代碼限為三個大寫英文字母。" msgstr "代碼限為三個大寫英文字母。"
#: src/accounting/currency/forms.py:56 #: src/accounting/currency/forms.py:56
#: src/accounting/static/js/currency-form.js:99 #: src/accounting/static/js/currency-form.js:98
msgid "This code is not available." msgid "This code is not available."
msgstr "不能用這個代碼。" msgstr "不能用這個代碼。"
#: src/accounting/currency/forms.py:62 #: src/accounting/currency/forms.py:62
#: src/accounting/static/js/currency-form.js:169 #: src/accounting/static/js/currency-form.js:168
msgid "Please fill in the name." msgid "Please fill in the name."
msgstr "請填上名稱。" msgstr "請填上名稱。"
@ -145,329 +122,56 @@ msgstr "貨幣存好了。"
msgid "The currency is deleted successfully." msgid "The currency is deleted successfully."
msgstr "貨幣刪掉了" msgstr "貨幣刪掉了"
#: src/accounting/report/income_expense_account.py:62 #: src/accounting/static/js/account-form.js:177
msgid "current assets and liabilities"
msgstr "流動資產與負債"
#: src/accounting/report/period.py:252
msgid "for all time"
msgstr "全部"
#: src/accounting/report/period.py:284
#, python-format
msgid "since %(start)s"
msgstr "%(start)s至今"
#: src/accounting/report/period.py:302
#, python-format
msgid "until %(end)s"
msgstr "%(end)s前"
#: src/accounting/report/period.py:385
#, python-format
msgid "in %(period)s"
msgstr "%(period)s"
#: src/accounting/report/period.py:395
#, python-format
msgid "in %(start)s-%(end)s"
msgstr "%(start)s-%(end)s"
#: src/accounting/report/period.py:410
#: src/accounting/templates/accounting/report/include/period-chooser.html:58
msgid "This month"
msgstr "這個月"
#: src/accounting/report/period.py:430
#: src/accounting/templates/accounting/report/include/period-chooser.html:62
msgid "Last month"
msgstr "上個月"
#: src/accounting/report/period.py:450
#: src/accounting/templates/accounting/report/include/period-chooser.html:65
msgid "Since last month"
msgstr "上個月至今"
#: src/accounting/report/period.py:465
#: src/accounting/templates/accounting/report/include/period-chooser.html:77
msgid "This year"
msgstr "今年"
#: src/accounting/report/period.py:480
#: src/accounting/templates/accounting/report/include/period-chooser.html:81
msgid "Last year"
msgstr "去年"
#: src/accounting/report/reports/balance_sheet.py:437
#: src/accounting/report/reports/balance_sheet.py:441
#: src/accounting/report/reports/balance_sheet.py:453
#: src/accounting/report/reports/balance_sheet.py:455
#: src/accounting/report/reports/income_expenses.py:179
#: src/accounting/report/reports/income_expenses.py:444
#: src/accounting/report/reports/income_statement.py:316
#: src/accounting/report/reports/ledger.py:160
#: src/accounting/report/reports/ledger.py:396
#: src/accounting/report/reports/trial_balance.py:245
#: src/accounting/templates/accounting/report/balance-sheet.html:71
#: src/accounting/templates/accounting/report/balance-sheet.html:83
#: src/accounting/templates/accounting/report/balance-sheet.html:93
#: src/accounting/templates/accounting/report/balance-sheet.html:99
#: src/accounting/templates/accounting/report/balance-sheet.html:108
#: src/accounting/templates/accounting/report/balance-sheet.html:115
#: src/accounting/templates/accounting/report/income-expenses.html:94
#: src/accounting/templates/accounting/report/income-statement.html:95
#: src/accounting/templates/accounting/report/ledger.html:93
#: src/accounting/templates/accounting/report/trial-balance.html:86
#: src/accounting/templates/accounting/transaction/expense/detail.html:53
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:68
#: src/accounting/templates/accounting/transaction/income/detail.html:53
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:68
#: src/accounting/templates/accounting/transaction/transfer/detail.html:49
#: src/accounting/templates/accounting/transaction/transfer/detail.html:75
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:70
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:110
msgid "Total"
msgstr "合計"
#: src/accounting/report/reports/income_expenses.py:129
#: src/accounting/report/reports/ledger.py:125
msgid "Brought forward"
msgstr "前期轉入"
#: src/accounting/report/reports/income_expenses.py:428
#: src/accounting/report/reports/journal.py:219
#: src/accounting/report/reports/ledger.py:382
#: src/accounting/report/reports/search.py:193
#: src/accounting/templates/accounting/report/include/period-chooser.html:111
#: src/accounting/templates/accounting/report/income-expenses.html:68
#: src/accounting/templates/accounting/report/journal.html:64
#: src/accounting/templates/accounting/report/ledger.html:68
#: src/accounting/templates/accounting/report/search.html:65
#: src/accounting/templates/accounting/transaction/include/form.html:48
msgid "Date"
msgstr "日期"
#: src/accounting/report/reports/income_expenses.py:428
#: src/accounting/report/reports/journal.py:220
#: src/accounting/report/reports/search.py:194
#: src/accounting/report/reports/trial_balance.py:241
#: src/accounting/templates/accounting/report/include/action-buttons.html:96
#: src/accounting/templates/accounting/report/income-expenses.html:69
#: src/accounting/templates/accounting/report/journal.html:66
#: src/accounting/templates/accounting/report/search.html:67
#: src/accounting/templates/accounting/report/trial-balance.html:67
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:33
msgid "Account"
msgstr "科目"
#: src/accounting/report/reports/income_expenses.py:429
#: src/accounting/report/reports/journal.py:220
#: src/accounting/report/reports/ledger.py:382
#: src/accounting/report/reports/search.py:194
#: src/accounting/templates/accounting/report/income-expenses.html:70
#: src/accounting/templates/accounting/report/journal.html:67
#: src/accounting/templates/accounting/report/ledger.html:69
#: src/accounting/templates/accounting/report/search.html:68
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:41
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:28
msgid "Summary"
msgstr "摘要"
#: src/accounting/report/reports/income_expenses.py:429
#: src/accounting/templates/accounting/report/income-expenses.html:71
msgid "Income"
msgstr "收入"
#: src/accounting/report/reports/income_expenses.py:430
#: src/accounting/templates/accounting/report/income-expenses.html:72
msgid "Expense"
msgstr "支出"
#: src/accounting/report/reports/income_expenses.py:430
#: src/accounting/report/reports/ledger.py:384
#: src/accounting/templates/accounting/report/income-expenses.html:73
#: src/accounting/templates/accounting/report/ledger.html:72
msgid "Balance"
msgstr "餘額"
#: src/accounting/report/reports/income_expenses.py:431
#: src/accounting/report/reports/journal.py:222
#: src/accounting/report/reports/ledger.py:384
#: src/accounting/report/reports/search.py:196
#: src/accounting/templates/accounting/transaction/include/form.html:71
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:169
msgid "Note"
msgstr "備註"
#: src/accounting/report/reports/income_statement.py:232
msgid "total operating revenue"
msgstr "營業收入總額"
#: src/accounting/report/reports/income_statement.py:233
msgid "gross income"
msgstr "營業毛利"
#: src/accounting/report/reports/income_statement.py:234
msgid "operating income"
msgstr "營業淨利"
#: src/accounting/report/reports/income_statement.py:235
msgid "before tax income"
msgstr "稅前淨利"
#: src/accounting/report/reports/income_statement.py:236
msgid "after tax income"
msgstr "稅後淨利"
#: src/accounting/report/reports/income_statement.py:237
msgid "net income or loss for current period"
msgstr "本期損益"
#: src/accounting/report/reports/income_statement.py:317
#: src/accounting/templates/accounting/report/income-statement.html:67
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:49
msgid "Amount"
msgstr "金額"
#: src/accounting/report/reports/journal.py:219
#: src/accounting/report/reports/search.py:193
#: src/accounting/templates/accounting/report/include/action-buttons.html:79
#: src/accounting/templates/accounting/report/journal.html:65
#: src/accounting/templates/accounting/report/search.html:66
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:32
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:32
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:32
msgid "Currency"
msgstr "貨幣"
#: src/accounting/report/reports/journal.py:221
#: src/accounting/report/reports/ledger.py:383
#: src/accounting/report/reports/search.py:195
#: src/accounting/report/reports/trial_balance.py:241
#: src/accounting/templates/accounting/report/journal.html:68
#: src/accounting/templates/accounting/report/ledger.html:70
#: src/accounting/templates/accounting/report/search.html:69
#: src/accounting/templates/accounting/report/trial-balance.html:68
#: src/accounting/templates/accounting/transaction/transfer/detail.html:33
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:47
msgid "Debit"
msgstr "借方"
#: src/accounting/report/reports/journal.py:221
#: src/accounting/report/reports/ledger.py:383
#: src/accounting/report/reports/search.py:195
#: src/accounting/report/reports/trial_balance.py:242
#: src/accounting/templates/accounting/report/journal.html:69
#: src/accounting/templates/accounting/report/ledger.html:71
#: src/accounting/templates/accounting/report/search.html:70
#: src/accounting/templates/accounting/report/trial-balance.html:69
#: src/accounting/templates/accounting/transaction/transfer/detail.html:59
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:87
msgid "Credit"
msgstr "貸方"
#: src/accounting/report/reports/utils/report_chooser.py:87
msgid "Journal"
msgstr "日記簿"
#: src/accounting/report/reports/utils/report_chooser.py:103
msgid "Ledger"
msgstr "分類帳"
#: src/accounting/report/reports/utils/report_chooser.py:122
msgid "Income and Expenses Log"
msgstr "收支帳"
#: src/accounting/report/reports/utils/report_chooser.py:137
msgid "Trial Balance"
msgstr "試算表"
#: src/accounting/report/reports/utils/report_chooser.py:152
msgid "Income Statement"
msgstr "損益表"
#: src/accounting/report/reports/utils/report_chooser.py:167
msgid "Balance Sheet"
msgstr "資產負債表"
#: src/accounting/static/js/account-form.js:178
msgid "Please fill in the title." msgid "Please fill in the title."
msgstr "請填上標題。" msgstr "請填上標題。"
#: src/accounting/static/js/period-chooser.js:269 #: src/accounting/static/js/summary-helper.js:441
#: src/accounting/static/js/transaction-form.js:489 #: src/accounting/static/js/summary-helper.js:512
#: src/accounting/transaction/forms.py:578
#: src/accounting/transaction/forms.py:652
#: src/accounting/transaction/forms.py:762
msgid "Please fill in the date."
msgstr "請填上日期。"
#: src/accounting/static/js/period-chooser.js:274
msgid "The date is too early."
msgstr "日期太早。"
#: src/accounting/static/js/period-chooser.js:371
msgid "Please fill in the start date."
msgstr "請填上開始日期。"
#: src/accounting/static/js/period-chooser.js:376
msgid "The start date is too early."
msgstr "開始日期太早。"
#: src/accounting/static/js/period-chooser.js:381
msgid "The start date cannot be beyond the end date."
msgstr "開始日期不可晚於結束日期。"
#: src/accounting/static/js/period-chooser.js:399
msgid "Please fill in the end date."
msgstr "請填上結束日期。"
#: src/accounting/static/js/period-chooser.js:404
msgid "The end date cannot be beyond the start date."
msgstr "結束日期不可早於開始日期。"
#: src/accounting/static/js/summary-editor.js:817
#: src/accounting/static/js/summary-editor.js:1003
msgid "Please fill in the tag." msgid "Please fill in the tag."
msgstr "請填上標籤。" msgstr "請填上標籤。"
#: src/accounting/static/js/summary-editor.js:827 #: src/accounting/static/js/summary-helper.js:460
#: src/accounting/static/js/summary-editor.js:1023 #: src/accounting/static/js/summary-helper.js:550
msgid "Please fill in the origin." msgid "Please fill in the origin."
msgstr "請填上起點。" msgstr "請填上起點。"
#: src/accounting/static/js/summary-editor.js:837 #: src/accounting/static/js/summary-helper.js:479
#: src/accounting/static/js/summary-editor.js:1033 #: src/accounting/static/js/summary-helper.js:569
msgid "Please fill in the destination." msgid "Please fill in the destination."
msgstr "請填上終點。" msgstr "請填上終點。"
#: src/accounting/static/js/summary-editor.js:1013 #: src/accounting/static/js/summary-helper.js:531
msgid "Please fill in the route." msgid "Please fill in the route."
msgstr "請填上路線名稱。" msgstr "請填上路線名稱。"
#: src/accounting/static/js/transaction-form.js:290 #: src/accounting/static/js/transaction-form.js:289
#: src/accounting/static/js/transaction-form.js:612 #: src/accounting/static/js/transaction-form.js:611
#: src/accounting/transaction/forms.py:47 #: src/accounting/transaction/forms.py:47
msgid "Please select the account." msgid "Please select the account."
msgstr "請選擇科目。" msgstr "請選擇科目。"
#: src/accounting/static/js/transaction-form.js:325 #: src/accounting/static/js/transaction-form.js:324
#: src/accounting/static/js/transaction-form.js:617 #: src/accounting/static/js/transaction-form.js:616
msgid "Please fill in the amount." msgid "Please fill in the amount."
msgstr "請填上金額。" msgstr "請填上金額。"
#: src/accounting/static/js/transaction-form.js:524 #: src/accounting/static/js/transaction-form.js:488
msgid "Please fill in the date."
msgstr "請填上日期。"
#: src/accounting/static/js/transaction-form.js:523
#: src/accounting/transaction/forms.py:57 #: src/accounting/transaction/forms.py:57
msgid "Please add some currencies." msgid "Please add some currencies."
msgstr "請加上貨幣。" msgstr "請加上貨幣。"
#: src/accounting/static/js/transaction-form.js:590 #: src/accounting/static/js/transaction-form.js:589
#: src/accounting/transaction/forms.py:78 #: src/accounting/transaction/forms.py:78
msgid "Please add some journal entries." msgid "Please add some journal entries."
msgstr "請加上分錄。" msgstr "請加上分錄。"
#: src/accounting/static/js/transaction-form.js:655 #: src/accounting/static/js/transaction-form.js:654
#: src/accounting/transaction/forms.py:700 #: src/accounting/transaction/forms.py:672
msgid "The totals of the debit and credit amounts do not match." msgid "The totals of the debit and credit amounts do not match."
msgstr "借方貸方合計不符。 " msgstr "借方貸方合計不符。 "
@ -511,12 +215,10 @@ msgstr "科目刪除確認"
#: src/accounting/templates/accounting/account/detail.html:70 #: src/accounting/templates/accounting/account/detail.html:70
#: src/accounting/templates/accounting/account/include/form.html:91 #: src/accounting/templates/accounting/account/include/form.html:91
#: src/accounting/templates/accounting/currency/detail.html:66 #: src/accounting/templates/accounting/currency/detail.html:66
#: src/accounting/templates/accounting/report/include/period-chooser.html:27
#: src/accounting/templates/accounting/report/include/search-modal.html:28
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:27 #: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:27
#: src/accounting/templates/accounting/transaction/include/detail.html:71 #: src/accounting/templates/accounting/transaction/include/detail.html:71
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:28 #: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:28
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:30 #: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:30
msgid "Close" msgid "Close"
msgstr "關閉" msgstr "關閉"
@ -527,17 +229,15 @@ msgstr "你確定要刪掉這個科目嗎?"
#: src/accounting/templates/accounting/account/detail.html:76 #: src/accounting/templates/accounting/account/detail.html:76
#: src/accounting/templates/accounting/account/include/form.html:112 #: src/accounting/templates/accounting/account/include/form.html:112
#: src/accounting/templates/accounting/currency/detail.html:72 #: src/accounting/templates/accounting/currency/detail.html:72
#: src/accounting/templates/accounting/report/include/search-modal.html:37
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:49 #: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:49
#: src/accounting/templates/accounting/transaction/include/detail.html:77 #: src/accounting/templates/accounting/transaction/include/detail.html:77
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:54 #: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:54
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:184 #: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:175
msgid "Cancel" msgid "Cancel"
msgstr "取消" msgstr "取消"
#: src/accounting/templates/accounting/account/detail.html:77 #: src/accounting/templates/accounting/account/detail.html:77
#: src/accounting/templates/accounting/currency/detail.html:73 #: src/accounting/templates/accounting/currency/detail.html:73
#: src/accounting/templates/accounting/report/include/period-chooser.html:141
#: src/accounting/templates/accounting/transaction/include/detail.html:78 #: src/accounting/templates/accounting/transaction/include/detail.html:78
msgid "Confirm" msgid "Confirm"
msgstr "確定" msgstr "確定"
@ -562,7 +262,6 @@ msgstr "%(account)s設定"
#: src/accounting/templates/accounting/account/list.html:24 #: src/accounting/templates/accounting/account/list.html:24
#: src/accounting/templates/accounting/base-account/list.html:24 #: src/accounting/templates/accounting/base-account/list.html:24
#: src/accounting/templates/accounting/currency/list.html:24 #: src/accounting/templates/accounting/currency/list.html:24
#: src/accounting/templates/accounting/report/search.html:28
#: src/accounting/templates/accounting/transaction/list.html:28 #: src/accounting/templates/accounting/transaction/list.html:28
#, python-format #, python-format
msgid "Search Result for \"%(query)s\"" msgid "Search Result for \"%(query)s\""
@ -574,7 +273,6 @@ msgstr "科目管理"
#: src/accounting/templates/accounting/account/list.html:32 #: src/accounting/templates/accounting/account/list.html:32
#: src/accounting/templates/accounting/currency/list.html:32 #: src/accounting/templates/accounting/currency/list.html:32
#: src/accounting/templates/accounting/report/include/action-buttons.html:27
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:75 #: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:75
#: src/accounting/templates/accounting/transaction/include/form.html:62 #: src/accounting/templates/accounting/transaction/include/form.html:62
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:75 #: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:75
@ -586,7 +284,6 @@ msgstr "新增"
#: src/accounting/templates/accounting/account/list.html:35 #: src/accounting/templates/accounting/account/list.html:35
#: src/accounting/templates/accounting/currency/list.html:35 #: src/accounting/templates/accounting/currency/list.html:35
#: src/accounting/templates/accounting/report/search.html:37
#: src/accounting/templates/accounting/transaction/list.html:57 #: src/accounting/templates/accounting/transaction/list.html:57
msgid "Search for Desktop" msgid "Search for Desktop"
msgstr "桌機版檢索" msgstr "桌機版檢索"
@ -598,11 +295,6 @@ msgstr "桌機版檢索"
#: src/accounting/templates/accounting/base-account/list.html:34 #: src/accounting/templates/accounting/base-account/list.html:34
#: src/accounting/templates/accounting/currency/list.html:40 #: src/accounting/templates/accounting/currency/list.html:40
#: src/accounting/templates/accounting/currency/list.html:52 #: src/accounting/templates/accounting/currency/list.html:52
#: src/accounting/templates/accounting/report/include/action-buttons.html:67
#: src/accounting/templates/accounting/report/include/action-buttons.html:119
#: src/accounting/templates/accounting/report/include/search-modal.html:27
#: src/accounting/templates/accounting/report/include/search-modal.html:33
#: src/accounting/templates/accounting/report/include/search-modal.html:38
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:34 #: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:34
#: src/accounting/templates/accounting/transaction/list.html:62 #: src/accounting/templates/accounting/transaction/list.html:62
#: src/accounting/templates/accounting/transaction/list.html:74 #: src/accounting/templates/accounting/transaction/list.html:74
@ -611,7 +303,6 @@ msgstr "搜尋"
#: src/accounting/templates/accounting/account/list.html:47 #: src/accounting/templates/accounting/account/list.html:47
#: src/accounting/templates/accounting/currency/list.html:47 #: src/accounting/templates/accounting/currency/list.html:47
#: src/accounting/templates/accounting/report/search.html:50
#: src/accounting/templates/accounting/transaction/list.html:69 #: src/accounting/templates/accounting/transaction/list.html:69
msgid "Search for Mobile" msgid "Search for Mobile"
msgstr "行動版檢索" msgstr "行動版檢索"
@ -621,13 +312,6 @@ msgstr "行動版檢索"
#: src/accounting/templates/accounting/account/order.html:81 #: src/accounting/templates/accounting/account/order.html:81
#: src/accounting/templates/accounting/base-account/list.html:51 #: src/accounting/templates/accounting/base-account/list.html:51
#: src/accounting/templates/accounting/currency/list.html:77 #: src/accounting/templates/accounting/currency/list.html:77
#: src/accounting/templates/accounting/report/balance-sheet.html:122
#: src/accounting/templates/accounting/report/income-expenses.html:126
#: src/accounting/templates/accounting/report/income-statement.html:108
#: src/accounting/templates/accounting/report/journal.html:114
#: src/accounting/templates/accounting/report/ledger.html:125
#: src/accounting/templates/accounting/report/search.html:115
#: src/accounting/templates/accounting/report/trial-balance.html:94
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:46 #: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:46
#: src/accounting/templates/accounting/transaction/list.html:93 #: src/accounting/templates/accounting/transaction/list.html:93
#: src/accounting/templates/accounting/transaction/order.html:80 #: src/accounting/templates/accounting/transaction/order.html:80
@ -644,7 +328,7 @@ msgstr "%(base)s下的科目"
#: src/accounting/templates/accounting/currency/include/form.html:57 #: src/accounting/templates/accounting/currency/include/form.html:57
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:55 #: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:55
#: src/accounting/templates/accounting/transaction/include/form.html:78 #: src/accounting/templates/accounting/transaction/include/form.html:78
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:185 #: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:176
#: src/accounting/templates/accounting/transaction/order.html:61 #: src/accounting/templates/accounting/transaction/order.html:61
msgid "Save" msgid "Save"
msgstr "儲存" msgstr "儲存"
@ -708,27 +392,13 @@ msgstr "代碼"
msgid "Name" msgid "Name"
msgstr "名稱" msgstr "名稱"
#: src/accounting/templates/accounting/include/add-txn-material-fab.html:26
msgid "Cash expense"
msgstr "現金支出"
#: src/accounting/templates/accounting/include/add-txn-material-fab.html:29
msgid "Cash income"
msgstr "現金收入"
#: src/accounting/templates/accounting/include/add-txn-material-fab.html:32
#: src/accounting/templates/accounting/report/include/action-buttons.html:42
#: src/accounting/templates/accounting/transaction/list.html:51
msgid "Transfer"
msgstr "轉帳"
#: src/accounting/templates/accounting/include/nav.html:27 #: src/accounting/templates/accounting/include/nav.html:27
msgid "Accounting" msgid "Accounting"
msgstr "記帳" msgstr "記帳"
#: src/accounting/templates/accounting/include/nav.html:33 #: src/accounting/templates/accounting/include/nav.html:33
msgid "Reports" msgid "Transactions"
msgstr "報表" msgstr "傳票"
#: src/accounting/templates/accounting/include/nav.html:39 #: src/accounting/templates/accounting/include/nav.html:39
msgid "Accounts" msgid "Accounts"
@ -746,100 +416,22 @@ msgstr "貨幣"
msgid "Page navigation" msgid "Page navigation"
msgstr "分頁瀏覽" msgstr "分頁瀏覽"
#: src/accounting/templates/accounting/report/balance-sheet.html:29 #: src/accounting/templates/accounting/transaction/list.html:28
#: src/accounting/templates/accounting/report/balance-sheet.html:61 msgid "Transaction Management"
#, python-format msgstr "傳票管理"
msgid "Balance Sheet of %(currency)s %(period)s"
msgstr "%(period)s%(currency)s資產負債表"
#: src/accounting/templates/accounting/report/income-expenses.html:29
#, python-format
msgid "Income and Expenses Log of %(account)s in %(currency)s %(period)s"
msgstr "%(period)s%(currency)s%(account)s收支帳"
#: src/accounting/templates/accounting/report/income-statement.html:29
#: src/accounting/templates/accounting/report/income-statement.html:61
#, python-format
msgid "Income Statement of %(currency)s %(period)s"
msgstr "%(period)s%(currency)s損益表"
#: src/accounting/templates/accounting/report/journal.html:29
#, python-format
msgid "Journal %(period)s"
msgstr "%(period)s日記簿"
#: src/accounting/templates/accounting/report/ledger.html:29
#, python-format
msgid "Ledger of %(account)s in %(currency)s %(period)s"
msgstr "%(period)s%(currency)s%(account)s分類帳"
#: src/accounting/templates/accounting/report/trial-balance.html:29
#: src/accounting/templates/accounting/report/trial-balance.html:61
#, python-format
msgid "Trial Balance of %(currency)s %(period)s"
msgstr "%(period)s%(currency)s試算表"
#: src/accounting/templates/accounting/report/include/action-buttons.html:32
#: src/accounting/templates/accounting/transaction/list.html:42 #: src/accounting/templates/accounting/transaction/list.html:42
msgid "Cash Expense" msgid "Cash Expense"
msgstr "現金支出" msgstr "現金支出"
#: src/accounting/templates/accounting/report/include/action-buttons.html:37
#: src/accounting/templates/accounting/transaction/list.html:46 #: src/accounting/templates/accounting/transaction/list.html:46
msgid "Cash Income" msgid "Cash Income"
msgstr "現金收入" msgstr "現金收入"
#: src/accounting/templates/accounting/report/include/action-buttons.html:55 #: src/accounting/templates/accounting/transaction/include/add-new-material-fab.html:32
msgid "Report" #: src/accounting/templates/accounting/transaction/list.html:51
msgstr "報表" msgid "Transfer"
msgstr "轉帳"
#: src/accounting/templates/accounting/report/include/action-buttons.html:126
msgid "Download"
msgstr "下載"
#: src/accounting/templates/accounting/report/include/period-chooser.html:26
msgid "Period Chooser"
msgstr "選擇日期範圍"
#: src/accounting/templates/accounting/report/include/period-chooser.html:34
msgid "Month"
msgstr "月"
#: src/accounting/templates/accounting/report/include/period-chooser.html:39
msgid "Year"
msgstr "年"
#: src/accounting/templates/accounting/report/include/period-chooser.html:44
msgid "Day"
msgstr "日"
#: src/accounting/templates/accounting/report/include/period-chooser.html:49
msgid "Custom"
msgstr "自訂"
#: src/accounting/templates/accounting/report/include/period-chooser.html:122
msgid "All"
msgstr "全部"
#: src/accounting/templates/accounting/report/include/period-chooser.html:129
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:102
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:143
msgid "From"
msgstr "從"
#: src/accounting/templates/accounting/report/include/period-chooser.html:135
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:111
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:148
msgid "To"
msgstr "至"
#: src/accounting/templates/accounting/report/include/search-modal.html:22
msgid "Search the Accounting Data"
msgstr "搜尋帳務資料"
#: src/accounting/templates/accounting/transaction/list.html:28
msgid "Transaction Management"
msgstr "傳票管理"
#: src/accounting/templates/accounting/transaction/order.html:29 #: src/accounting/templates/accounting/transaction/order.html:29
#, python-format #, python-format
@ -863,6 +455,17 @@ msgstr "改轉帳"
msgid "Content" msgid "Content"
msgstr "內容" msgstr "內容"
#: src/accounting/templates/accounting/transaction/expense/detail.html:53
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:68
#: src/accounting/templates/accounting/transaction/income/detail.html:53
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:68
#: src/accounting/templates/accounting/transaction/transfer/detail.html:49
#: src/accounting/templates/accounting/transaction/transfer/detail.html:75
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:70
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:110
msgid "Total"
msgstr "合計"
#: src/accounting/templates/accounting/transaction/expense/edit.html:24 #: src/accounting/templates/accounting/transaction/expense/edit.html:24
#: src/accounting/templates/accounting/transaction/income/edit.html:24 #: src/accounting/templates/accounting/transaction/income/edit.html:24
#: src/accounting/templates/accounting/transaction/transfer/edit.html:24 #: src/accounting/templates/accounting/transaction/transfer/edit.html:24
@ -870,6 +473,12 @@ msgstr "內容"
msgid "Editing %(txn)s" msgid "Editing %(txn)s"
msgstr "編輯%(txn)s" msgstr "編輯%(txn)s"
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:32
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:32
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:32
msgid "Currency"
msgstr "貨幣"
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:26 #: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:26
msgid "Select Account" msgid "Select Account"
msgstr "選擇科目" msgstr "選擇科目"
@ -878,6 +487,14 @@ msgstr "選擇科目"
msgid "More…" msgid "More…"
msgstr "更多…" msgstr "更多…"
#: src/accounting/templates/accounting/transaction/include/add-new-material-fab.html:26
msgid "Cash expense"
msgstr "現金支出"
#: src/accounting/templates/accounting/transaction/include/add-new-material-fab.html:29
msgid "Cash income"
msgstr "現金收入"
#: src/accounting/templates/accounting/transaction/include/detail.html:70 #: src/accounting/templates/accounting/transaction/include/detail.html:70
msgid "Delete Transaction Confirmation" msgid "Delete Transaction Confirmation"
msgstr "傳票刪除確認" msgstr "傳票刪除確認"
@ -890,37 +507,68 @@ msgstr "你確定要刪掉這張傳票嗎?"
msgid "Journal Entry Content" msgid "Journal Entry Content"
msgstr "分錄內容" msgstr "分錄內容"
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:41 #: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:33
msgid "Account"
msgstr "科目"
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:41
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:28
msgid "Summary"
msgstr "摘要"
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:49
msgid "Amount"
msgstr "金額"
#: src/accounting/templates/accounting/transaction/include/form.html:48
msgid "Date"
msgstr "日期"
#: src/accounting/templates/accounting/transaction/include/form.html:71
msgid "Note"
msgstr "備註"
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:39
msgid "General" msgid "General"
msgstr "一般" msgstr "一般"
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:46 #: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:44
msgid "Travel" msgid "Travel"
msgstr "差旅" msgstr "差旅"
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:51 #: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:49
msgid "Bus" msgid "Bus"
msgstr "公車" msgstr "公車"
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:56 #: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:54
msgid "Regular" msgid "Regular"
msgstr "帳單" msgstr "帳單"
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:61 #: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:59
msgid "Annotation" msgid "Number"
msgstr "註記" msgstr "數量"
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:70 #: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:67
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:87 #: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:84
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:122 #: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:119
msgid "Tag" msgid "Tag"
msgstr "標籤" msgstr "標籤"
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:127 #: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:99
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:140
msgid "From"
msgstr "從"
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:108
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:145
msgid "To"
msgstr "至"
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:124
msgid "Route" msgid "Route"
msgstr "路線" msgstr "路線"
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:163 #: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:160
msgid "The number of items" msgid "The number of items"
msgstr "數量" msgstr "數量"
@ -932,6 +580,16 @@ msgstr "新增現金收入傳票"
msgid "Add a New Transfer Transaction" msgid "Add a New Transfer Transaction"
msgstr "新增轉帳傳票" msgstr "新增轉帳傳票"
#: src/accounting/templates/accounting/transaction/transfer/detail.html:33
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:47
msgid "Debit"
msgstr "借方"
#: src/accounting/templates/accounting/transaction/transfer/detail.html:59
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:87
msgid "Credit"
msgstr "貸方"
#: src/accounting/transaction/forms.py:45 #: src/accounting/transaction/forms.py:45
msgid "Please select the currency." msgid "Please select the currency."
msgstr "請選擇貨幣。" msgstr "請選擇貨幣。"
@ -952,23 +610,43 @@ msgstr "金額請填正數。"
msgid "This account is not for debit entries." msgid "This account is not for debit entries."
msgstr "科目不是借方科目。" msgstr "科目不是借方科目。"
#: src/accounting/transaction/forms.py:230 #: src/accounting/transaction/forms.py:201
msgid "This account is not for credit entries." msgid "This account is not for credit entries."
msgstr "科目不是貸方科目。" msgstr "科目不是貸方科目。"
#: src/accounting/transaction/views.py:106 #: src/accounting/transaction/template.py:97
msgid "Today"
msgstr "今天"
#: src/accounting/transaction/template.py:99
msgid "Yesterday"
msgstr "昨天"
#: src/accounting/transaction/template.py:101
msgid "Tomorrow"
msgstr "明天"
#: src/accounting/transaction/template.py:105
msgid "The day before yesterday"
msgstr "前天"
#: src/accounting/transaction/template.py:107
msgid "The day after tomorrow"
msgstr "後天"
#: src/accounting/transaction/views.py:108
msgid "The transaction is added successfully" msgid "The transaction is added successfully"
msgstr "傳票加好了。" msgstr "傳票加好了。"
#: src/accounting/transaction/views.py:160 #: src/accounting/transaction/views.py:162
msgid "The transaction was not modified." msgid "The transaction was not modified."
msgstr "傳票未異動。" msgstr "傳票未異動。"
#: src/accounting/transaction/views.py:165 #: src/accounting/transaction/views.py:167
msgid "The transaction is updated successfully." msgid "The transaction is updated successfully."
msgstr "傳票存好了。" msgstr "傳票存好了。"
#: src/accounting/transaction/views.py:181 #: src/accounting/transaction/views.py:183
msgid "The transaction is deleted successfully." msgid "The transaction is deleted successfully."
msgstr "傳票刪掉了" msgstr "傳票刪掉了"
@ -982,9 +660,3 @@ msgctxt "Pagination|"
msgid "Next" msgid "Next"
msgstr "下一頁" msgstr "下一頁"
#~ msgid "Number"
#~ msgstr "數量"
#~ msgid "in %(time)s"
#~ msgstr "%(period)s"

View File

@ -26,7 +26,7 @@ from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse, \
from flask import request from flask import request
from werkzeug.routing import RequestRedirect from werkzeug.routing import RequestRedirect
from accounting.locale import pgettext from accounting.locale import gettext, pgettext
class Link: class Link:

View File

@ -35,8 +35,6 @@ from testlib_txn import Accounts, get_add_form, get_unchanged_update_form, \
PREFIX: str = "/accounting/transactions" PREFIX: str = "/accounting/transactions"
"""The URL prefix for the transaction management.""" """The URL prefix for the transaction management."""
RETURN_TO_URI: str = "/accounting/reports/journal"
"""The URL to return to after the operation."""
class CashIncomeTransactionTestCase(unittest.TestCase): class CashIncomeTransactionTestCase(unittest.TestCase):
@ -84,6 +82,9 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
update_form["csrf_token"] = csrf_token update_form["csrf_token"] = csrf_token
response: httpx.Response response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{txn_id}") response = client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
@ -116,6 +117,9 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
update_form["csrf_token"] = csrf_token update_form["csrf_token"] = csrf_token
response: httpx.Response response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/{txn_id}") response = client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -145,6 +149,9 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
update_form: dict[str, str] = self.__get_update_form(txn_id) update_form: dict[str, str] = self.__get_update_form(txn_id)
response: httpx.Response response: httpx.Response
response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{txn_id}") response = self.client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -168,7 +175,7 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/{txn_id}/delete", response = self.client.post(f"{PREFIX}/{txn_id}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], RETURN_TO_URI) self.assertEqual(response.headers["Location"], PREFIX)
def test_add(self) -> None: def test_add(self) -> None:
"""Tests to add the transactions. """Tests to add the transactions.
@ -636,6 +643,9 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
update_form["csrf_token"] = csrf_token update_form["csrf_token"] = csrf_token
response: httpx.Response response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{txn_id}") response = client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
@ -668,6 +678,9 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
update_form["csrf_token"] = csrf_token update_form["csrf_token"] = csrf_token
response: httpx.Response response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/{txn_id}") response = client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -697,6 +710,9 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
update_form: dict[str, str] = self.__get_update_form(txn_id) update_form: dict[str, str] = self.__get_update_form(txn_id)
response: httpx.Response response: httpx.Response
response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{txn_id}") response = self.client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -720,7 +736,7 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/{txn_id}/delete", response = self.client.post(f"{PREFIX}/{txn_id}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], RETURN_TO_URI) self.assertEqual(response.headers["Location"], PREFIX)
def test_add(self) -> None: def test_add(self) -> None:
"""Tests to add the transactions. """Tests to add the transactions.
@ -1195,6 +1211,9 @@ class TransferTransactionTestCase(unittest.TestCase):
update_form["csrf_token"] = csrf_token update_form["csrf_token"] = csrf_token
response: httpx.Response response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{txn_id}") response = client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
@ -1227,6 +1246,9 @@ class TransferTransactionTestCase(unittest.TestCase):
update_form["csrf_token"] = csrf_token update_form["csrf_token"] = csrf_token
response: httpx.Response response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/{txn_id}") response = client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -1256,6 +1278,9 @@ class TransferTransactionTestCase(unittest.TestCase):
update_form: dict[str, str] = self.__get_update_form(txn_id) update_form: dict[str, str] = self.__get_update_form(txn_id)
response: httpx.Response response: httpx.Response
response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{txn_id}") response = self.client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -1279,7 +1304,7 @@ class TransferTransactionTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/{txn_id}/delete", response = self.client.post(f"{PREFIX}/{txn_id}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], RETURN_TO_URI) self.assertEqual(response.headers["Location"], PREFIX)
def test_add(self) -> None: def test_add(self) -> None:
"""Tests to add the transactions. """Tests to add the transactions.
@ -1716,7 +1741,7 @@ class TransferTransactionTestCase(unittest.TestCase):
""" """
from accounting.models import Transaction, TransactionCurrency from accounting.models import Transaction, TransactionCurrency
txn_id: int = add_txn(self.client, self.__get_add_form()) txn_id: int = add_txn(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" detail_uri: str = f"{PREFIX}/{txn_id}?as=income&next=%2F_next"
update_uri: str = f"{PREFIX}/{txn_id}/update?as=income" update_uri: str = f"{PREFIX}/{txn_id}/update?as=income"
form_0: dict[str, str] = self.__get_update_form(txn_id) form_0: dict[str, str] = self.__get_update_form(txn_id)
form_0 = {x: form_0[x] for x in form_0 if "-debit-" not in x} form_0 = {x: form_0[x] for x in form_0 if "-debit-" not in x}
@ -1815,7 +1840,7 @@ class TransferTransactionTestCase(unittest.TestCase):
""" """
from accounting.models import Transaction, TransactionCurrency from accounting.models import Transaction, TransactionCurrency
txn_id: int = add_txn(self.client, self.__get_add_form()) txn_id: int = add_txn(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next" detail_uri: str = f"{PREFIX}/{txn_id}?as=expense&next=%2F_next"
update_uri: str = f"{PREFIX}/{txn_id}/update?as=expense" update_uri: str = f"{PREFIX}/{txn_id}/update?as=expense"
form_0: dict[str, str] = self.__get_update_form(txn_id) form_0: dict[str, str] = self.__get_update_form(txn_id)
form_0 = {x: form_0[x] for x in form_0 if "-credit-" not in x} form_0 = {x: form_0[x] for x in form_0 if "-credit-" not in x}

View File

@ -137,48 +137,48 @@ def get_unchanged_update_form(txn_id: int, app: Flask, csrf_token: str) \
assert txn is not None assert txn is not None
currencies: list[TransactionCurrency] = txn.currencies currencies: list[TransactionCurrency] = txn.currencies
form: dict[str, str] = {"csrf_token": csrf_token, form: dict[str, str] = {"csrf_token": csrf_token,
"next": NEXT_URI, "next": NEXT_URI,
"date": txn.date, "date": txn.date,
"note": " \n \n\n " if txn.note is None "note": " \n \n\n " if txn.note is None
else f"\n \n\n \n \n{txn.note} \n\n "} else f"\n \n\n \n \n{txn.note} \n\n "}
currency_indices_used: set[int] = set() currency_indices_used: set[int] = set()
currency_no: int = 0 currency_no: int = 0
for currency in currencies: for currency in currencies:
currency_index: int = __get_new_index(currency_indices_used) currency_index: int = __get_new_index(currency_indices_used)
currency_no = currency_no + 3 + randbelow(3) currency_no = currency_no + 3 + randbelow(3)
currency_prefix: str = f"currency-{currency_index}" currency_prefix: str = f"currency-{currency_index}"
form[f"{currency_prefix}-no"] = str(currency_no) form[f"{currency_prefix}-no"] = str(currency_no)
form[f"{currency_prefix}-code"] = currency.code form[f"{currency_prefix}-code"] = currency.code
entry_indices_used: set[int] entry_indices_used: set[int]
entry_no: int entry_no: int
prefix: str prefix: str
entry_indices_used = set() entry_indices_used = set()
entry_no = 0 entry_no = 0
for entry in currency.debit: for entry in currency.debit:
entry_index: int = __get_new_index(entry_indices_used) entry_index: int = __get_new_index(entry_indices_used)
entry_no = entry_no + 3 + randbelow(3) entry_no = entry_no + 3 + randbelow(3)
prefix = f"{currency_prefix}-debit-{entry_index}" prefix = f"{currency_prefix}-debit-{entry_index}"
form[f"{prefix}-eid"] = str(entry.id) form[f"{prefix}-eid"] = str(entry.id)
form[f"{prefix}-no"] = str(entry_no) form[f"{prefix}-no"] = str(entry_no)
form[f"{prefix}-account_code"] = entry.account.code form[f"{prefix}-account_code"] = entry.account.code
form[f"{prefix}-summary"] \ form[f"{prefix}-summary"] \
= " " if entry.summary is None else f" {entry.summary} " = " " if entry.summary is None else f" {entry.summary} "
form[f"{prefix}-amount"] = str(entry.amount) form[f"{prefix}-amount"] = str(entry.amount)
entry_indices_used = set() entry_indices_used = set()
entry_no = 0 entry_no = 0
for entry in currency.credit: for entry in currency.credit:
entry_index: int = __get_new_index(entry_indices_used) entry_index: int = __get_new_index(entry_indices_used)
entry_no = entry_no + 3 + randbelow(3) entry_no = entry_no + 3 + randbelow(3)
prefix = f"{currency_prefix}-credit-{entry_index}" prefix = f"{currency_prefix}-credit-{entry_index}"
form[f"{prefix}-eid"] = str(entry.id) form[f"{prefix}-eid"] = str(entry.id)
form[f"{prefix}-no"] = str(entry_no) form[f"{prefix}-no"] = str(entry_no)
form[f"{prefix}-account_code"] = entry.account.code form[f"{prefix}-account_code"] = entry.account.code
form[f"{prefix}-summary"] \ form[f"{prefix}-summary"] \
= " " if entry.summary is None else f" {entry.summary} " = " " if entry.summary is None else f" {entry.summary} "
form[f"{prefix}-amount"] = str(entry.amount) form[f"{prefix}-amount"] = str(entry.amount)
return form return form