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:
:show-inheritance:
accounting.account.queries module
---------------------------------
accounting.account.query module
-------------------------------
.. automodule:: accounting.account.queries
.. automodule:: accounting.account.query
:members:
:undoc-members:
:show-inheritance:

View File

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

View File

@ -28,10 +28,10 @@ accounting.currency.forms module
:undoc-members:
:show-inheritance:
accounting.currency.queries module
----------------------------------
accounting.currency.query module
--------------------------------
.. automodule:: accounting.currency.queries
.. automodule:: accounting.currency.query
:members:
:undoc-members:
: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.base_account
accounting.currency
accounting.report
accounting.transaction
accounting.utils
@ -33,22 +32,6 @@ accounting.models module
:undoc-members:
: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
---------------

View File

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

View File

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

View File

@ -52,7 +52,7 @@ class BaseAccount(db.Model):
:return: The string representation of the base account.
"""
return f"{self.code} {self.title}"
return F"{self.code} {self.title}"
@property
def title(self) -> str:
@ -141,11 +141,17 @@ class Account(db.Model):
entries = db.relationship("JournalEntry", back_populates="account")
"""The journal entries."""
CASH_CODE: str = "1111-001"
__CASH = "1111-001"
"""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,"""
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,"""
def __str__(self) -> str:
@ -153,7 +159,7 @@ class Account(db.Model):
: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
def code(self) -> str:
@ -161,7 +167,7 @@ class Account(db.Model):
:return: The code.
"""
return f"{self.base_code}-{self.no:03d}"
return F"{self.base_code}-{self.no:03d}"
@property
def title(self) -> str:
@ -265,7 +271,23 @@ class Account(db.Model):
: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
def accumulated_change(cls) -> t.Self:
@ -273,7 +295,23 @@ class Account(db.Model):
: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
def is_modified(self) -> bool:
@ -354,7 +392,7 @@ class Currency(db.Model):
:return: The string representation of the currency.
"""
return f"{self.name} ({self.code})"
return F"{self.name} ({self.code})"
@property
def name(self) -> str:
@ -550,7 +588,7 @@ class Transaction(db.Model):
for currency in self.currencies:
if len(currency.debit) > 1:
return False
if currency.debit[0].account.code != Account.CASH_CODE:
if currency.debit[0].account.code != "1111-001":
return False
return True
@ -564,7 +602,7 @@ class Transaction(db.Model):
for currency in self.currencies:
if len(currency.credit) > 1:
return False
if currency.credit[0].account.code != Account.CASH_CODE:
if currency.credit[0].account.code != "1111-001":
return False
return True
@ -617,7 +655,7 @@ class JournalEntry(db.Model):
onupdate="CASCADE"),
nullable=False)
"""The account ID."""
account = db.relationship(Account, back_populates="entries", lazy=False)
account = db.relationship(Account, back_populates="entries")
"""The account."""
summary = db.Column(db.String, nullable=True)
"""The summary."""
@ -640,19 +678,3 @@ class JournalEntry(db.Model):
:return: The 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):
"""The supplier converter to convert the income and expenses log pseudo
account code from and to the corresponding pseudo account in the routes."""
"""The supplier converter to convert the income and expenses pseudo account
code from and to the corresponding pseudo account in the routes."""
def to_python(self, value: str) -> IncomeExpensesAccount:
"""Converts an account code to an account.

View File

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

View File

@ -63,8 +63,6 @@ class Period:
"""The period specification."""
self.desc: str = ""
"""The text description."""
self.is_a_month: bool = False
"""Whether the period is a whole month."""
self.is_type_month: bool = False
"""Whether the period is for the month chooser."""
self.is_a_year: bool = False
@ -87,13 +85,12 @@ class Period:
:return: None.
"""
self.spec = PeriodSpecification(self).spec
self.desc = PeriodDescription(self).desc
self.spec = self.__get_spec()
self.desc = self.__get_desc()
if self.start is None or self.end is None:
return
self.is_a_month = self.start.day == 1 \
and self.end == _month_end(self.start)
self.is_type_month = self.is_a_month
self.is_type_month \
= self.start.day == 1 and self.end == _month_end(self.start)
self.is_a_year = self.start == datetime.date(self.start.year, 1, 1) \
and self.end == datetime.date(self.start.year, 12, 31)
self.is_a_day = self.start == self.end
@ -126,6 +123,189 @@ class Period:
raise ValueError
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:
"""Returns whether the period is the specific year period.
@ -156,266 +336,6 @@ class Period:
return Period(None, self.start - datetime.timedelta(days=1))
class PeriodSpecification:
"""The period specification composer."""
def __init__(self, period: Period):
"""Constructs the period specification composer.
:param period: The period.
"""
self.__start: datetime.date = period.start
self.__end: datetime.date = period.end
self.spec: str = self.__get_spec()
def __get_spec(self) -> str:
"""Returns the period specification.
:return: The period specification.
"""
if self.__start is None and self.__end is None:
return "-"
if self.__end is None:
return self.__get_since_spec()
if self.__start is None:
return self.__get_until_spec()
try:
return self.__get_year_spec()
except ValueError:
pass
try:
return self.__get_month_spec()
except ValueError:
pass
return self.__get_day_spec()
def __get_since_spec(self) -> str:
"""Returns the period specification without the end day.
:return: The period specification without the end day
"""
if self.__start.month == 1 and self.__start.day == 1:
return self.__start.strftime("%Y-")
if self.__start.day == 1:
return self.__start.strftime("%Y-%m-")
return self.__start.strftime("%Y-%m-%d-")
def __get_until_spec(self) -> str:
"""Returns the period specification without the start day.
:return: The period specification without the start day
"""
if self.__end.month == 12 and self.__end.day == 31:
return self.__end.strftime("-%Y")
if (self.__end + datetime.timedelta(days=1)).day == 1:
return self.__end.strftime("-%Y-%m")
return self.__end.strftime("-%Y-%m-%d")
def __get_year_spec(self) -> str:
"""Returns the period specification as a year range.
:return: The period specification as a year range.
:raise ValueError: The period is not a year range.
"""
if self.__start.month != 1 or self.__start.day != 1 \
or self.__end.month != 12 or self.__end.day != 31:
raise ValueError
if self.__start.year == self.__end.year:
return "%04d" % self.__start.year
return "%04d-%04d" % (self.__start.year, self.__end.year)
def __get_month_spec(self) -> str:
"""Returns the period specification as a month range.
:return: The period specification as a month range.
:raise ValueError: The period is not a month range.
"""
if self.__start.day != 1 or self.__end != _month_end(self.__end):
raise ValueError
if self.__start.year == self.__end.year \
and self.__start.month == self.__end.month:
return "%04d-%02d" % (self.__start.year, self.__start.month)
return "%04d-%02d-%04d-%02d" % (
self.__start.year, self.__start.month,
self.__end.year, self.__end.month)
def __get_day_spec(self) -> str:
"""Returns the period specification as a day range.
:return: The period specification as a day range.
:raise ValueError: The period is a month or year range.
"""
if self.__start == self.__end:
return "%04d-%02d-%02d" % (
self.__start.year, self.__start.month, self.__start.day)
return "%04d-%02d-%02d-%04d-%02d-%02d" % (
self.__start.year, self.__start.month, self.__start.day,
self.__end.year, self.__end.month, self.__end.day)
class PeriodDescription:
"""The period description composer."""
def __init__(self, period: Period):
"""Constructs the period description composer.
:param period: The period.
"""
self.__start: datetime.date = period.start
self.__end: datetime.date = period.end
self.desc: str = self.__get_desc()
def __get_desc(self) -> str:
"""Returns the period description.
:return: The period description.
"""
if self.__start is None and self.__end is None:
return gettext("for all time")
if self.__start is None:
return self.__get_until_desc()
if self.__end is None:
return self.__get_since_desc()
try:
return self.__get_year_desc()
except ValueError:
pass
try:
return self.__get_month_desc()
except ValueError:
pass
return self.__get_day_desc()
def __get_since_desc(self) -> str:
"""Returns the description without the end day.
:return: The description without the end day.
"""
def get_start_desc() -> str:
"""Returns the description of the start day.
:return: The description of the start day.
"""
if self.__start.month == 1 and self.__start.day == 1:
return str(self.__start.year)
if self.__start.day == 1:
return self.__format_month(self.__start)
return self.__format_date(self.__start)
return gettext("since %(start)s", start=get_start_desc())
def __get_until_desc(self) -> str:
"""Returns the description without the start day.
:return: The description without the start day.
"""
def get_end_desc() -> str:
"""Returns the description of the end day.
:return: The description of the end day.
"""
if self.__end.month == 12 and self.__end.day == 31:
return str(self.__end.year)
if (self.__end + datetime.timedelta(days=1)).day == 1:
return self.__format_month(self.__end)
return self.__format_date(self.__end)
return gettext("until %(end)s", end=get_end_desc())
def __get_year_desc(self) -> str:
"""Returns the description as a year range.
:return: The description as a year range.
:raise ValueError: The period is not a year range.
"""
if self.__start.month != 1 or self.__start.day != 1 \
or self.__end.month != 12 or self.__end.day != 31:
raise ValueError
start: str = str(self.__start.year)
if self.__start.year == self.__end.year:
return self.__get_in_desc(start)
return self.__get_from_to_desc(start, str(self.__end.year))
def __get_month_desc(self) -> str:
"""Returns the description as a month range.
:return: The description as a month range.
:raise ValueError: The period is not a month range.
"""
if self.__start.day != 1 or self.__end != _month_end(self.__end):
raise ValueError
start: str = self.__format_month(self.__start)
if self.__start.year == self.__end.year \
and self.__start.month == self.__end.month:
return self.__get_in_desc(start)
if self.__start.year == self.__end.year:
return self.__get_from_to_desc(start, str(self.__end.month))
return self.__get_from_to_desc(start, self.__format_month(self.__end))
def __get_day_desc(self) -> str:
"""Returns the description as a day range.
:return: The description as a day range.
:raise ValueError: The period is a month or year range.
"""
start: str = self.__format_day(self.__start)
if self.__start == self.__end:
return self.__get_in_desc(start)
if self.__start.year == self.__end.year \
and self.__start.month == self.__end.month:
return self.__get_from_to_desc(start, str(self.__end.day))
if self.__start.year == self.__end.year:
end_month_day: str = f"{self.__end.month}/{self.__end.day}"
return self.__get_from_to_desc(start, end_month_day)
return self.__get_from_to_desc(start, self.__format_day(self.__end))
@staticmethod
def __format_date(date: datetime.date) -> str:
"""Formats a date.
:param date: The date.
:return: The formatted date.
"""
return f"{date.year}/{date.month}/{date.day}"
@staticmethod
def __format_month(month: datetime.date) -> str:
"""Formats a month.
:param month: The month.
:return: The formatted month.
"""
return f"{month.year}/{month.month}"
@staticmethod
def __format_day(day: datetime.date) -> str:
"""Formats a day.
:param day: The day.
:return: The formatted day.
"""
return f"{day.year}/{day.month}/{day.day}"
@staticmethod
def __get_in_desc(period: str) -> str:
"""Returns the description of a whole year, month, or day.
:param period: The time period.
:return: The description of a whole year, month, or day.
"""
return gettext("in %(period)s", period=period)
@staticmethod
def __get_from_to_desc(start: str, end: str) -> str:
"""Returns the description of a separated start and end.
:param start: The start.
:param end: The end.
:return: The description of the separated start and end.
"""
return gettext("in %(start)s-%(end)s", start=start, end=end)
class ThisMonth(Period):
"""The period of this month."""
def __init__(self):
@ -553,9 +473,8 @@ class YearPeriod(Period):
self.spec = str(year)
self.is_a_year = True
DATE_SPEC_RE: str = r"(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?"
"""The regular expression of a date specification."""
def _set_properties(self) -> None:
pass
def _parse_period_spec(text: str) \
@ -567,19 +486,22 @@ def _parse_period_spec(text: str) \
may be None.
: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 == "-":
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:
return __get_start(m[1], m[2], m[3]), \
__get_end(m[1], m[2], m[3])
m = re.match(f"^{DATE_SPEC_RE}-$", text)
m = re.match(r"^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?-$", text)
if m is not 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:
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:
return __get_start(m[1], m[2], m[3]), \
__get_end(m[4], m[5], m[6])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,35 +17,163 @@
"""The search.
"""
from datetime import datetime
from datetime import date, datetime
from decimal import Decimal
import sqlalchemy as sa
from flask import Response, render_template, request
from sqlalchemy.orm import selectinload
from accounting.locale import gettext
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
Transaction, JournalEntry
from accounting.utils.pagination import Pagination
from accounting.utils.query import parse_query_keywords
from .journal import get_csv_rows
from .utils.base_page_params import BasePageParams
from .utils.base_report import BaseReport
from .utils.csv_export import csv_download
from .utils.csv_export import BaseCSVRow, csv_download
from .utils.page_params import PageParams
from .utils.report_chooser import ReportChooser
from .utils.report_type import ReportType
class EntryCollector:
"""The report entry collector."""
class Entry:
"""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):
"""Constructs the report entry collector."""
self.entries: list[JournalEntry] = self.__query_entries()
"""The report entries."""
"""Constructs a search."""
"""The account."""
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.
:return: The journal entries.
@ -55,23 +183,15 @@ class EntryCollector:
return []
conditions: list[sa.BinaryExpression] = []
for k in keywords:
sub_conditions: list[sa.BinaryExpression] \
= [JournalEntry.summary.contains(k),
JournalEntry.account_id.in_(
self.__get_account_condition(k)),
conditions.append(sa.or_(
JournalEntry.summary.contains(k),
sa.cast(JournalEntry.amount, sa.String).contains(k),
JournalEntry.account_id.in_(self.__get_account_condition(k)),
JournalEntry.currency_code.in_(
self.__get_currency_condition(k)),
JournalEntry.transaction_id.in_(
self.__get_transaction_condition(k))]
try:
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()
self.__get_transaction_condition(k))))
return [Entry(x) for x in JournalEntry.query.filter(*conditions)]
@staticmethod
def __get_account_condition(k: str) -> sa.Select:
@ -116,7 +236,8 @@ class EntryCollector:
:param k: The keyword.
: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
try:
txn_date = datetime.strptime(k, "%Y")
@ -140,62 +261,39 @@ class EntryCollector:
pass
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:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
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:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
pagination: Pagination[JournalEntry] \
= Pagination[JournalEntry](self.__entries)
params: PageParams = PageParams(pagination=pagination,
entries=pagination.list)
pagination: Pagination[Entry] = Pagination[Entry](self.__entries)
page_entries: list[Entry] = pagination.list
_populate_entries(page_entries)
params: SearchPageParams = SearchPageParams(pagination=pagination,
entries=page_entries)
return render_template("accounting/report/search.html",
report=params)

View File

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

View File

@ -14,19 +14,16 @@
# 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 export the report as CSV for download.
"""The utility to export the report as CSV for download.
"""
import csv
from abc import ABC, abstractmethod
from datetime import timedelta, date
from decimal import Decimal
from io import StringIO
from flask import Response
from accounting.report.period import Period
class BaseCSVRow(ABC):
"""The base CSV row."""
@ -55,54 +52,3 @@ def csv_download(filename: str, rows: list[BaseCSVRow]) -> Response:
response.headers["Content-Disposition"] \
= f"attachment; filename={filename}"
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:
"""An option link."""
def __init__(self, title: str, url: str, is_active: bool,
fa_icon: str | None = None):
def __init__(self, title: str, url: str, is_active: bool):
"""Constructs an option link.
:param title: The title.
@ -33,4 +32,3 @@ class OptionLink:
self.title: str = title
self.url: str = url
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, \
urlunparse
import sqlalchemy as sa
from flask import request
from accounting import db
from accounting.models import Currency, JournalEntry
from accounting.utils.txn_types import TransactionType
from .option_link import OptionLink
from .report_chooser import ReportChooser
class BasePageParams(ABC):
"""The base HTML page parameters class."""
class PageParams(ABC):
"""The page parameters of a report."""
@property
@abstractmethod
@ -70,19 +66,3 @@ class BasePageParams(ABC):
parts: list[str] = list(uri_p)
parts[4] = urlencode(params)
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).
"""
import typing as t
from abc import ABC, abstractmethod
from datetime import date
from flask import url_for
from accounting.models import Currency, Account, Transaction
from accounting.report.income_expense_account import IncomeExpensesAccount
from accounting.report.period import YearPeriod, Period, ThisMonth, \
LastMonth, SinceLastMonth, ThisYear, LastYear, Today, Yesterday, \
TemplatePeriod
from .urls import journal_url, ledger_url, income_expenses_url, \
trial_balance_url, income_statement_url, balance_sheet_url
class PeriodChooser(ABC):
@ -72,7 +73,7 @@ class PeriodChooser(ABC):
"""Whether there is data in last year."""
self.has_yesterday: bool = False
"""Whether there is data in yesterday."""
self.available_years: list[int] = []
self.available_years: t.Iterator[int] = []
"""The available years."""
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_year = start.year < today.year
self.has_yesterday = start < today
self.available_years: t.Iterator[int] = []
if start.year < today.year - 1:
self.available_years \
= reversed(range(start.year, today.year - 1))
@ -112,7 +114,9 @@ class JournalPeriodChooser(PeriodChooser):
super().__init__(None if first is None else first.date)
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):
@ -129,14 +133,19 @@ class LedgerPeriodChooser(PeriodChooser):
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
return ledger_url(self.currency, self.account, period)
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):
"""The income and expenses log period chooser."""
"""The income and expenses period chooser."""
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
"""The currency."""
self.account: IncomeExpensesAccount = account
@ -146,7 +155,12 @@ class IncomeExpensesPeriodChooser(PeriodChooser):
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
return income_expenses_url(self.currency, self.account, period)
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):
@ -161,7 +175,11 @@ class TrialBalancePeriodChooser(PeriodChooser):
super().__init__(None if first is None else first.date)
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):
@ -176,7 +194,11 @@ class IncomeStatementPeriodChooser(PeriodChooser):
super().__init__(None if first is None else first.date)
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):
@ -191,4 +213,8 @@ class BalanceSheetPeriodChooser(PeriodChooser):
super().__init__(None if first is None else first.date)
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 typing as t
from flask import url_for
from flask_babel import LazyString
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, Account
from accounting.report.income_expense_account import IncomeExpensesAccount
from accounting.report.period import Period
from accounting.template_globals import default_currency_code
from .option_link import OptionLink
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:
@ -60,15 +58,13 @@ class ReportChooser:
Currency, default_currency_code()) \
if currency is None else currency
"""The currency."""
self.__account: Account = Account.cash() if account is None \
else account
self.__account: Account = Account.find_by_code("1111-001") \
if account is None else account
"""The currency."""
self.__reports: list[OptionLink] = []
"""The links to the reports."""
self.current_report: str | LazyString = ""
"""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.__ledger)
self.__reports.append(self.__income_expenses)
@ -78,8 +74,6 @@ class ReportChooser:
for report in self.__reports:
if report.is_active:
self.current_report = report.title
if self.is_search:
self.current_report = gettext("Search")
@property
def __journal(self) -> OptionLink:
@ -87,9 +81,11 @@ class ReportChooser:
:return: The journal.
"""
return OptionLink(gettext("Journal"), journal_url(self.__period),
self.__active_report == ReportType.JOURNAL,
fa_icon="fa-solid fa-book")
url: str = url_for("accounting.report.journal-default") \
if self.__period.is_default \
else url_for("accounting.report.journal", period=self.__period)
return OptionLink(gettext("Journal"), url,
self.__active_report == ReportType.JOURNAL)
@property
def __ledger(self) -> OptionLink:
@ -97,27 +93,32 @@ class ReportChooser:
:return: The ledger.
"""
return OptionLink(gettext("Ledger"),
ledger_url(self.__currency, self.__account,
self.__period),
self.__active_report == ReportType.LEDGER,
fa_icon="fa-solid fa-clipboard")
url: str = url_for("accounting.report.ledger-default",
currency=self.__currency, account=self.__account) \
if self.__period.is_default \
else url_for("accounting.report.ledger",
currency=self.__currency, account=self.__account,
period=self.__period)
return OptionLink(gettext("Ledger"), url,
self.__active_report == ReportType.LEDGER)
@property
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
if not re.match(r"[12][12]", account.base_code):
account: Account = Account.cash()
return OptionLink(gettext("Income and Expenses Log"),
income_expenses_url(self.__currency,
IncomeExpensesAccount(account),
self.__period),
self.__active_report == ReportType.INCOME_EXPENSES,
fa_icon="fa-solid fa-money-bill-wave")
account: Account = Account.find_by_code("1111-001")
url: str = url_for("accounting.report.income-expenses-default",
currency=self.__currency, account=account) \
if self.__period.is_default \
else url_for("accounting.report.income-expenses",
currency=self.__currency, account=account,
period=self.__period)
return OptionLink(gettext("Income and Expenses"), url,
self.__active_report == ReportType.INCOME_EXPENSES)
@property
def __trial_balance(self) -> OptionLink:
@ -125,10 +126,13 @@ class ReportChooser:
:return: The trial balance.
"""
return OptionLink(gettext("Trial Balance"),
trial_balance_url(self.__currency, self.__period),
self.__active_report == ReportType.TRIAL_BALANCE,
fa_icon="fa-solid fa-scale-unbalanced")
url: str = url_for("accounting.report.trial-balance-default",
currency=self.__currency) \
if self.__period.is_default \
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
def __income_statement(self) -> OptionLink:
@ -136,10 +140,13 @@ class ReportChooser:
:return: The income statement.
"""
return OptionLink(gettext("Income Statement"),
income_statement_url(self.__currency, self.__period),
self.__active_report == ReportType.INCOME_STATEMENT,
fa_icon="fa-solid fa-file-invoice-dollar")
url: str = url_for("accounting.report.income-statement-default",
currency=self.__currency) \
if self.__period.is_default \
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
def __balance_sheet(self) -> OptionLink:
@ -147,10 +154,13 @@ class ReportChooser:
:return: The balance sheet.
"""
return OptionLink(gettext("Balance Sheet"),
balance_sheet_url(self.__currency, self.__period),
self.__active_report == ReportType.BALANCE_SHEET,
fa_icon="fa-solid fa-scale-balanced")
url: str = url_for("accounting.report.balance-sheet-default",
currency=self.__currency) \
if self.__period.is_default \
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]:
"""Returns the iteration of the reports.

View File

@ -27,7 +27,7 @@ class ReportType(Enum):
LEDGER: str = "ledger"
"""The ledger."""
INCOME_EXPENSES: str = "income-expenses"
"""The income and expenses log."""
"""The income and expenses."""
TRIAL_BALANCE: str = "trial-balance"
"""The trial balance."""
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,
account: IncomeExpensesAccount) \
-> 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 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())
@ -131,12 +131,12 @@ def get_default_income_expenses_list(currency: Currency,
def get_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount,
period: Period) -> str | Response:
"""Returns the income and expenses log.
"""Returns the income and expenses.
:param currency: The currency.
:param account: The account.
: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)
@ -144,12 +144,12 @@ def get_income_expenses_list(currency: Currency,
def __get_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount,
period: Period) -> str | Response:
"""Returns the income and expenses log.
"""Returns the income and expenses.
:param currency: The currency.
:param account: The account.
: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)
if "as" in request.args and request.args["as"] == "csv":

View File

@ -24,6 +24,19 @@
.accounting-clickable {
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 {
height: 6rem;
}
@ -32,61 +45,6 @@
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 */
.accounting-card {
padding: 2em 1.5em;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ First written: 2023/2/1
{% 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 %}">
<i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }}

View File

@ -25,19 +25,31 @@ First written: 2023/1/30
{% block content %}
<div class="mb-2 accounting-toolbar">
<div class="btn-group mb-2 d-none d-md-inline-flex">
{% 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>
{{ A_("New") }}
</a>
{% 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">
<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">
<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-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>
<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>
</label>
</form>

View File

@ -25,13 +25,13 @@ First written: 2023/1/26
{% block content %}
<div class="mb-2 accounting-toolbar">
<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">
<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">
<div class="btn-group mb-2">
<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-search" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label for="accounting-search" class="accounting-search-label">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
<span class="d-none d-md-inline">{{ A_("Search") }}</span>
{{ A_("Search") }}
</button>
</label>
</form>

View File

@ -27,7 +27,7 @@ First written: 2023/2/6
{% 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 %}">
<i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }}

View File

@ -25,19 +25,31 @@ First written: 2023/2/6
{% block content %}
<div class="mb-2 accounting-toolbar">
<div class="btn-group mb-2 d-none d-md-inline-flex">
{% 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>
{{ A_("New") }}
</a>
{% 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">
<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">
<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-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>
<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>
</label>
</form>

View File

@ -22,13 +22,13 @@ First written: 2023/2/25
{% 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-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>
<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>
<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>
</div>

View File

@ -26,35 +26,129 @@ First written: 2023/3/7
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% 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 %}
<div class="mb-3 accounting-toolbar">
{% with use_currency_chooser = true,
use_period_chooser = true %}
{% include "accounting/report/include/toolbar-buttons.html" %}
<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=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 %}
<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>
{% 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" %}
{% if report.has_data %}
<div class="accounting-sheet">
<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 class="row accounting-report-table accounting-balance-sheet-table">
<div class="col-sm-6">
{% if report.assets.subsections %}
{% with section = report.assets %}
{% include "accounting/report/include/balance-sheet-section.html" %}
{% endwith %}
<div class="accounting-report-table-row accounting-balance-sheet-section">
<div>{{ report.assets.title.title|title }}</div>
</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>{{ A_("Total") }}</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">
{% if report.liabilities.subsections %}
{% with section = report.liabilities %}
{% include "accounting/report/include/balance-sheet-section.html" %}
{% endwith %}
<div class="accounting-report-table-row accounting-balance-sheet-section">
<div>{{ report.liabilities.title.title|title }}</div>
</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>{{ A_("Total") }}</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 %}
{% if report.owner_s_equity.subsections %}
{% with section = report.owner_s_equity %}
{% include "accounting/report/include/balance-sheet-section.html" %}
{% endwith %}
<div class="accounting-report-table-row accounting-balance-sheet-section">
<div>{{ report.owner_s_equity.title.title|title }}</div>
</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>{{ 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>

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
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.

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
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.

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)
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-content">
<div class="modal-header">
@ -30,62 +30,62 @@ First written: 2023/3/4
{# Tab navigation #}
<ul class="nav nav-tabs mb-2">
<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") }}
</span>
</li>
<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") }}
</span>
</li>
<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") }}
</span>
</li>
<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") }}
</span>
</li>
</ul>
{# 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>
<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>
{% if report.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 }}">
{% if period_chooser.has_last_month %}
<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>
<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>
{% endif %}
</div>
{% if report.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>
{% if period_chooser.has_data %}
<div id="accounting-period-chooser-month-chooser" class="mt-3" data-start="{{ period_chooser.data_start }}" data-default="{{ period.start }}"></div>
{% endif %}
</div>
{# 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">
<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 }}">
<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 period.is_this_year %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.this_year_url }}">
{{ A_("This year") }}
</a>
{% if report.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 }}">
{% if period_chooser.has_last_year %}
<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>
{% endif %}
{% if report.period_chooser.available_years %}
{% if period_chooser.available_years %}
<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">
<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>
{% endfor %}
</ul>
@ -93,21 +93,21 @@ First written: 2023/3/4
</div>
{# 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>
<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>
{% if report.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 }}">
{% if period_chooser.has_yesterday %}
<a class="btn {% if period.is_yesterday %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.yesterday_url }}">
{{ A_("Yesterday") }}
</a>
{% endif %}
</div>
{% if report.period_chooser.has_data %}
{% if period_chooser.has_data %}
<div class="mt-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>
<div id="accounting-period-chooser-day-date-error" class="invalid-feedback"></div>
</div>
@ -116,22 +116,22 @@ First written: 2023/3/4
</div>
{# 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>
<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>
</div>
{% if report.period_chooser.has_data %}
{% if period_chooser.has_data %}
<div class="mt-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>
<div id="accounting-period-chooser-custom-start-error" class="invalid-feedback"></div>
</div>
<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>
<div id="accounting-period-chooser-custom-end-error" class="invalid-feedback"></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
income-expenses.html: The income and expenses log
income-expenses.html: The income and expenses
Copyright (c) 2023 imacat.
@ -24,23 +24,127 @@ First written: 2023/3/5
{% 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/period-chooser.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script>
{% 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 %}
<div class="mb-3 accounting-toolbar">
{% with use_currency_chooser = true,
use_account_chooser = true,
use_period_chooser = true %}
{% include "accounting/report/include/toolbar-buttons.html" %}
<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=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 %}
<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>
{% 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" %}
@ -64,13 +168,23 @@ First written: 2023/3/5
{% if report.brought_forward %}
{% with entry = report.brought_forward %}
<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>
{% endwith %}
{% endif %}
{% for entry in report.entries %}
<a class="accounting-report-table-row" href="{{ entry.url|accounting_append_next }}">
{% include "accounting/report/include/income-expenses-row-desktop.html" %}
<a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
<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>
{% endfor %}
</div>
@ -92,19 +206,19 @@ First written: 2023/3/5
{% if report.brought_forward %}
{% with entry = report.brought_forward %}
<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>
{% endwith %}
{% endif %}
{% 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 }}">
{% include "accounting/report/include/income-expenses-row-mobile.html" %}
<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-mobile-row.html" %}
</a>
{% endfor %}
{% if report.total %}
{% with entry = report.total %}
<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>
{% endwith %}
{% endif %}

View File

@ -26,27 +26,102 @@ First written: 2023/3/7
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% 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 %}
<div class="mb-3 accounting-toolbar">
{% with use_currency_chooser = true,
use_period_chooser = true %}
{% include "accounting/report/include/toolbar-buttons.html" %}
<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=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 %}
<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>
{% 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" %}
{% if report.has_data %}
<div class="accounting-sheet">
<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 class="accounting-report-table accounting-income-statement-table">

View File

@ -24,21 +24,69 @@ First written: 2023/3/4
{% 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/period-chooser.js") }}"></script>
{# <script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script> #}
{% 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 %}
<div class="mb-3 accounting-toolbar">
{% with use_period_chooser = true %}
{% include "accounting/report/include/toolbar-buttons.html" %}
<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=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 %}
<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>
{% 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" %}

View File

@ -24,23 +24,127 @@ First written: 2023/3/5
{% 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/period-chooser.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script>
{% 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 %}
<div class="mb-3 accounting-toolbar">
{% with use_currency_chooser = true,
use_account_chooser = true,
use_period_chooser = true %}
{% include "accounting/report/include/toolbar-buttons.html" %}
<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=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 %}
<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>
{% 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" %}
@ -63,13 +167,21 @@ First written: 2023/3/5
{% if report.brought_forward %}
{% with entry = report.brought_forward %}
<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>
{% endwith %}
{% endif %}
{% for entry in report.entries %}
<a class="accounting-report-table-row" href="{{ entry.url|accounting_append_next }}">
{% include "accounting/report/include/ledger-row-desktop.html" %}
<a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
<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>
{% endfor %}
</div>
@ -91,19 +203,19 @@ First written: 2023/3/5
{% if report.brought_forward %}
{% with entry = report.brought_forward %}
<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>
{% endwith %}
{% endif %}
{% 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 }}">
{% include "accounting/report/include/ledger-row-mobile.html" %}
<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-mobile-row.html" %}
</a>
{% endfor %}
{% if report.total %}
{% with entry = report.total %}
<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>
{% endwith %}
{% endif %}

View File

@ -23,19 +23,75 @@ First written: 2023/3/8
{% 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/period-chooser.js") }}"></script>
{% endblock %}
{% block header %}{% block title %}{{ A_("Search Result for \"%(query)s\"", query=request.args.q) }}{% endblock %}{% endblock %}
{% block content %}
<div class="mb-3 accounting-toolbar">
{% with use_search = true %}
{% include "accounting/report/include/toolbar-buttons.html" %}
<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=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 %}
<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>
{% 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" %}

View File

@ -26,27 +26,102 @@ First written: 2023/3/5
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% 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 %}
<div class="mb-3 accounting-toolbar">
{% with use_currency_chooser = true,
use_period_chooser = true %}
{% include "accounting/report/include/toolbar-buttons.html" %}
<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=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 %}
<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>
{% 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" %}
{% if report.has_data %}
<div class="accounting-sheet">
<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 class="accounting-report-table accounting-trial-balance-table">
@ -77,7 +152,6 @@ First written: 2023/3/5
</div>
</div>
</div>
</div>
{% else %}
<p>{{ A_("There is no data.") }}</p>
{% endif %}

View File

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

View File

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

View File

@ -30,7 +30,7 @@ First written: 2023/2/26
{% 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 %}">
<i class="fa-solid fa-circle-chevron-left"></i>
{{ 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 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 %}

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 %}
<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>
{{ A_("Back") }}
</a>

View File

@ -23,6 +23,6 @@ First written: 2023/2/25
{% block header %}{% block title %}{{ A_("Add a New Transfer Transaction") }}{% endblock %}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.journal-default") }}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting.transaction.list") }}{% 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."""
MISSING_ACCOUNT: LazyString = lazy_gettext("Please select the account.")
"""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:
@ -577,7 +574,8 @@ class IncomeCurrencyForm(CurrencyForm):
class IncomeTransactionForm(TransactionForm):
"""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."""
currencies = FieldList(FormField(IncomeCurrencyForm), name="currency",
validators=[NeedSomeCurrencies()])
@ -650,7 +648,8 @@ class ExpenseCurrencyForm(CurrencyForm):
class ExpenseTransactionForm(TransactionForm):
"""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."""
currencies = FieldList(FormField(ExpenseCurrencyForm), name="currency",
validators=[NeedSomeCurrencies()])
@ -759,7 +758,8 @@ class TransferCurrencyForm(CurrencyForm):
class TransferTransactionForm(TransactionForm):
"""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."""
currencies = FieldList(FormField(TransferCurrencyForm), name="currency",
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.utils.flash_errors import flash_form_errors
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.txn_types import TransactionType
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 .forms import sort_transactions_in, TransactionReorderForm
from .queries import get_transaction_query
from .template_filters import with_type, to_transfer, format_amount_input, \
text2html
@ -47,6 +49,20 @@ bp.add_app_template_filter(format_amount_input,
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")
@has_permission(can_edit)
def show_add_transaction_form(txn_type: TransactionType) -> str:
@ -142,12 +158,12 @@ def update_transaction(txn: Transaction) -> redirect:
form.populate_obj(txn)
if not form.is_modified:
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_at = sa.func.now()
db.session.commit()
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")
@ -163,7 +179,7 @@ def delete_transaction(txn: Transaction) -> redirect:
sort_transactions_in(txn.date, txn.id)
db.session.commit()
flash(lazy_gettext("The transaction is deleted successfully."), "success")
return redirect(or_next(url_for("accounting.report.journal-default")))
return redirect(or_next(with_type(url_for("accounting.transaction.list"))))
@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")
@has_permission(can_edit)
def sort_transactions(txn_date: date) -> redirect:
def sort_accounts(txn_date: date) -> redirect:
"""Reorders the transactions in a date.
:param txn_date: The date.
@ -194,10 +210,10 @@ def sort_transactions(txn_date: date) -> redirect:
form.save_order()
if not form.is_modified:
flash(lazy_gettext("The order was not modified."), "success")
return redirect(or_next(url_for("accounting.report.journal-default")))
return redirect(or_next(url_for("accounting.account.list")))
db.session.commit()
flash(lazy_gettext("The order is updated successfully."), "success")
return redirect(or_next(url_for("accounting.report.journal-default")))
return redirect(or_next(url_for("accounting.account.list")))
def __get_detail_uri(txn: Transaction) -> str:

View File

@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Mia! Accounting Flask 0.0.0\n"
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
"POT-Creation-Date: 2023-03-08 19:11+0800\n"
"PO-Revision-Date: 2023-03-08 19:11+0800\n"
"POT-Creation-Date: 2023-03-01 00:51+0800\n"
"PO-Revision-Date: 2023-03-01 00:51+0800\n"
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
"Language: zh_Hant\n"
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
@ -17,45 +17,23 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\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
msgid "Cash Expense Transaction#%(id)s"
msgstr "現金支出傳票#%(id)s"
#: src/accounting/models.py:520
#: src/accounting/models.py:577
#, python-format
msgid "Cash Income Transaction#%(id)s"
msgstr "現金收入傳票#%(id)s"
#: src/accounting/models.py:521
#: src/accounting/models.py:578
#, python-format
msgid "Transfer Transaction#%(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
msgid "The base account does not exist."
msgstr "沒有這個基本科目。"
@ -65,7 +43,7 @@ msgid "The base account is not available."
msgstr "不能選這個基本科目。"
#: 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."
msgstr "請選擇基本科目。"
@ -73,8 +51,7 @@ msgstr "請選擇基本科目。"
msgid "Please fill in the title"
msgstr "請填上標題。"
#: src/accounting/account/queries.py:50
#: src/accounting/report/reports/search.py:90
#: src/accounting/account/query.py:50
#: src/accounting/templates/accounting/account/detail.html:90
#: src/accounting/templates/accounting/account/list.html:74
msgid "Pay-off needed"
@ -96,36 +73,36 @@ msgstr "科目存好了。"
msgid "The account is deleted successfully."
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."
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."
msgstr "順序存好了。"
#: 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."
msgstr "代碼與其它貨幣重複。"
#: 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."
msgstr "請填上代碼。"
#: 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."
msgstr "代碼限為三個大寫英文字母。"
#: 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."
msgstr "不能用這個代碼。"
#: 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."
msgstr "請填上名稱。"
@ -145,329 +122,56 @@ msgstr "貨幣存好了。"
msgid "The currency is deleted successfully."
msgstr "貨幣刪掉了"
#: src/accounting/report/income_expense_account.py:62
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
#: src/accounting/static/js/account-form.js:177
msgid "Please fill in the title."
msgstr "請填上標題。"
#: src/accounting/static/js/period-chooser.js:269
#: src/accounting/static/js/transaction-form.js:489
#: 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
#: src/accounting/static/js/summary-helper.js:441
#: src/accounting/static/js/summary-helper.js:512
msgid "Please fill in the tag."
msgstr "請填上標籤。"
#: src/accounting/static/js/summary-editor.js:827
#: src/accounting/static/js/summary-editor.js:1023
#: src/accounting/static/js/summary-helper.js:460
#: src/accounting/static/js/summary-helper.js:550
msgid "Please fill in the origin."
msgstr "請填上起點。"
#: src/accounting/static/js/summary-editor.js:837
#: src/accounting/static/js/summary-editor.js:1033
#: src/accounting/static/js/summary-helper.js:479
#: src/accounting/static/js/summary-helper.js:569
msgid "Please fill in the destination."
msgstr "請填上終點。"
#: src/accounting/static/js/summary-editor.js:1013
#: src/accounting/static/js/summary-helper.js:531
msgid "Please fill in the route."
msgstr "請填上路線名稱。"
#: src/accounting/static/js/transaction-form.js:290
#: src/accounting/static/js/transaction-form.js:612
#: src/accounting/static/js/transaction-form.js:289
#: src/accounting/static/js/transaction-form.js:611
#: src/accounting/transaction/forms.py:47
msgid "Please select the account."
msgstr "請選擇科目。"
#: src/accounting/static/js/transaction-form.js:325
#: src/accounting/static/js/transaction-form.js:617
#: src/accounting/static/js/transaction-form.js:324
#: src/accounting/static/js/transaction-form.js:616
msgid "Please fill in the amount."
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
msgid "Please add some currencies."
msgstr "請加上貨幣。"
#: src/accounting/static/js/transaction-form.js:590
#: src/accounting/static/js/transaction-form.js:589
#: src/accounting/transaction/forms.py:78
msgid "Please add some journal entries."
msgstr "請加上分錄。"
#: src/accounting/static/js/transaction-form.js:655
#: src/accounting/transaction/forms.py:700
#: src/accounting/static/js/transaction-form.js:654
#: src/accounting/transaction/forms.py:672
msgid "The totals of the debit and credit amounts do not match."
msgstr "借方貸方合計不符。 "
@ -511,12 +215,10 @@ msgstr "科目刪除確認"
#: src/accounting/templates/accounting/account/detail.html:70
#: src/accounting/templates/accounting/account/include/form.html:91
#: 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/detail.html:71
#: 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"
msgstr "關閉"
@ -527,17 +229,15 @@ msgstr "你確定要刪掉這個科目嗎?"
#: src/accounting/templates/accounting/account/detail.html:76
#: src/accounting/templates/accounting/account/include/form.html:112
#: 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/detail.html:77
#: 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"
msgstr "取消"
#: src/accounting/templates/accounting/account/detail.html:77
#: 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
msgid "Confirm"
msgstr "確定"
@ -562,7 +262,6 @@ msgstr "%(account)s設定"
#: src/accounting/templates/accounting/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/report/search.html:28
#: src/accounting/templates/accounting/transaction/list.html:28
#, python-format
msgid "Search Result for \"%(query)s\""
@ -574,7 +273,6 @@ msgstr "科目管理"
#: src/accounting/templates/accounting/account/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/include/form.html:62
#: 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/currency/list.html:35
#: src/accounting/templates/accounting/report/search.html:37
#: src/accounting/templates/accounting/transaction/list.html:57
msgid "Search for Desktop"
msgstr "桌機版檢索"
@ -598,11 +295,6 @@ msgstr "桌機版檢索"
#: src/accounting/templates/accounting/base-account/list.html:34
#: src/accounting/templates/accounting/currency/list.html:40
#: 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/list.html:62
#: 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/currency/list.html:47
#: src/accounting/templates/accounting/report/search.html:50
#: src/accounting/templates/accounting/transaction/list.html:69
msgid "Search for Mobile"
msgstr "行動版檢索"
@ -621,13 +312,6 @@ msgstr "行動版檢索"
#: src/accounting/templates/accounting/account/order.html:81
#: src/accounting/templates/accounting/base-account/list.html:51
#: 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/list.html:93
#: 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/transaction/include/entry-form-modal.html:55
#: 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
msgid "Save"
msgstr "儲存"
@ -708,27 +392,13 @@ msgstr "代碼"
msgid "Name"
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
msgid "Accounting"
msgstr "記帳"
#: src/accounting/templates/accounting/include/nav.html:33
msgid "Reports"
msgstr "報表"
msgid "Transactions"
msgstr "傳票"
#: src/accounting/templates/accounting/include/nav.html:39
msgid "Accounts"
@ -746,100 +416,22 @@ msgstr "貨幣"
msgid "Page navigation"
msgstr "分頁瀏覽"
#: src/accounting/templates/accounting/report/balance-sheet.html:29
#: src/accounting/templates/accounting/report/balance-sheet.html:61
#, python-format
msgid "Balance Sheet of %(currency)s %(period)s"
msgstr "%(period)s%(currency)s資產負債表"
#: src/accounting/templates/accounting/transaction/list.html:28
msgid "Transaction Management"
msgstr "傳票管理"
#: 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
msgid "Cash Expense"
msgstr "現金支出"
#: src/accounting/templates/accounting/report/include/action-buttons.html:37
#: src/accounting/templates/accounting/transaction/list.html:46
msgid "Cash Income"
msgstr "現金收入"
#: src/accounting/templates/accounting/report/include/action-buttons.html:55
msgid "Report"
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/include/add-new-material-fab.html:32
#: src/accounting/templates/accounting/transaction/list.html:51
msgid "Transfer"
msgstr "轉帳"
#: src/accounting/templates/accounting/transaction/order.html:29
#, python-format
@ -863,6 +455,17 @@ msgstr "改轉帳"
msgid "Content"
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/income/edit.html:24
#: src/accounting/templates/accounting/transaction/transfer/edit.html:24
@ -870,6 +473,12 @@ msgstr "內容"
msgid "Editing %(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
msgid "Select Account"
msgstr "選擇科目"
@ -878,6 +487,14 @@ msgstr "選擇科目"
msgid "More…"
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
msgid "Delete Transaction Confirmation"
msgstr "傳票刪除確認"
@ -890,37 +507,68 @@ msgstr "你確定要刪掉這張傳票嗎?"
msgid "Journal Entry Content"
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"
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"
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"
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"
msgstr "帳單"
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:61
msgid "Annotation"
msgstr "註記"
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:59
msgid "Number"
msgstr "數量"
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:70
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:87
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:122
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:67
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:84
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:119
msgid "Tag"
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"
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"
msgstr "數量"
@ -932,6 +580,16 @@ msgstr "新增現金收入傳票"
msgid "Add a New Transfer Transaction"
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
msgid "Please select the currency."
msgstr "請選擇貨幣。"
@ -952,23 +610,43 @@ msgstr "金額請填正數。"
msgid "This account is not for debit entries."
msgstr "科目不是借方科目。"
#: src/accounting/transaction/forms.py:230
#: src/accounting/transaction/forms.py:201
msgid "This account is not for credit entries."
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"
msgstr "傳票加好了。"
#: src/accounting/transaction/views.py:160
#: src/accounting/transaction/views.py:162
msgid "The transaction was not modified."
msgstr "傳票未異動。"
#: src/accounting/transaction/views.py:165
#: src/accounting/transaction/views.py:167
msgid "The transaction is updated successfully."
msgstr "傳票存好了。"
#: src/accounting/transaction/views.py:181
#: src/accounting/transaction/views.py:183
msgid "The transaction is deleted successfully."
msgstr "傳票刪掉了"
@ -982,9 +660,3 @@ msgctxt "Pagination|"
msgid "Next"
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 werkzeug.routing import RequestRedirect
from accounting.locale import pgettext
from accounting.locale import gettext, pgettext
class Link:

View File

@ -35,8 +35,6 @@ from testlib_txn import Accounts, get_add_form, get_unchanged_update_form, \
PREFIX: str = "/accounting/transactions"
"""The URL prefix for the transaction management."""
RETURN_TO_URI: str = "/accounting/reports/journal"
"""The URL to return to after the operation."""
class CashIncomeTransactionTestCase(unittest.TestCase):
@ -84,6 +82,9 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
update_form["csrf_token"] = csrf_token
response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 403)
@ -116,6 +117,9 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
update_form["csrf_token"] = csrf_token
response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/{txn_id}")
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)
response: httpx.Response
response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 200)
@ -168,7 +175,7 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/{txn_id}/delete",
data={"csrf_token": self.csrf_token})
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:
"""Tests to add the transactions.
@ -636,6 +643,9 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
update_form["csrf_token"] = csrf_token
response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 403)
@ -668,6 +678,9 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
update_form["csrf_token"] = csrf_token
response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/{txn_id}")
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)
response: httpx.Response
response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 200)
@ -720,7 +736,7 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/{txn_id}/delete",
data={"csrf_token": self.csrf_token})
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:
"""Tests to add the transactions.
@ -1195,6 +1211,9 @@ class TransferTransactionTestCase(unittest.TestCase):
update_form["csrf_token"] = csrf_token
response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 403)
@ -1227,6 +1246,9 @@ class TransferTransactionTestCase(unittest.TestCase):
update_form["csrf_token"] = csrf_token
response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/{txn_id}")
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)
response: httpx.Response
response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{txn_id}")
self.assertEqual(response.status_code, 200)
@ -1279,7 +1304,7 @@ class TransferTransactionTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/{txn_id}/delete",
data={"csrf_token": self.csrf_token})
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:
"""Tests to add the transactions.
@ -1716,7 +1741,7 @@ class TransferTransactionTestCase(unittest.TestCase):
"""
from accounting.models import Transaction, TransactionCurrency
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"
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}
@ -1815,7 +1840,7 @@ class TransferTransactionTestCase(unittest.TestCase):
"""
from accounting.models import Transaction, TransactionCurrency
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"
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}