Compare commits

...

1049 Commits
v0.0.0 ... main

Author SHA1 Message Date
f20c462685 Advanced to version 1.6.0. 2024-06-04 08:29:26 +08:00
80ae4bd91c Revised the calculation of "today" to use the client's timezone instead of the server's timezone. 2024-06-04 08:28:59 +08:00
6ee3ee76ea Updated optional dependencies in pyproject.toml. 2024-06-04 08:28:58 +08:00
2bfcc8b889 Updated the dependencies in pyproject.toml. 2024-06-04 08:28:15 +08:00
99564c02d0 Updated the Bootstrap, FontAwesome, and Tempus-Dominus versions in the test site. 2024-04-21 22:41:46 +02:00
25d9904180 Applied the new type parameter syntax to the generic classes for Python 3.12. 2024-03-03 07:39:37 +08:00
1cf83adf87 Applied the "type" statement to type aliases for Python 3.12. 2024-03-03 07:39:20 +08:00
8e3d1f11b5 Updated Python version to 3.12. 2024-03-03 07:38:59 +08:00
0ab14aa34d Updated the copyright year in README.rst. 2024-03-03 07:38:32 +08:00
e0ed81ad1f Advanced to version 1.5.11. 2023-12-16 21:52:15 +08:00
ece7481e9e Refined to enable the selection of the 3351-001 Accumulated Profit or Loss account. 2023-12-16 21:51:14 +08:00
50d4526e0b Advanced to version 1.5.10. 2023-11-28 08:27:31 +08:00
3f0a0b4227 Fixed the form validator to enable the selection of Accumulated Profit or Loss accounts other than 3351-001. 2023-11-28 08:26:37 +08:00
dcc9626b23 Fixed the release date of version 1.5.9 in the change log. 2023-11-28 08:17:25 +08:00
79eb077129 Advanced to version 1.5.9. 2023-11-28 08:10:00 +08:00
d5719ad223 Refined to enable the selection of Accumulated Profit or Loss accounts other than 3351-001, facilitating the consolidation of existing balances. 2023-11-28 08:09:35 +08:00
eb3fa8f414 Added docs/requirements.txt and the "sphinx_rtd_theme" theme to the readthedocs configuration, as Read the Docs does not install sphinx_rtd_theme by default
after August 7, 2023.
2023-11-28 08:04:11 +08:00
937908717b Advanced to version 1.5.8. 2023-10-24 05:00:53 +05:30
0104fa4c21 Fixed an icon in the detail of the cash receipt journal entry. 2023-10-24 04:43:11 +05:30
14365ca255 Advanced to version 1.5.7. 2023-07-29 13:24:52 +08:00
cd86651606 Added the "accounting-titleize" console command to capitalize the existing account titles that were already initialized. 2023-07-29 13:11:47 +08:00
9147744ff7 Renamed the test_init test to test_init_db in the ConsoleCommandTestCase test case. 2023-07-29 13:07:08 +08:00
1a212a5330 Updated the documentation of the test_init test of the ConsoleCommandTestCase test case. 2023-07-29 13:07:08 +08:00
0614457b7b Moved dropping tables from the setUp method to the test_init test in the ConsoleCommandTestCase test case. The other tests may not need to drop the tables first. 2023-07-29 13:07:08 +08:00
545f49043b Updated the Sphinx documentation. 2023-07-29 13:07:08 +08:00
cac0d66ca1 Updated the translation. 2023-07-29 13:07:08 +08:00
5ffd37c859 Revised to capitalize the account titles when initializing the base accounts instead of when displaying the account titles, so that the titles of the user-added accounts are not capitalized incorrectly. 2023-07-29 13:06:32 +08:00
9ae8c1bce9 Updated the translation. 2023-07-29 10:11:45 +08:00
ec0ff3e2e6 Updated the log in message at the home page, and removed the next URI from the log in link. The next URI is not clear text but encrypted now. There is no need to attach the next URI, as it defaults redirects to the accounting application without the next URI. 2023-07-29 10:11:45 +08:00
40a8080751 Removed unused imports from the test site. 2023-07-29 10:11:45 +08:00
736a4086ee Removed an unused import from testlib_journal_entry.py. 2023-07-29 10:11:45 +08:00
6723077b72 Revised the code to read from the CSV data files in the __test_base_account_data method of the ConsoleCommandTestCase test case, to prevent PyCharm from complaining. 2023-07-29 10:11:45 +08:00
0ae00bce79 Changed the properties of the test cases from public to private. 2023-07-29 10:11:45 +08:00
356d2010cc Removed the CSRF token from the get_client function in testlib.py, so that type hints and documentation can be added to the client and the CSRF token properties separately. 2023-07-29 10:11:45 +08:00
501c4b1d22 Added missing documentation to the global variables, class properties, and object properties. 2023-07-29 10:11:44 +08:00
64b9c8c11f Removed an excess property declaration in the populate_obj method of the JournalEntryForm form. 2023-07-29 10:03:46 +08:00
9072de82d4 Added the "decode_next" utility in the "accounting.utils.next_uri" module, and applied the "encode_next" and "decode_next" utilities to the NextUriTestCase test case, so that the test case do not need to get involved into the detail of the next URI encryption. 2023-07-29 10:03:45 +08:00
30fd9c2164 Fixed the documentation of the "is_default" property of the Period utility. 2023-06-05 22:43:35 +08:00
7cb01b4cee Revised the documentation of the columns of the data models. 2023-06-05 16:55:25 +08:00
9a4e04c41f Renamed the HTML ID "collapsible-navbar" to "accounting-collapsible-navbar" in the test site. 2023-06-03 11:12:28 +08:00
a9c4fa9de0 Advanced to version 1.5.6. 2023-05-23 09:32:48 +08:00
3a676e0b5a Fixed the back URL of the creation forms, applying the accounting_or_next filter for the decoded next URI instead of getting the next URI directly. 2023-05-23 09:32:48 +08:00
9cc7b64bb3 Moved the "__as_next" utility from the test site to the "accounting.utils.next_uri" module, and applied it to the template of the unmatched offset list. 2023-05-23 09:32:48 +08:00
352867797d Advanced to version 1.5.5. 2023-05-23 09:30:33 +08:00
09a344d749 Removed excess spaces from the test_change_date test of the JournalEntryReorderTestCase test case. 2023-05-23 09:30:33 +08:00
818c357613 Revised the next URI utilities to apply URLSafeSerializer for encoding and decoding the next URI, in order to prevent tampering with the next URI. 2023-05-23 09:30:19 +08:00
822c8fc49b Renamed the "__get_next_uri" function to "__get_next" in the "accounting.utils.next_uri" module. 2023-05-23 07:10:30 +08:00
3b8a2e3bb1 Replaced the "accounting-dummy-form" name with the dummy CSRF token to work with OWASP ZAP CSRF token scans. 2023-05-22 18:32:24 +08:00
9e4927ee0b Replaced the get_errors_view with the get_messages_view in the create_test_app function in testlib.py. 2023-05-22 00:03:13 +08:00
3b030c577c Added the integrity value of the CDN stylesheet links in the base template of the test site. 2023-05-19 18:17:29 +08:00
60b33f2a3b Revised the link to the stylesheet of tempus dominus in the base template of the test site. 2023-05-19 18:17:20 +08:00
08fdf59844 Revised the indent of the flashed success messages in the base template of the test site. 2023-05-19 18:17:11 +08:00
b397515457 Removed the size restriction in the next URI utilities. Buffer overflow may happen with any parameter, not only the "next" parameter. It should be solved in uWSGI, but not the application. 2023-05-18 23:30:36 +08:00
abe90d3483 Advanced to version 1.5.4. 2023-05-18 00:06:16 +08:00
65e7dcdf6d Replaced the "/next" next URI with the NEXT_URI constant in the test_reorder test of the JournalEntryReorderTestCase test case. 2023-05-18 00:06:05 +08:00
74e414badf Removed unnecessary f-strings from the test_reorder test of the JournalEntryReorderTestCase test case. 2023-05-17 23:54:52 +08:00
69175979ff Added the form name to the dummy forms so that they can be excluded by OWASP ZAP scanner for Anti-CSRF tokens. 2023-05-17 22:56:47 +08:00
2f69e0f215 Added the form name to the search forms so that they can be excluded by OWASP ZAP scanner for Anti-CSRF tokens. 2023-05-17 21:43:21 +08:00
961385c389 Added SESSION_COOKIE_SAMESITE and SESSION_COOKIE_SECURE to create_app of the test site, to set the SameSite and Secure flags for the session cookie. 2023-05-17 19:57:38 +08:00
a691cfd2da Applied the or_next utility to the set local route of the test site. 2023-05-17 19:57:23 +08:00
482a0faa23 Added safeguard to the next URI utilities from invalid or insecure next URI. 2023-05-17 16:26:35 +08:00
0ecf7b6617 Revised the documentation of the "accounting.utils.cast" module. 2023-05-17 15:33:42 +08:00
4408bbfc82 Updated the JavaScript library versions, and added decimal.js-light to the documentation. 2023-05-06 23:59:06 +08:00
433110f486 Revised the way to query accounts with Flask-SQLAlchemy style queries in the accounts method of the CurrentAccount data model. 2023-05-04 09:35:20 +08:00
0b1dd4f4fc Advanced to version 1.5.3. 2023-04-30 15:07:46 +08:00
46bd27e126 Revised the saveOriginalLineItem method of the JavaScript JournalEntryLineItemEditor class not to override the existing amount when the existing amount is less than the net balance. This make it easier when updating the existing journal entries. 2023-04-30 15:03:59 +08:00
b718d19450 Resolved an issue where, in cases where there was no existing localized title and the default title was submitted, the submitted account title or currency name would be erroneously saved as the localized title. 2023-04-30 15:03:58 +08:00
2969e83afe Advanced to version 1.5.2. 2023-04-30 06:43:18 +08:00
a732656746 Revised the coding style in the "__get_line_items" method of the OffsetMatcher class. 2023-04-30 06:38:43 +08:00
1daed940b6 Corrected the definition of the "is_offset" property in the "__get_line_items" method of the OffsetMatcher class. 2023-04-30 06:38:01 +08:00
f29cb00aec Advanced to version 1.5.1. 2023-04-30 05:53:37 +08:00
693f07a49c Removed the "timestamp" and
"user_pk" type aliases for the columns in the data models.  They do not work with the current version of Flask-SQLAlchemy when creating Sphinx documentation.
2023-04-30 05:51:31 +08:00
8c899776f2 Corrected the filename in the csv method of the AccountsWithUnmatchedOffsets report class. 2023-04-30 05:35:13 +08:00
f9aa226bf9 Removed an unnecessary f-string from the csv method of the AccountsWithUnappliedOriginalLineItems report class. 2023-04-30 05:34:34 +08:00
c9bb4197be Fixed the error calling the old "setEnableDescriptionAccount" method in the saveOriginalLineItem method of the JavaScript JournalEntryLineItemEditor class. 2023-04-30 05:27:09 +08:00
9ae8d587d8 Removed the unused "random_pk" annotated type alias. 2023-04-29 04:16:11 +08:00
158058dcfb Updated the documentation of the created_at, created_by, updated_at, updated_by, and visited_at columns of the data models, for consistency. 2023-04-28 21:53:11 +08:00
0bc9947234 Revised the documentation of the new_id function. 2023-04-26 20:36:09 +08:00
8c58a9083a Added type hint subscription for the cls parameter of the new_id function. 2023-04-26 18:31:13 +08:00
f45663754c Fixed the documentation of the "accounting.utils.random_id" module. 2023-04-26 18:30:18 +08:00
cda9e4e3c6 Replaced importing the "typing" module as "t" with importing the individual names in the "typing" module. Since Python 3.9 introduced type hinting generics in standard collections, we do not have as many names to import now. This is also to be consistent with the practices of most major and standard packages and examples. 2023-04-26 18:22:45 +08:00
ee5b447c23 Renamed the "journal_entry_date" variable to "date" in the "__form" method of the JournalEntryData class in the lib module of the test site. 2023-04-26 13:42:47 +08:00
25bfcf4aa4 Fixed the documentation of the balance pseudo property of the JournalEntryLineItem data model. 2023-04-26 13:40:48 +08:00
5956d2cd4c Renamed the "match" parameter to "value" in the setter of the "match" pseudo property of the JournalEntryLineItem data model, for consistency. 2023-04-26 13:40:48 +08:00
833285d924 Renamed the "is_offset" parameter to "value" in the setter of the "is_offset" pseudo property of the JournalEntryLineItem data model, for consistency. 2023-04-26 13:40:43 +08:00
dee4f5e83f Renamed the "balance" parameter to "value" in the setter of the "balance" pseudo property of the JournalEntryLineItem data model, for consistency. 2023-04-26 13:40:37 +08:00
f0d1cae32d Renamed the "net_balance" parameter to "value" in the setter of the "net_balance" pseudo property of the JournalEntryLineItem data model, for consistency. 2023-04-26 13:40:25 +08:00
5dc71697b3 Renamed the "credit" parameter to "value" in the setter of the "credit" pseudo property of the JournalEntryLineItem data model, for consistency. 2023-04-26 13:40:20 +08:00
1bb1e03c08 Renamed the "debit" parameter to "value" in the setter of the "debit" pseudo property of the JournalEntryLineItem data model, for consistency. 2023-04-26 13:40:09 +08:00
914ff92e0f Renamed the "count" parameter to "value" in the setter of the "count" pseudo property of the Account data model, for consistency. 2023-04-26 13:39:56 +08:00
8a1cf463b1 Renamed the "journal_entry_date" parameter to "date" in the constructor of the CSVRow class in the "accounting.report.reports.ledger" module. 2023-04-26 13:34:14 +08:00
d4cf224d6b Renamed the "journal_entry_date" parameter to "date" in the constructor of the CSVRow class in the "accounting.report.reports.income_expenses" module. 2023-04-26 13:33:50 +08:00
8d412ec00a Renamed the "journal_entry_date" parameter to "date" in the show_journal_entry_order route. 2023-04-26 13:32:42 +08:00
2986c518ce Renamed the "journal_entry_date" parameter to "date" in the sort_journal_entries route. 2023-04-26 13:32:30 +08:00
f1351243a6 Renamed the "journal_entry_date" parameter to "date" in the constructor of the JournalEntryReorderForm form. 2023-04-26 13:29:55 +08:00
969e8c76a6 Renamed the "journal_entry_date" parameter to "date" in the "sort_journal_entries_in" function in the "accounting.journal_entry.forms.reorder" module. 2023-04-26 13:29:14 +08:00
10f5e75752 Renamed the "journal_entry_date" variable to "date" in the "test_reorder" test of the JournalEntryReorderTestCase test case. 2023-04-26 13:28:07 +08:00
169b3c292a Renamed the "journal_entry_date" variable to "date" in the "__get_journal_entry_condition" method of the LineItemCollector class in the "accounting.report.reports.search" module. 2023-04-26 13:26:38 +08:00
3eb3aef2f2 Renamed the "j_date" parameter to "date" in the "__next_j_no" method of the BaseTestData class in the lib module of the test site. 2023-04-26 13:24:19 +08:00
6c455a615c Renamed the "j_date" variable to "date" in the "_add_journal_entry" method of the BaseTestData class in the lib module of the test site. 2023-04-26 13:23:53 +08:00
4f3339bf68 Renamed the "j_date" variable to "date" in the "__add_usd_recurring" method of the SampleData class in the reset module of the test site. 2023-04-26 13:23:23 +08:00
b5aa7e923f Renamed the "j_date" variable to "date" in the "_init_data" method of the ReportTestData class in test_report.py. 2023-04-26 13:22:46 +08:00
359c335662 Revised the way to import from the datetime package, to avoid name conflict with the common "date" and "time" names. 2023-04-26 13:17:31 +08:00
c11ae23885 Removed an unused import from the "accounting.utils.cast" module. 2023-04-26 13:15:18 +08:00
e083b11394 Removed the random_pk type alias, because autoincrement=False does not seem to work with it. 2023-04-24 23:29:58 +08:00
167990fc4c Renamed the random_id type alias to random_pk. 2023-04-24 20:22:33 +08:00
d5c1be3d80 Rewrote the data model declaration of the test site with the mapped type hint and the mapped columns in SQLAlchemy 2.0. 2023-04-24 14:02:56 +08:00
f6567794e0 Added the documentation to the authentication blueprint of the test site. 2023-04-24 14:00:32 +08:00
ded85d88f7 Added the timestamp, user_pk, and random_id type aliases to simplify the column definition of the data models. 2023-04-24 03:37:33 +08:00
6d780e9296 Revised the title of the change log. 2023-04-23 22:19:01 +08:00
86637267d3 Advanced to version 1.5.0. 2023-04-23 20:13:34 +08:00
71e97721aa Removed the documentation link from the documentation in intro.rst. It does not make sense for a circular link to itself. 2023-04-23 20:13:33 +08:00
5815608288 Added the VERSION constant to the accounting module for the package version, and revised pyproject.toml and conf.py to read the version from it. 2023-04-23 20:13:10 +08:00
5f75d93c6a Simplified README.rst. 2023-04-23 18:42:54 +08:00
118c4b458e Added the change log. 2023-04-23 18:42:42 +08:00
3f7e4c0dda Rewrote the data model declaration with the mapped type hint and the mapped columns in SQLAlchemy 2.0. Added "SQLAlchemy >= 2" to the dependencies. 2023-04-23 13:21:54 +08:00
eed4c923f6 Removed the "be" cast function to cast data type for the binary expressions. It is to be replaced by the mapped type hints. 2023-04-23 13:21:48 +08:00
09dd5ae541 Revised the long line in the JournalEntryConverter converter. 2023-04-23 09:52:21 +08:00
172a12b134 Fixed the type hint of the "currency_options" function. 2023-04-23 09:44:25 +08:00
f3c558f48a Advanced to version 1.4.1. 2023-04-22 18:22:47 +08:00
988757d30e Revised the JavaScript journal entry line item editor to only override the description with the description of the original line item when there is no existing description. 2023-04-20 00:28:28 +08:00
50cea90d1b Revised the JavaScript journal entry line item editor to allow editing the description for offsets and partially-offset original items. 2023-04-20 00:26:58 +08:00
71dfb6f003 Advanced to version 1.4.0. 2023-04-18 09:33:35 +08:00
be628b4aa1 Updated the Sphinx documentation. 2023-04-18 09:33:00 +08:00
5d444adec4 Updated the translation. 2023-04-18 09:32:38 +08:00
014d67f7b8 Removed the period filter from the unapplied original line items and unmatched offsets. It does not make sense for these two reports. 2023-04-18 09:21:42 +08:00
26b4d4388f Revised the imports in the "accounting.report.reports.unmatched" module. 2023-04-18 09:10:25 +08:00
6e2e92d0fb Removed the redundant currency from the title of the reports when the currency is the default currency. 2023-04-18 08:46:23 +08:00
b1f87cb707 Updated the icon of the unmatched offsets. 2023-04-18 08:20:51 +08:00
928dea8312 Added the account information to the original line item selector of the journal entry form. 2023-04-18 08:15:33 +08:00
b8cec8a2af Revised the account options in the report toolbar to be scrollable. 2023-04-18 08:10:33 +08:00
b6ae946f32 Removed the account code from the journal entry form for mobile screens. 2023-04-18 08:10:23 +08:00
a9acc18a6f Removed the account code from the journal entry detail for mobile screens. 2023-04-18 07:13:10 +08:00
5468010c87 Removed the account code from the account list with unmatched offsets for mobile screens. 2023-04-18 07:10:26 +08:00
b505e380df Removed the account code from the account list with unapplied original line items for mobile screens. 2023-04-18 07:10:15 +08:00
412da170e1 Added the get_net_balances function to the "accounting.report.utils.unapplied" module to replace the __get_net_balances methods of the OffsetMatcher and UnappliedOriginalLineItems classes. 2023-04-18 07:04:00 +08:00
fa237795cf Simplified the query for the unapplied original line items, replacing the offset matcher with its own query. 2023-04-18 01:26:02 +08:00
e2f854b5cc Changed the unmatched offsets from a module to a report, and to show both the unapplied original line items and the unmatched offsets instead of only the unmatched offsets, and added the accumulated balance, in order for ease of use. Removed the match information from the unapplied original line item report. Added the currency and period filters to both the unapplied original line item report and unmatched offset reports. 2023-04-18 01:12:04 +08:00
f8895e3bff Revised the documentation of the "__get_unmatched_offsets" method of the OffsetMatcher class. 2023-04-16 22:52:14 +08:00
84ad065782 Merged the "accounting.utils.unapplied" module into the "accounting.utils.offset_matcher" module as the "__get_unapplied" method of the OffsetMatcher class. It is only used in the offset matcher. 2023-04-16 22:51:46 +08:00
260e3cbe82 Advanced to version 1.3.3. 2023-04-13 09:56:16 +08:00
cd039520b6 Added permission checks to the reset routes in the test site. 2023-04-13 09:54:20 +08:00
05e652aa62 Changed the "_journal_entries" and "_line_items" properties in the BaseTestData class from protected to private, renaming them to "__journal_entries" and "__line_items", respectively. There is no need to access it from the child classes anymore. 2023-04-13 09:28:53 +08:00
5c9bf0638c Removed the "csv_data" pseudo property from BaseTestData. 2023-04-13 09:25:50 +08:00
bbc78433fd Moved the sample data generation from the make-sample.py script to the test site. The sample data is generated at real time. This avoids the problem with pre-recorded sample data that the beginning of the months and weeks changes with the day resetting the sample data. 2023-04-13 09:23:57 +08:00
7bcc2b28b2 Moved the JournalEntryLineItemData, JournalEntryCurrencyData, JournalEntryData, and BaseTestData classes from testlib.py to the ".lib" module in the test site. 2023-04-13 08:30:07 +08:00
c1d9ca284c Changed the new_form and update_form methods of the JournalEntryData class in testlib.py to receive the next URI as the parameter instead of the constant, so that the JournalEntryData class can move to other places. 2023-04-13 08:23:52 +08:00
165e28441a Changed the sample data format from JSON to CSV for the test site live demonstration. 2023-04-12 21:33:34 +08:00
621020b0f0 Advanced to version 1.3.2. 2023-04-12 18:05:13 +08:00
6ad36cfaa3 Updated the translation of the test site. 2023-04-12 18:05:13 +08:00
20b0412091 Added the sample data generation and database reset on the test site for live demonstration. 2023-04-12 18:05:13 +08:00
3ca246d3e0 Revised the strings in babel-utils.py and babel-utils-test-site.py. 2023-04-12 15:04:32 +08:00
85d1b13ccd Added the "populate" method to the BaseTestData class, and changed it so that the tests need to call the "populate" method to populate the data, so that it may return the data with populating the database in the future. 2023-04-12 12:28:34 +08:00
3bada28b8f Revised the BaseTestData class in testlib.py to add journal entries directly to the database instead of through the API, in order to allow the data to be reused, and to speed up the test. 2023-04-12 12:12:11 +08:00
8f2cef8d81 Revised the imports in the accounting.journal_entry.converters module. 2023-04-12 00:41:29 +08:00
e62316c477 Advanced to version 1.3.1. 2023-04-11 22:32:22 +08:00
24ddb0c278 Updated the translation of the test site. 2023-04-11 22:27:31 +08:00
536f3390aa Revised the home page of the test site. 2023-04-11 22:27:31 +08:00
fadd8e73b6 Revised the log in process of the test site to return to the previous page after logging in. 2023-04-11 22:27:11 +08:00
12ccf658bf Revised the documentation in README.rst and intro.rst. 2023-04-11 21:56:49 +08:00
e30d1257e5 Revised the navigation bar so that viewers do not see the menu of the unmatched offsets. 2023-04-11 07:50:10 +08:00
404b902d88 Advanced to version 1.3.0. 2023-04-11 00:08:53 +08:00
a560ff175a Updated the Sphinx documentation. 2023-04-11 00:06:17 +08:00
4be1ead6b5 Added the "accounting-init-db" console command to the database initialization of the test site, for simplicity. 2023-04-10 23:58:08 +08:00
700e4f822a Merged the "init-db" console command to the Flask application initialization in the test site, to simplify the code. 2023-04-10 23:50:16 +08:00
c21ed59dfe Replaced SQLAlchemy 1.x-style bulk_save_objects(objects) with SQLAlchemy 2.x-style execute(insert(model), data). 2023-04-10 23:38:31 +08:00
c4a8326bfc Added the "accounting-init-db" console command to replace the trivial "accounting-init-base", "accounting-init-accounts" and "accounting-init-currencies" console commands. 2023-04-10 23:38:27 +08:00
371c80f668 Removed the unused CurrencyData custom type from the "accounting.currency.commands" module. 2023-04-10 23:12:49 +08:00
40be3fb664 Replaced SQLAlchemy 1.x-style bulk_insert_mappings(model, data) with SQLAlchemy 2.x-style execute(insert(model), data). 2023-04-10 19:56:16 +08:00
1e56403b35 Advanced to version 1.2.1. 2023-04-09 21:04:18 +08:00
650c26036a Fixed the search result to allow full year/month/day specification. 2023-04-09 21:03:18 +08:00
b19a6e5ffe Advanced to version 1.2.0. 2023-04-09 12:16:20 +08:00
1224d6f83e Added the CSV_MIME constant to test_report.py to simplify the ReportTestCase test case. 2023-04-09 12:09:52 +08:00
3a8618f7c3 Fixed the csv_download function when downloading data with non-US-ASCII filenames in the "accounting.report.utils.csv_export" module. 2023-04-09 12:07:31 +08:00
5d87205659 Changed the data in the ReportTestData class to be non-US-ASCII. 2023-04-09 11:55:15 +08:00
04de4f5c5e Merged testlib_offset.py into testlib.py. 2023-04-09 11:46:55 +08:00
f8ea863b80 Moved the add_journal_entry and match_journal_entry_detail functions from testlib_journal_entry.py to testlib.py. They are used by everyone, and testlib_journal_entry.py is only for test_journal_entry.py to shorten the code in one single file. 2023-04-09 11:46:55 +08:00
5ae0d03b32 Revised the imports in testlib_journal_entry.py. 2023-04-09 11:46:55 +08:00
a9a3ad5871 Fixed the data type of the original line item ID in the forms in the OffsetTestCase test case. 2023-04-09 11:46:55 +08:00
5edc95afce Moved the TestData class from testlib_offset.py to test_offset.py, and renamed it to OffsetTestData. It is only used in test_offset.py now. 2023-04-09 11:46:55 +08:00
943ace6fc7 Added ReportTestData as the test data for the ReportTestCase test case. 2023-04-09 11:46:46 +08:00
a63bc977e9 Added the _add_simple_journal_entry method to the BaseTestData class in testlib_offset.py to simplify the code. 2023-04-09 10:50:50 +08:00
dabe6ddbca Renamed the _set_is_need_offset method to _set_need_offset in the BaseTestData class in testlib_offset.py. 2023-04-09 10:42:18 +08:00
f47e9b3150 Renamed the CurrencyData class to JournalEntryCurrencyData in testlib_offset.py, to be clear. 2023-04-09 10:42:18 +08:00
bb5383febe Removed the test data from the OptionTestCase test case. It does not need data. 2023-04-09 10:42:18 +08:00
87f9063ceb Added the BaseTestData class in testlib_offset.py to simplify the test data, and changed the TestData, DifferentTestData, and SameTestData classes to its subclasses. 2023-04-09 10:31:44 +08:00
51f0185bcf Added to test the search in the ReportTestCase test case. 2023-04-09 10:08:23 +08:00
7ca08d6cc8 Added to test the CSV download in the ReportTestCase test case. 2023-04-09 10:08:23 +08:00
c8e9e562be Fixed a URL in the test_nobody test of the ReportTestCase test case. 2023-04-09 10:08:23 +08:00
ba43bd7e90 Simplify the URL of the default reports. 2023-04-09 10:08:23 +08:00
4e550413ba Revised the styles for blueprints to specify the URL, for consistency in the base account, account, currency, and journal entry management. 2023-04-09 10:08:22 +08:00
59a3cbb472 Added the ReportTestCase test case. 2023-04-09 10:08:22 +08:00
d1b64d069e Added the test_empty_db test to the UnmatchedOffsetTestCase test case. 2023-04-09 10:08:11 +08:00
d823d3254f Fixed the date in test_unmatched_offset.py. 2023-04-09 10:07:56 +08:00
5e9a2fb0c3 Renamed test_offset_matcher.py to test_unmatched_offset.py, and the OffsetMatcherTestCase test case to UnmatchedOffsetTestCase. 2023-04-09 10:06:53 +08:00
3f2e659ba5 Added the test_nobody, test_viewer, and test_editor tests to test the permissions in the OffsetMatcherTestCase test case. 2023-04-09 10:06:33 +08:00
9f7bb6b9de Added match_uri to the tests of the OffsetMatcherTestCase test case, for readability. 2023-04-09 08:25:34 +08:00
6857164702 Added the PREFIX constant to simplify the OffsetMatcherTestCase test case. 2023-04-09 08:22:25 +08:00
6bac76be64 Fixed an error in the formatted string in the translation. 2023-04-09 01:41:42 +08:00
370d2668e5 Advanced to version 1.1.0. 2023-04-09 00:48:57 +08:00
5e3e695e62 Updated the Sphinx documentation. 2023-04-09 00:41:14 +08:00
510d369e9c Updated the translation. 2023-04-09 00:39:46 +08:00
b65cae9252 Added the OffsetMatcherTestCase test case. 2023-04-09 00:39:46 +08:00
285c12406b Revised the property names in the TestData class in testlib_offset.py. 2023-04-09 00:39:46 +08:00
df240472a4 Changed the permission to the offset matcher so that editors can use it. 2023-04-09 00:39:45 +08:00
1218b224fc Renamed the "accounting.unmatched_offset.forms" module to "accounting.utils.offset_matcher". 2023-04-09 00:39:45 +08:00
79689ac0e5 Revised the unapplied original line item report to mark matched offsets for administrators when there are unmatched offsets. 2023-04-09 00:39:45 +08:00
1660e66766 Revised the background color of the report tables, for better look on non-white backgrounds. 2023-04-09 00:39:45 +08:00
12d00c9c7d Added the unmatched offset list and the offset matcher. 2023-04-09 00:39:11 +08:00
428018e4a9 Added the match pseudo property to the JournalEntryLineItem data model. 2023-04-08 18:12:57 +08:00
a8f318b0bb Reordered the methods in the JournalEntryLineItem data model. 2023-04-08 18:12:57 +08:00
a3507494e5 Added the refundable deposit accounts to the default list of accounts that need offset in the accounting-init-accounts console command. 2023-04-08 18:12:57 +08:00
3aa6c8d6f6 Removed the empty value in the __is_need_offset function in the "accounting.account.commands" console.command. 2023-04-08 18:12:56 +08:00
052b62cdd4 Moved the __query_line_items method in the UnappliedOriginalLineItems report to the new "accounting.utils.unapplied" module, to share this query. 2023-04-08 18:12:56 +08:00
3728a4037d Renamed the UnappliedAccountConverter path converter to NeedOffsetAccountConverter. 2023-04-08 18:12:56 +08:00
6eee17d44f Added the account list as the default page for the unapplied original line items. 2023-04-08 18:12:55 +08:00
e5cc2b5a2f Added the "count" pseudo property to the Account data model. 2023-04-08 18:12:55 +08:00
ac3b5523b1 Fixed the documentation of the default_currency and default_ie_account pseudo property in the Options class. 2023-04-08 18:12:55 +08:00
5af6fd9619 Moved the "accounting.journal_entry.utils.offset_alias" module to "accounting.utils.offset_alias". 2023-04-08 18:12:55 +08:00
71a20cba29 Replaced the "default_currency_text" pseudo property with the "default_currency" pseudo property in the Options class. 2023-04-08 18:12:54 +08:00
4a4cf1ea40 Removed the redundant "default_ie_account_code_text" pseudo property from the Options class. 2023-04-08 18:12:54 +08:00
e9824808ec Added the UnappliedAccountConverter path converter to only allow the accounts that need offsets. 2023-04-08 18:12:54 +08:00
c984d2d596 Renamed the IncomeExpensesAccountConverter path converter to CurrentAccountConverter. 2023-04-08 18:12:54 +08:00
720e77c814 Fixed the documentation of the PeriodConverter and IncomeExpensesAccountConverter path converters. 2023-04-08 18:12:54 +08:00
0f0412827d Added the unapplied original line item report. 2023-04-08 18:12:45 +08:00
3a0e978f76 Removed an unused import from the "accounting.journal_entry.forms.line_item" module. 2023-04-08 00:44:13 +08:00
8c10d42d7b Added documentation to the currency and account parameters of the CSVRow class, and the pagination parameter of the PageParams class in the "accounting.report.reports.journal" module. 2023-04-08 00:44:13 +08:00
04ec51afbe Changed the "offsets" relationship to a pseudo property, to apply the correct but complex ordering rules. 2023-04-07 16:04:54 +08:00
fe7a8842ce Fixed the query in the JournalEntryConverter converter. 2023-04-07 15:31:06 +08:00
66daa5c42c Fixed the query in the KeepAccountWhenHavingOffset validator. 2023-04-07 15:29:17 +08:00
27fb44937d Fixed the incorrect query in the "offsets" pseudo property of the LineItemForm form. 2023-04-07 15:11:04 +08:00
7026ed3a65 Fixed the order of the items in the "offsets" pseudo property of the LineItemForm form. 2023-04-07 15:01:22 +08:00
fdd3e93778 Fixed the net balance in the line items in the journal entry detail. 2023-04-07 14:57:24 +08:00
def7559457 Fixed the #filterOptions in the JavaScript JournalEntryAccountSelector to show the "more" option when there is no matches, but it is not showing all the accounts. 2023-04-07 12:34:24 +08:00
7905820d68 Revised the imports in the "accounting.base_account.views" and "accounting.currency.views" modules. 2023-04-06 16:09:36 +08:00
7ae332c975 Moved the "Test Site and Live Demonstration" section to the front of the documentation. 2023-04-06 10:00:24 +08:00
86c5b91697 Advanced to version 1.0.1. 2023-04-06 08:43:14 +08:00
9168840e64 Fixed an error in the example configuration. 2023-04-06 08:38:39 +08:00
21b9cfa8b8 Revised the documentation. 2023-04-06 08:31:19 +08:00
b0b3b3acb1 Moved the history section out from README.rst and intro.rst, to the new history.rst. 2023-04-06 08:21:32 +08:00
cb1d254cf0 Advanced to version 1.0.0. Hooray! 2023-04-06 02:55:19 +08:00
eb9ad57e72 Updated the translation. 2023-04-06 02:55:17 +08:00
ec26f8ef4d Added the documentation. 2023-04-06 02:54:45 +08:00
7ed29115ed Revised the inclusion in the base template of the test site. 2023-04-06 02:01:05 +08:00
95955197ac Updated the copyright year in pyproject.toml. 2023-04-05 22:50:54 +08:00
d5a0f79e4b Revised the Read the Docs configuration, and removed the redundant requirements.txt for Read the Docs. 2023-04-05 22:01:53 +08:00
6aa655aa64 Replaced setup.cfg with pyproject.toml for the package settings, and rewrote the packaging rules in MANIFEST.in. 2023-04-05 19:49:52 +08:00
6e532af26e Added the Read the Docs documentation link to README.rst. 2023-04-05 14:25:33 +08:00
fa1818d124 Added the Read the Docs configuration file. 2023-04-05 14:12:46 +08:00
f21ecc2aa9 Added requirements.txt for Read the Docs. 2023-04-05 14:07:37 +08:00
5ae1ab95ae Advanced to version 0.11.1. 2023-04-05 13:00:46 +08:00
7a5b3b78fc Removed the rows with zero balance from the income statement. 2023-04-05 12:59:50 +08:00
7df4051452 Removed the rows with zero balance from the trial balance. 2023-04-05 12:56:28 +08:00
85084c68fd Removed the rows with zero balance from the balance sheet. 2023-04-05 12:29:58 +08:00
0185c16654 Advanced to version 0.11.0. 2023-04-05 09:59:23 +08:00
7dd007f3cf Revised README.rst. 2023-04-05 09:57:34 +08:00
38b8a028d5 Reversed the original line items in the original line item selector. 2023-04-05 09:25:41 +08:00
213981a8b2 Revised the style of the buttons in the description editor, to avoid overwhelming the modal when there are too many buttons. 2023-04-05 09:11:27 +08:00
a4d1789b58 Moved the income and expenses log to the first item of the report chooser. 2023-04-05 08:15:16 +08:00
91620d7db2 Revised the init_app function in the "accounting" module. 2023-04-05 08:07:17 +08:00
02fcabb0ce Updated the URI of the reports to be the default views of the application. 2023-04-05 08:06:00 +08:00
4c2dcc5070 Renamed the project from "Mia! Accounting Flask" to "Mia! Accounting". 2023-04-04 18:26:54 +08:00
c9166fda4d Fixed the order in the get_selectable_original_line_items function in the "accounting.journal_entry.utils.original_line_item" module. 2023-04-04 10:54:43 +08:00
3a0f0873e2 Added documentation to the bp, babel_js, csrf, and db variables in the test site. 2023-04-03 22:18:58 +08:00
a17395b43e Advanced to version 0.10.0. 2023-04-03 22:08:02 +08:00
17c8d9d1a9 Revised the styles of the buttons of the suggested accounts in the description editor. 2023-04-03 22:07:56 +08:00
fa94cd407e Added the JavaScript setElementShown function in the journal entry form for readability. 2023-04-03 21:37:51 +08:00
9a704c8185 Revised the JavaScript account reorder code to avoid nested template literals, for readability. 2023-04-03 21:20:24 +08:00
8286c0c6d8 Revised the JavaScript MonthTab class in the period chooser to avoid nested template literals, for readability. 2023-04-03 21:19:48 +08:00
f7efacad75 Added the unauthorized method to the UserUtilityInterface interface, so that when the user has not logged in, the permission decorator can ask the user to log in instead of failing with HTTP 403 Forbidden. 2023-04-03 19:50:47 +08:00
9263ae0274 Changed the "account" property to private as "__account" in the DescriptionAccount class. 2023-04-03 19:50:47 +08:00
78a9d7794c Revised the JavaScript OriginalLineItem class to store the form instead of the selector. The selector is only used in the constructor. 2023-04-03 19:50:47 +08:00
f3ae37a409 Removed the "#selector" attribute from the JavaScript RecurringAccount class. It is only used in the constructor. There is no need to hold a reference to it. 2023-04-03 19:50:47 +08:00
ddc1081252 Removed the "#selector" attribute from the JavaScript BaseAccountOption class. It is only used in the constructor. There is no need to hold a reference to it. 2023-04-03 19:50:46 +08:00
202d51a032 Removed the "#selector" attribute from the JavaScript JournalEntryAccountOption class. It is only used in the constructor. There is no need to hold a reference to it. 2023-04-03 19:50:46 +08:00
562bc47be7 Revised the saveDescription method of the JournalEntryLineItemEditor editor to also save the isAccountConfirmed status of the DescriptionEditor editor, so that when the user selected any suggested account other than the confirmed account, the confirmed account is released from the next edit. 2023-04-03 19:50:46 +08:00
f3d43a66cc Fixed the operator in the selectAccount method of the JavaScript DescriptionEditor editor. 2023-04-03 19:50:46 +08:00
c3fc6d9a87 Revised the onOpen method of the JavaScript DescriptionEditor editor, to clear the tab planes after the confirmed account is set, so that it works in an environment where the confirmed account is already set. 2023-04-03 19:50:46 +08:00
e1a0380628 Revised the saveDescription method of the JavaScript JournalEntryLineItemEditor to accept the description editor instead of the separated description and account values. 2023-04-03 19:50:46 +08:00
f2a2fcdd32 Revised the "#onDescriptionChange" method to also reset the selected account in the JavaScript DescriptionEditor editor. 2023-04-03 19:50:46 +08:00
ab29166f1e Renamed the "#reset" method to "#resetTabPlanes" in the JavaScript DescriptionEditor, to be clear. 2023-04-03 19:50:46 +08:00
8033921181 Revised the JavaScript DescriptionEditor class so that the #reset() method is triggered by the #onDescriptionChange event, but not the onOpen event, so that user-edited description updates also clear the tab planes. 2023-04-03 19:50:45 +08:00
08732c1e66 Renamed the description attribute to #descriptionInput, and added the description getter and setter to the JavaScript DescriptionEditor editor, to hide the actual implementation of the description input. 2023-04-03 19:50:45 +08:00
4adc464d3d Merged the saveDescriptionWithAccount into the saveDescription method in the JavaScript JournalEntryLineItemEditor class. 2023-04-03 19:12:06 +08:00
2f9d2e36cb Revised the parameters of the saveDescriptionWithAccount method of the JavaScript JournalEntryLineItemEditor class to accept an DescriptionEditorAccount instance instead of the individual account values. 2023-04-03 19:12:06 +08:00
5bb10bf6ba Added the JavaScript DescriptionEditorAccount, DescriptionEditorSuggestedAccount, and DescriptionEditorConfirmedAccount classes, and revised the DescriptionEditor editor to work with these class instances instead of the HTML elements, for simplicity and readability. 2023-04-03 19:12:06 +08:00
06e7b6ddff Added the missing "is_need_offset" property to the DescriptionAccount class. 2023-04-03 19:11:10 +08:00
20e1982984 Renamed the "accounting-is-need-offset" class to "accounting-account-is-need-offset" in the line item sub-form of the journal entry form, for consistency. 2023-04-02 22:29:27 +08:00
a70720be50 Renamed the #selectedAccount attribute to #selectedAccountButton, and the filterSuggestedAccounts, #selectSuggestedAccount, clearSuggestedAccounts, #initializeSuggestedAccounts, #selectAccount, #setConfirmedAccount, and #setSuggestedAccounts methods to filterSuggestedAccountButtons, #selectSuggestedAccountButton, clearSuggestedAccountButtons, #initializeSuggestedAccountButtons, #selectAccountButton, #setConfirmedAccountButton, and #setSuggestedAccountButtons, respectively, in the JavaScript DescriptionEditor class. 2023-04-02 22:16:29 +08:00
cb6de08152 Moved the JournalEntryAccount class from journal-entry-line-item-editor.js to journal-entry-form.js. 2023-04-01 22:42:58 +08:00
211821b4d7 Added the "confirmed account" to the description editor so that it does not override the user's selected account when the user specifically selected it or already confirmed it. 2023-04-01 18:05:48 +08:00
0faca49540 Revised the save method of the JavaScript LineItemSubForm class to update whether it needs offsetting, too. 2023-04-01 00:34:29 +08:00
14e79df571 Revised the line item sub-form to store the information whether it needs offsetting as a class instead of a dataset attribute, and store it in the account code input instead of the whole element, for simplicity and readability. 2023-04-01 00:29:04 +08:00
04fbb725d2 Revised the logic to save the account in the save method of the LineItemSubForm class, since when saving from the line item editor, the account is never null. 2023-04-01 00:19:32 +08:00
a1d6844e52 Replaced the accountCode and accountText getters with the account getter in the JavaScript LineItemSubForm class. 2023-04-01 00:14:47 +08:00
94391b02a6 Added the copy() method to the JavaScript JournalEntryAccount class, and replaced the accountCode and accountText fields with the account field in the OriginalLineItem class. 2023-03-31 23:54:56 +08:00
1cb8a7563e Added the JavaScript JournalEntryAccount class, and added the account field to the JournalEntryLineItemEditor class to replace the accountCode, accountText, and isNeedOffset fields. 2023-03-31 23:33:38 +08:00
63f0f28948 Prefix the classes in the JavaScript description editor with the "DescriptionEditor". 2023-03-27 07:22:36 +08:00
3431922f12 Removed an unused import from the "accounting.models" module. 2023-03-26 01:06:19 +08:00
d5a9e1af18 Removed an unnecessary "start" variable in the constructor of the JavaScript MonthTab class. 2023-03-25 08:37:17 +08:00
73f5d63f44 Replaced string concatenations with ES6 template literals. 2023-03-25 08:37:13 +08:00
bf2c7bb785 Advanced to version 0.9.1. 2023-03-24 09:16:54 +08:00
93ba086548 Simplified the code in the query_values pseudo property of the JournalEntryLineItem data model. 2023-03-24 09:15:29 +08:00
5c4f6017b8 Removed the redundant partial time in the query_values pseudo property of the JournalEntryLineItem data model. They are redundant since it is always partial match now. 2023-03-24 09:13:26 +08:00
cb16b2f0ff Updated the translation of the test site. 2023-03-24 08:53:35 +08:00
d2f11e8779 Replaced the "editor" and "editor2" accounts in the test site with "admin" and "editor", to be clear. 2023-03-24 08:53:35 +08:00
4ccaf01b3c Revised the template of the option detail to be visually different from the option edit form, to avoid confusion. 2023-03-24 08:52:58 +08:00
7c512b1c15 Revised the JavaScript DebitCreditSubForm to have a better visual effect when the line item editor is opened and closed with no line items. 2023-03-24 07:58:32 +08:00
dc432da398 Revised the coding style in the constructor of the JavaScript JournalEntryLineItemEditor class. 2023-03-24 07:49:58 +08:00
c8504bcbf5 Revised the #isQueryMatched method to match the current net balance instead of the net balance but the current form in the JavaScript OriginalLineItem class. 2023-03-24 07:47:41 +08:00
c865141583 Revised the #isQueryMatched method to always does partial match in the JavaScript OriginalLineItem class. Removed the full match from the query values. It is really wierd to type in the half with no match until you type the full term. It may create misunderstanding that there is no further match if you keep typing. 2023-03-24 07:38:17 +08:00
8c1ecd6eac Renamed the #isDebitCreditMatches and #isQueryMatches methods in the JavaScript OriginalLineItem class to #isDebitCreditMatched and #isQueryMatched, respectively. 2023-03-24 07:32:23 +08:00
e8e4100677 Revised the documentation of the JavaScript #isQueryMatches method of the OriginalLineItem class. 2023-03-24 07:31:13 +08:00
6a8773c531 Revised the code in the constructor of the JavaScript OriginalLineItemSelector class. 2023-03-24 07:30:09 +08:00
30e0c7682c Renamed the JavaScript AccountSelector class to JournalEntryAccountSelector, to avoid confusion. There is a RecurringAccountSelector in the option form now. 2023-03-24 07:27:52 +08:00
eb5a7bef7e Added the JavaScript AccountOption class to object-ize the account options in the journal entry form. 2023-03-24 07:23:56 +08:00
8a174d8847 Renamed the setBaseAccount method to saveBaseAccount in the JavaScript AccountForm form, for consistency. 2023-03-24 07:20:40 +08:00
7459afd63a Renamed the hasAnyMatched variable to isAnyMatched in the JavaScript #filterOptions method of the BaseAccountSelector, RecurringAccountSelector, and OriginalLineItemSelector classes. 2023-03-24 06:52:14 +08:00
a9afc385e9 Added the "baseCode" getter to the JavaScript AccountForm form, and removed the "baseCode" parameter from the onOpen method of the BaseAccountSelector class. It can retrieve the base code directly from the parent account form now. 2023-03-24 06:46:22 +08:00
a8be739ec7 Fixed the documentation of the JavaScript BaseAccountOption class. 2023-03-24 06:35:44 +08:00
0130bc58a9 Added prefix to the constructor of the BaseAccountSelector class, to simplify the code. 2023-03-24 00:37:59 +08:00
821059fa80 Added the JavaScript BaseAccountOption class to object-ize the base account options in the account form. 2023-03-24 00:35:50 +08:00
5b4f57d0b3 Removed a debugging log from the onOpen method of the RecurringAccountSelector class. 2023-03-24 00:27:37 +08:00
4bfac2d545 Removed an unused "noinspection JSValidateTypes" comment from the constructor of the JavaScript DebitCreditSubForm class. 2023-03-24 00:21:33 +08:00
f105f0cf7b Removed an orphan comment from the JavaScript RecurringTransactionTab class. 2023-03-24 00:20:59 +08:00
5e320729d7 Removed an excess blank line in testlib.py. 2023-03-23 17:30:38 +08:00
7515032082 Moved the Accounts shortcut from testlib_journal_entry.py to testlib.py. 2023-03-23 17:26:27 +08:00
361b18e411 Moved the duplicated NEXT_URI constant from test_account.py and testlib_journal_entry.py to testlib.py. 2023-03-23 17:22:57 +08:00
7d084e570e Revised the debit-credit content to have a better look when it is still empty. 2023-03-23 09:13:52 +08:00
cb397910f8 Added the "resetNo" method to the RecurringItemSubForm, CurrencySubForm, and LineItemSubForm forms to provide a simpler way to reset the order number, and removed the "elementId" getter and "no" setter. 2023-03-23 08:55:16 +08:00
5f8b0dec98 Renamed the JavaScript "lineItemIndex" property to "index" in the LineItemSubForm form. 2023-03-23 08:44:20 +08:00
8398d1e8bb Fixed a type error in the constructor of the JavaScript LineItemSubForm form. 2023-03-23 08:42:48 +08:00
562801692a Added the JavaScript setDeleteButtonShown method to the CurrencySubForm and LineItemSubForm forms, and hides the implementation of the delete buttons from outside. Changed the delete buttons to private. 2023-03-23 08:40:19 +08:00
faee1e61c6 Added the JavaScript elementId getter and no setter to the RecurringItemSubForm, CurrencySubForm, and LineItemSubForm forms, to hide the actual implementation of the element ID and order number. 2023-03-23 08:24:58 +08:00
57a4177037 Replaced the JavaScript getXXX methods with the "get XXX" getters. 2023-03-23 08:11:11 +08:00
fa1dedf207 Unified the documentation of the JavaScript prefix attribute. 2023-03-23 07:10:16 +08:00
7ed13dc0af Replaced the JavaScript prefix attributes that are only used in the class constructors with the prefix constant variables in the constructor. 2023-03-23 07:06:58 +08:00
52807c5322 Advanced to version 0.9.0. 2023-03-23 00:48:14 +08:00
231a71feea Updated the Sphinx documentation. 2023-03-23 00:47:04 +08:00
4902eecae0 Updated the translation. 2023-03-23 00:46:18 +08:00
889e4c058e Revised the option form to have a better look when there is no recurring expense and income. 2023-03-23 00:45:19 +08:00
7262a6cb42 Removed an unused ID in the form-recurring-item.html template. 2023-03-23 00:27:23 +08:00
c4ff4ecb3d Fixed the layout in the option detail when there is no recurring expenses or incomes. 2023-03-23 00:24:57 +08:00
2859f628ea Fixed the error finding the account in the default_ie_account_code_text pseudo property of the Options data model. 2023-03-23 00:21:31 +08:00
e0355b2af1 Revised the error message of the CurrentAccountExists and AccountNotCurrent validators. 2023-03-23 00:09:57 +08:00
b4d390c33a Renamed the isMatches method to isMatched in the JavaScript RecurringAccount class. 2023-03-23 00:00:39 +08:00
a4ab8a761c Renamed the "content" dataset to "text" in the base account selector of the account form, for consistency. 2023-03-22 23:56:37 +08:00
907ce6d06e Renamed the "content" dataset to "text" in the account selector of the journal entry form, for consistency. 2023-03-22 23:55:28 +08:00
7e1388735e Added the OptionTestCase test case. 2023-03-22 23:50:14 +08:00
6f773dd837 Added the ACCOUNT_REQUIRED validator to the account_code field of the RecurringExpenseForm and RecurringIncomeForm forms. 2023-03-22 23:25:20 +08:00
87fa5aa6bc Moved the ACCOUNT_REQUIRED validator from the "accounting.journal_entry.forms.line_item" module to the "accounting.forms" module, in order to share it. 2023-03-22 23:23:53 +08:00
35e05b3708 Revised the IsDebitAccount and IsCreditAccount validators to require the error message, and moved their default error messages to the DebitLineItemForm and CreditLineItemForm forms. 2023-03-22 23:19:52 +08:00
7ccc96bda0 Added the CurrentAccountExists and AccountNotCurrent validators to the default_ie_account_code field of the OptionForm form, to validate that the current account exists and is a current account. 2023-03-22 23:16:31 +08:00
283758ebe9 Revised the shortcut accounts in testlib_journal_entry.py. 2023-03-22 22:56:37 +08:00
b673c7aeaf Renamed the "default_currency" option to "default_currency_code". 2023-03-22 22:34:13 +08:00
0ad2ac53dd Added the "sql_condition" method to the CurrentAccount data model to simplify the queries. 2023-03-22 21:43:58 +08:00
7e90ec5a8f Replaced the "current_accounts" function with the "accounts" class method of the CurrentAccount data model. 2023-03-22 21:39:18 +08:00
7755365467 Revised the documentation of the CurrentAccount data model. 2023-03-22 21:36:07 +08:00
979eea606a Added the missing documentation to the account property of the CurrentAccount data model. 2023-03-22 20:40:43 +08:00
5a9e08f2c4 Moved the AccountExists, IsDebitAccount, and IsCreditAccount validators from the "accounting.journal_entry.forms.line_item" module to the "accounting.forms" module, to share it with the "accounting.option.forms" module. Removed the redundant AccountExists, IsExpenseAccount, and IsIncomeAccount validators from the "accounting.option.forms" module. 2023-03-22 20:37:53 +08:00
68c810d492 Renamed the debit and credit methods in the Account data model to selectable_debit and selectable_credit, to be clear. 2023-03-22 20:21:52 +08:00
5f88260507 Revised the elements in the option detail page for better layout. 2023-03-22 20:16:08 +08:00
779d89f8c4 Replaced the "clear" method with the "onOpen" method when the account is clicked in the RecurringItemEditor in the JavaScript RecurringAccountSelector class. 2023-03-22 20:09:41 +08:00
5d4bf4361b Revised the error messages of the NotStartPayableFromExpense and NotStartReceivableFromIncome validators. 2023-03-22 20:00:17 +08:00
10170d613d Fixed the debit and credit methods of the Account data model, removing the payable accounts that need offset from the debit accounts, and the receivable accounts that need offset from the credit accounts. They should not be selectable. 2023-03-22 19:54:27 +08:00
c885c08c37 Moved the "accounting.option.options" module to "accounting.utils.options", because it is meant to shared by other submodules. 2023-03-22 19:47:37 +08:00
e2a4340f2a Revised the imports in the "accounting.option.views" module. 2023-03-22 19:43:10 +08:00
9728ff30e0 Renamed the IsDebitAccount, IsCreditAccount, NotStartPayableFromDebit, and NotStartReceivableFromCredit validators to IsExpenseAccount, IsIncomeAccount, NotStartPayableFromExpense, and NotStartReceivableFromIncome, respectively, in the "accounting.option.forms" module. 2023-03-22 19:41:54 +08:00
a4644ede5f Fixed and replaced the IsDebitAccount validator with the IsCreditAccount validator in the account_code field of the RecurringIncomeForm form. 2023-03-22 19:39:02 +08:00
8f477dd6f1 Added the all_errors pseudo property to the RecurringItemForm form, and applied it to the form-recurring-item.html template. 2023-03-22 19:37:20 +08:00
44ac53f15c Fixed and added the missing validation in the update_options route. 2023-03-22 19:33:21 +08:00
5edb5465c5 Fixed the incomes field of the RecurringForm form to use the RecurringIncomeForm form instead of the RecurringExpenseForm form as its sub-forms. 2023-03-22 19:29:42 +08:00
067afdb165 Fixed and moved the account_text pseudo property from the RecurringExpenseForm form to its base RecurringItemForm form. 2023-03-22 19:28:46 +08:00
37a4c26f86 Fixed the label in the option detail and option form. 2023-03-22 19:21:24 +08:00
89e43830b4 Fixed the __get_accounts method of the DescriptionEditor class not to do empty queries. 2023-03-22 19:17:25 +08:00
671dbfb692 Moved the CURRENCY_REQUIRED validator back from the "accounting.forms" module to the "accounting.journal_entry.forms.currency" module. It is not shared with other module anymore. 2023-03-22 19:14:51 +08:00
2014344d25 Revised to use its own error message for the DataRequired validator in the default_currency field of the OptionForm form. 2023-03-22 19:13:08 +08:00
f9c39709c8 Revised the text messages in the option forms. 2023-03-22 19:11:07 +08:00
b394c58ec6 Added support to sort the recurring items. 2023-03-22 19:01:02 +08:00
0af3e2785b Removed an unused import from the "accounting.option.forms" module. 2023-03-22 18:50:24 +08:00
7066f75e72 Added the read-only view for the options. 2023-03-22 16:08:16 +08:00
619540da49 Fixed the documentation of the form-recurring-expense-income.html template. 2023-03-22 15:47:19 +08:00
567004f7d9 Renamed IncomeExpensesAccount to CurrentAccount. 2023-03-22 15:42:44 +08:00
761d5a5824 Added the option management, and moved the configuration of the default currency, the default account for the income and expenses log, and the recurring expenses and incomes to the options. 2023-03-22 15:34:28 +08:00
fa3cdace7f Renamed the #validateForm method to #validate in the JavaScript AccountForm and CurrencyForm. 2023-03-22 11:22:46 +08:00
656762850c Moved the IncomeExpensesAccount data model from the "accounting.report.utils.ie_account" module to the "accounting.utils.ie_account" module. 2023-03-22 07:29:41 +08:00
e2325f08d0 Moved the CURRENCY_REQUIRED and CurrencyExists validators from the "accounting.journal_entry.forms.currency" module to the "accounting.forms" module. 2023-03-22 07:17:45 +08:00
855356084e Fixed the documentation of the can_view and can_edit functions in the "accounting.utils.permission" module. 2023-03-22 04:50:12 +08:00
7aaeb32a3d Added the missing "role=" to the "<a...></a>" links that act like buttons. 2023-03-22 02:35:07 +08:00
b376cf1580 Revised the toolbar layout so that it looks better with only one toolbar button on the mobile devices. 2023-03-22 02:28:58 +08:00
ccbdc779ac Restored the "Back" button on the toolbar for the mobile devices. It is still necessary, because the user may get lost in the navigation history. 2023-03-22 02:28:29 +08:00
61ee08fda2 Revised the date format in the journal entry order page, and removed the individual date in the page, as it is redundant. 2023-03-22 02:12:19 +08:00
d8afadda02 Advanced to version 0.8.0. 2023-03-22 01:52:24 +08:00
c8e1270d8f Updated the translation. 2023-03-22 01:50:18 +08:00
2a78799404 Revised the page to reorder the journal entries in a same day. 2023-03-22 01:47:11 +08:00
863d7a9368 Simplified the "can_delete" pseudo property of the JournalEntry data model. SQLAlchemy caches the query result. There is no need to cache the result again. 2023-03-22 01:02:09 +08:00
6fd37b21d9 Fixed so that the journal entries that has offset cannot be deleted. 2023-03-22 00:59:43 +08:00
bbf3ee3320 Added the limitation so that the default currency and the currencies in use cannot be deleted. 2023-03-22 00:37:39 +08:00
b60cc7902d Revised the test_delete test in the AccountTestCase test case. 2023-03-22 00:37:26 +08:00
623313b58a Renamed the constants to be upper-cased in test_account.py. 2023-03-22 00:37:26 +08:00
d0d2d77a2e Added the limitation so that essential accounts, like cash, and the accounts in use, cannot be deleted. 2023-03-22 00:37:26 +08:00
494faeffea Revised the toolbar of the reports to fit better in desktop browsers. 2023-03-21 23:16:47 +08:00
871a5fd1d8 Changed the "settings" button to "edit" in the account, currency, and journal entry detail pages. 2023-03-21 23:10:33 +08:00
e615ad2690 Revised the style of the toolbar buttons for better layout on mobile devices. Hid the "Back" button on mobile devices for better layout and saving spaces. 2023-03-21 23:07:05 +08:00
da92a0b42c Replaced the BABEL_DEFAULT_LOCALE configuration variable with the default_locale from the Flask-Babel instance, to get rid of the dependency to the specific configuration variable. 2023-03-21 22:34:44 +08:00
678d0aa773 Fixed the CSS version of Tempus-Dominus in the base template of the test site. 2023-03-21 21:22:48 +08:00
9248ba7e3b Removed the redundant Flask App context from the default_currency_code Jinja2 global and the default_ie_account_code function. They are always under the Flask app context. 2023-03-21 21:17:10 +08:00
446087b212 Added the ACCOUNTING_DEFAULT_CURRENCY and ACCOUNTING_DEFAULT_IE_ACCOUNT configuration to the test site configuration, for demonstration. 2023-03-21 21:15:14 +08:00
a42e7d13a2 Renamed the configuration DEFAULT_CURRENCY, DEFAULT_IE_ACCOUNT, and RECURRING to "ACCOUNTING_DEFAULT_CURRENCY", "ACCOUNTING_DEFAULT_IE_ACCOUNT", and "ACCOUNTING_RECURRING", respectively. 2023-03-21 21:13:03 +08:00
a82f5091f1 Revised the styles of the buttons in the description editor. 2023-03-21 19:50:57 +08:00
3455827c09 Added the recurring transactions. 2023-03-21 19:45:56 +08:00
5dccf99a55 Renamed "regular" to "recurring" in the description editor. 2023-03-21 17:48:19 +08:00
8818b46e01 Moved the tag initialization from the constructor to the __init_tags method in the DescriptionEditor class. 2023-03-21 17:32:07 +08:00
2f3ad99467 Removed redundant code in the templates of the journal entry form. 2023-03-21 11:54:45 +08:00
592910187b Added the common form-debit-credit.html template to reduce the duplicated code for the currency sub-forms in the transaction form. 2023-03-21 11:47:05 +08:00
cb7a0d377f Added the common form-currency.html template to reduce the duplicated code for the currency sub-forms in the transaction form. 2023-03-21 11:26:28 +08:00
79175285f8 Changed "to transfer" to "as transfer", and updated its Font Awesome icon in the toolbar of the journal entries. 2023-03-21 11:04:45 +08:00
fef474977c Adjust the location of the Material Design floating action buttons for mobile screen. 2023-03-21 10:57:08 +08:00
fa1a55cd3d Adjusted the style for the mobile toolbar for Firefox on Android with large font size. 2023-03-21 10:56:21 +08:00
2253ec7e6d Advanced to version 0.7.0. 2023-03-21 00:54:44 +08:00
32aa532548 Updated the Sphinx documentation. 2023-03-21 00:54:26 +08:00
56138f7de3 Updated the translation. 2023-03-21 00:53:52 +08:00
21ef944259 Fixed the error in the navigation menu when there is no matching endpoint. 2023-03-21 00:53:27 +08:00
760f1c2877 Fixed the query to be case-insensitive in the base account selector in the account form and the account selector in the journal entry form. 2023-03-20 23:54:50 +08:00
e377eac407 Fixed the capitalization of the currencies, base accounts, and accounts. 2023-03-20 23:54:49 +08:00
77787eee9f Fixed the search queries to be case-insensitive. 2023-03-20 23:54:38 +08:00
03265a1232 Fixed the text in the buttons to add new journal entries. 2023-03-20 23:16:57 +08:00
079dc1ab6d Renamed the "eid" field to "id" in the LineItemForm form, since the problem is found. It was the "id" property of the enclosing FormField. If we extract the form from FormField, we can still access the "id" field. 2023-03-20 23:06:57 +08:00
d4fe91ec4a Fixed the capitalization of the shortcut periods in the period chooser. 2023-03-20 22:57:04 +08:00
acc5b4d6ea Fixed the capitalization of the label of the number of items in the description editor. 2023-03-20 22:52:35 +08:00
19a93cb4c3 Fixed the text message in the add_journal_entry view. 2023-03-20 22:44:55 +08:00
116089d1d2 Fixed the text message in the add_currency view. 2023-03-20 22:44:26 +08:00
50dd6078c7 Replaced "Need offset" with "Needs Offset" as the text of the badge. 2023-03-20 22:43:53 +08:00
9a4531b26c Revised the title of the delete confirmation modal from "Delete XXX Confirmation" to "Confirm Delete XXX", as suggested by ChatGPT. 2023-03-20 22:38:35 +08:00
b1af1d7425 Renamed "voucher" to "journal entry". 2023-03-20 22:33:14 +08:00
8f909965a9 Renamed "voucher line item" to "journal entry line item". 2023-03-20 20:52:35 +08:00
e26af6f3fc Renamed "side" to "debit-credit". 2023-03-20 20:35:10 +08:00
02fffc3400 Removed the unused offset_original_line_item_id field from the DebitLineItemForm form. 2023-03-20 18:56:38 +08:00
d7d6929bf2 Fixed the parameter passed to the credit line item subform in the transfer voucher form. 2023-03-20 18:53:16 +08:00
e4cc61552e Simplified the parameter passed to the "form-line-item.html" template for the line item subform of the voucher form, to be less error-prone. 2023-03-20 18:53:11 +08:00
d18dd7d4d2 Renamed "summary" to "description" in the voucher line item. 2023-03-20 18:45:50 +08:00
3251660092 Added the SVG favicon from Font Awesome 6 to the test site. 2023-03-20 08:41:37 +08:00
c1235608d8 Renamed "journal entry" to "voucher line item", and "entry type" to "side". 2023-03-19 22:09:40 +08:00
25c45b16ae Removed the unused imports from the "accounting.voucher.utils.original_entries" module. 2023-03-19 14:15:50 +08:00
78f570b81b Removed an excess trailing blank line in test_summary_editor.py. 2023-03-19 14:05:36 +08:00
5db13393cc Renamed "transaction" to "voucher", "cash expense transaction" to "cash disbursement voucher", and "cash income transaction" to "cash receipt voucher". 2023-03-19 13:56:46 +08:00
1e286fbeba Renamed the #originalEntry, #summary, #account, and #amount private attributes of the JavaScript JournalEntryEditor class to #originalEntryText, #summaryText, #accountText, and #amountInput, to avoid confusion with the public attributes with similar names. 2023-03-19 10:42:37 +08:00
d4b3fe67b9 Removed the originalEntryId parameter from the onOpen method of the JavaScript OriginalEntrySelector class. It can be obtained from the JournalEntryEditor instance, and the parameter is not needed anymore. 2023-03-19 10:25:33 +08:00
5d0757c845 Added the JavaScript JournalEntryEditor instance to the parameters of the constructor of the JavaScript OriginalEntrySelector class, so that it always have access to the JournalEntryEditor instance. Removed the JournalEntryEditor instance from the parameters of the onOpen method of the JournalEntryEditor class. 2023-03-19 10:22:18 +08:00
b69a519904 Updated tempus-dominus from 6.2.10 to 6.4.3 in the base template of the test site. 2023-03-19 10:16:12 +08:00
122b7b059c Changed the default date and min date for the Tempus Dominus month chooser from strings to the JavaScript Date objects. 2023-03-19 10:15:32 +08:00
4977847dd8 Changed the entryType attribute of the JavaScript AccountSelector class from public to private, renamed it from entryType to #entryType. 2023-03-19 07:30:03 +08:00
b9b197ea27 Removed the unused #modal property from the JavaScript OriginalEntrySelector class. 2023-03-19 07:27:25 +08:00
884e37fe1b Advanced to version 0.6.0. 2023-03-18 23:38:41 +08:00
cc6a73211e Updated the Sphinx documentation. 2023-03-18 23:38:19 +08:00
2299b86d0f Updated the translation. 2023-03-18 23:37:08 +08:00
6d293a1aac Added the JavaScript getInstances method to the SummaryEditor and AccountSelector classes, so that it is easier to deal with the case when the debit and credit versions are not both exist. 2023-03-18 23:36:38 +08:00
a2311aee24 Revised to prevent word wrapping in the button to choose the original entry in the summary editor. 2023-03-18 23:20:49 +08:00
5571c0d01f Renamed all the is_XXX_needed properties to is_need_XXX. For example, especially the is_offset_needed property to is_need_offset, to be clear and understandable. 2023-03-18 22:52:29 +08:00
98e1bad413 Renamed the test_not_needed test to test_not_need in the PaginationTestCase test case. 2023-03-18 22:43:53 +08:00
7ff52d99e6 Removed the unused is_offset_chooser_needed property from the AccountOption data model. 2023-03-18 22:40:56 +08:00
cc440a4110 Renamed the "_is_payable_needed" and "_is_receivable_needed" properties in the TransactionForm form to "_is_need_payable" and "_is_need_receivable", respectively, for readability and understandability. 2023-03-18 22:39:26 +08:00
f5149a0c37 Replaced the long parameter list with the JournalEntrySubForm instance in the onEdit method of the JavaScript JournalEntryEditor, to simplify the code. 2023-03-18 22:36:50 +08:00
ca928636fd Replaces the datasets with object attributes to store the currency code and entry type in the JavaScript OriginalEntrySelector class. 2023-03-18 22:11:45 +08:00
4a8297d594 Revised the documentation of the JavaScript OriginalEntrySelector class. 2023-03-18 22:11:45 +08:00
915e4408e1 Revised the JavaScript to initialize the OriginalEntrySelector instance in JournalEntryEditor, so that the journal entry editor holds the OriginalEntrySelector instance. It can find the OriginalEntrySelector instance without needing to invoke its static methods. Removed the redundant static methods from the OriginalEntrySelector class. 2023-03-18 22:11:45 +08:00
fd9eac06f6 Merged the code in the #initializeSummaryEditors method into the constructor in the JavaScript JournalEntryEditor class. 2023-03-18 22:11:45 +08:00
403942dfc0 Added a missing semicolon in the saveOriginalEntry method of the JavaScript JournalEntryEditor class. 2023-03-18 22:11:45 +08:00
35dc513760 Revised the #initializeSummaryEditors method of the JavaScript JournalEntryEditor class to construct the SummaryEditor instances with the entry type instead of the form element. Replaced the form element with the entry type in the constructor of the SummaryEditor class. Removed the unused accounting-summary-editor class and data-entry-type attributes from the template of the summary editor. 2023-03-18 22:11:45 +08:00
01861f0b6a Merged the code in the #initializeAccountSelectors method into the constructor in the JavaScript JournalEntryEditor class. 2023-03-18 22:11:45 +08:00
8c10f1e96a Revised the ledger not to show the accumulated balance of the nominal accounts. The accumulated balance does not make sense for nominal accounts. 2023-03-18 22:11:45 +08:00
5f7fc0b8e8 Added the is_real pseudo property to the Account data model, and changed the is_nominal pseudo property to be the opposite of the is_real pseudo property. 2023-03-18 22:11:45 +08:00
700c179774 Applied the is_nominal pseudo property to the __get_brought_forward_entry method of the EntryCollector of the ledger. 2023-03-18 22:11:45 +08:00
cabe02f7d0 Added the is_nominal pseudo property to the Account data model. 2023-03-18 22:11:45 +08:00
5ceb9f2e83 Fixed and renamed the "__query_currency_period" method of the AccountCollector of the balance sheet to "__query_current_period". 2023-03-18 22:11:45 +08:00
fe1c7669b6 Added owner's equity accounts (base code starts with "3") when calculating the change of owner's equity in the specified period. 2023-03-18 22:11:45 +08:00
4eac10981f Added owner's equity (base code starts with "3") to the accounts that can take offset. 2023-03-18 22:11:44 +08:00
c869bccc04 Reordered the methods of the Account data model. 2023-03-18 22:11:44 +08:00
61c111db69 Revised the journal, the ledger, the income and expenses log, and the search result to show the last page first as the default upon pagination. 2023-03-18 22:11:44 +08:00
34f63c1cdf Renamed the "isOriginalEntry", "is-original-entry", "is_original_entry", and "isOriginalEntry()" methods and properties of journal entries to "isNeedOffset", "is-need-offset", "is_need_offset", and "isNeedOffset()", to be clear and understandable. 2023-03-18 22:11:44 +08:00
a643d9e811 Renamed the isAccountOffsetNeeded parameter to isAccountNeedOffset in the saveSummaryWithAccount method of the JournalEntryEditor class. 2023-03-18 22:11:44 +08:00
2239ddfad1 Revised the JavaScript to initialize the AccountSelector instances in JournalEntryEditor, so that the journal entry editor holds the AccountSelector instances. It can find the AccountSelector instance without needing to invoke its static methods. Removed the redundant static methods from the AccountSelector class. 2023-03-18 22:11:44 +08:00
12fbe36b9a Revised the JavaScript to initialize the SummaryEditor instances in JournalEntryEditor, so that the journal entry editor holds the SummaryEditor instances. It can find the SummaryEditor instance without needing to invoke its static methods. Removed the redundant static methods from the SummaryEditor class. 2023-03-18 22:11:44 +08:00
46e34bb89a Removed setting the redundant "entryType" dataset from the "onAddNew" and "onEdit" methods of the JournalEntryEditor class. It is not used anymore. 2023-03-18 22:11:44 +08:00
c9453d3023 Removed the redundant "entryType" parameter from the static "start" method of JavaScript AccountSelector. 2023-03-18 22:11:44 +08:00
fc766724c4 Removed the redundant "summary" parameter from the "#onOpen" and static "start" methods of JavaScript SummaryEditor. 2023-03-18 22:11:44 +08:00
38c394c0af Added the TransactionForm instance to the constructor of the JournalEntryEditor instance, so that the journal entry editor holds an instance of the transaction form, too. It does not need to find the transaction form all the way from the side property that may not be available. Retired the redundant getTransactionForm method from the JournalEntryEditor class. 2023-03-18 22:11:44 +08:00
67e2b06d37 Revised the JavaScript to initialize the JournalEntryEditor in TransactionForm, so that the transaction form holds the JournalEntryEditor instance. The DebitCreditSideSubForm and JournalEntrySubForm instances can find the JournalEntryEditor instance from the parent form, without needing to invoke its static methods. Removed the redundant static methods from the JournalEntryEditor class. 2023-03-18 22:11:44 +08:00
be10a8d99e Revised the coding style with the JavaScript arrow functions for the transaction form. 2023-03-18 22:11:44 +08:00
fbeec600b7 Replaced the long parameter list with the JournalEntryEditor instance in the save method of the JavaScript JournalEntrySubForm sub-form, to simplify the code. 2023-03-18 22:11:44 +08:00
1a54592d4c Added the amount attribute to the JavaScript JournalEntryEditor class to pass the amount to the JournalEntrySubForm without exposing the amount input element. 2023-03-18 22:11:44 +08:00
94a527caf2 Replaces the datasets with object attributes to store the column values in the JavaScript JournalEntryEditor class. 2023-03-18 22:10:28 +08:00
0a1bbbdd47 Replaced the isOriginalEntry dataset attribute with the isNeedOffset property in the JavaScript JournalEntryEditor. It does not make sense to store that information in the HTML. 2023-03-18 09:47:57 +08:00
82b63e4bd4 Revised the coding style in the JavaScript journal entry editor. 2023-03-18 04:02:48 +08:00
e1d1aff0c1 Simplified the code in the #resetDeleteCurrencyButtons method of the JavaScript TransactionForm form. 2023-03-18 03:55:55 +08:00
2e5f9ee01f Simplified the text data in the TestData clas in testlib_offset.py. 2023-03-18 03:41:51 +08:00
f901a0020f Revised the amount limitation tests in the OffsetTestCase test case, to be clear. 2023-03-18 03:38:07 +08:00
fc2be75c3b Changed the type of the amount property in the testing JournalEntryData data model from string to Decimal. 2023-03-18 03:21:47 +08:00
96c131940b Revised the date limitation tests in the OffsetTestCase test case, to be clear. 2023-03-18 03:08:08 +08:00
b9435a255b Added the "/.errors" route to the application in the "create_test_app" function in testlib.py, to make it easier to test. 2023-03-18 02:59:28 +08:00
56045f0faf Removed the unused __entry pseudo property from the JournalEntryForm form. 2023-03-17 22:52:38 +08:00
08d1e60238 Fixed the journal, the ledger, the income ane expenses log, and the search result to respect the transaction number before the debit/credit and the journal entry nuber. 2023-03-17 22:39:29 +08:00
d88b3ac770 Added to track the net balance and offset of the original entries. 2023-03-17 22:32:01 +08:00
40e329d37f Reordered the validators in the "accounting.transaction.forms.journal_entry" module. 2023-03-16 20:42:32 +08:00
23a0721d8d Added assert in the be function in the "accounting.utils.cast" module, to insure the correctness of the expression received. 2023-03-15 23:23:01 +08:00
2b2c665eb6 Replaced the if checks with assert in the IsBalanced validator of the currency sub-form of the transaction form, the NoOffsetNominalAccount validator of the account form, and the CodeUnique validator of the currency form. 2023-03-15 22:25:24 +08:00
954173a2c2 Removed the unused list-group-item-success class from style.css. 2023-03-15 01:43:49 +08:00
91e6dc6668 Removed an excess tailing blank line from the "accounting.currency.views" module. 2023-03-15 01:10:05 +08:00
e9d8a8fcd8 Added the "accounting.utils.cast" module to cast the things to the expected type in order to supress the warnings from PyCharm. 2023-03-15 01:09:59 +08:00
4c84686395 Removed an unused import from the "accounting" module. 2023-03-15 00:51:56 +08:00
61fd1849ed Removed the annotation future import from the "accounting.transaction.utils.account_option", "accounting.transaction.forms.journal_entry", and "accounting.transaction.forms.reorder" modules. 2023-03-14 21:51:19 +08:00
a67158f8f6 Moved the CodeUnique validator from an inner class of the CurrencyForm form to an independent class, and removed the annotation future import from the "accounting.currency.forms" module. 2023-03-14 21:48:11 +08:00
5c6bfd8b49 Revised the coding style of the NeedSomeCurrencies validator. 2023-03-14 21:42:02 +08:00
d9ecf51c6d Added the "create_test_app" function in testlib.py to replace "create_app" to prevent common mistakes. Added a get_csrf_token_view route to the application, and changed the get_csrf_token function to retrieve the CSRF token with the route without parsing the HTML for the CSRF token. 2023-03-14 21:28:35 +08:00
5d31eb9172 Removed the unnecessary future annotation import from the "accounting.transaction.forms.transaction" module. 2023-03-14 20:44:06 +08:00
fadce244c5 Revised the type hint and the coding style of the NeedSomeCurrencies validator. 2023-03-14 20:43:28 +08:00
cbe7c6ca6d Added dummy.js to .gitignore and MANIFEST.in for exclusion. 2023-03-14 17:03:22 +08:00
b03938fb2e Added test_temp.py to the exclusion in MANIFEST.in. 2023-03-14 17:03:21 +08:00
8061a23fdc Renamed the AbstractUserUtils class to UserUtilityInterface, and added the can_view and can_edit functions to the UserUtilityInterface interface. There is no need to separately supply two additional can_view and can_edit callbacks. 2023-03-14 17:03:18 +08:00
cd8a480cd0 Revised the documentation of the AbstractUserUtils class. 2023-03-14 17:03:16 +08:00
b8b87714eb Revised the documentation of the JavaScript summary editor. 2023-03-14 17:03:12 +08:00
bf2f96621d Revised so that when the account selector finds the codes from the form, the journal entry editor is used to find the form instead of messing-up with the TransactionForm class and its static method that was a shortcut to the private instance method of the same name. 2023-03-14 17:03:10 +08:00
2d771f04be Fixed so that saving the journal entry from the journal entry editor triggers updating the total of the debit or credit side, which in turn triggers validating the balance if it is on a transfer form. This fixed the problem that deleting a journal entry updates total but not re-validating the balance. 2023-03-14 17:03:06 +08:00
3a12472d4b Fixed the indent in the template of the account selector. 2023-03-14 17:03:01 +08:00
d5a686a5d8 Removed trailing blank spaces from the JavaScript summary editor. 2023-03-14 17:02:58 +08:00
690f89e29a Removed the unused accounting-debit-account-code and accounting-credit-account-code HTML classes. 2023-03-14 17:02:54 +08:00
82a6a53dc4 Revised the account selector to find the account codes from the form through the TransactionForm class, but not finding the codes by itself. 2023-03-14 17:02:52 +08:00
cdd31b1047 Added the missing documentation to the static initialize method of JavaScript TransactionForm class. 2023-03-14 17:02:50 +08:00
5bad949cfa Fixed the documentation of the entryType parameter in the constructor of the JavaScript DebitCreditSideSubForm sub-form. 2023-03-14 17:02:48 +08:00
3826646d06 Reordered the journal entry editor and put the summary first and the account later. 2023-03-14 17:02:46 +08:00
74071e8997 Removed the unused static validateAccount method from the JavaScript journal entry editor. 2023-03-14 17:02:43 +08:00
3ce34803f3 Moved the journal entry editor from the transaction-form.js to a new independent JavaScript file journal-entry-editor.js. 2023-03-14 17:02:41 +08:00
232f73172f Removed the prefix from the journal entry sub-form. 2023-03-14 17:02:39 +08:00
ff1bb7142b Removed the unused data-prefix attribute from the currency sub-forms of the transaction form. 2023-03-14 17:02:37 +08:00
7155bf635a Removed the data-entry-type attribute from the journal entry editor form. The entry type is passed by the object. There is no need to store this information in the HTML anymore. 2023-03-14 17:02:35 +08:00
c306ff8009 Revised the JavaScript journal entry editor and account selector so that the account selector work with the journal entry editor and does not get into the detail of the journal entry editor. 2023-03-14 17:02:32 +08:00
b344abce06 Added the #prefix property to the journal entry editor to simplify the consistency. 2023-03-14 17:02:29 +08:00
b3c666c872 Fixed the addJournalEntry method of the DebitCreditSideSubForm sub-form to re-validate the whole side after a new journal entry is added. 2023-03-14 17:02:14 +08:00
6a671cac84 Revised the JavaScript journal entry editor and summary editor so that the summary editor work with the journal entry editor and does not get into the detail of the journal entry editor. 2023-03-14 16:59:54 +08:00
fe87c3a7de Fixed the documentation of the #side property of the JavaScript JournalEntryEditor class. 2023-03-14 16:59:51 +08:00
2013f8cbd9 Removed the initializeNewJournalEntry method from the JavaScript SummaryEditor. It does not do meaningful things at all. 2023-03-14 16:59:49 +08:00
2325842471 Fixed the documentation of the JavaScript for the transaction form. 2023-03-14 16:59:48 +08:00
c80e58b049 Renamed the journal entry form to journal entry editor, to be clear. 2023-03-14 16:59:46 +08:00
be0ae5eba4 Replaced the function-based JavaScript with the object-oriented TransactionForm, CurrencySubForm, DebitCreditSideSubForm, JournalEntrySubForm, and JournalEntryForm classes for the transaction form. 2023-03-14 16:59:36 +08:00
2b84f64554 Replaced the function-based JavaScript with the object-oriented AccountForm class for the currency form. 2023-03-12 22:15:56 +08:00
0a658a76e8 Replaced the function-based JavaScript with the object-oriented AccountForm class for the account form. 2023-03-12 22:15:54 +08:00
50dc79d865 Added the missing is-invalid class on errors to the currency field in the currency sub-forms of the transaction form. 2023-03-12 16:51:25 +08:00
8e5377a416 Replaced the payable account with the petty-cash account in the SummeryEditorTestCase test case. 2023-03-12 01:34:47 +08:00
4299fd6fbd Revised the code in the JavaScript initializeBaseAccountSelector function in the account form. 2023-03-12 01:34:45 +08:00
1d6a53f7cd Revised the account form so that the if-offset-needed option is only available for real accounts. 2023-03-12 01:34:42 +08:00
bb2993b0c0 Reordered the code in the "accounting.transaction.forms.journal_entry" module. 2023-03-11 20:36:38 +08:00
f6946c1165 Revised the IsBalanced validator so that it no longer need the __future__ annotation. 2023-03-11 19:10:47 +08:00
8e219d8006 Fixed the type hint of the form parameter in the NeedSomeJournalEntries validator. 2023-03-11 19:10:44 +08:00
53565eb9e6 Changed the IsBalanced validator from an inner class inside the TransferCurrencyForm form to an independent class. 2023-03-11 19:10:42 +08:00
965e78d8ad Revised the rule for the accounts that need offset in the accounting-init-accounts console command. 2023-03-11 17:15:08 +08:00
74b81d3e23 Renamed the offset_original_id column to original_entry_id, and the offset_original relationship to original_entry in the JournalEntry data model. 2023-03-11 16:34:30 +08:00
a0fba6387f Added the order to the search report. 2023-03-11 16:34:30 +08:00
d28bdf2064 Revised the parameter order in the template of the currency sub-form of the transaction form. 2023-03-11 16:34:29 +08:00
edf0c00e34 Shortened the names of the #filterAccountOptions, #getAccountCodeUsedInForm, and #shouldAccountOptionShow methods to #filterOptions, #getCodesUsedInForm, and #shouldOptionShow, respectively, in the JavaScript AccountSelector class. 2023-03-11 16:34:29 +08:00
107d161379 Removed a debug output from the JavaScript AccountSelector class. 2023-03-11 16:34:29 +08:00
f2c184f769 Rewrote the JavaScript AccountSelector to store the page elements in the object. 2023-03-11 16:34:28 +08:00
b45986ecfc Fixed the parameter type for the JavaScript AccountSelector class. 2023-03-11 16:34:28 +08:00
a2c2452ec5 Added a missing blank line to the JavaScript AccountSelector class. 2023-03-11 16:34:28 +08:00
5194258b48 Removed the redundant #init method from the JavaScript AccountSelector class. 2023-03-11 16:34:28 +08:00
3fe7eb41ac Removed the unused "__in_use_account_id" property from the TransactionForm form. 2023-03-11 16:34:28 +08:00
7fb9e2f0a1 Added missing documentation to the OptionLink data model in the "accounting.report.utils.option_link" module. 2023-03-11 16:34:28 +08:00
1d443f7b76 Renamed the "accounting.transaction.form" module to "accounting.transaction.forms". It only contains forms now. 2023-03-11 16:34:28 +08:00
6ad4fba9cd Moved the "accounting.transaction.operators", "accounting.transaction.summary_editor" and "accounting.transaction.form.account_option" modules into the "accounting.transaction.utils" module. 2023-03-11 16:34:28 +08:00
3dda6531b5 Split the "accounting.transaction.forms" module into various submodules in the "accounting.transaction.form" module. 2023-03-11 16:33:51 +08:00
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
9993f65627 Added the search to the accounting data. 2023-03-09 06:37:31 +08:00
fe01d5418d Fixed to limit the width of the search box in the currency list, base account list, account list, and transaction list. 2023-03-09 06:37:31 +08:00
2f7b9932a0 Added the base report class to ensure that the reports can both be shown on the page and downloaded as CSV. 2023-03-09 06:37:28 +08:00
1eed16b732 Added the pseudo account for the income and expenses log to query the income and expenses log of the current assets and liabilities. 2023-03-09 06:37:28 +08:00
ede1160943 Fixed the ledger and the income and expenses log not to show the total entry when there is actually no data. 2023-03-09 06:37:27 +08:00
3814f0cb18 Added the missing accounting_format_amount filter to the total of the ledger. 2023-03-09 06:37:27 +08:00
24315b8203 Fixed the styles of the negative numbers in the reports with red and braced absolute values. 2023-03-09 06:37:26 +08:00
3c200d0dc6 Fixed the sign of the amount in income statement. 2023-03-09 06:37:26 +08:00
9f1e724875 Added the "accounting.report.reports.csv_export" module to handle the CSV export in one place. 2023-03-09 06:37:26 +08:00
f838e7f893 Moved the utilities that are only for the report generators from the "accounting.report" module to the "accounting.report.reports.utils" module. 2023-03-09 06:37:25 +08:00
edb893ecd3 Replaced the report generators with a separated module for each report, to work with the diversity of the report formats without messing-up one another. 2023-03-09 06:37:23 +08:00
436a4c367f Added the balance sheet. 2023-03-09 06:37:22 +08:00
1813ce0cfa Removed the unused and empty __get_category method from the IncomeStatement report. 2023-03-09 06:37:22 +08:00
7683347997 Removed a non-existing parameter from the documentation of the constructor of the IncomeStatementParams class. 2023-03-09 06:37:22 +08:00
46ffc7a73d Changed the display style of the rows in the income statement from grid to flex, to simplify the layout. 2023-03-09 06:37:21 +08:00
e0a807d625 Fixed the style of the indent and total line of the income statement. 2023-03-09 06:37:20 +08:00
dffcf6d2ce Removed the unused is_account pseudo property in the IncomeStatementRow row. 2023-03-09 06:37:19 +08:00
84d239e4b1 Added the income statement. 2023-03-09 06:37:19 +08:00
fcefc64117 Removed an excess closing </div> in the template of the trial balance. 2023-03-09 06:37:18 +08:00
81fbb380b4 Renamed the variable select_trial_balance to select_balances in the __query_balances method of the TrialBalance report. 2023-03-09 06:37:17 +08:00
d7ac8a3dcf Fixed the documentation of the TrialBalanceRow class. 2023-03-09 06:37:17 +08:00
bcd3418e2c Fixed the documentation in the constructor of the trail balance. 2023-03-09 06:37:16 +08:00
ef9e5cb5b3 Split the report parameters from the report class so that it works better with both CSV export and HTML templates. 2023-03-09 06:37:16 +08:00
e797cfeb8c Simplified the logic to test the total row in the ledger and income and expenses. 2023-03-09 06:37:16 +08:00
22bae7f766 Replaced the <ul></ul> list with CSS "display: grid" for the trial balance, to allow using <a></a> as the table row. 2023-03-09 06:37:15 +08:00
aa669e9f53 Replaced tables with CSS "display: grid" for the journal, ledger, and income and expenses, to allow using <a></a> as the table row. 2023-03-09 06:37:14 +08:00
898a1af7b5 Revised the separation lines in the table headers and footers of the ledger tables. 2023-03-09 06:37:13 +08:00
f762bcf48f Replaced the duplicated "accounting-transaction-card" and "accounting-report-card" CSS classes with the "accounting-sheet" class, for simplicity. 2023-03-09 06:37:12 +08:00
5d4effa360 Added the "debit" and "credit" properties to, and removed the "is_debit" property from the JournalRow row, to reduce the amount of logic in the template of the journal. 2023-03-09 06:37:11 +08:00
dd05478bf3 Simplified the syntax to retrieve query arguments in the templates, and reduced the amount of logic in the templates. 2023-03-09 06:37:11 +08:00
9450915404 Added the "default" filter to reduce the amount of logic in the templates, which differs from the Jinja2 "default" filter in that it looks for None instead of undefined values. 2023-03-09 06:37:10 +08:00
8d126e183f Changed the date field of the transaction forms to set the default value in the view, but not the form, so that the default value is not set when it did not receive a value. 2023-03-09 06:37:10 +08:00
bfb08cf5fc Changed the format_amount_input filter to accept None, and return an empty string if the value is None. 2023-03-09 06:37:09 +08:00
a7bcf4b5c1 Changed the format_amount template filter to return None when the value is None. 2023-03-09 06:37:08 +08:00
cd49ca44b1 Fixed to avoid getting the income and expenses with accounts that are not current assets and liabilities when switching from the ledger. 2023-03-09 06:37:07 +08:00
734362396f Adding the missing currency when constructing the report chooser in the trial balance. 2023-03-09 06:37:06 +08:00
88147bea66 Revised the currency in the titles and options of the ledger, income and expenses, and trial balance. 2023-03-09 06:37:05 +08:00
cca43c68a6 Added trial balance. 2023-03-09 06:37:05 +08:00
480e2d2d8f Simplified the invocation of the super class constructor in the subclasses of Period. 2023-03-09 06:37:05 +08:00
be100ce7ec Simplified the constructors of the period choosers. 2023-03-09 06:37:04 +08:00
eca91d32ed Fixed the documentation of the IncomeExpensesPeriodChooser class. 2023-03-09 06:37:04 +08:00
1f95212494 Revised to use the title case in the CSV output of the journal and income and expenses. 2023-03-09 06:37:03 +08:00
0173104c84 Revised the currency field in the CSV output of the journal. 2023-03-09 06:37:03 +08:00
6e33fa775d Revised the __query_entries method of the IncomeExpenses report to be clear. 2023-03-09 06:37:02 +08:00
e244ff70e6 Simplified the SQL query in the currency_options and account_options pseudo properties in the Ledger report and the account_options pseudo property in the IncomeExpenses report. 2023-03-09 06:37:01 +08:00
ace782a26b Replaced "sa.select" with "sa.Select" in the account_options pseudo property of the IncomeExpenses report. 2023-03-09 06:37:01 +08:00
90289a0db2 Fixed the account options to list only the current assets and liabilities for the income and expenses. 2023-03-09 06:37:01 +08:00
7e7e1a2844 Revised so that the amounts won't wrap in the income and expenses. 2023-03-09 06:37:01 +08:00
ddd028736c Revised the balance in the mobile view of the income and expenses. 2023-03-09 06:37:01 +08:00
e1d35a64da Revised the account shown in the journal. 2023-03-09 06:37:01 +08:00
39807ef480 Added the income and expenses. 2023-03-09 06:37:00 +08:00
39723b1299 Removed the lazy setting from the account relationship of the JournalEntry data model. It results in problems in the income and expense report. 2023-03-09 06:36:25 +08:00
8cd004bede Revised the documentation of the ledger. 2023-03-05 18:16:43 +08:00
4f112dd386 Revised the documentation of the report row classes. 2023-03-05 18:14:32 +08:00
b806b1ed1f Added note to the CSV output of ledgers. 2023-03-05 17:55:47 +08:00
1d0a79e33c Changed the CSV field name to be title-cased. 2023-03-05 17:55:47 +08:00
d4a690ebbc Removed the text of the action buttons of currency and account filters for the small screens, to fit in the screen. 2023-03-05 17:55:46 +08:00
68687897f3 Changed the icons for the reports and accounts to be more accessible. 2023-03-05 17:55:46 +08:00
a7250fd9bf Replaced transactions with reports in the navigation menu. 2023-03-05 17:55:46 +08:00
eabe80b790 Added ledger. 2023-03-05 17:55:38 +08:00
fe77f87110 Fixed a regular expression in the _parse_period_spec function of the period utility. 2023-03-05 14:24:51 +08:00
32c27d7c07 Fixed an error finding the end of month in the __get_month_spec method of the Period utility. 2023-03-05 12:06:16 +08:00
14b871b57a Fixed the format_amount filter to deal with negative numbers with decimals correctly. 2023-03-05 11:48:34 +08:00
9d5fce2752 Fixed the documentation of the Journal report. 2023-03-04 20:14:08 +08:00
d333151731 Revised the documentation of the __get_journal_list view. 2023-03-04 20:12:13 +08:00
b2e500a714 Fixed the documentation of the get_default_journal_list view. 2023-03-04 20:10:59 +08:00
b705795b44 Moved the ReportType enumeration from the "accounting.report.report_chooser" module to the new "accounting.report.report_type" module. 2023-03-04 19:56:07 +08:00
250f4ff1ae Revised the imports in the "accounting.report.reports" module 2023-03-04 19:52:45 +08:00
6bed180790 Renamed the TransactionTypeEnum enum to TransactionType. 2023-03-04 19:39:13 +08:00
10fbc3f638 Renamed the "accounting.transaction.dispatcher" module to "accounting.transaction.operators". 2023-03-04 19:36:53 +08:00
f65dc6fc42 Renamed the TransactionType class to TransactionOperator. 2023-03-04 19:36:07 +08:00
9833bac6e4 Added the TransactionTypeEnum in the new "accounting.utils.txn_types" module to remove the dependency from the "accounting.report" module to the "accounting.transaction" module. 2023-03-04 19:32:36 +08:00
7d412b20d7 Moved the material floating action button template to add new transactions from the "accounting/transaction/include" directory to the "accounting/include" directory, and renamed it from add-new-material-fab.html to add-txn-material-fab.htm, as it will also be used in the reports, not only the transaction management. 2023-03-04 18:40:00 +08:00
9bfcd3c50c Added the journal report as the first accounting report. 2023-03-04 18:31:33 +08:00
55c2ce6695 Added Tempus Dominus to the CDN of the test site. 2023-03-04 14:36:10 +08:00
493677e0aa Added "crossorigin" to the CDN stylesheets in the test site. 2023-03-04 14:36:08 +08:00
710c26d016 Fixed the documentation in the JavaScript SummeryHelper class. 2023-03-04 13:45:30 +08:00
24415018b7 Reordered the properties in the JavaScript SummeryHelper class. 2023-03-04 13:45:29 +08:00
c50b9a2000 Removed the unused tab and page classes from the templates of the summary editor. 2023-03-04 11:52:45 +08:00
af9bd14eed Removed the unused tab ID from the template of the summary editor. 2023-03-04 11:52:45 +08:00
9e1ff16e96 Fixed the aria-labelledby in the template of the summary editor. 2023-03-04 11:52:45 +08:00
f7c1fd77f2 Added blank lines and documentation to the template of the summary editor. 2023-03-04 11:52:45 +08:00
641315537d Replaced the traditional function expressions with ES6 arrow function expressions in the JavaScript for the transaction order, account order, currency form, and the speed dial for the material floating action buttons. 2023-03-04 11:52:45 +08:00
a895bd8560 Replaced the traditional function expressions with ES6 arrow function expressions in the JavaScript for the drop-and-drop reorder. 2023-03-04 11:52:44 +08:00
ca86a08f3e Replaced the traditional function expressions with ES6 arrow function expressions in the JavaScript for the account form. 2023-03-04 11:52:44 +08:00
e118422441 Replaced the traditional function expressions with ES6 arrow function expressions in the JavaScript for the account selector. 2023-03-04 11:52:44 +08:00
b3777cffbf Replaced the traditional function expressions with ES6 arrow function expressions in the JavaScript for the transaction form. 2023-03-04 11:52:44 +08:00
39c9c17007 Replaced the traditional function expressions with ES6 arrow function expressions in the JavaScript for the summary editor to avoid messing up with the "this" object. 2023-03-04 11:52:44 +08:00
3ab4eacf9f Moved the "accounting.transaction.template_globals" module to "accounting.template_globals", for the two template globals will be used in the reports beside the transaction management. 2023-03-04 07:06:03 +08:00
cff3d1b6bd Revised the code order in the init_app function in the "accounting" module. 2023-03-04 07:01:03 +08:00
f41db78831 Revised the summary helper so that when the summary is changed with the tag changed, the on-change callback is run to check the tag button status. 2023-03-04 07:01:03 +08:00
73f7d14e7b Fixed so that the values of the input fields are trimmed before composing the summary when they are changed. 2023-03-04 07:01:03 +08:00
f6ed6b10a7 Revised the summary editor to allow the "*" start character as the multiplication operation in addition to the "×" times character. 2023-03-04 07:01:03 +08:00
b5aaee4d15 Renamed the "number" tab plane to "annotation". 2023-03-04 07:01:03 +08:00
c849d6b3d4 Revised the numer tab plane in the summary editor. 2023-03-04 07:01:03 +08:00
a9908a7df4 Simplified the regular expression in the populate method of the GeneralTagTab class in the summary editor. 2023-03-04 07:01:02 +08:00
063c769158 Renamed the variables in the summary editor. 2023-03-04 07:01:02 +08:00
f8e9871300 Fixed to trim the summary when it is changed in the summary editor. 2023-03-04 07:01:02 +08:00
78a62a9575 Added a "note" field to the summary editor. 2023-03-04 07:01:02 +08:00
85fde6219e Fixed an HTML ID in the summary editor modal. 2023-03-04 07:01:02 +08:00
4eb9346d8d Renamed summary helper to summary editor. 2023-03-04 07:00:46 +08:00
11966a52ba Fixed a variable name in the #initializeAccountQuery method of the JavaScript AccountSelector class. 2023-03-04 06:57:35 +08:00
8cf81b5459 Revised the documentation of the "accounting.account.queries", "accounting.base_account.queries", "accounting.currency.queries", and "accounting.transaction.queries" modules. 2023-03-04 06:57:35 +08:00
cc958a39b3 Moved the format_amount and format_date template filters from the "accounting.transaction.template_filters" module to the "accounting.template_filters" module, and rename the filters from "accounting_txn_format_amount" and "accounting_txn_format_date" to "accounting_format_amount" and "accounting_format_date", respectively. They will not only be used in the transaction management, but also the reports. 2023-03-04 06:57:10 +08:00
9065686cc5 Split the "accounting.transaction.template" module into the "accounting.transaction.template_filters" and "accounting.transaction.template_globals" modules. 2023-03-03 18:28:59 +08:00
9a41cb10a1 Rewrote the summary helper, added the TabPlane classes so that the internal states of the summary helper is stored in the tab plane objects instead of passing the as parameters and variables. 2023-03-03 18:09:36 +08:00
6957e52d0d Renamed the "accounting.account.query", "accounting.base_account.query", "accounting.currency.query", and "accounting.transaction.query" modules to "accounting.account.queries", "accounting.base_account.queries", "accounting.currency.queries", and "accounting.transaction.queries", respectively. There will be more than one query in the next report module. 2023-03-03 01:12:36 +08:00
9cd9e90be0 Renamed the variables in the tests of the AccountTestCase and CurrencyTestCase test cases, for simplicity. 2023-03-01 21:09:14 +08:00
2839dc60b4 Revised the documentation of the PREFIX constant in test_account.py, test_currency.py, and test_transaction.py. 2023-03-01 20:22:27 +08:00
f3548a2327 Advanced to version 0.4.0. 2023-03-01 01:49:08 +08:00
79883d6940 Changed the Sphinx documentation scheme from "nature" to "sphinx_rtd_theme", to prepare for publishing in the future. 2023-03-01 01:48:56 +08:00
b2bc993416 Replaced the #populate method with the #parseAndPopulate method that is used both when starting the summary helper and when the summary input is updated. 2023-03-01 01:45:38 +08:00
453b3f0da5 Renamed the #tagInputOnChange method to #onTagInputChange in the JavaScript summary helper. 2023-03-01 01:31:25 +08:00
63ae3f0746 Replace the is_in_use pseudo property of the Account data model with the AccountOption class, and revised the #getAccountCodeUsedInForm method of the SummaryHelper, to solve the issue that the list of used accounts should be different for debit and credit entries. 2023-03-01 01:28:25 +08:00
da4cc6489f Removed the direction arrows from the tab navigation in the summary helper. 2023-03-01 00:59:40 +08:00
1102a3a4f3 Updated the translation. 2023-03-01 00:51:58 +08:00
1402a12f04 Simplified the logic in the add_txn function in testlib_txn.py. 2023-03-01 00:51:24 +08:00
f049b5d7ee Revised the form data used in the SummeryHelperTestCase test case, to avoid problems with SonarQube. 2023-03-01 00:51:24 +08:00
14ed4ca354 Added the #initializeTagButtons and #tagInputOnChange methods to the JavaScript SummaryHelper to simplify the code. 2023-03-01 00:51:24 +08:00
535ff96ab3 Revised the JavaScript regular expressions used in the summary helper, as suggested by SonarQube for security. 2023-03-01 00:51:24 +08:00
57482f81fc Revised the transaction form to start a new journal entry with the journal entry form instead of the summary helper, because it feels strange when the user want to leave the summary empty. 2023-03-01 00:51:24 +08:00
a31ce3c400 Replaced the function-based JavaScript account selector with the AccountSelector class that does things better. 2023-03-01 00:51:11 +08:00
319f0aed90 Fixed a documentation in the JavaScript summary helper. 2023-02-28 22:54:20 +08:00
826dcf0f86 Revised the documentation of the JavaScript for the summary helper. 2023-02-28 22:47:04 +08:00
b2411aee74 Updated the Sphinx documentation. 2023-02-28 22:44:40 +08:00
731acdced0 Revised the HTML in the summary helper template. 2023-02-28 22:41:56 +08:00
35b3bca1e6 Renamed the variables for the button elements in the summary helper, to be clear. 2023-02-28 22:37:46 +08:00
3c413497ae Split the JavaScript for the account selector from transaction-form.js to account-selector.js, to modularize the complex JavaScript. 2023-02-28 22:33:14 +08:00
1b5e516413 Renamed the HTML ID and class name prefix of the account selector modal, for consistency. 2023-02-28 22:24:12 +08:00
20cb5cecc4 Renamed the accounting-selector-modal class to accounting-account-selector-modal in the account selector. 2023-02-28 22:14:03 +08:00
08dc24605d Replaced the forEach loops with the for-of loops in the JavaScript for the currency form, account form, and the drag-and-drop reorder library functions. 2023-02-28 22:09:39 +08:00
bb7e9e94ee Replaced the forEach loops with the for-of loops whenever appropriate in the JavaScript for the transaction form. 2023-02-28 22:00:19 +08:00
2680a1c872 Merged debit-account-modal.html and credit-account-modal.html into account-selector-modal.html, because they are almost the same. 2023-02-28 21:45:10 +08:00
20a7ce591c Renamed the account_selector_modals block to form_modals in the transaction form templates. 2023-02-28 21:37:08 +08:00
474e844ed9 Revised the loading of the summary helper so that only the required helpers are loaded, but not both the debit and credit helpers. 2023-02-28 21:35:02 +08:00
b34955f2fb Replaced the forEach loops with the for-of loops in the JavaScript summary helper. The for-of loops are more consistent with the other languages and the traditional for loops, and do not mess up with the "this" object. 2023-02-28 20:20:36 +08:00
2bd0f0f14d Fixed the target in the initShow method of the JavaScript summary helper. 2023-02-28 19:13:08 +08:00
8b77d9ff93 Added the suggested accounts to the summary helper. 2023-02-28 19:11:09 +08:00
a9c7360020 Renamed the variables in the #reset method of the JavaScript SummaryHelper class, for consistency. 2023-02-28 17:14:02 +08:00
d02c87602b Added validation to the summary helper. 2023-02-28 16:38:50 +08:00
9f966643b5 Added ARIA labels to the different pages in the summary helper. 2023-02-28 16:38:19 +08:00
5746e2a3d6 Added a missing amount filter to the debit entries of the transaction form. 2023-02-28 15:52:30 +08:00
d5c2231794 Added the summary helper for the transaction form. 2023-02-28 15:49:01 +08:00
fc8e257a10 Added missing documentation to the currencies_errors pseudo property of the TransactionForm form. 2023-02-28 09:36:20 +08:00
2e9bf382fb Revised the documentation of the "accounting.transaction.dispatcher" module. 2023-02-28 09:31:46 +08:00
de48c848da Revised the code in the common account shorts in testlib_txn.py. 2023-02-28 08:24:15 +08:00
9cdcc828a7 Added the add_txn function to testlib_txn.py and applied it in the transaction test cases. 2023-02-28 08:14:23 +08:00
b28d446d07 Advanced to version 0.3.1. 2023-02-28 00:16:20 +08:00
274a38a588 Fixed a localization error in the transaction detail. 2023-02-28 00:16:12 +08:00
fff89a9957 Replaced the direct database add with the relationship append in the JournalEntryCollector class, to fix the PostgreSQL error that the new journal entries are added when the transaction is not added yet. 2023-02-28 00:04:32 +08:00
5613657c8f Fixed the JavaScript filterAccountOptions function in the transaction form so that the accounting list is not hidden when there is no account in use. 2023-02-27 23:00:49 +08:00
26bb16dd40 Revised the translation. 2023-02-27 18:59:50 +08:00
f0d39bb27b Added the action button to convert a cash income or cash expense transaction to a transfer transaction. 2023-02-27 18:59:42 +08:00
4c17310ebf Fixed an error to recognize the current transaction type in the supplied URI in the with_type filter in the "accounting.transaction.template" module. 2023-02-27 18:47:19 +08:00
fd36672877 Revised the imports in the "accounting.transaction.views" module. 2023-02-27 18:44:33 +08:00
d67c57056b Added the accounting_txn_format_amount_input template filter to properly format the decimal amount for the number input fields. 2023-02-27 18:40:54 +08:00
59c55ef574 Fixed the amount display in the template of the journal entry sub-form. 2023-02-27 18:34:02 +08:00
329027969a Advanced to version 0.3.0. 2023-02-27 17:23:20 +08:00
9f7a8c9540 Revised the Sphinx documentation. 2023-02-27 17:22:56 +08:00
384bb2c46d Added the dummy commented <ul>...</ul> to the navigation menu and the journal entry sub-form templates, for SonarQube not to complain about incorrect HTML. 2023-02-27 17:20:43 +08:00
cabfe268ce Added the page_37 and page_size_15_default constants in the test_malformed test of the PaginationTestCase test case, for consistency. 2023-02-27 17:15:29 +08:00
26df71014b Added the LIST_URL and DETAIL_URI constants to test_base_account.py, for consistency. 2023-02-27 16:37:01 +08:00
3126ee8153 Added the NEXT_URI constant to test_account.py, for consistency. 2023-02-27 16:37:01 +08:00
cb622f4bad Added the __get_detail_uri function to the "accounting.currency.views" module, for simplicity. 2023-02-27 16:31:41 +08:00
515d39e61c Added the __get_detail_uri and __get_list_uri functions to the "accounting.account.views" module, for simplicity. 2023-02-27 16:29:56 +08:00
952061c4bb Added the TEST_SERVER constant in testlib.py, for consistency. 2023-02-27 16:25:36 +08:00
788225826d Added resource integrity to the decimal.js-light CDN in the test site. 2023-02-27 16:21:30 +08:00
c52081e528 Replaced decimal.js CDN from cdnjs with decimal.js-light CDN from jsDelivr in the base template of the test site. 2023-02-27 16:20:58 +08:00
1f235acdf9 Added resource integrity to the bootstrap CDN in the test site. 2023-02-27 16:07:59 +08:00
0f6c23e1f3 Replaced the regular expression replace with trimEnd() in the validateNote validator in the JavaScript for the transaction form. 2023-02-27 16:03:25 +08:00
488e72679e Revised the NextUriTestCase view, split the test_next_uri test into the two test_next_uri and test_no_next_uri tests, and replaced the decorator with add_url_rule to work around the security audit from SonarQube. 2023-02-27 15:57:39 +08:00
6d43b14862 Added CSRF to the test_next_uri test in the NextUriTestCase test case. 2023-02-27 15:35:35 +08:00
685213cdbb Revised the translation. 2023-02-27 15:29:37 +08:00
05fde3a742 Added the transaction management. 2023-02-27 15:28:45 +08:00
9383f5484f Revised aria-label in the templates, added necessary aria labels, removed excess aria labels, and added localization. 2023-02-27 12:54:41 +08:00
88314e1e45 Revised the regular expression in the find_by_code method of the Account data model. 2023-02-27 12:54:38 +08:00
83b5761bca Replaced the for loop with the for-of loop in the search-as-you-type JavaScript of the account form. 2023-02-27 10:30:21 +08:00
f25c993b75 Revised the translation of the test site. 2023-02-27 10:30:19 +08:00
6d02f8033d Revised the font awesome icon of the accounting application in the navigation menu. 2023-02-27 10:18:29 +08:00
2c367703e4 Removed a debug logging in the JavaScript for the account form. 2023-02-27 10:18:29 +08:00
284b5be128 Fixed the typo "model" to "modal" in the templates. 2023-02-27 10:18:10 +08:00
a672a13789 Revised the strip_text filter to return None when the text is empty. 2023-02-26 08:00:58 +08:00
9af9afd14d Added the height for the textarea with floating labels. 2023-02-26 07:59:25 +08:00
d98e9f8f05 Added the accounting-dragged class to replace the list-group-item-dark class when reordering with drag-and-drop, because the dragged list may not be a list group. 2023-02-26 07:54:23 +08:00
652bddc07a Fixed an error in the onDragOver function in drag-and-drop-reorder.js that sometimes the dragged object may be null. 2023-02-26 07:54:20 +08:00
5a6e4f5b5e Replaced the import for the db object from the accounting model with the test site in test_account.py and test_currency.py. They are the same object, and the db object from the test site is safe at the compile time. 2023-02-25 18:04:32 +08:00
f878ba5535 Revised to rewind the time in the test_update_not_modified tests of the AccountTestCase and CurrencyTestCase test cases, so that the test cases don't have to wait for the time to be different. 2023-02-25 18:04:29 +08:00
e7c36ba13a Revised the type hints in the test_update_not_modified tests of the AccountTestCase and CurrencyTestCase test cases. 2023-02-25 18:04:27 +08:00
4cfe7c7c59 Added the flash_all_errors utility in the "accounting.utils.flash_errors" module to recursively flush all form errors in the sub-forms. 2023-02-25 12:27:55 +08:00
b0b30a8ae6 Fixed the broken action button group in the account list and currency list, by adding a separated action button group for the mobile screens. 2023-02-25 10:37:28 +08:00
2e3633b205 Revised to sort the accounts in the same base before saving an account to a new base, and added the test_change_base_code test to the AccountTestCase test case for this. 2023-02-25 09:44:17 +08:00
d68aa91c33 Removed the redundant post_update methods from the AccountForm and CurrencyForm forms. 2023-02-24 17:18:55 +08:00
3f63fb0bda Fixed a type hint in the populate_obj method of the AccountForm form. 2023-02-24 00:18:55 +08:00
d5af5de3c1 Renamed offset to pay-off, to be clear. 2023-02-23 11:32:55 +08:00
d9c08568cf Revised the test_update_not_modified tests to be more specific in the AccountTestCase and CurrencyTestCase test cases. 2023-02-21 09:38:31 +08:00
a4c89f1494 Added the type hint and the documentation for the obj parameter of the post_update method of the AccountForm and CurrencyForm forms. 2023-02-20 16:08:49 +08:00
a73e3204b9 Renamed the "accounting.utils.next_url" module to "accounting.utils.next_uri". 2023-02-20 16:08:32 +08:00
330a71ebf2 Fixed the logic in the __set_next method in the "accounting.utils.next_url" module. 2023-02-20 08:17:31 +08:00
36b0bb3a0e Revised the import in the "accounting.account.view" module. 2023-02-18 18:40:11 +08:00
2ab60b2224 Replaced "unittest.TestCase.assert*" methods with "assert" in the common test functions, for simplicity. 2023-02-13 19:18:41 +08:00
36f55900c7 Renamed "fh" to "fp" when opening files, following the Python convention. 2023-02-09 00:02:14 +08:00
d99f592cff Merged the "accounting.database" module into the "accounting" module. It has only one member as "db", the database instance, and does not need to be separated into another file. 2023-02-08 11:13:09 +08:00
e24ed61b99 Added 7 currencies. 2023-02-08 11:05:59 +08:00
354f1ff3d8 Moved the currency data from the "accounting.currency.commands" module into the currencies.csv file, separating the code and the data. Rewrote the test case to test against each all the content imported. The locales are read from the CSV file instead of hard-coded in the code, so that the translations are not hard-coded to Mandarin. 2023-02-08 10:50:20 +08:00
d8e0e30c41 Revised the coding style in the test_init test of the BaseAccountCommandTestCase test case. 2023-02-08 10:47:55 +08:00
d58859bcf3 Removed the unused BaseAccountData data type. 2023-02-08 10:39:13 +08:00
40e64c4d2e Rewrote the test_init test of the BaseAccountCommandTestCase test case to test all the content imported. The translations are not hard-coded to Mandarin now. 2023-02-08 10:28:41 +08:00
2aacb67988 Moved the base account data from the "accounting.base_account.commands" module into the data directory as a CSV file, separating the code and the data. The locales are read from the CSV file instead of hard-coded in the code, so that the translations are not hard-coded to Mandarin. 2023-02-08 10:28:02 +08:00
a839c5a41a Fixed the path of the test site in MANIFEST.in. 2023-02-08 09:45:25 +08:00
356d10eb6e Added the test_api_exists test to the CurrencyTestCase test case. 2023-02-07 21:44:58 +08:00
8dc340dbf1 Advanced to version 0.2.0. 2023-02-07 20:55:00 +08:00
4b5b348270 Implemented the incremental search (search-as-you-type) in the base account selector of the account form. 2023-02-07 20:27:25 +08:00
d9585f0e53 Fixed a translated message. 2023-02-07 20:20:18 +08:00
5737d6cef4 Fixed the error message in the Javascript validateAsyncCodeIsDuplicated function in currency-form.js. 2023-02-07 20:20:16 +08:00
1d61fa93d3 Prepend all the HTML ID and class names with "accounting-" to avoid name conflict. 2023-02-07 20:20:01 +08:00
b1c7bc61c4 Renamed the can_view_accounting and can_edit_accounting template functions to accounting_can_view and accounting_can_edit, respectively. 2023-02-07 17:15:05 +08:00
708a434b5d Renamed the append_next, inherit_next, and or_next template filters to accounting_append_next, accounting_inherit_next, and accounting_or_next, to avoid name conflict. 2023-02-07 17:13:06 +08:00
8e524674a3 Added the init_app function to the "accounting.utils.next_url" module to initialize the template filters, and apply it to the init_app function of the accounting application. 2023-02-07 17:10:03 +08:00
699db20308 Revised the init_app function of the "accounting.utils.permission" module to register the "can_view" and "can_edit" functions under the blueprint instead of the whole application. 2023-02-07 17:05:27 +08:00
c3cedf714b Revised the documentation of the init_app function of the "accounting.locale", "accounting.base_account", "accounting.account", and "accounting.currency" modules. 2023-02-07 17:01:08 +08:00
c67ed4471c Fixed the permission so that the template helper also use the wrapper can_view and can_edit, that can_edit always requires the user to log in first. 2023-02-07 16:58:46 +08:00
2d3b9f68b8 Added the missing Material Floating Action Button to add a new currency for the mobile devices to the currency list. 2023-02-07 16:43:42 +08:00
f82278b48a Updated the icon of the currency management in the navigation menu. 2023-02-07 16:36:18 +08:00
85480804e7 Updated the translation. 2023-02-07 16:23:46 +08:00
9e85c14431 Changed the can_edit permission to at least require the user to log in first. 2023-02-07 16:03:13 +08:00
31dc8fab04 Changed the type hint of the "current_user" pseudo property of the AbstractUserUtils class to return None when the user has not logged in. 2023-02-07 16:00:51 +08:00
dc24af1db0 Added the get_current_user function to the "accounting.utils.user" module to retrieve the currently logged-in user and cache it in the current request. 2023-02-07 16:00:47 +08:00
59795635ee Updated the Sphinx documentation. 2023-02-07 11:41:28 +08:00
399afe56c8 Added the initial values for the database instance, the user class and the user primary key column, to allow the Sphinx documentation system to work properly. 2023-02-07 11:40:48 +08:00
16e2a146db Fixed the documentation in the Account data model. 2023-02-07 11:30:25 +08:00
f7ce94902f Revised the AccountTestCase test case, added the test_add, test_basic_update, test_update_not_modified, test_created_updated_by, test_l10n, and test_delete test to replace the simple test_change_base test. 2023-02-07 11:29:09 +08:00
5cf3cb1e11 Added the "is_modified" pseudo property to the Account data model, and applied it to the update_account view, to count the localized titles for modification. 2023-02-07 11:14:15 +08:00
a78057a8c3 Renamed the variable in the test_created_updated_by test of the CurrencyTestCase test case. 2023-02-07 09:47:32 +08:00
0491614ae4 Added the PREFIX constant to simplify the CurrencyTestCase test case. 2023-02-07 09:46:54 +08:00
fb9ff1d7ff Added to validate if the base account is available in the AccountForm form with the BaseAccountAvailable validator. 2023-02-07 09:30:06 +08:00
be10984cbb Fixed the documentation of the BaseAccountExists validator. 2023-02-07 09:28:10 +08:00
7b2089bdfb Revised the currency test cases. 2023-02-07 08:24:24 +08:00
be8dc21c5a Revised the code in the test_l10n test of the CurrencyTestCase test case. 2023-02-07 00:38:41 +08:00
2f8c6f6981 Removed the redundant unique constraint from the AccountL10n and CurrencyL10n data models. 2023-02-07 00:24:36 +08:00
cdd010427b Added documentation to the columns of the AccountL10n data model. 2023-02-07 00:23:45 +08:00
d78b941674 Applied the delete method of the Account data model to the delete_account view, to make things easier. 2023-02-07 00:22:23 +08:00
570c84c196 Added the currency management. 2023-02-07 00:13:33 +08:00
7873e16cc3 Added the editor2 user to the test site. 2023-02-06 23:28:21 +08:00
52351c52bc Revised the imports in test_base_account.py and test_account.py. 2023-02-06 21:45:56 +08:00
591fb4a7ab Replaced the UserClient class and the get_user_client function with the get_client function in the tests, for simplicity. 2023-02-06 21:45:28 +08:00
2a6c5de6d6 Removed the unused clients from the setUp method of the BaseAccountTestCase test case. 2023-02-06 21:37:41 +08:00
6b94cfb908 Removed excess blank lines in test_account.py and test_base_account.py. 2023-02-06 19:57:19 +08:00
eb90e83c98 Removed an unused import from the "accounting" module. 2023-02-06 19:31:06 +08:00
6bf18be455 Revised the coding style in the title setter of the Account data model. 2023-02-06 11:42:22 +08:00
895bca2508 Fixed the documentation of the list_accounts view. 2023-02-06 11:07:18 +08:00
6af29e7df7 Updated the icon to create a new account in the account list. 2023-02-06 10:08:50 +08:00
50f8f06687 Revised the translation. 2023-02-06 09:50:03 +08:00
cd5b1b97fd Added a different the page title of the search result in the base account list and account list, to be clear. 2023-02-06 09:47:19 +08:00
b7dd53d2f9 Added a complex query to the test_malformed test of the QueryKeywordParserTestCase test case. 2023-02-04 14:54:32 +08:00
b07b0e3be4 Added a complex query to the test_default test of the QueryKeywordParserTestCase test case. 2023-02-04 14:53:18 +08:00
e7fb2288ce Revised the parse_query_keywords utility to handle the case with an open double quotation mark without its corresponding close double quotation mark. 2023-02-04 14:51:09 +08:00
17ba7659b6 Removed the CSRF token from the NextUriTestCase test case, for simplicity. 2023-02-04 14:38:25 +08:00
2c8d5e7c8a Revised the translation. 2023-02-04 13:27:04 +08:00
e2f707f696 Replaced gettext with pgettext in the Pagination utility. 2023-02-04 13:26:58 +08:00
b5c0d0b7b3 Added the pgettext function to the "accounting.locale" module. 2023-02-04 13:26:32 +08:00
7fe2bb6135 Removed an excess blank line from the "accounting.utils.pagination" module. 2023-02-04 12:57:38 +08:00
4d870f1dcc Added the page size to the public properties of the Pagination utility. It is used in the pagination template. 2023-02-04 12:55:30 +08:00
16b2eb1c93 Renamed the page_links and page_sizes properties to pages and page_size_options in the Pagination utility. 2023-02-04 12:51:30 +08:00
fd63149066 Revised the pagination utility to handle the empty data. better 2023-02-04 12:19:30 +08:00
a7a432914d Added the empty condition in the __get_page_sizes method of the Pagination utility. 2023-02-04 11:37:00 +08:00
1a44f08b90 Revised the empty condition in the __get_page_links method of the Pagination utility. 2023-02-04 11:36:42 +08:00
3e68cfe690 Removed incorrect documentation in the Pagination utility. 2023-02-04 11:31:09 +08:00
809f2b6df3 Changed the page number and page size properties to private in the Pagination utility. 2023-02-04 11:26:33 +08:00
c286aa8b8b Added the missing parameter in the __uri_set method of the Pagination utility. 2023-02-04 11:24:10 +08:00
1326d9538c Added the missing is_found = True in the __uri_set method of the Pagination utility. 2023-02-04 11:21:22 +08:00
b9cecf343a Added the generic type to the pagination utility in the PaginationTestCase test case. 2023-02-04 11:09:20 +08:00
3d9e6c10da Removed the invalid page number handler in the __set_list method of the Pagination utility. The invalid page numbers are handled and redirected in the __get_page_no method now. 2023-02-04 11:07:04 +08:00
5090e59bb1 Added to redirect when the page size is invalid in the Pagination utility. 2023-02-04 10:55:49 +08:00
62697fb782 Added the exception to the documentation of the constructor of the Pagination utility. 2023-02-04 10:51:07 +08:00
8c462e7b2c Replaced the messy __get_base_uri_params __uri_set_params methods with the unified __uri_set method in the Pagination utility. 2023-02-04 10:49:35 +08:00
90a8229db9 Revised the Pagination so that the page size and page number that are the same as the default values are redirected and removed, too. 2023-02-04 10:37:39 +08:00
8be44ccf5f Renamed the is_needed property to is_paged in the Pagination utility. 2023-02-04 10:26:28 +08:00
511328a0bd Renamed the PageLink class to Link in the "accounting.utils.pagination" module. 2023-02-04 10:18:22 +08:00
0d8cf85ec0 Removed an excess blank line in test_utils.py. 2023-02-04 09:51:19 +08:00
6e212f0e33 Revised the Pagination utility to handle the malformed and illegal page number and page size values. 2023-02-04 09:34:52 +08:00
2fbe137243 Added test_utils.py with the NextUriTestCase, QueryKeywordParserTestCase, and PaginationTestCase test cases for the independent utilities. 2023-02-04 08:12:24 +08:00
f4e2c21ece Added test_temp.py to .gitignore, for temporary tests that should not be committed. 2023-02-04 08:11:52 +08:00
fff07a2552 Removed node_models from .gitignore. 2023-02-03 23:06:28 +08:00
975b00bce9 Advanced to version 0.1.1. 2023-02-03 17:14:47 +08:00
d648538fbb Added onupdate="CASCADE" to the foreign keys. 2023-02-03 17:14:32 +08:00
dde9c38bb8 Fixed the primary key of the Account data model to be not auto-incrementing. 2023-02-03 13:32:19 +08:00
fecf33baa8 Updated the minimal python version to 3.11, as for the use of the typing.Self type hint. 2023-02-03 13:01:03 +08:00
cea2a44226 Added the order and sorting routes to the test_nobody, test_viewer, and test_editor tests of the AccountTestCase test case. 2023-02-03 12:57:53 +08:00
b5d87d2387 Revised to allow the viewers to view the account order page. 2023-02-03 12:57:53 +08:00
784e7bde49 Added the test_reorder test to the AccountTestCase test case. 2023-02-03 12:57:53 +08:00
60280f415d Shortened the variable names in the test_change_base test of the AccountTestCase test case. 2023-02-03 12:57:53 +08:00
f32d268494 Revised the order and sorting routes from "/base/" to "/bases/". 2023-02-03 12:57:53 +08:00
1c1be87f3e Revised the accounting reordering to handle the cases with only one account or no account. 2023-02-03 12:57:53 +08:00
589da0c1c6 Renamed "sorting" to "reorder", and the "sort-form" route to "order". 2023-02-03 12:57:53 +08:00
8363ce6602 Fixed the endpoint name in the account detail template. 2023-02-03 12:57:53 +08:00
6a83f95c9f Added the test_change_base test to the AccountTestCase test case. 2023-02-03 12:57:53 +08:00
7dc754174c Revised the documentation of the views. 2023-02-03 12:57:53 +08:00
5238168b2d Added support to sort the accounts under the same base account. 2023-02-03 12:57:53 +08:00
eeb05b8616 Removed the unique constraint in the Account data model. 2023-02-03 12:57:53 +08:00
9920377266 Added a missing semicolon in account-form.js. 2023-02-03 12:57:53 +08:00
9f9c40c30e Revised the code to find the next number in the populate_obj method of the AccountForm form. 2023-02-03 12:57:53 +08:00
d368c5e062 Renamed the variable in the new_id function from "new" to "obj_id", to be clear. 2023-02-03 12:57:53 +08:00
4aed2f6ba7 Renamed the "testsite" application to "test_site". 2023-02-03 12:57:53 +08:00
6876fdf75e Added the test_editor test to the AccountTestCase test case. 2023-02-03 12:57:53 +08:00
d9624c7be6 Revised the AccountTestCase test case for simplicity. 2023-02-03 12:57:53 +08:00
8364025668 Split the BaseAccountTestCase into BaseAccountCommandTestCase and BaseAccountTestCase, and rewrote the BaseAccountTestCase for simplicity. 2023-02-03 12:57:53 +08:00
dd3690dd6a Added the AccountTestCase test case with the test_nobody and test_viewer tests. 2023-02-03 12:57:53 +08:00
3312c835fd Added the AccountCommandTestCase test case. 2023-02-03 12:57:53 +08:00
fce9d04896 Removed SQLALCHEMY_ECHO from the test site. 2023-02-03 12:57:53 +08:00
c68786f78a Revised the import in the test_init test of the BaseAccountTestCase test case. 2023-02-03 12:57:53 +08:00
581e803707 Moved the user utilities from the "accounting.database" module to the "accounting.utils.users" module, and simplified its use. 2023-02-03 12:57:53 +08:00
15007ada4f Advanced to version 0.1.0. 2023-02-03 12:57:53 +08:00
e50d6267d5 Revised the options.package_data section in setup.cfg to include the static CSS and JavaScript files. 2023-02-03 12:55:59 +08:00
2359842e80 Added to query the "offset needed" in the get_account_query function. 2023-02-03 12:55:58 +08:00
9497fa371e Updated the translation. 2023-02-03 12:55:58 +08:00
b0ef4fb059 Added the "offset needed" option and badge in the account list, account detail and account form. 2023-02-03 12:55:57 +08:00
9f63db174c Merged the search box to the action button group in the base account list and account list. 2023-02-03 12:55:56 +08:00
9b22331a5a Added the base account detail page that shows its descendant accounts. 2023-02-03 12:55:56 +08:00
cb0dea58f1 Fixed the documentation of the AccountConverter converter. 2023-02-03 12:55:55 +08:00
241ad337c8 Added the "accounting.utils.next_uri" utilities to fixed how the next URI works. 2023-02-03 12:55:55 +08:00
2964f206a6 Revised the test site to use the secret key specified in the environment variables when possible, so that the secret key and session keeps in the development environment. 2023-02-03 12:55:55 +08:00
9118b631e4 Updated the translation. 2023-02-03 12:55:55 +08:00
ce6c8508df Revised the titles of the base account list and the account list, to be clear. 2023-02-03 12:55:54 +08:00
e29b99b0a7 Merged the "accounting.base_account.models" and "accounting.account.models" modules into the new "accounting.modules" module, so that the data models can reference one another. 2023-02-03 12:55:54 +08:00
e9f6b769f4 Added the account management. 2023-02-03 12:55:54 +08:00
235 changed files with 36005 additions and 1395 deletions

5
.gitignore vendored
View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/27
# Copyright (c) 2022 imacat.
@ -37,4 +37,5 @@ excludes
*.pot
*.mo
zh_Hans
node_modules
test_temp.py
dummy.js

41
.readthedocs.yaml Normal file
View File

@ -0,0 +1,41 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/5
# 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.
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
# Build documentation in the docs/ directory with Sphinx
# If using Sphinx, optionally build your docs in additional formats such as PDF
formats: all
# Optionally declare the Python requirements required to build your docs
python:
install:
- method: pip
path: .
- requirements: docs/requirements.txt

View File

@ -1,4 +1,4 @@
# The Mia! Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
# Copyright (c) 2022-2023 imacat.
@ -15,14 +15,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
include src/accounting/translations/*
include src/accounting/translations/*/LC_MESSAGES/*
include docs/*
include docs/source/*
include docs/source/_static/*
include docs/source/_templates/*
include tests/*
include tests/testsite/*
include tests/testsite/templates/*
include tests/testsite/translations/*
include tests/testsite/translations/*/LC_MESSAGES/*
recursive-include src/accounting/static *
exclude src/accounting/static/js/dummy.js
recursive-include src/accounting/templates *
recursive-include src/accounting/translations *
recursive-include src/accounting/data *
recursive-include docs *
recursive-exclude docs/build *
recursive-include tests *
exclude tests/test_temp.py
recursive-exclude tests *.pyc
recursive-exclude tests/instance *

View File

@ -1,30 +1,65 @@
=====================
Mia! Accounting Flask
=====================
===============
Mia! Accounting
===============
Description
===========
This is the Mia! Accounting Flask project. It is an accounting
module for the Flask_ applications.
*Mia! Accounting* is an accounting module for Flask_ applications.
It is designed both for mobile and desktop environments. It
implements `double-entry bookkeeping`_. It generates the following
accounting reports:
* Trial balance
* Income statement
* Balance sheet
In addition, *Mia! Accounting* tracks offsets for unpaid payables and
receivables.
Install
=======
Live Demonstration and Test Site
================================
Install the latest source from the
`Mia! Accounting Flask repository`_.
There is a `live demonstration`_ for *Mia! Accounting*. It runs the
same code as the `test site`_ in the `source distribution`_. It is
the simplest website that works with *Mia! Accounting*. It is also
used in the automatic tests.
If you do not have a running Flask application or do not know how to
start one, you may start with the test site.
Installation
============
Install *Mia! Accounting* with ``pip``:
::
pip install git+https://gitea.imacat.idv.tw/imacat/mia-accounting-flask.git
pip install mia-accounting
You may also download from the `PyPI project page`_ or the
`release page`_ on the `Git repository`_.
Documentation
=============
Refer to the `documentation on Read the Docs`_.
Change Log
==========
Refer to the `change log`_.
Copyright
=========
Copyright (c) 2023 imacat.
Copyright (c) 2023-2024 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -46,5 +81,14 @@ Authors
| imacat@mail.imacat.idv.tw
| 2023/1/27
.. _Flask: https://flask.palletsprojects.com
.. _Mia! Accounting Flask repository: https://gitea.imacat.idv.tw/imacat/mia-accounting-flask
.. _double-entry bookkeeping: https://en.wikipedia.org/wiki/Double-entry_bookkeeping
.. _live demonstration: https://accounting.imacat.idv.tw
.. _test site: https://github.com/imacat/mia-accounting/tree/main/tests/test_site
.. _source distribution: https://pypi.org/project/mia-accounting/#files
.. _PyPI project page: https://pypi.org/project/mia-accounting
.. _release page: https://github.com/imacat/mia-accounting/releases
.. _Git repository: https://github.com/imacat/mia-accounting
.. _documentation on Read the Docs: https://mia-accounting.readthedocs.io
.. _change log: https://mia-accounting.readthedocs.io/en/latest/changelog.html

1
docs/requirements.txt Normal file
View File

@ -0,0 +1 @@
sphinx_rtd_theme

View File

@ -0,0 +1,53 @@
accounting.account package
==========================
Submodules
----------
accounting.account.commands module
----------------------------------
.. automodule:: accounting.account.commands
:members:
:undoc-members:
:show-inheritance:
accounting.account.converters module
------------------------------------
.. automodule:: accounting.account.converters
:members:
:undoc-members:
:show-inheritance:
accounting.account.forms module
-------------------------------
.. automodule:: accounting.account.forms
:members:
:undoc-members:
:show-inheritance:
accounting.account.queries module
---------------------------------
.. automodule:: accounting.account.queries
:members:
:undoc-members:
:show-inheritance:
accounting.account.views module
-------------------------------
.. automodule:: accounting.account.views
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.account
:members:
:undoc-members:
:show-inheritance:

View File

@ -12,26 +12,18 @@ accounting.base\_account.commands module
:undoc-members:
:show-inheritance:
accounting.base\_account.database module
----------------------------------------
accounting.base\_account.converters module
------------------------------------------
.. automodule:: accounting.base_account.database
.. automodule:: accounting.base_account.converters
:members:
:undoc-members:
:show-inheritance:
accounting.base\_account.models module
--------------------------------------
accounting.base\_account.queries module
---------------------------------------
.. automodule:: accounting.base_account.models
:members:
:undoc-members:
:show-inheritance:
accounting.base\_account.query module
-------------------------------------
.. automodule:: accounting.base_account.query
.. automodule:: accounting.base_account.queries
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,53 @@
accounting.currency package
===========================
Submodules
----------
accounting.currency.commands module
-----------------------------------
.. automodule:: accounting.currency.commands
:members:
:undoc-members:
:show-inheritance:
accounting.currency.converters module
-------------------------------------
.. automodule:: accounting.currency.converters
:members:
:undoc-members:
:show-inheritance:
accounting.currency.forms module
--------------------------------
.. automodule:: accounting.currency.forms
:members:
:undoc-members:
:show-inheritance:
accounting.currency.queries module
----------------------------------
.. automodule:: accounting.currency.queries
:members:
:undoc-members:
:show-inheritance:
accounting.currency.views module
--------------------------------
.. automodule:: accounting.currency.views
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.currency
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,45 @@
accounting.journal\_entry.forms package
=======================================
Submodules
----------
accounting.journal\_entry.forms.currency module
-----------------------------------------------
.. automodule:: accounting.journal_entry.forms.currency
:members:
:undoc-members:
:show-inheritance:
accounting.journal\_entry.forms.journal\_entry module
-----------------------------------------------------
.. automodule:: accounting.journal_entry.forms.journal_entry
:members:
:undoc-members:
:show-inheritance:
accounting.journal\_entry.forms.line\_item module
-------------------------------------------------
.. automodule:: accounting.journal_entry.forms.line_item
:members:
:undoc-members:
:show-inheritance:
accounting.journal\_entry.forms.reorder module
----------------------------------------------
.. automodule:: accounting.journal_entry.forms.reorder
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.journal_entry.forms
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,46 @@
accounting.journal\_entry package
=================================
Subpackages
-----------
.. toctree::
:maxdepth: 4
accounting.journal_entry.forms
accounting.journal_entry.utils
Submodules
----------
accounting.journal\_entry.converters module
-------------------------------------------
.. automodule:: accounting.journal_entry.converters
:members:
:undoc-members:
:show-inheritance:
accounting.journal\_entry.template\_filters module
--------------------------------------------------
.. automodule:: accounting.journal_entry.template_filters
:members:
:undoc-members:
:show-inheritance:
accounting.journal\_entry.views module
--------------------------------------
.. automodule:: accounting.journal_entry.views
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.journal_entry
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,45 @@
accounting.journal\_entry.utils package
=======================================
Submodules
----------
accounting.journal\_entry.utils.account\_option module
------------------------------------------------------
.. automodule:: accounting.journal_entry.utils.account_option
:members:
:undoc-members:
:show-inheritance:
accounting.journal\_entry.utils.description\_editor module
----------------------------------------------------------
.. automodule:: accounting.journal_entry.utils.description_editor
:members:
:undoc-members:
:show-inheritance:
accounting.journal\_entry.utils.operators module
------------------------------------------------
.. automodule:: accounting.journal_entry.utils.operators
:members:
:undoc-members:
:show-inheritance:
accounting.journal\_entry.utils.original\_line\_items module
------------------------------------------------------------
.. automodule:: accounting.journal_entry.utils.original_line_items
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.journal_entry.utils
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,29 @@
accounting.option package
=========================
Submodules
----------
accounting.option.forms module
------------------------------
.. automodule:: accounting.option.forms
:members:
:undoc-members:
:show-inheritance:
accounting.option.views module
------------------------------
.. automodule:: accounting.option.views
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.option
:members:
:undoc-members:
: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,101 @@
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:
accounting.report.reports.unapplied module
------------------------------------------
.. automodule:: accounting.report.reports.unapplied
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.unapplied\_accounts module
----------------------------------------------------
.. automodule:: accounting.report.reports.unapplied_accounts
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.unmatched module
------------------------------------------
.. automodule:: accounting.report.reports.unmatched
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.unmatched\_accounts module
----------------------------------------------------
.. automodule:: accounting.report.reports.unmatched_accounts
: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,93 @@
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.offset\_matcher module
----------------------------------------------
.. automodule:: accounting.report.utils.offset_matcher
: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.unapplied module
----------------------------------------
.. automodule:: accounting.report.utils.unapplied
:members:
:undoc-members:
:show-inheritance:
accounting.report.utils.unmatched module
----------------------------------------
.. automodule:: accounting.report.utils.unmatched
: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

@ -7,12 +7,33 @@ Subpackages
.. toctree::
:maxdepth: 4
accounting.account
accounting.base_account
accounting.currency
accounting.journal_entry
accounting.option
accounting.report
accounting.utils
Submodules
----------
accounting.commands module
--------------------------
.. automodule:: accounting.commands
:members:
:undoc-members:
:show-inheritance:
accounting.forms module
-----------------------
.. automodule:: accounting.forms
:members:
:undoc-members:
:show-inheritance:
accounting.locale module
------------------------
@ -21,6 +42,30 @@ accounting.locale module
:undoc-members:
:show-inheritance:
accounting.models module
------------------------
.. automodule:: accounting.models
:members:
:undoc-members:
:show-inheritance:
accounting.template\_filters module
-----------------------------------
.. automodule:: accounting.template_filters
:members:
:undoc-members:
:show-inheritance:
accounting.template\_globals module
-----------------------------------
.. automodule:: accounting.template_globals
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -4,6 +4,62 @@ accounting.utils package
Submodules
----------
accounting.utils.cast module
----------------------------
.. automodule:: accounting.utils.cast
:members:
:undoc-members:
:show-inheritance:
accounting.utils.current\_account module
----------------------------------------
.. automodule:: accounting.utils.current_account
:members:
:undoc-members:
:show-inheritance:
accounting.utils.flash\_errors module
-------------------------------------
.. automodule:: accounting.utils.flash_errors
:members:
:undoc-members:
:show-inheritance:
accounting.utils.journal\_entry\_types module
---------------------------------------------
.. automodule:: accounting.utils.journal_entry_types
:members:
:undoc-members:
:show-inheritance:
accounting.utils.next\_uri module
---------------------------------
.. automodule:: accounting.utils.next_uri
:members:
:undoc-members:
:show-inheritance:
accounting.utils.offset\_alias module
-------------------------------------
.. automodule:: accounting.utils.offset_alias
:members:
:undoc-members:
:show-inheritance:
accounting.utils.options module
-------------------------------
.. automodule:: accounting.utils.options
:members:
:undoc-members:
:show-inheritance:
accounting.utils.pagination module
----------------------------------
@ -28,6 +84,38 @@ accounting.utils.query module
:undoc-members:
:show-inheritance:
accounting.utils.random\_id module
----------------------------------
.. automodule:: accounting.utils.random_id
:members:
:undoc-members:
:show-inheritance:
accounting.utils.strip\_text module
-----------------------------------
.. automodule:: accounting.utils.strip_text
:members:
:undoc-members:
:show-inheritance:
accounting.utils.title\_case module
-----------------------------------
.. automodule:: accounting.utils.title_case
:members:
:undoc-members:
:show-inheritance:
accounting.utils.user module
----------------------------
.. automodule:: accounting.utils.user
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------

455
docs/source/changelog.rst Normal file
View File

@ -0,0 +1,455 @@
Change Log
==========
Version 1.6.0
--------------
Released 2024/6/4
* Updated Python version to 3.12.
* Revised the calculation of "today" to use the client's timezone instead of
the server's timezone.
* Updated the Bootstrap, FontAwesome, and Tempus-Dominus versions in the test
site.
Version 1.5.11
--------------
Released 2023/12/26
Bug fix.
* Refined to enable the selection of the 3351-001 Accumulated Profit or Loss
account.
Version 1.5.10
--------------
Released 2023/11/28
Bug fix.
* Fixed the form validator to enable the selection of Accumulated Profit or
Loss accounts other than 3351-001.
Version 1.5.9
-------------
Released 2023/11/28
Bug fix.
* Refined to enable the selection of Accumulated Profit or Loss accounts other
than 3351-001, facilitating the consolidation of existing balances.
Version 1.5.8
-------------
Released 2023/10/24
Bug fix.
* Fixed an icon in the detail of the cash receipt journal entry.
Released at Jaipur, India on vacation.
Version 1.5.7
-------------
Released 2023/7/29
Revised account title capitalization to capitalize account titles
upon initialization of base accounts, rather than when displaying
the accounts. This prevents the system from incorrectly
capitalizing titles of user-added accounts.
For existing installation, run the ``accounting-titleize`` console
command to capitalize the existing account titles that were already
initialized.
Other fixes:
* Added missing documentation to the global variables, class
properties, and object properties.
* Various minor fixes.
Version 1.5.6
-------------
Released 2023/5/23
Bug fixes.
* Fixed the return URI of the creation forms to decode the next URI.
* Fixed the unmatched offset list to use the encoded next URI.
Version 1.5.5
-------------
Released 2023/5/23
Security fixes.
* Revised the next URI utilities to encode and decode the next URI
preventing tampering with the next URI.
* Added the integrity value of the CDN stylesheet links.
* Various fixes.
Version 1.5.4
-------------
Released 2023/5/18
Security fixes.
* Added safeguard to the next URI utilities, to prevent Cross-Site
Scripting (XSS) attacks.
* Applied the safe next URI utilities to the test site.
* Added the ``SameSite`` and ``Secure`` flags to the session cookie
of the test site.
Version 1.5.3
-------------
Released 2023/4/30
* Fixed the error of the net balance in the unmatched offset list.
* Revised the original line item editor not to override the existing
amount when the existing amount is less or equal to the net
balance.
Version 1.5.2
-------------
Released 2023/4/30
* Fixed the error of the net balance in the unmatched offset list.
Version 1.5.1
-------------
Released 2023/4/30
* Fixed the error calling the old ``setEnableDescriptionAccount``
method in the ``saveOriginalLineItem`` method of the JavaScript
``JournalEntryLineItemEditor`` class.
Version 1.5.0
-------------
Released 2023/4/23
* Updated to require ``SQLAlchemy >= 2``.
* Added the change log.
* Added the ``VERSION`` constant to the ``accounting`` module for
the package version, and revised ``pyproject.toml`` and ``conf.py``
to read the version from it.
Version 1.4.1
-------------
Released 2023/4/22
* Updated to allow editing the description of the journal entry line
item with offsets or are offsetting to original line items.
* Updated not to override the existing description of a journal entry
line item after choosing the original line item to offset to.
Version 1.4.0
-------------
Released 2023/4/18
* Rewrote the unapplied original line items and unmatched offsets.
* The unapplied original line items and unmatched offsets are both
in the report submodule. They can be filtered with currency and
period now.
* Show the unapplied original line items and unmatched offsets
together, and added the accumulated balance in the unmatched
offset list, for ease of reference.
* Removed the account code from the journal entry detail and journal
entry form for mobile devices.
* Made the account options in the reports to be scrollable.
Version 1.3.3
-------------
Released 2023/4/13
Changed the sample data generation in the test site live demonstration
from pre-recorded data to real-time generation, to avoid the problem
with the start of months and weeks changed with the date of the
import.
Version 1.3.2
-------------
Released 2023/4/12
Added the sample data generation and database reset on the test site
for live demonstration.
Version 1.3.1
-------------
Released 2023/4/11
* Fixed the permission of the navigation menu of the unmatched offsets.
* Revised the test site to be more accessible as the live demonstration.
Version 1.3.0
-------------
Released 2023/4/11
Added the ``accounting-init-db`` console command to replace all the
other console commands to initialize the accounting database. The
test site does not work with previous versions (<1.3.0).
Version 1.2.1
-------------
Released 2023/4/9
Fixed the search result to allow full ``year/month/day``
specification.
Version 1.2.0
-------------
Released 2023/4/9
* Simplified the URL of the default reports.
* Fixed the crash with malformed Chinese translation.
* Fixed the crash when downloading CSV data with non-US-ASCII
filenames.
Version 1.1.0
-------------
Released 2023/4/9
* Added the unapplied original line item list, to track unpaid
payables, unreceived receivables, assets, prepaids, refundable
deposits, etc.
* Added the offset matcher to match unapplied original line items
with unmatched offsets.
Version 1.0.1
-------------
Released 2023/4/6
Documentation fixes.
Version 1.0.0
-------------
Released 2023/4/6
The first formal release in Flask.
Added the documentation.
Version 0.11.1 (Pre-release)
----------------------------
Released 2023/4/5
Removed the zero balances from the trial balance, the income
statement, and the balance sheet.
Version 0.11.0 (Pre-release)
----------------------------
Released 2023/4/5
* Renamed the project from ``mia-accounting-flask`` to
``mia-accounting``.
* Updated the URL of the reports, as the default views of the
accounting application.
* Updated ``README``.
* Various fixes.
Version 0.10.0 (Pre-release)
----------------------------
Released 2023/4/3
* Added the unauthorized method to the ``UserUtilityInterface``
interface to allow fine control to how to handle the case when the
user has not logged in.
* Revised the JavaScript description editor to respect the account
that the user has confirmed or specifically selected.
* Various fixes.
Version 0.9.1 (Pre-release)
---------------------------
Released 2023/3/24
* A distinguishable look in the option detail than the option form.
* A better look in the new journal entry forms when there is no line
item yet.
* Fixed the search in the original entry selector in the journal
entry form to always do a partial match, to fix the problem that
there is no match when typing is not finished yet.
* Fixed the search in the original entry selector to search the net
balance correctly.
* Replaced the ``editor`` and ``editor2`` accounts with the ``admin``
and ``editor`` accounts.
* Various fixes.
Version 0.9.0 (Pre-release)
---------------------------
Released 2023/3/23
Moved the settings from the ``.env`` file to the option table in the
database that can be set and updated on the web interface. Added the
settings page to show and update the settings.
Version 0.8.0 (Pre-release)
---------------------------
Released 2023/3/22
* Added the recurring transactions to the description editor.
* Added prevention to delete database objects that are essential or
referenced by others with foreign keys.
* Various fixes on the visual layout.
Version 0.7.0 (Pre-release)
---------------------------
Released 2023/3/21
* Renamed "transaction" to "journal entry", and "journal entry" to
"journal entry line item".
* Renamed ``summary`` to ``description``.
* Updated tempus-dominus from version 6.2.10 to 6.4.3.
* Fixed titles and capitalization.
* Fixed to search case-insensitively.
* Added favicon to the test site.
* Fixed the navigation menu when there is no matching endpoint.
* Various fixes.
Version 0.6.0 (Pre-release)
---------------------------
Released 2023/3/18
* Added offset tracking to the journal entries in the payable and
receivable accounts.
* Renamed the ``is_offset_needed`` column to ``is_need_offset`` in
the ``Account`` data model.
Version 0.5.0 (Pre-release)
---------------------------
Released 2023/3/10
Added the accounting reports.
Version 0.4.0 (Pre-release)
---------------------------
Released 2023/3/1
Added the transaction summary helper.
Version 0.3.1 (Pre-release)
---------------------------
Released 2023/2/28
* Fixed the error that cannot select any account when adding new
transactions.
* Fixed the database error when adding new transactions.
* Added the button to convert a cash income or cash expense
transaction to a transfer transaction.
Version 0.3.0 (Pre-release)
---------------------------
Released 2023/2/27
Added the transaction management.
Version 0.2.0 (Pre-release)
---------------------------
Released 2023/2/7
* Added the currency management.
* Changed the ``can_edit`` permission to at least require the user to
log in first.
* Changed the type hint of the ``current_user`` pseudo property of
the ``AbstractUserUtils`` class to return ``None`` when the user
has not logged in.
Version 0.1.1 (Pre-release)
---------------------------
Released 2023/2/3
Finalized the account management, with tests and reordering.
Version 0.1.0 (Pre-release)
---------------------------
Released 2023/2/3
Added the account management, and updated the API to initialize the
accounting application.
Version 0.0.0 (Pre-release)
---------------------------
Released 2023/2/3
Initial release with main account list, localization, pagination,
query, permission, Sphinx documentation, and a test case based on a
test demonstration site.

View File

@ -6,14 +6,15 @@ import os
import sys
sys.path.insert(0, os.path.abspath('../../src/'))
import accounting
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'Mia! Accounting Flask'
project = 'Mia! Accounting'
copyright = '2023, imacat'
author = 'imacat'
release = '0.0.0'
release = accounting.VERSION
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
@ -28,5 +29,5 @@ exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'nature'
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']

61
docs/source/examples.rst Normal file
View File

@ -0,0 +1,61 @@
Examples
========
.. _example-userutils:
An Example Configuration
------------------------
The following is an example configuration for *Mia! Accounting*.
::
from flask import Response, redirect
from .auth import current_user()
from .modules import User
def create_app(test_config=None) -> Flask:
app: Flask = Flask(__name__)
... (Configuration of SQLAlchemy, CSRF, Babel_JS, ... etc) ...
import accounting
class UserUtils(accounting.UserUtilityInterface[User]):
def can_view(self) -> bool:
return True
def can_edit(self) -> bool:
return "editor" in current_user().roles
def can_admin(self) -> bool:
return current_user().is_admin
def unauthorized(self) -> Response:
return redirect("/login")
@property
def cls(self) -> t.Type[User]:
return User
@property
def pk_column(self) -> Column:
return User.id
@property
def current_user(self) -> User | None:
return current_user()
def get_by_username(self, username: str) -> User | None:
return User.query.filter(User.username == username).first()
def get_pk(self, user: User) -> int:
return user.id
accounting.init_app(app, UserUtils())
... (Any other configuration) ...
return app

57
docs/source/history.rst Normal file
View File

@ -0,0 +1,57 @@
History
=======
I created my own private accounting application in Perl_/mod_perl_ in
2007, as part of my personal website. The first revision was made
using Perl/Mojolicious_ in 2019, with the aim of making it
mobile-friendly using Bootstrap_, and with modern back-end and
front-end technologies such as jQuery.
The second revision was done in Python_/Django_ in 2020, as I was
looking to change my career from PHP_/Laravel_ to Python, but lacked
experience with large Python projects. I needed something in my
portfolio and decided to work on the somewhat outdated Mojolicious
project.
Despite having no prior experience with Django, I spent two months
working late nights to create the `Mia! Accounting Django`_
application. It took me another 1.5 months to make it an independent
module, which I later released as an open source project on PyPI.
The application worked nicely for my household bookkeeping for two
years. However, new demands arose over time, especially with tracking
payables and receivables. This was critical `during the pandemic`_ as
more payments were made online with credit cards.
The biggest issue I encountered was with
`Django's MTV architectural pattern`_. Django takes over the control
flow. I had to override several parts of the `class-based views`_ for
different but yet simple control flow logic. In the end, it became
very difficult to track whether things went wrong because I overrode
something or because it just wouldn't work with the basic assumption
of the class-based views. By the time I realized it, it was too late
for me to drop Django's MTV and rewrite everything from class-based
views to function-based views.
Therefore, I decided to turn to microframeworks_ like Flask_. After
working with modularized Flask and FastAPI_ applications for two
years, I returned to the project and wrote its third revision using
Flask in 2023.
.. _Perl: https://www.perl.org
.. _mod_perl: https://perl.apache.org
.. _Mojolicious: https://mojolicious.org
.. _Bootstrap: https://getbootstrap.com
.. _jQuery: https://jquery.com
.. _Python: https://www.python.org
.. _Django: https://www.djangoproject.com
.. _PHP: https://www.php.net
.. _Laravel: https://laravel.com
.. _Mia! Accounting Django: https://github.com/imacat/mia-accounting-django
.. _during the pandemic: https://en.wikipedia.org/wiki/COVID-19_pandemic
.. _FastAPI: https://fastapi.tiangolo.com
.. _Django's MTV architectural pattern: https://docs.djangoproject.com/en/dev/faq/general/#django-appears-to-be-a-mvc-framework-but-you-call-the-controller-the-view-and-the-view-the-template-how-come-you-don-t-use-the-standard-names
.. _class-based views: https://docs.djangoproject.com/en/4.2/topics/class-based-views/
.. _microframeworks: https://en.wikipedia.org/wiki/Microframework
.. _Flask: https://flask.palletsprojects.com

View File

@ -1,15 +1,21 @@
.. Mia! Accounting Flask documentation master file, created by
.. Mia! Accounting documentation master file, created by
sphinx-quickstart on Fri Jan 27 12:20:04 2023.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Mia! Accounting Flask's documentation!
=================================================
Welcome to Mia! Accounting's documentation!
===========================================
.. toctree::
:maxdepth: 2
:caption: Contents:
intro
accounting
examples
history
changelog
Indices and tables

120
docs/source/intro.rst Normal file
View File

@ -0,0 +1,120 @@
Introduction
============
*Mia! Accounting* is an accounting module for Flask_ applications.
It is designed both for mobile and desktop environments. It
implements `double-entry bookkeeping`_. It generates the following
accounting reports:
* Trial balance
* Income statement
* Balance sheet
In addition, *Mia! Accounting* tracks offsets for unpaid payables and
receivables.
Live Demonstration and Test Site
--------------------------------
There is a `live demonstration`_ for *Mia! Accounting*. It runs the
same code as the `test site`_ in the `source distribution`_. It is
the simplest website that works with *Mia! Accounting*. It is also
used in the automatic tests.
If you do not have a running Flask application or do not know how to
start one, you may start with the test site.
Installation
------------
Install *Mia! Accounting* with ``pip``:
::
pip install mia-accounting
You may also download from the `PyPI project page`_ or the
`release page`_ on the `Git repository`_.
Prerequisites
-------------
You need a running Flask application with database user login.
The primary key of the user data model must be integer. You also
need at least one user.
The following front-end JavaScript libraries must be loaded. You may
download it locally or use CDN_.
* Bootstrap_ 5.2.3 or above
* FontAwesome_ 6.4.0 or above
* `decimal.js`_ 10.4.3 or above, or `decimal.js-light`_ 2.5.1 or above.
* `Tempus-Dominus`_ 6.7.7 or above
Configuration
-------------
You need to pass the Flask *app* and an implementation of
:py:class:`accounting.utils.user.UserUtilityInterface` to the
:py:func:`accounting.init_app` function. ``UserUtilityInterface``
contains everything *Mia! Accounting* needs.
See an example in :ref:`example-userutils`.
Database Initialization
-----------------------
After the configuration, run the ``accounting-init-db`` console
command to initialize the accounting database. You need to specify
the username of a user as the data creator.
::
% flask --app myapp accounting-init-db -u username
Navigation Menu
---------------
Include the navigation menu in the `Bootstrap navigation bar`_ in your
base template:
::
<nav class="navbar navbar-expand-lg bg-body-tertiary bg-dark navbar-dark">
<div class="container-fluid">
...
<div id="collapsible-navbar" class="collapse navbar-collapse">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
...
{% include "accounting/include/nav.html" %}
...
</ul>
...
</div>
</div>
</nav>
Check your Flask application and see how it works.
.. _Flask: https://flask.palletsprojects.com
.. _double-entry bookkeeping: https://en.wikipedia.org/wiki/Double-entry_bookkeeping
.. _live demonstration: https://accounting.imacat.idv.tw
.. _test site: https://github.com/imacat/mia-accounting/tree/main/tests/test_site
.. _source distribution: https://pypi.org/project/mia-accounting/#files
.. _PyPI project page: https://pypi.org/project/mia-accounting
.. _release page: https://github.com/imacat/mia-accounting/releases
.. _Git repository: https://github.com/imacat/mia-accounting
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
.. _Bootstrap: https://getbootstrap.com
.. _FontAwesome: https://fontawesome.com
.. _decimal.js: https://mikemcl.github.io/decimal.js
.. _decimal.js-light: https://mikemcl.github.io/decimal.js-light
.. _Tempus-Dominus: https://getdatepicker.com
.. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/

View File

@ -1,7 +1,7 @@
# The Mia! Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
# Copyright (c) 2022 imacat.
# Copyright (c) 2022-2024 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -15,6 +15,55 @@
# See the License for the specific language governing permissions and
# limitations under the License.
[project]
name = "mia-accounting"
dynamic = ["version"]
description = "A Flask accounting module."
readme = "README.rst"
requires-python = ">=3.12"
authors = [
{name = "imacat", email = "imacat@mail.imacat.idv.tw"},
]
keywords = ["mia", "accounting", "flask"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Framework :: Flask",
"Topic :: Office/Business :: Financial :: Accounting",
]
dependencies = [
"Flask",
"SQLAlchemy >= 2",
"Flask-SQLAlchemy",
"Flask-WTF",
"Flask-Babel >= 3",
"Flask-Babel-JS",
]
[project.optional-dependencies]
devel = [
"httpx",
"OpenCC",
]
[project.urls]
"Documentation" = "https://mia-accounting.readthedocs.io"
"Change Log" = "https://mia-accounting.readthedocs.io/en/latest/changelog.html"
"Repository" = "https://github.com/imacat/mia-accounting"
"Bug Tracker" = "https://github.com/imacat/mia-accounting/issues"
"Demonstration" = "https://accounting.imacat.idv.tw"
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[tool.setuptools.dynamic]
version = {attr = "accounting.VERSION"}
[tool.setuptools.exclude-package-data]
"*" = [
"babel.cfg",
"*.pot",
"*.po",
]

View File

@ -1,56 +0,0 @@
# The Mia! Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
# Copyright (c) 2022-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.
[metadata]
name = mia-accounting-flask
version = 0.0.0
author = imacat
author_email = imacat@mail.imacat.idv.tw
description = The Mia! Accounting Flask project.
long_description = file: README.rst
long_description_content_type = text/x-rst
url = https://github.com/imacat/mia-accounting-flask
project_urls =
Bug Tracker = https://github.com/imacat/mia-accounting-flask/issues
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: Apache Software License
Operating System :: OS Independent
Framework :: Flask
Topic :: Office/Business :: Financial :: Accounting
[options]
package_dir =
= src
python_requires = >=3.10
install_requires =
flask
Flask-SQLAlchemy
Flask-WTF
Flask-Babel >= 3
Flask-Babel-JS
tests_require =
unittest
httpx
OpenCC
[options.package_data]
accounting =
templates/**
translations/*/LC_MESSAGES/*.mo
accounting.base_account =
templates/**

View File

@ -1,7 +1,7 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
# Copyright (c) 2023 imacat.
# Copyright (c) 2023-2024 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -17,41 +17,81 @@
"""The accounting application.
"""
import typing as t
from pathlib import Path
from flask import Flask, Blueprint
from flask_sqlalchemy import SQLAlchemy
from accounting.utils.user import UserUtilityInterface
VERSION: str = "1.6.0"
"""The package version."""
db: SQLAlchemy = SQLAlchemy()
"""The database instance."""
data_dir: Path = Path(__file__).parent / "data"
"""The data directory."""
def init_app(app: Flask, url_prefix: str = "/accounting",
can_view_func: t.Callable[[], bool] | None = None,
can_edit_func: t.Callable[[], bool] | None = None) -> None:
def init_app(app: Flask, user_utils: UserUtilityInterface,
url_prefix: str = "/accounting") -> None:
"""Initialize the application.
:param app: The Flask application.
:param user_utils: The user utilities.
:param url_prefix: The URL prefix of the accounting application.
:param can_view_func: A callback that returns whether the current user can
view the accounting data.
:param can_edit_func: A callback that returns whether the current user can
edit the accounting data.
:return: None.
"""
# The database instance must be set before loading everything
# in the application.
from .database import set_db
set_db(app.extensions["sqlalchemy"])
global db
db = app.extensions["sqlalchemy"]
from .utils.user import init_user_utils
init_user_utils(user_utils)
bp: Blueprint = Blueprint("accounting", __name__,
url_prefix=url_prefix,
template_folder="templates",
static_folder="static")
from .template_filters import format_amount, format_date, default
bp.add_app_template_filter(format_amount, "accounting_format_amount")
bp.add_app_template_filter(format_date, "accounting_format_date")
bp.add_app_template_filter(default, "accounting_default")
from .template_globals import currency_options, default_currency_code
bp.add_app_template_global(currency_options,
"accounting_currency_options")
bp.add_app_template_global(default_currency_code,
"accounting_default_currency_code")
from .commands import init_db_command, titleize_command
app.cli.add_command(init_db_command)
app.cli.add_command(titleize_command)
from . import locale
locale.init_app(app, bp)
from .utils import permission
permission.init_app(app, can_view_func, can_edit_func)
permission.init_app(bp, user_utils)
from .utils import next_uri
next_uri.init_app(bp)
from . import base_account
base_account.init_app(app, bp)
app.register_blueprint(bp)
from . import account
account.init_app(app, bp)
from . import currency
currency.init_app(app, bp)
from . import journal_entry
journal_entry.init_app(app, bp)
from . import report
report.init_app(app, url_prefix)
from . import option
option.init_app(bp)
app.register_blueprint(bp, url_prefix=url_prefix)

View File

@ -0,0 +1,36 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
# 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 account management.
"""
from flask import Flask, Blueprint
from .commands import init_accounts_command
def init_app(app: Flask, bp: Blueprint) -> None:
"""Initialize the application.
:param app: The Flask application.
:param bp: The blueprint of the accounting application.
:return: None.
"""
from .converters import AccountConverter
app.url_map.converters["account"] = AccountConverter
from .views import bp as account_bp
bp.register_blueprint(account_bp, url_prefix="/accounts")

View File

@ -0,0 +1,107 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
# Copyright (c) 2023-2024 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 console commands for the account management.
"""
from secrets import randbelow
from typing import Any
import click
import sqlalchemy as sa
from accounting import db
from accounting.models import BaseAccount, Account, AccountL10n
from accounting.utils.user import get_user_pk
type AccountData = tuple[int, str, int, str, str, str, bool]
"""The format of the account data, as a list of (ID, base account code, number,
English, Traditional Chinese, Simplified Chinese, is-need-offset) tuples."""
def init_accounts_command(username: str) -> None:
"""Initializes the accounts."""
creator_pk: int = get_user_pk(username)
bases: list[BaseAccount] = BaseAccount.query\
.filter(db.func.length(BaseAccount.code) == 4)\
.order_by(BaseAccount.code).all()
if len(bases) == 0:
raise click.Abort
existing: list[Account] = Account.query.all()
existing_base_code: set[str] = {x.base_code for x in existing}
bases_to_add: list[BaseAccount] = [x for x in bases
if x.code not in existing_base_code]
if len(bases_to_add) == 0:
return
existing_id: set[int] = {x.id for x in existing}
def get_new_id() -> int:
"""Returns a new random account ID.
:return: The newly-generated random account ID.
"""
while True:
new_id: int = 100000000 + randbelow(900000000)
if new_id not in existing_id:
existing_id.add(new_id)
return new_id
data: list[dict[str, Any]] = []
l10n_data: list[dict[str, Any]] = []
for base in bases_to_add:
l10n: dict[str, str] = {x.locale: x.title for x in base.l10n}
account_id: int = get_new_id()
data.append({"id": account_id,
"base_code": base.code,
"no": 1,
"title_l10n": base.title_l10n,
"is_need_offset": __is_need_offset(base.code),
"created_by_id": creator_pk,
"updated_by_id": creator_pk})
for locale in {"zh_Hant", "zh_Hans"}:
l10n_data.append({"account_id": account_id,
"locale": locale,
"title": l10n[locale]})
db.session.execute(sa.insert(Account), data)
db.session.execute(sa.insert(AccountL10n), l10n_data)
def __is_need_offset(base_code: str) -> bool:
"""Checks that whether journal entry line items in the account need offset.
:param base_code: The code of the base account.
:return: True if journal entry line items in the account need offset, or
False otherwise.
"""
# Assets
if base_code[0] == "1":
if base_code[:3] in {"113", "114", "118", "184", "186"}:
return True
if base_code in {"1286", "1411", "1421", "1431", "1441", "1511",
"1521", "1581", "1611", "1851"}:
return True
return False
# Liabilities
if base_code[0] == "2":
if base_code in {"2111", "2114", "2284", "2293", "2861"}:
return False
return True
# Only assets and liabilities need offset
return False

View File

@ -0,0 +1,47 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/31
# 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 path converters for the account management.
"""
from flask import abort
from werkzeug.routing import BaseConverter
from accounting.models import Account
class AccountConverter(BaseConverter):
"""The account converter to convert the account code and to the
corresponding account in the routes."""
def to_python(self, value: str) -> Account:
"""Converts an account code to an account.
:param value: The account code.
:return: The corresponding account.
"""
account: Account | None = Account.find_by_code(value)
if account is None:
abort(404)
return account
def to_url(self, value: Account) -> str:
"""Converts an account to its code.
:param value: The account.
:return: The code.
"""
return value.code

View File

@ -0,0 +1,206 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
# 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 forms for the account management.
"""
import sqlalchemy as sa
from flask import request
from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField
from wtforms.validators import DataRequired, ValidationError
from accounting import db
from accounting.locale import lazy_gettext
from accounting.models import BaseAccount, Account
from accounting.utils.random_id import new_id
from accounting.utils.strip_text import strip_text
from accounting.utils.user import get_current_user_pk
class BaseAccountExists:
"""The validator to check if the base account exists."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
if field.data == "":
return
if db.session.get(BaseAccount, field.data) is None:
raise ValidationError(lazy_gettext(
"The base account does not exist."))
class BaseAccountAvailable:
"""The validator to check if the base account is available."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
if field.data == "":
return
if len(field.data) != 4:
raise ValidationError(lazy_gettext(
"The base account is not available."))
class NoOffsetNominalAccount:
"""The validator to check nominal account is not to be offset."""
def __call__(self, form: FlaskForm, field: BooleanField) -> None:
assert isinstance(form, AccountForm)
if not field.data:
return
if form.base_code.data is None:
return
if form.base_code.data[0] not in {"1", "2", "3"}:
raise ValidationError(lazy_gettext(
"A nominal account does not need offset."))
class AccountForm(FlaskForm):
"""The form to create or edit an account."""
base_code = StringField(
filters=[strip_text],
validators=[
DataRequired(lazy_gettext("Please select the base account.")),
BaseAccountExists(),
BaseAccountAvailable()])
"""The code of the base account."""
title = StringField(
filters=[strip_text],
validators=[DataRequired(lazy_gettext("Please fill in the title"))])
"""The title."""
is_need_offset = BooleanField(
validators=[NoOffsetNominalAccount()])
"""Whether the the journal entry line items of this account need offset."""
def populate_obj(self, obj: Account) -> None:
"""Populates the form data into an account object.
:param obj: The account object.
:return: None.
"""
is_new: bool = obj.id is None
if is_new:
obj.id = new_id(Account)
if obj.base_code != self.base_code.data:
if obj.base_code is not None:
sort_accounts_in(obj.base_code, obj.id)
sort_accounts_in(self.base_code.data, obj.id)
count: int = Account.query\
.filter(Account.base_code == self.base_code.data).count()
obj.base_code = self.base_code.data
obj.no = count + 1
obj.title = self.title.data
if self.base_code.data[0] in {"1", "2", "3"}:
obj.is_need_offset = self.is_need_offset.data
else:
obj.is_need_offset = False
if is_new:
current_user_pk: int = get_current_user_pk()
obj.created_by_id = current_user_pk
obj.updated_by_id = current_user_pk
def post_update(self, obj: Account) -> None:
"""The post-processing after the update.
:param obj: The account object.
:return: None
"""
current_user_pk: int = get_current_user_pk()
obj.updated_by_id = current_user_pk
obj.updated_at = sa.func.now()
if hasattr(self, "__post_update"):
getattr(self, "__post_update")()
@property
def selected_base(self) -> BaseAccount | None:
"""The selected base account in the form.
:return: The selected base account in the form.
"""
return db.session.get(BaseAccount, self.base_code.data)
@property
def base_options(self) -> list[BaseAccount]:
"""The selectable base accounts.
:return: The selectable base accounts.
"""
return BaseAccount.query\
.filter(sa.func.char_length(BaseAccount.code) == 4)\
.order_by(BaseAccount.code).all()
def sort_accounts_in(base_code: str, exclude: int) -> None:
"""Sorts the accounts under a base account after changing the base
account or deleting an account.
:param base_code: The code of the base account.
:param exclude: The account ID to exclude.
:return: None.
"""
accounts: list[Account] = Account.query\
.filter(Account.base_code == base_code,
Account.id != exclude)\
.order_by(Account.no).all()
for i in range(len(accounts)):
if accounts[i].no != i + 1:
accounts[i].no = i + 1
class AccountReorderForm:
"""The form to reorder the accounts."""
def __init__(self, base: BaseAccount):
"""Constructs the form to reorder the accounts under a base account.
:param base: The base account.
"""
self.base: BaseAccount = base
"""The base account."""
self.is_modified: bool = False
"""Whether the order is modified."""
def save_order(self) -> None:
"""Saves the order of the account.
:return:
"""
accounts: list[Account] = self.base.accounts
# Collects the specified order.
orders: dict[Account, int] = {}
for account in accounts:
if f"{account.id}-no" in request.form:
try:
orders[account] = int(request.form[f"{account.id}-no"])
except ValueError:
pass
# Missing and invalid orders are appended to the end.
missing: list[Account] = [x for x in accounts if x not in orders]
if len(missing) > 0:
next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
for account in missing:
orders[account] = next_no
# Sort by the specified order first, and their original order.
accounts = sorted(accounts, key=lambda x: (orders[x], x.no, x.code))
# Update the orders.
with db.session.no_autoflush:
for i in range(len(accounts)):
if accounts[i].no != i + 1:
accounts[i].no = i + 1
self.is_modified = True

View File

@ -0,0 +1,55 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
# 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 account management.
"""
import sqlalchemy as sa
from flask import request
from accounting.locale import gettext
from accounting.models import Account, AccountL10n
from accounting.utils.query import parse_query_keywords
def get_account_query() -> list[Account]:
"""Returns the accounts, optionally filtered by the query.
:return: The accounts.
"""
keywords: list[str] = parse_query_keywords(request.args.get("q"))
if len(keywords) == 0:
return Account.query.order_by(Account.base_code, Account.no).all()
code: sa.BinaryExpression = Account.base_code + "-" \
+ sa.func.substr("000" + sa.cast(Account.no, sa.String),
sa.func.char_length(sa.cast(Account.no,
sa.String)) + 1)
conditions: list[sa.BinaryExpression] = []
for k in keywords:
l10n: list[AccountL10n] = AccountL10n.query\
.filter(AccountL10n.title.icontains(k)).all()
l10n_matches: set[str] = {x.account_id for x in l10n}
sub_conditions: list[sa.BinaryExpression] \
= [Account.base_code.contains(k),
Account.title_l10n.icontains(k),
code.contains(k),
Account.id.in_(l10n_matches)]
if k in gettext("Needs Offset"):
sub_conditions.append(Account.is_need_offset)
conditions.append(sa.or_(*sub_conditions))
return Account.query.filter(*conditions)\
.order_by(Account.base_code, Account.no).all()

View File

@ -0,0 +1,214 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
# 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 views for the account management.
"""
from urllib.parse import parse_qsl, urlencode
import sqlalchemy as sa
from flask import Blueprint, render_template, session, redirect, flash, \
url_for, request
from werkzeug.datastructures import ImmutableMultiDict
from accounting import db
from accounting.locale import lazy_gettext
from accounting.models import Account, BaseAccount
from accounting.utils.cast import s
from accounting.utils.flash_errors import flash_form_errors
from accounting.utils.next_uri import inherit_next, or_next
from accounting.utils.pagination import Pagination
from accounting.utils.permission import can_view, has_permission, can_edit
from accounting.utils.user import get_current_user_pk
from .forms import AccountForm, sort_accounts_in, AccountReorderForm
from .queries import get_account_query
bp: Blueprint = Blueprint("account", __name__)
"""The view blueprint for the account management."""
@bp.get("", endpoint="list")
@has_permission(can_view)
def list_accounts() -> str:
"""Lists the accounts.
:return: The account list.
"""
accounts: list[BaseAccount] = get_account_query()
pagination: Pagination = Pagination[BaseAccount](accounts)
return render_template("accounting/account/list.html",
list=pagination.list, pagination=pagination)
@bp.get("create", endpoint="create")
@has_permission(can_edit)
def show_add_account_form() -> str:
"""Shows the form to add an account.
:return: The form to add an account.
"""
if "form" in session:
form = AccountForm(ImmutableMultiDict(parse_qsl(session["form"])))
del session["form"]
form.validate()
else:
form = AccountForm()
return render_template("accounting/account/create.html",
form=form)
@bp.post("store", endpoint="store")
@has_permission(can_edit)
def add_account() -> redirect:
"""Adds an account.
:return: The redirection to the account detail on success, or the account
creation form on error.
"""
form = AccountForm(request.form)
if not form.validate():
flash_form_errors(form)
session["form"] = urlencode(list(request.form.items()))
return redirect(inherit_next(url_for("accounting.account.create")))
account: Account = Account()
form.populate_obj(account)
db.session.add(account)
db.session.commit()
flash(s(lazy_gettext("The account is added successfully")), "success")
return redirect(inherit_next(__get_detail_uri(account)))
@bp.get("<account:account>", endpoint="detail")
@has_permission(can_view)
def show_account_detail(account: Account) -> str:
"""Shows the account detail.
:param account: The account.
:return: The detail.
"""
return render_template("accounting/account/detail.html", obj=account)
@bp.get("<account:account>/edit", endpoint="edit")
@has_permission(can_edit)
def show_account_edit_form(account: Account) -> str:
"""Shows the form to edit an account.
:param account: The account.
:return: The form to edit the account.
"""
form: AccountForm
if "form" in session:
form = AccountForm(ImmutableMultiDict(parse_qsl(session["form"])))
del session["form"]
form.validate()
else:
form = AccountForm(obj=account)
return render_template("accounting/account/edit.html",
account=account, form=form)
@bp.post("<account:account>/update", endpoint="update")
@has_permission(can_edit)
def update_account(account: Account) -> redirect:
"""Updates an account.
:param account: The account.
:return: The redirection to the account detail on success, or the account
edit form on error.
"""
form = AccountForm(request.form)
if not form.validate():
flash_form_errors(form)
session["form"] = urlencode(list(request.form.items()))
return redirect(inherit_next(url_for("accounting.account.edit",
account=account)))
with db.session.no_autoflush:
form.populate_obj(account)
if not account.is_modified:
flash(s(lazy_gettext("The account was not modified.")), "success")
return redirect(inherit_next(__get_detail_uri(account)))
account.updated_by_id = get_current_user_pk()
account.updated_at = sa.func.now()
db.session.commit()
flash(s(lazy_gettext("The account is updated successfully.")), "success")
return redirect(inherit_next(__get_detail_uri(account)))
@bp.post("<account:account>/delete", endpoint="delete")
@has_permission(can_edit)
def delete_account(account: Account) -> redirect:
"""Deletes an account.
:param account: The account.
:return: The redirection to the account list on success, or the account
detail on error.
"""
if not account.can_delete:
flash(s(lazy_gettext("The account cannot be deleted.")), "error")
return redirect(inherit_next(__get_detail_uri(account)))
account.delete()
sort_accounts_in(account.base_code, account.id)
db.session.commit()
flash(s(lazy_gettext("The account is deleted successfully.")), "success")
return redirect(or_next(__get_list_uri()))
@bp.get("bases/<baseAccount:base>", endpoint="order")
@has_permission(can_view)
def show_account_order(base: BaseAccount) -> str:
"""Shows the order of the accounts under a same base account.
:param base: The base account.
:return: The order of the accounts under the base account.
"""
return render_template("accounting/account/order.html", base=base)
@bp.post("bases/<baseAccount:base>", endpoint="sort")
@has_permission(can_edit)
def sort_accounts(base: BaseAccount) -> redirect:
"""Reorders the accounts under a base account.
:param base: The base account.
:return: The redirection to the incoming account or the account list. The
reordering operation does not fail.
"""
form: AccountReorderForm = AccountReorderForm(base)
form.save_order()
if not form.is_modified:
flash(s(lazy_gettext("The order was not modified.")), "success")
return redirect(or_next(__get_list_uri()))
db.session.commit()
flash(s(lazy_gettext("The order is updated successfully.")), "success")
return redirect(or_next(__get_list_uri()))
def __get_detail_uri(account: Account) -> str:
"""Returns the detail URI of an account.
:param account: The account.
:return: The detail URI of the account.
"""
return url_for("accounting.account.detail", account=account)
def __get_list_uri() -> str:
"""Returns the account list URI.
:return: The account list URI.
"""
return url_for("accounting.account.list")

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
# Copyright (c) 2023 imacat.
@ -19,16 +19,18 @@
"""
from flask import Flask, Blueprint
from .commands import init_base_accounts_command
def init_app(app: Flask, bp: Blueprint) -> None:
"""Initialize the application.
:param bp: The blueprint of the accounting application.
:param app: The Flask application.
:param bp: The blueprint of the accounting application.
:return: None.
"""
from .converters import BaseAccountConverter
app.url_map.converters["baseAccount"] = BaseAccountConverter
from .views import bp as base_account_bp
bp.register_blueprint(base_account_bp, url_prefix="/base-accounts")
from .commands import init_base_accounts_command
app.cli.add_command(init_base_accounts_command)

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
# Copyright (c) 2023 imacat.
@ -17,693 +17,30 @@
"""The console commands for the base account management.
"""
import click
from flask.cli import with_appcontext
import csv
from accounting.database import db
from .models import BaseAccount, BaseAccountL10n
import sqlalchemy as sa
BaseAccountData = tuple[int, str, str, str]
"""The format of the base account data, as a list of (code, English,
Traditional Chinese, Simplified Chinese) tuples."""
from accounting import data_dir
from accounting import db
from accounting.models import BaseAccount, BaseAccountL10n
from accounting.utils.title_case import title_case
@click.command("accounting-init-base")
@with_appcontext
def init_base_accounts_command() -> None:
"""Initializes the base accounts."""
if BaseAccount.query.first() is not None:
click.echo("Base accounts already exist.")
raise click.Abort
return
db.session.bulk_save_objects(
[BaseAccount(code=str(x[0]), title_l10n=x[1]) for x in DATA])
db.session.bulk_save_objects(
[BaseAccountL10n(account_code=x[0], locale=y[0], title=y[1])
for x in DATA for y in (("zh_Hant", x[2]), ("zh_Hans", x[3]))])
db.session.commit()
click.echo("Base accounts initialized.")
DATA: list[BaseAccountData] = [
(1, "assets", "資產", "资产"),
(2, "liabilities", "負債", "负债"),
(3, "owners equity", "業主權益", "业主权益"),
(4, "operating revenue", "營業收入", "营业收入"),
(5, "operating costs", "營業成本", "营业成本"),
(6, "operating expenses", "營業費用", "营业费用"),
(7, "non-operating revenue and expenses, other income (expense)",
"營業外收入及費用", "营业外收入及费用"),
(8, "income tax expense (or benefit)", "所得稅費用(或利益)",
"所得税费用(或利益)"),
(9, "nonrecurring gain or loss", "非經常營業損益", "非经常营业损益"),
(11, "current assets", "流動資產", "流动资产"),
(12, "current assets", "流動資產", "流动资产"),
(13, "funds and long-term investments", "基金及長期投資", "基金及长期投资"),
(14, "property , plant, and equipment", "固定資產", "固定资产"),
(15, "property , plant, and equipment", "固定資產", "固定资产"),
(16, "depletable assets", "遞耗資產", "递耗资产"),
(17, "intangible assets", "無形資產", "无形资产"),
(18, "other assets", "其他資產", "其他资产"),
(21, "current liabilities", "流動負債", "流动负债"),
(22, "current liabilities", "流動負債", "流动负债"),
(23, "long-term liabilities", "長期負債", "长期负债"),
(28, "other liabilities", "其他負債", "其他负债"),
(31, "capital", "資本", "资本"),
(32, "additional paid-in capital", "資本公積", "资本公积"),
(33, "retained earnings (accumulated deficit)", "保留盈餘(或累積虧損)",
"保留盈余(或累积亏损)"),
(34, "equity adjustments", "權益調整", "权益调整"),
(35, "treasury stock", "庫藏股", "库藏股"),
(36, "minority interest", "少數股權", "少数股权"),
(41, "sales revenue", "銷貨收入", "销货收入"),
(46, "service revenue", "勞務收入", "劳务收入"),
(47, "agency revenue", "業務收入", "业务收入"),
(48, "other operating revenue", "其他營業收入", "其他营业收入"),
(51, "cost of goods sold", "銷貨成本", "销货成本"),
(56, "service costs", "勞務成本", "劳务成本"),
(57, "agency costs", "業務成本", "业务成本"),
(58, "other operating costs", "其他營業成本", "其他营业成本"),
(61, "selling expenses", "推銷費用", "推销费用"),
(62, "general & administrative expenses", "管理及總務費用", "管理及总务费用"),
(63, "research and development expenses", "研究發展費用", "研究发展费用"),
(71, "non-operating revenue", "營業外收入", "营业外收入"),
(72, "non-operating revenue", "營業外收入", "营业外收入"),
(73, "non-operating revenue", "營業外收入", "营业外收入"),
(74, "non-operating revenue", "營業外收入", "营业外收入"),
(75, "non-operating expenses", "營業外費用", "营业外费用"),
(76, "non-operating expenses", "營業外費用", "营业外费用"),
(77, "non-operating expenses", "營業外費用", "营业外费用"),
(78, "non-operating expenses", "營業外費用", "营业外费用"),
(81, "income tax expense (or benefit)", "所得稅費用(或利益)",
"所得税费用(或利益)"),
(91, "gain (loss) from discontinued operations", "停業部門損益",
"停业部门损益"),
(92, "extraordinary gain or loss", "非常損益", "非常损益"),
(93, "cumulative effect of changes in accounting principles",
"會計原則變動累積影響數", "会计原则变动累积影响数"),
(94, "minority interest income", "少數股權淨利", "少数股权净利"),
(111, "cash and cash equivalents", "現金及約當現金", "现金及约当现金"),
(112, "short-term investments", "短期投資", "短期投资"),
(113, "notes receivable", "應收票據", "应收票据"),
(114, "accounts receivable", "應收帳款", "应收帐款"),
(118, "other receivables", "其他應收款", "其他应收款"),
(121, "inventories", "存貨", "存货"),
(122, "inventories", "存貨", "存货"),
(125, "prepaid expenses", "預付費用", "预付费用"),
(126, "prepayments", "預付款項", "预付款项"),
(128, "other current assets", "其他流動資產", "其他流动资产"),
(129, "other current assets", "其他流動資產", "其他流动资产"),
(131, "funds", "基金", "基金"),
(132, "long-term investments", "長期投資", "长期投资"),
(141, "land", "土地", "土地"),
(142, "land improvements", "土地改良物", "土地改良物"),
(143, "buildings", "房屋及建物", "房屋及建物"),
(144, "machinery and equipment", "機(器)具及設備", "机(器)具及设备"),
(145, "machinery and equipment", "機(器)具及設備", "机(器)具及设备"),
(146, "machinery and equipment", "機(器)具及設備", "机(器)具及设备"),
(151, "leased assets", "租賃資產", "租赁资产"),
(152, "leasehold improvements", "租賃權益改良", "租赁权益改良"),
(156, "construction in progress and prepayments for equipment",
"未完工程及預付購置設備款", "未完工程及预付购置设备款"),
(158, "miscellaneous property, plant, and equipment", "雜項固定資產",
"杂项固定资产"),
(161, "depletable assets", "遞耗資產", "递耗资产"),
(171, "trademarks", "商標權", "商标权"),
(172, "patents", "專利權", "专利权"),
(173, "franchise", "特許權", "特许权"),
(174, "copyright", "著作權", "著作权"),
(175, "computer software", "電腦軟體", "电脑软体"),
(176, "goodwill", "商譽", "商誉"),
(177, "organization costs", "開辦費", "开办费"),
(178, "other intangibles", "其他無形資產", "其他无形资产"),
(181, "deferred assets", "遞延資產", "递延资产"),
(182, "idle assets", "閒置資產", "闲置资产"),
(184, "long-term notes , accounts and overdue receivables",
"長期應收票據及款項與催收帳款", "长期应收票据及款项与催收帐款"),
(185, "assets leased to others", "出租資產", "出租资产"),
(186, "refundable deposit", "存出保證金", "存出保证金"),
(188, "miscellaneous assets", "雜項資產", "杂项资产"),
(211, "short-term borrowings (debt)", "短期借款", "短期借款"),
(212, "short-term notes and bills payable", "應付短期票券", "应付短期票券"),
(213, "notes payable", "應付票據", "应付票据"),
(214, "accounts pay able", "應付帳款", "应付帐款"),
(216, "income taxes payable", "應付所得稅", "应付所得税"),
(217, "accrued expenses", "應付費用", "应付费用"),
(218, "other payables", "其他應付款", "其他应付款"),
(219, "other payables", "其他應付款", "其他应付款"),
(226, "advance receipts", "預收款項", "预收款项"),
(227, "long-term liabilities -current portion",
"一年或一營業週期內到期長期負債", "一年或一营业周期内到期长期负债"),
(228, "other current liabilities", "其他流動負債",
"其他流动负债"),
(229, "other current liabilities", "其他流動負債",
"其他流动负债"),
(231, "corporate bonds payable", "應付公司債", "应付公司债"),
(232, "long-term loans payable", "長期借款", "长期借款"),
(233, "long-term notes and accounts payable", "長期應付票據及款項",
"长期应付票据及款项"),
(234, "accrued liabilities for land value increment tax",
"估計應付土地增值稅", "估计应付土地增值税"),
(235, "accrued pension liabilities", "應計退休金負債", "应计退休金负债"),
(238, "other long-term liabilities", "其他長期負債", "其他长期负债"),
(281, "deferred liabilities", "遞延負債", "递延负债"),
(286, "deposits received", "存入保證金", "存入保证金"),
(288, "miscellaneous liabilities", "雜項負債", "杂项负债"),
(311, "capital", "資本(或股本)", "资本(或股本)"),
(321, "paid-in capital in excess of par", "股票溢價", "股票溢价"),
(323, "capital surplus from assets revaluation", "資產重估增值準備",
"资产重估增值准备"),
(324, "capital surplus from gain on disposal of assets", "處分資產溢價公積",
"处分资产溢价公积"),
(325, "capital surplus from business combination", "合併公積", "合并公积"),
(326, "donated surplus", "受贈公積", "受赠公积"),
(328, "other additional paid-in capital", "其他資本公積", "其他资本公积"),
(331, "legal reserve", "法定盈餘公積", "法定盈余公积"),
(332, "special reserve", "特別盈餘公積", "特别盈余公积"),
(335, "retained earnings-unappropriated (or accumulated deficit)",
"未分配盈餘(或累積虧損)", "未分配盈余(或累积亏损)"),
(341,
"unrealized loss on market value decline of long-term equity investments",
"長期股權投資未實現跌價損失", "长期股权投资未实现跌价损失"),
(342, "cumulative translation adjustment", "累積換算調整數", "累积换算调整数"),
(343, "net loss not recognized as pension cost", "未認列為退休金成本之淨損失",
"未认列为退休金成本之净损失"),
(351, "treasury stock", "庫藏股", "库藏股"),
(361, "minority interest", "少數股權", "少数股权"),
(411, "sales revenue", "銷貨收入", "销货收入"),
(417, "sales return", "銷貨退回", "销货退回"),
(419, "sales allowances", "銷貨折讓", "销货折让"),
(461, "service revenue", "勞務收入", "劳务收入"),
(471, "agency revenue", "業務收入", "业务收入"),
(488, "other operating revenue", "其他營業收入—其他", "其他营业收入—其他"),
(511, "cost of goods sold", "銷貨成本", "销货成本"),
(512, "purchases", "進貨", "进货"),
(513, "materials purchased", "進料", "进料"),
(514, "direct labor", "直接人工", "直接人工"),
(515, "manufacturing overhead", "製造費用", "制造费用"),
(516, "manufacturing overhead", "製造費用", "制造费用"),
(517, "manufacturing overhead", "製造費用", "制造费用"),
(518, "manufacturing overhead", "製造費用", "制造费用"),
(561, "service costs", "勞務成本", "劳务成本"),
(571, "agency costs", "業務成本", "业务成本"),
(588, "other operating costs-other", "其他營業成本—其他", "其他营业成本—其他"),
(615, "selling expenses", "推銷費用", "推销费用"),
(616, "selling expenses", "推銷費用", "推销费用"),
(617, "selling expenses", "推銷費用", "推销费用"),
(618, "selling expenses", "推銷費用", "推销费用"),
(625, "general & administrative expenses", "管理及總務費用", "管理及总务费用"),
(626, "general & administrative expenses", "管理及總務費用", "管理及总务费用"),
(627, "general & administrative expenses", "管理及總務費用", "管理及总务费用"),
(628, "general & administrative expenses", "管理及總務費用", "管理及总务费用"),
(635, "research and development expenses", "研究發展費用", "研究发展费用"),
(636, "research and development expenses", "研究發展費用", "研究发展费用"),
(637, "research and development expenses", "研究發展費用", "研究发展费用"),
(638, "research and development expenses", "研究發展費用", "研究发展费用"),
(711, "interest revenue", "利息收入", "利息收入"),
(712, "investment income", "投資收益", "投资收益"),
(713, "foreign exchange gain", "兌換利益", "兑换利益"),
(714, "gain on disposal of investments", "處分投資收益", "处分投资收益"),
(715, "gain on disposal of assets", "處分資產溢價收入", "处分资产溢价收入"),
(748, "other non-operating revenue", "其他營業外收入", "其他营业外收入"),
(751, "interest expense", "利息費用", "利息费用"),
(752, "investment loss", "投資損失", "投资损失"),
(753, "foreign exchange loss", "兌換損失", "兑换损失"),
(754, "loss on disposal of investments", "處分投資損失", "处分投资损失"),
(755, "loss on disposal of assets", "處分資產損失", "处分资产损失"),
(788, "other non-operating expenses", "其他營業外費用", "其他营业外费用"),
(811, "income tax expense (or benefit)", "所得稅費用(或利益)",
"所得税费用(或利益)"),
(911, "income (loss) from operations of discontinued segments",
"停業部門損益—停業前營業損益", "停业部门损益—停业前营业损益"),
(912, "gain (loss) from disposal of discontinued segments",
"停業部門損益—處分損益", "停业部门损益—处分损益"),
(921, "extraordinary gain or loss", "非常損益", "非常损益"),
(931, "cumulative effect of changes in accounting principles",
"會計原則變動累積影響數", "会计原则变动累积影响数"),
(941, "minority interest income", "少數股權淨利", "少数股权净利"),
(1111, "cash on hand", "庫存現金", "库存现金"),
(1112, "petty cash/revolving funds", "零用金/週轉金", "零用金/周转金"),
(1113, "cash in banks", "銀行存款", "银行存款"),
(1116, "cash in transit", "在途現金", "在途现金"),
(1117, "cash equivalents", "約當現金", "约当现金"),
(1118, "other cash and cash equivalents", "其他現金及約當現金",
"其他现金及约当现金"),
(1121, "short-term investments stock", "短期投資—股票", "短期投资—股票"),
(1122, "short-term investments short-term notes and bills",
"短期投資—短期票券", "短期投资—短期票券"),
(1123, "short-term investments government bonds", "短期投資—政府債券",
"短期投资—政府债券"),
(1124, "short-term investments beneficiary certificates",
"短期投資—受益憑證", "短期投资—受益凭证"),
(1125, "short-term investments corporate bonds", "短期投資—公司債",
"短期投资—公司债"),
(1128, "short-term investments other", "短期投資—其他", "短期投资—其他"),
(1129, "allowance for reduction of short-term investment to market",
"備抵短期投資跌價損失", "备抵短期投资跌价损失"),
(1131, "notes receivable", "應收票據", "应收票据"),
(1132, "discounted notes receivable", "應收票據貼現", "应收票据贴现"),
(1137, "notes receivable related parties", "應收票據—關係人",
"应收票据—关系人"),
(1138, "other notes receivable", "其他應收票據", "其他应收票据"),
(1139, "allowance for uncollectible accounts notes receivable",
"備抵呆帳-應收票據", "备抵呆帐-应收票据"),
(1141, "accounts receivable", "應收帳款", "应收帐款"),
(1142, "installment accounts receivable", "應收分期帳款",
"应收分期帐款"),
(1147, "accounts receivable related parties", "應收帳款—關係人",
"应收帐款—关系人"),
(1149, "allowance for uncollectible accounts accounts receivable",
"備抵呆帳-應收帳款", "备抵呆帐-应收帐款"),
(1181, "forward exchange contract receivable", "應收出售遠匯款",
"应收出售远汇款"),
(1182, "forward exchange contract receivable foreign currencies",
"應收遠匯款—外幣", "应收远汇款—外币"),
(1183, "discount on forward ex-change contract", "買賣遠匯折價",
"买卖远汇折价"),
(1184, "earned revenue receivable", "應收收益", "应收收益"),
(1185, "income tax refund receivable", "應收退稅款", "应收退税款"),
(1187, "other receivables related parties", "其他應收款—關係人",
"其他应收款—关系人"),
(1188, "other receivables other", "其他應收款—其他", "其他应收款—其他"),
(1189, "allowance for uncollectible accounts other receivables",
"備抵呆帳—其他應收款", "备抵呆帐—其他应收款"),
(1211, "merchandise inventory", "商品存貨", "商品存货"),
(1212, "consigned goods", "寄銷商品", "寄销商品"),
(1213, "goods in transit", "在途商品", "在途商品"),
(1219, "allowance for reduction of inventory to market", "備抵存貨跌價損失",
"备抵存货跌价损失"),
(1221, "finished goods", "製成品", "制成品"),
(1222, "consigned finished goods", "寄銷製成品", "寄销制成品"),
(1223, "by-products", "副產品", "副产品"),
(1224, "work in process", "在製品", "在制品"),
(1225, "work in process outsourced", "委外加工", "委外加工"),
(1226, "raw materials", "原料", "原料"),
(1227, "supplies", "物料", "物料"),
(1228, "materials and supplies in transit", "在途原物料", "在途原物料"),
(1229, "allowance for reduction of inventory to market", "備抵存貨跌價損失",
"备抵存货跌价损失"),
(1251, "prepaid payroll", "預付薪資", "预付薪资"),
(1252, "prepaid rents", "預付租金", "预付租金"),
(1253, "prepaid insurance", "預付保險費", "预付保险费"),
(1254, "office supplies", "用品盤存", "用品盘存"),
(1255, "prepaid income tax", "預付所得稅", "预付所得税"),
(1258, "other prepaid expenses", "其他預付費用", "其他预付费用"),
(1261, "prepayment for purchases", "預付貨款", "预付货款"),
(1268, "other prepayments", "其他預付款項", "其他预付款项"),
(1281, "VAT paid ( or input tax)", "進項稅額", "进项税额"),
(1282, "excess VAT paid (or overpaid VAT)", "留抵稅額", "留抵税额"),
(1283, "temporary payments", "暫付款", "暂付款"),
(1284, "payment on behalf of others", "代付款", "代付款"),
(1285, "advances to employees", "員工借支", "员工借支"),
(1286, "refundable deposits", "存出保證金", "存出保证金"),
(1287, "certificate of deposit-restricted", "受限制存款", "受限制存款"),
(1291, "deferred income tax assets", "遞延所得稅資產", "递延所得税资产"),
(1292, "deferred foreign exchange losses", "遞延兌換損失", "递延兑换损失"),
(1293, "owners (stockholders) current account", "業主(股東)往來",
"业主(股东)往来"),
(1294, "current account with others", "同業往來", "同业往来"),
(1298, "other current assets other", "其他流動資產—其他",
"其他流动资产—其他"),
(1311, "redemption fund (or sinking fund)", "償債基金", "偿债基金"),
(1312, "fund for improvement and expansion", "改良及擴充基金",
"改良及扩充基金"),
(1313, "contingency fund", "意外損失準備基金", "意外损失准备基金"),
(1314, "pension fund", "退休基金", "退休基金"),
(1318, "other funds", "其他基金", "其他基金"),
(1321, "long-term equity investments", "長期股權投資", "长期股权投资"),
(1322, "long-term bond investments", "長期債券投資", "长期债券投资"),
(1323, "long-term real estate in-vestments", "長期不動產投資",
"长期不动产投资"),
(1324, "cash surrender value of life insurance", "人壽保險現金解約價值",
"人寿保险现金解约价值"),
(1328, "other long-term investments", "其他長期投資", "其他长期投资"),
(1329,
"allowance for excess of cost over market value of long-term investments",
"備抵長期投資跌價損失", "备抵长期投资跌价损失"),
(1411, "land", "土地", "土地"),
(1418, "land revaluation increments", "土地—重估增值", "土地—重估增值"),
(1421, "land improvements", "土地改良物", "土地改良物"),
(1428, "land improvements revaluation increments", "土地改良物—重估增值",
"土地改良物—重估增值"),
(1429, "accumulated depreciation land improvements", "累積折舊—土地改良物",
"累积折旧—土地改良物"),
(1431, "buildings", "房屋及建物", "房屋及建物"),
(1438, "buildings revaluation increments", "房屋及建物—重估增值",
"房屋及建物—重估增值"),
(1439, "accumulated depreciation buildings", "累積折舊—房屋及建物",
"累积折旧—房屋及建物"),
(1441, "machinery", "機(器)具", "机(器)具"),
(1448, "machinery revaluation increments", "機(器)具—重估增值",
"机(器)具—重估增值"),
(1449, "accumulated depreciation machinery", "累積折舊—機(器)具",
"累积折旧—机(器)具"),
(1511, "leased assets", "租賃資產", "租赁资产"),
(1519, "accumulated depreciation leased assets", "累積折舊—租賃資產",
"累积折旧—租赁资产"),
(1521, "leasehold improvements", "租賃權益改良", "租赁权益改良"),
(1529, "accumulated depreciation leasehold improvements",
"累積折舊—租賃權益改良", "累积折旧—租赁权益改良"),
(1561, "construction in progress", "未完工程", "未完工程"),
(1562, "prepayment for equipment", "預付購置設備款", "预付购置设备款"),
(1581, "miscellaneous property, plant, and equipment", "雜項固定資產",
"杂项固定资产"),
(1588,
"miscellaneous property, plant, and equipment revaluation increments",
"雜項固定資產—重估增值", "杂项固定资产—重估增值"),
(1589,
"accumulated depreciation miscellaneous property, plant, and equipment",
"累積折舊—雜項固定資產", "累积折旧—杂项固定资产"),
(1611, "natural resources", "天然資源", "天然资源"),
(1618, "natural resources revaluation increments", "天然資源—重估增值",
"天然资源—重估增值"),
(1619, "accumulated depletion natural resources", "累積折耗—天然資源",
"累积折耗—天然资源"),
(1711, "trademarks", "商標權", "商标权"),
(1721, "patents", "專利權", "专利权"),
(1731, "franchise", "特許權", "特许权"),
(1741, "copyright", "著作權", "著作权"),
(1751, "computer software cost", "電腦軟體", "电脑软体"),
(1761, "goodwill", "商譽", "商誉"),
(1771, "organization costs", "開辦費", "开办费"),
(1781, "deferred pension costs", "遞延退休金成本", "递延退休金成本"),
(1782, "leasehold improvements", "租賃權益改良", "租赁权益改良"),
(1788, "other intangible assets other", "其他無形資產—其他",
"其他无形资产—其他"),
(1811, "deferred bond issuance costs", "債券發行成本", "债券发行成本"),
(1812, "long-term prepaid rent", "長期預付租金", "长期预付租金"),
(1813, "long-term prepaid insurance", "長期預付保險費", "长期预付保险费"),
(1814, "deferred income tax assets", "遞延所得稅資產", "递延所得税资产"),
(1815, "prepaid pension cost", "預付退休金", "预付退休金"),
(1818, "other deferred assets", "其他遞延資產", "其他递延资产"),
(1821, "idle assets", "閒置資產", "闲置资产"),
(1841, "long-term notes receivable", "長期應收票據", "长期应收票据"),
(1842, "long-term accounts receivable", "長期應收帳款", "长期应收帐款"),
(1843, "overdue receivables", "催收帳款", "催收帐款"),
(1847,
"long-term notes, accounts and overdue receivables related parties",
"長期應收票據及款項與催收帳款—關係人", "长期应收票据及款项与催收帐款—关系人"),
(1848, "other long-term receivables", "其他長期應收款項", "其他长期应收款项"),
(1849,
"allowance for uncollectible accounts long-term notes, accounts and"
" overdue receivables",
"備抵呆帳—長期應收票據及款項與催收帳款", "备抵呆帐—长期应收票据及款项与催收帐款"),
(1851, "assets leased to others", "出租資產", "出租资产"),
(1858, "assets leased to others incremental value from revaluation",
"出租資產—重估增值", "出租资产—重估增值"),
(1859, "accumulated depreciation assets leased to others",
"累積折舊—出租資產", "累积折旧—出租资产"),
(1861, "refundable deposits", "存出保證金", "存出保证金"),
(1881, "certificate of deposit restricted", "受限制存款", "受限制存款"),
(1888, "miscellaneous assets other", "雜項資產—其他", "杂项资产—其他"),
(2111, "bank overdraft", "銀行透支", "银行透支"),
(2112, "bank loan", "銀行借款", "银行借款"),
(2114, "short-term borrowings owners", "短期借款—業主", "短期借款—业主"),
(2115, "short-term borrowings employees", "短期借款—員工", "短期借款—员工"),
(2117, "short-term borrowings related parties", "短期借款—關係人",
"短期借款—关系人"),
(2118, "short-term borrowings other", "短期借款—其他", "短期借款—其他"),
(2121, "commercial paper payable", "應付商業本票", "应付商业本票"),
(2122, "bank acceptance", "銀行承兌匯票", "银行承兑汇票"),
(2128, "other short-term notes and bills payable", "其他應付短期票券",
"其他应付短期票券"),
(2129, "discount on short-term notes and bills payable", "應付短期票券折價",
"应付短期票券折价"),
(2131, "notes payable", "應付票據", "应付票据"),
(2137, "notes payable related parties", "應付票據—關係人",
"应付票据—关系人"),
(2138, "other notes payable", "其他應付票據", "其他应付票据"),
(2141, "accounts payable", "應付帳款", "应付帐款"),
(2147, "accounts payable related parties", "應付帳款—關係人",
"应付帐款—关系人"),
(2161, "income tax payable", "應付所得稅", "应付所得税"),
(2171, "accrued payroll", "應付薪工", "应付薪工"),
(2172, "accrued rent payable", "應付租金", "应付租金"),
(2173, "accrued interest payable", "應付利息", "应付利息"),
(2174, "accrued VAT payable", "應付營業稅", "应付营业税"),
(2175, "accrued taxes payable other", "應付稅捐—其他", "应付税捐—其他"),
(2178, "other accrued expenses payable", "其他應付費用", "其他应付费用"),
(2181, "forward exchange contract payable", "應付購入遠匯款", "应付购入远汇款"),
(2182, "forward exchange contract payable foreign currencies",
"應付遠匯款—外幣", "应付远汇款—外币"),
(2183, "premium on forward exchange contract", "買賣遠匯溢價", "买卖远汇溢价"),
(2184, "payables on land and building purchased", "應付土地房屋款",
"应付土地房屋款"),
(2185, "Payables on equipment", "應付設備款", "应付设备款"),
(2187, "other payables related parties", "其他應付款—關係人",
"其他应付款—关系人"),
(2191, "dividend payable", "應付股利", "应付股利"),
(2192, "bonus payable", "應付紅利", "应付红利"),
(2193, "compensation payable to directors and supervisors", "應付董監事酬勞",
"应付董监事酬劳"),
(2198, "other payables other", "其他應付款—其他", "其他应付款—其他"),
(2261, "sales revenue received in advance", "預收貨款", "预收货款"),
(2262, "revenue received in advance", "預收收入", "预收收入"),
(2268, "other advance receipts", "其他預收款", "其他预收款"),
(2271, "corporate bonds payable current portion",
"一年或一營業週期內到期公司債", "一年或一营业周期内到期公司债"),
(2272, "long-term loans payable current portion",
"一年或一營業週期內到期長期借款", "一年或一营业周期内到期长期借款"),
(2273,
"long-term notes and accounts payable due within one year or one"
" operating cycle",
"一年或一營業週期內到期長期應付票據及款項",
"一年或一营业周期内到期长期应付票据及款项"),
(2277,
"long-term notes and accounts payables to related parties current"
" portion",
"一年或一營業週期內到期長期應付票據及款項—關係人",
"一年或一营业周期内到期长期应付票据及款项—关系人"),
(2278, "other long-term liabilities current portion",
"其他一年或一營業週期內到期長期負債", "其他一年或一营业周期内到期长期负债"),
(2281, "VAT received (or output tax)", "銷項稅額", "销项税额"),
(2283, "temporary receipts", "暫收款", "暂收款"),
(2284, "receipts under custody", "代收款", "代收款"),
(2285, "estimated warranty liabilities", "估計售後服務/保固負債",
"估计售后服务/保固负债"),
(2291, "deferred income tax liabilities", "遞延所得稅負債", "递延所得税负债"),
(2292, "deferred foreign exchange gain", "遞延兌換利益", "递延兑换利益"),
(2293, "owners current account", "業主(股東)往來", "业主(股东)往来"),
(2294, "current account with others", "同業往來", "同业往来"),
(2298, "other current liabilities others", "其他流動負債—其他",
"其他流动负债—其他"),
(2311, "corporate bonds payable", "應付公司債", "应付公司债"),
(2319, "premium (discount) on corporate bonds payable",
"應付公司債溢(折)價", "应付公司债溢(折)价"),
(2321, "long-term loans payable bank", "長期銀行借款", "长期银行借款"),
(2324, "long-term loans payable owners", "長期借款—業主", "长期借款—业主"),
(2325, "long-term loans payable employees", "長期借款—員工",
"长期借款—员工"),
(2327, "long-term loans payable related parties", "長期借款—關係人",
"长期借款—关系人"),
(2328, "long-term loans payable other", "長期借款—其他", "长期借款—其他"),
(2331, "long-term notes payable", "長期應付票據", "长期应付票据"),
(2332, "long-term accounts pay-able", "長期應付帳款", "长期应付帐款"),
(2333, "long-term capital lease liabilities", "長期應付租賃負債",
"长期应付租赁负债"),
(2337, "Long-term notes and accounts payable related parties",
"長期應付票據及款項—關係人", "长期应付票据及款项—关系人"),
(2338, "other long-term payables", "其他長期應付款項", "其他长期应付款项"),
(2341, "estimated accrued land value incremental tax pay-able",
"估計應付土地增值稅", "估计应付土地增值税"),
(2351, "accrued pension liabilities", "應計退休金負債", "应计退休金负债"),
(2388, "other long-term liabilities other", "其他長期負債—其他",
"其他长期负债—其他"),
(2811, "deferred revenue", "遞延收入", "递延收入"),
(2814, "deferred income tax liabilities", "遞延所得稅負債", "递延所得税负债"),
(2818, "other deferred liabilities", "其他遞延負債", "其他递延负债"),
(2861, "guarantee deposit received", "存入保證金", "存入保证金"),
(2888, "miscellaneous liabilities other", "雜項負債—其他", "杂项负债—其他"),
(3111, "capital common stock", "普通股股本", "普通股股本"),
(3112, "capital preferred stock", "特別股股本", "特别股股本"),
(3113, "capital collected in advance", "預收股本", "预收股本"),
(3114, "stock dividends to be distributed", "待分配股票股利",
"待分配股票股利"),
(3115, "capital", "資本", "资本"),
(3211, "paid-in capital in excess of par- common stock", "普通股股票溢價",
"普通股股票溢价"),
(3212, "paid-in capital in excess of par- preferred stock", "特別股股票溢價",
"特别股股票溢价"),
(3231, "capital surplus from assets revaluation", "資產重估增值準備",
"资产重估增值准备"),
(3241, "capital surplus from gain on disposal of assets", "處分資產溢價公積",
"处分资产溢价公积"),
(3251, "capital surplus from business combination", "合併公積", "合并公积"),
(3261, "donated surplus", "受贈公積", "受赠公积"),
(3281, "additional paid-in capital from investee under equity method",
"權益法長期股權投資資本公積", "权益法长期股权投资资本公积"),
(3282, "additional paid-in capital treasury stock trans-actions",
"資本公積—庫藏股票交易", "资本公积—库藏股票交易"),
(3311, "legal reserve", "法定盈餘公積", "法定盈余公积"),
(3321, "contingency reserve", "意外損失準備", "意外损失准备"),
(3322, "improvement and expansion reserve", "改良擴充準備", "改良扩充准备"),
(3323, "special reserve for redemption of liabilities", "償債準備",
"偿债准备"),
(3328, "other special reserve", "其他特別盈餘公積", "其他特别盈余公积"),
(3351, "accumulated profit or loss", "累積盈虧", "累积盈亏"),
(3352, "prior period adjustments", "前期損益調整", "前期损益调整"),
(3353, "net income or loss for current period", "本期損益", "本期损益"),
(3411,
"unrealized loss on market value decline of long-term equity investments",
"長期股權投資未實現跌價損失", "长期股权投资未实现跌价损失"),
(3421, "cumulative translation adjustments", "累積換算調整數",
"累积换算调整数"),
(3431, "net loss not recognized as pension costs",
"未認列為退休金成本之淨損失", "未认列为退休金成本之净损失"),
(3511, "treasury stock", "庫藏股", "库藏股"),
(3611, "minority interest", "少數股權", "少数股权"),
(4111, "sales revenue", "銷貨收入", "销货收入"),
(4112, "installment sales revenue", "分期付款銷貨收入", "分期付款销货收入"),
(4171, "sales return", "銷貨退回", "销货退回"),
(4191, "sales discounts and allowances", "銷貨折讓", "销货折让"),
(4611, "service revenue", "勞務收入", "劳务收入"),
(4711, "agency revenue", "業務收入", "业务收入"),
(4888, "other operating revenue other", "其他營業收入—其他",
"其他营业收入—其他"),
(5111, "cost of goods sold", "銷貨成本", "销货成本"),
(5112, "installment cost of goods sold", "分期付款銷貨成本",
"分期付款销货成本"),
(5121, "purchases", "進貨", "进货"),
(5122, "purchase expenses", "進貨費用", "进货费用"),
(5123, "purchase returns", "進貨退出", "进货退出"),
(5124, "charges on purchased merchandise", "進貨折讓", "进货折让"),
(5131, "material purchased", "進料", "进料"),
(5132, "charges on purchased material", "進料費用", "进料费用"),
(5133, "material purchase returns", "進料退出", "进料退出"),
(5134, "material purchase allowances", "進料折讓", "进料折让"),
(5141, "direct labor", "直接人工", "直接人工"),
(5151, "indirect labor", "間接人工", "间接人工"),
(5152, "rent expense, rent", "租金支出", "租金支出"),
(5153, "office supplies (expense)", "文具用品", "文具用品"),
(5154, "travelling expense, travel", "旅費", "旅费"),
(5155, "shipping expenses, freight", "運費", "运费"),
(5156, "postage (expenses)", "郵電費", "邮电费"),
(5157, "repair (s) and maintenance (expense )", "修繕費", "修缮费"),
(5158, "packing expenses", "包裝費", "包装费"),
(5161, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"),
(5162, "insurance (expense)", "保險費", "保险费"),
(5163, "manufacturing overhead outsourced", "加工費", "加工费"),
(5166, "taxes", "稅捐", "税捐"),
(5168, "depreciation expense", "折舊", "折旧"),
(5169, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"),
(5172, "meal (expenses)", "伙食費", "伙食费"),
(5173, "employee benefits/welfare", "職工福利", "职工福利"),
(5176, "training (expense)", "訓練費", "训练费"),
(5177, "indirect materials", "間接材料", "间接材料"),
(5188, "other manufacturing expenses", "其他製造費用", "其他制造费用"),
(5611, "service costs", "勞務成本", "劳务成本"),
(5711, "agency costs", "業務成本", "业务成本"),
(5888, "other operating costs other", "其他營業成本—其他",
"其他营业成本—其他"),
(6151, "payroll expense", "薪資支出", "薪资支出"),
(6152, "rent expense, rent", "租金支出", "租金支出"),
(6153, "office supplies (expense)", "文具用品", "文具用品"),
(6154, "travelling expense, travel", "旅費", "旅费"),
(6155, "shipping expenses, freight", "運費", "运费"),
(6156, "postage (expenses)", "郵電費", "邮电费"),
(6157, "repair (s) and maintenance (expense)", "修繕費", "修缮费"),
(6159, "advertisement expense, advertisement", "廣告費", "广告费"),
(6161, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"),
(6162, "insurance (expense)", "保險費", "保险费"),
(6164, "entertainment (expense)", "交際費", "交际费"),
(6165, "donation (expense)", "捐贈", "捐赠"),
(6166, "taxes", "稅捐", "税捐"),
(6167, "loss on uncollectible accounts", "呆帳損失", "呆帐损失"),
(6168, "depreciation expense", "折舊", "折旧"),
(6169, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"),
(6172, "meal (expenses)", "伙食費", "伙食费"),
(6173, "employee benefits/welfare", "職工福利", "职工福利"),
(6175, "commission (expense)", "佣金支出", "佣金支出"),
(6176, "training (expense)", "訓練費", "训练费"),
(6188, "other selling expenses", "其他推銷費用", "其他推销费用"),
(6251, "payroll expense", "薪資支出", "薪资支出"),
(6252, "rent expense, rent", "租金支出", "租金支出"),
(6253, "office supplies", "文具用品", "文具用品"),
(6254, "travelling expense, travel", "旅費", "旅费"),
(6255, "shipping expenses,freight", "運費", "运费"),
(6256, "postage (expenses)", "郵電費", "邮电费"),
(6257, "repair (s) and maintenance (expense)", "修繕費", "修缮费"),
(6259, "advertisement expense, advertisement", "廣告費", "广告费"),
(6261, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"),
(6262, "insurance (expense)", "保險費", "保险费"),
(6264, "entertainment (expense)", "交際費", "交际费"),
(6265, "donation (expense)", "捐贈", "捐赠"),
(6266, "taxes", "稅捐", "税捐"),
(6267, "loss on uncollectible accounts", "呆帳損失", "呆帐损失"),
(6268, "depreciation expense", "折舊", "折旧"),
(6269, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"),
(6271, "loss on export sales", "外銷損失", "外销损失"),
(6272, "meal (expenses)", "伙食費", "伙食费"),
(6273, "employee benefits/welfare", "職工福利", "职工福利"),
(6274, "research and development expense", "研究發展費用", "研究发展费用"),
(6275, "commission (expense)", "佣金支出", "佣金支出"),
(6276, "training (expense)", "訓練費", "训练费"),
(6278, "professional service fees", "勞務費", "劳务费"),
(6288, "other general and administrative expenses", "其他管理及總務費用",
"其他管理及总务费用"),
(6351, "payroll expense", "薪資支出", "薪资支出"),
(6352, "rent expense, rent", "租金支出", "租金支出"),
(6353, "office supplies", "文具用品", "文具用品"),
(6354, "travelling expense, travel", "旅費", "旅费"),
(6355, "shipping expenses, freight", "運費", "运费"),
(6356, "postage (expenses)", "郵電費", "邮电费"),
(6357, "repair (s) and maintenance (expense)", "修繕費", "修缮费"),
(6361, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"),
(6362, "insurance (expense)", "保險費", "保险费"),
(6364, "entertainment (expense)", "交際費", "交际费"),
(6366, "taxes", "稅捐", "税捐"),
(6368, "depreciation expense", "折舊", "折旧"),
(6369, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"),
(6372, "meal (expenses)", "伙食費", "伙食费"),
(6373, "employee benefits/welfare", "職工福利", "职工福利"),
(6376, "training (expense)", "訓練費", "训练费"),
(6378, "other research and development expenses", "其他研究發展費用",
"其他研究发展费用"),
(7111, "interest revenue/income", "利息收入", "利息收入"),
(7121, "investment income recognized under equity method",
"權益法認列之投資收益", "权益法认列之投资收益"),
(7122, "dividends income", "股利收入", "股利收入"),
(7123, "gain on market price recovery of short-term investment",
"短期投資市價回升利益", "短期投资市价回升利益"),
(7131, "foreign exchange gain", "兌換利益", "兑换利益"),
(7141, "gain on disposal of investments", "處分投資收益", "处分投资收益"),
(7151, "gain on disposal of assets", "處分資產溢價收入", "处分资产溢价收入"),
(7481, "donation income", "捐贈收入", "捐赠收入"),
(7482, "rent revenue/income", "租金收入", "租金收入"),
(7483, "commission revenue/income", "佣金收入", "佣金收入"),
(7484, "revenue from sale of scraps", "出售下腳及廢料收入",
"出售下脚及废料收入"),
(7485, "gain on physical inventory", "存貨盤盈", "存货盘盈"),
(7486, "gain from price recovery of inventory", "存貨跌價回升利益",
"存货跌价回升利益"),
(7487, "gain on reversal of bad debts", "壞帳轉回利益", "坏帐转回利益"),
(7488, "other non-operating revenue other items", "其他營業外收入—其他",
"其他营业外收入—其他"),
(7511, "interest expense", "利息費用", "利息费用"),
(7521, "investment loss recognized under equity method",
"權益法認列之投資損失", "权益法认列之投资损失"),
(7523, "unrealized loss on reduction of short-term investments to market",
"短期投資未實現跌價損失", "短期投资未实现跌价损失"),
(7531, "foreign exchange loss", "兌換損失", "兑换损失"),
(7541, "loss on disposal of investments", "處分投資損失", "处分投资损失"),
(7551, "loss on disposal of assets", "處分資產損失", "处分资产损失"),
(7881, "loss on work stoppages", "停工損失", "停工损失"),
(7882, "casualty loss", "災害損失", "灾害损失"),
(7885, "loss on physical inventory", "存貨盤損", "存货盘损"),
(7886,
"loss for market price decline and obsolete and slow-moving inventories",
"存貨跌價及呆滯損失", "存货跌价及呆滞损失"),
(7888, "other non-operating expenses other", "其他營業外費用—其他",
"其他营业外费用—其他"),
(8111, "income tax expense ( or benefit)", "所得稅費用(或利益)",
"所得税费用(或利益)"),
(9111, "income (loss) from operations of discontinued segment",
"停業部門損益—停業前營業損益", "停业部门损益—停业前营业损益"),
(9121, "gain (loss) from disposal of discontinued segment",
"停業部門損益—處分損益", "停业部门损益—处分损益"),
(9211, "extraordinary gain or loss", "非常損益", "非常损益"),
(9311, "cumulative effect of changes in accounting principles",
"會計原則變動累積影響數", "会计原则变动累积影响数"),
(9411, "minority interest income", "少數股權淨利", "少数股权净利"),
]
"""The base account data."""
with open(data_dir / "base_accounts.csv") as fp:
data: list[dict[str, str]] = [x for x in csv.DictReader(fp)]
account_data: list[dict[str, str]] = [{"code": x["code"],
"title_l10n": title_case(x["title"])}
for x in data]
locales: list[str] = [x[5:] for x in data[0] if x.startswith("l10n-")]
l10n_data: list[dict[str, str]] = [{"account_code": x["code"],
"locale": y,
"title": x[f"l10n-{y}"]}
for x in data for y in locales]
db.session.execute(sa.insert(BaseAccount), account_data)
db.session.execute(sa.insert(BaseAccountL10n), l10n_data)

View File

@ -0,0 +1,48 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
# 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 path converters for the base account management.
"""
from flask import abort
from werkzeug.routing import BaseConverter
from accounting import db
from accounting.models import BaseAccount
class BaseAccountConverter(BaseConverter):
"""The account converter to convert the account code and to the
corresponding base account in the routes."""
def to_python(self, value: str) -> BaseAccount:
"""Converts an account code to a base account.
:param value: The account code.
:return: The corresponding base account.
"""
account: BaseAccount | None = db.session.get(BaseAccount, value)
if account is None:
abort(404)
return account
def to_url(self, value: BaseAccount) -> str:
"""Converts a base account to its code.
:param value: The base account.
:return: The code.
"""
return value.code

View File

@ -1,73 +0,0 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
# 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 data models for the base account management.
"""
from flask import current_app
from flask_babel import get_locale
from accounting.database import db
class BaseAccount(db.Model):
"""A base account."""
__tablename__ = "accounting_base_accounts"
"""The table name."""
code = db.Column(db.String, nullable=False, primary_key=True)
"""The code."""
title_l10n = db.Column("title", db.String, nullable=False)
"""The title."""
l10n = db.relationship("BaseAccountL10n", back_populates="account",
lazy=False)
"""The localized titles."""
def __str__(self) -> str:
"""Returns the string representation of the base account.
:return: The string representation of the base account.
"""
return F"{self.code} {self.title}"
@property
def title(self) -> str:
"""Returns the title in the current locale.
:return: The title in the current locale.
"""
current_locale = str(get_locale())
if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
return self.title_l10n
for l10n in self.l10n:
if l10n.locale == current_locale:
return l10n.title
return self.title_l10n
class BaseAccountL10n(db.Model):
"""A localized base account title."""
__tablename__ = "accounting_base_accounts_l10n"
"""The table name."""
account_code = db.Column(db.String, db.ForeignKey(BaseAccount.code,
ondelete="CASCADE"),
nullable=False, primary_key=True)
"""The code of the account."""
account = db.relationship(BaseAccount, back_populates="l10n")
"""The account."""
locale = db.Column(db.String, nullable=False, primary_key=True)
"""The locale."""
title = db.Column(db.String, nullable=False)
"""The localized title."""

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26
# Copyright (c) 2023 imacat.
@ -14,14 +14,14 @@
# 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 base account query.
"""The queries for the base account management.
"""
import sqlalchemy as sa
from flask import request
from accounting.models import BaseAccount, BaseAccountL10n
from accounting.utils.query import parse_query_keywords
from .models import BaseAccount, BaseAccountL10n
def get_base_account_query() -> list[BaseAccount]:
@ -35,10 +35,10 @@ def get_base_account_query() -> list[BaseAccount]:
conditions: list[sa.BinaryExpression] = []
for k in keywords:
l10n: list[BaseAccountL10n] = BaseAccountL10n.query\
.filter(BaseAccountL10n.title.contains(k)).all()
.filter(BaseAccountL10n.title.icontains(k)).all()
l10n_matches: set[str] = {x.account_code for x in l10n}
conditions.append(sa.or_(BaseAccount.code.contains(k),
BaseAccount.title_l10n.contains(k),
BaseAccount.title_l10n.icontains(k),
BaseAccount.code.in_(l10n_matches)))
return BaseAccount.query.filter(*conditions)\
.order_by(BaseAccount.code).all()

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26
# Copyright (c) 2023 imacat.
@ -19,8 +19,10 @@
"""
from flask import Blueprint, render_template
from accounting.models import BaseAccount
from accounting.utils.pagination import Pagination
from accounting.utils.permission import has_permission, can_view
from .queries import get_base_account_query
bp: Blueprint = Blueprint("base-account", __name__)
"""The view blueprint for the base account management."""
@ -33,9 +35,19 @@ def list_accounts() -> str:
:return: The account list.
"""
from .models import BaseAccount
from .query import get_base_account_query
accounts: list[BaseAccount] = get_base_account_query()
pagination: Pagination = Pagination[BaseAccount](accounts)
return render_template("accounting/base-account/list.html",
list=pagination.list, pagination=pagination)
@bp.get("<baseAccount:account>", endpoint="detail")
@has_permission(can_view)
def show_account_detail(account: BaseAccount) -> str:
"""Shows the account detail.
:param account: The account.
:return: The detail.
"""
return render_template("accounting/base-account/detail.html", obj=account)

View File

@ -0,0 +1,94 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/10
# 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 console commands.
"""
import os
import click
from flask.cli import with_appcontext
from accounting import db
from accounting.account import init_accounts_command
from accounting.base_account import init_base_accounts_command
from accounting.currency import init_currencies_command
from accounting.models import BaseAccount, Account
from accounting.utils.title_case import title_case
from accounting.utils.user import has_user, get_user_pk
import sqlalchemy as sa
def __validate_username(ctx: click.core.Context, param: click.core.Option,
value: str) -> str:
"""Validates the username for the click console command.
:param ctx: The console command context.
:param param: The console command option.
:param value: The username.
:raise click.BadParameter: When validation fails.
:return: The username.
"""
value = value.strip()
if value == "":
raise click.BadParameter("Username empty.")
if not has_user(value):
raise click.BadParameter(f"User {value} does not exist.")
return value
@click.command("accounting-init-db")
@click.option("-u", "--username", metavar="USERNAME", prompt=True,
help="The username.", callback=__validate_username,
default=lambda: os.getlogin())
@with_appcontext
def init_db_command(username: str) -> None:
"""Initializes the accounting database."""
db.create_all()
init_base_accounts_command()
init_accounts_command(username)
init_currencies_command(username)
db.session.commit()
click.echo("Accounting database initialized.")
@click.command("accounting-titleize")
@click.option("-u", "--username", metavar="USERNAME", prompt=True,
help="The username.", callback=__validate_username,
default=lambda: os.getlogin())
@with_appcontext
def titleize_command(username: str) -> None:
"""Capitalize the account titles."""
updater_pk: int = get_user_pk(username)
updated: int = 0
for base in BaseAccount.query:
new_title: str = title_case(base.title_l10n)
if base.title_l10n != new_title:
base.title_l10n = new_title
updated = updated + 1
for account in Account.query:
if account.title_l10n.lower() == account.base.title_l10n.lower():
new_title: str = title_case(account.title_l10n)
if account.title_l10n != new_title:
account.title_l10n = new_title
account.updated_at = sa.func.now()
account.updated_by_id = updater_pk
updated = updated + 1
if updated == 0:
click.echo("All account titles were already capitalized.")
return
db.session.commit()
click.echo(f"{updated} account titles capitalized.")

View File

@ -0,0 +1,37 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
# 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 currency management.
"""
from flask import Flask, Blueprint
from .commands import init_currencies_command
def init_app(app: Flask, bp: Blueprint) -> None:
"""Initialize the application.
:param app: The Flask application.
:param bp: The blueprint of the accounting application.
:return: None.
"""
from .converters import CurrencyConverter
app.url_map.converters["currency"] = CurrencyConverter
from .views import bp as currency_bp, api_bp as currency_api_bp
bp.register_blueprint(currency_bp, url_prefix="/currencies")
bp.register_blueprint(currency_api_bp, url_prefix="/api/currencies")

View File

@ -0,0 +1,53 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
# 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 console commands for the currency management.
"""
import csv
from typing import Any
import sqlalchemy as sa
from accounting import db, data_dir
from accounting.models import Currency, CurrencyL10n
from accounting.utils.user import get_user_pk
def init_currencies_command(username: str) -> None:
"""Initializes the currencies."""
existing_codes: set[str] = {x.code for x in Currency.query.all()}
with open(data_dir / "currencies.csv") as fp:
data: list[dict[str, str]] = [x for x in csv.DictReader(fp)]
to_add: list[dict[str, str]] = [x for x in data
if x["code"] not in existing_codes]
if len(to_add) == 0:
return
creator_pk: int = get_user_pk(username)
currency_data: list[dict[str, Any]] = [{"code": x["code"],
"name_l10n": x["name"],
"created_by_id": creator_pk,
"updated_by_id": creator_pk}
for x in to_add]
locales: list[str] = [x[5:] for x in to_add[0] if x.startswith("l10n-")]
l10n_data: list[dict[str, str]] = [{"currency_code": x["code"],
"locale": y,
"name": x[f"l10n-{y}"]}
for x in to_add for y in locales]
db.session.execute(sa.insert(Currency), currency_data)
db.session.execute(sa.insert(CurrencyL10n), l10n_data)

View File

@ -0,0 +1,48 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
# 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 path converters for the currency management.
"""
from flask import abort
from werkzeug.routing import BaseConverter
from accounting import db
from accounting.models import Currency
class CurrencyConverter(BaseConverter):
"""The currency converter to convert the currency code and to the
corresponding currency in the routes."""
def to_python(self, value: str) -> Currency:
"""Converts a currency code to a currency.
:param value: The currency code.
:return: The corresponding currency.
"""
currency: Currency | None = db.session.get(Currency, value)
if currency is None:
abort(404)
return currency
def to_url(self, value: Currency) -> str:
"""Converts a currency to its code.
:param value: The currency.
:return: The code.
"""
return value.code

View File

@ -0,0 +1,83 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
# 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 forms for the currency management.
"""
from flask_wtf import FlaskForm
from wtforms import StringField, ValidationError
from wtforms.validators import DataRequired, Regexp, NoneOf
from accounting import db
from accounting.locale import lazy_gettext
from accounting.models import Currency
from accounting.utils.strip_text import strip_text
from accounting.utils.user import get_current_user_pk
class CodeUnique:
"""The validator to check if the code is unique."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
assert isinstance(form, CurrencyForm)
if field.data == "":
return
if form.obj_code is not None and form.obj_code == field.data:
return
if db.session.get(Currency, field.data) is not None:
raise ValidationError(lazy_gettext(
"Code conflicts with another currency."))
class CurrencyForm(FlaskForm):
"""The form to create or edit a currency."""
CODE_BLOCKLIST: list[str] = ["create", "store", "exists-code"]
"""The reserved codes that are not available."""
code = StringField(
filters=[strip_text],
validators=[DataRequired(lazy_gettext("Please fill in the code.")),
Regexp(r"^[A-Z]{3}$",
message=lazy_gettext(
"Code can only be composed of 3 upper-cased"
" letters.")),
NoneOf(CODE_BLOCKLIST, message=lazy_gettext(
"This code is not available.")),
CodeUnique()])
"""The code. It may not conflict with another currency."""
name = StringField(
filters=[strip_text],
validators=[DataRequired(lazy_gettext("Please fill in the name."))])
"""The name."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.obj_code: str | None = None
"""The current code of the currency, or None when adding a new
currency."""
def populate_obj(self, obj: Currency) -> None:
"""Populates the form data into a currency object.
:param obj: The currency object.
:return: None.
"""
is_new: bool = obj.code is None
obj.code = self.code.data
obj.name = self.name.data
if is_new:
current_user_pk: int = get_current_user_pk()
obj.created_by_id = current_user_pk
obj.updated_by_id = current_user_pk

View File

@ -0,0 +1,44 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
# 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 currency management.
"""
import sqlalchemy as sa
from flask import request
from accounting.models import Currency, CurrencyL10n
from accounting.utils.query import parse_query_keywords
def get_currency_query() -> list[Currency]:
"""Returns the base accounts, optionally filtered by the query.
:return: The base accounts.
"""
keywords: list[str] = parse_query_keywords(request.args.get("q"))
if len(keywords) == 0:
return Currency.query.order_by(Currency.code).all()
conditions: list[sa.BinaryExpression] = []
for k in keywords:
l10n: list[CurrencyL10n] = CurrencyL10n.query\
.filter(CurrencyL10n.name.icontains(k)).all()
l10n_matches: set[str] = {x.account_code for x in l10n}
conditions.append(sa.or_(Currency.code.icontains(k),
Currency.name_l10n.icontains(k),
Currency.code.in_(l10n_matches)))
return Currency.query.filter(*conditions)\
.order_by(Currency.code).all()

View File

@ -0,0 +1,188 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
# 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 views for the currency management.
"""
from urllib.parse import urlencode, parse_qsl
import sqlalchemy as sa
from flask import Blueprint, render_template, redirect, session, request, \
flash, url_for
from werkzeug.datastructures import ImmutableMultiDict
from accounting import db
from accounting.locale import lazy_gettext
from accounting.models import Currency
from accounting.utils.cast import s
from accounting.utils.flash_errors import flash_form_errors
from accounting.utils.next_uri import inherit_next, or_next
from accounting.utils.pagination import Pagination
from accounting.utils.permission import has_permission, can_view, can_edit
from accounting.utils.user import get_current_user_pk
from .forms import CurrencyForm
from .queries import get_currency_query
bp: Blueprint = Blueprint("currency", __name__)
"""The view blueprint for the currency management."""
api_bp: Blueprint = Blueprint("currency-api", __name__)
"""The view blueprint for the currency management API."""
@bp.get("", endpoint="list")
@has_permission(can_view)
def list_currencies() -> str:
"""Lists the currencies.
:return: The currency list.
"""
currencies: list[Currency] = get_currency_query()
pagination: Pagination = Pagination[Currency](currencies)
return render_template("accounting/currency/list.html",
list=pagination.list, pagination=pagination)
@bp.get("create", endpoint="create")
@has_permission(can_edit)
def show_add_currency_form() -> str:
"""Shows the form to add a currency.
:return: The form to add a currency.
"""
if "form" in session:
form = CurrencyForm(ImmutableMultiDict(parse_qsl(session["form"])))
del session["form"]
form.validate()
else:
form = CurrencyForm()
return render_template("accounting/currency/create.html",
form=form)
@bp.post("store", endpoint="store")
@has_permission(can_edit)
def add_currency() -> redirect:
"""Adds a currency.
:return: The redirection to the currency detail on success, or the currency
creation form on error.
"""
form = CurrencyForm(request.form)
if not form.validate():
flash_form_errors(form)
session["form"] = urlencode(list(request.form.items()))
return redirect(inherit_next(url_for("accounting.currency.create")))
currency: Currency = Currency()
form.populate_obj(currency)
db.session.add(currency)
db.session.commit()
flash(s(lazy_gettext("The currency is added successfully.")), "success")
return redirect(inherit_next(__get_detail_uri(currency)))
@bp.get("<currency:currency>", endpoint="detail")
@has_permission(can_view)
def show_currency_detail(currency: Currency) -> str:
"""Shows the currency detail.
:param currency: The currency.
:return: The detail.
"""
return render_template("accounting/currency/detail.html", obj=currency)
@bp.get("<currency:currency>/edit", endpoint="edit")
@has_permission(can_edit)
def show_currency_edit_form(currency: Currency) -> str:
"""Shows the form to edit a currency.
:param currency: The currency.
:return: The form to edit the currency.
"""
form: CurrencyForm
if "form" in session:
form = CurrencyForm(ImmutableMultiDict(parse_qsl(session["form"])))
del session["form"]
form.validate()
else:
form = CurrencyForm(obj=currency)
return render_template("accounting/currency/edit.html",
currency=currency, form=form)
@bp.post("<currency:currency>/update", endpoint="update")
@has_permission(can_edit)
def update_currency(currency: Currency) -> redirect:
"""Updates a currency.
:param currency: The currency.
:return: The redirection to the currency detail on success, or the currency
edit form on error.
"""
form = CurrencyForm(request.form)
form.obj_code = currency.code
if not form.validate():
flash_form_errors(form)
session["form"] = urlencode(list(request.form.items()))
return redirect(inherit_next(url_for("accounting.currency.edit",
currency=currency)))
with db.session.no_autoflush:
form.populate_obj(currency)
if not currency.is_modified:
flash(s(lazy_gettext("The currency was not modified.")), "success")
return redirect(inherit_next(__get_detail_uri(currency)))
currency.updated_by_id = get_current_user_pk()
currency.updated_at = sa.func.now()
db.session.commit()
flash(s(lazy_gettext("The currency is updated successfully.")), "success")
return redirect(inherit_next(__get_detail_uri(currency)))
@bp.post("<currency:currency>/delete", endpoint="delete")
@has_permission(can_edit)
def delete_currency(currency: Currency) -> redirect:
"""Deletes a currency.
:param currency: The currency.
:return: The redirection to the currency list on success, or the currency
detail on error.
"""
if not currency.can_delete:
flash(s(lazy_gettext("The currency cannot be deleted.")), "error")
return redirect(inherit_next(__get_detail_uri(currency)))
currency.delete()
db.session.commit()
flash(s(lazy_gettext("The currency is deleted successfully.")), "success")
return redirect(or_next(url_for("accounting.currency.list")))
@api_bp.get("exists-code", endpoint="exists")
@has_permission(can_edit)
def exists_code() -> dict[str, bool]:
"""Validates whether a currency code exists.
:return: Whether the currency code exists.
"""
return {"exists": db.session.get(Currency, request.args["q"]) is not None}
def __get_detail_uri(currency: Currency) -> str:
"""Returns the detail URI of a currency.
:param currency: The currency.
:return: The detail URI of the currency.
"""
return url_for("accounting.currency.detail", currency=currency)

View File

@ -0,0 +1,528 @@
code,title,l10n-zh_Hant,l10n-zh_Hans
1,assets,資產,资产
2,liabilities,負債,负债
3,owners equity,業主權益,业主权益
4,operating revenue,營業收入,营业收入
5,operating costs,營業成本,营业成本
6,operating expenses,營業費用,营业费用
7,"non-operating revenue and expenses, other income (expense)",營業外收入及費用,营业外收入及费用
8,income tax expense (or benefit),所得稅費用(或利益),所得税费用(或利益)
9,nonrecurring gain or loss,非經常營業損益,非经常营业损益
11,current assets,流動資產,流动资产
12,current assets,流動資產,流动资产
13,funds and long-term investments,基金及長期投資,基金及长期投资
14,"property , plant, and equipment",固定資產,固定资产
15,"property , plant, and equipment",固定資產,固定资产
16,depletable assets,遞耗資產,递耗资产
17,intangible assets,無形資產,无形资产
18,other assets,其他資產,其他资产
21,current liabilities,流動負債,流动负债
22,current liabilities,流動負債,流动负债
23,long-term liabilities,長期負債,长期负债
28,other liabilities,其他負債,其他负债
31,capital,資本,资本
32,additional paid-in capital,資本公積,资本公积
33,retained earnings (accumulated deficit),保留盈餘(或累積虧損),保留盈余(或累积亏损)
34,equity adjustments,權益調整,权益调整
35,treasury stock,庫藏股,库藏股
36,minority interest,少數股權,少数股权
41,sales revenue,銷貨收入,销货收入
46,service revenue,勞務收入,劳务收入
47,agency revenue,業務收入,业务收入
48,other operating revenue,其他營業收入,其他营业收入
51,cost of goods sold,銷貨成本,销货成本
56,service costs,勞務成本,劳务成本
57,agency costs,業務成本,业务成本
58,other operating costs,其他營業成本,其他营业成本
61,selling expenses,推銷費用,推销费用
62,general & administrative expenses,管理及總務費用,管理及总务费用
63,research and development expenses,研究發展費用,研究发展费用
71,non-operating revenue,營業外收入,营业外收入
72,non-operating revenue,營業外收入,营业外收入
73,non-operating revenue,營業外收入,营业外收入
74,non-operating revenue,營業外收入,营业外收入
75,non-operating expenses,營業外費用,营业外费用
76,non-operating expenses,營業外費用,营业外费用
77,non-operating expenses,營業外費用,营业外费用
78,non-operating expenses,營業外費用,营业外费用
81,income tax expense (or benefit),所得稅費用(或利益),所得税费用(或利益)
91,gain (loss) from discontinued operations,停業部門損益,停业部门损益
92,extraordinary gain or loss,非常損益,非常损益
93,cumulative effect of changes in accounting principles,會計原則變動累積影響數,会计原则变动累积影响数
94,minority interest income,少數股權淨利,少数股权净利
111,cash and cash equivalents,現金及約當現金,现金及约当现金
112,short-term investments,短期投資,短期投资
113,notes receivable,應收票據,应收票据
114,accounts receivable,應收帳款,应收帐款
118,other receivables,其他應收款,其他应收款
121,inventories,存貨,存货
122,inventories,存貨,存货
125,prepaid expenses,預付費用,预付费用
126,prepayments,預付款項,预付款项
128,other current assets,其他流動資產,其他流动资产
129,other current assets,其他流動資產,其他流动资产
131,funds,基金,基金
132,long-term investments,長期投資,长期投资
141,land,土地,土地
142,land improvements,土地改良物,土地改良物
143,buildings,房屋及建物,房屋及建物
144,machinery and equipment,機(器)具及設備,机(器)具及设备
145,machinery and equipment,機(器)具及設備,机(器)具及设备
146,machinery and equipment,機(器)具及設備,机(器)具及设备
151,leased assets,租賃資產,租赁资产
152,leasehold improvements,租賃權益改良,租赁权益改良
156,construction in progress and prepayments for equipment,未完工程及預付購置設備款,未完工程及预付购置设备款
158,"miscellaneous property, plant, and equipment",雜項固定資產,杂项固定资产
161,depletable assets,遞耗資產,递耗资产
171,trademarks,商標權,商标权
172,patents,專利權,专利权
173,franchise,特許權,特许权
174,copyright,著作權,著作权
175,computer software,電腦軟體,电脑软体
176,goodwill,商譽,商誉
177,organization costs,開辦費,开办费
178,other intangibles,其他無形資產,其他无形资产
181,deferred assets,遞延資產,递延资产
182,idle assets,閒置資產,闲置资产
184,"long-term notes , accounts and overdue receivables",長期應收票據及款項與催收帳款,长期应收票据及款项与催收帐款
185,assets leased to others,出租資產,出租资产
186,refundable deposit,存出保證金,存出保证金
188,miscellaneous assets,雜項資產,杂项资产
211,short-term borrowings (debt),短期借款,短期借款
212,short-term notes and bills payable,應付短期票券,应付短期票券
213,notes payable,應付票據,应付票据
214,accounts pay able,應付帳款,应付帐款
216,income taxes payable,應付所得稅,应付所得税
217,accrued expenses,應付費用,应付费用
218,other payables,其他應付款,其他应付款
219,other payables,其他應付款,其他应付款
226,advance receipts,預收款項,预收款项
227,long-term liabilities -current portion,一年或一營業週期內到期長期負債,一年或一营业周期内到期长期负债
228,other current liabilities,其他流動負債,其他流动负债
229,other current liabilities,其他流動負債,其他流动负债
231,corporate bonds payable,應付公司債,应付公司债
232,long-term loans payable,長期借款,长期借款
233,long-term notes and accounts payable,長期應付票據及款項,长期应付票据及款项
234,accrued liabilities for land value increment tax,估計應付土地增值稅,估计应付土地增值税
235,accrued pension liabilities,應計退休金負債,应计退休金负债
238,other long-term liabilities,其他長期負債,其他长期负债
281,deferred liabilities,遞延負債,递延负债
286,deposits received,存入保證金,存入保证金
288,miscellaneous liabilities,雜項負債,杂项负债
311,capital,資本(或股本),资本(或股本)
321,paid-in capital in excess of par,股票溢價,股票溢价
323,capital surplus from assets revaluation,資產重估增值準備,资产重估增值准备
324,capital surplus from gain on disposal of assets,處分資產溢價公積,处分资产溢价公积
325,capital surplus from business combination,合併公積,合并公积
326,donated surplus,受贈公積,受赠公积
328,other additional paid-in capital,其他資本公積,其他资本公积
331,legal reserve,法定盈餘公積,法定盈余公积
332,special reserve,特別盈餘公積,特别盈余公积
335,retained earnings-unappropriated (or accumulated deficit),未分配盈餘(或累積虧損),未分配盈余(或累积亏损)
341,unrealized loss on market value decline of long-term equity investments,長期股權投資未實現跌價損失,长期股权投资未实现跌价损失
342,cumulative translation adjustment,累積換算調整數,累积换算调整数
343,net loss not recognized as pension cost,未認列為退休金成本之淨損失,未认列为退休金成本之净损失
351,treasury stock,庫藏股,库藏股
361,minority interest,少數股權,少数股权
411,sales revenue,銷貨收入,销货收入
417,sales return,銷貨退回,销货退回
419,sales allowances,銷貨折讓,销货折让
461,service revenue,勞務收入,劳务收入
471,agency revenue,業務收入,业务收入
488,other operating revenue,其他營業收入—其他,其他营业收入—其他
511,cost of goods sold,銷貨成本,销货成本
512,purchases,進貨,进货
513,materials purchased,進料,进料
514,direct labor,直接人工,直接人工
515,manufacturing overhead,製造費用,制造费用
516,manufacturing overhead,製造費用,制造费用
517,manufacturing overhead,製造費用,制造费用
518,manufacturing overhead,製造費用,制造费用
561,service costs,勞務成本,劳务成本
571,agency costs,業務成本,业务成本
588,other operating costs-other,其他營業成本—其他,其他营业成本—其他
615,selling expenses,推銷費用,推销费用
616,selling expenses,推銷費用,推销费用
617,selling expenses,推銷費用,推销费用
618,selling expenses,推銷費用,推销费用
625,general & administrative expenses,管理及總務費用,管理及总务费用
626,general & administrative expenses,管理及總務費用,管理及总务费用
627,general & administrative expenses,管理及總務費用,管理及总务费用
628,general & administrative expenses,管理及總務費用,管理及总务费用
635,research and development expenses,研究發展費用,研究发展费用
636,research and development expenses,研究發展費用,研究发展费用
637,research and development expenses,研究發展費用,研究发展费用
638,research and development expenses,研究發展費用,研究发展费用
711,interest revenue,利息收入,利息收入
712,investment income,投資收益,投资收益
713,foreign exchange gain,兌換利益,兑换利益
714,gain on disposal of investments,處分投資收益,处分投资收益
715,gain on disposal of assets,處分資產溢價收入,处分资产溢价收入
748,other non-operating revenue,其他營業外收入,其他营业外收入
751,interest expense,利息費用,利息费用
752,investment loss,投資損失,投资损失
753,foreign exchange loss,兌換損失,兑换损失
754,loss on disposal of investments,處分投資損失,处分投资损失
755,loss on disposal of assets,處分資產損失,处分资产损失
788,other non-operating expenses,其他營業外費用,其他营业外费用
811,income tax expense (or benefit),所得稅費用(或利益),所得税费用(或利益)
911,income (loss) from operations of discontinued segments,停業部門損益—停業前營業損益,停业部门损益—停业前营业损益
912,gain (loss) from disposal of discontinued segments,停業部門損益—處分損益,停业部门损益—处分损益
921,extraordinary gain or loss,非常損益,非常损益
931,cumulative effect of changes in accounting principles,會計原則變動累積影響數,会计原则变动累积影响数
941,minority interest income,少數股權淨利,少数股权净利
1111,cash on hand,庫存現金,库存现金
1112,petty cash/revolving funds,零用金/週轉金,零用金/周转金
1113,cash in banks,銀行存款,银行存款
1116,cash in transit,在途現金,在途现金
1117,cash equivalents,約當現金,约当现金
1118,other cash and cash equivalents,其他現金及約當現金,其他现金及约当现金
1121,short-term investments stock,短期投資—股票,短期投资—股票
1122,short-term investments short-term notes and bills,短期投資—短期票券,短期投资—短期票券
1123,short-term investments government bonds,短期投資—政府債券,短期投资—政府债券
1124,short-term investments beneficiary certificates,短期投資—受益憑證,短期投资—受益凭证
1125,short-term investments corporate bonds,短期投資—公司債,短期投资—公司债
1128,short-term investments other,短期投資—其他,短期投资—其他
1129,allowance for reduction of short-term investment to market,備抵短期投資跌價損失,备抵短期投资跌价损失
1131,notes receivable,應收票據,应收票据
1132,discounted notes receivable,應收票據貼現,应收票据贴现
1137,notes receivable related parties,應收票據—關係人,应收票据—关系人
1138,other notes receivable,其他應收票據,其他应收票据
1139,allowance for uncollectible accounts notes receivable,備抵呆帳-應收票據,备抵呆帐-应收票据
1141,accounts receivable,應收帳款,应收帐款
1142,installment accounts receivable,應收分期帳款,应收分期帐款
1147,accounts receivable related parties,應收帳款—關係人,应收帐款—关系人
1149,allowance for uncollectible accounts accounts receivable,備抵呆帳-應收帳款,备抵呆帐-应收帐款
1181,forward exchange contract receivable,應收出售遠匯款,应收出售远汇款
1182,forward exchange contract receivable foreign currencies,應收遠匯款—外幣,应收远汇款—外币
1183,discount on forward ex-change contract,買賣遠匯折價,买卖远汇折价
1184,earned revenue receivable,應收收益,应收收益
1185,income tax refund receivable,應收退稅款,应收退税款
1187,other receivables related parties,其他應收款—關係人,其他应收款—关系人
1188,other receivables other,其他應收款—其他,其他应收款—其他
1189,allowance for uncollectible accounts other receivables,備抵呆帳—其他應收款,备抵呆帐—其他应收款
1211,merchandise inventory,商品存貨,商品存货
1212,consigned goods,寄銷商品,寄销商品
1213,goods in transit,在途商品,在途商品
1219,allowance for reduction of inventory to market,備抵存貨跌價損失,备抵存货跌价损失
1221,finished goods,製成品,制成品
1222,consigned finished goods,寄銷製成品,寄销制成品
1223,by-products,副產品,副产品
1224,work in process,在製品,在制品
1225,work in process outsourced,委外加工,委外加工
1226,raw materials,原料,原料
1227,supplies,物料,物料
1228,materials and supplies in transit,在途原物料,在途原物料
1229,allowance for reduction of inventory to market,備抵存貨跌價損失,备抵存货跌价损失
1251,prepaid payroll,預付薪資,预付薪资
1252,prepaid rents,預付租金,预付租金
1253,prepaid insurance,預付保險費,预付保险费
1254,office supplies,用品盤存,用品盘存
1255,prepaid income tax,預付所得稅,预付所得税
1258,other prepaid expenses,其他預付費用,其他预付费用
1261,prepayment for purchases,預付貨款,预付货款
1268,other prepayments,其他預付款項,其他预付款项
1281,VAT paid ( or input tax),進項稅額,进项税额
1282,excess VAT paid (or overpaid VAT),留抵稅額,留抵税额
1283,temporary payments,暫付款,暂付款
1284,payment on behalf of others,代付款,代付款
1285,advances to employees,員工借支,员工借支
1286,refundable deposits,存出保證金,存出保证金
1287,certificate of deposit-restricted,受限制存款,受限制存款
1291,deferred income tax assets,遞延所得稅資產,递延所得税资产
1292,deferred foreign exchange losses,遞延兌換損失,递延兑换损失
1293,owners (stockholders) current account,業主(股東)往來,业主(股东)往来
1294,current account with others,同業往來,同业往来
1298,other current assets other,其他流動資產—其他,其他流动资产—其他
1311,redemption fund (or sinking fund),償債基金,偿债基金
1312,fund for improvement and expansion,改良及擴充基金,改良及扩充基金
1313,contingency fund,意外損失準備基金,意外损失准备基金
1314,pension fund,退休基金,退休基金
1318,other funds,其他基金,其他基金
1321,long-term equity investments,長期股權投資,长期股权投资
1322,long-term bond investments,長期債券投資,长期债券投资
1323,long-term real estate in-vestments,長期不動產投資,长期不动产投资
1324,cash surrender value of life insurance,人壽保險現金解約價值,人寿保险现金解约价值
1328,other long-term investments,其他長期投資,其他长期投资
1329,allowance for excess of cost over market value of long-term investments,備抵長期投資跌價損失,备抵长期投资跌价损失
1411,land,土地,土地
1418,land revaluation increments,土地—重估增值,土地—重估增值
1421,land improvements,土地改良物,土地改良物
1428,land improvements revaluation increments,土地改良物—重估增值,土地改良物—重估增值
1429,accumulated depreciation land improvements,累積折舊—土地改良物,累积折旧—土地改良物
1431,buildings,房屋及建物,房屋及建物
1438,buildings revaluation increments,房屋及建物—重估增值,房屋及建物—重估增值
1439,accumulated depreciation buildings,累積折舊—房屋及建物,累积折旧—房屋及建物
1441,machinery,機(器)具,机(器)具
1448,machinery revaluation increments,機(器)具—重估增值,机(器)具—重估增值
1449,accumulated depreciation machinery,累積折舊—機(器)具,累积折旧—机(器)具
1511,leased assets,租賃資產,租赁资产
1519,accumulated depreciation leased assets,累積折舊—租賃資產,累积折旧—租赁资产
1521,leasehold improvements,租賃權益改良,租赁权益改良
1529,accumulated depreciation leasehold improvements,累積折舊—租賃權益改良,累积折旧—租赁权益改良
1561,construction in progress,未完工程,未完工程
1562,prepayment for equipment,預付購置設備款,预付购置设备款
1581,"miscellaneous property, plant, and equipment",雜項固定資產,杂项固定资产
1588,"miscellaneous property, plant, and equipment revaluation increments",雜項固定資產—重估增值,杂项固定资产—重估增值
1589,"accumulated depreciation miscellaneous property, plant, and equipment",累積折舊—雜項固定資產,累积折旧—杂项固定资产
1611,natural resources,天然資源,天然资源
1618,natural resources revaluation increments,天然資源—重估增值,天然资源—重估增值
1619,accumulated depletion natural resources,累積折耗—天然資源,累积折耗—天然资源
1711,trademarks,商標權,商标权
1721,patents,專利權,专利权
1731,franchise,特許權,特许权
1741,copyright,著作權,著作权
1751,computer software cost,電腦軟體,电脑软体
1761,goodwill,商譽,商誉
1771,organization costs,開辦費,开办费
1781,deferred pension costs,遞延退休金成本,递延退休金成本
1782,leasehold improvements,租賃權益改良,租赁权益改良
1788,other intangible assets other,其他無形資產—其他,其他无形资产—其他
1811,deferred bond issuance costs,債券發行成本,债券发行成本
1812,long-term prepaid rent,長期預付租金,长期预付租金
1813,long-term prepaid insurance,長期預付保險費,长期预付保险费
1814,deferred income tax assets,遞延所得稅資產,递延所得税资产
1815,prepaid pension cost,預付退休金,预付退休金
1818,other deferred assets,其他遞延資產,其他递延资产
1821,idle assets,閒置資產,闲置资产
1841,long-term notes receivable,長期應收票據,长期应收票据
1842,long-term accounts receivable,長期應收帳款,长期应收帐款
1843,overdue receivables,催收帳款,催收帐款
1847,"long-term notes, accounts and overdue receivables related parties",長期應收票據及款項與催收帳款—關係人,长期应收票据及款项与催收帐款—关系人
1848,other long-term receivables,其他長期應收款項,其他长期应收款项
1849,"allowance for uncollectible accounts long-term notes, accounts and overdue receivables",備抵呆帳—長期應收票據及款項與催收帳款,备抵呆帐—长期应收票据及款项与催收帐款
1851,assets leased to others,出租資產,出租资产
1858,assets leased to others incremental value from revaluation,出租資產—重估增值,出租资产—重估增值
1859,accumulated depreciation assets leased to others,累積折舊—出租資產,累积折旧—出租资产
1861,refundable deposits,存出保證金,存出保证金
1881,certificate of deposit restricted,受限制存款,受限制存款
1888,miscellaneous assets other,雜項資產—其他,杂项资产—其他
2111,bank overdraft,銀行透支,银行透支
2112,bank loan,銀行借款,银行借款
2114,short-term borrowings owners,短期借款—業主,短期借款—业主
2115,short-term borrowings employees,短期借款—員工,短期借款—员工
2117,short-term borrowings related parties,短期借款—關係人,短期借款—关系人
2118,short-term borrowings other,短期借款—其他,短期借款—其他
2121,commercial paper payable,應付商業本票,应付商业本票
2122,bank acceptance,銀行承兌匯票,银行承兑汇票
2128,other short-term notes and bills payable,其他應付短期票券,其他应付短期票券
2129,discount on short-term notes and bills payable,應付短期票券折價,应付短期票券折价
2131,notes payable,應付票據,应付票据
2137,notes payable related parties,應付票據—關係人,应付票据—关系人
2138,other notes payable,其他應付票據,其他应付票据
2141,accounts payable,應付帳款,应付帐款
2147,accounts payable related parties,應付帳款—關係人,应付帐款—关系人
2161,income tax payable,應付所得稅,应付所得税
2171,accrued payroll,應付薪工,应付薪工
2172,accrued rent payable,應付租金,应付租金
2173,accrued interest payable,應付利息,应付利息
2174,accrued VAT payable,應付營業稅,应付营业税
2175,accrued taxes payable other,應付稅捐—其他,应付税捐—其他
2178,other accrued expenses payable,其他應付費用,其他应付费用
2181,forward exchange contract payable,應付購入遠匯款,应付购入远汇款
2182,forward exchange contract payable foreign currencies,應付遠匯款—外幣,应付远汇款—外币
2183,premium on forward exchange contract,買賣遠匯溢價,买卖远汇溢价
2184,payables on land and building purchased,應付土地房屋款,应付土地房屋款
2185,Payables on equipment,應付設備款,应付设备款
2187,other payables related parties,其他應付款—關係人,其他应付款—关系人
2191,dividend payable,應付股利,应付股利
2192,bonus payable,應付紅利,应付红利
2193,compensation payable to directors and supervisors,應付董監事酬勞,应付董监事酬劳
2198,other payables other,其他應付款—其他,其他应付款—其他
2261,sales revenue received in advance,預收貨款,预收货款
2262,revenue received in advance,預收收入,预收收入
2268,other advance receipts,其他預收款,其他预收款
2271,corporate bonds payable current portion,一年或一營業週期內到期公司債,一年或一营业周期内到期公司债
2272,long-term loans payable current portion,一年或一營業週期內到期長期借款,一年或一营业周期内到期长期借款
2273,long-term notes and accounts payable due within one year or one operating cycle,一年或一營業週期內到期長期應付票據及款項,一年或一营业周期内到期长期应付票据及款项
2277,long-term notes and accounts payables to related parties current portion,一年或一營業週期內到期長期應付票據及款項—關係人,一年或一营业周期内到期长期应付票据及款项—关系人
2278,other long-term liabilities current portion,其他一年或一營業週期內到期長期負債,其他一年或一营业周期内到期长期负债
2281,VAT received (or output tax),銷項稅額,销项税额
2283,temporary receipts,暫收款,暂收款
2284,receipts under custody,代收款,代收款
2285,estimated warranty liabilities,估計售後服務/保固負債,估计售后服务/保固负债
2291,deferred income tax liabilities,遞延所得稅負債,递延所得税负债
2292,deferred foreign exchange gain,遞延兌換利益,递延兑换利益
2293,owners current account,業主(股東)往來,业主(股东)往来
2294,current account with others,同業往來,同业往来
2298,other current liabilities others,其他流動負債—其他,其他流动负债—其他
2311,corporate bonds payable,應付公司債,应付公司债
2319,premium (discount) on corporate bonds payable,應付公司債溢(折)價,应付公司债溢(折)价
2321,long-term loans payable bank,長期銀行借款,长期银行借款
2324,long-term loans payable owners,長期借款—業主,长期借款—业主
2325,long-term loans payable employees,長期借款—員工,长期借款—员工
2327,long-term loans payable related parties,長期借款—關係人,长期借款—关系人
2328,long-term loans payable other,長期借款—其他,长期借款—其他
2331,long-term notes payable,長期應付票據,长期应付票据
2332,long-term accounts pay-able,長期應付帳款,长期应付帐款
2333,long-term capital lease liabilities,長期應付租賃負債,长期应付租赁负债
2337,Long-term notes and accounts payable related parties,長期應付票據及款項—關係人,长期应付票据及款项—关系人
2338,other long-term payables,其他長期應付款項,其他长期应付款项
2341,estimated accrued land value incremental tax pay-able,估計應付土地增值稅,估计应付土地增值税
2351,accrued pension liabilities,應計退休金負債,应计退休金负债
2388,other long-term liabilities other,其他長期負債—其他,其他长期负债—其他
2811,deferred revenue,遞延收入,递延收入
2814,deferred income tax liabilities,遞延所得稅負債,递延所得税负债
2818,other deferred liabilities,其他遞延負債,其他递延负债
2861,guarantee deposit received,存入保證金,存入保证金
2888,miscellaneous liabilities other,雜項負債—其他,杂项负债—其他
3111,capital common stock,普通股股本,普通股股本
3112,capital preferred stock,特別股股本,特别股股本
3113,capital collected in advance,預收股本,预收股本
3114,stock dividends to be distributed,待分配股票股利,待分配股票股利
3115,capital,資本,资本
3211,paid-in capital in excess of par- common stock,普通股股票溢價,普通股股票溢价
3212,paid-in capital in excess of par- preferred stock,特別股股票溢價,特别股股票溢价
3231,capital surplus from assets revaluation,資產重估增值準備,资产重估增值准备
3241,capital surplus from gain on disposal of assets,處分資產溢價公積,处分资产溢价公积
3251,capital surplus from business combination,合併公積,合并公积
3261,donated surplus,受贈公積,受赠公积
3281,additional paid-in capital from investee under equity method,權益法長期股權投資資本公積,权益法长期股权投资资本公积
3282,additional paid-in capital treasury stock trans-actions,資本公積—庫藏股票交易,资本公积—库藏股票交易
3311,legal reserve,法定盈餘公積,法定盈余公积
3321,contingency reserve,意外損失準備,意外损失准备
3322,improvement and expansion reserve,改良擴充準備,改良扩充准备
3323,special reserve for redemption of liabilities,償債準備,偿债准备
3328,other special reserve,其他特別盈餘公積,其他特别盈余公积
3351,accumulated profit or loss,累積盈虧,累积盈亏
3352,prior period adjustments,前期損益調整,前期损益调整
3353,net income or loss for current period,本期損益,本期损益
3411,unrealized loss on market value decline of long-term equity investments,長期股權投資未實現跌價損失,长期股权投资未实现跌价损失
3421,cumulative translation adjustments,累積換算調整數,累积换算调整数
3431,net loss not recognized as pension costs,未認列為退休金成本之淨損失,未认列为退休金成本之净损失
3511,treasury stock,庫藏股,库藏股
3611,minority interest,少數股權,少数股权
4111,sales revenue,銷貨收入,销货收入
4112,installment sales revenue,分期付款銷貨收入,分期付款销货收入
4171,sales return,銷貨退回,销货退回
4191,sales discounts and allowances,銷貨折讓,销货折让
4611,service revenue,勞務收入,劳务收入
4711,agency revenue,業務收入,业务收入
4888,other operating revenue other,其他營業收入—其他,其他营业收入—其他
5111,cost of goods sold,銷貨成本,销货成本
5112,installment cost of goods sold,分期付款銷貨成本,分期付款销货成本
5121,purchases,進貨,进货
5122,purchase expenses,進貨費用,进货费用
5123,purchase returns,進貨退出,进货退出
5124,charges on purchased merchandise,進貨折讓,进货折让
5131,material purchased,進料,进料
5132,charges on purchased material,進料費用,进料费用
5133,material purchase returns,進料退出,进料退出
5134,material purchase allowances,進料折讓,进料折让
5141,direct labor,直接人工,直接人工
5151,indirect labor,間接人工,间接人工
5152,"rent expense, rent",租金支出,租金支出
5153,office supplies (expense),文具用品,文具用品
5154,"travelling expense, travel",旅費,旅费
5155,"shipping expenses, freight",運費,运费
5156,postage (expenses),郵電費,邮电费
5157,repair (s) and maintenance (expense ),修繕費,修缮费
5158,packing expenses,包裝費,包装费
5161,utilities (expense),水電瓦斯費,水电瓦斯费
5162,insurance (expense),保險費,保险费
5163,manufacturing overhead outsourced,加工費,加工费
5166,taxes,稅捐,税捐
5168,depreciation expense,折舊,折旧
5169,various amortization,各項耗竭及攤提,各项耗竭及摊提
5172,meal (expenses),伙食費,伙食费
5173,employee benefits/welfare,職工福利,职工福利
5176,training (expense),訓練費,训练费
5177,indirect materials,間接材料,间接材料
5188,other manufacturing expenses,其他製造費用,其他制造费用
5611,service costs,勞務成本,劳务成本
5711,agency costs,業務成本,业务成本
5888,other operating costs other,其他營業成本—其他,其他营业成本—其他
6151,payroll expense,薪資支出,薪资支出
6152,"rent expense, rent",租金支出,租金支出
6153,office supplies (expense),文具用品,文具用品
6154,"travelling expense, travel",旅費,旅费
6155,"shipping expenses, freight",運費,运费
6156,postage (expenses),郵電費,邮电费
6157,repair (s) and maintenance (expense),修繕費,修缮费
6159,"advertisement expense, advertisement",廣告費,广告费
6161,utilities (expense),水電瓦斯費,水电瓦斯费
6162,insurance (expense),保險費,保险费
6164,entertainment (expense),交際費,交际费
6165,donation (expense),捐贈,捐赠
6166,taxes,稅捐,税捐
6167,loss on uncollectible accounts,呆帳損失,呆帐损失
6168,depreciation expense,折舊,折旧
6169,various amortization,各項耗竭及攤提,各项耗竭及摊提
6172,meal (expenses),伙食費,伙食费
6173,employee benefits/welfare,職工福利,职工福利
6175,commission (expense),佣金支出,佣金支出
6176,training (expense),訓練費,训练费
6188,other selling expenses,其他推銷費用,其他推销费用
6251,payroll expense,薪資支出,薪资支出
6252,"rent expense, rent",租金支出,租金支出
6253,office supplies,文具用品,文具用品
6254,"travelling expense, travel",旅費,旅费
6255,"shipping expenses,freight",運費,运费
6256,postage (expenses),郵電費,邮电费
6257,repair (s) and maintenance (expense),修繕費,修缮费
6259,"advertisement expense, advertisement",廣告費,广告费
6261,utilities (expense),水電瓦斯費,水电瓦斯费
6262,insurance (expense),保險費,保险费
6264,entertainment (expense),交際費,交际费
6265,donation (expense),捐贈,捐赠
6266,taxes,稅捐,税捐
6267,loss on uncollectible accounts,呆帳損失,呆帐损失
6268,depreciation expense,折舊,折旧
6269,various amortization,各項耗竭及攤提,各项耗竭及摊提
6271,loss on export sales,外銷損失,外销损失
6272,meal (expenses),伙食費,伙食费
6273,employee benefits/welfare,職工福利,职工福利
6274,research and development expense,研究發展費用,研究发展费用
6275,commission (expense),佣金支出,佣金支出
6276,training (expense),訓練費,训练费
6278,professional service fees,勞務費,劳务费
6288,other general and administrative expenses,其他管理及總務費用,其他管理及总务费用
6351,payroll expense,薪資支出,薪资支出
6352,"rent expense, rent",租金支出,租金支出
6353,office supplies,文具用品,文具用品
6354,"travelling expense, travel",旅費,旅费
6355,"shipping expenses, freight",運費,运费
6356,postage (expenses),郵電費,邮电费
6357,repair (s) and maintenance (expense),修繕費,修缮费
6361,utilities (expense),水電瓦斯費,水电瓦斯费
6362,insurance (expense),保險費,保险费
6364,entertainment (expense),交際費,交际费
6366,taxes,稅捐,税捐
6368,depreciation expense,折舊,折旧
6369,various amortization,各項耗竭及攤提,各项耗竭及摊提
6372,meal (expenses),伙食費,伙食费
6373,employee benefits/welfare,職工福利,职工福利
6376,training (expense),訓練費,训练费
6378,other research and development expenses,其他研究發展費用,其他研究发展费用
7111,interest revenue/income,利息收入,利息收入
7121,investment income recognized under equity method,權益法認列之投資收益,权益法认列之投资收益
7122,dividends income,股利收入,股利收入
7123,gain on market price recovery of short-term investment,短期投資市價回升利益,短期投资市价回升利益
7131,foreign exchange gain,兌換利益,兑换利益
7141,gain on disposal of investments,處分投資收益,处分投资收益
7151,gain on disposal of assets,處分資產溢價收入,处分资产溢价收入
7481,donation income,捐贈收入,捐赠收入
7482,rent revenue/income,租金收入,租金收入
7483,commission revenue/income,佣金收入,佣金收入
7484,revenue from sale of scraps,出售下腳及廢料收入,出售下脚及废料收入
7485,gain on physical inventory,存貨盤盈,存货盘盈
7486,gain from price recovery of inventory,存貨跌價回升利益,存货跌价回升利益
7487,gain on reversal of bad debts,壞帳轉回利益,坏帐转回利益
7488,other non-operating revenue other items,其他營業外收入—其他,其他营业外收入—其他
7511,interest expense,利息費用,利息费用
7521,investment loss recognized under equity method,權益法認列之投資損失,权益法认列之投资损失
7523,unrealized loss on reduction of short-term investments to market,短期投資未實現跌價損失,短期投资未实现跌价损失
7531,foreign exchange loss,兌換損失,兑换损失
7541,loss on disposal of investments,處分投資損失,处分投资损失
7551,loss on disposal of assets,處分資產損失,处分资产损失
7881,loss on work stoppages,停工損失,停工损失
7882,casualty loss,災害損失,灾害损失
7885,loss on physical inventory,存貨盤損,存货盘损
7886,loss for market price decline and obsolete and slow-moving inventories,存貨跌價及呆滯損失,存货跌价及呆滞损失
7888,other non-operating expenses other,其他營業外費用—其他,其他营业外费用—其他
8111,income tax expense ( or benefit),所得稅費用(或利益),所得税费用(或利益)
9111,income (loss) from operations of discontinued segment,停業部門損益—停業前營業損益,停业部门损益—停业前营业损益
9121,gain (loss) from disposal of discontinued segment,停業部門損益—處分損益,停业部门损益—处分损益
9211,extraordinary gain or loss,非常損益,非常损益
9311,cumulative effect of changes in accounting principles,會計原則變動累積影響數,会计原则变动累积影响数
9411,minority interest income,少數股權淨利,少数股权净利
1 code title l10n-zh_Hant l10n-zh_Hans
2 1 assets 資產 资产
3 2 liabilities 負債 负债
4 3 owners’ equity 業主權益 业主权益
5 4 operating revenue 營業收入 营业收入
6 5 operating costs 營業成本 营业成本
7 6 operating expenses 營業費用 营业费用
8 7 non-operating revenue and expenses, other income (expense) 營業外收入及費用 营业外收入及费用
9 8 income tax expense (or benefit) 所得稅費用(或利益) 所得税费用(或利益)
10 9 nonrecurring gain or loss 非經常營業損益 非经常营业损益
11 11 current assets 流動資產 流动资产
12 12 current assets 流動資產 流动资产
13 13 funds and long-term investments 基金及長期投資 基金及长期投资
14 14 property , plant, and equipment 固定資產 固定资产
15 15 property , plant, and equipment 固定資產 固定资产
16 16 depletable assets 遞耗資產 递耗资产
17 17 intangible assets 無形資產 无形资产
18 18 other assets 其他資產 其他资产
19 21 current liabilities 流動負債 流动负债
20 22 current liabilities 流動負債 流动负债
21 23 long-term liabilities 長期負債 长期负债
22 28 other liabilities 其他負債 其他负债
23 31 capital 資本 资本
24 32 additional paid-in capital 資本公積 资本公积
25 33 retained earnings (accumulated deficit) 保留盈餘(或累積虧損) 保留盈余(或累积亏损)
26 34 equity adjustments 權益調整 权益调整
27 35 treasury stock 庫藏股 库藏股
28 36 minority interest 少數股權 少数股权
29 41 sales revenue 銷貨收入 销货收入
30 46 service revenue 勞務收入 劳务收入
31 47 agency revenue 業務收入 业务收入
32 48 other operating revenue 其他營業收入 其他营业收入
33 51 cost of goods sold 銷貨成本 销货成本
34 56 service costs 勞務成本 劳务成本
35 57 agency costs 業務成本 业务成本
36 58 other operating costs 其他營業成本 其他营业成本
37 61 selling expenses 推銷費用 推销费用
38 62 general & administrative expenses 管理及總務費用 管理及总务费用
39 63 research and development expenses 研究發展費用 研究发展费用
40 71 non-operating revenue 營業外收入 营业外收入
41 72 non-operating revenue 營業外收入 营业外收入
42 73 non-operating revenue 營業外收入 营业外收入
43 74 non-operating revenue 營業外收入 营业外收入
44 75 non-operating expenses 營業外費用 营业外费用
45 76 non-operating expenses 營業外費用 营业外费用
46 77 non-operating expenses 營業外費用 营业外费用
47 78 non-operating expenses 營業外費用 营业外费用
48 81 income tax expense (or benefit) 所得稅費用(或利益) 所得税费用(或利益)
49 91 gain (loss) from discontinued operations 停業部門損益 停业部门损益
50 92 extraordinary gain or loss 非常損益 非常损益
51 93 cumulative effect of changes in accounting principles 會計原則變動累積影響數 会计原则变动累积影响数
52 94 minority interest income 少數股權淨利 少数股权净利
53 111 cash and cash equivalents 現金及約當現金 现金及约当现金
54 112 short-term investments 短期投資 短期投资
55 113 notes receivable 應收票據 应收票据
56 114 accounts receivable 應收帳款 应收帐款
57 118 other receivables 其他應收款 其他应收款
58 121 inventories 存貨 存货
59 122 inventories 存貨 存货
60 125 prepaid expenses 預付費用 预付费用
61 126 prepayments 預付款項 预付款项
62 128 other current assets 其他流動資產 其他流动资产
63 129 other current assets 其他流動資產 其他流动资产
64 131 funds 基金 基金
65 132 long-term investments 長期投資 长期投资
66 141 land 土地 土地
67 142 land improvements 土地改良物 土地改良物
68 143 buildings 房屋及建物 房屋及建物
69 144 machinery and equipment 機(器)具及設備 机(器)具及设备
70 145 machinery and equipment 機(器)具及設備 机(器)具及设备
71 146 machinery and equipment 機(器)具及設備 机(器)具及设备
72 151 leased assets 租賃資產 租赁资产
73 152 leasehold improvements 租賃權益改良 租赁权益改良
74 156 construction in progress and prepayments for equipment 未完工程及預付購置設備款 未完工程及预付购置设备款
75 158 miscellaneous property, plant, and equipment 雜項固定資產 杂项固定资产
76 161 depletable assets 遞耗資產 递耗资产
77 171 trademarks 商標權 商标权
78 172 patents 專利權 专利权
79 173 franchise 特許權 特许权
80 174 copyright 著作權 著作权
81 175 computer software 電腦軟體 电脑软体
82 176 goodwill 商譽 商誉
83 177 organization costs 開辦費 开办费
84 178 other intangibles 其他無形資產 其他无形资产
85 181 deferred assets 遞延資產 递延资产
86 182 idle assets 閒置資產 闲置资产
87 184 long-term notes , accounts and overdue receivables 長期應收票據及款項與催收帳款 长期应收票据及款项与催收帐款
88 185 assets leased to others 出租資產 出租资产
89 186 refundable deposit 存出保證金 存出保证金
90 188 miscellaneous assets 雜項資產 杂项资产
91 211 short-term borrowings (debt) 短期借款 短期借款
92 212 short-term notes and bills payable 應付短期票券 应付短期票券
93 213 notes payable 應付票據 应付票据
94 214 accounts pay able 應付帳款 应付帐款
95 216 income taxes payable 應付所得稅 应付所得税
96 217 accrued expenses 應付費用 应付费用
97 218 other payables 其他應付款 其他应付款
98 219 other payables 其他應付款 其他应付款
99 226 advance receipts 預收款項 预收款项
100 227 long-term liabilities -current portion 一年或一營業週期內到期長期負債 一年或一营业周期内到期长期负债
101 228 other current liabilities 其他流動負債 其他流动负债
102 229 other current liabilities 其他流動負債 其他流动负债
103 231 corporate bonds payable 應付公司債 应付公司债
104 232 long-term loans payable 長期借款 长期借款
105 233 long-term notes and accounts payable 長期應付票據及款項 长期应付票据及款项
106 234 accrued liabilities for land value increment tax 估計應付土地增值稅 估计应付土地增值税
107 235 accrued pension liabilities 應計退休金負債 应计退休金负债
108 238 other long-term liabilities 其他長期負債 其他长期负债
109 281 deferred liabilities 遞延負債 递延负债
110 286 deposits received 存入保證金 存入保证金
111 288 miscellaneous liabilities 雜項負債 杂项负债
112 311 capital 資本(或股本) 资本(或股本)
113 321 paid-in capital in excess of par 股票溢價 股票溢价
114 323 capital surplus from assets revaluation 資產重估增值準備 资产重估增值准备
115 324 capital surplus from gain on disposal of assets 處分資產溢價公積 处分资产溢价公积
116 325 capital surplus from business combination 合併公積 合并公积
117 326 donated surplus 受贈公積 受赠公积
118 328 other additional paid-in capital 其他資本公積 其他资本公积
119 331 legal reserve 法定盈餘公積 法定盈余公积
120 332 special reserve 特別盈餘公積 特别盈余公积
121 335 retained earnings-unappropriated (or accumulated deficit) 未分配盈餘(或累積虧損) 未分配盈余(或累积亏损)
122 341 unrealized loss on market value decline of long-term equity investments 長期股權投資未實現跌價損失 长期股权投资未实现跌价损失
123 342 cumulative translation adjustment 累積換算調整數 累积换算调整数
124 343 net loss not recognized as pension cost 未認列為退休金成本之淨損失 未认列为退休金成本之净损失
125 351 treasury stock 庫藏股 库藏股
126 361 minority interest 少數股權 少数股权
127 411 sales revenue 銷貨收入 销货收入
128 417 sales return 銷貨退回 销货退回
129 419 sales allowances 銷貨折讓 销货折让
130 461 service revenue 勞務收入 劳务收入
131 471 agency revenue 業務收入 业务收入
132 488 other operating revenue 其他營業收入—其他 其他营业收入—其他
133 511 cost of goods sold 銷貨成本 销货成本
134 512 purchases 進貨 进货
135 513 materials purchased 進料 进料
136 514 direct labor 直接人工 直接人工
137 515 manufacturing overhead 製造費用 制造费用
138 516 manufacturing overhead 製造費用 制造费用
139 517 manufacturing overhead 製造費用 制造费用
140 518 manufacturing overhead 製造費用 制造费用
141 561 service costs 勞務成本 劳务成本
142 571 agency costs 業務成本 业务成本
143 588 other operating costs-other 其他營業成本—其他 其他营业成本—其他
144 615 selling expenses 推銷費用 推销费用
145 616 selling expenses 推銷費用 推销费用
146 617 selling expenses 推銷費用 推销费用
147 618 selling expenses 推銷費用 推销费用
148 625 general & administrative expenses 管理及總務費用 管理及总务费用
149 626 general & administrative expenses 管理及總務費用 管理及总务费用
150 627 general & administrative expenses 管理及總務費用 管理及总务费用
151 628 general & administrative expenses 管理及總務費用 管理及总务费用
152 635 research and development expenses 研究發展費用 研究发展费用
153 636 research and development expenses 研究發展費用 研究发展费用
154 637 research and development expenses 研究發展費用 研究发展费用
155 638 research and development expenses 研究發展費用 研究发展费用
156 711 interest revenue 利息收入 利息收入
157 712 investment income 投資收益 投资收益
158 713 foreign exchange gain 兌換利益 兑换利益
159 714 gain on disposal of investments 處分投資收益 处分投资收益
160 715 gain on disposal of assets 處分資產溢價收入 处分资产溢价收入
161 748 other non-operating revenue 其他營業外收入 其他营业外收入
162 751 interest expense 利息費用 利息费用
163 752 investment loss 投資損失 投资损失
164 753 foreign exchange loss 兌換損失 兑换损失
165 754 loss on disposal of investments 處分投資損失 处分投资损失
166 755 loss on disposal of assets 處分資產損失 处分资产损失
167 788 other non-operating expenses 其他營業外費用 其他营业外费用
168 811 income tax expense (or benefit) 所得稅費用(或利益) 所得税费用(或利益)
169 911 income (loss) from operations of discontinued segments 停業部門損益—停業前營業損益 停业部门损益—停业前营业损益
170 912 gain (loss) from disposal of discontinued segments 停業部門損益—處分損益 停业部门损益—处分损益
171 921 extraordinary gain or loss 非常損益 非常损益
172 931 cumulative effect of changes in accounting principles 會計原則變動累積影響數 会计原则变动累积影响数
173 941 minority interest income 少數股權淨利 少数股权净利
174 1111 cash on hand 庫存現金 库存现金
175 1112 petty cash/revolving funds 零用金/週轉金 零用金/周转金
176 1113 cash in banks 銀行存款 银行存款
177 1116 cash in transit 在途現金 在途现金
178 1117 cash equivalents 約當現金 约当现金
179 1118 other cash and cash equivalents 其他現金及約當現金 其他现金及约当现金
180 1121 short-term investments – stock 短期投資—股票 短期投资—股票
181 1122 short-term investments – short-term notes and bills 短期投資—短期票券 短期投资—短期票券
182 1123 short-term investments – government bonds 短期投資—政府債券 短期投资—政府债券
183 1124 short-term investments – beneficiary certificates 短期投資—受益憑證 短期投资—受益凭证
184 1125 short-term investments – corporate bonds 短期投資—公司債 短期投资—公司债
185 1128 short-term investments – other 短期投資—其他 短期投资—其他
186 1129 allowance for reduction of short-term investment to market 備抵短期投資跌價損失 备抵短期投资跌价损失
187 1131 notes receivable 應收票據 应收票据
188 1132 discounted notes receivable 應收票據貼現 应收票据贴现
189 1137 notes receivable – related parties 應收票據—關係人 应收票据—关系人
190 1138 other notes receivable 其他應收票據 其他应收票据
191 1139 allowance for uncollectible accounts – notes receivable 備抵呆帳-應收票據 备抵呆帐-应收票据
192 1141 accounts receivable 應收帳款 应收帐款
193 1142 installment accounts receivable 應收分期帳款 应收分期帐款
194 1147 accounts receivable – related parties 應收帳款—關係人 应收帐款—关系人
195 1149 allowance for uncollectible accounts – accounts receivable 備抵呆帳-應收帳款 备抵呆帐-应收帐款
196 1181 forward exchange contract receivable 應收出售遠匯款 应收出售远汇款
197 1182 forward exchange contract receivable – foreign currencies 應收遠匯款—外幣 应收远汇款—外币
198 1183 discount on forward ex-change contract 買賣遠匯折價 买卖远汇折价
199 1184 earned revenue receivable 應收收益 应收收益
200 1185 income tax refund receivable 應收退稅款 应收退税款
201 1187 other receivables – related parties 其他應收款—關係人 其他应收款—关系人
202 1188 other receivables – other 其他應收款—其他 其他应收款—其他
203 1189 allowance for uncollectible accounts – other receivables 備抵呆帳—其他應收款 备抵呆帐—其他应收款
204 1211 merchandise inventory 商品存貨 商品存货
205 1212 consigned goods 寄銷商品 寄销商品
206 1213 goods in transit 在途商品 在途商品
207 1219 allowance for reduction of inventory to market 備抵存貨跌價損失 备抵存货跌价损失
208 1221 finished goods 製成品 制成品
209 1222 consigned finished goods 寄銷製成品 寄销制成品
210 1223 by-products 副產品 副产品
211 1224 work in process 在製品 在制品
212 1225 work in process – outsourced 委外加工 委外加工
213 1226 raw materials 原料 原料
214 1227 supplies 物料 物料
215 1228 materials and supplies in transit 在途原物料 在途原物料
216 1229 allowance for reduction of inventory to market 備抵存貨跌價損失 备抵存货跌价损失
217 1251 prepaid payroll 預付薪資 预付薪资
218 1252 prepaid rents 預付租金 预付租金
219 1253 prepaid insurance 預付保險費 预付保险费
220 1254 office supplies 用品盤存 用品盘存
221 1255 prepaid income tax 預付所得稅 预付所得税
222 1258 other prepaid expenses 其他預付費用 其他预付费用
223 1261 prepayment for purchases 預付貨款 预付货款
224 1268 other prepayments 其他預付款項 其他预付款项
225 1281 VAT paid ( or input tax) 進項稅額 进项税额
226 1282 excess VAT paid (or overpaid VAT) 留抵稅額 留抵税额
227 1283 temporary payments 暫付款 暂付款
228 1284 payment on behalf of others 代付款 代付款
229 1285 advances to employees 員工借支 员工借支
230 1286 refundable deposits 存出保證金 存出保证金
231 1287 certificate of deposit-restricted 受限制存款 受限制存款
232 1291 deferred income tax assets 遞延所得稅資產 递延所得税资产
233 1292 deferred foreign exchange losses 遞延兌換損失 递延兑换损失
234 1293 owners’ (stockholders’) current account 業主(股東)往來 业主(股东)往来
235 1294 current account with others 同業往來 同业往来
236 1298 other current assets – other 其他流動資產—其他 其他流动资产—其他
237 1311 redemption fund (or sinking fund) 償債基金 偿债基金
238 1312 fund for improvement and expansion 改良及擴充基金 改良及扩充基金
239 1313 contingency fund 意外損失準備基金 意外损失准备基金
240 1314 pension fund 退休基金 退休基金
241 1318 other funds 其他基金 其他基金
242 1321 long-term equity investments 長期股權投資 长期股权投资
243 1322 long-term bond investments 長期債券投資 长期债券投资
244 1323 long-term real estate in-vestments 長期不動產投資 长期不动产投资
245 1324 cash surrender value of life insurance 人壽保險現金解約價值 人寿保险现金解约价值
246 1328 other long-term investments 其他長期投資 其他长期投资
247 1329 allowance for excess of cost over market value of long-term investments 備抵長期投資跌價損失 备抵长期投资跌价损失
248 1411 land 土地 土地
249 1418 land – revaluation increments 土地—重估增值 土地—重估增值
250 1421 land improvements 土地改良物 土地改良物
251 1428 land improvements – revaluation increments 土地改良物—重估增值 土地改良物—重估增值
252 1429 accumulated depreciation – land improvements 累積折舊—土地改良物 累积折旧—土地改良物
253 1431 buildings 房屋及建物 房屋及建物
254 1438 buildings –revaluation increments 房屋及建物—重估增值 房屋及建物—重估增值
255 1439 accumulated depreciation – buildings 累積折舊—房屋及建物 累积折旧—房屋及建物
256 1441 machinery 機(器)具 机(器)具
257 1448 machinery – revaluation increments 機(器)具—重估增值 机(器)具—重估增值
258 1449 accumulated depreciation – machinery 累積折舊—機(器)具 累积折旧—机(器)具
259 1511 leased assets 租賃資產 租赁资产
260 1519 accumulated depreciation – leased assets 累積折舊—租賃資產 累积折旧—租赁资产
261 1521 leasehold improvements 租賃權益改良 租赁权益改良
262 1529 accumulated depreciation – leasehold improvements 累積折舊—租賃權益改良 累积折旧—租赁权益改良
263 1561 construction in progress 未完工程 未完工程
264 1562 prepayment for equipment 預付購置設備款 预付购置设备款
265 1581 miscellaneous property, plant, and equipment 雜項固定資產 杂项固定资产
266 1588 miscellaneous property, plant, and equipment – revaluation increments 雜項固定資產—重估增值 杂项固定资产—重估增值
267 1589 accumulated depreciation – miscellaneous property, plant, and equipment 累積折舊—雜項固定資產 累积折旧—杂项固定资产
268 1611 natural resources 天然資源 天然资源
269 1618 natural resources –revaluation increments 天然資源—重估增值 天然资源—重估增值
270 1619 accumulated depletion – natural resources 累積折耗—天然資源 累积折耗—天然资源
271 1711 trademarks 商標權 商标权
272 1721 patents 專利權 专利权
273 1731 franchise 特許權 特许权
274 1741 copyright 著作權 著作权
275 1751 computer software cost 電腦軟體 电脑软体
276 1761 goodwill 商譽 商誉
277 1771 organization costs 開辦費 开办费
278 1781 deferred pension costs 遞延退休金成本 递延退休金成本
279 1782 leasehold improvements 租賃權益改良 租赁权益改良
280 1788 other intangible assets – other 其他無形資產—其他 其他无形资产—其他
281 1811 deferred bond issuance costs 債券發行成本 债券发行成本
282 1812 long-term prepaid rent 長期預付租金 长期预付租金
283 1813 long-term prepaid insurance 長期預付保險費 长期预付保险费
284 1814 deferred income tax assets 遞延所得稅資產 递延所得税资产
285 1815 prepaid pension cost 預付退休金 预付退休金
286 1818 other deferred assets 其他遞延資產 其他递延资产
287 1821 idle assets 閒置資產 闲置资产
288 1841 long-term notes receivable 長期應收票據 长期应收票据
289 1842 long-term accounts receivable 長期應收帳款 长期应收帐款
290 1843 overdue receivables 催收帳款 催收帐款
291 1847 long-term notes, accounts and overdue receivables – related parties 長期應收票據及款項與催收帳款—關係人 长期应收票据及款项与催收帐款—关系人
292 1848 other long-term receivables 其他長期應收款項 其他长期应收款项
293 1849 allowance for uncollectible accounts – long-term notes, accounts and overdue receivables 備抵呆帳—長期應收票據及款項與催收帳款 备抵呆帐—长期应收票据及款项与催收帐款
294 1851 assets leased to others 出租資產 出租资产
295 1858 assets leased to others – incremental value from revaluation 出租資產—重估增值 出租资产—重估增值
296 1859 accumulated depreciation – assets leased to others 累積折舊—出租資產 累积折旧—出租资产
297 1861 refundable deposits 存出保證金 存出保证金
298 1881 certificate of deposit – restricted 受限制存款 受限制存款
299 1888 miscellaneous assets – other 雜項資產—其他 杂项资产—其他
300 2111 bank overdraft 銀行透支 银行透支
301 2112 bank loan 銀行借款 银行借款
302 2114 short-term borrowings – owners 短期借款—業主 短期借款—业主
303 2115 short-term borrowings – employees 短期借款—員工 短期借款—员工
304 2117 short-term borrowings – related parties 短期借款—關係人 短期借款—关系人
305 2118 short-term borrowings – other 短期借款—其他 短期借款—其他
306 2121 commercial paper payable 應付商業本票 应付商业本票
307 2122 bank acceptance 銀行承兌匯票 银行承兑汇票
308 2128 other short-term notes and bills payable 其他應付短期票券 其他应付短期票券
309 2129 discount on short-term notes and bills payable 應付短期票券折價 应付短期票券折价
310 2131 notes payable 應付票據 应付票据
311 2137 notes payable – related parties 應付票據—關係人 应付票据—关系人
312 2138 other notes payable 其他應付票據 其他应付票据
313 2141 accounts payable 應付帳款 应付帐款
314 2147 accounts payable – related parties 應付帳款—關係人 应付帐款—关系人
315 2161 income tax payable 應付所得稅 应付所得税
316 2171 accrued payroll 應付薪工 应付薪工
317 2172 accrued rent payable 應付租金 应付租金
318 2173 accrued interest payable 應付利息 应付利息
319 2174 accrued VAT payable 應付營業稅 应付营业税
320 2175 accrued taxes payable – other 應付稅捐—其他 应付税捐—其他
321 2178 other accrued expenses payable 其他應付費用 其他应付费用
322 2181 forward exchange contract payable 應付購入遠匯款 应付购入远汇款
323 2182 forward exchange contract payable – foreign currencies 應付遠匯款—外幣 应付远汇款—外币
324 2183 premium on forward exchange contract 買賣遠匯溢價 买卖远汇溢价
325 2184 payables on land and building purchased 應付土地房屋款 应付土地房屋款
326 2185 Payables on equipment 應付設備款 应付设备款
327 2187 other payables – related parties 其他應付款—關係人 其他应付款—关系人
328 2191 dividend payable 應付股利 应付股利
329 2192 bonus payable 應付紅利 应付红利
330 2193 compensation payable to directors and supervisors 應付董監事酬勞 应付董监事酬劳
331 2198 other payables – other 其他應付款—其他 其他应付款—其他
332 2261 sales revenue received in advance 預收貨款 预收货款
333 2262 revenue received in advance 預收收入 预收收入
334 2268 other advance receipts 其他預收款 其他预收款
335 2271 corporate bonds payable – current portion 一年或一營業週期內到期公司債 一年或一营业周期内到期公司债
336 2272 long-term loans payable – current portion 一年或一營業週期內到期長期借款 一年或一营业周期内到期长期借款
337 2273 long-term notes and accounts payable due within one year or one operating cycle 一年或一營業週期內到期長期應付票據及款項 一年或一营业周期内到期长期应付票据及款项
338 2277 long-term notes and accounts payables to related parties – current portion 一年或一營業週期內到期長期應付票據及款項—關係人 一年或一营业周期内到期长期应付票据及款项—关系人
339 2278 other long-term liabilities – current portion 其他一年或一營業週期內到期長期負債 其他一年或一营业周期内到期长期负债
340 2281 VAT received (or output tax) 銷項稅額 销项税额
341 2283 temporary receipts 暫收款 暂收款
342 2284 receipts under custody 代收款 代收款
343 2285 estimated warranty liabilities 估計售後服務/保固負債 估计售后服务/保固负债
344 2291 deferred income tax liabilities 遞延所得稅負債 递延所得税负债
345 2292 deferred foreign exchange gain 遞延兌換利益 递延兑换利益
346 2293 owners’ current account 業主(股東)往來 业主(股东)往来
347 2294 current account with others 同業往來 同业往来
348 2298 other current liabilities – others 其他流動負債—其他 其他流动负债—其他
349 2311 corporate bonds payable 應付公司債 应付公司债
350 2319 premium (discount) on corporate bonds payable 應付公司債溢(折)價 应付公司债溢(折)价
351 2321 long-term loans payable – bank 長期銀行借款 长期银行借款
352 2324 long-term loans payable – owners 長期借款—業主 长期借款—业主
353 2325 long-term loans payable – employees 長期借款—員工 长期借款—员工
354 2327 long-term loans payable – related parties 長期借款—關係人 长期借款—关系人
355 2328 long-term loans payable – other 長期借款—其他 长期借款—其他
356 2331 long-term notes payable 長期應付票據 长期应付票据
357 2332 long-term accounts pay-able 長期應付帳款 长期应付帐款
358 2333 long-term capital lease liabilities 長期應付租賃負債 长期应付租赁负债
359 2337 Long-term notes and accounts payable – related parties 長期應付票據及款項—關係人 长期应付票据及款项—关系人
360 2338 other long-term payables 其他長期應付款項 其他长期应付款项
361 2341 estimated accrued land value incremental tax pay-able 估計應付土地增值稅 估计应付土地增值税
362 2351 accrued pension liabilities 應計退休金負債 应计退休金负债
363 2388 other long-term liabilities – other 其他長期負債—其他 其他长期负债—其他
364 2811 deferred revenue 遞延收入 递延收入
365 2814 deferred income tax liabilities 遞延所得稅負債 递延所得税负债
366 2818 other deferred liabilities 其他遞延負債 其他递延负债
367 2861 guarantee deposit received 存入保證金 存入保证金
368 2888 miscellaneous liabilities – other 雜項負債—其他 杂项负债—其他
369 3111 capital – common stock 普通股股本 普通股股本
370 3112 capital – preferred stock 特別股股本 特别股股本
371 3113 capital collected in advance 預收股本 预收股本
372 3114 stock dividends to be distributed 待分配股票股利 待分配股票股利
373 3115 capital 資本 资本
374 3211 paid-in capital in excess of par- common stock 普通股股票溢價 普通股股票溢价
375 3212 paid-in capital in excess of par- preferred stock 特別股股票溢價 特别股股票溢价
376 3231 capital surplus from assets revaluation 資產重估增值準備 资产重估增值准备
377 3241 capital surplus from gain on disposal of assets 處分資產溢價公積 处分资产溢价公积
378 3251 capital surplus from business combination 合併公積 合并公积
379 3261 donated surplus 受贈公積 受赠公积
380 3281 additional paid-in capital from investee under equity method 權益法長期股權投資資本公積 权益法长期股权投资资本公积
381 3282 additional paid-in capital – treasury stock trans-actions 資本公積—庫藏股票交易 资本公积—库藏股票交易
382 3311 legal reserve 法定盈餘公積 法定盈余公积
383 3321 contingency reserve 意外損失準備 意外损失准备
384 3322 improvement and expansion reserve 改良擴充準備 改良扩充准备
385 3323 special reserve for redemption of liabilities 償債準備 偿债准备
386 3328 other special reserve 其他特別盈餘公積 其他特别盈余公积
387 3351 accumulated profit or loss 累積盈虧 累积盈亏
388 3352 prior period adjustments 前期損益調整 前期损益调整
389 3353 net income or loss for current period 本期損益 本期损益
390 3411 unrealized loss on market value decline of long-term equity investments 長期股權投資未實現跌價損失 长期股权投资未实现跌价损失
391 3421 cumulative translation adjustments 累積換算調整數 累积换算调整数
392 3431 net loss not recognized as pension costs 未認列為退休金成本之淨損失 未认列为退休金成本之净损失
393 3511 treasury stock 庫藏股 库藏股
394 3611 minority interest 少數股權 少数股权
395 4111 sales revenue 銷貨收入 销货收入
396 4112 installment sales revenue 分期付款銷貨收入 分期付款销货收入
397 4171 sales return 銷貨退回 销货退回
398 4191 sales discounts and allowances 銷貨折讓 销货折让
399 4611 service revenue 勞務收入 劳务收入
400 4711 agency revenue 業務收入 业务收入
401 4888 other operating revenue – other 其他營業收入—其他 其他营业收入—其他
402 5111 cost of goods sold 銷貨成本 销货成本
403 5112 installment cost of goods sold 分期付款銷貨成本 分期付款销货成本
404 5121 purchases 進貨 进货
405 5122 purchase expenses 進貨費用 进货费用
406 5123 purchase returns 進貨退出 进货退出
407 5124 charges on purchased merchandise 進貨折讓 进货折让
408 5131 material purchased 進料 进料
409 5132 charges on purchased material 進料費用 进料费用
410 5133 material purchase returns 進料退出 进料退出
411 5134 material purchase allowances 進料折讓 进料折让
412 5141 direct labor 直接人工 直接人工
413 5151 indirect labor 間接人工 间接人工
414 5152 rent expense, rent 租金支出 租金支出
415 5153 office supplies (expense) 文具用品 文具用品
416 5154 travelling expense, travel 旅費 旅费
417 5155 shipping expenses, freight 運費 运费
418 5156 postage (expenses) 郵電費 邮电费
419 5157 repair (s) and maintenance (expense ) 修繕費 修缮费
420 5158 packing expenses 包裝費 包装费
421 5161 utilities (expense) 水電瓦斯費 水电瓦斯费
422 5162 insurance (expense) 保險費 保险费
423 5163 manufacturing overhead – outsourced 加工費 加工费
424 5166 taxes 稅捐 税捐
425 5168 depreciation expense 折舊 折旧
426 5169 various amortization 各項耗竭及攤提 各项耗竭及摊提
427 5172 meal (expenses) 伙食費 伙食费
428 5173 employee benefits/welfare 職工福利 职工福利
429 5176 training (expense) 訓練費 训练费
430 5177 indirect materials 間接材料 间接材料
431 5188 other manufacturing expenses 其他製造費用 其他制造费用
432 5611 service costs 勞務成本 劳务成本
433 5711 agency costs 業務成本 业务成本
434 5888 other operating costs – other 其他營業成本—其他 其他营业成本—其他
435 6151 payroll expense 薪資支出 薪资支出
436 6152 rent expense, rent 租金支出 租金支出
437 6153 office supplies (expense) 文具用品 文具用品
438 6154 travelling expense, travel 旅費 旅费
439 6155 shipping expenses, freight 運費 运费
440 6156 postage (expenses) 郵電費 邮电费
441 6157 repair (s) and maintenance (expense) 修繕費 修缮费
442 6159 advertisement expense, advertisement 廣告費 广告费
443 6161 utilities (expense) 水電瓦斯費 水电瓦斯费
444 6162 insurance (expense) 保險費 保险费
445 6164 entertainment (expense) 交際費 交际费
446 6165 donation (expense) 捐贈 捐赠
447 6166 taxes 稅捐 税捐
448 6167 loss on uncollectible accounts 呆帳損失 呆帐损失
449 6168 depreciation expense 折舊 折旧
450 6169 various amortization 各項耗竭及攤提 各项耗竭及摊提
451 6172 meal (expenses) 伙食費 伙食费
452 6173 employee benefits/welfare 職工福利 职工福利
453 6175 commission (expense) 佣金支出 佣金支出
454 6176 training (expense) 訓練費 训练费
455 6188 other selling expenses 其他推銷費用 其他推销费用
456 6251 payroll expense 薪資支出 薪资支出
457 6252 rent expense, rent 租金支出 租金支出
458 6253 office supplies 文具用品 文具用品
459 6254 travelling expense, travel 旅費 旅费
460 6255 shipping expenses,freight 運費 运费
461 6256 postage (expenses) 郵電費 邮电费
462 6257 repair (s) and maintenance (expense) 修繕費 修缮费
463 6259 advertisement expense, advertisement 廣告費 广告费
464 6261 utilities (expense) 水電瓦斯費 水电瓦斯费
465 6262 insurance (expense) 保險費 保险费
466 6264 entertainment (expense) 交際費 交际费
467 6265 donation (expense) 捐贈 捐赠
468 6266 taxes 稅捐 税捐
469 6267 loss on uncollectible accounts 呆帳損失 呆帐损失
470 6268 depreciation expense 折舊 折旧
471 6269 various amortization 各項耗竭及攤提 各项耗竭及摊提
472 6271 loss on export sales 外銷損失 外销损失
473 6272 meal (expenses) 伙食費 伙食费
474 6273 employee benefits/welfare 職工福利 职工福利
475 6274 research and development expense 研究發展費用 研究发展费用
476 6275 commission (expense) 佣金支出 佣金支出
477 6276 training (expense) 訓練費 训练费
478 6278 professional service fees 勞務費 劳务费
479 6288 other general and administrative expenses 其他管理及總務費用 其他管理及总务费用
480 6351 payroll expense 薪資支出 薪资支出
481 6352 rent expense, rent 租金支出 租金支出
482 6353 office supplies 文具用品 文具用品
483 6354 travelling expense, travel 旅費 旅费
484 6355 shipping expenses, freight 運費 运费
485 6356 postage (expenses) 郵電費 邮电费
486 6357 repair (s) and maintenance (expense) 修繕費 修缮费
487 6361 utilities (expense) 水電瓦斯費 水电瓦斯费
488 6362 insurance (expense) 保險費 保险费
489 6364 entertainment (expense) 交際費 交际费
490 6366 taxes 稅捐 税捐
491 6368 depreciation expense 折舊 折旧
492 6369 various amortization 各項耗竭及攤提 各项耗竭及摊提
493 6372 meal (expenses) 伙食費 伙食费
494 6373 employee benefits/welfare 職工福利 职工福利
495 6376 training (expense) 訓練費 训练费
496 6378 other research and development expenses 其他研究發展費用 其他研究发展费用
497 7111 interest revenue/income 利息收入 利息收入
498 7121 investment income recognized under equity method 權益法認列之投資收益 权益法认列之投资收益
499 7122 dividends income 股利收入 股利收入
500 7123 gain on market price recovery of short-term investment 短期投資市價回升利益 短期投资市价回升利益
501 7131 foreign exchange gain 兌換利益 兑换利益
502 7141 gain on disposal of investments 處分投資收益 处分投资收益
503 7151 gain on disposal of assets 處分資產溢價收入 处分资产溢价收入
504 7481 donation income 捐贈收入 捐赠收入
505 7482 rent revenue/income 租金收入 租金收入
506 7483 commission revenue/income 佣金收入 佣金收入
507 7484 revenue from sale of scraps 出售下腳及廢料收入 出售下脚及废料收入
508 7485 gain on physical inventory 存貨盤盈 存货盘盈
509 7486 gain from price recovery of inventory 存貨跌價回升利益 存货跌价回升利益
510 7487 gain on reversal of bad debts 壞帳轉回利益 坏帐转回利益
511 7488 other non-operating revenue – other items 其他營業外收入—其他 其他营业外收入—其他
512 7511 interest expense 利息費用 利息费用
513 7521 investment loss recognized under equity method 權益法認列之投資損失 权益法认列之投资损失
514 7523 unrealized loss on reduction of short-term investments to market 短期投資未實現跌價損失 短期投资未实现跌价损失
515 7531 foreign exchange loss 兌換損失 兑换损失
516 7541 loss on disposal of investments 處分投資損失 处分投资损失
517 7551 loss on disposal of assets 處分資產損失 处分资产损失
518 7881 loss on work stoppages 停工損失 停工损失
519 7882 casualty loss 災害損失 灾害损失
520 7885 loss on physical inventory 存貨盤損 存货盘损
521 7886 loss for market price decline and obsolete and slow-moving inventories 存貨跌價及呆滯損失 存货跌价及呆滞损失
522 7888 other non-operating expenses – other 其他營業外費用—其他 其他营业外费用—其他
523 8111 income tax expense ( or benefit) 所得稅費用(或利益) 所得税费用(或利益)
524 9111 income (loss) from operations of discontinued segment 停業部門損益—停業前營業損益 停业部门损益—停业前营业损益
525 9121 gain (loss) from disposal of discontinued segment 停業部門損益—處分損益 停业部门损益—处分损益
526 9211 extraordinary gain or loss 非常損益 非常损益
527 9311 cumulative effect of changes in accounting principles 會計原則變動累積影響數 会计原则变动累积影响数
528 9411 minority interest income 少數股權淨利 少数股权净利

View File

@ -0,0 +1,10 @@
code,name,l10n-zh_Hant,l10n-zh_Hans
TWD,New Taiwan dollar,新臺幣,新台币
USD,United States dollar,美元,美元
JPY,Japanese yen,日圓,日圆
CNY,Renminbi,人民幣,人民币
HKD,Hong Kong dollar,港元,港元
EUR,Euro,歐元,欧元
MOP,Macanese pataca,澳門元,澳门元
AUD,Australian dollar,澳洲元,澳大利亚元
ETH,Ethereum,以太坊,以太坊
1 code name l10n-zh_Hant l10n-zh_Hans
2 TWD New Taiwan dollar 新臺幣 新台币
3 USD United States dollar 美元 美元
4 JPY Japanese yen 日圓 日圆
5 CNY Renminbi 人民幣 人民币
6 HKD Hong Kong dollar 港元 港元
7 EUR Euro 歐元 欧元
8 MOP Macanese pataca 澳門元 澳门元
9 AUD Australian dollar 澳洲元 澳大利亚元
10 ETH Ethereum 以太坊 以太坊

96
src/accounting/forms.py Normal file
View File

@ -0,0 +1,96 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
# 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 forms.
"""
import re
from flask_babel import LazyString
from flask_wtf import FlaskForm
from wtforms import StringField, ValidationError
from wtforms.validators import DataRequired
from accounting import db
from accounting.locale import lazy_gettext
from accounting.models import Currency, Account
ACCOUNT_REQUIRED: DataRequired = DataRequired(
lazy_gettext("Please select the account."))
"""The validator to check if the account code is empty."""
class CurrencyExists:
"""The validator to check if the account exists."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
if field.data is None:
return
if db.session.get(Currency, field.data) is None:
raise ValidationError(lazy_gettext(
"The currency does not exist."))
class AccountExists:
"""The validator to check if the account exists."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
if field.data is None:
return
if Account.find_by_code(field.data) is None:
raise ValidationError(lazy_gettext(
"The account does not exist."))
class IsDebitAccount:
"""The validator to check if the account is for debit line items."""
def __init__(self, message: str | LazyString):
"""Constructs the validator.
:param message: The error message.
"""
self.__message: str | LazyString = message
"""The error message."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
if field.data is None:
return
if re.match(r"^(?:[1235689]|7[5678])", field.data) \
and not field.data.startswith("3353-"):
return
raise ValidationError(self.__message)
class IsCreditAccount:
"""The validator to check if the account is for credit line items."""
def __init__(self, message: str | LazyString):
"""Constructs the validator.
:param message: The error message.
"""
self.__message: str | LazyString = message
"""The error message."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
if field.data is None:
return
if re.match(r"^(?:[123489]|7[1234])", field.data) \
and not field.data.startswith("3353-"):
return
raise ValidationError(self.__message)

View File

@ -0,0 +1,37 @@
# The Mia! Accounting 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 journal entry management.
"""
from flask import Flask, Blueprint
def init_app(app: Flask, bp: Blueprint) -> None:
"""Initialize the application.
:param app: The Flask application.
:param bp: The blueprint of the accounting application.
:return: None.
"""
from .converters import JournalEntryConverter, JournalEntryTypeConverter, \
DateConverter
app.url_map.converters["journalEntry"] = JournalEntryConverter
app.url_map.converters["journalEntryType"] = JournalEntryTypeConverter
app.url_map.converters["date"] = DateConverter
from .views import bp as journal_entry_bp
bp.register_blueprint(journal_entry_bp, url_prefix="/journal-entries")

View File

@ -0,0 +1,102 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
# 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 path converters for the journal entry management.
"""
import datetime as dt
from flask import abort
from werkzeug.routing import BaseConverter
from accounting import db
from accounting.models import JournalEntry
from accounting.utils.journal_entry_types import JournalEntryType
class JournalEntryConverter(BaseConverter):
"""The journal entry converter to convert the journal entry ID from and to
the corresponding journal entry in the routes."""
def to_python(self, value: str) -> JournalEntry:
"""Converts a journal entry ID to a journal entry.
:param value: The journal entry ID.
:return: The corresponding journal entry.
"""
journal_entry: JournalEntry | None \
= db.session.get(JournalEntry, value)
if journal_entry is None:
abort(404)
return journal_entry
def to_url(self, value: JournalEntry) -> str:
"""Converts a journal entry to its ID.
:param value: The journal entry.
:return: The ID.
"""
return str(value.id)
class JournalEntryTypeConverter(BaseConverter):
"""The journal entry converter to convert the journal entry type ID from
and to the corresponding journal entry type in the routes."""
def to_python(self, value: str) -> JournalEntryType:
"""Converts a journal entry ID to a journal entry.
:param value: The journal entry ID.
:return: The corresponding journal entry type.
"""
type_dict: dict[str, JournalEntryType] \
= {x.value: x for x in JournalEntryType}
journal_entry_type: JournalEntryType | None = type_dict.get(value)
if journal_entry_type is None:
abort(404)
return journal_entry_type
def to_url(self, value: JournalEntryType) -> str:
"""Converts a journal entry type to its ID.
:param value: The journal entry type.
:return: The ID.
"""
return str(value.value)
class DateConverter(BaseConverter):
"""The date converter to convert the ISO date from and to the
corresponding date in the routes."""
def to_python(self, value: str) -> dt.date:
"""Converts an ISO date to a date.
:param value: The ISO date.
:return: The corresponding date.
"""
try:
return dt.date.fromisoformat(value)
except ValueError:
abort(404)
def to_url(self, value: dt.date) -> str:
"""Converts a date to its ISO date.
:param value: The date.
:return: The ISO date.
"""
return value.isoformat()

View File

@ -0,0 +1,22 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# 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 forms for the journal entry management.
"""
from .reorder import sort_journal_entries_in, JournalEntryReorderForm
from .journal_entry import JournalEntryForm, CashReceiptJournalEntryForm, \
CashDisbursementJournalEntryForm, TransferJournalEntryForm

View File

@ -0,0 +1,292 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# 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 currency sub-forms for the journal entry management.
"""
from decimal import Decimal
import sqlalchemy as sa
from flask_babel import LazyString
from flask_wtf import FlaskForm
from wtforms import StringField, ValidationError, FieldList, IntegerField, \
BooleanField, FormField
from wtforms.validators import DataRequired
from accounting import db
from accounting.forms import CurrencyExists
from accounting.locale import lazy_gettext
from accounting.models import JournalEntryLineItem
from accounting.utils.offset_alias import offset_alias
from accounting.utils.strip_text import strip_text
from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
CURRENCY_REQUIRED: DataRequired = DataRequired(
lazy_gettext("Please select the currency."))
"""The validator to check if the currency code is empty."""
class SameCurrencyAsOriginalLineItems:
"""The validator to check if the currency is the same as the
original line items."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
assert isinstance(form, CurrencyForm)
if field.data is None:
return
original_line_item_id: set[int] \
= {x.original_line_item_id.data
for x in form.line_items
if x.original_line_item_id.data is not None}
if len(original_line_item_id) == 0:
return
original_line_item_currency_codes: set[str] = set(db.session.scalars(
sa.select(JournalEntryLineItem.currency_code)
.filter(JournalEntryLineItem.id.in_(original_line_item_id))).all())
for currency_code in original_line_item_currency_codes:
if field.data != currency_code:
raise ValidationError(lazy_gettext(
"The currency must be the same as the"
" original line item."))
class KeepCurrencyWhenHavingOffset:
"""The validator to check if the currency is the same when there is
offset."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
assert isinstance(form, CurrencyForm)
if field.data is None:
return
offset: sa.Alias = offset_alias()
original_line_items: list[JournalEntryLineItem]\
= JournalEntryLineItem.query\
.join(offset,
JournalEntryLineItem.id == offset.c.original_line_item_id,
isouter=True)\
.filter(JournalEntryLineItem.id
.in_({x.id.data for x in form.line_items
if x.id.data is not None}))\
.group_by(JournalEntryLineItem.id,
JournalEntryLineItem.currency_code)\
.having(sa.func.count(offset.c.id) > 0).all()
for original_line_item in original_line_items:
if original_line_item.currency_code != field.data:
raise ValidationError(lazy_gettext(
"The currency must not be changed when there is offset."))
class NeedSomeLineItems:
"""The validator to check if there is any line item sub-form."""
def __call__(self, form: FlaskForm, field: FieldList) -> None:
if len(field) == 0:
raise ValidationError(lazy_gettext(
"Please add some line items."))
class IsBalanced:
"""The validator to check that the total amount of the debit and credit
line items are equal."""
def __call__(self, form: FlaskForm, field: BooleanField) -> None:
assert isinstance(form, TransferCurrencyForm)
if len(form.debit) == 0 or len(form.credit) == 0:
return
if form.debit_total != form.credit_total:
raise ValidationError(lazy_gettext(
"The totals of the debit and credit amounts do not match."))
class CurrencyForm(FlaskForm):
"""The form to create or edit a currency in a journal entry."""
no = IntegerField()
"""The order in the journal entry."""
code = StringField()
"""The currency code."""
whole_form = BooleanField()
"""The pseudo field for the whole form validators."""
@property
def line_items(self) -> list[LineItemForm]:
"""Returns the line item sub-forms.
:return: The line item sub-forms.
"""
line_item_forms: list[LineItemForm] = []
if isinstance(self, CashReceiptCurrencyForm):
line_item_forms.extend([x.form for x in self.credit])
elif isinstance(self, CashDisbursementCurrencyForm):
line_item_forms.extend([x.form for x in self.debit])
elif isinstance(self, TransferCurrencyForm):
line_item_forms.extend([x.form for x in self.debit])
line_item_forms.extend([x.form for x in self.credit])
return line_item_forms
@property
def is_code_locked(self) -> bool:
"""Returns whether the currency code should not be changed.
:return: True if the currency code should not be changed, or False
otherwise
"""
line_item_forms: list[LineItemForm] = self.line_items
original_line_item_id: set[int] \
= {x.original_line_item_id.data for x in line_item_forms
if x.original_line_item_id.data is not None}
if len(original_line_item_id) > 0:
return True
line_item_id: set[int] = {x.id.data for x in line_item_forms
if x.id.data is not None}
select: sa.Select = sa.select(sa.func.count(JournalEntryLineItem.id))\
.filter(JournalEntryLineItem.original_line_item_id
.in_(line_item_id))
return db.session.scalar(select) > 0
class CashReceiptCurrencyForm(CurrencyForm):
"""The form to create or edit a currency in a
cash receipt journal entry."""
no = IntegerField()
"""The order in the journal entry."""
code = StringField(
filters=[strip_text],
validators=[CURRENCY_REQUIRED,
CurrencyExists(),
SameCurrencyAsOriginalLineItems(),
KeepCurrencyWhenHavingOffset()])
"""The currency code."""
credit = FieldList(FormField(CreditLineItemForm),
validators=[NeedSomeLineItems()])
"""The credit line items."""
whole_form = BooleanField()
"""The pseudo field for the whole form validators."""
@property
def credit_total(self) -> Decimal:
"""Returns the total amount of the credit line items.
:return: The total amount of the credit line items.
"""
return sum([x.amount.data for x in self.credit
if x.amount.data is not None])
@property
def credit_errors(self) -> list[str | LazyString]:
"""Returns the credit line item errors, without the errors in their
sub-forms.
:return:
"""
return [x for x in self.credit.errors
if isinstance(x, str) or isinstance(x, LazyString)]
class CashDisbursementCurrencyForm(CurrencyForm):
"""The form to create or edit a currency in a
cash disbursement journal entry."""
no = IntegerField()
"""The order in the journal entry."""
code = StringField(
filters=[strip_text],
validators=[CURRENCY_REQUIRED,
CurrencyExists(),
SameCurrencyAsOriginalLineItems(),
KeepCurrencyWhenHavingOffset()])
"""The currency code."""
debit = FieldList(FormField(DebitLineItemForm),
validators=[NeedSomeLineItems()])
"""The debit line items."""
whole_form = BooleanField()
"""The pseudo field for the whole form validators."""
@property
def debit_total(self) -> Decimal:
"""Returns the total amount of the debit line items.
:return: The total amount of the debit line items.
"""
return sum([x.amount.data for x in self.debit
if x.amount.data is not None])
@property
def debit_errors(self) -> list[str | LazyString]:
"""Returns the debit line item errors, without the errors in their
sub-forms.
:return:
"""
return [x for x in self.debit.errors
if isinstance(x, str) or isinstance(x, LazyString)]
class TransferCurrencyForm(CurrencyForm):
"""The form to create or edit a currency in a transfer journal entry."""
no = IntegerField()
"""The order in the journal entry."""
code = StringField(
filters=[strip_text],
validators=[CURRENCY_REQUIRED,
CurrencyExists(),
SameCurrencyAsOriginalLineItems(),
KeepCurrencyWhenHavingOffset()])
"""The currency code."""
debit = FieldList(FormField(DebitLineItemForm),
validators=[NeedSomeLineItems()])
"""The debit line items."""
credit = FieldList(FormField(CreditLineItemForm),
validators=[NeedSomeLineItems()])
"""The credit line items."""
whole_form = BooleanField(validators=[IsBalanced()])
"""The pseudo field for the whole form validators."""
@property
def debit_total(self) -> Decimal:
"""Returns the total amount of the debit line items.
:return: The total amount of the debit line items.
"""
return sum([x.amount.data for x in self.debit
if x.amount.data is not None])
@property
def credit_total(self) -> Decimal:
"""Returns the total amount of the credit line items.
:return: The total amount of the credit line items.
"""
return sum([x.amount.data for x in self.credit
if x.amount.data is not None])
@property
def debit_errors(self) -> list[str | LazyString]:
"""Returns the debit line item errors, without the errors in their
sub-forms.
:return:
"""
return [x for x in self.debit.errors
if isinstance(x, str) or isinstance(x, LazyString)]
@property
def credit_errors(self) -> list[str | LazyString]:
"""Returns the credit line item errors, without the errors in their
sub-forms.
:return:
"""
return [x for x in self.credit.errors
if isinstance(x, str) or isinstance(x, LazyString)]

View File

@ -0,0 +1,588 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
# Copyright (c) 2023-2024 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 journal entry forms for the journal entry management.
"""
import datetime as dt
from abc import ABC, abstractmethod
from typing import Type
import sqlalchemy as sa
from flask_babel import LazyString
from flask_wtf import FlaskForm
from wtforms import DateField, FieldList, FormField, TextAreaField, \
BooleanField
from wtforms.validators import DataRequired, ValidationError
from accounting import db
from accounting.journal_entry.utils.account_option import AccountOption
from accounting.journal_entry.utils.description_editor import DescriptionEditor
from accounting.journal_entry.utils.original_line_items import \
get_selectable_original_line_items
from accounting.locale import lazy_gettext
from accounting.models import JournalEntry, Account, JournalEntryLineItem, \
JournalEntryCurrency
from accounting.utils.random_id import new_id
from accounting.utils.strip_text import strip_multiline_text
from accounting.utils.user import get_current_user_pk
from .currency import CurrencyForm, CashReceiptCurrencyForm, \
CashDisbursementCurrencyForm, TransferCurrencyForm
from .line_item import LineItemForm, DebitLineItemForm, CreditLineItemForm
from .reorder import sort_journal_entries_in
DATE_REQUIRED: DataRequired = DataRequired(
lazy_gettext("Please fill in the date."))
"""The validator to check if the date is empty."""
class NotBeforeOriginalLineItems:
"""The validator to check if the date is not before the
original line items."""
def __call__(self, form: FlaskForm, field: DateField) -> None:
assert isinstance(form, JournalEntryForm)
if field.data is None:
return
min_date: dt.date | None = form.min_date
if min_date is None:
return
if field.data < min_date:
raise ValidationError(lazy_gettext(
"The date cannot be earlier than the original line items."))
class NotAfterOffsetItems:
"""The validator to check if the date is not after the offset items."""
def __call__(self, form: FlaskForm, field: DateField) -> None:
assert isinstance(form, JournalEntryForm)
if field.data is None:
return
max_date: dt.date | None = form.max_date
if max_date is None:
return
if field.data > max_date:
raise ValidationError(lazy_gettext(
"The date cannot be later than the offset items."))
class NeedSomeCurrencies:
"""The validator to check if there is any currency sub-form."""
def __call__(self, form: FlaskForm, field: FieldList) -> None:
if len(field) == 0:
raise ValidationError(lazy_gettext("Please add some currencies."))
class CannotDeleteOriginalLineItemsWithOffset:
"""The validator to check the original line items with offset."""
def __call__(self, form: FlaskForm, field: FieldList) -> None:
assert isinstance(form, JournalEntryForm)
if form.obj is None:
return
existing_matched_original_line_item_id: set[int] \
= {x.id for x in form.obj.line_items if len(x.offsets) > 0}
line_item_id_in_form: set[int] \
= {x.id.data for x in form.line_items if x.id.data is not None}
for line_item_id in existing_matched_original_line_item_id:
if line_item_id not in line_item_id_in_form:
raise ValidationError(lazy_gettext(
"Line items with offset cannot be deleted."))
class JournalEntryForm(FlaskForm):
"""The base form to create or edit a journal entry."""
date = DateField()
"""The date."""
currencies = FieldList(FormField(CurrencyForm))
"""The line items categorized by their currencies."""
note = TextAreaField()
"""The note."""
def __init__(self, *args, **kwargs):
"""Constructs a base journal entry form.
:param args: The arguments.
:param kwargs: The keyword arguments.
"""
super().__init__(*args, **kwargs)
self.is_modified: bool = False
"""Whether the journal entry is modified during populate_obj()."""
self.collector: Type[LineItemCollector] = LineItemCollector
"""The line item collector. The default is the base abstract
collector only to provide the correct type. The subclass forms should
provide their own collectors."""
self.obj: JournalEntry | None = kwargs.get("obj")
"""The journal entry, when editing an existing one."""
self._is_need_payable: bool = False
"""Whether we need the payable original line items."""
self._is_need_receivable: bool = False
"""Whether we need the receivable original line items."""
self.__original_line_item_options: list[JournalEntryLineItem] | None \
= None
"""The options of the original line items."""
self.__net_balance_exceeded: dict[int, LazyString] | None = None
"""The original line items whose net balances were exceeded by the
amounts in the line item sub-forms."""
for line_item in self.line_items:
line_item.journal_entry_form = self
def populate_obj(self, obj: JournalEntry) -> None:
"""Populates the form data into a journal entry object.
:param obj: The journal entry object.
:return: None.
"""
is_new: bool = obj.id is None
if is_new:
obj.id = new_id(JournalEntry)
self.__set_date(obj, self.date.data)
obj.note = self.note.data
collector_cls: Type[LineItemCollector] = self.collector
collector: collector_cls = collector_cls(self, obj)
collector.collect()
to_delete: set[int] = {x.id for x in obj.line_items
if x.id not in collector.to_keep}
if len(to_delete) > 0:
JournalEntryLineItem.query\
.filter(JournalEntryLineItem.id.in_(to_delete)).delete()
self.is_modified = True
if is_new or db.session.is_modified(obj):
self.is_modified = True
if is_new:
current_user_pk: int = get_current_user_pk()
obj.created_by_id = current_user_pk
obj.updated_by_id = current_user_pk
@property
def line_items(self) -> list[LineItemForm]:
"""Collects and returns the line item sub-forms.
:return: The line item sub-forms.
"""
line_items: list[LineItemForm] = []
for currency in self.currencies:
line_items.extend(currency.line_items)
return line_items
def __set_date(self, obj: JournalEntry, new_date: dt.date) -> None:
"""Sets the journal entry date and number.
:param obj: The journal entry object.
:param new_date: The new date.
:return: None.
"""
if obj.date is None or obj.date != new_date:
if obj.date is not None:
sort_journal_entries_in(obj.date, obj.id)
if self.max_date is not None and new_date == self.max_date:
db_min_no: int | None = db.session.scalar(
sa.select(sa.func.min(JournalEntry.no))
.filter(JournalEntry.date == new_date))
if db_min_no is None:
obj.date = new_date
obj.no = 1
else:
obj.date = new_date
obj.no = db_min_no - 1
sort_journal_entries_in(new_date)
else:
sort_journal_entries_in(new_date, obj.id)
count: int = JournalEntry.query\
.filter(JournalEntry.date == new_date).count()
obj.date = new_date
obj.no = count + 1
@property
def debit_account_options(self) -> list[AccountOption]:
"""The selectable debit accounts.
:return: The selectable debit accounts.
"""
accounts: list[AccountOption] \
= [AccountOption(x) for x in Account.selectable_debit()
if not (x.code[0] == "2" and x.is_need_offset)]
in_use: set[int] = set(db.session.scalars(
sa.select(JournalEntryLineItem.account_id)
.filter(JournalEntryLineItem.is_debit)
.group_by(JournalEntryLineItem.account_id)).all())
for account in accounts:
account.is_in_use = account.id in in_use
return accounts
@property
def credit_account_options(self) -> list[AccountOption]:
"""The selectable credit accounts.
:return: The selectable credit accounts.
"""
accounts: list[AccountOption] \
= [AccountOption(x) for x in Account.selectable_credit()
if not (x.code[0] == "1" and x.is_need_offset)]
in_use: set[int] = set(db.session.scalars(
sa.select(JournalEntryLineItem.account_id)
.filter(sa.not_(JournalEntryLineItem.is_debit))
.group_by(JournalEntryLineItem.account_id)).all())
for account in accounts:
account.is_in_use = account.id in in_use
return accounts
@property
def currencies_errors(self) -> list[str | LazyString]:
"""Returns the currency errors, without the errors in their sub-forms.
:return: The currency errors, without the errors in their sub-forms.
"""
return [x for x in self.currencies.errors
if isinstance(x, str) or isinstance(x, LazyString)]
@property
def description_editor(self) -> DescriptionEditor:
"""Returns the description editor.
:return: The description editor.
"""
return DescriptionEditor()
@property
def original_line_item_options(self) -> list[JournalEntryLineItem]:
"""Returns the selectable original line items.
:return: The selectable original line items.
"""
if self.__original_line_item_options is None:
self.__original_line_item_options \
= get_selectable_original_line_items(
{x.id.data for x in self.line_items
if x.id.data is not None},
self._is_need_payable, self._is_need_receivable)
return self.__original_line_item_options
@property
def min_date(self) -> dt.date | None:
"""Returns the minimal available date.
:return: The minimal available date.
"""
original_line_item_id: set[int] \
= {x.original_line_item_id.data for x in self.line_items
if x.original_line_item_id.data is not None}
if len(original_line_item_id) == 0:
return None
select: sa.Select = sa.select(sa.func.max(JournalEntry.date))\
.join(JournalEntryLineItem)\
.filter(JournalEntryLineItem.id.in_(original_line_item_id))
return db.session.scalar(select)
@property
def max_date(self) -> dt.date | None:
"""Returns the maximum available date.
:return: The maximum available date.
"""
line_item_id: set[int] = {x.id.data for x in self.line_items
if x.id.data is not None}
select: sa.Select = sa.select(sa.func.min(JournalEntry.date))\
.join(JournalEntryLineItem)\
.filter(JournalEntryLineItem.original_line_item_id
.in_(line_item_id))
return db.session.scalar(select)
class LineItemCollector[T: JournalEntryForm](ABC):
"""The line item collector."""
def __init__(self, form: T, obj: JournalEntry):
"""Constructs the line item collector.
:param form: The journal entry form.
:param obj: The journal entry.
"""
self.form: T = form
"""The journal entry form."""
self.__obj: JournalEntry = obj
"""The journal entry object."""
self.__line_items: list[JournalEntryLineItem] = list(obj.line_items)
"""The existing line items."""
self.__line_items_by_id: dict[int, JournalEntryLineItem] \
= {x.id: x for x in self.__line_items}
"""A dictionary from the line item ID to their line items."""
self.__no_by_id: dict[int, int] \
= {x.id: x.no for x in self.__line_items}
"""A dictionary from the line item number to their line items."""
self.__currencies: list[JournalEntryCurrency] = obj.currencies
"""The currencies in the journal entry."""
self._debit_no: int = 1
"""The number index for the debit line items."""
self._credit_no: int = 1
"""The number index for the credit line items."""
self.to_keep: set[int] = set()
"""The ID of the existing line items to keep."""
@abstractmethod
def collect(self) -> set[int]:
"""Collects the line items.
:return: The ID of the line items to keep.
"""
def _add_line_item(self, form: LineItemForm, currency_code: str, no: int) \
-> None:
"""Composes a line item from the form.
:param form: The line item form.
:param currency_code: The code of the currency.
:param no: The number of the line item.
:return: None.
"""
line_item: JournalEntryLineItem | None \
= self.__line_items_by_id.get(form.id.data)
if line_item is not None:
line_item.currency_code = currency_code
form.populate_obj(line_item)
line_item.no = no
if db.session.is_modified(line_item):
self.form.is_modified = True
else:
line_item = JournalEntryLineItem()
line_item.currency_code = currency_code
form.populate_obj(line_item)
line_item.no = no
self.__obj.line_items.append(line_item)
self.form.is_modified = True
self.to_keep.add(line_item.id)
def _make_cash_line_item(self, forms: list[LineItemForm], is_debit: bool,
currency_code: str, no: int) -> None:
"""Composes the cash line item at the other debit or credit of the
cash journal entry.
:param forms: The line item forms in the same currency.
:param is_debit: True for a cash receipt journal entry, or False for a
cash disbursement journal entry.
:param currency_code: The code of the currency.
:param no: The number of the line item.
:return: None.
"""
candidates: list[JournalEntryLineItem] \
= [x for x in self.__line_items
if x.is_debit == is_debit and x.currency_code == currency_code]
line_item: JournalEntryLineItem
if len(candidates) > 0:
candidates.sort(key=lambda x: x.no)
line_item = candidates[0]
line_item.account_id = Account.cash().id
line_item.description = None
line_item.amount = sum([x.amount.data for x in forms])
line_item.no = no
if db.session.is_modified(line_item):
self.form.is_modified = True
else:
line_item = JournalEntryLineItem()
line_item.id = new_id(JournalEntryLineItem)
line_item.is_debit = is_debit
line_item.currency_code = currency_code
line_item.account_id = Account.cash().id
line_item.description = None
line_item.amount = sum([x.amount.data for x in forms])
line_item.no = no
self.__obj.line_items.append(line_item)
self.form.is_modified = True
self.to_keep.add(line_item.id)
def _sort_line_item_forms(self, forms: list[LineItemForm]) -> None:
"""Sorts the line item sub-forms.
:param forms: The line item sub-forms.
:return: None.
"""
missing_no: int = 100 if len(self.__no_by_id) == 0 \
else max(self.__no_by_id.values()) + 100
ord_by_form: dict[LineItemForm, int] \
= {forms[i]: i for i in range(len(forms))}
recv_no: set[int] = {x.no.data for x in forms if x.no.data is not None}
missing_recv_no: int = 100 if len(recv_no) == 0 else max(recv_no) + 100
forms.sort(key=lambda x: (x.no.data or missing_recv_no,
missing_no if x.id.data is None else
self.__no_by_id.get(x.id.data, missing_no),
ord_by_form.get(x)))
def _sort_currency_forms(self, forms: list[CurrencyForm]) -> None:
"""Sorts the currency forms.
:param forms: The currency forms.
:return: None.
"""
missing_no: int = len(self.__currencies) + 100
no_by_code: dict[str, int] = {self.__currencies[i].code: i
for i in range(len(self.__currencies))}
ord_by_form: dict[CurrencyForm, int] \
= {forms[i]: i for i in range(len(forms))}
recv_no: set[int] = {x.no.data for x in forms if x.no.data is not None}
missing_recv_no: int = 100 if len(recv_no) == 0 else max(recv_no) + 100
forms.sort(key=lambda x: (x.no.data or missing_recv_no,
no_by_code.get(x.code.data, missing_no),
ord_by_form.get(x)))
class CashReceiptJournalEntryForm(JournalEntryForm):
"""The form to create or edit a cash receipt journal entry."""
date = DateField(
validators=[DATE_REQUIRED,
NotBeforeOriginalLineItems(),
NotAfterOffsetItems()])
"""The date."""
currencies = FieldList(FormField(CashReceiptCurrencyForm), name="currency",
validators=[NeedSomeCurrencies()])
"""The line items categorized by their currencies."""
note = TextAreaField(filters=[strip_multiline_text])
"""The note."""
whole_form = BooleanField(
validators=[CannotDeleteOriginalLineItemsWithOffset()])
"""The pseudo field for the whole form validators."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._is_need_receivable = True
class Collector(LineItemCollector[CashReceiptJournalEntryForm]):
"""The line item collector for the cash receipt journal entries."""
def collect(self) -> None:
currencies: list[CashReceiptCurrencyForm] \
= [x.form for x in self.form.currencies]
self._sort_currency_forms(currencies)
for currency in currencies:
# The debit cash line item
self._make_cash_line_item(list(currency.credit), True,
currency.code.data,
self._debit_no)
self._debit_no = self._debit_no + 1
# The credit forms
credit_forms: list[CreditLineItemForm] \
= [x.form for x in currency.credit]
self._sort_line_item_forms(credit_forms)
for credit_form in credit_forms:
self._add_line_item(credit_form, currency.code.data,
self._credit_no)
self._credit_no = self._credit_no + 1
self.collector = Collector
class CashDisbursementJournalEntryForm(JournalEntryForm):
"""The form to create or edit a cash disbursement journal entry."""
date = DateField(
validators=[DATE_REQUIRED,
NotBeforeOriginalLineItems(),
NotAfterOffsetItems()])
"""The date."""
currencies = FieldList(FormField(CashDisbursementCurrencyForm),
name="currency",
validators=[NeedSomeCurrencies()])
"""The line items categorized by their currencies."""
note = TextAreaField(filters=[strip_multiline_text])
"""The note."""
whole_form = BooleanField(
validators=[CannotDeleteOriginalLineItemsWithOffset()])
"""The pseudo field for the whole form validators."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._is_need_payable = True
class Collector(LineItemCollector[CashDisbursementJournalEntryForm]):
"""The line item collector for the cash disbursement journal
entries."""
def collect(self) -> None:
currencies: list[CashDisbursementCurrencyForm] \
= [x.form for x in self.form.currencies]
self._sort_currency_forms(currencies)
for currency in currencies:
# The debit forms
debit_forms: list[DebitLineItemForm] \
= [x.form for x in currency.debit]
self._sort_line_item_forms(debit_forms)
for debit_form in debit_forms:
self._add_line_item(debit_form, currency.code.data,
self._debit_no)
self._debit_no = self._debit_no + 1
# The credit forms
self._make_cash_line_item(list(currency.debit), False,
currency.code.data,
self._credit_no)
self._credit_no = self._credit_no + 1
self.collector = Collector
class TransferJournalEntryForm(JournalEntryForm):
"""The form to create or edit a transfer journal entry."""
date = DateField(
validators=[DATE_REQUIRED,
NotBeforeOriginalLineItems(),
NotAfterOffsetItems()])
"""The date."""
currencies = FieldList(FormField(TransferCurrencyForm), name="currency",
validators=[NeedSomeCurrencies()])
"""The line items categorized by their currencies."""
note = TextAreaField(filters=[strip_multiline_text])
"""The note."""
whole_form = BooleanField(
validators=[CannotDeleteOriginalLineItemsWithOffset()])
"""The pseudo field for the whole form validators."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._is_need_payable = True
self._is_need_receivable = True
class Collector(LineItemCollector[TransferJournalEntryForm]):
"""The line item collector for the transfer journal entries."""
def collect(self) -> None:
currencies: list[TransferCurrencyForm] \
= [x.form for x in self.form.currencies]
self._sort_currency_forms(currencies)
for currency in currencies:
# The debit forms
debit_forms: list[DebitLineItemForm] \
= [x.form for x in currency.debit]
self._sort_line_item_forms(debit_forms)
for debit_form in debit_forms:
self._add_line_item(debit_form, currency.code.data,
self._debit_no)
self._debit_no = self._debit_no + 1
# The credit forms
credit_forms: list[CreditLineItemForm] \
= [x.form for x in currency.credit]
self._sort_line_item_forms(credit_forms)
for credit_form in credit_forms:
self._add_line_item(credit_form, currency.code.data,
self._credit_no)
self._credit_no = self._credit_no + 1
self.collector = Collector

View File

@ -0,0 +1,507 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# 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 line item sub-forms for the journal entry management.
"""
import datetime as dt
from decimal import Decimal
import sqlalchemy as sa
from flask_babel import LazyString
from flask_wtf import FlaskForm
from sqlalchemy.orm import selectinload
from wtforms import StringField, ValidationError, DecimalField, IntegerField
from wtforms.validators import Optional
from accounting import db
from accounting.forms import ACCOUNT_REQUIRED, AccountExists, IsDebitAccount, \
IsCreditAccount
from accounting.locale import lazy_gettext
from accounting.models import Account, JournalEntry, JournalEntryLineItem
from accounting.template_filters import format_amount
from accounting.utils.random_id import new_id
from accounting.utils.strip_text import strip_text
from accounting.utils.user import get_current_user_pk
class OriginalLineItemExists:
"""The validator to check if the original line item exists."""
def __call__(self, form: FlaskForm, field: IntegerField) -> None:
if field.data is None:
return
if db.session.get(JournalEntryLineItem, field.data) is None:
raise ValidationError(lazy_gettext(
"The original line item does not exist."))
class OriginalLineItemOppositeDebitCredit:
"""The validator to check if the original line item is on the opposite
debit or credit."""
def __call__(self, form: FlaskForm, field: IntegerField) -> None:
if field.data is None:
return
original_line_item: JournalEntryLineItem | None \
= db.session.get(JournalEntryLineItem, field.data)
if original_line_item is None:
return
if isinstance(form, CreditLineItemForm) \
and original_line_item.is_debit:
return
if isinstance(form, DebitLineItemForm) \
and not original_line_item.is_debit:
return
raise ValidationError(lazy_gettext(
"The original line item is on the same debit or credit."))
class OriginalLineItemNeedOffset:
"""The validator to check if the original line item needs offset."""
def __call__(self, form: FlaskForm, field: IntegerField) -> None:
if field.data is None:
return
original_line_item: JournalEntryLineItem | None \
= db.session.get(JournalEntryLineItem, field.data)
if original_line_item is None:
return
if not original_line_item.account.is_need_offset:
raise ValidationError(lazy_gettext(
"The original line item does not need offset."))
class OriginalLineItemNotOffset:
"""The validator to check if the original line item is not itself an
offset item."""
def __call__(self, form: FlaskForm, field: IntegerField) -> None:
if field.data is None:
return
original_line_item: JournalEntryLineItem | None \
= db.session.get(JournalEntryLineItem, field.data)
if original_line_item is None:
return
if original_line_item.original_line_item_id is not None:
raise ValidationError(lazy_gettext(
"The original line item cannot be an offset item."))
class SameAccountAsOriginalLineItem:
"""The validator to check if the account is the same as the
original line item."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
assert isinstance(form, LineItemForm)
if field.data is None or form.original_line_item_id.data is None:
return
original_line_item: JournalEntryLineItem | None \
= db.session.get(JournalEntryLineItem,
form.original_line_item_id.data)
if original_line_item is None:
return
if field.data != original_line_item.account_code:
raise ValidationError(lazy_gettext(
"The account must be the same as the original line item."))
class KeepAccountWhenHavingOffset:
"""The validator to check if the account is the same when having offset."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
assert isinstance(form, LineItemForm)
if field.data is None or form.id.data is None:
return
line_item: JournalEntryLineItem | None \
= db.session.get(JournalEntryLineItem, form.id.data)
if line_item is None or len(line_item.offsets) == 0:
return
if field.data != line_item.account_code:
raise ValidationError(lazy_gettext(
"The account must not be changed when there is offset."))
class NotStartPayableFromDebit:
"""The validator to check that a payable line item does not start from
debit."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
assert isinstance(form, DebitLineItemForm)
if field.data is None \
or field.data[0] != "2" \
or form.original_line_item_id.data is not None:
return
account: Account | None = Account.find_by_code(field.data)
if account is not None and account.is_need_offset:
raise ValidationError(lazy_gettext(
"A payable line item cannot start from debit."))
class NotStartReceivableFromCredit:
"""The validator to check that a receivable line item does not start
from credit."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
assert isinstance(form, CreditLineItemForm)
if field.data is None \
or field.data[0] != "1" \
or form.original_line_item_id.data is not None:
return
account: Account | None = Account.find_by_code(field.data)
if account is not None and account.is_need_offset:
raise ValidationError(lazy_gettext(
"A receivable line item cannot start from credit."))
class PositiveAmount:
"""The validator to check if the amount is positive."""
def __call__(self, form: FlaskForm, field: DecimalField) -> None:
if field.data is None:
return
if field.data <= 0:
raise ValidationError(lazy_gettext(
"Please fill in a positive amount."))
class NotExceedingOriginalLineItemNetBalance:
"""The validator to check if the amount exceeds the net balance of the
original line item."""
def __call__(self, form: FlaskForm, field: DecimalField) -> None:
assert isinstance(form, LineItemForm)
if field.data is None or form.original_line_item_id.data is None:
return
original_line_item: JournalEntryLineItem | None \
= db.session.get(JournalEntryLineItem,
form.original_line_item_id.data)
if original_line_item is None:
return
is_debit: bool = isinstance(form, DebitLineItemForm)
existing_line_item_id: set[int] = set()
if form.journal_entry_form.obj is not None:
existing_line_item_id \
= {x.id for x in form.journal_entry_form.obj.line_items}
offset_total_func: sa.Function = sa.func.sum(sa.case(
(JournalEntryLineItem.is_debit == is_debit,
JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount))
offset_total_but_form: Decimal | None = db.session.scalar(
sa.select(offset_total_func)
.filter(JournalEntryLineItem.original_line_item_id
== original_line_item.id,
JournalEntryLineItem.id.not_in(existing_line_item_id)))
if offset_total_but_form is None:
offset_total_but_form = Decimal("0")
offset_total_on_form: Decimal = sum(
[x.amount.data for x in form.journal_entry_form.line_items
if x.original_line_item_id.data == original_line_item.id
and x.amount != field and x.amount.data is not None])
net_balance: Decimal = original_line_item.amount \
- offset_total_but_form - offset_total_on_form
if field.data > net_balance:
raise ValidationError(lazy_gettext(
"The amount must not exceed the net balance %(balance)s of the"
" original line item.", balance=format_amount(net_balance)))
class NotLessThanOffsetTotal:
"""The validator to check if the amount is less than the offset total."""
def __call__(self, form: FlaskForm, field: DecimalField) -> None:
assert isinstance(form, LineItemForm)
if field.data is None or form.id.data is None:
return
is_debit: bool = isinstance(form, DebitLineItemForm)
select_offset_total: sa.Select = sa.select(sa.func.sum(sa.case(
(JournalEntryLineItem.is_debit != is_debit,
JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount)))\
.filter(JournalEntryLineItem.original_line_item_id == form.id.data)
offset_total: Decimal | None = db.session.scalar(select_offset_total)
if offset_total is not None and field.data < offset_total:
raise ValidationError(lazy_gettext(
"The amount must not be less than the offset total %(total)s.",
total=format_amount(offset_total)))
class LineItemForm(FlaskForm):
"""The base form to create or edit a line item."""
id = IntegerField()
"""The existing line item ID."""
no = IntegerField()
"""The order in the currency."""
original_line_item_id = IntegerField()
"""The Id of the original line item."""
account_code = StringField()
"""The account code."""
description = StringField()
"""The description."""
amount = DecimalField()
"""The amount."""
def __init__(self, *args, **kwargs):
"""Constructs a base line item form.
:param args: The arguments.
:param kwargs: The keyword arguments.
"""
super().__init__(*args, **kwargs)
from .journal_entry import JournalEntryForm
self.journal_entry_form: JournalEntryForm | None = None
"""The source journal entry form."""
@property
def account_title(self) -> str:
"""Returns the title of the account.
:return: The title of the account.
"""
if self.account_code.data is None:
return ""
account: Account | None = Account.find_by_code(self.account_code.data)
if account is None:
return ""
return account.title
@property
def account_text(self) -> str:
"""Returns the text representation of the account.
:return: The text representation of the account.
"""
if self.account_code.data is None:
return ""
account: Account | None = Account.find_by_code(self.account_code.data)
if account is None:
return ""
return str(account)
@property
def __original_line_item(self) -> JournalEntryLineItem | None:
"""Returns the original line item.
:return: The original line item.
"""
if not hasattr(self, "____original_line_item"):
def get_line_item() -> JournalEntryLineItem | None:
if self.original_line_item_id.data is None:
return None
return db.session.get(JournalEntryLineItem,
self.original_line_item_id.data)
setattr(self, "____original_line_item", get_line_item())
return getattr(self, "____original_line_item")
@property
def original_line_item_date(self) -> dt.date | None:
"""Returns the text representation of the original line item.
:return: The text representation of the original line item.
"""
return None if self.__original_line_item is None \
else self.__original_line_item.journal_entry.date
@property
def original_line_item_text(self) -> str | None:
"""Returns the text representation of the original line item.
:return: The text representation of the original line item.
"""
return None if self.__original_line_item is None \
else str(self.__original_line_item)
@property
def is_need_offset(self) -> bool:
"""Returns whether the line item needs offset.
:return: True if the line item needs offset, or False otherwise.
"""
if self.account_code.data is None:
return False
if self.account_code.data[0] == "1":
if isinstance(self, CreditLineItemForm):
return False
elif self.account_code.data[0] == "2":
if isinstance(self, DebitLineItemForm):
return False
else:
return False
account: Account | None = Account.find_by_code(self.account_code.data)
return account is not None and account.is_need_offset
@property
def offsets(self) -> list[JournalEntryLineItem]:
"""Returns the offsets.
:return: The offsets.
"""
if not hasattr(self, "__offsets"):
def get_offsets() -> list[JournalEntryLineItem]:
if not self.is_need_offset or self.id.data is None:
return []
return JournalEntryLineItem.query.join(JournalEntry)\
.filter(JournalEntryLineItem.original_line_item_id
== self.id.data)\
.order_by(JournalEntry.date, JournalEntry.no,
JournalEntryLineItem.no)\
.options(selectinload(JournalEntryLineItem.journal_entry),
selectinload(JournalEntryLineItem.account)).all()
setattr(self, "__offsets", get_offsets())
return getattr(self, "__offsets")
@property
def offset_total(self) -> Decimal | None:
"""Returns the total amount of the offsets.
:return: The total amount of the offsets.
"""
if not hasattr(self, "__offset_total"):
def get_offset_total():
if not self.is_need_offset or self.id.data is None:
return None
is_debit: bool = isinstance(self, DebitLineItemForm)
return sum([x.amount if x.is_debit != is_debit else -x.amount
for x in self.offsets])
setattr(self, "__offset_total", get_offset_total())
return getattr(self, "__offset_total")
@property
def net_balance(self) -> Decimal | None:
"""Returns the net balance.
:return: The net balance.
"""
if not self.is_need_offset or self.id.data is None \
or self.amount.data is None:
return None
return self.amount.data - self.offset_total
@property
def all_errors(self) -> list[str | LazyString]:
"""Returns all the errors of the form.
:return: All the errors of the form.
"""
all_errors: list[str | LazyString] = []
for key in self.errors:
if key != "csrf_token":
all_errors.extend(self.errors[key])
return all_errors
class DebitLineItemForm(LineItemForm):
"""The form to create or edit a debit line item."""
id = IntegerField()
"""The existing line item ID."""
no = IntegerField()
"""The order in the currency."""
original_line_item_id = IntegerField(
validators=[Optional(),
OriginalLineItemExists(),
OriginalLineItemOppositeDebitCredit(),
OriginalLineItemNeedOffset(),
OriginalLineItemNotOffset()])
"""The ID of the original line item."""
account_code = StringField(
filters=[strip_text],
validators=[
ACCOUNT_REQUIRED,
AccountExists(),
IsDebitAccount(lazy_gettext(
"This account is not for debit line items.")),
SameAccountAsOriginalLineItem(),
KeepAccountWhenHavingOffset(),
NotStartPayableFromDebit()])
"""The account code."""
description = StringField(filters=[strip_text])
"""The description."""
amount = DecimalField(
validators=[PositiveAmount(),
NotExceedingOriginalLineItemNetBalance(),
NotLessThanOffsetTotal()])
"""The amount."""
def populate_obj(self, obj: JournalEntryLineItem) -> None:
"""Populates the form data into a line item object.
:param obj: The line item object.
:return: None.
"""
is_new: bool = obj.id is None
if is_new:
obj.id = new_id(JournalEntryLineItem)
obj.original_line_item_id = self.original_line_item_id.data
obj.account_id = Account.find_by_code(self.account_code.data).id
obj.description = self.description.data
obj.is_debit = True
obj.amount = self.amount.data
if is_new:
current_user_pk: int = get_current_user_pk()
obj.created_by_id = current_user_pk
obj.updated_by_id = current_user_pk
class CreditLineItemForm(LineItemForm):
"""The form to create or edit a credit line item."""
id = IntegerField()
"""The existing line item ID."""
no = IntegerField()
"""The order in the currency."""
original_line_item_id = IntegerField(
validators=[Optional(),
OriginalLineItemExists(),
OriginalLineItemOppositeDebitCredit(),
OriginalLineItemNeedOffset(),
OriginalLineItemNotOffset()])
"""The ID of the original line item."""
account_code = StringField(
filters=[strip_text],
validators=[
ACCOUNT_REQUIRED,
AccountExists(),
IsCreditAccount(lazy_gettext(
"This account is not for credit line items.")),
SameAccountAsOriginalLineItem(),
KeepAccountWhenHavingOffset(),
NotStartReceivableFromCredit()])
"""The account code."""
description = StringField(filters=[strip_text])
"""The description."""
amount = DecimalField(
validators=[PositiveAmount(),
NotExceedingOriginalLineItemNetBalance(),
NotLessThanOffsetTotal()])
"""The amount."""
def populate_obj(self, obj: JournalEntryLineItem) -> None:
"""Populates the form data into a line item object.
:param obj: The line item object.
:return: None.
"""
is_new: bool = obj.id is None
if is_new:
obj.id = new_id(JournalEntryLineItem)
obj.original_line_item_id = self.original_line_item_id.data
obj.account_id = Account.find_by_code(self.account_code.data).id
obj.description = self.description.data
obj.is_debit = False
obj.amount = self.amount.data
if is_new:
current_user_pk: int = get_current_user_pk()
obj.created_by_id = current_user_pk
obj.updated_by_id = current_user_pk

View File

@ -0,0 +1,95 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# 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 reorder forms for the journal entry management.
"""
import datetime as dt
import sqlalchemy as sa
from flask import request
from accounting import db
from accounting.models import JournalEntry
def sort_journal_entries_in(date: dt.date, exclude: int | None = None) -> None:
"""Sorts the journal entries under a date after changing the date or
deleting a journal entry.
:param date: The date of the journal entry.
:param exclude: The journal entry ID to exclude.
:return: None.
"""
conditions: list[sa.BinaryExpression] = [JournalEntry.date == date]
if exclude is not None:
conditions.append(JournalEntry.id != exclude)
journal_entries: list[JournalEntry] = JournalEntry.query\
.filter(*conditions)\
.order_by(JournalEntry.no).all()
for i in range(len(journal_entries)):
if journal_entries[i].no != i + 1:
journal_entries[i].no = i + 1
class JournalEntryReorderForm:
"""The form to reorder the journal entries."""
def __init__(self, date: dt.date):
"""Constructs the form to reorder the journal entries in a day.
:param date: The date.
"""
self.date: dt.date = date
"""The date."""
self.is_modified: bool = False
"""Whether the order is modified."""
def save_order(self) -> None:
"""Saves the order of the account.
:return:
"""
journal_entries: list[JournalEntry] = JournalEntry.query\
.filter(JournalEntry.date == self.date).all()
# Collects the specified order.
orders: dict[JournalEntry, int] = {}
for journal_entry in journal_entries:
if f"{journal_entry.id}-no" in request.form:
try:
orders[journal_entry] \
= int(request.form[f"{journal_entry.id}-no"])
except ValueError:
pass
# Missing and invalid orders are appended to the end.
missing: list[JournalEntry] \
= [x for x in journal_entries if x not in orders]
if len(missing) > 0:
next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
for journal_entry in missing:
orders[journal_entry] = next_no
# Sort by the specified order first, and their original order.
journal_entries.sort(key=lambda x: (orders[x], x.no))
# Update the orders.
with db.session.no_autoflush:
for i in range(len(journal_entries)):
if journal_entries[i].no != i + 1:
journal_entries[i].no = i + 1
self.is_modified = True

View File

@ -0,0 +1,82 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
# 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 template filters for the journal entry management.
"""
from decimal import Decimal
from html import escape
from urllib.parse import ParseResult, urlparse, parse_qsl, urlencode, \
urlunparse
from flask import request
def with_type(uri: str) -> str:
"""Adds the journal entry type to the URI, if it is specified.
:param uri: The URI.
:return: The result URL, optionally with the journal entry type added.
"""
if "as" not in request.args:
return uri
uri_p: ParseResult = urlparse(uri)
params: list[tuple[str, str]] = parse_qsl(uri_p.query)
params = [x for x in params if x[0] != "as"]
params.append(("as", request.args["as"]))
parts: list[str] = list(uri_p)
parts[4] = urlencode(params)
return urlunparse(parts)
def to_transfer(uri: str) -> str:
"""Adds the transfer journal entry type to the URI.
:param uri: The URI.
:return: The result URL, with the transfer journal entry type added.
"""
uri_p: ParseResult = urlparse(uri)
params: list[tuple[str, str]] = parse_qsl(uri_p.query)
params = [x for x in params if x[0] != "as"]
params.append(("as", "transfer"))
parts: list[str] = list(uri_p)
parts[4] = urlencode(params)
return urlunparse(parts)
def format_amount_input(value: Decimal | None) -> str:
"""Format an amount for an input value.
:param value: The amount.
:return: The formatted amount text for an input value.
"""
if value is None:
return ""
whole: int = int(value)
frac: Decimal = (value - whole).normalize()
return str(whole) + str(frac)[1:]
def text2html(value: str) -> str:
"""Converts plain text into HTML.
:param value: The plain text.
:return: The HTML.
"""
s: str = escape(value)
s = s.replace("\n", "<br>")
s = s.replace(" ", " &nbsp;")
return s

View File

@ -0,0 +1,19 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# 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 for the journal entry management.
"""

View File

@ -0,0 +1,51 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# 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 account option for the journal entry management.
"""
from accounting.models import Account
class AccountOption:
"""An account option."""
def __init__(self, account: Account):
"""Constructs an account option.
:param account: The account.
"""
self.id: str = account.id
"""The account ID."""
self.code: str = account.code
"""The account code."""
self.title: str = account.title
"""The account title."""
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
"""True if this account is in use, or False otherwise."""
self.is_need_offset: bool = account.is_need_offset
"""True if this account needs offset, or False otherwise."""
def __str__(self) -> str:
"""Returns the string representation of the account option.
:return: The string representation of the account option.
"""
return self.__str

View File

@ -0,0 +1,357 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/27
# 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 description editor.
"""
import re
from typing import Literal
import sqlalchemy as sa
from accounting import db
from accounting.models import Account, JournalEntryLineItem
from accounting.utils.options import options, Recurring
class DescriptionAccount:
"""An account for a description tag."""
def __init__(self, account: Account, freq: int):
"""Constructs an account for a description tag.
:param account: The account.
:param freq: The frequency of the tag with the account.
"""
self.__account: Account = account
"""The account."""
self.id: int = account.id
"""The account ID."""
self.code: str = account.code
"""The account code."""
self.is_need_offset: bool = account.is_need_offset
"""Whether the journal entry line items of this account need offset."""
self.freq: int = freq
"""The frequency of the tag with the account."""
def __str__(self) -> str:
"""Returns the string representation of the account.
:return: The string representation of the account.
"""
return str(self.__account)
@property
def title(self) -> str:
"""Returns the account title.
:return: The account title.
"""
return self.__account.title
def add_freq(self, freq: int) -> None:
"""Adds the frequency of an account.
:param freq: The frequency of the tag name with the account.
:return: None.
"""
self.freq = self.freq + freq
class DescriptionTag:
"""A description tag."""
def __init__(self, name: str):
"""Constructs a description tag.
:param name: The tag name.
"""
self.name: str = name
"""The tag name."""
self.__account_dict: dict[int, DescriptionAccount] = {}
"""The accounts that come with the tag, in the order of their
frequency."""
self.freq: int = 0
"""The frequency of the tag."""
def __str__(self) -> str:
"""Returns the string representation of the tag.
:return: The string representation of the tag.
"""
return self.name
def add_account(self, account: Account, freq: int):
"""Adds an account.
:param account: The associated account.
:param freq: The frequency of the tag name with the account.
:return: None.
"""
self.__account_dict[account.id] = DescriptionAccount(account, freq)
self.freq = self.freq + freq
@property
def accounts(self) -> list[DescriptionAccount]:
"""Returns the accounts by the order of their frequencies.
:return: The accounts by the order of their frequencies.
"""
return sorted(self.__account_dict.values(), key=lambda x: -x.freq)
@property
def account_codes(self) -> list[str]:
"""Returns the account codes by the order of their frequencies.
:return: The account codes by the order of their frequencies.
"""
return [x.code for x in self.accounts]
class DescriptionType:
"""A description type"""
def __init__(self, type_id: Literal["general", "travel", "bus"]):
"""Constructs a description type.
:param type_id: The type ID, either "general", "travel", or "bus".
"""
self.id: Literal["general", "travel", "bus"] = type_id
"""The type ID."""
self.__tag_dict: dict[str, DescriptionTag] = {}
"""A dictionary from the tag name to their corresponding tag."""
def add_tag(self, name: str, account: Account, freq: int) -> None:
"""Adds a tag.
:param name: The tag name.
:param account: The associated account.
:param freq: The frequency of the tag name with the account.
:return: None.
"""
if name not in self.__tag_dict:
self.__tag_dict[name] = DescriptionTag(name)
self.__tag_dict[name].add_account(account, freq)
@property
def tags(self) -> list[DescriptionTag]:
"""Returns the tags by the order of their frequencies.
:return: The tags by the order of their frequencies.
"""
return sorted(self.__tag_dict.values(), key=lambda x: -x.freq)
class DescriptionRecurring:
"""A recurring transaction."""
def __init__(self, name: str, account: Account, description_template: str):
"""Constructs a recurring transaction.
:param name: The name.
:param description_template: The description template.
:param account: The account.
"""
self.name: str = name
"""The name."""
self.account: DescriptionAccount = DescriptionAccount(account, 0)
"""The account."""
self.description_template: str = description_template
"""The description template."""
@property
def account_codes(self) -> list[str]:
"""Returns the account codes by the order of their frequencies.
:return: The account codes by the order of their frequencies.
"""
return [self.account.code]
class DescriptionDebitCredit:
"""The description on debit or credit."""
def __init__(self, debit_credit: Literal["debit", "credit"]):
"""Constructs the description on debit or credit.
:param debit_credit: Either "debit" or "credit".
"""
self.debit_credit: Literal["debit", "credit"] = debit_credit
"""Either debit or credit."""
self.general: DescriptionType = DescriptionType("general")
"""The general tags."""
self.travel: DescriptionType = DescriptionType("travel")
"""The travel tags."""
self.bus: DescriptionType = DescriptionType("bus")
"""The bus tags."""
self.__type_dict: dict[Literal["general", "travel", "bus"],
DescriptionType] \
= {x.id: x for x in {self.general, self.travel, self.bus}}
"""A dictionary from the type ID to the corresponding tags."""
self.recurring: list[DescriptionRecurring] = []
"""The recurring transactions."""
def add_tag(self, tag_type: Literal["general", "travel", "bus"],
name: str, account: Account, freq: int) -> None:
"""Adds a tag.
:param tag_type: The tag type, either "general", "travel", or "bus".
:param name: The name.
:param account: The associated account.
:param freq: The frequency of the tag name with the account.
:return: None.
"""
self.__type_dict[tag_type].add_tag(name, account, freq)
@property
def accounts(self) -> list[DescriptionAccount]:
"""Returns the suggested accounts of all tags in the description editor
in debit or credit, in their frequency order.
:return: The suggested accounts of all tags, in their frequency order.
"""
accounts: dict[int, DescriptionAccount] = {}
freq: dict[int, int] = {}
for tag_type in self.__type_dict.values():
for tag in tag_type.tags:
for account in tag.accounts:
accounts[account.id] = account
if account.id not in freq:
freq[account.id] = 0
freq[account.id] \
= freq[account.id] + account.freq
for recurring in self.recurring:
accounts[recurring.account.id] = recurring.account
if recurring.account.id not in freq:
freq[recurring.account.id] = 0
return [accounts[y] for y in sorted(freq.keys(),
key=lambda x: -freq[x])]
class DescriptionEditor:
"""The description editor."""
def __init__(self):
"""Constructs the description editor."""
self.debit: DescriptionDebitCredit = DescriptionDebitCredit("debit")
"""The debit tags."""
self.credit: DescriptionDebitCredit = DescriptionDebitCredit("credit")
"""The credit tags."""
self.__init_tags()
self.__init_recurring()
def __init_tags(self):
"""Initializes the tags.
:return: None.
"""
debit_credit: sa.Label = sa.case(
(JournalEntryLineItem.is_debit, "debit"),
else_="credit").label("debit_credit")
tag_type: sa.Label = sa.case(
(JournalEntryLineItem.description.like("_%—_%—_%→_%"), "bus"),
(sa.or_(JournalEntryLineItem.description.like("_%—_%→_%"),
JournalEntryLineItem.description.like("_%—_%↔_%")),
"travel"),
else_="general").label("tag_type")
tag: sa.Label = get_prefix(JournalEntryLineItem.description, "")\
.label("tag")
select: sa.Select = sa.Select(debit_credit, tag_type, tag,
JournalEntryLineItem.account_id,
sa.func.count().label("freq"))\
.filter(JournalEntryLineItem.description.is_not(None),
JournalEntryLineItem.description.like("_%—_%"),
JournalEntryLineItem.original_line_item_id.is_(None))\
.group_by(debit_credit, tag_type, tag,
JournalEntryLineItem.account_id)
result: list[sa.Row] = db.session.execute(select).all()
accounts: dict[int, Account] \
= {x.id: x for x in Account.query
.filter(Account.id.in_({x.account_id for x in result})).all()}
debit_credit_dict: dict[Literal["debit", "credit"],
DescriptionDebitCredit] \
= {x.debit_credit: x for x in {self.debit, self.credit}}
for row in result:
debit_credit_dict[row.debit_credit].add_tag(
row.tag_type, row.tag, accounts[row.account_id], row.freq)
def __init_recurring(self) -> None:
"""Initializes the recurring transactions.
:return: None.
"""
recurring: Recurring = options.recurring
accounts: dict[str, Account] \
= self.__get_accounts(recurring.codes)
self.debit.recurring \
= [DescriptionRecurring(x.name, accounts[x.account_code],
x.description_template)
for x in recurring.expenses]
self.credit.recurring \
= [DescriptionRecurring(x.name, accounts[x.account_code],
x.description_template)
for x in recurring.incomes]
@staticmethod
def __get_accounts(codes: set[str]) -> dict[str, Account]:
"""Finds and returns the accounts by codes.
:param codes: The account codes.
:return: The account.
"""
if len(codes) == 0:
return {}
def get_condition(code0: str) -> sa.BinaryExpression:
m: re.Match = re.match(r"^(\d{4})-(\d{3})$", code0)
assert m is not None,\
f"Malformed account code \"{code0}\" for regular transactions."
return sa.and_(Account.base_code == m.group(1),
Account.no == int(m.group(2)))
conditions: list[sa.BinaryExpression] \
= [get_condition(x) for x in codes]
accounts: dict[str, Account] \
= {x.code: x for x in
Account.query.filter(sa.or_(*conditions)).all()}
for code in codes:
assert code in accounts,\
f"Unknown account \"{code}\" for regular transactions."
return accounts
def get_prefix(string: str | sa.Column, separator: str | sa.Column) \
-> sa.Function:
"""Returns the SQL function to find the prefix of a string.
:param string: The string.
:param separator: The separator.
:return: The position of the substring, starting from 1.
"""
return sa.func.substr(string, 0, get_position(string, separator))
def get_position(string: str | sa.Column, substring: str | sa.Column) \
-> sa.Function:
"""Returns the SQL function to find the position of a substring.
:param string: The string.
:param substring: The substring.
:return: The position of the substring, starting from 1.
"""
if db.engine.name == "postgresql":
return sa.func.strpos(string, substring)
return sa.func.instr(string, substring)

View File

@ -0,0 +1,336 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
# 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 operators for different journal entry types.
"""
from abc import ABC, abstractmethod
from typing import Type
from flask import render_template, request, abort
from flask_wtf import FlaskForm
from accounting.journal_entry.forms import JournalEntryForm, \
CashReceiptJournalEntryForm, CashDisbursementJournalEntryForm, \
TransferJournalEntryForm
from accounting.journal_entry.forms.line_item import LineItemForm
from accounting.models import JournalEntry
from accounting.template_globals import default_currency_code
from accounting.utils.journal_entry_types import JournalEntryType
class JournalEntryOperator(ABC):
"""The base journal entry operator."""
CHECK_ORDER: int = -1
"""The order when checking the journal entry operator."""
@property
@abstractmethod
def form(self) -> Type[JournalEntryForm]:
"""Returns the form class.
:return: The form class.
"""
@abstractmethod
def render_create_template(self, form: FlaskForm) -> str:
"""Renders the template for the form to create a journal entry.
:param form: The journal entry form.
:return: the form to create a journal entry.
"""
@abstractmethod
def render_detail_template(self, journal_entry: JournalEntry) -> str:
"""Renders the template for the detail page.
:param journal_entry: The journal entry.
:return: the detail page.
"""
@abstractmethod
def render_edit_template(self, journal_entry: JournalEntry,
form: FlaskForm) -> str:
"""Renders the template for the form to edit a journal entry.
:param journal_entry: The journal entry.
:param form: The form.
:return: the form to edit a journal entry.
"""
@abstractmethod
def is_my_type(self, journal_entry: JournalEntry) -> bool:
"""Checks and returns whether the journal entry belongs to the type.
:param journal_entry: The journal entry.
:return: True if the journal entry belongs to the type, or False
otherwise.
"""
@property
def _line_item_template(self) -> str:
"""Renders and returns the template for the line item sub-form.
:return: The template for the line item sub-form.
"""
return render_template(
"accounting/journal-entry/include/form-line-item.html",
currency_index="CURRENCY_INDEX",
debit_credit="DEBIT_CREDIT",
line_item_index="LINE_ITEM_INDEX",
form=LineItemForm())
class CashReceiptJournalEntry(JournalEntryOperator):
"""A cash receipt journal entry."""
CHECK_ORDER: int = 2
"""The order when checking the journal entry operator."""
@property
def form(self) -> Type[JournalEntryForm]:
"""Returns the form class.
:return: The form class.
"""
return CashReceiptJournalEntryForm
def render_create_template(self, form: CashReceiptJournalEntryForm) -> str:
"""Renders the template for the form to create a journal entry.
:param form: The journal entry form.
:return: the form to create a journal entry.
"""
return render_template(
"accounting/journal-entry/receipt/create.html",
form=form,
journal_entry_type=JournalEntryType.CASH_RECEIPT,
currency_template=self.__currency_template,
line_item_template=self._line_item_template)
def render_detail_template(self, journal_entry: JournalEntry) -> str:
"""Renders the template for the detail page.
:param journal_entry: The journal entry.
:return: the detail page.
"""
return render_template("accounting/journal-entry/receipt/detail.html",
obj=journal_entry)
def render_edit_template(self, journal_entry: JournalEntry,
form: CashReceiptJournalEntryForm) -> str:
"""Renders the template for the form to edit a journal entry.
:param journal_entry: The journal entry.
:param form: The form.
:return: the form to edit a journal entry.
"""
return render_template("accounting/journal-entry/receipt/edit.html",
journal_entry=journal_entry, form=form,
currency_template=self.__currency_template,
line_item_template=self._line_item_template)
def is_my_type(self, journal_entry: JournalEntry) -> bool:
"""Checks and returns whether the journal entry belongs to the type.
:param journal_entry: The journal entry.
:return: True if the journal entry belongs to the type, or False
otherwise.
"""
return journal_entry.is_cash_receipt
@property
def __currency_template(self) -> str:
"""Renders and returns the template for the currency sub-form.
:return: The template for the currency sub-form.
"""
return render_template(
"accounting/journal-entry/receipt/include/form-currency.html",
currency_index="CURRENCY_INDEX",
currency_code_data=default_currency_code(),
credit_total="-")
class CashDisbursementJournalEntry(JournalEntryOperator):
"""A cash disbursement journal entry."""
CHECK_ORDER: int = 1
"""The order when checking the journal entry operator."""
@property
def form(self) -> Type[JournalEntryForm]:
"""Returns the form class.
:return: The form class.
"""
return CashDisbursementJournalEntryForm
def render_create_template(self, form: CashDisbursementJournalEntryForm) \
-> str:
"""Renders the template for the form to create a journal entry.
:param form: The journal entry form.
:return: the form to create a journal entry.
"""
return render_template(
"accounting/journal-entry/disbursement/create.html",
form=form,
journal_entry_type=JournalEntryType.CASH_DISBURSEMENT,
currency_template=self.__currency_template,
line_item_template=self._line_item_template)
def render_detail_template(self, journal_entry: JournalEntry) -> str:
"""Renders the template for the detail page.
:param journal_entry: The journal entry.
:return: the detail page.
"""
return render_template(
"accounting/journal-entry/disbursement/detail.html",
obj=journal_entry)
def render_edit_template(self, journal_entry: JournalEntry,
form: CashDisbursementJournalEntryForm) -> str:
"""Renders the template for the form to edit a journal entry.
:param journal_entry: The journal entry.
:param form: The form.
:return: the form to edit a journal entry.
"""
return render_template(
"accounting/journal-entry/disbursement/edit.html",
journal_entry=journal_entry, form=form,
currency_template=self.__currency_template,
line_item_template=self._line_item_template)
def is_my_type(self, journal_entry: JournalEntry) -> bool:
"""Checks and returns whether the journal entry belongs to the type.
:param journal_entry: The journal entry.
:return: True if the journal entry belongs to the type, or False
otherwise.
"""
return journal_entry.is_cash_disbursement
@property
def __currency_template(self) -> str:
"""Renders and returns the template for the currency sub-form.
:return: The template for the currency sub-form.
"""
return render_template(
"accounting/journal-entry/disbursement/include/form-currency.html",
currency_index="CURRENCY_INDEX",
currency_code_data=default_currency_code(),
debit_total="-")
class TransferJournalEntry(JournalEntryOperator):
"""A transfer journal entry."""
CHECK_ORDER: int = 3
"""The order when checking the journal entry operator."""
@property
def form(self) -> Type[JournalEntryForm]:
"""Returns the form class.
:return: The form class.
"""
return TransferJournalEntryForm
def render_create_template(self, form: TransferJournalEntryForm) -> str:
"""Renders the template for the form to create a journal entry.
:param form: The journal entry form.
:return: the form to create a journal entry.
"""
return render_template(
"accounting/journal-entry/transfer/create.html",
form=form,
journal_entry_type=JournalEntryType.TRANSFER,
currency_template=self.__currency_template,
line_item_template=self._line_item_template)
def render_detail_template(self, journal_entry: JournalEntry) -> str:
"""Renders the template for the detail page.
:param journal_entry: The journal entry.
:return: the detail page.
"""
return render_template("accounting/journal-entry/transfer/detail.html",
obj=journal_entry)
def render_edit_template(self, journal_entry: JournalEntry,
form: TransferJournalEntryForm) -> str:
"""Renders the template for the form to edit a journal entry.
:param journal_entry: The journal entry.
:param form: The form.
:return: the form to edit a journal entry.
"""
return render_template("accounting/journal-entry/transfer/edit.html",
journal_entry=journal_entry, form=form,
currency_template=self.__currency_template,
line_item_template=self._line_item_template)
def is_my_type(self, journal_entry: JournalEntry) -> bool:
"""Checks and returns whether the journal entry belongs to the type.
:param journal_entry: The journal entry.
:return: True if the journal entry belongs to the type, or False
otherwise.
"""
return True
@property
def __currency_template(self) -> str:
"""Renders and returns the template for the currency sub-form.
:return: The template for the currency sub-form.
"""
return render_template(
"accounting/journal-entry/transfer/include/form-currency.html",
currency_index="CURRENCY_INDEX",
currency_code_data=default_currency_code(),
debit_total="-", credit_total="-")
JOURNAL_ENTRY_TYPE_TO_OP: dict[JournalEntryType, JournalEntryOperator] \
= {JournalEntryType.CASH_RECEIPT: CashReceiptJournalEntry(),
JournalEntryType.CASH_DISBURSEMENT: CashDisbursementJournalEntry(),
JournalEntryType.TRANSFER: TransferJournalEntry()}
"""The map from the journal entry types to their operators."""
def get_journal_entry_op(journal_entry: JournalEntry,
is_check_as: bool = False) -> JournalEntryOperator:
"""Returns the journal entry operator that may be specified in the "as"
query parameter. If it is not specified, check the journal entry type from
the journal entry.
:param journal_entry: The journal entry.
:param is_check_as: True to check the "as" parameter, or False otherwise.
:return: None.
"""
if is_check_as and "as" in request.args:
type_dict: dict[str, JournalEntryType] \
= {x.value: x for x in JournalEntryType}
if request.args["as"] not in type_dict:
abort(404)
return JOURNAL_ENTRY_TYPE_TO_OP[type_dict[request.args["as"]]]
for journal_entry_type in sorted(JOURNAL_ENTRY_TYPE_TO_OP.values(),
key=lambda x: x.CHECK_ORDER):
if journal_entry_type.is_my_type(journal_entry):
return journal_entry_type

View File

@ -0,0 +1,83 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# 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 selectable original line items.
"""
from decimal import Decimal
import sqlalchemy as sa
from sqlalchemy.orm import selectinload
from accounting import db
from accounting.models import Account, JournalEntry, JournalEntryLineItem
from accounting.utils.offset_alias import offset_alias
def get_selectable_original_line_items(
line_item_id_on_form: set[int], is_payable: bool,
is_receivable: bool) -> list[JournalEntryLineItem]:
"""Queries and returns the selectable original line items, with their net
balances. The offset amounts of the form is excluded.
:param line_item_id_on_form: The ID of the line items on the form.
:param is_payable: True to check the payable original line items, or False
otherwise.
:param is_receivable: True to check the receivable original line items, or
False otherwise.
:return: The selectable original line items, with their net balances.
"""
assert is_payable or is_receivable
offset: sa.Alias = offset_alias()
net_balance: sa.Label = (JournalEntryLineItem.amount + sa.func.sum(sa.case(
(offset.c.id.in_(line_item_id_on_form), 0),
(offset.c.is_debit == JournalEntryLineItem.is_debit, offset.c.amount),
else_=-offset.c.amount))).label("net_balance")
conditions: list[sa.BinaryExpression] = [Account.is_need_offset]
sub_conditions: list[sa.BinaryExpression] = []
if is_payable:
sub_conditions.append(sa.and_(Account.base_code.startswith("2"),
sa.not_(JournalEntryLineItem.is_debit)))
if is_receivable:
sub_conditions.append(sa.and_(Account.base_code.startswith("1"),
JournalEntryLineItem.is_debit))
conditions.append(sa.or_(*sub_conditions))
select_net_balances: sa.Select \
= sa.select(JournalEntryLineItem.id, net_balance)\
.join(Account)\
.join(offset,
JournalEntryLineItem.id == offset.c.original_line_item_id,
isouter=True)\
.filter(*conditions)\
.group_by(JournalEntryLineItem.id)\
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
net_balances: dict[int, Decimal] \
= {x.id: x.net_balance
for x in db.session.execute(select_net_balances).all()}
line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query\
.filter(JournalEntryLineItem.id.in_({x for x in net_balances}))\
.join(JournalEntry)\
.order_by(JournalEntry.date, JournalEntry.no,
JournalEntryLineItem.is_debit, JournalEntryLineItem.no)\
.options(selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.account),
selectinload(JournalEntryLineItem.journal_entry)).all()
line_items.reverse()
for line_item in line_items:
line_item.net_balance = line_item.amount \
if net_balances[line_item.id] is None \
else net_balances[line_item.id]
return line_items

View File

@ -0,0 +1,239 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
# Copyright (c) 2023-2024 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 views for the journal entry management.
"""
import datetime as dt
from urllib.parse import parse_qsl, urlencode
import sqlalchemy as sa
from flask import Blueprint, render_template, session, redirect, request, \
flash, url_for
from werkzeug.datastructures import ImmutableMultiDict
from accounting import db
from accounting.locale import lazy_gettext
from accounting.models import JournalEntry
from accounting.utils.cast import s
from accounting.utils.flash_errors import flash_form_errors
from accounting.utils.journal_entry_types import JournalEntryType
from accounting.utils.next_uri import inherit_next, or_next
from accounting.utils.permission import has_permission, can_view, can_edit
from accounting.utils.timezone import get_tz_today
from accounting.utils.user import get_current_user_pk
from .forms import sort_journal_entries_in, JournalEntryReorderForm
from .template_filters import with_type, to_transfer, format_amount_input, \
text2html
from .utils.operators import JournalEntryOperator, JOURNAL_ENTRY_TYPE_TO_OP, \
get_journal_entry_op
bp: Blueprint = Blueprint("journal-entry", __name__)
"""The view blueprint for the journal entry management."""
bp.add_app_template_filter(with_type, "accounting_journal_entry_with_type")
bp.add_app_template_filter(to_transfer, "accounting_journal_entry_to_transfer")
bp.add_app_template_filter(format_amount_input,
"accounting_journal_entry_format_amount_input")
bp.add_app_template_filter(text2html, "accounting_journal_entry_text2html")
@bp.get("create/<journalEntryType:journal_entry_type>", endpoint="create")
@has_permission(can_edit)
def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
"""Shows the form to add a journal entry.
:param journal_entry_type: The journal entry type.
:return: The form to add a journal entry.
"""
journal_entry_op: JournalEntryOperator \
= JOURNAL_ENTRY_TYPE_TO_OP[journal_entry_type]
form: journal_entry_op.form
if "form" in session:
form = journal_entry_op.form(
ImmutableMultiDict(parse_qsl(session["form"])))
del session["form"]
form.validate()
else:
form = journal_entry_op.form()
form.date.data = get_tz_today()
return journal_entry_op.render_create_template(form)
@bp.post("store/<journalEntryType:journal_entry_type>", endpoint="store")
@has_permission(can_edit)
def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
"""Adds a journal entry.
:param journal_entry_type: The journal entry type.
:return: The redirection to the journal entry detail on success, or the
journal entry creation form on error.
"""
journal_entry_op: JournalEntryOperator \
= JOURNAL_ENTRY_TYPE_TO_OP[journal_entry_type]
form: journal_entry_op.form = journal_entry_op.form(request.form)
if not form.validate():
flash_form_errors(form)
session["form"] = urlencode(list(request.form.items()))
return redirect(inherit_next(with_type(
url_for("accounting.journal-entry.create",
journal_entry_type=journal_entry_type))))
journal_entry: JournalEntry = JournalEntry()
form.populate_obj(journal_entry)
db.session.add(journal_entry)
db.session.commit()
flash(s(lazy_gettext("The journal entry is added successfully.")),
"success")
return redirect(inherit_next(__get_detail_uri(journal_entry)))
@bp.get("<journalEntry:journal_entry>", endpoint="detail")
@has_permission(can_view)
def show_journal_entry_detail(journal_entry: JournalEntry) -> str:
"""Shows the journal entry detail.
:param journal_entry: The journal entry.
:return: The detail.
"""
journal_entry_op: JournalEntryOperator \
= get_journal_entry_op(journal_entry)
return journal_entry_op.render_detail_template(journal_entry)
@bp.get("<journalEntry:journal_entry>/edit", endpoint="edit")
@has_permission(can_edit)
def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
"""Shows the form to edit a journal entry.
:param journal_entry: The journal entry.
:return: The form to edit the journal entry.
"""
journal_entry_op: JournalEntryOperator \
= get_journal_entry_op(journal_entry, is_check_as=True)
form: journal_entry_op.form
if "form" in session:
form = journal_entry_op.form(
ImmutableMultiDict(parse_qsl(session["form"])))
del session["form"]
form.obj = journal_entry
form.validate()
else:
form = journal_entry_op.form(obj=journal_entry)
return journal_entry_op.render_edit_template(journal_entry, form)
@bp.post("<journalEntry:journal_entry>/update", endpoint="update")
@has_permission(can_edit)
def update_journal_entry(journal_entry: JournalEntry) -> redirect:
"""Updates a journal entry.
:param journal_entry: The journal entry.
:return: The redirection to the journal entry detail on success, or the
journal entry edit form on error.
"""
journal_entry_op: JournalEntryOperator \
= get_journal_entry_op(journal_entry, is_check_as=True)
form: journal_entry_op.form = journal_entry_op.form(request.form)
form.obj = journal_entry
if not form.validate():
flash_form_errors(form)
session["form"] = urlencode(list(request.form.items()))
return redirect(inherit_next(with_type(
url_for("accounting.journal-entry.edit",
journal_entry=journal_entry))))
with db.session.no_autoflush:
form.populate_obj(journal_entry)
if not form.is_modified:
flash(s(lazy_gettext("The journal entry was not modified.")),
"success")
return redirect(inherit_next(__get_detail_uri(journal_entry)))
journal_entry.updated_by_id = get_current_user_pk()
journal_entry.updated_at = sa.func.now()
db.session.commit()
flash(s(lazy_gettext("The journal entry is updated successfully.")),
"success")
return redirect(inherit_next(__get_detail_uri(journal_entry)))
@bp.post("<journalEntry:journal_entry>/delete", endpoint="delete")
@has_permission(can_edit)
def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
"""Deletes a journal entry.
:param journal_entry: The journal entry.
:return: The redirection to the journal entry list on success, or the
journal entry detail on error.
"""
if not journal_entry.can_delete:
flash(s(lazy_gettext("The journal entry cannot be deleted.")), "error")
return redirect(inherit_next(__get_detail_uri(journal_entry)))
journal_entry.delete()
sort_journal_entries_in(journal_entry.date, journal_entry.id)
db.session.commit()
flash(s(lazy_gettext("The journal entry is deleted successfully.")),
"success")
return redirect(or_next(__get_default_page_uri()))
@bp.get("dates/<date:date>", endpoint="order")
@has_permission(can_view)
def show_journal_entry_order(date: dt.date) -> str:
"""Shows the order of the journal entries in a same date.
:param date: The date.
:return: The order of the journal entries in the date.
"""
journal_entries: list[JournalEntry] = JournalEntry.query \
.filter(JournalEntry.date == date) \
.order_by(JournalEntry.no).all()
return render_template("accounting/journal-entry/order.html",
date=date, list=journal_entries)
@bp.post("dates/<date:date>", endpoint="sort")
@has_permission(can_edit)
def sort_journal_entries(date: dt.date) -> redirect:
"""Reorders the journal entries in a date.
:param date: The date.
:return: The redirection to the incoming account or the account list. The
reordering operation does not fail.
"""
form: JournalEntryReorderForm = JournalEntryReorderForm(date)
form.save_order()
if not form.is_modified:
flash(s(lazy_gettext("The order was not modified.")), "success")
return redirect(or_next(__get_default_page_uri()))
db.session.commit()
flash(s(lazy_gettext("The order is updated successfully.")), "success")
return redirect(or_next(__get_default_page_uri()))
def __get_detail_uri(journal_entry: JournalEntry) -> str:
"""Returns the detail URI of a journal entry.
:param journal_entry: The journal entry.
:return: The detail URI of the journal entry.
"""
return url_for("accounting.journal-entry.detail",
journal_entry=journal_entry)
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

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
# Copyright (c) 2023 imacat.
@ -25,8 +25,10 @@ from flask_babel import LazyString, Domain
from flask_babel_js import JAVASCRIPT, c2js
translation_dir: Path = Path(__file__).parent / "translations"
"""The directory of the translation files."""
domain: Domain = Domain(translation_directories=[translation_dir],
domain="accounting")
"""The message domain."""
def gettext(string, **variables) -> str:
@ -39,6 +41,17 @@ def gettext(string, **variables) -> str:
return domain.gettext(string, **variables)
def pgettext(context, string, **variables) -> str:
"""A replacement of the Babel gettext() function..
:param context: The context.
:param string: The message to translate.
:param variables: The variable substitution.
:return: The translated message.
"""
return domain.pgettext(context, string, **variables)
def lazy_gettext(string, **variables) -> LazyString:
"""A replacement of the Babel lazy_gettext() function..
@ -105,10 +118,9 @@ def __babel_js_catalog_view() -> Response:
def init_app(app: Flask, bp: Blueprint) -> None:
"""Initializes the application.
:param bp: The blueprint of the accounting application.
:param app: The Flask application.
:param bp: The blueprint of the accounting application.
:return: None.
"""
bp.add_url_rule("/_jstrans.js", "babel_catalog",
__babel_js_catalog_view)
bp.add_url_rule("/_jstrans.js", "babel_catalog", __babel_js_catalog_view)
app.jinja_env.globals["A_"] = domain.gettext

904
src/accounting/models.py Normal file
View File

@ -0,0 +1,904 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
# 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 data models.
"""
from __future__ import annotations
import datetime as dt
import re
from decimal import Decimal
from typing import Type, Self
import sqlalchemy as sa
from babel import Locale
from flask_babel import get_locale, get_babel
from sqlalchemy import text
from sqlalchemy.orm import Mapped, mapped_column
from accounting import db
from accounting.locale import gettext
from accounting.utils.user import user_cls, user_pk_column
class BaseAccount(db.Model):
"""A base account."""
__tablename__ = "accounting_base_accounts"
"""The table name."""
code: Mapped[str] = mapped_column(primary_key=True)
"""The account code."""
title_l10n: Mapped[str] = mapped_column("title")
"""The title."""
l10n: Mapped[list[BaseAccountL10n]] \
= db.relationship(back_populates="account", lazy=False)
"""The localized titles."""
accounts: Mapped[list[Account]] = db.relationship(back_populates="base")
"""The descendant accounts under the base account."""
def __str__(self) -> str:
"""Returns the string representation of the base account.
:return: The string representation of the base account.
"""
return f"{self.code} {self.title}"
@property
def title(self) -> str:
"""Returns the title in the current locale.
:return: The title in the current locale.
"""
current_locale: Locale = get_locale()
if current_locale == get_babel().instance.default_locale:
return self.title_l10n
for l10n in self.l10n:
if l10n.locale == str(current_locale):
return l10n.title
return self.title_l10n
@property
def query_values(self) -> list[str]:
"""Returns the values to be queried.
:return: The values to be queried.
"""
return [self.code, self.title_l10n] + [x.title for x in self.l10n]
class BaseAccountL10n(db.Model):
"""A localized base account title."""
__tablename__ = "accounting_base_accounts_l10n"
"""The table name."""
account_code: Mapped[str] \
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True)
"""The account code."""
account: Mapped[BaseAccount] = db.relationship(back_populates="l10n")
"""The account."""
locale: Mapped[str] = mapped_column(primary_key=True)
"""The locale."""
title: Mapped[str]
"""The localized title."""
class Account(db.Model):
"""An account."""
__tablename__ = "accounting_accounts"
"""The table name."""
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
"""The account ID."""
base_code: Mapped[str] \
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
ondelete="CASCADE"))
"""The code of the base account."""
base: Mapped[BaseAccount] = db.relationship(back_populates="accounts")
"""The base account."""
no: Mapped[int] = mapped_column(default=text("1"))
"""The account number under the base account."""
title_l10n: Mapped[str] = mapped_column("title")
"""The title."""
is_need_offset: Mapped[bool] = mapped_column(default=False)
"""Whether the journal entry line items of this account need offset."""
created_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
server_default=db.func.now())
"""The date and time when this record was created."""
created_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the user who created the record."""
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
"""The user who created the record."""
updated_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
server_default=db.func.now())
"""The date and time when this record was last updated."""
updated_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the last user who updated the record."""
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
"""The last user who updated the record."""
l10n: Mapped[list[AccountL10n]] \
= db.relationship(back_populates="account", lazy=False)
"""The localized titles."""
line_items: Mapped[list[JournalEntryLineItem]] \
= db.relationship(back_populates="account")
"""The journal entry line items."""
CASH_CODE: str = "1111-001"
"""The code of the cash account,"""
ACCUMULATED_CHANGE_CODE: str = "3351-001"
"""The code of the accumulated-change account,"""
NET_CHANGE_CODE: str = "3353-001"
"""The code of the net-change account,"""
def __str__(self) -> str:
"""Returns the string representation of this account.
:return: The string representation of this account.
"""
return f"{self.base_code}-{self.no:03d} {self.title}"
@property
def code(self) -> str:
"""Returns the code.
:return: The code.
"""
return f"{self.base_code}-{self.no:03d}"
@property
def title(self) -> str:
"""Returns the title in the current locale.
:return: The title in the current locale.
"""
current_locale: Locale = get_locale()
if current_locale == get_babel().instance.default_locale:
return self.title_l10n
for l10n in self.l10n:
if l10n.locale == str(current_locale):
return l10n.title
return self.title_l10n
@title.setter
def title(self, value: str) -> None:
"""Sets the title in the current locale.
:param value: The new title.
:return: None.
"""
if self.title == value:
return
if self.title_l10n is None:
self.title_l10n = value
return
current_locale: Locale = get_locale()
if current_locale == get_babel().instance.default_locale:
self.title_l10n = value
return
for l10n in self.l10n:
if l10n.locale == str(current_locale):
l10n.title = value
return
self.l10n.append(AccountL10n(locale=str(current_locale), title=value))
@property
def is_real(self) -> bool:
"""Returns whether the account is a real account.
:return: True if the account is a real account, or False otherwise.
"""
return self.base_code[0] in {"1", "2", "3"}
@property
def is_nominal(self) -> bool:
"""Returns whether the account is a nominal account.
:return: True if the account is a nominal account, or False otherwise.
"""
return not self.is_real
@property
def count(self) -> int:
"""Returns the number of items in the account.
:return: The number of items in the account.
"""
if not hasattr(self, "__count"):
setattr(self, "__count", 0)
return getattr(self, "__count")
@count.setter
def count(self, value: int) -> None:
"""Sets the number of items in the account.
:param value: The number of items in the account.
:return: None.
"""
setattr(self, "__count", value)
@property
def query_values(self) -> list[str]:
"""Returns the values to be queried.
:return: The values to be queried.
"""
return [self.code, self.title_l10n] + [x.title for x in self.l10n]
@property
def is_modified(self) -> bool:
"""Returns whether a product account was modified.
:return: True if modified, or False otherwise.
"""
if db.session.is_modified(self):
return True
for l10n in self.l10n:
if db.session.is_modified(l10n):
return True
return False
@property
def can_delete(self) -> bool:
"""Returns whether the account can be deleted.
:return: True if the account can be deleted, or False otherwise.
"""
if self.code in {"1111-001", "3351-001", "3353-001"}:
return False
return len(self.line_items) == 0
def delete(self) -> None:
"""Deletes this account.
:return: None.
"""
AccountL10n.query.filter(AccountL10n.account == self).delete()
cls: Type[Self] = self.__class__
cls.query.filter(cls.id == self.id).delete()
@classmethod
def find_by_code(cls, code: str) -> Self | None:
"""Finds an account by its code.
:param code: The code.
:return: The account, or None if this account does not exist.
"""
m = re.match(r"^([1-9]{4})-(\d{3})$", code)
if m is None:
return None
return cls.query.filter(cls.base_code == m.group(1),
cls.no == int(m.group(2))).first()
@classmethod
def selectable_debit(cls) -> list[Self]:
"""Returns the selectable debit accounts.
Payable line items can not start from debit.
:return: The selectable debit accounts.
"""
return cls.query.filter(sa.or_(cls.base_code.startswith("1"),
sa.and_(cls.base_code.startswith("2"),
sa.not_(cls.is_need_offset)),
cls.base_code.startswith("3"),
cls.base_code.startswith("5"),
cls.base_code.startswith("6"),
cls.base_code.startswith("75"),
cls.base_code.startswith("76"),
cls.base_code.startswith("77"),
cls.base_code.startswith("78"),
cls.base_code.startswith("8"),
cls.base_code.startswith("9")),
cls.base_code != "3353")\
.order_by(cls.base_code, cls.no).all()
@classmethod
def selectable_credit(cls) -> list[Self]:
"""Returns the selectable debit accounts.
Receivable line items can not start from credit.
:return: The selectable debit accounts.
"""
return cls.query.filter(sa.or_(sa.and_(cls.base_code.startswith("1"),
sa.not_(cls.is_need_offset)),
cls.base_code.startswith("2"),
cls.base_code.startswith("3"),
cls.base_code.startswith("4"),
cls.base_code.startswith("71"),
cls.base_code.startswith("72"),
cls.base_code.startswith("73"),
cls.base_code.startswith("74"),
cls.base_code.startswith("8"),
cls.base_code.startswith("9")),
cls.base_code != "3353")\
.order_by(cls.base_code, cls.no).all()
@classmethod
def cash(cls) -> Self:
"""Returns the cash account.
:return: The cash account
"""
return cls.find_by_code(cls.CASH_CODE)
@classmethod
def accumulated_change(cls) -> Self:
"""Returns the accumulated-change account.
:return: The accumulated-change account
"""
return cls.find_by_code(cls.ACCUMULATED_CHANGE_CODE)
class AccountL10n(db.Model):
"""A localized account title."""
__tablename__ = "accounting_accounts_l10n"
"""The table name."""
account_id: Mapped[int] \
= mapped_column(db.ForeignKey(Account.id, onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True)
"""The account ID."""
account: Mapped[Account] = db.relationship(back_populates="l10n")
"""The account."""
locale: Mapped[str] = mapped_column(primary_key=True)
"""The locale."""
title: Mapped[str]
"""The localized title."""
class Currency(db.Model):
"""A currency."""
__tablename__ = "accounting_currencies"
"""The table name."""
code: Mapped[str] = mapped_column(primary_key=True)
"""The currency code."""
name_l10n: Mapped[str] = mapped_column("name")
"""The currency name."""
created_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
server_default=db.func.now())
"""The date and time when this record was created."""
created_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the user who created the record."""
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
"""The user who created the record."""
updated_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
server_default=db.func.now())
"""The date and time when this record was last updated."""
updated_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the last user who updated the record."""
updated_by: Mapped[user_cls] \
= db.relationship(foreign_keys=updated_by_id)
"""The last user who updated the record."""
l10n: Mapped[list[CurrencyL10n]] \
= db.relationship(back_populates="currency", lazy=False)
"""The localized names."""
line_items: Mapped[list[JournalEntryLineItem]] \
= db.relationship(back_populates="currency")
"""The journal entry line items."""
def __str__(self) -> str:
"""Returns the string representation of the currency.
:return: The string representation of the currency.
"""
return f"{self.name.title()} ({self.code})"
@property
def name(self) -> str:
"""Returns the name in the current locale.
:return: The name in the current locale.
"""
current_locale: Locale = get_locale()
if current_locale == get_babel().instance.default_locale:
return self.name_l10n
for l10n in self.l10n:
if l10n.locale == str(current_locale):
return l10n.name
return self.name_l10n
@name.setter
def name(self, value: str) -> None:
"""Sets the name in the current locale.
:param value: The new name.
:return: None.
"""
if self.name == value:
return
if self.name_l10n is None:
self.name_l10n = value
return
current_locale: Locale = get_locale()
if current_locale == get_babel().instance.default_locale:
self.name_l10n = value
return
for l10n in self.l10n:
if l10n.locale == str(current_locale):
l10n.name = value
return
self.l10n.append(CurrencyL10n(locale=str(current_locale), name=value))
@property
def is_modified(self) -> bool:
"""Returns whether a product account was modified.
:return: True if modified, or False otherwise.
"""
if db.session.is_modified(self):
return True
for l10n in self.l10n:
if db.session.is_modified(l10n):
return True
return False
@property
def can_delete(self) -> bool:
"""Returns whether the currency can be deleted.
:return: True if the currency can be deleted, or False otherwise.
"""
from accounting.template_globals import default_currency_code
if self.code == default_currency_code():
return False
return len(self.line_items) == 0
def delete(self) -> None:
"""Deletes the currency.
:return: None.
"""
CurrencyL10n.query.filter(CurrencyL10n.currency == self).delete()
cls: Type[Self] = self.__class__
cls.query.filter(cls.code == self.code).delete()
class CurrencyL10n(db.Model):
"""A localized currency name."""
__tablename__ = "accounting_currencies_l10n"
"""The table name."""
currency_code: Mapped[str] \
= mapped_column(db.ForeignKey(Currency.code, onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True)
"""The currency code."""
currency: Mapped[Currency] = db.relationship(back_populates="l10n")
"""The currency."""
locale: Mapped[str] = mapped_column(primary_key=True)
"""The locale."""
name: Mapped[str]
"""The localized name."""
class JournalEntryCurrency:
"""A currency in a journal entry."""
def __init__(self, code: str, debit: list[JournalEntryLineItem],
credit: list[JournalEntryLineItem]):
"""Constructs the currency in the journal entry.
:param code: The currency code.
:param debit: The debit line items.
:param credit: The credit line items.
"""
self.code: str = code
"""The currency code."""
self.debit: list[JournalEntryLineItem] = debit
"""The debit line items."""
self.credit: list[JournalEntryLineItem] = credit
"""The credit line items."""
@property
def name(self) -> str:
"""Returns the currency name.
:return: The currency name.
"""
return db.session.get(Currency, self.code).name
@property
def debit_total(self) -> Decimal:
"""Returns the total amount of the debit line items.
:return: The total amount of the debit line items.
"""
return sum([x.amount for x in self.debit])
@property
def credit_total(self) -> str:
"""Returns the total amount of the credit line items.
:return: The total amount of the credit line items.
"""
return sum([x.amount for x in self.credit])
class JournalEntry(db.Model):
"""A journal entry."""
__tablename__ = "accounting_journal_entries"
"""The table name."""
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
"""The journal entry ID."""
date: Mapped[dt.date]
"""The date."""
no: Mapped[int] = mapped_column(default=text("1"))
"""The journal entry number under the date."""
note: Mapped[str | None]
"""The note."""
created_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
server_default=db.func.now())
"""The date and time when this record was created."""
created_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the user who created the record."""
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
"""The user who created the record."""
updated_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
server_default=db.func.now())
"""The date and time when this record was last updated."""
updated_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the last user who updated the record."""
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
"""The last user who updated the record."""
line_items: Mapped[list[JournalEntryLineItem]] \
= db.relationship(back_populates="journal_entry")
"""The line items."""
def __str__(self) -> str:
"""Returns the string representation of this journal entry.
:return: The string representation of this journal entry.
"""
if self.is_cash_disbursement:
return gettext("Cash Disbursement Journal Entry#%(id)s",
id=self.id)
if self.is_cash_receipt:
return gettext("Cash Receipt Journal Entry#%(id)s", id=self.id)
return gettext("Transfer Journal Entry#%(id)s", id=self.id)
@property
def currencies(self) -> list[JournalEntryCurrency]:
"""Returns the line items categorized by their currencies.
:return: The currency categories.
"""
line_items: list[JournalEntryLineItem] = sorted(self.line_items,
key=lambda x: x.no)
codes: list[str] = []
by_currency: dict[str, list[JournalEntryLineItem]] = {}
for line_item in line_items:
if line_item.currency_code not in by_currency:
codes.append(line_item.currency_code)
by_currency[line_item.currency_code] = []
by_currency[line_item.currency_code].append(line_item)
return [JournalEntryCurrency(code=x,
debit=[y for y in by_currency[x]
if y.is_debit],
credit=[y for y in by_currency[x]
if not y.is_debit])
for x in codes]
@property
def is_cash_receipt(self) -> bool:
"""Returns whether this is a cash receipt journal entry.
:return: True if this is a cash receipt journal entry, or False
otherwise.
"""
for currency in self.currencies:
if len(currency.debit) > 1:
return False
if currency.debit[0].account.code != Account.CASH_CODE:
return False
return True
@property
def is_cash_disbursement(self) -> bool:
"""Returns whether this is a cash disbursement journal entry.
:return: True if this is a cash disbursement journal entry, or False
otherwise.
"""
for currency in self.currencies:
if len(currency.credit) > 1:
return False
if currency.credit[0].account.code != Account.CASH_CODE:
return False
return True
@property
def can_delete(self) -> bool:
"""Returns whether the journal entry can be deleted.
:return: True if the journal entry can be deleted, or False otherwise.
"""
for line_item in self.line_items:
if len(line_item.offsets) > 0:
return False
return True
def delete(self) -> None:
"""Deletes the journal entry.
:return: None.
"""
JournalEntryLineItem.query\
.filter(JournalEntryLineItem.journal_entry_id == self.id).delete()
db.session.delete(self)
class JournalEntryLineItem(db.Model):
"""A line item in the journal entry."""
__tablename__ = "accounting_journal_entry_line_items"
"""The table name."""
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
"""The line item ID."""
journal_entry_id: Mapped[int] \
= mapped_column(db.ForeignKey(JournalEntry.id, onupdate="CASCADE",
ondelete="CASCADE"))
"""The journal entry ID."""
journal_entry: Mapped[JournalEntry] \
= db.relationship(back_populates="line_items")
"""The journal entry."""
is_debit: Mapped[bool]
"""True for a debit line item, or False for a credit line item."""
no: Mapped[int]
"""The line item number under the journal entry and debit or credit."""
original_line_item_id: Mapped[int | None] \
= mapped_column(db.ForeignKey(id, onupdate="CASCADE"))
"""The ID of the original line item."""
original_line_item: Mapped[JournalEntryLineItem | None] \
= db.relationship(remote_side=id, passive_deletes=True)
"""The original line item."""
currency_code: Mapped[str] \
= mapped_column(db.ForeignKey(Currency.code, onupdate="CASCADE"))
"""The currency code."""
currency: Mapped[Currency] = db.relationship(back_populates="line_items")
"""The currency."""
account_id: Mapped[int] \
= mapped_column(db.ForeignKey(Account.id, onupdate="CASCADE"))
"""The account ID."""
account: Mapped[Account] \
= db.relationship(back_populates="line_items", lazy=False)
"""The account."""
description: Mapped[str | None]
"""The description."""
amount: Mapped[Decimal] = mapped_column(db.Numeric(14, 2))
"""The amount."""
def __str__(self) -> str:
"""Returns the string representation of the line item.
:return: The string representation of the line item.
"""
if not hasattr(self, "__str"):
from accounting.template_filters import format_date, format_amount
setattr(self, "__str",
gettext("%(date)s %(description)s %(amount)s",
date=format_date(self.journal_entry.date),
description="" if self.description is None
else self.description,
amount=format_amount(self.amount)))
return getattr(self, "__str")
@property
def account_code(self) -> str:
"""Returns the account code.
:return: The account code.
"""
return self.account.code
@property
def is_need_offset(self) -> bool:
"""Returns whether the line item needs offset.
:return: True if the line item needs offset, or False otherwise.
"""
if not self.account.is_need_offset:
return False
if self.account.base_code[0] == "1" and not self.is_debit:
return False
if self.account.base_code[0] == "2" and self.is_debit:
return False
return True
@property
def debit(self) -> Decimal | None:
"""Returns the debit amount.
:return: The debit amount, or None if this is not a debit line item.
"""
if not hasattr(self, "__debit"):
setattr(self, "__debit", self.amount if self.is_debit else None)
return getattr(self, "__debit")
@debit.setter
def debit(self, value: Decimal | None) -> None:
"""Sets the debit amount.
:param value: The debit amount.
:return: None.
"""
setattr(self, "__debit", value)
@property
def credit(self) -> Decimal | None:
"""Returns the credit amount.
:return: The credit amount, or None if this is not a credit line item.
"""
if not hasattr(self, "__credit"):
setattr(self, "__credit", None if self.is_debit else self.amount)
return getattr(self, "__credit")
@credit.setter
def credit(self, value: Decimal | None) -> None:
"""Sets the credit amount.
:param value: The credit amount.
:return: None.
"""
setattr(self, "__credit", value)
@property
def net_balance(self) -> Decimal:
"""Returns the net balance.
:return: The net balance.
"""
if not hasattr(self, "__net_balance"):
setattr(self, "__net_balance", self.amount + sum(
[x.amount if x.is_debit == self.is_debit else -x.amount
for x in self.offsets]))
return getattr(self, "__net_balance")
@net_balance.setter
def net_balance(self, value: Decimal) -> None:
"""Sets the net balance.
:param value: The net balance.
:return: None.
"""
setattr(self, "__net_balance", value)
@property
def balance(self) -> Decimal:
"""Returns the balance.
:return: The balance.
"""
if not hasattr(self, "__balance"):
setattr(self, "__balance", Decimal("0"))
return getattr(self, "__balance")
@balance.setter
def balance(self, value: Decimal) -> None:
"""Sets the balance.
:param value: The balance.
:return: None.
"""
setattr(self, "__balance", value)
@property
def offsets(self) -> list[Self]:
"""Returns the offset items.
:return: The offset items.
"""
if not hasattr(self, "__offsets"):
cls: Type[Self] = self.__class__
offsets: list[Self] = cls.query.join(JournalEntry)\
.filter(JournalEntryLineItem.original_line_item_id == self.id)\
.order_by(JournalEntry.date, JournalEntry.no,
cls.is_debit, cls.no).all()
setattr(self, "__offsets", offsets)
return getattr(self, "__offsets")
@property
def is_offset(self) -> bool:
"""Returns whether the line item is an offset.
:return: True if the line item is an offset, or False otherwise.
"""
if not hasattr(self, "__is_offset"):
setattr(self, "__is_offset", False)
return getattr(self, "__is_offset")
@is_offset.setter
def is_offset(self, value: bool) -> None:
"""Sets whether the line item is an offset.
:param value: True if the line item is an offset, or False otherwise.
:return: None.
"""
setattr(self, "__is_offset", value)
@property
def match(self) -> Self | None:
"""Returns the match of the line item.
:return: The match of the line item.
"""
if not hasattr(self, "__match"):
setattr(self, "__match", None)
return getattr(self, "__match")
@match.setter
def match(self, value: Self) -> None:
"""Sets the match of the line item.
:param value: The matcho of the line item.
:return: None.
"""
setattr(self, "__match", value)
@property
def query_values(self) -> list[str]:
"""Returns the values to be queried.
:return: The values to be queried.
"""
def format_amount(value: Decimal) -> str:
whole: int = int(value)
frac: Decimal = (value - whole).normalize()
return str(whole) + str(abs(frac))[1:]
return ["{}/{}/{}".format(self.journal_entry.date.year,
self.journal_entry.date.month,
self.journal_entry.date.day),
"" if self.description is None else self.description,
str(self.account),
format_amount(self.amount)]
class Option(db.Model):
"""An option."""
__tablename__ = "accounting_options"
"""The table name."""
name: Mapped[str] = mapped_column(primary_key=True)
"""The name."""
value: Mapped[str] = mapped_column(db.Text)
"""The option value."""
created_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
server_default=db.func.now())
"""The date and time when this record was created."""
created_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the user who created the record."""
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
"""The user who created the record."""
updated_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
server_default=db.func.now())
"""The date and time when this record was last updated."""
updated_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the last user who updated the record."""
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
"""The last user who updated the record."""

View File

@ -1,5 +1,5 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
# Copyright (c) 2023 imacat.
#
@ -14,25 +14,17 @@
# 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 database instance factory for the base account management.
This is to overcome the problem that the database instance needs to be
initialized at compile time, but as a submodule it is only available at run
time.
"""The option management.
"""
from flask_sqlalchemy import SQLAlchemy
db: SQLAlchemy
"""The database instance."""
from flask import Blueprint
def set_db(new_db: SQLAlchemy) -> None:
"""Sets the database instance.
def init_app(bp: Blueprint) -> None:
"""Initialize the application.
:param new_db: The database instance.
:param bp: The blueprint of the accounting application.
:return: None.
"""
global db
db = new_db
from .views import bp as option_bp
bp.register_blueprint(option_bp, url_prefix="/options")

View File

@ -0,0 +1,269 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
# 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 forms for the option management.
"""
from flask import render_template
from flask_babel import LazyString
from flask_wtf import FlaskForm
from wtforms import StringField, FieldList, FormField, IntegerField
from wtforms.validators import DataRequired, ValidationError
from accounting.forms import ACCOUNT_REQUIRED, CurrencyExists, AccountExists, \
IsDebitAccount, IsCreditAccount
from accounting.locale import lazy_gettext
from accounting.models import Account
from accounting.utils.current_account import CurrentAccount
from accounting.utils.options import Options
from accounting.utils.strip_text import strip_text
class CurrentAccountExists:
"""The validator to check that the current account exists."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
if field.data is None or field.data == CurrentAccount.CURRENT_AL_CODE:
return
if Account.find_by_code(field.data) is None:
raise ValidationError(lazy_gettext(
"The account does not exist."))
class AccountNotCurrent:
"""The validator to check that the account is a current account."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
if field.data is None or field.data == CurrentAccount.CURRENT_AL_CODE:
return
if field.data[:2] not in {"11", "12", "21", "22"}:
raise ValidationError(lazy_gettext(
"This is not a current account."))
class NotStartPayableFromExpense:
"""The validator to check that a payable line item does not start from
expense."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
if field.data is None or field.data[0] != "2":
return
account: Account | None = Account.find_by_code(field.data)
if account is not None and account.is_need_offset:
raise ValidationError(lazy_gettext(
"You cannot select a payable account as expense."))
class NotStartReceivableFromIncome:
"""The validator to check that a receivable line item does not start
from income."""
def __call__(self, form: FlaskForm, field: StringField) -> None:
if field.data is None or field.data[0] != "1":
return
account: Account | None = Account.find_by_code(field.data)
if account is not None and account.is_need_offset:
raise ValidationError(lazy_gettext(
"You cannot select a receivable account as income."))
class RecurringItemForm(FlaskForm):
"""The base sub-form to add or update the recurring item."""
no = IntegerField()
"""The order number of this recurring item."""
name = StringField()
"""The name of the recurring item."""
account_code = StringField()
"""The account code."""
description_template = StringField()
"""The description template."""
@property
def account_text(self) -> str | None:
"""Returns the account text.
:return: The account text.
"""
if self.account_code.data is None:
return None
account: Account | None = Account.find_by_code(self.account_code.data)
return None if account is None else str(account)
@property
def all_errors(self) -> list[str | LazyString]:
"""Returns all the errors of the form.
:return: All the errors of the form.
"""
all_errors: list[str | LazyString] = []
for key in self.errors:
if key != "csrf_token":
all_errors.extend(self.errors[key])
return all_errors
class RecurringExpenseForm(RecurringItemForm):
"""The sub-form to add or update the recurring expenses."""
no = IntegerField()
"""The order number of this recurring item."""
name = StringField(
filters=[strip_text],
validators=[DataRequired(lazy_gettext("Please fill in the name."))])
"""The name of the recurring item."""
account_code = StringField(
filters=[strip_text],
validators=[
ACCOUNT_REQUIRED,
AccountExists(),
IsDebitAccount(lazy_gettext("This account is not for expense.")),
NotStartPayableFromExpense()])
"""The account code."""
description_template = StringField(
filters=[strip_text],
validators=[
DataRequired(lazy_gettext(
"Please fill in the description template."))])
"""The template for the line item description."""
class RecurringIncomeForm(RecurringItemForm):
"""The sub-form to add or update the recurring incomes."""
no = IntegerField()
"""The order number of this recurring item."""
name = StringField(
filters=[strip_text],
validators=[DataRequired(lazy_gettext("Please fill in the name."))])
"""The name of the recurring item."""
account_code = StringField(
filters=[strip_text],
validators=[
ACCOUNT_REQUIRED,
AccountExists(),
IsCreditAccount(lazy_gettext("This account is not for income.")),
NotStartReceivableFromIncome()])
"""The account code."""
description_template = StringField(
filters=[strip_text],
validators=[
DataRequired(lazy_gettext(
"Please fill in the description template."))])
"""The description template."""
class RecurringForm(RecurringItemForm):
"""The sub-form for the recurring expenses and incomes."""
expenses = FieldList(FormField(RecurringExpenseForm), name="expense")
"""The recurring expenses."""
incomes = FieldList(FormField(RecurringIncomeForm), name="income")
"""The recurring incomes."""
@property
def item_template(self) -> str:
"""Returns the template of a recurring item.
:return: The template of a recurring item.
"""
return render_template(
"accounting/option/include/form-recurring-item.html",
expense_income="EXPENSE_INCOME",
item_index="ITEM_INDEX",
form=RecurringItemForm())
@property
def expense_accounts(self) -> list[Account]:
"""The expense accounts.
:return: None.
"""
return Account.selectable_debit()
@property
def income_accounts(self) -> list[Account]:
"""The income accounts.
:return: None.
"""
return Account.selectable_credit()
@property
def as_data(self) -> dict[str, list[tuple[str, str, str]]]:
"""Returns the form data.
:return: The form data.
"""
def as_tuple(item: RecurringItemForm) -> tuple[str, str, str]:
return (item.name.data, item.account_code.data,
item.description_template.data)
expenses: list[RecurringItemForm] = [x.form for x in self.expenses]
self.__sort_item_forms(expenses)
incomes: list[RecurringItemForm] = [x.form for x in self.incomes]
self.__sort_item_forms(incomes)
return {"expense": [as_tuple(x) for x in expenses],
"income": [as_tuple(x) for x in incomes]}
@staticmethod
def __sort_item_forms(forms: list[RecurringItemForm]) -> None:
"""Sorts the recurring item sub-forms.
:param forms: The recurring item sub-forms.
:return: None.
"""
ord_by_form: dict[RecurringItemForm, int] \
= {forms[i]: i for i in range(len(forms))}
recv_no: set[int] = {x.no.data for x in forms if x.no.data is not None}
missing_recv_no: int = 100 if len(recv_no) == 0 else max(recv_no) + 100
forms.sort(key=lambda x: (x.no.data or missing_recv_no,
ord_by_form.get(x)))
class OptionForm(FlaskForm):
"""The form to update the options."""
default_currency_code = StringField(
filters=[strip_text],
validators=[
DataRequired(lazy_gettext("Please select the default currency.")),
CurrencyExists()])
"""The default currency code."""
default_ie_account_code = StringField(
filters=[strip_text],
validators=[
DataRequired(lazy_gettext(
"Please select the default account"
" for the income and expenses log.")),
CurrentAccountExists(),
AccountNotCurrent()])
"""The default account code for the income and expenses log."""
recurring = FormField(RecurringForm)
"""The recurring expenses and incomes."""
def populate_obj(self, obj: Options) -> None:
"""Populates the form data into a currency object.
:param obj: The currency object.
:return: None.
"""
obj.default_currency_code = self.default_currency_code.data
obj.default_ie_account_code = self.default_ie_account_code.data
obj.recurring_data = self.recurring.form.as_data
@property
def current_accounts(self) -> list[CurrentAccount]:
"""Returns the current accounts.
:return: The current accounts.
"""
return CurrentAccount.accounts()

View File

@ -0,0 +1,83 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
# 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 views for the option management.
"""
from urllib.parse import parse_qsl, urlencode
from flask import Blueprint, render_template, redirect, session, request, \
flash, url_for
from werkzeug.datastructures import ImmutableMultiDict
from accounting.locale import lazy_gettext
from accounting.utils.cast import s
from accounting.utils.flash_errors import flash_form_errors
from accounting.utils.next_uri import inherit_next
from accounting.utils.options import options
from accounting.utils.permission import has_permission, can_admin
from .forms import OptionForm
bp: Blueprint = Blueprint("option", __name__)
"""The view blueprint for the currency management."""
@bp.get("", endpoint="detail")
@has_permission(can_admin)
def show_options() -> str:
"""Shows the options.
:return: The options.
"""
return render_template("accounting/option/detail.html", obj=options)
@bp.get("edit", endpoint="edit")
@has_permission(can_admin)
def show_option_form() -> str:
"""Shows the option form.
:return: The option form.
"""
form: OptionForm
if "form" in session:
form = OptionForm(ImmutableMultiDict(parse_qsl(session["form"])))
del session["form"]
form.validate()
else:
form = OptionForm(obj=options)
return render_template("accounting/option/form.html", form=form)
@bp.post("update", endpoint="update")
@has_permission(can_admin)
def update_options() -> redirect:
"""Updates the options.
:return: The redirection to the option form.
"""
form = OptionForm(request.form)
if not form.validate():
flash_form_errors(form)
session["form"] = urlencode(list(request.form.items()))
return redirect(inherit_next(url_for("accounting.option.edit")))
form.populate_obj(options)
if not options.is_modified:
flash(s(lazy_gettext("The settings were not modified.")), "success")
return redirect(inherit_next(url_for("accounting.option.detail")))
options.commit()
flash(s(lazy_gettext("The settings are saved successfully.")), "success")
return redirect(inherit_next(url_for("accounting.option.detail")))

View File

@ -0,0 +1,37 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
# 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 report management.
"""
from flask import Flask
def init_app(app: Flask, url_prefix: str) -> None:
"""Initialize the application.
:param app: The Flask application.
:param url_prefix: The URL prefix of the accounting application.
:return: None.
"""
from .converters import PeriodConverter, CurrentAccountConverter, \
NeedOffsetAccountConverter
app.url_map.converters["period"] = PeriodConverter
app.url_map.converters["currentAccount"] = CurrentAccountConverter
app.url_map.converters["needOffsetAccount"] = NeedOffsetAccountConverter
from .views import bp as report_bp
app.register_blueprint(report_bp, url_prefix=url_prefix)

View File

@ -0,0 +1,105 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
# 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 path converters for the report management.
"""
import re
from flask import abort
from werkzeug.routing import BaseConverter
from accounting.models import Account
from accounting.utils.current_account import CurrentAccount
from .period import Period, get_period
class PeriodConverter(BaseConverter):
"""The converter to convert the period specification from and to the
corresponding period in the routes."""
def to_python(self, value: str) -> Period:
"""Converts a period specification to a period.
:param value: The period specification.
:return: The corresponding period.
"""
try:
return get_period(value)
except ValueError:
abort(404)
def to_url(self, value: Period) -> str:
"""Converts a period to its specification.
:param value: The period.
:return: Its specification.
"""
return value.spec
class CurrentAccountConverter(BaseConverter):
"""The converter to convert the current account code from and to the
corresponding account in the routes."""
def to_python(self, value: str) -> CurrentAccount:
"""Converts an account code to an account.
:param value: The account code.
:return: The corresponding account.
"""
if value == CurrentAccount.CURRENT_AL_CODE:
return CurrentAccount.current_assets_and_liabilities()
if not re.match("^[12][12]", value):
abort(404)
account: Account | None = Account.find_by_code(value)
if account is None:
abort(404)
return CurrentAccount(account)
def to_url(self, value: CurrentAccount) -> str:
"""Converts an account to account code.
:param value: The account.
:return: Its code.
"""
return value.code
class NeedOffsetAccountConverter(BaseConverter):
"""The converter to convert the unapplied original line item account code
from and to the corresponding account in the routes."""
def to_python(self, value: str) -> Account:
"""Converts an account code to an account.
:param value: The account code.
:return: The corresponding account.
"""
account: Account | None = Account.find_by_code(value)
if account is None:
abort(404)
if not account.is_need_offset:
abort(404)
return account
def to_url(self, value: Account) -> str:
"""Converts an account to account code.
:param value: The account.
:return: Its code.
"""
return value.code

View File

@ -0,0 +1,22 @@
# The Mia! Accounting 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,98 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023-2024 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 datetime as dt
from collections.abc import Callable
from accounting.models import JournalEntry
from accounting.utils.timezone import get_tz_today
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: 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: 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: JournalEntry | None \
= JournalEntry.query.order_by(JournalEntry.date).first()
start: dt.date | None = None if first is None else first.date
# Attributes
self.data_start: dt.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: dt.date = get_tz_today()
self.has_last_month = start < dt.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 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.
"""
import datetime as dt
from accounting.locale import gettext
def get_desc(start: dt.date | None, end: dt.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: dt.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: dt.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 + dt.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: dt.date, end: dt.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: dt.date, end: dt.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 + dt.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: dt.date, end: dt.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: dt.date) -> str:
"""Formats a month.
:param month: The month.
:return: The formatted month.
"""
return f"{month.year}/{month.month}"
def __format_day(day: dt.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 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
import datetime as dt
def month_end(day: dt.date) -> dt.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 dt.date(day.year, day.month, last_day)

View File

@ -0,0 +1,120 @@
# The Mia! Accounting 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 datetime as dt
import re
from collections.abc import Callable
from typing import Type
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, Type[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[dt.date | None, dt.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) -> dt.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 dt.date(int(year), int(month), int(day))
if month is not None:
return dt.date(int(year), int(month), 1)
return dt.date(int(year), 1, 1)
def __get_end(year: str, month: str | None, day: str | None) -> dt.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 dt.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 dt.date(year_n, month_n, day_n)
return dt.date(int(year), 12, 31)

View File

@ -0,0 +1,129 @@
# The Mia! Accounting 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 datetime as dt
from typing import Self
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: dt.date | None, end: dt.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: dt.date | None = start
"""The start of the period."""
self.end: dt.date | None = end
"""The end of the period."""
self.is_default: bool = False
"""Whether this 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 == dt.date(self.start.year, 1, 1) \
and self.end == dt.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) -> 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 - dt.timedelta(days=1))

View File

@ -0,0 +1,169 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023-2024 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.
"""
import datetime as dt
from accounting.locale import gettext
from accounting.utils.timezone import get_tz_today
from .month_end import month_end
from .period import Period
class ThisMonth(Period):
"""The period of this month."""
def __init__(self):
today: dt.date = get_tz_today()
this_month_start: dt.date = dt.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: dt.date = get_tz_today()
year: int = today.year
month: int = today.month - 1
if month < 1:
year = year - 1
month = 12
start: dt.date = dt.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: dt.date = get_tz_today()
year: int = today.year
month: int = today.month - 1
if month < 1:
year = year - 1
month = 12
start: dt.date = dt.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 = get_tz_today().year
start: dt.date = dt.date(year, 1, 1)
end: dt.date = dt.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 = get_tz_today().year
start: dt.date = dt.date(year - 1, 1, 1)
end: dt.date = dt.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: dt.date = get_tz_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: dt.date = get_tz_today() - dt.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: dt.date = dt.date(year, 1, 1)
end: dt.date = dt.date(year, 12, 31)
super().__init__(start, end)

View File

@ -0,0 +1,120 @@
# The Mia! Accounting 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.
"""
import datetime as dt
def get_spec(start: dt.date | None, end: dt.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: dt.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: dt.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 + dt.timedelta(days=1)).day == 1:
return end.strftime("-%Y-%m")
return end.strftime("-%Y-%m-%d")
def __get_year_spec(start: dt.date, end: dt.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: dt.date, end: dt.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 + dt.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: dt.date, end: dt.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

@ -0,0 +1,26 @@
# The Mia! Accounting 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 reports.
"""
from .balance_sheet import BalanceSheet
from .income_expenses import IncomeExpenses
from .income_statement import IncomeStatement
from .journal import Journal
from .ledger import Ledger
from .search import Search
from .trial_balance import TrialBalance

View File

@ -0,0 +1,477 @@
# The Mia! Accounting 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 balance sheet.
"""
from decimal import Decimal
import sqlalchemy as sa
from flask import render_template, Response
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, BaseAccount, Account, JournalEntry, \
JournalEntryLineItem
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, balance_sheet_url, \
income_statement_url
class ReportAccount:
"""An account in the report."""
def __init__(self, account: Account, amount: Decimal, url: str):
"""Constructs an account in the report.
:param account: The account.
:param amount: The amount.
:param url: The URL to the ledger of the account.
"""
self.account: Account = account
"""The account."""
self.amount: Decimal = amount
"""The amount of the account."""
self.url: str = url
"""The URL to the ledger of the account."""
class Subsection:
"""A subsection."""
def __init__(self, title: BaseAccount):
"""Constructs a subsection.
:param title: The title account.
"""
self.title: BaseAccount = title
"""The title account."""
self.accounts: list[ReportAccount] = []
"""The accounts in the subsection."""
@property
def total(self) -> Decimal:
"""Returns the total of the subsection.
:return: The total of the subsection.
"""
return sum([x.amount for x in self.accounts])
class Section:
"""A section."""
def __init__(self, title: BaseAccount):
"""Constructs a section.
:param title: The title account.
"""
self.title: BaseAccount = title
"""The title account."""
self.subsections: list[Subsection] = []
"""The subsections in the section."""
@property
def total(self) -> Decimal:
"""Returns the total of the section.
:return: The total of the section.
"""
return sum([x.total for x in self.subsections])
class AccountCollector:
"""The balance sheet account collector."""
def __init__(self, currency: Currency, period: Period):
"""Constructs the balance sheet account collector.
:param currency: The currency.
:param period: The period.
"""
self.__currency: Currency = currency
"""The currency."""
self.__period: Period = period
"""The period."""
self.accounts: list[ReportAccount] = self.__query_balances()
"""The balance sheet accounts."""
def __query_balances(self) -> list[ReportAccount]:
"""Queries and returns the balances.
:return: The balances.
"""
sub_conditions: list[sa.BinaryExpression] \
= [Account.base_code.startswith(x) for x in {"1", "2", "3"}]
conditions: list[sa.BinaryExpression] \
= [JournalEntryLineItem.currency_code == self.__currency.code,
sa.or_(*sub_conditions)]
if self.__period.end is not None:
conditions.append(JournalEntry.date <= self.__period.end)
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount)).label("balance")
select_balance: sa.Select \
= sa.select(Account.id, Account.base_code, Account.no,
balance_func)\
.join(JournalEntry).join(Account)\
.filter(*conditions)\
.group_by(Account.id, Account.base_code, Account.no)\
.having(balance_func != 0)\
.order_by(Account.base_code, Account.no)
account_balances: list[sa.Row] \
= db.session.execute(select_balance).all()
self.__all_accounts: list[Account] = Account.query\
.filter(sa.or_(Account.id.in_({x.id for x in account_balances}),
Account.base_code == "3351",
Account.base_code == "3353")).all()
"""The accounts."""
account_by_id: dict[int, Account] \
= {x.id: x for x in self.__all_accounts}
self.accounts: list[ReportAccount] \
= [ReportAccount(account=account_by_id[x.id],
amount=x.balance,
url=ledger_url(self.__currency,
account_by_id[x.id],
self.__period))
for x in account_balances]
"""The accounts on the balance sheet."""
self.__add_accumulated()
self.__add_current_period()
self.accounts.sort(key=lambda x: (x.account.base_code, x.account.no))
for balance in self.accounts:
if not balance.account.base_code.startswith("1"):
balance.amount = -balance.amount
return self.accounts
def __add_accumulated(self) -> None:
"""Adds the accumulated profit or loss to the balances.
:return: None.
"""
self.__add_owner_s_equity(Account.ACCUMULATED_CHANGE_CODE,
self.__query_accumulated(),
self.__period)
def __query_accumulated(self) -> Decimal | None:
"""Queries and returns the accumulated profit or loss.
:return: The accumulated profit or loss.
"""
if self.__period.start is None:
return None
conditions: list[sa.BinaryExpression] \
= [JournalEntryLineItem.currency_code == self.__currency.code,
JournalEntry.date < self.__period.start]
return self.__query_balance(conditions)
def __add_current_period(self) -> None:
"""Adds the accumulated profit or loss to the balances.
:return: None.
"""
self.__add_owner_s_equity(Account.NET_CHANGE_CODE,
self.__query_current_period(),
self.__period)
def __query_current_period(self) -> Decimal | None:
"""Queries and returns the net income or loss for current period.
:return: The net income or loss for current period.
"""
conditions: list[sa.BinaryExpression] \
= [JournalEntryLineItem.currency_code == self.__currency.code]
if self.__period.start is not None:
conditions.append(JournalEntry.date >= self.__period.start)
if self.__period.end is not None:
conditions.append(JournalEntry.date <= self.__period.end)
return self.__query_balance(conditions)
@staticmethod
def __query_balance(conditions: list[sa.BinaryExpression])\
-> Decimal:
"""Queries the balance.
:param conditions: The SQL conditions for the balance.
:return: The balance.
"""
conditions.extend([sa.not_(Account.base_code.startswith(x))
for x in {"1", "2", "3"}])
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount))
select_balance: sa.Select = sa.select(balance_func)\
.join(JournalEntry).join(Account).filter(*conditions)
return db.session.scalar(select_balance)
def __add_owner_s_equity(self, code: str, amount: Decimal | None,
period: Period) -> None:
"""Adds an owner's equity balance.
:param code: The code of the account to add.
:param amount: The amount.
:param period: The period.
:return: None.
"""
if amount is None:
return
url: str = income_statement_url(self.__currency, period)
# There is an existing balance.
account_balance_by_code: dict[str, ReportAccount] \
= {x.account.code: x for x in self.accounts}
if code in account_balance_by_code:
balance: ReportAccount = account_balance_by_code[code]
balance.amount = balance.amount + amount
balance.url = url
return
# Add a new balance
account_by_code: dict[str, Account] \
= {x.code: x for x in self.__all_accounts}
self.accounts.append(ReportAccount(account=account_by_code[code],
amount=amount,
url=url))
class CSVHalfRow:
"""A half row in the CSV."""
def __init__(self, title: str | None, amount: Decimal | None):
"""The constructs a half row in the CSV.
:param title: The title.
:param amount: The amount.
"""
self.title: str | None = title
"""The title."""
self.amount: Decimal | None = amount
"""The amount."""
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self):
"""Constructs a row in the CSV."""
self.asset_title: str | None = None
"""The title of the asset."""
self.asset_amount: Decimal | None = None
"""The amount of the asset."""
self.liability_title: str | None = None
"""The title of the liability."""
self.liability_amount: Decimal | None = None
"""The amount of the liability."""
@property
def values(self) -> list[str | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.
"""
return [self.asset_title, self.asset_amount,
self.liability_title, self.liability_amount]
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, currency: Currency,
period: Period,
has_data: bool,
assets: Section,
liabilities: Section,
owner_s_equity: Section):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param period: The period.
:param has_data: True if there is any data, or False otherwise.
:param assets: The assets.
:param liabilities: The liabilities.
:param owner_s_equity: The owner's equity.
"""
self.currency: Currency = currency
"""The currency."""
self.period: Period = period
"""The period."""
self.__has_data: bool = has_data
"""True if there is any data, or False otherwise."""
self.assets: Section = assets
"""The assets."""
self.liabilities: Section = liabilities
"""The liabilities."""
self.owner_s_equity: Section = owner_s_equity
"""The owner's equity."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: balance_sheet_url(currency, x))
"""The period chooser."""
@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 self.__has_data
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
return ReportChooser(ReportType.BALANCE_SHEET,
currency=self.currency,
period=self.period)
@property
def currency_options(self) -> list[OptionLink]:
"""Returns the currency options.
:return: The currency options.
"""
return self._get_currency_options(
lambda x: balance_sheet_url(x, self.period), self.currency)
class BalanceSheet(BaseReport):
"""The balance sheet."""
def __init__(self, currency: Currency, period: Period):
"""Constructs a balance sheet.
:param currency: The currency.
:param period: The period.
"""
self.__currency: Currency = currency
"""The currency."""
self.__period: Period = period
"""The period."""
self.__has_data: bool
"""True if there is any data, or False otherwise."""
self.__assets: Section
"""The assets."""
self.__liabilities: Section
"""The liabilities."""
self.__owner_s_equity: Section
"""The owner's equity."""
self.__set_data()
def __set_data(self) -> None:
"""Queries and sets assets, the liabilities, and the owner's equity
sections in the balance sheet.
:return: None.
"""
balances: list[ReportAccount] = AccountCollector(
self.__currency, self.__period).accounts
titles: list[BaseAccount] = BaseAccount.query\
.filter(BaseAccount.code.in_({"1", "2", "3"})).all()
subtitles: list[BaseAccount] = BaseAccount.query\
.filter(BaseAccount.code.in_({x.account.base_code[:2]
for x in balances})).all()
sections: dict[str, Section] = {x.code: Section(x) for x in titles}
subsections: dict[str, Subsection] = {x.code: Subsection(x)
for x in subtitles}
for subsection in subsections.values():
sections[subsection.title.code[0]].subsections.append(subsection)
for balance in balances:
subsections[balance.account.base_code[:2]].accounts.append(balance)
self.__has_data = len(balances) > 0
self.__assets = sections["1"]
self.__liabilities = sections["2"]
self.__owner_s_equity = sections["3"]
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = "balance-sheet-{currency}-{period}.csv"\
.format(currency=self.__currency.code,
period=period_spec(self.__period))
return csv_download(filename, self.__get_csv_rows())
def __get_csv_rows(self) -> list[CSVRow]:
"""Composes and returns the CSV rows.
:return: The CSV rows.
"""
asset_rows: list[CSVHalfRow] = self.__section_csv_rows(self.__assets)
liability_rows: list[CSVHalfRow] = []
liability_rows.extend(self.__section_csv_rows(self.__liabilities))
liability_rows.append(CSVHalfRow(gettext("Total"),
self.__liabilities.total))
liability_rows.append(CSVHalfRow(None, None))
liability_rows.extend(self.__section_csv_rows(self.__owner_s_equity))
liability_rows.append(CSVHalfRow(gettext("Total"),
self.__owner_s_equity.total))
rows: list[CSVRow] = [CSVRow() for _ in
range(max(len(asset_rows), len(liability_rows)))]
for i in range(len(rows)):
if i < len(asset_rows):
rows[i].asset_title = asset_rows[i].title
rows[i].asset_amount = asset_rows[i].amount
if i < len(liability_rows) and liability_rows[i].title is not None:
rows[i].liability_title = liability_rows[i].title
rows[i].liability_amount = liability_rows[i].amount
total: CSVRow = CSVRow()
total.asset_title = gettext("Total")
total.asset_amount = self.__assets.total
total.liability_title = gettext("Total")
total.liability_amount \
= self.__liabilities.total + self.__owner_s_equity.total
rows.append(total)
return rows
@staticmethod
def __section_csv_rows(section: Section) -> list[CSVHalfRow]:
"""Gathers the CSV rows for a section.
:param section: The section.
:return: The CSV rows for the section.
"""
rows: list[CSVHalfRow] \
= [CSVHalfRow(section.title.title, None)]
for subsection in section.subsections:
rows.append(CSVHalfRow(f" {subsection.title.title}", None))
for account in subsection.accounts:
rows.append(CSVHalfRow(f" {str(account.account)}",
account.amount))
return rows
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
params: PageParams = PageParams(currency=self.__currency,
period=self.__period,
has_data=self.__has_data,
assets=self.__assets,
liabilities=self.__liabilities,
owner_s_equity=self.__owner_s_equity)
return render_template("accounting/report/balance-sheet.html",
report=params)

View File

@ -0,0 +1,457 @@
# The Mia! Accounting 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 income and expenses log.
"""
import datetime as dt
from decimal import Decimal
import sqlalchemy as sa
from flask import url_for, render_template, Response
from sqlalchemy.orm import selectinload
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, Account, JournalEntry, \
JournalEntryLineItem
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 income_expenses_url
from accounting.utils.current_account import CurrentAccount
from accounting.utils.pagination import Pagination
class ReportLineItem:
"""A line item in the report."""
def __init__(self, line_item: JournalEntryLineItem | None = None):
"""Constructs the line item in the report.
:param line_item: The journal entry line item.
"""
self.is_brought_forward: bool = False
"""Whether this is the brought-forward line item."""
self.is_total: bool = False
"""Whether this is the total line item."""
self.date: dt.date | None = None
"""The date."""
self.account: Account | None = None
"""The account."""
self.description: str | None = None
"""The description."""
self.income: Decimal | None = None
"""The income amount."""
self.expense: Decimal | None = None
"""The expense amount."""
self.balance: Decimal | None = None
"""The balance."""
self.note: str | None = None
"""The note."""
self.url: str | None = None
"""The URL to the journal entry line item."""
if line_item is not None:
self.date = line_item.journal_entry.date
self.account = line_item.account
self.description = line_item.description
self.income = None if line_item.is_debit else line_item.amount
self.expense = line_item.amount if line_item.is_debit else None
self.note = line_item.journal_entry.note
self.url = url_for("accounting.journal-entry.detail",
journal_entry=line_item.journal_entry)
class LineItemCollector:
"""The line item collector."""
def __init__(self, currency: Currency, account: CurrentAccount,
period: Period):
"""Constructs the line item collector.
:param currency: The currency.
:param account: The account.
:param period: The period.
"""
self.__currency: Currency = currency
"""The currency."""
self.__account: CurrentAccount = account
"""The account."""
self.__period: Period = period
"""The period"""
self.brought_forward: ReportLineItem | None
"""The brought-forward line item."""
self.line_items: list[ReportLineItem]
"""The line items."""
self.total: ReportLineItem | None
"""The total line item."""
self.brought_forward = self.__get_brought_forward()
self.line_items = self.__query_line_items()
self.total = self.__get_total()
self.__populate_balance()
def __get_brought_forward(self) -> ReportLineItem | None:
"""Queries, composes and returns the brought-forward line item.
:return: The brought-forward line item, or None if the period starts
from the beginning.
"""
if self.__period.start is None:
return None
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount))
select: sa.Select = sa.Select(balance_func)\
.join(JournalEntry).join(Account)\
.filter(JournalEntryLineItem.currency_code == self.__currency.code,
self.__account_condition,
JournalEntry.date < self.__period.start)
balance: int | None = db.session.scalar(select)
if balance is None:
return None
line_item: ReportLineItem = ReportLineItem()
line_item.is_brought_forward = True
line_item.date = self.__period.start
line_item.account = Account.accumulated_change()
line_item.description = gettext("Brought forward")
if balance > 0:
line_item.income = balance
elif balance < 0:
line_item.expense = -balance
line_item.balance = balance
return line_item
def __query_line_items(self) -> list[ReportLineItem]:
"""Queries and returns the line items.
:return: The line items.
"""
conditions: list[sa.BinaryExpression] \
= [JournalEntryLineItem.currency_code == self.__currency.code,
self.__account_condition]
if self.__period.start is not None:
conditions.append(JournalEntry.date >= self.__period.start)
if self.__period.end is not None:
conditions.append(JournalEntry.date <= self.__period.end)
journal_entry_with_account: sa.Select = sa.Select(JournalEntry.id).\
join(JournalEntryLineItem).join(Account).filter(*conditions)
return [ReportLineItem(x)
for x in JournalEntryLineItem.query
.join(JournalEntry).join(Account)
.filter(JournalEntryLineItem.journal_entry_id
.in_(journal_entry_with_account),
JournalEntryLineItem.currency_code
== self.__currency.code,
sa.not_(self.__account_condition))
.order_by(JournalEntry.date,
JournalEntry.no,
JournalEntryLineItem.is_debit,
JournalEntryLineItem.no)
.options(selectinload(JournalEntryLineItem.account),
selectinload(JournalEntryLineItem.journal_entry))]
@property
def __account_condition(self) -> sa.BinaryExpression:
if self.__account.code == CurrentAccount.CURRENT_AL_CODE:
return CurrentAccount.sql_condition()
return Account.id == self.__account.id
def __get_total(self) -> ReportLineItem | None:
"""Composes the total line item.
:return: The total line item, or None if there is no data.
"""
if self.brought_forward is None and len(self.line_items) == 0:
return None
line_item: ReportLineItem = ReportLineItem()
line_item.is_total = True
line_item.description = gettext("Total")
line_item.income = sum([x.income for x in self.line_items
if x.income is not None])
line_item.expense = sum([x.expense for x in self.line_items
if x.expense is not None])
line_item.balance = line_item.income - line_item.expense
if self.brought_forward is not None:
line_item.balance \
= self.brought_forward.balance + line_item.balance
return line_item
def __populate_balance(self) -> None:
"""Populates the balance of the line items.
:return: None.
"""
balance: Decimal = 0 if self.brought_forward is None \
else self.brought_forward.balance
for line_item in self.line_items:
if line_item.income is not None:
balance = balance + line_item.income
if line_item.expense is not None:
balance = balance - line_item.expense
line_item.balance = balance
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, date: dt.date | str | None,
account: str | None,
description: str | None,
income: str | Decimal | None,
expense: str | Decimal | None,
balance: str | Decimal | None,
note: str | None):
"""Constructs a row in the CSV.
:param date: The journal entry date.
:param account: The account.
:param description: The description.
:param income: The income.
:param expense: The expense.
:param balance: The balance.
:param note: The note.
"""
self.date: dt.date | str | None = date
"""The date."""
self.account: str | None = account
"""The account."""
self.description: str | None = description
"""The description."""
self.income: str | Decimal | None = income
"""The income."""
self.expense: str | Decimal | None = expense
"""The expense."""
self.balance: str | Decimal | None = balance
"""The balance."""
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.account, self.description,
self.income, self.expense, self.balance, self.note]
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, currency: Currency,
account: CurrentAccount,
period: Period,
has_data: bool,
pagination: Pagination[ReportLineItem],
brought_forward: ReportLineItem | None,
line_items: list[ReportLineItem],
total: ReportLineItem | None):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param account: The account.
:param period: The period.
:param has_data: True if there is any data, or False otherwise.
:param brought_forward: The brought-forward line item.
:param line_items: The line items.
:param total: The total line item.
"""
self.currency: Currency = currency
"""The currency."""
self.account: CurrentAccount = account
"""The account."""
self.period: Period = period
"""The period."""
self.__has_data: bool = has_data
"""True if there is any data, or False otherwise."""
self.pagination: Pagination[ReportLineItem] = pagination
"""The pagination."""
self.brought_forward: ReportLineItem | None = brought_forward
"""The brought-forward line item."""
self.line_items: list[ReportLineItem] = line_items
"""The line items."""
self.total: ReportLineItem | None = total
"""The total line item."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: income_expenses_url(currency, account, x))
"""The period chooser."""
@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 self.__has_data
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
if self.account.account is None:
return ReportChooser(ReportType.INCOME_EXPENSES,
currency=self.currency,
account=Account.cash(),
period=self.period)
return ReportChooser(ReportType.INCOME_EXPENSES,
currency=self.currency,
account=self.account.account,
period=self.period)
@property
def currency_options(self) -> list[OptionLink]:
"""Returns the currency options.
:return: The currency options.
"""
return self._get_currency_options(
lambda x: income_expenses_url(x, self.account, self.period),
self.currency)
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
current_al: CurrentAccount \
= CurrentAccount.current_assets_and_liabilities()
options: list[OptionLink] \
= [OptionLink(str(current_al),
income_expenses_url(self.currency, current_al,
self.period),
self.account.id == 0)]
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
.join(Account)\
.filter(JournalEntryLineItem.currency_code == self.currency.code,
CurrentAccount.sql_condition())\
.group_by(JournalEntryLineItem.account_id)
options.extend([OptionLink(str(x),
income_expenses_url(
self.currency,
CurrentAccount(x),
self.period),
x.id == self.account.id)
for x in Account.query.filter(Account.id.in_(in_use))
.order_by(Account.base_code, Account.no).all()])
return options
class IncomeExpenses(BaseReport):
"""The income and expenses log."""
def __init__(self, currency: Currency, account: CurrentAccount,
period: Period):
"""Constructs an income and expenses log.
:param currency: The currency.
:param account: The account.
:param period: The period.
"""
self.__currency: Currency = currency
"""The currency."""
self.__account: CurrentAccount = account
"""The account."""
self.__period: Period = period
"""The period."""
collector: LineItemCollector = LineItemCollector(
self.__currency, self.__account, self.__period)
self.__brought_forward: ReportLineItem | None \
= collector.brought_forward
"""The brought-forward line item."""
self.__line_items: list[ReportLineItem] = collector.line_items
"""The line items."""
self.__total: ReportLineItem | None = collector.total
"""The total line item."""
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = "income-expenses-{currency}-{account}-{period}.csv"\
.format(currency=self.__currency.code, account=self.__account.code,
period=period_spec(self.__period))
return csv_download(filename, self.__get_csv_rows())
def __get_csv_rows(self) -> list[CSVRow]:
"""Composes and returns the CSV rows.
:return: The CSV rows.
"""
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Account"),
gettext("Description"), gettext("Income"),
gettext("Expense"), gettext("Balance"),
gettext("Note"))]
if self.__brought_forward is not None:
rows.append(CSVRow(self.__brought_forward.date,
str(self.__brought_forward.account),
self.__brought_forward.description,
self.__brought_forward.income,
self.__brought_forward.expense,
self.__brought_forward.balance,
None))
rows.extend([CSVRow(x.date, str(x.account), x.description,
x.income, x.expense, x.balance, x.note)
for x in self.__line_items])
if self.__total is not None:
rows.append(CSVRow(gettext("Total"), None, None,
self.__total.income, self.__total.expense,
self.__total.balance, None))
return rows
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
all_line_items: list[ReportLineItem] = []
if self.__brought_forward is not None:
all_line_items.append(self.__brought_forward)
all_line_items.extend(self.__line_items)
if self.__total is not None:
all_line_items.append(self.__total)
pagination: Pagination[ReportLineItem] \
= Pagination[ReportLineItem](all_line_items, is_reversed=True)
page_line_items: list[ReportLineItem] = pagination.list
has_data: bool = len(page_line_items) > 0
brought_forward: ReportLineItem | None = None
if len(page_line_items) > 0 and page_line_items[0].is_brought_forward:
brought_forward = page_line_items[0]
page_line_items = page_line_items[1:]
total: ReportLineItem | None = None
if len(page_line_items) > 0 and page_line_items[-1].is_total:
total = page_line_items[-1]
page_line_items = page_line_items[:-1]
params: PageParams = PageParams(currency=self.__currency,
account=self.__account,
period=self.__period,
has_data=has_data,
pagination=pagination,
brought_forward=brought_forward,
line_items=page_line_items,
total=total)
return render_template("accounting/report/income-expenses.html",
report=params)

View File

@ -0,0 +1,327 @@
# The Mia! Accounting 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 income statement.
"""
from decimal import Decimal
import sqlalchemy as sa
from flask import render_template, Response
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, BaseAccount, Account, JournalEntry, \
JournalEntryLineItem
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, income_statement_url
class ReportAccount:
"""An account in the report."""
def __init__(self, account: Account, amount: Decimal, url: str):
"""Constructs an account in the report.
:param account: The account.
:param amount: The amount.
:param url: The URL to the ledger of the account.
"""
self.account: Account = account
"""The account."""
self.amount: Decimal = amount
"""The amount of the account."""
self.url: str = url
"""The URL to the ledger of the account."""
class AccumulatedTotal:
"""An accumulated total."""
def __init__(self, title: str):
"""Constructs an accumulated total.
:param title: The title.
"""
self.title: str = title
"""The account."""
self.amount: Decimal = Decimal("0")
"""The amount of the account."""
class Subsection:
"""A subsection."""
def __init__(self, title: BaseAccount):
"""Constructs a subsection.
:param title: The title account.
"""
self.title: BaseAccount = title
"""The title account."""
self.accounts: list[ReportAccount] = []
"""The accounts in the subsection."""
@property
def total(self) -> Decimal:
"""Returns the total of the subsection.
:return: The total of the subsection.
"""
return sum([x.amount for x in self.accounts])
class Section:
"""A section."""
def __init__(self, title: BaseAccount, accumulated_title: str):
"""Constructs a section.
:param title: The title account.
:param accumulated_title: The title for the accumulated total.
"""
self.title: BaseAccount = title
"""The title account."""
self.subsections: list[Subsection] = []
"""The subsections in the section."""
self.accumulated: AccumulatedTotal \
= AccumulatedTotal(accumulated_title)
"""The accumulated total."""
@property
def total(self) -> Decimal:
"""Returns the total of the section.
:return: The total of the section.
"""
return sum([x.total for x in self.subsections])
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, text: str | None, amount: str | Decimal | None):
"""Constructs a row in the CSV.
:param text: The text.
:param amount: The amount.
"""
self.text: str | None = text
"""The text."""
self.amount: str | Decimal | None = amount
"""The amount."""
@property
def values(self) -> list[str | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.
"""
return [self.text, self.amount]
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, currency: Currency,
period: Period,
has_data: bool,
sections: list[Section], ):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param period: The period.
:param has_data: True if there is any data, or False otherwise.
"""
self.currency: Currency = currency
"""The currency."""
self.period: Period = period
"""The period."""
self.__has_data: bool = has_data
"""True if there is any data, or False otherwise."""
self.sections: list[Section] = sections
"""The sections in the income statement."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: income_statement_url(currency, x))
"""The period chooser."""
@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 self.__has_data
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
return ReportChooser(ReportType.INCOME_STATEMENT,
currency=self.currency,
period=self.period)
@property
def currency_options(self) -> list[OptionLink]:
"""Returns the currency options.
:return: The currency options.
"""
return self._get_currency_options(
lambda x: income_statement_url(x, self.period), self.currency)
class IncomeStatement(BaseReport):
"""The income statement."""
def __init__(self, currency: Currency, period: Period):
"""Constructs an income statement.
:param currency: The currency.
:param period: The period.
"""
self.__currency: Currency = currency
"""The currency."""
self.__period: Period = period
"""The period."""
self.__has_data: bool
"""True if there is any data, or False otherwise."""
self.__sections: list[Section]
"""The sections."""
self.__set_data()
def __set_data(self) -> None:
"""Queries and sets data sections in the income statement.
:return: None.
"""
balances: list[ReportAccount] = self.__query_balances()
titles: list[BaseAccount] = BaseAccount.query\
.filter(BaseAccount.code.in_({"4", "5", "6", "7", "8", "9"})).all()
subtitles: list[BaseAccount] = BaseAccount.query\
.filter(BaseAccount.code.in_({x.account.base_code[:2]
for x in balances})).all()
total_titles: dict[str, str] \
= {"4": gettext("Total Operating Revenue"),
"5": gettext("Gross Income"),
"6": gettext("Operating Income"),
"7": gettext("Before Tax Income"),
"8": gettext("After Tax Income"),
"9": gettext("Net Income or Loss for Current Period")}
sections: dict[str, Section] \
= {x.code: Section(x, total_titles[x.code]) for x in titles}
subsections: dict[str, Subsection] \
= {x.code: Subsection(x) for x in subtitles}
for subsection in subsections.values():
sections[subsection.title.code[0]].subsections.append(subsection)
for balance in balances:
subsections[balance.account.base_code[:2]].accounts.append(balance)
self.__has_data = len(balances) > 0
self.__sections = sorted(sections.values(), key=lambda x: x.title.code)
total: Decimal = Decimal("0")
for section in self.__sections:
total = total + section.total
section.accumulated.amount = total
def __query_balances(self) -> list[ReportAccount]:
"""Queries and returns the balances.
:return: The balances.
"""
sub_conditions: list[sa.BinaryExpression] \
= [Account.base_code.startswith(str(x)) for x in range(4, 10)]
conditions: list[sa.BinaryExpression] \
= [JournalEntryLineItem.currency_code == self.__currency.code,
sa.or_(*sub_conditions)]
if self.__period.start is not None:
conditions.append(JournalEntry.date >= self.__period.start)
if self.__period.end is not None:
conditions.append(JournalEntry.date <= self.__period.end)
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntryLineItem.is_debit, -JournalEntryLineItem.amount),
else_=JournalEntryLineItem.amount)).label("balance")
select_balances: sa.Select = sa.select(Account.id, balance_func)\
.join(JournalEntry).join(Account)\
.filter(*conditions)\
.group_by(Account.id)\
.having(balance_func != 0)\
.order_by(Account.base_code, Account.no)
balances: list[sa.Row] = db.session.execute(select_balances).all()
accounts: dict[int, Account] \
= {x.id: x for x in Account.query
.filter(Account.id.in_([x.id for x in balances])).all()}
return [ReportAccount(account=accounts[x.id],
amount=x.balance,
url=ledger_url(self.__currency,
accounts[x.id],
self.__period))
for x in balances]
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = "income-statement-{currency}-{period}.csv"\
.format(currency=self.__currency.code,
period=period_spec(self.__period))
return csv_download(filename, self.__get_csv_rows())
def __get_csv_rows(self) -> list[CSVRow]:
"""Composes and returns the CSV rows.
:return: The CSV rows.
"""
total_str: str = gettext("Total")
rows: list[CSVRow] = [CSVRow(None, gettext("Amount"))]
for section in self.__sections:
rows.append(CSVRow(str(section.title), None))
for subsection in section.subsections:
rows.append(CSVRow(f" {str(subsection.title)}", None))
for account in subsection.accounts:
rows.append(CSVRow(f" {str(account.account)}",
account.amount))
rows.append(CSVRow(f" {total_str}", subsection.total))
rows.append(CSVRow(section.accumulated.title,
section.accumulated.amount))
rows.append(CSVRow(None, None))
rows = rows[:-1]
return rows
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
params: PageParams = PageParams(currency=self.__currency,
period=self.__period,
has_data=self.__has_data,
sections=self.__sections)
return render_template("accounting/report/income-statement.html",
report=params)

View File

@ -0,0 +1,223 @@
# The Mia! Accounting 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 journal.
"""
import datetime as dt
from decimal import Decimal
import sqlalchemy as sa
from flask import render_template, Response
from sqlalchemy.orm import selectinload
from accounting.locale import gettext
from accounting.models import Currency, Account, JournalEntry, \
JournalEntryLineItem
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
class ReportLineItem:
"""A line item in the report."""
def __init__(self, line_item: JournalEntryLineItem):
"""Constructs the line item in the report.
:param line_item: The journal entry line item.
"""
self.line_item: JournalEntryLineItem = line_item
"""The journal entry line item."""
self.journal_entry: JournalEntry = line_item.journal_entry
"""The journal entry."""
self.currency: Currency = line_item.currency
"""The account."""
self.account: Account = line_item.account
"""The account."""
self.description: str | None = line_item.description
"""The description."""
self.debit: Decimal | None = line_item.debit
"""The debit amount."""
self.credit: Decimal | None = line_item.credit
"""The credit amount."""
self.amount: Decimal = line_item.amount
"""The amount."""
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, journal_entry_date: str | dt.date,
currency: str,
account: str,
description: str | None,
debit: str | Decimal | None,
credit: str | Decimal | None,
note: str | None):
"""Constructs a row in the CSV.
:param journal_entry_date: The journal entry date.
:param currency: The currency.
:param account: The account.
:param description: The description.
:param debit: The debit amount.
:param credit: The credit amount.
:param note: The note.
"""
self.date: str | dt.date = journal_entry_date
"""The date."""
self.currency: str = currency
"""The currency."""
self.account: str = account
"""The account."""
self.description: str | None = description
"""The description."""
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.description,
self.debit, self.credit, self.note]
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, period: Period,
pagination: Pagination[JournalEntryLineItem],
line_items: list[JournalEntryLineItem]):
"""Constructs the HTML page parameters.
:param period: The period.
:param pagination: The pagination.
:param line_items: The line items.
"""
self.period: Period = period
"""The period."""
self.pagination: Pagination[JournalEntryLineItem] = pagination
"""The pagination."""
self.line_items: list[JournalEntryLineItem] = line_items
"""The line items."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: journal_url(x))
"""The period chooser."""
@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.line_items) > 0
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
return ReportChooser(ReportType.JOURNAL,
period=self.period)
def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:
"""Composes and returns the CSV rows from the line items.
:param line_items: The line items.
:return: The CSV rows.
"""
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
gettext("Account"), gettext("Description"),
gettext("Debit"), gettext("Credit"),
gettext("Note"))]
rows.extend([CSVRow(x.journal_entry.date, x.currency.code,
str(x.account), x.description,
x.debit, x.credit, x.journal_entry.note)
for x in line_items])
return rows
class Journal(BaseReport):
"""The journal."""
def __init__(self, period: Period):
"""Constructs a journal.
:param period: The period.
"""
self.__period: Period = period
"""The period."""
self.__line_items: list[JournalEntryLineItem] \
= self.__query_line_items()
"""The line items."""
def __query_line_items(self) -> list[JournalEntryLineItem]:
"""Queries and returns the line items.
:return: The line items.
"""
conditions: list[sa.BinaryExpression] = []
if self.__period.start is not None:
conditions.append(JournalEntry.date >= self.__period.start)
if self.__period.end is not None:
conditions.append(JournalEntry.date <= self.__period.end)
return JournalEntryLineItem.query.join(JournalEntry)\
.filter(*conditions)\
.order_by(JournalEntry.date,
JournalEntry.no,
JournalEntryLineItem.is_debit.desc(),
JournalEntryLineItem.no)\
.options(selectinload(JournalEntryLineItem.account),
selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.journal_entry)).all()
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = f"journal-{period_spec(self.__period)}.csv"
return csv_download(filename, get_csv_rows(self.__line_items))
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
pagination: Pagination[JournalEntryLineItem] \
= Pagination[JournalEntryLineItem](self.__line_items,
is_reversed=True)
params: PageParams = PageParams(period=self.__period,
pagination=pagination,
line_items=pagination.list)
return render_template("accounting/report/journal.html",
report=params)

View File

@ -0,0 +1,413 @@
# The Mia! Accounting 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 ledger.
"""
import datetime as dt
from decimal import Decimal
import sqlalchemy as sa
from flask import url_for, render_template, Response
from sqlalchemy.orm import selectinload
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, Account, JournalEntry, \
JournalEntryLineItem
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
class ReportLineItem:
"""A line item in the report."""
def __init__(self, line_item: JournalEntryLineItem | None = None):
"""Constructs the line item in the report.
:param line_item: The journal entry line item.
"""
self.is_brought_forward: bool = False
"""Whether this is the brought-forward line item."""
self.is_total: bool = False
"""Whether this is the total line item."""
self.date: dt.date | None = None
"""The date."""
self.description: str | None = None
"""The description."""
self.debit: Decimal | None = None
"""The debit amount."""
self.credit: Decimal | None = None
"""The credit amount."""
self.balance: Decimal | None = None
"""The balance."""
self.note: str | None = None
"""The note."""
self.url: str | None = None
"""The URL to the journal entry line item."""
if line_item is not None:
self.date = line_item.journal_entry.date
self.description = line_item.description
self.debit = line_item.amount if line_item.is_debit else None
self.credit = None if line_item.is_debit else line_item.amount
self.note = line_item.journal_entry.note
self.url = url_for("accounting.journal-entry.detail",
journal_entry=line_item.journal_entry)
class LineItemCollector:
"""The line item collector."""
def __init__(self, currency: Currency, account: Account, period: Period):
"""Constructs the line item collector.
:param currency: The currency.
:param account: The account.
:param period: The period.
"""
self.__currency: Currency = currency
"""The currency."""
self.__account: Account = account
"""The account."""
self.__period: Period = period
"""The period"""
self.brought_forward: ReportLineItem | None
"""The brought-forward line item."""
self.line_items: list[ReportLineItem]
"""The line items."""
self.total: ReportLineItem | None
"""The total line item."""
self.brought_forward = self.__get_brought_forward()
self.line_items = self.__query_line_items()
self.total = self.__get_total()
self.__populate_balance()
def __get_brought_forward(self) -> ReportLineItem | None:
"""Queries, composes and returns the brought-forward line item.
:return: The brought-forward line item, or None if the report starts
from the beginning.
"""
if self.__period.start is None:
return None
if self.__account.is_nominal:
return None
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount))
select: sa.Select = sa.Select(balance_func).join(JournalEntry)\
.filter(JournalEntryLineItem.currency_code == self.__currency.code,
JournalEntryLineItem.account_id == self.__account.id,
JournalEntry.date < self.__period.start)
balance: int | None = db.session.scalar(select)
if balance is None:
return None
line_item: ReportLineItem = ReportLineItem()
line_item.is_brought_forward = True
line_item.date = self.__period.start
line_item.description = gettext("Brought forward")
if balance > 0:
line_item.debit = balance
elif balance < 0:
line_item.credit = -balance
line_item.balance = balance
return line_item
def __query_line_items(self) -> list[ReportLineItem]:
"""Queries and returns the line items.
:return: The line items.
"""
conditions: list[sa.BinaryExpression] \
= [JournalEntryLineItem.currency_code == self.__currency.code,
JournalEntryLineItem.account_id == self.__account.id]
if self.__period.start is not None:
conditions.append(JournalEntry.date >= self.__period.start)
if self.__period.end is not None:
conditions.append(JournalEntry.date <= self.__period.end)
return [ReportLineItem(x) for x in JournalEntryLineItem.query
.join(JournalEntry)
.filter(*conditions)
.order_by(JournalEntry.date,
JournalEntry.no,
JournalEntryLineItem.is_debit.desc(),
JournalEntryLineItem.no)
.options(selectinload(JournalEntryLineItem.journal_entry))
.all()]
def __get_total(self) -> ReportLineItem | None:
"""Composes the total line item.
:return: The total line item, or None if there is no data.
"""
if self.brought_forward is None and len(self.line_items) == 0:
return None
line_item: ReportLineItem = ReportLineItem()
line_item.is_total = True
line_item.description = gettext("Total")
line_item.debit = sum([x.debit for x in self.line_items
if x.debit is not None])
line_item.credit = sum([x.credit for x in self.line_items
if x.credit is not None])
line_item.balance = line_item.debit - line_item.credit
if self.brought_forward is not None:
line_item.balance \
= self.brought_forward.balance + line_item.balance
return line_item
def __populate_balance(self) -> None:
"""Populates the balance of the line items.
:return: None.
"""
if self.__account.is_nominal:
return None
balance: Decimal = 0 if self.brought_forward is None \
else self.brought_forward.balance
for line_item in self.line_items:
if line_item.debit is not None:
balance = balance + line_item.debit
if line_item.credit is not None:
balance = balance - line_item.credit
line_item.balance = balance
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, date: dt.date | str | None,
description: str | None,
debit: str | Decimal | None,
credit: str | Decimal | None,
balance: str | Decimal | None,
note: str | None):
"""Constructs a row in the CSV.
:param date: The journal entry date.
:param description: The description.
:param debit: The debit amount.
:param credit: The credit amount.
:param balance: The balance.
:param note: The note.
"""
self.date: dt.date | str | None = date
"""The date."""
self.description: str | None = description
"""The description."""
self.debit: str | Decimal | None = debit
"""The debit amount."""
self.credit: str | Decimal | None = credit
"""The credit amount."""
self.balance: str | Decimal | None = balance
"""The balance."""
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.description,
self.debit, self.credit, self.balance, self.note]
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, currency: Currency,
account: Account,
period: Period,
has_data: bool,
pagination: Pagination[ReportLineItem],
brought_forward: ReportLineItem | None,
line_items: list[ReportLineItem],
total: ReportLineItem | None):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param account: The account.
:param period: The period.
:param has_data: True if there is any data, or False otherwise.
:param brought_forward: The brought-forward line item.
:param line_items: The line items.
:param total: The total line item.
"""
self.currency: Currency = currency
"""The currency."""
self.account: Account = account
"""The account."""
self.period: Period = period
"""The period."""
self.__has_data: bool = has_data
"""True if there is any data, or False otherwise."""
self.pagination: Pagination[ReportLineItem] = pagination
"""The pagination."""
self.brought_forward: ReportLineItem | None = brought_forward
"""The brought-forward line item."""
self.line_items: list[ReportLineItem] = line_items
"""The line items."""
self.total: ReportLineItem | None = total
"""The total line item."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: ledger_url(currency, account, x))
"""The period chooser."""
@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 self.__has_data
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
return ReportChooser(ReportType.LEDGER,
currency=self.currency,
account=self.account,
period=self.period)
@property
def currency_options(self) -> list[OptionLink]:
"""Returns the currency options.
:return: The currency options.
"""
return self._get_currency_options(
lambda x: ledger_url(x, self.account, self.period), self.currency)
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
.filter(JournalEntryLineItem.currency_code == self.currency.code)\
.group_by(JournalEntryLineItem.account_id)
return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
x.id == self.account.id)
for x in Account.query.filter(Account.id.in_(in_use))
.order_by(Account.base_code, Account.no).all()]
class Ledger(BaseReport):
"""The ledger."""
def __init__(self, currency: Currency, account: Account, period: Period):
"""Constructs a ledger.
:param currency: The currency.
:param account: The account.
:param period: The period.
"""
self.__currency: Currency = currency
"""The currency."""
self.__account: Account = account
"""The account."""
self.__period: Period = period
"""The period."""
collector: LineItemCollector = LineItemCollector(
self.__currency, self.__account, self.__period)
self.__brought_forward: ReportLineItem | None \
= collector.brought_forward
"""The brought-forward line item."""
self.__line_items: list[ReportLineItem] = collector.line_items
"""The line items."""
self.__total: ReportLineItem | None = collector.total
"""The total line item."""
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = "ledger-{currency}-{account}-{period}.csv"\
.format(currency=self.__currency.code, account=self.__account.code,
period=period_spec(self.__period))
return csv_download(filename, self.__get_csv_rows())
def __get_csv_rows(self) -> list[CSVRow]:
"""Composes and returns the CSV rows.
:return: The CSV rows.
"""
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Description"),
gettext("Debit"), gettext("Credit"),
gettext("Balance"), gettext("Note"))]
if self.__brought_forward is not None:
rows.append(CSVRow(self.__brought_forward.date,
self.__brought_forward.description,
self.__brought_forward.debit,
self.__brought_forward.credit,
self.__brought_forward.balance,
None))
rows.extend([CSVRow(x.date, x.description,
x.debit, x.credit, x.balance, x.note)
for x in self.__line_items])
if self.__total is not None:
rows.append(CSVRow(gettext("Total"), None,
self.__total.debit, self.__total.credit,
self.__total.balance, None))
return rows
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
all_line_items: list[ReportLineItem] = []
if self.__brought_forward is not None:
all_line_items.append(self.__brought_forward)
all_line_items.extend(self.__line_items)
if self.__total is not None:
all_line_items.append(self.__total)
pagination: Pagination[ReportLineItem] \
= Pagination[ReportLineItem](all_line_items, is_reversed=True)
page_line_items: list[ReportLineItem] = pagination.list
has_data: bool = len(page_line_items) > 0
brought_forward: ReportLineItem | None = None
if len(page_line_items) > 0 and page_line_items[0].is_brought_forward:
brought_forward = page_line_items[0]
page_line_items = page_line_items[1:]
total: ReportLineItem | None = None
if len(page_line_items) > 0 and page_line_items[-1].is_total:
total = page_line_items[-1]
page_line_items = page_line_items[:-1]
params: PageParams = PageParams(currency=self.__currency,
account=self.__account,
period=self.__period,
has_data=has_data,
pagination=pagination,
brought_forward=brought_forward,
line_items=page_line_items,
total=total)
return render_template("accounting/report/ledger.html",
report=params)

View File

@ -0,0 +1,218 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
# 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 search.
"""
import datetime as dt
from decimal import Decimal
import sqlalchemy as sa
from flask import Response, render_template, request
from sqlalchemy.orm import selectinload
from accounting.locale import gettext
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
JournalEntry, JournalEntryLineItem
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.query import parse_query_keywords
from .journal import get_csv_rows
class LineItemCollector:
"""The line item collector."""
def __init__(self):
"""Constructs the line item collector."""
self.line_items: list[JournalEntryLineItem] = self.__query_line_items()
"""The line items."""
def __query_line_items(self) -> list[JournalEntryLineItem]:
"""Queries and returns the line items.
:return: The line items.
"""
keywords: list[str] = parse_query_keywords(request.args.get("q"))
if len(keywords) == 0:
return []
conditions: list[sa.BinaryExpression] = []
for k in keywords:
sub_conditions: list[sa.BinaryExpression] \
= [JournalEntryLineItem.description.icontains(k),
JournalEntryLineItem.account_id.in_(
self.__get_account_condition(k)),
JournalEntryLineItem.currency_code.in_(
self.__get_currency_condition(k)),
JournalEntryLineItem.journal_entry_id.in_(
self.__get_journal_entry_condition(k))]
try:
sub_conditions.append(
JournalEntryLineItem.amount == Decimal(k))
except ArithmeticError:
pass
conditions.append(sa.or_(*sub_conditions))
return JournalEntryLineItem.query.join(JournalEntry)\
.filter(*conditions)\
.order_by(JournalEntry.date,
JournalEntry.no,
JournalEntryLineItem.is_debit,
JournalEntryLineItem.no)\
.options(selectinload(JournalEntryLineItem.account),
selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.journal_entry)).all()
@staticmethod
def __get_account_condition(k: str) -> sa.Select:
"""Composes and returns the condition to filter the account.
:param k: The keyword.
:return: The condition to filter the account.
"""
code: sa.BinaryExpression = Account.base_code + "-" \
+ sa.func.substr("000" + sa.cast(Account.no, sa.String),
sa.func.char_length(sa.cast(Account.no,
sa.String)) + 1)
select_l10n: sa.Select = sa.select(AccountL10n.account_id)\
.filter(AccountL10n.title.icontains(k))
conditions: list[sa.BinaryExpression] \
= [Account.base_code.contains(k),
Account.title_l10n.icontains(k),
code.contains(k),
Account.id.in_(select_l10n)]
if k in gettext("Needs Offset"):
conditions.append(Account.is_need_offset)
return sa.select(Account.id).filter(sa.or_(*conditions))
@staticmethod
def __get_currency_condition(k: str) -> sa.Select:
"""Composes and returns the condition to filter the currency.
:param k: The keyword.
:return: The condition to filter the currency.
"""
select_l10n: sa.Select = sa.select(CurrencyL10n.currency_code)\
.filter(CurrencyL10n.name.icontains(k))
return sa.select(Currency.code).filter(
sa.or_(Currency.code.icontains(k),
Currency.name_l10n.icontains(k),
Currency.code.in_(select_l10n)))
@staticmethod
def __get_journal_entry_condition(k: str) -> sa.Select:
"""Composes and returns the condition to filter the journal entry.
:param k: The keyword.
:return: The condition to filter the journal entry.
"""
conditions: list[sa.BinaryExpression] \
= [JournalEntry.note.icontains(k)]
date: dt.datetime
try:
date = dt.datetime.strptime(k, "%Y")
conditions.append(
sa.extract("year", JournalEntry.date) == date.year)
except ValueError:
pass
try:
date = dt.datetime.strptime(k, "%Y/%m")
conditions.append(sa.and_(
sa.extract("year", JournalEntry.date) == date.year,
sa.extract("month", JournalEntry.date) == date.month))
except ValueError:
pass
try:
date = dt.datetime.strptime(f"2000/{k}", "%Y/%m/%d")
conditions.append(sa.and_(
sa.extract("month", JournalEntry.date) == date.month,
sa.extract("day", JournalEntry.date) == date.day))
except ValueError:
pass
try:
date = dt.datetime.strptime(k, "%Y/%m/%d")
conditions.append(sa.and_(
sa.extract("year", JournalEntry.date) == date.year,
sa.extract("month", JournalEntry.date) == date.month,
sa.extract("day", JournalEntry.date) == date.day))
except ValueError:
pass
return sa.select(JournalEntry.id).filter(sa.or_(*conditions))
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, pagination: Pagination[JournalEntryLineItem],
line_items: list[JournalEntryLineItem]):
"""Constructs the HTML page parameters.
:param line_items: The search result line items.
"""
self.pagination: Pagination[JournalEntryLineItem] = pagination
"""The pagination."""
self.line_items: list[JournalEntryLineItem] = line_items
"""The line items."""
@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.line_items) > 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.__line_items: list[JournalEntryLineItem] \
= LineItemCollector().line_items
"""The line items."""
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = "search-{q}.csv".format(q=request.args["q"])
return csv_download(filename, get_csv_rows(self.__line_items))
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
pagination: Pagination[JournalEntryLineItem] \
= Pagination[JournalEntryLineItem](self.__line_items,
is_reversed=True)
params: PageParams = PageParams(pagination=pagination,
line_items=pagination.list)
return render_template("accounting/report/search.html",
report=params)

View File

@ -0,0 +1,243 @@
# The Mia! Accounting 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 trial balance.
"""
from decimal import Decimal
import sqlalchemy as sa
from flask import Response, render_template
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, Account, JournalEntry, \
JournalEntryLineItem
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, trial_balance_url
class ReportAccount:
"""An account in the report."""
def __init__(self, account: Account, amount: Decimal, url: str):
"""Constructs an account in the report.
:param account: The account.
:param amount: The amount.
:param url: The URL to the ledger of the account.
"""
self.account: Account = account
"""The account."""
self.debit: Decimal | None = amount if amount > 0 else None
"""The debit amount."""
self.credit: Decimal | None = -amount if amount < 0 else None
"""The credit amount."""
self.url: str = url
"""The URL to the ledger of the account."""
class Total:
"""The totals."""
def __init__(self, debit: Decimal, credit: Decimal):
"""Constructs the total in the trial balance.
:param debit: The debit amount.
:param credit: The credit amount.
"""
self.debit: Decimal | None = debit
"""The debit amount."""
self.credit: Decimal | None = credit
"""The credit amount."""
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, text: str | None,
debit: str | Decimal | None,
credit: str | Decimal | None):
"""Constructs a row in the CSV.
:param text: The text.
:param debit: The debit amount.
:param credit: The credit amount.
"""
self.text: str | None = text
"""The text."""
self.debit: str | Decimal | None = debit
"""The debit amount."""
self.credit: str | Decimal | None = credit
"""The credit amount."""
@property
def values(self) -> list[str | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.
"""
return [self.text, self.debit, self.credit]
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, currency: Currency,
period: Period,
accounts: list[ReportAccount],
total: Total):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param period: The period.
:param accounts: The accounts in the trial balance.
:param total: The total of the trial balance.
"""
self.currency: Currency = currency
"""The currency."""
self.period: Period = period
"""The period."""
self.accounts: list[ReportAccount] = accounts
"""The accounts in the trial balance."""
self.total: Total = total
"""The total of the trial balance."""
self.period_chooser: PeriodChooser = PeriodChooser(
lambda x: trial_balance_url(currency, x))
"""The period chooser."""
@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.accounts) > 0
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
return ReportChooser(ReportType.TRIAL_BALANCE,
currency=self.currency,
period=self.period)
@property
def currency_options(self) -> list[OptionLink]:
"""Returns the currency options.
:return: The currency options.
"""
return self._get_currency_options(
lambda x: trial_balance_url(x, self.period), self.currency)
class TrialBalance(BaseReport):
"""The trial balance."""
def __init__(self, currency: Currency, period: Period):
"""Constructs a trial balance.
:param currency: The currency.
:param period: The period.
"""
self.__currency: Currency = currency
"""The currency."""
self.__period: Period = period
"""The period."""
self.__accounts: list[ReportAccount]
"""The accounts in the trial balance."""
self.__total: Total
"""The total of the trial balance."""
self.__set_data()
def __set_data(self) -> None:
"""Queries and sets data sections in the trial balance.
:return: None.
"""
conditions: list[sa.BinaryExpression] \
= [JournalEntryLineItem.currency_code == self.__currency.code]
if self.__period.start is not None:
conditions.append(JournalEntry.date >= self.__period.start)
if self.__period.end is not None:
conditions.append(JournalEntry.date <= self.__period.end)
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount)).label("balance")
select_balances: sa.Select = sa.select(Account.id, balance_func)\
.join(JournalEntry).join(Account)\
.filter(*conditions)\
.group_by(Account.id)\
.having(balance_func != 0)\
.order_by(Account.base_code, Account.no)
balances: list[sa.Row] = db.session.execute(select_balances).all()
accounts: dict[int, Account] \
= {x.id: x for x in Account.query
.filter(Account.id.in_([x.id for x in balances])).all()}
self.__accounts = [ReportAccount(account=accounts[x.id],
amount=x.balance,
url=ledger_url(self.__currency,
accounts[x.id],
self.__period))
for x in balances]
self.__total = Total(
sum([x.debit for x in self.__accounts if x.debit is not None]),
sum([x.credit for x in self.__accounts if x.credit is not None]))
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = "trial-balance-{currency}-{period}.csv"\
.format(currency=self.__currency.code,
period=period_spec(self.__period))
return csv_download(filename, self.__get_csv_rows())
def __get_csv_rows(self) -> list[CSVRow]:
"""Composes and returns the CSV rows.
:return: The CSV rows.
"""
rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Debit"),
gettext("Credit"))]
rows.extend([CSVRow(str(x.account), x.debit, x.credit)
for x in self.__accounts])
rows.append(CSVRow(gettext("Total"), self.__total.debit,
self.__total.credit))
return rows
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
params: PageParams = PageParams(currency=self.__currency,
period=self.__period,
accounts=self.__accounts,
total=self.__total)
return render_template("accounting/report/trial-balance.html",
report=params)

View File

@ -0,0 +1,216 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/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 unapplied original line items.
"""
import datetime as dt
from decimal import Decimal
from flask import render_template, Response
from sqlalchemy.orm import selectinload
from accounting.locale import gettext
from accounting.models import Currency, Account, JournalEntry, \
JournalEntryLineItem
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
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.unapplied import get_accounts_with_unapplied, \
get_net_balances
from accounting.report.utils.urls import unapplied_url
from accounting.utils.pagination import Pagination
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, journal_entry_date: str | dt.date, currency: str,
description: str | None, amount: str | Decimal,
net_balance: str | Decimal):
"""Constructs a row in the CSV.
:param journal_entry_date: The journal entry date.
:param currency: The currency.
:param description: The description.
:param amount: The amount.
:param net_balance: The net balance.
"""
self.date: str | dt.date = journal_entry_date
"""The date."""
self.currency: str = currency
"""The currency."""
self.description: str | None = description
"""The description."""
self.amount: str | Decimal = amount
"""The amount."""
self.net_balance: str | Decimal = net_balance
"""The net balance."""
@property
def values(self) -> list[str | dt.date | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.
"""
return [self.date, self.currency, self.description, self.amount,
self.net_balance]
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, currency: Currency,
account: Account,
pagination: Pagination[JournalEntryLineItem],
line_items: list[JournalEntryLineItem]):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param account: The account.
:param pagination: The pagination.
:param line_items: The line items.
"""
self.currency: Currency = currency
"""The currency."""
self.account: Account = account
"""The account."""
self.pagination: Pagination[JournalEntryLineItem] = pagination
"""The pagination."""
self.line_items: list[JournalEntryLineItem] = line_items
"""The line items."""
@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.line_items) > 0
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
return ReportChooser(ReportType.UNAPPLIED, currency=self.currency,
account=self.account)
@property
def currency_options(self) -> list[OptionLink]:
"""Returns the currency options.
:return: The currency options.
"""
return self._get_currency_options(
lambda x: unapplied_url(x, self.account), self.currency)
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
options: list[OptionLink] \
= [OptionLink(gettext("Accounts"),
unapplied_url(self.currency, None),
False)]
options.extend(
[OptionLink(str(x), unapplied_url(self.currency, x),
x.id == self.account.id)
for x in get_accounts_with_unapplied(self.currency)])
return options
def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:
"""Composes and returns the CSV rows from the line items.
:param line_items: The line items.
:return: The CSV rows.
"""
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
gettext("Description"), gettext("Amount"),
gettext("Net Balance"))]
rows.extend([CSVRow(x.journal_entry.date, x.currency.code,
x.description, x.amount, x.net_balance)
for x in line_items])
return rows
class UnappliedOriginalLineItems(BaseReport):
"""The unapplied original line items."""
def __init__(self, currency: Currency, account: Account):
"""Constructs the unapplied original line items.
:param currency: The currency.
:param account: The account.
"""
self.__currency: Currency = currency
"""The currency."""
self.__account: Account = account
"""The account."""
self.__line_items: list[JournalEntryLineItem] \
= self.__query_line_items()
"""The line items."""
def __query_line_items(self) -> list[JournalEntryLineItem]:
"""Queries and returns the line items.
:return: The line items.
"""
net_balances: dict[int, Decimal | None] \
= get_net_balances(self.__currency, self.__account)
line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query \
.join(Account).join(JournalEntry) \
.filter(JournalEntryLineItem.id.in_(net_balances)) \
.order_by(JournalEntry.date, JournalEntry.no,
JournalEntryLineItem.is_debit, JournalEntryLineItem.no) \
.options(selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.journal_entry)).all()
for line_item in line_items:
line_item.net_balance = line_item.amount \
if net_balances[line_item.id] is None \
else net_balances[line_item.id]
return line_items
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = "unapplied-{currency}-{account}.csv"\
.format(currency=self.__currency.code, account=self.__account.code)
return csv_download(filename, get_csv_rows(self.__line_items))
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
pagination: Pagination[JournalEntryLineItem] \
= Pagination[JournalEntryLineItem](self.__line_items,
is_reversed=True)
params: PageParams = PageParams(currency=self.__currency,
account=self.__account,
pagination=pagination,
line_items=pagination.list)
return render_template("accounting/report/unapplied.html",
report=params)

View File

@ -0,0 +1,155 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/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 accounts with unapplied original line items.
"""
import datetime as dt
from decimal import Decimal
from flask import render_template, Response
from accounting.locale import gettext
from accounting.models import Currency, Account
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
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.unapplied import get_accounts_with_unapplied
from accounting.report.utils.urls import unapplied_url
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, account: str, count: int | str):
"""Constructs a row in the CSV.
:param account: The account.
:param count: The number of unapplied original line items.
"""
self.account: str = account
"""The currency."""
self.count: int | str = count
"""The number of unapplied original line items."""
@property
def values(self) -> list[str | dt.date | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.
"""
return [self.account, self.count]
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, currency: Currency, accounts: list[Account]):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param accounts: The accounts.
"""
self.currency: Currency = currency
"""The currency."""
self.accounts: list[Account] = accounts
"""The accounts."""
@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.accounts) > 0
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
return ReportChooser(ReportType.UNAPPLIED, currency=self.currency,
account=None)
@property
def currency_options(self) -> list[OptionLink]:
"""Returns the currency options.
:return: The currency options.
"""
return self._get_currency_options(lambda x: unapplied_url(x, None),
self.currency)
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
options: list[OptionLink] \
= [OptionLink(gettext("Accounts"),
unapplied_url(self.currency, None),
True)]
options.extend(
[OptionLink(str(x), unapplied_url(self.currency, x), False)
for x in self.accounts])
return options
def get_csv_rows(accounts: list[Account]) -> list[CSVRow]:
"""Composes and returns the CSV rows from the line items.
:param accounts: The accounts.
:return: The CSV rows.
"""
rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Count"))]
rows.extend([CSVRow(str(x), x.count) for x in accounts])
return rows
class AccountsWithUnappliedOriginalLineItems(BaseReport):
"""The accounts with unapplied original line items."""
def __init__(self, currency: Currency):
"""Constructs the outstanding balances.
:param currency: The currency.
"""
self.__currency: Currency = currency
"""The currency."""
self.__accounts: list[Account] = get_accounts_with_unapplied(currency)
"""The accounts."""
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = "unapplied-accounts.csv"
return csv_download(filename, get_csv_rows(self.__accounts))
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
return render_template("accounting/report/unapplied-accounts.html",
report=PageParams(currency=self.__currency,
accounts=self.__accounts))

View File

@ -0,0 +1,214 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/17
# 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 unmatched offsets.
"""
import datetime as dt
from decimal import Decimal
from flask import render_template, Response
from flask_babel import LazyString
from accounting.locale import gettext
from accounting.models import Currency, Account, JournalEntryLineItem
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
from accounting.report.utils.offset_matcher import OffsetMatcher, OffsetPair
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.unmatched import get_accounts_with_unmatched
from accounting.report.utils.urls import unmatched_url
from accounting.utils.pagination import Pagination
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, journal_entry_date: str | dt.date, currency: str,
description: str | None, debit: str | Decimal,
credit: str | Decimal, balance: str | Decimal):
"""Constructs a row in the CSV.
:param journal_entry_date: The journal entry date.
:param currency: The currency.
:param description: The description.
:param debit: The debit amount.
:param credit: The credit amount.
:param balance: The balance.
"""
self.date: str | dt.date = journal_entry_date
"""The date."""
self.currency: str = currency
"""The currency."""
self.description: str | None = description
"""The description."""
self.debit: str | Decimal | None = debit
"""The debit amount."""
self.credit: str | Decimal | None = credit
"""The credit amount."""
self.balance: str | Decimal = balance
"""The balance."""
@property
def values(self) -> list[str | dt.date | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.
"""
return [self.date, self.currency, self.description, self.debit,
self.credit, self.balance]
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, currency: Currency,
account: Account,
match_status: str | LazyString,
matched_pairs: list[OffsetPair],
pagination: Pagination[JournalEntryLineItem],
line_items: list[JournalEntryLineItem]):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param account: The account.
:param match_status: The match status message.
:param matched_pairs: A list of matched pairs.
:param pagination: The pagination.
:param line_items: The line items.
"""
self.currency: Currency = currency
"""The currency."""
self.account: Account = account
"""The account."""
self.match_status: str | LazyString = match_status
"""The match status message."""
self.matched_pairs: list[OffsetPair] = matched_pairs
"""A list of matched pairs."""
self.pagination: Pagination[JournalEntryLineItem] = pagination
"""The pagination."""
self.line_items: list[JournalEntryLineItem] = line_items
"""The line items."""
@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.line_items) > 0
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
return ReportChooser(ReportType.UNMATCHED, currency=self.currency,
account=self.account)
@property
def currency_options(self) -> list[OptionLink]:
"""Returns the currency options.
:return: The currency options.
"""
return self._get_currency_options(
lambda x: unmatched_url(x, self.account), self.currency)
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
options: list[OptionLink] \
= [OptionLink(gettext("Accounts"),
unmatched_url(self.currency, None),
False)]
options.extend(
[OptionLink(str(x), unmatched_url(self.currency, x),
x.id == self.account.id)
for x in get_accounts_with_unmatched(self.currency)])
return options
def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:
"""Composes and returns the CSV rows from the line items.
:param line_items: The line items.
:return: The CSV rows.
"""
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
gettext("Description"), gettext("Debit"),
gettext("Credit"), gettext("Balance"))]
rows.extend([CSVRow(x.journal_entry.date, x.currency.code,
x.description, x.debit, x.credit, x.balance)
for x in line_items])
return rows
class UnmatchedOffsets(BaseReport):
"""The unmatched offsets."""
def __init__(self, currency: Currency, account: Account):
"""Constructs the unmatched offsets.
:param currency: The currency.
:param account: The account.
"""
self.__currency: Currency = currency
"""The currency."""
self.__account: Account = account
"""The account."""
offset_matcher: OffsetMatcher \
= OffsetMatcher(self.__currency, self.__account)
self.__line_items: list[JournalEntryLineItem] \
= offset_matcher.line_items
"""The line items."""
self.__match_status: str | LazyString = offset_matcher.status
"""The match status message."""
self.__matched_pairs: list[OffsetPair] = offset_matcher.matched_pairs
"""A list of matched pairs."""
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = "unmatched-{currency}-{account}.csv"\
.format(currency=self.__currency.code, account=self.__account.code)
return csv_download(filename, get_csv_rows(self.__line_items))
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
pagination: Pagination[JournalEntryLineItem] \
= Pagination[JournalEntryLineItem](self.__line_items,
is_reversed=True)
params: PageParams = PageParams(currency=self.__currency,
account=self.__account,
match_status=self.__match_status,
matched_pairs=self.__matched_pairs,
pagination=pagination,
line_items=pagination.list)
return render_template("accounting/report/unmatched.html",
report=params)

View File

@ -0,0 +1,156 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/17
# 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 accounts with unmatched offsets.
"""
import datetime as dt
from decimal import Decimal
from flask import render_template, Response
from accounting.locale import gettext
from accounting.models import Currency, Account
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
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.unmatched import get_accounts_with_unmatched
from accounting.report.utils.urls import unmatched_url
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, account: str, count: int | str):
"""Constructs a row in the CSV.
:param account: The account.
:param count: The number of unapplied original line items.
"""
self.account: str = account
"""The currency."""
self.count: int | str = count
"""The number of unapplied original line items."""
@property
def values(self) -> list[str | dt.date | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.
"""
return [self.account, self.count]
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, currency: Currency, accounts: list[Account]):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param accounts: The accounts.
"""
self.currency: Currency = currency
"""The currency."""
self.accounts: list[Account] = accounts
"""The accounts."""
@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.accounts) > 0
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
return ReportChooser(ReportType.UNMATCHED, currency=self.currency,
account=None)
@property
def currency_options(self) -> list[OptionLink]:
"""Returns the currency options.
:return: The currency options.
"""
return self._get_currency_options(lambda x: unmatched_url(x, None),
self.currency)
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
options: list[OptionLink] \
= [OptionLink(gettext("Accounts"),
unmatched_url(self.currency, None),
True)]
options.extend(
[OptionLink(str(x), unmatched_url(self.currency, x), False)
for x in self.accounts])
return options
def get_csv_rows(accounts: list[Account]) -> list[CSVRow]:
"""Composes and returns the CSV rows from the line items.
:param accounts: The accounts.
:return: The CSV rows.
"""
rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Count"))]
rows.extend([CSVRow(str(x), x.count) for x in accounts])
return rows
class AccountsWithUnmatchedOffsets(BaseReport):
"""The accounts with unmatched offsets."""
def __init__(self, currency: Currency):
"""Constructs the outstanding balances.
:param currency: The currency.
"""
self.__currency: Currency = currency
"""The currency."""
self.__accounts: list[Account] \
= get_accounts_with_unmatched(currency)
"""The accounts."""
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = "unmatched-accounts.csv"
return csv_download(filename, get_csv_rows(self.__accounts))
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
return render_template("accounting/report/unmatched-accounts.html",
report=PageParams(currency=self.__currency,
accounts=self.__accounts))

View File

@ -0,0 +1,37 @@
# The Mia! Accounting 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 template filters for the reports.
"""
from decimal import Decimal
from accounting.template_filters import format_amount as core_format_amount
def format_amount(value: Decimal | None) -> str | None:
"""Formats an amount for the report.
:param value: The amount.
:return: The formatted amount text.
"""
if value is None:
return ""
is_negative: bool = value < 0
formatted: str = core_format_amount(abs(value))
if is_negative:
formatted = f"({formatted})"
return formatted

View File

@ -0,0 +1,19 @@
# The Mia! Accounting 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 for the reports.
"""

View File

@ -0,0 +1,89 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/6
# 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 page parameters of a report.
"""
from abc import ABC, abstractmethod
from collections.abc import Callable
from typing import Type
from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \
urlunparse
import sqlalchemy as sa
from flask import request
from accounting import db
from accounting.models import Currency, JournalEntryLineItem
from accounting.utils.journal_entry_types import JournalEntryType
from .option_link import OptionLink
from .report_chooser import ReportChooser
class BasePageParams(ABC):
"""The base HTML page parameters class."""
@property
@abstractmethod
def has_data(self) -> bool:
"""Returns whether there is any data on the page.
:return: True if there is any data, or False otherwise.
"""
@property
@abstractmethod
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
@property
def journal_entry_types(self) -> Type[JournalEntryType]:
"""Returns the journal entry types.
:return: The journal entry types.
"""
return JournalEntryType
@property
def csv_uri(self) -> str:
uri: str = request.full_path if request.query_string \
else request.path
uri_p: ParseResult = urlparse(uri)
params: list[tuple[str, str]] = parse_qsl(uri_p.query)
params = [x for x in params if x[0] != "as"]
params.append(("as", "csv"))
parts: list[str] = list(uri_p)
parts[4] = urlencode(params)
return urlunparse(parts)
@staticmethod
def _get_currency_options(get_url: 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(JournalEntryLineItem.currency_code)
.group_by(JournalEntryLineItem.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,40 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
# 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 base report.
"""
from abc import ABC, abstractmethod
from flask import Response
class BaseReport(ABC):
"""The base report class."""
@abstractmethod
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
@abstractmethod
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""

View File

@ -0,0 +1,109 @@
# The Mia! Accounting 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
import datetime as dt
from abc import ABC, abstractmethod
from decimal import Decimal
from io import StringIO
from urllib.parse import quote
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={quote(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: dt.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: dt.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 + dt.timedelta(days=1)).day == 1:
return end.strftime("%Y%m")
return end.strftime("%Y%m%d")

View File

@ -0,0 +1,178 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/8
# 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 forms for the unmatched offset management.
"""
from decimal import Decimal
import sqlalchemy as sa
from flask_babel import LazyString
from sqlalchemy.orm import selectinload
from accounting.locale import lazy_gettext
from accounting.models import Currency, Account, JournalEntry, \
JournalEntryLineItem
from accounting.report.utils.unapplied import get_net_balances
class OffsetPair:
"""A pair of an original line item and its offset."""
def __init__(self, original_line_item: JournalEntryLineItem,
offset: JournalEntryLineItem):
"""Constructs a pair of an original line item and its offset.
:param original_line_item: The original line item.
:param offset: The offset.
"""
self.original_line_item: JournalEntryLineItem = original_line_item
"""The original line item."""
self.offset: JournalEntryLineItem = offset
"""The offset."""
class OffsetMatcher:
"""The offset matcher."""
def __init__(self, currency: Currency, account: Account):
"""Constructs the offset matcher.
:param currency: The currency.
:param account: The account.
"""
self.__currency: Account = currency
"""The currency."""
self.__account: Account = account
"""The account."""
self.matched_pairs: list[OffsetPair] = []
"""A list of matched pairs."""
self.line_items: list[JournalEntryLineItem] = []
"""The unapplied debits or credits and unmatched offsets."""
self.unapplied: list[JournalEntryLineItem] = []
"""The unapplied debits or credits."""
self.unmatched: list[JournalEntryLineItem] = []
"""The unmatched offsets."""
self.__find_matches()
def __find_matches(self) -> None:
"""Finds the matched original line items and their offsets.
:return: None.
"""
self.__get_line_items()
if len(self.unapplied) == 0 or len(self.unmatched) == 0:
return
remains: list[JournalEntryLineItem] = self.unmatched.copy()
for original_item in self.unapplied:
offset_candidates: list[JournalEntryLineItem] \
= [x for x in remains
if (x.journal_entry.date > original_item.journal_entry.date
or (x.journal_entry.date
== original_item.journal_entry.date
and x.journal_entry.no
> original_item.journal_entry.no))
and x.currency_code == original_item.currency_code
and x.description == original_item.description
and x.amount == original_item.net_balance]
if len(offset_candidates) == 0:
continue
self.matched_pairs.append(
OffsetPair(original_item, offset_candidates[0]))
original_item.match = offset_candidates[0]
offset_candidates[0].match = original_item
remains.remove(offset_candidates[0])
def __get_line_items(self) -> None:
"""Returns the unapplied original line items and unmatched offsets of
the account.
:return: The unapplied original line items and unmatched offsets of the
account.
"""
net_balances: dict[int, Decimal | None] \
= get_net_balances(self.__currency, self.__account)
unmatched_offset_condition: sa.BinaryExpression \
= sa.and_(Account.id == self.__account.id,
JournalEntryLineItem.currency_code
== self.__currency.code,
JournalEntryLineItem.original_line_item_id.is_(None),
sa.or_(sa.and_(Account.base_code.startswith("2"),
JournalEntryLineItem.is_debit),
sa.and_(Account.base_code.startswith("1"),
sa.not_(JournalEntryLineItem.is_debit))))
self.line_items = JournalEntryLineItem.query \
.join(Account).join(JournalEntry) \
.filter(sa.or_(JournalEntryLineItem.id.in_(net_balances),
unmatched_offset_condition)) \
.order_by(JournalEntry.date, JournalEntry.no,
JournalEntryLineItem.is_debit, JournalEntryLineItem.no) \
.options(selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.journal_entry)).all()
for line_item in self.line_items:
line_item.is_offset = line_item.id not in net_balances
self.unapplied = [x for x in self.line_items if not x.is_offset]
for line_item in self.unapplied:
line_item.net_balance = line_item.amount \
if net_balances[line_item.id] is None \
else net_balances[line_item.id]
self.unmatched = [x for x in self.line_items if x.is_offset]
self.__populate_accumulated_balances()
def __populate_accumulated_balances(self) -> None:
"""Populates the accumulated balances of the line items.
:return: None.
"""
balance: Decimal = Decimal("0")
for line_item in self.line_items:
amount: Decimal = line_item.amount if line_item.is_offset \
else line_item.net_balance
if line_item.is_debit:
line_item.debit = amount
line_item.credit = None
balance = balance + amount
else:
line_item.debit = None
line_item.credit = amount
balance = balance - amount
line_item.balance = balance
@property
def status(self) -> str | LazyString:
"""Returns the match status message.
:return: The match status message.
"""
if len(self.unmatched) == 0:
return lazy_gettext("There is no unmatched offset.")
if len(self.matched_pairs) == 0:
return lazy_gettext(
"%(total)s unmatched offsets without original items.",
total=len(self.unmatched))
return lazy_gettext(
"%(matches)s unmatched offsets out of %(total)s"
" can match with their original items.",
matches=len(self.matched_pairs),
total=len(self.unmatched))
def match(self) -> None:
"""Matches the original line items with offsets.
:return: None.
"""
for pair in self.matched_pairs:
pair.offset.original_line_item_id = pair.original_line_item.id

View File

@ -0,0 +1,41 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/5
# 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 option link.
"""
class OptionLink:
"""An option link."""
def __init__(self, title: str, url: str, is_active: bool,
fa_icon: str | None = None):
"""Constructs an option link.
:param title: The title.
:param url: The URL.
:param is_active: True if active, or False otherwise
:param fa_icon: The font-awesome icon, if any.
"""
self.title: str = title
"""The title."""
self.url: str = url
"""The URL."""
self.is_active: bool = is_active
"""True if active, or False otherwise."""
self.fa_icon: str | None = fa_icon
"""The font-awesome icon, if any."""

View File

@ -0,0 +1,198 @@
# The Mia! Accounting 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 report chooser.
This file is largely taken from the NanoParma ERP project, first written in
2021/9/16 by imacat (imacat@nanoparma.com).
"""
import re
from collections.abc import Iterator
from flask_babel import LazyString
from accounting import db
from accounting.locale import gettext
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.current_account import CurrentAccount
from accounting.utils.permission import can_edit
from .option_link import OptionLink
from .report_type import ReportType
from .urls import journal_url, ledger_url, income_expenses_url, \
trial_balance_url, income_statement_url, balance_sheet_url, \
unapplied_url, unmatched_url
class ReportChooser:
"""The report chooser."""
def __init__(self, active_report: ReportType,
period: Period | None = None,
currency: Currency | None = None,
account: Account | None = None):
"""Constructs the report chooser.
:param active_report: The active report.
:param period: The period.
:param currency: The currency.
:param account: The account.
"""
self.__active_report: ReportType = active_report
"""The currently active report."""
self.__period: Period = get_period() if period is None else period
"""The period."""
self.__currency: Currency = db.session.get(
Currency, default_currency_code()) \
if currency is None else currency
"""The currency."""
self.__account: Account = Account.cash() if account is None \
else account
"""The currency."""
self.__reports: list[OptionLink] = []
"""The links to the reports."""
self.current_report: str | LazyString = ""
"""The title of the current report."""
self.is_search: bool = active_report == ReportType.SEARCH
"""Whether the current report is the search page."""
self.__reports.append(self.__income_expenses)
self.__reports.append(self.__ledger)
self.__reports.append(self.__journal)
self.__reports.append(self.__trial_balance)
self.__reports.append(self.__income_statement)
self.__reports.append(self.__balance_sheet)
self.__reports.append(self.__unapplied)
if can_edit():
self.__reports.append(self.__unmatched)
for report in self.__reports:
if report.is_active:
self.current_report = report.title
if self.is_search:
self.current_report = gettext("Search")
@property
def __income_expenses(self) -> OptionLink:
"""Returns the income and expenses log.
:return: The income and expenses log.
"""
account: Account = self.__account
if not re.match(r"[12][12]", account.base_code):
account: Account = Account.cash()
return OptionLink(gettext("Income and Expenses Log"),
income_expenses_url(self.__currency,
CurrentAccount(account),
self.__period),
self.__active_report == ReportType.INCOME_EXPENSES,
fa_icon="fa-solid fa-money-bill-wave")
@property
def __ledger(self) -> OptionLink:
"""Returns the ledger.
:return: The ledger.
"""
return OptionLink(gettext("Ledger"),
ledger_url(self.__currency, self.__account,
self.__period),
self.__active_report == ReportType.LEDGER,
fa_icon="fa-solid fa-clipboard")
@property
def __journal(self) -> OptionLink:
"""Returns the journal.
:return: The journal.
"""
return OptionLink(gettext("Journal"), journal_url(self.__period),
self.__active_report == ReportType.JOURNAL,
fa_icon="fa-solid fa-book")
@property
def __trial_balance(self) -> OptionLink:
"""Returns the trial balance.
:return: The trial balance.
"""
return OptionLink(gettext("Trial Balance"),
trial_balance_url(self.__currency, self.__period),
self.__active_report == ReportType.TRIAL_BALANCE,
fa_icon="fa-solid fa-scale-unbalanced")
@property
def __income_statement(self) -> OptionLink:
"""Returns the income statement.
:return: The income statement.
"""
return OptionLink(gettext("Income Statement"),
income_statement_url(self.__currency, self.__period),
self.__active_report == ReportType.INCOME_STATEMENT,
fa_icon="fa-solid fa-file-invoice-dollar")
@property
def __balance_sheet(self) -> OptionLink:
"""Returns the balance sheet.
:return: The balance sheet.
"""
return OptionLink(gettext("Balance Sheet"),
balance_sheet_url(self.__currency, self.__period),
self.__active_report == ReportType.BALANCE_SHEET,
fa_icon="fa-solid fa-scale-balanced")
@property
def __unapplied(self) -> OptionLink:
"""Returns the unapplied original line items.
:return: The unapplied original line items.
"""
account: Account = self.__account
if not account.is_need_offset:
return OptionLink(gettext("Unapplied Items"),
unapplied_url(self.__currency, None),
self.__active_report == ReportType.UNAPPLIED,
fa_icon="fa-solid fa-link-slash")
return OptionLink(gettext("Unapplied Items"),
unapplied_url(self.__currency, self.__account),
self.__active_report == ReportType.UNAPPLIED,
fa_icon="fa-solid fa-link-slash")
@property
def __unmatched(self) -> OptionLink:
"""Returns the unmatched offsets.
:return: The unmatched offsets.
"""
account: Account = self.__account
if not account.is_need_offset:
return OptionLink(gettext("Unmatched Offsets"),
unmatched_url(self.__currency, None),
self.__active_report == ReportType.UNMATCHED,
fa_icon="fa-solid fa-file-circle-question")
return OptionLink(gettext("Unmatched Offsets"),
unmatched_url(self.__currency, self.__account),
self.__active_report == ReportType.UNMATCHED,
fa_icon="fa-solid fa-file-circle-question")
def __iter__(self) -> Iterator[OptionLink]:
"""Returns the iteration of the reports.
:return: The iteration of the reports.
"""
return iter(self.__reports)

View File

@ -0,0 +1,42 @@
# The Mia! Accounting 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 report types.
"""
from enum import Enum
class ReportType(Enum):
"""The report types."""
JOURNAL: str = "journal"
"""The journal."""
LEDGER: str = "ledger"
"""The ledger."""
INCOME_EXPENSES: str = "income-expenses"
"""The income and expenses log."""
TRIAL_BALANCE: str = "trial-balance"
"""The trial balance."""
INCOME_STATEMENT: str = "income-statement"
"""The income statement."""
BALANCE_SHEET: str = "balance-sheet"
"""The balance sheet."""
UNAPPLIED: str = "unapplied"
"""The unapplied original line items."""
UNMATCHED: str = "unmatched"
"""The unmatched offsets."""
SEARCH: str = "search"
"""The search."""

View File

@ -0,0 +1,104 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/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 unapplied original line item utilities.
"""
from decimal import Decimal
import sqlalchemy as sa
from accounting import db
from accounting.models import Currency, Account, JournalEntry, \
JournalEntryLineItem
from accounting.utils.offset_alias import offset_alias
def get_accounts_with_unapplied(currency: Currency) -> list[Account]:
"""Returns the accounts with unapplied original line items.
:param currency: The currency.
:return: The accounts with unapplied original line items.
"""
offset: sa.Alias = offset_alias()
net_balance: sa.Label \
= (JournalEntryLineItem.amount
+ sa.func.sum(sa.case(
(offset.c.is_debit == JournalEntryLineItem.is_debit,
offset.c.amount),
else_=-offset.c.amount))).label("net_balance")
select_unapplied: sa.Select \
= sa.select(JournalEntryLineItem.id)\
.join(JournalEntry).join(Account)\
.join(offset,
JournalEntryLineItem.id == offset.c.original_line_item_id,
isouter=True)\
.filter(Account.is_need_offset,
JournalEntryLineItem.currency_code == currency.code,
sa.or_(sa.and_(Account.base_code.startswith("2"),
sa.not_(JournalEntryLineItem.is_debit)),
sa.and_(Account.base_code.startswith("1"),
JournalEntryLineItem.is_debit)))\
.group_by(JournalEntryLineItem.id)\
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
count_func: sa.Label \
= sa.func.count(JournalEntryLineItem.id).label("count")
select: sa.Select = sa.select(Account.id, count_func)\
.join(JournalEntryLineItem, isouter=True)\
.filter(JournalEntryLineItem.id.in_(select_unapplied))\
.group_by(Account.id)\
.having(count_func > 0)
counts: dict[int, int] \
= {x.id: x.count for x in db.session.execute(select)}
accounts: list[Account] = Account.query.filter(Account.id.in_(counts))\
.order_by(Account.base_code, Account.no).all()
for account in accounts:
account.count = counts[account.id]
return accounts
def get_net_balances(currency: Currency, account: Account) \
-> dict[int, Decimal | None]:
"""Returns the net balances of the unapplied line items of the account.
:param currency: The currency.
:param account: The account.
:return: The net balances of the unapplied line items of the account.
"""
offset: sa.Alias = offset_alias()
net_balance: sa.Label \
= (JournalEntryLineItem.amount
+ sa.func.sum(sa.case(
(offset.c.is_debit == JournalEntryLineItem.is_debit,
offset.c.amount),
else_=-offset.c.amount))).label("net_balance")
select_net_balances: sa.Select \
= sa.select(JournalEntryLineItem.id, net_balance) \
.join(JournalEntry).join(Account) \
.join(offset,
JournalEntryLineItem.id == offset.c.original_line_item_id,
isouter=True) \
.filter(Account.id == account.id,
JournalEntryLineItem.currency_code == currency.code,
sa.or_(sa.and_(Account.base_code.startswith("2"),
sa.not_(JournalEntryLineItem.is_debit)),
sa.and_(Account.base_code.startswith("1"),
JournalEntryLineItem.is_debit))) \
.group_by(JournalEntryLineItem.id) \
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
return {x.id: x.net_balance
for x in db.session.execute(select_net_balances).all()}

Some files were not shown because too many files have changed in this diff Show More