Compare commits
No commits in common. "d7bc01ccb4b8e697ed1228499911fc3145f4377d" and "9993f6562772c119322d808d7d4f30f89a98b8fc" have entirely different histories.
d7bc01ccb4
...
9993f65627
@ -28,10 +28,10 @@ accounting.account.forms module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.account.queries module
|
accounting.account.query module
|
||||||
---------------------------------
|
-------------------------------
|
||||||
|
|
||||||
.. automodule:: accounting.account.queries
|
.. automodule:: accounting.account.query
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
@ -20,10 +20,10 @@ accounting.base\_account.converters module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.base\_account.queries module
|
accounting.base\_account.query module
|
||||||
---------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
.. automodule:: accounting.base_account.queries
|
.. automodule:: accounting.base_account.query
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
@ -28,10 +28,10 @@ accounting.currency.forms module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.currency.queries module
|
accounting.currency.query module
|
||||||
----------------------------------
|
--------------------------------
|
||||||
|
|
||||||
.. automodule:: accounting.currency.queries
|
.. automodule:: accounting.currency.query
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
@ -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:
|
|
@ -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:
|
|
@ -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:
|
|
@ -10,7 +10,6 @@ Subpackages
|
|||||||
accounting.account
|
accounting.account
|
||||||
accounting.base_account
|
accounting.base_account
|
||||||
accounting.currency
|
accounting.currency
|
||||||
accounting.report
|
|
||||||
accounting.transaction
|
accounting.transaction
|
||||||
accounting.utils
|
accounting.utils
|
||||||
|
|
||||||
@ -33,22 +32,6 @@ accounting.models module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.template\_filters module
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
.. automodule:: accounting.template_filters
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
accounting.template\_globals module
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
.. automodule:: accounting.template_globals
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
Module contents
|
Module contents
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
@ -12,6 +12,14 @@ accounting.transaction.converters module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.transaction.dispatcher module
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.transaction.dispatcher
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.transaction.forms module
|
accounting.transaction.forms module
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
@ -20,26 +28,26 @@ accounting.transaction.forms module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.transaction.operators module
|
accounting.transaction.query module
|
||||||
---------------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
.. automodule:: accounting.transaction.operators
|
.. automodule:: accounting.transaction.query
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.transaction.summary\_editor module
|
accounting.transaction.summary\_helper module
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
|
|
||||||
.. automodule:: accounting.transaction.summary_editor
|
.. automodule:: accounting.transaction.summary_helper
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.transaction.template\_filters module
|
accounting.transaction.template module
|
||||||
-----------------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
.. automodule:: accounting.transaction.template_filters
|
.. automodule:: accounting.transaction.template
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
@ -60,14 +60,6 @@ accounting.utils.strip\_text module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.utils.txn\_types module
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
.. automodule:: accounting.utils.txn_types
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
accounting.utils.user module
|
accounting.utils.user module
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ class BaseAccount(db.Model):
|
|||||||
|
|
||||||
:return: The string representation of the base account.
|
:return: The string representation of the base account.
|
||||||
"""
|
"""
|
||||||
return f"{self.code} {self.title}"
|
return F"{self.code} {self.title}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self) -> str:
|
def title(self) -> str:
|
||||||
@ -141,11 +141,17 @@ class Account(db.Model):
|
|||||||
entries = db.relationship("JournalEntry", back_populates="account")
|
entries = db.relationship("JournalEntry", back_populates="account")
|
||||||
"""The journal entries."""
|
"""The journal entries."""
|
||||||
|
|
||||||
CASH_CODE: str = "1111-001"
|
__CASH = "1111-001"
|
||||||
"""The code of the cash account,"""
|
"""The code of the cash account,"""
|
||||||
ACCUMULATED_CHANGE_CODE: str = "3351-001"
|
__RECEIVABLE = "1141-001"
|
||||||
|
"""The code of the receivable account,"""
|
||||||
|
__PAYABLE = "2141-001"
|
||||||
|
"""The code of the payable account,"""
|
||||||
|
__ACCUMULATED_CHANGE = "3351-001"
|
||||||
"""The code of the accumulated-change account,"""
|
"""The code of the accumulated-change account,"""
|
||||||
NET_CHANGE_CODE: str = "3353-001"
|
__BROUGHT_FORWARD = "3352-001"
|
||||||
|
"""The code of the brought-forward account,"""
|
||||||
|
__NET_CHANGE = "3353-001"
|
||||||
"""The code of the net-change account,"""
|
"""The code of the net-change account,"""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -153,7 +159,7 @@ class Account(db.Model):
|
|||||||
|
|
||||||
:return: The string representation of this account.
|
:return: The string representation of this account.
|
||||||
"""
|
"""
|
||||||
return f"{self.base_code}-{self.no:03d} {self.title}"
|
return F"{self.base_code}-{self.no:03d} {self.title}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def code(self) -> str:
|
def code(self) -> str:
|
||||||
@ -161,7 +167,7 @@ class Account(db.Model):
|
|||||||
|
|
||||||
:return: The code.
|
:return: The code.
|
||||||
"""
|
"""
|
||||||
return f"{self.base_code}-{self.no:03d}"
|
return F"{self.base_code}-{self.no:03d}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self) -> str:
|
def title(self) -> str:
|
||||||
@ -265,7 +271,23 @@ class Account(db.Model):
|
|||||||
|
|
||||||
:return: The cash account
|
:return: The cash account
|
||||||
"""
|
"""
|
||||||
return cls.find_by_code(cls.CASH_CODE)
|
return cls.find_by_code(cls.__CASH)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def receivable(cls) -> t.Self:
|
||||||
|
"""Returns the receivable account.
|
||||||
|
|
||||||
|
:return: The receivable account
|
||||||
|
"""
|
||||||
|
return cls.find_by_code(cls.__RECEIVABLE)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def payable(cls) -> t.Self:
|
||||||
|
"""Returns the payable account.
|
||||||
|
|
||||||
|
:return: The payable account
|
||||||
|
"""
|
||||||
|
return cls.find_by_code(cls.__PAYABLE)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def accumulated_change(cls) -> t.Self:
|
def accumulated_change(cls) -> t.Self:
|
||||||
@ -273,7 +295,23 @@ class Account(db.Model):
|
|||||||
|
|
||||||
:return: The accumulated-change account
|
:return: The accumulated-change account
|
||||||
"""
|
"""
|
||||||
return cls.find_by_code(cls.ACCUMULATED_CHANGE_CODE)
|
return cls.find_by_code(cls.__ACCUMULATED_CHANGE)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def brought_forward(cls) -> t.Self:
|
||||||
|
"""Returns the brought-forward account.
|
||||||
|
|
||||||
|
:return: The brought-forward account
|
||||||
|
"""
|
||||||
|
return cls.find_by_code(cls.__BROUGHT_FORWARD)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def net_change(cls) -> t.Self:
|
||||||
|
"""Returns the net-change account.
|
||||||
|
|
||||||
|
:return: The net-change account
|
||||||
|
"""
|
||||||
|
return cls.find_by_code(cls.__NET_CHANGE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_modified(self) -> bool:
|
def is_modified(self) -> bool:
|
||||||
@ -354,7 +392,7 @@ class Currency(db.Model):
|
|||||||
|
|
||||||
:return: The string representation of the currency.
|
:return: The string representation of the currency.
|
||||||
"""
|
"""
|
||||||
return f"{self.name} ({self.code})"
|
return F"{self.name} ({self.code})"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@ -550,7 +588,7 @@ class Transaction(db.Model):
|
|||||||
for currency in self.currencies:
|
for currency in self.currencies:
|
||||||
if len(currency.debit) > 1:
|
if len(currency.debit) > 1:
|
||||||
return False
|
return False
|
||||||
if currency.debit[0].account.code != Account.CASH_CODE:
|
if currency.debit[0].account.code != "1111-001":
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -564,7 +602,7 @@ class Transaction(db.Model):
|
|||||||
for currency in self.currencies:
|
for currency in self.currencies:
|
||||||
if len(currency.credit) > 1:
|
if len(currency.credit) > 1:
|
||||||
return False
|
return False
|
||||||
if currency.credit[0].account.code != Account.CASH_CODE:
|
if currency.credit[0].account.code != "1111-001":
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -617,7 +655,7 @@ class JournalEntry(db.Model):
|
|||||||
onupdate="CASCADE"),
|
onupdate="CASCADE"),
|
||||||
nullable=False)
|
nullable=False)
|
||||||
"""The account ID."""
|
"""The account ID."""
|
||||||
account = db.relationship(Account, back_populates="entries", lazy=False)
|
account = db.relationship(Account, back_populates="entries")
|
||||||
"""The account."""
|
"""The account."""
|
||||||
summary = db.Column(db.String, nullable=True)
|
summary = db.Column(db.String, nullable=True)
|
||||||
"""The summary."""
|
"""The summary."""
|
||||||
@ -640,19 +678,3 @@ class JournalEntry(db.Model):
|
|||||||
:return: The account code.
|
:return: The account code.
|
||||||
"""
|
"""
|
||||||
return self.account.code
|
return self.account.code
|
||||||
|
|
||||||
@property
|
|
||||||
def debit(self) -> Decimal | None:
|
|
||||||
"""Returns the debit amount.
|
|
||||||
|
|
||||||
:return: The debit amount, or None if this is not a debit entry.
|
|
||||||
"""
|
|
||||||
return self.amount if self.is_debit else None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def credit(self) -> Decimal | None:
|
|
||||||
"""Returns the credit amount.
|
|
||||||
|
|
||||||
:return: The credit amount, or None if this is not a credit entry.
|
|
||||||
"""
|
|
||||||
return None if self.is_debit else self.amount
|
|
||||||
|
@ -52,8 +52,8 @@ class PeriodConverter(BaseConverter):
|
|||||||
|
|
||||||
|
|
||||||
class IncomeExpensesAccountConverter(BaseConverter):
|
class IncomeExpensesAccountConverter(BaseConverter):
|
||||||
"""The supplier converter to convert the income and expenses log pseudo
|
"""The supplier converter to convert the income and expenses pseudo account
|
||||||
account code from and to the corresponding pseudo account in the routes."""
|
code from and to the corresponding pseudo account in the routes."""
|
||||||
|
|
||||||
def to_python(self, value: str) -> IncomeExpensesAccount:
|
def to_python(self, value: str) -> IncomeExpensesAccount:
|
||||||
"""Converts an account code to an account.
|
"""Converts an account code to an account.
|
||||||
|
@ -33,15 +33,21 @@ class IncomeExpensesAccount:
|
|||||||
|
|
||||||
:param account: The actual account.
|
:param account: The actual account.
|
||||||
"""
|
"""
|
||||||
self.account: Account | None = account
|
self.account: Account | None = None
|
||||||
self.id: int = -1 if account is None else account.id
|
self.id: int | None = None
|
||||||
"""The ID."""
|
"""The ID."""
|
||||||
self.code: str = "" if account is None else account.code
|
self.code: str | None = None
|
||||||
"""The code."""
|
"""The code."""
|
||||||
self.title: str = "" if account is None else account.title
|
self.title: str | None = None
|
||||||
"""The title."""
|
"""The title."""
|
||||||
self.str: str = "" if account is None else str(account)
|
self.str: str = ""
|
||||||
"""The string representation of the account."""
|
"""The string representation of the account."""
|
||||||
|
if account is not None:
|
||||||
|
self.account = account
|
||||||
|
self.id = account.id
|
||||||
|
self.code = account.code
|
||||||
|
self.title = account.title
|
||||||
|
self.str = str(account)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Returns the string representation of the account.
|
"""Returns the string representation of the account.
|
||||||
|
@ -63,8 +63,6 @@ class Period:
|
|||||||
"""The period specification."""
|
"""The period specification."""
|
||||||
self.desc: str = ""
|
self.desc: str = ""
|
||||||
"""The text description."""
|
"""The text description."""
|
||||||
self.is_a_month: bool = False
|
|
||||||
"""Whether the period is a whole month."""
|
|
||||||
self.is_type_month: bool = False
|
self.is_type_month: bool = False
|
||||||
"""Whether the period is for the month chooser."""
|
"""Whether the period is for the month chooser."""
|
||||||
self.is_a_year: bool = False
|
self.is_a_year: bool = False
|
||||||
@ -87,13 +85,12 @@ class Period:
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
self.spec = PeriodSpecification(self).spec
|
self.spec = self.__get_spec()
|
||||||
self.desc = PeriodDescription(self).desc
|
self.desc = self.__get_desc()
|
||||||
if self.start is None or self.end is None:
|
if self.start is None or self.end is None:
|
||||||
return
|
return
|
||||||
self.is_a_month = self.start.day == 1 \
|
self.is_type_month \
|
||||||
and self.end == _month_end(self.start)
|
= self.start.day == 1 and self.end == _month_end(self.start)
|
||||||
self.is_type_month = self.is_a_month
|
|
||||||
self.is_a_year = self.start == datetime.date(self.start.year, 1, 1) \
|
self.is_a_year = self.start == datetime.date(self.start.year, 1, 1) \
|
||||||
and self.end == datetime.date(self.start.year, 12, 31)
|
and self.end == datetime.date(self.start.year, 12, 31)
|
||||||
self.is_a_day = self.start == self.end
|
self.is_a_day = self.start == self.end
|
||||||
@ -126,6 +123,189 @@ class Period:
|
|||||||
raise ValueError
|
raise ValueError
|
||||||
return cls(start, end)
|
return cls(start, end)
|
||||||
|
|
||||||
|
def __get_spec(self) -> str:
|
||||||
|
"""Returns the period specification.
|
||||||
|
|
||||||
|
:return: The period specification.
|
||||||
|
"""
|
||||||
|
if self.start is None:
|
||||||
|
if self.end is None:
|
||||||
|
return "-"
|
||||||
|
else:
|
||||||
|
if self.end.day != _month_end(self.end).day:
|
||||||
|
return "-%04d-%02d-%02d" % (
|
||||||
|
self.end.year, self.end.month, self.end.day)
|
||||||
|
if self.end.month != 12:
|
||||||
|
return "-%04d-%02d" % (self.end.year, self.end.month)
|
||||||
|
return "-%04d" % self.end.year
|
||||||
|
else:
|
||||||
|
if self.end is None:
|
||||||
|
if self.start.day != 1:
|
||||||
|
return "%04d-%02d-%02d-" % (
|
||||||
|
self.start.year, self.start.month, self.start.day)
|
||||||
|
if self.start.month != 1:
|
||||||
|
return "%04d-%02d-" % (self.start.year, self.start.month)
|
||||||
|
return "%04d-" % self.start.year
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return self.__get_year_spec()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return self.__get_month_spec()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return self.__get_day_spec()
|
||||||
|
|
||||||
|
def __get_year_spec(self) -> str:
|
||||||
|
"""Returns the period specification as a year range.
|
||||||
|
|
||||||
|
:return: The period specification as a year range.
|
||||||
|
:raise ValueError: The period is not a year range.
|
||||||
|
"""
|
||||||
|
if self.start.month != 1 or self.start.day != 1 \
|
||||||
|
or self.end.month != 12 or self.end.day != 31:
|
||||||
|
raise ValueError
|
||||||
|
if self.start.year == self.end.year:
|
||||||
|
return "%04d" % self.start.year
|
||||||
|
return "%04d-%04d" % (self.start.year, self.end.year)
|
||||||
|
|
||||||
|
def __get_month_spec(self) -> str:
|
||||||
|
"""Returns the period specification as a month range.
|
||||||
|
|
||||||
|
:return: The period specification as a month range.
|
||||||
|
:raise ValueError: The period is not a month range.
|
||||||
|
"""
|
||||||
|
if self.start.day != 1 or self.end != _month_end(self.end):
|
||||||
|
raise ValueError
|
||||||
|
if self.start.year == self.end.year \
|
||||||
|
and self.start.month == self.end.month:
|
||||||
|
return "%04d-%02d" % (self.start.year, self.start.month)
|
||||||
|
return "%04d-%02d-%04d-%02d" % (
|
||||||
|
self.start.year, self.start.month,
|
||||||
|
self.end.year, self.end.month)
|
||||||
|
|
||||||
|
def __get_day_spec(self) -> str:
|
||||||
|
"""Returns the period specification as a day range.
|
||||||
|
|
||||||
|
:return: The period specification as a day range.
|
||||||
|
:raise ValueError: The period is a month or year range.
|
||||||
|
"""
|
||||||
|
if self.start == self.end:
|
||||||
|
return "%04d-%02d-%02d" % (
|
||||||
|
self.start.year, self.start.month, self.start.day)
|
||||||
|
return "%04d-%02d-%02d-%04d-%02d-%02d" % (
|
||||||
|
self.start.year, self.start.month, self.start.day,
|
||||||
|
self.end.year, self.end.month, self.end.day)
|
||||||
|
|
||||||
|
def __get_desc(self) -> str:
|
||||||
|
"""Returns the period description.
|
||||||
|
|
||||||
|
:return: The period description.
|
||||||
|
"""
|
||||||
|
cls: t.Type[t.Self] = self.__class__
|
||||||
|
if self.start is None:
|
||||||
|
if self.end is None:
|
||||||
|
return gettext("for all time")
|
||||||
|
else:
|
||||||
|
if self.end != _month_end(self.end):
|
||||||
|
return gettext("until %(end)s",
|
||||||
|
end=cls.__format_date(self.end))
|
||||||
|
if self.end.month != 12:
|
||||||
|
return gettext("until %(end)s",
|
||||||
|
end=cls.__format_month(self.end))
|
||||||
|
return gettext("until %(end)s", end=str(self.end.year))
|
||||||
|
else:
|
||||||
|
if self.end is None:
|
||||||
|
if self.start.day != 1:
|
||||||
|
return gettext("since %(start)s",
|
||||||
|
start=cls.__format_date(self.start))
|
||||||
|
if self.start.month != 1:
|
||||||
|
return gettext("since %(start)s",
|
||||||
|
start=cls.__format_month(self.start))
|
||||||
|
return gettext("since %(start)s", start=str(self.start.year))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return self.__get_year_desc()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return self.__get_month_desc()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return self.__get_day_desc()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __format_date(date: datetime.date) -> str:
|
||||||
|
"""Formats a date.
|
||||||
|
|
||||||
|
:param date: The date.
|
||||||
|
:return: The formatted date.
|
||||||
|
"""
|
||||||
|
return F"{date.year}/{date.month}/{date.day}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __format_month(month: datetime.date) -> str:
|
||||||
|
"""Formats a month.
|
||||||
|
|
||||||
|
:param month: The month.
|
||||||
|
:return: The formatted month.
|
||||||
|
"""
|
||||||
|
return F"{month.year}/{month.month}"
|
||||||
|
|
||||||
|
def __get_year_desc(self) -> str:
|
||||||
|
"""Returns the description as a year range.
|
||||||
|
|
||||||
|
:return: The description as a year range.
|
||||||
|
:raise ValueError: The period is not a year range.
|
||||||
|
"""
|
||||||
|
if self.start.month != 1 or self.start.day != 1 \
|
||||||
|
or self.end.month != 12 or self.end.day != 31:
|
||||||
|
raise ValueError
|
||||||
|
start: str = str(self.start.year)
|
||||||
|
if self.start.year == self.end.year:
|
||||||
|
return gettext("in %(period)s", period=start)
|
||||||
|
end: str = str(self.end.year)
|
||||||
|
return gettext("in %(start)s-%(end)s", start=start, end=end)
|
||||||
|
|
||||||
|
def __get_month_desc(self) -> str:
|
||||||
|
"""Returns the description as a month range.
|
||||||
|
|
||||||
|
:return: The description as a month range.
|
||||||
|
:raise ValueError: The period is not a month range.
|
||||||
|
"""
|
||||||
|
if self.start.day != 1 or self.end != _month_end(self.end):
|
||||||
|
raise ValueError
|
||||||
|
start: str = F"{self.start.year}/{self.start.month}"
|
||||||
|
if self.start.year == self.end.year \
|
||||||
|
and self.start.month == self.end.month:
|
||||||
|
return gettext("in %(period)s", period=start)
|
||||||
|
if self.start.year == self.end.year:
|
||||||
|
end_month: str = str(self.end.month)
|
||||||
|
return gettext("in %(start)s-%(end)s", start=start, end=end_month)
|
||||||
|
end: str = F"{self.end.year}/{self.end.month}"
|
||||||
|
return gettext("in %(start)s-%(end)s", start=start, end=end)
|
||||||
|
|
||||||
|
def __get_day_desc(self) -> str:
|
||||||
|
"""Returns the description as a day range.
|
||||||
|
|
||||||
|
:return: The description as a day range.
|
||||||
|
:raise ValueError: The period is a month or year range.
|
||||||
|
"""
|
||||||
|
start: str = F"{self.start.year}/{self.start.month}/{self.start.day}"
|
||||||
|
if self.start == self.end:
|
||||||
|
return gettext("in %(period)s", period=start)
|
||||||
|
if self.start.year == self.end.year \
|
||||||
|
and self.start.month == self.end.month:
|
||||||
|
end_day: str = str(self.end.day)
|
||||||
|
return gettext("in %(start)s-%(end)s", start=start, end=end_day)
|
||||||
|
if self.start.year == self.end.year:
|
||||||
|
end_month_day: str = F"{self.end.month}/{self.end.day}"
|
||||||
|
return gettext("in %(start)s-%(end)s",
|
||||||
|
start=start, end=end_month_day)
|
||||||
|
end: str = F"{self.end.year}/{self.end.month}/{self.end.day}"
|
||||||
|
return gettext("in %(start)s-%(end)s", start=start, end=end)
|
||||||
|
|
||||||
def is_year(self, year: int) -> bool:
|
def is_year(self, year: int) -> bool:
|
||||||
"""Returns whether the period is the specific year period.
|
"""Returns whether the period is the specific year period.
|
||||||
|
|
||||||
@ -156,266 +336,6 @@ class Period:
|
|||||||
return Period(None, self.start - datetime.timedelta(days=1))
|
return Period(None, self.start - datetime.timedelta(days=1))
|
||||||
|
|
||||||
|
|
||||||
class PeriodSpecification:
|
|
||||||
"""The period specification composer."""
|
|
||||||
|
|
||||||
def __init__(self, period: Period):
|
|
||||||
"""Constructs the period specification composer.
|
|
||||||
|
|
||||||
:param period: The period.
|
|
||||||
"""
|
|
||||||
self.__start: datetime.date = period.start
|
|
||||||
self.__end: datetime.date = period.end
|
|
||||||
self.spec: str = self.__get_spec()
|
|
||||||
|
|
||||||
def __get_spec(self) -> str:
|
|
||||||
"""Returns the period specification.
|
|
||||||
|
|
||||||
:return: The period specification.
|
|
||||||
"""
|
|
||||||
if self.__start is None and self.__end is None:
|
|
||||||
return "-"
|
|
||||||
if self.__end is None:
|
|
||||||
return self.__get_since_spec()
|
|
||||||
if self.__start is None:
|
|
||||||
return self.__get_until_spec()
|
|
||||||
try:
|
|
||||||
return self.__get_year_spec()
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
return self.__get_month_spec()
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return self.__get_day_spec()
|
|
||||||
|
|
||||||
def __get_since_spec(self) -> str:
|
|
||||||
"""Returns the period specification without the end day.
|
|
||||||
|
|
||||||
:return: The period specification without the end day
|
|
||||||
"""
|
|
||||||
if self.__start.month == 1 and self.__start.day == 1:
|
|
||||||
return self.__start.strftime("%Y-")
|
|
||||||
if self.__start.day == 1:
|
|
||||||
return self.__start.strftime("%Y-%m-")
|
|
||||||
return self.__start.strftime("%Y-%m-%d-")
|
|
||||||
|
|
||||||
def __get_until_spec(self) -> str:
|
|
||||||
"""Returns the period specification without the start day.
|
|
||||||
|
|
||||||
:return: The period specification without the start day
|
|
||||||
"""
|
|
||||||
if self.__end.month == 12 and self.__end.day == 31:
|
|
||||||
return self.__end.strftime("-%Y")
|
|
||||||
if (self.__end + datetime.timedelta(days=1)).day == 1:
|
|
||||||
return self.__end.strftime("-%Y-%m")
|
|
||||||
return self.__end.strftime("-%Y-%m-%d")
|
|
||||||
|
|
||||||
def __get_year_spec(self) -> str:
|
|
||||||
"""Returns the period specification as a year range.
|
|
||||||
|
|
||||||
:return: The period specification as a year range.
|
|
||||||
:raise ValueError: The period is not a year range.
|
|
||||||
"""
|
|
||||||
if self.__start.month != 1 or self.__start.day != 1 \
|
|
||||||
or self.__end.month != 12 or self.__end.day != 31:
|
|
||||||
raise ValueError
|
|
||||||
if self.__start.year == self.__end.year:
|
|
||||||
return "%04d" % self.__start.year
|
|
||||||
return "%04d-%04d" % (self.__start.year, self.__end.year)
|
|
||||||
|
|
||||||
def __get_month_spec(self) -> str:
|
|
||||||
"""Returns the period specification as a month range.
|
|
||||||
|
|
||||||
:return: The period specification as a month range.
|
|
||||||
:raise ValueError: The period is not a month range.
|
|
||||||
"""
|
|
||||||
if self.__start.day != 1 or self.__end != _month_end(self.__end):
|
|
||||||
raise ValueError
|
|
||||||
if self.__start.year == self.__end.year \
|
|
||||||
and self.__start.month == self.__end.month:
|
|
||||||
return "%04d-%02d" % (self.__start.year, self.__start.month)
|
|
||||||
return "%04d-%02d-%04d-%02d" % (
|
|
||||||
self.__start.year, self.__start.month,
|
|
||||||
self.__end.year, self.__end.month)
|
|
||||||
|
|
||||||
def __get_day_spec(self) -> str:
|
|
||||||
"""Returns the period specification as a day range.
|
|
||||||
|
|
||||||
:return: The period specification as a day range.
|
|
||||||
:raise ValueError: The period is a month or year range.
|
|
||||||
"""
|
|
||||||
if self.__start == self.__end:
|
|
||||||
return "%04d-%02d-%02d" % (
|
|
||||||
self.__start.year, self.__start.month, self.__start.day)
|
|
||||||
return "%04d-%02d-%02d-%04d-%02d-%02d" % (
|
|
||||||
self.__start.year, self.__start.month, self.__start.day,
|
|
||||||
self.__end.year, self.__end.month, self.__end.day)
|
|
||||||
|
|
||||||
|
|
||||||
class PeriodDescription:
|
|
||||||
"""The period description composer."""
|
|
||||||
|
|
||||||
def __init__(self, period: Period):
|
|
||||||
"""Constructs the period description composer.
|
|
||||||
|
|
||||||
:param period: The period.
|
|
||||||
"""
|
|
||||||
self.__start: datetime.date = period.start
|
|
||||||
self.__end: datetime.date = period.end
|
|
||||||
self.desc: str = self.__get_desc()
|
|
||||||
|
|
||||||
def __get_desc(self) -> str:
|
|
||||||
"""Returns the period description.
|
|
||||||
|
|
||||||
:return: The period description.
|
|
||||||
"""
|
|
||||||
if self.__start is None and self.__end is None:
|
|
||||||
return gettext("for all time")
|
|
||||||
if self.__start is None:
|
|
||||||
return self.__get_until_desc()
|
|
||||||
if self.__end is None:
|
|
||||||
return self.__get_since_desc()
|
|
||||||
try:
|
|
||||||
return self.__get_year_desc()
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
return self.__get_month_desc()
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return self.__get_day_desc()
|
|
||||||
|
|
||||||
def __get_since_desc(self) -> str:
|
|
||||||
"""Returns the description without the end day.
|
|
||||||
|
|
||||||
:return: The description without the end day.
|
|
||||||
"""
|
|
||||||
def get_start_desc() -> str:
|
|
||||||
"""Returns the description of the start day.
|
|
||||||
|
|
||||||
:return: The description of the start day.
|
|
||||||
"""
|
|
||||||
if self.__start.month == 1 and self.__start.day == 1:
|
|
||||||
return str(self.__start.year)
|
|
||||||
if self.__start.day == 1:
|
|
||||||
return self.__format_month(self.__start)
|
|
||||||
return self.__format_date(self.__start)
|
|
||||||
|
|
||||||
return gettext("since %(start)s", start=get_start_desc())
|
|
||||||
|
|
||||||
def __get_until_desc(self) -> str:
|
|
||||||
"""Returns the description without the start day.
|
|
||||||
|
|
||||||
:return: The description without the start day.
|
|
||||||
"""
|
|
||||||
def get_end_desc() -> str:
|
|
||||||
"""Returns the description of the end day.
|
|
||||||
|
|
||||||
:return: The description of the end day.
|
|
||||||
"""
|
|
||||||
if self.__end.month == 12 and self.__end.day == 31:
|
|
||||||
return str(self.__end.year)
|
|
||||||
if (self.__end + datetime.timedelta(days=1)).day == 1:
|
|
||||||
return self.__format_month(self.__end)
|
|
||||||
return self.__format_date(self.__end)
|
|
||||||
|
|
||||||
return gettext("until %(end)s", end=get_end_desc())
|
|
||||||
|
|
||||||
def __get_year_desc(self) -> str:
|
|
||||||
"""Returns the description as a year range.
|
|
||||||
|
|
||||||
:return: The description as a year range.
|
|
||||||
:raise ValueError: The period is not a year range.
|
|
||||||
"""
|
|
||||||
if self.__start.month != 1 or self.__start.day != 1 \
|
|
||||||
or self.__end.month != 12 or self.__end.day != 31:
|
|
||||||
raise ValueError
|
|
||||||
start: str = str(self.__start.year)
|
|
||||||
if self.__start.year == self.__end.year:
|
|
||||||
return self.__get_in_desc(start)
|
|
||||||
return self.__get_from_to_desc(start, str(self.__end.year))
|
|
||||||
|
|
||||||
def __get_month_desc(self) -> str:
|
|
||||||
"""Returns the description as a month range.
|
|
||||||
|
|
||||||
:return: The description as a month range.
|
|
||||||
:raise ValueError: The period is not a month range.
|
|
||||||
"""
|
|
||||||
if self.__start.day != 1 or self.__end != _month_end(self.__end):
|
|
||||||
raise ValueError
|
|
||||||
start: str = self.__format_month(self.__start)
|
|
||||||
if self.__start.year == self.__end.year \
|
|
||||||
and self.__start.month == self.__end.month:
|
|
||||||
return self.__get_in_desc(start)
|
|
||||||
if self.__start.year == self.__end.year:
|
|
||||||
return self.__get_from_to_desc(start, str(self.__end.month))
|
|
||||||
return self.__get_from_to_desc(start, self.__format_month(self.__end))
|
|
||||||
|
|
||||||
def __get_day_desc(self) -> str:
|
|
||||||
"""Returns the description as a day range.
|
|
||||||
|
|
||||||
:return: The description as a day range.
|
|
||||||
:raise ValueError: The period is a month or year range.
|
|
||||||
"""
|
|
||||||
start: str = self.__format_day(self.__start)
|
|
||||||
if self.__start == self.__end:
|
|
||||||
return self.__get_in_desc(start)
|
|
||||||
if self.__start.year == self.__end.year \
|
|
||||||
and self.__start.month == self.__end.month:
|
|
||||||
return self.__get_from_to_desc(start, str(self.__end.day))
|
|
||||||
if self.__start.year == self.__end.year:
|
|
||||||
end_month_day: str = f"{self.__end.month}/{self.__end.day}"
|
|
||||||
return self.__get_from_to_desc(start, end_month_day)
|
|
||||||
return self.__get_from_to_desc(start, self.__format_day(self.__end))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __format_date(date: datetime.date) -> str:
|
|
||||||
"""Formats a date.
|
|
||||||
|
|
||||||
:param date: The date.
|
|
||||||
:return: The formatted date.
|
|
||||||
"""
|
|
||||||
return f"{date.year}/{date.month}/{date.day}"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __format_month(month: datetime.date) -> str:
|
|
||||||
"""Formats a month.
|
|
||||||
|
|
||||||
:param month: The month.
|
|
||||||
:return: The formatted month.
|
|
||||||
"""
|
|
||||||
return f"{month.year}/{month.month}"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __format_day(day: datetime.date) -> str:
|
|
||||||
"""Formats a day.
|
|
||||||
|
|
||||||
:param day: The day.
|
|
||||||
:return: The formatted day.
|
|
||||||
"""
|
|
||||||
return f"{day.year}/{day.month}/{day.day}"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __get_in_desc(period: str) -> str:
|
|
||||||
"""Returns the description of a whole year, month, or day.
|
|
||||||
|
|
||||||
:param period: The time period.
|
|
||||||
:return: The description of a whole year, month, or day.
|
|
||||||
"""
|
|
||||||
return gettext("in %(period)s", period=period)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __get_from_to_desc(start: str, end: str) -> str:
|
|
||||||
"""Returns the description of a separated start and end.
|
|
||||||
|
|
||||||
:param start: The start.
|
|
||||||
:param end: The end.
|
|
||||||
:return: The description of the separated start and end.
|
|
||||||
"""
|
|
||||||
return gettext("in %(start)s-%(end)s", start=start, end=end)
|
|
||||||
|
|
||||||
|
|
||||||
class ThisMonth(Period):
|
class ThisMonth(Period):
|
||||||
"""The period of this month."""
|
"""The period of this month."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -553,9 +473,8 @@ class YearPeriod(Period):
|
|||||||
self.spec = str(year)
|
self.spec = str(year)
|
||||||
self.is_a_year = True
|
self.is_a_year = True
|
||||||
|
|
||||||
|
def _set_properties(self) -> None:
|
||||||
DATE_SPEC_RE: str = r"(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?"
|
pass
|
||||||
"""The regular expression of a date specification."""
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_period_spec(text: str) \
|
def _parse_period_spec(text: str) \
|
||||||
@ -567,19 +486,22 @@ def _parse_period_spec(text: str) \
|
|||||||
may be None.
|
may be None.
|
||||||
:raise ValueError: When the date is invalid.
|
:raise ValueError: When the date is invalid.
|
||||||
"""
|
"""
|
||||||
|
if text == "this-month":
|
||||||
|
today: datetime.date = datetime.date.today()
|
||||||
|
return datetime.date(today.year, today.month, 1), _month_end(today)
|
||||||
if text == "-":
|
if text == "-":
|
||||||
return None, None
|
return None, None
|
||||||
m = re.match(f"^{DATE_SPEC_RE}$", text)
|
m = re.match(r"^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$", text)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
return __get_start(m[1], m[2], m[3]), \
|
return __get_start(m[1], m[2], m[3]), \
|
||||||
__get_end(m[1], m[2], m[3])
|
__get_end(m[1], m[2], m[3])
|
||||||
m = re.match(f"^{DATE_SPEC_RE}-$", text)
|
m = re.match(r"^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?-$", text)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
return __get_start(m[1], m[2], m[3]), None
|
return __get_start(m[1], m[2], m[3]), None
|
||||||
m = re.match(f"-{DATE_SPEC_RE}$", text)
|
m = re.match(r"-(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$", text)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
return None, __get_end(m[1], m[2], m[3])
|
return None, __get_end(m[1], m[2], m[3])
|
||||||
m = re.match(f"^{DATE_SPEC_RE}-{DATE_SPEC_RE}$", text)
|
m = re.match(r"^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?-(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$", text)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
return __get_start(m[1], m[2], m[3]), \
|
return __get_start(m[1], m[2], m[3]), \
|
||||||
__get_end(m[4], m[5], m[6])
|
__get_end(m[4], m[5], m[6])
|
||||||
|
@ -20,28 +20,27 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import render_template, Response
|
from flask import url_for, render_template, Response
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from accounting.models import Currency, BaseAccount, Account, Transaction, \
|
from accounting.models import Currency, BaseAccount, Account, Transaction, \
|
||||||
JournalEntry
|
JournalEntry
|
||||||
from accounting.report.period import Period
|
from accounting.report.period import Period
|
||||||
from .utils.base_page_params import BasePageParams
|
|
||||||
from .utils.base_report import BaseReport
|
from .utils.base_report import BaseReport
|
||||||
from .utils.csv_export import BaseCSVRow, csv_download, period_spec
|
from .utils.csv_export import BaseCSVRow, csv_download
|
||||||
from .utils.option_link import OptionLink
|
from .utils.option_link import OptionLink
|
||||||
|
from .utils.page_params import PageParams
|
||||||
from .utils.period_choosers import BalanceSheetPeriodChooser
|
from .utils.period_choosers import BalanceSheetPeriodChooser
|
||||||
from .utils.report_chooser import ReportChooser
|
from .utils.report_chooser import ReportChooser
|
||||||
from .utils.report_type import ReportType
|
from .utils.report_type import ReportType
|
||||||
from .utils.urls import ledger_url, balance_sheet_url, income_statement_url
|
|
||||||
|
|
||||||
|
|
||||||
class ReportAccount:
|
class BalanceSheetAccount:
|
||||||
"""An account in the report."""
|
"""An account in the balance sheet."""
|
||||||
|
|
||||||
def __init__(self, account: Account, amount: Decimal, url: str):
|
def __init__(self, account: Account, amount: Decimal, url: str):
|
||||||
"""Constructs an account in the report.
|
"""Constructs an account in the balance sheet.
|
||||||
|
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
:param amount: The amount.
|
:param amount: The amount.
|
||||||
@ -55,17 +54,17 @@ class ReportAccount:
|
|||||||
"""The URL to the ledger of the account."""
|
"""The URL to the ledger of the account."""
|
||||||
|
|
||||||
|
|
||||||
class Subsection:
|
class BalanceSheetSubsection:
|
||||||
"""A subsection."""
|
"""A subsection in the balance sheet."""
|
||||||
|
|
||||||
def __init__(self, title: BaseAccount):
|
def __init__(self, title: BaseAccount):
|
||||||
"""Constructs a subsection.
|
"""Constructs a subsection in the balance sheet.
|
||||||
|
|
||||||
:param title: The title account.
|
:param title: The title account.
|
||||||
"""
|
"""
|
||||||
self.title: BaseAccount = title
|
self.title: BaseAccount = title
|
||||||
"""The title account."""
|
"""The title account."""
|
||||||
self.accounts: list[ReportAccount] = []
|
self.accounts: list[BalanceSheetAccount] = []
|
||||||
"""The accounts in the subsection."""
|
"""The accounts in the subsection."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -77,17 +76,17 @@ class Subsection:
|
|||||||
return sum([x.amount for x in self.accounts])
|
return sum([x.amount for x in self.accounts])
|
||||||
|
|
||||||
|
|
||||||
class Section:
|
class BalanceSheetSection:
|
||||||
"""A section."""
|
"""A section in the balance sheet."""
|
||||||
|
|
||||||
def __init__(self, title: BaseAccount):
|
def __init__(self, title: BaseAccount):
|
||||||
"""Constructs a section.
|
"""Constructs a section in the balance sheet.
|
||||||
|
|
||||||
:param title: The title account.
|
:param title: The title account.
|
||||||
"""
|
"""
|
||||||
self.title: BaseAccount = title
|
self.title: BaseAccount = title
|
||||||
"""The title account."""
|
"""The title account."""
|
||||||
self.subsections: list[Subsection] = []
|
self.subsections: list[BalanceSheetSubsection] = []
|
||||||
"""The subsections in the section."""
|
"""The subsections in the section."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -112,10 +111,10 @@ class AccountCollector:
|
|||||||
"""The currency."""
|
"""The currency."""
|
||||||
self.__period: Period = period
|
self.__period: Period = period
|
||||||
"""The period."""
|
"""The period."""
|
||||||
self.accounts: list[ReportAccount] = self.__query_balances()
|
self.accounts: list[BalanceSheetAccount] = self.__query_balances()
|
||||||
"""The balance sheet accounts."""
|
"""The balance sheet accounts."""
|
||||||
|
|
||||||
def __query_balances(self) -> list[ReportAccount]:
|
def __query_balances(self) -> list[BalanceSheetAccount]:
|
||||||
"""Queries and returns the balances.
|
"""Queries and returns the balances.
|
||||||
|
|
||||||
:return: The balances.
|
:return: The balances.
|
||||||
@ -145,12 +144,24 @@ class AccountCollector:
|
|||||||
Account.base_code == "3353")).all()
|
Account.base_code == "3353")).all()
|
||||||
account_by_id: dict[int, Account] \
|
account_by_id: dict[int, Account] \
|
||||||
= {x.id: x for x in self.__all_accounts}
|
= {x.id: x for x in self.__all_accounts}
|
||||||
self.accounts: list[ReportAccount] \
|
|
||||||
= [ReportAccount(account=account_by_id[x.id],
|
def get_url(account: Account) -> str:
|
||||||
amount=x.balance,
|
"""Returns the ledger URL of an account.
|
||||||
url=ledger_url(self.__currency,
|
|
||||||
account_by_id[x.id],
|
:param account: The account.
|
||||||
self.__period))
|
:return: The ledger URL of the account.
|
||||||
|
"""
|
||||||
|
if self.__period.is_default:
|
||||||
|
return url_for("accounting.report.ledger-default",
|
||||||
|
currency=self.__currency, account=account)
|
||||||
|
return url_for("accounting.report.ledger",
|
||||||
|
currency=self.__currency, account=account,
|
||||||
|
period=self.__period)
|
||||||
|
|
||||||
|
self.accounts: list[BalanceSheetAccount] \
|
||||||
|
= [BalanceSheetAccount(account=account_by_id[x.id],
|
||||||
|
amount=x.balance,
|
||||||
|
url=get_url(account_by_id[x.id]))
|
||||||
for x in account_balances]
|
for x in account_balances]
|
||||||
self.__add_accumulated()
|
self.__add_accumulated()
|
||||||
self.__add_current_period()
|
self.__add_current_period()
|
||||||
@ -165,9 +176,12 @@ class AccountCollector:
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
self.__add_owner_s_equity(Account.ACCUMULATED_CHANGE_CODE,
|
code: str = "3351-001"
|
||||||
self.__query_accumulated(),
|
amount: Decimal | None = self.__query_accumulated()
|
||||||
self.__period)
|
url: str = url_for("accounting.report.income-statement",
|
||||||
|
currency=self.__currency,
|
||||||
|
period=self.__period.before)
|
||||||
|
self.__add_owner_s_equity(code, amount, url)
|
||||||
|
|
||||||
def __query_accumulated(self) -> Decimal | None:
|
def __query_accumulated(self) -> Decimal | None:
|
||||||
"""Queries and returns the accumulated profit or loss.
|
"""Queries and returns the accumulated profit or loss.
|
||||||
@ -179,16 +193,25 @@ class AccountCollector:
|
|||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.BinaryExpression] \
|
||||||
= [JournalEntry.currency_code == self.__currency.code,
|
= [JournalEntry.currency_code == self.__currency.code,
|
||||||
Transaction.date < self.__period.start]
|
Transaction.date < self.__period.start]
|
||||||
return self.__query_balance(conditions)
|
conditions.extend([sa.not_(Account.base_code.startswith(x))
|
||||||
|
for x in {"1", "2"}])
|
||||||
|
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||||
|
(JournalEntry.is_debit, JournalEntry.amount),
|
||||||
|
else_=-JournalEntry.amount)).label("balance")
|
||||||
|
select_balance: sa.Select = sa.select(balance_func)\
|
||||||
|
.join(Transaction).join(Account).filter(*conditions)
|
||||||
|
return db.session.scalar(select_balance)
|
||||||
|
|
||||||
def __add_current_period(self) -> None:
|
def __add_current_period(self) -> None:
|
||||||
"""Adds the accumulated profit or loss to the balances.
|
"""Adds the accumulated profit or loss to the balances.
|
||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
self.__add_owner_s_equity(Account.NET_CHANGE_CODE,
|
code: str = "3353-001"
|
||||||
self.__query_currency_period(),
|
amount: Decimal | None = self.__query_currency_period()
|
||||||
self.__period)
|
url: str = url_for("accounting.report.income-statement",
|
||||||
|
currency=self.__currency, period=self.__period)
|
||||||
|
self.__add_owner_s_equity(code, amount, url)
|
||||||
|
|
||||||
def __query_currency_period(self) -> Decimal | None:
|
def __query_currency_period(self) -> Decimal | None:
|
||||||
"""Queries and returns the net income or loss for current period.
|
"""Queries and returns the net income or loss for current period.
|
||||||
@ -201,58 +224,47 @@ class AccountCollector:
|
|||||||
conditions.append(Transaction.date >= self.__period.start)
|
conditions.append(Transaction.date >= self.__period.start)
|
||||||
if self.__period.end is not None:
|
if self.__period.end is not None:
|
||||||
conditions.append(Transaction.date <= self.__period.end)
|
conditions.append(Transaction.date <= self.__period.end)
|
||||||
return self.__query_balance(conditions)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __query_balance(conditions: list[sa.BinaryExpression])\
|
|
||||||
-> Decimal:
|
|
||||||
"""Queries the balance.
|
|
||||||
|
|
||||||
:param conditions: The SQL conditions for the balance.
|
|
||||||
:return: The balance.
|
|
||||||
"""
|
|
||||||
conditions.extend([sa.not_(Account.base_code.startswith(x))
|
conditions.extend([sa.not_(Account.base_code.startswith(x))
|
||||||
for x in {"1", "2"}])
|
for x in {"1", "2"}])
|
||||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||||
(JournalEntry.is_debit, JournalEntry.amount),
|
(JournalEntry.is_debit, JournalEntry.amount),
|
||||||
else_=-JournalEntry.amount))
|
else_=-JournalEntry.amount)).label("balance")
|
||||||
select_balance: sa.Select = sa.select(balance_func)\
|
select_balance: sa.Select = sa.select(balance_func)\
|
||||||
.join(Transaction).join(Account).filter(*conditions)
|
.join(Transaction).join(Account).filter(*conditions)
|
||||||
return db.session.scalar(select_balance)
|
return db.session.scalar(select_balance)
|
||||||
|
|
||||||
def __add_owner_s_equity(self, code: str, amount: Decimal | None,
|
def __add_owner_s_equity(self, code: str, amount: Decimal | None,
|
||||||
period: Period) -> None:
|
url: str) -> None:
|
||||||
"""Adds an owner's equity balance.
|
"""Adds an owner's equity balance.
|
||||||
|
|
||||||
:param code: The code of the account to add.
|
:param code: The code of the account to add.
|
||||||
:param amount: The amount.
|
:param amount: The amount.
|
||||||
:param period: The period.
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
if amount is None:
|
|
||||||
return
|
|
||||||
url: str = income_statement_url(self.__currency, period)
|
|
||||||
# There is an existing balance.
|
# There is an existing balance.
|
||||||
account_balance_by_code: dict[str, ReportAccount] \
|
account_balance_by_code: dict[str, BalanceSheetAccount] \
|
||||||
= {x.account.code: x for x in self.accounts}
|
= {x.account.code: x for x in self.accounts}
|
||||||
if code in account_balance_by_code:
|
if code in account_balance_by_code:
|
||||||
balance: ReportAccount = account_balance_by_code[code]
|
balance: BalanceSheetAccount = account_balance_by_code[code]
|
||||||
balance.amount = balance.amount + amount
|
|
||||||
balance.url = url
|
balance.url = url
|
||||||
|
if amount is not None:
|
||||||
|
balance.amount = balance.amount + amount
|
||||||
return
|
return
|
||||||
# Add a new balance
|
# Add a new balance
|
||||||
|
if amount is None:
|
||||||
|
return
|
||||||
account_by_code: dict[str, Account] \
|
account_by_code: dict[str, Account] \
|
||||||
= {x.code: x for x in self.__all_accounts}
|
= {x.code: x for x in self.__all_accounts}
|
||||||
self.accounts.append(ReportAccount(account=account_by_code[code],
|
self.accounts.append(BalanceSheetAccount(account=account_by_code[code],
|
||||||
amount=amount,
|
amount=amount,
|
||||||
url=url))
|
url=url))
|
||||||
|
|
||||||
|
|
||||||
class CSVHalfRow:
|
class CSVHalfRow:
|
||||||
"""A half row in the CSV."""
|
"""A half row in the CSV balance sheet."""
|
||||||
|
|
||||||
def __init__(self, title: str | None, amount: Decimal | None):
|
def __init__(self, title: str | None, amount: Decimal | None):
|
||||||
"""The constructs a half row in the CSV.
|
"""The constructs a half row in the CSV balance sheet.
|
||||||
|
|
||||||
:param title: The title.
|
:param title: The title.
|
||||||
:param amount: The amount.
|
:param amount: The amount.
|
||||||
@ -264,10 +276,10 @@ class CSVHalfRow:
|
|||||||
|
|
||||||
|
|
||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""A row in the CSV balance sheet."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Constructs a row in the CSV."""
|
"""Constructs a row in the CSV balance sheet."""
|
||||||
self.asset_title: str | None = None
|
self.asset_title: str | None = None
|
||||||
"""The title of the asset."""
|
"""The title of the asset."""
|
||||||
self.asset_amount: Decimal | None = None
|
self.asset_amount: Decimal | None = None
|
||||||
@ -287,16 +299,16 @@ class CSVRow(BaseCSVRow):
|
|||||||
self.liability_title, self.liability_amount]
|
self.liability_title, self.liability_amount]
|
||||||
|
|
||||||
|
|
||||||
class PageParams(BasePageParams):
|
class BalanceSheetPageParams(PageParams):
|
||||||
"""The HTML page parameters."""
|
"""The HTML parameters of the balance sheet."""
|
||||||
|
|
||||||
def __init__(self, currency: Currency,
|
def __init__(self, currency: Currency,
|
||||||
period: Period,
|
period: Period,
|
||||||
has_data: bool,
|
has_data: bool,
|
||||||
assets: Section,
|
assets: BalanceSheetSection,
|
||||||
liabilities: Section,
|
liabilities: BalanceSheetSection,
|
||||||
owner_s_equity: Section):
|
owner_s_equity: BalanceSheetSection):
|
||||||
"""Constructs the HTML page parameters.
|
"""Constructs the HTML parameters of the balance sheet.
|
||||||
|
|
||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
:param period: The period.
|
:param period: The period.
|
||||||
@ -311,11 +323,11 @@ class PageParams(BasePageParams):
|
|||||||
"""The period."""
|
"""The period."""
|
||||||
self.__has_data: bool = has_data
|
self.__has_data: bool = has_data
|
||||||
"""True if there is any data, or False otherwise."""
|
"""True if there is any data, or False otherwise."""
|
||||||
self.assets: Section = assets
|
self.assets: BalanceSheetSection = assets
|
||||||
"""The assets."""
|
"""The assets."""
|
||||||
self.liabilities: Section = liabilities
|
self.liabilities: BalanceSheetSection = liabilities
|
||||||
"""The liabilities."""
|
"""The liabilities."""
|
||||||
self.owner_s_equity: Section = owner_s_equity
|
self.owner_s_equity: BalanceSheetSection = owner_s_equity
|
||||||
"""The owner's equity."""
|
"""The owner's equity."""
|
||||||
self.period_chooser: BalanceSheetPeriodChooser \
|
self.period_chooser: BalanceSheetPeriodChooser \
|
||||||
= BalanceSheetPeriodChooser(currency)
|
= BalanceSheetPeriodChooser(currency)
|
||||||
@ -345,8 +357,19 @@ class PageParams(BasePageParams):
|
|||||||
|
|
||||||
:return: The currency options.
|
:return: The currency options.
|
||||||
"""
|
"""
|
||||||
return self._get_currency_options(
|
def get_url(currency: Currency):
|
||||||
lambda x: balance_sheet_url(x, self.period), self.currency)
|
if self.period.is_default:
|
||||||
|
return url_for("accounting.report.balance-sheet-default",
|
||||||
|
currency=currency)
|
||||||
|
return url_for("accounting.report.balance-sheet",
|
||||||
|
currency=currency, period=self.period)
|
||||||
|
|
||||||
|
in_use: set[str] = set(db.session.scalars(
|
||||||
|
sa.select(JournalEntry.currency_code)
|
||||||
|
.group_by(JournalEntry.currency_code)).all())
|
||||||
|
return [OptionLink(str(x), get_url(x), x.code == self.currency.code)
|
||||||
|
for x in Currency.query.filter(Currency.code.in_(in_use))
|
||||||
|
.order_by(Currency.code).all()]
|
||||||
|
|
||||||
|
|
||||||
class BalanceSheet(BaseReport):
|
class BalanceSheet(BaseReport):
|
||||||
@ -364,11 +387,11 @@ class BalanceSheet(BaseReport):
|
|||||||
"""The period."""
|
"""The period."""
|
||||||
self.__has_data: bool
|
self.__has_data: bool
|
||||||
"""True if there is any data, or False otherwise."""
|
"""True if there is any data, or False otherwise."""
|
||||||
self.__assets: Section
|
self.__assets: BalanceSheetSection
|
||||||
"""The assets."""
|
"""The assets."""
|
||||||
self.__liabilities: Section
|
self.__liabilities: BalanceSheetSection
|
||||||
"""The liabilities."""
|
"""The liabilities."""
|
||||||
self.__owner_s_equity: Section
|
self.__owner_s_equity: BalanceSheetSection
|
||||||
"""The owner's equity."""
|
"""The owner's equity."""
|
||||||
self.__set_data()
|
self.__set_data()
|
||||||
|
|
||||||
@ -378,7 +401,7 @@ class BalanceSheet(BaseReport):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
balances: list[ReportAccount] = AccountCollector(
|
balances: list[BalanceSheetAccount] = AccountCollector(
|
||||||
self.__currency, self.__period).accounts
|
self.__currency, self.__period).accounts
|
||||||
|
|
||||||
titles: list[BaseAccount] = BaseAccount.query\
|
titles: list[BaseAccount] = BaseAccount.query\
|
||||||
@ -387,9 +410,10 @@ class BalanceSheet(BaseReport):
|
|||||||
.filter(BaseAccount.code.in_({x.account.base_code[:2]
|
.filter(BaseAccount.code.in_({x.account.base_code[:2]
|
||||||
for x in balances})).all()
|
for x in balances})).all()
|
||||||
|
|
||||||
sections: dict[str, Section] = {x.code: Section(x) for x in titles}
|
sections: dict[str, BalanceSheetSection] \
|
||||||
subsections: dict[str, Subsection] = {x.code: Subsection(x)
|
= {x.code: BalanceSheetSection(x) for x in titles}
|
||||||
for x in subtitles}
|
subsections: dict[str, BalanceSheetSubsection] \
|
||||||
|
= {x.code: BalanceSheetSubsection(x) for x in subtitles}
|
||||||
for subsection in subsections.values():
|
for subsection in subsections.values():
|
||||||
sections[subsection.title.code[0]].subsections.append(subsection)
|
sections[subsection.title.code[0]].subsections.append(subsection)
|
||||||
for balance in balances:
|
for balance in balances:
|
||||||
@ -406,8 +430,7 @@ class BalanceSheet(BaseReport):
|
|||||||
:return: The response of the report for download.
|
:return: The response of the report for download.
|
||||||
"""
|
"""
|
||||||
filename: str = "balance-sheet-{currency}-{period}.csv"\
|
filename: str = "balance-sheet-{currency}-{period}.csv"\
|
||||||
.format(currency=self.__currency.code,
|
.format(currency=self.__currency.code, period=self.__period.spec)
|
||||||
period=period_spec(self.__period))
|
|
||||||
return csv_download(filename, self.__get_csv_rows())
|
return csv_download(filename, self.__get_csv_rows())
|
||||||
|
|
||||||
def __get_csv_rows(self) -> list[CSVRow]:
|
def __get_csv_rows(self) -> list[CSVRow]:
|
||||||
@ -443,7 +466,7 @@ class BalanceSheet(BaseReport):
|
|||||||
return rows
|
return rows
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __section_csv_rows(section: Section) -> list[CSVHalfRow]:
|
def __section_csv_rows(section: BalanceSheetSection) -> list[CSVHalfRow]:
|
||||||
"""Gathers the CSV rows for a section.
|
"""Gathers the CSV rows for a section.
|
||||||
|
|
||||||
:param section: The section.
|
:param section: The section.
|
||||||
@ -463,11 +486,12 @@ class BalanceSheet(BaseReport):
|
|||||||
|
|
||||||
:return: The report as HTML.
|
:return: The report as HTML.
|
||||||
"""
|
"""
|
||||||
params: PageParams = PageParams(currency=self.__currency,
|
params: BalanceSheetPageParams = BalanceSheetPageParams(
|
||||||
period=self.__period,
|
currency=self.__currency,
|
||||||
has_data=self.__has_data,
|
period=self.__period,
|
||||||
assets=self.__assets,
|
has_data=self.__has_data,
|
||||||
liabilities=self.__liabilities,
|
assets=self.__assets,
|
||||||
owner_s_equity=self.__owner_s_equity)
|
liabilities=self.__liabilities,
|
||||||
|
owner_s_equity=self.__owner_s_equity)
|
||||||
return render_template("accounting/report/balance-sheet.html",
|
return render_template("accounting/report/balance-sheet.html",
|
||||||
report=params)
|
report=params)
|
||||||
|
@ -22,7 +22,6 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import url_for, render_template, Response
|
from flask import url_for, render_template, Response
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
@ -30,24 +29,27 @@ from accounting.models import Currency, Account, Transaction, JournalEntry
|
|||||||
from accounting.report.income_expense_account import IncomeExpensesAccount
|
from accounting.report.income_expense_account import IncomeExpensesAccount
|
||||||
from accounting.report.period import Period
|
from accounting.report.period import Period
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
from .utils.base_page_params import BasePageParams
|
|
||||||
from .utils.base_report import BaseReport
|
from .utils.base_report import BaseReport
|
||||||
from .utils.csv_export import BaseCSVRow, csv_download, period_spec
|
from .utils.csv_export import BaseCSVRow, csv_download
|
||||||
from .utils.urls import income_expenses_url
|
|
||||||
from .utils.option_link import OptionLink
|
from .utils.option_link import OptionLink
|
||||||
|
from .utils.page_params import PageParams
|
||||||
from .utils.period_choosers import IncomeExpensesPeriodChooser
|
from .utils.period_choosers import IncomeExpensesPeriodChooser
|
||||||
from .utils.report_chooser import ReportChooser
|
from .utils.report_chooser import ReportChooser
|
||||||
from .utils.report_type import ReportType
|
from .utils.report_type import ReportType
|
||||||
|
|
||||||
|
|
||||||
class ReportEntry:
|
class Entry:
|
||||||
"""An entry in the report."""
|
"""An entry in the income and expenses log."""
|
||||||
|
|
||||||
def __init__(self, entry: JournalEntry | None = None):
|
def __init__(self, entry: JournalEntry | None = None):
|
||||||
"""Constructs the entry in the report.
|
"""Constructs the entry in the income and expenses log.
|
||||||
|
|
||||||
:param entry: The journal entry.
|
:param entry: The journal entry.
|
||||||
"""
|
"""
|
||||||
|
self.entry: JournalEntry | None = None
|
||||||
|
"""The journal entry."""
|
||||||
|
self.transaction: Transaction | None = None
|
||||||
|
"""The transaction."""
|
||||||
self.is_brought_forward: bool = False
|
self.is_brought_forward: bool = False
|
||||||
"""Whether this is the brought-forward entry."""
|
"""Whether this is the brought-forward entry."""
|
||||||
self.is_total: bool = False
|
self.is_total: bool = False
|
||||||
@ -66,25 +68,19 @@ class ReportEntry:
|
|||||||
"""The balance."""
|
"""The balance."""
|
||||||
self.note: str | None = None
|
self.note: str | None = None
|
||||||
"""The note."""
|
"""The note."""
|
||||||
self.url: str | None = None
|
|
||||||
"""The URL to the journal entry."""
|
|
||||||
if entry is not None:
|
if entry is not None:
|
||||||
self.date = entry.transaction.date
|
self.entry = entry
|
||||||
self.account = entry.account
|
|
||||||
self.summary = entry.summary
|
self.summary = entry.summary
|
||||||
self.income = None if entry.is_debit else entry.amount
|
self.income = None if entry.is_debit else entry.amount
|
||||||
self.expense = entry.amount if entry.is_debit else None
|
self.expense = entry.amount if entry.is_debit else None
|
||||||
self.note = entry.transaction.note
|
|
||||||
self.url = url_for("accounting.transaction.detail",
|
|
||||||
txn=entry.transaction)
|
|
||||||
|
|
||||||
|
|
||||||
class EntryCollector:
|
class EntryCollector:
|
||||||
"""The report entry collector."""
|
"""The income and expenses log entry collector."""
|
||||||
|
|
||||||
def __init__(self, currency: Currency, account: IncomeExpensesAccount,
|
def __init__(self, currency: Currency, account: IncomeExpensesAccount,
|
||||||
period: Period):
|
period: Period):
|
||||||
"""Constructs the report entry collector.
|
"""Constructs the income and expenses log entry collector.
|
||||||
|
|
||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
@ -96,18 +92,18 @@ class EntryCollector:
|
|||||||
"""The account."""
|
"""The account."""
|
||||||
self.__period: Period = period
|
self.__period: Period = period
|
||||||
"""The period"""
|
"""The period"""
|
||||||
self.brought_forward: ReportEntry | None
|
self.brought_forward: Entry | None
|
||||||
"""The brought-forward entry."""
|
"""The brought-forward entry."""
|
||||||
self.entries: list[ReportEntry]
|
self.entries: list[Entry]
|
||||||
"""The log entries."""
|
"""The log entries."""
|
||||||
self.total: ReportEntry | None
|
self.total: Entry | None
|
||||||
"""The total entry."""
|
"""The total entry."""
|
||||||
self.brought_forward = self.__get_brought_forward_entry()
|
self.brought_forward = self.__get_brought_forward_entry()
|
||||||
self.entries = self.__query_entries()
|
self.entries = self.__query_entries()
|
||||||
self.total = self.__get_total_entry()
|
self.total = self.__get_total_entry()
|
||||||
self.__populate_balance()
|
self.__populate_balance()
|
||||||
|
|
||||||
def __get_brought_forward_entry(self) -> ReportEntry | None:
|
def __get_brought_forward_entry(self) -> Entry | None:
|
||||||
"""Queries, composes and returns the brought-forward entry.
|
"""Queries, composes and returns the brought-forward entry.
|
||||||
|
|
||||||
:return: The brought-forward entry, or None if the period starts from
|
:return: The brought-forward entry, or None if the period starts from
|
||||||
@ -126,10 +122,10 @@ class EntryCollector:
|
|||||||
balance: int | None = db.session.scalar(select)
|
balance: int | None = db.session.scalar(select)
|
||||||
if balance is None:
|
if balance is None:
|
||||||
return None
|
return None
|
||||||
entry: ReportEntry = ReportEntry()
|
entry: Entry = Entry()
|
||||||
entry.is_brought_forward = True
|
entry.is_brought_forward = True
|
||||||
entry.date = self.__period.start
|
entry.date = self.__period.start
|
||||||
entry.account = Account.accumulated_change()
|
entry.account = Account.find_by_code("3351-001")
|
||||||
entry.summary = gettext("Brought forward")
|
entry.summary = gettext("Brought forward")
|
||||||
if balance > 0:
|
if balance > 0:
|
||||||
entry.income = balance
|
entry.income = balance
|
||||||
@ -138,7 +134,7 @@ class EntryCollector:
|
|||||||
entry.balance = balance
|
entry.balance = balance
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
def __query_entries(self) -> list[ReportEntry]:
|
def __query_entries(self) -> list[Entry]:
|
||||||
"""Queries and returns the log entries.
|
"""Queries and returns the log entries.
|
||||||
|
|
||||||
:return: The log entries.
|
:return: The log entries.
|
||||||
@ -153,16 +149,14 @@ class EntryCollector:
|
|||||||
txn_with_account: sa.Select = sa.Select(Transaction.id).\
|
txn_with_account: sa.Select = sa.Select(Transaction.id).\
|
||||||
join(JournalEntry).join(Account).filter(*conditions)
|
join(JournalEntry).join(Account).filter(*conditions)
|
||||||
|
|
||||||
return [ReportEntry(x)
|
return [Entry(x)
|
||||||
for x in JournalEntry.query.join(Transaction).join(Account)
|
for x in JournalEntry.query.join(Transaction).join(Account)
|
||||||
.filter(JournalEntry.transaction_id.in_(txn_with_account),
|
.filter(JournalEntry.transaction_id.in_(txn_with_account),
|
||||||
JournalEntry.currency_code == self.__currency.code,
|
JournalEntry.currency_code == self.__currency.code,
|
||||||
sa.not_(self.__account_condition))
|
sa.not_(self.__account_condition))
|
||||||
.order_by(Transaction.date,
|
.order_by(Transaction.date,
|
||||||
JournalEntry.is_debit,
|
JournalEntry.is_debit,
|
||||||
JournalEntry.no)
|
JournalEntry.no)]
|
||||||
.options(selectinload(JournalEntry.account),
|
|
||||||
selectinload(JournalEntry.transaction))]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def __account_condition(self) -> sa.BinaryExpression:
|
def __account_condition(self) -> sa.BinaryExpression:
|
||||||
@ -173,14 +167,14 @@ class EntryCollector:
|
|||||||
Account.base_code.startswith("22"))
|
Account.base_code.startswith("22"))
|
||||||
return Account.id == self.__account.id
|
return Account.id == self.__account.id
|
||||||
|
|
||||||
def __get_total_entry(self) -> ReportEntry | None:
|
def __get_total_entry(self) -> Entry | None:
|
||||||
"""Composes the total entry.
|
"""Composes the total entry.
|
||||||
|
|
||||||
:return: The total entry, or None if there is no data.
|
:return: The total entry, or None if there is no data.
|
||||||
"""
|
"""
|
||||||
if self.brought_forward is None and len(self.entries) == 0:
|
if self.brought_forward is None and len(self.entries) == 0:
|
||||||
return None
|
return None
|
||||||
entry: ReportEntry = ReportEntry()
|
entry: Entry = Entry()
|
||||||
entry.is_total = True
|
entry.is_total = True
|
||||||
entry.summary = gettext("Total")
|
entry.summary = gettext("Total")
|
||||||
entry.income = sum([x.income for x in self.entries
|
entry.income = sum([x.income for x in self.entries
|
||||||
@ -208,7 +202,7 @@ class EntryCollector:
|
|||||||
|
|
||||||
|
|
||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""A row in the CSV income and expenses log."""
|
||||||
|
|
||||||
def __init__(self, txn_date: date | str | None,
|
def __init__(self, txn_date: date | str | None,
|
||||||
account: str | None,
|
account: str | None,
|
||||||
@ -217,7 +211,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
expense: str | Decimal | None,
|
expense: str | Decimal | None,
|
||||||
balance: str | Decimal | None,
|
balance: str | Decimal | None,
|
||||||
note: str | None):
|
note: str | None):
|
||||||
"""Constructs a row in the CSV.
|
"""Constructs a row in the CSV income and expenses log.
|
||||||
|
|
||||||
:param txn_date: The transaction date.
|
:param txn_date: The transaction date.
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
@ -252,18 +246,18 @@ class CSVRow(BaseCSVRow):
|
|||||||
self.income, self.expense, self.balance, self.note]
|
self.income, self.expense, self.balance, self.note]
|
||||||
|
|
||||||
|
|
||||||
class PageParams(BasePageParams):
|
class IncomeExpensesPageParams(PageParams):
|
||||||
"""The HTML page parameters."""
|
"""The HTML parameters of the income and expenses log."""
|
||||||
|
|
||||||
def __init__(self, currency: Currency,
|
def __init__(self, currency: Currency,
|
||||||
account: IncomeExpensesAccount,
|
account: IncomeExpensesAccount,
|
||||||
period: Period,
|
period: Period,
|
||||||
has_data: bool,
|
has_data: bool,
|
||||||
pagination: Pagination[ReportEntry],
|
pagination: Pagination[Entry],
|
||||||
brought_forward: ReportEntry | None,
|
brought_forward: Entry | None,
|
||||||
entries: list[ReportEntry],
|
entries: list[Entry],
|
||||||
total: ReportEntry | None):
|
total: Entry | None):
|
||||||
"""Constructs the HTML page parameters.
|
"""Constructs the HTML parameters of the income and expenses log.
|
||||||
|
|
||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
@ -281,13 +275,13 @@ class PageParams(BasePageParams):
|
|||||||
"""The period."""
|
"""The period."""
|
||||||
self.__has_data: bool = has_data
|
self.__has_data: bool = has_data
|
||||||
"""True if there is any data, or False otherwise."""
|
"""True if there is any data, or False otherwise."""
|
||||||
self.pagination: Pagination[ReportEntry] = pagination
|
self.pagination: Pagination[Entry] = pagination
|
||||||
"""The pagination."""
|
"""The pagination."""
|
||||||
self.brought_forward: ReportEntry | None = brought_forward
|
self.brought_forward: Entry | None = brought_forward
|
||||||
"""The brought-forward entry."""
|
"""The brought-forward entry."""
|
||||||
self.entries: list[ReportEntry] = entries
|
self.entries: list[Entry] = entries
|
||||||
"""The report entries."""
|
"""The entries."""
|
||||||
self.total: ReportEntry | None = total
|
self.total: Entry | None = total
|
||||||
"""The total entry."""
|
"""The total entry."""
|
||||||
self.period_chooser: IncomeExpensesPeriodChooser \
|
self.period_chooser: IncomeExpensesPeriodChooser \
|
||||||
= IncomeExpensesPeriodChooser(currency, account)
|
= IncomeExpensesPeriodChooser(currency, account)
|
||||||
@ -323,9 +317,20 @@ class PageParams(BasePageParams):
|
|||||||
|
|
||||||
:return: The currency options.
|
:return: The currency options.
|
||||||
"""
|
"""
|
||||||
return self._get_currency_options(
|
def get_url(currency: Currency):
|
||||||
lambda x: income_expenses_url(x, self.account, self.period),
|
if self.period.is_default:
|
||||||
self.currency)
|
return url_for("accounting.report.income-expenses-default",
|
||||||
|
currency=currency, account=self.account)
|
||||||
|
return url_for("accounting.report.income-expenses",
|
||||||
|
currency=currency, account=self.account,
|
||||||
|
period=self.period)
|
||||||
|
|
||||||
|
in_use: set[str] = set(db.session.scalars(
|
||||||
|
sa.select(JournalEntry.currency_code)
|
||||||
|
.group_by(JournalEntry.currency_code)).all())
|
||||||
|
return [OptionLink(str(x), get_url(x), x.code == self.currency.code)
|
||||||
|
for x in Currency.query.filter(Currency.code.in_(in_use))
|
||||||
|
.order_by(Currency.code).all()]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def account_options(self) -> list[OptionLink]:
|
def account_options(self) -> list[OptionLink]:
|
||||||
@ -333,12 +338,18 @@ class PageParams(BasePageParams):
|
|||||||
|
|
||||||
:return: The account options.
|
:return: The account options.
|
||||||
"""
|
"""
|
||||||
|
def get_url(account: IncomeExpensesAccount):
|
||||||
|
if self.period.is_default:
|
||||||
|
return url_for("accounting.report.income-expenses-default",
|
||||||
|
currency=self.currency, account=account)
|
||||||
|
return url_for("accounting.report.income-expenses",
|
||||||
|
currency=self.currency, account=account,
|
||||||
|
period=self.period)
|
||||||
|
|
||||||
current_al: IncomeExpensesAccount \
|
current_al: IncomeExpensesAccount \
|
||||||
= IncomeExpensesAccount.current_assets_and_liabilities()
|
= IncomeExpensesAccount.current_assets_and_liabilities()
|
||||||
options: list[OptionLink] \
|
options: list[OptionLink] \
|
||||||
= [OptionLink(str(current_al),
|
= [OptionLink(str(current_al), get_url(current_al),
|
||||||
income_expenses_url(self.currency, current_al,
|
|
||||||
self.period),
|
|
||||||
self.account.id == 0)]
|
self.account.id == 0)]
|
||||||
in_use: sa.Select = sa.Select(JournalEntry.account_id)\
|
in_use: sa.Select = sa.Select(JournalEntry.account_id)\
|
||||||
.join(Account)\
|
.join(Account)\
|
||||||
@ -348,17 +359,35 @@ class PageParams(BasePageParams):
|
|||||||
Account.base_code.startswith("21"),
|
Account.base_code.startswith("21"),
|
||||||
Account.base_code.startswith("22")))\
|
Account.base_code.startswith("22")))\
|
||||||
.group_by(JournalEntry.account_id)
|
.group_by(JournalEntry.account_id)
|
||||||
options.extend([OptionLink(str(x),
|
options.extend([OptionLink(str(x), get_url(IncomeExpensesAccount(x)),
|
||||||
income_expenses_url(
|
|
||||||
self.currency,
|
|
||||||
IncomeExpensesAccount(x),
|
|
||||||
self.period),
|
|
||||||
x.id == self.account.id)
|
x.id == self.account.id)
|
||||||
for x in Account.query.filter(Account.id.in_(in_use))
|
for x in Account.query.filter(Account.id.in_(in_use))
|
||||||
.order_by(Account.base_code, Account.no).all()])
|
.order_by(Account.base_code, Account.no).all()])
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def _populate_entries(entries: list[Entry]) -> None:
|
||||||
|
"""Populates the income and expenses entries with relative data.
|
||||||
|
|
||||||
|
:param entries: The income and expenses entries.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
transactions: dict[int, Transaction] \
|
||||||
|
= {x.id: x for x in Transaction.query.filter(
|
||||||
|
Transaction.id.in_({x.entry.transaction_id for x in entries
|
||||||
|
if x.entry is not None}))}
|
||||||
|
accounts: dict[int, Account] \
|
||||||
|
= {x.id: x for x in Account.query.filter(
|
||||||
|
Account.id.in_({x.entry.account_id for x in entries
|
||||||
|
if x.entry is not None}))}
|
||||||
|
for entry in entries:
|
||||||
|
if entry.entry is not None:
|
||||||
|
entry.transaction = transactions[entry.entry.transaction_id]
|
||||||
|
entry.date = entry.transaction.date
|
||||||
|
entry.note = entry.transaction.note
|
||||||
|
entry.account = accounts[entry.entry.account_id]
|
||||||
|
|
||||||
|
|
||||||
class IncomeExpenses(BaseReport):
|
class IncomeExpenses(BaseReport):
|
||||||
"""The income and expenses log."""
|
"""The income and expenses log."""
|
||||||
|
|
||||||
@ -378,11 +407,11 @@ class IncomeExpenses(BaseReport):
|
|||||||
"""The period."""
|
"""The period."""
|
||||||
collector: EntryCollector = EntryCollector(
|
collector: EntryCollector = EntryCollector(
|
||||||
self.__currency, self.__account, self.__period)
|
self.__currency, self.__account, self.__period)
|
||||||
self.__brought_forward: ReportEntry | None = collector.brought_forward
|
self.__brought_forward: Entry | None = collector.brought_forward
|
||||||
"""The brought-forward entry."""
|
"""The brought-forward entry."""
|
||||||
self.__entries: list[ReportEntry] = collector.entries
|
self.__entries: list[Entry] = collector.entries
|
||||||
"""The report entries."""
|
"""The log entries."""
|
||||||
self.__total: ReportEntry | None = collector.total
|
self.__total: Entry | None = collector.total
|
||||||
"""The total entry."""
|
"""The total entry."""
|
||||||
|
|
||||||
def csv(self) -> Response:
|
def csv(self) -> Response:
|
||||||
@ -392,7 +421,7 @@ class IncomeExpenses(BaseReport):
|
|||||||
"""
|
"""
|
||||||
filename: str = "income-expenses-{currency}-{account}-{period}.csv"\
|
filename: str = "income-expenses-{currency}-{account}-{period}.csv"\
|
||||||
.format(currency=self.__currency.code, account=self.__account.code,
|
.format(currency=self.__currency.code, account=self.__account.code,
|
||||||
period=period_spec(self.__period))
|
period=self.__period.spec)
|
||||||
return csv_download(filename, self.__get_csv_rows())
|
return csv_download(filename, self.__get_csv_rows())
|
||||||
|
|
||||||
def __get_csv_rows(self) -> list[CSVRow]:
|
def __get_csv_rows(self) -> list[CSVRow]:
|
||||||
@ -400,6 +429,7 @@ class IncomeExpenses(BaseReport):
|
|||||||
|
|
||||||
:return: The CSV rows.
|
:return: The CSV rows.
|
||||||
"""
|
"""
|
||||||
|
_populate_entries(self.__entries)
|
||||||
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Account"),
|
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Account"),
|
||||||
gettext("Summary"), gettext("Income"),
|
gettext("Summary"), gettext("Income"),
|
||||||
gettext("Expense"), gettext("Balance"),
|
gettext("Expense"), gettext("Balance"),
|
||||||
@ -426,31 +456,32 @@ class IncomeExpenses(BaseReport):
|
|||||||
|
|
||||||
:return: The report as HTML.
|
:return: The report as HTML.
|
||||||
"""
|
"""
|
||||||
all_entries: list[ReportEntry] = []
|
all_entries: list[Entry] = []
|
||||||
if self.__brought_forward is not None:
|
if self.__brought_forward is not None:
|
||||||
all_entries.append(self.__brought_forward)
|
all_entries.append(self.__brought_forward)
|
||||||
all_entries.extend(self.__entries)
|
all_entries.extend(self.__entries)
|
||||||
if self.__total is not None:
|
if self.__total is not None:
|
||||||
all_entries.append(self.__total)
|
all_entries.append(self.__total)
|
||||||
pagination: Pagination[ReportEntry] \
|
pagination: Pagination[Entry] = Pagination[Entry](all_entries)
|
||||||
= Pagination[ReportEntry](all_entries)
|
page_entries: list[Entry] = pagination.list
|
||||||
page_entries: list[ReportEntry] = pagination.list
|
|
||||||
has_data: bool = len(page_entries) > 0
|
has_data: bool = len(page_entries) > 0
|
||||||
brought_forward: ReportEntry | None = None
|
_populate_entries(page_entries)
|
||||||
|
brought_forward: Entry | None = None
|
||||||
if len(page_entries) > 0 and page_entries[0].is_brought_forward:
|
if len(page_entries) > 0 and page_entries[0].is_brought_forward:
|
||||||
brought_forward = page_entries[0]
|
brought_forward = page_entries[0]
|
||||||
page_entries = page_entries[1:]
|
page_entries = page_entries[1:]
|
||||||
total: ReportEntry | None = None
|
total: Entry | None = None
|
||||||
if len(page_entries) > 0 and page_entries[-1].is_total:
|
if len(page_entries) > 0 and page_entries[-1].is_total:
|
||||||
total = page_entries[-1]
|
total = page_entries[-1]
|
||||||
page_entries = page_entries[:-1]
|
page_entries = page_entries[:-1]
|
||||||
params: PageParams = PageParams(currency=self.__currency,
|
params: IncomeExpensesPageParams = IncomeExpensesPageParams(
|
||||||
account=self.__account,
|
currency=self.__currency,
|
||||||
period=self.__period,
|
account=self.__account,
|
||||||
has_data=has_data,
|
period=self.__period,
|
||||||
pagination=pagination,
|
has_data=has_data,
|
||||||
brought_forward=brought_forward,
|
pagination=pagination,
|
||||||
entries=page_entries,
|
brought_forward=brought_forward,
|
||||||
total=total)
|
entries=page_entries,
|
||||||
|
total=total)
|
||||||
return render_template("accounting/report/income-expenses.html",
|
return render_template("accounting/report/income-expenses.html",
|
||||||
report=params)
|
report=params)
|
||||||
|
@ -20,28 +20,27 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import render_template, Response
|
from flask import url_for, render_template, Response
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from accounting.models import Currency, BaseAccount, Account, Transaction, \
|
from accounting.models import Currency, BaseAccount, Account, Transaction, \
|
||||||
JournalEntry
|
JournalEntry
|
||||||
from accounting.report.period import Period
|
from accounting.report.period import Period
|
||||||
from .utils.base_page_params import BasePageParams
|
|
||||||
from .utils.base_report import BaseReport
|
from .utils.base_report import BaseReport
|
||||||
from .utils.csv_export import BaseCSVRow, csv_download, period_spec
|
from .utils.csv_export import BaseCSVRow, csv_download
|
||||||
from .utils.urls import ledger_url, income_statement_url
|
|
||||||
from .utils.option_link import OptionLink
|
from .utils.option_link import OptionLink
|
||||||
|
from .utils.page_params import PageParams
|
||||||
from .utils.period_choosers import IncomeStatementPeriodChooser
|
from .utils.period_choosers import IncomeStatementPeriodChooser
|
||||||
from .utils.report_chooser import ReportChooser
|
from .utils.report_chooser import ReportChooser
|
||||||
from .utils.report_type import ReportType
|
from .utils.report_type import ReportType
|
||||||
|
|
||||||
|
|
||||||
class ReportAccount:
|
class IncomeStatementAccount:
|
||||||
"""An account in the report."""
|
"""An account in the income statement."""
|
||||||
|
|
||||||
def __init__(self, account: Account, amount: Decimal, url: str):
|
def __init__(self, account: Account, amount: Decimal, url: str):
|
||||||
"""Constructs an account in the report.
|
"""Constructs an account in the income statement.
|
||||||
|
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
:param amount: The amount.
|
:param amount: The amount.
|
||||||
@ -55,11 +54,11 @@ class ReportAccount:
|
|||||||
"""The URL to the ledger of the account."""
|
"""The URL to the ledger of the account."""
|
||||||
|
|
||||||
|
|
||||||
class AccumulatedTotal:
|
class IncomeStatementAccumulatedTotal:
|
||||||
"""An accumulated total."""
|
"""An accumulated total in the income statement."""
|
||||||
|
|
||||||
def __init__(self, title: str):
|
def __init__(self, title: str):
|
||||||
"""Constructs an accumulated total.
|
"""Constructs an accumulated total in the income statement.
|
||||||
|
|
||||||
:param title: The title.
|
:param title: The title.
|
||||||
"""
|
"""
|
||||||
@ -69,17 +68,17 @@ class AccumulatedTotal:
|
|||||||
"""The amount of the account."""
|
"""The amount of the account."""
|
||||||
|
|
||||||
|
|
||||||
class Subsection:
|
class IncomeStatementSubsection:
|
||||||
"""A subsection."""
|
"""A subsection in the income statement."""
|
||||||
|
|
||||||
def __init__(self, title: BaseAccount):
|
def __init__(self, title: BaseAccount):
|
||||||
"""Constructs a subsection.
|
"""Constructs a subsection in the income statement.
|
||||||
|
|
||||||
:param title: The title account.
|
:param title: The title account.
|
||||||
"""
|
"""
|
||||||
self.title: BaseAccount = title
|
self.title: BaseAccount = title
|
||||||
"""The title account."""
|
"""The title account."""
|
||||||
self.accounts: list[ReportAccount] = []
|
self.accounts: list[IncomeStatementAccount] = []
|
||||||
"""The accounts in the subsection."""
|
"""The accounts in the subsection."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -91,21 +90,21 @@ class Subsection:
|
|||||||
return sum([x.amount for x in self.accounts])
|
return sum([x.amount for x in self.accounts])
|
||||||
|
|
||||||
|
|
||||||
class Section:
|
class IncomeStatementSection:
|
||||||
"""A section."""
|
"""A section in the income statement."""
|
||||||
|
|
||||||
def __init__(self, title: BaseAccount, accumulated_title: str):
|
def __init__(self, title: BaseAccount, accumulated_title: str):
|
||||||
"""Constructs a section.
|
"""Constructs a section in the income statement.
|
||||||
|
|
||||||
:param title: The title account.
|
:param title: The title account.
|
||||||
:param accumulated_title: The title for the accumulated total.
|
:param accumulated_title: The title for the accumulated total.
|
||||||
"""
|
"""
|
||||||
self.title: BaseAccount = title
|
self.title: BaseAccount = title
|
||||||
"""The title account."""
|
"""The title account."""
|
||||||
self.subsections: list[Subsection] = []
|
self.subsections: list[IncomeStatementSubsection] = []
|
||||||
"""The subsections in the section."""
|
"""The subsections in the section."""
|
||||||
self.accumulated: AccumulatedTotal \
|
self.accumulated: IncomeStatementAccumulatedTotal \
|
||||||
= AccumulatedTotal(accumulated_title)
|
= IncomeStatementAccumulatedTotal(accumulated_title)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total(self) -> Decimal:
|
def total(self) -> Decimal:
|
||||||
@ -117,10 +116,10 @@ class Section:
|
|||||||
|
|
||||||
|
|
||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""A row in the CSV income statement."""
|
||||||
|
|
||||||
def __init__(self, text: str | None, amount: str | Decimal | None):
|
def __init__(self, text: str | None, amount: str | Decimal | None):
|
||||||
"""Constructs a row in the CSV.
|
"""Constructs a row in the CSV income statement.
|
||||||
|
|
||||||
:param text: The text.
|
:param text: The text.
|
||||||
:param amount: The amount.
|
:param amount: The amount.
|
||||||
@ -139,14 +138,14 @@ class CSVRow(BaseCSVRow):
|
|||||||
return [self.text, self.amount]
|
return [self.text, self.amount]
|
||||||
|
|
||||||
|
|
||||||
class PageParams(BasePageParams):
|
class IncomeStatementPageParams(PageParams):
|
||||||
"""The HTML page parameters."""
|
"""The HTML parameters of the income statement."""
|
||||||
|
|
||||||
def __init__(self, currency: Currency,
|
def __init__(self, currency: Currency,
|
||||||
period: Period,
|
period: Period,
|
||||||
has_data: bool,
|
has_data: bool,
|
||||||
sections: list[Section], ):
|
sections: list[IncomeStatementSection],):
|
||||||
"""Constructs the HTML page parameters.
|
"""Constructs the HTML parameters of the income statement.
|
||||||
|
|
||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
:param period: The period.
|
:param period: The period.
|
||||||
@ -158,7 +157,7 @@ class PageParams(BasePageParams):
|
|||||||
"""The period."""
|
"""The period."""
|
||||||
self.__has_data: bool = has_data
|
self.__has_data: bool = has_data
|
||||||
"""True if there is any data, or False otherwise."""
|
"""True if there is any data, or False otherwise."""
|
||||||
self.sections: list[Section] = sections
|
self.sections: list[IncomeStatementSection] = sections
|
||||||
self.period_chooser: IncomeStatementPeriodChooser \
|
self.period_chooser: IncomeStatementPeriodChooser \
|
||||||
= IncomeStatementPeriodChooser(currency)
|
= IncomeStatementPeriodChooser(currency)
|
||||||
"""The period chooser."""
|
"""The period chooser."""
|
||||||
@ -187,8 +186,19 @@ class PageParams(BasePageParams):
|
|||||||
|
|
||||||
:return: The currency options.
|
:return: The currency options.
|
||||||
"""
|
"""
|
||||||
return self._get_currency_options(
|
def get_url(currency: Currency):
|
||||||
lambda x: income_statement_url(x, self.period), self.currency)
|
if self.period.is_default:
|
||||||
|
return url_for("accounting.report.income-statement-default",
|
||||||
|
currency=currency)
|
||||||
|
return url_for("accounting.report.income-statement",
|
||||||
|
currency=currency, period=self.period)
|
||||||
|
|
||||||
|
in_use: set[str] = set(db.session.scalars(
|
||||||
|
sa.select(JournalEntry.currency_code)
|
||||||
|
.group_by(JournalEntry.currency_code)).all())
|
||||||
|
return [OptionLink(str(x), get_url(x), x.code == self.currency.code)
|
||||||
|
for x in Currency.query.filter(Currency.code.in_(in_use))
|
||||||
|
.order_by(Currency.code).all()]
|
||||||
|
|
||||||
|
|
||||||
class IncomeStatement(BaseReport):
|
class IncomeStatement(BaseReport):
|
||||||
@ -206,7 +216,7 @@ class IncomeStatement(BaseReport):
|
|||||||
"""The period."""
|
"""The period."""
|
||||||
self.__has_data: bool
|
self.__has_data: bool
|
||||||
"""True if there is any data, or False otherwise."""
|
"""True if there is any data, or False otherwise."""
|
||||||
self.__sections: list[Section]
|
self.__sections: list[IncomeStatementSection]
|
||||||
"""The sections."""
|
"""The sections."""
|
||||||
self.__set_data()
|
self.__set_data()
|
||||||
|
|
||||||
@ -215,7 +225,7 @@ class IncomeStatement(BaseReport):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
balances: list[ReportAccount] = self.__query_balances()
|
balances: list[IncomeStatementAccount] = self.__query_balances()
|
||||||
|
|
||||||
titles: list[BaseAccount] = BaseAccount.query\
|
titles: list[BaseAccount] = BaseAccount.query\
|
||||||
.filter(BaseAccount.code.in_({"4", "5", "6", "7", "8", "9"})).all()
|
.filter(BaseAccount.code.in_({"4", "5", "6", "7", "8", "9"})).all()
|
||||||
@ -224,17 +234,18 @@ class IncomeStatement(BaseReport):
|
|||||||
for x in balances})).all()
|
for x in balances})).all()
|
||||||
|
|
||||||
total_titles: dict[str, str] \
|
total_titles: dict[str, str] \
|
||||||
= {"4": gettext("total operating revenue"),
|
= {"4": gettext("total revenue"),
|
||||||
"5": gettext("gross income"),
|
"5": gettext("gross income"),
|
||||||
"6": gettext("operating income"),
|
"6": gettext("operating income"),
|
||||||
"7": gettext("before tax income"),
|
"7": gettext("before tax income"),
|
||||||
"8": gettext("after tax income"),
|
"8": gettext("after tax income"),
|
||||||
"9": gettext("net income or loss for current period")}
|
"9": gettext("net income or loss for current period")}
|
||||||
|
|
||||||
sections: dict[str, Section] \
|
sections: dict[str, IncomeStatementSection] \
|
||||||
= {x.code: Section(x, total_titles[x.code]) for x in titles}
|
= {x.code: IncomeStatementSection(x, total_titles[x.code])
|
||||||
subsections: dict[str, Subsection] \
|
for x in titles}
|
||||||
= {x.code: Subsection(x) for x in subtitles}
|
subsections: dict[str, IncomeStatementSubsection] \
|
||||||
|
= {x.code: IncomeStatementSubsection(x) for x in subtitles}
|
||||||
for subsection in subsections.values():
|
for subsection in subsections.values():
|
||||||
sections[subsection.title.code[0]].subsections.append(subsection)
|
sections[subsection.title.code[0]].subsections.append(subsection)
|
||||||
for balance in balances:
|
for balance in balances:
|
||||||
@ -247,7 +258,7 @@ class IncomeStatement(BaseReport):
|
|||||||
total = total + section.total
|
total = total + section.total
|
||||||
section.accumulated.amount = total
|
section.accumulated.amount = total
|
||||||
|
|
||||||
def __query_balances(self) -> list[ReportAccount]:
|
def __query_balances(self) -> list[IncomeStatementAccount]:
|
||||||
"""Queries and returns the balances.
|
"""Queries and returns the balances.
|
||||||
|
|
||||||
:return: The balances.
|
:return: The balances.
|
||||||
@ -264,20 +275,33 @@ class IncomeStatement(BaseReport):
|
|||||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||||
(JournalEntry.is_debit, -JournalEntry.amount),
|
(JournalEntry.is_debit, -JournalEntry.amount),
|
||||||
else_=JournalEntry.amount)).label("balance")
|
else_=JournalEntry.amount)).label("balance")
|
||||||
select_balances: sa.Select = sa.select(Account.id, balance_func)\
|
select_balance: sa.Select \
|
||||||
|
= sa.select(JournalEntry.account_id, balance_func)\
|
||||||
.join(Transaction).join(Account)\
|
.join(Transaction).join(Account)\
|
||||||
.filter(*conditions)\
|
.filter(*conditions)\
|
||||||
.group_by(Account.id)\
|
.group_by(JournalEntry.account_id)\
|
||||||
.order_by(Account.base_code, Account.no)
|
.order_by(Account.base_code, Account.no)
|
||||||
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
balances: list[sa.Row] = db.session.execute(select_balance).all()
|
||||||
accounts: dict[int, Account] \
|
accounts: dict[int, Account] \
|
||||||
= {x.id: x for x in Account.query
|
= {x.id: x for x in Account.query
|
||||||
.filter(Account.id.in_([x.id for x in balances])).all()}
|
.filter(Account.id.in_([x.account_id for x in balances])).all()}
|
||||||
return [ReportAccount(account=accounts[x.id],
|
|
||||||
amount=x.balance,
|
def get_url(account: Account) -> str:
|
||||||
url=ledger_url(self.__currency,
|
"""Returns the ledger URL of an account.
|
||||||
accounts[x.id],
|
|
||||||
self.__period))
|
:param account: The account.
|
||||||
|
:return: The ledger URL of the account.
|
||||||
|
"""
|
||||||
|
if self.__period.is_default:
|
||||||
|
return url_for("accounting.report.ledger-default",
|
||||||
|
currency=self.__currency, account=account)
|
||||||
|
return url_for("accounting.report.ledger",
|
||||||
|
currency=self.__currency, account=account,
|
||||||
|
period=self.__period)
|
||||||
|
|
||||||
|
return [IncomeStatementAccount(account=accounts[x.account_id],
|
||||||
|
amount=x.balance,
|
||||||
|
url=get_url(accounts[x.account_id]))
|
||||||
for x in balances]
|
for x in balances]
|
||||||
|
|
||||||
def csv(self) -> Response:
|
def csv(self) -> Response:
|
||||||
@ -286,8 +310,7 @@ class IncomeStatement(BaseReport):
|
|||||||
:return: The response of the report for download.
|
:return: The response of the report for download.
|
||||||
"""
|
"""
|
||||||
filename: str = "income-statement-{currency}-{period}.csv"\
|
filename: str = "income-statement-{currency}-{period}.csv"\
|
||||||
.format(currency=self.__currency.code,
|
.format(currency=self.__currency.code, period=self.__period.spec)
|
||||||
period=period_spec(self.__period))
|
|
||||||
return csv_download(filename, self.__get_csv_rows())
|
return csv_download(filename, self.__get_csv_rows())
|
||||||
|
|
||||||
def __get_csv_rows(self) -> list[CSVRow]:
|
def __get_csv_rows(self) -> list[CSVRow]:
|
||||||
@ -316,9 +339,10 @@ class IncomeStatement(BaseReport):
|
|||||||
|
|
||||||
:return: The report as HTML.
|
:return: The report as HTML.
|
||||||
"""
|
"""
|
||||||
params: PageParams = PageParams(currency=self.__currency,
|
params: IncomeStatementPageParams = IncomeStatementPageParams(
|
||||||
period=self.__period,
|
currency=self.__currency,
|
||||||
has_data=self.__has_data,
|
period=self.__period,
|
||||||
sections=self.__sections)
|
has_data=self.__has_data,
|
||||||
|
sections=self.__sections)
|
||||||
return render_template("accounting/report/income-statement.html",
|
return render_template("accounting/report/income-statement.html",
|
||||||
report=params)
|
report=params)
|
||||||
|
@ -22,48 +22,56 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import render_template, Response
|
from flask import render_template, Response
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
|
|
||||||
|
from accounting import db
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from accounting.models import Currency, Account, Transaction, JournalEntry
|
from accounting.models import Currency, Account, Transaction, JournalEntry
|
||||||
from accounting.report.period import Period
|
from accounting.report.period import Period
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
from .utils.base_page_params import BasePageParams
|
|
||||||
from .utils.base_report import BaseReport
|
from .utils.base_report import BaseReport
|
||||||
from .utils.csv_export import BaseCSVRow, csv_download, period_spec
|
from .utils.csv_export import BaseCSVRow, csv_download
|
||||||
|
from .utils.page_params import PageParams
|
||||||
from .utils.period_choosers import JournalPeriodChooser
|
from .utils.period_choosers import JournalPeriodChooser
|
||||||
from .utils.report_chooser import ReportChooser
|
from .utils.report_chooser import ReportChooser
|
||||||
from .utils.report_type import ReportType
|
from .utils.report_type import ReportType
|
||||||
|
|
||||||
|
|
||||||
class ReportEntry:
|
class Entry:
|
||||||
"""An entry in the report."""
|
"""An entry in the journal."""
|
||||||
|
|
||||||
def __init__(self, entry: JournalEntry):
|
def __init__(self, entry: JournalEntry | None = None):
|
||||||
"""Constructs the entry in the report.
|
"""Constructs the entry in the journal.
|
||||||
|
|
||||||
:param entry: The journal entry.
|
:param entry: The journal entry.
|
||||||
"""
|
"""
|
||||||
self.entry: JournalEntry = entry
|
self.entry: JournalEntry | None = None
|
||||||
"""The journal entry."""
|
"""The journal entry."""
|
||||||
self.transaction: Transaction = entry.transaction
|
self.transaction: Transaction | None = None
|
||||||
"""The transaction."""
|
"""The transaction."""
|
||||||
self.currency: Currency = entry.currency
|
self.is_total: bool = False
|
||||||
|
"""Whether this is the total entry."""
|
||||||
|
self.currency: Currency | None = None
|
||||||
"""The account."""
|
"""The account."""
|
||||||
self.account: Account = entry.account
|
self.account: Account | None = None
|
||||||
"""The account."""
|
"""The account."""
|
||||||
self.summary: str | None = entry.summary
|
self.summary: str | None = None
|
||||||
"""The summary."""
|
"""The summary."""
|
||||||
self.debit: Decimal | None = entry.debit
|
self.debit: Decimal | None = None
|
||||||
"""The debit amount."""
|
"""The debit amount."""
|
||||||
self.credit: Decimal | None = entry.credit
|
self.credit: Decimal | None = None
|
||||||
"""The credit amount."""
|
"""The credit amount."""
|
||||||
self.amount: Decimal = entry.amount
|
self.amount: Decimal | None = None
|
||||||
"""The amount."""
|
"""The amount."""
|
||||||
|
if entry is not None:
|
||||||
|
self.entry = entry
|
||||||
|
self.summary = entry.summary
|
||||||
|
self.debit = entry.amount if entry.is_debit else None
|
||||||
|
self.credit = None if entry.is_debit else entry.amount
|
||||||
|
self.amount = entry.amount
|
||||||
|
|
||||||
|
|
||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""A row in the CSV journal."""
|
||||||
|
|
||||||
def __init__(self, txn_date: str | date,
|
def __init__(self, txn_date: str | date,
|
||||||
currency: str,
|
currency: str,
|
||||||
@ -72,7 +80,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
debit: str | Decimal | None,
|
debit: str | Decimal | None,
|
||||||
credit: str | Decimal | None,
|
credit: str | Decimal | None,
|
||||||
note: str | None):
|
note: str | None):
|
||||||
"""Constructs a row in the CSV.
|
"""Constructs a row in the CSV journal.
|
||||||
|
|
||||||
:param txn_date: The transaction date.
|
:param txn_date: The transaction date.
|
||||||
:param summary: The summary.
|
:param summary: The summary.
|
||||||
@ -105,22 +113,22 @@ class CSVRow(BaseCSVRow):
|
|||||||
self.debit, self.credit, self.note]
|
self.debit, self.credit, self.note]
|
||||||
|
|
||||||
|
|
||||||
class PageParams(BasePageParams):
|
class JournalPageParams(PageParams):
|
||||||
"""The HTML page parameters."""
|
"""The HTML parameters of the journal."""
|
||||||
|
|
||||||
def __init__(self, period: Period,
|
def __init__(self, period: Period,
|
||||||
pagination: Pagination[JournalEntry],
|
pagination: Pagination[Entry],
|
||||||
entries: list[JournalEntry]):
|
entries: list[Entry]):
|
||||||
"""Constructs the HTML page parameters.
|
"""Constructs the HTML parameters of the journal.
|
||||||
|
|
||||||
:param period: The period.
|
:param period: The period.
|
||||||
:param entries: The journal entries.
|
:param entries: The journal entries.
|
||||||
"""
|
"""
|
||||||
self.period: Period = period
|
self.period: Period = period
|
||||||
"""The period."""
|
"""The period."""
|
||||||
self.pagination: Pagination[JournalEntry] = pagination
|
self.pagination: Pagination[Entry] = pagination
|
||||||
"""The pagination."""
|
"""The pagination."""
|
||||||
self.entries: list[JournalEntry] = entries
|
self.entries: list[Entry] = entries
|
||||||
"""The entries."""
|
"""The entries."""
|
||||||
self.period_chooser: JournalPeriodChooser \
|
self.period_chooser: JournalPeriodChooser \
|
||||||
= JournalPeriodChooser()
|
= JournalPeriodChooser()
|
||||||
@ -144,21 +152,25 @@ class PageParams(BasePageParams):
|
|||||||
period=self.period)
|
period=self.period)
|
||||||
|
|
||||||
|
|
||||||
def get_csv_rows(entries: list[JournalEntry]) -> list[CSVRow]:
|
def _populate_entries(entries: list[Entry]) -> None:
|
||||||
"""Composes and returns the CSV rows from the report entries.
|
"""Populates the journal entries with relative data.
|
||||||
|
|
||||||
:param entries: The report entries.
|
:param entries: The journal entries.
|
||||||
:return: The CSV rows.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
|
transactions: dict[int, Transaction] \
|
||||||
gettext("Account"), gettext("Summary"),
|
= {x.id: x for x in Transaction.query.filter(
|
||||||
gettext("Debit"), gettext("Credit"),
|
Transaction.id.in_({x.entry.transaction_id for x in entries}))}
|
||||||
gettext("Note"))]
|
accounts: dict[int, Account] \
|
||||||
rows.extend([CSVRow(x.transaction.date, x.currency.code,
|
= {x.id: x for x in Account.query.filter(
|
||||||
str(x.account).title(), x.summary,
|
Account.id.in_({x.entry.account_id for x in entries}))}
|
||||||
x.debit, x.credit, x.transaction.note)
|
currencies: dict[int, Currency] \
|
||||||
for x in entries])
|
= {x.code: x for x in Currency.query.filter(
|
||||||
return rows
|
Currency.code.in_({x.entry.currency_code for x in entries}))}
|
||||||
|
for entry in entries:
|
||||||
|
entry.transaction = transactions[entry.entry.transaction_id]
|
||||||
|
entry.account = accounts[entry.entry.account_id]
|
||||||
|
entry.currency = currencies[entry.entry.currency_code]
|
||||||
|
|
||||||
|
|
||||||
class Journal(BaseReport):
|
class Journal(BaseReport):
|
||||||
@ -169,12 +181,13 @@ class Journal(BaseReport):
|
|||||||
|
|
||||||
:param period: The period.
|
:param period: The period.
|
||||||
"""
|
"""
|
||||||
|
"""The account."""
|
||||||
self.__period: Period = period
|
self.__period: Period = period
|
||||||
"""The period."""
|
"""The period."""
|
||||||
self.__entries: list[JournalEntry] = self.__query_entries()
|
self.__entries: list[Entry] = self.__query_entries()
|
||||||
"""The journal entries."""
|
"""The journal entries."""
|
||||||
|
|
||||||
def __query_entries(self) -> list[JournalEntry]:
|
def __query_entries(self) -> list[Entry]:
|
||||||
"""Queries and returns the journal entries.
|
"""Queries and returns the journal entries.
|
||||||
|
|
||||||
:return: The journal entries.
|
:return: The journal entries.
|
||||||
@ -184,32 +197,47 @@ class Journal(BaseReport):
|
|||||||
conditions.append(Transaction.date >= self.__period.start)
|
conditions.append(Transaction.date >= self.__period.start)
|
||||||
if self.__period.end is not None:
|
if self.__period.end is not None:
|
||||||
conditions.append(Transaction.date <= self.__period.end)
|
conditions.append(Transaction.date <= self.__period.end)
|
||||||
return JournalEntry.query.join(Transaction)\
|
return [Entry(x) for x in db.session
|
||||||
.filter(*conditions)\
|
.query(JournalEntry).join(Transaction).filter(*conditions)
|
||||||
.order_by(Transaction.date,
|
.order_by(Transaction.date,
|
||||||
JournalEntry.is_debit.desc(),
|
JournalEntry.is_debit.desc(),
|
||||||
JournalEntry.no)\
|
JournalEntry.no).all()]
|
||||||
.options(selectinload(JournalEntry.account),
|
|
||||||
selectinload(JournalEntry.currency),
|
|
||||||
selectinload(JournalEntry.transaction)).all()
|
|
||||||
|
|
||||||
def csv(self) -> Response:
|
def csv(self) -> Response:
|
||||||
"""Returns the report as CSV for download.
|
"""Returns the report as CSV for download.
|
||||||
|
|
||||||
:return: The response of the report for download.
|
:return: The response of the report for download.
|
||||||
"""
|
"""
|
||||||
filename: str = f"journal-{period_spec(self.__period)}.csv"
|
filename: str = f"journal-{self.__period.spec}.csv"
|
||||||
return csv_download(filename, get_csv_rows(self.__entries))
|
return csv_download(filename, self.__get_csv_rows())
|
||||||
|
|
||||||
|
def __get_csv_rows(self) -> list[CSVRow]:
|
||||||
|
"""Composes and returns the CSV rows.
|
||||||
|
|
||||||
|
:return: The CSV rows.
|
||||||
|
"""
|
||||||
|
_populate_entries(self.__entries)
|
||||||
|
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
|
||||||
|
gettext("Account"), gettext("Summary"),
|
||||||
|
gettext("Debit"), gettext("Credit"),
|
||||||
|
gettext("Note"))]
|
||||||
|
rows.extend([CSVRow(x.transaction.date, x.currency.code,
|
||||||
|
str(x.account).title(), x.summary,
|
||||||
|
x.debit, x.credit, x.transaction.note)
|
||||||
|
for x in self.__entries])
|
||||||
|
return rows
|
||||||
|
|
||||||
def html(self) -> str:
|
def html(self) -> str:
|
||||||
"""Composes and returns the report as HTML.
|
"""Composes and returns the report as HTML.
|
||||||
|
|
||||||
:return: The report as HTML.
|
:return: The report as HTML.
|
||||||
"""
|
"""
|
||||||
pagination: Pagination[JournalEntry] \
|
pagination: Pagination[Entry] = Pagination[Entry](self.__entries)
|
||||||
= Pagination[JournalEntry](self.__entries)
|
page_entries: list[Entry] = pagination.list
|
||||||
params: PageParams = PageParams(period=self.__period,
|
_populate_entries(page_entries)
|
||||||
pagination=pagination,
|
params: JournalPageParams = JournalPageParams(
|
||||||
entries=pagination.list)
|
period=self.__period,
|
||||||
|
pagination=pagination,
|
||||||
|
entries=page_entries)
|
||||||
return render_template("accounting/report/journal.html",
|
return render_template("accounting/report/journal.html",
|
||||||
report=params)
|
report=params)
|
||||||
|
@ -22,37 +22,41 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import url_for, render_template, Response
|
from flask import url_for, render_template, Response
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from accounting.models import Currency, Account, Transaction, JournalEntry
|
from accounting.models import Currency, Account, Transaction, JournalEntry
|
||||||
from accounting.report.period import Period
|
from accounting.report.period import Period
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
from .utils.base_page_params import BasePageParams
|
|
||||||
from .utils.base_report import BaseReport
|
from .utils.base_report import BaseReport
|
||||||
from .utils.csv_export import BaseCSVRow, csv_download, period_spec
|
from .utils.csv_export import BaseCSVRow, csv_download
|
||||||
from .utils.urls import ledger_url
|
|
||||||
from .utils.option_link import OptionLink
|
from .utils.option_link import OptionLink
|
||||||
|
from .utils.page_params import PageParams
|
||||||
from .utils.period_choosers import LedgerPeriodChooser
|
from .utils.period_choosers import LedgerPeriodChooser
|
||||||
from .utils.report_chooser import ReportChooser
|
from .utils.report_chooser import ReportChooser
|
||||||
from .utils.report_type import ReportType
|
from .utils.report_type import ReportType
|
||||||
|
|
||||||
|
|
||||||
class ReportEntry:
|
class Entry:
|
||||||
"""An entry in the report."""
|
"""An entry in the ledger."""
|
||||||
|
|
||||||
def __init__(self, entry: JournalEntry | None = None):
|
def __init__(self, entry: JournalEntry | None = None):
|
||||||
"""Constructs the entry in the report.
|
"""Constructs the entry in the ledger.
|
||||||
|
|
||||||
:param entry: The journal entry.
|
:param entry: The journal entry.
|
||||||
"""
|
"""
|
||||||
|
self.entry: JournalEntry | None = None
|
||||||
|
"""The journal entry."""
|
||||||
|
self.transaction: Transaction | None = None
|
||||||
|
"""The transaction."""
|
||||||
self.is_brought_forward: bool = False
|
self.is_brought_forward: bool = False
|
||||||
"""Whether this is the brought-forward entry."""
|
"""Whether this is the brought-forward entry."""
|
||||||
self.is_total: bool = False
|
self.is_total: bool = False
|
||||||
"""Whether this is the total entry."""
|
"""Whether this is the total entry."""
|
||||||
self.date: date | None = None
|
self.date: date | None = None
|
||||||
"""The date."""
|
"""The date."""
|
||||||
|
self.account: Account | None = None
|
||||||
|
"""The account."""
|
||||||
self.summary: str | None = None
|
self.summary: str | None = None
|
||||||
"""The summary."""
|
"""The summary."""
|
||||||
self.debit: Decimal | None = None
|
self.debit: Decimal | None = None
|
||||||
@ -63,23 +67,18 @@ class ReportEntry:
|
|||||||
"""The balance."""
|
"""The balance."""
|
||||||
self.note: str | None = None
|
self.note: str | None = None
|
||||||
"""The note."""
|
"""The note."""
|
||||||
self.url: str | None = None
|
|
||||||
"""The URL to the journal entry."""
|
|
||||||
if entry is not None:
|
if entry is not None:
|
||||||
self.date = entry.transaction.date
|
self.entry = entry
|
||||||
self.summary = entry.summary
|
self.summary = entry.summary
|
||||||
self.debit = entry.amount if entry.is_debit else None
|
self.debit = entry.amount if entry.is_debit else None
|
||||||
self.credit = None if entry.is_debit else entry.amount
|
self.credit = None if entry.is_debit else entry.amount
|
||||||
self.note = entry.transaction.note
|
|
||||||
self.url = url_for("accounting.transaction.detail",
|
|
||||||
txn=entry.transaction)
|
|
||||||
|
|
||||||
|
|
||||||
class EntryCollector:
|
class EntryCollector:
|
||||||
"""The report entry collector."""
|
"""The ledger entry collector."""
|
||||||
|
|
||||||
def __init__(self, currency: Currency, account: Account, period: Period):
|
def __init__(self, currency: Currency, account: Account, period: Period):
|
||||||
"""Constructs the report entry collector.
|
"""Constructs the ledger entry collector.
|
||||||
|
|
||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
@ -91,21 +90,21 @@ class EntryCollector:
|
|||||||
"""The account."""
|
"""The account."""
|
||||||
self.__period: Period = period
|
self.__period: Period = period
|
||||||
"""The period"""
|
"""The period"""
|
||||||
self.brought_forward: ReportEntry | None
|
self.brought_forward: Entry | None
|
||||||
"""The brought-forward entry."""
|
"""The brought-forward entry."""
|
||||||
self.entries: list[ReportEntry]
|
self.entries: list[Entry]
|
||||||
"""The report entries."""
|
"""The ledger entries."""
|
||||||
self.total: ReportEntry | None
|
self.total: Entry | None
|
||||||
"""The total entry."""
|
"""The total entry."""
|
||||||
self.brought_forward = self.__get_brought_forward_entry()
|
self.brought_forward = self.__get_brought_forward_entry()
|
||||||
self.entries = self.__query_entries()
|
self.entries = self.__query_entries()
|
||||||
self.total = self.__get_total_entry()
|
self.total = self.__get_total_entry()
|
||||||
self.__populate_balance()
|
self.__populate_balance()
|
||||||
|
|
||||||
def __get_brought_forward_entry(self) -> ReportEntry | None:
|
def __get_brought_forward_entry(self) -> Entry | None:
|
||||||
"""Queries, composes and returns the brought-forward entry.
|
"""Queries, composes and returns the brought-forward entry.
|
||||||
|
|
||||||
:return: The brought-forward entry, or None if the report starts from
|
:return: The brought-forward entry, or None if the ledger starts from
|
||||||
the beginning.
|
the beginning.
|
||||||
"""
|
"""
|
||||||
if self.__period.start is None:
|
if self.__period.start is None:
|
||||||
@ -120,7 +119,7 @@ class EntryCollector:
|
|||||||
balance: int | None = db.session.scalar(select)
|
balance: int | None = db.session.scalar(select)
|
||||||
if balance is None:
|
if balance is None:
|
||||||
return None
|
return None
|
||||||
entry: ReportEntry = ReportEntry()
|
entry: Entry = Entry()
|
||||||
entry.is_brought_forward = True
|
entry.is_brought_forward = True
|
||||||
entry.date = self.__period.start
|
entry.date = self.__period.start
|
||||||
entry.summary = gettext("Brought forward")
|
entry.summary = gettext("Brought forward")
|
||||||
@ -131,10 +130,10 @@ class EntryCollector:
|
|||||||
entry.balance = balance
|
entry.balance = balance
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
def __query_entries(self) -> list[ReportEntry]:
|
def __query_entries(self) -> list[Entry]:
|
||||||
"""Queries and returns the report entries.
|
"""Queries and returns the ledger entries.
|
||||||
|
|
||||||
:return: The report entries.
|
:return: The ledger entries.
|
||||||
"""
|
"""
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.BinaryExpression] \
|
||||||
= [JournalEntry.currency_code == self.__currency.code,
|
= [JournalEntry.currency_code == self.__currency.code,
|
||||||
@ -143,21 +142,20 @@ class EntryCollector:
|
|||||||
conditions.append(Transaction.date >= self.__period.start)
|
conditions.append(Transaction.date >= self.__period.start)
|
||||||
if self.__period.end is not None:
|
if self.__period.end is not None:
|
||||||
conditions.append(Transaction.date <= self.__period.end)
|
conditions.append(Transaction.date <= self.__period.end)
|
||||||
return [ReportEntry(x) for x in JournalEntry.query.join(Transaction)
|
return [Entry(x) for x in JournalEntry.query.join(Transaction)
|
||||||
.filter(*conditions)
|
.filter(*conditions)
|
||||||
.order_by(Transaction.date,
|
.order_by(Transaction.date,
|
||||||
JournalEntry.is_debit.desc(),
|
JournalEntry.is_debit.desc(),
|
||||||
JournalEntry.no)
|
JournalEntry.no).all()]
|
||||||
.options(selectinload(JournalEntry.transaction)).all()]
|
|
||||||
|
|
||||||
def __get_total_entry(self) -> ReportEntry | None:
|
def __get_total_entry(self) -> Entry | None:
|
||||||
"""Composes the total entry.
|
"""Composes the total entry.
|
||||||
|
|
||||||
:return: The total entry, or None if there is no data.
|
:return: The total entry, or None if there is no data.
|
||||||
"""
|
"""
|
||||||
if self.brought_forward is None and len(self.entries) == 0:
|
if self.brought_forward is None and len(self.entries) == 0:
|
||||||
return None
|
return None
|
||||||
entry: ReportEntry = ReportEntry()
|
entry: Entry = Entry()
|
||||||
entry.is_total = True
|
entry.is_total = True
|
||||||
entry.summary = gettext("Total")
|
entry.summary = gettext("Total")
|
||||||
entry.debit = sum([x.debit for x in self.entries
|
entry.debit = sum([x.debit for x in self.entries
|
||||||
@ -185,7 +183,7 @@ class EntryCollector:
|
|||||||
|
|
||||||
|
|
||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""A row in the CSV ledger."""
|
||||||
|
|
||||||
def __init__(self, txn_date: date | str | None,
|
def __init__(self, txn_date: date | str | None,
|
||||||
summary: str | None,
|
summary: str | None,
|
||||||
@ -193,7 +191,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
credit: str | Decimal | None,
|
credit: str | Decimal | None,
|
||||||
balance: str | Decimal | None,
|
balance: str | Decimal | None,
|
||||||
note: str | None):
|
note: str | None):
|
||||||
"""Constructs a row in the CSV.
|
"""Constructs a row in the CSV ledger.
|
||||||
|
|
||||||
:param txn_date: The transaction date.
|
:param txn_date: The transaction date.
|
||||||
:param summary: The summary.
|
:param summary: The summary.
|
||||||
@ -225,25 +223,25 @@ class CSVRow(BaseCSVRow):
|
|||||||
self.debit, self.credit, self.balance, self.note]
|
self.debit, self.credit, self.balance, self.note]
|
||||||
|
|
||||||
|
|
||||||
class PageParams(BasePageParams):
|
class LedgerPageParams(PageParams):
|
||||||
"""The HTML page parameters."""
|
"""The HTML parameters of the ledger."""
|
||||||
|
|
||||||
def __init__(self, currency: Currency,
|
def __init__(self, currency: Currency,
|
||||||
account: Account,
|
account: Account,
|
||||||
period: Period,
|
period: Period,
|
||||||
has_data: bool,
|
has_data: bool,
|
||||||
pagination: Pagination[ReportEntry],
|
pagination: Pagination[Entry],
|
||||||
brought_forward: ReportEntry | None,
|
brought_forward: Entry | None,
|
||||||
entries: list[ReportEntry],
|
entries: list[Entry],
|
||||||
total: ReportEntry | None):
|
total: Entry | None):
|
||||||
"""Constructs the HTML page parameters.
|
"""Constructs the HTML parameters of the ledger.
|
||||||
|
|
||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
:param period: The period.
|
:param period: The period.
|
||||||
:param has_data: True if there is any data, or False otherwise.
|
:param has_data: True if there is any data, or False otherwise.
|
||||||
:param brought_forward: The brought-forward entry.
|
:param brought_forward: The brought-forward entry.
|
||||||
:param entries: The report entries.
|
:param entries: The ledger entries.
|
||||||
:param total: The total entry.
|
:param total: The total entry.
|
||||||
"""
|
"""
|
||||||
self.currency: Currency = currency
|
self.currency: Currency = currency
|
||||||
@ -254,13 +252,13 @@ class PageParams(BasePageParams):
|
|||||||
"""The period."""
|
"""The period."""
|
||||||
self.__has_data: bool = has_data
|
self.__has_data: bool = has_data
|
||||||
"""True if there is any data, or False otherwise."""
|
"""True if there is any data, or False otherwise."""
|
||||||
self.pagination: Pagination[ReportEntry] = pagination
|
self.pagination: Pagination[Entry] = pagination
|
||||||
"""The pagination."""
|
"""The pagination."""
|
||||||
self.brought_forward: ReportEntry | None = brought_forward
|
self.brought_forward: Entry | None = brought_forward
|
||||||
"""The brought-forward entry."""
|
"""The brought-forward entry."""
|
||||||
self.entries: list[ReportEntry] = entries
|
self.entries: list[Entry] = entries
|
||||||
"""The entries."""
|
"""The entries."""
|
||||||
self.total: ReportEntry | None = total
|
self.total: Entry | None = total
|
||||||
"""The total entry."""
|
"""The total entry."""
|
||||||
self.period_chooser: LedgerPeriodChooser \
|
self.period_chooser: LedgerPeriodChooser \
|
||||||
= LedgerPeriodChooser(currency, account)
|
= LedgerPeriodChooser(currency, account)
|
||||||
@ -291,8 +289,20 @@ class PageParams(BasePageParams):
|
|||||||
|
|
||||||
:return: The currency options.
|
:return: The currency options.
|
||||||
"""
|
"""
|
||||||
return self._get_currency_options(
|
def get_url(currency: Currency):
|
||||||
lambda x: ledger_url(x, self.account, self.period), self.currency)
|
if self.period.is_default:
|
||||||
|
return url_for("accounting.report.ledger-default",
|
||||||
|
currency=currency, account=self.account)
|
||||||
|
return url_for("accounting.report.ledger",
|
||||||
|
currency=currency, account=self.account,
|
||||||
|
period=self.period)
|
||||||
|
|
||||||
|
in_use: set[str] = set(db.session.scalars(
|
||||||
|
sa.select(JournalEntry.currency_code)
|
||||||
|
.group_by(JournalEntry.currency_code)).all())
|
||||||
|
return [OptionLink(str(x), get_url(x), x.code == self.currency.code)
|
||||||
|
for x in Currency.query.filter(Currency.code.in_(in_use))
|
||||||
|
.order_by(Currency.code).all()]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def account_options(self) -> list[OptionLink]:
|
def account_options(self) -> list[OptionLink]:
|
||||||
@ -300,15 +310,39 @@ class PageParams(BasePageParams):
|
|||||||
|
|
||||||
:return: The account options.
|
:return: The account options.
|
||||||
"""
|
"""
|
||||||
|
def get_url(account: Account):
|
||||||
|
if self.period.is_default:
|
||||||
|
return url_for("accounting.report.ledger-default",
|
||||||
|
currency=self.currency, account=account)
|
||||||
|
return url_for("accounting.report.ledger",
|
||||||
|
currency=self.currency, account=account,
|
||||||
|
period=self.period)
|
||||||
|
|
||||||
in_use: sa.Select = sa.Select(JournalEntry.account_id)\
|
in_use: sa.Select = sa.Select(JournalEntry.account_id)\
|
||||||
.filter(JournalEntry.currency_code == self.currency.code)\
|
.filter(JournalEntry.currency_code == self.currency.code)\
|
||||||
.group_by(JournalEntry.account_id)
|
.group_by(JournalEntry.account_id)
|
||||||
return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
|
return [OptionLink(str(x), get_url(x), x.id == self.account.id)
|
||||||
x.id == self.account.id)
|
|
||||||
for x in Account.query.filter(Account.id.in_(in_use))
|
for x in Account.query.filter(Account.id.in_(in_use))
|
||||||
.order_by(Account.base_code, Account.no).all()]
|
.order_by(Account.base_code, Account.no).all()]
|
||||||
|
|
||||||
|
|
||||||
|
def _populate_entries(entries: list[Entry]) -> None:
|
||||||
|
"""Populates the ledger entries with relative data.
|
||||||
|
|
||||||
|
:param entries: The ledger entries.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
transactions: dict[int, Transaction] \
|
||||||
|
= {x.id: x for x in Transaction.query.filter(
|
||||||
|
Transaction.id.in_({x.entry.transaction_id for x in entries
|
||||||
|
if x.entry is not None}))}
|
||||||
|
for entry in entries:
|
||||||
|
if entry.entry is not None:
|
||||||
|
entry.transaction = transactions[entry.entry.transaction_id]
|
||||||
|
entry.date = entry.transaction.date
|
||||||
|
entry.note = entry.transaction.note
|
||||||
|
|
||||||
|
|
||||||
class Ledger(BaseReport):
|
class Ledger(BaseReport):
|
||||||
"""The ledger."""
|
"""The ledger."""
|
||||||
|
|
||||||
@ -327,11 +361,11 @@ class Ledger(BaseReport):
|
|||||||
"""The period."""
|
"""The period."""
|
||||||
collector: EntryCollector = EntryCollector(
|
collector: EntryCollector = EntryCollector(
|
||||||
self.__currency, self.__account, self.__period)
|
self.__currency, self.__account, self.__period)
|
||||||
self.__brought_forward: ReportEntry | None = collector.brought_forward
|
self.__brought_forward: Entry | None = collector.brought_forward
|
||||||
"""The brought-forward entry."""
|
"""The brought-forward entry."""
|
||||||
self.__entries: list[ReportEntry] = collector.entries
|
self.__entries: list[Entry] = collector.entries
|
||||||
"""The report entries."""
|
"""The ledger entries."""
|
||||||
self.__total: ReportEntry | None = collector.total
|
self.__total: Entry | None = collector.total
|
||||||
"""The total entry."""
|
"""The total entry."""
|
||||||
|
|
||||||
def csv(self) -> Response:
|
def csv(self) -> Response:
|
||||||
@ -341,7 +375,7 @@ class Ledger(BaseReport):
|
|||||||
"""
|
"""
|
||||||
filename: str = "ledger-{currency}-{account}-{period}.csv"\
|
filename: str = "ledger-{currency}-{account}-{period}.csv"\
|
||||||
.format(currency=self.__currency.code, account=self.__account.code,
|
.format(currency=self.__currency.code, account=self.__account.code,
|
||||||
period=period_spec(self.__period))
|
period=self.__period.spec)
|
||||||
return csv_download(filename, self.__get_csv_rows())
|
return csv_download(filename, self.__get_csv_rows())
|
||||||
|
|
||||||
def __get_csv_rows(self) -> list[CSVRow]:
|
def __get_csv_rows(self) -> list[CSVRow]:
|
||||||
@ -349,6 +383,7 @@ class Ledger(BaseReport):
|
|||||||
|
|
||||||
:return: The CSV rows.
|
:return: The CSV rows.
|
||||||
"""
|
"""
|
||||||
|
_populate_entries(self.__entries)
|
||||||
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Summary"),
|
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Summary"),
|
||||||
gettext("Debit"), gettext("Credit"),
|
gettext("Debit"), gettext("Credit"),
|
||||||
gettext("Balance"), gettext("Note"))]
|
gettext("Balance"), gettext("Note"))]
|
||||||
@ -373,31 +408,32 @@ class Ledger(BaseReport):
|
|||||||
|
|
||||||
:return: The report as HTML.
|
:return: The report as HTML.
|
||||||
"""
|
"""
|
||||||
all_entries: list[ReportEntry] = []
|
all_entries: list[Entry] = []
|
||||||
if self.__brought_forward is not None:
|
if self.__brought_forward is not None:
|
||||||
all_entries.append(self.__brought_forward)
|
all_entries.append(self.__brought_forward)
|
||||||
all_entries.extend(self.__entries)
|
all_entries.extend(self.__entries)
|
||||||
if self.__total is not None:
|
if self.__total is not None:
|
||||||
all_entries.append(self.__total)
|
all_entries.append(self.__total)
|
||||||
pagination: Pagination[ReportEntry] \
|
pagination: Pagination[Entry] = Pagination[Entry](all_entries)
|
||||||
= Pagination[ReportEntry](all_entries)
|
page_entries: list[Entry] = pagination.list
|
||||||
page_entries: list[ReportEntry] = pagination.list
|
|
||||||
has_data: bool = len(page_entries) > 0
|
has_data: bool = len(page_entries) > 0
|
||||||
brought_forward: ReportEntry | None = None
|
_populate_entries(page_entries)
|
||||||
|
brought_forward: Entry | None = None
|
||||||
if len(page_entries) > 0 and page_entries[0].is_brought_forward:
|
if len(page_entries) > 0 and page_entries[0].is_brought_forward:
|
||||||
brought_forward = page_entries[0]
|
brought_forward = page_entries[0]
|
||||||
page_entries = page_entries[1:]
|
page_entries = page_entries[1:]
|
||||||
total: ReportEntry | None = None
|
total: Entry | None = None
|
||||||
if len(page_entries) > 0 and page_entries[-1].is_total:
|
if len(page_entries) > 0 and page_entries[-1].is_total:
|
||||||
total = page_entries[-1]
|
total = page_entries[-1]
|
||||||
page_entries = page_entries[:-1]
|
page_entries = page_entries[:-1]
|
||||||
params: PageParams = PageParams(currency=self.__currency,
|
params: LedgerPageParams = LedgerPageParams(
|
||||||
account=self.__account,
|
currency=self.__currency,
|
||||||
period=self.__period,
|
account=self.__account,
|
||||||
has_data=has_data,
|
period=self.__period,
|
||||||
pagination=pagination,
|
has_data=has_data,
|
||||||
brought_forward=brought_forward,
|
pagination=pagination,
|
||||||
entries=page_entries,
|
brought_forward=brought_forward,
|
||||||
total=total)
|
entries=page_entries,
|
||||||
|
total=total)
|
||||||
return render_template("accounting/report/ledger.html",
|
return render_template("accounting/report/ledger.html",
|
||||||
report=params)
|
report=params)
|
||||||
|
@ -17,35 +17,163 @@
|
|||||||
"""The search.
|
"""The search.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import date, datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import Response, render_template, request
|
from flask import Response, render_template, request
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
|
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
|
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
|
||||||
Transaction, JournalEntry
|
Transaction, JournalEntry
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
from accounting.utils.query import parse_query_keywords
|
from accounting.utils.query import parse_query_keywords
|
||||||
from .journal import get_csv_rows
|
|
||||||
from .utils.base_page_params import BasePageParams
|
|
||||||
from .utils.base_report import BaseReport
|
from .utils.base_report import BaseReport
|
||||||
from .utils.csv_export import csv_download
|
from .utils.csv_export import BaseCSVRow, csv_download
|
||||||
|
from .utils.page_params import PageParams
|
||||||
from .utils.report_chooser import ReportChooser
|
from .utils.report_chooser import ReportChooser
|
||||||
from .utils.report_type import ReportType
|
from .utils.report_type import ReportType
|
||||||
|
|
||||||
|
|
||||||
class EntryCollector:
|
class Entry:
|
||||||
"""The report entry collector."""
|
"""An entry in the search result."""
|
||||||
|
|
||||||
|
def __init__(self, entry: JournalEntry | None = None):
|
||||||
|
"""Constructs the entry in the search result.
|
||||||
|
|
||||||
|
:param entry: The journal entry.
|
||||||
|
"""
|
||||||
|
self.entry: JournalEntry | None = None
|
||||||
|
"""The journal entry."""
|
||||||
|
self.transaction: Transaction | None = None
|
||||||
|
"""The transaction."""
|
||||||
|
self.is_total: bool = False
|
||||||
|
"""Whether this is the total entry."""
|
||||||
|
self.currency: Currency | None = None
|
||||||
|
"""The account."""
|
||||||
|
self.account: Account | None = None
|
||||||
|
"""The account."""
|
||||||
|
self.summary: str | None = None
|
||||||
|
"""The summary."""
|
||||||
|
self.debit: Decimal | None = None
|
||||||
|
"""The debit amount."""
|
||||||
|
self.credit: Decimal | None = None
|
||||||
|
"""The credit amount."""
|
||||||
|
self.amount: Decimal | None = None
|
||||||
|
"""The amount."""
|
||||||
|
if entry is not None:
|
||||||
|
self.entry = entry
|
||||||
|
self.summary = entry.summary
|
||||||
|
self.debit = entry.amount if entry.is_debit else None
|
||||||
|
self.credit = None if entry.is_debit else entry.amount
|
||||||
|
self.amount = entry.amount
|
||||||
|
|
||||||
|
|
||||||
|
class CSVRow(BaseCSVRow):
|
||||||
|
"""A row in the CSV search result."""
|
||||||
|
|
||||||
|
def __init__(self, txn_date: str | date,
|
||||||
|
currency: str,
|
||||||
|
account: str,
|
||||||
|
summary: str | None,
|
||||||
|
debit: str | Decimal | None,
|
||||||
|
credit: str | Decimal | None,
|
||||||
|
note: str | None):
|
||||||
|
"""Constructs a row in the CSV search result.
|
||||||
|
|
||||||
|
:param txn_date: The transaction date.
|
||||||
|
:param summary: The summary.
|
||||||
|
:param debit: The debit amount.
|
||||||
|
:param credit: The credit amount.
|
||||||
|
:param note: The note.
|
||||||
|
"""
|
||||||
|
self.date: str | date = txn_date
|
||||||
|
"""The date."""
|
||||||
|
self.currency: str = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.account: str = account
|
||||||
|
"""The account."""
|
||||||
|
self.summary: str | None = summary
|
||||||
|
"""The summary."""
|
||||||
|
self.debit: str | Decimal | None = debit
|
||||||
|
"""The debit amount."""
|
||||||
|
self.credit: str | Decimal | None = credit
|
||||||
|
"""The credit amount."""
|
||||||
|
self.note: str | None = note
|
||||||
|
"""The note."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def values(self) -> list[str | Decimal | None]:
|
||||||
|
"""Returns the values of the row.
|
||||||
|
|
||||||
|
:return: The values of the row.
|
||||||
|
"""
|
||||||
|
return [self.date, self.currency, self.account, self.summary,
|
||||||
|
self.debit, self.credit, self.note]
|
||||||
|
|
||||||
|
|
||||||
|
class SearchPageParams(PageParams):
|
||||||
|
"""The HTML parameters of the search result."""
|
||||||
|
|
||||||
|
def __init__(self, pagination: Pagination[Entry],
|
||||||
|
entries: list[Entry]):
|
||||||
|
"""Constructs the HTML parameters of the search result.
|
||||||
|
|
||||||
|
:param entries: The search result entries.
|
||||||
|
"""
|
||||||
|
self.pagination: Pagination[Entry] = pagination
|
||||||
|
"""The pagination."""
|
||||||
|
self.entries: list[Entry] = entries
|
||||||
|
"""The entries."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_data(self) -> bool:
|
||||||
|
"""Returns whether there is any data on the page.
|
||||||
|
|
||||||
|
:return: True if there is any data, or False otherwise.
|
||||||
|
"""
|
||||||
|
return len(self.entries) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def report_chooser(self) -> ReportChooser:
|
||||||
|
"""Returns the report chooser.
|
||||||
|
|
||||||
|
:return: The report chooser.
|
||||||
|
"""
|
||||||
|
return ReportChooser(ReportType.SEARCH)
|
||||||
|
|
||||||
|
|
||||||
|
def _populate_entries(entries: list[Entry]) -> None:
|
||||||
|
"""Populates the search result entries with relative data.
|
||||||
|
|
||||||
|
:param entries: The search result entries.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
transactions: dict[int, Transaction] \
|
||||||
|
= {x.id: x for x in Transaction.query.filter(
|
||||||
|
Transaction.id.in_({x.entry.transaction_id for x in entries}))}
|
||||||
|
accounts: dict[int, Account] \
|
||||||
|
= {x.id: x for x in Account.query.filter(
|
||||||
|
Account.id.in_({x.entry.account_id for x in entries}))}
|
||||||
|
currencies: dict[int, Currency] \
|
||||||
|
= {x.code: x for x in Currency.query.filter(
|
||||||
|
Currency.code.in_({x.entry.currency_code for x in entries}))}
|
||||||
|
for entry in entries:
|
||||||
|
entry.transaction = transactions[entry.entry.transaction_id]
|
||||||
|
entry.account = accounts[entry.entry.account_id]
|
||||||
|
entry.currency = currencies[entry.entry.currency_code]
|
||||||
|
|
||||||
|
|
||||||
|
class Search(BaseReport):
|
||||||
|
"""The search."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Constructs the report entry collector."""
|
"""Constructs a search."""
|
||||||
self.entries: list[JournalEntry] = self.__query_entries()
|
"""The account."""
|
||||||
"""The report entries."""
|
self.__entries: list[Entry] = self.__query_entries()
|
||||||
|
"""The journal entries."""
|
||||||
|
|
||||||
def __query_entries(self) -> list[JournalEntry]:
|
def __query_entries(self) -> list[Entry]:
|
||||||
"""Queries and returns the journal entries.
|
"""Queries and returns the journal entries.
|
||||||
|
|
||||||
:return: The journal entries.
|
:return: The journal entries.
|
||||||
@ -55,23 +183,15 @@ class EntryCollector:
|
|||||||
return []
|
return []
|
||||||
conditions: list[sa.BinaryExpression] = []
|
conditions: list[sa.BinaryExpression] = []
|
||||||
for k in keywords:
|
for k in keywords:
|
||||||
sub_conditions: list[sa.BinaryExpression] \
|
conditions.append(sa.or_(
|
||||||
= [JournalEntry.summary.contains(k),
|
JournalEntry.summary.contains(k),
|
||||||
JournalEntry.account_id.in_(
|
sa.cast(JournalEntry.amount, sa.String).contains(k),
|
||||||
self.__get_account_condition(k)),
|
JournalEntry.account_id.in_(self.__get_account_condition(k)),
|
||||||
JournalEntry.currency_code.in_(
|
JournalEntry.currency_code.in_(
|
||||||
self.__get_currency_condition(k)),
|
self.__get_currency_condition(k)),
|
||||||
JournalEntry.transaction_id.in_(
|
JournalEntry.transaction_id.in_(
|
||||||
self.__get_transaction_condition(k))]
|
self.__get_transaction_condition(k))))
|
||||||
try:
|
return [Entry(x) for x in JournalEntry.query.filter(*conditions)]
|
||||||
sub_conditions.append(JournalEntry.amount == Decimal(k))
|
|
||||||
except ArithmeticError:
|
|
||||||
pass
|
|
||||||
conditions.append(sa.or_(*sub_conditions))
|
|
||||||
return JournalEntry.query.filter(*conditions)\
|
|
||||||
.options(selectinload(JournalEntry.account),
|
|
||||||
selectinload(JournalEntry.currency),
|
|
||||||
selectinload(JournalEntry.transaction)).all()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_account_condition(k: str) -> sa.Select:
|
def __get_account_condition(k: str) -> sa.Select:
|
||||||
@ -116,7 +236,8 @@ class EntryCollector:
|
|||||||
:param k: The keyword.
|
:param k: The keyword.
|
||||||
:return: The condition to filter the transaction.
|
:return: The condition to filter the transaction.
|
||||||
"""
|
"""
|
||||||
conditions: list[sa.BinaryExpression] = [Transaction.note.contains(k)]
|
conditions: list[sa.BinaryExpression] \
|
||||||
|
= [Transaction.note.contains(k)]
|
||||||
txn_date: datetime
|
txn_date: datetime
|
||||||
try:
|
try:
|
||||||
txn_date = datetime.strptime(k, "%Y")
|
txn_date = datetime.strptime(k, "%Y")
|
||||||
@ -140,62 +261,39 @@ class EntryCollector:
|
|||||||
pass
|
pass
|
||||||
return sa.select(Transaction.id).filter(sa.or_(*conditions))
|
return sa.select(Transaction.id).filter(sa.or_(*conditions))
|
||||||
|
|
||||||
|
|
||||||
class PageParams(BasePageParams):
|
|
||||||
"""The HTML page parameters."""
|
|
||||||
|
|
||||||
def __init__(self, pagination: Pagination[JournalEntry],
|
|
||||||
entries: list[JournalEntry]):
|
|
||||||
"""Constructs the HTML page parameters.
|
|
||||||
|
|
||||||
:param entries: The search result entries.
|
|
||||||
"""
|
|
||||||
self.pagination: Pagination[JournalEntry] = pagination
|
|
||||||
"""The pagination."""
|
|
||||||
self.entries: list[JournalEntry] = entries
|
|
||||||
"""The entries."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_data(self) -> bool:
|
|
||||||
"""Returns whether there is any data on the page.
|
|
||||||
|
|
||||||
:return: True if there is any data, or False otherwise.
|
|
||||||
"""
|
|
||||||
return len(self.entries) > 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def report_chooser(self) -> ReportChooser:
|
|
||||||
"""Returns the report chooser.
|
|
||||||
|
|
||||||
:return: The report chooser.
|
|
||||||
"""
|
|
||||||
return ReportChooser(ReportType.SEARCH)
|
|
||||||
|
|
||||||
|
|
||||||
class Search(BaseReport):
|
|
||||||
"""The search."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Constructs a search."""
|
|
||||||
self.__entries: list[JournalEntry] = EntryCollector().entries
|
|
||||||
"""The journal entries."""
|
|
||||||
|
|
||||||
def csv(self) -> Response:
|
def csv(self) -> Response:
|
||||||
"""Returns the report as CSV for download.
|
"""Returns the report as CSV for download.
|
||||||
|
|
||||||
:return: The response of the report for download.
|
:return: The response of the report for download.
|
||||||
"""
|
"""
|
||||||
filename: str = "search-{q}.csv".format(q=request.args["q"])
|
filename: str = "search-{q}.csv".format(q=request.args["q"])
|
||||||
return csv_download(filename, get_csv_rows(self.__entries))
|
return csv_download(filename, self.__get_csv_rows())
|
||||||
|
|
||||||
|
def __get_csv_rows(self) -> list[CSVRow]:
|
||||||
|
"""Composes and returns the CSV rows.
|
||||||
|
|
||||||
|
:return: The CSV rows.
|
||||||
|
"""
|
||||||
|
_populate_entries(self.__entries)
|
||||||
|
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
|
||||||
|
gettext("Account"), gettext("Summary"),
|
||||||
|
gettext("Debit"), gettext("Credit"),
|
||||||
|
gettext("Note"))]
|
||||||
|
rows.extend([CSVRow(x.transaction.date, x.currency.code,
|
||||||
|
str(x.account).title(), x.summary,
|
||||||
|
x.debit, x.credit, x.transaction.note)
|
||||||
|
for x in self.__entries])
|
||||||
|
return rows
|
||||||
|
|
||||||
def html(self) -> str:
|
def html(self) -> str:
|
||||||
"""Composes and returns the report as HTML.
|
"""Composes and returns the report as HTML.
|
||||||
|
|
||||||
:return: The report as HTML.
|
:return: The report as HTML.
|
||||||
"""
|
"""
|
||||||
pagination: Pagination[JournalEntry] \
|
pagination: Pagination[Entry] = Pagination[Entry](self.__entries)
|
||||||
= Pagination[JournalEntry](self.__entries)
|
page_entries: list[Entry] = pagination.list
|
||||||
params: PageParams = PageParams(pagination=pagination,
|
_populate_entries(page_entries)
|
||||||
entries=pagination.list)
|
params: SearchPageParams = SearchPageParams(pagination=pagination,
|
||||||
|
entries=page_entries)
|
||||||
return render_template("accounting/report/search.html",
|
return render_template("accounting/report/search.html",
|
||||||
report=params)
|
report=params)
|
||||||
|
@ -20,27 +20,26 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import Response, render_template
|
from flask import url_for, Response, render_template
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from accounting.models import Currency, Account, Transaction, JournalEntry
|
from accounting.models import Currency, Account, Transaction, JournalEntry
|
||||||
from accounting.report.period import Period
|
from accounting.report.period import Period
|
||||||
from .utils.base_page_params import BasePageParams
|
|
||||||
from .utils.base_report import BaseReport
|
from .utils.base_report import BaseReport
|
||||||
from .utils.csv_export import BaseCSVRow, csv_download, period_spec
|
from .utils.csv_export import BaseCSVRow, csv_download
|
||||||
from .utils.urls import ledger_url, trial_balance_url
|
|
||||||
from .utils.option_link import OptionLink
|
from .utils.option_link import OptionLink
|
||||||
|
from .utils.page_params import PageParams
|
||||||
from .utils.period_choosers import TrialBalancePeriodChooser
|
from .utils.period_choosers import TrialBalancePeriodChooser
|
||||||
from .utils.report_chooser import ReportChooser
|
from .utils.report_chooser import ReportChooser
|
||||||
from .utils.report_type import ReportType
|
from .utils.report_type import ReportType
|
||||||
|
|
||||||
|
|
||||||
class ReportAccount:
|
class TrialBalanceAccount:
|
||||||
"""An account in the report."""
|
"""An account in the trial balance."""
|
||||||
|
|
||||||
def __init__(self, account: Account, amount: Decimal, url: str):
|
def __init__(self, account: Account, amount: Decimal, url: str):
|
||||||
"""Constructs an account in the report.
|
"""Constructs an account in the trial balance.
|
||||||
|
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
:param amount: The amount.
|
:param amount: The amount.
|
||||||
@ -56,8 +55,8 @@ class ReportAccount:
|
|||||||
"""The URL to the ledger of the account."""
|
"""The URL to the ledger of the account."""
|
||||||
|
|
||||||
|
|
||||||
class Total:
|
class TrialBalanceTotal:
|
||||||
"""The totals."""
|
"""The total in the trial balance."""
|
||||||
|
|
||||||
def __init__(self, debit: Decimal, credit: Decimal):
|
def __init__(self, debit: Decimal, credit: Decimal):
|
||||||
"""Constructs the total in the trial balance.
|
"""Constructs the total in the trial balance.
|
||||||
@ -72,12 +71,12 @@ class Total:
|
|||||||
|
|
||||||
|
|
||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""A row in the CSV trial balance."""
|
||||||
|
|
||||||
def __init__(self, text: str | None,
|
def __init__(self, text: str | None,
|
||||||
debit: str | Decimal | None,
|
debit: str | Decimal | None,
|
||||||
credit: str | Decimal | None):
|
credit: str | Decimal | None):
|
||||||
"""Constructs a row in the CSV.
|
"""Constructs a row in the CSV trial balance.
|
||||||
|
|
||||||
:param text: The text.
|
:param text: The text.
|
||||||
:param debit: The debit amount.
|
:param debit: The debit amount.
|
||||||
@ -99,14 +98,14 @@ class CSVRow(BaseCSVRow):
|
|||||||
return [self.text, self.debit, self.credit]
|
return [self.text, self.debit, self.credit]
|
||||||
|
|
||||||
|
|
||||||
class PageParams(BasePageParams):
|
class TrialBalancePageParams(PageParams):
|
||||||
"""The HTML page parameters."""
|
"""The HTML parameters of the trial balance."""
|
||||||
|
|
||||||
def __init__(self, currency: Currency,
|
def __init__(self, currency: Currency,
|
||||||
period: Period,
|
period: Period,
|
||||||
accounts: list[ReportAccount],
|
accounts: list[TrialBalanceAccount],
|
||||||
total: Total):
|
total: TrialBalanceTotal):
|
||||||
"""Constructs the HTML page parameters.
|
"""Constructs the HTML parameters of the trial balance.
|
||||||
|
|
||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
:param period: The period.
|
:param period: The period.
|
||||||
@ -117,9 +116,9 @@ class PageParams(BasePageParams):
|
|||||||
"""The currency."""
|
"""The currency."""
|
||||||
self.period: Period = period
|
self.period: Period = period
|
||||||
"""The period."""
|
"""The period."""
|
||||||
self.accounts: list[ReportAccount] = accounts
|
self.accounts: list[TrialBalanceAccount] = accounts
|
||||||
"""The accounts in the trial balance."""
|
"""The accounts in the trial balance."""
|
||||||
self.total: Total = total
|
self.total: TrialBalanceTotal = total
|
||||||
"""The total of the trial balance."""
|
"""The total of the trial balance."""
|
||||||
self.period_chooser: TrialBalancePeriodChooser \
|
self.period_chooser: TrialBalancePeriodChooser \
|
||||||
= TrialBalancePeriodChooser(currency)
|
= TrialBalancePeriodChooser(currency)
|
||||||
@ -149,8 +148,19 @@ class PageParams(BasePageParams):
|
|||||||
|
|
||||||
:return: The currency options.
|
:return: The currency options.
|
||||||
"""
|
"""
|
||||||
return self._get_currency_options(
|
def get_url(currency: Currency):
|
||||||
lambda x: trial_balance_url(x, self.period), self.currency)
|
if self.period.is_default:
|
||||||
|
return url_for("accounting.report.trial-balance-default",
|
||||||
|
currency=currency)
|
||||||
|
return url_for("accounting.report.trial-balance",
|
||||||
|
currency=currency, period=self.period)
|
||||||
|
|
||||||
|
in_use: set[str] = set(db.session.scalars(
|
||||||
|
sa.select(JournalEntry.currency_code)
|
||||||
|
.group_by(JournalEntry.currency_code)).all())
|
||||||
|
return [OptionLink(str(x), get_url(x), x.code == self.currency.code)
|
||||||
|
for x in Currency.query.filter(Currency.code.in_(in_use))
|
||||||
|
.order_by(Currency.code).all()]
|
||||||
|
|
||||||
|
|
||||||
class TrialBalance(BaseReport):
|
class TrialBalance(BaseReport):
|
||||||
@ -166,9 +176,9 @@ class TrialBalance(BaseReport):
|
|||||||
"""The currency."""
|
"""The currency."""
|
||||||
self.__period: Period = period
|
self.__period: Period = period
|
||||||
"""The period."""
|
"""The period."""
|
||||||
self.__accounts: list[ReportAccount]
|
self.__accounts: list[TrialBalanceAccount]
|
||||||
"""The accounts in the trial balance."""
|
"""The accounts in the trial balance."""
|
||||||
self.__total: Total
|
self.__total: TrialBalanceTotal
|
||||||
"""The total of the trial balance."""
|
"""The total of the trial balance."""
|
||||||
self.__set_data()
|
self.__set_data()
|
||||||
|
|
||||||
@ -186,22 +196,35 @@ class TrialBalance(BaseReport):
|
|||||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||||
(JournalEntry.is_debit, JournalEntry.amount),
|
(JournalEntry.is_debit, JournalEntry.amount),
|
||||||
else_=-JournalEntry.amount)).label("balance")
|
else_=-JournalEntry.amount)).label("balance")
|
||||||
select_balances: sa.Select = sa.select(Account.id, balance_func)\
|
select_balances: sa.Select \
|
||||||
|
= sa.select(Account.id, balance_func)\
|
||||||
.join(Transaction).join(Account)\
|
.join(Transaction).join(Account)\
|
||||||
.filter(*conditions)\
|
.filter(*conditions)\
|
||||||
.group_by(Account.id)\
|
.group_by(JournalEntry.account_id)\
|
||||||
.order_by(Account.base_code, Account.no)
|
.order_by(Account.base_code, Account.no)
|
||||||
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
||||||
accounts: dict[int, Account] \
|
accounts: dict[int, Account] \
|
||||||
= {x.id: x for x in Account.query
|
= {x.id: x for x in Account.query
|
||||||
.filter(Account.id.in_([x.id for x in balances])).all()}
|
.filter(Account.id.in_([x.id for x in balances])).all()}
|
||||||
self.__accounts = [ReportAccount(account=accounts[x.id],
|
|
||||||
amount=x.balance,
|
def get_url(account: Account) -> str:
|
||||||
url=ledger_url(self.__currency,
|
"""Returns the ledger URL of an account.
|
||||||
accounts[x.id],
|
|
||||||
self.__period))
|
:param account: The account.
|
||||||
|
:return: The ledger URL of the account.
|
||||||
|
"""
|
||||||
|
if self.__period.is_default:
|
||||||
|
return url_for("accounting.report.ledger-default",
|
||||||
|
currency=self.__currency, account=account)
|
||||||
|
return url_for("accounting.report.ledger",
|
||||||
|
currency=self.__currency, account=account,
|
||||||
|
period=self.__period)
|
||||||
|
|
||||||
|
self.__accounts = [TrialBalanceAccount(account=accounts[x.id],
|
||||||
|
amount=x.balance,
|
||||||
|
url=get_url(accounts[x.id]))
|
||||||
for x in balances]
|
for x in balances]
|
||||||
self.__total = Total(
|
self.__total = TrialBalanceTotal(
|
||||||
sum([x.debit for x in self.__accounts if x.debit is not None]),
|
sum([x.debit for x in self.__accounts if x.debit is not None]),
|
||||||
sum([x.credit for x in self.__accounts if x.credit is not None]))
|
sum([x.credit for x in self.__accounts if x.credit is not None]))
|
||||||
|
|
||||||
@ -211,8 +234,7 @@ class TrialBalance(BaseReport):
|
|||||||
:return: The response of the report for download.
|
:return: The response of the report for download.
|
||||||
"""
|
"""
|
||||||
filename: str = "trial-balance-{currency}-{period}.csv"\
|
filename: str = "trial-balance-{currency}-{period}.csv"\
|
||||||
.format(currency=self.__currency.code,
|
.format(currency=self.__currency.code, period=self.__period.spec)
|
||||||
period=period_spec(self.__period))
|
|
||||||
return csv_download(filename, self.__get_csv_rows())
|
return csv_download(filename, self.__get_csv_rows())
|
||||||
|
|
||||||
def __get_csv_rows(self) -> list[CSVRow]:
|
def __get_csv_rows(self) -> list[CSVRow]:
|
||||||
@ -233,9 +255,10 @@ class TrialBalance(BaseReport):
|
|||||||
|
|
||||||
:return: The report as HTML.
|
:return: The report as HTML.
|
||||||
"""
|
"""
|
||||||
params: PageParams = PageParams(currency=self.__currency,
|
params: TrialBalancePageParams = TrialBalancePageParams(
|
||||||
period=self.__period,
|
currency=self.__currency,
|
||||||
accounts=self.__accounts,
|
period=self.__period,
|
||||||
total=self.__total)
|
accounts=self.__accounts,
|
||||||
|
total=self.__total)
|
||||||
return render_template("accounting/report/trial-balance.html",
|
return render_template("accounting/report/trial-balance.html",
|
||||||
report=params)
|
report=params)
|
||||||
|
@ -14,19 +14,16 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The utilities to export the report as CSV for download.
|
"""The utility to export the report as CSV for download.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import csv
|
import csv
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import timedelta, date
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
from flask import Response
|
from flask import Response
|
||||||
|
|
||||||
from accounting.report.period import Period
|
|
||||||
|
|
||||||
|
|
||||||
class BaseCSVRow(ABC):
|
class BaseCSVRow(ABC):
|
||||||
"""The base CSV row."""
|
"""The base CSV row."""
|
||||||
@ -55,54 +52,3 @@ def csv_download(filename: str, rows: list[BaseCSVRow]) -> Response:
|
|||||||
response.headers["Content-Disposition"] \
|
response.headers["Content-Disposition"] \
|
||||||
= f"attachment; filename={filename}"
|
= f"attachment; filename={filename}"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def period_spec(period: Period) -> str:
|
|
||||||
"""Constructs the period specification to be used in the filename.
|
|
||||||
|
|
||||||
:param period: The period.
|
|
||||||
:return: The period specification to be used in the filename.
|
|
||||||
"""
|
|
||||||
start: str | None = __get_start_str(period.start)
|
|
||||||
end: str | None = __get_end_str(period.end)
|
|
||||||
if period.start is None and period.end is None:
|
|
||||||
return "all-time"
|
|
||||||
if start == end:
|
|
||||||
return start
|
|
||||||
if period.start is None:
|
|
||||||
return f"until-{end}"
|
|
||||||
if period.end is None:
|
|
||||||
return f"since-{start}"
|
|
||||||
return f"{start}-{end}"
|
|
||||||
|
|
||||||
|
|
||||||
def __get_start_str(start: date | None) -> str | None:
|
|
||||||
"""Returns the string representation of the start date.
|
|
||||||
|
|
||||||
:param start: The start date.
|
|
||||||
:return: The string representation of the start date, or None if the start
|
|
||||||
date is None.
|
|
||||||
"""
|
|
||||||
if start is None:
|
|
||||||
return None
|
|
||||||
if start.month == 1 and start.day == 1:
|
|
||||||
return str(start.year)
|
|
||||||
if start.day == 1:
|
|
||||||
return start.strftime("%Y%m")
|
|
||||||
return start.strftime("%Y%m%d")
|
|
||||||
|
|
||||||
|
|
||||||
def __get_end_str(end: date | None) -> str | None:
|
|
||||||
"""Returns the string representation of the end date.
|
|
||||||
|
|
||||||
:param end: The end date.
|
|
||||||
:return: The string representation of the end date, or None if the end
|
|
||||||
date is None.
|
|
||||||
"""
|
|
||||||
if end is None:
|
|
||||||
return None
|
|
||||||
if end.month == 12 and end.day == 31:
|
|
||||||
return str(end.year)
|
|
||||||
if (end + timedelta(days=1)).day == 1:
|
|
||||||
return end.strftime("%Y%m")
|
|
||||||
return end.strftime("%Y%m%d")
|
|
||||||
|
@ -22,8 +22,7 @@
|
|||||||
class OptionLink:
|
class OptionLink:
|
||||||
"""An option link."""
|
"""An option link."""
|
||||||
|
|
||||||
def __init__(self, title: str, url: str, is_active: bool,
|
def __init__(self, title: str, url: str, is_active: bool):
|
||||||
fa_icon: str | None = None):
|
|
||||||
"""Constructs an option link.
|
"""Constructs an option link.
|
||||||
|
|
||||||
:param title: The title.
|
:param title: The title.
|
||||||
@ -33,4 +32,3 @@ class OptionLink:
|
|||||||
self.title: str = title
|
self.title: str = title
|
||||||
self.url: str = url
|
self.url: str = url
|
||||||
self.is_active: bool = is_active
|
self.is_active: bool = is_active
|
||||||
self.fa_icon: str | None = fa_icon
|
|
||||||
|
@ -22,18 +22,14 @@ from abc import ABC, abstractmethod
|
|||||||
from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \
|
from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \
|
||||||
urlunparse
|
urlunparse
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from accounting import db
|
|
||||||
from accounting.models import Currency, JournalEntry
|
|
||||||
from accounting.utils.txn_types import TransactionType
|
from accounting.utils.txn_types import TransactionType
|
||||||
from .option_link import OptionLink
|
|
||||||
from .report_chooser import ReportChooser
|
from .report_chooser import ReportChooser
|
||||||
|
|
||||||
|
|
||||||
class BasePageParams(ABC):
|
class PageParams(ABC):
|
||||||
"""The base HTML page parameters class."""
|
"""The page parameters of a report."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -70,19 +66,3 @@ class BasePageParams(ABC):
|
|||||||
parts: list[str] = list(uri_p)
|
parts: list[str] = list(uri_p)
|
||||||
parts[4] = urlencode(params)
|
parts[4] = urlencode(params)
|
||||||
return urlunparse(parts)
|
return urlunparse(parts)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_currency_options(get_url: t.Callable[[Currency], str],
|
|
||||||
active_currency: Currency) -> list[OptionLink]:
|
|
||||||
"""Returns the currency options.
|
|
||||||
|
|
||||||
:param get_url: The callback to return the URL of a currency.
|
|
||||||
:param active_currency: The active currency.
|
|
||||||
:return: The currency options.
|
|
||||||
"""
|
|
||||||
in_use: set[str] = set(db.session.scalars(
|
|
||||||
sa.select(JournalEntry.currency_code)
|
|
||||||
.group_by(JournalEntry.currency_code)).all())
|
|
||||||
return [OptionLink(str(x), get_url(x), x.code == active_currency.code)
|
|
||||||
for x in Currency.query.filter(Currency.code.in_(in_use))
|
|
||||||
.order_by(Currency.code).all()]
|
|
@ -20,16 +20,17 @@ This file is largely taken from the NanoParma ERP project, first written in
|
|||||||
2021/9/16 by imacat (imacat@nanoparma.com).
|
2021/9/16 by imacat (imacat@nanoparma.com).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import typing as t
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
|
from flask import url_for
|
||||||
|
|
||||||
from accounting.models import Currency, Account, Transaction
|
from accounting.models import Currency, Account, Transaction
|
||||||
from accounting.report.income_expense_account import IncomeExpensesAccount
|
from accounting.report.income_expense_account import IncomeExpensesAccount
|
||||||
from accounting.report.period import YearPeriod, Period, ThisMonth, \
|
from accounting.report.period import YearPeriod, Period, ThisMonth, \
|
||||||
LastMonth, SinceLastMonth, ThisYear, LastYear, Today, Yesterday, \
|
LastMonth, SinceLastMonth, ThisYear, LastYear, Today, Yesterday, \
|
||||||
TemplatePeriod
|
TemplatePeriod
|
||||||
from .urls import journal_url, ledger_url, income_expenses_url, \
|
|
||||||
trial_balance_url, income_statement_url, balance_sheet_url
|
|
||||||
|
|
||||||
|
|
||||||
class PeriodChooser(ABC):
|
class PeriodChooser(ABC):
|
||||||
@ -72,7 +73,7 @@ class PeriodChooser(ABC):
|
|||||||
"""Whether there is data in last year."""
|
"""Whether there is data in last year."""
|
||||||
self.has_yesterday: bool = False
|
self.has_yesterday: bool = False
|
||||||
"""Whether there is data in yesterday."""
|
"""Whether there is data in yesterday."""
|
||||||
self.available_years: list[int] = []
|
self.available_years: t.Iterator[int] = []
|
||||||
"""The available years."""
|
"""The available years."""
|
||||||
|
|
||||||
if self.has_data is not None:
|
if self.has_data is not None:
|
||||||
@ -80,6 +81,7 @@ class PeriodChooser(ABC):
|
|||||||
self.has_last_month = start < date(today.year, today.month, 1)
|
self.has_last_month = start < date(today.year, today.month, 1)
|
||||||
self.has_last_year = start.year < today.year
|
self.has_last_year = start.year < today.year
|
||||||
self.has_yesterday = start < today
|
self.has_yesterday = start < today
|
||||||
|
self.available_years: t.Iterator[int] = []
|
||||||
if start.year < today.year - 1:
|
if start.year < today.year - 1:
|
||||||
self.available_years \
|
self.available_years \
|
||||||
= reversed(range(start.year, today.year - 1))
|
= reversed(range(start.year, today.year - 1))
|
||||||
@ -112,7 +114,9 @@ class JournalPeriodChooser(PeriodChooser):
|
|||||||
super().__init__(None if first is None else first.date)
|
super().__init__(None if first is None else first.date)
|
||||||
|
|
||||||
def _url_for(self, period: Period) -> str:
|
def _url_for(self, period: Period) -> str:
|
||||||
return journal_url(period)
|
if period.is_default:
|
||||||
|
return url_for("accounting.report.journal-default")
|
||||||
|
return url_for("accounting.report.journal", period=period)
|
||||||
|
|
||||||
|
|
||||||
class LedgerPeriodChooser(PeriodChooser):
|
class LedgerPeriodChooser(PeriodChooser):
|
||||||
@ -129,14 +133,19 @@ class LedgerPeriodChooser(PeriodChooser):
|
|||||||
super().__init__(None if first is None else first.date)
|
super().__init__(None if first is None else first.date)
|
||||||
|
|
||||||
def _url_for(self, period: Period) -> str:
|
def _url_for(self, period: Period) -> str:
|
||||||
return ledger_url(self.currency, self.account, period)
|
if period.is_default:
|
||||||
|
return url_for("accounting.report.ledger-default",
|
||||||
|
currency=self.currency, account=self.account)
|
||||||
|
return url_for("accounting.report.ledger",
|
||||||
|
currency=self.currency, account=self.account,
|
||||||
|
period=period)
|
||||||
|
|
||||||
|
|
||||||
class IncomeExpensesPeriodChooser(PeriodChooser):
|
class IncomeExpensesPeriodChooser(PeriodChooser):
|
||||||
"""The income and expenses log period chooser."""
|
"""The income and expenses period chooser."""
|
||||||
|
|
||||||
def __init__(self, currency: Currency, account: IncomeExpensesAccount):
|
def __init__(self, currency: Currency, account: IncomeExpensesAccount):
|
||||||
"""Constructs the income and expenses log period chooser."""
|
"""Constructs the income and expenses period chooser."""
|
||||||
self.currency: Currency = currency
|
self.currency: Currency = currency
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
self.account: IncomeExpensesAccount = account
|
self.account: IncomeExpensesAccount = account
|
||||||
@ -146,7 +155,12 @@ class IncomeExpensesPeriodChooser(PeriodChooser):
|
|||||||
super().__init__(None if first is None else first.date)
|
super().__init__(None if first is None else first.date)
|
||||||
|
|
||||||
def _url_for(self, period: Period) -> str:
|
def _url_for(self, period: Period) -> str:
|
||||||
return income_expenses_url(self.currency, self.account, period)
|
if period.is_default:
|
||||||
|
return url_for("accounting.report.income-expenses-default",
|
||||||
|
currency=self.currency, account=self.account)
|
||||||
|
return url_for("accounting.report.income-expenses",
|
||||||
|
currency=self.currency, account=self.account,
|
||||||
|
period=period)
|
||||||
|
|
||||||
|
|
||||||
class TrialBalancePeriodChooser(PeriodChooser):
|
class TrialBalancePeriodChooser(PeriodChooser):
|
||||||
@ -161,7 +175,11 @@ class TrialBalancePeriodChooser(PeriodChooser):
|
|||||||
super().__init__(None if first is None else first.date)
|
super().__init__(None if first is None else first.date)
|
||||||
|
|
||||||
def _url_for(self, period: Period) -> str:
|
def _url_for(self, period: Period) -> str:
|
||||||
return trial_balance_url(self.currency, period)
|
if period.is_default:
|
||||||
|
return url_for("accounting.report.trial-balance-default",
|
||||||
|
currency=self.currency)
|
||||||
|
return url_for("accounting.report.trial-balance",
|
||||||
|
currency=self.currency, period=period)
|
||||||
|
|
||||||
|
|
||||||
class IncomeStatementPeriodChooser(PeriodChooser):
|
class IncomeStatementPeriodChooser(PeriodChooser):
|
||||||
@ -176,7 +194,11 @@ class IncomeStatementPeriodChooser(PeriodChooser):
|
|||||||
super().__init__(None if first is None else first.date)
|
super().__init__(None if first is None else first.date)
|
||||||
|
|
||||||
def _url_for(self, period: Period) -> str:
|
def _url_for(self, period: Period) -> str:
|
||||||
return income_statement_url(self.currency, period)
|
if period.is_default:
|
||||||
|
return url_for("accounting.report.income-statement-default",
|
||||||
|
currency=self.currency)
|
||||||
|
return url_for("accounting.report.income-statement",
|
||||||
|
currency=self.currency, period=period)
|
||||||
|
|
||||||
|
|
||||||
class BalanceSheetPeriodChooser(PeriodChooser):
|
class BalanceSheetPeriodChooser(PeriodChooser):
|
||||||
@ -191,4 +213,8 @@ class BalanceSheetPeriodChooser(PeriodChooser):
|
|||||||
super().__init__(None if first is None else first.date)
|
super().__init__(None if first is None else first.date)
|
||||||
|
|
||||||
def _url_for(self, period: Period) -> str:
|
def _url_for(self, period: Period) -> str:
|
||||||
return balance_sheet_url(self.currency, period)
|
if period.is_default:
|
||||||
|
return url_for("accounting.report.balance-sheet-default",
|
||||||
|
currency=self.currency)
|
||||||
|
return url_for("accounting.report.balance-sheet",
|
||||||
|
currency=self.currency, period=period)
|
||||||
|
@ -23,18 +23,16 @@ This file is largely taken from the NanoParma ERP project, first written in
|
|||||||
import re
|
import re
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
|
from flask import url_for
|
||||||
from flask_babel import LazyString
|
from flask_babel import LazyString
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from accounting.models import Currency, Account
|
from accounting.models import Currency, Account
|
||||||
from accounting.report.income_expense_account import IncomeExpensesAccount
|
|
||||||
from accounting.report.period import Period
|
from accounting.report.period import Period
|
||||||
from accounting.template_globals import default_currency_code
|
from accounting.template_globals import default_currency_code
|
||||||
from .option_link import OptionLink
|
from .option_link import OptionLink
|
||||||
from .report_type import ReportType
|
from .report_type import ReportType
|
||||||
from .urls import journal_url, ledger_url, income_expenses_url, \
|
|
||||||
trial_balance_url, income_statement_url, balance_sheet_url
|
|
||||||
|
|
||||||
|
|
||||||
class ReportChooser:
|
class ReportChooser:
|
||||||
@ -60,15 +58,13 @@ class ReportChooser:
|
|||||||
Currency, default_currency_code()) \
|
Currency, default_currency_code()) \
|
||||||
if currency is None else currency
|
if currency is None else currency
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
self.__account: Account = Account.cash() if account is None \
|
self.__account: Account = Account.find_by_code("1111-001") \
|
||||||
else account
|
if account is None else account
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
self.__reports: list[OptionLink] = []
|
self.__reports: list[OptionLink] = []
|
||||||
"""The links to the reports."""
|
"""The links to the reports."""
|
||||||
self.current_report: str | LazyString = ""
|
self.current_report: str | LazyString = ""
|
||||||
"""The title of the current report."""
|
"""The title of the current report."""
|
||||||
self.is_search: bool = active_report == ReportType.SEARCH
|
|
||||||
"""Whether the current report is the search page."""
|
|
||||||
self.__reports.append(self.__journal)
|
self.__reports.append(self.__journal)
|
||||||
self.__reports.append(self.__ledger)
|
self.__reports.append(self.__ledger)
|
||||||
self.__reports.append(self.__income_expenses)
|
self.__reports.append(self.__income_expenses)
|
||||||
@ -78,8 +74,6 @@ class ReportChooser:
|
|||||||
for report in self.__reports:
|
for report in self.__reports:
|
||||||
if report.is_active:
|
if report.is_active:
|
||||||
self.current_report = report.title
|
self.current_report = report.title
|
||||||
if self.is_search:
|
|
||||||
self.current_report = gettext("Search")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def __journal(self) -> OptionLink:
|
def __journal(self) -> OptionLink:
|
||||||
@ -87,9 +81,11 @@ class ReportChooser:
|
|||||||
|
|
||||||
:return: The journal.
|
:return: The journal.
|
||||||
"""
|
"""
|
||||||
return OptionLink(gettext("Journal"), journal_url(self.__period),
|
url: str = url_for("accounting.report.journal-default") \
|
||||||
self.__active_report == ReportType.JOURNAL,
|
if self.__period.is_default \
|
||||||
fa_icon="fa-solid fa-book")
|
else url_for("accounting.report.journal", period=self.__period)
|
||||||
|
return OptionLink(gettext("Journal"), url,
|
||||||
|
self.__active_report == ReportType.JOURNAL)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def __ledger(self) -> OptionLink:
|
def __ledger(self) -> OptionLink:
|
||||||
@ -97,27 +93,32 @@ class ReportChooser:
|
|||||||
|
|
||||||
:return: The ledger.
|
:return: The ledger.
|
||||||
"""
|
"""
|
||||||
return OptionLink(gettext("Ledger"),
|
url: str = url_for("accounting.report.ledger-default",
|
||||||
ledger_url(self.__currency, self.__account,
|
currency=self.__currency, account=self.__account) \
|
||||||
self.__period),
|
if self.__period.is_default \
|
||||||
self.__active_report == ReportType.LEDGER,
|
else url_for("accounting.report.ledger",
|
||||||
fa_icon="fa-solid fa-clipboard")
|
currency=self.__currency, account=self.__account,
|
||||||
|
period=self.__period)
|
||||||
|
return OptionLink(gettext("Ledger"), url,
|
||||||
|
self.__active_report == ReportType.LEDGER)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def __income_expenses(self) -> OptionLink:
|
def __income_expenses(self) -> OptionLink:
|
||||||
"""Returns the income and expenses log.
|
"""Returns the income and expenses.
|
||||||
|
|
||||||
:return: The income and expenses log.
|
:return: The income and expenses.
|
||||||
"""
|
"""
|
||||||
account: Account = self.__account
|
account: Account = self.__account
|
||||||
if not re.match(r"[12][12]", account.base_code):
|
if not re.match(r"[12][12]", account.base_code):
|
||||||
account: Account = Account.cash()
|
account: Account = Account.find_by_code("1111-001")
|
||||||
return OptionLink(gettext("Income and Expenses Log"),
|
url: str = url_for("accounting.report.income-expenses-default",
|
||||||
income_expenses_url(self.__currency,
|
currency=self.__currency, account=account) \
|
||||||
IncomeExpensesAccount(account),
|
if self.__period.is_default \
|
||||||
self.__period),
|
else url_for("accounting.report.income-expenses",
|
||||||
self.__active_report == ReportType.INCOME_EXPENSES,
|
currency=self.__currency, account=account,
|
||||||
fa_icon="fa-solid fa-money-bill-wave")
|
period=self.__period)
|
||||||
|
return OptionLink(gettext("Income and Expenses"), url,
|
||||||
|
self.__active_report == ReportType.INCOME_EXPENSES)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def __trial_balance(self) -> OptionLink:
|
def __trial_balance(self) -> OptionLink:
|
||||||
@ -125,10 +126,13 @@ class ReportChooser:
|
|||||||
|
|
||||||
:return: The trial balance.
|
:return: The trial balance.
|
||||||
"""
|
"""
|
||||||
return OptionLink(gettext("Trial Balance"),
|
url: str = url_for("accounting.report.trial-balance-default",
|
||||||
trial_balance_url(self.__currency, self.__period),
|
currency=self.__currency) \
|
||||||
self.__active_report == ReportType.TRIAL_BALANCE,
|
if self.__period.is_default \
|
||||||
fa_icon="fa-solid fa-scale-unbalanced")
|
else url_for("accounting.report.trial-balance",
|
||||||
|
currency=self.__currency, period=self.__period)
|
||||||
|
return OptionLink(gettext("Trial Balance"), url,
|
||||||
|
self.__active_report == ReportType.TRIAL_BALANCE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def __income_statement(self) -> OptionLink:
|
def __income_statement(self) -> OptionLink:
|
||||||
@ -136,10 +140,13 @@ class ReportChooser:
|
|||||||
|
|
||||||
:return: The income statement.
|
:return: The income statement.
|
||||||
"""
|
"""
|
||||||
return OptionLink(gettext("Income Statement"),
|
url: str = url_for("accounting.report.income-statement-default",
|
||||||
income_statement_url(self.__currency, self.__period),
|
currency=self.__currency) \
|
||||||
self.__active_report == ReportType.INCOME_STATEMENT,
|
if self.__period.is_default \
|
||||||
fa_icon="fa-solid fa-file-invoice-dollar")
|
else url_for("accounting.report.income-statement",
|
||||||
|
currency=self.__currency, period=self.__period)
|
||||||
|
return OptionLink(gettext("Income Statement"), url,
|
||||||
|
self.__active_report == ReportType.INCOME_STATEMENT)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def __balance_sheet(self) -> OptionLink:
|
def __balance_sheet(self) -> OptionLink:
|
||||||
@ -147,10 +154,13 @@ class ReportChooser:
|
|||||||
|
|
||||||
:return: The balance sheet.
|
:return: The balance sheet.
|
||||||
"""
|
"""
|
||||||
return OptionLink(gettext("Balance Sheet"),
|
url: str = url_for("accounting.report.balance-sheet-default",
|
||||||
balance_sheet_url(self.__currency, self.__period),
|
currency=self.__currency) \
|
||||||
self.__active_report == ReportType.BALANCE_SHEET,
|
if self.__period.is_default \
|
||||||
fa_icon="fa-solid fa-scale-balanced")
|
else url_for("accounting.report.balance-sheet",
|
||||||
|
currency=self.__currency, period=self.__period)
|
||||||
|
return OptionLink(gettext("Balance Sheet"), url,
|
||||||
|
self.__active_report == ReportType.BALANCE_SHEET)
|
||||||
|
|
||||||
def __iter__(self) -> t.Iterator[OptionLink]:
|
def __iter__(self) -> t.Iterator[OptionLink]:
|
||||||
"""Returns the iteration of the reports.
|
"""Returns the iteration of the reports.
|
||||||
|
@ -27,7 +27,7 @@ class ReportType(Enum):
|
|||||||
LEDGER: str = "ledger"
|
LEDGER: str = "ledger"
|
||||||
"""The ledger."""
|
"""The ledger."""
|
||||||
INCOME_EXPENSES: str = "income-expenses"
|
INCOME_EXPENSES: str = "income-expenses"
|
||||||
"""The income and expenses log."""
|
"""The income and expenses."""
|
||||||
TRIAL_BALANCE: str = "trial-balance"
|
TRIAL_BALANCE: str = "trial-balance"
|
||||||
"""The trial balance."""
|
"""The trial balance."""
|
||||||
INCOME_STATEMENT: str = "income-statement"
|
INCOME_STATEMENT: str = "income-statement"
|
||||||
|
@ -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)
|
|
@ -115,11 +115,11 @@ def __get_ledger_list(currency: Currency, account: Account, period: Period) \
|
|||||||
def get_default_income_expenses_list(currency: Currency,
|
def get_default_income_expenses_list(currency: Currency,
|
||||||
account: IncomeExpensesAccount) \
|
account: IncomeExpensesAccount) \
|
||||||
-> str | Response:
|
-> str | Response:
|
||||||
"""Returns the income and expenses log in the default period.
|
"""Returns the income and expenses in the default period.
|
||||||
|
|
||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
:return: The income and expenses log in the default period.
|
:return: The income and expenses in the default period.
|
||||||
"""
|
"""
|
||||||
return __get_income_expenses_list(currency, account, Period.get_instance())
|
return __get_income_expenses_list(currency, account, Period.get_instance())
|
||||||
|
|
||||||
@ -131,12 +131,12 @@ def get_default_income_expenses_list(currency: Currency,
|
|||||||
def get_income_expenses_list(currency: Currency,
|
def get_income_expenses_list(currency: Currency,
|
||||||
account: IncomeExpensesAccount,
|
account: IncomeExpensesAccount,
|
||||||
period: Period) -> str | Response:
|
period: Period) -> str | Response:
|
||||||
"""Returns the income and expenses log.
|
"""Returns the income and expenses.
|
||||||
|
|
||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
:param period: The period.
|
:param period: The period.
|
||||||
:return: The income and expenses log in the period.
|
:return: The income and expenses in the period.
|
||||||
"""
|
"""
|
||||||
return __get_income_expenses_list(currency, account, period)
|
return __get_income_expenses_list(currency, account, period)
|
||||||
|
|
||||||
@ -144,12 +144,12 @@ def get_income_expenses_list(currency: Currency,
|
|||||||
def __get_income_expenses_list(currency: Currency,
|
def __get_income_expenses_list(currency: Currency,
|
||||||
account: IncomeExpensesAccount,
|
account: IncomeExpensesAccount,
|
||||||
period: Period) -> str | Response:
|
period: Period) -> str | Response:
|
||||||
"""Returns the income and expenses log.
|
"""Returns the income and expenses.
|
||||||
|
|
||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
:param period: The period.
|
:param period: The period.
|
||||||
:return: The income and expenses log in the period.
|
:return: The income and expenses in the period.
|
||||||
"""
|
"""
|
||||||
report: IncomeExpenses = IncomeExpenses(currency, account, period)
|
report: IncomeExpenses = IncomeExpenses(currency, account, period)
|
||||||
if "as" in request.args and request.args["as"] == "csv":
|
if "as" in request.args and request.args["as"] == "csv":
|
||||||
|
@ -24,6 +24,19 @@
|
|||||||
.accounting-clickable {
|
.accounting-clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.accounting-search-desktop-form {
|
||||||
|
max-width: 16rem;
|
||||||
|
}
|
||||||
|
.btn-group .btn .accounting-search-input {
|
||||||
|
min-height: calc(1em + .5rem + 2px);
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
.btn-group .btn .accounting-search-label button {
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
.form-floating > textarea.form-control {
|
.form-floating > textarea.form-control {
|
||||||
height: 6rem;
|
height: 6rem;
|
||||||
}
|
}
|
||||||
@ -32,61 +45,6 @@
|
|||||||
background-color: #D3D3D4;
|
background-color: #D3D3D4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The toolbar */
|
|
||||||
.accounting-toolbar {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.accounting-toolbar .input-group > .input-group-text {
|
|
||||||
padding: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
color: inherit;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
.accounting-toolbar .input-group > .input-group-text > button {
|
|
||||||
background-color: transparent;
|
|
||||||
color: inherit;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
.accounting-toolbar form.btn > .form-control {
|
|
||||||
min-height: calc(1.5em + 2px);
|
|
||||||
padding-top: 0.1rem;
|
|
||||||
padding-bottom: 0.1rem;
|
|
||||||
}
|
|
||||||
@media(min-width: 768px) {
|
|
||||||
.accounting-toolbar > .btn, .accounting-toolbar > .btn-group > .btn {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
.accounting-toolbar > .btn:first-child, .accounting-toolbar > .btn-group:first-child > .btn {
|
|
||||||
border-top-left-radius: 0.375rem;
|
|
||||||
border-bottom-left-radius: 0.375rem;
|
|
||||||
}
|
|
||||||
.accounting-toolbar > .btn:last-child, .accounting-toolbar > .btn-group:last-child > .btn {
|
|
||||||
border-top-right-radius: 0.375rem;
|
|
||||||
border-bottom-right-radius: 0.375rem;
|
|
||||||
}
|
|
||||||
.accounting-toolbar .btn.input-group {
|
|
||||||
width: 16rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media(max-width:767px) {
|
|
||||||
.accounting-toolbar > .btn:not(form), .accounting-toolbar > .btn-group > .btn {
|
|
||||||
height: 3.2rem;
|
|
||||||
width: 3.2rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
.accounting-toolbar > a.btn, .accounting-toolbar > .btn-group > a.btn {
|
|
||||||
padding-top: 0.7rem;
|
|
||||||
}
|
|
||||||
.accounting-toolbar > form.btn {
|
|
||||||
width: 12rem;
|
|
||||||
height: 2.6rem;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
margin-top: 0.3rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The card layout */
|
/** The card layout */
|
||||||
.accounting-card {
|
.accounting-card {
|
||||||
padding: 2em 1.5em;
|
padding: 2em 1.5em;
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
* First written: 2023/2/1
|
* First written: 2023/2/1
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
* First written: 2023/2/2
|
* First written: 2023/2/2
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
* First written: 2023/2/28
|
* First written: 2023/2/28
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
* First written: 2023/2/6
|
* First written: 2023/2/6
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
* First written: 2023/2/3
|
* First written: 2023/2/3
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the drag-and-drop reordering on a list.
|
* Initializes the drag-and-drop reordering on a list.
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
* First written: 2023/2/25
|
* First written: 2023/2/25
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
@ -20,11 +20,10 @@
|
|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
* First written: 2023/3/4
|
* First written: 2023/3/4
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
PeriodChooser.initialize();
|
new PeriodChooser();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,20 +62,6 @@ class PeriodChooser {
|
|||||||
this.tabPlanes[tab.tabId()] = tab;
|
this.tabPlanes[tab.tabId()] = tab;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The period chooser.
|
|
||||||
* @type {PeriodChooser}
|
|
||||||
*/
|
|
||||||
static #chooser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the period chooser.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
static initialize() {
|
|
||||||
this.#chooser = new PeriodChooser();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,12 +142,6 @@ class TabPlane {
|
|||||||
*/
|
*/
|
||||||
class MonthTab extends TabPlane {
|
class MonthTab extends TabPlane {
|
||||||
|
|
||||||
/**
|
|
||||||
* The month chooser.
|
|
||||||
* @type {tempusDominus.TempusDominus}
|
|
||||||
*/
|
|
||||||
#monthChooser
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a tab plane.
|
* Constructs a tab plane.
|
||||||
*
|
*
|
||||||
@ -172,7 +151,7 @@ class MonthTab extends TabPlane {
|
|||||||
super(chooser);
|
super(chooser);
|
||||||
const monthChooser = document.getElementById(this.prefix + "-chooser");
|
const monthChooser = document.getElementById(this.prefix + "-chooser");
|
||||||
let start = monthChooser.dataset.start;
|
let start = monthChooser.dataset.start;
|
||||||
this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, {
|
new tempusDominus.TempusDominus(monthChooser, {
|
||||||
restrictions: {
|
restrictions: {
|
||||||
minDate: start,
|
minDate: start,
|
||||||
},
|
},
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
* First written: 2023/2/28
|
* First written: 2023/2/28
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
@ -763,7 +762,7 @@ class GeneralTripTab extends TagTabPlane {
|
|||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
populate() {
|
populate() {
|
||||||
const found = this.editor.summary.value.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
|
const found = this.editor.summary.value.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:[*×]\d+)?(?:\([^)]+\))?$/);
|
||||||
if (found === null) {
|
if (found === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -956,7 +955,7 @@ class BusTripTab extends TagTabPlane {
|
|||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
populate() {
|
populate() {
|
||||||
const found = this.editor.summary.value.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
|
const found = this.editor.summary.value.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:[*×]\d+)?(?:\([^)]+\))?$/);
|
||||||
if (found === null) {
|
if (found === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1141,7 +1140,7 @@ class AnnotationTab extends TabPlane {
|
|||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
updateSummary() {
|
updateSummary() {
|
||||||
const found = this.editor.summary.value.match(/^(.*?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
|
const found = this.editor.summary.value.match(/^(.*?)(?:[*×]\d+)?(?:\([^)]+\))?$/);
|
||||||
if (found !== null) {
|
if (found !== null) {
|
||||||
this.editor.summary.value = found[1];
|
this.editor.summary.value = found[1];
|
||||||
}
|
}
|
||||||
@ -1170,7 +1169,7 @@ class AnnotationTab extends TabPlane {
|
|||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
populate() {
|
populate() {
|
||||||
const found = this.editor.summary.value.match(/^(.*?)(?:[*×](\d+))?(?:\(([^()]+)\))?$/);
|
const found = this.editor.summary.value.match(/^(.*?)(?:[*×](\d+))?(?:\(([^)]+)\))?$/);
|
||||||
this.editor.summary.value = found[1];
|
this.editor.summary.value = found[1];
|
||||||
if (found[2] === undefined || parseInt(found[2]) === 1) {
|
if (found[2] === undefined || parseInt(found[2]) === 1) {
|
||||||
this.editor.number.value = "";
|
this.editor.number.value = "";
|
||||||
|
41
src/accounting/static/js/table-row-link.js
Normal file
41
src/accounting/static/js/table-row-link.js
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,6 @@
|
|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
* First written: 2023/2/25
|
* First written: 2023/2/25
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
* First written: 2023/2/26
|
* First written: 2023/2/26
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
@ -27,7 +27,7 @@ First written: 2023/2/1
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="btn-group mb-3">
|
<div class="btn-group btn-actions mb-3">
|
||||||
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
|
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left"></i>
|
||||||
{{ A_("Back") }}
|
{{ A_("Back") }}
|
||||||
|
@ -25,19 +25,31 @@ First written: 2023/1/30
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="mb-2 accounting-toolbar">
|
<div class="btn-group mb-2 d-none d-md-inline-flex">
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<a class="btn btn-primary text-nowrap d-none d-md-block" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
|
<a class="btn btn-primary text-nowrap" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
|
||||||
<i class="fa-solid fa-plus"></i>
|
<i class="fa-solid fa-plus"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
<form class="btn btn-primary d-flex input-group accounting-search-desktop-form" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-label="{{ A_("Search for Desktop") }}">
|
||||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
<input id="accounting-search-desktop" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
<label for="accounting-search-desktop" class="accounting-search-label">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Search") }}</span>
|
{{ A_("Search") }}
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group mb-2 d-md-none">
|
||||||
|
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-label="{{ A_("Search for Mobile") }}">
|
||||||
|
<input id="accounting-search-mobile" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
|
<label for="accounting-search-mobile" class="accounting-search-label">
|
||||||
|
<button type="submit">
|
||||||
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
|
{{ A_("Search") }}
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
</form>
|
</form>
|
||||||
|
@ -25,13 +25,13 @@ First written: 2023/1/26
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="mb-2 accounting-toolbar">
|
<div class="btn-group mb-2">
|
||||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.base-account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
<form class="btn btn-primary d-flex input-group accounting-search-desktop-form" action="{{ url_for("accounting.base-account.list") }}" method="get" role="search" aria-label="{{ A_("Search") }}">
|
||||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
<input id="accounting-search" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
<label for="accounting-search" class="accounting-search-label">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Search") }}</span>
|
{{ A_("Search") }}
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
</form>
|
</form>
|
||||||
|
@ -27,7 +27,7 @@ First written: 2023/2/6
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="btn-group mb-3">
|
<div class="btn-group btn-actions mb-3">
|
||||||
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
|
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left"></i>
|
||||||
{{ A_("Back") }}
|
{{ A_("Back") }}
|
||||||
|
@ -25,19 +25,31 @@ First written: 2023/2/6
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="mb-2 accounting-toolbar">
|
<div class="btn-group mb-2 d-none d-md-inline-flex">
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<a class="btn btn-primary text-nowrap d-none d-md-block" href="{{ url_for("accounting.currency.create")|accounting_append_next }}">
|
<a class="btn btn-primary text-nowrap" href="{{ url_for("accounting.currency.create")|accounting_append_next }}">
|
||||||
<i class="fa-solid fa-plus"></i>
|
<i class="fa-solid fa-plus"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
<form class="btn btn-primary d-flex input-group accounting-search-desktop-form" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-label="{{ A_("Search for Desktop") }}">
|
||||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
<input id="accounting-search-desktop" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
<label for="accounting-search-desktop" class="accounting-search-label">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Search") }}</span>
|
{{ A_("Search") }}
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group mb-2 d-md-none">
|
||||||
|
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-label="{{ A_("Search for Mobile") }}">
|
||||||
|
<input id="accounting-search-mobile" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
|
<label for="accounting-search-mobile" class="accounting-search-label">
|
||||||
|
<button type="submit">
|
||||||
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
|
{{ A_("Search") }}
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
</form>
|
</form>
|
||||||
|
@ -22,13 +22,13 @@ First written: 2023/2/25
|
|||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<div id="accounting-material-fab-speed-dial" class="d-md-none accounting-material-fab">
|
<div id="accounting-material-fab-speed-dial" class="d-md-none accounting-material-fab">
|
||||||
<div id="accounting-material-fab-speed-dial-actions" class="d-md-none accounting-material-fab-speed-dial-group">
|
<div id="accounting-material-fab-speed-dial-actions" class="d-md-none accounting-material-fab-speed-dial-group">
|
||||||
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||||
{{ A_("Cash expense") }}
|
{{ A_("Cash expense") }}
|
||||||
</a>
|
</a>
|
||||||
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||||
{{ A_("Cash income") }}
|
{{ A_("Cash income") }}
|
||||||
</a>
|
</a>
|
||||||
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
|
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.TRANSFER)|accounting_append_next }}">
|
||||||
{{ A_("Transfer") }}
|
{{ A_("Transfer") }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
@ -26,35 +26,129 @@ First written: 2023/3/7
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ _("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="btn-group mb-2 d-none d-md-inline-flex">
|
||||||
{% with use_currency_chooser = true,
|
{% if accounting_can_edit() %}
|
||||||
use_period_chooser = true %}
|
<div class="btn-group" role="group">
|
||||||
{% include "accounting/report/include/toolbar-buttons.html" %}
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
{{ A_("New") }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Expense") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Income") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
|
||||||
|
{{ A_("Transfer") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
{{ report.currency.name|title }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ report.period.desc|title }}
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
|
||||||
|
<i class="fa-solid fa-download"></i>
|
||||||
|
{{ A_("Download") }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
{% with txn_types = report.txn_types %}
|
||||||
|
{% include "accounting/include/add-txn-material-fab.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% include "accounting/report/include/period-chooser.html" %}
|
<div class="btn-group btn-actions mb-3 d-md-none">
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ A_("Period") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with period = report.period, period_chooser = report.period_chooser %}
|
||||||
|
{% include "accounting/report/include/period-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% include "accounting/report/include/search-modal.html" %}
|
{% include "accounting/report/include/search-modal.html" %}
|
||||||
|
|
||||||
{% if report.has_data %}
|
{% if report.has_data %}
|
||||||
<div class="accounting-sheet">
|
<div class="accounting-sheet">
|
||||||
<div class="d-none d-sm-flex justify-content-center mb-3">
|
<div class="d-none d-sm-flex justify-content-center mb-3">
|
||||||
<h2 class="text-center">{{ A_("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
|
<h2 class="text-center">{{ _("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row accounting-report-table accounting-balance-sheet-table">
|
<div class="row accounting-report-table accounting-balance-sheet-table">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
{% if report.assets.subsections %}
|
{% if report.assets.subsections %}
|
||||||
{% with section = report.assets %}
|
<div class="accounting-report-table-row accounting-balance-sheet-section">
|
||||||
{% include "accounting/report/include/balance-sheet-section.html" %}
|
<div>{{ report.assets.title.title|title }}</div>
|
||||||
{% endwith %}
|
</div>
|
||||||
|
<div class="accounting-report-table-body">
|
||||||
|
{% for subsection in report.assets.subsections %}
|
||||||
|
<div class="accounting-report-table-row accounting-balance-sheet-subsection">
|
||||||
|
<div>
|
||||||
|
<span class="d-none d-md-inline">{{ subsection.title.code }}</span>
|
||||||
|
{{ subsection.title.title|title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% for account in subsection.accounts %}
|
||||||
|
<a class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-account" href="{{ account.url }}">
|
||||||
|
<div>
|
||||||
|
<span class="d-none d-md-inline">{{ account.account.code }}</span>
|
||||||
|
{{ account.account.title|title }}
|
||||||
|
</div>
|
||||||
|
<div class="accounting-amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_report_format_amount }}</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
<div class="d-md-none d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-total">
|
<div class="d-md-none d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-total">
|
||||||
<div>{{ A_("Total") }}</div>
|
<div>{{ A_("Total") }}</div>
|
||||||
<div class="accounting-amount {% if report.assets.total < 0 %} text-danger {% endif %}">{{ report.assets.total|accounting_report_format_amount }}</div>
|
<div class="accounting-amount {% if report.assets.total < 0 %} text-danger {% endif %}">{{ report.assets.total|accounting_report_format_amount }}</div>
|
||||||
@ -64,9 +158,28 @@ First written: 2023/3/7
|
|||||||
|
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
{% if report.liabilities.subsections %}
|
{% if report.liabilities.subsections %}
|
||||||
{% with section = report.liabilities %}
|
<div class="accounting-report-table-row accounting-balance-sheet-section">
|
||||||
{% include "accounting/report/include/balance-sheet-section.html" %}
|
<div>{{ report.liabilities.title.title|title }}</div>
|
||||||
{% endwith %}
|
</div>
|
||||||
|
<div class="accounting-report-table-body">
|
||||||
|
{% for subsection in report.liabilities.subsections %}
|
||||||
|
<div class="accounting-report-table-row accounting-balance-sheet-subsection">
|
||||||
|
<div>
|
||||||
|
<span class="d-none d-md-inline">{{ subsection.title.code }}</span>
|
||||||
|
{{ subsection.title.title|title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% for account in subsection.accounts %}
|
||||||
|
<a class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-account" href="{{ account.url }}">
|
||||||
|
<div>
|
||||||
|
<span class="d-none d-md-inline">{{ account.account.code }}</span>
|
||||||
|
{{ account.account.title|title }}
|
||||||
|
</div>
|
||||||
|
<div class="accounting-amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_report_format_amount }}</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
<div class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-subtotal">
|
<div class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-subtotal">
|
||||||
<div>{{ A_("Total") }}</div>
|
<div>{{ A_("Total") }}</div>
|
||||||
<div class="accounting-amount {% if report.liabilities.total < 0 %} text-danger {% endif %}">{{ report.liabilities.total|accounting_report_format_amount }}</div>
|
<div class="accounting-amount {% if report.liabilities.total < 0 %} text-danger {% endif %}">{{ report.liabilities.total|accounting_report_format_amount }}</div>
|
||||||
@ -74,9 +187,28 @@ First written: 2023/3/7
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if report.owner_s_equity.subsections %}
|
{% if report.owner_s_equity.subsections %}
|
||||||
{% with section = report.owner_s_equity %}
|
<div class="accounting-report-table-row accounting-balance-sheet-section">
|
||||||
{% include "accounting/report/include/balance-sheet-section.html" %}
|
<div>{{ report.owner_s_equity.title.title|title }}</div>
|
||||||
{% endwith %}
|
</div>
|
||||||
|
<div class="accounting-report-table-body">
|
||||||
|
{% for subsection in report.owner_s_equity.subsections %}
|
||||||
|
<div class="accounting-report-table-row accounting-balance-sheet-subsection">
|
||||||
|
<div>
|
||||||
|
<span class="d-none d-md-inline">{{ subsection.title.code }}</span>
|
||||||
|
{{ subsection.title.title|title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% for account in subsection.accounts %}
|
||||||
|
<a class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-account" href="{{ account.url }}">
|
||||||
|
<div>
|
||||||
|
<span class="d-none d-md-inline">{{ account.account.code }}</span>
|
||||||
|
{{ account.account.title|title }}
|
||||||
|
</div>
|
||||||
|
<div class="accounting-amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_report_format_amount }}</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
<div class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-subtotal">
|
<div class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-subtotal">
|
||||||
<div>{{ A_("Total") }}</div>
|
<div>{{ A_("Total") }}</div>
|
||||||
<div class="accounting-amount {% if report.owner_s_equity.total < 0 %} text-danger {% endif %}">{{ report.owner_s_equity.total|accounting_report_format_amount }}</div>
|
<div class="accounting-amount {% if report.owner_s_equity.total < 0 %} text-danger {% endif %}">{{ report.owner_s_equity.total|accounting_report_format_amount }}</div>
|
||||||
|
@ -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>
|
|
@ -1,6 +1,6 @@
|
|||||||
{#
|
{#
|
||||||
The Mia! Accounting Flask Project
|
The Mia! Accounting Flask Project
|
||||||
income-expenses-row-mobile.html: The row in the income and expenses log for the mobile devices
|
income-expenses-mobile-row.html: The row in the income and expenses for the mobile devices
|
||||||
|
|
||||||
Copyright (c) 2023 imacat.
|
Copyright (c) 2023 imacat.
|
||||||
|
|
@ -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>
|
|
@ -1,6 +1,6 @@
|
|||||||
{#
|
{#
|
||||||
The Mia! Accounting Flask Project
|
The Mia! Accounting Flask Project
|
||||||
ledger-row-mobile.html: The row in the ledger for the mobile devices
|
ledger-mobile-row.html: The row in the ledger for the mobile devices
|
||||||
|
|
||||||
Copyright (c) 2023 imacat.
|
Copyright (c) 2023 imacat.
|
||||||
|
|
@ -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>
|
|
@ -19,7 +19,7 @@ period-chooser.html: The period chooser
|
|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
First written: 2023/3/4
|
First written: 2023/3/4
|
||||||
#}
|
#}
|
||||||
<div id="accounting-period-chooser-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-period-chooser-modal-label" aria-hidden="true" data-url-template="{{ report.period_chooser.url_template }}">
|
<div id="accounting-period-chooser-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-period-chooser-modal-label" aria-hidden="true" data-url-template="{{ period_chooser.url_template }}">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -30,62 +30,62 @@ First written: 2023/3/4
|
|||||||
{# Tab navigation #}
|
{# Tab navigation #}
|
||||||
<ul class="nav nav-tabs mb-2">
|
<ul class="nav nav-tabs mb-2">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<span id="accounting-period-chooser-month-tab" class="nav-link {% if report.period.is_type_month %} active {% endif %} accounting-clickable" aria-current="{% if report.period.is_type_month %} page {% else %} false {% endif %}" data-tab-id="month">
|
<span id="accounting-period-chooser-month-tab" class="nav-link {% if period.is_type_month %} active {% endif %} accounting-clickable" aria-current="{% if period.is_type_month %} page {% else %} false {% endif %}" data-tab-id="month">
|
||||||
{{ A_("Month") }}
|
{{ A_("Month") }}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<span id="accounting-period-chooser-year-tab" class="nav-link {% if report.period.is_a_year %} active {% endif %} accounting-clickable" aria-current="{% if report.period.is_a_year %} page {% else %} false {% endif %}" data-tab-id="year">
|
<span id="accounting-period-chooser-year-tab" class="nav-link {% if period.is_a_year %} active {% endif %} accounting-clickable" aria-current="{% if period.is_a_year %} page {% else %} false {% endif %}" data-tab-id="year">
|
||||||
{{ A_("Year") }}
|
{{ A_("Year") }}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<span id="accounting-period-chooser-day-tab" class="nav-link {% if report.period.is_a_day %} active {% endif %} accounting-clickable" aria-current="{% if report.period.is_a_day %} page {% else %} false {% endif %}" data-tab-id="day">
|
<span id="accounting-period-chooser-day-tab" class="nav-link {% if period.is_a_day %} active {% endif %} accounting-clickable" aria-current="{% if period.is_a_day %} page {% else %} false {% endif %}" data-tab-id="day">
|
||||||
{{ A_("Day") }}
|
{{ A_("Day") }}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<span id="accounting-period-chooser-custom-tab" class="nav-link {% if report.period.is_type_arbitrary %} active {% endif %} accounting-clickable" aria-current="{% if report.period.is_type_arbitrary %} page {% else %} false {% endif %}" data-tab-id="custom">
|
<span id="accounting-period-chooser-custom-tab" class="nav-link {% if period.is_type_arbitrary %} active {% endif %} accounting-clickable" aria-current="{% if period.is_type_arbitrary %} page {% else %} false {% endif %}" data-tab-id="custom">
|
||||||
{{ A_("Custom") }}
|
{{ A_("Custom") }}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{# The month periods #}
|
{# The month periods #}
|
||||||
<div id="accounting-period-chooser-month-page" {% if report.period.is_type_month %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-month-tab">
|
<div id="accounting-period-chooser-month-page" {% if period.is_type_month %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-month-tab">
|
||||||
<div>
|
<div>
|
||||||
<a class="btn {% if report.period.is_this_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.this_month_url }}">
|
<a class="btn {% if period.is_this_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.this_month_url }}">
|
||||||
{{ A_("This month") }}
|
{{ A_("This month") }}
|
||||||
</a>
|
</a>
|
||||||
{% if report.period_chooser.has_last_month %}
|
{% if period_chooser.has_last_month %}
|
||||||
<a class="btn {% if report.period.is_last_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.last_month_url }}">
|
<a class="btn {% if period.is_last_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.last_month_url }}">
|
||||||
{{ A_("Last month") }}
|
{{ A_("Last month") }}
|
||||||
</a>
|
</a>
|
||||||
<a class="btn {% if report.period.is_since_last_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.since_last_month_url }}">
|
<a class="btn {% if period.is_since_last_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.since_last_month_url }}">
|
||||||
{{ A_("Since last month") }}
|
{{ A_("Since last month") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if report.period_chooser.has_data %}
|
{% if period_chooser.has_data %}
|
||||||
<div id="accounting-period-chooser-month-chooser" class="mt-3" data-start="{{ report.period_chooser.data_start }}" data-default="{{ report.period.start|accounting_default(report.period_chooser.data_start) }}"></div>
|
<div id="accounting-period-chooser-month-chooser" class="mt-3" data-start="{{ period_chooser.data_start }}" data-default="{{ period.start }}"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# The year periods #}
|
{# The year periods #}
|
||||||
<div id="accounting-period-chooser-year-page" {% if report.period.is_a_year %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-year-tab">
|
<div id="accounting-period-chooser-year-page" {% if period.is_a_year %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-year-tab">
|
||||||
<a class="btn {% if report.period.is_this_year %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.this_year_url }}">
|
<a class="btn {% if period.is_this_year %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.this_year_url }}">
|
||||||
{{ A_("This year") }}
|
{{ A_("This year") }}
|
||||||
</a>
|
</a>
|
||||||
{% if report.period_chooser.has_last_year %}
|
{% if period_chooser.has_last_year %}
|
||||||
<a class="btn {% if report.period.is_last_year %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.last_year_url }}">
|
<a class="btn {% if period.is_last_year %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.last_year_url }}">
|
||||||
{{ A_("Last year") }}
|
{{ A_("Last year") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if report.period_chooser.available_years %}
|
{% if period_chooser.available_years %}
|
||||||
<ul class="nav nav-pills mt-3">
|
<ul class="nav nav-pills mt-3">
|
||||||
{% for year in report.period_chooser.available_years %}
|
{% for year in period_chooser.available_years %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if report.period.is_year(year) %} active {% endif %}" href="{{ report.period_chooser.year_url(year) }}">{{ year }}</a>
|
<a class="nav-link {% if period.is_year(year) %} active {% endif %}" href="{{ period_chooser.year_url(year) }}">{{ year }}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
@ -93,21 +93,21 @@ First written: 2023/3/4
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# The day periods #}
|
{# The day periods #}
|
||||||
<div id="accounting-period-chooser-day-page" {% if report.period.is_a_day %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-day-tab">
|
<div id="accounting-period-chooser-day-page" {% if period.is_a_day %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-day-tab">
|
||||||
<div>
|
<div>
|
||||||
<a class="btn {% if report.period.is_today %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.today_url }}">
|
<a class="btn {% if period.is_today %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.today_url }}">
|
||||||
{{ A_("Today") }}
|
{{ A_("Today") }}
|
||||||
</a>
|
</a>
|
||||||
{% if report.period_chooser.has_yesterday %}
|
{% if period_chooser.has_yesterday %}
|
||||||
<a class="btn {% if report.period.is_yesterday %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.yesterday_url }}">
|
<a class="btn {% if period.is_yesterday %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.yesterday_url }}">
|
||||||
{{ A_("Yesterday") }}
|
{{ A_("Yesterday") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if report.period_chooser.has_data %}
|
{% if period_chooser.has_data %}
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<input id="accounting-period-chooser-day-date" class="form-control" type="date" value="{{ report.period.start|accounting_default }}" min="{{ report.period_chooser.data_start }}" required="required">
|
<input id="accounting-period-chooser-day-date" class="form-control" type="date" value="{{ period.start|accounting_default }}" min="{{ period_chooser.data_start }}" required="required">
|
||||||
<label for="accounting-period-chooser-day-date" class="form-label">{{ A_("Date") }}</label>
|
<label for="accounting-period-chooser-day-date" class="form-label">{{ A_("Date") }}</label>
|
||||||
<div id="accounting-period-chooser-day-date-error" class="invalid-feedback"></div>
|
<div id="accounting-period-chooser-day-date-error" class="invalid-feedback"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -116,22 +116,22 @@ First written: 2023/3/4
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# The custom periods #}
|
{# The custom periods #}
|
||||||
<div id="accounting-period-chooser-custom-page" {% if report.period.is_type_arbitrary %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-custom-tab">
|
<div id="accounting-period-chooser-custom-page" {% if period.is_type_arbitrary %} aria-current="page" {% else %} class="d-none" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-custom-tab">
|
||||||
<div>
|
<div>
|
||||||
<a class="btn {% if report.period.is_all %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.all_url }}">
|
<a class="btn {% if period.is_all %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ period_chooser.all_url }}">
|
||||||
{{ A_("All") }}
|
{{ A_("All") }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% if report.period_chooser.has_data %}
|
{% if period_chooser.has_data %}
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<input id="accounting-period-chooser-custom-start" class="form-control" type="date" value="{{ report.period.start|accounting_default }}" min="{{ report.period_chooser.data_start }}" max="{{ report.period.end }}" required="required">
|
<input id="accounting-period-chooser-custom-start" class="form-control" type="date" value="{{ period.start|accounting_default }}" min="{{ period_chooser.data_start }}" max="{{ period.end }}" required="required">
|
||||||
<label for="accounting-period-chooser-custom-start" class="form-label">{{ A_("From") }}</label>
|
<label for="accounting-period-chooser-custom-start" class="form-label">{{ A_("From") }}</label>
|
||||||
<div id="accounting-period-chooser-custom-start-error" class="invalid-feedback"></div>
|
<div id="accounting-period-chooser-custom-start-error" class="invalid-feedback"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<input id="accounting-period-chooser-custom-end" class="form-control" type="date" value="{{ report.period.end|accounting_default }}" min="{{ report.period.start }}" required="required">
|
<input id="accounting-period-chooser-custom-end" class="form-control" type="date" value="{{ period.end|accounting_default }}" min="{{ period.start }}" required="required">
|
||||||
<label for="accounting-period-chooser-custom-end" class="form-label">{{ A_("To") }}</label>
|
<label for="accounting-period-chooser-custom-end" class="form-label">{{ A_("To") }}</label>
|
||||||
<div id="accounting-period-chooser-custom-end-error" class="invalid-feedback"></div>
|
<div id="accounting-period-chooser-custom-end-error" class="invalid-feedback"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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>
|
@ -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 %}
|
|
@ -1,6 +1,6 @@
|
|||||||
{#
|
{#
|
||||||
The Mia! Accounting Flask Project
|
The Mia! Accounting Flask Project
|
||||||
income-expenses.html: The income and expenses log
|
income-expenses.html: The income and expenses
|
||||||
|
|
||||||
Copyright (c) 2023 imacat.
|
Copyright (c) 2023 imacat.
|
||||||
|
|
||||||
@ -24,23 +24,127 @@ First written: 2023/3/5
|
|||||||
{% block accounting_scripts %}
|
{% block accounting_scripts %}
|
||||||
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Income and Expenses Log of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account.title|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ _("Income and Expenses of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="btn-group mb-2 d-none d-md-inline-flex">
|
||||||
{% with use_currency_chooser = true,
|
{% if accounting_can_edit() %}
|
||||||
use_account_chooser = true,
|
<div class="btn-group" role="group">
|
||||||
use_period_chooser = true %}
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
{% include "accounting/report/include/toolbar-buttons.html" %}
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
{{ A_("New") }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Expense") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Income") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
|
||||||
|
{{ A_("Transfer") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
{{ report.currency.name|title }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-clipboard"></i>
|
||||||
|
{{ report.account.title|title }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for account in report.account_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
|
||||||
|
{{ account.title|title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ report.period.desc|title }}
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
|
||||||
|
<i class="fa-solid fa-download"></i>
|
||||||
|
{{ A_("Download") }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
{% with txn_types = report.txn_types %}
|
||||||
|
{% include "accounting/include/add-txn-material-fab.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% include "accounting/report/include/period-chooser.html" %}
|
<div class="btn-group btn-actions mb-3 d-md-none">
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for account in report.account_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
|
||||||
|
{{ account.title|title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ A_("Period") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with period = report.period, period_chooser = report.period_chooser %}
|
||||||
|
{% include "accounting/report/include/period-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% include "accounting/report/include/search-modal.html" %}
|
{% include "accounting/report/include/search-modal.html" %}
|
||||||
|
|
||||||
@ -64,13 +168,23 @@ First written: 2023/3/5
|
|||||||
{% if report.brought_forward %}
|
{% if report.brought_forward %}
|
||||||
{% with entry = report.brought_forward %}
|
{% with entry = report.brought_forward %}
|
||||||
<div class="accounting-report-table-row">
|
<div class="accounting-report-table-row">
|
||||||
{% include "accounting/report/include/income-expenses-row-desktop.html" %}
|
<div>{{ entry.date|accounting_format_date }}</div>
|
||||||
|
<div>{{ entry.account.title|title }}</div>
|
||||||
|
<div>{{ entry.summary|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount">{{ entry.income|accounting_format_amount|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount">{{ entry.expense|accounting_format_amount|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for entry in report.entries %}
|
{% for entry in report.entries %}
|
||||||
<a class="accounting-report-table-row" href="{{ entry.url|accounting_append_next }}">
|
<a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
|
||||||
{% include "accounting/report/include/income-expenses-row-desktop.html" %}
|
<div>{{ entry.date|accounting_format_date }}</div>
|
||||||
|
<div>{{ entry.account.title|title }}</div>
|
||||||
|
<div>{{ entry.summary|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount">{{ entry.income|accounting_format_amount|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount">{{ entry.expense|accounting_format_amount|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@ -92,19 +206,19 @@ First written: 2023/3/5
|
|||||||
{% if report.brought_forward %}
|
{% if report.brought_forward %}
|
||||||
{% with entry = report.brought_forward %}
|
{% with entry = report.brought_forward %}
|
||||||
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||||
{% include "accounting/report/include/income-expenses-row-mobile.html" %}
|
{% include "accounting/report/include/income-expenses-mobile-row.html" %}
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for entry in report.entries %}
|
{% for entry in report.entries %}
|
||||||
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ entry.url|accounting_append_next }}">
|
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
|
||||||
{% include "accounting/report/include/income-expenses-row-mobile.html" %}
|
{% include "accounting/report/include/income-expenses-mobile-row.html" %}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if report.total %}
|
{% if report.total %}
|
||||||
{% with entry = report.total %}
|
{% with entry = report.total %}
|
||||||
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||||
{% include "accounting/report/include/income-expenses-row-mobile.html" %}
|
{% include "accounting/report/include/income-expenses-mobile-row.html" %}
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -26,27 +26,102 @@ First written: 2023/3/7
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ _("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="btn-group mb-2 d-none d-md-inline-flex">
|
||||||
{% with use_currency_chooser = true,
|
{% if accounting_can_edit() %}
|
||||||
use_period_chooser = true %}
|
<div class="btn-group" role="group">
|
||||||
{% include "accounting/report/include/toolbar-buttons.html" %}
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
{{ A_("New") }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Expense") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Income") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
|
||||||
|
{{ A_("Transfer") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
{{ report.currency.name|title }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ report.period.desc|title }}
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
|
||||||
|
<i class="fa-solid fa-download"></i>
|
||||||
|
{{ A_("Download") }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
{% with txn_types = report.txn_types %}
|
||||||
|
{% include "accounting/include/add-txn-material-fab.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% include "accounting/report/include/period-chooser.html" %}
|
<div class="btn-group btn-actions mb-3 d-md-none">
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ A_("Period") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with period = report.period, period_chooser = report.period_chooser %}
|
||||||
|
{% include "accounting/report/include/period-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% include "accounting/report/include/search-modal.html" %}
|
{% include "accounting/report/include/search-modal.html" %}
|
||||||
|
|
||||||
{% if report.has_data %}
|
{% if report.has_data %}
|
||||||
<div class="accounting-sheet">
|
<div class="accounting-sheet">
|
||||||
<div class="d-none d-sm-flex justify-content-center mb-3">
|
<div class="d-none d-sm-flex justify-content-center mb-3">
|
||||||
<h2 class="text-center">{{ A_("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
|
<h2 class="text-center">{{ _("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="accounting-report-table accounting-income-statement-table">
|
<div class="accounting-report-table accounting-income-statement-table">
|
||||||
|
@ -24,21 +24,69 @@ First written: 2023/3/4
|
|||||||
{% block accounting_scripts %}
|
{% block accounting_scripts %}
|
||||||
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
|
{# <script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script> #}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Journal %(period)s", period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ _("Journal %(period)s", period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="btn-group mb-2 d-none d-md-inline-flex">
|
||||||
{% with use_period_chooser = true %}
|
{% if accounting_can_edit() %}
|
||||||
{% include "accounting/report/include/toolbar-buttons.html" %}
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
{{ A_("New") }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Expense") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Income") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
|
||||||
|
{{ A_("Transfer") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ report.period.desc|title }}
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
|
||||||
|
<i class="fa-solid fa-download"></i>
|
||||||
|
{{ A_("Download") }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
{% with txn_types = report.txn_types %}
|
||||||
|
{% include "accounting/include/add-txn-material-fab.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% include "accounting/report/include/period-chooser.html" %}
|
<div class="btn-group btn-actions mb-3 d-md-none">
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ A_("Period") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with period = report.period, period_chooser = report.period_chooser %}
|
||||||
|
{% include "accounting/report/include/period-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% include "accounting/report/include/search-modal.html" %}
|
{% include "accounting/report/include/search-modal.html" %}
|
||||||
|
|
||||||
|
@ -24,23 +24,127 @@ First written: 2023/3/5
|
|||||||
{% block accounting_scripts %}
|
{% block accounting_scripts %}
|
||||||
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Ledger of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account.title|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ _("Ledger of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="btn-group mb-2 d-none d-md-inline-flex">
|
||||||
{% with use_currency_chooser = true,
|
{% if accounting_can_edit() %}
|
||||||
use_account_chooser = true,
|
<div class="btn-group" role="group">
|
||||||
use_period_chooser = true %}
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
{% include "accounting/report/include/toolbar-buttons.html" %}
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
{{ A_("New") }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Expense") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Income") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
|
||||||
|
{{ A_("Transfer") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
{{ report.currency.name|title }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-clipboard"></i>
|
||||||
|
{{ report.account.title|title }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for account in report.account_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
|
||||||
|
{{ account.title|title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ report.period.desc|title }}
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
|
||||||
|
<i class="fa-solid fa-download"></i>
|
||||||
|
{{ A_("Download") }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
{% with txn_types = report.txn_types %}
|
||||||
|
{% include "accounting/include/add-txn-material-fab.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% include "accounting/report/include/period-chooser.html" %}
|
<div class="btn-group btn-actions mb-3 d-md-none">
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for account in report.account_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
|
||||||
|
{{ account.title|title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ A_("Period") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with period = report.period, period_chooser = report.period_chooser %}
|
||||||
|
{% include "accounting/report/include/period-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% include "accounting/report/include/search-modal.html" %}
|
{% include "accounting/report/include/search-modal.html" %}
|
||||||
|
|
||||||
@ -63,13 +167,21 @@ First written: 2023/3/5
|
|||||||
{% if report.brought_forward %}
|
{% if report.brought_forward %}
|
||||||
{% with entry = report.brought_forward %}
|
{% with entry = report.brought_forward %}
|
||||||
<div class="accounting-report-table-row">
|
<div class="accounting-report-table-row">
|
||||||
{% include "accounting/report/include/ledger-row-desktop.html" %}
|
<div>{{ entry.date|accounting_format_date }}</div>
|
||||||
|
<div>{{ entry.summary|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount">{{ entry.debit|accounting_format_amount|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount">{{ entry.credit|accounting_format_amount|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for entry in report.entries %}
|
{% for entry in report.entries %}
|
||||||
<a class="accounting-report-table-row" href="{{ entry.url|accounting_append_next }}">
|
<a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
|
||||||
{% include "accounting/report/include/ledger-row-desktop.html" %}
|
<div>{{ entry.date|accounting_format_date }}</div>
|
||||||
|
<div>{{ entry.summary|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount">{{ entry.debit|accounting_format_amount|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount">{{ entry.credit|accounting_format_amount|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@ -91,19 +203,19 @@ First written: 2023/3/5
|
|||||||
{% if report.brought_forward %}
|
{% if report.brought_forward %}
|
||||||
{% with entry = report.brought_forward %}
|
{% with entry = report.brought_forward %}
|
||||||
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||||
{% include "accounting/report/include/ledger-row-mobile.html" %}
|
{% include "accounting/report/include/ledger-mobile-row.html" %}
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for entry in report.entries %}
|
{% for entry in report.entries %}
|
||||||
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ entry.url|accounting_append_next }}">
|
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
|
||||||
{% include "accounting/report/include/ledger-row-mobile.html" %}
|
{% include "accounting/report/include/ledger-mobile-row.html" %}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if report.total %}
|
{% if report.total %}
|
||||||
{% with entry = report.total %}
|
{% with entry = report.total %}
|
||||||
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||||
{% include "accounting/report/include/ledger-row-mobile.html" %}
|
{% include "accounting/report/include/ledger-mobile-row.html" %}
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -23,19 +23,75 @@ First written: 2023/3/8
|
|||||||
|
|
||||||
{% block accounting_scripts %}
|
{% block accounting_scripts %}
|
||||||
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Search Result for \"%(query)s\"", query=request.args.q) }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ A_("Search Result for \"%(query)s\"", query=request.args.q) }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="btn-group mb-2 d-none d-md-inline-flex">
|
||||||
{% with use_search = true %}
|
{% if accounting_can_edit() %}
|
||||||
{% include "accounting/report/include/toolbar-buttons.html" %}
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
{{ A_("New") }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Expense") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Income") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
|
||||||
|
{{ A_("Transfer") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
<form class="btn btn-primary d-flex input-group accounting-search-desktop-form" action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-label="{{ A_("Search for Desktop") }}">
|
||||||
|
<input id="accounting-search-desktop" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
|
<label for="accounting-search-desktop" class="accounting-search-label">
|
||||||
|
<button type="submit">
|
||||||
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
|
{{ A_("Search") }}
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
|
||||||
|
<i class="fa-solid fa-download"></i>
|
||||||
|
{{ A_("Download") }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
{% with txn_types = report.txn_types %}
|
||||||
|
{% include "accounting/include/add-txn-material-fab.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="btn-group btn-actions mb-3 d-md-none">
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-label="{{ A_("Search for Mobile") }}">
|
||||||
|
<input id="accounting-search-mobile" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
|
<label for="accounting-search-mobile" class="accounting-search-label">
|
||||||
|
<button type="submit">
|
||||||
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
|
{{ A_("Search") }}
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% include "accounting/report/include/search-modal.html" %}
|
{% include "accounting/report/include/search-modal.html" %}
|
||||||
|
|
||||||
|
@ -26,27 +26,102 @@ First written: 2023/3/5
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ _("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="btn-group mb-2 d-none d-md-inline-flex">
|
||||||
{% with use_currency_chooser = true,
|
{% if accounting_can_edit() %}
|
||||||
use_period_chooser = true %}
|
<div class="btn-group" role="group">
|
||||||
{% include "accounting/report/include/toolbar-buttons.html" %}
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
{{ A_("New") }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Expense") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||||
|
{{ A_("Cash Income") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
|
||||||
|
{{ A_("Transfer") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
{{ report.currency.name|title }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ report.period.desc|title }}
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
|
||||||
|
<i class="fa-solid fa-download"></i>
|
||||||
|
{{ A_("Download") }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
{% with txn_types = report.txn_types %}
|
||||||
|
{% include "accounting/include/add-txn-material-fab.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% include "accounting/report/include/period-chooser.html" %}
|
<div class="btn-group btn-actions mb-3 d-md-none">
|
||||||
|
{% with report_chooser = report.report_chooser %}
|
||||||
|
{% include "accounting/report/include/report-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ A_("Period") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with period = report.period, period_chooser = report.period_chooser %}
|
||||||
|
{% include "accounting/report/include/period-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% include "accounting/report/include/search-modal.html" %}
|
{% include "accounting/report/include/search-modal.html" %}
|
||||||
|
|
||||||
{% if report.has_data %}
|
{% if report.has_data %}
|
||||||
<div class="accounting-sheet">
|
<div class="accounting-sheet">
|
||||||
<div class="d-none d-sm-flex justify-content-center mb-3">
|
<div class="d-none d-sm-flex justify-content-center mb-3">
|
||||||
<h2 class="text-center">{{ A_("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
|
<h2 class="text-center">{{ _("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="accounting-report-table accounting-trial-balance-table">
|
<div class="accounting-report-table accounting-trial-balance-table">
|
||||||
@ -74,7 +149,6 @@ First written: 2023/3/5
|
|||||||
<div>{{ A_("Total") }}</div>
|
<div>{{ A_("Total") }}</div>
|
||||||
<div class="accounting-amount">{{ report.total.debit|accounting_format_amount }}</div>
|
<div class="accounting-amount">{{ report.total.debit|accounting_format_amount }}</div>
|
||||||
<div class="accounting-amount">{{ report.total.credit|accounting_format_amount }}</div>
|
<div class="accounting-amount">{{ report.total.credit|accounting_format_amount }}</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,6 +23,6 @@ First written: 2023/2/25
|
|||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Add a New Cash Expense Transaction") }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ A_("Add a New Cash Expense Transaction") }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.journal-default") }}{% endblock %}
|
{% block back_url %}{{ request.args.get("next") or url_for("accounting.transaction.list") }}{% endblock %}
|
||||||
|
|
||||||
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}
|
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/2/26
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="btn-group mb-3">
|
<div class="btn-group mb-3">
|
||||||
<a class="btn btn-primary" href="{{ url_for("accounting.report.journal-default")|accounting_or_next }}">
|
<a class="btn btn-primary" href="{{ url_for("accounting.transaction.list")|accounting_or_next }}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left"></i>
|
||||||
{{ A_("Back") }}
|
{{ A_("Back") }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -30,7 +30,7 @@ First written: 2023/2/26
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="btn-group mb-3">
|
<div class="btn-group btn-actions mb-3">
|
||||||
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
|
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left"></i>
|
||||||
{{ A_("Back") }}
|
{{ A_("Back") }}
|
||||||
|
@ -23,6 +23,6 @@ First written: 2023/2/25
|
|||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Add a New Cash Income Transaction") }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ A_("Add a New Cash Income Transaction") }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.journal-default") }}{% endblock %}
|
{% block back_url %}{{ request.args.get("next") or url_for("accounting.transaction.list") }}{% endblock %}
|
||||||
|
|
||||||
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}
|
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}
|
||||||
|
96
src/accounting/templates/accounting/transaction/list.html
Normal file
96
src/accounting/templates/accounting/transaction/list.html
Normal 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 %}
|
@ -31,7 +31,7 @@ First written: 2023/2/26
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="btn-group mb-3">
|
<div class="btn-group mb-3">
|
||||||
<a class="btn btn-primary" href="{{ url_for("accounting.report.journal-default")|accounting_or_next }}">
|
<a class="btn btn-primary" href="{{ url_for("accounting.transaction.list")|accounting_or_next }}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left"></i>
|
||||||
{{ A_("Back") }}
|
{{ A_("Back") }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -23,6 +23,6 @@ First written: 2023/2/25
|
|||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Add a New Transfer Transaction") }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ A_("Add a New Transfer Transaction") }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.journal-default") }}{% endblock %}
|
{% block back_url %}{{ request.args.get("next") or url_for("accounting.transaction.list") }}{% endblock %}
|
||||||
|
|
||||||
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}
|
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}
|
||||||
|
@ -46,9 +46,6 @@ MISSING_CURRENCY: LazyString = lazy_gettext("Please select the currency.")
|
|||||||
"""The error message when the currency code is empty."""
|
"""The error message when the currency code is empty."""
|
||||||
MISSING_ACCOUNT: LazyString = lazy_gettext("Please select the account.")
|
MISSING_ACCOUNT: LazyString = lazy_gettext("Please select the account.")
|
||||||
"""The error message when the account code is empty."""
|
"""The error message when the account code is empty."""
|
||||||
DATE_REQUIRED: DataRequired = DataRequired(
|
|
||||||
lazy_gettext("Please fill in the date."))
|
|
||||||
"""The validator to check if the date is empty."""
|
|
||||||
|
|
||||||
|
|
||||||
class NeedSomeCurrencies:
|
class NeedSomeCurrencies:
|
||||||
@ -577,7 +574,8 @@ class IncomeCurrencyForm(CurrencyForm):
|
|||||||
|
|
||||||
class IncomeTransactionForm(TransactionForm):
|
class IncomeTransactionForm(TransactionForm):
|
||||||
"""The form to create or edit a cash income transaction."""
|
"""The form to create or edit a cash income transaction."""
|
||||||
date = DateField(validators=[DATE_REQUIRED])
|
date = DateField(
|
||||||
|
validators=[DataRequired(lazy_gettext("Please fill in the date."))])
|
||||||
"""The date."""
|
"""The date."""
|
||||||
currencies = FieldList(FormField(IncomeCurrencyForm), name="currency",
|
currencies = FieldList(FormField(IncomeCurrencyForm), name="currency",
|
||||||
validators=[NeedSomeCurrencies()])
|
validators=[NeedSomeCurrencies()])
|
||||||
@ -650,7 +648,8 @@ class ExpenseCurrencyForm(CurrencyForm):
|
|||||||
|
|
||||||
class ExpenseTransactionForm(TransactionForm):
|
class ExpenseTransactionForm(TransactionForm):
|
||||||
"""The form to create or edit a cash expense transaction."""
|
"""The form to create or edit a cash expense transaction."""
|
||||||
date = DateField(validators=[DATE_REQUIRED])
|
date = DateField(
|
||||||
|
validators=[DataRequired(lazy_gettext("Please fill in the date."))])
|
||||||
"""The date."""
|
"""The date."""
|
||||||
currencies = FieldList(FormField(ExpenseCurrencyForm), name="currency",
|
currencies = FieldList(FormField(ExpenseCurrencyForm), name="currency",
|
||||||
validators=[NeedSomeCurrencies()])
|
validators=[NeedSomeCurrencies()])
|
||||||
@ -759,7 +758,8 @@ class TransferCurrencyForm(CurrencyForm):
|
|||||||
|
|
||||||
class TransferTransactionForm(TransactionForm):
|
class TransferTransactionForm(TransactionForm):
|
||||||
"""The form to create or edit a transfer transaction."""
|
"""The form to create or edit a transfer transaction."""
|
||||||
date = DateField(validators=[DATE_REQUIRED])
|
date = DateField(
|
||||||
|
validators=[DataRequired(lazy_gettext("Please fill in the date."))])
|
||||||
"""The date."""
|
"""The date."""
|
||||||
currencies = FieldList(FormField(TransferCurrencyForm), name="currency",
|
currencies = FieldList(FormField(TransferCurrencyForm), name="currency",
|
||||||
validators=[NeedSomeCurrencies()])
|
validators=[NeedSomeCurrencies()])
|
||||||
|
65
src/accounting/transaction/queries.py
Normal file
65
src/accounting/transaction/queries.py
Normal 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()
|
@ -30,11 +30,13 @@ from accounting.locale import lazy_gettext
|
|||||||
from accounting.models import Transaction
|
from accounting.models import Transaction
|
||||||
from accounting.utils.flash_errors import flash_form_errors
|
from accounting.utils.flash_errors import flash_form_errors
|
||||||
from accounting.utils.next_uri import inherit_next, or_next
|
from accounting.utils.next_uri import inherit_next, or_next
|
||||||
|
from accounting.utils.pagination import Pagination
|
||||||
from accounting.utils.permission import has_permission, can_view, can_edit
|
from accounting.utils.permission import has_permission, can_view, can_edit
|
||||||
from accounting.utils.txn_types import TransactionType
|
from accounting.utils.txn_types import TransactionType
|
||||||
from accounting.utils.user import get_current_user_pk
|
from accounting.utils.user import get_current_user_pk
|
||||||
from .forms import sort_transactions_in, TransactionReorderForm
|
|
||||||
from .operators import TransactionOperator, TXN_TYPE_TO_OP, get_txn_op
|
from .operators import TransactionOperator, TXN_TYPE_TO_OP, get_txn_op
|
||||||
|
from .forms import sort_transactions_in, TransactionReorderForm
|
||||||
|
from .queries import get_transaction_query
|
||||||
from .template_filters import with_type, to_transfer, format_amount_input, \
|
from .template_filters import with_type, to_transfer, format_amount_input, \
|
||||||
text2html
|
text2html
|
||||||
|
|
||||||
@ -47,6 +49,20 @@ bp.add_app_template_filter(format_amount_input,
|
|||||||
bp.add_app_template_filter(text2html, "accounting_txn_text2html")
|
bp.add_app_template_filter(text2html, "accounting_txn_text2html")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("", endpoint="list")
|
||||||
|
@has_permission(can_view)
|
||||||
|
def list_transactions() -> str:
|
||||||
|
"""Lists the transactions.
|
||||||
|
|
||||||
|
:return: The transaction list.
|
||||||
|
"""
|
||||||
|
transactions: list[Transaction] = get_transaction_query()
|
||||||
|
pagination: Pagination = Pagination[Transaction](transactions)
|
||||||
|
return render_template("accounting/transaction/list.html",
|
||||||
|
list=pagination.list, pagination=pagination,
|
||||||
|
txn_types=TransactionType)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/create/<transactionType:txn_type>", endpoint="create")
|
@bp.get("/create/<transactionType:txn_type>", endpoint="create")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def show_add_transaction_form(txn_type: TransactionType) -> str:
|
def show_add_transaction_form(txn_type: TransactionType) -> str:
|
||||||
@ -142,12 +158,12 @@ def update_transaction(txn: Transaction) -> redirect:
|
|||||||
form.populate_obj(txn)
|
form.populate_obj(txn)
|
||||||
if not form.is_modified:
|
if not form.is_modified:
|
||||||
flash(lazy_gettext("The transaction was not modified."), "success")
|
flash(lazy_gettext("The transaction was not modified."), "success")
|
||||||
return redirect(inherit_next(__get_detail_uri(txn)))
|
return redirect(inherit_next(with_type(__get_detail_uri(txn))))
|
||||||
txn.updated_by_id = get_current_user_pk()
|
txn.updated_by_id = get_current_user_pk()
|
||||||
txn.updated_at = sa.func.now()
|
txn.updated_at = sa.func.now()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(lazy_gettext("The transaction is updated successfully."), "success")
|
flash(lazy_gettext("The transaction is updated successfully."), "success")
|
||||||
return redirect(inherit_next(__get_detail_uri(txn)))
|
return redirect(inherit_next(with_type(__get_detail_uri(txn))))
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<transaction:txn>/delete", endpoint="delete")
|
@bp.post("/<transaction:txn>/delete", endpoint="delete")
|
||||||
@ -163,7 +179,7 @@ def delete_transaction(txn: Transaction) -> redirect:
|
|||||||
sort_transactions_in(txn.date, txn.id)
|
sort_transactions_in(txn.date, txn.id)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(lazy_gettext("The transaction is deleted successfully."), "success")
|
flash(lazy_gettext("The transaction is deleted successfully."), "success")
|
||||||
return redirect(or_next(url_for("accounting.report.journal-default")))
|
return redirect(or_next(with_type(url_for("accounting.transaction.list"))))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/dates/<date:txn_date>", endpoint="order")
|
@bp.get("/dates/<date:txn_date>", endpoint="order")
|
||||||
@ -183,7 +199,7 @@ def show_transaction_order(txn_date: date) -> str:
|
|||||||
|
|
||||||
@bp.post("/dates/<date:txn_date>", endpoint="sort")
|
@bp.post("/dates/<date:txn_date>", endpoint="sort")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def sort_transactions(txn_date: date) -> redirect:
|
def sort_accounts(txn_date: date) -> redirect:
|
||||||
"""Reorders the transactions in a date.
|
"""Reorders the transactions in a date.
|
||||||
|
|
||||||
:param txn_date: The date.
|
:param txn_date: The date.
|
||||||
@ -194,10 +210,10 @@ def sort_transactions(txn_date: date) -> redirect:
|
|||||||
form.save_order()
|
form.save_order()
|
||||||
if not form.is_modified:
|
if not form.is_modified:
|
||||||
flash(lazy_gettext("The order was not modified."), "success")
|
flash(lazy_gettext("The order was not modified."), "success")
|
||||||
return redirect(or_next(url_for("accounting.report.journal-default")))
|
return redirect(or_next(url_for("accounting.account.list")))
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(lazy_gettext("The order is updated successfully."), "success")
|
flash(lazy_gettext("The order is updated successfully."), "success")
|
||||||
return redirect(or_next(url_for("accounting.report.journal-default")))
|
return redirect(or_next(url_for("accounting.account.list")))
|
||||||
|
|
||||||
|
|
||||||
def __get_detail_uri(txn: Transaction) -> str:
|
def __get_detail_uri(txn: Transaction) -> str:
|
||||||
|
@ -8,8 +8,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Mia! Accounting Flask 0.0.0\n"
|
"Project-Id-Version: Mia! Accounting Flask 0.0.0\n"
|
||||||
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
|
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
|
||||||
"POT-Creation-Date: 2023-03-08 19:11+0800\n"
|
"POT-Creation-Date: 2023-03-01 00:51+0800\n"
|
||||||
"PO-Revision-Date: 2023-03-08 19:11+0800\n"
|
"PO-Revision-Date: 2023-03-01 00:51+0800\n"
|
||||||
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
|
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
|
||||||
"Language: zh_Hant\n"
|
"Language: zh_Hant\n"
|
||||||
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
|
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
|
||||||
@ -17,45 +17,23 @@ msgstr ""
|
|||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.12.1\n"
|
"Generated-By: Babel 2.11.0\n"
|
||||||
|
|
||||||
#: src/accounting/models.py:518
|
#: src/accounting/models.py:575
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Cash Expense Transaction#%(id)s"
|
msgid "Cash Expense Transaction#%(id)s"
|
||||||
msgstr "現金支出傳票#%(id)s"
|
msgstr "現金支出傳票#%(id)s"
|
||||||
|
|
||||||
#: src/accounting/models.py:520
|
#: src/accounting/models.py:577
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Cash Income Transaction#%(id)s"
|
msgid "Cash Income Transaction#%(id)s"
|
||||||
msgstr "現金收入傳票#%(id)s"
|
msgstr "現金收入傳票#%(id)s"
|
||||||
|
|
||||||
#: src/accounting/models.py:521
|
#: src/accounting/models.py:578
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Transfer Transaction#%(id)s"
|
msgid "Transfer Transaction#%(id)s"
|
||||||
msgstr "轉帳傳票#%(id)s"
|
msgstr "轉帳傳票#%(id)s"
|
||||||
|
|
||||||
#: src/accounting/report/period.py:493 src/accounting/template_filters.py:52
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:99
|
|
||||||
msgid "Today"
|
|
||||||
msgstr "今天"
|
|
||||||
|
|
||||||
#: src/accounting/report/period.py:508 src/accounting/template_filters.py:54
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:103
|
|
||||||
msgid "Yesterday"
|
|
||||||
msgstr "昨天"
|
|
||||||
|
|
||||||
#: src/accounting/template_filters.py:56
|
|
||||||
msgid "Tomorrow"
|
|
||||||
msgstr "明天"
|
|
||||||
|
|
||||||
#: src/accounting/template_filters.py:60
|
|
||||||
msgid "The day before yesterday"
|
|
||||||
msgstr "前天"
|
|
||||||
|
|
||||||
#: src/accounting/template_filters.py:62
|
|
||||||
msgid "The day after tomorrow"
|
|
||||||
msgstr "後天"
|
|
||||||
|
|
||||||
#: src/accounting/account/forms.py:41
|
#: src/accounting/account/forms.py:41
|
||||||
msgid "The base account does not exist."
|
msgid "The base account does not exist."
|
||||||
msgstr "沒有這個基本科目。"
|
msgstr "沒有這個基本科目。"
|
||||||
@ -65,7 +43,7 @@ msgid "The base account is not available."
|
|||||||
msgstr "不能選這個基本科目。"
|
msgstr "不能選這個基本科目。"
|
||||||
|
|
||||||
#: src/accounting/account/forms.py:61
|
#: src/accounting/account/forms.py:61
|
||||||
#: src/accounting/static/js/account-form.js:158
|
#: src/accounting/static/js/account-form.js:157
|
||||||
msgid "Please select the base account."
|
msgid "Please select the base account."
|
||||||
msgstr "請選擇基本科目。"
|
msgstr "請選擇基本科目。"
|
||||||
|
|
||||||
@ -73,8 +51,7 @@ msgstr "請選擇基本科目。"
|
|||||||
msgid "Please fill in the title"
|
msgid "Please fill in the title"
|
||||||
msgstr "請填上標題。"
|
msgstr "請填上標題。"
|
||||||
|
|
||||||
#: src/accounting/account/queries.py:50
|
#: src/accounting/account/query.py:50
|
||||||
#: src/accounting/report/reports/search.py:90
|
|
||||||
#: src/accounting/templates/accounting/account/detail.html:90
|
#: src/accounting/templates/accounting/account/detail.html:90
|
||||||
#: src/accounting/templates/accounting/account/list.html:74
|
#: src/accounting/templates/accounting/account/list.html:74
|
||||||
msgid "Pay-off needed"
|
msgid "Pay-off needed"
|
||||||
@ -96,36 +73,36 @@ msgstr "科目存好了。"
|
|||||||
msgid "The account is deleted successfully."
|
msgid "The account is deleted successfully."
|
||||||
msgstr "科目刪掉了"
|
msgstr "科目刪掉了"
|
||||||
|
|
||||||
#: src/accounting/account/views.py:189 src/accounting/transaction/views.py:212
|
#: src/accounting/account/views.py:189 src/accounting/transaction/views.py:214
|
||||||
msgid "The order was not modified."
|
msgid "The order was not modified."
|
||||||
msgstr "順序未異動。"
|
msgstr "順序未異動。"
|
||||||
|
|
||||||
#: src/accounting/account/views.py:192 src/accounting/transaction/views.py:215
|
#: src/accounting/account/views.py:192 src/accounting/transaction/views.py:217
|
||||||
msgid "The order is updated successfully."
|
msgid "The order is updated successfully."
|
||||||
msgstr "順序存好了。"
|
msgstr "順序存好了。"
|
||||||
|
|
||||||
#: src/accounting/currency/forms.py:46
|
#: src/accounting/currency/forms.py:46
|
||||||
#: src/accounting/static/js/currency-form.js:137
|
#: src/accounting/static/js/currency-form.js:136
|
||||||
msgid "Code conflicts with another currency."
|
msgid "Code conflicts with another currency."
|
||||||
msgstr "代碼與其它貨幣重複。"
|
msgstr "代碼與其它貨幣重複。"
|
||||||
|
|
||||||
#: src/accounting/currency/forms.py:51
|
#: src/accounting/currency/forms.py:51
|
||||||
#: src/accounting/static/js/currency-form.js:93
|
#: src/accounting/static/js/currency-form.js:92
|
||||||
msgid "Please fill in the code."
|
msgid "Please fill in the code."
|
||||||
msgstr "請填上代碼。"
|
msgstr "請填上代碼。"
|
||||||
|
|
||||||
#: src/accounting/currency/forms.py:53
|
#: src/accounting/currency/forms.py:53
|
||||||
#: src/accounting/static/js/currency-form.js:104
|
#: src/accounting/static/js/currency-form.js:103
|
||||||
msgid "Code can only be composed of 3 upper-cased letters."
|
msgid "Code can only be composed of 3 upper-cased letters."
|
||||||
msgstr "代碼限為三個大寫英文字母。"
|
msgstr "代碼限為三個大寫英文字母。"
|
||||||
|
|
||||||
#: src/accounting/currency/forms.py:56
|
#: src/accounting/currency/forms.py:56
|
||||||
#: src/accounting/static/js/currency-form.js:99
|
#: src/accounting/static/js/currency-form.js:98
|
||||||
msgid "This code is not available."
|
msgid "This code is not available."
|
||||||
msgstr "不能用這個代碼。"
|
msgstr "不能用這個代碼。"
|
||||||
|
|
||||||
#: src/accounting/currency/forms.py:62
|
#: src/accounting/currency/forms.py:62
|
||||||
#: src/accounting/static/js/currency-form.js:169
|
#: src/accounting/static/js/currency-form.js:168
|
||||||
msgid "Please fill in the name."
|
msgid "Please fill in the name."
|
||||||
msgstr "請填上名稱。"
|
msgstr "請填上名稱。"
|
||||||
|
|
||||||
@ -145,329 +122,56 @@ msgstr "貨幣存好了。"
|
|||||||
msgid "The currency is deleted successfully."
|
msgid "The currency is deleted successfully."
|
||||||
msgstr "貨幣刪掉了"
|
msgstr "貨幣刪掉了"
|
||||||
|
|
||||||
#: src/accounting/report/income_expense_account.py:62
|
#: src/accounting/static/js/account-form.js:177
|
||||||
msgid "current assets and liabilities"
|
|
||||||
msgstr "流動資產與負債"
|
|
||||||
|
|
||||||
#: src/accounting/report/period.py:252
|
|
||||||
msgid "for all time"
|
|
||||||
msgstr "全部"
|
|
||||||
|
|
||||||
#: src/accounting/report/period.py:284
|
|
||||||
#, python-format
|
|
||||||
msgid "since %(start)s"
|
|
||||||
msgstr "%(start)s至今"
|
|
||||||
|
|
||||||
#: src/accounting/report/period.py:302
|
|
||||||
#, python-format
|
|
||||||
msgid "until %(end)s"
|
|
||||||
msgstr "%(end)s前"
|
|
||||||
|
|
||||||
#: src/accounting/report/period.py:385
|
|
||||||
#, python-format
|
|
||||||
msgid "in %(period)s"
|
|
||||||
msgstr "%(period)s"
|
|
||||||
|
|
||||||
#: src/accounting/report/period.py:395
|
|
||||||
#, python-format
|
|
||||||
msgid "in %(start)s-%(end)s"
|
|
||||||
msgstr "%(start)s-%(end)s"
|
|
||||||
|
|
||||||
#: src/accounting/report/period.py:410
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:58
|
|
||||||
msgid "This month"
|
|
||||||
msgstr "這個月"
|
|
||||||
|
|
||||||
#: src/accounting/report/period.py:430
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:62
|
|
||||||
msgid "Last month"
|
|
||||||
msgstr "上個月"
|
|
||||||
|
|
||||||
#: src/accounting/report/period.py:450
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:65
|
|
||||||
msgid "Since last month"
|
|
||||||
msgstr "上個月至今"
|
|
||||||
|
|
||||||
#: src/accounting/report/period.py:465
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:77
|
|
||||||
msgid "This year"
|
|
||||||
msgstr "今年"
|
|
||||||
|
|
||||||
#: src/accounting/report/period.py:480
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:81
|
|
||||||
msgid "Last year"
|
|
||||||
msgstr "去年"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/balance_sheet.py:437
|
|
||||||
#: src/accounting/report/reports/balance_sheet.py:441
|
|
||||||
#: src/accounting/report/reports/balance_sheet.py:453
|
|
||||||
#: src/accounting/report/reports/balance_sheet.py:455
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:179
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:444
|
|
||||||
#: src/accounting/report/reports/income_statement.py:316
|
|
||||||
#: src/accounting/report/reports/ledger.py:160
|
|
||||||
#: src/accounting/report/reports/ledger.py:396
|
|
||||||
#: src/accounting/report/reports/trial_balance.py:245
|
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:71
|
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:83
|
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:93
|
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:99
|
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:108
|
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:115
|
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:94
|
|
||||||
#: src/accounting/templates/accounting/report/income-statement.html:95
|
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:93
|
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:86
|
|
||||||
#: src/accounting/templates/accounting/transaction/expense/detail.html:53
|
|
||||||
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:68
|
|
||||||
#: src/accounting/templates/accounting/transaction/income/detail.html:53
|
|
||||||
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:68
|
|
||||||
#: src/accounting/templates/accounting/transaction/transfer/detail.html:49
|
|
||||||
#: src/accounting/templates/accounting/transaction/transfer/detail.html:75
|
|
||||||
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:70
|
|
||||||
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:110
|
|
||||||
msgid "Total"
|
|
||||||
msgstr "合計"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:129
|
|
||||||
#: src/accounting/report/reports/ledger.py:125
|
|
||||||
msgid "Brought forward"
|
|
||||||
msgstr "前期轉入"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:428
|
|
||||||
#: src/accounting/report/reports/journal.py:219
|
|
||||||
#: src/accounting/report/reports/ledger.py:382
|
|
||||||
#: src/accounting/report/reports/search.py:193
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:111
|
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:68
|
|
||||||
#: src/accounting/templates/accounting/report/journal.html:64
|
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:68
|
|
||||||
#: src/accounting/templates/accounting/report/search.html:65
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/form.html:48
|
|
||||||
msgid "Date"
|
|
||||||
msgstr "日期"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:428
|
|
||||||
#: src/accounting/report/reports/journal.py:220
|
|
||||||
#: src/accounting/report/reports/search.py:194
|
|
||||||
#: src/accounting/report/reports/trial_balance.py:241
|
|
||||||
#: src/accounting/templates/accounting/report/include/action-buttons.html:96
|
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:69
|
|
||||||
#: src/accounting/templates/accounting/report/journal.html:66
|
|
||||||
#: src/accounting/templates/accounting/report/search.html:67
|
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:67
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:33
|
|
||||||
msgid "Account"
|
|
||||||
msgstr "科目"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:429
|
|
||||||
#: src/accounting/report/reports/journal.py:220
|
|
||||||
#: src/accounting/report/reports/ledger.py:382
|
|
||||||
#: src/accounting/report/reports/search.py:194
|
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:70
|
|
||||||
#: src/accounting/templates/accounting/report/journal.html:67
|
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:69
|
|
||||||
#: src/accounting/templates/accounting/report/search.html:68
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:41
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:28
|
|
||||||
msgid "Summary"
|
|
||||||
msgstr "摘要"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:429
|
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:71
|
|
||||||
msgid "Income"
|
|
||||||
msgstr "收入"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:430
|
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:72
|
|
||||||
msgid "Expense"
|
|
||||||
msgstr "支出"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:430
|
|
||||||
#: src/accounting/report/reports/ledger.py:384
|
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:73
|
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:72
|
|
||||||
msgid "Balance"
|
|
||||||
msgstr "餘額"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:431
|
|
||||||
#: src/accounting/report/reports/journal.py:222
|
|
||||||
#: src/accounting/report/reports/ledger.py:384
|
|
||||||
#: src/accounting/report/reports/search.py:196
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/form.html:71
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:169
|
|
||||||
msgid "Note"
|
|
||||||
msgstr "備註"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:232
|
|
||||||
msgid "total operating revenue"
|
|
||||||
msgstr "營業收入總額"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:233
|
|
||||||
msgid "gross income"
|
|
||||||
msgstr "營業毛利"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:234
|
|
||||||
msgid "operating income"
|
|
||||||
msgstr "營業淨利"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:235
|
|
||||||
msgid "before tax income"
|
|
||||||
msgstr "稅前淨利"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:236
|
|
||||||
msgid "after tax income"
|
|
||||||
msgstr "稅後淨利"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:237
|
|
||||||
msgid "net income or loss for current period"
|
|
||||||
msgstr "本期損益"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:317
|
|
||||||
#: src/accounting/templates/accounting/report/income-statement.html:67
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:49
|
|
||||||
msgid "Amount"
|
|
||||||
msgstr "金額"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/journal.py:219
|
|
||||||
#: src/accounting/report/reports/search.py:193
|
|
||||||
#: src/accounting/templates/accounting/report/include/action-buttons.html:79
|
|
||||||
#: src/accounting/templates/accounting/report/journal.html:65
|
|
||||||
#: src/accounting/templates/accounting/report/search.html:66
|
|
||||||
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:32
|
|
||||||
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:32
|
|
||||||
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:32
|
|
||||||
msgid "Currency"
|
|
||||||
msgstr "貨幣"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/journal.py:221
|
|
||||||
#: src/accounting/report/reports/ledger.py:383
|
|
||||||
#: src/accounting/report/reports/search.py:195
|
|
||||||
#: src/accounting/report/reports/trial_balance.py:241
|
|
||||||
#: src/accounting/templates/accounting/report/journal.html:68
|
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:70
|
|
||||||
#: src/accounting/templates/accounting/report/search.html:69
|
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:68
|
|
||||||
#: src/accounting/templates/accounting/transaction/transfer/detail.html:33
|
|
||||||
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:47
|
|
||||||
msgid "Debit"
|
|
||||||
msgstr "借方"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/journal.py:221
|
|
||||||
#: src/accounting/report/reports/ledger.py:383
|
|
||||||
#: src/accounting/report/reports/search.py:195
|
|
||||||
#: src/accounting/report/reports/trial_balance.py:242
|
|
||||||
#: src/accounting/templates/accounting/report/journal.html:69
|
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:71
|
|
||||||
#: src/accounting/templates/accounting/report/search.html:70
|
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:69
|
|
||||||
#: src/accounting/templates/accounting/transaction/transfer/detail.html:59
|
|
||||||
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:87
|
|
||||||
msgid "Credit"
|
|
||||||
msgstr "貸方"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/utils/report_chooser.py:87
|
|
||||||
msgid "Journal"
|
|
||||||
msgstr "日記簿"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/utils/report_chooser.py:103
|
|
||||||
msgid "Ledger"
|
|
||||||
msgstr "分類帳"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/utils/report_chooser.py:122
|
|
||||||
msgid "Income and Expenses Log"
|
|
||||||
msgstr "收支帳"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/utils/report_chooser.py:137
|
|
||||||
msgid "Trial Balance"
|
|
||||||
msgstr "試算表"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/utils/report_chooser.py:152
|
|
||||||
msgid "Income Statement"
|
|
||||||
msgstr "損益表"
|
|
||||||
|
|
||||||
#: src/accounting/report/reports/utils/report_chooser.py:167
|
|
||||||
msgid "Balance Sheet"
|
|
||||||
msgstr "資產負債表"
|
|
||||||
|
|
||||||
#: src/accounting/static/js/account-form.js:178
|
|
||||||
msgid "Please fill in the title."
|
msgid "Please fill in the title."
|
||||||
msgstr "請填上標題。"
|
msgstr "請填上標題。"
|
||||||
|
|
||||||
#: src/accounting/static/js/period-chooser.js:269
|
#: src/accounting/static/js/summary-helper.js:441
|
||||||
#: src/accounting/static/js/transaction-form.js:489
|
#: src/accounting/static/js/summary-helper.js:512
|
||||||
#: src/accounting/transaction/forms.py:578
|
|
||||||
#: src/accounting/transaction/forms.py:652
|
|
||||||
#: src/accounting/transaction/forms.py:762
|
|
||||||
msgid "Please fill in the date."
|
|
||||||
msgstr "請填上日期。"
|
|
||||||
|
|
||||||
#: src/accounting/static/js/period-chooser.js:274
|
|
||||||
msgid "The date is too early."
|
|
||||||
msgstr "日期太早。"
|
|
||||||
|
|
||||||
#: src/accounting/static/js/period-chooser.js:371
|
|
||||||
msgid "Please fill in the start date."
|
|
||||||
msgstr "請填上開始日期。"
|
|
||||||
|
|
||||||
#: src/accounting/static/js/period-chooser.js:376
|
|
||||||
msgid "The start date is too early."
|
|
||||||
msgstr "開始日期太早。"
|
|
||||||
|
|
||||||
#: src/accounting/static/js/period-chooser.js:381
|
|
||||||
msgid "The start date cannot be beyond the end date."
|
|
||||||
msgstr "開始日期不可晚於結束日期。"
|
|
||||||
|
|
||||||
#: src/accounting/static/js/period-chooser.js:399
|
|
||||||
msgid "Please fill in the end date."
|
|
||||||
msgstr "請填上結束日期。"
|
|
||||||
|
|
||||||
#: src/accounting/static/js/period-chooser.js:404
|
|
||||||
msgid "The end date cannot be beyond the start date."
|
|
||||||
msgstr "結束日期不可早於開始日期。"
|
|
||||||
|
|
||||||
#: src/accounting/static/js/summary-editor.js:817
|
|
||||||
#: src/accounting/static/js/summary-editor.js:1003
|
|
||||||
msgid "Please fill in the tag."
|
msgid "Please fill in the tag."
|
||||||
msgstr "請填上標籤。"
|
msgstr "請填上標籤。"
|
||||||
|
|
||||||
#: src/accounting/static/js/summary-editor.js:827
|
#: src/accounting/static/js/summary-helper.js:460
|
||||||
#: src/accounting/static/js/summary-editor.js:1023
|
#: src/accounting/static/js/summary-helper.js:550
|
||||||
msgid "Please fill in the origin."
|
msgid "Please fill in the origin."
|
||||||
msgstr "請填上起點。"
|
msgstr "請填上起點。"
|
||||||
|
|
||||||
#: src/accounting/static/js/summary-editor.js:837
|
#: src/accounting/static/js/summary-helper.js:479
|
||||||
#: src/accounting/static/js/summary-editor.js:1033
|
#: src/accounting/static/js/summary-helper.js:569
|
||||||
msgid "Please fill in the destination."
|
msgid "Please fill in the destination."
|
||||||
msgstr "請填上終點。"
|
msgstr "請填上終點。"
|
||||||
|
|
||||||
#: src/accounting/static/js/summary-editor.js:1013
|
#: src/accounting/static/js/summary-helper.js:531
|
||||||
msgid "Please fill in the route."
|
msgid "Please fill in the route."
|
||||||
msgstr "請填上路線名稱。"
|
msgstr "請填上路線名稱。"
|
||||||
|
|
||||||
#: src/accounting/static/js/transaction-form.js:290
|
#: src/accounting/static/js/transaction-form.js:289
|
||||||
#: src/accounting/static/js/transaction-form.js:612
|
#: src/accounting/static/js/transaction-form.js:611
|
||||||
#: src/accounting/transaction/forms.py:47
|
#: src/accounting/transaction/forms.py:47
|
||||||
msgid "Please select the account."
|
msgid "Please select the account."
|
||||||
msgstr "請選擇科目。"
|
msgstr "請選擇科目。"
|
||||||
|
|
||||||
#: src/accounting/static/js/transaction-form.js:325
|
#: src/accounting/static/js/transaction-form.js:324
|
||||||
#: src/accounting/static/js/transaction-form.js:617
|
#: src/accounting/static/js/transaction-form.js:616
|
||||||
msgid "Please fill in the amount."
|
msgid "Please fill in the amount."
|
||||||
msgstr "請填上金額。"
|
msgstr "請填上金額。"
|
||||||
|
|
||||||
#: src/accounting/static/js/transaction-form.js:524
|
#: src/accounting/static/js/transaction-form.js:488
|
||||||
|
msgid "Please fill in the date."
|
||||||
|
msgstr "請填上日期。"
|
||||||
|
|
||||||
|
#: src/accounting/static/js/transaction-form.js:523
|
||||||
#: src/accounting/transaction/forms.py:57
|
#: src/accounting/transaction/forms.py:57
|
||||||
msgid "Please add some currencies."
|
msgid "Please add some currencies."
|
||||||
msgstr "請加上貨幣。"
|
msgstr "請加上貨幣。"
|
||||||
|
|
||||||
#: src/accounting/static/js/transaction-form.js:590
|
#: src/accounting/static/js/transaction-form.js:589
|
||||||
#: src/accounting/transaction/forms.py:78
|
#: src/accounting/transaction/forms.py:78
|
||||||
msgid "Please add some journal entries."
|
msgid "Please add some journal entries."
|
||||||
msgstr "請加上分錄。"
|
msgstr "請加上分錄。"
|
||||||
|
|
||||||
#: src/accounting/static/js/transaction-form.js:655
|
#: src/accounting/static/js/transaction-form.js:654
|
||||||
#: src/accounting/transaction/forms.py:700
|
#: src/accounting/transaction/forms.py:672
|
||||||
msgid "The totals of the debit and credit amounts do not match."
|
msgid "The totals of the debit and credit amounts do not match."
|
||||||
msgstr "借方貸方合計不符。 "
|
msgstr "借方貸方合計不符。 "
|
||||||
|
|
||||||
@ -511,12 +215,10 @@ msgstr "科目刪除確認"
|
|||||||
#: src/accounting/templates/accounting/account/detail.html:70
|
#: src/accounting/templates/accounting/account/detail.html:70
|
||||||
#: src/accounting/templates/accounting/account/include/form.html:91
|
#: src/accounting/templates/accounting/account/include/form.html:91
|
||||||
#: src/accounting/templates/accounting/currency/detail.html:66
|
#: src/accounting/templates/accounting/currency/detail.html:66
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:27
|
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:28
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:27
|
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:27
|
||||||
#: src/accounting/templates/accounting/transaction/include/detail.html:71
|
#: src/accounting/templates/accounting/transaction/include/detail.html:71
|
||||||
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:28
|
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:28
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:30
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:30
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr "關閉"
|
msgstr "關閉"
|
||||||
|
|
||||||
@ -527,17 +229,15 @@ msgstr "你確定要刪掉這個科目嗎?"
|
|||||||
#: src/accounting/templates/accounting/account/detail.html:76
|
#: src/accounting/templates/accounting/account/detail.html:76
|
||||||
#: src/accounting/templates/accounting/account/include/form.html:112
|
#: src/accounting/templates/accounting/account/include/form.html:112
|
||||||
#: src/accounting/templates/accounting/currency/detail.html:72
|
#: src/accounting/templates/accounting/currency/detail.html:72
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:37
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:49
|
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:49
|
||||||
#: src/accounting/templates/accounting/transaction/include/detail.html:77
|
#: src/accounting/templates/accounting/transaction/include/detail.html:77
|
||||||
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:54
|
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:54
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:184
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:175
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "取消"
|
msgstr "取消"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/account/detail.html:77
|
#: src/accounting/templates/accounting/account/detail.html:77
|
||||||
#: src/accounting/templates/accounting/currency/detail.html:73
|
#: src/accounting/templates/accounting/currency/detail.html:73
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:141
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/detail.html:78
|
#: src/accounting/templates/accounting/transaction/include/detail.html:78
|
||||||
msgid "Confirm"
|
msgid "Confirm"
|
||||||
msgstr "確定"
|
msgstr "確定"
|
||||||
@ -562,7 +262,6 @@ msgstr "%(account)s設定"
|
|||||||
#: src/accounting/templates/accounting/account/list.html:24
|
#: src/accounting/templates/accounting/account/list.html:24
|
||||||
#: src/accounting/templates/accounting/base-account/list.html:24
|
#: src/accounting/templates/accounting/base-account/list.html:24
|
||||||
#: src/accounting/templates/accounting/currency/list.html:24
|
#: src/accounting/templates/accounting/currency/list.html:24
|
||||||
#: src/accounting/templates/accounting/report/search.html:28
|
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:28
|
#: src/accounting/templates/accounting/transaction/list.html:28
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Search Result for \"%(query)s\""
|
msgid "Search Result for \"%(query)s\""
|
||||||
@ -574,7 +273,6 @@ msgstr "科目管理"
|
|||||||
|
|
||||||
#: src/accounting/templates/accounting/account/list.html:32
|
#: src/accounting/templates/accounting/account/list.html:32
|
||||||
#: src/accounting/templates/accounting/currency/list.html:32
|
#: src/accounting/templates/accounting/currency/list.html:32
|
||||||
#: src/accounting/templates/accounting/report/include/action-buttons.html:27
|
|
||||||
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:75
|
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:75
|
||||||
#: src/accounting/templates/accounting/transaction/include/form.html:62
|
#: src/accounting/templates/accounting/transaction/include/form.html:62
|
||||||
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:75
|
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:75
|
||||||
@ -586,7 +284,6 @@ msgstr "新增"
|
|||||||
|
|
||||||
#: src/accounting/templates/accounting/account/list.html:35
|
#: src/accounting/templates/accounting/account/list.html:35
|
||||||
#: src/accounting/templates/accounting/currency/list.html:35
|
#: src/accounting/templates/accounting/currency/list.html:35
|
||||||
#: src/accounting/templates/accounting/report/search.html:37
|
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:57
|
#: src/accounting/templates/accounting/transaction/list.html:57
|
||||||
msgid "Search for Desktop"
|
msgid "Search for Desktop"
|
||||||
msgstr "桌機版檢索"
|
msgstr "桌機版檢索"
|
||||||
@ -598,11 +295,6 @@ msgstr "桌機版檢索"
|
|||||||
#: src/accounting/templates/accounting/base-account/list.html:34
|
#: src/accounting/templates/accounting/base-account/list.html:34
|
||||||
#: src/accounting/templates/accounting/currency/list.html:40
|
#: src/accounting/templates/accounting/currency/list.html:40
|
||||||
#: src/accounting/templates/accounting/currency/list.html:52
|
#: src/accounting/templates/accounting/currency/list.html:52
|
||||||
#: src/accounting/templates/accounting/report/include/action-buttons.html:67
|
|
||||||
#: src/accounting/templates/accounting/report/include/action-buttons.html:119
|
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:27
|
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:33
|
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:38
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:34
|
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:34
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:62
|
#: src/accounting/templates/accounting/transaction/list.html:62
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:74
|
#: src/accounting/templates/accounting/transaction/list.html:74
|
||||||
@ -611,7 +303,6 @@ msgstr "搜尋"
|
|||||||
|
|
||||||
#: src/accounting/templates/accounting/account/list.html:47
|
#: src/accounting/templates/accounting/account/list.html:47
|
||||||
#: src/accounting/templates/accounting/currency/list.html:47
|
#: src/accounting/templates/accounting/currency/list.html:47
|
||||||
#: src/accounting/templates/accounting/report/search.html:50
|
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:69
|
#: src/accounting/templates/accounting/transaction/list.html:69
|
||||||
msgid "Search for Mobile"
|
msgid "Search for Mobile"
|
||||||
msgstr "行動版檢索"
|
msgstr "行動版檢索"
|
||||||
@ -621,13 +312,6 @@ msgstr "行動版檢索"
|
|||||||
#: src/accounting/templates/accounting/account/order.html:81
|
#: src/accounting/templates/accounting/account/order.html:81
|
||||||
#: src/accounting/templates/accounting/base-account/list.html:51
|
#: src/accounting/templates/accounting/base-account/list.html:51
|
||||||
#: src/accounting/templates/accounting/currency/list.html:77
|
#: src/accounting/templates/accounting/currency/list.html:77
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:122
|
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:126
|
|
||||||
#: src/accounting/templates/accounting/report/income-statement.html:108
|
|
||||||
#: src/accounting/templates/accounting/report/journal.html:114
|
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:125
|
|
||||||
#: src/accounting/templates/accounting/report/search.html:115
|
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:94
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:46
|
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:46
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:93
|
#: src/accounting/templates/accounting/transaction/list.html:93
|
||||||
#: src/accounting/templates/accounting/transaction/order.html:80
|
#: src/accounting/templates/accounting/transaction/order.html:80
|
||||||
@ -644,7 +328,7 @@ msgstr "%(base)s下的科目"
|
|||||||
#: src/accounting/templates/accounting/currency/include/form.html:57
|
#: src/accounting/templates/accounting/currency/include/form.html:57
|
||||||
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:55
|
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:55
|
||||||
#: src/accounting/templates/accounting/transaction/include/form.html:78
|
#: src/accounting/templates/accounting/transaction/include/form.html:78
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:185
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:176
|
||||||
#: src/accounting/templates/accounting/transaction/order.html:61
|
#: src/accounting/templates/accounting/transaction/order.html:61
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr "儲存"
|
msgstr "儲存"
|
||||||
@ -708,27 +392,13 @@ msgstr "代碼"
|
|||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr "名稱"
|
msgstr "名稱"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/include/add-txn-material-fab.html:26
|
|
||||||
msgid "Cash expense"
|
|
||||||
msgstr "現金支出"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/include/add-txn-material-fab.html:29
|
|
||||||
msgid "Cash income"
|
|
||||||
msgstr "現金收入"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/include/add-txn-material-fab.html:32
|
|
||||||
#: src/accounting/templates/accounting/report/include/action-buttons.html:42
|
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:51
|
|
||||||
msgid "Transfer"
|
|
||||||
msgstr "轉帳"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/include/nav.html:27
|
#: src/accounting/templates/accounting/include/nav.html:27
|
||||||
msgid "Accounting"
|
msgid "Accounting"
|
||||||
msgstr "記帳"
|
msgstr "記帳"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/include/nav.html:33
|
#: src/accounting/templates/accounting/include/nav.html:33
|
||||||
msgid "Reports"
|
msgid "Transactions"
|
||||||
msgstr "報表"
|
msgstr "傳票"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/include/nav.html:39
|
#: src/accounting/templates/accounting/include/nav.html:39
|
||||||
msgid "Accounts"
|
msgid "Accounts"
|
||||||
@ -746,100 +416,22 @@ msgstr "貨幣"
|
|||||||
msgid "Page navigation"
|
msgid "Page navigation"
|
||||||
msgstr "分頁瀏覽"
|
msgstr "分頁瀏覽"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:29
|
#: src/accounting/templates/accounting/transaction/list.html:28
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:61
|
msgid "Transaction Management"
|
||||||
#, python-format
|
msgstr "傳票管理"
|
||||||
msgid "Balance Sheet of %(currency)s %(period)s"
|
|
||||||
msgstr "%(period)s%(currency)s資產負債表"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:29
|
|
||||||
#, python-format
|
|
||||||
msgid "Income and Expenses Log of %(account)s in %(currency)s %(period)s"
|
|
||||||
msgstr "%(period)s%(currency)s%(account)s收支帳"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/income-statement.html:29
|
|
||||||
#: src/accounting/templates/accounting/report/income-statement.html:61
|
|
||||||
#, python-format
|
|
||||||
msgid "Income Statement of %(currency)s %(period)s"
|
|
||||||
msgstr "%(period)s%(currency)s損益表"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/journal.html:29
|
|
||||||
#, python-format
|
|
||||||
msgid "Journal %(period)s"
|
|
||||||
msgstr "%(period)s日記簿"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:29
|
|
||||||
#, python-format
|
|
||||||
msgid "Ledger of %(account)s in %(currency)s %(period)s"
|
|
||||||
msgstr "%(period)s%(currency)s%(account)s分類帳"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:29
|
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:61
|
|
||||||
#, python-format
|
|
||||||
msgid "Trial Balance of %(currency)s %(period)s"
|
|
||||||
msgstr "%(period)s%(currency)s試算表"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/include/action-buttons.html:32
|
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:42
|
#: src/accounting/templates/accounting/transaction/list.html:42
|
||||||
msgid "Cash Expense"
|
msgid "Cash Expense"
|
||||||
msgstr "現金支出"
|
msgstr "現金支出"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/include/action-buttons.html:37
|
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:46
|
#: src/accounting/templates/accounting/transaction/list.html:46
|
||||||
msgid "Cash Income"
|
msgid "Cash Income"
|
||||||
msgstr "現金收入"
|
msgstr "現金收入"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/include/action-buttons.html:55
|
#: src/accounting/templates/accounting/transaction/include/add-new-material-fab.html:32
|
||||||
msgid "Report"
|
#: src/accounting/templates/accounting/transaction/list.html:51
|
||||||
msgstr "報表"
|
msgid "Transfer"
|
||||||
|
msgstr "轉帳"
|
||||||
#: src/accounting/templates/accounting/report/include/action-buttons.html:126
|
|
||||||
msgid "Download"
|
|
||||||
msgstr "下載"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:26
|
|
||||||
msgid "Period Chooser"
|
|
||||||
msgstr "選擇日期範圍"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:34
|
|
||||||
msgid "Month"
|
|
||||||
msgstr "月"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:39
|
|
||||||
msgid "Year"
|
|
||||||
msgstr "年"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:44
|
|
||||||
msgid "Day"
|
|
||||||
msgstr "日"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:49
|
|
||||||
msgid "Custom"
|
|
||||||
msgstr "自訂"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:122
|
|
||||||
msgid "All"
|
|
||||||
msgstr "全部"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:129
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:102
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:143
|
|
||||||
msgid "From"
|
|
||||||
msgstr "從"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:135
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:111
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:148
|
|
||||||
msgid "To"
|
|
||||||
msgstr "至"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:22
|
|
||||||
msgid "Search the Accounting Data"
|
|
||||||
msgstr "搜尋帳務資料"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:28
|
|
||||||
msgid "Transaction Management"
|
|
||||||
msgstr "傳票管理"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/order.html:29
|
#: src/accounting/templates/accounting/transaction/order.html:29
|
||||||
#, python-format
|
#, python-format
|
||||||
@ -863,6 +455,17 @@ msgstr "改轉帳"
|
|||||||
msgid "Content"
|
msgid "Content"
|
||||||
msgstr "內容"
|
msgstr "內容"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/expense/detail.html:53
|
||||||
|
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:68
|
||||||
|
#: src/accounting/templates/accounting/transaction/income/detail.html:53
|
||||||
|
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:68
|
||||||
|
#: src/accounting/templates/accounting/transaction/transfer/detail.html:49
|
||||||
|
#: src/accounting/templates/accounting/transaction/transfer/detail.html:75
|
||||||
|
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:70
|
||||||
|
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:110
|
||||||
|
msgid "Total"
|
||||||
|
msgstr "合計"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/expense/edit.html:24
|
#: src/accounting/templates/accounting/transaction/expense/edit.html:24
|
||||||
#: src/accounting/templates/accounting/transaction/income/edit.html:24
|
#: src/accounting/templates/accounting/transaction/income/edit.html:24
|
||||||
#: src/accounting/templates/accounting/transaction/transfer/edit.html:24
|
#: src/accounting/templates/accounting/transaction/transfer/edit.html:24
|
||||||
@ -870,6 +473,12 @@ msgstr "內容"
|
|||||||
msgid "Editing %(txn)s"
|
msgid "Editing %(txn)s"
|
||||||
msgstr "編輯%(txn)s"
|
msgstr "編輯%(txn)s"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:32
|
||||||
|
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:32
|
||||||
|
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:32
|
||||||
|
msgid "Currency"
|
||||||
|
msgstr "貨幣"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:26
|
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:26
|
||||||
msgid "Select Account"
|
msgid "Select Account"
|
||||||
msgstr "選擇科目"
|
msgstr "選擇科目"
|
||||||
@ -878,6 +487,14 @@ msgstr "選擇科目"
|
|||||||
msgid "More…"
|
msgid "More…"
|
||||||
msgstr "更多…"
|
msgstr "更多…"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/add-new-material-fab.html:26
|
||||||
|
msgid "Cash expense"
|
||||||
|
msgstr "現金支出"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/add-new-material-fab.html:29
|
||||||
|
msgid "Cash income"
|
||||||
|
msgstr "現金收入"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/detail.html:70
|
#: src/accounting/templates/accounting/transaction/include/detail.html:70
|
||||||
msgid "Delete Transaction Confirmation"
|
msgid "Delete Transaction Confirmation"
|
||||||
msgstr "傳票刪除確認"
|
msgstr "傳票刪除確認"
|
||||||
@ -890,37 +507,68 @@ msgstr "你確定要刪掉這張傳票嗎?"
|
|||||||
msgid "Journal Entry Content"
|
msgid "Journal Entry Content"
|
||||||
msgstr "分錄內容"
|
msgstr "分錄內容"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:41
|
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:33
|
||||||
|
msgid "Account"
|
||||||
|
msgstr "科目"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:41
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:28
|
||||||
|
msgid "Summary"
|
||||||
|
msgstr "摘要"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:49
|
||||||
|
msgid "Amount"
|
||||||
|
msgstr "金額"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/form.html:48
|
||||||
|
msgid "Date"
|
||||||
|
msgstr "日期"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/form.html:71
|
||||||
|
msgid "Note"
|
||||||
|
msgstr "備註"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:39
|
||||||
msgid "General"
|
msgid "General"
|
||||||
msgstr "一般"
|
msgstr "一般"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:46
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:44
|
||||||
msgid "Travel"
|
msgid "Travel"
|
||||||
msgstr "差旅"
|
msgstr "差旅"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:51
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:49
|
||||||
msgid "Bus"
|
msgid "Bus"
|
||||||
msgstr "公車"
|
msgstr "公車"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:56
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:54
|
||||||
msgid "Regular"
|
msgid "Regular"
|
||||||
msgstr "帳單"
|
msgstr "帳單"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:61
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:59
|
||||||
msgid "Annotation"
|
msgid "Number"
|
||||||
msgstr "註記"
|
msgstr "數量"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:70
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:67
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:87
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:84
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:122
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:119
|
||||||
msgid "Tag"
|
msgid "Tag"
|
||||||
msgstr "標籤"
|
msgstr "標籤"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:127
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:99
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:140
|
||||||
|
msgid "From"
|
||||||
|
msgstr "從"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:108
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:145
|
||||||
|
msgid "To"
|
||||||
|
msgstr "至"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:124
|
||||||
msgid "Route"
|
msgid "Route"
|
||||||
msgstr "路線"
|
msgstr "路線"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/summary-editor-modal.html:163
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:160
|
||||||
msgid "The number of items"
|
msgid "The number of items"
|
||||||
msgstr "數量"
|
msgstr "數量"
|
||||||
|
|
||||||
@ -932,6 +580,16 @@ msgstr "新增現金收入傳票"
|
|||||||
msgid "Add a New Transfer Transaction"
|
msgid "Add a New Transfer Transaction"
|
||||||
msgstr "新增轉帳傳票"
|
msgstr "新增轉帳傳票"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/transfer/detail.html:33
|
||||||
|
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:47
|
||||||
|
msgid "Debit"
|
||||||
|
msgstr "借方"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/transfer/detail.html:59
|
||||||
|
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:87
|
||||||
|
msgid "Credit"
|
||||||
|
msgstr "貸方"
|
||||||
|
|
||||||
#: src/accounting/transaction/forms.py:45
|
#: src/accounting/transaction/forms.py:45
|
||||||
msgid "Please select the currency."
|
msgid "Please select the currency."
|
||||||
msgstr "請選擇貨幣。"
|
msgstr "請選擇貨幣。"
|
||||||
@ -952,23 +610,43 @@ msgstr "金額請填正數。"
|
|||||||
msgid "This account is not for debit entries."
|
msgid "This account is not for debit entries."
|
||||||
msgstr "科目不是借方科目。"
|
msgstr "科目不是借方科目。"
|
||||||
|
|
||||||
#: src/accounting/transaction/forms.py:230
|
#: src/accounting/transaction/forms.py:201
|
||||||
msgid "This account is not for credit entries."
|
msgid "This account is not for credit entries."
|
||||||
msgstr "科目不是貸方科目。"
|
msgstr "科目不是貸方科目。"
|
||||||
|
|
||||||
#: src/accounting/transaction/views.py:106
|
#: src/accounting/transaction/template.py:97
|
||||||
|
msgid "Today"
|
||||||
|
msgstr "今天"
|
||||||
|
|
||||||
|
#: src/accounting/transaction/template.py:99
|
||||||
|
msgid "Yesterday"
|
||||||
|
msgstr "昨天"
|
||||||
|
|
||||||
|
#: src/accounting/transaction/template.py:101
|
||||||
|
msgid "Tomorrow"
|
||||||
|
msgstr "明天"
|
||||||
|
|
||||||
|
#: src/accounting/transaction/template.py:105
|
||||||
|
msgid "The day before yesterday"
|
||||||
|
msgstr "前天"
|
||||||
|
|
||||||
|
#: src/accounting/transaction/template.py:107
|
||||||
|
msgid "The day after tomorrow"
|
||||||
|
msgstr "後天"
|
||||||
|
|
||||||
|
#: src/accounting/transaction/views.py:108
|
||||||
msgid "The transaction is added successfully"
|
msgid "The transaction is added successfully"
|
||||||
msgstr "傳票加好了。"
|
msgstr "傳票加好了。"
|
||||||
|
|
||||||
#: src/accounting/transaction/views.py:160
|
#: src/accounting/transaction/views.py:162
|
||||||
msgid "The transaction was not modified."
|
msgid "The transaction was not modified."
|
||||||
msgstr "傳票未異動。"
|
msgstr "傳票未異動。"
|
||||||
|
|
||||||
#: src/accounting/transaction/views.py:165
|
#: src/accounting/transaction/views.py:167
|
||||||
msgid "The transaction is updated successfully."
|
msgid "The transaction is updated successfully."
|
||||||
msgstr "傳票存好了。"
|
msgstr "傳票存好了。"
|
||||||
|
|
||||||
#: src/accounting/transaction/views.py:181
|
#: src/accounting/transaction/views.py:183
|
||||||
msgid "The transaction is deleted successfully."
|
msgid "The transaction is deleted successfully."
|
||||||
msgstr "傳票刪掉了"
|
msgstr "傳票刪掉了"
|
||||||
|
|
||||||
@ -982,9 +660,3 @@ msgctxt "Pagination|"
|
|||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr "下一頁"
|
msgstr "下一頁"
|
||||||
|
|
||||||
#~ msgid "Number"
|
|
||||||
#~ msgstr "數量"
|
|
||||||
|
|
||||||
#~ msgid "in %(time)s"
|
|
||||||
#~ msgstr "%(period)s"
|
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse, \
|
|||||||
from flask import request
|
from flask import request
|
||||||
from werkzeug.routing import RequestRedirect
|
from werkzeug.routing import RequestRedirect
|
||||||
|
|
||||||
from accounting.locale import pgettext
|
from accounting.locale import gettext, pgettext
|
||||||
|
|
||||||
|
|
||||||
class Link:
|
class Link:
|
||||||
|
@ -35,8 +35,6 @@ from testlib_txn import Accounts, get_add_form, get_unchanged_update_form, \
|
|||||||
|
|
||||||
PREFIX: str = "/accounting/transactions"
|
PREFIX: str = "/accounting/transactions"
|
||||||
"""The URL prefix for the transaction management."""
|
"""The URL prefix for the transaction management."""
|
||||||
RETURN_TO_URI: str = "/accounting/reports/journal"
|
|
||||||
"""The URL to return to after the operation."""
|
|
||||||
|
|
||||||
|
|
||||||
class CashIncomeTransactionTestCase(unittest.TestCase):
|
class CashIncomeTransactionTestCase(unittest.TestCase):
|
||||||
@ -84,6 +82,9 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
|
|||||||
update_form["csrf_token"] = csrf_token
|
update_form["csrf_token"] = csrf_token
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = client.get(PREFIX)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
response = client.get(f"{PREFIX}/{txn_id}")
|
response = client.get(f"{PREFIX}/{txn_id}")
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@ -116,6 +117,9 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
|
|||||||
update_form["csrf_token"] = csrf_token
|
update_form["csrf_token"] = csrf_token
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = client.get(PREFIX)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = client.get(f"{PREFIX}/{txn_id}")
|
response = client.get(f"{PREFIX}/{txn_id}")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@ -145,6 +149,9 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
|
|||||||
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = self.client.get(PREFIX)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.get(f"{PREFIX}/{txn_id}")
|
response = self.client.get(f"{PREFIX}/{txn_id}")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@ -168,7 +175,7 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
|
|||||||
response = self.client.post(f"{PREFIX}/{txn_id}/delete",
|
response = self.client.post(f"{PREFIX}/{txn_id}/delete",
|
||||||
data={"csrf_token": self.csrf_token})
|
data={"csrf_token": self.csrf_token})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], RETURN_TO_URI)
|
self.assertEqual(response.headers["Location"], PREFIX)
|
||||||
|
|
||||||
def test_add(self) -> None:
|
def test_add(self) -> None:
|
||||||
"""Tests to add the transactions.
|
"""Tests to add the transactions.
|
||||||
@ -636,6 +643,9 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
|
|||||||
update_form["csrf_token"] = csrf_token
|
update_form["csrf_token"] = csrf_token
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = client.get(PREFIX)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
response = client.get(f"{PREFIX}/{txn_id}")
|
response = client.get(f"{PREFIX}/{txn_id}")
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@ -668,6 +678,9 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
|
|||||||
update_form["csrf_token"] = csrf_token
|
update_form["csrf_token"] = csrf_token
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = client.get(PREFIX)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = client.get(f"{PREFIX}/{txn_id}")
|
response = client.get(f"{PREFIX}/{txn_id}")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@ -697,6 +710,9 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
|
|||||||
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = self.client.get(PREFIX)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.get(f"{PREFIX}/{txn_id}")
|
response = self.client.get(f"{PREFIX}/{txn_id}")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@ -720,7 +736,7 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
|
|||||||
response = self.client.post(f"{PREFIX}/{txn_id}/delete",
|
response = self.client.post(f"{PREFIX}/{txn_id}/delete",
|
||||||
data={"csrf_token": self.csrf_token})
|
data={"csrf_token": self.csrf_token})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], RETURN_TO_URI)
|
self.assertEqual(response.headers["Location"], PREFIX)
|
||||||
|
|
||||||
def test_add(self) -> None:
|
def test_add(self) -> None:
|
||||||
"""Tests to add the transactions.
|
"""Tests to add the transactions.
|
||||||
@ -1195,6 +1211,9 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
update_form["csrf_token"] = csrf_token
|
update_form["csrf_token"] = csrf_token
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = client.get(PREFIX)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
response = client.get(f"{PREFIX}/{txn_id}")
|
response = client.get(f"{PREFIX}/{txn_id}")
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@ -1227,6 +1246,9 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
update_form["csrf_token"] = csrf_token
|
update_form["csrf_token"] = csrf_token
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = client.get(PREFIX)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = client.get(f"{PREFIX}/{txn_id}")
|
response = client.get(f"{PREFIX}/{txn_id}")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@ -1256,6 +1278,9 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = self.client.get(PREFIX)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.get(f"{PREFIX}/{txn_id}")
|
response = self.client.get(f"{PREFIX}/{txn_id}")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@ -1279,7 +1304,7 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
response = self.client.post(f"{PREFIX}/{txn_id}/delete",
|
response = self.client.post(f"{PREFIX}/{txn_id}/delete",
|
||||||
data={"csrf_token": self.csrf_token})
|
data={"csrf_token": self.csrf_token})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], RETURN_TO_URI)
|
self.assertEqual(response.headers["Location"], PREFIX)
|
||||||
|
|
||||||
def test_add(self) -> None:
|
def test_add(self) -> None:
|
||||||
"""Tests to add the transactions.
|
"""Tests to add the transactions.
|
||||||
@ -1716,7 +1741,7 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
from accounting.models import Transaction, TransactionCurrency
|
from accounting.models import Transaction, TransactionCurrency
|
||||||
txn_id: int = add_txn(self.client, self.__get_add_form())
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?as=income&next=%2F_next"
|
||||||
update_uri: str = f"{PREFIX}/{txn_id}/update?as=income"
|
update_uri: str = f"{PREFIX}/{txn_id}/update?as=income"
|
||||||
form_0: dict[str, str] = self.__get_update_form(txn_id)
|
form_0: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
form_0 = {x: form_0[x] for x in form_0 if "-debit-" not in x}
|
form_0 = {x: form_0[x] for x in form_0 if "-debit-" not in x}
|
||||||
@ -1815,7 +1840,7 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
from accounting.models import Transaction, TransactionCurrency
|
from accounting.models import Transaction, TransactionCurrency
|
||||||
txn_id: int = add_txn(self.client, self.__get_add_form())
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?as=expense&next=%2F_next"
|
||||||
update_uri: str = f"{PREFIX}/{txn_id}/update?as=expense"
|
update_uri: str = f"{PREFIX}/{txn_id}/update?as=expense"
|
||||||
form_0: dict[str, str] = self.__get_update_form(txn_id)
|
form_0: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
form_0 = {x: form_0[x] for x in form_0 if "-credit-" not in x}
|
form_0 = {x: form_0[x] for x in form_0 if "-credit-" not in x}
|
||||||
|
@ -137,48 +137,48 @@ def get_unchanged_update_form(txn_id: int, app: Flask, csrf_token: str) \
|
|||||||
assert txn is not None
|
assert txn is not None
|
||||||
currencies: list[TransactionCurrency] = txn.currencies
|
currencies: list[TransactionCurrency] = txn.currencies
|
||||||
|
|
||||||
form: dict[str, str] = {"csrf_token": csrf_token,
|
form: dict[str, str] = {"csrf_token": csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": NEXT_URI,
|
||||||
"date": txn.date,
|
"date": txn.date,
|
||||||
"note": " \n \n\n " if txn.note is None
|
"note": " \n \n\n " if txn.note is None
|
||||||
else f"\n \n\n \n \n{txn.note} \n\n "}
|
else f"\n \n\n \n \n{txn.note} \n\n "}
|
||||||
currency_indices_used: set[int] = set()
|
currency_indices_used: set[int] = set()
|
||||||
currency_no: int = 0
|
currency_no: int = 0
|
||||||
for currency in currencies:
|
for currency in currencies:
|
||||||
currency_index: int = __get_new_index(currency_indices_used)
|
currency_index: int = __get_new_index(currency_indices_used)
|
||||||
currency_no = currency_no + 3 + randbelow(3)
|
currency_no = currency_no + 3 + randbelow(3)
|
||||||
currency_prefix: str = f"currency-{currency_index}"
|
currency_prefix: str = f"currency-{currency_index}"
|
||||||
form[f"{currency_prefix}-no"] = str(currency_no)
|
form[f"{currency_prefix}-no"] = str(currency_no)
|
||||||
form[f"{currency_prefix}-code"] = currency.code
|
form[f"{currency_prefix}-code"] = currency.code
|
||||||
entry_indices_used: set[int]
|
entry_indices_used: set[int]
|
||||||
entry_no: int
|
entry_no: int
|
||||||
prefix: str
|
prefix: str
|
||||||
|
|
||||||
entry_indices_used = set()
|
entry_indices_used = set()
|
||||||
entry_no = 0
|
entry_no = 0
|
||||||
for entry in currency.debit:
|
for entry in currency.debit:
|
||||||
entry_index: int = __get_new_index(entry_indices_used)
|
entry_index: int = __get_new_index(entry_indices_used)
|
||||||
entry_no = entry_no + 3 + randbelow(3)
|
entry_no = entry_no + 3 + randbelow(3)
|
||||||
prefix = f"{currency_prefix}-debit-{entry_index}"
|
prefix = f"{currency_prefix}-debit-{entry_index}"
|
||||||
form[f"{prefix}-eid"] = str(entry.id)
|
form[f"{prefix}-eid"] = str(entry.id)
|
||||||
form[f"{prefix}-no"] = str(entry_no)
|
form[f"{prefix}-no"] = str(entry_no)
|
||||||
form[f"{prefix}-account_code"] = entry.account.code
|
form[f"{prefix}-account_code"] = entry.account.code
|
||||||
form[f"{prefix}-summary"] \
|
form[f"{prefix}-summary"] \
|
||||||
= " " if entry.summary is None else f" {entry.summary} "
|
= " " if entry.summary is None else f" {entry.summary} "
|
||||||
form[f"{prefix}-amount"] = str(entry.amount)
|
form[f"{prefix}-amount"] = str(entry.amount)
|
||||||
|
|
||||||
entry_indices_used = set()
|
entry_indices_used = set()
|
||||||
entry_no = 0
|
entry_no = 0
|
||||||
for entry in currency.credit:
|
for entry in currency.credit:
|
||||||
entry_index: int = __get_new_index(entry_indices_used)
|
entry_index: int = __get_new_index(entry_indices_used)
|
||||||
entry_no = entry_no + 3 + randbelow(3)
|
entry_no = entry_no + 3 + randbelow(3)
|
||||||
prefix = f"{currency_prefix}-credit-{entry_index}"
|
prefix = f"{currency_prefix}-credit-{entry_index}"
|
||||||
form[f"{prefix}-eid"] = str(entry.id)
|
form[f"{prefix}-eid"] = str(entry.id)
|
||||||
form[f"{prefix}-no"] = str(entry_no)
|
form[f"{prefix}-no"] = str(entry_no)
|
||||||
form[f"{prefix}-account_code"] = entry.account.code
|
form[f"{prefix}-account_code"] = entry.account.code
|
||||||
form[f"{prefix}-summary"] \
|
form[f"{prefix}-summary"] \
|
||||||
= " " if entry.summary is None else f" {entry.summary} "
|
= " " if entry.summary is None else f" {entry.summary} "
|
||||||
form[f"{prefix}-amount"] = str(entry.amount)
|
form[f"{prefix}-amount"] = str(entry.amount)
|
||||||
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user