Compare commits
	
		
			164 Commits
		
	
	
		
			v0.7.0
			...
			7dd007f3cf
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7dd007f3cf | |||
| 38b8a028d5 | |||
| 213981a8b2 | |||
| a4d1789b58 | |||
| 91620d7db2 | |||
| 02fcabb0ce | |||
| 4c2dcc5070 | |||
| c9166fda4d | |||
| 3a0f0873e2 | |||
| a17395b43e | |||
| 17c8d9d1a9 | |||
| fa94cd407e | |||
| 9a704c8185 | |||
| 8286c0c6d8 | |||
| f7efacad75 | |||
| 9263ae0274 | |||
| 78a9d7794c | |||
| f3ae37a409 | |||
| ddc1081252 | |||
| 202d51a032 | |||
| 562bc47be7 | |||
| f3d43a66cc | |||
| c3fc6d9a87 | |||
| e1a0380628 | |||
| f2a2fcdd32 | |||
| ab29166f1e | |||
| 8033921181 | |||
| 08732c1e66 | |||
| 4adc464d3d | |||
| 2f9d2e36cb | |||
| 5bb10bf6ba | |||
| 06e7b6ddff | |||
| 20e1982984 | |||
| a70720be50 | |||
| cb6de08152 | |||
| 211821b4d7 | |||
| 0faca49540 | |||
| 14e79df571 | |||
| 04fbb725d2 | |||
| a1d6844e52 | |||
| 94391b02a6 | |||
| 1cb8a7563e | |||
| 63f0f28948 | |||
| 3431922f12 | |||
| d5a9e1af18 | |||
| 73f5d63f44 | |||
| bf2c7bb785 | |||
| 93ba086548 | |||
| 5c4f6017b8 | |||
| cb16b2f0ff | |||
| d2f11e8779 | |||
| 4ccaf01b3c | |||
| 7c512b1c15 | |||
| dc432da398 | |||
| c8504bcbf5 | |||
| c865141583 | |||
| 8c1ecd6eac | |||
| e8e4100677 | |||
| 6a8773c531 | |||
| 30e0c7682c | |||
| eb5a7bef7e | |||
| 8a174d8847 | |||
| 7459afd63a | |||
| a9afc385e9 | |||
| a8be739ec7 | |||
| 0130bc58a9 | |||
| 821059fa80 | |||
| 5b4f57d0b3 | |||
| 4bfac2d545 | |||
| f105f0cf7b | |||
| 5e320729d7 | |||
| 7515032082 | |||
| 361b18e411 | |||
| 7d084e570e | |||
| cb397910f8 | |||
| 5f8b0dec98 | |||
| 8398d1e8bb | |||
| 562801692a | |||
| faee1e61c6 | |||
| 57a4177037 | |||
| fa1dedf207 | |||
| 7ed13dc0af | |||
| 52807c5322 | |||
| 231a71feea | |||
| 4902eecae0 | |||
| 889e4c058e | |||
| 7262a6cb42 | |||
| c4ff4ecb3d | |||
| 2859f628ea | |||
| e0355b2af1 | |||
| b4d390c33a | |||
| a4ab8a761c | |||
| 907ce6d06e | |||
| 7e1388735e | |||
| 6f773dd837 | |||
| 87fa5aa6bc | |||
| 35e05b3708 | |||
| 7ccc96bda0 | |||
| 283758ebe9 | |||
| b673c7aeaf | |||
| 0ad2ac53dd | |||
| 7e90ec5a8f | |||
| 7755365467 | |||
| 979eea606a | |||
| 5a9e08f2c4 | |||
| 68c810d492 | |||
| 5f88260507 | |||
| 779d89f8c4 | |||
| 5d4bf4361b | |||
| 10170d613d | |||
| c885c08c37 | |||
| e2a4340f2a | |||
| 9728ff30e0 | |||
| a4644ede5f | |||
| 8f477dd6f1 | |||
| 44ac53f15c | |||
| 5edb5465c5 | |||
| 067afdb165 | |||
| 37a4c26f86 | |||
| 89e43830b4 | |||
| 671dbfb692 | |||
| 2014344d25 | |||
| f9c39709c8 | |||
| b394c58ec6 | |||
| 0af3e2785b | |||
| 7066f75e72 | |||
| 619540da49 | |||
| 567004f7d9 | |||
| 761d5a5824 | |||
| fa3cdace7f | |||
| 656762850c | |||
| e2325f08d0 | |||
| 855356084e | |||
| 7aaeb32a3d | |||
| b376cf1580 | |||
| ccbdc779ac | |||
| 61ee08fda2 | |||
| d8afadda02 | |||
| c8e1270d8f | |||
| 2a78799404 | |||
| 863d7a9368 | |||
| 6fd37b21d9 | |||
| bbf3ee3320 | |||
| b60cc7902d | |||
| 623313b58a | |||
| d0d2d77a2e | |||
| 494faeffea | |||
| 871a5fd1d8 | |||
| e615ad2690 | |||
| da92a0b42c | |||
| 678d0aa773 | |||
| 9248ba7e3b | |||
| 446087b212 | |||
| a42e7d13a2 | |||
| a82f5091f1 | |||
| 3455827c09 | |||
| 5dccf99a55 | |||
| 8818b46e01 | |||
| 2f3ad99467 | |||
| 592910187b | |||
| cb7a0d377f | |||
| 79175285f8 | |||
| fef474977c | |||
| fa1a55cd3d | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/27
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/27
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2022 imacat.
 | 
					#  Copyright (c) 2022 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2022-2023 imacat.
 | 
					#  Copyright (c) 2022-2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										26
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								README.rst
									
									
									
									
									
								
							@@ -1,24 +1,33 @@
 | 
				
			|||||||
=====================
 | 
					===============
 | 
				
			||||||
Mia! Accounting Flask
 | 
					Mia! Accounting
 | 
				
			||||||
=====================
 | 
					===============
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Description
 | 
					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.
 | 
					module for the Flask_ applications.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Install
 | 
					Install
 | 
				
			||||||
=======
 | 
					=======
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Install the latest source from the
 | 
					Install ``mia-accounting`` with ``pip``.
 | 
				
			||||||
`Mia! Accounting Flask repository`_.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
::
 | 
					::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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
 | 
					Copyright
 | 
				
			||||||
@@ -47,4 +56,5 @@ Authors
 | 
				
			|||||||
| 2023/1/27
 | 
					| 2023/1/27
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. _Flask: https://flask.palletsprojects.com
 | 
					.. _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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										29
									
								
								docs/source/accounting.option.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								docs/source/accounting.option.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					accounting.option package
 | 
				
			||||||
 | 
					=========================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Submodules
 | 
				
			||||||
 | 
					----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.option.forms module
 | 
				
			||||||
 | 
					------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.option.forms
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.option.views module
 | 
				
			||||||
 | 
					------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.option.views
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Module contents
 | 
				
			||||||
 | 
					---------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.option
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
@@ -28,14 +28,6 @@ accounting.report.utils.csv\_export module
 | 
				
			|||||||
   :undoc-members:
 | 
					   :undoc-members:
 | 
				
			||||||
   :show-inheritance:
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
accounting.report.utils.ie\_account module
 | 
					 | 
				
			||||||
------------------------------------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.. automodule:: accounting.report.utils.ie_account
 | 
					 | 
				
			||||||
   :members:
 | 
					 | 
				
			||||||
   :undoc-members:
 | 
					 | 
				
			||||||
   :show-inheritance:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
accounting.report.utils.option\_link module
 | 
					accounting.report.utils.option\_link module
 | 
				
			||||||
-------------------------------------------
 | 
					-------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,12 +11,21 @@ Subpackages
 | 
				
			|||||||
   accounting.base_account
 | 
					   accounting.base_account
 | 
				
			||||||
   accounting.currency
 | 
					   accounting.currency
 | 
				
			||||||
   accounting.journal_entry
 | 
					   accounting.journal_entry
 | 
				
			||||||
 | 
					   accounting.option
 | 
				
			||||||
   accounting.report
 | 
					   accounting.report
 | 
				
			||||||
   accounting.utils
 | 
					   accounting.utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Submodules
 | 
					Submodules
 | 
				
			||||||
----------
 | 
					----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.forms module
 | 
				
			||||||
 | 
					-----------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.forms
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
accounting.locale module
 | 
					accounting.locale module
 | 
				
			||||||
------------------------
 | 
					------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,14 @@ accounting.utils.cast module
 | 
				
			|||||||
   :undoc-members:
 | 
					   :undoc-members:
 | 
				
			||||||
   :show-inheritance:
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.utils.current\_account module
 | 
				
			||||||
 | 
					----------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.utils.current_account
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
accounting.utils.flash\_errors module
 | 
					accounting.utils.flash\_errors module
 | 
				
			||||||
-------------------------------------
 | 
					-------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -36,6 +44,14 @@ accounting.utils.next\_uri module
 | 
				
			|||||||
   :undoc-members:
 | 
					   :undoc-members:
 | 
				
			||||||
   :show-inheritance:
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.utils.options module
 | 
				
			||||||
 | 
					-------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.utils.options
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
accounting.utils.pagination module
 | 
					accounting.utils.pagination module
 | 
				
			||||||
----------------------------------
 | 
					----------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,10 +10,10 @@ sys.path.insert(0, os.path.abspath('../../src/'))
 | 
				
			|||||||
# -- Project information -----------------------------------------------------
 | 
					# -- Project information -----------------------------------------------------
 | 
				
			||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
 | 
					# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
 | 
				
			||||||
 | 
					
 | 
				
			||||||
project = 'Mia! Accounting Flask'
 | 
					project = 'Mia! Accounting'
 | 
				
			||||||
copyright = '2023, imacat'
 | 
					copyright = '2023, imacat'
 | 
				
			||||||
author = 'imacat'
 | 
					author = 'imacat'
 | 
				
			||||||
release = '0.7.0'
 | 
					release = '0.10.0'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# -- General configuration ---------------------------------------------------
 | 
					# -- General configuration ---------------------------------------------------
 | 
				
			||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 | 
					# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.
 | 
					   sphinx-quickstart on Fri Jan 27 12:20:04 2023.
 | 
				
			||||||
   You can adapt this file completely to your liking, but it should at least
 | 
					   You can adapt this file completely to your liking, but it should at least
 | 
				
			||||||
   contain the root `toctree` directive.
 | 
					   contain the root `toctree` directive.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Welcome to Mia! Accounting Flask's documentation!
 | 
					Welcome to Mia! Accounting's documentation!
 | 
				
			||||||
=================================================
 | 
					===========================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. toctree::
 | 
					.. toctree::
 | 
				
			||||||
   :maxdepth: 2
 | 
					   :maxdepth: 2
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2022 imacat.
 | 
					#  Copyright (c) 2022 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								setup.cfg
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								setup.cfg
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2022-2023 imacat.
 | 
					#  Copyright (c) 2022-2023 imacat.
 | 
				
			||||||
@@ -16,16 +16,16 @@
 | 
				
			|||||||
#  limitations under the License.
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[metadata]
 | 
					[metadata]
 | 
				
			||||||
name = mia-accounting-flask
 | 
					name = mia-accounting
 | 
				
			||||||
version = 0.7.0
 | 
					version = 0.10.0
 | 
				
			||||||
author = imacat
 | 
					author = imacat
 | 
				
			||||||
author_email = imacat@mail.imacat.idv.tw
 | 
					author_email = imacat@mail.imacat.idv.tw
 | 
				
			||||||
description = The Mia! Accounting Flask project.
 | 
					description = The Mia! Accounting project.
 | 
				
			||||||
long_description = file: README.rst
 | 
					long_description = file: README.rst
 | 
				
			||||||
long_description_content_type = text/x-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 =
 | 
					project_urls =
 | 
				
			||||||
    Bug Tracker = https://github.com/imacat/mia-accounting-flask/issues
 | 
					    Bug Tracker = https://github.com/imacat/mia-accounting/issues
 | 
				
			||||||
classifiers =
 | 
					classifiers =
 | 
				
			||||||
    Programming Language :: Python :: 3
 | 
					    Programming Language :: Python :: 3
 | 
				
			||||||
    License :: OSI Approved :: Apache Software License
 | 
					    License :: OSI Approved :: Apache Software License
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -47,7 +47,6 @@ def init_app(app: Flask, user_utils: UserUtilityInterface,
 | 
				
			|||||||
    init_user_utils(user_utils)
 | 
					    init_user_utils(user_utils)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    bp: Blueprint = Blueprint("accounting", __name__,
 | 
					    bp: Blueprint = Blueprint("accounting", __name__,
 | 
				
			||||||
                              url_prefix=url_prefix,
 | 
					 | 
				
			||||||
                              template_folder="templates",
 | 
					                              template_folder="templates",
 | 
				
			||||||
                              static_folder="static")
 | 
					                              static_folder="static")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -84,6 +83,9 @@ def init_app(app: Flask, user_utils: UserUtilityInterface,
 | 
				
			|||||||
    journal_entry.init_app(app, bp)
 | 
					    journal_entry.init_app(app, bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from . import report
 | 
					    from . import report
 | 
				
			||||||
    report.init_app(app, bp)
 | 
					    report.init_app(app, url_prefix)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    app.register_blueprint(bp)
 | 
					    from . import option
 | 
				
			||||||
 | 
					    option.init_app(bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app.register_blueprint(bp, url_prefix=url_prefix)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/31
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/31
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -157,6 +157,9 @@ def delete_account(account: Account) -> redirect:
 | 
				
			|||||||
    :return: The redirection to the account list on success, or the account
 | 
					    :return: The redirection to the account list on success, or the account
 | 
				
			||||||
        detail on error.
 | 
					        detail on error.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    if not account.can_delete:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The account cannot be deleted.")), "error")
 | 
				
			||||||
 | 
					        return redirect(inherit_next(__get_detail_uri(account)))
 | 
				
			||||||
    account.delete()
 | 
					    account.delete()
 | 
				
			||||||
    sort_accounts_in(account.base_code, account.id)
 | 
					    sort_accounts_in(account.base_code, account.id)
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -160,6 +160,9 @@ def delete_currency(currency: Currency) -> redirect:
 | 
				
			|||||||
    :return: The redirection to the currency list on success, or the currency
 | 
					    :return: The redirection to the currency list on success, or the currency
 | 
				
			||||||
        detail on error.
 | 
					        detail on error.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    if not currency.can_delete:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The currency cannot be deleted.")), "error")
 | 
				
			||||||
 | 
					        return redirect(inherit_next(__get_detail_uri(currency)))
 | 
				
			||||||
    currency.delete()
 | 
					    currency.delete()
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
    flash(s(lazy_gettext("The currency is deleted successfully.")), "success")
 | 
					    flash(s(lazy_gettext("The currency is deleted successfully.")), "success")
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										96
									
								
								src/accounting/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/accounting/forms.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask_babel import LazyString
 | 
				
			||||||
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
 | 
					from wtforms import StringField, ValidationError
 | 
				
			||||||
 | 
					from wtforms.validators import DataRequired
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
 | 
					from accounting.models import Currency, Account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ACCOUNT_REQUIRED: DataRequired = DataRequired(
 | 
				
			||||||
 | 
					    lazy_gettext("Please select the account."))
 | 
				
			||||||
 | 
					"""The validator to check if the account code is empty."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CurrencyExists:
 | 
				
			||||||
 | 
					    """The validator to check if the account exists."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if db.session.get(Currency, field.data) is None:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The currency does not exist."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AccountExists:
 | 
				
			||||||
 | 
					    """The validator to check if the account exists."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if Account.find_by_code(field.data) is None:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The account does not exist."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IsDebitAccount:
 | 
				
			||||||
 | 
					    """The validator to check if the account is for debit line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, message: str | LazyString):
 | 
				
			||||||
 | 
					        """Constructs the validator.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param message: The error message.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__message: str | LazyString = message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if re.match(r"^(?:[1235689]|7[5678])", field.data) \
 | 
				
			||||||
 | 
					                and not field.data.startswith("3351-") \
 | 
				
			||||||
 | 
					                and not field.data.startswith("3353-"):
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        raise ValidationError(self.__message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IsCreditAccount:
 | 
				
			||||||
 | 
					    """The validator to check if the account is for credit line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, message: str | LazyString):
 | 
				
			||||||
 | 
					        """Constructs the validator.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param message: The error message.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__message: str | LazyString = message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if re.match(r"^(?:[123489]|7[1234])", field.data) \
 | 
				
			||||||
 | 
					                and not field.data.startswith("3351-") \
 | 
				
			||||||
 | 
					                and not field.data.startswith("3353-"):
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        raise ValidationError(self.__message)
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -27,29 +27,20 @@ from wtforms import StringField, ValidationError, FieldList, IntegerField, \
 | 
				
			|||||||
from wtforms.validators import DataRequired
 | 
					from wtforms.validators import DataRequired
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.locale import lazy_gettext
 | 
					from accounting.forms import CurrencyExists
 | 
				
			||||||
from accounting.models import Currency, JournalEntryLineItem
 | 
					 | 
				
			||||||
from accounting.journal_entry.utils.offset_alias import offset_alias
 | 
					from accounting.journal_entry.utils.offset_alias import offset_alias
 | 
				
			||||||
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
 | 
					from accounting.models import JournalEntryLineItem
 | 
				
			||||||
from accounting.utils.cast import be
 | 
					from accounting.utils.cast import be
 | 
				
			||||||
from accounting.utils.strip_text import strip_text
 | 
					from accounting.utils.strip_text import strip_text
 | 
				
			||||||
from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
 | 
					from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CURRENCY_REQUIRED: DataRequired = DataRequired(
 | 
					CURRENCY_REQUIRED: DataRequired = DataRequired(
 | 
				
			||||||
    lazy_gettext("Please select the currency."))
 | 
					    lazy_gettext("Please select the currency."))
 | 
				
			||||||
"""The validator to check if the currency code is empty."""
 | 
					"""The validator to check if the currency code is empty."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CurrencyExists:
 | 
					 | 
				
			||||||
    """The validator to check if the account exists."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
					 | 
				
			||||||
        if field.data is None:
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        if db.session.get(Currency, field.data) is None:
 | 
					 | 
				
			||||||
            raise ValidationError(lazy_gettext(
 | 
					 | 
				
			||||||
                "The currency does not exist."))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SameCurrencyAsOriginalLineItems:
 | 
					class SameCurrencyAsOriginalLineItems:
 | 
				
			||||||
    """The validator to check if the currency is the same as the
 | 
					    """The validator to check if the currency is the same as the
 | 
				
			||||||
    original line items."""
 | 
					    original line items."""
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -220,7 +220,7 @@ class JournalEntryForm(FlaskForm):
 | 
				
			|||||||
        :return: The selectable debit accounts.
 | 
					        :return: The selectable debit accounts.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        accounts: list[AccountOption] \
 | 
					        accounts: list[AccountOption] \
 | 
				
			||||||
            = [AccountOption(x) for x in Account.debit()
 | 
					            = [AccountOption(x) for x in Account.selectable_debit()
 | 
				
			||||||
               if not (x.code[0] == "2" and x.is_need_offset)]
 | 
					               if not (x.code[0] == "2" and x.is_need_offset)]
 | 
				
			||||||
        in_use: set[int] = set(db.session.scalars(
 | 
					        in_use: set[int] = set(db.session.scalars(
 | 
				
			||||||
            sa.select(JournalEntryLineItem.account_id)
 | 
					            sa.select(JournalEntryLineItem.account_id)
 | 
				
			||||||
@@ -237,7 +237,7 @@ class JournalEntryForm(FlaskForm):
 | 
				
			|||||||
        :return: The selectable credit accounts.
 | 
					        :return: The selectable credit accounts.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        accounts: list[AccountOption] \
 | 
					        accounts: list[AccountOption] \
 | 
				
			||||||
            = [AccountOption(x) for x in Account.credit()
 | 
					            = [AccountOption(x) for x in Account.selectable_credit()
 | 
				
			||||||
               if not (x.code[0] == "1" and x.is_need_offset)]
 | 
					               if not (x.code[0] == "1" and x.is_need_offset)]
 | 
				
			||||||
        in_use: set[int] = set(db.session.scalars(
 | 
					        in_use: set[int] = set(db.session.scalars(
 | 
				
			||||||
            sa.select(JournalEntryLineItem.account_id)
 | 
					            sa.select(JournalEntryLineItem.account_id)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -17,7 +17,6 @@
 | 
				
			|||||||
"""The line item sub-forms for the journal entry management.
 | 
					"""The line item sub-forms for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
import re
 | 
					 | 
				
			||||||
from datetime import date
 | 
					from datetime import date
 | 
				
			||||||
from decimal import Decimal
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -26,9 +25,11 @@ from flask_babel import LazyString
 | 
				
			|||||||
from flask_wtf import FlaskForm
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
from sqlalchemy.orm import selectinload
 | 
					from sqlalchemy.orm import selectinload
 | 
				
			||||||
from wtforms import StringField, ValidationError, DecimalField, IntegerField
 | 
					from wtforms import StringField, ValidationError, DecimalField, IntegerField
 | 
				
			||||||
from wtforms.validators import DataRequired, Optional
 | 
					from wtforms.validators import Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.forms import ACCOUNT_REQUIRED, AccountExists, IsDebitAccount, \
 | 
				
			||||||
 | 
					    IsCreditAccount
 | 
				
			||||||
from accounting.locale import lazy_gettext
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
from accounting.models import Account, JournalEntryLineItem
 | 
					from accounting.models import Account, JournalEntryLineItem
 | 
				
			||||||
from accounting.template_filters import format_amount
 | 
					from accounting.template_filters import format_amount
 | 
				
			||||||
@@ -37,10 +38,6 @@ from accounting.utils.random_id import new_id
 | 
				
			|||||||
from accounting.utils.strip_text import strip_text
 | 
					from accounting.utils.strip_text import strip_text
 | 
				
			||||||
from accounting.utils.user import get_current_user_pk
 | 
					from accounting.utils.user import get_current_user_pk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ACCOUNT_REQUIRED: DataRequired = DataRequired(
 | 
					 | 
				
			||||||
    lazy_gettext("Please select the account."))
 | 
					 | 
				
			||||||
"""The validator to check if the account code is empty."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OriginalLineItemExists:
 | 
					class OriginalLineItemExists:
 | 
				
			||||||
    """The validator to check if the original line item exists."""
 | 
					    """The validator to check if the original line item exists."""
 | 
				
			||||||
@@ -105,45 +102,6 @@ class OriginalLineItemNotOffset:
 | 
				
			|||||||
                "The original line item cannot be an offset item."))
 | 
					                "The original line item cannot be an offset item."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountExists:
 | 
					 | 
				
			||||||
    """The validator to check if the account exists."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
					 | 
				
			||||||
        if field.data is None:
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        if Account.find_by_code(field.data) is None:
 | 
					 | 
				
			||||||
            raise ValidationError(lazy_gettext(
 | 
					 | 
				
			||||||
                "The account does not exist."))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class IsDebitAccount:
 | 
					 | 
				
			||||||
    """The validator to check if the account is for debit line items."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
					 | 
				
			||||||
        if field.data is None:
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        if re.match(r"^(?:[1235689]|7[5678])", field.data) \
 | 
					 | 
				
			||||||
                and not field.data.startswith("3351-") \
 | 
					 | 
				
			||||||
                and not field.data.startswith("3353-"):
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        raise ValidationError(lazy_gettext(
 | 
					 | 
				
			||||||
            "This account is not for debit line items."))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class IsCreditAccount:
 | 
					 | 
				
			||||||
    """The validator to check if the account is for credit line items."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
					 | 
				
			||||||
        if field.data is None:
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        if re.match(r"^(?:[123489]|7[1234])", field.data) \
 | 
					 | 
				
			||||||
                and not field.data.startswith("3351-") \
 | 
					 | 
				
			||||||
                and not field.data.startswith("3353-"):
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        raise ValidationError(lazy_gettext(
 | 
					 | 
				
			||||||
            "This account is not for credit line items."))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SameAccountAsOriginalLineItem:
 | 
					class SameAccountAsOriginalLineItem:
 | 
				
			||||||
    """The validator to check if the account is the same as the
 | 
					    """The validator to check if the account is the same as the
 | 
				
			||||||
    original line item."""
 | 
					    original line item."""
 | 
				
			||||||
@@ -452,12 +410,14 @@ class DebitLineItemForm(LineItemForm):
 | 
				
			|||||||
    """The ID of the original line item."""
 | 
					    """The ID of the original line item."""
 | 
				
			||||||
    account_code = StringField(
 | 
					    account_code = StringField(
 | 
				
			||||||
        filters=[strip_text],
 | 
					        filters=[strip_text],
 | 
				
			||||||
        validators=[ACCOUNT_REQUIRED,
 | 
					        validators=[
 | 
				
			||||||
                    AccountExists(),
 | 
					            ACCOUNT_REQUIRED,
 | 
				
			||||||
                    IsDebitAccount(),
 | 
					            AccountExists(),
 | 
				
			||||||
                    SameAccountAsOriginalLineItem(),
 | 
					            IsDebitAccount(lazy_gettext(
 | 
				
			||||||
                    KeepAccountWhenHavingOffset(),
 | 
					                "This account is not for debit line items.")),
 | 
				
			||||||
                    NotStartPayableFromDebit()])
 | 
					            SameAccountAsOriginalLineItem(),
 | 
				
			||||||
 | 
					            KeepAccountWhenHavingOffset(),
 | 
				
			||||||
 | 
					            NotStartPayableFromDebit()])
 | 
				
			||||||
    """The account code."""
 | 
					    """The account code."""
 | 
				
			||||||
    description = StringField(filters=[strip_text])
 | 
					    description = StringField(filters=[strip_text])
 | 
				
			||||||
    """The description."""
 | 
					    """The description."""
 | 
				
			||||||
@@ -502,12 +462,14 @@ class CreditLineItemForm(LineItemForm):
 | 
				
			|||||||
    """The ID of the original line item."""
 | 
					    """The ID of the original line item."""
 | 
				
			||||||
    account_code = StringField(
 | 
					    account_code = StringField(
 | 
				
			||||||
        filters=[strip_text],
 | 
					        filters=[strip_text],
 | 
				
			||||||
        validators=[ACCOUNT_REQUIRED,
 | 
					        validators=[
 | 
				
			||||||
                    AccountExists(),
 | 
					            ACCOUNT_REQUIRED,
 | 
				
			||||||
                    IsCreditAccount(),
 | 
					            AccountExists(),
 | 
				
			||||||
                    SameAccountAsOriginalLineItem(),
 | 
					            IsCreditAccount(lazy_gettext(
 | 
				
			||||||
                    KeepAccountWhenHavingOffset(),
 | 
					                "This account is not for credit line items.")),
 | 
				
			||||||
                    NotStartReceivableFromCredit()])
 | 
					            SameAccountAsOriginalLineItem(),
 | 
				
			||||||
 | 
					            KeepAccountWhenHavingOffset(),
 | 
				
			||||||
 | 
					            NotStartReceivableFromCredit()])
 | 
				
			||||||
    """The account code."""
 | 
					    """The account code."""
 | 
				
			||||||
    description = StringField(filters=[strip_text])
 | 
					    description = StringField(filters=[strip_text])
 | 
				
			||||||
    """The description."""
 | 
					    """The description."""
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/27
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/27
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -17,12 +17,14 @@
 | 
				
			|||||||
"""The description editor.
 | 
					"""The description editor.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
import typing as t
 | 
					import typing as t
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import sqlalchemy as sa
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.models import Account, JournalEntryLineItem
 | 
					from accounting.models import Account, JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.utils.options import options, Recurring
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DescriptionAccount:
 | 
					class DescriptionAccount:
 | 
				
			||||||
@@ -34,12 +36,14 @@ class DescriptionAccount:
 | 
				
			|||||||
        :param account: The account.
 | 
					        :param account: The account.
 | 
				
			||||||
        :param freq: The frequency of the tag with the account.
 | 
					        :param freq: The frequency of the tag with the account.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.account: Account = account
 | 
					        self.__account: Account = account
 | 
				
			||||||
        """The account."""
 | 
					        """The account."""
 | 
				
			||||||
        self.id: int = account.id
 | 
					        self.id: int = account.id
 | 
				
			||||||
        """The account ID."""
 | 
					        """The account ID."""
 | 
				
			||||||
        self.code: str = account.code
 | 
					        self.code: str = account.code
 | 
				
			||||||
        """The 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
 | 
					        self.freq: int = freq
 | 
				
			||||||
        """The frequency of the tag with the account."""
 | 
					        """The frequency of the tag with the account."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -48,7 +52,7 @@ class DescriptionAccount:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The string representation of the account.
 | 
					        :return: The string representation of the account.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return str(self.account)
 | 
					        return str(self.__account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_freq(self, freq: int) -> None:
 | 
					    def add_freq(self, freq: int) -> None:
 | 
				
			||||||
        """Adds the frequency of an account.
 | 
					        """Adds the frequency of an account.
 | 
				
			||||||
@@ -143,6 +147,29 @@ class DescriptionType:
 | 
				
			|||||||
        return sorted(self.__tag_dict.values(), key=lambda x: -x.freq)
 | 
					        return sorted(self.__tag_dict.values(), key=lambda x: -x.freq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DescriptionRecurring:
 | 
				
			||||||
 | 
					    """A recurring transaction."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, name: str, account: Account, description_template: str):
 | 
				
			||||||
 | 
					        """Constructs a recurring transaction.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param name: The name.
 | 
				
			||||||
 | 
					        :param description_template: The description template.
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.name: str = name
 | 
				
			||||||
 | 
					        self.account: DescriptionAccount = DescriptionAccount(account, 0)
 | 
				
			||||||
 | 
					        self.description_template: str = description_template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def account_codes(self) -> list[str]:
 | 
				
			||||||
 | 
					        """Returns the account codes by the order of their frequencies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The account codes by the order of their frequencies.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [self.account.code]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DescriptionDebitCredit:
 | 
					class DescriptionDebitCredit:
 | 
				
			||||||
    """The description on debit or credit."""
 | 
					    """The description on debit or credit."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -163,6 +190,8 @@ class DescriptionDebitCredit:
 | 
				
			|||||||
                               DescriptionType] \
 | 
					                               DescriptionType] \
 | 
				
			||||||
            = {x.id: x for x in {self.general, self.travel, self.bus}}
 | 
					            = {x.id: x for x in {self.general, self.travel, self.bus}}
 | 
				
			||||||
        """A dictionary from the type ID to the corresponding tags."""
 | 
					        """A dictionary from the type ID to the corresponding tags."""
 | 
				
			||||||
 | 
					        self.recurring: list[DescriptionRecurring] = []
 | 
				
			||||||
 | 
					        """The recurring transactions."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_tag(self, tag_type: t.Literal["general", "travel", "bus"],
 | 
					    def add_tag(self, tag_type: t.Literal["general", "travel", "bus"],
 | 
				
			||||||
                name: str, account: Account, freq: int) -> None:
 | 
					                name: str, account: Account, freq: int) -> None:
 | 
				
			||||||
@@ -193,6 +222,10 @@ class DescriptionDebitCredit:
 | 
				
			|||||||
                        freq[account.id] = 0
 | 
					                        freq[account.id] = 0
 | 
				
			||||||
                    freq[account.id] \
 | 
					                    freq[account.id] \
 | 
				
			||||||
                        = freq[account.id] + account.freq
 | 
					                        = freq[account.id] + account.freq
 | 
				
			||||||
 | 
					        for recurring in self.recurring:
 | 
				
			||||||
 | 
					            accounts[recurring.account.id] = recurring.account
 | 
				
			||||||
 | 
					            if recurring.account.id not in freq:
 | 
				
			||||||
 | 
					                freq[recurring.account.id] = 0
 | 
				
			||||||
        return [accounts[y] for y in sorted(freq.keys(),
 | 
					        return [accounts[y] for y in sorted(freq.keys(),
 | 
				
			||||||
                                            key=lambda x: -freq[x])]
 | 
					                                            key=lambda x: -freq[x])]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -206,6 +239,14 @@ class DescriptionEditor:
 | 
				
			|||||||
        """The debit tags."""
 | 
					        """The debit tags."""
 | 
				
			||||||
        self.credit: DescriptionDebitCredit = DescriptionDebitCredit("credit")
 | 
					        self.credit: DescriptionDebitCredit = DescriptionDebitCredit("credit")
 | 
				
			||||||
        """The credit tags."""
 | 
					        """The credit tags."""
 | 
				
			||||||
 | 
					        self.__init_tags()
 | 
				
			||||||
 | 
					        self.__init_recurring()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init_tags(self):
 | 
				
			||||||
 | 
					        """Initializes the tags.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
        debit_credit: sa.Label = sa.case(
 | 
					        debit_credit: sa.Label = sa.case(
 | 
				
			||||||
            (JournalEntryLineItem.is_debit, "debit"),
 | 
					            (JournalEntryLineItem.is_debit, "debit"),
 | 
				
			||||||
            else_="credit").label("debit_credit")
 | 
					            else_="credit").label("debit_credit")
 | 
				
			||||||
@@ -236,6 +277,50 @@ class DescriptionEditor:
 | 
				
			|||||||
            debit_credit_dict[row.debit_credit].add_tag(
 | 
					            debit_credit_dict[row.debit_credit].add_tag(
 | 
				
			||||||
                row.tag_type, row.tag, accounts[row.account_id], row.freq)
 | 
					                row.tag_type, row.tag, accounts[row.account_id], row.freq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init_recurring(self) -> None:
 | 
				
			||||||
 | 
					        """Initializes the recurring transactions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        recurring: Recurring = options.recurring
 | 
				
			||||||
 | 
					        accounts: dict[str, Account] \
 | 
				
			||||||
 | 
					            = self.__get_accounts(recurring.codes)
 | 
				
			||||||
 | 
					        self.debit.recurring \
 | 
				
			||||||
 | 
					            = [DescriptionRecurring(x.name, accounts[x.account_code],
 | 
				
			||||||
 | 
					                                    x.description_template)
 | 
				
			||||||
 | 
					               for x in recurring.expenses]
 | 
				
			||||||
 | 
					        self.credit.recurring \
 | 
				
			||||||
 | 
					            = [DescriptionRecurring(x.name, accounts[x.account_code],
 | 
				
			||||||
 | 
					                                    x.description_template)
 | 
				
			||||||
 | 
					               for x in recurring.incomes]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def __get_accounts(codes: set[str]) -> dict[str, Account]:
 | 
				
			||||||
 | 
					        """Finds and returns the accounts by codes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param codes: The account codes.
 | 
				
			||||||
 | 
					        :return: The account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if len(codes) == 0:
 | 
				
			||||||
 | 
					            return {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def get_condition(code0: str) -> sa.BinaryExpression:
 | 
				
			||||||
 | 
					            m: re.Match = re.match(r"^(\d{4})-(\d{3})$", code0)
 | 
				
			||||||
 | 
					            assert m is not None,\
 | 
				
			||||||
 | 
					                f"Malformed account code \"{code0}\" for regular transactions."
 | 
				
			||||||
 | 
					            return sa.and_(Account.base_code == m.group(1),
 | 
				
			||||||
 | 
					                           Account.no == int(m.group(2)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [get_condition(x) for x in codes]
 | 
				
			||||||
 | 
					        accounts: dict[str, Account] \
 | 
				
			||||||
 | 
					            = {x.code: x for x in
 | 
				
			||||||
 | 
					               Account.query.filter(sa.or_(*conditions)).all()}
 | 
				
			||||||
 | 
					        for code in codes:
 | 
				
			||||||
 | 
					            assert code in accounts,\
 | 
				
			||||||
 | 
					                f"Unknown account \"{code}\" for regular transactions."
 | 
				
			||||||
 | 
					        return accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_prefix(string: str | sa.Column, separator: str | sa.Column) \
 | 
					def get_prefix(string: str | sa.Column, separator: str | sa.Column) \
 | 
				
			||||||
        -> sa.Function:
 | 
					        -> sa.Function:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/15
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/15
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -72,11 +72,12 @@ def get_selectable_original_line_items(
 | 
				
			|||||||
    line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query\
 | 
					    line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query\
 | 
				
			||||||
        .filter(JournalEntryLineItem.id.in_({x for x in net_balances}))\
 | 
					        .filter(JournalEntryLineItem.id.in_({x for x in net_balances}))\
 | 
				
			||||||
        .join(JournalEntry)\
 | 
					        .join(JournalEntry)\
 | 
				
			||||||
        .order_by(JournalEntry.date, JournalEntryLineItem.is_debit,
 | 
					        .order_by(JournalEntry.date, JournalEntry.no,
 | 
				
			||||||
                  JournalEntryLineItem.no)\
 | 
					                  JournalEntryLineItem.is_debit, JournalEntryLineItem.no)\
 | 
				
			||||||
        .options(selectinload(JournalEntryLineItem.currency),
 | 
					        .options(selectinload(JournalEntryLineItem.currency),
 | 
				
			||||||
                 selectinload(JournalEntryLineItem.account),
 | 
					                 selectinload(JournalEntryLineItem.account),
 | 
				
			||||||
                 selectinload(JournalEntryLineItem.journal_entry)).all()
 | 
					                 selectinload(JournalEntryLineItem.journal_entry)).all()
 | 
				
			||||||
 | 
					    line_items.reverse()
 | 
				
			||||||
    for line_item in line_items:
 | 
					    for line_item in line_items:
 | 
				
			||||||
        line_item.net_balance = line_item.amount \
 | 
					        line_item.net_balance = line_item.amount \
 | 
				
			||||||
            if net_balances[line_item.id] is None \
 | 
					            if net_balances[line_item.id] is None \
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -175,6 +175,9 @@ def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
 | 
				
			|||||||
    :return: The redirection to the journal entry list on success, or the
 | 
					    :return: The redirection to the journal entry list on success, or the
 | 
				
			||||||
        journal entry detail on error.
 | 
					        journal entry detail on error.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    if not journal_entry.can_delete:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The journal entry cannot be deleted.")), "error")
 | 
				
			||||||
 | 
					        return redirect(inherit_next(__get_detail_uri(journal_entry)))
 | 
				
			||||||
    journal_entry.delete()
 | 
					    journal_entry.delete()
 | 
				
			||||||
    sort_journal_entries_in(journal_entry.date, journal_entry.id)
 | 
					    sort_journal_entries_in(journal_entry.date, journal_entry.id)
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
@@ -232,4 +235,4 @@ def __get_default_page_uri() -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    :return: The URI for the default page.
 | 
					    :return: The URI for the default page.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    return url_for("accounting.report.default")
 | 
					    return url_for("accounting-report.default")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -21,12 +21,11 @@ from __future__ import annotations
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
import typing as t
 | 
					import typing as t
 | 
				
			||||||
from datetime import date
 | 
					 | 
				
			||||||
from decimal import Decimal
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import sqlalchemy as sa
 | 
					import sqlalchemy as sa
 | 
				
			||||||
from flask import current_app
 | 
					from babel import Locale
 | 
				
			||||||
from flask_babel import get_locale
 | 
					from flask_babel import get_locale, get_babel
 | 
				
			||||||
from sqlalchemy import text
 | 
					from sqlalchemy import text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
@@ -61,11 +60,11 @@ class BaseAccount(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The title in the current locale.
 | 
					        :return: The title in the current locale.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            return self.title_l10n
 | 
					            return self.title_l10n
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                return l10n.title
 | 
					                return l10n.title
 | 
				
			||||||
        return self.title_l10n
 | 
					        return self.title_l10n
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -171,11 +170,11 @@ class Account(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The title in the current locale.
 | 
					        :return: The title in the current locale.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            return self.title_l10n
 | 
					            return self.title_l10n
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                return l10n.title
 | 
					                return l10n.title
 | 
				
			||||||
        return self.title_l10n
 | 
					        return self.title_l10n
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -189,15 +188,15 @@ class Account(db.Model):
 | 
				
			|||||||
        if self.title_l10n is None:
 | 
					        if self.title_l10n is None:
 | 
				
			||||||
            self.title_l10n = value
 | 
					            self.title_l10n = value
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            self.title_l10n = value
 | 
					            self.title_l10n = value
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                l10n.title = value
 | 
					                l10n.title = value
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
        self.l10n.append(AccountL10n(locale=current_locale, title=value))
 | 
					        self.l10n.append(AccountL10n(locale=str(current_locale), title=value))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def is_real(self) -> bool:
 | 
					    def is_real(self) -> bool:
 | 
				
			||||||
@@ -236,6 +235,16 @@ class Account(db.Model):
 | 
				
			|||||||
                return True
 | 
					                return True
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def can_delete(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether the account can be deleted.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if the account can be deleted, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.code in {"1111-001", "3351-001", "3353-001"}:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        return len(self.line_items) == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self) -> None:
 | 
					    def delete(self) -> None:
 | 
				
			||||||
        """Deletes this account.
 | 
					        """Deletes this account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -259,13 +268,15 @@ class Account(db.Model):
 | 
				
			|||||||
                                cls.no == int(m.group(2))).first()
 | 
					                                cls.no == int(m.group(2))).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def debit(cls) -> list[t.Self]:
 | 
					    def selectable_debit(cls) -> list[t.Self]:
 | 
				
			||||||
        """Returns the debit accounts.
 | 
					        """Returns the selectable debit accounts.
 | 
				
			||||||
 | 
					        Payable line items can not start from debit.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: The debit accounts.
 | 
					        :return: The selectable debit accounts.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return cls.query.filter(sa.or_(cls.base_code.startswith("1"),
 | 
					        return cls.query.filter(sa.or_(cls.base_code.startswith("1"),
 | 
				
			||||||
                                       cls.base_code.startswith("2"),
 | 
					                                       sa.and_(cls.base_code.startswith("2"),
 | 
				
			||||||
 | 
					                                               sa.not_(cls.is_need_offset)),
 | 
				
			||||||
                                       cls.base_code.startswith("3"),
 | 
					                                       cls.base_code.startswith("3"),
 | 
				
			||||||
                                       cls.base_code.startswith("5"),
 | 
					                                       cls.base_code.startswith("5"),
 | 
				
			||||||
                                       cls.base_code.startswith("6"),
 | 
					                                       cls.base_code.startswith("6"),
 | 
				
			||||||
@@ -280,12 +291,14 @@ class Account(db.Model):
 | 
				
			|||||||
            .order_by(cls.base_code, cls.no).all()
 | 
					            .order_by(cls.base_code, cls.no).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def credit(cls) -> list[t.Self]:
 | 
					    def selectable_credit(cls) -> list[t.Self]:
 | 
				
			||||||
        """Returns the debit accounts.
 | 
					        """Returns the selectable debit accounts.
 | 
				
			||||||
 | 
					        Receivable line items can not start from credit.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: The debit accounts.
 | 
					        :return: The selectable debit accounts.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return cls.query.filter(sa.or_(cls.base_code.startswith("1"),
 | 
					        return cls.query.filter(sa.or_(sa.and_(cls.base_code.startswith("1"),
 | 
				
			||||||
 | 
					                                               sa.not_(cls.is_need_offset)),
 | 
				
			||||||
                                       cls.base_code.startswith("2"),
 | 
					                                       cls.base_code.startswith("2"),
 | 
				
			||||||
                                       cls.base_code.startswith("3"),
 | 
					                                       cls.base_code.startswith("3"),
 | 
				
			||||||
                                       cls.base_code.startswith("4"),
 | 
					                                       cls.base_code.startswith("4"),
 | 
				
			||||||
@@ -381,11 +394,11 @@ class Currency(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The name in the current locale.
 | 
					        :return: The name in the current locale.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            return self.name_l10n
 | 
					            return self.name_l10n
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                return l10n.name
 | 
					                return l10n.name
 | 
				
			||||||
        return self.name_l10n
 | 
					        return self.name_l10n
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -399,15 +412,15 @@ class Currency(db.Model):
 | 
				
			|||||||
        if self.name_l10n is None:
 | 
					        if self.name_l10n is None:
 | 
				
			||||||
            self.name_l10n = value
 | 
					            self.name_l10n = value
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            self.name_l10n = value
 | 
					            self.name_l10n = value
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                l10n.name = value
 | 
					                l10n.name = value
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
        self.l10n.append(CurrencyL10n(locale=current_locale, name=value))
 | 
					        self.l10n.append(CurrencyL10n(locale=str(current_locale), name=value))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def is_modified(self) -> bool:
 | 
					    def is_modified(self) -> bool:
 | 
				
			||||||
@@ -422,6 +435,17 @@ class Currency(db.Model):
 | 
				
			|||||||
                return True
 | 
					                return True
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def can_delete(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether the currency can be deleted.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if the currency can be deleted, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        from accounting.template_globals import default_currency_code
 | 
				
			||||||
 | 
					        if self.code == default_currency_code():
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        return len(self.line_items) == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self) -> None:
 | 
					    def delete(self) -> None:
 | 
				
			||||||
        """Deletes the currency.
 | 
					        """Deletes the currency.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -597,14 +621,10 @@ class JournalEntry(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: True if the journal entry can be deleted, or False otherwise.
 | 
					        :return: True if the journal entry can be deleted, or False otherwise.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if not hasattr(self, "__can_delete"):
 | 
					        for line_item in self.line_items:
 | 
				
			||||||
            def has_offset() -> bool:
 | 
					            if len(line_item.offsets) > 0:
 | 
				
			||||||
                for line_item in self.line_items:
 | 
					 | 
				
			||||||
                    if len(line_item.offsets) > 0:
 | 
					 | 
				
			||||||
                        return True
 | 
					 | 
				
			||||||
                return False
 | 
					                return False
 | 
				
			||||||
            setattr(self, "__can_delete", not has_offset())
 | 
					        return True
 | 
				
			||||||
        return getattr(self, "__can_delete")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self) -> None:
 | 
					    def delete(self) -> None:
 | 
				
			||||||
        """Deletes the journal entry.
 | 
					        """Deletes the journal entry.
 | 
				
			||||||
@@ -739,7 +759,7 @@ class JournalEntryLineItem(db.Model):
 | 
				
			|||||||
        setattr(self, "__net_balance", net_balance)
 | 
					        setattr(self, "__net_balance", net_balance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def query_values(self) -> tuple[list[str], list[str]]:
 | 
					    def query_values(self) -> list[str]:
 | 
				
			||||||
        """Returns the values to be queried.
 | 
					        """Returns the values to be queried.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: The values to be queried.
 | 
					        :return: The values to be queried.
 | 
				
			||||||
@@ -749,16 +769,38 @@ class JournalEntryLineItem(db.Model):
 | 
				
			|||||||
            frac: Decimal = (value - whole).normalize()
 | 
					            frac: Decimal = (value - whole).normalize()
 | 
				
			||||||
            return str(whole) + str(abs(frac))[1:]
 | 
					            return str(whole) + str(abs(frac))[1:]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        journal_entry_day: date = self.journal_entry.date
 | 
					        return ["{}/{}/{}".format(self.journal_entry.date.year,
 | 
				
			||||||
        description: str = "" if self.description is None else self.description
 | 
					                                  self.journal_entry.date.month,
 | 
				
			||||||
        return ([description],
 | 
					                                  self.journal_entry.date.day),
 | 
				
			||||||
                [str(journal_entry_day.year),
 | 
					                "" if self.description is None else self.description,
 | 
				
			||||||
                 "{}/{}".format(journal_entry_day.year,
 | 
					                format_amount(self.amount)]
 | 
				
			||||||
                                journal_entry_day.month),
 | 
					
 | 
				
			||||||
                 "{}/{}".format(journal_entry_day.month,
 | 
					
 | 
				
			||||||
                                journal_entry_day.day),
 | 
					class Option(db.Model):
 | 
				
			||||||
                 "{}/{}/{}".format(journal_entry_day.year,
 | 
					    """An option."""
 | 
				
			||||||
                                   journal_entry_day.month,
 | 
					    __tablename__ = "accounting_options"
 | 
				
			||||||
                                   journal_entry_day.day),
 | 
					    """The table name."""
 | 
				
			||||||
                 format_amount(self.amount),
 | 
					    name = db.Column(db.String, nullable=False, primary_key=True)
 | 
				
			||||||
                 format_amount(self.net_balance)])
 | 
					    """The name."""
 | 
				
			||||||
 | 
					    value = db.Column(db.Text, nullable=False)
 | 
				
			||||||
 | 
					    """The option value."""
 | 
				
			||||||
 | 
					    created_at = db.Column(db.DateTime(timezone=True), nullable=False,
 | 
				
			||||||
 | 
					                           server_default=db.func.now())
 | 
				
			||||||
 | 
					    """The time of creation."""
 | 
				
			||||||
 | 
					    created_by_id = db.Column(db.Integer,
 | 
				
			||||||
 | 
					                              db.ForeignKey(user_pk_column,
 | 
				
			||||||
 | 
					                                            onupdate="CASCADE"),
 | 
				
			||||||
 | 
					                              nullable=False)
 | 
				
			||||||
 | 
					    """The ID of the creator."""
 | 
				
			||||||
 | 
					    created_by = db.relationship(user_cls, foreign_keys=created_by_id)
 | 
				
			||||||
 | 
					    """The creator."""
 | 
				
			||||||
 | 
					    updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
 | 
				
			||||||
 | 
					                           server_default=db.func.now())
 | 
				
			||||||
 | 
					    """The time of last update."""
 | 
				
			||||||
 | 
					    updated_by_id = db.Column(db.Integer,
 | 
				
			||||||
 | 
					                              db.ForeignKey(user_pk_column,
 | 
				
			||||||
 | 
					                                            onupdate="CASCADE"),
 | 
				
			||||||
 | 
					                              nullable=False)
 | 
				
			||||||
 | 
					    """The ID of the updator."""
 | 
				
			||||||
 | 
					    updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
 | 
				
			||||||
 | 
					    """The updator."""
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										30
									
								
								src/accounting/option/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/accounting/option/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The option management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from flask import Blueprint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def init_app(bp: Blueprint) -> None:
 | 
				
			||||||
 | 
					    """Initialize the application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param bp: The blueprint of the accounting application.
 | 
				
			||||||
 | 
					    :return: None.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    from .views import bp as option_bp
 | 
				
			||||||
 | 
					    bp.register_blueprint(option_bp, url_prefix="/options")
 | 
				
			||||||
							
								
								
									
										269
									
								
								src/accounting/option/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								src/accounting/option/forms.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,269 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The forms for the option management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from flask import render_template
 | 
				
			||||||
 | 
					from flask_babel import LazyString
 | 
				
			||||||
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
 | 
					from wtforms import StringField, FieldList, FormField, IntegerField
 | 
				
			||||||
 | 
					from wtforms.validators import DataRequired, ValidationError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.forms import ACCOUNT_REQUIRED, CurrencyExists, AccountExists, \
 | 
				
			||||||
 | 
					    IsDebitAccount, IsCreditAccount
 | 
				
			||||||
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
 | 
					from accounting.models import Account
 | 
				
			||||||
 | 
					from accounting.utils.current_account import CurrentAccount
 | 
				
			||||||
 | 
					from accounting.utils.options import Options
 | 
				
			||||||
 | 
					from accounting.utils.strip_text import strip_text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CurrentAccountExists:
 | 
				
			||||||
 | 
					    """The validator to check that the current account exists."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None or field.data == CurrentAccount.CURRENT_AL_CODE:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if Account.find_by_code(field.data) is None:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The account does not exist."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AccountNotCurrent:
 | 
				
			||||||
 | 
					    """The validator to check that the account is a current account."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None or field.data == CurrentAccount.CURRENT_AL_CODE:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if field.data[:2] not in {"11", "12", "21", "22"}:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "This is not a current account."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotStartPayableFromExpense:
 | 
				
			||||||
 | 
					    """The validator to check that a payable line item does not start from
 | 
				
			||||||
 | 
					    expense."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None or field.data[0] != "2":
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        account: Account | None = Account.find_by_code(field.data)
 | 
				
			||||||
 | 
					        if account is not None and account.is_need_offset:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "You cannot select a payable account as expense."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotStartReceivableFromIncome:
 | 
				
			||||||
 | 
					    """The validator to check that a receivable line item does not start
 | 
				
			||||||
 | 
					    from income."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None or field.data[0] != "1":
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        account: Account | None = Account.find_by_code(field.data)
 | 
				
			||||||
 | 
					        if account is not None and account.is_need_offset:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "You cannot select a receivable account as income."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RecurringItemForm(FlaskForm):
 | 
				
			||||||
 | 
					    """The base sub-form to add or update the recurring item."""
 | 
				
			||||||
 | 
					    no = IntegerField()
 | 
				
			||||||
 | 
					    """The order number of this recurring item."""
 | 
				
			||||||
 | 
					    name = StringField()
 | 
				
			||||||
 | 
					    """The name of the recurring item."""
 | 
				
			||||||
 | 
					    account_code = StringField()
 | 
				
			||||||
 | 
					    """The account code."""
 | 
				
			||||||
 | 
					    description_template = StringField()
 | 
				
			||||||
 | 
					    """The description template."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def account_text(self) -> str | None:
 | 
				
			||||||
 | 
					        """Returns the account text.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The account text.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.account_code.data is None:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        account: Account | None = Account.find_by_code(self.account_code.data)
 | 
				
			||||||
 | 
					        return None if account is None else str(account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def all_errors(self) -> list[str | LazyString]:
 | 
				
			||||||
 | 
					        """Returns all the errors of the form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: All the errors of the form.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        all_errors: list[str | LazyString] = []
 | 
				
			||||||
 | 
					        for key in self.errors:
 | 
				
			||||||
 | 
					            if key != "csrf_token":
 | 
				
			||||||
 | 
					                all_errors.extend(self.errors[key])
 | 
				
			||||||
 | 
					        return all_errors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RecurringExpenseForm(RecurringItemForm):
 | 
				
			||||||
 | 
					    """The sub-form to add or update the recurring expenses."""
 | 
				
			||||||
 | 
					    no = IntegerField()
 | 
				
			||||||
 | 
					    """The order number of this recurring item."""
 | 
				
			||||||
 | 
					    name = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[DataRequired(lazy_gettext("Please fill in the name."))])
 | 
				
			||||||
 | 
					    """The name of the recurring item."""
 | 
				
			||||||
 | 
					    account_code = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            ACCOUNT_REQUIRED,
 | 
				
			||||||
 | 
					            AccountExists(),
 | 
				
			||||||
 | 
					            IsDebitAccount(lazy_gettext("This account is not for expense.")),
 | 
				
			||||||
 | 
					            NotStartPayableFromExpense()])
 | 
				
			||||||
 | 
					    """The account code."""
 | 
				
			||||||
 | 
					    description_template = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            DataRequired(lazy_gettext(
 | 
				
			||||||
 | 
					                "Please fill in the description template."))])
 | 
				
			||||||
 | 
					    """The template for the line item description."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RecurringIncomeForm(RecurringItemForm):
 | 
				
			||||||
 | 
					    """The sub-form to add or update the recurring incomes."""
 | 
				
			||||||
 | 
					    no = IntegerField()
 | 
				
			||||||
 | 
					    """The order number of this recurring item."""
 | 
				
			||||||
 | 
					    name = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[DataRequired(lazy_gettext("Please fill in the name."))])
 | 
				
			||||||
 | 
					    """The name of the recurring item."""
 | 
				
			||||||
 | 
					    account_code = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            ACCOUNT_REQUIRED,
 | 
				
			||||||
 | 
					            AccountExists(),
 | 
				
			||||||
 | 
					            IsCreditAccount(lazy_gettext("This account is not for income.")),
 | 
				
			||||||
 | 
					            NotStartReceivableFromIncome()])
 | 
				
			||||||
 | 
					    """The account code."""
 | 
				
			||||||
 | 
					    description_template = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            DataRequired(lazy_gettext(
 | 
				
			||||||
 | 
					                "Please fill in the description template."))])
 | 
				
			||||||
 | 
					    """The description template."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RecurringForm(RecurringItemForm):
 | 
				
			||||||
 | 
					    """The sub-form for the recurring expenses and incomes."""
 | 
				
			||||||
 | 
					    expenses = FieldList(FormField(RecurringExpenseForm), name="expense")
 | 
				
			||||||
 | 
					    """The recurring expenses."""
 | 
				
			||||||
 | 
					    incomes = FieldList(FormField(RecurringIncomeForm), name="income")
 | 
				
			||||||
 | 
					    """The recurring incomes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def item_template(self) -> str:
 | 
				
			||||||
 | 
					        """Returns the template of a recurring item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The template of a recurring item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/option/include/form-recurring-item.html",
 | 
				
			||||||
 | 
					            expense_income="EXPENSE_INCOME",
 | 
				
			||||||
 | 
					            item_index="ITEM_INDEX",
 | 
				
			||||||
 | 
					            form=RecurringItemForm())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def expense_accounts(self) -> list[Account]:
 | 
				
			||||||
 | 
					        """The expense accounts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return Account.selectable_debit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def income_accounts(self) -> list[Account]:
 | 
				
			||||||
 | 
					        """The income accounts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return Account.selectable_credit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def as_data(self) -> dict[str, list[tuple[str, str, str]]]:
 | 
				
			||||||
 | 
					        """Returns the form data.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The form data.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        def as_tuple(item: RecurringItemForm) -> tuple[str, str, str]:
 | 
				
			||||||
 | 
					            return (item.name.data, item.account_code.data,
 | 
				
			||||||
 | 
					                    item.description_template.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expenses: list[RecurringItemForm] = [x.form for x in self.expenses]
 | 
				
			||||||
 | 
					        self.__sort_item_forms(expenses)
 | 
				
			||||||
 | 
					        incomes: list[RecurringItemForm] = [x.form for x in self.incomes]
 | 
				
			||||||
 | 
					        self.__sort_item_forms(incomes)
 | 
				
			||||||
 | 
					        return {"expense": [as_tuple(x) for x in expenses],
 | 
				
			||||||
 | 
					                "income": [as_tuple(x) for x in incomes]}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def __sort_item_forms(forms: list[RecurringItemForm]) -> None:
 | 
				
			||||||
 | 
					        """Sorts the recurring item sub-forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param forms: The recurring item sub-forms.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        ord_by_form: dict[RecurringItemForm, int] \
 | 
				
			||||||
 | 
					            = {forms[i]: i for i in range(len(forms))}
 | 
				
			||||||
 | 
					        recv_no: set[int] = {x.no.data for x in forms if x.no.data is not None}
 | 
				
			||||||
 | 
					        missing_recv_no: int = 100 if len(recv_no) == 0 else max(recv_no) + 100
 | 
				
			||||||
 | 
					        forms.sort(key=lambda x: (x.no.data or missing_recv_no,
 | 
				
			||||||
 | 
					                                  ord_by_form.get(x)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OptionForm(FlaskForm):
 | 
				
			||||||
 | 
					    """The form to update the options."""
 | 
				
			||||||
 | 
					    default_currency_code = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            DataRequired(lazy_gettext("Please select the default currency.")),
 | 
				
			||||||
 | 
					            CurrencyExists()])
 | 
				
			||||||
 | 
					    """The default currency code."""
 | 
				
			||||||
 | 
					    default_ie_account_code = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            DataRequired(lazy_gettext(
 | 
				
			||||||
 | 
					                "Please select the default account"
 | 
				
			||||||
 | 
					                " for the income and expenses log.")),
 | 
				
			||||||
 | 
					            CurrentAccountExists(),
 | 
				
			||||||
 | 
					            AccountNotCurrent()])
 | 
				
			||||||
 | 
					    """The default account code for the income and expenses log."""
 | 
				
			||||||
 | 
					    recurring = FormField(RecurringForm)
 | 
				
			||||||
 | 
					    """The recurring expenses and incomes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def populate_obj(self, obj: Options) -> None:
 | 
				
			||||||
 | 
					        """Populates the form data into a currency object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param obj: The currency object.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        obj.default_currency_code = self.default_currency_code.data
 | 
				
			||||||
 | 
					        obj.default_ie_account_code = self.default_ie_account_code.data
 | 
				
			||||||
 | 
					        obj.recurring_data = self.recurring.form.as_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def current_accounts(self) -> list[CurrentAccount]:
 | 
				
			||||||
 | 
					        """Returns the current accounts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The current accounts.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return CurrentAccount.accounts()
 | 
				
			||||||
							
								
								
									
										83
									
								
								src/accounting/option/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/accounting/option/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The views for the option management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from urllib.parse import parse_qsl, urlencode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask import Blueprint, render_template, redirect, session, request, \
 | 
				
			||||||
 | 
					    flash, url_for
 | 
				
			||||||
 | 
					from werkzeug.datastructures import ImmutableMultiDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
 | 
					from accounting.utils.cast import s
 | 
				
			||||||
 | 
					from accounting.utils.flash_errors import flash_form_errors
 | 
				
			||||||
 | 
					from accounting.utils.next_uri import inherit_next
 | 
				
			||||||
 | 
					from accounting.utils.options import options
 | 
				
			||||||
 | 
					from accounting.utils.permission import has_permission, can_admin
 | 
				
			||||||
 | 
					from .forms import OptionForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bp: Blueprint = Blueprint("option", __name__)
 | 
				
			||||||
 | 
					"""The view blueprint for the currency management."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("", endpoint="detail")
 | 
				
			||||||
 | 
					@has_permission(can_admin)
 | 
				
			||||||
 | 
					def show_options() -> str:
 | 
				
			||||||
 | 
					    """Shows the options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The options.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return render_template("accounting/option/detail.html", obj=options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("edit", endpoint="edit")
 | 
				
			||||||
 | 
					@has_permission(can_admin)
 | 
				
			||||||
 | 
					def show_option_form() -> str:
 | 
				
			||||||
 | 
					    """Shows the option form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The option form.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    form: OptionForm
 | 
				
			||||||
 | 
					    if "form" in session:
 | 
				
			||||||
 | 
					        form = OptionForm(ImmutableMultiDict(parse_qsl(session["form"])))
 | 
				
			||||||
 | 
					        del session["form"]
 | 
				
			||||||
 | 
					        form.validate()
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        form = OptionForm(obj=options)
 | 
				
			||||||
 | 
					    return render_template("accounting/option/form.html", form=form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.post("update", endpoint="update")
 | 
				
			||||||
 | 
					@has_permission(can_admin)
 | 
				
			||||||
 | 
					def update_options() -> redirect:
 | 
				
			||||||
 | 
					    """Updates the options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The redirection to the option form.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    form = OptionForm(request.form)
 | 
				
			||||||
 | 
					    if not form.validate():
 | 
				
			||||||
 | 
					        flash_form_errors(form)
 | 
				
			||||||
 | 
					        session["form"] = urlencode(list(request.form.items()))
 | 
				
			||||||
 | 
					        return redirect(inherit_next(url_for("accounting.option.edit")))
 | 
				
			||||||
 | 
					    form.populate_obj(options)
 | 
				
			||||||
 | 
					    if not options.is_modified:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The settings were not modified.")), "success")
 | 
				
			||||||
 | 
					        return redirect(inherit_next(url_for("accounting.option.detail")))
 | 
				
			||||||
 | 
					    options.commit()
 | 
				
			||||||
 | 
					    flash(s(lazy_gettext("The settings are saved successfully.")), "success")
 | 
				
			||||||
 | 
					    return redirect(inherit_next(url_for("accounting.option.detail")))
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -17,14 +17,14 @@
 | 
				
			|||||||
"""The report management.
 | 
					"""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.
 | 
					    """Initialize the application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param app: The Flask 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.
 | 
					    :return: None.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    from .converters import PeriodConverter, IncomeExpensesAccountConverter
 | 
					    from .converters import PeriodConverter, IncomeExpensesAccountConverter
 | 
				
			||||||
@@ -32,4 +32,4 @@ def init_app(app: Flask, bp: Blueprint) -> None:
 | 
				
			|||||||
    app.url_map.converters["ieAccount"] = IncomeExpensesAccountConverter
 | 
					    app.url_map.converters["ieAccount"] = IncomeExpensesAccountConverter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .views import bp as report_bp
 | 
					    from .views import bp as report_bp
 | 
				
			||||||
    bp.register_blueprint(report_bp, url_prefix="/reports")
 | 
					    app.register_blueprint(report_bp, url_prefix=url_prefix)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -23,8 +23,8 @@ from flask import abort
 | 
				
			|||||||
from werkzeug.routing import BaseConverter
 | 
					from werkzeug.routing import BaseConverter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.models import Account
 | 
					from accounting.models import Account
 | 
				
			||||||
 | 
					from accounting.utils.current_account import CurrentAccount
 | 
				
			||||||
from .period import Period, get_period
 | 
					from .period import Period, get_period
 | 
				
			||||||
from .utils.ie_account import IncomeExpensesAccount
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PeriodConverter(BaseConverter):
 | 
					class PeriodConverter(BaseConverter):
 | 
				
			||||||
@@ -55,22 +55,22 @@ class IncomeExpensesAccountConverter(BaseConverter):
 | 
				
			|||||||
    """The supplier converter to convert the income and expenses log pseudo
 | 
					    """The supplier converter to convert the income and expenses log pseudo
 | 
				
			||||||
    account code from and to the corresponding pseudo account in the routes."""
 | 
					    account code from and to the corresponding pseudo account in the routes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def to_python(self, value: str) -> IncomeExpensesAccount:
 | 
					    def to_python(self, value: str) -> CurrentAccount:
 | 
				
			||||||
        """Converts an account code to an account.
 | 
					        """Converts an account code to an account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param value: The account code.
 | 
					        :param value: The account code.
 | 
				
			||||||
        :return: The corresponding account.
 | 
					        :return: The corresponding account.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if value == IncomeExpensesAccount.CURRENT_AL_CODE:
 | 
					        if value == CurrentAccount.CURRENT_AL_CODE:
 | 
				
			||||||
            return IncomeExpensesAccount.current_assets_and_liabilities()
 | 
					            return CurrentAccount.current_assets_and_liabilities()
 | 
				
			||||||
        if not re.match("^[12][12]", value):
 | 
					        if not re.match("^[12][12]", value):
 | 
				
			||||||
            abort(404)
 | 
					            abort(404)
 | 
				
			||||||
        account: Account | None = Account.find_by_code(value)
 | 
					        account: Account | None = Account.find_by_code(value)
 | 
				
			||||||
        if account is None:
 | 
					        if account is None:
 | 
				
			||||||
            abort(404)
 | 
					            abort(404)
 | 
				
			||||||
        return IncomeExpensesAccount(account)
 | 
					        return CurrentAccount(account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def to_url(self, value: IncomeExpensesAccount) -> str:
 | 
					    def to_url(self, value: CurrentAccount) -> str:
 | 
				
			||||||
        """Converts an account to account code.
 | 
					        """Converts an account to account code.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param value: The account.
 | 
					        :param value: The account.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -33,12 +33,12 @@ from accounting.report.utils.base_page_params import BasePageParams
 | 
				
			|||||||
from accounting.report.utils.base_report import BaseReport
 | 
					from accounting.report.utils.base_report import BaseReport
 | 
				
			||||||
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
 | 
					from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
 | 
				
			||||||
    period_spec
 | 
					    period_spec
 | 
				
			||||||
from accounting.report.utils.ie_account import IncomeExpensesAccount
 | 
					 | 
				
			||||||
from accounting.report.utils.option_link import OptionLink
 | 
					from accounting.report.utils.option_link import OptionLink
 | 
				
			||||||
from accounting.report.utils.report_chooser import ReportChooser
 | 
					from accounting.report.utils.report_chooser import ReportChooser
 | 
				
			||||||
from accounting.report.utils.report_type import ReportType
 | 
					from accounting.report.utils.report_type import ReportType
 | 
				
			||||||
from accounting.report.utils.urls import income_expenses_url
 | 
					from accounting.report.utils.urls import income_expenses_url
 | 
				
			||||||
from accounting.utils.cast import be
 | 
					from accounting.utils.cast import be
 | 
				
			||||||
 | 
					from accounting.utils.current_account import CurrentAccount
 | 
				
			||||||
from accounting.utils.pagination import Pagination
 | 
					from accounting.utils.pagination import Pagination
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -84,7 +84,7 @@ class ReportLineItem:
 | 
				
			|||||||
class LineItemCollector:
 | 
					class LineItemCollector:
 | 
				
			||||||
    """The line item collector."""
 | 
					    """The line item collector."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, currency: Currency, account: IncomeExpensesAccount,
 | 
					    def __init__(self, currency: Currency, account: CurrentAccount,
 | 
				
			||||||
                 period: Period):
 | 
					                 period: Period):
 | 
				
			||||||
        """Constructs the line item collector.
 | 
					        """Constructs the line item collector.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -94,7 +94,7 @@ class LineItemCollector:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        self.__currency: Currency = currency
 | 
					        self.__currency: Currency = currency
 | 
				
			||||||
        """The currency."""
 | 
					        """The currency."""
 | 
				
			||||||
        self.__account: IncomeExpensesAccount = account
 | 
					        self.__account: CurrentAccount = account
 | 
				
			||||||
        """The account."""
 | 
					        """The account."""
 | 
				
			||||||
        self.__period: Period = period
 | 
					        self.__period: Period = period
 | 
				
			||||||
        """The period"""
 | 
					        """The period"""
 | 
				
			||||||
@@ -173,11 +173,8 @@ class LineItemCollector:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def __account_condition(self) -> sa.BinaryExpression:
 | 
					    def __account_condition(self) -> sa.BinaryExpression:
 | 
				
			||||||
        if self.__account.code == IncomeExpensesAccount.CURRENT_AL_CODE:
 | 
					        if self.__account.code == CurrentAccount.CURRENT_AL_CODE:
 | 
				
			||||||
            return sa.or_(Account.base_code.startswith("11"),
 | 
					            return CurrentAccount.sql_condition()
 | 
				
			||||||
                          Account.base_code.startswith("12"),
 | 
					 | 
				
			||||||
                          Account.base_code.startswith("21"),
 | 
					 | 
				
			||||||
                          Account.base_code.startswith("22"))
 | 
					 | 
				
			||||||
        return Account.id == self.__account.id
 | 
					        return Account.id == self.__account.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __get_total(self) -> ReportLineItem | None:
 | 
					    def __get_total(self) -> ReportLineItem | None:
 | 
				
			||||||
@@ -264,7 +261,7 @@ class PageParams(BasePageParams):
 | 
				
			|||||||
    """The HTML page parameters."""
 | 
					    """The HTML page parameters."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, currency: Currency,
 | 
					    def __init__(self, currency: Currency,
 | 
				
			||||||
                 account: IncomeExpensesAccount,
 | 
					                 account: CurrentAccount,
 | 
				
			||||||
                 period: Period,
 | 
					                 period: Period,
 | 
				
			||||||
                 has_data: bool,
 | 
					                 has_data: bool,
 | 
				
			||||||
                 pagination: Pagination[ReportLineItem],
 | 
					                 pagination: Pagination[ReportLineItem],
 | 
				
			||||||
@@ -283,7 +280,7 @@ class PageParams(BasePageParams):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        self.currency: Currency = currency
 | 
					        self.currency: Currency = currency
 | 
				
			||||||
        """The currency."""
 | 
					        """The currency."""
 | 
				
			||||||
        self.account: IncomeExpensesAccount = account
 | 
					        self.account: CurrentAccount = account
 | 
				
			||||||
        """The account."""
 | 
					        """The account."""
 | 
				
			||||||
        self.period: Period = period
 | 
					        self.period: Period = period
 | 
				
			||||||
        """The period."""
 | 
					        """The period."""
 | 
				
			||||||
@@ -341,8 +338,8 @@ class PageParams(BasePageParams):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The account options.
 | 
					        :return: The account options.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        current_al: IncomeExpensesAccount \
 | 
					        current_al: CurrentAccount \
 | 
				
			||||||
            = IncomeExpensesAccount.current_assets_and_liabilities()
 | 
					            = CurrentAccount.current_assets_and_liabilities()
 | 
				
			||||||
        options: list[OptionLink] \
 | 
					        options: list[OptionLink] \
 | 
				
			||||||
            = [OptionLink(str(current_al),
 | 
					            = [OptionLink(str(current_al),
 | 
				
			||||||
                          income_expenses_url(self.currency, current_al,
 | 
					                          income_expenses_url(self.currency, current_al,
 | 
				
			||||||
@@ -352,15 +349,12 @@ class PageParams(BasePageParams):
 | 
				
			|||||||
            .join(Account)\
 | 
					            .join(Account)\
 | 
				
			||||||
            .filter(be(JournalEntryLineItem.currency_code
 | 
					            .filter(be(JournalEntryLineItem.currency_code
 | 
				
			||||||
                       == self.currency.code),
 | 
					                       == self.currency.code),
 | 
				
			||||||
                    sa.or_(Account.base_code.startswith("11"),
 | 
					                    CurrentAccount.sql_condition())\
 | 
				
			||||||
                           Account.base_code.startswith("12"),
 | 
					 | 
				
			||||||
                           Account.base_code.startswith("21"),
 | 
					 | 
				
			||||||
                           Account.base_code.startswith("22")))\
 | 
					 | 
				
			||||||
            .group_by(JournalEntryLineItem.account_id)
 | 
					            .group_by(JournalEntryLineItem.account_id)
 | 
				
			||||||
        options.extend([OptionLink(str(x),
 | 
					        options.extend([OptionLink(str(x),
 | 
				
			||||||
                                   income_expenses_url(
 | 
					                                   income_expenses_url(
 | 
				
			||||||
                                       self.currency,
 | 
					                                       self.currency,
 | 
				
			||||||
                                       IncomeExpensesAccount(x),
 | 
					                                       CurrentAccount(x),
 | 
				
			||||||
                                       self.period),
 | 
					                                       self.period),
 | 
				
			||||||
                                   x.id == self.account.id)
 | 
					                                   x.id == self.account.id)
 | 
				
			||||||
                        for x in Account.query.filter(Account.id.in_(in_use))
 | 
					                        for x in Account.query.filter(Account.id.in_(in_use))
 | 
				
			||||||
@@ -371,7 +365,7 @@ class PageParams(BasePageParams):
 | 
				
			|||||||
class IncomeExpenses(BaseReport):
 | 
					class IncomeExpenses(BaseReport):
 | 
				
			||||||
    """The income and expenses log."""
 | 
					    """The income and expenses log."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, currency: Currency, account: IncomeExpensesAccount,
 | 
					    def __init__(self, currency: Currency, account: CurrentAccount,
 | 
				
			||||||
                 period: Period):
 | 
					                 period: Period):
 | 
				
			||||||
        """Constructs an income and expenses log.
 | 
					        """Constructs an income and expenses log.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -381,7 +375,7 @@ class IncomeExpenses(BaseReport):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        self.__currency: Currency = currency
 | 
					        self.__currency: Currency = currency
 | 
				
			||||||
        """The currency."""
 | 
					        """The currency."""
 | 
				
			||||||
        self.__account: IncomeExpensesAccount = account
 | 
					        self.__account: CurrentAccount = account
 | 
				
			||||||
        """The account."""
 | 
					        """The account."""
 | 
				
			||||||
        self.__period: Period = period
 | 
					        self.__period: Period = period
 | 
				
			||||||
        """The period."""
 | 
					        """The period."""
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/6
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/5
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -30,7 +30,7 @@ from accounting.locale import gettext
 | 
				
			|||||||
from accounting.models import Currency, Account
 | 
					from accounting.models import Currency, Account
 | 
				
			||||||
from accounting.report.period import Period, get_period
 | 
					from accounting.report.period import Period, get_period
 | 
				
			||||||
from accounting.template_globals import default_currency_code
 | 
					from accounting.template_globals import default_currency_code
 | 
				
			||||||
from .ie_account import IncomeExpensesAccount
 | 
					from accounting.utils.current_account import CurrentAccount
 | 
				
			||||||
from .option_link import OptionLink
 | 
					from .option_link import OptionLink
 | 
				
			||||||
from .report_type import ReportType
 | 
					from .report_type import ReportType
 | 
				
			||||||
from .urls import journal_url, ledger_url, income_expenses_url, \
 | 
					from .urls import journal_url, ledger_url, income_expenses_url, \
 | 
				
			||||||
@@ -68,9 +68,9 @@ class ReportChooser:
 | 
				
			|||||||
        """The title of the current report."""
 | 
					        """The title of the current report."""
 | 
				
			||||||
        self.is_search: bool = active_report == ReportType.SEARCH
 | 
					        self.is_search: bool = active_report == ReportType.SEARCH
 | 
				
			||||||
        """Whether the current report is the search page."""
 | 
					        """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.__income_expenses)
 | 
				
			||||||
 | 
					        self.__reports.append(self.__ledger)
 | 
				
			||||||
 | 
					        self.__reports.append(self.__journal)
 | 
				
			||||||
        self.__reports.append(self.__trial_balance)
 | 
					        self.__reports.append(self.__trial_balance)
 | 
				
			||||||
        self.__reports.append(self.__income_statement)
 | 
					        self.__reports.append(self.__income_statement)
 | 
				
			||||||
        self.__reports.append(self.__balance_sheet)
 | 
					        self.__reports.append(self.__balance_sheet)
 | 
				
			||||||
@@ -81,14 +81,20 @@ class ReportChooser:
 | 
				
			|||||||
            self.current_report = gettext("Search")
 | 
					            self.current_report = gettext("Search")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def __journal(self) -> OptionLink:
 | 
					    def __income_expenses(self) -> OptionLink:
 | 
				
			||||||
        """Returns the journal.
 | 
					        """Returns the income and expenses log.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: The journal.
 | 
					        :return: The income and expenses log.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return OptionLink(gettext("Journal"), journal_url(self.__period),
 | 
					        account: Account = self.__account
 | 
				
			||||||
                          self.__active_report == ReportType.JOURNAL,
 | 
					        if not re.match(r"[12][12]", account.base_code):
 | 
				
			||||||
                          fa_icon="fa-solid fa-book")
 | 
					            account: Account = Account.cash()
 | 
				
			||||||
 | 
					        return OptionLink(gettext("Income and Expenses Log"),
 | 
				
			||||||
 | 
					                          income_expenses_url(self.__currency,
 | 
				
			||||||
 | 
					                                              CurrentAccount(account),
 | 
				
			||||||
 | 
					                                              self.__period),
 | 
				
			||||||
 | 
					                          self.__active_report == ReportType.INCOME_EXPENSES,
 | 
				
			||||||
 | 
					                          fa_icon="fa-solid fa-money-bill-wave")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def __ledger(self) -> OptionLink:
 | 
					    def __ledger(self) -> OptionLink:
 | 
				
			||||||
@@ -103,20 +109,14 @@ class ReportChooser:
 | 
				
			|||||||
                          fa_icon="fa-solid fa-clipboard")
 | 
					                          fa_icon="fa-solid fa-clipboard")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def __income_expenses(self) -> OptionLink:
 | 
					    def __journal(self) -> OptionLink:
 | 
				
			||||||
        """Returns the income and expenses log.
 | 
					        """Returns the journal.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: The income and expenses log.
 | 
					        :return: The journal.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        account: Account = self.__account
 | 
					        return OptionLink(gettext("Journal"), journal_url(self.__period),
 | 
				
			||||||
        if not re.match(r"[12][12]", account.base_code):
 | 
					                          self.__active_report == ReportType.JOURNAL,
 | 
				
			||||||
            account: Account = Account.cash()
 | 
					                          fa_icon="fa-solid fa-book")
 | 
				
			||||||
        return OptionLink(gettext("Income and Expenses Log"),
 | 
					 | 
				
			||||||
                          income_expenses_url(self.__currency,
 | 
					 | 
				
			||||||
                                              IncomeExpensesAccount(account),
 | 
					 | 
				
			||||||
                                              self.__period),
 | 
					 | 
				
			||||||
                          self.__active_report == ReportType.INCOME_EXPENSES,
 | 
					 | 
				
			||||||
                          fa_icon="fa-solid fa-money-bill-wave")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def __trial_balance(self) -> OptionLink:
 | 
					    def __trial_balance(self) -> OptionLink:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -22,7 +22,8 @@ from flask import url_for
 | 
				
			|||||||
from accounting.models import Currency, Account
 | 
					from accounting.models import Currency, Account
 | 
				
			||||||
from accounting.report.period import Period
 | 
					from accounting.report.period import Period
 | 
				
			||||||
from accounting.template_globals import default_currency_code
 | 
					from accounting.template_globals import default_currency_code
 | 
				
			||||||
from .ie_account import IncomeExpensesAccount, default_ie_account_code
 | 
					from accounting.utils.current_account import CurrentAccount
 | 
				
			||||||
 | 
					from accounting.utils.options import options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def journal_url(period: Period) \
 | 
					def journal_url(period: Period) \
 | 
				
			||||||
@@ -33,8 +34,8 @@ def journal_url(period: Period) \
 | 
				
			|||||||
    :return: The URL of the journal.
 | 
					    :return: The URL of the journal.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if period.is_default:
 | 
					    if period.is_default:
 | 
				
			||||||
        return url_for("accounting.report.journal-default")
 | 
					        return url_for("accounting-report.journal-default")
 | 
				
			||||||
    return url_for("accounting.report.journal", period=period)
 | 
					    return url_for("accounting-report.journal", period=period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def ledger_url(currency: Currency, account: Account, period: Period) \
 | 
					def ledger_url(currency: Currency, account: Account, period: Period) \
 | 
				
			||||||
@@ -47,14 +48,14 @@ def ledger_url(currency: Currency, account: Account, period: Period) \
 | 
				
			|||||||
    :return: The URL of the ledger.
 | 
					    :return: The URL of the ledger.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if period.is_default:
 | 
					    if period.is_default:
 | 
				
			||||||
        return url_for("accounting.report.ledger-default",
 | 
					        return url_for("accounting-report.ledger-default",
 | 
				
			||||||
                       currency=currency, account=account)
 | 
					                       currency=currency, account=account)
 | 
				
			||||||
    return url_for("accounting.report.ledger",
 | 
					    return url_for("accounting-report.ledger",
 | 
				
			||||||
                   currency=currency, account=account,
 | 
					                   currency=currency, account=account,
 | 
				
			||||||
                   period=period)
 | 
					                   period=period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def income_expenses_url(currency: Currency, account: IncomeExpensesAccount,
 | 
					def income_expenses_url(currency: Currency, account: CurrentAccount,
 | 
				
			||||||
                        period: Period) -> str:
 | 
					                        period: Period) -> str:
 | 
				
			||||||
    """Returns the URL of an income and expenses log.
 | 
					    """Returns the URL of an income and expenses log.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -64,13 +65,13 @@ def income_expenses_url(currency: Currency, account: IncomeExpensesAccount,
 | 
				
			|||||||
    :return: The URL of the income and expenses log.
 | 
					    :return: The URL of the income and expenses log.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if currency.code == default_currency_code() \
 | 
					    if currency.code == default_currency_code() \
 | 
				
			||||||
            and account.code == default_ie_account_code() \
 | 
					            and account.code == options.default_ie_account_code \
 | 
				
			||||||
            and period.is_default:
 | 
					            and period.is_default:
 | 
				
			||||||
        return url_for("accounting.report.default")
 | 
					        return url_for("accounting-report.default")
 | 
				
			||||||
    if period.is_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)
 | 
					                       currency=currency, account=account)
 | 
				
			||||||
    return url_for("accounting.report.income-expenses",
 | 
					    return url_for("accounting-report.income-expenses",
 | 
				
			||||||
                   currency=currency, account=account,
 | 
					                   currency=currency, account=account,
 | 
				
			||||||
                   period=period)
 | 
					                   period=period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -83,9 +84,9 @@ def trial_balance_url(currency: Currency, period: Period) -> str:
 | 
				
			|||||||
    :return: The URL of the trial balance.
 | 
					    :return: The URL of the trial balance.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if period.is_default:
 | 
					    if period.is_default:
 | 
				
			||||||
        return url_for("accounting.report.trial-balance-default",
 | 
					        return url_for("accounting-report.trial-balance-default",
 | 
				
			||||||
                       currency=currency)
 | 
					                       currency=currency)
 | 
				
			||||||
    return url_for("accounting.report.trial-balance",
 | 
					    return url_for("accounting-report.trial-balance",
 | 
				
			||||||
                   currency=currency, period=period)
 | 
					                   currency=currency, period=period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -97,9 +98,9 @@ def income_statement_url(currency: Currency, period: Period) -> str:
 | 
				
			|||||||
    :return: The URL of the income statement.
 | 
					    :return: The URL of the income statement.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if period.is_default:
 | 
					    if period.is_default:
 | 
				
			||||||
        return url_for("accounting.report.income-statement-default",
 | 
					        return url_for("accounting-report.income-statement-default",
 | 
				
			||||||
                       currency=currency)
 | 
					                       currency=currency)
 | 
				
			||||||
    return url_for("accounting.report.income-statement",
 | 
					    return url_for("accounting-report.income-statement",
 | 
				
			||||||
                   currency=currency, period=period)
 | 
					                   currency=currency, period=period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -111,7 +112,7 @@ def balance_sheet_url(currency: Currency, period: Period) -> str:
 | 
				
			|||||||
    :return: The URL of the balance sheet.
 | 
					    :return: The URL of the balance sheet.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if period.is_default:
 | 
					    if period.is_default:
 | 
				
			||||||
        return url_for("accounting.report.balance-sheet-default",
 | 
					        return url_for("accounting-report.balance-sheet-default",
 | 
				
			||||||
                       currency=currency)
 | 
					                       currency=currency)
 | 
				
			||||||
    return url_for("accounting.report.balance-sheet",
 | 
					    return url_for("accounting-report.balance-sheet",
 | 
				
			||||||
                   currency=currency, period=period)
 | 
					                   currency=currency, period=period)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -23,13 +23,14 @@ from accounting import db
 | 
				
			|||||||
from accounting.models import Currency, Account
 | 
					from accounting.models import Currency, Account
 | 
				
			||||||
from accounting.report.period import Period, get_period
 | 
					from accounting.report.period import Period, get_period
 | 
				
			||||||
from accounting.template_globals import default_currency_code
 | 
					from accounting.template_globals import default_currency_code
 | 
				
			||||||
 | 
					from accounting.utils.current_account import CurrentAccount
 | 
				
			||||||
 | 
					from accounting.utils.options import options
 | 
				
			||||||
from accounting.utils.permission import has_permission, can_view
 | 
					from accounting.utils.permission import has_permission, can_view
 | 
				
			||||||
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
 | 
					from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
 | 
				
			||||||
    IncomeStatement, BalanceSheet, Search
 | 
					    IncomeStatement, BalanceSheet, Search
 | 
				
			||||||
from .template_filters import format_amount
 | 
					from .template_filters import format_amount
 | 
				
			||||||
from .utils.ie_account import IncomeExpensesAccount, default_ie_account
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
bp: Blueprint = Blueprint("report", __name__)
 | 
					bp: Blueprint = Blueprint("accounting-report", __name__)
 | 
				
			||||||
"""The view blueprint for the reports."""
 | 
					"""The view blueprint for the reports."""
 | 
				
			||||||
bp.add_app_template_filter(format_amount, "accounting_report_format_amount")
 | 
					bp.add_app_template_filter(format_amount, "accounting_report_format_amount")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -43,7 +44,7 @@ def get_default_report() -> str | Response:
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    return __get_income_expenses(
 | 
					    return __get_income_expenses(
 | 
				
			||||||
        db.session.get(Currency, default_currency_code()),
 | 
					        db.session.get(Currency, default_currency_code()),
 | 
				
			||||||
        default_ie_account(),
 | 
					        options.default_ie_account,
 | 
				
			||||||
        get_period())
 | 
					        get_period())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -126,8 +127,7 @@ def __get_ledger(currency: Currency, account: Account, period: Period) \
 | 
				
			|||||||
@bp.get("income-expenses/<currency:currency>/<ieAccount:account>",
 | 
					@bp.get("income-expenses/<currency:currency>/<ieAccount:account>",
 | 
				
			||||||
        endpoint="income-expenses-default")
 | 
					        endpoint="income-expenses-default")
 | 
				
			||||||
@has_permission(can_view)
 | 
					@has_permission(can_view)
 | 
				
			||||||
def get_default_income_expenses(currency: Currency,
 | 
					def get_default_income_expenses(currency: Currency, account: CurrentAccount) \
 | 
				
			||||||
                                account: IncomeExpensesAccount) \
 | 
					 | 
				
			||||||
        -> str | Response:
 | 
					        -> str | Response:
 | 
				
			||||||
    """Returns the income and expenses log in the default period.
 | 
					    """Returns the income and expenses log in the default period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -142,7 +142,7 @@ def get_default_income_expenses(currency: Currency,
 | 
				
			|||||||
    "income-expenses/<currency:currency>/<ieAccount:account>/<period:period>",
 | 
					    "income-expenses/<currency:currency>/<ieAccount:account>/<period:period>",
 | 
				
			||||||
    endpoint="income-expenses")
 | 
					    endpoint="income-expenses")
 | 
				
			||||||
@has_permission(can_view)
 | 
					@has_permission(can_view)
 | 
				
			||||||
def get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
 | 
					def get_income_expenses(currency: Currency, account: CurrentAccount,
 | 
				
			||||||
                        period: Period) -> str | Response:
 | 
					                        period: Period) -> str | Response:
 | 
				
			||||||
    """Returns the income and expenses log.
 | 
					    """Returns the income and expenses log.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -154,7 +154,7 @@ def get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
 | 
				
			|||||||
    return __get_income_expenses(currency, account, period)
 | 
					    return __get_income_expenses(currency, account, period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def __get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
 | 
					def __get_income_expenses(currency: Currency, account: CurrentAccount,
 | 
				
			||||||
                          period: Period) -> str | Response:
 | 
					                          period: Period) -> str | Response:
 | 
				
			||||||
    """Returns the income and expenses log.
 | 
					    """Returns the income and expenses log.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
/* The Mia! Accounting Flask Project
 | 
					/* The Mia! Accounting Project
 | 
				
			||||||
 * style.css: The style sheet for the accounting application.
 | 
					 * style.css: The style sheet for the accounting application.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -76,7 +76,7 @@
 | 
				
			|||||||
        height: 3.2rem;
 | 
					        height: 3.2rem;
 | 
				
			||||||
        width: 3.2rem;
 | 
					        width: 3.2rem;
 | 
				
			||||||
        border-radius: 50%;
 | 
					        border-radius: 50%;
 | 
				
			||||||
        margin-left: 1rem;
 | 
					        margin-right: 0.5rem;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    .accounting-toolbar > a.btn, .accounting-toolbar > .btn-group > a.btn {
 | 
					    .accounting-toolbar > a.btn, .accounting-toolbar > .btn-group > a.btn {
 | 
				
			||||||
        padding-top: 0.7rem;
 | 
					        padding-top: 0.7rem;
 | 
				
			||||||
@@ -315,6 +315,34 @@ a.accounting-report-table-row {
 | 
				
			|||||||
    padding-left: 1rem;
 | 
					    padding-left: 1rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* The description editor */
 | 
				
			||||||
 | 
					.accounting-description-editor-buttons {
 | 
				
			||||||
 | 
					    max-height: 7rem;
 | 
				
			||||||
 | 
					    overflow-y: scroll;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-description-editor-buttons .btn {
 | 
				
			||||||
 | 
					    margin-bottom: 0.3rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* The order of the journal entries in a same day */
 | 
				
			||||||
 | 
					.accounting-journal-entry-order-item, .accounting-journal-entry-order-item:hover {
 | 
				
			||||||
 | 
					    color: inherit;
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-journal-entry-order-item-currency {
 | 
				
			||||||
 | 
					    margin-left: 0.5rem;
 | 
				
			||||||
 | 
					    border-top: thin solid lightgray;
 | 
				
			||||||
 | 
					    margin-top: 0.2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* The illustration of the description template for the recurring transactions */
 | 
				
			||||||
 | 
					.accounting-recurring-description-template-illustration p {
 | 
				
			||||||
 | 
					    margin: 0.2rem 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-recurring-description-template-illustration ul {
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* The Material Design text field (floating form control in Bootstrap) */
 | 
					/* The Material Design text field (floating form control in Bootstrap) */
 | 
				
			||||||
.accounting-material-text-field {
 | 
					.accounting-material-text-field {
 | 
				
			||||||
    position: relative;
 | 
					    position: relative;
 | 
				
			||||||
@@ -339,7 +367,7 @@ a.accounting-report-table-row {
 | 
				
			|||||||
.accounting-material-fab {
 | 
					.accounting-material-fab {
 | 
				
			||||||
    position: fixed;
 | 
					    position: fixed;
 | 
				
			||||||
    right: 2rem;
 | 
					    right: 2rem;
 | 
				
			||||||
    bottom: 1rem;
 | 
					    bottom: 2rem;
 | 
				
			||||||
    z-index: 10;
 | 
					    z-index: 10;
 | 
				
			||||||
    flex-direction: column-reverse;
 | 
					    flex-direction: column-reverse;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
/* The Mia! Accounting Flask Project
 | 
					/* The Mia! Accounting Project
 | 
				
			||||||
 * account-form.js: The JavaScript for the account form
 | 
					 * account-form.js: The JavaScript for the account form
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -110,14 +110,23 @@ class AccountForm {
 | 
				
			|||||||
        this.#isNeedOffsetControl = document.getElementById("accounting-is-need-offset-control");
 | 
					        this.#isNeedOffsetControl = document.getElementById("accounting-is-need-offset-control");
 | 
				
			||||||
        this.#isNeedOffset = document.getElementById("accounting-is-need-offset");
 | 
					        this.#isNeedOffset = document.getElementById("accounting-is-need-offset");
 | 
				
			||||||
        this.#formElement.onsubmit = () => {
 | 
					        this.#formElement.onsubmit = () => {
 | 
				
			||||||
            return this.#validateForm();
 | 
					            return this.#validate();
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        this.#baseControl.onclick = () => {
 | 
					        this.#baseControl.onclick = () => {
 | 
				
			||||||
            this.#baseControl.classList.add("accounting-not-empty");
 | 
					            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.
 | 
					     * 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 account {BaseAccountOption} the selected base account
 | 
				
			||||||
     * @param text {string} the text for the base account
 | 
					 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    setBaseAccount(code, text) {
 | 
					    saveBaseAccount(account) {
 | 
				
			||||||
        this.#baseCode.value = code;
 | 
					        this.#baseCode.value = account.code;
 | 
				
			||||||
        this.#base.innerText = text;
 | 
					        this.#base.innerText = account.text;
 | 
				
			||||||
        if (["1", "2", "3"].includes(code.substring(0, 1))) {
 | 
					        if (["1", "2", "3"].includes(account.code.substring(0, 1))) {
 | 
				
			||||||
            this.#isNeedOffsetControl.classList.remove("d-none");
 | 
					            this.#isNeedOffsetControl.classList.remove("d-none");
 | 
				
			||||||
            this.#isNeedOffset.disabled = false;
 | 
					            this.#isNeedOffset.disabled = false;
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@@ -163,7 +171,7 @@ class AccountForm {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     * @returns {boolean} true if valid, or false otherwise
 | 
					     * @returns {boolean} true if valid, or false otherwise
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #validateForm() {
 | 
					    #validate() {
 | 
				
			||||||
        let isValid = true;
 | 
					        let isValid = true;
 | 
				
			||||||
        isValid = this.#validateBase() && isValid;
 | 
					        isValid = this.#validateBase() && isValid;
 | 
				
			||||||
        isValid = this.#validateTitle() && isValid;
 | 
					        isValid = this.#validateTitle() && isValid;
 | 
				
			||||||
@@ -225,7 +233,7 @@ class BaseAccountSelector {
 | 
				
			|||||||
     * The account form
 | 
					     * The account form
 | 
				
			||||||
     * @type {AccountForm}
 | 
					     * @type {AccountForm}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #form;
 | 
					    form;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The selector modal
 | 
					     * The selector modal
 | 
				
			||||||
@@ -253,7 +261,7 @@ class BaseAccountSelector {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The options
 | 
					     * The options
 | 
				
			||||||
     * @type {HTMLLIElement[]}
 | 
					     * @type {BaseAccountOption[]}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #options;
 | 
					    #options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -269,83 +277,54 @@ class BaseAccountSelector {
 | 
				
			|||||||
     * @param form {AccountForm} the form
 | 
					     * @param form {AccountForm} the form
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(form) {
 | 
					    constructor(form) {
 | 
				
			||||||
        this.#form = form;
 | 
					        this.form = form;
 | 
				
			||||||
        this.#modal = document.getElementById("accounting-base-selector-modal");
 | 
					        const prefix = "accounting-base-selector";
 | 
				
			||||||
        this.#query = document.getElementById("accounting-base-selector-query");
 | 
					        this.#modal = document.getElementById(`${prefix}-modal`);
 | 
				
			||||||
        this.#optionList = document.getElementById("accounting-base-selector-option-list");
 | 
					        this.#query = document.getElementById(`${prefix}-query`);
 | 
				
			||||||
        // noinspection JSValidateTypes
 | 
					        this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
 | 
				
			||||||
        this.#options = Array.from(document.getElementsByClassName("accounting-base-selector-option"));
 | 
					        this.#optionList = document.getElementById(`${prefix}-option-list`);
 | 
				
			||||||
        this.#clearButton = document.getElementById("accounting-base-selector-clear");
 | 
					        this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new BaseAccountOption(this, element));
 | 
				
			||||||
        this.#queryNoResult = document.getElementById("accounting-base-selector-option-no-result");
 | 
					        this.#clearButton = document.getElementById(`${prefix}-clear`);
 | 
				
			||||||
        this.#modal.addEventListener("hidden.bs.modal", () => {
 | 
					
 | 
				
			||||||
            this.#form.onBaseAccountSelectorClosed();
 | 
					        this.#modal.addEventListener("hidden.bs.modal", () => this.form.onBaseAccountSelectorClosed());
 | 
				
			||||||
        });
 | 
					        this.#query.oninput = () => this.#filterOptions();
 | 
				
			||||||
        for (const option of this.#options) {
 | 
					        this.#clearButton.onclick = () => this.form.clearBaseAccount();
 | 
				
			||||||
            option.onclick = () => {
 | 
					 | 
				
			||||||
                this.#form.setBaseAccount(option.dataset.code, option.dataset.content);
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this.#clearButton.onclick = () => {
 | 
					 | 
				
			||||||
            this.#form.clearBaseAccount();
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        this.#initializeBaseAccountQuery();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Initializes the query.
 | 
					     * Filters the options.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #initializeBaseAccountQuery() {
 | 
					    #filterOptions() {
 | 
				
			||||||
        this.#query.addEventListener("input", () => {
 | 
					        let isAnyMatched = false;
 | 
				
			||||||
            if (this.#query.value === "") {
 | 
					        for (const option of this.#options) {
 | 
				
			||||||
                for (const option of this.#options) {
 | 
					            if (option.isMatched(this.#query.value)) {
 | 
				
			||||||
                    option.classList.remove("d-none");
 | 
					                option.setShown(true);
 | 
				
			||||||
                }
 | 
					                isAnyMatched = true;
 | 
				
			||||||
                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");
 | 
					 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                this.#optionList.classList.remove("d-none");
 | 
					                option.setShown(false);
 | 
				
			||||||
                this.#queryNoResult.classList.add("d-none");
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        }
 | 
				
			||||||
 | 
					        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.
 | 
					     * 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) {
 | 
					        for (const option of this.#options) {
 | 
				
			||||||
            if (option.dataset.code === baseCode) {
 | 
					            option.setActive(option.code === this.form.baseCode);
 | 
				
			||||||
                option.classList.add("active");
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                option.classList.remove("active");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (baseCode === "") {
 | 
					        if (this.form.baseCode === null) {
 | 
				
			||||||
            this.#clearButton.classList.add("btn-secondary")
 | 
					            this.#clearButton.classList.add("btn-secondary")
 | 
				
			||||||
            this.#clearButton.classList.remove("btn-danger");
 | 
					            this.#clearButton.classList.remove("btn-danger");
 | 
				
			||||||
            this.#clearButton.disabled = true;
 | 
					            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");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
/* The Mia! Accounting Flask Project
 | 
					/* The Mia! Accounting Project
 | 
				
			||||||
 * account-order.js: The JavaScript for the account order
 | 
					 * account-order.js: The JavaScript for the account order
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,10 +29,11 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
        const onReorder = () => {
 | 
					        const onReorder = () => {
 | 
				
			||||||
            const accounts = Array.from(list.children);
 | 
					            const accounts = Array.from(list.children);
 | 
				
			||||||
            for (let i = 0; i < accounts.length; i++) {
 | 
					            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`);
 | 
				
			||||||
                const code = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-code");
 | 
					                const code = document.getElementById(`accounting-order-${accounts[i].dataset.id}-code`);
 | 
				
			||||||
                no.value = String(i + 1);
 | 
					                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);
 | 
					        initializeDragAndDropReordering(list, onReorder);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.content, 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;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
/* The Mia! Accounting Flask Project
 | 
					/* The Mia! Accounting Project
 | 
				
			||||||
 * currency-form.js: The JavaScript for the currency form
 | 
					 * currency-form.js: The JavaScript for the currency form
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -81,7 +81,7 @@ class CurrencyForm {
 | 
				
			|||||||
            this.#validateName();
 | 
					            this.#validateName();
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        this.#formElement.onsubmit = () => {
 | 
					        this.#formElement.onsubmit = () => {
 | 
				
			||||||
            this.#validateForm().then((isValid) => {
 | 
					            this.#validate().then((isValid) => {
 | 
				
			||||||
                if (isValid) {
 | 
					                if (isValid) {
 | 
				
			||||||
                    this.#formElement.submit();
 | 
					                    this.#formElement.submit();
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@@ -95,7 +95,7 @@ class CurrencyForm {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     * @returns {Promise<boolean>} true if valid, or false otherwise
 | 
					     * @returns {Promise<boolean>} true if valid, or false otherwise
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    async #validateForm() {
 | 
					    async #validate() {
 | 
				
			||||||
        let isValid = true;
 | 
					        let isValid = true;
 | 
				
			||||||
        isValid = await this.#validateCode() && isValid;
 | 
					        isValid = await this.#validateCode() && isValid;
 | 
				
			||||||
        isValid = this.#validateName() && isValid;
 | 
					        isValid = this.#validateName() && isValid;
 | 
				
			||||||
@@ -128,7 +128,7 @@ class CurrencyForm {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        const original = this.#code.dataset.original;
 | 
					        const original = this.#code.dataset.original;
 | 
				
			||||||
        if (original === "" || this.#code.value !== 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();
 | 
					            const data = await response.json();
 | 
				
			||||||
            if (data["exists"]) {
 | 
					            if (data["exists"]) {
 | 
				
			||||||
                this.#code.classList.add("is-invalid");
 | 
					                this.#code.classList.add("is-invalid");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
/* The Mia! Accounting Flask Project
 | 
					/* The Mia! Accounting Project
 | 
				
			||||||
 * description-editor.js: The JavaScript for the description editor
 | 
					 * description-editor.js: The JavaScript for the description editor
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,7 +32,7 @@ class DescriptionEditor {
 | 
				
			|||||||
     * The line item editor
 | 
					     * The line item editor
 | 
				
			||||||
     * @type {JournalEntryLineItemEditor}
 | 
					     * @type {JournalEntryLineItemEditor}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #lineItemEditor;
 | 
					    lineItemEditor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The description editor form
 | 
					     * The description editor form
 | 
				
			||||||
@@ -41,7 +41,7 @@ class DescriptionEditor {
 | 
				
			|||||||
    #form;
 | 
					    #form;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The prefix of the HTML ID and class
 | 
					     * The prefix of the HTML ID and class names
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    prefix;
 | 
					    prefix;
 | 
				
			||||||
@@ -60,7 +60,7 @@ class DescriptionEditor {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The current tab
 | 
					     * The current tab
 | 
				
			||||||
     * @type {TabPlane}
 | 
					     * @type {DescriptionEditorTabPlane}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    currentTab;
 | 
					    currentTab;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -68,7 +68,7 @@ class DescriptionEditor {
 | 
				
			|||||||
     * The description input
 | 
					     * The description input
 | 
				
			||||||
     * @type {HTMLInputElement}
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    description;
 | 
					    #descriptionInput;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The button to the original line item selector
 | 
					     * The button to the original line item selector
 | 
				
			||||||
@@ -89,20 +89,44 @@ class DescriptionEditor {
 | 
				
			|||||||
    note;
 | 
					    note;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The account buttons
 | 
					     * The placeholder of the confirmed account
 | 
				
			||||||
     * @type {HTMLButtonElement[]}
 | 
					     * @type {DescriptionEditorConfirmedAccount}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #accountButtons;
 | 
					    #confirmedAccountPlaceholder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The selected account button
 | 
					     * All the suggested accounts
 | 
				
			||||||
     * @type {HTMLButtonElement|null}
 | 
					     * @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
 | 
					     * The tab planes
 | 
				
			||||||
     * @type {{general: GeneralTagTab, travel: GeneralTripTab, bus: BusTripTab, regular: RegularPaymentTab, annotation: AnnotationTab}}
 | 
					     * @type {{general: DescriptionEditorGeneralTagTab, travel: DescriptionEditorGeneralTripTab, bus: DescriptionEditorBusTripTab, recurring: DescriptionEditorRecurringTab, annotation: DescriptionEditorAnnotationTab}}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    tabPlanes = {};
 | 
					    tabPlanes = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -113,26 +137,25 @@ class DescriptionEditor {
 | 
				
			|||||||
     * @param debitCredit {string} either "debit" or "credit"
 | 
					     * @param debitCredit {string} either "debit" or "credit"
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(lineItemEditor, debitCredit) {
 | 
					    constructor(lineItemEditor, debitCredit) {
 | 
				
			||||||
        this.#lineItemEditor = lineItemEditor;
 | 
					        this.lineItemEditor = lineItemEditor;
 | 
				
			||||||
        this.debitCredit = debitCredit;
 | 
					        this.debitCredit = debitCredit;
 | 
				
			||||||
        this.prefix = "accounting-description-editor-" + debitCredit;
 | 
					        this.prefix = `accounting-description-editor-${debitCredit}`;
 | 
				
			||||||
        this.#form = document.getElementById(this.prefix);
 | 
					        this.#form = document.getElementById(this.prefix);
 | 
				
			||||||
        this.#modal = document.getElementById(this.prefix + "-modal");
 | 
					        this.#modal = document.getElementById(`${this.prefix}-modal`);
 | 
				
			||||||
        this.description = document.getElementById(this.prefix + "-description");
 | 
					        this.#descriptionInput = document.getElementById(`${this.prefix}-description`);
 | 
				
			||||||
        this.#offsetButton = document.getElementById(this.prefix + "-offset");
 | 
					        this.#offsetButton = document.getElementById(`${this.prefix}-offset`);
 | 
				
			||||||
        this.number = document.getElementById(this.prefix + "-annotation-number");
 | 
					        this.number = document.getElementById(`${this.prefix}-annotation-number`);
 | 
				
			||||||
        this.note = document.getElementById(this.prefix + "-annotation-note");
 | 
					        this.note = document.getElementById(`${this.prefix}-annotation-note`);
 | 
				
			||||||
        // noinspection JSValidateTypes
 | 
					        this.#confirmedAccountPlaceholder = new DescriptionEditorConfirmedAccount(this, document.getElementById(`${this.prefix}-account-confirmed`));
 | 
				
			||||||
        this.#accountButtons = Array.from(document.getElementsByClassName(this.prefix + "-account"));
 | 
					        this.#allSuggestedAccounts = Array.from(document.getElementsByClassName(`${this.prefix}-account`)).map((button) => new DescriptionEditorSuggestedAccount(this, button));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const cls of [GeneralTagTab, GeneralTripTab, BusTripTab, RegularPaymentTab, AnnotationTab]) {
 | 
					        for (const cls of [DescriptionEditorGeneralTagTab, DescriptionEditorGeneralTripTab, DescriptionEditorBusTripTab, DescriptionEditorRecurringTab, DescriptionEditorAnnotationTab]) {
 | 
				
			||||||
            const tab = new cls(this);
 | 
					            const tab = new cls(this);
 | 
				
			||||||
            this.tabPlanes[tab.tabId()] = tab;
 | 
					            this.tabPlanes[tab.tabId()] = tab;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.currentTab = this.tabPlanes.general;
 | 
					        this.currentTab = this.tabPlanes.general;
 | 
				
			||||||
        this.#initializeSuggestedAccounts();
 | 
					        this.#descriptionInput.onchange = () => this.#onDescriptionChange();
 | 
				
			||||||
        this.description.onchange = () => this.#onDescriptionChange();
 | 
					        this.#offsetButton.onclick = () => this.lineItemEditor.originalLineItemSelector.onOpen();
 | 
				
			||||||
        this.#offsetButton.onclick = () => this.#lineItemEditor.originalLineItemSelector.onOpen();
 | 
					 | 
				
			||||||
        this.#form.onsubmit = () => {
 | 
					        this.#form.onsubmit = () => {
 | 
				
			||||||
            if (this.currentTab.validate()) {
 | 
					            if (this.currentTab.validate()) {
 | 
				
			||||||
                this.#submit();
 | 
					                this.#submit();
 | 
				
			||||||
@@ -141,13 +164,45 @@ 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.
 | 
					     * The callback when the description input is changed.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #onDescriptionChange() {
 | 
					    #onDescriptionChange() {
 | 
				
			||||||
        this.description.value = this.description.value.trim();
 | 
					        this.#resetTabPlanes();
 | 
				
			||||||
        for (const tabPlane of [this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) {
 | 
					        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()) {
 | 
					            if (tabPlane.populate()) {
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -156,56 +211,81 @@ class DescriptionEditor {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Filters the suggested accounts.
 | 
					     * Resets the tab planes.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param tagButton {HTMLButtonElement|null} the tag button
 | 
					 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    filterSuggestedAccounts(tagButton) {
 | 
					    #resetTabPlanes() {
 | 
				
			||||||
        for (const accountButton of this.#accountButtons) {
 | 
					        for (const tabPlane of Object.values(this.tabPlanes)) {
 | 
				
			||||||
            accountButton.classList.add("d-none");
 | 
					            tabPlane.reset();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (tagButton === null) {
 | 
					        this.tabPlanes.general.switchToMe();
 | 
				
			||||||
            this.#selectAccount(null);
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Updates the current suggested accounts.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param tagButton {HTMLButtonElement} the tag button
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    updateCurrentSuggestedAccounts(tagButton) {
 | 
				
			||||||
 | 
					        this.clearSuggestedAccounts();
 | 
				
			||||||
 | 
					        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;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        const suggested = JSON.parse(tagButton.dataset.accounts);
 | 
					        for (const account of this.#currentAccountOptions) {
 | 
				
			||||||
        let selectedAccountButton = null;
 | 
					            if (account.code === code) {
 | 
				
			||||||
        for (const accountButton of this.#accountButtons) {
 | 
					                this.selectAccount(account);
 | 
				
			||||||
            if (suggested.includes(accountButton.dataset.code)) {
 | 
					                return;
 | 
				
			||||||
                accountButton.classList.remove("d-none");
 | 
					 | 
				
			||||||
                if (accountButton.dataset.code === suggested[0]) {
 | 
					 | 
				
			||||||
                    selectedAccountButton = accountButton;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.#selectAccount(selectedAccountButton);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Initializes the suggested accounts.
 | 
					     * Clears the suggested accounts.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #initializeSuggestedAccounts() {
 | 
					    clearSuggestedAccounts() {
 | 
				
			||||||
        for (const accountButton of this.#accountButtons) {
 | 
					        for (const account of this.#allSuggestedAccounts) {
 | 
				
			||||||
            accountButton.onclick = () => this.#selectAccount(accountButton);
 | 
					            account.setShown(false);
 | 
				
			||||||
 | 
					            account.setActive(false);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        this.#currentSuggestedAccounts = [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Select a suggested account.
 | 
					     * Select an account.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param selectedAccountButton {HTMLButtonElement|null} the account button, or null to deselect the account
 | 
					     * @param selectedAccount {DescriptionEditorAccount|null} the account, or null to deselect the account
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #selectAccount(selectedAccountButton) {
 | 
					    selectAccount(selectedAccount) {
 | 
				
			||||||
        for (const accountButton of this.#accountButtons) {
 | 
					        for (const account of this.#currentAccountOptions) {
 | 
				
			||||||
            accountButton.classList.remove("btn-primary");
 | 
					            account.setActive(false);
 | 
				
			||||||
            accountButton.classList.add("btn-outline-primary");
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (selectedAccountButton !== null) {
 | 
					        if (selectedAccount !== null) {
 | 
				
			||||||
            selectedAccountButton.classList.remove("btn-outline-primary");
 | 
					            selectedAccount.setActive(true);
 | 
				
			||||||
            selectedAccountButton.classList.add("btn-primary");
 | 
					        }
 | 
				
			||||||
 | 
					        this.selectedAccount = selectedAccount;
 | 
				
			||||||
 | 
					        if (this.selectedAccount !== null) {
 | 
				
			||||||
 | 
					            this.isAccountConfirmed &&= this.selectedAccount.isConfirmedAccount;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.#selectedAccount = selectedAccountButton;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -214,11 +294,7 @@ class DescriptionEditor {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    #submit() {
 | 
					    #submit() {
 | 
				
			||||||
        bootstrap.Modal.getOrCreateInstance(this.#modal).hide();
 | 
					        bootstrap.Modal.getOrCreateInstance(this.#modal).hide();
 | 
				
			||||||
        if (this.#selectedAccount !== null) {
 | 
					        this.lineItemEditor.saveDescription(this);
 | 
				
			||||||
            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);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -226,21 +302,27 @@ class DescriptionEditor {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    onOpen() {
 | 
					    onOpen() {
 | 
				
			||||||
        this.#reset();
 | 
					        this.description = this.lineItemEditor.description === null? "": this.lineItemEditor.description;
 | 
				
			||||||
        this.description.value = this.#lineItemEditor.description === null? "": this.#lineItemEditor.description;
 | 
					        this.#setConfirmedAccount();
 | 
				
			||||||
        this.#onDescriptionChange();
 | 
					        this.#onDescriptionChange();
 | 
				
			||||||
 | 
					        if (this.isAccountConfirmed) {
 | 
				
			||||||
 | 
					            this.selectAccount(this.#confirmedAccount);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Resets the description editor.
 | 
					     * Sets the confirmed account.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #reset() {
 | 
					    #setConfirmedAccount() {
 | 
				
			||||||
        this.description.value = "";
 | 
					        this.isAccountConfirmed = this.lineItemEditor.isAccountConfirmed;
 | 
				
			||||||
        for (const tabPlane of Object.values(this.tabPlanes)) {
 | 
					        this.#confirmedAccountPlaceholder.setShown(this.isAccountConfirmed);
 | 
				
			||||||
            tabPlane.reset();
 | 
					        if (this.isAccountConfirmed) {
 | 
				
			||||||
 | 
					            this.#confirmedAccountPlaceholder.initializeFrom(this.lineItemEditor.account);
 | 
				
			||||||
 | 
					            this.#confirmedAccount = this.#confirmedAccountPlaceholder;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#confirmedAccount = null;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.tabPlanes.general.switchToMe();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -259,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.
 | 
					 * A tab plane.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @abstract
 | 
					 * @abstract
 | 
				
			||||||
 * @private
 | 
					 * @private
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class TabPlane {
 | 
					class DescriptionEditorTabPlane {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The parent description editor
 | 
					     * The parent description editor
 | 
				
			||||||
@@ -274,7 +473,7 @@ class TabPlane {
 | 
				
			|||||||
    editor;
 | 
					    editor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The prefix of the HTML ID and classes
 | 
					     * The prefix of the HTML ID and class names
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    prefix;
 | 
					    prefix;
 | 
				
			||||||
@@ -298,9 +497,9 @@ class TabPlane {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(editor) {
 | 
					    constructor(editor) {
 | 
				
			||||||
        this.editor = editor;
 | 
					        this.editor = editor;
 | 
				
			||||||
        this.prefix = this.editor.prefix + "-" + this.tabId();
 | 
					        this.prefix = `${this.editor.prefix}-${this.tabId()}`;
 | 
				
			||||||
        this.#tab = document.getElementById(this.prefix + "-tab");
 | 
					        this.#tab = document.getElementById(`${this.prefix}-tab`);
 | 
				
			||||||
        this.#page = document.getElementById(this.prefix + "-page");
 | 
					        this.#page = document.getElementById(`${this.prefix}-page`);
 | 
				
			||||||
        this.#tab.onclick = () => this.switchToMe();
 | 
					        this.#tab.onclick = () => this.switchToMe();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -360,7 +559,7 @@ class TabPlane {
 | 
				
			|||||||
 * @abstract
 | 
					 * @abstract
 | 
				
			||||||
 * @private
 | 
					 * @private
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class TagTabPlane extends TabPlane {
 | 
					class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The tag input
 | 
					     * The tag input
 | 
				
			||||||
@@ -388,10 +587,10 @@ class TagTabPlane extends TabPlane {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(editor) {
 | 
					    constructor(editor) {
 | 
				
			||||||
        super(editor);
 | 
					        super(editor);
 | 
				
			||||||
        this.tag = document.getElementById(this.prefix + "-tag");
 | 
					        this.tag = document.getElementById(`${this.prefix}-tag`);
 | 
				
			||||||
        this.tagError = document.getElementById(this.prefix + "-tag-error");
 | 
					        this.tagError = document.getElementById(`${this.prefix}-tag-error`);
 | 
				
			||||||
        // noinspection JSValidateTypes
 | 
					        // 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.initializeTagButtons();
 | 
				
			||||||
        this.tag.onchange = () => {
 | 
					        this.tag.onchange = () => {
 | 
				
			||||||
            this.onTagChange();
 | 
					            this.onTagChange();
 | 
				
			||||||
@@ -410,7 +609,7 @@ class TagTabPlane extends TabPlane {
 | 
				
			|||||||
            if (tagButton.dataset.value === this.tag.value) {
 | 
					            if (tagButton.dataset.value === this.tag.value) {
 | 
				
			||||||
                tagButton.classList.remove("btn-outline-primary");
 | 
					                tagButton.classList.remove("btn-outline-primary");
 | 
				
			||||||
                tagButton.classList.add("btn-primary");
 | 
					                tagButton.classList.add("btn-primary");
 | 
				
			||||||
                this.editor.filterSuggestedAccounts(tagButton);
 | 
					                this.editor.updateCurrentSuggestedAccounts(tagButton);
 | 
				
			||||||
                isMatched = true;
 | 
					                isMatched = true;
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                tagButton.classList.remove("btn-primary");
 | 
					                tagButton.classList.remove("btn-primary");
 | 
				
			||||||
@@ -418,7 +617,7 @@ class TagTabPlane extends TabPlane {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (!isMatched) {
 | 
					        if (!isMatched) {
 | 
				
			||||||
            this.editor.filterSuggestedAccounts(null);
 | 
					            this.editor.clearSuggestedAccounts();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.validateTag();
 | 
					        this.validateTag();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -436,14 +635,13 @@ class TagTabPlane extends TabPlane {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    switchToMe() {
 | 
					    switchToMe() {
 | 
				
			||||||
        super.switchToMe();
 | 
					        super.switchToMe();
 | 
				
			||||||
        let selectedTagButton = null;
 | 
					 | 
				
			||||||
        for (const tagButton of this.tagButtons) {
 | 
					        for (const tagButton of this.tagButtons) {
 | 
				
			||||||
            if (tagButton.classList.contains("btn-primary")) {
 | 
					            if (tagButton.classList.contains("btn-primary")) {
 | 
				
			||||||
                selectedTagButton = tagButton;
 | 
					                this.editor.updateCurrentSuggestedAccounts(tagButton);
 | 
				
			||||||
                break;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.editor.filterSuggestedAccounts(selectedTagButton);
 | 
					        this.editor.clearSuggestedAccounts();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -460,7 +658,7 @@ class TagTabPlane extends TabPlane {
 | 
				
			|||||||
                tagButton.classList.remove("btn-outline-primary");
 | 
					                tagButton.classList.remove("btn-outline-primary");
 | 
				
			||||||
                tagButton.classList.add("btn-primary");
 | 
					                tagButton.classList.add("btn-primary");
 | 
				
			||||||
                this.tag.value = tagButton.dataset.value;
 | 
					                this.tag.value = tagButton.dataset.value;
 | 
				
			||||||
                this.editor.filterSuggestedAccounts(tagButton);
 | 
					                this.editor.updateCurrentSuggestedAccounts(tagButton);
 | 
				
			||||||
                this.updateDescription();
 | 
					                this.updateDescription();
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -519,7 +717,7 @@ class TagTabPlane extends TabPlane {
 | 
				
			|||||||
 *
 | 
					 *
 | 
				
			||||||
 * @private
 | 
					 * @private
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class GeneralTagTab extends TagTabPlane {
 | 
					class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The tab ID
 | 
					     * The tab ID
 | 
				
			||||||
@@ -537,12 +735,12 @@ class GeneralTagTab extends TagTabPlane {
 | 
				
			|||||||
     * @override
 | 
					     * @override
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    updateDescription() {
 | 
					    updateDescription() {
 | 
				
			||||||
        const pos = this.editor.description.value.indexOf("—");
 | 
					        const pos = this.editor.description.indexOf("—");
 | 
				
			||||||
        const prefix = this.tag.value === ""? "": this.tag.value + "—";
 | 
					        const prefix = this.tag.value === ""? "": `${this.tag.value}—`;
 | 
				
			||||||
        if (pos === -1) {
 | 
					        if (pos === -1) {
 | 
				
			||||||
            this.editor.description.value = prefix + this.editor.description.value;
 | 
					            this.editor.description = `${prefix}${this.editor.description}`;
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            this.editor.description.value = prefix + this.editor.description.value.substring(pos + 1);
 | 
					            this.editor.description = `${prefix}${this.editor.description.substring(pos + 1)}`;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -553,7 +751,7 @@ class GeneralTagTab extends TagTabPlane {
 | 
				
			|||||||
     * @override
 | 
					     * @override
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    populate() {
 | 
					    populate() {
 | 
				
			||||||
        const found = this.editor.description.value.match(/^([^—]+)—/);
 | 
					        const found = this.editor.description.match(/^([^—]+)—/);
 | 
				
			||||||
        if (found === null) {
 | 
					        if (found === null) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -561,13 +759,6 @@ class GeneralTagTab extends TagTabPlane {
 | 
				
			|||||||
            this.tag.value = found[1];
 | 
					            this.tag.value = found[1];
 | 
				
			||||||
            this.onTagChange();
 | 
					            this.onTagChange();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        for (const tagButton of this.tagButtons) {
 | 
					 | 
				
			||||||
            if (tagButton.dataset.value === this.tag.value) {
 | 
					 | 
				
			||||||
                tagButton.classList.remove("btn-outline-primary");
 | 
					 | 
				
			||||||
                tagButton.classList.add("btn-primary");
 | 
					 | 
				
			||||||
                this.editor.filterSuggestedAccounts(tagButton);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this.switchToMe();
 | 
					        this.switchToMe();
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -587,7 +778,7 @@ class GeneralTagTab extends TagTabPlane {
 | 
				
			|||||||
 *
 | 
					 *
 | 
				
			||||||
 * @private
 | 
					 * @private
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class GeneralTripTab extends TagTabPlane {
 | 
					class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The origin
 | 
					     * The origin
 | 
				
			||||||
@@ -627,12 +818,12 @@ class GeneralTripTab extends TagTabPlane {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(editor) {
 | 
					    constructor(editor) {
 | 
				
			||||||
        super(editor);
 | 
					        super(editor);
 | 
				
			||||||
        this.#from = document.getElementById(this.prefix + "-from");
 | 
					        this.#from = document.getElementById(`${this.prefix}-from`);
 | 
				
			||||||
        this.#fromError = document.getElementById(this.prefix + "-from-error");
 | 
					        this.#fromError = document.getElementById(`${this.prefix}-from-error`);
 | 
				
			||||||
        this.#to = document.getElementById(this.prefix + "-to");
 | 
					        this.#to = document.getElementById(`${this.prefix}-to`);
 | 
				
			||||||
        this.#toError = document.getElementById(this.prefix + "-to-error")
 | 
					        this.#toError = document.getElementById(`${this.prefix}-to-error`)
 | 
				
			||||||
        // noinspection JSValidateTypes
 | 
					        // 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.onchange = () => {
 | 
				
			||||||
            this.#from.value = this.#from.value.trim();
 | 
					            this.#from.value = this.#from.value.trim();
 | 
				
			||||||
            this.updateDescription();
 | 
					            this.updateDescription();
 | 
				
			||||||
@@ -679,7 +870,7 @@ class GeneralTripTab extends TagTabPlane {
 | 
				
			|||||||
                break;
 | 
					                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}`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -713,7 +904,7 @@ class GeneralTripTab extends TagTabPlane {
 | 
				
			|||||||
     * @override
 | 
					     * @override
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    populate() {
 | 
					    populate() {
 | 
				
			||||||
        const found = this.editor.description.value.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
 | 
					        const found = this.editor.description.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
 | 
				
			||||||
        if (found === null) {
 | 
					        if (found === null) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -732,13 +923,6 @@ class GeneralTripTab extends TagTabPlane {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.#to.value = found[4];
 | 
					        this.#to.value = found[4];
 | 
				
			||||||
        for (const tagButton of this.tagButtons) {
 | 
					 | 
				
			||||||
            if (tagButton.dataset.value === this.tag.value) {
 | 
					 | 
				
			||||||
                tagButton.classList.remove("btn-outline-primary");
 | 
					 | 
				
			||||||
                tagButton.classList.add("btn-primary");
 | 
					 | 
				
			||||||
                this.editor.filterSuggestedAccounts(tagButton);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this.switchToMe();
 | 
					        this.switchToMe();
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -793,7 +977,7 @@ class GeneralTripTab extends TagTabPlane {
 | 
				
			|||||||
 *
 | 
					 *
 | 
				
			||||||
 * @private
 | 
					 * @private
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class BusTripTab extends TagTabPlane {
 | 
					class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The route
 | 
					     * The route
 | 
				
			||||||
@@ -839,12 +1023,12 @@ class BusTripTab extends TagTabPlane {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(editor) {
 | 
					    constructor(editor) {
 | 
				
			||||||
        super(editor);
 | 
					        super(editor);
 | 
				
			||||||
        this.#route = document.getElementById(this.prefix + "-route");
 | 
					        this.#route = document.getElementById(`${this.prefix}-route`);
 | 
				
			||||||
        this.#routeError = document.getElementById(this.prefix + "-route-error");
 | 
					        this.#routeError = document.getElementById(`${this.prefix}-route-error`);
 | 
				
			||||||
        this.#from = document.getElementById(this.prefix + "-from");
 | 
					        this.#from = document.getElementById(`${this.prefix}-from`);
 | 
				
			||||||
        this.#fromError = document.getElementById(this.prefix + "-from-error");
 | 
					        this.#fromError = document.getElementById(`${this.prefix}-from-error`);
 | 
				
			||||||
        this.#to = document.getElementById(this.prefix + "-to");
 | 
					        this.#to = document.getElementById(`${this.prefix}-to`);
 | 
				
			||||||
        this.#toError = document.getElementById(this.prefix + "-to-error")
 | 
					        this.#toError = document.getElementById(`${this.prefix}-to-error`)
 | 
				
			||||||
        this.#route.onchange = () => {
 | 
					        this.#route.onchange = () => {
 | 
				
			||||||
            this.#route.value = this.#route.value.trim();
 | 
					            this.#route.value = this.#route.value.trim();
 | 
				
			||||||
            this.updateDescription();
 | 
					            this.updateDescription();
 | 
				
			||||||
@@ -878,7 +1062,7 @@ class BusTripTab extends TagTabPlane {
 | 
				
			|||||||
     * @override
 | 
					     * @override
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    updateDescription() {
 | 
					    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}`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -906,7 +1090,7 @@ class BusTripTab extends TagTabPlane {
 | 
				
			|||||||
     * @override
 | 
					     * @override
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    populate() {
 | 
					    populate() {
 | 
				
			||||||
        const found = this.editor.description.value.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
 | 
					        const found = this.editor.description.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
 | 
				
			||||||
        if (found === null) {
 | 
					        if (found === null) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -917,14 +1101,6 @@ class BusTripTab extends TagTabPlane {
 | 
				
			|||||||
        this.#route.value = found[2];
 | 
					        this.#route.value = found[2];
 | 
				
			||||||
        this.#from.value = found[3];
 | 
					        this.#from.value = found[3];
 | 
				
			||||||
        this.#to.value = found[4];
 | 
					        this.#to.value = found[4];
 | 
				
			||||||
        for (const tagButton of this.tagButtons) {
 | 
					 | 
				
			||||||
            if (tagButton.dataset.value === this.tag.value) {
 | 
					 | 
				
			||||||
                tagButton.classList.remove("btn-outline-primary");
 | 
					 | 
				
			||||||
                tagButton.classList.add("btn-primary");
 | 
					 | 
				
			||||||
                this.editor.filterSuggestedAccounts(tagButton);
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this.switchToMe();
 | 
					        this.switchToMe();
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -985,19 +1161,24 @@ class BusTripTab extends TagTabPlane {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The regular payment tab plane.
 | 
					 * The recurring transaction tab plane.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @private
 | 
					 * @private
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class RegularPaymentTab extends TabPlane {
 | 
					class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The payment buttons
 | 
					     * The month names
 | 
				
			||||||
 | 
					     * @type {string[]}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #monthNames;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The buttons of the recurring items
 | 
				
			||||||
     * @type {HTMLButtonElement[]}
 | 
					     * @type {HTMLButtonElement[]}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #payments;
 | 
					    #itemButtons;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // noinspection JSValidateTypes
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Constructs a tab plane.
 | 
					     * Constructs a tab plane.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
@@ -1006,8 +1187,44 @@ class RegularPaymentTab extends TabPlane {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(editor) {
 | 
					    constructor(editor) {
 | 
				
			||||||
        super(editor);
 | 
					        super(editor);
 | 
				
			||||||
 | 
					        this.#monthNames = [
 | 
				
			||||||
 | 
					            "",
 | 
				
			||||||
 | 
					            A_("January"), A_("February"), A_("March"), A_("April"),
 | 
				
			||||||
 | 
					            A_("May"), A_("June"), A_("July"), A_("August"),
 | 
				
			||||||
 | 
					            A_("September"), A_("October"), A_("November"), A_("December"),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
        // noinspection JSValidateTypes
 | 
					        // noinspection JSValidateTypes
 | 
				
			||||||
        this.#payments = Array.from(document.getElementsByClassName(this.prefix + "-payment"));
 | 
					        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 = this.#getDescription(itemButton);
 | 
				
			||||||
 | 
					                this.editor.updateCurrentSuggestedAccounts(itemButton);
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns the description for a recurring item.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param itemButton {HTMLButtonElement} the recurring item
 | 
				
			||||||
 | 
					     * @return {string} the description of the recurring item
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #getDescription(itemButton) {
 | 
				
			||||||
 | 
					        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);
 | 
				
			||||||
 | 
					        const lastBimonthlyTo = ((thisMonth + thisMonth % 2 + 9) % 12 + 1);
 | 
				
			||||||
 | 
					        return itemButton.dataset.template
 | 
				
			||||||
 | 
					            .replaceAll("{this_month_number}", String(thisMonth))
 | 
				
			||||||
 | 
					            .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]}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -1017,7 +1234,7 @@ class RegularPaymentTab extends TabPlane {
 | 
				
			|||||||
     * @abstract
 | 
					     * @abstract
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    tabId() {
 | 
					    tabId() {
 | 
				
			||||||
        return "regular";
 | 
					        return "recurring";
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -1026,9 +1243,9 @@ class RegularPaymentTab extends TabPlane {
 | 
				
			|||||||
     * @override
 | 
					     * @override
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    reset() {
 | 
					    reset() {
 | 
				
			||||||
        for (const payment of this.#payments) {
 | 
					        for (const itemButton of this.#itemButtons) {
 | 
				
			||||||
            payment.classList.remove("btn-primary");
 | 
					            itemButton.classList.remove("btn-primary");
 | 
				
			||||||
            payment.classList.add("btn-outline-primary");
 | 
					            itemButton.classList.add("btn-outline-primary");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1039,9 +1256,32 @@ class RegularPaymentTab extends TabPlane {
 | 
				
			|||||||
     * @override
 | 
					     * @override
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    populate() {
 | 
					    populate() {
 | 
				
			||||||
 | 
					        for (const itemButton of this.#itemButtons) {
 | 
				
			||||||
 | 
					            if (this.#getDescription(itemButton) === this.editor.description) {
 | 
				
			||||||
 | 
					                itemButton.classList.add("btn-primary");
 | 
				
			||||||
 | 
					                itemButton.classList.remove("btn-outline-primary");
 | 
				
			||||||
 | 
					                this.switchToMe();
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Switches to the tab plane.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    switchToMe() {
 | 
				
			||||||
 | 
					        super.switchToMe();
 | 
				
			||||||
 | 
					        for (const itemButton of this.#itemButtons) {
 | 
				
			||||||
 | 
					            if (itemButton.classList.contains("btn-primary")) {
 | 
				
			||||||
 | 
					                this.editor.updateCurrentSuggestedAccounts(itemButton);
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.editor.clearSuggestedAccounts();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Validates the input in the tab plane.
 | 
					     * Validates the input in the tab plane.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
@@ -1058,7 +1298,7 @@ class RegularPaymentTab extends TabPlane {
 | 
				
			|||||||
 *
 | 
					 *
 | 
				
			||||||
 * @private
 | 
					 * @private
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class AnnotationTab extends TabPlane {
 | 
					class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Constructs a tab plane.
 | 
					     * Constructs a tab plane.
 | 
				
			||||||
@@ -1091,15 +1331,15 @@ class AnnotationTab extends TabPlane {
 | 
				
			|||||||
     * @override
 | 
					     * @override
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    updateDescription() {
 | 
					    updateDescription() {
 | 
				
			||||||
        const found = this.editor.description.value.match(/^(.*?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
 | 
					        const found = this.editor.description.match(/^(.*?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
 | 
				
			||||||
        if (found !== null) {
 | 
					        if (found !== null) {
 | 
				
			||||||
            this.editor.description.value = found[1];
 | 
					            this.editor.description = found[1];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (parseInt(this.editor.number.value) > 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 !== "") {
 | 
					        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})`;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1120,19 +1360,19 @@ class AnnotationTab extends TabPlane {
 | 
				
			|||||||
     * @override
 | 
					     * @override
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    populate() {
 | 
					    populate() {
 | 
				
			||||||
        const found = this.editor.description.value.match(/^(.*?)(?:[*×](\d+))?(?:\(([^()]+)\))?$/);
 | 
					        const found = this.editor.description.match(/^(.*?)(?:[*×](\d+))?(?:\(([^()]+)\))?$/);
 | 
				
			||||||
        this.editor.description.value = found[1];
 | 
					        this.editor.description = found[1];
 | 
				
			||||||
        if (found[2] === undefined || parseInt(found[2]) === 1) {
 | 
					        if (found[2] === undefined || parseInt(found[2]) === 1) {
 | 
				
			||||||
            this.editor.number.value = "";
 | 
					            this.editor.number.value = "";
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            this.editor.number.value = found[2];
 | 
					            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) {
 | 
					        if (found[3] === undefined) {
 | 
				
			||||||
            this.editor.note.value = "";
 | 
					            this.editor.note.value = "";
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            this.editor.note.value = found[3];
 | 
					            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;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
					 * drag-and-drop-reorder.js: The JavaScript for the reorder a list with drag-and-drop
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										312
									
								
								src/accounting/static/js/journal-entry-account-selector.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								src/accounting/static/js/journal-entry-account-selector.js
									
									
									
									
									
										Normal 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");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
/* The Mia! Accounting Flask Project
 | 
					/* The Mia! Accounting Project
 | 
				
			||||||
 * journal-entry-form.js: The JavaScript for the journal entry form
 | 
					 * journal-entry-form.js: The JavaScript for the journal entry form
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -111,6 +111,7 @@ class JournalEntryForm {
 | 
				
			|||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
        this.#element = document.getElementById("accounting-form");
 | 
					        this.#element = document.getElementById("accounting-form");
 | 
				
			||||||
        this.lineItemTemplate = this.#element.dataset.lineItemTemplate;
 | 
					        this.lineItemTemplate = this.#element.dataset.lineItemTemplate;
 | 
				
			||||||
 | 
					        this.lineItemEditor = new JournalEntryLineItemEditor(this);
 | 
				
			||||||
        this.#date = document.getElementById("accounting-date");
 | 
					        this.#date = document.getElementById("accounting-date");
 | 
				
			||||||
        this.#dateError = document.getElementById("accounting-date-error");
 | 
					        this.#dateError = document.getElementById("accounting-date-error");
 | 
				
			||||||
        this.#currencyControl = document.getElementById("accounting-currencies");
 | 
					        this.#currencyControl = document.getElementById("accounting-currencies");
 | 
				
			||||||
@@ -121,14 +122,13 @@ class JournalEntryForm {
 | 
				
			|||||||
        this.#addCurrencyButton = document.getElementById("accounting-add-currency");
 | 
					        this.#addCurrencyButton = document.getElementById("accounting-add-currency");
 | 
				
			||||||
        this.#note = document.getElementById("accounting-note");
 | 
					        this.#note = document.getElementById("accounting-note");
 | 
				
			||||||
        this.#noteError = document.getElementById("accounting-note-error");
 | 
					        this.#noteError = document.getElementById("accounting-note-error");
 | 
				
			||||||
        this.lineItemEditor = new JournalEntryLineItemEditor(this);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.#addCurrencyButton.onclick = () => {
 | 
					        this.#addCurrencyButton.onclick = () => {
 | 
				
			||||||
            const newIndex = 1 + (this.#currencies.length === 0? 0: Math.max(...this.#currencies.map((currency) => currency.index)));
 | 
					            const newIndex = 1 + (this.#currencies.length === 0? 0: Math.max(...this.#currencies.map((currency) => currency.index)));
 | 
				
			||||||
            const html = this.#element.dataset.currencyTemplate
 | 
					            const html = this.#element.dataset.currencyTemplate
 | 
				
			||||||
                .replaceAll("CURRENCY_INDEX", escapeHtml(String(newIndex)));
 | 
					                .replaceAll("CURRENCY_INDEX", escapeHtml(String(newIndex)));
 | 
				
			||||||
            this.#currencyList.insertAdjacentHTML("beforeend", html);
 | 
					            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.#currencies.push(new CurrencySubForm(this, element));
 | 
				
			||||||
            this.#resetDeleteCurrencyButtons();
 | 
					            this.#resetDeleteCurrencyButtons();
 | 
				
			||||||
            this.#initializeDragAndDropReordering();
 | 
					            this.#initializeDragAndDropReordering();
 | 
				
			||||||
@@ -159,7 +159,7 @@ class JournalEntryForm {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    #resetDeleteCurrencyButtons() {
 | 
					    #resetDeleteCurrencyButtons() {
 | 
				
			||||||
        if (this.#currencies.length === 1) {
 | 
					        if (this.#currencies.length === 1) {
 | 
				
			||||||
            this.#currencies[0].deleteButton.classList.add("d-none");
 | 
					            this.#currencies[0].setDeleteButtonShown(false);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            for (const currency of this.#currencies) {
 | 
					            for (const currency of this.#currencies) {
 | 
				
			||||||
                let isAnyLineItemMatched = false;
 | 
					                let isAnyLineItemMatched = false;
 | 
				
			||||||
@@ -169,11 +169,7 @@ class JournalEntryForm {
 | 
				
			|||||||
                        break;
 | 
					                        break;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                if (isAnyLineItemMatched) {
 | 
					                currency.setDeleteButtonShown(!isAnyLineItemMatched);
 | 
				
			||||||
                    currency.deleteButton.classList.add("d-none");
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    currency.deleteButton.classList.remove("d-none");
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -184,10 +180,8 @@ class JournalEntryForm {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    #initializeDragAndDropReordering() {
 | 
					    #initializeDragAndDropReordering() {
 | 
				
			||||||
        initializeDragAndDropReordering(this.#currencyList, () => {
 | 
					        initializeDragAndDropReordering(this.#currencyList, () => {
 | 
				
			||||||
            const currencyId = Array.from(this.#currencyList.children).map((currency) => currency.id);
 | 
					            for (const currency of this.#currencies) {
 | 
				
			||||||
            this.#currencies.sort((a, b) => currencyId.indexOf(a.element.id) - currencyId.indexOf(b.element.id));
 | 
					                currency.resetNo();
 | 
				
			||||||
            for (let i = 0; i < this.#currencies.length; i++) {
 | 
					 | 
				
			||||||
                this.#currencies[i].no.value = String(i + 1);
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -213,8 +207,8 @@ class JournalEntryForm {
 | 
				
			|||||||
     * @return {string[]} the account codes used in the form
 | 
					     * @return {string[]} the account codes used in the form
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getAccountCodesUsed(debitCredit) {
 | 
					    getAccountCodesUsed(debitCredit) {
 | 
				
			||||||
        return this.getLineItems(debitCredit).map((lineItem) => lineItem.getAccountCode())
 | 
					        return this.getLineItems(debitCredit).filter((lineItem) => lineItem.account !== null)
 | 
				
			||||||
            .filter((code) => code !== null);
 | 
					            .map((lineItem) => lineItem.account.code);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -222,7 +216,7 @@ class JournalEntryForm {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     * @return {string} the date
 | 
					     * @return {string} the date
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getDate() {
 | 
					    get date() {
 | 
				
			||||||
        return this.#date.value;
 | 
					        return this.#date.value;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -233,7 +227,7 @@ class JournalEntryForm {
 | 
				
			|||||||
    updateMinDate() {
 | 
					    updateMinDate() {
 | 
				
			||||||
        let lastOriginalLineItemDate = null;
 | 
					        let lastOriginalLineItemDate = null;
 | 
				
			||||||
        for (const lineItem of this.getLineItems()) {
 | 
					        for (const lineItem of this.getLineItems()) {
 | 
				
			||||||
            const date = lineItem.getOriginalLineItemDate();
 | 
					            const date = lineItem.originalLineItemDate;
 | 
				
			||||||
            if (date !== null) {
 | 
					            if (date !== null) {
 | 
				
			||||||
                if (lastOriginalLineItemDate === null || lastOriginalLineItemDate < date) {
 | 
					                if (lastOriginalLineItemDate === null || lastOriginalLineItemDate < date) {
 | 
				
			||||||
                    lastOriginalLineItemDate = date;
 | 
					                    lastOriginalLineItemDate = date;
 | 
				
			||||||
@@ -349,7 +343,7 @@ class CurrencySubForm {
 | 
				
			|||||||
     * The element
 | 
					     * The element
 | 
				
			||||||
     * @type {HTMLDivElement}
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    element;
 | 
					    #element;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The journal entry form
 | 
					     * The journal entry form
 | 
				
			||||||
@@ -363,12 +357,6 @@ class CurrencySubForm {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    index;
 | 
					    index;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * The prefix of the HTML ID and class
 | 
					 | 
				
			||||||
     * @type {string}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    #prefix;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The control
 | 
					     * The control
 | 
				
			||||||
     * @type {HTMLDivElement}
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
@@ -385,7 +373,7 @@ class CurrencySubForm {
 | 
				
			|||||||
     * The number
 | 
					     * The number
 | 
				
			||||||
     * @type {HTMLInputElement}
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    no;
 | 
					    #no;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The currency code
 | 
					     * The currency code
 | 
				
			||||||
@@ -403,7 +391,7 @@ class CurrencySubForm {
 | 
				
			|||||||
     * The button to delete the currency
 | 
					     * The button to delete the currency
 | 
				
			||||||
     * @type {HTMLButtonElement}
 | 
					     * @type {HTMLButtonElement}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    deleteButton;
 | 
					    #deleteButton;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The debit sub-form
 | 
					     * The debit sub-form
 | 
				
			||||||
@@ -424,36 +412,54 @@ class CurrencySubForm {
 | 
				
			|||||||
     * @param element {HTMLDivElement} the currency sub-form element
 | 
					     * @param element {HTMLDivElement} the currency sub-form element
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(form, element) {
 | 
					    constructor(form, element) {
 | 
				
			||||||
        this.element = element;
 | 
					        this.#element = element;
 | 
				
			||||||
        this.form = form;
 | 
					        this.form = form;
 | 
				
			||||||
        this.index = parseInt(this.element.dataset.index);
 | 
					        this.index = parseInt(this.#element.dataset.index);
 | 
				
			||||||
        this.#prefix = "accounting-currency-" + String(this.index);
 | 
					        const prefix = `accounting-currency-${String(this.index)}`;
 | 
				
			||||||
        this.#control = document.getElementById(this.#prefix + "-control");
 | 
					        this.#control = document.getElementById(`${prefix}-control`);
 | 
				
			||||||
        this.#error = document.getElementById(this.#prefix + "-error");
 | 
					        this.#error = document.getElementById(`${prefix}-error`);
 | 
				
			||||||
        this.no = document.getElementById(this.#prefix + "-no");
 | 
					        this.#no = document.getElementById(`${prefix}-no`);
 | 
				
			||||||
        this.#code = document.getElementById(this.#prefix + "-code");
 | 
					        this.#code = document.getElementById(`${prefix}-code`);
 | 
				
			||||||
        this.#codeSelect = document.getElementById(this.#prefix + "-code-select");
 | 
					        this.#codeSelect = document.getElementById(`${prefix}-code-select`);
 | 
				
			||||||
        this.deleteButton = document.getElementById(this.#prefix + "-delete");
 | 
					        this.#deleteButton = document.getElementById(`${prefix}-delete`);
 | 
				
			||||||
        const debitElement = document.getElementById(this.#prefix + "-debit");
 | 
					        const debitElement = document.getElementById(`${prefix}-debit`);
 | 
				
			||||||
        this.#debit = debitElement === null? null: new DebitCreditSubForm(this, debitElement, "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.#credit = creditElement == null? null: new DebitCreditSubForm(this, creditElement, "credit");
 | 
				
			||||||
        this.#codeSelect.onchange = () => this.#code.value = this.#codeSelect.value;
 | 
					        this.#codeSelect.onchange = () => this.#code.value = this.#codeSelect.value;
 | 
				
			||||||
        this.deleteButton.onclick = () => {
 | 
					        this.#deleteButton.onclick = () => {
 | 
				
			||||||
            this.element.parentElement.removeChild(this.element);
 | 
					            this.#element.parentElement.removeChild(this.#element);
 | 
				
			||||||
            this.form.deleteCurrency(this);
 | 
					            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.
 | 
					     * Returns the currency code.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @return {string} the currency code
 | 
					     * @return {string} the currency code
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getCurrencyCode() {
 | 
					    get currencyCode() {
 | 
				
			||||||
        return this.#code.value;
 | 
					        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.
 | 
					     * Returns all the line items in the form.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
@@ -479,7 +485,7 @@ class CurrencySubForm {
 | 
				
			|||||||
    updateCodeSelectorStatus() {
 | 
					    updateCodeSelectorStatus() {
 | 
				
			||||||
        let isEnabled = true;
 | 
					        let isEnabled = true;
 | 
				
			||||||
        for (const lineItem of this.getLineItems()) {
 | 
					        for (const lineItem of this.getLineItems()) {
 | 
				
			||||||
            if (lineItem.getOriginalLineItemId() !== null) {
 | 
					            if (lineItem.originalLineItemId !== null) {
 | 
				
			||||||
                isEnabled = false;
 | 
					                isEnabled = false;
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -511,7 +517,7 @@ class CurrencySubForm {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    validateBalance() {
 | 
					    validateBalance() {
 | 
				
			||||||
        if (this.#debit !== null && this.#credit !== null) {
 | 
					        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.#control.classList.add("is-invalid");
 | 
				
			||||||
                this.#error.innerText = A_("The totals of the debit and credit amounts do not match.");
 | 
					                this.#error.innerText = A_("The totals of the debit and credit amounts do not match.");
 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
@@ -541,6 +547,12 @@ class DebitCreditSubForm {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    #element;
 | 
					    #element;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The content
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The currencyIndex
 | 
					     * The currencyIndex
 | 
				
			||||||
     * @type {number}
 | 
					     * @type {number}
 | 
				
			||||||
@@ -554,7 +566,7 @@ class DebitCreditSubForm {
 | 
				
			|||||||
    debitCredit;
 | 
					    debitCredit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The prefix of the HTML ID and class
 | 
					     * The prefix of the HTML ID and class names
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #prefix;
 | 
					    #prefix;
 | 
				
			||||||
@@ -601,32 +613,45 @@ class DebitCreditSubForm {
 | 
				
			|||||||
        this.#element = element;
 | 
					        this.#element = element;
 | 
				
			||||||
        this.#currencyIndex = currency.index;
 | 
					        this.#currencyIndex = currency.index;
 | 
				
			||||||
        this.debitCredit = debitCredit;
 | 
					        this.debitCredit = debitCredit;
 | 
				
			||||||
        this.#prefix = "accounting-currency-" + String(this.#currencyIndex) + "-" + debitCredit;
 | 
					        this.#prefix = `accounting-currency-${String(this.#currencyIndex)}-${debitCredit}`;
 | 
				
			||||||
        this.#error = document.getElementById(this.#prefix + "-error");
 | 
					        this.#content = document.getElementById(`${this.#prefix}-content`);
 | 
				
			||||||
        this.#lineItemList = document.getElementById(this.#prefix + "-list");
 | 
					        this.#error = document.getElementById(`${this.#prefix}-error`);
 | 
				
			||||||
        // noinspection JSValidateTypes
 | 
					        this.#lineItemList = document.getElementById(`${this.#prefix}-list`);
 | 
				
			||||||
        this.lineItems = Array.from(document.getElementsByClassName(this.#prefix)).map((element) => new LineItemSubForm(this, element));
 | 
					        this.lineItems = Array.from(document.getElementsByClassName(this.#prefix)).map((element) => new LineItemSubForm(this, element));
 | 
				
			||||||
        this.#total = document.getElementById(this.#prefix + "-total");
 | 
					        this.#total = document.getElementById(`${this.#prefix}-total`);
 | 
				
			||||||
        this.#addLineItemButton = document.getElementById(this.#prefix + "-add-line-item");
 | 
					        this.#addLineItemButton = document.getElementById(`${this.#prefix}-add-line-item`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.#resetContent();
 | 
				
			||||||
        this.#addLineItemButton.onclick = () => this.currency.form.lineItemEditor.onAddNew(this);
 | 
					        this.#addLineItemButton.onclick = () => this.currency.form.lineItemEditor.onAddNew(this);
 | 
				
			||||||
        this.#resetDeleteLineItemButtons();
 | 
					        this.#resetDeleteLineItemButtons();
 | 
				
			||||||
        this.#initializeDragAndDropReordering();
 | 
					        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
 | 
					     * Adds a new line item sub-form
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @returns {LineItemSubForm} the newly-added line item sub-form
 | 
					     * @returns {LineItemSubForm} the newly-added line item sub-form
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    addLineItem() {
 | 
					    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
 | 
					        const html = this.currency.form.lineItemTemplate
 | 
				
			||||||
            .replaceAll("CURRENCY_INDEX", escapeHtml(String(this.#currencyIndex)))
 | 
					            .replaceAll("CURRENCY_INDEX", escapeHtml(String(this.#currencyIndex)))
 | 
				
			||||||
            .replaceAll("DEBIT_CREDIT", escapeHtml(this.debitCredit))
 | 
					            .replaceAll("DEBIT_CREDIT", escapeHtml(this.debitCredit))
 | 
				
			||||||
            .replaceAll("LINE_ITEM_INDEX", escapeHtml(String(newIndex)));
 | 
					            .replaceAll("LINE_ITEM_INDEX", escapeHtml(String(newIndex)));
 | 
				
			||||||
        this.#lineItemList.insertAdjacentHTML("beforeend", html);
 | 
					        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.lineItems.push(lineItem);
 | 
				
			||||||
 | 
					        this.#resetContent();
 | 
				
			||||||
        this.#resetDeleteLineItemButtons();
 | 
					        this.#resetDeleteLineItemButtons();
 | 
				
			||||||
        this.#initializeDragAndDropReordering();
 | 
					        this.#initializeDragAndDropReordering();
 | 
				
			||||||
        this.validate();
 | 
					        this.validate();
 | 
				
			||||||
@@ -644,6 +669,7 @@ class DebitCreditSubForm {
 | 
				
			|||||||
        this.updateTotal();
 | 
					        this.updateTotal();
 | 
				
			||||||
        this.currency.updateCodeSelectorStatus();
 | 
					        this.currency.updateCodeSelectorStatus();
 | 
				
			||||||
        this.currency.form.updateMinDate();
 | 
					        this.currency.form.updateMinDate();
 | 
				
			||||||
 | 
					        this.#resetContent();
 | 
				
			||||||
        this.#resetDeleteLineItemButtons();
 | 
					        this.#resetDeleteLineItemButtons();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -653,27 +679,47 @@ class DebitCreditSubForm {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    #resetDeleteLineItemButtons() {
 | 
					    #resetDeleteLineItemButtons() {
 | 
				
			||||||
        if (this.lineItems.length === 1) {
 | 
					        if (this.lineItems.length === 1) {
 | 
				
			||||||
            this.lineItems[0].deleteButton.classList.add("d-none");
 | 
					            this.lineItems[0].setDeleteButtonShown(false);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            for (const lineItem of this.lineItems) {
 | 
					            for (const lineItem of this.lineItems) {
 | 
				
			||||||
                if (lineItem.isMatched) {
 | 
					                lineItem.setDeleteButtonShown(!lineItem.isMatched);
 | 
				
			||||||
                    lineItem.deleteButton.classList.add("d-none");
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    lineItem.deleteButton.classList.remove("d-none");
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 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.
 | 
					     * Returns the total amount.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @return {Decimal} the total amount
 | 
					     * @return {Decimal} the total amount
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getTotal() {
 | 
					    get total() {
 | 
				
			||||||
        let total = new Decimal("0");
 | 
					        let total = new Decimal("0");
 | 
				
			||||||
        for (const lineItem of this.lineItems) {
 | 
					        for (const lineItem of this.lineItems) {
 | 
				
			||||||
            const amount = lineItem.getAmount();
 | 
					            const amount = lineItem.amount;
 | 
				
			||||||
            if (amount !== null) {
 | 
					            if (amount !== null) {
 | 
				
			||||||
                total = total.plus(amount);
 | 
					                total = total.plus(amount);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -686,7 +732,7 @@ class DebitCreditSubForm {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    updateTotal() {
 | 
					    updateTotal() {
 | 
				
			||||||
        this.#total.innerText = formatDecimal(this.getTotal());
 | 
					        this.#total.innerText = formatDecimal(this.total);
 | 
				
			||||||
        this.currency.validateBalance();
 | 
					        this.currency.validateBalance();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -696,10 +742,8 @@ class DebitCreditSubForm {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    #initializeDragAndDropReordering() {
 | 
					    #initializeDragAndDropReordering() {
 | 
				
			||||||
        initializeDragAndDropReordering(this.#lineItemList, () => {
 | 
					        initializeDragAndDropReordering(this.#lineItemList, () => {
 | 
				
			||||||
            const lineItemId = Array.from(this.#lineItemList.children).map((lineItem) => lineItem.id);
 | 
					            for (const lineItem of this.lineItems) {
 | 
				
			||||||
            this.lineItems.sort((a, b) => lineItemId.indexOf(a.element.id) - lineItemId.indexOf(b.element.id));
 | 
					                lineItem.resetNo();
 | 
				
			||||||
            for (let i = 0; i < this.lineItems.length; i++) {
 | 
					 | 
				
			||||||
                this.lineItems[i].no.value = String(i + 1);
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -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.
 | 
					 * The line item sub-form.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
@@ -751,7 +842,7 @@ class LineItemSubForm {
 | 
				
			|||||||
     * The element
 | 
					     * The element
 | 
				
			||||||
     * @type {HTMLLIElement}
 | 
					     * @type {HTMLLIElement}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    element;
 | 
					    #element;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Either "debit" or "credit"
 | 
					     * Either "debit" or "credit"
 | 
				
			||||||
@@ -763,7 +854,7 @@ class LineItemSubForm {
 | 
				
			|||||||
     * The line item index
 | 
					     * The line item index
 | 
				
			||||||
     * @type {number}
 | 
					     * @type {number}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    lineItemIndex;
 | 
					    index;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Whether this is an original line item with offsets
 | 
					     * Whether this is an original line item with offsets
 | 
				
			||||||
@@ -771,12 +862,6 @@ class LineItemSubForm {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    isMatched;
 | 
					    isMatched;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * The prefix of the HTML ID and class
 | 
					 | 
				
			||||||
     * @type {string}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    #prefix;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The control
 | 
					     * The control
 | 
				
			||||||
     * @type {HTMLDivElement}
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
@@ -793,7 +878,7 @@ class LineItemSubForm {
 | 
				
			|||||||
     * The number
 | 
					     * The number
 | 
				
			||||||
     * @type {HTMLInputElement}
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    no;
 | 
					    #no;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The account code
 | 
					     * The account code
 | 
				
			||||||
@@ -853,7 +938,7 @@ class LineItemSubForm {
 | 
				
			|||||||
     * The button to delete line item
 | 
					     * The button to delete line item
 | 
				
			||||||
     * @type {HTMLButtonElement}
 | 
					     * @type {HTMLButtonElement}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    deleteButton;
 | 
					    #deleteButton;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Constructs the line item sub-form.
 | 
					     * Constructs the line item sub-form.
 | 
				
			||||||
@@ -863,38 +948,38 @@ class LineItemSubForm {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(debitCredit, element) {
 | 
					    constructor(debitCredit, element) {
 | 
				
			||||||
        this.debitCreditSubForm = debitCredit;
 | 
					        this.debitCreditSubForm = debitCredit;
 | 
				
			||||||
        this.element = element;
 | 
					        this.#element = element;
 | 
				
			||||||
        this.debitCredit = element.dataset.debitCredit;
 | 
					        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.isMatched = element.classList.contains("accounting-matched-line-item");
 | 
				
			||||||
        this.#prefix = "accounting-currency-" + element.dataset.currencyIndex + "-" + this.debitCredit + "-" + this.lineItemIndex;
 | 
					        const prefix = `accounting-currency-${element.dataset.currencyIndex}-${this.debitCredit}-${String(this.index)}`;
 | 
				
			||||||
        this.#control = document.getElementById(this.#prefix + "-control");
 | 
					        this.#control = document.getElementById(`${prefix}-control`);
 | 
				
			||||||
        this.#error = document.getElementById(this.#prefix + "-error");
 | 
					        this.#error = document.getElementById(`${prefix}-error`);
 | 
				
			||||||
        this.no = document.getElementById(this.#prefix + "-no");
 | 
					        this.#no = document.getElementById(`${prefix}-no`);
 | 
				
			||||||
        this.#accountCode = document.getElementById(this.#prefix + "-account-code");
 | 
					        this.#accountCode = document.getElementById(`${prefix}-account-code`);
 | 
				
			||||||
        this.#accountText = document.getElementById(this.#prefix + "-account-text");
 | 
					        this.#accountText = document.getElementById(`${prefix}-account-text`);
 | 
				
			||||||
        this.#description = document.getElementById(this.#prefix + "-description");
 | 
					        this.#description = document.getElementById(`${prefix}-description`);
 | 
				
			||||||
        this.#descriptionText = document.getElementById(this.#prefix + "-description-text");
 | 
					        this.#descriptionText = document.getElementById(`${prefix}-description-text`);
 | 
				
			||||||
        this.#originalLineItemId = document.getElementById(this.#prefix + "-original-line-item-id");
 | 
					        this.#originalLineItemId = document.getElementById(`${prefix}-original-line-item-id`);
 | 
				
			||||||
        this.#originalLineItemText = document.getElementById(this.#prefix + "-original-line-item-text");
 | 
					        this.#originalLineItemText = document.getElementById(`${prefix}-original-line-item-text`);
 | 
				
			||||||
        this.#offsets = document.getElementById(this.#prefix + "-offsets");
 | 
					        this.#offsets = document.getElementById(`${prefix}-offsets`);
 | 
				
			||||||
        this.#amount = document.getElementById(this.#prefix + "-amount");
 | 
					        this.#amount = document.getElementById(`${prefix}-amount`);
 | 
				
			||||||
        this.#amountText = document.getElementById(this.#prefix + "-amount-text");
 | 
					        this.#amountText = document.getElementById(`${prefix}-amount-text`);
 | 
				
			||||||
        this.deleteButton = document.getElementById(this.#prefix + "-delete");
 | 
					        this.#deleteButton = document.getElementById(`${prefix}-delete`);
 | 
				
			||||||
        this.#control.onclick = () => this.debitCreditSubForm.currency.form.lineItemEditor.onEdit(this);
 | 
					        this.#control.onclick = () => this.debitCreditSubForm.currency.form.lineItemEditor.onEdit(this);
 | 
				
			||||||
        this.deleteButton.onclick = () => {
 | 
					        this.#deleteButton.onclick = () => {
 | 
				
			||||||
            this.element.parentElement.removeChild(this.element);
 | 
					            this.#element.parentElement.removeChild(this.#element);
 | 
				
			||||||
            this.debitCreditSubForm.deleteLineItem(this);
 | 
					            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() {
 | 
					    resetNo() {
 | 
				
			||||||
        return "isNeedOffset" in this.element.dataset;
 | 
					        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
 | 
					     * @return {string|null} the ID of the original line item
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getOriginalLineItemId() {
 | 
					    get originalLineItemId() {
 | 
				
			||||||
        return this.#originalLineItemId.value === ""? null: this.#originalLineItemId.value;
 | 
					        return this.#originalLineItemId.value === ""? null: this.#originalLineItemId.value;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -911,7 +996,7 @@ class LineItemSubForm {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     * @return {string|null} the date of the original line item
 | 
					     * @return {string|null} the date of the original line item
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getOriginalLineItemDate() {
 | 
					    get originalLineItemDate() {
 | 
				
			||||||
        return this.#originalLineItemId.dataset.date === ""? null: this.#originalLineItemId.dataset.date;
 | 
					        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
 | 
					     * @return {string|null} the text of the original line item
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getOriginalLineItemText() {
 | 
					    get originalLineItemText() {
 | 
				
			||||||
        return this.#originalLineItemId.dataset.text === ""? null: this.#originalLineItemId.dataset.text;
 | 
					        return this.#originalLineItemId.dataset.text === ""? null: this.#originalLineItemId.dataset.text;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -929,26 +1014,17 @@ class LineItemSubForm {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     * @return {string|null} the description
 | 
					     * @return {string|null} the description
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getDescription() {
 | 
					    get description() {
 | 
				
			||||||
        return this.#description.value === ""? null: this.#description.value;
 | 
					        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() {
 | 
					    get account() {
 | 
				
			||||||
        return this.#accountCode.value === ""? null: this.#accountCode.value;
 | 
					        return this.#accountCode.value === null? null: new JournalEntryAccount(this.#accountCode.value, this.#accountCode.dataset.text, this.#accountCode.classList.contains("accounting-account-is-need-offset"));
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Returns the account text.
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return {string|null} the account text
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    getAccountText() {
 | 
					 | 
				
			||||||
        return this.#accountCode.dataset.text === ""? null: this.#accountCode.dataset.text;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -956,7 +1032,7 @@ class LineItemSubForm {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     * @return {Decimal|null} the amount
 | 
					     * @return {Decimal|null} the amount
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getAmount() {
 | 
					    get amount() {
 | 
				
			||||||
        return this.#amount.value === ""? null: new Decimal(this.#amount.value);
 | 
					        return this.#amount.value === ""? null: new Decimal(this.#amount.value);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -965,10 +1041,19 @@ class LineItemSubForm {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     * @return {Decimal|null} the minimal amount
 | 
					     * @return {Decimal|null} the minimal amount
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getAmountMin() {
 | 
					    get amountMin() {
 | 
				
			||||||
        return this.#amount.dataset.min === ""? null: new Decimal(this.#amount.dataset.min);
 | 
					        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.
 | 
					     * Validates the form.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
@@ -996,24 +1081,24 @@ class LineItemSubForm {
 | 
				
			|||||||
     * @param editor {JournalEntryLineItemEditor} the line item editor
 | 
					     * @param editor {JournalEntryLineItemEditor} the line item editor
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    save(editor) {
 | 
					    save(editor) {
 | 
				
			||||||
        if (editor.isNeedOffset) {
 | 
					        setElementShown(this.#offsets, editor.account.isNeedOffset);
 | 
				
			||||||
            this.#offsets.classList.remove("d-none");
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            this.#offsets.classList.add("d-none");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this.#originalLineItemId.value = editor.originalLineItemId === null? "": editor.originalLineItemId;
 | 
					        this.#originalLineItemId.value = editor.originalLineItemId === null? "": editor.originalLineItemId;
 | 
				
			||||||
        this.#originalLineItemId.dataset.date = editor.originalLineItemDate === null? "": editor.originalLineItemDate;
 | 
					        this.#originalLineItemId.dataset.date = editor.originalLineItemDate === null? "": editor.originalLineItemDate;
 | 
				
			||||||
        this.#originalLineItemId.dataset.text = editor.originalLineItemText === null? "": editor.originalLineItemText;
 | 
					        this.#originalLineItemId.dataset.text = editor.originalLineItemText === null? "": editor.originalLineItemText;
 | 
				
			||||||
 | 
					        setElementShown(this.#originalLineItemText, editor.originalLineItemText !== null);
 | 
				
			||||||
        if (editor.originalLineItemText === null) {
 | 
					        if (editor.originalLineItemText === null) {
 | 
				
			||||||
            this.#originalLineItemText.classList.add("d-none");
 | 
					 | 
				
			||||||
            this.#originalLineItemText.innerText = "";
 | 
					            this.#originalLineItemText.innerText = "";
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            this.#originalLineItemText.classList.remove("d-none");
 | 
					 | 
				
			||||||
            this.#originalLineItemText.innerText = A_("Offset %(item)s", {item: editor.originalLineItemText});
 | 
					            this.#originalLineItemText.innerText = A_("Offset %(item)s", {item: editor.originalLineItemText});
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.#accountCode.value = editor.accountCode === null? "": editor.accountCode;
 | 
					        this.#accountCode.value = editor.account.code;
 | 
				
			||||||
        this.#accountCode.dataset.text = editor.accountText === null? "": editor.accountText;
 | 
					        this.#accountCode.dataset.text = editor.account.text;
 | 
				
			||||||
        this.#accountText.innerText = editor.accountText === null? "": editor.accountText;
 | 
					        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.#description.value = editor.description === null? "": editor.description;
 | 
				
			||||||
        this.#descriptionText.innerText = editor.description === null? "": editor.description;
 | 
					        this.#descriptionText.innerText = editor.description === null? "": editor.description;
 | 
				
			||||||
        this.#amount.value = editor.amount;
 | 
					        this.#amount.value = editor.amount;
 | 
				
			||||||
@@ -1054,3 +1139,18 @@ function formatDecimal(number) {
 | 
				
			|||||||
    const whole = Number(number.minus(frac)).toLocaleString();
 | 
					    const whole = Number(number.minus(frac)).toLocaleString();
 | 
				
			||||||
    return whole + String(frac).substring(1);
 | 
					    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");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
					 * journal-entry-line-item-editor.js: The JavaScript for the journal entry line item editor
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -44,7 +44,7 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
     * The bootstrap modal
 | 
					     * The bootstrap modal
 | 
				
			||||||
     * @type {HTMLDivElement}
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #modal;
 | 
					    modal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Either "debit" or "credit"
 | 
					     * Either "debit" or "credit"
 | 
				
			||||||
@@ -53,7 +53,7 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
    debitCredit;
 | 
					    debitCredit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The prefix of the HTML ID and class
 | 
					     * The prefix of the HTML ID and class names
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #prefix = "accounting-line-item-editor"
 | 
					    #prefix = "accounting-line-item-editor"
 | 
				
			||||||
@@ -148,12 +148,6 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    #debitCreditSubForm;
 | 
					    #debitCreditSubForm;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Whether the journal entry line item needs offset
 | 
					 | 
				
			||||||
     * @type {boolean}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    isNeedOffset = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The ID of the original line item
 | 
					     * The ID of the original line item
 | 
				
			||||||
     * @type {string|null}
 | 
					     * @type {string|null}
 | 
				
			||||||
@@ -173,16 +167,16 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
    originalLineItemText = null;
 | 
					    originalLineItemText = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The account code
 | 
					     * The account
 | 
				
			||||||
     * @type {string|null}
 | 
					     * @type {JournalEntryAccount|null}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    accountCode = null;
 | 
					    account = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The account text
 | 
					     * Whether the user has confirmed the account
 | 
				
			||||||
     * @type {string|null}
 | 
					     * @type {boolean}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    accountText = null;
 | 
					    isAccountConfirmed = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The description
 | 
					     * The description
 | 
				
			||||||
@@ -190,12 +184,6 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    description = null;
 | 
					    description = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * The amount
 | 
					 | 
				
			||||||
     * @type {string}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    amount = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The description editors
 | 
					     * The description editors
 | 
				
			||||||
     * @type {{debit: DescriptionEditor, credit: DescriptionEditor}}
 | 
					     * @type {{debit: DescriptionEditor, credit: DescriptionEditor}}
 | 
				
			||||||
@@ -204,7 +192,7 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The account selectors
 | 
					     * The account selectors
 | 
				
			||||||
     * @type {{debit: AccountSelector, credit: AccountSelector}}
 | 
					     * @type {{debit: JournalEntryAccountSelector, credit: JournalEntryAccountSelector}}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #accountSelectors;
 | 
					    #accountSelectors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -222,23 +210,24 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
    constructor(form) {
 | 
					    constructor(form) {
 | 
				
			||||||
        this.form = form;
 | 
					        this.form = form;
 | 
				
			||||||
        this.#element = document.getElementById(this.#prefix);
 | 
					        this.#element = document.getElementById(this.#prefix);
 | 
				
			||||||
        this.#modal = document.getElementById(this.#prefix + "-modal");
 | 
					        this.modal = document.getElementById(`${this.#prefix}-modal`);
 | 
				
			||||||
        this.#originalLineItemContainer = document.getElementById(this.#prefix + "-original-line-item-container");
 | 
					        this.#originalLineItemContainer = document.getElementById(`${this.#prefix}-original-line-item-container`);
 | 
				
			||||||
        this.#originalLineItemControl = document.getElementById(this.#prefix + "-original-line-item-control");
 | 
					        this.#originalLineItemControl = document.getElementById(`${this.#prefix}-original-line-item-control`);
 | 
				
			||||||
        this.#originalLineItemText = document.getElementById(this.#prefix + "-original-line-item");
 | 
					        this.#originalLineItemText = document.getElementById(`${this.#prefix}-original-line-item`);
 | 
				
			||||||
        this.#originalLineItemError = document.getElementById(this.#prefix + "-original-line-item-error");
 | 
					        this.#originalLineItemError = document.getElementById(`${this.#prefix}-original-line-item-error`);
 | 
				
			||||||
        this.#originalLineItemDelete = document.getElementById(this.#prefix + "-original-line-item-delete");
 | 
					        this.#originalLineItemDelete = document.getElementById(`${this.#prefix}-original-line-item-delete`);
 | 
				
			||||||
        this.#descriptionControl = document.getElementById(this.#prefix + "-description-control");
 | 
					        this.#descriptionControl = document.getElementById(`${this.#prefix}-description-control`);
 | 
				
			||||||
        this.#descriptionText = document.getElementById(this.#prefix + "-description");
 | 
					        this.#descriptionText = document.getElementById(`${this.#prefix}-description`);
 | 
				
			||||||
        this.#descriptionError = document.getElementById(this.#prefix + "-description-error");
 | 
					        this.#descriptionError = document.getElementById(`${this.#prefix}-description-error`);
 | 
				
			||||||
        this.#accountControl = document.getElementById(this.#prefix + "-account-control");
 | 
					        this.#accountControl = document.getElementById(`${this.#prefix}-account-control`);
 | 
				
			||||||
        this.#accountText = document.getElementById(this.#prefix + "-account");
 | 
					        this.#accountText = document.getElementById(`${this.#prefix}-account`);
 | 
				
			||||||
        this.#accountError = document.getElementById(this.#prefix + "-account-error")
 | 
					        this.#accountError = document.getElementById(`${this.#prefix}-account-error`)
 | 
				
			||||||
        this.#amountInput = document.getElementById(this.#prefix + "-amount");
 | 
					        this.#amountInput = document.getElementById(`${this.#prefix}-amount`);
 | 
				
			||||||
        this.#amountError = document.getElementById(this.#prefix + "-amount-error");
 | 
					        this.#amountError = document.getElementById(`${this.#prefix}-amount-error`);
 | 
				
			||||||
        this.#descriptionEditors = DescriptionEditor.getInstances(this);
 | 
					        this.#descriptionEditors = DescriptionEditor.getInstances(this);
 | 
				
			||||||
        this.#accountSelectors = AccountSelector.getInstances(this);
 | 
					        this.#accountSelectors = JournalEntryAccountSelector.getInstances(this);
 | 
				
			||||||
        this.originalLineItemSelector = new OriginalLineItemSelector(this);
 | 
					        this.originalLineItemSelector = new OriginalLineItemSelector(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.#originalLineItemControl.onclick = () => this.originalLineItemSelector.onOpen()
 | 
					        this.#originalLineItemControl.onclick = () => this.originalLineItemSelector.onOpen()
 | 
				
			||||||
        this.#originalLineItemDelete.onclick = () => this.clearOriginalLineItem();
 | 
					        this.#originalLineItemDelete.onclick = () => this.clearOriginalLineItem();
 | 
				
			||||||
        this.#descriptionControl.onclick = () => this.#descriptionEditors[this.debitCredit].onOpen();
 | 
					        this.#descriptionControl.onclick = () => this.#descriptionEditors[this.debitCredit].onOpen();
 | 
				
			||||||
@@ -249,12 +238,30 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
                if (this.lineItem === null) {
 | 
					                if (this.lineItem === null) {
 | 
				
			||||||
                    this.lineItem = this.#debitCreditSubForm.addLineItem();
 | 
					                    this.lineItem = this.#debitCreditSubForm.addLineItem();
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                this.amount = this.#amountInput.value;
 | 
					 | 
				
			||||||
                this.lineItem.save(this);
 | 
					                this.lineItem.save(this);
 | 
				
			||||||
                bootstrap.Modal.getInstance(this.#modal).hide();
 | 
					                bootstrap.Modal.getInstance(this.modal).hide();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return false;
 | 
					            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
 | 
					     * @param originalLineItem {OriginalLineItem} the original line item
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    saveOriginalLineItem(originalLineItem) {
 | 
					    saveOriginalLineItem(originalLineItem) {
 | 
				
			||||||
        this.isNeedOffset = false;
 | 
					 | 
				
			||||||
        this.#originalLineItemContainer.classList.remove("d-none");
 | 
					        this.#originalLineItemContainer.classList.remove("d-none");
 | 
				
			||||||
        this.#originalLineItemControl.classList.add("accounting-not-empty");
 | 
					        this.#originalLineItemControl.classList.add("accounting-not-empty");
 | 
				
			||||||
        this.originalLineItemId = originalLineItem.id;
 | 
					        this.originalLineItemId = originalLineItem.id;
 | 
				
			||||||
@@ -279,9 +285,9 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
        this.description = originalLineItem.description === ""? null: originalLineItem.description;
 | 
					        this.description = originalLineItem.description === ""? null: originalLineItem.description;
 | 
				
			||||||
        this.#descriptionText.innerText = originalLineItem.description;
 | 
					        this.#descriptionText.innerText = originalLineItem.description;
 | 
				
			||||||
        this.#accountControl.classList.add("accounting-not-empty");
 | 
					        this.#accountControl.classList.add("accounting-not-empty");
 | 
				
			||||||
        this.accountCode = originalLineItem.accountCode;
 | 
					        this.account = originalLineItem.account.copy();
 | 
				
			||||||
        this.accountText = originalLineItem.accountText;
 | 
					        this.isAccountConfirmed = false;
 | 
				
			||||||
        this.#accountText.innerText = originalLineItem.accountText;
 | 
					        this.#accountText.innerText = this.account.text;
 | 
				
			||||||
        this.#amountInput.value = String(originalLineItem.netBalance);
 | 
					        this.#amountInput.value = String(originalLineItem.netBalance);
 | 
				
			||||||
        this.#amountInput.max = String(originalLineItem.netBalance);
 | 
					        this.#amountInput.max = String(originalLineItem.netBalance);
 | 
				
			||||||
        this.#amountInput.min = "0";
 | 
					        this.#amountInput.min = "0";
 | 
				
			||||||
@@ -293,7 +299,6 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    clearOriginalLineItem() {
 | 
					    clearOriginalLineItem() {
 | 
				
			||||||
        this.isNeedOffset = false;
 | 
					 | 
				
			||||||
        this.#originalLineItemContainer.classList.add("d-none");
 | 
					        this.#originalLineItemContainer.classList.add("d-none");
 | 
				
			||||||
        this.#originalLineItemControl.classList.remove("accounting-not-empty");
 | 
					        this.#originalLineItemControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
        this.originalLineItemId = null;
 | 
					        this.originalLineItemId = null;
 | 
				
			||||||
@@ -302,54 +307,34 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
        this.#originalLineItemText.innerText = "";
 | 
					        this.#originalLineItemText.innerText = "";
 | 
				
			||||||
        this.#setEnableDescriptionAccount(true);
 | 
					        this.#setEnableDescriptionAccount(true);
 | 
				
			||||||
        this.#accountControl.classList.remove("accounting-not-empty");
 | 
					        this.#accountControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
        this.accountCode = null;
 | 
					        this.account = null;
 | 
				
			||||||
        this.accountText = null;
 | 
					        this.isAccountConfirmed = false;
 | 
				
			||||||
        this.#accountText.innerText = "";
 | 
					        this.#accountText.innerText = "";
 | 
				
			||||||
        this.#amountInput.max = "";
 | 
					        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.
 | 
					     * Saves the description from the description editor.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param description {string} the description
 | 
					     * @param editor {DescriptionEditor} the description editor
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    saveDescription(description) {
 | 
					    saveDescription(editor) {
 | 
				
			||||||
        if (description === "") {
 | 
					        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");
 | 
					            this.#descriptionControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            this.#descriptionControl.classList.add("accounting-not-empty");
 | 
					            this.#descriptionControl.classList.add("accounting-not-empty");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.description = description === ""? null: description;
 | 
					        this.description = editor.description === ""? null: editor.description;
 | 
				
			||||||
        this.#descriptionText.innerText = description;
 | 
					        this.#descriptionText.innerText = editor.description;
 | 
				
			||||||
        this.#validateDescription();
 | 
					        this.#validateDescription();
 | 
				
			||||||
        bootstrap.Modal.getOrCreateInstance(this.#modal).show();
 | 
					        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)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -357,27 +342,23 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    clearAccount() {
 | 
					    clearAccount() {
 | 
				
			||||||
        this.isNeedOffset = false;
 | 
					 | 
				
			||||||
        this.#accountControl.classList.remove("accounting-not-empty");
 | 
					        this.#accountControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
        this.accountCode = null;
 | 
					        this.account = null;
 | 
				
			||||||
        this.accountText = null;
 | 
					        this.isAccountConfirmed = false;
 | 
				
			||||||
        this.#accountText.innerText = "";
 | 
					        this.#accountText.innerText = "";
 | 
				
			||||||
        this.#validateAccount();
 | 
					        this.#validateAccount();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Sets the account.
 | 
					     * Saves the selected account.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param code {string} the account code
 | 
					     * @param account {JournalEntryAccountOption} the selected account
 | 
				
			||||||
     * @param text {string} the account text
 | 
					 | 
				
			||||||
     * @param isNeedOffset {boolean} true if the line items in the account need offset or false otherwise
 | 
					 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    saveAccount(code, text, isNeedOffset) {
 | 
					    saveAccount(account) {
 | 
				
			||||||
        this.isNeedOffset = isNeedOffset;
 | 
					 | 
				
			||||||
        this.#accountControl.classList.add("accounting-not-empty");
 | 
					        this.#accountControl.classList.add("accounting-not-empty");
 | 
				
			||||||
        this.accountCode = code;
 | 
					        this.account = new JournalEntryAccount(account.code, account.text, account.isNeedOffset);
 | 
				
			||||||
        this.accountText = text;
 | 
					        this.isAccountConfirmed = true;
 | 
				
			||||||
        this.#accountText.innerText = text;
 | 
					        this.#accountText.innerText = account.text;
 | 
				
			||||||
        this.#validateAccount();
 | 
					        this.#validateAccount();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -425,7 +406,7 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
     * @return {boolean} true if valid, or false otherwise
 | 
					     * @return {boolean} true if valid, or false otherwise
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #validateAccount() {
 | 
					    #validateAccount() {
 | 
				
			||||||
        if (this.accountCode === null) {
 | 
					        if (this.account === null) {
 | 
				
			||||||
            this.#accountControl.classList.add("is-invalid");
 | 
					            this.#accountControl.classList.add("is-invalid");
 | 
				
			||||||
            this.#accountError.innerText = A_("Please select the account.");
 | 
					            this.#accountError.innerText = A_("Please select the account.");
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
@@ -484,7 +465,6 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
        this.lineItem = null;
 | 
					        this.lineItem = null;
 | 
				
			||||||
        this.#debitCreditSubForm = debitCredit;
 | 
					        this.#debitCreditSubForm = debitCredit;
 | 
				
			||||||
        this.debitCredit = this.#debitCreditSubForm.debitCredit;
 | 
					        this.debitCredit = this.#debitCreditSubForm.debitCredit;
 | 
				
			||||||
        this.isNeedOffset = false;
 | 
					 | 
				
			||||||
        this.#originalLineItemContainer.classList.add("d-none");
 | 
					        this.#originalLineItemContainer.classList.add("d-none");
 | 
				
			||||||
        this.#originalLineItemControl.classList.remove("accounting-not-empty");
 | 
					        this.#originalLineItemControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
        this.#originalLineItemControl.classList.remove("is-invalid");
 | 
					        this.#originalLineItemControl.classList.remove("is-invalid");
 | 
				
			||||||
@@ -500,8 +480,8 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
        this.#descriptionError.innerText = ""
 | 
					        this.#descriptionError.innerText = ""
 | 
				
			||||||
        this.#accountControl.classList.remove("accounting-not-empty");
 | 
					        this.#accountControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
        this.#accountControl.classList.remove("is-invalid");
 | 
					        this.#accountControl.classList.remove("is-invalid");
 | 
				
			||||||
        this.accountCode = null;
 | 
					        this.account = null;
 | 
				
			||||||
        this.accountText = null;
 | 
					        this.isAccountConfirmed = false;
 | 
				
			||||||
        this.#accountText.innerText = "";
 | 
					        this.#accountText.innerText = "";
 | 
				
			||||||
        this.#accountError.innerText = "";
 | 
					        this.#accountError.innerText = "";
 | 
				
			||||||
        this.#amountInput.value = "";
 | 
					        this.#amountInput.value = "";
 | 
				
			||||||
@@ -520,10 +500,9 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
        this.lineItem = lineItem;
 | 
					        this.lineItem = lineItem;
 | 
				
			||||||
        this.#debitCreditSubForm = lineItem.debitCreditSubForm;
 | 
					        this.#debitCreditSubForm = lineItem.debitCreditSubForm;
 | 
				
			||||||
        this.debitCredit = this.#debitCreditSubForm.debitCredit;
 | 
					        this.debitCredit = this.#debitCreditSubForm.debitCredit;
 | 
				
			||||||
        this.isNeedOffset = lineItem.isNeedOffset();
 | 
					        this.originalLineItemId = lineItem.originalLineItemId;
 | 
				
			||||||
        this.originalLineItemId = lineItem.getOriginalLineItemId();
 | 
					        this.originalLineItemDate = lineItem.originalLineItemDate;
 | 
				
			||||||
        this.originalLineItemDate = lineItem.getOriginalLineItemDate();
 | 
					        this.originalLineItemText = lineItem.originalLineItemText;
 | 
				
			||||||
        this.originalLineItemText = lineItem.getOriginalLineItemText();
 | 
					 | 
				
			||||||
        this.#originalLineItemText.innerText = this.originalLineItemText;
 | 
					        this.#originalLineItemText.innerText = this.originalLineItemText;
 | 
				
			||||||
        if (this.originalLineItemId === null) {
 | 
					        if (this.originalLineItemId === null) {
 | 
				
			||||||
            this.#originalLineItemContainer.classList.add("d-none");
 | 
					            this.#originalLineItemContainer.classList.add("d-none");
 | 
				
			||||||
@@ -533,25 +512,25 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
            this.#originalLineItemControl.classList.add("accounting-not-empty");
 | 
					            this.#originalLineItemControl.classList.add("accounting-not-empty");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.#setEnableDescriptionAccount(!lineItem.isMatched && this.originalLineItemId === null);
 | 
					        this.#setEnableDescriptionAccount(!lineItem.isMatched && this.originalLineItemId === null);
 | 
				
			||||||
        this.description = lineItem.getDescription();
 | 
					        this.description = lineItem.description;
 | 
				
			||||||
        if (this.description === null) {
 | 
					        if (this.description === null) {
 | 
				
			||||||
            this.#descriptionControl.classList.remove("accounting-not-empty");
 | 
					            this.#descriptionControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            this.#descriptionControl.classList.add("accounting-not-empty");
 | 
					            this.#descriptionControl.classList.add("accounting-not-empty");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.#descriptionText.innerText = this.description === null? "": this.description;
 | 
					        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");
 | 
					            this.#accountControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            this.#accountControl.classList.add("accounting-not-empty");
 | 
					            this.#accountControl.classList.add("accounting-not-empty");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.accountCode = lineItem.getAccountCode();
 | 
					        this.#accountText.innerText = this.account.text;
 | 
				
			||||||
        this.accountText = lineItem.getAccountText();
 | 
					        this.#amountInput.value = lineItem.amount === null? "": String(lineItem.amount);
 | 
				
			||||||
        this.#accountText.innerText = this.accountText;
 | 
					 | 
				
			||||||
        this.#amountInput.value = lineItem.getAmount() === null? "": String(lineItem.getAmount());
 | 
					 | 
				
			||||||
        const maxAmount = this.#getMaxAmount();
 | 
					        const maxAmount = this.#getMaxAmount();
 | 
				
			||||||
        this.#amountInput.max = maxAmount === null? "": maxAmount;
 | 
					        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();
 | 
					        this.#validate();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -575,11 +554,11 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
    #setEnableDescriptionAccount(isEnabled) {
 | 
					    #setEnableDescriptionAccount(isEnabled) {
 | 
				
			||||||
        if (isEnabled) {
 | 
					        if (isEnabled) {
 | 
				
			||||||
            this.#descriptionControl.dataset.bsToggle = "modal";
 | 
					            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.remove("accounting-disabled");
 | 
				
			||||||
            this.#descriptionControl.classList.add("accounting-clickable");
 | 
					            this.#descriptionControl.classList.add("accounting-clickable");
 | 
				
			||||||
            this.#accountControl.dataset.bsToggle = "modal";
 | 
					            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.remove("accounting-disabled");
 | 
				
			||||||
            this.#accountControl.classList.add("accounting-clickable");
 | 
					            this.#accountControl.classList.add("accounting-clickable");
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@@ -594,3 +573,4 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
/* The Mia! Accounting Flask Project
 | 
					/* The Mia! Accounting Project
 | 
				
			||||||
 * journal-entry-order.js: The JavaScript for the journal entry order
 | 
					 * journal-entry-order.js: The JavaScript for the journal entry order
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,7 +29,7 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
        const onReorder = () => {
 | 
					        const onReorder = () => {
 | 
				
			||||||
            const accounts = Array.from(list.children);
 | 
					            const accounts = Array.from(list.children);
 | 
				
			||||||
            for (let i = 0; i < accounts.length; i++) {
 | 
					            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);
 | 
					                no.value = String(i + 1);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
					 * material-fab-speed-dial.js: The JavaScript for the speed dial for the material floating buttons
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1029
									
								
								src/accounting/static/js/option-form.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1029
									
								
								src/accounting/static/js/option-form.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -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
 | 
					 * original-line-item-selector.js: The JavaScript for the original line item selector
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -35,7 +35,7 @@ class OriginalLineItemSelector {
 | 
				
			|||||||
    lineItemEditor;
 | 
					    lineItemEditor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The prefix of the HTML ID and class
 | 
					     * The prefix of the HTML ID and class names
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #prefix = "accounting-original-line-item-selector";
 | 
					    #prefix = "accounting-original-line-item-selector";
 | 
				
			||||||
@@ -88,17 +88,15 @@ class OriginalLineItemSelector {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(lineItemEditor) {
 | 
					    constructor(lineItemEditor) {
 | 
				
			||||||
        this.lineItemEditor = lineItemEditor;
 | 
					        this.lineItemEditor = lineItemEditor;
 | 
				
			||||||
        this.#query = document.getElementById(this.#prefix + "-query");
 | 
					        this.#query = document.getElementById(`${this.#prefix}-query`);
 | 
				
			||||||
        this.#queryNoResult = document.getElementById(this.#prefix + "-option-no-result");
 | 
					        this.#queryNoResult = document.getElementById(`${this.#prefix}-option-no-result`);
 | 
				
			||||||
        this.#optionList = document.getElementById(this.#prefix + "-option-list");
 | 
					        this.#optionList = document.getElementById(`${this.#prefix}-option-list`);
 | 
				
			||||||
        this.#options = Array.from(document.getElementsByClassName(this.#prefix + "-option")).map((element) => new OriginalLineItem(this, element));
 | 
					        this.#options = Array.from(document.getElementsByClassName(`${this.#prefix}-option`)).map((element) => new OriginalLineItem(this, element));
 | 
				
			||||||
        this.#optionById = {};
 | 
					        this.#optionById = {};
 | 
				
			||||||
        for (const option of this.#options) {
 | 
					        for (const option of this.#options) {
 | 
				
			||||||
            this.#optionById[option.id] = option;
 | 
					            this.#optionById[option.id] = option;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.#query.addEventListener("input", () => {
 | 
					        this.#query.oninput = () => this.#filterOptions();
 | 
				
			||||||
            this.#filterOptions();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -113,8 +111,8 @@ class OriginalLineItemSelector {
 | 
				
			|||||||
        const otherLineItems = form.getLineItems().filter((lineItem) => lineItem !== currentLineItem);
 | 
					        const otherLineItems = form.getLineItems().filter((lineItem) => lineItem !== currentLineItem);
 | 
				
			||||||
        let otherOffset = new Decimal(0);
 | 
					        let otherOffset = new Decimal(0);
 | 
				
			||||||
        for (const otherLineItem of otherLineItems) {
 | 
					        for (const otherLineItem of otherLineItems) {
 | 
				
			||||||
            if (otherLineItem.getOriginalLineItemId() === originalLineItemId) {
 | 
					            if (otherLineItem.originalLineItemId === originalLineItemId) {
 | 
				
			||||||
                const amount = otherLineItem.getAmount();
 | 
					                const amount = otherLineItem.amount;
 | 
				
			||||||
                if (amount !== null) {
 | 
					                if (amount !== null) {
 | 
				
			||||||
                    otherOffset = otherOffset.plus(amount);
 | 
					                    otherOffset = otherOffset.plus(amount);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@@ -131,8 +129,8 @@ class OriginalLineItemSelector {
 | 
				
			|||||||
        const otherLineItems = this.lineItemEditor.form.getLineItems().filter((lineItem) => lineItem !== this.lineItemEditor.lineItem);
 | 
					        const otherLineItems = this.lineItemEditor.form.getLineItems().filter((lineItem) => lineItem !== this.lineItemEditor.lineItem);
 | 
				
			||||||
        const otherOffsets = {}
 | 
					        const otherOffsets = {}
 | 
				
			||||||
        for (const otherLineItem of otherLineItems) {
 | 
					        for (const otherLineItem of otherLineItems) {
 | 
				
			||||||
            const otherOriginalLineItemId = otherLineItem.getOriginalLineItemId();
 | 
					            const otherOriginalLineItemId = otherLineItem.originalLineItemId;
 | 
				
			||||||
            const amount = otherLineItem.getAmount();
 | 
					            const amount = otherLineItem.amount;
 | 
				
			||||||
            if (otherOriginalLineItemId === null || amount === null) {
 | 
					            if (otherOriginalLineItemId === null || amount === null) {
 | 
				
			||||||
                continue;
 | 
					                continue;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -155,16 +153,16 @@ class OriginalLineItemSelector {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #filterOptions() {
 | 
					    #filterOptions() {
 | 
				
			||||||
        let hasAnyMatched = false;
 | 
					        let isAnyMatched = false;
 | 
				
			||||||
        for (const option of this.#options) {
 | 
					        for (const option of this.#options) {
 | 
				
			||||||
            if (option.isMatched(this.#debitCredit, this.#currencyCode, this.#query.value)) {
 | 
					            if (option.isMatched(this.#debitCredit, this.#currencyCode, this.#query.value)) {
 | 
				
			||||||
                option.setShown(true);
 | 
					                option.setShown(true);
 | 
				
			||||||
                hasAnyMatched = true;
 | 
					                isAnyMatched = true;
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                option.setShown(false);
 | 
					                option.setShown(false);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (!hasAnyMatched) {
 | 
					        if (!isAnyMatched) {
 | 
				
			||||||
            this.#optionList.classList.add("d-none");
 | 
					            this.#optionList.classList.add("d-none");
 | 
				
			||||||
            this.#queryNoResult.classList.remove("d-none");
 | 
					            this.#queryNoResult.classList.remove("d-none");
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@@ -178,7 +176,7 @@ class OriginalLineItemSelector {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    onOpen() {
 | 
					    onOpen() {
 | 
				
			||||||
        this.#currencyCode = this.lineItemEditor.getCurrencyCode();
 | 
					        this.#currencyCode = this.lineItemEditor.currencyCode;
 | 
				
			||||||
        this.#debitCredit = this.lineItemEditor.debitCredit;
 | 
					        this.#debitCredit = this.lineItemEditor.debitCredit;
 | 
				
			||||||
        for (const option of this.#options) {
 | 
					        for (const option of this.#options) {
 | 
				
			||||||
            option.setActive(option.id === this.lineItemEditor.originalLineItemId);
 | 
					            option.setActive(option.id === this.lineItemEditor.originalLineItemId);
 | 
				
			||||||
@@ -196,10 +194,10 @@ class OriginalLineItemSelector {
 | 
				
			|||||||
class OriginalLineItem {
 | 
					class OriginalLineItem {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The original line item selector
 | 
					     * The journal entry form
 | 
				
			||||||
     * @type {OriginalLineItemSelector}
 | 
					     * @type {JournalEntryForm}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #selector;
 | 
					    #form;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The element
 | 
					     * The element
 | 
				
			||||||
@@ -232,16 +230,10 @@ class OriginalLineItem {
 | 
				
			|||||||
    #currencyCode;
 | 
					    #currencyCode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The account code
 | 
					     * The account
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {JournalEntryAccount}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    accountCode;
 | 
					    account;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * The account text
 | 
					 | 
				
			||||||
     * @type {string}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    accountText;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The description
 | 
					     * The description
 | 
				
			||||||
@@ -275,7 +267,7 @@ class OriginalLineItem {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The values to query against
 | 
					     * The values to query against
 | 
				
			||||||
     * @type {string[][]}
 | 
					     * @type {string[]}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #queryValues;
 | 
					    #queryValues;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -286,21 +278,20 @@ class OriginalLineItem {
 | 
				
			|||||||
     * @param element {HTMLLIElement} the element
 | 
					     * @param element {HTMLLIElement} the element
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(selector, element) {
 | 
					    constructor(selector, element) {
 | 
				
			||||||
        this.#selector = selector;
 | 
					        this.#form = selector.lineItemEditor.form;
 | 
				
			||||||
        this.#element = element;
 | 
					        this.#element = element;
 | 
				
			||||||
        this.id = element.dataset.id;
 | 
					        this.id = element.dataset.id;
 | 
				
			||||||
        this.date = element.dataset.date;
 | 
					        this.date = element.dataset.date;
 | 
				
			||||||
        this.#debitCredit = element.dataset.debitCredit;
 | 
					        this.#debitCredit = element.dataset.debitCredit;
 | 
				
			||||||
        this.#currencyCode = element.dataset.currencyCode;
 | 
					        this.#currencyCode = element.dataset.currencyCode;
 | 
				
			||||||
        this.accountCode = element.dataset.accountCode;
 | 
					        this.account = new JournalEntryAccount(element.dataset.accountCode, element.dataset.accountText, false);
 | 
				
			||||||
        this.accountText = element.dataset.accountText;
 | 
					 | 
				
			||||||
        this.description = element.dataset.description;
 | 
					        this.description = element.dataset.description;
 | 
				
			||||||
        this.bareNetBalance = new Decimal(element.dataset.netBalance);
 | 
					        this.bareNetBalance = new Decimal(element.dataset.netBalance);
 | 
				
			||||||
        this.netBalance = this.bareNetBalance;
 | 
					        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.text = element.dataset.text;
 | 
				
			||||||
        this.#queryValues = JSON.parse(element.dataset.queryValues);
 | 
					        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) {
 | 
					    isMatched(debitCredit, currencyCode, query = null) {
 | 
				
			||||||
        return this.netBalance.greaterThan(0)
 | 
					        return this.netBalance.greaterThan(0)
 | 
				
			||||||
            && this.date <= this.#selector.lineItemEditor.form.getDate()
 | 
					            && this.date <= this.#form.date
 | 
				
			||||||
            && this.#isDebitCreditMatches(debitCredit)
 | 
					            && this.#isDebitCreditMatched(debitCredit)
 | 
				
			||||||
            && this.#currencyCode === currencyCode
 | 
					            && this.#currencyCode === currencyCode
 | 
				
			||||||
            && this.#isQueryMatches(query);
 | 
					            && this.#isQueryMatched(query);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -353,34 +344,43 @@ class OriginalLineItem {
 | 
				
			|||||||
     * @param debitCredit {string} either "debit" or credit
 | 
					     * @param debitCredit {string} either "debit" or credit
 | 
				
			||||||
     * @return {boolean} true if the option matches, or false otherwise
 | 
					     * @return {boolean} true if the option matches, or false otherwise
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #isDebitCreditMatches(debitCredit) {
 | 
					    #isDebitCreditMatched(debitCredit) {
 | 
				
			||||||
        return (debitCredit === "debit" && this.#debitCredit === "credit")
 | 
					        return (debitCredit === "debit" && this.#debitCredit === "credit")
 | 
				
			||||||
            || (debitCredit === "credit" && this.#debitCredit === "debit");
 | 
					            || (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
 | 
					     * @param query {string|null} the query term
 | 
				
			||||||
     * @return {boolean} true if the option matches, or false otherwise
 | 
					     * @return {boolean} true if the option matches, or false otherwise
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #isQueryMatches(query) {
 | 
					    #isQueryMatched(query) {
 | 
				
			||||||
        if (query === "") {
 | 
					        if (query === "") {
 | 
				
			||||||
            return true;
 | 
					            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())) {
 | 
					            if (queryValue.toLowerCase().includes(query.toLowerCase())) {
 | 
				
			||||||
                return true;
 | 
					                return true;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        for (const queryValue of this.#queryValues[1]) {
 | 
					 | 
				
			||||||
            if (queryValue === query) {
 | 
					 | 
				
			||||||
                return true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return false;
 | 
					        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.
 | 
					     * Sets whether the option is shown.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
/* The Mia! Accounting Flask Project
 | 
					/* The Mia! Accounting Project
 | 
				
			||||||
 * period-chooser.js: The JavaScript for the period chooser
 | 
					 * period-chooser.js: The JavaScript for the period chooser
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -33,12 +33,6 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
class PeriodChooser {
 | 
					class PeriodChooser {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * The prefix of the HTML ID and class
 | 
					 | 
				
			||||||
     * @type {string}
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    prefix;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The modal of the period chooser
 | 
					     * The modal of the period chooser
 | 
				
			||||||
     * @type {HTMLDivElement}
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
@@ -56,8 +50,8 @@ class PeriodChooser {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
        this.prefix = "accounting-period-chooser";
 | 
					        const prefix = "accounting-period-chooser";
 | 
				
			||||||
        this.modal = document.getElementById(this.prefix + "-modal");
 | 
					        this.modal = document.getElementById(`${prefix}-modal`);
 | 
				
			||||||
        for (const cls of [MonthTab, YearTab, DayTab, CustomTab]) {
 | 
					        for (const cls of [MonthTab, YearTab, DayTab, CustomTab]) {
 | 
				
			||||||
            const tab = new cls(this);
 | 
					            const tab = new cls(this);
 | 
				
			||||||
            this.tabPlanes[tab.tabId()] = tab;
 | 
					            this.tabPlanes[tab.tabId()] = tab;
 | 
				
			||||||
@@ -94,7 +88,7 @@ class TabPlane {
 | 
				
			|||||||
    chooser;
 | 
					    chooser;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The prefix of the HTML ID and class
 | 
					     * The prefix of the HTML ID and class names
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    prefix;
 | 
					    prefix;
 | 
				
			||||||
@@ -118,9 +112,9 @@ class TabPlane {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(chooser) {
 | 
					    constructor(chooser) {
 | 
				
			||||||
        this.chooser = chooser;
 | 
					        this.chooser = chooser;
 | 
				
			||||||
        this.prefix = "accounting-period-chooser-" + this.tabId();
 | 
					        this.prefix = `accounting-period-chooser-${this.tabId()}`;
 | 
				
			||||||
        this.#tab = document.getElementById(this.prefix + "-tab");
 | 
					        this.#tab = document.getElementById(`${this.prefix}-tab`);
 | 
				
			||||||
        this.#page = document.getElementById(this.prefix + "-page");
 | 
					        this.#page = document.getElementById(`${this.prefix}-page`);
 | 
				
			||||||
        this.#tab.onclick = () => this.#switchToMe();
 | 
					        this.#tab.onclick = () => this.#switchToMe();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -170,12 +164,11 @@ class MonthTab extends TabPlane {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(chooser) {
 | 
					    constructor(chooser) {
 | 
				
			||||||
        super(chooser);
 | 
					        super(chooser);
 | 
				
			||||||
        const monthChooser = document.getElementById(this.prefix + "-chooser");
 | 
					        const monthChooser = document.getElementById(`${this.prefix}-chooser`);
 | 
				
			||||||
        if (monthChooser !== null) {
 | 
					        if (monthChooser !== null) {
 | 
				
			||||||
            let start = monthChooser.dataset.start;
 | 
					 | 
				
			||||||
            this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, {
 | 
					            this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, {
 | 
				
			||||||
                restrictions: {
 | 
					                restrictions: {
 | 
				
			||||||
                    minDate: new Date(start),
 | 
					                    minDate: new Date(monthChooser.dataset.start),
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                display: {
 | 
					                display: {
 | 
				
			||||||
                    inline: true,
 | 
					                    inline: true,
 | 
				
			||||||
@@ -188,9 +181,8 @@ class MonthTab extends TabPlane {
 | 
				
			|||||||
            });
 | 
					            });
 | 
				
			||||||
            monthChooser.addEventListener(tempusDominus.Namespace.events.change, (e) => {
 | 
					            monthChooser.addEventListener(tempusDominus.Namespace.events.change, (e) => {
 | 
				
			||||||
                const date = e.detail.date;
 | 
					                const date = e.detail.date;
 | 
				
			||||||
                const year = date.year;
 | 
					                const zeroPaddedMonth = `0${date.month + 1}`.slice(-2)
 | 
				
			||||||
                const month = date.month + 1;
 | 
					                const period = `${date.year}-${zeroPaddedMonth}`;
 | 
				
			||||||
                const period = month < 10? year + "-0" + month: year + "-" + month;
 | 
					 | 
				
			||||||
                window.location = chooser.modal.dataset.urlTemplate
 | 
					                window.location = chooser.modal.dataset.urlTemplate
 | 
				
			||||||
                    .replaceAll("PERIOD", period);
 | 
					                    .replaceAll("PERIOD", period);
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@@ -250,8 +242,8 @@ class DayTab extends TabPlane {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(chooser) {
 | 
					    constructor(chooser) {
 | 
				
			||||||
        super(chooser);
 | 
					        super(chooser);
 | 
				
			||||||
        this.#date = document.getElementById(this.prefix + "-date");
 | 
					        this.#date = document.getElementById(`${this.prefix}-date`);
 | 
				
			||||||
        this.#dateError = document.getElementById(this.prefix + "-date-error");
 | 
					        this.#dateError = document.getElementById(`${this.prefix}-date-error`);
 | 
				
			||||||
        if (this.#date !== null) {
 | 
					        if (this.#date !== null) {
 | 
				
			||||||
            this.#date.onchange = () => {
 | 
					            this.#date.onchange = () => {
 | 
				
			||||||
                if (this.#validateDate()) {
 | 
					                if (this.#validateDate()) {
 | 
				
			||||||
@@ -337,11 +329,11 @@ class CustomTab extends TabPlane {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(chooser) {
 | 
					    constructor(chooser) {
 | 
				
			||||||
        super(chooser);
 | 
					        super(chooser);
 | 
				
			||||||
        this.#start = document.getElementById(this.prefix + "-start");
 | 
					        this.#start = document.getElementById(`${this.prefix}-start`);
 | 
				
			||||||
        this.#startError = document.getElementById(this.prefix + "-start-error");
 | 
					        this.#startError = document.getElementById(`${this.prefix}-start-error`);
 | 
				
			||||||
        this.#end = document.getElementById(this.prefix + "-end");
 | 
					        this.#end = document.getElementById(`${this.prefix}-end`);
 | 
				
			||||||
        this.#endError = document.getElementById(this.prefix + "-end-error");
 | 
					        this.#endError = document.getElementById(`${this.prefix}-end-error`);
 | 
				
			||||||
        this.#conform = document.getElementById(this.prefix + "-confirm");
 | 
					        this.#conform = document.getElementById(`${this.prefix}-confirm`);
 | 
				
			||||||
        if (this.#start !== null) {
 | 
					        if (this.#start !== null) {
 | 
				
			||||||
            this.#start.onchange = () => {
 | 
					            this.#start.onchange = () => {
 | 
				
			||||||
                if (this.#validateStart()) {
 | 
					                if (this.#validateStart()) {
 | 
				
			||||||
@@ -359,7 +351,7 @@ class CustomTab extends TabPlane {
 | 
				
			|||||||
                isValid = this.#validateEnd() && isValid;
 | 
					                isValid = this.#validateEnd() && isValid;
 | 
				
			||||||
                if (isValid) {
 | 
					                if (isValid) {
 | 
				
			||||||
                    window.location = chooser.modal.dataset.urlTemplate
 | 
					                    window.location = chooser.modal.dataset.urlTemplate
 | 
				
			||||||
                        .replaceAll("PERIOD", this.#start.value + "-" + this.#end.value);
 | 
					                        .replaceAll("PERIOD", `${this.#start.value}-${this.#end.value}`);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -17,9 +17,8 @@
 | 
				
			|||||||
"""The template globals.
 | 
					"""The template globals.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
from flask import current_app
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from accounting.models import Currency
 | 
					from accounting.models import Currency
 | 
				
			||||||
 | 
					from accounting.utils.options import options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def currency_options() -> str:
 | 
					def currency_options() -> str:
 | 
				
			||||||
@@ -35,5 +34,4 @@ def default_currency_code() -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    :return: The default currency code.
 | 
					    :return: The default currency code.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    with current_app.app_context():
 | 
					    return options.default_currency_code
 | 
				
			||||||
        return current_app.config.get("DEFAULT_CURRENCY", "USD")
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Project
 | 
				
			||||||
create.html: The account creation form
 | 
					create.html: The account creation form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Project
 | 
				
			||||||
detail.html: The account detail
 | 
					detail.html: The account detail
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -25,38 +25,45 @@ First written: 2023/1/31
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group mb-3">
 | 
					<div class="mb-3 accounting-toolbar">
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
 | 
					  <a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
					    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
				
			||||||
    {{ A_("Back") }}
 | 
					    <span class="d-none d-md-inline">{{ A_("Back") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
  {% if accounting_can_edit() %}
 | 
					  {% if accounting_can_edit() %}
 | 
				
			||||||
    <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
 | 
					    <a class="btn btn-primary d-none d-md-inline" role="button" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
 | 
				
			||||||
      <i class="fa-solid fa-gear"></i>
 | 
					      <i class="fa-solid fa-pen-to-square"></i>
 | 
				
			||||||
      {{ A_("Settings") }}
 | 
					      {{ A_("Edit") }}
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.account.order", base=obj.base)|accounting_append_next }}">
 | 
					  <a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.order", base=obj.base)|accounting_append_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-bars-staggered"></i>
 | 
					    <i class="fa-solid fa-bars-staggered"></i>
 | 
				
			||||||
    {{ A_("Order") }}
 | 
					    <span class="d-none d-md-inline">{{ A_("Order") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
  {% if accounting_can_edit() %}
 | 
					  {% if accounting_can_edit() %}
 | 
				
			||||||
    <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
 | 
					    {% if obj.can_delete %}
 | 
				
			||||||
      <i class="fa-solid fa-trash"></i>
 | 
					      <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
 | 
				
			||||||
      {{ A_("Delete") }}
 | 
					        <i class="fa-solid fa-trash"></i>
 | 
				
			||||||
    </button>
 | 
					        <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    {% else %}
 | 
				
			||||||
 | 
					      <button class="btn btn-secondary" type="button" disabled="disabled">
 | 
				
			||||||
 | 
					        <i class="fa-solid fa-trash"></i>
 | 
				
			||||||
 | 
					        <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% if accounting_can_edit() %}
 | 
					{% if accounting_can_edit() %}
 | 
				
			||||||
  <div class="d-md-none accounting-material-fab">
 | 
					  <div class="d-md-none accounting-material-fab">
 | 
				
			||||||
    <a class="btn btn-primary" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
 | 
					    <a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
 | 
				
			||||||
      <i class="fa-solid fa-pen-to-square"></i>
 | 
					      <i class="fa-solid fa-pen-to-square"></i>
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% if accounting_can_edit() %}
 | 
					{% if accounting_can_edit() and obj.can_delete %}
 | 
				
			||||||
  <form action="{{ url_for("accounting.account.delete", account=obj) }}" method="post">
 | 
					  <form action="{{ url_for("accounting.account.delete", account=obj) }}" method="post">
 | 
				
			||||||
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
					    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
				
			||||||
    {% if request.args.next %}
 | 
					    {% if request.args.next %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Project
 | 
				
			||||||
edit.html: The account edit form
 | 
					edit.html: The account edit form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Project
 | 
				
			||||||
form.html: The account form
 | 
					form.html: The account form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -27,10 +27,10 @@ First written: 2023/2/1
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group mb-3">
 | 
					<div class="mb-3 accounting-toolbar">
 | 
				
			||||||
  <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
 | 
					  <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
 | 
				
			||||||
    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
					    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
				
			||||||
    {{ A_("Back") }}
 | 
					    <span class="d-none d-md-inline">{{ A_("Back") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -101,7 +101,7 @@ First written: 2023/2/1
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        <ul id="accounting-base-selector-option-list" class="list-group accounting-selector-list">
 | 
					        <ul id="accounting-base-selector-option-list" class="list-group accounting-selector-list">
 | 
				
			||||||
          {% for base in form.base_options %}
 | 
					          {% for base in form.base_options %}
 | 
				
			||||||
            <li class="list-group-item accounting-clickable accounting-base-selector-option" data-code="{{ base.code }}" data-content="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}" data-bs-dismiss="modal">
 | 
					            <li class="list-group-item accounting-clickable accounting-base-selector-option" data-code="{{ base.code }}" data-text="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}" data-bs-dismiss="modal">
 | 
				
			||||||
              {{ base }}
 | 
					              {{ base }}
 | 
				
			||||||
            </li>
 | 
					            </li>
 | 
				
			||||||
          {% endfor %}
 | 
					          {% endfor %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Project
 | 
				
			||||||
list.html: The account list
 | 
					list.html: The account list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -27,7 +27,7 @@ First written: 2023/1/30
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<div class="mb-2 accounting-toolbar">
 | 
					<div class="mb-2 accounting-toolbar">
 | 
				
			||||||
  {% if accounting_can_edit() %}
 | 
					  {% if accounting_can_edit() %}
 | 
				
			||||||
    <a class="btn btn-primary text-nowrap d-none d-md-block" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
 | 
					    <a class="btn btn-primary text-nowrap d-none d-md-block" role="button" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
 | 
				
			||||||
      <i class="fa-solid fa-plus"></i>
 | 
					      <i class="fa-solid fa-plus"></i>
 | 
				
			||||||
      {{ A_("New") }}
 | 
					      {{ A_("New") }}
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
@@ -45,7 +45,7 @@ First written: 2023/1/30
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% if accounting_can_edit() %}
 | 
					{% if accounting_can_edit() %}
 | 
				
			||||||
  <div class="d-md-none accounting-material-fab">
 | 
					  <div class="d-md-none accounting-material-fab">
 | 
				
			||||||
    <a class="btn btn-primary" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
 | 
					    <a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
 | 
				
			||||||
      <i class="fa-solid fa-plus"></i>
 | 
					      <i class="fa-solid fa-plus"></i>
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user