155 Commits

Author SHA1 Message Date
4d11517e21 Advanced to version 0.5.0. 2023-03-10 08:36:22 +08:00
308e4ac69d Updated the Sphinx documentation. 2023-03-10 08:36:08 +08:00
de09e1498b Added the __get_default_page_uri() function to the "accounting.transaction.views" module to simplify the code. 2023-03-10 08:34:44 +08:00
c26c4686c5 Renamed the "original_id" column to "offset_original_id", and the "original" and "offset" relationships to "offset_original" and "offsets", respectively, in the JournalEntry data model. 2023-03-10 08:25:38 +08:00
c95f4fcc47 Added the __str property and changed the query_values property from a pseudo property to a real property in the AccountOption data model, so that it does not need to hold the account object forever. 2023-03-09 22:50:18 +08:00
71af74fc8a Added documentation to the properties of the AccountOption data model. 2023-03-09 22:47:46 +08:00
56e972c371 Fixed so that the download buttons on the report pages are disabled when there is no data. 2023-03-09 22:29:44 +08:00
7feb6da062 Fixed the JavaScript period chooser error when there is no data. 2023-03-09 22:25:26 +08:00
af71874f9d Fixed an error checking if there is any data in the PeriodChooser utility. 2023-03-09 22:20:24 +08:00
3fa8818a27 Added the is_check_as parameter to the get_txn_op function so that the "as" query parameter is not checked when showing the transaction detail. 2023-03-09 22:14:22 +08:00
be46d8aa14 Renamed the default_io_account_code and default_io_account functions to default_ie_account_code and default_ie_account, respectively. That was a mistake. 2023-03-09 20:59:21 +08:00
20f55058ac Shortened the name of the "accounting.report.utils.income_expenses_account" module to "accounting.report.utils.ie_account". 2023-03-09 20:59:21 +08:00
e9d1a53e03 Shortened the name of the "accounting.report.utils.income_expenses_account" module to "accounting.report.utils.ie_account". 2023-03-09 20:59:21 +08:00
38141759fd Removed an excess blank line in the "accounting.report.view" module. 2023-03-09 20:59:20 +08:00
7fb3e3bc2c Shortened the names of the views of the reports. 2023-03-09 20:59:20 +08:00
05ac5158f8 Added the default report view as the income and expenses log with the default currency, default account and default period. Changed the previous default journal links to the current default. 2023-03-09 20:59:09 +08:00
ec257a4b57 Renamed the "accounting.report.period.periods" module to "accounting.report.period.shortcuts", to be clear. 2023-03-09 20:13:15 +08:00
5ebb89a6d5 Moved the month_end utility from the "accounting.report.period.period" module to the new "accounting.report.period.month_end" module. 2023-03-09 19:56:06 +08:00
900d60d1ae Moved the shortcut named periods from the "accounting.report.period.period" module to the "accounting.report.period.periods" module. 2023-03-09 19:44:53 +08:00
bc792c145f Replaced the Period.get_instance method with the get_period function in the "accounting.report.period.parser" module. Changed the parse_spec function in the "accounting.report.period.parser" to private. 2023-03-09 19:40:34 +08:00
4432484acd Replaced the PeriodSpecification object-based utility with the get_spec function-based utility, for simplicity. 2023-03-09 19:30:36 +08:00
7ad3f9e0cb Replaced the PeriodDescription object-based utility with the get_desc function-based utility, for simplicity. 2023-03-09 19:25:43 +08:00
060a52f7a2 Moved the period specification parser from the "accounting.report.period.period" module to the "accounting.report.period.parser" module. 2023-03-09 19:10:21 +08:00
c17430d211 Renamed the "accounting.report.period.period_chooser" module to "accounting.report.period.chooser", for simplicity. 2023-03-09 19:07:58 +08:00
8fd99bb617 Simplified the import of the datetime module in the "accounting.report.period.period" module. 2023-03-09 19:05:27 +08:00
ce388eb6c8 Moved the PeriodSpecification and PeriodDescription utilities from the "accounting.report.period.period" module to the "accounting.report.period.specification" and "accounting.report.period.description" modules, respectively. 2023-03-09 18:57:29 +08:00
1850f9787e Moved the period and period chooser to the "accounting.report.period" module. 2023-03-09 18:30:41 +08:00
c6d55fad1c Renamed the "accounting.report.utils.period_choosers" module to "accounting.report.utils.period_chooser", because there is only period chooser now. 2023-03-09 18:14:20 +08:00
0c647d8f21 Moved the "accounting.reports.period" and "accounting.reports.income_expense_account" utility modules into the "accounting.reports.utils" module. 2023-03-09 18:13:18 +08:00
5d1f87582e Moved the "accounting.report.reports.utils" module to "accounting.report.utils". It does not make sense to have a wierd and long module name just to make the import pretty. 2023-03-09 18:09:08 +08:00
ef086b3f81 Revised to simplify the YearPeriod period. 2023-03-09 18:03:02 +08:00
b4be1db712 Revised the imports in the "accounting.report.reports.utils.period_chooser" module. 2023-03-09 18:00:38 +08:00
5d44ebdfd8 Revised the properties of the Today, Yesterday, and AllTime periods. 2023-03-09 17:58:49 +08:00
9859604c81 Revised the documentation of the _set_properties method of the Period utility. 2023-03-09 17:56:27 +08:00
d31e495f6b Added the AllTime class as a named period. 2023-03-09 17:49:55 +08:00
7c4102be44 Fixed the documentation of the "ReportType.SEARCH" enum item. 2023-03-09 17:49:05 +08:00
1fd50e23d9 Changed the PeriodChooser utility from abstract to real, and replaced the various trivial subclasses with the get_url callable as the parameter. 2023-03-09 17:43:21 +08:00
9635448f18 Added the missing documentation to the sections property of the PageParams data model in the income statement report. 2023-03-09 17:36:33 +08:00
e7f1ca332e Revised the imports in the modules of ledger, income and expenses log, trial balance, and income statement. 2023-03-09 17:32:22 +08:00
3d2e40865e Revised the PeriodChooser utility to find the start of the data by itself. It can do that. It's child classes are all doing the same thing. There is no need to do that in its child classes. 2023-03-09 17:20:52 +08:00
5132141c68 Renamed the "is_pay_off_needed" column of the Account data model to "is_offset_needed", and the "pay_off_target_id" column of the JournalEntry data model to "original_id". 2023-03-09 17:16:05 +08:00
e37f6792c9 Replaced aria-label with aria-labelled-by in the search modal of the report, for simplicity. 2023-03-09 16:42:13 +08:00
e6b1136a14 Fixed so that the brought-forward row is not added for norminal accounts in the ledger. 2023-03-09 16:25:59 +08:00
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
96 changed files with 3177 additions and 3159 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,69 @@
accounting.report.period package
================================
Submodules
----------
accounting.report.period.chooser module
---------------------------------------
.. automodule:: accounting.report.period.chooser
:members:
:undoc-members:
:show-inheritance:
accounting.report.period.description module
-------------------------------------------
.. automodule:: accounting.report.period.description
:members:
:undoc-members:
:show-inheritance:
accounting.report.period.month\_end module
------------------------------------------
.. automodule:: accounting.report.period.month_end
:members:
:undoc-members:
:show-inheritance:
accounting.report.period.parser module
--------------------------------------
.. automodule:: accounting.report.period.parser
:members:
:undoc-members:
:show-inheritance:
accounting.report.period.period module
--------------------------------------
.. automodule:: accounting.report.period.period
:members:
:undoc-members:
:show-inheritance:
accounting.report.period.shortcuts module
-----------------------------------------
.. automodule:: accounting.report.period.shortcuts
:members:
:undoc-members:
:show-inheritance:
accounting.report.period.specification module
---------------------------------------------
.. automodule:: accounting.report.period.specification
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.report.period
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,69 @@
accounting.report.reports package
=================================
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,47 @@
accounting.report package
=========================
Subpackages
-----------
.. toctree::
:maxdepth: 4
accounting.report.period
accounting.report.reports
accounting.report.utils
Submodules
----------
accounting.report.converters module
-----------------------------------
.. automodule:: accounting.report.converters
: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

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@
[metadata] [metadata]
name = mia-accounting-flask name = mia-accounting-flask
version = 0.4.0 version = 0.5.0
author = imacat author = imacat
author_email = imacat@mail.imacat.idv.tw author_email = imacat@mail.imacat.idv.tw
description = The Mia! Accounting Flask project. description = The Mia! Accounting Flask project.

View File

@ -30,7 +30,7 @@ from accounting.utils.user import has_user, get_user_pk
AccountData = tuple[int, str, int, str, str, str, bool] AccountData = tuple[int, str, int, str, str, str, bool]
"""The format of the account data, as a list of (ID, base account code, number, """The format of the account data, as a list of (ID, base account code, number,
English, Traditional Chinese, Simplified Chinese, is-pay-off-needed) tuples.""" English, Traditional Chinese, Simplified Chinese, is-offset-needed) tuples."""
def __validate_username(ctx: click.core.Context, param: click.core.Option, def __validate_username(ctx: click.core.Context, param: click.core.Option,
@ -93,10 +93,10 @@ def init_accounts_command(username: str) -> None:
data: list[AccountData] = [] data: list[AccountData] = []
for base in bases_to_add: for base in bases_to_add:
l10n: dict[str, str] = {x.locale: x.title for x in base.l10n} l10n: dict[str, str] = {x.locale: x.title for x in base.l10n}
is_pay_off_needed: bool = True if re.match("^[12]1[34]", base.code) \ is_offset_needed: bool = True if re.match("^[12]1[34]", base.code) \
else False else False
data.append((get_new_id(), base.code, 1, base.title_l10n, data.append((get_new_id(), base.code, 1, base.title_l10n,
l10n["zh_Hant"], l10n["zh_Hans"], is_pay_off_needed)) l10n["zh_Hant"], l10n["zh_Hans"], is_offset_needed))
__add_accounting_accounts(data, creator_pk) __add_accounting_accounts(data, creator_pk)
click.echo(F"{len(data)} added. Accounting accounts initialized.") click.echo(F"{len(data)} added. Accounting accounts initialized.")
@ -113,7 +113,7 @@ def __add_accounting_accounts(data: list[AccountData], creator_pk: int)\
base_code=x[1], base_code=x[1],
no=x[2], no=x[2],
title_l10n=x[3], title_l10n=x[3],
is_pay_off_needed=x[6], is_offset_needed=x[6],
created_by_id=creator_pk, created_by_id=creator_pk,
updated_by_id=creator_pk) updated_by_id=creator_pk)
for x in data] for x in data]

View File

@ -66,8 +66,8 @@ class AccountForm(FlaskForm):
filters=[strip_text], filters=[strip_text],
validators=[DataRequired(lazy_gettext("Please fill in the title"))]) validators=[DataRequired(lazy_gettext("Please fill in the title"))])
"""The title.""" """The title."""
is_pay_off_needed = BooleanField() is_offset_needed = BooleanField()
"""Whether the the entries of this account need pay-off.""" """Whether the the entries of this account need offset."""
def populate_obj(self, obj: Account) -> None: def populate_obj(self, obj: Account) -> None:
"""Populates the form data into an account object. """Populates the form data into an account object.
@ -87,7 +87,7 @@ class AccountForm(FlaskForm):
obj.base_code = self.base_code.data obj.base_code = self.base_code.data
obj.no = count + 1 obj.no = count + 1
obj.title = self.title.data obj.title = self.title.data
obj.is_pay_off_needed = self.is_pay_off_needed.data obj.is_offset_needed = self.is_offset_needed.data
if is_new: if is_new:
current_user_pk: int = get_current_user_pk() current_user_pk: int = get_current_user_pk()
obj.created_by_id = current_user_pk obj.created_by_id = current_user_pk

View File

@ -47,8 +47,8 @@ def get_account_query() -> list[Account]:
Account.title_l10n.contains(k), Account.title_l10n.contains(k),
code.contains(k), code.contains(k),
Account.id.in_(l10n_matches)] Account.id.in_(l10n_matches)]
if k in gettext("Pay-off needed"): if k in gettext("Need offset"):
sub_conditions.append(Account.is_pay_off_needed) sub_conditions.append(Account.is_offset_needed)
conditions.append(sa.or_(*sub_conditions)) conditions.append(sa.or_(*sub_conditions))
return Account.query.filter(*conditions)\ return Account.query.filter(*conditions)\

View File

@ -52,7 +52,7 @@ class BaseAccount(db.Model):
:return: The string representation of the base account. :return: The string representation of the base account.
""" """
return F"{self.code} {self.title}" return f"{self.code} {self.title}"
@property @property
def title(self) -> str: def title(self) -> str:
@ -113,8 +113,8 @@ class Account(db.Model):
"""The account number under the base account.""" """The account number under the base account."""
title_l10n = db.Column("title", db.String, nullable=False) title_l10n = db.Column("title", db.String, nullable=False)
"""The title.""" """The title."""
is_pay_off_needed = db.Column(db.Boolean, nullable=False, default=False) is_offset_needed = db.Column(db.Boolean, nullable=False, default=False)
"""Whether the entries of this account need pay-off.""" """Whether the entries of this account need offset."""
created_at = db.Column(db.DateTime(timezone=True), nullable=False, created_at = db.Column(db.DateTime(timezone=True), nullable=False,
server_default=db.func.now()) server_default=db.func.now())
"""The time of creation.""" """The time of creation."""
@ -141,17 +141,11 @@ class Account(db.Model):
entries = db.relationship("JournalEntry", back_populates="account") entries = db.relationship("JournalEntry", back_populates="account")
"""The journal entries.""" """The journal entries."""
__CASH = "1111-001" CASH_CODE: str = "1111-001"
"""The code of the cash account,""" """The code of the cash account,"""
__RECEIVABLE = "1141-001" ACCUMULATED_CHANGE_CODE: str = "3351-001"
"""The code of the receivable account,"""
__PAYABLE = "2141-001"
"""The code of the payable account,"""
__ACCUMULATED_CHANGE = "3351-001"
"""The code of the accumulated-change account,""" """The code of the accumulated-change account,"""
__BROUGHT_FORWARD = "3352-001" NET_CHANGE_CODE: str = "3353-001"
"""The code of the brought-forward account,"""
__NET_CHANGE = "3353-001"
"""The code of the net-change account,""" """The code of the net-change account,"""
def __str__(self) -> str: def __str__(self) -> str:
@ -159,7 +153,7 @@ class Account(db.Model):
:return: The string representation of this account. :return: The string representation of this account.
""" """
return F"{self.base_code}-{self.no:03d} {self.title}" return f"{self.base_code}-{self.no:03d} {self.title}"
@property @property
def code(self) -> str: def code(self) -> str:
@ -167,7 +161,7 @@ class Account(db.Model):
:return: The code. :return: The code.
""" """
return F"{self.base_code}-{self.no:03d}" return f"{self.base_code}-{self.no:03d}"
@property @property
def title(self) -> str: def title(self) -> str:
@ -271,23 +265,7 @@ class Account(db.Model):
:return: The cash account :return: The cash account
""" """
return cls.find_by_code(cls.__CASH) return cls.find_by_code(cls.CASH_CODE)
@classmethod
def receivable(cls) -> t.Self:
"""Returns the receivable account.
:return: The receivable account
"""
return cls.find_by_code(cls.__RECEIVABLE)
@classmethod
def payable(cls) -> t.Self:
"""Returns the payable account.
:return: The payable account
"""
return cls.find_by_code(cls.__PAYABLE)
@classmethod @classmethod
def accumulated_change(cls) -> t.Self: def accumulated_change(cls) -> t.Self:
@ -295,23 +273,7 @@ class Account(db.Model):
:return: The accumulated-change account :return: The accumulated-change account
""" """
return cls.find_by_code(cls.__ACCUMULATED_CHANGE) return cls.find_by_code(cls.ACCUMULATED_CHANGE_CODE)
@classmethod
def brought_forward(cls) -> t.Self:
"""Returns the brought-forward account.
:return: The brought-forward account
"""
return cls.find_by_code(cls.__BROUGHT_FORWARD)
@classmethod
def net_change(cls) -> t.Self:
"""Returns the net-change account.
:return: The net-change account
"""
return cls.find_by_code(cls.__NET_CHANGE)
@property @property
def is_modified(self) -> bool: def is_modified(self) -> bool:
@ -392,7 +354,7 @@ class Currency(db.Model):
:return: The string representation of the currency. :return: The string representation of the currency.
""" """
return F"{self.name} ({self.code})" return f"{self.name} ({self.code})"
@property @property
def name(self) -> str: def name(self) -> str:
@ -588,7 +550,7 @@ class Transaction(db.Model):
for currency in self.currencies: for currency in self.currencies:
if len(currency.debit) > 1: if len(currency.debit) > 1:
return False return False
if currency.debit[0].account.code != "1111-001": if currency.debit[0].account.code != Account.CASH_CODE:
return False return False
return True return True
@ -602,7 +564,7 @@ class Transaction(db.Model):
for currency in self.currencies: for currency in self.currencies:
if len(currency.credit) > 1: if len(currency.credit) > 1:
return False return False
if currency.credit[0].account.code != "1111-001": if currency.credit[0].account.code != Account.CASH_CODE:
return False return False
return True return True
@ -635,15 +597,15 @@ class JournalEntry(db.Model):
"""True for a debit entry, or False for a credit entry.""" """True for a debit entry, or False for a credit entry."""
no = db.Column(db.Integer, nullable=False) no = db.Column(db.Integer, nullable=False)
"""The entry number under the transaction and debit or credit.""" """The entry number under the transaction and debit or credit."""
pay_off_target_id = db.Column(db.Integer, offset_original_id = db.Column(db.Integer,
db.ForeignKey(id, onupdate="CASCADE"), db.ForeignKey(id, onupdate="CASCADE"),
nullable=True) nullable=True)
"""The ID of the pay-off target entry.""" """The ID of the original entry to offset."""
pay_off_target = db.relationship("JournalEntry", back_populates="pay_off", offset_original = db.relationship("JournalEntry", back_populates="offsets",
remote_side=id, passive_deletes=True) remote_side=id, passive_deletes=True)
"""The pay-off target entry.""" """The original entry to offset."""
pay_off = db.relationship("JournalEntry", back_populates="pay_off_target") offsets = db.relationship("JournalEntry", back_populates="offset_original")
"""The pay-off entries.""" """The offset entries."""
currency_code = db.Column(db.String, currency_code = db.Column(db.String,
db.ForeignKey(Currency.code, onupdate="CASCADE"), db.ForeignKey(Currency.code, onupdate="CASCADE"),
nullable=False) nullable=False)
@ -655,7 +617,7 @@ class JournalEntry(db.Model):
onupdate="CASCADE"), onupdate="CASCADE"),
nullable=False) nullable=False)
"""The account ID.""" """The account ID."""
account = db.relationship(Account, back_populates="entries") account = db.relationship(Account, back_populates="entries", lazy=False)
"""The account.""" """The account."""
summary = db.Column(db.String, nullable=True) summary = db.Column(db.String, nullable=True)
"""The summary.""" """The summary."""
@ -678,3 +640,19 @@ class JournalEntry(db.Model):
:return: The account code. :return: The account code.
""" """
return self.account.code return self.account.code
@property
def debit(self) -> Decimal | None:
"""Returns the debit amount.
:return: The debit amount, or None if this is not a debit entry.
"""
return self.amount if self.is_debit else None
@property
def credit(self) -> Decimal | None:
"""Returns the credit amount.
:return: The credit amount, or None if this is not a credit entry.
"""
return None if self.is_debit else self.amount

View File

@ -29,7 +29,7 @@ def init_app(app: Flask, bp: Blueprint) -> None:
""" """
from .converters import PeriodConverter, IncomeExpensesAccountConverter from .converters import PeriodConverter, IncomeExpensesAccountConverter
app.url_map.converters["period"] = PeriodConverter app.url_map.converters["period"] = PeriodConverter
app.url_map.converters["ioAccount"] = IncomeExpensesAccountConverter app.url_map.converters["ieAccount"] = IncomeExpensesAccountConverter
from .views import bp as report_bp from .views import bp as report_bp
bp.register_blueprint(report_bp, url_prefix="/reports") bp.register_blueprint(report_bp, url_prefix="/reports")

View File

@ -23,8 +23,8 @@ from flask import abort
from werkzeug.routing import BaseConverter from werkzeug.routing import BaseConverter
from accounting.models import Account from accounting.models import Account
from .income_expense_account import IncomeExpensesAccount from .period import Period, get_period
from .period import Period from .utils.ie_account import IncomeExpensesAccount
class PeriodConverter(BaseConverter): class PeriodConverter(BaseConverter):
@ -38,7 +38,7 @@ class PeriodConverter(BaseConverter):
:return: The corresponding period. :return: The corresponding period.
""" """
try: try:
return Period.get_instance(value) return get_period(value)
except ValueError: except ValueError:
abort(404) abort(404)
@ -52,8 +52,8 @@ class PeriodConverter(BaseConverter):
class IncomeExpensesAccountConverter(BaseConverter): class IncomeExpensesAccountConverter(BaseConverter):
"""The supplier converter to convert the income and expenses pseudo account """The supplier converter to convert the income and expenses log pseudo
code from and to the corresponding pseudo account in the routes.""" account code from and to the corresponding pseudo account in the routes."""
def to_python(self, value: str) -> IncomeExpensesAccount: def to_python(self, value: str) -> IncomeExpensesAccount:
"""Converts an account code to an account. """Converts an account code to an account.

View File

@ -1,555 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The date period.
This file is largely taken from the NanoParma ERP project, first written in
2021/9/16 by imacat (imacat@nanoparma.com).
"""
import calendar
import datetime
import re
import typing as t
from accounting.locale import gettext
class Period:
"""A date period."""
def __init__(self, start: datetime.date | None, end: datetime.date | None):
"""Constructs a new date period.
:param start: The start date, or None from the very beginning.
:param end: The end date, or None till no end.
"""
self.start: datetime.date | None = start
"""The start of the period."""
self.end: datetime.date | None = end
"""The end of the period."""
self.is_default: bool = False
"""Whether the is the default period."""
self.is_this_month: bool = False
"""Whether the period is this month."""
self.is_last_month: bool = False
"""Whether the period is last month."""
self.is_since_last_month: bool = False
"""Whether the period is since last month."""
self.is_this_year: bool = False
"""Whether the period is this year."""
self.is_last_year: bool = False
"""Whether the period is last year."""
self.is_today: bool = False
"""Whether the period is today."""
self.is_yesterday: bool = False
"""Whether the period is yesterday."""
self.is_all: bool = start is None and end is None
"""Whether the period is all time."""
self.spec: str = ""
"""The period specification."""
self.desc: str = ""
"""The text description."""
self.is_type_month: bool = False
"""Whether the period is for the month chooser."""
self.is_a_year: bool = False
"""Whether the period is a whole year."""
self.is_a_day: bool = False
"""Whether the period is a single day."""
self._set_properties()
def _set_properties(self) -> None:
"""Sets the following properties.
* self.spec
* self.desc
* self.is_a_month
* self.is_type_month
* self.is_a_year
* self.is_a_day
Override this method to set the properties in the subclasses.
:return: None.
"""
self.spec = self.__get_spec()
self.desc = self.__get_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_year = self.start == datetime.date(self.start.year, 1, 1) \
and self.end == datetime.date(self.start.year, 12, 31)
self.is_a_day = self.start == self.end
@classmethod
def get_instance(cls, spec: str | None = None) -> t.Self:
"""Returns a period instance.
:param spec: The period specification, or omit for the default.
:return: The period instance.
:raise ValueError: When the period is invalid.
"""
if spec is None:
return ThisMonth()
named_periods: dict[str, t.Type[t.Callable[[], Period]]] = {
"this-month": lambda: ThisMonth(),
"last-month": lambda: LastMonth(),
"since-last-month": lambda: SinceLastMonth(),
"this-year": lambda: ThisYear(),
"last-year": lambda: LastYear(),
"today": lambda: Today(),
"yesterday": lambda: Yesterday(),
}
if spec in named_periods:
return named_periods[spec]()
start: datetime.date
end: datetime.date
start, end = _parse_period_spec(spec)
if start is not None and end is not None and start > end:
raise ValueError
return cls(start, end)
def __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.
:param year: The year.
:return: True if the period is the year period, or False otherwise.
"""
if not self.is_a_year:
return False
return self.start.year == year
@property
def is_type_arbitrary(self) -> bool:
"""Returns whether this period is an arbitrary period.
:return: True if this is an arbitrary period, or False otherwise.
"""
return not self.is_type_month and not self.is_a_year \
and not self.is_a_day
@property
def before(self) -> t.Self | None:
"""Returns the period before this period.
:return: The period before this period.
"""
if self.start is None:
return None
return Period(None, self.start - datetime.timedelta(days=1))
class ThisMonth(Period):
"""The period of this month."""
def __init__(self):
today: datetime.date = datetime.date.today()
this_month_start: datetime.date \
= datetime.date(today.year, today.month, 1)
super().__init__(this_month_start, _month_end(today))
self.is_default = True
self.is_this_month = True
def _set_properties(self) -> None:
self.spec = "this-month"
self.desc = gettext("This month")
self.is_a_month = True
self.is_type_month = True
class LastMonth(Period):
"""The period of this month."""
def __init__(self):
today: datetime.date = datetime.date.today()
year: int = today.year
month: int = today.month - 1
if month < 1:
year = year - 1
month = 12
start: datetime.date = datetime.date(year, month, 1)
super().__init__(start, _month_end(start))
self.is_last_month = True
def _set_properties(self) -> None:
self.spec = "last-month"
self.desc = gettext("Last month")
self.is_a_month = True
self.is_type_month = True
class SinceLastMonth(Period):
"""The period of this month."""
def __init__(self):
today: datetime.date = datetime.date.today()
year: int = today.year
month: int = today.month - 1
if month < 1:
year = year - 1
month = 12
start: datetime.date = datetime.date(year, month, 1)
super().__init__(start, None)
self.is_since_last_month = True
def _set_properties(self) -> None:
self.spec = "since-last-month"
self.desc = gettext("Since last month")
self.is_type_month = True
class ThisYear(Period):
"""The period of this year."""
def __init__(self):
year: int = datetime.date.today().year
start: datetime.date = datetime.date(year, 1, 1)
end: datetime.date = datetime.date(year, 12, 31)
super().__init__(start, end)
self.is_this_year = True
def _set_properties(self) -> None:
self.spec = "this-year"
self.desc = gettext("This year")
self.is_a_year = True
class LastYear(Period):
"""The period of last year."""
def __init__(self):
year: int = datetime.date.today().year
start: datetime.date = datetime.date(year - 1, 1, 1)
end: datetime.date = datetime.date(year - 1, 12, 31)
super().__init__(start, end)
self.is_last_year = True
def _set_properties(self) -> None:
self.spec = "last-year"
self.desc = gettext("Last year")
self.is_a_year = True
class Today(Period):
"""The period of today."""
def __init__(self):
today: datetime.date = datetime.date.today()
super().__init__(today, today)
self.is_this_year = True
def _set_properties(self) -> None:
self.spec = "today"
self.desc = gettext("Today")
self.is_a_day = True
self.is_today = True
class Yesterday(Period):
"""The period of yesterday."""
def __init__(self):
yesterday: datetime.date \
= datetime.date.today() - datetime.timedelta(days=1)
super().__init__(yesterday, yesterday)
self.is_this_year = True
def _set_properties(self) -> None:
self.spec = "yesterday"
self.desc = gettext("Yesterday")
self.is_a_day = True
self.is_yesterday = True
class TemplatePeriod(Period):
"""The period template."""
def __init__(self):
super().__init__(None, None)
def _set_properties(self) -> None:
self.spec = "PERIOD"
class YearPeriod(Period):
"""A year period."""
def __init__(self, year: int):
"""Constructs a year period.
:param year: The year.
"""
start: datetime.date = datetime.date(year, 1, 1)
end: datetime.date = datetime.date(year, 12, 31)
super().__init__(start, end)
self.spec = str(year)
self.is_a_year = True
def _set_properties(self) -> None:
pass
def _parse_period_spec(text: str) \
-> tuple[datetime.date | None, datetime.date | None]:
"""Parses the period specification.
:param text: The period specification.
:return: The start and end day of the period. The start and end day
may be None.
:raise ValueError: When the date is invalid.
"""
if text == "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)
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)
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)
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)
if m is not None:
return __get_start(m[1], m[2], m[3]), \
__get_end(m[4], m[5], m[6])
raise ValueError
def __get_start(year: str, month: str | None, day: str | None)\
-> datetime.date:
"""Returns the start of the period from the date representation.
:param year: The year.
:param month: The month, if any.
:param day: The day, if any.
:return: The start of the period.
:raise ValueError: When the date is invalid.
"""
if day is not None:
return datetime.date(int(year), int(month), int(day))
if month is not None:
return datetime.date(int(year), int(month), 1)
return datetime.date(int(year), 1, 1)
def __get_end(year: str, month: str | None, day: str | None)\
-> datetime.date:
"""Returns the end of the period from the date representation.
:param year: The year.
:param month: The month, if any.
:param day: The day, if any.
:return: The end of the period.
:raise ValueError: When the date is invalid.
"""
if day is not None:
return datetime.date(int(year), int(month), int(day))
if month is not None:
year_n: int = int(year)
month_n: int = int(month)
day_n: int = calendar.monthrange(year_n, month_n)[1]
return datetime.date(year_n, month_n, day_n)
return datetime.date(int(year), 12, 31)
def _month_end(date: datetime.date) -> datetime.date:
"""Returns the end day of month for a date.
:param date: The date.
:return: The end day of the month of that day.
"""
day: int = calendar.monthrange(date.year, date.month)[1]
return datetime.date(date.year, date.month, day)

View File

@ -0,0 +1,22 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The period utility.
"""
from .chooser import PeriodChooser
from .parser import get_period
from .period import Period

View File

@ -0,0 +1,97 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The period chooser.
This file is largely taken from the NanoParma ERP project, first written in
2021/9/16 by imacat (imacat@nanoparma.com).
"""
import typing as t
from datetime import date
from accounting.models import Transaction
from .period import Period
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
LastYear, Today, Yesterday, AllTime, TemplatePeriod, YearPeriod
class PeriodChooser:
"""The period chooser."""
def __init__(self, get_url: t.Callable[[Period], str]):
"""Constructs a period chooser.
:param get_url: The callback to return the URL of the current report in
a period.
"""
self.__get_url: t.Callable[[Period], str] = get_url
"""The callback to return the URL of the current report in a period."""
# Shortcut periods
self.this_month_url: str = get_url(ThisMonth())
"""The URL for this month."""
self.last_month_url: str = get_url(LastMonth())
"""The URL for last month."""
self.since_last_month_url: str = get_url(SinceLastMonth())
"""The URL since last mint."""
self.this_year_url: str = get_url(ThisYear())
"""The URL for this year."""
self.last_year_url: str = get_url(LastYear())
"""The URL for last year."""
self.today_url: str = get_url(Today())
"""The URL for today."""
self.yesterday_url: str = get_url(Yesterday())
"""The URL for yesterday."""
self.all_url: str = get_url(AllTime())
"""The URL for all period."""
self.url_template: str = get_url(TemplatePeriod())
"""The URL template."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
start: date | None = None if first is None else first.date
# Attributes
self.data_start: date | None = start
"""The start of the data."""
self.has_data: bool = start is not None
"""Whether there is any data."""
self.has_last_month: bool = False
"""Where there is data in last month."""
self.has_last_year: bool = False
"""Whether there is data in last year."""
self.has_yesterday: bool = False
"""Whether there is data in yesterday."""
self.available_years: list[int] = []
"""The available years."""
if self.has_data:
today: date = date.today()
self.has_last_month = start < date(today.year, today.month, 1)
self.has_last_year = start.year < today.year
self.has_yesterday = start < today
if start.year < today.year - 1:
self.available_years \
= reversed(range(start.year, today.year - 1))
def year_url(self, year: int) -> str:
"""Returns the period URL of a year.
:param year: The year
:return: The period URL of the year.
"""
return self.__get_url(YearPeriod(year))

View File

@ -0,0 +1,179 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The period description composer.
"""
from datetime import date, timedelta
from accounting.locale import gettext
def get_desc(start: date | None, end: date | None) -> str:
"""Returns the period description.
:param start: The start of the period.
:param end: The end of the period.
:return: The period description.
"""
if start is None and end is None:
return gettext("for all time")
if start is None:
return __get_until_desc(end)
if end is None:
return __get_since_desc(start)
try:
return __get_year_desc(start, end)
except ValueError:
pass
try:
return __get_month_desc(start, end)
except ValueError:
pass
return __get_day_desc(start, end)
def __get_since_desc(start: date) -> str:
"""Returns the description without the end day.
:param start: The start of the period.
:return: The description without the end day.
"""
def get_start_desc() -> str:
"""Returns the description of the start day.
:return: The description of the start day.
"""
if start.month == 1 and start.day == 1:
return str(start.year)
if start.day == 1:
return __format_month(start)
return __format_day(start)
return gettext("since %(start)s", start=get_start_desc())
def __get_until_desc(end: date) -> str:
"""Returns the description without the start day.
:param end: The end of the period.
:return: The description without the start day.
"""
def get_end_desc() -> str:
"""Returns the description of the end day.
:return: The description of the end day.
"""
if end.month == 12 and end.day == 31:
return str(end.year)
if (end + timedelta(days=1)).day == 1:
return __format_month(end)
return __format_day(end)
return gettext("until %(end)s", end=get_end_desc())
def __get_year_desc(start: date, end: date) -> str:
"""Returns the description as a year range.
:param start: The start of the period.
:param end: The end of the period.
:return: The description as a year range.
:raise ValueError: The period is not a year range.
"""
if start.month != 1 or start.day != 1 \
or end.month != 12 or end.day != 31:
raise ValueError
start_text: str = str(start.year)
if start.year == end.year:
return __get_in_desc(start_text)
return __get_from_to_desc(start_text, str(end.year))
def __get_month_desc(start: date, end: date) -> str:
"""Returns the description as a month range.
:param start: The start of the period.
:param end: The end of the period.
:return: The description as a month range.
:raise ValueError: The period is not a month range.
"""
if start.day != 1 or (end + timedelta(days=1)).day != 1:
raise ValueError
start_text: str = __format_month(start)
if start.year == end.year and start.month == end.month:
return __get_in_desc(start_text)
if start.year == end.year:
return __get_from_to_desc(start_text, str(end.month))
return __get_from_to_desc(start_text, __format_month(end))
def __get_day_desc(start: date, end: date) -> str:
"""Returns the description as a day range.
:param start: The start of the period.
:param end: The end of the period.
:return: The description as a day range.
:raise ValueError: The period is a month or year range.
"""
start_text: str = __format_day(start)
if start == end:
return __get_in_desc(start_text)
if start.year == end.year and start.month == end.month:
return __get_from_to_desc(start_text, str(end.day))
if start.year == end.year:
end_month_day: str = f"{end.month}/{end.day}"
return __get_from_to_desc(start_text, end_month_day)
return __get_from_to_desc(start_text, __format_day(end))
def __format_month(month: date) -> str:
"""Formats a month.
:param month: The month.
:return: The formatted month.
"""
return f"{month.year}/{month.month}"
def __format_day(day: date) -> str:
"""Formats a day.
:param day: The day.
:return: The formatted day.
"""
return f"{day.year}/{day.month}/{day.day}"
def __get_in_desc(period: str) -> str:
"""Returns the description of a whole year, month, or day.
:param period: The time period.
:return: The description of a whole year, month, or day.
"""
return gettext("in %(period)s", period=period)
def __get_from_to_desc(start: str, end: str) -> str:
"""Returns the description of a separated start and end.
:param start: The start.
:param end: The end.
:return: The description of the separated start and end.
"""
return gettext("in %(start)s-%(end)s", start=start, end=end)

View File

@ -0,0 +1,31 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The utility to return the end of a month.
"""
import calendar
from datetime import date
def month_end(day: date) -> date:
"""Returns the end day of month for a date.
:param day: The date.
:return: The end day of the month of that day.
"""
last_day: int = calendar.monthrange(day.year, day.month)[1]
return date(day.year, day.month, last_day)

View File

@ -0,0 +1,119 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The period specification parser.
"""
import calendar
import re
import typing as t
from datetime import date
from .period import Period
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
LastYear, Today, Yesterday, AllTime
DATE_SPEC_RE: str = r"(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?"
"""The regular expression of a date specification."""
def get_period(spec: str | None = None) -> Period:
"""Returns a period instance.
:param spec: The period specification, or omit for the default.
:return: The period instance.
:raise ValueError: When the period specification is invalid.
"""
if spec is None:
return ThisMonth()
named_periods: dict[str, t.Type[t.Callable[[], Period]]] = {
"this-month": lambda: ThisMonth(),
"last-month": lambda: LastMonth(),
"since-last-month": lambda: SinceLastMonth(),
"this-year": lambda: ThisYear(),
"last-year": lambda: LastYear(),
"today": lambda: Today(),
"yesterday": lambda: Yesterday(),
"all-time": lambda: AllTime(),
}
if spec in named_periods:
return named_periods[spec]()
start, end = __parse_spec(spec)
if start is not None and end is not None and start > end:
raise ValueError
return Period(start, end)
def __parse_spec(text: str) -> tuple[date | None, date | None]:
"""Parses the period specification.
:param text: The period specification.
:return: The start and end day of the period. The start and end day
may be None.
:raise ValueError: When the date is invalid.
"""
if text == "-":
return None, None
m = re.match(f"^{DATE_SPEC_RE}$", text)
if m is not None:
return __get_start(m[1], m[2], m[3]), \
__get_end(m[1], m[2], m[3])
m = re.match(f"^{DATE_SPEC_RE}-$", text)
if m is not None:
return __get_start(m[1], m[2], m[3]), None
m = re.match(f"-{DATE_SPEC_RE}$", text)
if m is not None:
return None, __get_end(m[1], m[2], m[3])
m = re.match(f"^{DATE_SPEC_RE}-{DATE_SPEC_RE}$", text)
if m is not None:
return __get_start(m[1], m[2], m[3]), \
__get_end(m[4], m[5], m[6])
raise ValueError
def __get_start(year: str, month: str | None, day: str | None) -> date:
"""Returns the start of the period from the date representation.
:param year: The year.
:param month: The month, if any.
:param day: The day, if any.
:return: The start of the period.
:raise ValueError: When the date is invalid.
"""
if day is not None:
return date(int(year), int(month), int(day))
if month is not None:
return date(int(year), int(month), 1)
return date(int(year), 1, 1)
def __get_end(year: str, month: str | None, day: str | None) -> date:
"""Returns the end of the period from the date representation.
:param year: The year.
:param month: The month, if any.
:param day: The day, if any.
:return: The end of the period.
:raise ValueError: When the date is invalid.
"""
if day is not None:
return date(int(year), int(month), int(day))
if month is not None:
year_n: int = int(year)
month_n: int = int(month)
day_n: int = calendar.monthrange(year_n, month_n)[1]
return date(year_n, month_n, day_n)
return date(int(year), 12, 31)

View File

@ -0,0 +1,129 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The date period.
This file is largely taken from the NanoParma ERP project, first written in
2021/9/16 by imacat (imacat@nanoparma.com).
"""
import typing as t
from datetime import date, timedelta
from .description import get_desc
from .month_end import month_end
from .specification import get_spec
class Period:
"""A date period."""
def __init__(self, start: date | None, end: date | None):
"""Constructs a new date period.
:param start: The start date, or None from the very beginning.
:param end: The end date, or None till no end.
"""
self.start: date | None = start
"""The start of the period."""
self.end: date | None = end
"""The end of the period."""
self.is_default: bool = False
"""Whether the is the default period."""
self.is_this_month: bool = False
"""Whether the period is this month."""
self.is_last_month: bool = False
"""Whether the period is last month."""
self.is_since_last_month: bool = False
"""Whether the period is since last month."""
self.is_this_year: bool = False
"""Whether the period is this year."""
self.is_last_year: bool = False
"""Whether the period is last year."""
self.is_today: bool = False
"""Whether the period is today."""
self.is_yesterday: bool = False
"""Whether the period is yesterday."""
self.is_all: bool = start is None and end is None
"""Whether the period is all time."""
self.spec: str = ""
"""The period specification."""
self.desc: str = ""
"""The text description."""
self.is_a_month: bool = False
"""Whether the period is a whole month."""
self.is_type_month: bool = False
"""Whether the period is for the month chooser."""
self.is_a_year: bool = False
"""Whether the period is a whole year."""
self.is_a_day: bool = False
"""Whether the period is a single day."""
self._set_properties()
def _set_properties(self) -> None:
"""Sets the following properties.
* self.spec
* self.desc
* self.is_a_month
* self.is_type_month
* self.is_a_year
* self.is_a_day
Override this method to set the properties in the subclasses, to skip
the calculation.
:return: None.
"""
self.spec = get_spec(self.start, self.end)
self.desc = get_desc(self.start, self.end)
if self.start is None or self.end is None:
return
self.is_a_month = self.start.day == 1 \
and self.end == month_end(self.start)
self.is_type_month = self.is_a_month
self.is_a_year = self.start == date(self.start.year, 1, 1) \
and self.end == date(self.start.year, 12, 31)
self.is_a_day = self.start == self.end
def is_year(self, year: int) -> bool:
"""Returns whether the period is the specific year period.
:param year: The year.
:return: True if the period is the year period, or False otherwise.
"""
if not self.is_a_year:
return False
return self.start.year == year
@property
def is_type_arbitrary(self) -> bool:
"""Returns whether this period is an arbitrary period.
:return: True if this is an arbitrary period, or False otherwise.
"""
return not self.is_type_month and not self.is_a_year \
and not self.is_a_day
@property
def before(self) -> t.Self | None:
"""Returns the period before this period.
:return: The period before this period.
"""
if self.start is None:
return None
return Period(None, self.start - timedelta(days=1))

View File

@ -0,0 +1,168 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The named shortcut periods.
"""
from datetime import date, timedelta
from accounting.locale import gettext
from .month_end import month_end
from .period import Period
class ThisMonth(Period):
"""The period of this month."""
def __init__(self):
today: date = date.today()
this_month_start: date = date(today.year, today.month, 1)
super().__init__(this_month_start, month_end(today))
self.is_default = True
self.is_this_month = True
def _set_properties(self) -> None:
self.spec = "this-month"
self.desc = gettext("This month")
self.is_a_month = True
self.is_type_month = True
class LastMonth(Period):
"""The period of this month."""
def __init__(self):
today: date = date.today()
year: int = today.year
month: int = today.month - 1
if month < 1:
year = year - 1
month = 12
start: date = date(year, month, 1)
super().__init__(start, month_end(start))
self.is_last_month = True
def _set_properties(self) -> None:
self.spec = "last-month"
self.desc = gettext("Last month")
self.is_a_month = True
self.is_type_month = True
class SinceLastMonth(Period):
"""The period of this month."""
def __init__(self):
today: date = date.today()
year: int = today.year
month: int = today.month - 1
if month < 1:
year = year - 1
month = 12
start: date = date(year, month, 1)
super().__init__(start, None)
self.is_since_last_month = True
def _set_properties(self) -> None:
self.spec = "since-last-month"
self.desc = gettext("Since last month")
self.is_type_month = True
class ThisYear(Period):
"""The period of this year."""
def __init__(self):
year: int = date.today().year
start: date = date(year, 1, 1)
end: date = date(year, 12, 31)
super().__init__(start, end)
self.is_this_year = True
def _set_properties(self) -> None:
self.spec = "this-year"
self.desc = gettext("This year")
self.is_a_year = True
class LastYear(Period):
"""The period of last year."""
def __init__(self):
year: int = date.today().year
start: date = date(year - 1, 1, 1)
end: date = date(year - 1, 12, 31)
super().__init__(start, end)
self.is_last_year = True
def _set_properties(self) -> None:
self.spec = "last-year"
self.desc = gettext("Last year")
self.is_a_year = True
class Today(Period):
"""The period of today."""
def __init__(self):
today: date = date.today()
super().__init__(today, today)
self.is_today = True
def _set_properties(self) -> None:
self.spec = "today"
self.desc = gettext("Today")
self.is_a_day = True
class Yesterday(Period):
"""The period of yesterday."""
def __init__(self):
yesterday: date = date.today() - timedelta(days=1)
super().__init__(yesterday, yesterday)
self.is_yesterday = True
def _set_properties(self) -> None:
self.spec = "yesterday"
self.desc = gettext("Yesterday")
self.is_a_day = True
class AllTime(Period):
"""The period of all time."""
def __init__(self):
super().__init__(None, None)
self.is_all = True
def _set_properties(self) -> None:
self.spec = "all-time"
self.desc = gettext("All")
class TemplatePeriod(Period):
"""The period template."""
def __init__(self):
super().__init__(None, None)
def _set_properties(self) -> None:
self.spec = "PERIOD"
class YearPeriod(Period):
"""A year period."""
def __init__(self, year: int):
"""Constructs a year period.
:param year: The year.
"""
start: date = date(year, 1, 1)
end: date = date(year, 12, 31)
super().__init__(start, end)

View File

@ -0,0 +1,120 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The period specification composer.
"""
from datetime import date, timedelta
def get_spec(start: date | None, end: date | None) -> str:
"""Returns the period specification.
:param start: The start of the period.
:param end: The end of the period.
:return: The period specification.
"""
if start is None and end is None:
return "-"
if end is None:
return __get_since_spec(start)
if start is None:
return __get_until_spec(end)
try:
return __get_year_spec(start, end)
except ValueError:
pass
try:
return __get_month_spec(start, end)
except ValueError:
pass
return __get_day_spec(start, end)
def __get_since_spec(start: date) -> str:
"""Returns the period specification without the end day.
:param start: The start of the period.
:return: The period specification without the end day
"""
if start.month == 1 and start.day == 1:
return start.strftime("%Y-")
if start.day == 1:
return start.strftime("%Y-%m-")
return start.strftime("%Y-%m-%d-")
def __get_until_spec(end: date) -> str:
"""Returns the period specification without the start day.
:param end: The end of the period.
:return: The period specification without the start day
"""
if end.month == 12 and end.day == 31:
return end.strftime("-%Y")
if (end + timedelta(days=1)).day == 1:
return end.strftime("-%Y-%m")
return end.strftime("-%Y-%m-%d")
def __get_year_spec(start: date, end: date) -> str:
"""Returns the period specification as a year range.
:param start: The start of the period.
:param end: The end of the period.
:return: The period specification as a year range.
:raise ValueError: The period is not a year range.
"""
if start.month != 1 or start.day != 1 \
or end.month != 12 or end.day != 31:
raise ValueError
start_spec: str = start.strftime("%Y")
if start.year == end.year:
return start_spec
end_spec: str = end.strftime("%Y")
return f"{start_spec}-{end_spec}"
def __get_month_spec(start: date, end: date) -> str:
"""Returns the period specification as a month range.
:param start: The start of the period.
:param end: The end of the period.
:return: The period specification as a month range.
:raise ValueError: The period is not a month range.
"""
if start.day != 1 or (end + timedelta(days=1)).day != 1:
raise ValueError
start_spec: str = start.strftime("%Y-%m")
if start.year == end.year and start.month == end.month:
return start_spec
end_spec: str = end.strftime("%Y-%m")
return f"{start_spec}-{end_spec}"
def __get_day_spec(start: date, end: date) -> str:
"""Returns the period specification as a day range.
:param start: The start of the period.
:param end: The end of the period.
:return: The period specification as a day range.
:raise ValueError: The period is a month or year range.
"""
start_spec: str = start.strftime("%Y-%m-%d")
if start == end:
return start_spec
end_spec: str = end.strftime("%Y-%m-%d")
return f"{start_spec}-{end_spec}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,54 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The utility to export the report as CSV for download.
"""
import csv
from abc import ABC, abstractmethod
from decimal import Decimal
from io import StringIO
from flask import Response
class BaseCSVRow(ABC):
"""The base CSV row."""
@property
@abstractmethod
def values(self) -> list[str | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.
"""
def csv_download(filename: str, rows: list[BaseCSVRow]) -> Response:
"""Exports the data rows as a CSV file for download.
:param filename: The download file name.
:param rows: The data rows.
:return: The response for download the CSV file.
"""
with StringIO() as fp:
writer = csv.writer(fp)
writer.writerows([x.values for x in rows])
fp.seek(0)
response: Response = Response(fp.read(), mimetype="text/csv")
response.headers["Content-Disposition"] \
= f"attachment; filename={filename}"
return response

View File

@ -1,220 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The period choosers.
This file is largely taken from the NanoParma ERP project, first written in
2021/9/16 by imacat (imacat@nanoparma.com).
"""
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
class PeriodChooser(ABC):
"""The period chooser."""
def __init__(self, start: date | None):
"""Constructs a period chooser.
:param start: The start of the period.
"""
# Shortcut periods
self.this_month_url: str = self._url_for(ThisMonth())
"""The URL for this month."""
self.last_month_url: str = self._url_for(LastMonth())
"""The URL for last month."""
self.since_last_month_url: str = self._url_for(SinceLastMonth())
"""The URL since last mint."""
self.this_year_url: str = self._url_for(ThisYear())
"""The URL for this year."""
self.last_year_url: str = self._url_for(LastYear())
"""The URL for last year."""
self.today_url: str = self._url_for(Today())
"""The URL for today."""
self.yesterday_url: str = self._url_for(Yesterday())
"""The URL for yesterday."""
self.all_url: str = self._url_for(Period(None, None))
"""The URL for all period."""
self.url_template: str = self._url_for(TemplatePeriod())
"""The URL template."""
# Attributes
self.data_start: date | None = start
"""The start of the data."""
self.has_data: bool = start is not None
"""Whether there is any data."""
self.has_last_month: bool = False
"""Where there is data in last month."""
self.has_last_year: bool = False
"""Whether there is data in last year."""
self.has_yesterday: bool = False
"""Whether there is data in yesterday."""
self.available_years: t.Iterator[int] = []
"""The available years."""
if self.has_data is not None:
today: date = date.today()
self.has_last_month = start < date(today.year, today.month, 1)
self.has_last_year = start.year < today.year
self.has_yesterday = start < today
self.available_years: t.Iterator[int] = []
if start.year < today.year - 1:
self.available_years \
= reversed(range(start.year, today.year - 1))
@abstractmethod
def _url_for(self, period: Period) -> str:
"""Returns the URL for a period.
:param period: The period.
:return: The URL for the period.
"""
pass
def year_url(self, year: int) -> str:
"""Returns the period URL of a year.
:param year: The year
:return: The period URL of the year.
"""
return self._url_for(YearPeriod(year))
class JournalPeriodChooser(PeriodChooser):
"""The journal period chooser."""
def __init__(self):
"""Constructs the journal period chooser."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
if period.is_default:
return url_for("accounting.report.journal-default")
return url_for("accounting.report.journal", period=period)
class LedgerPeriodChooser(PeriodChooser):
"""The ledger period chooser."""
def __init__(self, currency: Currency, account: Account):
"""Constructs the ledger period chooser."""
self.currency: Currency = currency
"""The currency."""
self.account: Account = account
"""The account."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
if period.is_default:
return url_for("accounting.report.ledger-default",
currency=self.currency, account=self.account)
return url_for("accounting.report.ledger",
currency=self.currency, account=self.account,
period=period)
class IncomeExpensesPeriodChooser(PeriodChooser):
"""The income and expenses period chooser."""
def __init__(self, currency: Currency, account: IncomeExpensesAccount):
"""Constructs the income and expenses period chooser."""
self.currency: Currency = currency
"""The currency."""
self.account: IncomeExpensesAccount = account
"""The account."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
if period.is_default:
return url_for("accounting.report.income-expenses-default",
currency=self.currency, account=self.account)
return url_for("accounting.report.income-expenses",
currency=self.currency, account=self.account,
period=period)
class TrialBalancePeriodChooser(PeriodChooser):
"""The trial balance period chooser."""
def __init__(self, currency: Currency):
"""Constructs the trial balance period chooser."""
self.currency: Currency = currency
"""The currency."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
if period.is_default:
return url_for("accounting.report.trial-balance-default",
currency=self.currency)
return url_for("accounting.report.trial-balance",
currency=self.currency, period=period)
class IncomeStatementPeriodChooser(PeriodChooser):
"""The income statement period chooser."""
def __init__(self, currency: Currency):
"""Constructs the income statement period chooser."""
self.currency: Currency = currency
"""The currency."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
if period.is_default:
return url_for("accounting.report.income-statement-default",
currency=self.currency)
return url_for("accounting.report.income-statement",
currency=self.currency, period=period)
class BalanceSheetPeriodChooser(PeriodChooser):
"""The balance sheet period chooser."""
def __init__(self, currency: Currency):
"""Constructs the balance sheet period chooser."""
self.currency: Currency = currency
"""The currency."""
first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first()
super().__init__(None if first is None else first.date)
def _url_for(self, period: Period) -> str:
if period.is_default:
return url_for("accounting.report.balance-sheet-default",
currency=self.currency)
return url_for("accounting.report.balance-sheet",
currency=self.currency, period=period)

View File

@ -14,6 +14,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""The utilities to generate reports. """The utilities for the reports.
""" """

View File

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

View File

@ -0,0 +1,108 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# 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 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."""
@property
@abstractmethod
def values(self) -> list[str | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.
"""
def csv_download(filename: str, rows: list[BaseCSVRow]) -> Response:
"""Exports the data rows as a CSV file for download.
:param filename: The download file name.
:param rows: The data rows.
:return: The response for download the CSV file.
"""
with StringIO() as fp:
writer = csv.writer(fp)
writer.writerows([x.values for x in rows])
fp.seek(0)
response: Response = Response(fp.read(), mimetype="text/csv")
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

@ -19,6 +19,8 @@
""" """
import typing as t import typing as t
from flask import current_app
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Account from accounting.models import Account
@ -33,21 +35,15 @@ class IncomeExpensesAccount:
:param account: The actual account. :param account: The actual account.
""" """
self.account: Account | None = None self.account: Account | None = account
self.id: int | None = None self.id: int = -1 if account is None else account.id
"""The ID.""" """The ID."""
self.code: str | None = None self.code: str = "" if account is None else account.code
"""The code.""" """The code."""
self.title: str | None = None self.title: str = "" if account is None else account.title
"""The title.""" """The title."""
self.str: str = "" self.str: str = "" if account is None else str(account)
"""The string representation of the account.""" """The string representation of the account."""
if account is not None:
self.account = account
self.id = account.id
self.code = account.code
self.title = account.title
self.str = str(account)
def __str__(self) -> str: def __str__(self) -> str:
"""Returns the string representation of the account. """Returns the string representation of the account.
@ -68,3 +64,23 @@ class IncomeExpensesAccount:
account.title = gettext("current assets and liabilities") account.title = gettext("current assets and liabilities")
account.str = account.title account.str = account.title
return account return account
def default_ie_account_code() -> str:
"""Returns the default account code for the income and expenses log.
:return: The default account code for the income and expenses log.
"""
with current_app.app_context():
return current_app.config.get("DEFAULT_IE_ACCOUNT", Account.CASH_CODE)
def default_ie_account() -> IncomeExpensesAccount:
"""Returns the default account for the income and expenses log.
:return: The default account for the income and expenses log.
"""
code: str = default_ie_account_code()
if code == IncomeExpensesAccount.CURRENT_AL_CODE:
return IncomeExpensesAccount.current_assets_and_liabilities()
return IncomeExpensesAccount(Account.find_by_code(code))

View File

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

View File

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

View File

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

View File

@ -0,0 +1,117 @@
# 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.period import Period
from accounting.template_globals import default_currency_code
from .ie_account import IncomeExpensesAccount, default_ie_account_code
def journal_url(period: Period) \
-> 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 currency.code == default_currency_code() \
and account.code == default_ie_account_code() \
and period.is_default:
return url_for("accounting.report.default")
if period.is_default:
return url_for("accounting.report.income-expenses-default",
currency=currency, account=account)
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

@ -19,41 +19,56 @@
""" """
from flask import Blueprint, request, Response from flask import Blueprint, request, Response
from accounting import db
from accounting.models import Currency, Account from accounting.models import Currency, Account
from accounting.report.period import Period, get_period
from accounting.template_globals import default_currency_code
from accounting.utils.permission import has_permission, can_view from accounting.utils.permission import has_permission, can_view
from .income_expense_account import IncomeExpensesAccount
from .period import Period
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \ from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
IncomeStatement, BalanceSheet, Search IncomeStatement, BalanceSheet, Search
from .template_filters import format_amount from .template_filters import format_amount
from .utils.ie_account import IncomeExpensesAccount, default_ie_account
bp: Blueprint = Blueprint("report", __name__) bp: Blueprint = Blueprint("report", __name__)
"""The view blueprint for the reports.""" """The view blueprint for the reports."""
bp.add_app_template_filter(format_amount, "accounting_report_format_amount") bp.add_app_template_filter(format_amount, "accounting_report_format_amount")
@bp.get("", endpoint="default")
@has_permission(can_view)
def get_default_report() -> str | Response:
"""Returns the income and expenses log in the default period.
:return: The income and expenses log in the default period.
"""
return __get_income_expenses(
db.session.get(Currency, default_currency_code()),
default_ie_account(),
get_period())
@bp.get("journal", endpoint="journal-default") @bp.get("journal", endpoint="journal-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_journal_list() -> str | Response: def get_default_journal() -> str | Response:
"""Returns the journal in the default period. """Returns the journal in the default period.
:return: The journal in the default period. :return: The journal in the default period.
""" """
return __get_journal_list(Period.get_instance()) return __get_journal(get_period())
@bp.get("journal/<period:period>", endpoint="journal") @bp.get("journal/<period:period>", endpoint="journal")
@has_permission(can_view) @has_permission(can_view)
def get_journal_list(period: Period) -> str | Response: def get_journal(period: Period) -> str | Response:
"""Returns the journal. """Returns the journal.
:param period: The period. :param period: The period.
:return: The journal in the period. :return: The journal in the period.
""" """
return __get_journal_list(period) return __get_journal(period)
def __get_journal_list(period: Period) -> str | Response: def __get_journal(period: Period) -> str | Response:
"""Returns the journal. """Returns the journal.
:param period: The period. :param period: The period.
@ -68,21 +83,20 @@ def __get_journal_list(period: Period) -> str | Response:
@bp.get("ledger/<currency:currency>/<account:account>", @bp.get("ledger/<currency:currency>/<account:account>",
endpoint="ledger-default") endpoint="ledger-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_ledger_list(currency: Currency, account: Account) \ def get_default_ledger(currency: Currency, account: Account) -> str | Response:
-> str | Response:
"""Returns the ledger in the default period. """Returns the ledger in the default period.
:param currency: The currency. :param currency: The currency.
:param account: The account. :param account: The account.
:return: The ledger in the default period. :return: The ledger in the default period.
""" """
return __get_ledger_list(currency, account, Period.get_instance()) return __get_ledger(currency, account, get_period())
@bp.get("ledger/<currency:currency>/<account:account>/<period:period>", @bp.get("ledger/<currency:currency>/<account:account>/<period:period>",
endpoint="ledger") endpoint="ledger")
@has_permission(can_view) @has_permission(can_view)
def get_ledger_list(currency: Currency, account: Account, period: Period) \ def get_ledger(currency: Currency, account: Account, period: Period) \
-> str | Response: -> str | Response:
"""Returns the ledger. """Returns the ledger.
@ -91,10 +105,10 @@ def get_ledger_list(currency: Currency, account: Account, period: Period) \
:param period: The period. :param period: The period.
:return: The ledger in the period. :return: The ledger in the period.
""" """
return __get_ledger_list(currency, account, period) return __get_ledger(currency, account, period)
def __get_ledger_list(currency: Currency, account: Account, period: Period) \ def __get_ledger(currency: Currency, account: Account, period: Period) \
-> str | Response: -> str | Response:
"""Returns the ledger. """Returns the ledger.
@ -109,47 +123,45 @@ def __get_ledger_list(currency: Currency, account: Account, period: Period) \
return report.html() return report.html()
@bp.get("income-expenses/<currency:currency>/<ioAccount:account>", @bp.get("income-expenses/<currency:currency>/<ieAccount:account>",
endpoint="income-expenses-default") endpoint="income-expenses-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_income_expenses_list(currency: Currency, def get_default_income_expenses(currency: Currency,
account: IncomeExpensesAccount) \ account: IncomeExpensesAccount) \
-> str | Response: -> 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 currency: The currency.
:param account: The account. :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()) return __get_income_expenses(currency, account, get_period())
@bp.get( @bp.get(
"income-expenses/<currency:currency>/<ioAccount:account>/<period:period>", "income-expenses/<currency:currency>/<ieAccount:account>/<period:period>",
endpoint="income-expenses") endpoint="income-expenses")
@has_permission(can_view) @has_permission(can_view)
def get_income_expenses_list(currency: Currency, def get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
account: IncomeExpensesAccount,
period: Period) -> str | Response: period: Period) -> str | Response:
"""Returns the income and expenses. """Returns the income and expenses log.
:param currency: The currency. :param currency: The currency.
:param account: The account. :param account: The account.
:param period: The period. :param period: The period.
:return: The income and expenses in the period. :return: The income and expenses log in the period.
""" """
return __get_income_expenses_list(currency, account, period) return __get_income_expenses(currency, account, period)
def __get_income_expenses_list(currency: Currency, def __get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
account: IncomeExpensesAccount,
period: Period) -> str | Response: period: Period) -> str | Response:
"""Returns the income and expenses. """Returns the income and expenses log.
:param currency: The currency. :param currency: The currency.
:param account: The account. :param account: The account.
:param period: The period. :param period: The period.
:return: The income and expenses in the period. :return: The income and expenses log in the period.
""" """
report: IncomeExpenses = IncomeExpenses(currency, account, period) report: IncomeExpenses = IncomeExpenses(currency, account, period)
if "as" in request.args and request.args["as"] == "csv": if "as" in request.args and request.args["as"] == "csv":
@ -160,31 +172,29 @@ def __get_income_expenses_list(currency: Currency,
@bp.get("trial-balance/<currency:currency>", @bp.get("trial-balance/<currency:currency>",
endpoint="trial-balance-default") endpoint="trial-balance-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_trial_balance_list(currency: Currency) -> str | Response: def get_default_trial_balance(currency: Currency) -> str | Response:
"""Returns the trial balance in the default period. """Returns the trial balance in the default period.
:param currency: The currency. :param currency: The currency.
:return: The trial balance in the default period. :return: The trial balance in the default period.
""" """
return __get_trial_balance_list(currency, Period.get_instance()) return __get_trial_balance(currency, get_period())
@bp.get("trial-balance/<currency:currency>/<period:period>", @bp.get("trial-balance/<currency:currency>/<period:period>",
endpoint="trial-balance") endpoint="trial-balance")
@has_permission(can_view) @has_permission(can_view)
def get_trial_balance_list(currency: Currency, period: Period) \ def get_trial_balance(currency: Currency, period: Period) -> str | Response:
-> str | Response:
"""Returns the trial balance. """Returns the trial balance.
:param currency: The currency. :param currency: The currency.
:param period: The period. :param period: The period.
:return: The trial balance in the period. :return: The trial balance in the period.
""" """
return __get_trial_balance_list(currency, period) return __get_trial_balance(currency, period)
def __get_trial_balance_list(currency: Currency, period: Period) \ def __get_trial_balance(currency: Currency, period: Period) -> str | Response:
-> str | Response:
"""Returns the trial balance. """Returns the trial balance.
:param currency: The currency. :param currency: The currency.
@ -200,30 +210,29 @@ def __get_trial_balance_list(currency: Currency, period: Period) \
@bp.get("income-statement/<currency:currency>", @bp.get("income-statement/<currency:currency>",
endpoint="income-statement-default") endpoint="income-statement-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_income_statement_list(currency: Currency) -> str | Response: def get_default_income_statement(currency: Currency) -> str | Response:
"""Returns the income statement in the default period. """Returns the income statement in the default period.
:param currency: The currency. :param currency: The currency.
:return: The income statement in the default period. :return: The income statement in the default period.
""" """
return __get_income_statement_list(currency, Period.get_instance()) return __get_income_statement(currency, get_period())
@bp.get("income-statement/<currency:currency>/<period:period>", @bp.get("income-statement/<currency:currency>/<period:period>",
endpoint="income-statement") endpoint="income-statement")
@has_permission(can_view) @has_permission(can_view)
def get_income_statement_list(currency: Currency, period: Period) \ def get_income_statement(currency: Currency, period: Period) -> str | Response:
-> str | Response:
"""Returns the income statement. """Returns the income statement.
:param currency: The currency. :param currency: The currency.
:param period: The period. :param period: The period.
:return: The income statement in the period. :return: The income statement in the period.
""" """
return __get_income_statement_list(currency, period) return __get_income_statement(currency, period)
def __get_income_statement_list(currency: Currency, period: Period) \ def __get_income_statement(currency: Currency, period: Period) \
-> str | Response: -> str | Response:
"""Returns the income statement. """Returns the income statement.
@ -240,19 +249,19 @@ def __get_income_statement_list(currency: Currency, period: Period) \
@bp.get("balance-sheet/<currency:currency>", @bp.get("balance-sheet/<currency:currency>",
endpoint="balance-sheet-default") endpoint="balance-sheet-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_balance_sheet_list(currency: Currency) -> str | Response: def get_default_balance_sheet(currency: Currency) -> str | Response:
"""Returns the balance sheet in the default period. """Returns the balance sheet in the default period.
:param currency: The currency. :param currency: The currency.
:return: The balance sheet in the default period. :return: The balance sheet in the default period.
""" """
return __get_balance_sheet_list(currency, Period.get_instance()) return __get_balance_sheet(currency, get_period())
@bp.get("balance-sheet/<currency:currency>/<period:period>", @bp.get("balance-sheet/<currency:currency>/<period:period>",
endpoint="balance-sheet") endpoint="balance-sheet")
@has_permission(can_view) @has_permission(can_view)
def get_balance_sheet_list(currency: Currency, period: Period) \ def get_balance_sheet(currency: Currency, period: Period) \
-> str | Response: -> str | Response:
"""Returns the balance sheet. """Returns the balance sheet.
@ -260,10 +269,10 @@ def get_balance_sheet_list(currency: Currency, period: Period) \
:param period: The period. :param period: The period.
:return: The balance sheet in the period. :return: The balance sheet in the period.
""" """
return __get_balance_sheet_list(currency, period) return __get_balance_sheet(currency, period)
def __get_balance_sheet_list(currency: Currency, period: Period) \ def __get_balance_sheet(currency: Currency, period: Period) \
-> str | Response: -> str | Response:
"""Returns the balance sheet. """Returns the balance sheet.
@ -288,4 +297,3 @@ def search() -> str | Response:
if "as" in request.args and request.args["as"] == "csv": if "as" in request.args and request.args["as"] == "csv":
return report.csv() return report.csv()
return report.html() return report.html()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,10 +20,11 @@
/* Author: imacat@mail.imacat.idv.tw (imacat) /* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/3/4 * First written: 2023/3/4
*/ */
"use strict";
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
new PeriodChooser(); PeriodChooser.initialize();
}); });
/** /**
@ -62,6 +63,20 @@ class PeriodChooser {
this.tabPlanes[tab.tabId()] = tab; this.tabPlanes[tab.tabId()] = tab;
} }
} }
/**
* The period chooser.
* @type {PeriodChooser}
*/
static #chooser;
/**
* Initializes the period chooser.
*
*/
static initialize() {
this.#chooser = new PeriodChooser();
}
} }
/** /**
@ -142,6 +157,12 @@ class TabPlane {
*/ */
class MonthTab extends TabPlane { class MonthTab extends TabPlane {
/**
* The month chooser.
* @type {tempusDominus.TempusDominus}
*/
#monthChooser
/** /**
* Constructs a tab plane. * Constructs a tab plane.
* *
@ -150,8 +171,9 @@ class MonthTab extends TabPlane {
constructor(chooser) { constructor(chooser) {
super(chooser); super(chooser);
const monthChooser = document.getElementById(this.prefix + "-chooser"); const monthChooser = document.getElementById(this.prefix + "-chooser");
if (monthChooser !== null) {
let start = monthChooser.dataset.start; let start = monthChooser.dataset.start;
new tempusDominus.TempusDominus(monthChooser, { this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, {
restrictions: { restrictions: {
minDate: start, minDate: start,
}, },
@ -173,6 +195,7 @@ class MonthTab extends TabPlane {
.replaceAll("PERIOD", period); .replaceAll("PERIOD", period);
}); });
} }
}
/** /**
* The tab ID * The tab ID
@ -229,6 +252,7 @@ class DayTab extends TabPlane {
super(chooser); super(chooser);
this.#date = document.getElementById(this.prefix + "-date"); this.#date = document.getElementById(this.prefix + "-date");
this.#dateError = document.getElementById(this.prefix + "-date-error"); this.#dateError = document.getElementById(this.prefix + "-date-error");
if (this.#date !== null) {
this.#date.onchange = () => { this.#date.onchange = () => {
if (this.#validateDate()) { if (this.#validateDate()) {
window.location = chooser.modal.dataset.urlTemplate window.location = chooser.modal.dataset.urlTemplate
@ -236,6 +260,7 @@ class DayTab extends TabPlane {
} }
}; };
} }
}
/** /**
* Validates the date. * Validates the date.
@ -317,6 +342,7 @@ class CustomTab extends TabPlane {
this.#end = document.getElementById(this.prefix + "-end"); this.#end = document.getElementById(this.prefix + "-end");
this.#endError = document.getElementById(this.prefix + "-end-error"); this.#endError = document.getElementById(this.prefix + "-end-error");
this.#conform = document.getElementById(this.prefix + "-confirm"); this.#conform = document.getElementById(this.prefix + "-confirm");
if (this.#start !== null) {
this.#start.onchange = () => { this.#start.onchange = () => {
if (this.#validateStart()) { if (this.#validateStart()) {
this.#end.min = this.#start.value; this.#end.min = this.#start.value;
@ -337,6 +363,7 @@ class CustomTab extends TabPlane {
} }
}; };
} }
}
/** /**
* Validates the start of the period. * Validates the start of the period.

View File

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

View File

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

View File

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

View File

@ -85,9 +85,9 @@ First written: 2023/1/31
<div class="accounting-card col-sm-6"> <div class="accounting-card col-sm-6">
<div class="accounting-card-title">{{ obj.title }}</div> <div class="accounting-card-title">{{ obj.title }}</div>
<div class="accounting-card-code">{{ obj.code }}</div> <div class="accounting-card-code">{{ obj.code }}</div>
{% if obj.is_pay_off_needed %} {% if obj.is_offset_needed %}
<div> <div>
<span class="badge rounded-pill bg-info">{{ A_("Pay-off needed") }}</span> <span class="badge rounded-pill bg-info">{{ A_("Need offset") }}</span>
</div> </div>
{% endif %} {% endif %}
<div class="small text-secondary fst-italic"> <div class="small text-secondary fst-italic">

View File

@ -27,7 +27,7 @@ First written: 2023/2/1
{% block content %} {% 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 %}"> <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}
@ -63,9 +63,9 @@ First written: 2023/2/1
</div> </div>
<div class="form-check form-switch mb-3"> <div class="form-check form-switch mb-3">
<input id="accounting-is-pay-off-needed" class="form-check-input" type="checkbox" name="is_pay_off_needed" value="1" {% if form.is_pay_off_needed.data %} checked="checked" {% endif %}> <input id="accounting-is-offset-needed" class="form-check-input" type="checkbox" name="is_offset_needed" value="1" {% if form.is_offset_needed.data %} checked="checked" {% endif %}>
<label class="form-check-label" for="accounting-is-pay-off-needed"> <label class="form-check-label" for="accounting-is-offset-needed">
{{ A_("The entries in the account need pay-off.") }} {{ A_("The entries in the account need offset.") }}
</label> </label>
</div> </div>

View File

@ -25,31 +25,19 @@ First written: 2023/1/30
{% block content %} {% block content %}
<div class="btn-group mb-2 d-none d-md-inline-flex"> <div class="mb-2 accounting-toolbar">
{% if accounting_can_edit() %} {% 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> <i class="fa-solid fa-plus"></i>
{{ A_("New") }} {{ A_("New") }}
</a> </a>
{% endif %} {% 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") }}"> <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-search-desktop" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required"> <input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label for="accounting-search-desktop" class="accounting-search-label"> <label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
<button type="submit"> <button type="submit">
<i class="fa-solid fa-magnifying-glass"></i> <i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }} <span class="d-none d-md-inline">{{ A_("Search") }}</span>
</button>
</label>
</form>
</div>
<div class="btn-group mb-2 d-md-none">
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-label="{{ A_("Search for Mobile") }}">
<input id="accounting-search-mobile" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label for="accounting-search-mobile" class="accounting-search-label">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
</button> </button>
</label> </label>
</form> </form>
@ -70,8 +58,8 @@ First written: 2023/1/30
{% for item in list %} {% for item in list %}
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.account.detail", account=item)|accounting_append_next }}"> <a class="list-group-item list-group-item-action" href="{{ url_for("accounting.account.detail", account=item)|accounting_append_next }}">
{{ item }} {{ item }}
{% if item.is_pay_off_needed %} {% if item.is_offset_needed %}
<span class="badge rounded-pill bg-info">{{ A_("Pay-off needed") }}</span> <span class="badge rounded-pill bg-info">{{ A_("Need offset") }}</span>
{% endif %} {% endif %}
</a> </a>
{% endfor %} {% endfor %}

View File

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

View File

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

View File

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

View File

@ -28,7 +28,7 @@ First written: 2023/1/26
</span> </span>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<a class="dropdown-item {% if request.endpoint.startswith("accounting.report.") %} active {% endif %}" href="{{ url_for("accounting.report.journal-default") }}"> <a class="dropdown-item {% if request.endpoint.startswith("accounting.report.") %} active {% endif %}" href="{{ url_for("accounting.report.default") }}">
<i class="fa-solid fa-book"></i> <i class="fa-solid fa-book"></i>
{{ A_("Reports") }} {{ A_("Reports") }}
</a> </a>

View File

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

View File

@ -22,13 +22,13 @@ First written: 2023/2/25
{% if accounting_can_edit() %} {% if accounting_can_edit() %}
<div id="accounting-material-fab-speed-dial" class="d-md-none accounting-material-fab"> <div id="accounting-material-fab-speed-dial" class="d-md-none accounting-material-fab">
<div id="accounting-material-fab-speed-dial-actions" class="d-md-none accounting-material-fab-speed-dial-group"> <div id="accounting-material-fab-speed-dial-actions" class="d-md-none accounting-material-fab-speed-dial-group">
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=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_("Cash expense") }}
</a> </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_("Cash income") }}
</a> </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_("Transfer") }}
</a> </a>
</div> </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 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. 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 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. Copyright (c) 2023 imacat.

View File

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

View File

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

@ -19,7 +19,7 @@ search-modal.html: The search modal
Author: imacat@mail.imacat.idv.tw (imacat) Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/8 First written: 2023/3/8
#} #}
<form action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-label="{{ A_("Search the Accounting Data") }}"> <form action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-labelledby="accounting-search-modal-label">
<div class="modal fade" id="accounting-search-modal" tabindex="-1" aria-labelledby="accounting-search-modal-label" aria-hidden="true"> <div class="modal fade" id="accounting-search-modal" tabindex="-1" aria-labelledby="accounting-search-modal-label" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">

View File

@ -0,0 +1,130 @@
{#
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 %}
{% if report.has_data %}
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
<i class="fa-solid fa-download"></i>
<span class="d-none d-md-inline">{{ A_("Download") }}</span>
</a>
{% else %}
<button class="btn btn-secondary" type="button" disabled="disabled">
<i class="fa-solid fa-download"></i>
<span class="d-none d-md-inline">{{ A_("Download") }}</span>
</button>
{% endif %}
{% if use_search %}
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
<span class="d-none d-md-inline">{{ A_("Search") }}</span>
</button>
</label>
</form>
{% endif %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,6 +23,6 @@ First written: 2023/2/25
{% block header %}{% block title %}{{ A_("Add a New Cash Expense Transaction") }}{% endblock %}{% endblock %} {% block 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.default") }}{% endblock %}
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %} {% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}

View File

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

View File

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

View File

@ -23,6 +23,6 @@ First written: 2023/2/25
{% block header %}{% block title %}{{ A_("Add a New Cash Income Transaction") }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ A_("Add a New Cash Income Transaction") }}{% endblock %}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting.transaction.list") }}{% endblock %} {% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %} {% 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 %} {% block content %}
<div class="btn-group mb-3"> <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.default")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}
</a> </a>

View File

@ -23,6 +23,6 @@ First written: 2023/2/25
{% block header %}{% block title %}{{ A_("Add a New Transfer Transaction") }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ A_("Add a New Transfer Transaction") }}{% endblock %}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting.transaction.list") }}{% endblock %} {% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %} {% 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.""" """The error message when the currency code is empty."""
MISSING_ACCOUNT: LazyString = lazy_gettext("Please select the account.") MISSING_ACCOUNT: LazyString = lazy_gettext("Please select the account.")
"""The error message when the account code is empty.""" """The error message when the account code is empty."""
DATE_REQUIRED: DataRequired = DataRequired(
lazy_gettext("Please fill in the date."))
"""The validator to check if the date is empty."""
class NeedSomeCurrencies: class NeedSomeCurrencies:
@ -123,25 +126,23 @@ class AccountOption:
:param account: The account. :param account: The account.
""" """
self.__account: Account = account
self.id: str = account.id self.id: str = account.id
"""The account ID."""
self.code: str = account.code self.code: str = account.code
"""The account code."""
self.query_values: list[str] = account.query_values
"""The values to be queried."""
self.__str: str = str(account)
"""The string representation of the account option."""
self.is_in_use: bool = False self.is_in_use: bool = False
"""True if this account is in use, or False otherwise."""
def __str__(self) -> str: def __str__(self) -> str:
"""Returns the string representation of the account option. """Returns the string representation of the account option.
:return: The string representation of the account option. :return: The string representation of the account option.
""" """
return str(self.__account) return self.__str
@property
def query_values(self) -> list[str]:
"""Returns the values to be queried.
:return: The values to be queried.
"""
return self.__account.query_values
class JournalEntryForm(FlaskForm): class JournalEntryForm(FlaskForm):
@ -574,8 +575,7 @@ class IncomeCurrencyForm(CurrencyForm):
class IncomeTransactionForm(TransactionForm): class IncomeTransactionForm(TransactionForm):
"""The form to create or edit a cash income transaction.""" """The form to create or edit a cash income transaction."""
date = DateField( date = DateField(validators=[DATE_REQUIRED])
validators=[DataRequired(lazy_gettext("Please fill in the date."))])
"""The date.""" """The date."""
currencies = FieldList(FormField(IncomeCurrencyForm), name="currency", currencies = FieldList(FormField(IncomeCurrencyForm), name="currency",
validators=[NeedSomeCurrencies()]) validators=[NeedSomeCurrencies()])
@ -648,8 +648,7 @@ class ExpenseCurrencyForm(CurrencyForm):
class ExpenseTransactionForm(TransactionForm): class ExpenseTransactionForm(TransactionForm):
"""The form to create or edit a cash expense transaction.""" """The form to create or edit a cash expense transaction."""
date = DateField( date = DateField(validators=[DATE_REQUIRED])
validators=[DataRequired(lazy_gettext("Please fill in the date."))])
"""The date.""" """The date."""
currencies = FieldList(FormField(ExpenseCurrencyForm), name="currency", currencies = FieldList(FormField(ExpenseCurrencyForm), name="currency",
validators=[NeedSomeCurrencies()]) validators=[NeedSomeCurrencies()])
@ -758,8 +757,7 @@ class TransferCurrencyForm(CurrencyForm):
class TransferTransactionForm(TransactionForm): class TransferTransactionForm(TransactionForm):
"""The form to create or edit a transfer transaction.""" """The form to create or edit a transfer transaction."""
date = DateField( date = DateField(validators=[DATE_REQUIRED])
validators=[DataRequired(lazy_gettext("Please fill in the date."))])
"""The date.""" """The date."""
currencies = FieldList(FormField(TransferCurrencyForm), name="currency", currencies = FieldList(FormField(TransferCurrencyForm), name="currency",
validators=[NeedSomeCurrencies()]) validators=[NeedSomeCurrencies()])

View File

@ -304,15 +304,17 @@ TXN_TYPE_TO_OP: dict[TransactionType, TransactionOperator] \
"""The map from the transaction types to their operators.""" """The map from the transaction types to their operators."""
def get_txn_op(txn: Transaction) -> TransactionOperator: def get_txn_op(txn: Transaction, is_check_as: bool = False) \
-> TransactionOperator:
"""Returns the transaction operator that may be specified in the "as" query """Returns the transaction operator that may be specified in the "as" query
parameter. If it is not specified, check the transaction type from the parameter. If it is not specified, check the transaction type from the
transaction. transaction.
:param txn: The transaction. :param txn: The transaction.
:param is_check_as: True to check the "as" parameter, or False otherwise.
:return: None. :return: None.
""" """
if "as" in request.args: if is_check_as and "as" in request.args:
type_dict: dict[str, TransactionType] \ type_dict: dict[str, TransactionType] \
= {x.value: x for x in TransactionType} = {x.value: x for x in TransactionType}
if request.args["as"] not in type_dict: if request.args["as"] not in type_dict:

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.models import Transaction
from accounting.utils.flash_errors import flash_form_errors from accounting.utils.flash_errors import flash_form_errors
from accounting.utils.next_uri import inherit_next, or_next from accounting.utils.next_uri import inherit_next, or_next
from accounting.utils.pagination import Pagination
from accounting.utils.permission import has_permission, can_view, can_edit from accounting.utils.permission import has_permission, can_view, can_edit
from accounting.utils.txn_types import TransactionType from accounting.utils.txn_types import TransactionType
from accounting.utils.user import get_current_user_pk from accounting.utils.user import get_current_user_pk
from .operators import TransactionOperator, TXN_TYPE_TO_OP, get_txn_op
from .forms import sort_transactions_in, TransactionReorderForm 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, \ from .template_filters import with_type, to_transfer, format_amount_input, \
text2html text2html
@ -49,20 +47,6 @@ bp.add_app_template_filter(format_amount_input,
bp.add_app_template_filter(text2html, "accounting_txn_text2html") bp.add_app_template_filter(text2html, "accounting_txn_text2html")
@bp.get("", endpoint="list")
@has_permission(can_view)
def list_transactions() -> str:
"""Lists the transactions.
:return: The transaction list.
"""
transactions: list[Transaction] = get_transaction_query()
pagination: Pagination = Pagination[Transaction](transactions)
return render_template("accounting/transaction/list.html",
list=pagination.list, pagination=pagination,
txn_types=TransactionType)
@bp.get("/create/<transactionType:txn_type>", endpoint="create") @bp.get("/create/<transactionType:txn_type>", endpoint="create")
@has_permission(can_edit) @has_permission(can_edit)
def show_add_transaction_form(txn_type: TransactionType) -> str: def show_add_transaction_form(txn_type: TransactionType) -> str:
@ -127,7 +111,7 @@ def show_transaction_edit_form(txn: Transaction) -> str:
:param txn: The transaction. :param txn: The transaction.
:return: The form to edit the transaction. :return: The form to edit the transaction.
""" """
txn_op: TransactionOperator = get_txn_op(txn) txn_op: TransactionOperator = get_txn_op(txn, is_check_as=True)
form: txn_op.form form: txn_op.form
if "form" in session: if "form" in session:
form = txn_op.form(ImmutableMultiDict(parse_qsl(session["form"]))) form = txn_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
@ -147,7 +131,7 @@ def update_transaction(txn: Transaction) -> redirect:
:return: The redirection to the transaction detail on success, or the :return: The redirection to the transaction detail on success, or the
transaction edit form on error. transaction edit form on error.
""" """
txn_op: TransactionOperator = get_txn_op(txn) txn_op: TransactionOperator = get_txn_op(txn, is_check_as=True)
form: txn_op.form = txn_op.form(request.form) form: txn_op.form = txn_op.form(request.form)
if not form.validate(): if not form.validate():
flash_form_errors(form) flash_form_errors(form)
@ -158,12 +142,12 @@ def update_transaction(txn: Transaction) -> redirect:
form.populate_obj(txn) form.populate_obj(txn)
if not form.is_modified: if not form.is_modified:
flash(lazy_gettext("The transaction was not modified."), "success") flash(lazy_gettext("The transaction was not modified."), "success")
return redirect(inherit_next(with_type(__get_detail_uri(txn)))) return redirect(inherit_next(__get_detail_uri(txn)))
txn.updated_by_id = get_current_user_pk() txn.updated_by_id = get_current_user_pk()
txn.updated_at = sa.func.now() txn.updated_at = sa.func.now()
db.session.commit() db.session.commit()
flash(lazy_gettext("The transaction is updated successfully."), "success") flash(lazy_gettext("The transaction is updated successfully."), "success")
return redirect(inherit_next(with_type(__get_detail_uri(txn)))) return redirect(inherit_next(__get_detail_uri(txn)))
@bp.post("/<transaction:txn>/delete", endpoint="delete") @bp.post("/<transaction:txn>/delete", endpoint="delete")
@ -179,7 +163,7 @@ def delete_transaction(txn: Transaction) -> redirect:
sort_transactions_in(txn.date, txn.id) sort_transactions_in(txn.date, txn.id)
db.session.commit() db.session.commit()
flash(lazy_gettext("The transaction is deleted successfully."), "success") flash(lazy_gettext("The transaction is deleted successfully."), "success")
return redirect(or_next(with_type(url_for("accounting.transaction.list")))) return redirect(or_next(__get_default_page_uri()))
@bp.get("/dates/<date:txn_date>", endpoint="order") @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") @bp.post("/dates/<date:txn_date>", endpoint="sort")
@has_permission(can_edit) @has_permission(can_edit)
def sort_accounts(txn_date: date) -> redirect: def sort_transactions(txn_date: date) -> redirect:
"""Reorders the transactions in a date. """Reorders the transactions in a date.
:param txn_date: The date. :param txn_date: The date.
@ -210,10 +194,10 @@ def sort_accounts(txn_date: date) -> redirect:
form.save_order() form.save_order()
if not form.is_modified: if not form.is_modified:
flash(lazy_gettext("The order was not modified."), "success") flash(lazy_gettext("The order was not modified."), "success")
return redirect(or_next(url_for("accounting.account.list"))) return redirect(or_next(__get_default_page_uri()))
db.session.commit() db.session.commit()
flash(lazy_gettext("The order is updated successfully."), "success") flash(lazy_gettext("The order is updated successfully."), "success")
return redirect(or_next(url_for("accounting.account.list"))) return redirect(or_next(__get_default_page_uri()))
def __get_detail_uri(txn: Transaction) -> str: def __get_detail_uri(txn: Transaction) -> str:
@ -223,3 +207,11 @@ def __get_detail_uri(txn: Transaction) -> str:
:return: The detail URI of the transaction. :return: The detail URI of the transaction.
""" """
return url_for("accounting.transaction.detail", txn=txn) return url_for("accounting.transaction.detail", txn=txn)
def __get_default_page_uri() -> str:
"""Returns the URI for the default page.
:return: The URI for the default page.
"""
return url_for("accounting.report.default")

View File

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

View File

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

View File

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