87 Commits

Author SHA1 Message Date
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
192 changed files with 1691 additions and 1268 deletions

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

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.

View File

@ -1,24 +1,33 @@
=====================
Mia! Accounting Flask
=====================
===============
Mia! Accounting
===============
Description
===========
This is the Mia! Accounting Flask project. It is an accounting
This is the Mia! Accounting project. It is an accounting
module for the Flask_ applications.
Install
=======
Install the latest source from the
`Mia! Accounting Flask repository`_.
Install ``mia-accounting`` with ``pip``.
::
pip install git+https://gitea.imacat.idv.tw/imacat/mia-accounting-flask.git
pip install mia-accounting
Usage
=====
This needs to be done. Currently, you can refer to the test site
located in the test directory on the `Mia! Accounting repository`_.
The test site is running as the
`live demonstration for Mia! Accounting`_.
Copyright
@ -47,4 +56,5 @@ Authors
| 2023/1/27
.. _Flask: https://flask.palletsprojects.com
.. _Mia! Accounting Flask repository: https://gitea.imacat.idv.tw/imacat/mia-accounting-flask
.. _Mia! Accounting repository: https://github.com/imacat/mia-accounting
.. _live demonstration for Mia! Accounting: https://accounting.imacat.idv.tw

View File

@ -10,10 +10,10 @@ sys.path.insert(0, os.path.abspath('../../src/'))
# -- 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.9.0'
release = '0.11.1'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@ -1,10 +1,10 @@
.. 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

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

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.
@ -16,16 +16,16 @@
# limitations under the License.
[metadata]
name = mia-accounting-flask
version = 0.9.0
name = mia-accounting
version = 0.11.1
author = imacat
author_email = imacat@mail.imacat.idv.tw
description = The Mia! Accounting Flask project.
description = The Mia! Accounting project.
long_description = file: README.rst
long_description_content_type = text/x-rst
url = https://github.com/imacat/mia-accounting-flask
url = https://github.com/imacat/mia-accounting
project_urls =
Bug Tracker = https://github.com/imacat/mia-accounting-flask/issues
Bug Tracker = https://github.com/imacat/mia-accounting/issues
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: Apache Software License

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.
@ -47,7 +47,6 @@ def init_app(app: Flask, user_utils: UserUtilityInterface,
init_user_utils(user_utils)
bp: Blueprint = Blueprint("accounting", __name__,
url_prefix=url_prefix,
template_folder="templates",
static_folder="static")
@ -84,9 +83,9 @@ def init_app(app: Flask, user_utils: UserUtilityInterface,
journal_entry.init_app(app, bp)
from . import report
report.init_app(app, bp)
report.init_app(app, url_prefix)
from . import option
option.init_app(bp)
app.register_blueprint(bp)
app.register_blueprint(bp, url_prefix=url_prefix)

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/31
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
# Copyright (c) 2023 imacat.

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.

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.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
# Copyright (c) 2023 imacat.

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.

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.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/27
# Copyright (c) 2023 imacat.
@ -36,12 +36,14 @@ class DescriptionAccount:
:param account: The account.
:param freq: The frequency of the tag with the account.
"""
self.account: Account = 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."""
@ -50,7 +52,7 @@ class DescriptionAccount:
:return: The string representation of the account.
"""
return str(self.account)
return str(self.__account)
def add_freq(self, freq: int) -> None:
"""Adds the frequency of an account.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/15
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# Copyright (c) 2023 imacat.
@ -72,11 +72,12 @@ def get_selectable_original_line_items(
line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query\
.filter(JournalEntryLineItem.id.in_({x for x in net_balances}))\
.join(JournalEntry)\
.order_by(JournalEntry.date, JournalEntryLineItem.is_debit,
JournalEntryLineItem.no)\
.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 \

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
# Copyright (c) 2023 imacat.
@ -235,4 +235,4 @@ def __get_default_page_uri() -> str:
:return: The URI for the default page.
"""
return url_for("accounting.report.default")
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.

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.
@ -21,7 +21,6 @@ from __future__ import annotations
import re
import typing as t
from datetime import date
from decimal import Decimal
import sqlalchemy as sa
@ -760,7 +759,7 @@ class JournalEntryLineItem(db.Model):
setattr(self, "__net_balance", net_balance)
@property
def query_values(self) -> tuple[list[str], list[str]]:
def query_values(self) -> list[str]:
"""Returns the values to be queried.
:return: The values to be queried.
@ -770,19 +769,11 @@ class JournalEntryLineItem(db.Model):
frac: Decimal = (value - whole).normalize()
return str(whole) + str(abs(frac))[1:]
journal_entry_day: date = self.journal_entry.date
description: str = "" if self.description is None else self.description
return ([description],
[str(journal_entry_day.year),
"{}/{}".format(journal_entry_day.year,
journal_entry_day.month),
"{}/{}".format(journal_entry_day.month,
journal_entry_day.day),
"{}/{}/{}".format(journal_entry_day.year,
journal_entry_day.month,
journal_entry_day.day),
format_amount(self.amount),
format_amount(self.net_balance)])
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,
format_amount(self.amount)]
class Option(db.Model):

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
# Copyright (c) 2023 imacat.
@ -17,14 +17,14 @@
"""The report management.
"""
from flask import Flask, Blueprint
from flask import Flask
def init_app(app: Flask, bp: Blueprint) -> None:
def init_app(app: Flask, url_prefix: str) -> None:
"""Initialize the application.
:param app: The Flask application.
:param bp: The blueprint of the accounting application.
:param url_prefix: The URL prefix of the accounting application.
:return: None.
"""
from .converters import PeriodConverter, IncomeExpensesAccountConverter
@ -32,4 +32,4 @@ def init_app(app: Flask, bp: Blueprint) -> None:
app.url_map.converters["ieAccount"] = IncomeExpensesAccountConverter
from .views import bp as report_bp
bp.register_blueprint(report_bp, url_prefix="/reports")
app.register_blueprint(report_bp, url_prefix=url_prefix)

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# Copyright (c) 2023 imacat.
@ -137,6 +137,7 @@ class AccountCollector:
.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()

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# Copyright (c) 2023 imacat.
@ -269,6 +269,7 @@ class IncomeStatement(BaseReport):
.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] \

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# Copyright (c) 2023 imacat.
@ -191,6 +191,7 @@ class TrialBalance(BaseReport):
.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] \

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/6
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/5
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.
@ -68,9 +68,9 @@ class ReportChooser:
"""The title of the current report."""
self.is_search: bool = active_report == ReportType.SEARCH
"""Whether the current report is the search page."""
self.__reports.append(self.__journal)
self.__reports.append(self.__ledger)
self.__reports.append(self.__income_expenses)
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)
@ -80,28 +80,6 @@ class ReportChooser:
if self.is_search:
self.current_report = gettext("Search")
@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 __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 __income_expenses(self) -> OptionLink:
"""Returns the income and expenses log.
@ -118,6 +96,28 @@ class ReportChooser:
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.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
# Copyright (c) 2023 imacat.
@ -34,8 +34,8 @@ def journal_url(period: Period) \
:return: The URL of the journal.
"""
if period.is_default:
return url_for("accounting.report.journal-default")
return url_for("accounting.report.journal", period=period)
return url_for("accounting-report.journal-default")
return url_for("accounting-report.journal", period=period)
def ledger_url(currency: Currency, account: Account, period: Period) \
@ -48,9 +48,9 @@ def ledger_url(currency: Currency, account: Account, period: Period) \
:return: The URL of the ledger.
"""
if period.is_default:
return url_for("accounting.report.ledger-default",
return url_for("accounting-report.ledger-default",
currency=currency, account=account)
return url_for("accounting.report.ledger",
return url_for("accounting-report.ledger",
currency=currency, account=account,
period=period)
@ -67,11 +67,11 @@ def income_expenses_url(currency: Currency, account: CurrentAccount,
if currency.code == default_currency_code() \
and account.code == options.default_ie_account_code \
and period.is_default:
return url_for("accounting.report.default")
return url_for("accounting-report.default")
if period.is_default:
return url_for("accounting.report.income-expenses-default",
return url_for("accounting-report.income-expenses-default",
currency=currency, account=account)
return url_for("accounting.report.income-expenses",
return url_for("accounting-report.income-expenses",
currency=currency, account=account,
period=period)
@ -84,9 +84,9 @@ def trial_balance_url(currency: Currency, period: Period) -> str:
:return: The URL of the trial balance.
"""
if period.is_default:
return url_for("accounting.report.trial-balance-default",
return url_for("accounting-report.trial-balance-default",
currency=currency)
return url_for("accounting.report.trial-balance",
return url_for("accounting-report.trial-balance",
currency=currency, period=period)
@ -98,9 +98,9 @@ def income_statement_url(currency: Currency, period: Period) -> str:
:return: The URL of the income statement.
"""
if period.is_default:
return url_for("accounting.report.income-statement-default",
return url_for("accounting-report.income-statement-default",
currency=currency)
return url_for("accounting.report.income-statement",
return url_for("accounting-report.income-statement",
currency=currency, period=period)
@ -112,7 +112,7 @@ def balance_sheet_url(currency: Currency, period: Period) -> str:
:return: The URL of the balance sheet.
"""
if period.is_default:
return url_for("accounting.report.balance-sheet-default",
return url_for("accounting-report.balance-sheet-default",
currency=currency)
return url_for("accounting.report.balance-sheet",
return url_for("accounting-report.balance-sheet",
currency=currency, period=period)

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
# Copyright (c) 2023 imacat.
@ -30,7 +30,7 @@ from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
IncomeStatement, BalanceSheet, Search
from .template_filters import format_amount
bp: Blueprint = Blueprint("report", __name__)
bp: Blueprint = Blueprint("accounting-report", __name__)
"""The view blueprint for the reports."""
bp.add_app_template_filter(format_amount, "accounting_report_format_amount")

View File

@ -1,4 +1,4 @@
/* The Mia! Accounting Flask Project
/* The Mia! Accounting Project
* style.css: The style sheet for the accounting application.
*/
@ -316,6 +316,10 @@ a.accounting-report-table-row {
}
/* The description editor */
.accounting-description-editor-buttons {
max-height: 7rem;
overflow-y: scroll;
}
.accounting-description-editor-buttons .btn {
margin-bottom: 0.3rem;
}

View File

@ -1,4 +1,4 @@
/* The Mia! Accounting Flask Project
/* The Mia! Accounting Project
* account-form.js: The JavaScript for the account form
*/
@ -114,10 +114,19 @@ class AccountForm {
};
this.#baseControl.onclick = () => {
this.#baseControl.classList.add("accounting-not-empty");
this.#baseAccountSelector.onOpen(this.#baseCode.value);
this.#baseAccountSelector.onOpen();
};
}
/**
* Returns the base code.
*
* @return {string|null}
*/
get baseCode() {
return this.#baseCode.value === ""? null: this.#baseCode.value;
}
/**
* The callback when the base account selector is closed.
*
@ -129,15 +138,14 @@ class AccountForm {
}
/**
* Sets the base account.
* Saves the selected base account.
*
* @param code {string} the base account code
* @param text {string} the text for the base account
* @param account {BaseAccountOption} the selected base account
*/
setBaseAccount(code, text) {
this.#baseCode.value = code;
this.#base.innerText = text;
if (["1", "2", "3"].includes(code.substring(0, 1))) {
saveBaseAccount(account) {
this.#baseCode.value = account.code;
this.#base.innerText = account.text;
if (["1", "2", "3"].includes(account.code.substring(0, 1))) {
this.#isNeedOffsetControl.classList.remove("d-none");
this.#isNeedOffset.disabled = false;
} else {
@ -225,7 +233,7 @@ class BaseAccountSelector {
* The account form
* @type {AccountForm}
*/
#form;
form;
/**
* The selector modal
@ -253,7 +261,7 @@ class BaseAccountSelector {
/**
* The options
* @type {HTMLLIElement[]}
* @type {BaseAccountOption[]}
*/
#options;
@ -269,83 +277,54 @@ class BaseAccountSelector {
* @param form {AccountForm} the form
*/
constructor(form) {
this.#form = form;
this.#modal = document.getElementById("accounting-base-selector-modal");
this.#query = document.getElementById("accounting-base-selector-query");
this.#optionList = document.getElementById("accounting-base-selector-option-list");
// noinspection JSValidateTypes
this.#options = Array.from(document.getElementsByClassName("accounting-base-selector-option"));
this.#clearButton = document.getElementById("accounting-base-selector-clear");
this.#queryNoResult = document.getElementById("accounting-base-selector-option-no-result");
this.#modal.addEventListener("hidden.bs.modal", () => {
this.#form.onBaseAccountSelectorClosed();
});
for (const option of this.#options) {
option.onclick = () => {
this.#form.setBaseAccount(option.dataset.code, option.dataset.text);
};
}
this.#clearButton.onclick = () => {
this.#form.clearBaseAccount();
};
this.#initializeBaseAccountQuery();
this.form = form;
const prefix = "accounting-base-selector";
this.#modal = document.getElementById(`${prefix}-modal`);
this.#query = document.getElementById(`${prefix}-query`);
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
this.#optionList = document.getElementById(`${prefix}-option-list`);
this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new BaseAccountOption(this, element));
this.#clearButton = document.getElementById(`${prefix}-clear`);
this.#modal.addEventListener("hidden.bs.modal", () => this.form.onBaseAccountSelectorClosed());
this.#query.oninput = () => this.#filterOptions();
this.#clearButton.onclick = () => this.form.clearBaseAccount();
}
/**
* Initializes the query.
* Filters the options.
*
*/
#initializeBaseAccountQuery() {
this.#query.addEventListener("input", () => {
if (this.#query.value === "") {
for (const option of this.#options) {
option.classList.remove("d-none");
}
this.#optionList.classList.remove("d-none");
this.#queryNoResult.classList.add("d-none");
return
}
let hasAnyMatched = false;
for (const option of this.#options) {
const queryValues = JSON.parse(option.dataset.queryValues);
let isMatched = false;
for (const queryValue of queryValues) {
if (queryValue.toLowerCase().includes(this.#query.value.toLowerCase())) {
isMatched = true;
break;
}
}
if (isMatched) {
option.classList.remove("d-none");
hasAnyMatched = true;
} else {
option.classList.add("d-none");
}
}
if (!hasAnyMatched) {
this.#optionList.classList.add("d-none");
this.#queryNoResult.classList.remove("d-none");
#filterOptions() {
let isAnyMatched = false;
for (const option of this.#options) {
if (option.isMatched(this.#query.value)) {
option.setShown(true);
isAnyMatched = true;
} else {
this.#optionList.classList.remove("d-none");
this.#queryNoResult.classList.add("d-none");
option.setShown(false);
}
});
}
if (!isAnyMatched) {
this.#optionList.classList.add("d-none");
this.#queryNoResult.classList.remove("d-none");
} else {
this.#optionList.classList.remove("d-none");
this.#queryNoResult.classList.add("d-none");
}
}
/**
* The callback when the base account selector is shown.
*
* @param baseCode {string} the active base code
*/
onOpen(baseCode) {
onOpen() {
this.#query.value = "";
this.#filterOptions();
for (const option of this.#options) {
if (option.dataset.code === baseCode) {
option.classList.add("active");
} else {
option.classList.remove("active");
}
option.setActive(option.code === this.form.baseCode);
}
if (baseCode === "") {
if (this.form.baseCode === null) {
this.#clearButton.classList.add("btn-secondary")
this.#clearButton.classList.remove("btn-danger");
this.#clearButton.disabled = true;
@ -356,3 +335,93 @@ class BaseAccountSelector {
}
}
}
/**
* A base account option.
*
*/
class BaseAccountOption {
/**
* The element
* @type {HTMLLIElement}
*/
#element;
/**
* The account code
* @type {string}
*/
code;
/**
* The account text
* @type {string}
*/
text;
/**
* The values to query against
* @type {string[]}
*/
#queryValues;
/**
* Constructs the account in the base account selector.
*
* @param selector {BaseAccountSelector} the base account selector
* @param element {HTMLLIElement} the element
*/
constructor(selector, element) {
this.#element = element;
this.code = element.dataset.code;
this.text = element.dataset.text;
this.#queryValues = JSON.parse(element.dataset.queryValues);
this.#element.onclick = () => selector.form.saveBaseAccount(this);
}
/**
* Returns whether the account matches the query.
*
* @param query {string} the query term
* @return {boolean} true if the option matches, or false otherwise
*/
isMatched(query) {
if (query === "") {
return true;
}
for (const queryValue of this.#queryValues) {
if (queryValue.toLowerCase().includes(query.toLowerCase())) {
return true;
}
}
return false;
}
/**
* Sets whether the option is shown.
*
* @param isShown {boolean} true to show, or false otherwise
*/
setShown(isShown) {
if (isShown) {
this.#element.classList.remove("d-none");
} else {
this.#element.classList.add("d-none");
}
}
/**
* Sets whether the option is active.
*
* @param isActive {boolean} true if active, or false otherwise
*/
setActive(isActive) {
if (isActive) {
this.#element.classList.add("active");
} else {
this.#element.classList.remove("active");
}
}
}

View File

@ -1,4 +1,4 @@
/* The Mia! Accounting Flask Project
/* The Mia! Accounting Project
* account-order.js: The JavaScript for the account order
*/
@ -29,10 +29,11 @@ document.addEventListener("DOMContentLoaded", () => {
const onReorder = () => {
const accounts = Array.from(list.children);
for (let i = 0; i < accounts.length; i++) {
const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no");
const code = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-code");
const no = document.getElementById(`accounting-order-${accounts[i].dataset.id}-no`);
const code = document.getElementById(`accounting-order-${accounts[i].dataset.id}-code`);
no.value = String(i + 1);
code.innerText = list.dataset.baseCode + "-" + ("000" + (i + 1)).slice(-3);
const zeroPaddedNo = `000${no.value}`.slice(-3)
code.innerText = `${list.dataset.baseCode}-${zeroPaddedNo}`;
}
};
initializeDragAndDropReordering(list, onReorder);

View File

@ -1,224 +0,0 @@
/* The Mia! Accounting Flask Project
* account-selector.js: The JavaScript for the account selector
*/
/* Copyright (c) 2023 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/2/28
*/
"use strict";
/**
* The account selector.
*
*/
class AccountSelector {
/**
* The line item editor
* @type {JournalEntryLineItemEditor}
*/
#lineItemEditor;
/**
* Either "debit" or "credit"
* @type {string}
*/
#debitCredit;
/**
* The prefix of the HTML ID and class
* @type {string}
*/
#prefix;
/**
* The button to clear the account
* @type {HTMLButtonElement}
*/
#clearButton
/**
* The query input
* @type {HTMLInputElement}
*/
#query;
/**
* The error message when the query has no result
* @type {HTMLParagraphElement}
*/
#queryNoResult;
/**
* The option list
* @type {HTMLUListElement}
*/
#optionList;
/**
* The options
* @type {HTMLLIElement[]}
*/
#options;
/**
* The more item to show all accounts
* @type {HTMLLIElement}
*/
#more;
/**
* Constructs an account selector.
*
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
* @param debitCredit {string} either "debit" or "credit"
*/
constructor(lineItemEditor, debitCredit) {
this.#lineItemEditor = lineItemEditor
this.#debitCredit = debitCredit;
this.#prefix = "accounting-account-selector-" + debitCredit;
this.#query = document.getElementById(this.#prefix + "-query");
this.#queryNoResult = document.getElementById(this.#prefix + "-option-no-result");
this.#optionList = document.getElementById(this.#prefix + "-option-list");
// noinspection JSValidateTypes
this.#options = Array.from(document.getElementsByClassName(this.#prefix + "-option"));
this.#more = document.getElementById(this.#prefix + "-more");
this.#clearButton = document.getElementById(this.#prefix + "-btn-clear");
this.#more.onclick = () => {
this.#more.classList.add("d-none");
this.#filterOptions();
};
this.#clearButton.onclick = () => this.#lineItemEditor.clearAccount();
for (const option of this.#options) {
option.onclick = () => this.#lineItemEditor.saveAccount(option.dataset.code, option.dataset.text, option.classList.contains("accounting-account-is-need-offset"));
}
this.#query.addEventListener("input", () => {
this.#filterOptions();
});
}
/**
* Filters the options.
*
*/
#filterOptions() {
const codesInUse = this.#getCodesUsedInForm();
let shouldAnyShow = false;
for (const option of this.#options) {
const shouldShow = this.#shouldOptionShow(option, this.#more, codesInUse, this.#query);
if (shouldShow) {
option.classList.remove("d-none");
shouldAnyShow = true;
} else {
option.classList.add("d-none");
}
}
if (!shouldAnyShow && this.#more.classList.contains("d-none")) {
this.#optionList.classList.add("d-none");
this.#queryNoResult.classList.remove("d-none");
} else {
this.#optionList.classList.remove("d-none");
this.#queryNoResult.classList.add("d-none");
}
}
/**
* Returns the account codes that are used in the form.
*
* @return {string[]} the account codes that are used in the form
*/
#getCodesUsedInForm() {
const inUse = this.#lineItemEditor.form.getAccountCodesUsed(this.#debitCredit);
if (this.#lineItemEditor.accountCode !== null) {
inUse.push(this.#lineItemEditor.accountCode);
}
return inUse
}
/**
* Returns whether an option should show.
*
* @param option {HTMLLIElement} the option
* @param more {HTMLLIElement} the more element
* @param inUse {string[]} the account codes that are used in the form
* @param query {HTMLInputElement} the query element, if any
* @return {boolean} true if the option should show, or false otherwise
*/
#shouldOptionShow(option, more, inUse, query) {
const isQueryMatched = () => {
if (query.value === "") {
return true;
}
const queryValues = JSON.parse(option.dataset.queryValues);
for (const queryValue of queryValues) {
if (queryValue.toLowerCase().includes(query.value.toLowerCase())) {
return true;
}
}
return false;
};
const isMoreMatched = () => {
if (more.classList.contains("d-none")) {
return true;
}
return option.classList.contains("accounting-account-in-use") || inUse.includes(option.dataset.code);
};
return isMoreMatched() && isQueryMatched();
}
/**
* The callback when the account selector is shown.
*
*/
onOpen() {
this.#query.value = "";
this.#more.classList.remove("d-none");
this.#filterOptions();
for (const option of this.#options) {
if (option.dataset.code === this.#lineItemEditor.accountCode) {
option.classList.add("active");
} else {
option.classList.remove("active");
}
}
if (this.#lineItemEditor.accountCode === null) {
this.#clearButton.classList.add("btn-secondary");
this.#clearButton.classList.remove("btn-danger");
this.#clearButton.disabled = true;
} else {
this.#clearButton.classList.add("btn-danger");
this.#clearButton.classList.remove("btn-secondary");
this.#clearButton.disabled = false;
}
}
/**
* Returns the account selector instances.
*
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
* @return {{debit: AccountSelector, credit: AccountSelector}}
*/
static getInstances(lineItemEditor) {
const selectors = {}
const modals = Array.from(document.getElementsByClassName("accounting-account-selector"));
for (const modal of modals) {
selectors[modal.dataset.debitCredit] = new AccountSelector(lineItemEditor, modal.dataset.debitCredit);
}
return selectors;
}
}

View File

@ -1,4 +1,4 @@
/* The Mia! Accounting Flask Project
/* The Mia! Accounting Project
* currency-form.js: The JavaScript for the currency form
*/
@ -128,7 +128,7 @@ class CurrencyForm {
}
const original = this.#code.dataset.original;
if (original === "" || this.#code.value !== original) {
const response = await fetch(this.#code.dataset.existsUrl + "?q=" + encodeURIComponent(this.#code.value));
const response = await fetch(`${this.#code.dataset.existsUrl}?q=${encodeURIComponent(this.#code.value)}`);
const data = await response.json();
if (data["exists"]) {
this.#code.classList.add("is-invalid");

View File

@ -1,4 +1,4 @@
/* The Mia! Accounting Flask Project
/* The Mia! Accounting Project
* description-editor.js: The JavaScript for the description editor
*/
@ -41,7 +41,7 @@ class DescriptionEditor {
#form;
/**
* The prefix of the HTML ID and class
* The prefix of the HTML ID and class names
* @type {string}
*/
prefix;
@ -60,7 +60,7 @@ class DescriptionEditor {
/**
* The current tab
* @type {TabPlane}
* @type {DescriptionEditorTabPlane}
*/
currentTab;
@ -68,7 +68,7 @@ class DescriptionEditor {
* The description input
* @type {HTMLInputElement}
*/
description;
#descriptionInput;
/**
* The button to the original line item selector
@ -89,20 +89,44 @@ class DescriptionEditor {
note;
/**
* The account buttons
* @type {HTMLButtonElement[]}
* The placeholder of the confirmed account
* @type {DescriptionEditorConfirmedAccount}
*/
#accountButtons;
#confirmedAccountPlaceholder;
/**
* The selected account button
* @type {HTMLButtonElement|null}
* All the suggested accounts
* @type {DescriptionEditorSuggestedAccount[]}
*/
#selectedAccount = null;
#allSuggestedAccounts;
/**
* The current suggested accounts
* @type {DescriptionEditorSuggestedAccount[]}
*/
#currentSuggestedAccounts;
/**
* The account that the user specified or confirmed
* @type {DescriptionEditorConfirmedAccount|null}
*/
#confirmedAccount = null;
/**
* Whether the user has confirmed the account
* @type {boolean}
*/
isAccountConfirmed = false;
/**
* The selected account.
* @type {DescriptionEditorAccount|null}
*/
selectedAccount = null;
/**
* The tab planes
* @type {{general: GeneralTagTab, travel: GeneralTripTab, bus: BusTripTab, recurring: RecurringTransactionTab, annotation: AnnotationTab}}
* @type {{general: DescriptionEditorGeneralTagTab, travel: DescriptionEditorGeneralTripTab, bus: DescriptionEditorBusTripTab, recurring: DescriptionEditorRecurringTab, annotation: DescriptionEditorAnnotationTab}}
*/
tabPlanes = {};
@ -115,23 +139,22 @@ class DescriptionEditor {
constructor(lineItemEditor, debitCredit) {
this.lineItemEditor = lineItemEditor;
this.debitCredit = debitCredit;
this.prefix = "accounting-description-editor-" + debitCredit;
this.prefix = `accounting-description-editor-${debitCredit}`;
this.#form = document.getElementById(this.prefix);
this.#modal = document.getElementById(this.prefix + "-modal");
this.description = document.getElementById(this.prefix + "-description");
this.#offsetButton = document.getElementById(this.prefix + "-offset");
this.number = document.getElementById(this.prefix + "-annotation-number");
this.note = document.getElementById(this.prefix + "-annotation-note");
// noinspection JSValidateTypes
this.#accountButtons = Array.from(document.getElementsByClassName(this.prefix + "-account"));
this.#modal = document.getElementById(`${this.prefix}-modal`);
this.#descriptionInput = document.getElementById(`${this.prefix}-description`);
this.#offsetButton = document.getElementById(`${this.prefix}-offset`);
this.number = document.getElementById(`${this.prefix}-annotation-number`);
this.note = document.getElementById(`${this.prefix}-annotation-note`);
this.#confirmedAccountPlaceholder = new DescriptionEditorConfirmedAccount(this, document.getElementById(`${this.prefix}-account-confirmed`));
this.#allSuggestedAccounts = Array.from(document.getElementsByClassName(`${this.prefix}-account`)).map((button) => new DescriptionEditorSuggestedAccount(this, button));
for (const cls of [GeneralTagTab, GeneralTripTab, BusTripTab, RecurringTransactionTab, AnnotationTab]) {
for (const cls of [DescriptionEditorGeneralTagTab, DescriptionEditorGeneralTripTab, DescriptionEditorBusTripTab, DescriptionEditorRecurringTab, DescriptionEditorAnnotationTab]) {
const tab = new cls(this);
this.tabPlanes[tab.tabId()] = tab;
}
this.currentTab = this.tabPlanes.general;
this.#initializeSuggestedAccounts();
this.description.onchange = () => this.#onDescriptionChange();
this.#descriptionInput.onchange = () => this.#onDescriptionChange();
this.#offsetButton.onclick = () => this.lineItemEditor.originalLineItemSelector.onOpen();
this.#form.onsubmit = () => {
if (this.currentTab.validate()) {
@ -141,12 +164,44 @@ class DescriptionEditor {
};
}
/**
* Returns the description.
*
* @return {string} the description
*/
get description() {
return this.#descriptionInput.value;
}
/**
* Sets the description.
*
* @param description {string} the description
*/
set description(description) {
this.#descriptionInput.value = description;
}
/**
* Returns the current account options.
*
* @return {DescriptionEditorAccount[]} the current account options.
*/
get #currentAccountOptions() {
if (this.#confirmedAccount === null) {
return this.#currentSuggestedAccounts;
}
return [this.#confirmedAccount].concat(this.#currentSuggestedAccounts);
}
/**
* The callback when the description input is changed.
*
*/
#onDescriptionChange() {
this.description.value = this.description.value.trim();
this.#resetTabPlanes();
this.selectedAccount = null;
this.description = this.description.trim();
for (const tabPlane of [this.tabPlanes.recurring, this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) {
if (tabPlane.populate()) {
break;
@ -156,20 +211,49 @@ class DescriptionEditor {
}
/**
* Filters the suggested accounts.
* Resets the tab planes.
*
*/
#resetTabPlanes() {
for (const tabPlane of Object.values(this.tabPlanes)) {
tabPlane.reset();
}
this.tabPlanes.general.switchToMe();
}
/**
* Updates the current suggested accounts.
*
* @param tagButton {HTMLButtonElement} the tag button
*/
filterSuggestedAccounts(tagButton) {
updateCurrentSuggestedAccounts(tagButton) {
this.clearSuggestedAccounts();
const suggested = JSON.parse(tagButton.dataset.accounts);
for (const accountButton of this.#accountButtons) {
if (suggested.includes(accountButton.dataset.code)) {
accountButton.classList.remove("d-none");
if (accountButton.dataset.code === suggested[0]) {
this.#selectAccount(accountButton);
return;
}
const suggestedAccountCodes = JSON.parse(tagButton.dataset.accounts);
this.#currentSuggestedAccounts = this.#allSuggestedAccounts.filter((account) => {
if (this.#confirmedAccount !== null && account.code === this.#confirmedAccount.code) {
return false;
}
return suggestedAccountCodes.includes(account.code);
});
for (const account of this.#currentSuggestedAccounts) {
account.setShown(true);
}
this.#selectSuggestedAccount(suggestedAccountCodes[0]);
}
/**
* Selects the suggested account.
*
* @param code {string} the code of the most-frequent suggested account
*/
#selectSuggestedAccount(code) {
if (this.isAccountConfirmed) {
return;
}
for (const account of this.#currentAccountOptions) {
if (account.code === code) {
this.selectAccount(account);
return;
}
}
}
@ -179,37 +263,29 @@ class DescriptionEditor {
*
*/
clearSuggestedAccounts() {
for (const accountButton of this.#accountButtons) {
accountButton.classList.add("d-none");
for (const account of this.#allSuggestedAccounts) {
account.setShown(false);
account.setActive(false);
}
this.#selectAccount(null);
this.#currentSuggestedAccounts = [];
}
/**
* Initializes the suggested accounts.
* Select an account.
*
* @param selectedAccount {DescriptionEditorAccount|null} the account, or null to deselect the account
*/
#initializeSuggestedAccounts() {
for (const accountButton of this.#accountButtons) {
accountButton.onclick = () => this.#selectAccount(accountButton);
selectAccount(selectedAccount) {
for (const account of this.#currentAccountOptions) {
account.setActive(false);
}
}
/**
* Select a suggested account.
*
* @param selectedAccountButton {HTMLButtonElement|null} the account button, or null to deselect the account
*/
#selectAccount(selectedAccountButton) {
for (const accountButton of this.#accountButtons) {
accountButton.classList.remove("btn-primary");
accountButton.classList.add("btn-outline-primary");
if (selectedAccount !== null) {
selectedAccount.setActive(true);
}
if (selectedAccountButton !== null) {
selectedAccountButton.classList.remove("btn-outline-primary");
selectedAccountButton.classList.add("btn-primary");
this.selectedAccount = selectedAccount;
if (this.selectedAccount !== null) {
this.isAccountConfirmed &&= this.selectedAccount.isConfirmedAccount;
}
this.#selectedAccount = selectedAccountButton;
}
/**
@ -218,11 +294,7 @@ class DescriptionEditor {
*/
#submit() {
bootstrap.Modal.getOrCreateInstance(this.#modal).hide();
if (this.#selectedAccount !== null) {
this.lineItemEditor.saveDescriptionWithAccount(this.description.value, this.#selectedAccount.dataset.code, this.#selectedAccount.dataset.text, this.#selectedAccount.classList.contains("accounting-account-is-need-offset"));
} else {
this.lineItemEditor.saveDescription(this.description.value);
}
this.lineItemEditor.saveDescription(this);
}
/**
@ -230,21 +302,27 @@ class DescriptionEditor {
*
*/
onOpen() {
this.#reset();
this.description.value = this.lineItemEditor.description === null? "": this.lineItemEditor.description;
this.description = this.lineItemEditor.description === null? "": this.lineItemEditor.description;
this.#setConfirmedAccount();
this.#onDescriptionChange();
if (this.isAccountConfirmed) {
this.selectAccount(this.#confirmedAccount);
}
}
/**
* Resets the description editor.
* Sets the confirmed account.
*
*/
#reset() {
this.description.value = "";
for (const tabPlane of Object.values(this.tabPlanes)) {
tabPlane.reset();
#setConfirmedAccount() {
this.isAccountConfirmed = this.lineItemEditor.isAccountConfirmed;
this.#confirmedAccountPlaceholder.setShown(this.isAccountConfirmed);
if (this.isAccountConfirmed) {
this.#confirmedAccountPlaceholder.initializeFrom(this.lineItemEditor.account);
this.#confirmedAccount = this.#confirmedAccountPlaceholder;
} else {
this.#confirmedAccount = null;
}
this.tabPlanes.general.switchToMe();
}
/**
@ -263,13 +341,130 @@ class DescriptionEditor {
}
}
/**
* An account option in the description editor.
*
*/
class DescriptionEditorAccount extends JournalEntryAccount {
/**
* The account button
* @type {HTMLButtonElement}
*/
#element;
/**
* Whether this is the account specified or confirmed by the user
* @type {boolean}
*/
isConfirmedAccount = false;
/**
* Constructs an account option in the description editor.
*
* @param editor {DescriptionEditor} the description editor
* @param code {string} the account code
* @param text {string} the account text
* @param isNeedOffset {boolean} true if the line items in the account needs offset, or false otherwise
* @param button {HTMLButtonElement} the account button
*/
constructor(editor, code, text, isNeedOffset, button) {
super(code, text, isNeedOffset);
this.#element = button;
this.#element.onclick = () => editor.selectAccount(this);
}
/**
* Sets whether the option is shown.
*
* @param isShown {boolean} true to show, or false otherwise
*/
setShown(isShown) {
if (isShown) {
this.#element.classList.remove("d-none");
} else {
this.#element.classList.add("d-none");
}
}
/**
* Sets whether the option is active.
*
* @param isActive {boolean} true if active, or false otherwise
*/
setActive(isActive) {
if (isActive) {
this.#element.classList.add("btn-primary");
this.#element.classList.remove("btn-outline-primary");
} else {
this.#element.classList.remove("btn-primary");
this.#element.classList.add("btn-outline-primary");
}
}
/**
* Sets the content of the account button.
*
*/
resetContent() {
this.#element.innerText = this.text;
}
}
/**
* A suggested account.
*
*/
class DescriptionEditorSuggestedAccount extends DescriptionEditorAccount {
/**
* Constructs a suggested account.
*
* @param editor {DescriptionEditor} the description editor
* @param button {HTMLButtonElement} the account button
*/
constructor(editor, button) {
super(editor, button.dataset.code, button.dataset.text, button.classList.contains("accounting-account-is-need-offset"), button);
}
}
/**
* The account option that is specified or confirmed by the user.
*
*/
class DescriptionEditorConfirmedAccount extends DescriptionEditorAccount {
/**
* Constructs the account option that is specified or confirmed by the user.
*
* @param editor {DescriptionEditor} the description editor
* @param button {HTMLButtonElement} the account button
*/
constructor(editor, button) {
super(editor, "", "", false, button);
this.isConfirmedAccount = true;
}
/**
* Initializes the confirmed account from the line item editor.
*
* @param account {JournalEntryAccount} the confirmed account from the line item editor
*/
initializeFrom(account) {
this.code = account.code;
this.text = account.text;
this.isNeedOffset = account.isNeedOffset;
this.resetContent();
}
}
/**
* A tab plane.
*
* @abstract
* @private
*/
class TabPlane {
class DescriptionEditorTabPlane {
/**
* The parent description editor
@ -278,7 +473,7 @@ class TabPlane {
editor;
/**
* The prefix of the HTML ID and classes
* The prefix of the HTML ID and class names
* @type {string}
*/
prefix;
@ -302,9 +497,9 @@ class TabPlane {
*/
constructor(editor) {
this.editor = editor;
this.prefix = this.editor.prefix + "-" + this.tabId();
this.#tab = document.getElementById(this.prefix + "-tab");
this.#page = document.getElementById(this.prefix + "-page");
this.prefix = `${this.editor.prefix}-${this.tabId()}`;
this.#tab = document.getElementById(`${this.prefix}-tab`);
this.#page = document.getElementById(`${this.prefix}-page`);
this.#tab.onclick = () => this.switchToMe();
}
@ -364,7 +559,7 @@ class TabPlane {
* @abstract
* @private
*/
class TagTabPlane extends TabPlane {
class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
/**
* The tag input
@ -392,10 +587,10 @@ class TagTabPlane extends TabPlane {
*/
constructor(editor) {
super(editor);
this.tag = document.getElementById(this.prefix + "-tag");
this.tagError = document.getElementById(this.prefix + "-tag-error");
this.tag = document.getElementById(`${this.prefix}-tag`);
this.tagError = document.getElementById(`${this.prefix}-tag-error`);
// noinspection JSValidateTypes
this.tagButtons = Array.from(document.getElementsByClassName(this.prefix + "-btn-tag"));
this.tagButtons = Array.from(document.getElementsByClassName(`${this.prefix}-btn-tag`));
this.initializeTagButtons();
this.tag.onchange = () => {
this.onTagChange();
@ -414,7 +609,7 @@ class TagTabPlane extends TabPlane {
if (tagButton.dataset.value === this.tag.value) {
tagButton.classList.remove("btn-outline-primary");
tagButton.classList.add("btn-primary");
this.editor.filterSuggestedAccounts(tagButton);
this.editor.updateCurrentSuggestedAccounts(tagButton);
isMatched = true;
} else {
tagButton.classList.remove("btn-primary");
@ -442,7 +637,7 @@ class TagTabPlane extends TabPlane {
super.switchToMe();
for (const tagButton of this.tagButtons) {
if (tagButton.classList.contains("btn-primary")) {
this.editor.filterSuggestedAccounts(tagButton);
this.editor.updateCurrentSuggestedAccounts(tagButton);
return;
}
}
@ -463,7 +658,7 @@ class TagTabPlane extends TabPlane {
tagButton.classList.remove("btn-outline-primary");
tagButton.classList.add("btn-primary");
this.tag.value = tagButton.dataset.value;
this.editor.filterSuggestedAccounts(tagButton);
this.editor.updateCurrentSuggestedAccounts(tagButton);
this.updateDescription();
};
}
@ -522,7 +717,7 @@ class TagTabPlane extends TabPlane {
*
* @private
*/
class GeneralTagTab extends TagTabPlane {
class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane {
/**
* The tab ID
@ -540,12 +735,12 @@ class GeneralTagTab extends TagTabPlane {
* @override
*/
updateDescription() {
const pos = this.editor.description.value.indexOf("—");
const prefix = this.tag.value === ""? "": this.tag.value + "—";
const pos = this.editor.description.indexOf("—");
const prefix = this.tag.value === ""? "": `${this.tag.value}`;
if (pos === -1) {
this.editor.description.value = prefix + this.editor.description.value;
this.editor.description = `${prefix}${this.editor.description}`;
} else {
this.editor.description.value = prefix + this.editor.description.value.substring(pos + 1);
this.editor.description = `${prefix}${this.editor.description.substring(pos + 1)}`;
}
}
@ -556,7 +751,7 @@ class GeneralTagTab extends TagTabPlane {
* @override
*/
populate() {
const found = this.editor.description.value.match(/^([^—]+)—/);
const found = this.editor.description.match(/^([^—]+)—/);
if (found === null) {
return false;
}
@ -583,7 +778,7 @@ class GeneralTagTab extends TagTabPlane {
*
* @private
*/
class GeneralTripTab extends TagTabPlane {
class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
/**
* The origin
@ -623,12 +818,12 @@ class GeneralTripTab extends TagTabPlane {
*/
constructor(editor) {
super(editor);
this.#from = document.getElementById(this.prefix + "-from");
this.#fromError = document.getElementById(this.prefix + "-from-error");
this.#to = document.getElementById(this.prefix + "-to");
this.#toError = document.getElementById(this.prefix + "-to-error")
this.#from = document.getElementById(`${this.prefix}-from`);
this.#fromError = document.getElementById(`${this.prefix}-from-error`);
this.#to = document.getElementById(`${this.prefix}-to`);
this.#toError = document.getElementById(`${this.prefix}-to-error`)
// noinspection JSValidateTypes
this.#directionButtons = Array.from(document.getElementsByClassName(this.prefix + "-direction"));
this.#directionButtons = Array.from(document.getElementsByClassName(`${this.prefix}-direction`));
this.#from.onchange = () => {
this.#from.value = this.#from.value.trim();
this.updateDescription();
@ -675,7 +870,7 @@ class GeneralTripTab extends TagTabPlane {
break;
}
}
this.editor.description.value = this.tag.value + "—" + this.#from.value + direction + this.#to.value;
this.editor.description = `${this.tag.value}${this.#from.value}${direction}${this.#to.value}`;
}
/**
@ -709,7 +904,7 @@ class GeneralTripTab extends TagTabPlane {
* @override
*/
populate() {
const found = this.editor.description.value.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
const found = this.editor.description.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
if (found === null) {
return false;
}
@ -782,7 +977,7 @@ class GeneralTripTab extends TagTabPlane {
*
* @private
*/
class BusTripTab extends TagTabPlane {
class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
/**
* The route
@ -828,12 +1023,12 @@ class BusTripTab extends TagTabPlane {
*/
constructor(editor) {
super(editor);
this.#route = document.getElementById(this.prefix + "-route");
this.#routeError = document.getElementById(this.prefix + "-route-error");
this.#from = document.getElementById(this.prefix + "-from");
this.#fromError = document.getElementById(this.prefix + "-from-error");
this.#to = document.getElementById(this.prefix + "-to");
this.#toError = document.getElementById(this.prefix + "-to-error")
this.#route = document.getElementById(`${this.prefix}-route`);
this.#routeError = document.getElementById(`${this.prefix}-route-error`);
this.#from = document.getElementById(`${this.prefix}-from`);
this.#fromError = document.getElementById(`${this.prefix}-from-error`);
this.#to = document.getElementById(`${this.prefix}-to`);
this.#toError = document.getElementById(`${this.prefix}-to-error`)
this.#route.onchange = () => {
this.#route.value = this.#route.value.trim();
this.updateDescription();
@ -867,7 +1062,7 @@ class BusTripTab extends TagTabPlane {
* @override
*/
updateDescription() {
this.editor.description.value = this.tag.value + "—" + this.#route.value + "—" + this.#from.value + "→" + this.#to.value;
this.editor.description = `${this.tag.value}${this.#route.value}${this.#from.value}${this.#to.value}`;
}
/**
@ -895,7 +1090,7 @@ class BusTripTab extends TagTabPlane {
* @override
*/
populate() {
const found = this.editor.description.value.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
const found = this.editor.description.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
if (found === null) {
return false;
}
@ -970,7 +1165,7 @@ class BusTripTab extends TagTabPlane {
*
* @private
*/
class RecurringTransactionTab extends TabPlane {
class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
/**
* The month names
@ -984,7 +1179,6 @@ class RecurringTransactionTab extends TabPlane {
*/
#itemButtons;
// noinspection JSValidateTypes
/**
* Constructs a tab plane.
*
@ -1000,14 +1194,14 @@ class RecurringTransactionTab extends TabPlane {
A_("September"), A_("October"), A_("November"), A_("December"),
];
// noinspection JSValidateTypes
this.#itemButtons = Array.from(document.getElementsByClassName(this.prefix + "-item"));
this.#itemButtons = Array.from(document.getElementsByClassName(`${this.prefix}-item`));
for (const itemButton of this.#itemButtons) {
itemButton.onclick = () => {
this.reset();
itemButton.classList.add("btn-primary");
itemButton.classList.remove("btn-outline-primary");
this.editor.description.value = this.#getDescription(itemButton);
this.editor.filterSuggestedAccounts(itemButton);
this.editor.description = this.#getDescription(itemButton);
this.editor.updateCurrentSuggestedAccounts(itemButton);
};
}
}
@ -1019,7 +1213,7 @@ class RecurringTransactionTab extends TabPlane {
* @return {string} the description of the recurring item
*/
#getDescription(itemButton) {
const today = new Date(this.editor.lineItemEditor.form.getDate());
const today = new Date(this.editor.lineItemEditor.form.date);
const thisMonth = today.getMonth() + 1;
const lastMonth = (thisMonth + 10) % 12 + 1;
const lastBimonthlyFrom = ((thisMonth + thisMonth % 2 + 8) % 12 + 1);
@ -1029,8 +1223,8 @@ class RecurringTransactionTab extends TabPlane {
.replaceAll("{this_month_name}", this.#monthNames[thisMonth])
.replaceAll("{last_month_number}", String(lastMonth))
.replaceAll("{last_month_name}", this.#monthNames[lastMonth])
.replaceAll("{last_bimonthly_number}", String(lastBimonthlyFrom) + "" + String(lastBimonthlyTo))
.replaceAll("{last_bimonthly_name}", this.#monthNames[lastBimonthlyFrom] + "" + this.#monthNames[lastBimonthlyTo]);
.replaceAll("{last_bimonthly_number}", `${String(lastBimonthlyFrom)}${String(lastBimonthlyTo)}`)
.replaceAll("{last_bimonthly_name}", `${this.#monthNames[lastBimonthlyFrom]}${this.#monthNames[lastBimonthlyTo]}`);
}
/**
@ -1063,7 +1257,7 @@ class RecurringTransactionTab extends TabPlane {
*/
populate() {
for (const itemButton of this.#itemButtons) {
if (this.#getDescription(itemButton) === this.editor.description.value) {
if (this.#getDescription(itemButton) === this.editor.description) {
itemButton.classList.add("btn-primary");
itemButton.classList.remove("btn-outline-primary");
this.switchToMe();
@ -1081,7 +1275,7 @@ class RecurringTransactionTab extends TabPlane {
super.switchToMe();
for (const itemButton of this.#itemButtons) {
if (itemButton.classList.contains("btn-primary")) {
this.editor.filterSuggestedAccounts(itemButton);
this.editor.updateCurrentSuggestedAccounts(itemButton);
return;
}
}
@ -1104,7 +1298,7 @@ class RecurringTransactionTab extends TabPlane {
*
* @private
*/
class AnnotationTab extends TabPlane {
class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane {
/**
* Constructs a tab plane.
@ -1137,15 +1331,15 @@ class AnnotationTab extends TabPlane {
* @override
*/
updateDescription() {
const found = this.editor.description.value.match(/^(.*?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
const found = this.editor.description.match(/^(.*?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
if (found !== null) {
this.editor.description.value = found[1];
this.editor.description = found[1];
}
if (parseInt(this.editor.number.value) > 1) {
this.editor.description.value = this.editor.description.value + "×" + this.editor.number.value;
this.editor.description = `${this.editor.description}×${this.editor.number.value}`;
}
if (this.editor.note.value !== "") {
this.editor.description.value = this.editor.description.value + "(" + this.editor.note.value + ")";
this.editor.description = `${this.editor.description}(${this.editor.note.value})`;
}
}
@ -1166,19 +1360,19 @@ class AnnotationTab extends TabPlane {
* @override
*/
populate() {
const found = this.editor.description.value.match(/^(.*?)(?:[*×](\d+))?(?:\(([^()]+)\))?$/);
this.editor.description.value = found[1];
const found = this.editor.description.match(/^(.*?)(?:[*×](\d+))?(?:\(([^()]+)\))?$/);
this.editor.description = found[1];
if (found[2] === undefined || parseInt(found[2]) === 1) {
this.editor.number.value = "";
} else {
this.editor.number.value = found[2];
this.editor.description.value = this.editor.description.value + "×" + this.editor.number.value;
this.editor.description = `${this.editor.description}×${this.editor.number.value}`;
}
if (found[3] === undefined) {
this.editor.note.value = "";
} else {
this.editor.note.value = found[3];
this.editor.description.value = this.editor.description.value + "(" + this.editor.note.value + ")";
this.editor.description = `${this.editor.description}(${this.editor.note.value})`;
}
return true;
}

View File

@ -1,4 +1,4 @@
/* The Mia! Accounting Flask Project
/* The Mia! Accounting Project
* drag-and-drop-reorder.js: The JavaScript for the reorder a list with drag-and-drop
*/

View File

@ -0,0 +1,312 @@
/* The Mia! Accounting Project
* journal-entry-account-selector.js: The JavaScript for the account selector of the journal entry form
*/
/* Copyright (c) 2023 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2023/2/28
*/
"use strict";
/**
* The account selector.
*
*/
class JournalEntryAccountSelector {
/**
* The line item editor
* @type {JournalEntryLineItemEditor}
*/
lineItemEditor;
/**
* Either "debit" or "credit"
* @type {string}
*/
#debitCredit;
/**
* The button to clear the account
* @type {HTMLButtonElement}
*/
#clearButton
/**
* The query input
* @type {HTMLInputElement}
*/
#query;
/**
* The error message when the query has no result
* @type {HTMLParagraphElement}
*/
#queryNoResult;
/**
* The option list
* @type {HTMLUListElement}
*/
#optionList;
/**
* The options
* @type {JournalEntryAccountOption[]}
*/
#options;
/**
* The more item to show all accounts
* @type {HTMLLIElement}
*/
#more;
/**
* Whether to show all accounts
* @type {boolean}
*/
#isShowMore = false;
/**
* Constructs an account selector.
*
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
* @param debitCredit {string} either "debit" or "credit"
*/
constructor(lineItemEditor, debitCredit) {
this.lineItemEditor = lineItemEditor
this.#debitCredit = debitCredit;
const prefix = `accounting-account-selector-${debitCredit}`;
this.#query = document.getElementById(`${prefix}-query`);
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
this.#optionList = document.getElementById(`${prefix}-option-list`);
this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new JournalEntryAccountOption(this, element));
this.#more = document.getElementById(`${prefix}-more`);
this.#clearButton = document.getElementById(`${prefix}-btn-clear`);
this.#more.onclick = () => {
this.#isShowMore = true;
this.#more.classList.add("d-none");
this.#filterOptions();
};
this.#query.oninput = () => this.#filterOptions();
this.#clearButton.onclick = () => this.lineItemEditor.clearAccount();
}
/**
* Filters the options.
*
*/
#filterOptions() {
const codesInUse = this.#getCodesUsedInForm();
let isAnyMatched = false;
for (const option of this.#options) {
if (option.isMatched(this.#isShowMore, codesInUse, this.#query.value)) {
option.setShown(true);
isAnyMatched = true;
} else {
option.setShown(false);
}
}
if (!isAnyMatched) {
this.#optionList.classList.add("d-none");
this.#queryNoResult.classList.remove("d-none");
} else {
this.#optionList.classList.remove("d-none");
this.#queryNoResult.classList.add("d-none");
}
}
/**
* Returns the account codes that are used in the form.
*
* @return {string[]} the account codes that are used in the form
*/
#getCodesUsedInForm() {
const inUse = this.lineItemEditor.form.getAccountCodesUsed(this.#debitCredit);
if (this.lineItemEditor.account !== null) {
inUse.push(this.lineItemEditor.account.code);
}
return inUse
}
/**
* The callback when the account selector is shown.
*
*/
onOpen() {
this.#query.value = "";
this.#isShowMore = false;
this.#more.classList.remove("d-none");
this.#filterOptions();
for (const option of this.#options) {
option.setActive(this.lineItemEditor.account !== null && option.code === this.lineItemEditor.account.code);
}
if (this.lineItemEditor.account === null) {
this.#clearButton.classList.add("btn-secondary");
this.#clearButton.classList.remove("btn-danger");
this.#clearButton.disabled = true;
} else {
this.#clearButton.classList.add("btn-danger");
this.#clearButton.classList.remove("btn-secondary");
this.#clearButton.disabled = false;
}
}
/**
* Returns the account selector instances.
*
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
* @return {{debit: JournalEntryAccountSelector, credit: JournalEntryAccountSelector}}
*/
static getInstances(lineItemEditor) {
const selectors = {}
const modals = Array.from(document.getElementsByClassName("accounting-account-selector"));
for (const modal of modals) {
selectors[modal.dataset.debitCredit] = new JournalEntryAccountSelector(lineItemEditor, modal.dataset.debitCredit);
}
return selectors;
}
}
/**
* An account option
*
*/
class JournalEntryAccountOption {
/**
* The element
* @type {HTMLLIElement}
*/
#element;
/**
* The account code
* @type {string}
*/
code;
/**
* The account text
* @type {string}
*/
text;
/**
* Whether the account is in use
* @type {boolean}
*/
#isInUse;
/**
* Whether line items in the account need offset
* @type {boolean}
*/
isNeedOffset;
/**
* The values to query against
* @type {string[]}
*/
#queryValues;
/**
* Constructs the account in the account selector.
*
* @param selector {JournalEntryAccountSelector} the account selector
* @param element {HTMLLIElement} the element
*/
constructor(selector, element) {
this.#element = element;
this.code = element.dataset.code;
this.text = element.dataset.text;
this.#isInUse = element.classList.contains("accounting-account-is-in-use");
this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset");
this.#queryValues = JSON.parse(element.dataset.queryValues);
this.#element.onclick = () => selector.lineItemEditor.saveAccount(this);
}
/**
* Returns whether the account matches the query.
*
* @param isShowMore {boolean} true to show all accounts, or false to show only those in use
* @param codesInUse {string[]} the account codes that are used in the form
* @param query {string} the query term
* @return {boolean} true if the option matches, or false otherwise
*/
isMatched(isShowMore, codesInUse, query) {
return this.#isInUseMatched(isShowMore, codesInUse) && this.#isQueryMatched(query);
}
/**
* Returns whether the account matches the "in-use" condition.
*
* @param isShowMore {boolean} true to show all accounts, or false to show only those in use
* @param codesInUse {string[]} the account codes that are used in the form
* @return {boolean} true if the option matches, or false otherwise
*/
#isInUseMatched(isShowMore, codesInUse) {
return isShowMore || this.#isInUse || codesInUse.includes(this.code);
}
/**
* Returns whether the account matches the query term.
*
* @param query {string} the query term
* @return {boolean} true if the option matches, or false otherwise
*/
#isQueryMatched(query) {
if (query === "") {
return true;
}
for (const queryValue of this.#queryValues) {
if (queryValue.toLowerCase().includes(query.toLowerCase())) {
return true;
}
}
return false;
}
/**
* Sets whether the option is shown.
*
* @param isShown {boolean} true to show, or false otherwise
*/
setShown(isShown) {
if (isShown) {
this.#element.classList.remove("d-none");
} else {
this.#element.classList.add("d-none");
}
}
/**
* Sets whether the option is active.
*
* @param isActive {boolean} true if active, or false otherwise
*/
setActive(isActive) {
if (isActive) {
this.#element.classList.add("active");
} else {
this.#element.classList.remove("active");
}
}
}

View File

@ -1,4 +1,4 @@
/* The Mia! Accounting Flask Project
/* The Mia! Accounting Project
* journal-entry-form.js: The JavaScript for the journal entry form
*/
@ -111,6 +111,7 @@ class JournalEntryForm {
constructor() {
this.#element = document.getElementById("accounting-form");
this.lineItemTemplate = this.#element.dataset.lineItemTemplate;
this.lineItemEditor = new JournalEntryLineItemEditor(this);
this.#date = document.getElementById("accounting-date");
this.#dateError = document.getElementById("accounting-date-error");
this.#currencyControl = document.getElementById("accounting-currencies");
@ -121,14 +122,13 @@ class JournalEntryForm {
this.#addCurrencyButton = document.getElementById("accounting-add-currency");
this.#note = document.getElementById("accounting-note");
this.#noteError = document.getElementById("accounting-note-error");
this.lineItemEditor = new JournalEntryLineItemEditor(this);
this.#addCurrencyButton.onclick = () => {
const newIndex = 1 + (this.#currencies.length === 0? 0: Math.max(...this.#currencies.map((currency) => currency.index)));
const html = this.#element.dataset.currencyTemplate
.replaceAll("CURRENCY_INDEX", escapeHtml(String(newIndex)));
this.#currencyList.insertAdjacentHTML("beforeend", html);
const element = document.getElementById("accounting-currency-" + String(newIndex));
const element = document.getElementById(`accounting-currency-${String(newIndex)}`);
this.#currencies.push(new CurrencySubForm(this, element));
this.#resetDeleteCurrencyButtons();
this.#initializeDragAndDropReordering();
@ -159,7 +159,7 @@ class JournalEntryForm {
*/
#resetDeleteCurrencyButtons() {
if (this.#currencies.length === 1) {
this.#currencies[0].deleteButton.classList.add("d-none");
this.#currencies[0].setDeleteButtonShown(false);
} else {
for (const currency of this.#currencies) {
let isAnyLineItemMatched = false;
@ -169,11 +169,7 @@ class JournalEntryForm {
break;
}
}
if (isAnyLineItemMatched) {
currency.deleteButton.classList.add("d-none");
} else {
currency.deleteButton.classList.remove("d-none");
}
currency.setDeleteButtonShown(!isAnyLineItemMatched);
}
}
}
@ -184,10 +180,8 @@ class JournalEntryForm {
*/
#initializeDragAndDropReordering() {
initializeDragAndDropReordering(this.#currencyList, () => {
const currencyId = Array.from(this.#currencyList.children).map((currency) => currency.id);
this.#currencies.sort((a, b) => currencyId.indexOf(a.element.id) - currencyId.indexOf(b.element.id));
for (let i = 0; i < this.#currencies.length; i++) {
this.#currencies[i].no.value = String(i + 1);
for (const currency of this.#currencies) {
currency.resetNo();
}
});
}
@ -213,8 +207,8 @@ class JournalEntryForm {
* @return {string[]} the account codes used in the form
*/
getAccountCodesUsed(debitCredit) {
return this.getLineItems(debitCredit).map((lineItem) => lineItem.getAccountCode())
.filter((code) => code !== null);
return this.getLineItems(debitCredit).filter((lineItem) => lineItem.account !== null)
.map((lineItem) => lineItem.account.code);
}
/**
@ -222,7 +216,7 @@ class JournalEntryForm {
*
* @return {string} the date
*/
getDate() {
get date() {
return this.#date.value;
}
@ -233,7 +227,7 @@ class JournalEntryForm {
updateMinDate() {
let lastOriginalLineItemDate = null;
for (const lineItem of this.getLineItems()) {
const date = lineItem.getOriginalLineItemDate();
const date = lineItem.originalLineItemDate;
if (date !== null) {
if (lastOriginalLineItemDate === null || lastOriginalLineItemDate < date) {
lastOriginalLineItemDate = date;
@ -349,7 +343,7 @@ class CurrencySubForm {
* The element
* @type {HTMLDivElement}
*/
element;
#element;
/**
* The journal entry form
@ -363,12 +357,6 @@ class CurrencySubForm {
*/
index;
/**
* The prefix of the HTML ID and class
* @type {string}
*/
#prefix;
/**
* The control
* @type {HTMLDivElement}
@ -385,7 +373,7 @@ class CurrencySubForm {
* The number
* @type {HTMLInputElement}
*/
no;
#no;
/**
* The currency code
@ -403,7 +391,7 @@ class CurrencySubForm {
* The button to delete the currency
* @type {HTMLButtonElement}
*/
deleteButton;
#deleteButton;
/**
* The debit sub-form
@ -424,36 +412,54 @@ class CurrencySubForm {
* @param element {HTMLDivElement} the currency sub-form element
*/
constructor(form, element) {
this.element = element;
this.#element = element;
this.form = form;
this.index = parseInt(this.element.dataset.index);
this.#prefix = "accounting-currency-" + String(this.index);
this.#control = document.getElementById(this.#prefix + "-control");
this.#error = document.getElementById(this.#prefix + "-error");
this.no = document.getElementById(this.#prefix + "-no");
this.#code = document.getElementById(this.#prefix + "-code");
this.#codeSelect = document.getElementById(this.#prefix + "-code-select");
this.deleteButton = document.getElementById(this.#prefix + "-delete");
const debitElement = document.getElementById(this.#prefix + "-debit");
this.index = parseInt(this.#element.dataset.index);
const prefix = `accounting-currency-${String(this.index)}`;
this.#control = document.getElementById(`${prefix}-control`);
this.#error = document.getElementById(`${prefix}-error`);
this.#no = document.getElementById(`${prefix}-no`);
this.#code = document.getElementById(`${prefix}-code`);
this.#codeSelect = document.getElementById(`${prefix}-code-select`);
this.#deleteButton = document.getElementById(`${prefix}-delete`);
const debitElement = document.getElementById(`${prefix}-debit`);
this.#debit = debitElement === null? null: new DebitCreditSubForm(this, debitElement, "debit");
const creditElement = document.getElementById(this.#prefix + "-credit");
const creditElement = document.getElementById(`${prefix}-credit`);
this.#credit = creditElement == null? null: new DebitCreditSubForm(this, creditElement, "credit");
this.#codeSelect.onchange = () => this.#code.value = this.#codeSelect.value;
this.deleteButton.onclick = () => {
this.element.parentElement.removeChild(this.element);
this.#deleteButton.onclick = () => {
this.#element.parentElement.removeChild(this.#element);
this.form.deleteCurrency(this);
};
}
/**
* Reset the order number.
*
*/
resetNo() {
const siblings = Array.from(this.#element.parentElement.children);
this.#no.value = String(siblings.indexOf(this.#element) + 1);
}
/**
* Returns the currency code.
*
* @return {string} the currency code
*/
getCurrencyCode() {
get currencyCode() {
return this.#code.value;
}
/**
* Sets whether the delete button is shown.
*
* @param isShown {boolean} true to show, or false otherwise
*/
setDeleteButtonShown(isShown) {
setElementShown(this.#deleteButton, isShown);
}
/**
* Returns all the line items in the form.
*
@ -479,7 +485,7 @@ class CurrencySubForm {
updateCodeSelectorStatus() {
let isEnabled = true;
for (const lineItem of this.getLineItems()) {
if (lineItem.getOriginalLineItemId() !== null) {
if (lineItem.originalLineItemId !== null) {
isEnabled = false;
break;
}
@ -511,7 +517,7 @@ class CurrencySubForm {
*/
validateBalance() {
if (this.#debit !== null && this.#credit !== null) {
if (!this.#debit.getTotal().equals(this.#credit.getTotal())) {
if (!this.#debit.total.equals(this.#credit.total)) {
this.#control.classList.add("is-invalid");
this.#error.innerText = A_("The totals of the debit and credit amounts do not match.");
return false;
@ -541,6 +547,12 @@ class DebitCreditSubForm {
*/
#element;
/**
* The content
* @type {HTMLDivElement}
*/
#content;
/**
* The currencyIndex
* @type {number}
@ -554,7 +566,7 @@ class DebitCreditSubForm {
debitCredit;
/**
* The prefix of the HTML ID and class
* The prefix of the HTML ID and class names
* @type {string}
*/
#prefix;
@ -601,32 +613,45 @@ class DebitCreditSubForm {
this.#element = element;
this.#currencyIndex = currency.index;
this.debitCredit = debitCredit;
this.#prefix = "accounting-currency-" + String(this.#currencyIndex) + "-" + debitCredit;
this.#error = document.getElementById(this.#prefix + "-error");
this.#lineItemList = document.getElementById(this.#prefix + "-list");
// noinspection JSValidateTypes
this.#prefix = `accounting-currency-${String(this.#currencyIndex)}-${debitCredit}`;
this.#content = document.getElementById(`${this.#prefix}-content`);
this.#error = document.getElementById(`${this.#prefix}-error`);
this.#lineItemList = document.getElementById(`${this.#prefix}-list`);
this.lineItems = Array.from(document.getElementsByClassName(this.#prefix)).map((element) => new LineItemSubForm(this, element));
this.#total = document.getElementById(this.#prefix + "-total");
this.#addLineItemButton = document.getElementById(this.#prefix + "-add-line-item");
this.#total = document.getElementById(`${this.#prefix}-total`);
this.#addLineItemButton = document.getElementById(`${this.#prefix}-add-line-item`);
this.#resetContent();
this.#addLineItemButton.onclick = () => this.currency.form.lineItemEditor.onAddNew(this);
this.#resetDeleteLineItemButtons();
this.#initializeDragAndDropReordering();
}
/**
* The callback when the line item editor is closed.
*
*/
onLineItemEditorClosed() {
if (this.lineItems.length === 0) {
this.#element.classList.remove("accounting-not-empty");
}
}
/**
* Adds a new line item sub-form
*
* @returns {LineItemSubForm} the newly-added line item sub-form
*/
addLineItem() {
const newIndex = 1 + (this.lineItems.length === 0? 0: Math.max(...this.lineItems.map((lineItem) => lineItem.lineItemIndex)));
const newIndex = 1 + (this.lineItems.length === 0? 0: Math.max(...this.lineItems.map((lineItem) => lineItem.index)));
const html = this.currency.form.lineItemTemplate
.replaceAll("CURRENCY_INDEX", escapeHtml(String(this.#currencyIndex)))
.replaceAll("DEBIT_CREDIT", escapeHtml(this.debitCredit))
.replaceAll("LINE_ITEM_INDEX", escapeHtml(String(newIndex)));
this.#lineItemList.insertAdjacentHTML("beforeend", html);
const lineItem = new LineItemSubForm(this, document.getElementById(this.#prefix + "-" + String(newIndex)));
const lineItem = new LineItemSubForm(this, document.getElementById(`${this.#prefix}-${String(newIndex)}`));
this.lineItems.push(lineItem);
this.#resetContent();
this.#resetDeleteLineItemButtons();
this.#initializeDragAndDropReordering();
this.validate();
@ -644,6 +669,7 @@ class DebitCreditSubForm {
this.updateTotal();
this.currency.updateCodeSelectorStatus();
this.currency.form.updateMinDate();
this.#resetContent();
this.#resetDeleteLineItemButtons();
}
@ -653,27 +679,47 @@ class DebitCreditSubForm {
*/
#resetDeleteLineItemButtons() {
if (this.lineItems.length === 1) {
this.lineItems[0].deleteButton.classList.add("d-none");
this.lineItems[0].setDeleteButtonShown(false);
} else {
for (const lineItem of this.lineItems) {
if (lineItem.isMatched) {
lineItem.deleteButton.classList.add("d-none");
} else {
lineItem.deleteButton.classList.remove("d-none");
}
lineItem.setDeleteButtonShown(!lineItem.isMatched);
}
}
}
/**
* Resets the layout of the content.
*
*/
#resetContent() {
if (this.lineItems.length === 0) {
this.#element.classList.remove("accounting-not-empty");
this.#element.classList.add("accounting-clickable");
this.#element.dataset.bsToggle = "modal"
this.#element.dataset.bsTarget = `#${this.currency.form.lineItemEditor.modal.id}`;
this.#element.onclick = () => {
this.#element.classList.add("accounting-not-empty");
this.currency.form.lineItemEditor.onAddNew(this);
};
} else {
this.#element.classList.add("accounting-not-empty");
this.#element.classList.remove("accounting-clickable");
delete this.#element.dataset.bsToggle;
delete this.#element.dataset.bsTarget;
this.#element.onclick = null;
}
setElementShown(this.#content, this.lineItems.length !== 0);
}
/**
* Returns the total amount.
*
* @return {Decimal} the total amount
*/
getTotal() {
get total() {
let total = new Decimal("0");
for (const lineItem of this.lineItems) {
const amount = lineItem.getAmount();
const amount = lineItem.amount;
if (amount !== null) {
total = total.plus(amount);
}
@ -686,7 +732,7 @@ class DebitCreditSubForm {
*
*/
updateTotal() {
this.#total.innerText = formatDecimal(this.getTotal());
this.#total.innerText = formatDecimal(this.total);
this.currency.validateBalance();
}
@ -696,10 +742,8 @@ class DebitCreditSubForm {
*/
#initializeDragAndDropReordering() {
initializeDragAndDropReordering(this.#lineItemList, () => {
const lineItemId = Array.from(this.#lineItemList.children).map((lineItem) => lineItem.id);
this.lineItems.sort((a, b) => lineItemId.indexOf(a.element.id) - lineItemId.indexOf(b.element.id));
for (let i = 0; i < this.lineItems.length; i++) {
this.lineItems[i].no.value = String(i + 1);
for (const lineItem of this.lineItems) {
lineItem.resetNo();
}
});
}
@ -735,6 +779,53 @@ class DebitCreditSubForm {
}
}
/**
* A journal entry account.
*
*/
class JournalEntryAccount {
/**
* The account code
* @type {string}
*/
code;
/**
* The account text
* @type {string}
*/
text;
/**
* Whether the line items in the account needs offset
* @type {boolean}
*/
isNeedOffset;
/**
* Constructs a journal entry account.
*
* @param code {string} the account code
* @param text {string} the account text
* @param isNeedOffset {boolean} true if the line items in the account needs offset, or false otherwise
*/
constructor(code, text, isNeedOffset) {
this.code = code;
this.text = text;
this.isNeedOffset = isNeedOffset;
}
/**
* Returns a copy of the account.
*
* @return {JournalEntryAccount} the copy of the account
*/
copy() {
return new JournalEntryAccount(this.code, this.text, this.isNeedOffset);
}
}
/**
* The line item sub-form.
*
@ -751,7 +842,7 @@ class LineItemSubForm {
* The element
* @type {HTMLLIElement}
*/
element;
#element;
/**
* Either "debit" or "credit"
@ -763,7 +854,7 @@ class LineItemSubForm {
* The line item index
* @type {number}
*/
lineItemIndex;
index;
/**
* Whether this is an original line item with offsets
@ -771,12 +862,6 @@ class LineItemSubForm {
*/
isMatched;
/**
* The prefix of the HTML ID and class
* @type {string}
*/
#prefix;
/**
* The control
* @type {HTMLDivElement}
@ -793,7 +878,7 @@ class LineItemSubForm {
* The number
* @type {HTMLInputElement}
*/
no;
#no;
/**
* The account code
@ -853,7 +938,7 @@ class LineItemSubForm {
* The button to delete line item
* @type {HTMLButtonElement}
*/
deleteButton;
#deleteButton;
/**
* Constructs the line item sub-form.
@ -863,38 +948,38 @@ class LineItemSubForm {
*/
constructor(debitCredit, element) {
this.debitCreditSubForm = debitCredit;
this.element = element;
this.#element = element;
this.debitCredit = element.dataset.debitCredit;
this.lineItemIndex = parseInt(element.dataset.lineItemIndex);
this.index = parseInt(element.dataset.lineItemIndex);
this.isMatched = element.classList.contains("accounting-matched-line-item");
this.#prefix = "accounting-currency-" + element.dataset.currencyIndex + "-" + this.debitCredit + "-" + this.lineItemIndex;
this.#control = document.getElementById(this.#prefix + "-control");
this.#error = document.getElementById(this.#prefix + "-error");
this.no = document.getElementById(this.#prefix + "-no");
this.#accountCode = document.getElementById(this.#prefix + "-account-code");
this.#accountText = document.getElementById(this.#prefix + "-account-text");
this.#description = document.getElementById(this.#prefix + "-description");
this.#descriptionText = document.getElementById(this.#prefix + "-description-text");
this.#originalLineItemId = document.getElementById(this.#prefix + "-original-line-item-id");
this.#originalLineItemText = document.getElementById(this.#prefix + "-original-line-item-text");
this.#offsets = document.getElementById(this.#prefix + "-offsets");
this.#amount = document.getElementById(this.#prefix + "-amount");
this.#amountText = document.getElementById(this.#prefix + "-amount-text");
this.deleteButton = document.getElementById(this.#prefix + "-delete");
const prefix = `accounting-currency-${element.dataset.currencyIndex}-${this.debitCredit}-${String(this.index)}`;
this.#control = document.getElementById(`${prefix}-control`);
this.#error = document.getElementById(`${prefix}-error`);
this.#no = document.getElementById(`${prefix}-no`);
this.#accountCode = document.getElementById(`${prefix}-account-code`);
this.#accountText = document.getElementById(`${prefix}-account-text`);
this.#description = document.getElementById(`${prefix}-description`);
this.#descriptionText = document.getElementById(`${prefix}-description-text`);
this.#originalLineItemId = document.getElementById(`${prefix}-original-line-item-id`);
this.#originalLineItemText = document.getElementById(`${prefix}-original-line-item-text`);
this.#offsets = document.getElementById(`${prefix}-offsets`);
this.#amount = document.getElementById(`${prefix}-amount`);
this.#amountText = document.getElementById(`${prefix}-amount-text`);
this.#deleteButton = document.getElementById(`${prefix}-delete`);
this.#control.onclick = () => this.debitCreditSubForm.currency.form.lineItemEditor.onEdit(this);
this.deleteButton.onclick = () => {
this.element.parentElement.removeChild(this.element);
this.#deleteButton.onclick = () => {
this.#element.parentElement.removeChild(this.#element);
this.debitCreditSubForm.deleteLineItem(this);
};
}
/**
* Returns whether the line item needs offset.
* Reset the order number.
*
* @return {boolean} true if the line item needs offset, or false otherwise
*/
isNeedOffset() {
return "isNeedOffset" in this.element.dataset;
resetNo() {
const siblings = Array.from(this.#element.parentElement.children);
this.#no.value = String(siblings.indexOf(this.#element) + 1);
}
/**
@ -902,7 +987,7 @@ class LineItemSubForm {
*
* @return {string|null} the ID of the original line item
*/
getOriginalLineItemId() {
get originalLineItemId() {
return this.#originalLineItemId.value === ""? null: this.#originalLineItemId.value;
}
@ -911,7 +996,7 @@ class LineItemSubForm {
*
* @return {string|null} the date of the original line item
*/
getOriginalLineItemDate() {
get originalLineItemDate() {
return this.#originalLineItemId.dataset.date === ""? null: this.#originalLineItemId.dataset.date;
}
@ -920,7 +1005,7 @@ class LineItemSubForm {
*
* @return {string|null} the text of the original line item
*/
getOriginalLineItemText() {
get originalLineItemText() {
return this.#originalLineItemId.dataset.text === ""? null: this.#originalLineItemId.dataset.text;
}
@ -929,26 +1014,17 @@ class LineItemSubForm {
*
* @return {string|null} the description
*/
getDescription() {
get description() {
return this.#description.value === ""? null: this.#description.value;
}
/**
* Returns the account code.
* Returns the account.
*
* @return {string|null} the account code
* @return {JournalEntryAccount|null} the account
*/
getAccountCode() {
return this.#accountCode.value === ""? null: this.#accountCode.value;
}
/**
* Returns the account text.
*
* @return {string|null} the account text
*/
getAccountText() {
return this.#accountCode.dataset.text === ""? null: this.#accountCode.dataset.text;
get account() {
return this.#accountCode.value === null? null: new JournalEntryAccount(this.#accountCode.value, this.#accountCode.dataset.text, this.#accountCode.classList.contains("accounting-account-is-need-offset"));
}
/**
@ -956,7 +1032,7 @@ class LineItemSubForm {
*
* @return {Decimal|null} the amount
*/
getAmount() {
get amount() {
return this.#amount.value === ""? null: new Decimal(this.#amount.value);
}
@ -965,10 +1041,19 @@ class LineItemSubForm {
*
* @return {Decimal|null} the minimal amount
*/
getAmountMin() {
get amountMin() {
return this.#amount.dataset.min === ""? null: new Decimal(this.#amount.dataset.min);
}
/**
* Sets whether the delete button is shown.
*
* @param isShown {boolean} true to show, or false otherwise
*/
setDeleteButtonShown(isShown) {
setElementShown(this.#deleteButton, isShown);
}
/**
* Validates the form.
*
@ -996,24 +1081,24 @@ class LineItemSubForm {
* @param editor {JournalEntryLineItemEditor} the line item editor
*/
save(editor) {
if (editor.isNeedOffset) {
this.#offsets.classList.remove("d-none");
} else {
this.#offsets.classList.add("d-none");
}
setElementShown(this.#offsets, editor.account.isNeedOffset);
this.#originalLineItemId.value = editor.originalLineItemId === null? "": editor.originalLineItemId;
this.#originalLineItemId.dataset.date = editor.originalLineItemDate === null? "": editor.originalLineItemDate;
this.#originalLineItemId.dataset.text = editor.originalLineItemText === null? "": editor.originalLineItemText;
setElementShown(this.#originalLineItemText, editor.originalLineItemText !== null);
if (editor.originalLineItemText === null) {
this.#originalLineItemText.classList.add("d-none");
this.#originalLineItemText.innerText = "";
} else {
this.#originalLineItemText.classList.remove("d-none");
this.#originalLineItemText.innerText = A_("Offset %(item)s", {item: editor.originalLineItemText});
}
this.#accountCode.value = editor.accountCode === null? "": editor.accountCode;
this.#accountCode.dataset.text = editor.accountText === null? "": editor.accountText;
this.#accountText.innerText = editor.accountText === null? "": editor.accountText;
this.#accountCode.value = editor.account.code;
this.#accountCode.dataset.text = editor.account.text;
if (editor.account.isNeedOffset) {
this.#accountCode.classList.add("accounting-account-is-need-offset");
} else {
this.#accountCode.classList.remove("accounting-account-is-need-offset");
}
this.#accountText.innerText = editor.account.text;
this.#description.value = editor.description === null? "": editor.description;
this.#descriptionText.innerText = editor.description === null? "": editor.description;
this.#amount.value = editor.amount;
@ -1054,3 +1139,18 @@ function formatDecimal(number) {
const whole = Number(number.minus(frac)).toLocaleString();
return whole + String(frac).substring(1);
}
/**
* Sets whether an element is shown.
*
* @param element {HTMLElement} the element
* @param isShown {boolean} true to show, or false otherwise
* @private
*/
function setElementShown(element, isShown) {
if (isShown) {
element.classList.remove("d-none");
} else {
element.classList.add("d-none");
}
}

View File

@ -1,4 +1,4 @@
/* The Mia! Accounting Flask Project
/* The Mia! Accounting Project
* journal-entry-line-item-editor.js: The JavaScript for the journal entry line item editor
*/
@ -44,7 +44,7 @@ class JournalEntryLineItemEditor {
* The bootstrap modal
* @type {HTMLDivElement}
*/
#modal;
modal;
/**
* Either "debit" or "credit"
@ -53,7 +53,7 @@ class JournalEntryLineItemEditor {
debitCredit;
/**
* The prefix of the HTML ID and class
* The prefix of the HTML ID and class names
* @type {string}
*/
#prefix = "accounting-line-item-editor"
@ -148,12 +148,6 @@ class JournalEntryLineItemEditor {
*/
#debitCreditSubForm;
/**
* Whether the journal entry line item needs offset
* @type {boolean}
*/
isNeedOffset = false;
/**
* The ID of the original line item
* @type {string|null}
@ -173,16 +167,16 @@ class JournalEntryLineItemEditor {
originalLineItemText = null;
/**
* The account code
* @type {string|null}
* The account
* @type {JournalEntryAccount|null}
*/
accountCode = null;
account = null;
/**
* The account text
* @type {string|null}
* Whether the user has confirmed the account
* @type {boolean}
*/
accountText = null;
isAccountConfirmed = false;
/**
* The description
@ -190,12 +184,6 @@ class JournalEntryLineItemEditor {
*/
description = null;
/**
* The amount
* @type {string}
*/
amount = "";
/**
* The description editors
* @type {{debit: DescriptionEditor, credit: DescriptionEditor}}
@ -204,7 +192,7 @@ class JournalEntryLineItemEditor {
/**
* The account selectors
* @type {{debit: AccountSelector, credit: AccountSelector}}
* @type {{debit: JournalEntryAccountSelector, credit: JournalEntryAccountSelector}}
*/
#accountSelectors;
@ -222,23 +210,24 @@ class JournalEntryLineItemEditor {
constructor(form) {
this.form = form;
this.#element = document.getElementById(this.#prefix);
this.#modal = document.getElementById(this.#prefix + "-modal");
this.#originalLineItemContainer = document.getElementById(this.#prefix + "-original-line-item-container");
this.#originalLineItemControl = document.getElementById(this.#prefix + "-original-line-item-control");
this.#originalLineItemText = document.getElementById(this.#prefix + "-original-line-item");
this.#originalLineItemError = document.getElementById(this.#prefix + "-original-line-item-error");
this.#originalLineItemDelete = document.getElementById(this.#prefix + "-original-line-item-delete");
this.#descriptionControl = document.getElementById(this.#prefix + "-description-control");
this.#descriptionText = document.getElementById(this.#prefix + "-description");
this.#descriptionError = document.getElementById(this.#prefix + "-description-error");
this.#accountControl = document.getElementById(this.#prefix + "-account-control");
this.#accountText = document.getElementById(this.#prefix + "-account");
this.#accountError = document.getElementById(this.#prefix + "-account-error")
this.#amountInput = document.getElementById(this.#prefix + "-amount");
this.#amountError = document.getElementById(this.#prefix + "-amount-error");
this.modal = document.getElementById(`${this.#prefix}-modal`);
this.#originalLineItemContainer = document.getElementById(`${this.#prefix}-original-line-item-container`);
this.#originalLineItemControl = document.getElementById(`${this.#prefix}-original-line-item-control`);
this.#originalLineItemText = document.getElementById(`${this.#prefix}-original-line-item`);
this.#originalLineItemError = document.getElementById(`${this.#prefix}-original-line-item-error`);
this.#originalLineItemDelete = document.getElementById(`${this.#prefix}-original-line-item-delete`);
this.#descriptionControl = document.getElementById(`${this.#prefix}-description-control`);
this.#descriptionText = document.getElementById(`${this.#prefix}-description`);
this.#descriptionError = document.getElementById(`${this.#prefix}-description-error`);
this.#accountControl = document.getElementById(`${this.#prefix}-account-control`);
this.#accountText = document.getElementById(`${this.#prefix}-account`);
this.#accountError = document.getElementById(`${this.#prefix}-account-error`)
this.#amountInput = document.getElementById(`${this.#prefix}-amount`);
this.#amountError = document.getElementById(`${this.#prefix}-amount-error`);
this.#descriptionEditors = DescriptionEditor.getInstances(this);
this.#accountSelectors = AccountSelector.getInstances(this);
this.#accountSelectors = JournalEntryAccountSelector.getInstances(this);
this.originalLineItemSelector = new OriginalLineItemSelector(this);
this.#originalLineItemControl.onclick = () => this.originalLineItemSelector.onOpen()
this.#originalLineItemDelete.onclick = () => this.clearOriginalLineItem();
this.#descriptionControl.onclick = () => this.#descriptionEditors[this.debitCredit].onOpen();
@ -249,12 +238,30 @@ class JournalEntryLineItemEditor {
if (this.lineItem === null) {
this.lineItem = this.#debitCreditSubForm.addLineItem();
}
this.amount = this.#amountInput.value;
this.lineItem.save(this);
bootstrap.Modal.getInstance(this.#modal).hide();
bootstrap.Modal.getInstance(this.modal).hide();
}
return false;
};
this.modal.addEventListener("hidden.bs.modal", () => this.#debitCreditSubForm.onLineItemEditorClosed());
}
/**
* Returns the amount.
*
* @return {string} the amount
*/
get amount() {
return this.#amountInput.value;
}
/**
* Returns the currency code.
*
* @return {string} the currency code
*/
get currencyCode() {
return this.#debitCreditSubForm.currency.currencyCode;
}
/**
@ -263,7 +270,6 @@ class JournalEntryLineItemEditor {
* @param originalLineItem {OriginalLineItem} the original line item
*/
saveOriginalLineItem(originalLineItem) {
this.isNeedOffset = false;
this.#originalLineItemContainer.classList.remove("d-none");
this.#originalLineItemControl.classList.add("accounting-not-empty");
this.originalLineItemId = originalLineItem.id;
@ -279,9 +285,9 @@ class JournalEntryLineItemEditor {
this.description = originalLineItem.description === ""? null: originalLineItem.description;
this.#descriptionText.innerText = originalLineItem.description;
this.#accountControl.classList.add("accounting-not-empty");
this.accountCode = originalLineItem.accountCode;
this.accountText = originalLineItem.accountText;
this.#accountText.innerText = originalLineItem.accountText;
this.account = originalLineItem.account.copy();
this.isAccountConfirmed = false;
this.#accountText.innerText = this.account.text;
this.#amountInput.value = String(originalLineItem.netBalance);
this.#amountInput.max = String(originalLineItem.netBalance);
this.#amountInput.min = "0";
@ -293,7 +299,6 @@ class JournalEntryLineItemEditor {
*
*/
clearOriginalLineItem() {
this.isNeedOffset = false;
this.#originalLineItemContainer.classList.add("d-none");
this.#originalLineItemControl.classList.remove("accounting-not-empty");
this.originalLineItemId = null;
@ -302,54 +307,34 @@ class JournalEntryLineItemEditor {
this.#originalLineItemText.innerText = "";
this.#setEnableDescriptionAccount(true);
this.#accountControl.classList.remove("accounting-not-empty");
this.accountCode = null;
this.accountText = null;
this.account = null;
this.isAccountConfirmed = false;
this.#accountText.innerText = "";
this.#amountInput.max = "";
}
/**
* Returns the currency code.
*
* @return {string} the currency code
*/
getCurrencyCode() {
return this.#debitCreditSubForm.currency.getCurrencyCode();
}
/**
* Saves the description from the description editor.
*
* @param description {string} the description
* @param editor {DescriptionEditor} the description editor
*/
saveDescription(description) {
if (description === "") {
saveDescription(editor) {
if (editor.selectedAccount !== null) {
this.#accountControl.classList.add("accounting-not-empty");
this.account = editor.selectedAccount.copy();
this.#accountText.innerText = editor.selectedAccount.text;
this.isAccountConfirmed = editor.isAccountConfirmed;
this.#validateAccount();
}
if (editor.description === "") {
this.#descriptionControl.classList.remove("accounting-not-empty");
} else {
this.#descriptionControl.classList.add("accounting-not-empty");
}
this.description = description === ""? null: description;
this.#descriptionText.innerText = description;
this.description = editor.description === ""? null: editor.description;
this.#descriptionText.innerText = editor.description;
this.#validateDescription();
bootstrap.Modal.getOrCreateInstance(this.#modal).show();
}
/**
* Saves the description with the suggested account from the description editor.
*
* @param description {string} the description
* @param accountCode {string} the account code
* @param accountText {string} the account text
* @param isAccountNeedOffset {boolean} true if the line items in the account need offset, or false otherwise
*/
saveDescriptionWithAccount(description, accountCode, accountText, isAccountNeedOffset) {
this.isNeedOffset = isAccountNeedOffset;
this.#accountControl.classList.add("accounting-not-empty");
this.accountCode = accountCode;
this.accountText = accountText;
this.#accountText.innerText = accountText;
this.#validateAccount();
this.saveDescription(description)
bootstrap.Modal.getOrCreateInstance(this.modal).show();
}
/**
@ -357,27 +342,23 @@ class JournalEntryLineItemEditor {
*
*/
clearAccount() {
this.isNeedOffset = false;
this.#accountControl.classList.remove("accounting-not-empty");
this.accountCode = null;
this.accountText = null;
this.account = null;
this.isAccountConfirmed = false;
this.#accountText.innerText = "";
this.#validateAccount();
}
/**
* Sets the account.
* Saves the selected account.
*
* @param code {string} the account code
* @param text {string} the account text
* @param isNeedOffset {boolean} true if the line items in the account need offset or false otherwise
* @param account {JournalEntryAccountOption} the selected account
*/
saveAccount(code, text, isNeedOffset) {
this.isNeedOffset = isNeedOffset;
saveAccount(account) {
this.#accountControl.classList.add("accounting-not-empty");
this.accountCode = code;
this.accountText = text;
this.#accountText.innerText = text;
this.account = new JournalEntryAccount(account.code, account.text, account.isNeedOffset);
this.isAccountConfirmed = true;
this.#accountText.innerText = account.text;
this.#validateAccount();
}
@ -425,7 +406,7 @@ class JournalEntryLineItemEditor {
* @return {boolean} true if valid, or false otherwise
*/
#validateAccount() {
if (this.accountCode === null) {
if (this.account === null) {
this.#accountControl.classList.add("is-invalid");
this.#accountError.innerText = A_("Please select the account.");
return false;
@ -484,7 +465,6 @@ class JournalEntryLineItemEditor {
this.lineItem = null;
this.#debitCreditSubForm = debitCredit;
this.debitCredit = this.#debitCreditSubForm.debitCredit;
this.isNeedOffset = false;
this.#originalLineItemContainer.classList.add("d-none");
this.#originalLineItemControl.classList.remove("accounting-not-empty");
this.#originalLineItemControl.classList.remove("is-invalid");
@ -500,8 +480,8 @@ class JournalEntryLineItemEditor {
this.#descriptionError.innerText = ""
this.#accountControl.classList.remove("accounting-not-empty");
this.#accountControl.classList.remove("is-invalid");
this.accountCode = null;
this.accountText = null;
this.account = null;
this.isAccountConfirmed = false;
this.#accountText.innerText = "";
this.#accountError.innerText = "";
this.#amountInput.value = "";
@ -520,10 +500,9 @@ class JournalEntryLineItemEditor {
this.lineItem = lineItem;
this.#debitCreditSubForm = lineItem.debitCreditSubForm;
this.debitCredit = this.#debitCreditSubForm.debitCredit;
this.isNeedOffset = lineItem.isNeedOffset();
this.originalLineItemId = lineItem.getOriginalLineItemId();
this.originalLineItemDate = lineItem.getOriginalLineItemDate();
this.originalLineItemText = lineItem.getOriginalLineItemText();
this.originalLineItemId = lineItem.originalLineItemId;
this.originalLineItemDate = lineItem.originalLineItemDate;
this.originalLineItemText = lineItem.originalLineItemText;
this.#originalLineItemText.innerText = this.originalLineItemText;
if (this.originalLineItemId === null) {
this.#originalLineItemContainer.classList.add("d-none");
@ -533,25 +512,25 @@ class JournalEntryLineItemEditor {
this.#originalLineItemControl.classList.add("accounting-not-empty");
}
this.#setEnableDescriptionAccount(!lineItem.isMatched && this.originalLineItemId === null);
this.description = lineItem.getDescription();
this.description = lineItem.description;
if (this.description === null) {
this.#descriptionControl.classList.remove("accounting-not-empty");
} else {
this.#descriptionControl.classList.add("accounting-not-empty");
}
this.#descriptionText.innerText = this.description === null? "": this.description;
if (lineItem.getAccountCode() === null) {
this.account = lineItem.account;
this.isAccountConfirmed = true;
if (this.account === null) {
this.#accountControl.classList.remove("accounting-not-empty");
} else {
this.#accountControl.classList.add("accounting-not-empty");
}
this.accountCode = lineItem.getAccountCode();
this.accountText = lineItem.getAccountText();
this.#accountText.innerText = this.accountText;
this.#amountInput.value = lineItem.getAmount() === null? "": String(lineItem.getAmount());
this.#accountText.innerText = this.account.text;
this.#amountInput.value = lineItem.amount === null? "": String(lineItem.amount);
const maxAmount = this.#getMaxAmount();
this.#amountInput.max = maxAmount === null? "": maxAmount;
this.#amountInput.min = lineItem.getAmountMin() === null? "": String(lineItem.getAmountMin());
this.#amountInput.min = lineItem.amountMin === null? "": String(lineItem.amountMin);
this.#validate();
}
@ -575,11 +554,11 @@ class JournalEntryLineItemEditor {
#setEnableDescriptionAccount(isEnabled) {
if (isEnabled) {
this.#descriptionControl.dataset.bsToggle = "modal";
this.#descriptionControl.dataset.bsTarget = "#accounting-description-editor-" + this.#debitCreditSubForm.debitCredit + "-modal";
this.#descriptionControl.dataset.bsTarget = `#accounting-description-editor-${this.#debitCreditSubForm.debitCredit}-modal`;
this.#descriptionControl.classList.remove("accounting-disabled");
this.#descriptionControl.classList.add("accounting-clickable");
this.#accountControl.dataset.bsToggle = "modal";
this.#accountControl.dataset.bsTarget = "#accounting-account-selector-" + this.#debitCreditSubForm.debitCredit + "-modal";
this.#accountControl.dataset.bsTarget = `#accounting-account-selector-${this.#debitCreditSubForm.debitCredit}-modal`;
this.#accountControl.classList.remove("accounting-disabled");
this.#accountControl.classList.add("accounting-clickable");
} else {
@ -594,3 +573,4 @@ class JournalEntryLineItemEditor {
}
}
}

View File

@ -1,4 +1,4 @@
/* The Mia! Accounting Flask Project
/* The Mia! Accounting Project
* journal-entry-order.js: The JavaScript for the journal entry order
*/
@ -29,7 +29,7 @@ document.addEventListener("DOMContentLoaded", () => {
const onReorder = () => {
const accounts = Array.from(list.children);
for (let i = 0; i < accounts.length; i++) {
const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no");
const no = document.getElementById(`accounting-order-${accounts[i].dataset.id}-no`);
no.value = String(i + 1);
}
};

View File

@ -1,4 +1,4 @@
/* The Mia! Accounting Flask Project
/* The Mia! Accounting Project
* material-fab-speed-dial.js: The JavaScript for the speed dial for the material floating buttons
*/

View File

@ -1,4 +1,4 @@
/* The Mia! Accounting Flask Project
/* The Mia! Accounting Project
* account-form.js: The JavaScript for the account form
*/
@ -197,7 +197,7 @@ class RecurringExpenseIncomeSubForm {
editor;
/**
* The prefix of HTML ID and class
* The prefix of the HTML ID and class names
* @type {string}
*/
#prefix;
@ -242,12 +242,12 @@ class RecurringExpenseIncomeSubForm {
this.#form = form;
this.expenseIncome = expenseIncome;
this.editor = new RecurringItemEditor(this);
this.#prefix = "accounting-recurring-" + expenseIncome;
this.#prefix = `accounting-recurring-${expenseIncome}`;
this.#element = document.getElementById(this.#prefix);
this.#content = document.getElementById(this.#prefix + "-content");
this.#itemList = document.getElementById(this.#prefix + "-list");
this.#items = Array.from(document.getElementsByClassName(this.#prefix + "-item")).map((element) => new RecurringItemSubForm(this, element));
this.#addButton = document.getElementById(this.#prefix + "-add");
this.#content = document.getElementById(`${this.#prefix}-content`);
this.#itemList = document.getElementById(`${this.#prefix}-list`);
this.#items = Array.from(document.getElementsByClassName(`${this.#prefix}-item`)).map((element) => new RecurringItemSubForm(this, element));
this.#addButton = document.getElementById(`${this.#prefix}-add`);
this.#resetContent();
this.#addButton.onclick = () => this.editor.onAddNew();
@ -265,7 +265,7 @@ class RecurringExpenseIncomeSubForm {
.replaceAll("EXPENSE_INCOME", escapeHtml(this.expenseIncome))
.replaceAll("ITEM_INDEX", escapeHtml(String(newIndex)));
this.#itemList.insertAdjacentHTML("beforeend", html);
const element = document.getElementById(this.#prefix + "-" + String(newIndex))
const element = document.getElementById(`${this.#prefix}-${String(newIndex)}`)
const item = new RecurringItemSubForm(this, element);
this.#items.push(item);
this.#resetContent();
@ -294,7 +294,7 @@ class RecurringExpenseIncomeSubForm {
this.#element.classList.remove("accounting-not-empty");
this.#element.classList.add("accounting-clickable");
this.#element.dataset.bsToggle = "modal"
this.#element.dataset.bsTarget = "#" + this.editor.modal.id;
this.#element.dataset.bsTarget = `#${this.editor.modal.id}`;
this.#element.onclick = () => this.editor.onAddNew();
this.#content.classList.add("d-none");
} else {
@ -313,10 +313,8 @@ class RecurringExpenseIncomeSubForm {
*/
#initializeDragAndDropReordering() {
initializeDragAndDropReordering(this.#itemList, () => {
const itemId = Array.from(this.#itemList.children).map((item) => item.id);
this.#items.sort((a, b) => itemId.indexOf(a.element.id) - itemId.indexOf(b.element.id));
for (let i = 0; i < this.#items.length; i++) {
this.#items[i].no.value = String(i + 1);
for (const item of this.#items) {
item.resetNo();
}
});
}
@ -365,7 +363,7 @@ class RecurringItemSubForm {
* The element
* @type {HTMLLIElement}
*/
element;
#element;
/**
* The item index
@ -386,10 +384,10 @@ class RecurringItemSubForm {
#error;
/**
* The number
* The order number
* @type {HTMLInputElement}
*/
no;
#no;
/**
* The name input
@ -441,33 +439,42 @@ class RecurringItemSubForm {
*/
constructor(expenseIncomeSubForm, element) {
this.#expenseIncomeSubForm = expenseIncomeSubForm
this.element = element;
this.#element = element;
this.itemIndex = parseInt(element.dataset.itemIndex);
const prefix = "accounting-recurring-" + expenseIncomeSubForm.expenseIncome + "-" + element.dataset.itemIndex;
this.#control = document.getElementById(prefix + "-control");
this.#error = document.getElementById(prefix + "-error");
this.no = document.getElementById(prefix + "-no");
this.#name = document.getElementById(prefix + "-name");
this.#nameText = document.getElementById(prefix + "-name-text");
this.#accountCode = document.getElementById(prefix + "-account-code");
this.#accountText = document.getElementById(prefix + "-account-text");
this.#descriptionTemplate = document.getElementById(prefix + "-description-template");
this.#descriptionTemplateText = document.getElementById(prefix + "-description-template-text");
this.deleteButton = document.getElementById(prefix + "-delete");
const prefix = `accounting-recurring-${expenseIncomeSubForm.expenseIncome}-${element.dataset.itemIndex}`;
this.#control = document.getElementById(`${prefix}-control`);
this.#error = document.getElementById(`${prefix}-error`);
this.#no = document.getElementById(`${prefix}-no`);
this.#name = document.getElementById(`${prefix}-name`);
this.#nameText = document.getElementById(`${prefix}-name-text`);
this.#accountCode = document.getElementById(`${prefix}-account-code`);
this.#accountText = document.getElementById(`${prefix}-account-text`);
this.#descriptionTemplate = document.getElementById(`${prefix}-description-template`);
this.#descriptionTemplateText = document.getElementById(`${prefix}-description-template-text`);
this.deleteButton = document.getElementById(`${prefix}-delete`);
this.#control.onclick = () => this.#expenseIncomeSubForm.editor.onEdit(this);
this.deleteButton.onclick = () => {
this.element.parentElement.removeChild(this.element);
this.#element.parentElement.removeChild(this.#element);
this.#expenseIncomeSubForm.deleteItem(this);
};
}
/**
* Reset the order number.
*
*/
resetNo() {
const siblings = Array.from(this.#element.parentElement.children);
this.#no.value = String(siblings.indexOf(this.#element) + 1);
}
/**
* Returns the name.
*
* @return {string|null} the name
*/
getName() {
get name() {
return this.#name.value === ""? null: this.#name.value;
}
@ -476,7 +483,7 @@ class RecurringItemSubForm {
*
* @return {string|null} the account code
*/
getAccountCode() {
get accountCode() {
return this.#accountCode.value === ""? null: this.#accountCode.value;
}
@ -485,7 +492,7 @@ class RecurringItemSubForm {
*
* @return {string|null} the account text
*/
getAccountText() {
get accountText() {
return this.#accountCode.dataset.text === ""? null: this.#accountCode.dataset.text;
}
@ -494,7 +501,7 @@ class RecurringItemSubForm {
*
* @return {string|null} the description template
*/
getDescriptionTemplate() {
get descriptionTemplate() {
return this.#descriptionTemplate.value === ""? null: this.#descriptionTemplate.value;
}
@ -504,12 +511,12 @@ class RecurringItemSubForm {
* @param editor {RecurringItemEditor} the recurring item editor
*/
save(editor) {
this.#name.value = editor.getName() === null? "": editor.getName();
this.#name.value = editor.name === null? "": editor.name;
this.#nameText.innerText = this.#name.value;
this.#accountCode.value = editor.accountCode;
this.#accountCode.dataset.text = editor.accountText;
this.#accountText.innerText = editor.accountText;
this.#descriptionTemplate.value = editor.getDescriptionTemplate() === null? "": editor.getDescriptionTemplate();
this.#descriptionTemplate.value = editor.descriptionTemplate === null? "": editor.descriptionTemplate;
this.#descriptionTemplateText.innerText = this.#descriptionTemplate.value;
this.validate();
}
@ -645,16 +652,16 @@ class RecurringItemEditor {
constructor(subForm) {
this.#subForm = subForm;
this.expenseIncome = subForm.expenseIncome;
const prefix = "accounting-recurring-item-editor-" + subForm.expenseIncome;
const prefix = `accounting-recurring-item-editor-${subForm.expenseIncome}`;
this.#form = document.getElementById(prefix);
this.modal = document.getElementById(prefix + "-modal");
this.#name = document.getElementById(prefix + "-name");
this.#nameError = document.getElementById(prefix + "-name-error");
this.#accountControl = document.getElementById(prefix + "-account-control");
this.#accountContainer = document.getElementById(prefix + "-account");
this.#accountError = document.getElementById(prefix + "-account-error");
this.#descriptionTemplate = document.getElementById(prefix + "-description-template");
this.#descriptionTemplateError = document.getElementById(prefix + "-description-template-error");
this.modal = document.getElementById(`${prefix}-modal`);
this.#name = document.getElementById(`${prefix}-name`);
this.#nameError = document.getElementById(`${prefix}-name-error`);
this.#accountControl = document.getElementById(`${prefix}-account-control`);
this.#accountContainer = document.getElementById(`${prefix}-account`);
this.#accountError = document.getElementById(`${prefix}-account-error`);
this.#descriptionTemplate = document.getElementById(`${prefix}-description-template`);
this.#descriptionTemplateError = document.getElementById(`${prefix}-description-template-error`);
this.#accountSelector = new RecurringAccountSelector(this);
this.#name.onchange = () => this.#validateName();
@ -677,7 +684,7 @@ class RecurringItemEditor {
*
* @return {string|null} the name
*/
getName() {
get name() {
return this.#name.value === ""? null: this.#name.value;
}
@ -686,7 +693,7 @@ class RecurringItemEditor {
*
* @return {string|null} the description template
*/
getDescriptionTemplate() {
get descriptionTemplate() {
return this.#descriptionTemplate.value === ""? null: this.#descriptionTemplate.value;
}
@ -742,16 +749,16 @@ class RecurringItemEditor {
*/
onEdit(item) {
this.#item = item;
this.#name.value = item.getName() === null? "": item.getName();
this.accountCode = item.getAccountCode();
this.accountText = item.getAccountText();
this.#name.value = item.name === null? "": item.name;
this.accountCode = item.accountCode;
this.accountText = item.accountText;
if (this.accountText === null) {
this.#accountControl.classList.remove("accounting-not-empty");
} else {
this.#accountControl.classList.add("accounting-not-empty");
}
this.#accountContainer.innerText = item.getAccountText() == null? "": item.getAccountText();
this.#descriptionTemplate.value = item.getDescriptionTemplate() === null? "": item.getDescriptionTemplate();
this.#accountContainer.innerText = this.accountText === null? "": this.accountText;
this.#descriptionTemplate.value = item.descriptionTemplate === null? "": item.descriptionTemplate;
this.#validate();
}
@ -875,12 +882,12 @@ class RecurringAccountSelector {
constructor(editor) {
this.editor = editor;
this.#expenseIncome = editor.expenseIncome;
const prefix = "accounting-recurring-accounting-selector-" + editor.expenseIncome;
this.#query = document.getElementById(prefix + "-query");
this.#queryNoResult = document.getElementById(prefix + "-option-no-result");
this.#optionList = document.getElementById(prefix + "-option-list");
this.#options = Array.from(document.getElementsByClassName(prefix + "-option")).map((element) => new RecurringAccount(this, element));
this.#clearButton = document.getElementById(prefix + "-clear");
const prefix = `accounting-recurring-accounting-selector-${editor.expenseIncome}`;
this.#query = document.getElementById(`${prefix}-query`);
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
this.#optionList = document.getElementById(`${prefix}-option-list`);
this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new RecurringAccount(this, element));
this.#clearButton = document.getElementById(`${prefix}-clear`);
this.#query.oninput = () => this.#filterOptions();
this.#clearButton.onclick = () => this.editor.clearAccount();
@ -891,16 +898,16 @@ class RecurringAccountSelector {
*
*/
#filterOptions() {
let hasAnyMatched = false;
let isAnyMatched = false;
for (const option of this.#options) {
if (option.isMatched(this.#query.value)) {
option.setShown(true);
hasAnyMatched = true;
isAnyMatched = true;
} else {
option.setShown(false);
}
}
if (!hasAnyMatched) {
if (!isAnyMatched) {
this.#optionList.classList.add("d-none");
this.#queryNoResult.classList.remove("d-none");
} else {
@ -916,7 +923,6 @@ class RecurringAccountSelector {
onOpen() {
this.#query.value = "";
this.#filterOptions();
console.log(this.editor.accountCode);
for (const option of this.#options) {
option.setActive(option.code === this.editor.accountCode);
}
@ -938,12 +944,6 @@ class RecurringAccountSelector {
*/
class RecurringAccount {
/**
* The account selector for the recurring item editor
* @type {RecurringAccountSelector}
*/
#selector;
/**
* The element
* @type {HTMLLIElement}
@ -975,13 +975,12 @@ class RecurringAccount {
* @param element {HTMLLIElement} the element
*/
constructor(selector, element) {
this.#selector = selector;
this.#element = element;
this.code = element.dataset.code;
this.text = element.dataset.text;
this.#queryValues = JSON.parse(element.dataset.queryValues);
this.#element.onclick = () => this.#selector.editor.saveAccount(this);
this.#element.onclick = () => selector.editor.saveAccount(this);
}
/**

View File

@ -1,4 +1,4 @@
/* The Mia! Accounting Flask Project
/* The Mia! Accounting Project
* original-line-item-selector.js: The JavaScript for the original line item selector
*/
@ -35,7 +35,7 @@ class OriginalLineItemSelector {
lineItemEditor;
/**
* The prefix of the HTML ID and class
* The prefix of the HTML ID and class names
* @type {string}
*/
#prefix = "accounting-original-line-item-selector";
@ -88,17 +88,15 @@ class OriginalLineItemSelector {
*/
constructor(lineItemEditor) {
this.lineItemEditor = lineItemEditor;
this.#query = document.getElementById(this.#prefix + "-query");
this.#queryNoResult = document.getElementById(this.#prefix + "-option-no-result");
this.#optionList = document.getElementById(this.#prefix + "-option-list");
this.#options = Array.from(document.getElementsByClassName(this.#prefix + "-option")).map((element) => new OriginalLineItem(this, element));
this.#query = document.getElementById(`${this.#prefix}-query`);
this.#queryNoResult = document.getElementById(`${this.#prefix}-option-no-result`);
this.#optionList = document.getElementById(`${this.#prefix}-option-list`);
this.#options = Array.from(document.getElementsByClassName(`${this.#prefix}-option`)).map((element) => new OriginalLineItem(this, element));
this.#optionById = {};
for (const option of this.#options) {
this.#optionById[option.id] = option;
}
this.#query.addEventListener("input", () => {
this.#filterOptions();
});
this.#query.oninput = () => this.#filterOptions();
}
/**
@ -113,8 +111,8 @@ class OriginalLineItemSelector {
const otherLineItems = form.getLineItems().filter((lineItem) => lineItem !== currentLineItem);
let otherOffset = new Decimal(0);
for (const otherLineItem of otherLineItems) {
if (otherLineItem.getOriginalLineItemId() === originalLineItemId) {
const amount = otherLineItem.getAmount();
if (otherLineItem.originalLineItemId === originalLineItemId) {
const amount = otherLineItem.amount;
if (amount !== null) {
otherOffset = otherOffset.plus(amount);
}
@ -131,8 +129,8 @@ class OriginalLineItemSelector {
const otherLineItems = this.lineItemEditor.form.getLineItems().filter((lineItem) => lineItem !== this.lineItemEditor.lineItem);
const otherOffsets = {}
for (const otherLineItem of otherLineItems) {
const otherOriginalLineItemId = otherLineItem.getOriginalLineItemId();
const amount = otherLineItem.getAmount();
const otherOriginalLineItemId = otherLineItem.originalLineItemId;
const amount = otherLineItem.amount;
if (otherOriginalLineItemId === null || amount === null) {
continue;
}
@ -155,16 +153,16 @@ class OriginalLineItemSelector {
*
*/
#filterOptions() {
let hasAnyMatched = false;
let isAnyMatched = false;
for (const option of this.#options) {
if (option.isMatched(this.#debitCredit, this.#currencyCode, this.#query.value)) {
option.setShown(true);
hasAnyMatched = true;
isAnyMatched = true;
} else {
option.setShown(false);
}
}
if (!hasAnyMatched) {
if (!isAnyMatched) {
this.#optionList.classList.add("d-none");
this.#queryNoResult.classList.remove("d-none");
} else {
@ -178,7 +176,7 @@ class OriginalLineItemSelector {
*
*/
onOpen() {
this.#currencyCode = this.lineItemEditor.getCurrencyCode();
this.#currencyCode = this.lineItemEditor.currencyCode;
this.#debitCredit = this.lineItemEditor.debitCredit;
for (const option of this.#options) {
option.setActive(option.id === this.lineItemEditor.originalLineItemId);
@ -196,10 +194,10 @@ class OriginalLineItemSelector {
class OriginalLineItem {
/**
* The original line item selector
* @type {OriginalLineItemSelector}
* The journal entry form
* @type {JournalEntryForm}
*/
#selector;
#form;
/**
* The element
@ -232,16 +230,10 @@ class OriginalLineItem {
#currencyCode;
/**
* The account code
* @type {string}
* The account
* @type {JournalEntryAccount}
*/
accountCode;
/**
* The account text
* @type {string}
*/
accountText;
account;
/**
* The description
@ -275,7 +267,7 @@ class OriginalLineItem {
/**
* The values to query against
* @type {string[][]}
* @type {string[]}
*/
#queryValues;
@ -286,21 +278,20 @@ class OriginalLineItem {
* @param element {HTMLLIElement} the element
*/
constructor(selector, element) {
this.#selector = selector;
this.#form = selector.lineItemEditor.form;
this.#element = element;
this.id = element.dataset.id;
this.date = element.dataset.date;
this.#debitCredit = element.dataset.debitCredit;
this.#currencyCode = element.dataset.currencyCode;
this.accountCode = element.dataset.accountCode;
this.accountText = element.dataset.accountText;
this.account = new JournalEntryAccount(element.dataset.accountCode, element.dataset.accountText, false);
this.description = element.dataset.description;
this.bareNetBalance = new Decimal(element.dataset.netBalance);
this.netBalance = this.bareNetBalance;
this.netBalanceText = document.getElementById("accounting-original-line-item-selector-option-" + this.id + "-net-balance");
this.netBalanceText = document.getElementById(`accounting-original-line-item-selector-option-${this.id}-net-balance`);
this.text = element.dataset.text;
this.#queryValues = JSON.parse(element.dataset.queryValues);
this.#element.onclick = () => this.#selector.lineItemEditor.saveOriginalLineItem(this);
this.#element.onclick = () => selector.lineItemEditor.saveOriginalLineItem(this);
}
/**
@ -341,10 +332,10 @@ class OriginalLineItem {
*/
isMatched(debitCredit, currencyCode, query = null) {
return this.netBalance.greaterThan(0)
&& this.date <= this.#selector.lineItemEditor.form.getDate()
&& this.#isDebitCreditMatches(debitCredit)
&& this.date <= this.#form.date
&& this.#isDebitCreditMatched(debitCredit)
&& this.#currencyCode === currencyCode
&& this.#isQueryMatches(query);
&& this.#isQueryMatched(query);
}
/**
@ -353,34 +344,43 @@ class OriginalLineItem {
* @param debitCredit {string} either "debit" or credit
* @return {boolean} true if the option matches, or false otherwise
*/
#isDebitCreditMatches(debitCredit) {
#isDebitCreditMatched(debitCredit) {
return (debitCredit === "debit" && this.#debitCredit === "credit")
|| (debitCredit === "credit" && this.#debitCredit === "debit");
}
/**
* Returns whether the original line item matches the query.
* Returns whether the original line item matches the query term.
*
* @param query {string|null} the query term
* @return {boolean} true if the option matches, or false otherwise
*/
#isQueryMatches(query) {
#isQueryMatched(query) {
if (query === "") {
return true;
}
for (const queryValue of this.#queryValues[0]) {
if (this.#getNetBalanceForQuery().includes(query.toLowerCase())) {
return true;
}
for (const queryValue of this.#queryValues) {
if (queryValue.toLowerCase().includes(query.toLowerCase())) {
return true;
}
}
for (const queryValue of this.#queryValues[1]) {
if (queryValue === query) {
return true;
}
}
return false;
}
/**
* Returns the net balance in the format for query match.
*
* @return {string} the net balance in the format for query match
*/
#getNetBalanceForQuery() {
const frac = this.netBalance.modulo(1);
const whole = Number(this.netBalance.minus(frac));
return String(whole) + String(frac).substring(1);
}
/**
* Sets whether the option is shown.
*

View File

@ -1,4 +1,4 @@
/* The Mia! Accounting Flask Project
/* The Mia! Accounting Project
* period-chooser.js: The JavaScript for the period chooser
*/
@ -33,12 +33,6 @@ document.addEventListener("DOMContentLoaded", () => {
*/
class PeriodChooser {
/**
* The prefix of the HTML ID and class
* @type {string}
*/
prefix;
/**
* The modal of the period chooser
* @type {HTMLDivElement}
@ -56,8 +50,8 @@ class PeriodChooser {
*
*/
constructor() {
this.prefix = "accounting-period-chooser";
this.modal = document.getElementById(this.prefix + "-modal");
const prefix = "accounting-period-chooser";
this.modal = document.getElementById(`${prefix}-modal`);
for (const cls of [MonthTab, YearTab, DayTab, CustomTab]) {
const tab = new cls(this);
this.tabPlanes[tab.tabId()] = tab;
@ -94,7 +88,7 @@ class TabPlane {
chooser;
/**
* The prefix of the HTML ID and class
* The prefix of the HTML ID and class names
* @type {string}
*/
prefix;
@ -118,9 +112,9 @@ class TabPlane {
*/
constructor(chooser) {
this.chooser = chooser;
this.prefix = "accounting-period-chooser-" + this.tabId();
this.#tab = document.getElementById(this.prefix + "-tab");
this.#page = document.getElementById(this.prefix + "-page");
this.prefix = `accounting-period-chooser-${this.tabId()}`;
this.#tab = document.getElementById(`${this.prefix}-tab`);
this.#page = document.getElementById(`${this.prefix}-page`);
this.#tab.onclick = () => this.#switchToMe();
}
@ -170,12 +164,11 @@ class MonthTab extends TabPlane {
*/
constructor(chooser) {
super(chooser);
const monthChooser = document.getElementById(this.prefix + "-chooser");
const monthChooser = document.getElementById(`${this.prefix}-chooser`);
if (monthChooser !== null) {
let start = monthChooser.dataset.start;
this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, {
restrictions: {
minDate: new Date(start),
minDate: new Date(monthChooser.dataset.start),
},
display: {
inline: true,
@ -188,9 +181,8 @@ class MonthTab extends TabPlane {
});
monthChooser.addEventListener(tempusDominus.Namespace.events.change, (e) => {
const date = e.detail.date;
const year = date.year;
const month = date.month + 1;
const period = month < 10? year + "-0" + month: year + "-" + month;
const zeroPaddedMonth = `0${date.month + 1}`.slice(-2)
const period = `${date.year}-${zeroPaddedMonth}`;
window.location = chooser.modal.dataset.urlTemplate
.replaceAll("PERIOD", period);
});
@ -250,8 +242,8 @@ class DayTab extends TabPlane {
*/
constructor(chooser) {
super(chooser);
this.#date = document.getElementById(this.prefix + "-date");
this.#dateError = document.getElementById(this.prefix + "-date-error");
this.#date = document.getElementById(`${this.prefix}-date`);
this.#dateError = document.getElementById(`${this.prefix}-date-error`);
if (this.#date !== null) {
this.#date.onchange = () => {
if (this.#validateDate()) {
@ -337,11 +329,11 @@ class CustomTab extends TabPlane {
*/
constructor(chooser) {
super(chooser);
this.#start = document.getElementById(this.prefix + "-start");
this.#startError = document.getElementById(this.prefix + "-start-error");
this.#end = document.getElementById(this.prefix + "-end");
this.#endError = document.getElementById(this.prefix + "-end-error");
this.#conform = document.getElementById(this.prefix + "-confirm");
this.#start = document.getElementById(`${this.prefix}-start`);
this.#startError = document.getElementById(`${this.prefix}-start-error`);
this.#end = document.getElementById(`${this.prefix}-end`);
this.#endError = document.getElementById(`${this.prefix}-end-error`);
this.#conform = document.getElementById(`${this.prefix}-confirm`);
if (this.#start !== null) {
this.#start.onchange = () => {
if (this.#validateStart()) {
@ -359,7 +351,7 @@ class CustomTab extends TabPlane {
isValid = this.#validateEnd() && isValid;
if (isValid) {
window.location = chooser.modal.dataset.urlTemplate
.replaceAll("PERIOD", this.#start.value + "-" + this.#end.value);
.replaceAll("PERIOD", `${this.#start.value}-${this.#end.value}`);
}
};
}

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
# Copyright (c) 2023 imacat.

View File

@ -1,4 +1,4 @@
# The Mia! Accounting Flask Project.
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
# Copyright (c) 2023 imacat.

View File

@ -1,5 +1,5 @@
{#
The Mia! Accounting Flask Project
The Mia! Accounting Project
create.html: The account creation form
Copyright (c) 2023 imacat.

View File

@ -1,5 +1,5 @@
{#
The Mia! Accounting Flask Project
The Mia! Accounting Project
detail.html: The account detail
Copyright (c) 2023 imacat.

View File

@ -1,5 +1,5 @@
{#
The Mia! Accounting Flask Project
The Mia! Accounting Project
edit.html: The account edit form
Copyright (c) 2023 imacat.

View File

@ -1,5 +1,5 @@
{#
The Mia! Accounting Flask Project
The Mia! Accounting Project
form.html: The account form
Copyright (c) 2023 imacat.

View File

@ -1,5 +1,5 @@
{#
The Mia! Accounting Flask Project
The Mia! Accounting Project
list.html: The account list
Copyright (c) 2023 imacat.

View File

@ -1,5 +1,5 @@
{#
The Mia! Accounting Flask Project
The Mia! Accounting Project
order.html: The order of the accounts under a same base account
Copyright (c) 2023 imacat.

View File

@ -1,5 +1,5 @@
{#
The Mia! Accounting Flask Project
The Mia! Accounting Project
detail.html: The base account detail
Copyright (c) 2023 imacat.

View File

@ -1,5 +1,5 @@
{#
The Mia! Accounting Flask Project
The Mia! Accounting Project
list.html: The base account list
Copyright (c) 2023 imacat.

View File

@ -1,5 +1,5 @@
{#
The Mia! Accounting Flask Project
The Mia! Accounting Project
base.html: The application-wide base template.
Copyright (c) 2023 imacat.

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