Compare commits

...

112 Commits

Author SHA1 Message Date
d7bc01ccb4 Updated the Sphinx documentation. 2023-03-09 14:38:45 +08:00
27beff3f8f Renamed the accounting-search and accounting-search-label HTML ID to accounting-toolbar-search and accounting-toolbar-search-label, respectively. 2023-03-09 14:37:03 +08:00
c6c545b99f Removed the unused accounting-search-form, accounting-search-desktop-form, accounting-search-input, and accounting-search-label classes. 2023-03-09 14:37:02 +08:00
6d5a2fae6a Applied the accounting-toolbar class to the base account list, account list, and currency list. 2023-03-09 14:37:02 +08:00
8819eabcd0 Replaced the separated toolbar for the desktop and mobile screen with the accounting-toolbar class that acts differently on different screen sizes. 2023-03-09 14:37:01 +08:00
3582d960ca Replaced the toolbar button group with individual buttons on the reports. 2023-03-09 14:37:01 +08:00
02e10a301a Removed the unused custom "btn-actions" class from the templates. 2023-03-09 14:36:59 +08:00
f0187434d2 Fixed the error from the month chooser in the period chooser when the current period has no start as the default month. 2023-03-09 14:36:59 +08:00
34af52e3c3 Revised the __add_owner_s_equity method of the AccountCollector of the balance sheet to receive the period instead of the URL, and does its job when there is an amount, so that the URL is build only when there is an amount. 2023-03-09 14:36:58 +08:00
965df82c1c Fixed the logic in the __add_owner_s_equity method of the AccountCollector of the balance sheet, that when there is an existing balance, only set the URL when there's amount to be added. 2023-03-09 14:36:57 +08:00
df53f06094 Renamed the "accounting.report.reports.utils.get_url" module to "accounting.report.reports.utils.urls", and shortened the names of the utilities, for readability. 2023-03-09 14:36:56 +08:00
140d3c6010 Added the get_balance_sheet_url utility to replace the common codes to retrieve the URL of an income statement. 2023-03-09 14:36:55 +08:00
a65dccac92 Added the get_journal_url utility to replace the common codes to retrieve the URL of an income statement. 2023-03-09 14:36:55 +08:00
740e1cfac1 Added the get_trial_balance_url utility to replace the common codes to retrieve the URL of an income statement. 2023-03-09 14:36:54 +08:00
b62f31d385 Added the get_income_statement_url utility to replace the common codes to retrieve the URL of an income statement. 2023-03-09 14:36:54 +08:00
1c740b9bbc Added the get_income_expenses_url utility to replace the common codes to retrieve the URL of an income and expenses log. 2023-03-09 14:36:53 +08:00
380256eda7 Revised the imports in the reports. 2023-03-09 14:36:52 +08:00
74b695c089 Added the get_ledger_url utility to replace the common codes to retrieve the URL of a ledger. 2023-03-09 14:36:50 +08:00
6d1e705e4b Revised the documentation of the "accounting.report.reports.utils.csv_export" module. 2023-03-09 14:36:49 +08:00
8abe20dba5 Revised the __set_data method of the trial balance and the __query_balances of the income statement for consistency. 2023-03-09 14:36:48 +08:00
ed7a8ac0fd Added the __query_balance method to the AccountCollector of balance sheet to simplify the queris in the __query_accumulated and __query_currency_period methods. 2023-03-09 14:36:48 +08:00
74eee034d0 Replaced the "transaction" property with the "url" property in the ReportEntry model of the income and expenses log, so that the report entry does not need to keep the transaction object. 2023-03-09 14:36:47 +08:00
d19d23fe37 Replaced the "transaction" property with the "url" property in the ReportEntry model of ledger, so that the report entry does not need to keep the transaction object. 2023-03-09 14:36:46 +08:00
4ce577d7d8 Removed the unused entry property from the ReportEntry model of the income and expenses log. 2023-03-09 14:36:45 +08:00
a340fad109 Removed the unused entry and account properties from the ReportEntry model of the ledger. 2023-03-09 14:36:45 +08:00
555ad388bc Added the debit and credit pseudo properties to the JournalEntry data model, and retired the redundant ReportEntry model from the "accounting.report.reports.journal" module. 2023-03-09 14:36:44 +08:00
2f27ad5bef Replaced querying the transactions later with the "selectinload" query option in the ledger. Retired the unused populate_entries function from the "accounting.report.reports.ledger" module. 2023-03-09 14:36:43 +08:00
c6487bf9d4 Replaced querying the transactions later with the "selectinload" query option in the income and expenses log. Retired the unused populate_entries function from the "accounting.report.reports.income_expenses" module. 2023-03-09 14:36:42 +08:00
ff3dd28cd7 Replaced querying the transactions later with the "selectinload" query option in the journal and search reports. Retired the unused populate_entries function from the "accounting.report.reports.journal" module. 2023-03-09 14:36:42 +08:00
a14ffa93ed Replaced querying the currencies later with the "selectinload" query option in the journal and search reports. 2023-03-09 14:36:41 +08:00
672fcbcbdf Replaced querying the accounts later with the "selectinload" query option in the income and expenses log. 2023-03-09 14:36:41 +08:00
cb4258dd6d Removed the unused "is_total" property from the ReportEntry class of the journal. 2023-03-09 14:36:40 +08:00
6fc21f82af Changed the entry parameter of the ReportEntry class in journal to be non-optional. There is no optional entry in its actual use. 2023-03-09 14:36:40 +08:00
13e3ef5875 Replaced querying the accounts later with the "selectinload" query option in the journal and search reports, and restored the lazy setting in the account relationship of the JournalEntry data model. 2023-03-09 14:36:40 +08:00
21b3320e66 Revised the add-txn-material-fab.html template to simplify the code to include it. 2023-03-09 14:36:39 +08:00
5c47e63ae3 Moved the add-txn-material-fab.html template from the accounting/include directory to the accounting/report/include directory, because it is only used in the reports now. 2023-03-09 14:36:39 +08:00
f59378002e Removed the list_transactions view that is not used now. 2023-03-09 14:36:38 +08:00
531e90e8ad Revised the imports in the "accounting.transaction.view" module. 2023-03-09 14:36:38 +08:00
8fc33131dd Changed the transaction operation to return to the default journal instead of the transaction list. The transaction list is to be removed. There is no link to the transaction list at all, and it's layout is undecided. 2023-03-09 14:36:38 +08:00
62716eb545 Fixed the report chooser to set the current report when the current report is the search page. 2023-03-09 14:36:36 +08:00
14d5d1e8d6 Renamed the action-buttons.html template to toolbar-buttons.html. 2023-03-09 14:36:35 +08:00
4306ed739f Added the is_search property to the report chooser to highlight the search when it is on the search page. 2023-03-09 14:36:34 +08:00
1f87bc00e8 Removed the excess "with_type" from the success redirection of the update_transaction view. 2023-03-09 14:36:33 +08:00
ff9ff4bdcf Removed the excess "with_type" from the success redirection of the delete_transaction view. 2023-03-09 14:36:33 +08:00
578233d66d Renamed the sort_accounts view to sort_transactions in the "accounting.transaction.views" module, and fixed its url endpoints on success. 2023-03-09 14:36:32 +08:00
5e7f790f87 Moved the __get_csv_rows method of the Journal report to the get_csv_rows function, and revised the Search report to use it, because both of their __get_csv_rows methods are identical. 2023-03-09 14:36:32 +08:00
d64f354ee0 Added the DATE_SPEC_RE constant to simplify the regular expression matching in the _parse_period_spec function. 2023-03-09 14:36:32 +08:00
ba3d8c6d4e Removed a redundant test in the _parse_period_spec function in the "accounting.report.period" module. 2023-03-09 14:36:31 +08:00
4f7f87b10d Removed an unused import from the "accounting.report.reports.utils" module. 2023-03-09 14:36:30 +08:00
4273f99644 Fixed the regular expression to match the extra note in the summary for security, as suggested by SonarQube. 2023-03-09 14:36:30 +08:00
ffe834bedd Added the DATE_REQUIRED constant to the "accounting.transaction.forms" module as the common date field validator. 2023-03-09 14:36:29 +08:00
e448e009c9 Simplified the declaration of the "available_years" property in the PeriodChooser utility. 2023-03-09 14:36:29 +08:00
b6802c51bb Removed an excess blank line in the __get_since_desc method of the PeriodDescription utility. 2023-03-09 14:36:29 +08:00
2515c1ea1f Added the __get_since_spec and __get_until_spec methods to simplify the __get_spec method in the PeriodSpecification utility. 2023-03-09 14:36:28 +08:00
0ef6409f75 Revised the documentation of the PeriodDescription utility. 2023-03-09 14:36:28 +08:00
ed18b81ad8 Moved the code to compose the period specification from the Period utility to the PeriodSpecification utility, to simplify the code. 2023-03-09 14:36:27 +08:00
b46cec6fab Updated the translation. 2023-03-09 14:36:27 +08:00
6c122666a0 Revised to simplify the PeriodDescription utility. 2023-03-09 14:36:27 +08:00
7ddc9ececf Added the __format_day method to the PeriodDescription utility to simplify the code. 2023-03-09 14:36:26 +08:00
4eebbd9692 Moved the code to compose the period description from the Period utility to the PeriodDescription utility, to simplify the code. 2023-03-09 14:36:25 +08:00
338b49c965 Added the __get_since_desc and __get_until_desc methods to simplify the __get_desc method in the Period utility. 2023-03-09 14:36:25 +08:00
f438f97571 Revised the styles of the f-strings in the Period utility. 2023-03-09 14:36:24 +08:00
9b273115a0 Removed the empty _set_properties method override from the YearPeriod period. 2023-03-09 14:36:24 +08:00
58d1add810 Added type hints to the CASH_CODE, ACCUMULATED_CHANGE_CODE, and NET_CHANGE_CODE constants. 2023-03-09 14:36:23 +08:00
c189615ca4 Renamed the CASH, ACCUMULATED_CHANGE, and NET_CHANGE constants to CASH_CODE, ACCUMULATED_CHANGE_CODE, and NET_CHANGE_CODE, respectively, to avoid confusion. 2023-03-09 14:36:23 +08:00
5687852dfb Added the _get_currency_options method to the BasePageParams class, and applied it to the currency_options pseudo property of the PageParams classes of the ledger, income and expenses log, trial balance, income statement, and balance sheet reports. 2023-03-09 14:36:22 +08:00
d74c62dbb7 Removed excess property documentation from the Journal and Search classes. 2023-03-09 14:36:22 +08:00
987e98ebc0 Moved the code to collect the report entries to the EntryCollector class in the Search report. 2023-03-09 14:36:21 +08:00
7083f22577 Revised the documentation in the page parameters and the report in the ledger and income and expenses log. 2023-03-09 14:36:21 +08:00
7b10eb68bc Revised the documentation of the EntryCollector class in the ledger and income and expenses log. 2023-03-09 14:36:20 +08:00
f277010991 Renamed the TrialBalanceTotal class to Total, to be short and clear. 2023-03-09 14:36:19 +08:00
729a7fd107 Renamed the TrialBalanceAccount, IncomeStatementAccount, and BalanceSheetAccount classes to ReportAccount, to be short and clear. 2023-03-09 14:36:19 +08:00
c8230c949d Renamed the Entry class to ReportEntry in the journal, ledger, income and expenses log, and search result, to be clear without confusion. 2023-03-09 14:36:18 +08:00
3c98960efe Replaced the Entry CSVRow, and populate_entries in the "accounting.report.reports.search" module with those in the journal module, because their contents are identical. 2023-03-09 14:36:18 +08:00
c5d0d91a7d Renamed the _populate_entries functions to populate_entries in journal, ledger, income and expenses log, and search result, changing them from protected to public so that they can be reused. 2023-03-09 14:36:17 +08:00
fb06e9db44 Shortened the names of the BalanceSheetSubsection and BalanceSheetSubsection classes to Section and Subsection, respectively. 2023-03-09 14:36:17 +08:00
d47e2e231b Shortened the names of the IncomeStatementSection, IncomeStatementSubsection, and IncomeStatementAccumulatedTotal classes to Section, Subsection, and AccumulatedTotal, respectively. 2023-03-09 14:36:17 +08:00
cb89f34455 Renamed the "PageParams" class to "BasePageParams", and renamed its module from "accounting.report.reports.utils.page_params" to "accounting.report.reports.utils.base_page_params". Renamed all its subclasses to PageParams, to shorten their names and make code more readable. 2023-03-09 14:36:17 +08:00
11ab4a4ba6 Revised the documentation of the CSV rows for the reports. 2023-03-09 14:36:16 +08:00
5dc8387ad9 Fixed the incorrect account in the __add_current_period method of the AccountCollector class in the "accounting.report.reports.balance_sheet" module. 2023-03-09 14:36:16 +08:00
26b70bb625 Fixed the logic for all-time in the period_spec function in the "accounting.report.reports.utils.csv_export" module. 2023-03-09 14:36:15 +08:00
f30a96d7e9 Simplified the logic in the period_spec method in the "accounting.report.reports.utils.csv_export" module. 2023-03-09 14:36:15 +08:00
a1627b7fbf Revised to use a simpler way to run the class methods in the __get_desc method of the Period utility, to prevent confusion with SonarQube. 2023-03-09 14:36:14 +08:00
7c3b8c8f44 Revised to store the newly-constructed period chooser and month chooser in variables to prevent SonarQube from complaining. 2023-03-09 14:36:13 +08:00
b19f4fa939 Added "use strict"; to all the JavaScript files. 2023-03-09 14:36:13 +08:00
41c3e06ce4 Removed the period chooser JavaScript from the search result page. 2023-03-09 14:36:12 +08:00
8a3df7a689 Revised the search report to match the amount when the query keyword is a number, instead of matching the amount as a text string. For example, "0150.00" matches 150, while "50" does not match 150. 2023-03-09 14:36:11 +08:00
196a115c99 Revised the coding style in the __get_transaction_condition method of the Search report. 2023-03-09 14:36:11 +08:00
005f9083aa Revised the constructor of the IncomeExpensesAccount pseudo account. 2023-03-09 14:36:10 +08:00
12dbae56c4 Revised the f-strings in the "accounting.models" module. 2023-03-09 14:36:10 +08:00
a98723c57b Removed an unused import from the "accounting.utils.pagination" module. 2023-03-09 14:36:10 +08:00
d5bd3b8383 Fixed an HTML error in the template of the trial balance. 2023-03-09 14:36:09 +08:00
617dd29f23 Added the period_spec function to be used to compose the download file name, to replace the spec property of the Period utility. 2023-03-09 14:36:08 +08:00
b0a4a735f3 Added the is_a_month property to the Period utility. 2023-03-09 14:36:08 +08:00
41770e38b8 Updated the translation. 2023-03-09 14:36:08 +08:00
d8a6614543 Fixed the text for the account used in the titles of the ledger and the income and expenses log. 2023-03-09 14:36:07 +08:00
8d76b5130e Fixed the localization function used in the titles of the reports. 2023-03-09 14:36:07 +08:00
43fc4b9b8d Renamed "total revenue" to "total operating revenue". 2023-03-09 14:36:07 +08:00
3ed8d7f1d2 Removed the now-unused table-row-link.js. It is replaced by the grid display. 2023-03-09 14:36:06 +08:00
ea7c194d7e Revised the period-chooser.html template to simplify the code to include it. 2023-03-09 14:36:06 +08:00
041a905fc0 Added font-awesome icons to the report chooser. 2023-03-09 14:36:05 +08:00
10d1be8bd1 Added the action-buttons.html template and retired the report-chooser.html and currency-chooser.html templates, as the template for the common action buttons. 2023-03-09 14:36:03 +08:00
6e1d35eda4 Revised the report-chooser.html template to simplify the reports. 2023-03-09 14:36:03 +08:00
52b5151fe0 Added the currency-chooser.html template to simplify the templates of the report. 2023-03-09 14:36:02 +08:00
f9fc033de6 Removed the unused RECEIVABLE, PAYABLE, and BROUGHT_FORWARD constants and the unused receivable(), payable(), brought_forward(), and net_change() shortcut methods from the Account data model. 2023-03-09 14:36:02 +08:00
116d00a557 Replaced the hard-coded cash account codes with the ACCUMULATED_CHANGE and NET_CHANGE constants and the accumulated_change() method of the Account data model. 2023-03-09 14:36:01 +08:00
329e3d5362 Replaced the hard-coded cash account codes with the CASH constant and the cash() method of the Account data model. 2023-03-09 14:36:00 +08:00
47e8944f06 Changed the constants of the common account codes in the Account data model from private to public. 2023-03-09 14:36:00 +08:00
e7c43ae390 Revised the documentation to use the term "income and expenses log" instead of "income and expenses", for consistency. 2023-03-09 14:36:00 +08:00
b8b51b34d3 Added the income-expenses-row-desktop.html and ledger-row-desktop.html templates to simplify templates of the income and expenses log and ledger. 2023-03-09 14:36:00 +08:00
d083036719 Renamed the income-expenses-mobile-row.html and ledger-mobile-row.html templates to income-expenses-row-mobile.html and ledger-row-mobile.html, respectively. 2023-03-09 14:35:59 +08:00
7fe81c710b Added the balance-sheet-section.html template to simplify the template of balance sheet. 2023-03-09 14:35:59 +08:00
75 changed files with 2241 additions and 2368 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,61 @@
accounting.report package
=========================
Subpackages
-----------
.. toctree::
:maxdepth: 4
accounting.report.reports
Submodules
----------
accounting.report.converters module
-----------------------------------
.. automodule:: accounting.report.converters
:members:
:undoc-members:
:show-inheritance:
accounting.report.income\_expense\_account module
-------------------------------------------------
.. automodule:: accounting.report.income_expense_account
:members:
:undoc-members:
:show-inheritance:
accounting.report.period module
-------------------------------
.. automodule:: accounting.report.period
:members:
:undoc-members:
:show-inheritance:
accounting.report.template\_filters module
------------------------------------------
.. automodule:: accounting.report.template_filters
:members:
:undoc-members:
:show-inheritance:
accounting.report.views module
------------------------------
.. automodule:: accounting.report.views
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.report
:members:
:undoc-members:
:show-inheritance:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,8 @@
class OptionLink:
"""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.
:param title: The title.
@ -32,3 +33,4 @@ class OptionLink:
self.title: str = title
self.url: str = url
self.is_active: bool = is_active
self.fa_icon: str | None = fa_icon

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,41 +0,0 @@
/* The Mia! Accounting Flask Project
* table-row-link.js: The JavaScript for table rows as links.
*/
/* Copyright (c) 2023 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/3/4
*/
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => {
initializeTableRowLinks();
});
/**
* Initializes the table rows as links.
*
* @private
*/
function initializeTableRowLinks() {
const rows = Array.from(document.getElementsByClassName("accounting-clickable accounting-table-row-link"));
for (const row of rows) {
row.onclick = () => {
window.location = row.dataset.href;
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,43 @@
{#
The Mia! Accounting Flask Project
balance-sheet-section.html: A section in the balance sheet.
Copyright (c) 2023 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/8
#}
<div class="accounting-report-table-row accounting-balance-sheet-section">
<div>{{ section.title.title|title }}</div>
</div>
<div class="accounting-report-table-body">
{% for subsection in section.subsections %}
<div class="accounting-report-table-row accounting-balance-sheet-subsection">
<div>
<span class="d-none d-md-inline">{{ subsection.title.code }}</span>
{{ subsection.title.title|title }}
</div>
</div>
{% for account in subsection.accounts %}
<a class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-account" href="{{ account.url }}">
<div>
<span class="d-none d-md-inline">{{ account.account.code }}</span>
{{ account.account.title|title }}
</div>
<div class="accounting-amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_report_format_amount }}</div>
</a>
{% endfor %}
{% endfor %}
</div>

View File

@ -0,0 +1,27 @@
{#
The Mia! Accounting Flask Project
income-expenses-row-desktop.html: The row in the income and expenses log for the desktop computers
Copyright (c) 2023 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/8
#}
<div>{{ entry.date|accounting_format_date }}</div>
<div>{{ entry.account.title|title }}</div>
<div>{{ entry.summary|accounting_default }}</div>
<div class="accounting-amount">{{ entry.income|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ entry.expense|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>

View File

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

View File

@ -0,0 +1,26 @@
{#
The Mia! Accounting Flask Project
ledger-row-desktop.html: The row in the ledger for the desktop computers
Copyright (c) 2023 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/8
#}
<div>{{ entry.date|accounting_format_date }}</div>
<div>{{ entry.summary|accounting_default }}</div>
<div class="accounting-amount">{{ entry.debit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ entry.credit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>

View File

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

View File

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

View File

@ -1,38 +0,0 @@
{#
The Mia! Accounting Flask Project
report-chooser.html: The report chooser
Copyright (c) 2023 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/4
#}
<div class="btn-group" role="group">
<button id="accounting-report-chooser" class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-book"></i>
<span class="d-none d-md-inline">{{ report_chooser.current_report }}</span>
<span class="d-md-none">{{ A_("Report") }}</span>
</button>
<ul class="dropdown-menu" aria-labelledby="accounting-report-chooser">
{% for report in report_chooser %}
<li><a class="dropdown-item {% if report.is_active %} active {% endif %}" href="{{ report.url }}">{{ report.title }}</a></li>
{% endfor %}
<li>
<span class="dropdown-item accounting-clickable" data-bs-toggle="modal" data-bs-target="#accounting-search-modal">
{{ A_("Search") }}
</span>
</li>
</ul>
</div>

View File

@ -0,0 +1,123 @@
{#
The Mia! Accounting Flask Project
toolbar-buttons.html: The toolbar buttons on the report
Copyright (c) 2023 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/8
#}
{% if accounting_can_edit() %}
<div class="btn-group d-none d-md-flex" role="group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-plus"></i>
<span class="d-none d-md-inline">{{ A_("New") }}</span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash Expense") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash Income") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }}
</a>
</li>
</ul>
</div>
{% endif %}
<div class="btn-group" role="group">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-book"></i>
<span class="d-none d-md-inline">{{ report.report_chooser.current_report }}</span>
</button>
<ul class="dropdown-menu" aria-label="{{ A_("Report") }}">
{% for report in report.report_chooser %}
<li>
<a class="dropdown-item {% if report.is_active %} active {% endif %}" href="{{ report.url }}">
<i class="{{ report.fa_icon }}"></i>
{{ report.title }}
</a>
</li>
{% endfor %}
<li>
<span class="dropdown-item {% if report.report_chooser.is_search %} active {% endif %} accounting-clickable" data-bs-toggle="modal" data-bs-target="#accounting-search-modal">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
</span>
</li>
</ul>
</div>
{% if use_currency_chooser %}
<div class="btn-group" role="group">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-money-bill-wave"></i>
<span class="d-none d-md-inline">{{ report.currency.name|title }}</span>
</button>
<ul class="dropdown-menu" aria-label="{{ A_("Currency") }}">
{% for currency in report.currency_options %}
<li>
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
{{ currency.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if use_account_chooser %}
<div class="btn-group" role="group">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-clipboard"></i>
<span class="d-none d-md-inline">{{ report.account.title|title }}</span>
</button>
<ul class="dropdown-menu" aria-label="{{ A_("Account") }}">
{% for account in report.account_options %}
<li>
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
{{ account.title|title }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if use_period_chooser %}
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
<span class="d-none d-md-inline">{{ report.period.desc|title }}</span>
</button>
{% endif %}
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i>
<span class="d-none d-md-inline">{{ A_("Download") }}</span>
</a>
{% if use_search %}
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
<span class="d-none d-md-inline">{{ A_("Search") }}</span>
</button>
</label>
</form>
{% endif %}

View File

@ -1,6 +1,6 @@
{#
The Mia! Accounting Flask Project
income-expenses.html: The income and expenses
income-expenses.html: The income and expenses log
Copyright (c) 2023 imacat.
@ -24,127 +24,23 @@ First written: 2023/3/5
{% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script>
{% endblock %}
{% block header %}{% block title %}{{ _("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 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 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=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" %}
<div class="mb-3 accounting-toolbar">
{% with use_currency_chooser = true,
use_account_chooser = true,
use_period_chooser = true %}
{% include "accounting/report/include/toolbar-buttons.html" %}
{% 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>
{% with txn_types = report.txn_types %}
{% include "accounting/include/add-txn-material-fab.html" %}
{% endwith %}
{% include "accounting/report/include/add-txn-material-fab.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/period-chooser.html" %}
{% include "accounting/report/include/search-modal.html" %}
@ -168,23 +64,13 @@ First written: 2023/3/5
{% if report.brought_forward %}
{% with entry = report.brought_forward %}
<div class="accounting-report-table-row">
<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>
{% include "accounting/report/include/income-expenses-row-desktop.html" %}
</div>
{% endwith %}
{% endif %}
{% for entry in report.entries %}
<a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
<div>{{ entry.date|accounting_format_date }}</div>
<div>{{ entry.account.title|title }}</div>
<div>{{ entry.summary|accounting_default }}</div>
<div class="accounting-amount">{{ entry.income|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ entry.expense|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
<a class="accounting-report-table-row" href="{{ entry.url|accounting_append_next }}">
{% include "accounting/report/include/income-expenses-row-desktop.html" %}
</a>
{% endfor %}
</div>
@ -206,19 +92,19 @@ First written: 2023/3/5
{% if report.brought_forward %}
{% with entry = report.brought_forward %}
<div class="list-group-item list-group-item-action d-flex justify-content-between">
{% include "accounting/report/include/income-expenses-mobile-row.html" %}
{% include "accounting/report/include/income-expenses-row-mobile.html" %}
</div>
{% endwith %}
{% endif %}
{% for entry in report.entries %}
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
{% include "accounting/report/include/income-expenses-mobile-row.html" %}
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ entry.url|accounting_append_next }}">
{% include "accounting/report/include/income-expenses-row-mobile.html" %}
</a>
{% endfor %}
{% if report.total %}
{% with entry = report.total %}
<div class="list-group-item list-group-item-action d-flex justify-content-between">
{% include "accounting/report/include/income-expenses-mobile-row.html" %}
{% include "accounting/report/include/income-expenses-row-mobile.html" %}
</div>
{% endwith %}
{% endif %}

View File

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

View File

@ -24,69 +24,21 @@ First written: 2023/3/4
{% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{# <script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script> #}
{% endblock %}
{% block header %}{% block title %}{{ _("Journal %(period)s", period=report.period.desc|title) }}{% endblock %}{% endblock %}
{% block header %}{% block title %}{{ A_("Journal %(period)s", period=report.period.desc|title) }}{% 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=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" %}
<div class="mb-3 accounting-toolbar">
{% with use_period_chooser = true %}
{% include "accounting/report/include/toolbar-buttons.html" %}
{% endwith %}
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i>
{{ report.period.desc|title }}
</button>
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i>
{{ A_("Download") }}
</a>
</div>
{% with txn_types = report.txn_types %}
{% include "accounting/include/add-txn-material-fab.html" %}
{% endwith %}
{% include "accounting/report/include/add-txn-material-fab.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/period-chooser.html" %}
{% include "accounting/report/include/search-modal.html" %}

View File

@ -24,127 +24,23 @@ First written: 2023/3/5
{% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script>
{% endblock %}
{% block header %}{% block title %}{{ _("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 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 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=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" %}
<div class="mb-3 accounting-toolbar">
{% with use_currency_chooser = true,
use_account_chooser = true,
use_period_chooser = true %}
{% include "accounting/report/include/toolbar-buttons.html" %}
{% 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>
{% with txn_types = report.txn_types %}
{% include "accounting/include/add-txn-material-fab.html" %}
{% endwith %}
{% include "accounting/report/include/add-txn-material-fab.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/period-chooser.html" %}
{% include "accounting/report/include/search-modal.html" %}
@ -167,21 +63,13 @@ First written: 2023/3/5
{% if report.brought_forward %}
{% with entry = report.brought_forward %}
<div class="accounting-report-table-row">
<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>
{% include "accounting/report/include/ledger-row-desktop.html" %}
</div>
{% endwith %}
{% endif %}
{% for entry in report.entries %}
<a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
<div>{{ entry.date|accounting_format_date }}</div>
<div>{{ entry.summary|accounting_default }}</div>
<div class="accounting-amount">{{ entry.debit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ entry.credit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount {% if entry.balance < 0 %} text-danger {% endif %}">{{ entry.balance|accounting_report_format_amount }}</div>
<a class="accounting-report-table-row" href="{{ entry.url|accounting_append_next }}">
{% include "accounting/report/include/ledger-row-desktop.html" %}
</a>
{% endfor %}
</div>
@ -203,19 +91,19 @@ First written: 2023/3/5
{% if report.brought_forward %}
{% with entry = report.brought_forward %}
<div class="list-group-item list-group-item-action d-flex justify-content-between">
{% include "accounting/report/include/ledger-mobile-row.html" %}
{% include "accounting/report/include/ledger-row-mobile.html" %}
</div>
{% endwith %}
{% endif %}
{% for entry in report.entries %}
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
{% include "accounting/report/include/ledger-mobile-row.html" %}
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ entry.url|accounting_append_next }}">
{% include "accounting/report/include/ledger-row-mobile.html" %}
</a>
{% endfor %}
{% if report.total %}
{% with entry = report.total %}
<div class="list-group-item list-group-item-action d-flex justify-content-between">
{% include "accounting/report/include/ledger-mobile-row.html" %}
{% include "accounting/report/include/ledger-row-mobile.html" %}
</div>
{% endwith %}
{% endif %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,96 +0,0 @@
{#
The Mia! Accounting Flask Project
list.html: The transaction list
Copyright (c) 2023 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/18
#}
{% extends "accounting/base.html" %}
{% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
{% endblock %}
{% block header %}{% block title %}{% if request.args.q %}{{ A_("Search Result for \"%(query)s\"", query=request.args.q) }}{% else %}{{ A_("Transaction Management") }}{% endif %}{% endblock %}{% endblock %}
{% block content %}
<div class="btn-group mb-2 d-none d-md-inline-flex">
{% if accounting_can_edit() %}
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-plus"></i>
{{ A_("New") }}
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.CASH_EXPENSE)|accounting_append_next }}">
{{ A_("Cash Expense") }}</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.CASH_INCOME)|accounting_append_next }}">
{{ A_("Cash Income") }}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=txn_types.TRANSFER)|accounting_append_next }}">
{{ A_("Transfer") }}
</a>
</li>
</ul>
</div>
{% endif %}
<form class="btn btn-primary d-flex input-group accounting-search-desktop-form" action="{{ url_for("accounting.transaction.list") }}" method="get" role="search" aria-label="{{ A_("Search for Desktop") }}">
<input id="accounting-search-desktop" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label for="accounting-search-desktop" class="accounting-search-label">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
</button>
</label>
</form>
</div>
<div class="btn-group mb-2 d-md-none">
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.transaction.list") }}" method="get" role="search" aria-label="{{ A_("Search for Mobile") }}">
<input id="accounting-search-mobile" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label for="accounting-search-mobile" class="accounting-search-label">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
</button>
</label>
</form>
</div>
{% include "accounting/include/add-txn-material-fab.html" %}
{% if list %}
{% include "accounting/include/pagination.html" %}
<div class="list-group">
{% for item in list %}
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.transaction.detail", txn=item)|accounting_append_next }}">
{{ item.date|accounting_format_date }} {{ item }}
</a>
{% endfor %}
</div>
{% else %}
<p>{{ A_("There is no data.") }}</p>
{% endif %}
{% endblock %}

View File

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

View File

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

View File

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

View File

@ -1,65 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The queries for the transaction management.
"""
from datetime import datetime
import sqlalchemy as sa
from flask import request
from accounting.models import Transaction
from accounting.utils.query import parse_query_keywords
def get_transaction_query() -> list[Transaction]:
"""Returns the transactions, optionally filtered by the query.
:return: The transactions.
"""
keywords: list[str] = parse_query_keywords(request.args.get("q"))
if len(keywords) == 0:
return Transaction.query\
.order_by(Transaction.date, Transaction.no).all()
conditions: list[sa.BinaryExpression] = []
for k in keywords:
sub_conditions: list[sa.BinaryExpression] \
= [Transaction.note.contains(k)]
date: datetime
try:
date = datetime.strptime(k, "%Y")
sub_conditions.append(
sa.extract("year", Transaction.date) == date.year)
except ValueError:
pass
try:
date = datetime.strptime(k, "%Y/%m")
sub_conditions.append(sa.and_(
sa.extract("year", Transaction.date) == date.year,
sa.extract("month", Transaction.date) == date.month))
except ValueError:
pass
try:
date = datetime.strptime(f"2000/{k}", "%Y/%m/%d")
sub_conditions.append(sa.and_(
sa.extract("month", Transaction.date) == date.month,
sa.extract("day", Transaction.date) == date.day))
except ValueError:
pass
conditions.append(sa.or_(*sub_conditions))
return Transaction.query.filter(*conditions)\
.order_by(Transaction.date, Transaction.no).all()

View File

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

View File

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

View File

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

View File

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

View File

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