Compare commits
183 Commits
Author | SHA1 | Date | |
---|---|---|---|
86c5b91697 | |||
9168840e64 | |||
21b9cfa8b8 | |||
b0b3b3acb1 | |||
cb1d254cf0 | |||
eb9ad57e72 | |||
ec26f8ef4d | |||
7ed29115ed | |||
95955197ac | |||
d5a0f79e4b | |||
6aa655aa64 | |||
6e532af26e | |||
fa1818d124 | |||
f21ecc2aa9 | |||
5ae1ab95ae | |||
7a5b3b78fc | |||
7df4051452 | |||
85084c68fd | |||
0185c16654 | |||
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.
|
||||||
|
40
.readthedocs.yaml
Normal file
40
.readthedocs.yaml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/5
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# .readthedocs.yaml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Set the version of Python and other tools you might need
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: "3.11"
|
||||||
|
|
||||||
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
|
|
||||||
|
# If using Sphinx, optionally build your docs in additional formats such as PDF
|
||||||
|
formats: all
|
||||||
|
|
||||||
|
# Optionally declare the Python requirements required to build your docs
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- method: pip
|
||||||
|
path: .
|
23
MANIFEST.in
23
MANIFEST.in
@ -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.
|
||||||
@ -15,17 +15,14 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
recursive-include src/accounting/static *
|
||||||
exclude src/accounting/static/js/dummy.js
|
exclude src/accounting/static/js/dummy.js
|
||||||
include src/accounting/translations/*
|
recursive-include src/accounting/templates *
|
||||||
include src/accounting/translations/*/LC_MESSAGES/*
|
recursive-include src/accounting/translations *
|
||||||
include docs/*
|
recursive-include src/accounting/data *
|
||||||
include docs/source/*
|
recursive-include docs *
|
||||||
include docs/source/_static/*
|
recursive-exclude docs/build *
|
||||||
include docs/source/_templates/*
|
recursive-include tests *
|
||||||
include tests/*
|
|
||||||
exclude tests/test_temp.py
|
exclude tests/test_temp.py
|
||||||
include tests/test_site/*
|
recursive-exclude tests *.pyc
|
||||||
include tests/test_site/static/*
|
recursive-exclude tests/instance *
|
||||||
include tests/test_site/templates/*
|
|
||||||
include tests/test_site/translations/*
|
|
||||||
include tests/test_site/translations/*/LC_MESSAGES/*
|
|
||||||
|
193
README.rst
193
README.rst
@ -1,24 +1,178 @@
|
|||||||
=====================
|
===============
|
||||||
Mia! Accounting Flask
|
Mia! Accounting
|
||||||
=====================
|
===============
|
||||||
|
|
||||||
|
|
||||||
Description
|
Description
|
||||||
===========
|
===========
|
||||||
|
|
||||||
This is the Mia! Accounting Flask project. It is an accounting
|
*Mia! Accounting* is an accounting module for Flask_ applications.
|
||||||
module for the Flask_ applications.
|
It implements `double-entry bookkeeping`_, and generates the following
|
||||||
|
accounting reports:
|
||||||
|
|
||||||
|
* Trial balance
|
||||||
|
* Income statement
|
||||||
|
* Balance sheet
|
||||||
|
|
||||||
|
In addition, *Mia! Accounting* tracks offsets for unpaid payables and
|
||||||
|
receivables.
|
||||||
|
|
||||||
|
You may try the `live demonstration`_.
|
||||||
|
|
||||||
|
|
||||||
Install
|
Installation
|
||||||
=======
|
============
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
You may also download the from the `PyPI project page`_ or the
|
||||||
|
`release page`_ on the `Git repository`_.
|
||||||
|
|
||||||
|
|
||||||
|
Prerequisites
|
||||||
|
=============
|
||||||
|
|
||||||
|
You need a running Flask application with database user login.
|
||||||
|
The primary key of the user data model must be integer.
|
||||||
|
|
||||||
|
The following front-end JavaScript libraries must be loaded. You may
|
||||||
|
download it locally or use CDN_.
|
||||||
|
|
||||||
|
* Bootstrap_ 5.2.3 or above
|
||||||
|
* FontAwesome_ 6.2.1 or above
|
||||||
|
* `Decimal.js`_ 6.4.3 or above
|
||||||
|
* `Tempus-Dominus`_ 6.4.3 or above
|
||||||
|
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
You need to pass the Flask *app* and an implementation of
|
||||||
|
`UserUtilityInterface`_ to the `init_app`_ function.
|
||||||
|
``UserUtilityInterface`` contains everything *Mia! Accounting* needs.
|
||||||
|
|
||||||
|
The following is an example configuration for *Mia! Accounting*.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
from flask import Response, redirect
|
||||||
|
from .auth import current_user()
|
||||||
|
from .modules import User
|
||||||
|
|
||||||
|
def create_app(test_config=None) -> Flask:
|
||||||
|
app: Flask = Flask(__name__)
|
||||||
|
|
||||||
|
... (Configuration of SQLAlchemy, CSRF, Babel_JS, ... etc) ...
|
||||||
|
|
||||||
|
import accounting
|
||||||
|
|
||||||
|
class UserUtils(accounting.UserUtilityInterface[User]):
|
||||||
|
|
||||||
|
def can_view(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def can_edit(self) -> bool:
|
||||||
|
return "editor" in current_user().roles
|
||||||
|
|
||||||
|
def can_admin(self) -> bool:
|
||||||
|
return current_user().is_admin
|
||||||
|
|
||||||
|
def unauthorized(self) -> Response:
|
||||||
|
return redirect("/login")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cls(self) -> t.Type[User]:
|
||||||
|
return User
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pk_column(self) -> Column:
|
||||||
|
return User.id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_user(self) -> User | None:
|
||||||
|
return current_user()
|
||||||
|
|
||||||
|
def get_by_username(self, username: str) -> User | None:
|
||||||
|
return User.query.filter(User.username == username).first()
|
||||||
|
|
||||||
|
def get_pk(self, user: User) -> int:
|
||||||
|
return user.id
|
||||||
|
|
||||||
|
accounting.init_app(app, UserUtils())
|
||||||
|
|
||||||
|
... (Any other configuration) ...
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
Database Initialization
|
||||||
|
=======================
|
||||||
|
|
||||||
|
After the configuration, you need to run
|
||||||
|
`flask_sqlalchemy.SQLAlchemy.create_all`_ to create the
|
||||||
|
database tables that *Mia! Accounting* uses.
|
||||||
|
|
||||||
|
*Mia! Accounting* adds three console commands:
|
||||||
|
|
||||||
|
* ``accounting-init-base``
|
||||||
|
* ``accounting-init-accounts``
|
||||||
|
* ``accounting-init-currencies``
|
||||||
|
|
||||||
|
After database tables are created, run
|
||||||
|
``accounting-init-base`` first, and then the other two commands.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
% flask --app myapp accounting-init-base
|
||||||
|
% flask --app myapp accounting-init-accounts
|
||||||
|
% flask --app myapp accounting-init-currencies
|
||||||
|
|
||||||
|
|
||||||
|
Navigation Menu
|
||||||
|
===============
|
||||||
|
|
||||||
|
Include the navigation menu in the `Bootstrap navigation bar`_ in your
|
||||||
|
base template:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg bg-body-tertiary bg-dark navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
...
|
||||||
|
<div id="collapsible-navbar" class="collapse navbar-collapse">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
...
|
||||||
|
{% include "accounting/include/nav.html" %}
|
||||||
|
...
|
||||||
|
</ul>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
Check your Flask application and see how it works.
|
||||||
|
|
||||||
|
|
||||||
|
Test Site and Live Demonstration
|
||||||
|
================================
|
||||||
|
|
||||||
|
You may find a working example in the `test site`_ in the
|
||||||
|
`source distribution`_. It is the simplest website that works with
|
||||||
|
*Mia! Accounting*. It is used in the automatic tests. It is the same
|
||||||
|
code run for `live demonstration`_.
|
||||||
|
|
||||||
|
If you do not have a running Flask application, you may start with the
|
||||||
|
test site.
|
||||||
|
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
=============
|
||||||
|
|
||||||
|
Refer to the `documentation on Read the Docs`_.
|
||||||
|
|
||||||
|
|
||||||
Copyright
|
Copyright
|
||||||
@ -46,5 +200,22 @@ Authors
|
|||||||
| imacat@mail.imacat.idv.tw
|
| imacat@mail.imacat.idv.tw
|
||||||
| 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
|
.. _double-entry bookkeeping: https://en.wikipedia.org/wiki/Double-entry_bookkeeping
|
||||||
|
.. _live demonstration: https://accounting.imacat.idv.tw
|
||||||
|
.. _PyPI project page: https://pypi.org/project/mia-accounting
|
||||||
|
.. _release page: https://github.com/imacat/mia-accounting/releases
|
||||||
|
.. _Git repository: https://github.com/imacat/mia-accounting
|
||||||
|
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
|
||||||
|
.. _Bootstrap: https://getbootstrap.com
|
||||||
|
.. _FontAwesome: https://fontawesome.com
|
||||||
|
.. _Decimal.js: https://mikemcl.github.io/decimal.js
|
||||||
|
.. _Tempus-Dominus: https://getdatepicker.com
|
||||||
|
.. _UserUtilityInterface: https://mia-accounting.readthedocs.io/en/latest/accounting.utils.html#accounting.utils.user.UserUtilityInterface
|
||||||
|
.. _init_app: https://mia-accounting.readthedocs.io/en/latest/accounting.html#accounting.init_app
|
||||||
|
.. _flask_sqlalchemy.SQLAlchemy.create_all: https://flask-sqlalchemy.palletsprojects.com/en/3.0.x/api/#flask_sqlalchemy.SQLAlchemy.create_all
|
||||||
|
.. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/
|
||||||
|
.. _test site: https://github.com/imacat/mia-accounting/tree/main/tests/test_site
|
||||||
|
.. _source distribution: https://pypi.org/project/mia-accounting/#files
|
||||||
|
.. _documentation on Read the Docs: https://mia-accounting.readthedocs.io
|
||||||
|
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 = '1.0.1'
|
||||||
|
|
||||||
# -- 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
|
||||||
|
61
docs/source/examples.rst
Normal file
61
docs/source/examples.rst
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
Examples
|
||||||
|
========
|
||||||
|
|
||||||
|
|
||||||
|
.. _example-userutils:
|
||||||
|
|
||||||
|
An Example Configuration
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
The following is an example configuration for *Mia! Accounting*.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
from flask import Response, redirect
|
||||||
|
from .auth import current_user()
|
||||||
|
from .modules import User
|
||||||
|
|
||||||
|
def create_app(test_config=None) -> Flask:
|
||||||
|
app: Flask = Flask(__name__)
|
||||||
|
|
||||||
|
... (Configuration of SQLAlchemy, CSRF, Babel_JS, ... etc) ...
|
||||||
|
|
||||||
|
import accounting
|
||||||
|
|
||||||
|
class UserUtils(accounting.UserUtilityInterface[User]):
|
||||||
|
|
||||||
|
def can_view(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def can_edit(self) -> bool:
|
||||||
|
return "editor" in current_user().roles
|
||||||
|
|
||||||
|
def can_admin(self) -> bool:
|
||||||
|
return current_user().is_admin
|
||||||
|
|
||||||
|
def unauthorized(self) -> Response:
|
||||||
|
return redirect("/login")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cls(self) -> t.Type[User]:
|
||||||
|
return User
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pk_column(self) -> Column:
|
||||||
|
return User.id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_user(self) -> User | None:
|
||||||
|
return current_user()
|
||||||
|
|
||||||
|
def get_by_username(self, username: str) -> User | None:
|
||||||
|
return User.query.filter(User.username == username).first()
|
||||||
|
|
||||||
|
def get_pk(self, user: User) -> int:
|
||||||
|
return user.id
|
||||||
|
|
||||||
|
accounting.init_app(app, UserUtils())
|
||||||
|
|
||||||
|
... (Any other configuration) ...
|
||||||
|
|
||||||
|
return app
|
57
docs/source/history.rst
Normal file
57
docs/source/history.rst
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
History
|
||||||
|
=======
|
||||||
|
|
||||||
|
I created my own private accounting application in Perl_/mod_perl_ in
|
||||||
|
2007, as part of my personal website. The first revision was made
|
||||||
|
using Perl/Mojolicious_ in 2019, with the aim of making it
|
||||||
|
mobile-friendly using Bootstrap_, and with modern back-end and
|
||||||
|
front-end technologies such as jQuery.
|
||||||
|
|
||||||
|
The second revision was done in Python_/Django_ in 2020, as I was
|
||||||
|
looking to change my career from PHP_/Laravel_ to Python, but lacked
|
||||||
|
experience with large Python projects. I needed something in my
|
||||||
|
portfolio and decided to work on the somewhat outdated Mojolicious
|
||||||
|
project.
|
||||||
|
|
||||||
|
Despite having no prior experience with Django, I spent two months
|
||||||
|
working late nights to create the `Mia! Accounting Django`_
|
||||||
|
application. It took me another 1.5 months to make it an independent
|
||||||
|
module, which I later released as an open source project on PyPI.
|
||||||
|
|
||||||
|
The application worked nicely for my household bookkeeping for two
|
||||||
|
years. However, new demands arose over time, especially with tracking
|
||||||
|
payables and receivables. This was critical `during the pandemic`_ as
|
||||||
|
more payments were made online with credit cards.
|
||||||
|
|
||||||
|
The biggest issue I encountered was with
|
||||||
|
`Django's MTV architectural pattern`_. Django takes over the control
|
||||||
|
flow. I had to override several parts of the `class-based views`_ for
|
||||||
|
different but yet simple control flow logic. In the end, it became
|
||||||
|
very difficult to track whether things went wrong because I overrode
|
||||||
|
something or because it just wouldn't work with the basic assumption
|
||||||
|
of the class-based views. By the time I realized it, it was too late
|
||||||
|
for me to drop Django's MTV and rewrite everything from class-based
|
||||||
|
views to function-based views.
|
||||||
|
|
||||||
|
Therefore, I decided to turn to microframeworks_ like Flask_. After
|
||||||
|
working with modularized Flask and FastAPI_ applications for two
|
||||||
|
years, I returned to the project and wrote its third revision using
|
||||||
|
Flask in 2023.
|
||||||
|
|
||||||
|
|
||||||
|
.. _Perl: https://www.perl.org
|
||||||
|
.. _mod_perl: https://perl.apache.org
|
||||||
|
.. _Mojolicious: https://mojolicious.org
|
||||||
|
.. _Bootstrap: https://getbootstrap.com
|
||||||
|
.. _jQuery: https://jquery.com
|
||||||
|
.. _Python: https://www.python.org
|
||||||
|
.. _Django: https://www.djangoproject.com
|
||||||
|
.. _PHP: https://www.php.net
|
||||||
|
.. _Laravel: https://laravel.com
|
||||||
|
.. _Mia! Accounting Django: https://github.com/imacat/mia-accounting-django
|
||||||
|
.. _during the pandemic: https://en.wikipedia.org/wiki/COVID-19_pandemic
|
||||||
|
.. _FastAPI: https://fastapi.tiangolo.com
|
||||||
|
.. _Django's MTV architectural pattern: https://docs.djangoproject.com/en/dev/faq/general/#django-appears-to-be-a-mvc-framework-but-you-call-the-controller-the-view-and-the-view-the-template-how-come-you-don-t-use-the-standard-names
|
||||||
|
.. _class-based views: https://docs.djangoproject.com/en/4.2/topics/class-based-views/
|
||||||
|
.. _microframeworks: https://en.wikipedia.org/wiki/Microframework
|
||||||
|
.. _Flask: https://flask.palletsprojects.com
|
@ -1,15 +1,20 @@
|
|||||||
.. 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
|
||||||
:caption: Contents:
|
:caption: Contents:
|
||||||
|
|
||||||
|
intro
|
||||||
|
accounting
|
||||||
|
examples
|
||||||
|
history
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
138
docs/source/intro.rst
Normal file
138
docs/source/intro.rst
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
Introduction
|
||||||
|
============
|
||||||
|
|
||||||
|
*Mia! Accounting* is an accounting module for Flask_ applications.
|
||||||
|
It implements `double-entry bookkeeping`_, and generates the following
|
||||||
|
accounting reports:
|
||||||
|
|
||||||
|
* Trial balance
|
||||||
|
* Income statement
|
||||||
|
* Balance sheet
|
||||||
|
|
||||||
|
In addition, *Mia! Accounting* tracks offsets for unpaid payables and
|
||||||
|
receivables.
|
||||||
|
|
||||||
|
You may try the `live demonstration`_.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Install *Mia! Accounting* with ``pip``:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pip install mia-accounting
|
||||||
|
|
||||||
|
You may also download the from the `PyPI project page`_ or the
|
||||||
|
`release page`_ on the `Git repository`_.
|
||||||
|
|
||||||
|
|
||||||
|
Prerequisites
|
||||||
|
-------------
|
||||||
|
|
||||||
|
You need a running Flask application with database user login.
|
||||||
|
The primary key of the user data model must be integer.
|
||||||
|
|
||||||
|
The following front-end JavaScript libraries must be loaded. You may
|
||||||
|
download it locally or use CDN_.
|
||||||
|
|
||||||
|
* Bootstrap_ 5.2.3 or above
|
||||||
|
* FontAwesome_ 6.2.1 or above
|
||||||
|
* `Decimal.js`_ 6.4.3 or above
|
||||||
|
* `Tempus-Dominus`_ 6.4.3 or above
|
||||||
|
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
-------------
|
||||||
|
|
||||||
|
You need to pass the Flask *app* and an implementation of
|
||||||
|
:py:class:`accounting.utils.user.UserUtilityInterface` to the
|
||||||
|
:py:func:`accounting.init_app` function. ``UserUtilityInterface``
|
||||||
|
contains everything *Mia! Accounting* needs.
|
||||||
|
|
||||||
|
See an example in :ref:`example-userutils`.
|
||||||
|
|
||||||
|
|
||||||
|
Database Initialization
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
After the configuration, you need to run
|
||||||
|
`flask_sqlalchemy.SQLAlchemy.create_all`_ to create the
|
||||||
|
database tables that *Mia! Accounting* uses.
|
||||||
|
|
||||||
|
*Mia! Accounting* adds three console commands:
|
||||||
|
|
||||||
|
* ``accounting-init-base``
|
||||||
|
* ``accounting-init-accounts``
|
||||||
|
* ``accounting-init-currencies``
|
||||||
|
|
||||||
|
After database tables are created, run
|
||||||
|
``accounting-init-base`` first, and then the other two commands.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
% flask --app myapp accounting-init-base
|
||||||
|
% flask --app myapp accounting-init-accounts
|
||||||
|
% flask --app myapp accounting-init-currencies
|
||||||
|
|
||||||
|
|
||||||
|
Navigation Menu
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Include the navigation menu in the `Bootstrap navigation bar`_ in your
|
||||||
|
base template:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg bg-body-tertiary bg-dark navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
...
|
||||||
|
<div id="collapsible-navbar" class="collapse navbar-collapse">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
...
|
||||||
|
{% include "accounting/include/nav.html" %}
|
||||||
|
...
|
||||||
|
</ul>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
Check your Flask application and see how it works.
|
||||||
|
|
||||||
|
|
||||||
|
Test Site and Live Demonstration
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
You may find a working example in the `test site`_ in the
|
||||||
|
`source distribution`_. It is the simplest website that works with
|
||||||
|
*Mia! Accounting*. It is used in the automatic tests. It is the same
|
||||||
|
code run for `live demonstration`_.
|
||||||
|
|
||||||
|
If you do not have a running Flask application, you may start with the
|
||||||
|
test site.
|
||||||
|
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Refer to the `documentation on Read the Docs`_.
|
||||||
|
|
||||||
|
|
||||||
|
.. _Flask: https://flask.palletsprojects.com
|
||||||
|
.. _double-entry bookkeeping: https://en.wikipedia.org/wiki/Double-entry_bookkeeping
|
||||||
|
.. _live demonstration: https://accounting.imacat.idv.tw
|
||||||
|
.. _PyPI project page: https://pypi.org/project/mia-accounting
|
||||||
|
.. _release page: https://github.com/imacat/mia-accounting/releases
|
||||||
|
.. _Git repository: https://github.com/imacat/mia-accounting
|
||||||
|
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
|
||||||
|
.. _Bootstrap: https://getbootstrap.com
|
||||||
|
.. _FontAwesome: https://fontawesome.com
|
||||||
|
.. _Decimal.js: https://mikemcl.github.io/decimal.js
|
||||||
|
.. _Tempus-Dominus: https://getdatepicker.com
|
||||||
|
.. _flask_sqlalchemy.SQLAlchemy.create_all: https://flask-sqlalchemy.palletsprojects.com/en/3.0.x/api/#flask_sqlalchemy.SQLAlchemy.create_all
|
||||||
|
.. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/
|
||||||
|
.. _test site: https://github.com/imacat/mia-accounting/tree/main/tests/test_site
|
||||||
|
.. _source distribution: https://pypi.org/project/mia-accounting/#files
|
||||||
|
.. _documentation on Read the Docs: https://mia-accounting.readthedocs.io
|
@ -1,7 +1,7 @@
|
|||||||
# 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-2023 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -15,6 +15,51 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "mia-accounting"
|
||||||
|
version = "1.0.1"
|
||||||
|
description = "A Flask accounting module."
|
||||||
|
readme = "README.rst"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
authors = [
|
||||||
|
{name = "imacat", email = "imacat@mail.imacat.idv.tw"},
|
||||||
|
]
|
||||||
|
keywords = ["mia", "accounting", "flask"]
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Framework :: Flask",
|
||||||
|
"Topic :: Office/Business :: Financial :: Accounting",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"flask",
|
||||||
|
"Flask-SQLAlchemy",
|
||||||
|
"Flask-WTF",
|
||||||
|
"Flask-Babel >= 3",
|
||||||
|
"Flask-Babel-JS",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
test = [
|
||||||
|
"unittest",
|
||||||
|
"httpx",
|
||||||
|
"OpenCC",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
"Documentation" = "https://mia-accounting.readthedocs.io"
|
||||||
|
"Repository" = "https://github.com/imacat/mia-accounting"
|
||||||
|
"Bug Tracker" = "https://github.com/imacat/mia-accounting/issues"
|
||||||
|
"Demonstration" = "https://accounting.imacat.idv.tw"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=42"]
|
requires = ["setuptools>=42"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools.exclude-package-data]
|
||||||
|
"*" = [
|
||||||
|
"babel.cfg",
|
||||||
|
"*.pot",
|
||||||
|
"*.po",
|
||||||
|
]
|
||||||
|
56
setup.cfg
56
setup.cfg
@ -1,56 +0,0 @@
|
|||||||
# The Mia! Flask Project.
|
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
|
|
||||||
|
|
||||||
# Copyright (c) 2022-2023 imacat.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
[metadata]
|
|
||||||
name = mia-accounting-flask
|
|
||||||
version = 0.7.0
|
|
||||||
author = imacat
|
|
||||||
author_email = imacat@mail.imacat.idv.tw
|
|
||||||
description = The Mia! Accounting Flask project.
|
|
||||||
long_description = file: README.rst
|
|
||||||
long_description_content_type = text/x-rst
|
|
||||||
url = https://github.com/imacat/mia-accounting-flask
|
|
||||||
project_urls =
|
|
||||||
Bug Tracker = https://github.com/imacat/mia-accounting-flask/issues
|
|
||||||
classifiers =
|
|
||||||
Programming Language :: Python :: 3
|
|
||||||
License :: OSI Approved :: Apache Software License
|
|
||||||
Operating System :: OS Independent
|
|
||||||
Framework :: Flask
|
|
||||||
Topic :: Office/Business :: Financial :: Accounting
|
|
||||||
|
|
||||||
[options]
|
|
||||||
package_dir =
|
|
||||||
= src
|
|
||||||
python_requires = >=3.11
|
|
||||||
install_requires =
|
|
||||||
flask
|
|
||||||
Flask-SQLAlchemy
|
|
||||||
Flask-WTF
|
|
||||||
Flask-Babel >= 3
|
|
||||||
Flask-Babel-JS
|
|
||||||
tests_require =
|
|
||||||
unittest
|
|
||||||
httpx
|
|
||||||
OpenCC
|
|
||||||
|
|
||||||
[options.package_data]
|
|
||||||
accounting =
|
|
||||||
static/**
|
|
||||||
templates/**
|
|
||||||
translations/*/LC_MESSAGES/*.mo
|
|
||||||
data/**
|
|
@ -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.
|
||||||
@ -137,6 +137,7 @@ class AccountCollector:
|
|||||||
.join(JournalEntry).join(Account)\
|
.join(JournalEntry).join(Account)\
|
||||||
.filter(*conditions)\
|
.filter(*conditions)\
|
||||||
.group_by(Account.id, Account.base_code, Account.no)\
|
.group_by(Account.id, Account.base_code, Account.no)\
|
||||||
|
.having(balance_func != 0)\
|
||||||
.order_by(Account.base_code, Account.no)
|
.order_by(Account.base_code, Account.no)
|
||||||
account_balances: list[sa.Row] \
|
account_balances: list[sa.Row] \
|
||||||
= db.session.execute(select_balance).all()
|
= db.session.execute(select_balance).all()
|
||||||
|
@ -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.
|
||||||
@ -269,6 +269,7 @@ class IncomeStatement(BaseReport):
|
|||||||
.join(JournalEntry).join(Account)\
|
.join(JournalEntry).join(Account)\
|
||||||
.filter(*conditions)\
|
.filter(*conditions)\
|
||||||
.group_by(Account.id)\
|
.group_by(Account.id)\
|
||||||
|
.having(balance_func != 0)\
|
||||||
.order_by(Account.base_code, Account.no)
|
.order_by(Account.base_code, Account.no)
|
||||||
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
||||||
accounts: dict[int, Account] \
|
accounts: dict[int, Account] \
|
||||||
|
@ -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.
|
||||||
@ -191,6 +191,7 @@ class TrialBalance(BaseReport):
|
|||||||
.join(JournalEntry).join(Account)\
|
.join(JournalEntry).join(Account)\
|
||||||
.filter(*conditions)\
|
.filter(*conditions)\
|
||||||
.group_by(Account.id)\
|
.group_by(Account.id)\
|
||||||
|
.having(balance_func != 0)\
|
||||||
.order_by(Account.base_code, Account.no)
|
.order_by(Account.base_code, Account.no)
|
||||||
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
||||||
accounts: dict[int, Account] \
|
accounts: dict[int, Account] \
|
||||||
|
@ -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.
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user