Compare commits
247 Commits
v0.7.0
...
1224d6f83e
Author | SHA1 | Date | |
---|---|---|---|
1224d6f83e | |||
3a8618f7c3 | |||
5d87205659 | |||
04de4f5c5e | |||
f8ea863b80 | |||
5ae0d03b32 | |||
a9a3ad5871 | |||
5edc95afce | |||
943ace6fc7 | |||
a63bc977e9 | |||
dabe6ddbca | |||
f47e9b3150 | |||
bb5383febe | |||
87f9063ceb | |||
51f0185bcf | |||
7ca08d6cc8 | |||
c8e9e562be | |||
ba43bd7e90 | |||
4e550413ba | |||
59a3cbb472 | |||
d1b64d069e | |||
d823d3254f | |||
5e9a2fb0c3 | |||
3f2e659ba5 | |||
9f7bb6b9de | |||
6857164702 | |||
6bac76be64 | |||
370d2668e5 | |||
5e3e695e62 | |||
510d369e9c | |||
b65cae9252 | |||
285c12406b | |||
df240472a4 | |||
1218b224fc | |||
79689ac0e5 | |||
1660e66766 | |||
12d00c9c7d | |||
428018e4a9 | |||
a8f318b0bb | |||
a3507494e5 | |||
3aa6c8d6f6 | |||
052b62cdd4 | |||
3728a4037d | |||
6eee17d44f | |||
e5cc2b5a2f | |||
ac3b5523b1 | |||
5af6fd9619 | |||
71a20cba29 | |||
4a4cf1ea40 | |||
e9824808ec | |||
c984d2d596 | |||
720e77c814 | |||
0f0412827d | |||
3a0e978f76 | |||
8c10d42d7b | |||
04ec51afbe | |||
fe7a8842ce | |||
66daa5c42c | |||
27fb44937d | |||
7026ed3a65 | |||
fdd3e93778 | |||
def7559457 | |||
7905820d68 | |||
7ae332c975 | |||
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/*
|
|
||||||
|
191
README.rst
191
README.rst
@ -1,24 +1,176 @@
|
|||||||
=====================
|
===============
|
||||||
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.
|
||||||
|
|
||||||
|
|
||||||
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`_.
|
||||||
|
|
||||||
|
|
||||||
|
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 or do not know how to
|
||||||
|
start one, you may start with the test site.
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
=============
|
||||||
|
|
||||||
|
Refer to the `documentation on Read the Docs`_.
|
||||||
|
|
||||||
|
|
||||||
Copyright
|
Copyright
|
||||||
@ -46,5 +198,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
|
||||||
|
.. _test site: https://github.com/imacat/mia-accounting/tree/main/tests/test_site
|
||||||
|
.. _source distribution: https://pypi.org/project/mia-accounting/#files
|
||||||
|
.. _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/
|
||||||
|
.. _documentation on Read the Docs: https://mia-accounting.readthedocs.io
|
||||||
|
@ -20,14 +20,6 @@ accounting.journal\_entry.utils.description\_editor module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.journal\_entry.utils.offset\_alias module
|
|
||||||
----------------------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: accounting.journal_entry.utils.offset_alias
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
accounting.journal\_entry.utils.operators module
|
accounting.journal\_entry.utils.operators module
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
|
|
||||||
|
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:
|
@ -60,6 +60,22 @@ accounting.report.reports.trial\_balance module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.report.reports.unapplied module
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.report.reports.unapplied
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.report.reports.unapplied\_accounts module
|
||||||
|
----------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.report.reports.unapplied_accounts
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Module contents
|
Module contents
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
@ -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
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
|
|
||||||
@ -60,6 +52,14 @@ accounting.report.utils.report\_type module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.report.utils.unapplied module
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.report.utils.unapplied
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.report.utils.urls module
|
accounting.report.utils.urls module
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|
@ -11,12 +11,22 @@ Subpackages
|
|||||||
accounting.base_account
|
accounting.base_account
|
||||||
accounting.currency
|
accounting.currency
|
||||||
accounting.journal_entry
|
accounting.journal_entry
|
||||||
|
accounting.option
|
||||||
accounting.report
|
accounting.report
|
||||||
|
accounting.unmatched_offset
|
||||||
accounting.utils
|
accounting.utils
|
||||||
|
|
||||||
Submodules
|
Submodules
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
accounting.forms module
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.forms
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.locale module
|
accounting.locale module
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
|
29
docs/source/accounting.unmatched_offset.rst
Normal file
29
docs/source/accounting.unmatched_offset.rst
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
accounting.unmatched\_offset package
|
||||||
|
====================================
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
accounting.unmatched\_offset.queries module
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.unmatched_offset.queries
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.unmatched\_offset.views module
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.unmatched_offset.views
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.unmatched_offset
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
@ -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,30 @@ accounting.utils.next\_uri module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.utils.offset\_alias module
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.utils.offset_alias
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.utils.offset\_matcher module
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.utils.offset_matcher
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.utils.options module
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.utils.options
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.utils.pagination module
|
accounting.utils.pagination module
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
@ -76,6 +108,14 @@ accounting.utils.strip\_text module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.utils.unapplied module
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.utils.unapplied
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.utils.user module
|
accounting.utils.user 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.1.0'
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
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
|
||||||
|
136
docs/source/intro.rst
Normal file
136
docs/source/intro.rst
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
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`_.
|
||||||
|
|
||||||
|
|
||||||
|
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 or do not know how to
|
||||||
|
start one, you may start with the test site.
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
.. _test site: https://github.com/imacat/mia-accounting/tree/main/tests/test_site
|
||||||
|
.. _source distribution: https://pypi.org/project/mia-accounting/#files
|
||||||
|
.. _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/
|
||||||
|
.. _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.1.0"
|
||||||
|
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,12 @@ 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)
|
||||||
|
|
||||||
|
from . import unmatched_offset
|
||||||
|
unmatched_offset.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.
|
||||||
@ -108,15 +108,15 @@ def __is_need_offset(base_code: str) -> bool:
|
|||||||
"""
|
"""
|
||||||
# Assets
|
# Assets
|
||||||
if base_code[0] == "1":
|
if base_code[0] == "1":
|
||||||
if base_code[:3] in {"113", "114", "118", "184"}:
|
if base_code[:3] in {"113", "114", "118", "184", "186"}:
|
||||||
return True
|
return True
|
||||||
if base_code in {"1411", "1421", "1431", "1441", "1511", "1521",
|
if base_code in {"1286", "1411", "1421", "1431", "1441", "1511",
|
||||||
"1581", "1611", "1851", ""}:
|
"1521", "1581", "1611", "1851"}:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
# Liabilities
|
# Liabilities
|
||||||
if base_code[0] == "2":
|
if base_code[0] == "2":
|
||||||
if base_code in {"2111", "2114", "2284", "2293"}:
|
if base_code in {"2111", "2114", "2284", "2293", "2861"}:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
# Only assets and liabilities need offset
|
# Only assets and liabilities need offset
|
||||||
|
@ -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.
|
||||||
@ -53,7 +53,7 @@ def list_accounts() -> str:
|
|||||||
list=pagination.list, pagination=pagination)
|
list=pagination.list, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/create", endpoint="create")
|
@bp.get("create", endpoint="create")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def show_add_account_form() -> str:
|
def show_add_account_form() -> str:
|
||||||
"""Shows the form to add an account.
|
"""Shows the form to add an account.
|
||||||
@ -70,7 +70,7 @@ def show_add_account_form() -> str:
|
|||||||
form=form)
|
form=form)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/store", endpoint="store")
|
@bp.post("store", endpoint="store")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def add_account() -> redirect:
|
def add_account() -> redirect:
|
||||||
"""Adds an account.
|
"""Adds an account.
|
||||||
@ -91,7 +91,7 @@ def add_account() -> redirect:
|
|||||||
return redirect(inherit_next(__get_detail_uri(account)))
|
return redirect(inherit_next(__get_detail_uri(account)))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<account:account>", endpoint="detail")
|
@bp.get("<account:account>", endpoint="detail")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def show_account_detail(account: Account) -> str:
|
def show_account_detail(account: Account) -> str:
|
||||||
"""Shows the account detail.
|
"""Shows the account detail.
|
||||||
@ -102,7 +102,7 @@ def show_account_detail(account: Account) -> str:
|
|||||||
return render_template("accounting/account/detail.html", obj=account)
|
return render_template("accounting/account/detail.html", obj=account)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<account:account>/edit", endpoint="edit")
|
@bp.get("<account:account>/edit", endpoint="edit")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def show_account_edit_form(account: Account) -> str:
|
def show_account_edit_form(account: Account) -> str:
|
||||||
"""Shows the form to edit an account.
|
"""Shows the form to edit an account.
|
||||||
@ -121,7 +121,7 @@ def show_account_edit_form(account: Account) -> str:
|
|||||||
account=account, form=form)
|
account=account, form=form)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<account:account>/update", endpoint="update")
|
@bp.post("<account:account>/update", endpoint="update")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def update_account(account: Account) -> redirect:
|
def update_account(account: Account) -> redirect:
|
||||||
"""Updates an account.
|
"""Updates an account.
|
||||||
@ -148,7 +148,7 @@ def update_account(account: Account) -> redirect:
|
|||||||
return redirect(inherit_next(__get_detail_uri(account)))
|
return redirect(inherit_next(__get_detail_uri(account)))
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<account:account>/delete", endpoint="delete")
|
@bp.post("<account:account>/delete", endpoint="delete")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def delete_account(account: Account) -> redirect:
|
def delete_account(account: Account) -> redirect:
|
||||||
"""Deletes an account.
|
"""Deletes an account.
|
||||||
@ -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()
|
||||||
@ -164,7 +167,7 @@ def delete_account(account: Account) -> redirect:
|
|||||||
return redirect(or_next(__get_list_uri()))
|
return redirect(or_next(__get_list_uri()))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/bases/<baseAccount:base>", endpoint="order")
|
@bp.get("bases/<baseAccount:base>", endpoint="order")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def show_account_order(base: BaseAccount) -> str:
|
def show_account_order(base: BaseAccount) -> str:
|
||||||
"""Shows the order of the accounts under a same base account.
|
"""Shows the order of the accounts under a same base account.
|
||||||
@ -175,7 +178,7 @@ def show_account_order(base: BaseAccount) -> str:
|
|||||||
return render_template("accounting/account/order.html", base=base)
|
return render_template("accounting/account/order.html", base=base)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/bases/<baseAccount:base>", endpoint="sort")
|
@bp.post("bases/<baseAccount:base>", endpoint="sort")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def sort_accounts(base: BaseAccount) -> redirect:
|
def sort_accounts(base: BaseAccount) -> redirect:
|
||||||
"""Reorders the accounts under a base account.
|
"""Reorders the accounts under a base account.
|
||||||
|
@ -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.
|
||||||
@ -22,6 +22,7 @@ from flask import Blueprint, render_template
|
|||||||
from accounting.models import BaseAccount
|
from accounting.models import BaseAccount
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
from accounting.utils.permission import has_permission, can_view
|
from accounting.utils.permission import has_permission, can_view
|
||||||
|
from .queries import get_base_account_query
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("base-account", __name__)
|
bp: Blueprint = Blueprint("base-account", __name__)
|
||||||
"""The view blueprint for the base account management."""
|
"""The view blueprint for the base account management."""
|
||||||
@ -34,14 +35,13 @@ def list_accounts() -> str:
|
|||||||
|
|
||||||
:return: The account list.
|
:return: The account list.
|
||||||
"""
|
"""
|
||||||
from .queries import get_base_account_query
|
|
||||||
accounts: list[BaseAccount] = get_base_account_query()
|
accounts: list[BaseAccount] = get_base_account_query()
|
||||||
pagination: Pagination = Pagination[BaseAccount](accounts)
|
pagination: Pagination = Pagination[BaseAccount](accounts)
|
||||||
return render_template("accounting/base-account/list.html",
|
return render_template("accounting/base-account/list.html",
|
||||||
list=pagination.list, pagination=pagination)
|
list=pagination.list, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<baseAccount:account>", endpoint="detail")
|
@bp.get("<baseAccount:account>", endpoint="detail")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def show_account_detail(account: BaseAccount) -> str:
|
def show_account_detail(account: BaseAccount) -> str:
|
||||||
"""Shows the account detail.
|
"""Shows the account detail.
|
||||||
|
@ -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.
|
||||||
@ -34,6 +34,7 @@ from accounting.utils.pagination import Pagination
|
|||||||
from accounting.utils.permission import has_permission, can_view, can_edit
|
from accounting.utils.permission import has_permission, can_view, can_edit
|
||||||
from accounting.utils.user import get_current_user_pk
|
from accounting.utils.user import get_current_user_pk
|
||||||
from .forms import CurrencyForm
|
from .forms import CurrencyForm
|
||||||
|
from .queries import get_currency_query
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("currency", __name__)
|
bp: Blueprint = Blueprint("currency", __name__)
|
||||||
"""The view blueprint for the currency management."""
|
"""The view blueprint for the currency management."""
|
||||||
@ -48,14 +49,13 @@ def list_currencies() -> str:
|
|||||||
|
|
||||||
:return: The currency list.
|
:return: The currency list.
|
||||||
"""
|
"""
|
||||||
from .queries import get_currency_query
|
|
||||||
currencies: list[Currency] = get_currency_query()
|
currencies: list[Currency] = get_currency_query()
|
||||||
pagination: Pagination = Pagination[Currency](currencies)
|
pagination: Pagination = Pagination[Currency](currencies)
|
||||||
return render_template("accounting/currency/list.html",
|
return render_template("accounting/currency/list.html",
|
||||||
list=pagination.list, pagination=pagination)
|
list=pagination.list, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/create", endpoint="create")
|
@bp.get("create", endpoint="create")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def show_add_currency_form() -> str:
|
def show_add_currency_form() -> str:
|
||||||
"""Shows the form to add a currency.
|
"""Shows the form to add a currency.
|
||||||
@ -72,7 +72,7 @@ def show_add_currency_form() -> str:
|
|||||||
form=form)
|
form=form)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/store", endpoint="store")
|
@bp.post("store", endpoint="store")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def add_currency() -> redirect:
|
def add_currency() -> redirect:
|
||||||
"""Adds a currency.
|
"""Adds a currency.
|
||||||
@ -93,7 +93,7 @@ def add_currency() -> redirect:
|
|||||||
return redirect(inherit_next(__get_detail_uri(currency)))
|
return redirect(inherit_next(__get_detail_uri(currency)))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<currency:currency>", endpoint="detail")
|
@bp.get("<currency:currency>", endpoint="detail")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def show_currency_detail(currency: Currency) -> str:
|
def show_currency_detail(currency: Currency) -> str:
|
||||||
"""Shows the currency detail.
|
"""Shows the currency detail.
|
||||||
@ -104,7 +104,7 @@ def show_currency_detail(currency: Currency) -> str:
|
|||||||
return render_template("accounting/currency/detail.html", obj=currency)
|
return render_template("accounting/currency/detail.html", obj=currency)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<currency:currency>/edit", endpoint="edit")
|
@bp.get("<currency:currency>/edit", endpoint="edit")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def show_currency_edit_form(currency: Currency) -> str:
|
def show_currency_edit_form(currency: Currency) -> str:
|
||||||
"""Shows the form to edit a currency.
|
"""Shows the form to edit a currency.
|
||||||
@ -123,7 +123,7 @@ def show_currency_edit_form(currency: Currency) -> str:
|
|||||||
currency=currency, form=form)
|
currency=currency, form=form)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<currency:currency>/update", endpoint="update")
|
@bp.post("<currency:currency>/update", endpoint="update")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def update_currency(currency: Currency) -> redirect:
|
def update_currency(currency: Currency) -> redirect:
|
||||||
"""Updates a currency.
|
"""Updates a currency.
|
||||||
@ -151,7 +151,7 @@ def update_currency(currency: Currency) -> redirect:
|
|||||||
return redirect(inherit_next(__get_detail_uri(currency)))
|
return redirect(inherit_next(__get_detail_uri(currency)))
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<currency:currency>/delete", endpoint="delete")
|
@bp.post("<currency:currency>/delete", endpoint="delete")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def delete_currency(currency: Currency) -> redirect:
|
def delete_currency(currency: Currency) -> redirect:
|
||||||
"""Deletes a currency.
|
"""Deletes a currency.
|
||||||
@ -160,13 +160,16 @@ 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")
|
||||||
return redirect(or_next(url_for("accounting.currency.list")))
|
return redirect(or_next(url_for("accounting.currency.list")))
|
||||||
|
|
||||||
|
|
||||||
@api_bp.get("/exists-code", endpoint="exists")
|
@api_bp.get("exists-code", endpoint="exists")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def exists_code() -> dict[str, bool]:
|
def exists_code() -> dict[str, bool]:
|
||||||
"""Validates whether a currency code exists.
|
"""Validates whether a currency code exists.
|
||||||
|
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.
|
||||||
@ -23,6 +23,7 @@ from flask import abort
|
|||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
|
from accounting import db
|
||||||
from accounting.models import JournalEntry, JournalEntryLineItem
|
from accounting.models import JournalEntry, JournalEntryLineItem
|
||||||
from accounting.utils.journal_entry_types import JournalEntryType
|
from accounting.utils.journal_entry_types import JournalEntryType
|
||||||
|
|
||||||
@ -37,13 +38,7 @@ class JournalEntryConverter(BaseConverter):
|
|||||||
:param value: The journal entry ID.
|
:param value: The journal entry ID.
|
||||||
:return: The corresponding journal entry.
|
:return: The corresponding journal entry.
|
||||||
"""
|
"""
|
||||||
journal_entry: JournalEntry | None = JournalEntry.query\
|
journal_entry: JournalEntry | None = db.session.get(JournalEntry, value)
|
||||||
.join(JournalEntryLineItem)\
|
|
||||||
.filter(JournalEntry.id == value)\
|
|
||||||
.options(selectinload(JournalEntry.line_items)
|
|
||||||
.selectinload(JournalEntryLineItem.offsets)
|
|
||||||
.selectinload(JournalEntryLineItem.journal_entry))\
|
|
||||||
.first()
|
|
||||||
if journal_entry is None:
|
if journal_entry is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
return journal_entry
|
return journal_entry
|
||||||
|
@ -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,10 +27,11 @@ 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.forms import CurrencyExists
|
||||||
from accounting.locale import lazy_gettext
|
from accounting.locale import lazy_gettext
|
||||||
from accounting.models import Currency, JournalEntryLineItem
|
from accounting.models import JournalEntryLineItem
|
||||||
from accounting.journal_entry.utils.offset_alias import offset_alias
|
|
||||||
from accounting.utils.cast import be
|
from accounting.utils.cast import be
|
||||||
|
from accounting.utils.offset_alias import offset_alias
|
||||||
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
|
||||||
|
|
||||||
@ -39,17 +40,6 @@ CURRENCY_REQUIRED: DataRequired = DataRequired(
|
|||||||
"""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,21 +25,19 @@ 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, JournalEntry, JournalEntryLineItem
|
||||||
from accounting.template_filters import format_amount
|
from accounting.template_filters import format_amount
|
||||||
from accounting.utils.cast import be
|
from accounting.utils.cast import be
|
||||||
from accounting.utils.random_id import new_id
|
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."""
|
||||||
@ -169,10 +127,8 @@ class KeepAccountWhenHavingOffset:
|
|||||||
assert isinstance(form, LineItemForm)
|
assert isinstance(form, LineItemForm)
|
||||||
if field.data is None or form.id.data is None:
|
if field.data is None or form.id.data is None:
|
||||||
return
|
return
|
||||||
line_item: JournalEntryLineItem | None = db.session\
|
line_item: JournalEntryLineItem | None \
|
||||||
.query(JournalEntryLineItem)\
|
= db.session.get(JournalEntryLineItem, form.id.data)
|
||||||
.filter(JournalEntryLineItem.id == form.id.data)\
|
|
||||||
.options(selectinload(JournalEntryLineItem.offsets)).first()
|
|
||||||
if line_item is None or len(line_item.offsets) == 0:
|
if line_item is None or len(line_item.offsets) == 0:
|
||||||
return
|
return
|
||||||
if field.data != line_item.account_code:
|
if field.data != line_item.account_code:
|
||||||
@ -386,14 +342,13 @@ class LineItemForm(FlaskForm):
|
|||||||
def get_offsets() -> list[JournalEntryLineItem]:
|
def get_offsets() -> list[JournalEntryLineItem]:
|
||||||
if not self.is_need_offset or self.id.data is None:
|
if not self.is_need_offset or self.id.data is None:
|
||||||
return []
|
return []
|
||||||
return JournalEntryLineItem.query\
|
return JournalEntryLineItem.query.join(JournalEntry)\
|
||||||
.filter(JournalEntryLineItem.original_line_item_id
|
.filter(JournalEntryLineItem.original_line_item_id
|
||||||
== self.id.data)\
|
== self.id.data)\
|
||||||
|
.order_by(JournalEntry.date, JournalEntry.no,
|
||||||
|
JournalEntryLineItem.no)\
|
||||||
.options(selectinload(JournalEntryLineItem.journal_entry),
|
.options(selectinload(JournalEntryLineItem.journal_entry),
|
||||||
selectinload(JournalEntryLineItem.account),
|
selectinload(JournalEntryLineItem.account)).all()
|
||||||
selectinload(JournalEntryLineItem.offsets)
|
|
||||||
.selectinload(
|
|
||||||
JournalEntryLineItem.journal_entry)).all()
|
|
||||||
setattr(self, "__offsets", get_offsets())
|
setattr(self, "__offsets", get_offsets())
|
||||||
return getattr(self, "__offsets")
|
return getattr(self, "__offsets")
|
||||||
|
|
||||||
@ -452,12 +407,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 +459,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/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.
|
||||||
@ -25,7 +25,7 @@ from sqlalchemy.orm import selectinload
|
|||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.models import Account, JournalEntry, JournalEntryLineItem
|
from accounting.models import Account, JournalEntry, JournalEntryLineItem
|
||||||
from accounting.utils.cast import be
|
from accounting.utils.cast import be
|
||||||
from .offset_alias import offset_alias
|
from accounting.utils.offset_alias import offset_alias
|
||||||
|
|
||||||
|
|
||||||
def get_selectable_original_line_items(
|
def get_selectable_original_line_items(
|
||||||
@ -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.
|
||||||
@ -49,7 +49,7 @@ bp.add_app_template_filter(format_amount_input,
|
|||||||
bp.add_app_template_filter(text2html, "accounting_journal_entry_text2html")
|
bp.add_app_template_filter(text2html, "accounting_journal_entry_text2html")
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/create/<journalEntryType:journal_entry_type>", endpoint="create")
|
@bp.get("create/<journalEntryType:journal_entry_type>", endpoint="create")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
|
def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
|
||||||
"""Shows the form to add a journal entry.
|
"""Shows the form to add a journal entry.
|
||||||
@ -71,7 +71,7 @@ def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
|
|||||||
return journal_entry_op.render_create_template(form)
|
return journal_entry_op.render_create_template(form)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/store/<journalEntryType:journal_entry_type>", endpoint="store")
|
@bp.post("store/<journalEntryType:journal_entry_type>", endpoint="store")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
|
def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
|
||||||
"""Adds a journal entry.
|
"""Adds a journal entry.
|
||||||
@ -98,7 +98,7 @@ def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
|
|||||||
return redirect(inherit_next(__get_detail_uri(journal_entry)))
|
return redirect(inherit_next(__get_detail_uri(journal_entry)))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<journalEntry:journal_entry>", endpoint="detail")
|
@bp.get("<journalEntry:journal_entry>", endpoint="detail")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def show_journal_entry_detail(journal_entry: JournalEntry) -> str:
|
def show_journal_entry_detail(journal_entry: JournalEntry) -> str:
|
||||||
"""Shows the journal entry detail.
|
"""Shows the journal entry detail.
|
||||||
@ -111,7 +111,7 @@ def show_journal_entry_detail(journal_entry: JournalEntry) -> str:
|
|||||||
return journal_entry_op.render_detail_template(journal_entry)
|
return journal_entry_op.render_detail_template(journal_entry)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<journalEntry:journal_entry>/edit", endpoint="edit")
|
@bp.get("<journalEntry:journal_entry>/edit", endpoint="edit")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
|
def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
|
||||||
"""Shows the form to edit a journal entry.
|
"""Shows the form to edit a journal entry.
|
||||||
@ -133,7 +133,7 @@ def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
|
|||||||
return journal_entry_op.render_edit_template(journal_entry, form)
|
return journal_entry_op.render_edit_template(journal_entry, form)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<journalEntry:journal_entry>/update", endpoint="update")
|
@bp.post("<journalEntry:journal_entry>/update", endpoint="update")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def update_journal_entry(journal_entry: JournalEntry) -> redirect:
|
def update_journal_entry(journal_entry: JournalEntry) -> redirect:
|
||||||
"""Updates a journal entry.
|
"""Updates a journal entry.
|
||||||
@ -166,7 +166,7 @@ def update_journal_entry(journal_entry: JournalEntry) -> redirect:
|
|||||||
return redirect(inherit_next(__get_detail_uri(journal_entry)))
|
return redirect(inherit_next(__get_detail_uri(journal_entry)))
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<journalEntry:journal_entry>/delete", endpoint="delete")
|
@bp.post("<journalEntry:journal_entry>/delete", endpoint="delete")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
|
def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
|
||||||
"""Deletes a journal entry.
|
"""Deletes a journal entry.
|
||||||
@ -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()
|
||||||
@ -183,7 +186,7 @@ def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
|
|||||||
return redirect(or_next(__get_default_page_uri()))
|
return redirect(or_next(__get_default_page_uri()))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/dates/<date:journal_entry_date>", endpoint="order")
|
@bp.get("dates/<date:journal_entry_date>", endpoint="order")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def show_journal_entry_order(journal_entry_date: date) -> str:
|
def show_journal_entry_order(journal_entry_date: date) -> str:
|
||||||
"""Shows the order of the journal entries in a same date.
|
"""Shows the order of the journal entries in a same date.
|
||||||
@ -198,7 +201,7 @@ def show_journal_entry_order(journal_entry_date: date) -> str:
|
|||||||
date=journal_entry_date, list=journal_entries)
|
date=journal_entry_date, list=journal_entries)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/dates/<date:journal_entry_date>", endpoint="sort")
|
@bp.post("dates/<date:journal_entry_date>", endpoint="sort")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def sort_journal_entries(journal_entry_date: date) -> redirect:
|
def sort_journal_entries(journal_entry_date: date) -> redirect:
|
||||||
"""Reorders the journal entries in a date.
|
"""Reorders the journal entries in a date.
|
||||||
@ -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:
|
||||||
@ -215,6 +214,25 @@ class Account(db.Model):
|
|||||||
"""
|
"""
|
||||||
return not self.is_real
|
return not self.is_real
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count(self) -> int:
|
||||||
|
"""Returns the number of items in the account.
|
||||||
|
|
||||||
|
:return: The number of items in the account.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "__count"):
|
||||||
|
setattr(self, "__count", 0)
|
||||||
|
return getattr(self, "__count")
|
||||||
|
|
||||||
|
@count.setter
|
||||||
|
def count(self, count: int) -> None:
|
||||||
|
"""Sets the number of items in the account.
|
||||||
|
|
||||||
|
:param count: The number of items in the account.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
setattr(self, "__count", count)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def query_values(self) -> list[str]:
|
def query_values(self) -> list[str]:
|
||||||
"""Returns the values to be queried.
|
"""Returns the values to be queried.
|
||||||
@ -236,6 +254,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 +287,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 +310,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 +413,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 +431,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 +454,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 +640,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.
|
||||||
@ -640,12 +679,8 @@ class JournalEntryLineItem(db.Model):
|
|||||||
nullable=True)
|
nullable=True)
|
||||||
"""The ID of the original line item."""
|
"""The ID of the original line item."""
|
||||||
original_line_item = db.relationship("JournalEntryLineItem",
|
original_line_item = db.relationship("JournalEntryLineItem",
|
||||||
back_populates="offsets",
|
|
||||||
remote_side=id, passive_deletes=True)
|
remote_side=id, passive_deletes=True)
|
||||||
"""The original line item."""
|
"""The original line item."""
|
||||||
offsets = db.relationship("JournalEntryLineItem",
|
|
||||||
back_populates="original_line_item")
|
|
||||||
"""The offset items."""
|
|
||||||
currency_code = db.Column(db.String,
|
currency_code = db.Column(db.String,
|
||||||
db.ForeignKey(Currency.code, onupdate="CASCADE"),
|
db.ForeignKey(Currency.code, onupdate="CASCADE"),
|
||||||
nullable=False)
|
nullable=False)
|
||||||
@ -687,14 +722,6 @@ class JournalEntryLineItem(db.Model):
|
|||||||
"""
|
"""
|
||||||
return self.account.code
|
return self.account.code
|
||||||
|
|
||||||
@property
|
|
||||||
def debit(self) -> Decimal | None:
|
|
||||||
"""Returns the debit amount.
|
|
||||||
|
|
||||||
:return: The debit amount, or None if this is not a debit line item.
|
|
||||||
"""
|
|
||||||
return self.amount if self.is_debit else None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_need_offset(self) -> bool:
|
def is_need_offset(self) -> bool:
|
||||||
"""Returns whether the line item needs offset.
|
"""Returns whether the line item needs offset.
|
||||||
@ -709,6 +736,14 @@ class JournalEntryLineItem(db.Model):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def debit(self) -> Decimal | None:
|
||||||
|
"""Returns the debit amount.
|
||||||
|
|
||||||
|
:return: The debit amount, or None if this is not a debit line item.
|
||||||
|
"""
|
||||||
|
return self.amount if self.is_debit else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def credit(self) -> Decimal | None:
|
def credit(self) -> Decimal | None:
|
||||||
"""Returns the credit amount.
|
"""Returns the credit amount.
|
||||||
@ -739,7 +774,41 @@ 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 offsets(self) -> list[t.Self]:
|
||||||
|
"""Returns the offset items.
|
||||||
|
|
||||||
|
:return: The offset items.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "__offsets"):
|
||||||
|
cls: t.Type[t.Self] = self.__class__
|
||||||
|
offsets: list[t.Self] = cls.query.join(JournalEntry)\
|
||||||
|
.filter(JournalEntryLineItem.original_line_item_id == self.id)\
|
||||||
|
.order_by(JournalEntry.date, JournalEntry.no,
|
||||||
|
cls.is_debit, cls.no).all()
|
||||||
|
setattr(self, "__offsets", offsets)
|
||||||
|
return getattr(self, "__offsets")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def match(self) -> t.Self | None:
|
||||||
|
"""Returns the match of the line item.
|
||||||
|
|
||||||
|
:return: The match of the line item.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "__match"):
|
||||||
|
setattr(self, "__match", None)
|
||||||
|
return getattr(self, "__match")
|
||||||
|
|
||||||
|
@match.setter
|
||||||
|
def match(self, match: t.Self) -> None:
|
||||||
|
"""Sets the match of the line item.
|
||||||
|
|
||||||
|
:param match: The matcho of the line item.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
setattr(self, "__match", match)
|
||||||
|
|
||||||
|
@property
|
||||||
|
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 +818,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,19 +17,21 @@
|
|||||||
"""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, CurrentAccountConverter, \
|
||||||
|
NeedOffsetAccountConverter
|
||||||
app.url_map.converters["period"] = PeriodConverter
|
app.url_map.converters["period"] = PeriodConverter
|
||||||
app.url_map.converters["ieAccount"] = IncomeExpensesAccountConverter
|
app.url_map.converters["currentAccount"] = CurrentAccountConverter
|
||||||
|
app.url_map.converters["needOffsetAccount"] = NeedOffsetAccountConverter
|
||||||
|
|
||||||
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,13 +23,13 @@ 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):
|
||||||
"""The supplier converter to convert the period specification from and to
|
"""The converter to convert the period specification from and to the
|
||||||
the corresponding period in the routes."""
|
corresponding period in the routes."""
|
||||||
|
|
||||||
def to_python(self, value: str) -> Period:
|
def to_python(self, value: str) -> Period:
|
||||||
"""Converts a period specification to a period.
|
"""Converts a period specification to a period.
|
||||||
@ -51,26 +51,52 @@ class PeriodConverter(BaseConverter):
|
|||||||
return value.spec
|
return value.spec
|
||||||
|
|
||||||
|
|
||||||
class IncomeExpensesAccountConverter(BaseConverter):
|
class CurrentAccountConverter(BaseConverter):
|
||||||
"""The supplier converter to convert the income and expenses log pseudo
|
"""The converter to convert the current account code from and to the
|
||||||
account code from and to the corresponding pseudo account in the routes."""
|
corresponding 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.
|
||||||
|
|
||||||
|
:param value: The account.
|
||||||
|
:return: Its code.
|
||||||
|
"""
|
||||||
|
return value.code
|
||||||
|
|
||||||
|
|
||||||
|
class NeedOffsetAccountConverter(BaseConverter):
|
||||||
|
"""The converter to convert the unapplied original line item account code
|
||||||
|
from and to the corresponding account in the routes."""
|
||||||
|
|
||||||
|
def to_python(self, value: str) -> Account:
|
||||||
|
"""Converts an account code to an account.
|
||||||
|
|
||||||
|
:param value: The account code.
|
||||||
|
:return: The corresponding account.
|
||||||
|
"""
|
||||||
|
account: Account | None = Account.find_by_code(value)
|
||||||
|
if account is None:
|
||||||
|
abort(404)
|
||||||
|
if not account.is_need_offset:
|
||||||
|
abort(404)
|
||||||
|
return account
|
||||||
|
|
||||||
|
def to_url(self, value: Account) -> str:
|
||||||
"""Converts an account to account code.
|
"""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.
|
||||||
@ -77,6 +77,8 @@ class CSVRow(BaseCSVRow):
|
|||||||
"""Constructs a row in the CSV.
|
"""Constructs a row in the CSV.
|
||||||
|
|
||||||
:param journal_entry_date: The journal entry date.
|
:param journal_entry_date: The journal entry date.
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
:param description: The description.
|
:param description: The description.
|
||||||
:param debit: The debit amount.
|
:param debit: The debit amount.
|
||||||
:param credit: The credit amount.
|
:param credit: The credit amount.
|
||||||
@ -116,6 +118,7 @@ class PageParams(BasePageParams):
|
|||||||
"""Constructs the HTML page parameters.
|
"""Constructs the HTML page parameters.
|
||||||
|
|
||||||
:param period: The period.
|
:param period: The period.
|
||||||
|
:param pagination: The pagination.
|
||||||
:param line_items: The line items.
|
:param line_items: The line items.
|
||||||
"""
|
"""
|
||||||
self.period: Period = period
|
self.period: Period = period
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# The Mia! Accounting Flask Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023 imacat.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# The Mia! Accounting Flask Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/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] \
|
||||||
|
185
src/accounting/report/reports/unapplied.py
Normal file
185
src/accounting/report/reports/unapplied.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/7
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The unapplied original line items.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import render_template, Response
|
||||||
|
|
||||||
|
from accounting.locale import gettext
|
||||||
|
from accounting.models import Account, JournalEntryLineItem
|
||||||
|
from accounting.report.utils.base_page_params import BasePageParams
|
||||||
|
from accounting.report.utils.base_report import BaseReport
|
||||||
|
from accounting.report.utils.csv_export import BaseCSVRow, csv_download
|
||||||
|
from accounting.report.utils.option_link import OptionLink
|
||||||
|
from accounting.report.utils.report_chooser import ReportChooser
|
||||||
|
from accounting.report.utils.report_type import ReportType
|
||||||
|
from accounting.report.utils.unapplied import get_accounts_with_unapplied
|
||||||
|
from accounting.report.utils.urls import unapplied_url
|
||||||
|
from accounting.utils.offset_matcher import OffsetMatcher
|
||||||
|
from accounting.utils.pagination import Pagination
|
||||||
|
from accounting.utils.permission import can_edit
|
||||||
|
|
||||||
|
|
||||||
|
class CSVRow(BaseCSVRow):
|
||||||
|
"""A row in the CSV."""
|
||||||
|
|
||||||
|
def __init__(self, journal_entry_date: str | date, currency: str,
|
||||||
|
description: str | None, amount: str | Decimal,
|
||||||
|
net_balance: str | Decimal):
|
||||||
|
"""Constructs a row in the CSV.
|
||||||
|
|
||||||
|
:param journal_entry_date: The journal entry date.
|
||||||
|
:param currency: The currency.
|
||||||
|
:param description: The description.
|
||||||
|
:param amount: The amount.
|
||||||
|
:param net_balance: The net balance.
|
||||||
|
"""
|
||||||
|
self.date: str | date = journal_entry_date
|
||||||
|
"""The date."""
|
||||||
|
self.currency: str = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.description: str | None = description
|
||||||
|
"""The description."""
|
||||||
|
self.amount: str | Decimal = amount
|
||||||
|
"""The amount."""
|
||||||
|
self.net_balance: str | Decimal = net_balance
|
||||||
|
"""The net balance."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def values(self) -> list[str | date | Decimal | None]:
|
||||||
|
"""Returns the values of the row.
|
||||||
|
|
||||||
|
:return: The values of the row.
|
||||||
|
"""
|
||||||
|
return [self.date, self.currency, self.description, self.amount,
|
||||||
|
self.net_balance]
|
||||||
|
|
||||||
|
|
||||||
|
class PageParams(BasePageParams):
|
||||||
|
"""The HTML page parameters."""
|
||||||
|
|
||||||
|
def __init__(self, account: Account,
|
||||||
|
is_mark_matches: bool,
|
||||||
|
pagination: Pagination[JournalEntryLineItem],
|
||||||
|
line_items: list[JournalEntryLineItem]):
|
||||||
|
"""Constructs the HTML page parameters.
|
||||||
|
|
||||||
|
:param account: The account.
|
||||||
|
:param is_mark_matches: Whether to mark the matched offsets.
|
||||||
|
:param pagination: The pagination.
|
||||||
|
:param line_items: The line items.
|
||||||
|
"""
|
||||||
|
self.account: Account = account
|
||||||
|
"""The account."""
|
||||||
|
self.pagination: Pagination[JournalEntryLineItem] = pagination
|
||||||
|
"""The pagination."""
|
||||||
|
self.line_items: list[JournalEntryLineItem] = line_items
|
||||||
|
"""The line items."""
|
||||||
|
self.is_mark_matches: bool = is_mark_matches
|
||||||
|
"""Whether to mark the matched offsets."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_data(self) -> bool:
|
||||||
|
"""Returns whether there is any data on the page.
|
||||||
|
|
||||||
|
:return: True if there is any data, or False otherwise.
|
||||||
|
"""
|
||||||
|
return len(self.line_items) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def report_chooser(self) -> ReportChooser:
|
||||||
|
"""Returns the report chooser.
|
||||||
|
|
||||||
|
:return: The report chooser.
|
||||||
|
"""
|
||||||
|
return ReportChooser(ReportType.UNAPPLIED,
|
||||||
|
account=self.account)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_options(self) -> list[OptionLink]:
|
||||||
|
"""Returns the account options.
|
||||||
|
|
||||||
|
:return: The account options.
|
||||||
|
"""
|
||||||
|
options: list[OptionLink] = [OptionLink(gettext("Accounts"),
|
||||||
|
unapplied_url(None),
|
||||||
|
False)]
|
||||||
|
options.extend([OptionLink(str(x),
|
||||||
|
unapplied_url(x),
|
||||||
|
x.id == self.account.id)
|
||||||
|
for x in get_accounts_with_unapplied()])
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:
|
||||||
|
"""Composes and returns the CSV rows from the line items.
|
||||||
|
|
||||||
|
:param line_items: The line items.
|
||||||
|
:return: The CSV rows.
|
||||||
|
"""
|
||||||
|
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
|
||||||
|
gettext("Description"), gettext("Amount"),
|
||||||
|
gettext("Net Balance"))]
|
||||||
|
rows.extend([CSVRow(x.journal_entry.date, x.currency.code,
|
||||||
|
x.description, x.amount, x.net_balance)
|
||||||
|
for x in line_items])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
class UnappliedOriginalLineItems(BaseReport):
|
||||||
|
"""The unapplied original line items."""
|
||||||
|
|
||||||
|
def __init__(self, account: Account):
|
||||||
|
"""Constructs the unapplied original line items.
|
||||||
|
|
||||||
|
:param account: The account.
|
||||||
|
"""
|
||||||
|
self.__account: Account = account
|
||||||
|
"""The account."""
|
||||||
|
offset_matcher: OffsetMatcher = OffsetMatcher(self.__account)
|
||||||
|
self.__line_items: list[JournalEntryLineItem] \
|
||||||
|
= offset_matcher.unapplied
|
||||||
|
"""The line items."""
|
||||||
|
self.__is_mark_matches: bool \
|
||||||
|
= can_edit() and len(offset_matcher.unmatched_offsets) > 0
|
||||||
|
"""Whether to mark the matched offsets."""
|
||||||
|
|
||||||
|
def csv(self) -> Response:
|
||||||
|
"""Returns the report as CSV for download.
|
||||||
|
|
||||||
|
:return: The response of the report for download.
|
||||||
|
"""
|
||||||
|
filename: str = f"unapplied-{self.__account.code}.csv"
|
||||||
|
return csv_download(filename, get_csv_rows(self.__line_items))
|
||||||
|
|
||||||
|
def html(self) -> str:
|
||||||
|
"""Composes and returns the report as HTML.
|
||||||
|
|
||||||
|
:return: The report as HTML.
|
||||||
|
"""
|
||||||
|
pagination: Pagination[JournalEntryLineItem] \
|
||||||
|
= Pagination[JournalEntryLineItem](self.__line_items,
|
||||||
|
is_reversed=True)
|
||||||
|
params: PageParams = PageParams(account=self.__account,
|
||||||
|
is_mark_matches=self.__is_mark_matches,
|
||||||
|
pagination=pagination,
|
||||||
|
line_items=pagination.list)
|
||||||
|
return render_template("accounting/report/unapplied.html",
|
||||||
|
report=params)
|
137
src/accounting/report/reports/unapplied_accounts.py
Normal file
137
src/accounting/report/reports/unapplied_accounts.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/7
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The accounts with unapplied original line items.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import render_template, Response
|
||||||
|
|
||||||
|
from accounting.locale import gettext
|
||||||
|
from accounting.models import Account
|
||||||
|
from accounting.report.utils.base_page_params import BasePageParams
|
||||||
|
from accounting.report.utils.base_report import BaseReport
|
||||||
|
from accounting.report.utils.csv_export import BaseCSVRow, csv_download
|
||||||
|
from accounting.report.utils.option_link import OptionLink
|
||||||
|
from accounting.report.utils.report_chooser import ReportChooser
|
||||||
|
from accounting.report.utils.report_type import ReportType
|
||||||
|
from accounting.report.utils.unapplied import get_accounts_with_unapplied
|
||||||
|
from accounting.report.utils.urls import unapplied_url
|
||||||
|
|
||||||
|
|
||||||
|
class CSVRow(BaseCSVRow):
|
||||||
|
"""A row in the CSV."""
|
||||||
|
|
||||||
|
def __init__(self, account: str, count: int | str):
|
||||||
|
"""Constructs a row in the CSV.
|
||||||
|
|
||||||
|
:param account: The account.
|
||||||
|
:param count: The number of unapplied original line items.
|
||||||
|
"""
|
||||||
|
self.account: str = account
|
||||||
|
"""The currency."""
|
||||||
|
self.count: int | str = count
|
||||||
|
"""The number of unapplied original line items."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def values(self) -> list[str | date | Decimal | None]:
|
||||||
|
"""Returns the values of the row.
|
||||||
|
|
||||||
|
:return: The values of the row.
|
||||||
|
"""
|
||||||
|
return [self.account, self.count]
|
||||||
|
|
||||||
|
|
||||||
|
class PageParams(BasePageParams):
|
||||||
|
"""The HTML page parameters."""
|
||||||
|
|
||||||
|
def __init__(self, accounts: list[Account]):
|
||||||
|
"""Constructs the HTML page parameters.
|
||||||
|
|
||||||
|
:param accounts: The accounts.
|
||||||
|
"""
|
||||||
|
self.accounts: list[Account] = accounts
|
||||||
|
"""The accounts."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_data(self) -> bool:
|
||||||
|
"""Returns whether there is any data on the page.
|
||||||
|
|
||||||
|
:return: True if there is any data, or False otherwise.
|
||||||
|
"""
|
||||||
|
return len(self.accounts) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def report_chooser(self) -> ReportChooser:
|
||||||
|
"""Returns the report chooser.
|
||||||
|
|
||||||
|
:return: The report chooser.
|
||||||
|
"""
|
||||||
|
return ReportChooser(ReportType.UNAPPLIED)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_options(self) -> list[OptionLink]:
|
||||||
|
"""Returns the account options.
|
||||||
|
|
||||||
|
:return: The account options.
|
||||||
|
"""
|
||||||
|
options: list[OptionLink] = [OptionLink(gettext("Accounts"),
|
||||||
|
unapplied_url(None),
|
||||||
|
True)]
|
||||||
|
options.extend([OptionLink(str(x),
|
||||||
|
unapplied_url(x),
|
||||||
|
False)
|
||||||
|
for x in self.accounts])
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def get_csv_rows(accounts: list[Account]) -> list[CSVRow]:
|
||||||
|
"""Composes and returns the CSV rows from the line items.
|
||||||
|
|
||||||
|
:param accounts: The accounts.
|
||||||
|
:return: The CSV rows.
|
||||||
|
"""
|
||||||
|
rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Count"))]
|
||||||
|
rows.extend([CSVRow(str(x).title(), x.count)
|
||||||
|
for x in accounts])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsWithUnappliedOriginalLineItems(BaseReport):
|
||||||
|
"""The accounts with unapplied original line items."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Constructs the outstanding balances."""
|
||||||
|
self.__accounts: list[Account] = get_accounts_with_unapplied()
|
||||||
|
"""The accounts."""
|
||||||
|
|
||||||
|
def csv(self) -> Response:
|
||||||
|
"""Returns the report as CSV for download.
|
||||||
|
|
||||||
|
:return: The response of the report for download.
|
||||||
|
"""
|
||||||
|
filename: str = f"unapplied-accounts.csv"
|
||||||
|
return csv_download(filename, get_csv_rows(self.__accounts))
|
||||||
|
|
||||||
|
def html(self) -> str:
|
||||||
|
"""Composes and returns the report as HTML.
|
||||||
|
|
||||||
|
:return: The report as HTML.
|
||||||
|
"""
|
||||||
|
return render_template("accounting/report/unapplied-accounts.html",
|
||||||
|
report=PageParams(accounts=self.__accounts))
|
@ -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.
|
||||||
@ -22,6 +22,7 @@ from abc import ABC, abstractmethod
|
|||||||
from datetime import timedelta, date
|
from datetime import timedelta, date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from flask import Response
|
from flask import Response
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ def csv_download(filename: str, rows: list[BaseCSVRow]) -> Response:
|
|||||||
fp.seek(0)
|
fp.seek(0)
|
||||||
response: Response = Response(fp.read(), mimetype="text/csv")
|
response: Response = Response(fp.read(), mimetype="text/csv")
|
||||||
response.headers["Content-Disposition"] \
|
response.headers["Content-Disposition"] \
|
||||||
= f"attachment; filename={filename}"
|
= f"attachment; filename={quote(filename)}"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,11 +30,11 @@ 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, \
|
||||||
trial_balance_url, income_statement_url, balance_sheet_url
|
trial_balance_url, income_statement_url, balance_sheet_url, unapplied_url
|
||||||
|
|
||||||
|
|
||||||
class ReportChooser:
|
class ReportChooser:
|
||||||
@ -68,12 +68,13 @@ 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)
|
||||||
|
self.__reports.append(self.__unapplied)
|
||||||
for report in self.__reports:
|
for report in self.__reports:
|
||||||
if report.is_active:
|
if report.is_active:
|
||||||
self.current_report = report.title
|
self.current_report = report.title
|
||||||
@ -81,14 +82,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 +110,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:
|
||||||
@ -151,6 +152,23 @@ class ReportChooser:
|
|||||||
self.__active_report == ReportType.BALANCE_SHEET,
|
self.__active_report == ReportType.BALANCE_SHEET,
|
||||||
fa_icon="fa-solid fa-scale-balanced")
|
fa_icon="fa-solid fa-scale-balanced")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __unapplied(self) -> OptionLink:
|
||||||
|
"""Returns the unapplied original line items.
|
||||||
|
|
||||||
|
:return: The unapplied original line items.
|
||||||
|
"""
|
||||||
|
account: Account = self.__account
|
||||||
|
if not account.is_need_offset:
|
||||||
|
return OptionLink(gettext("Unapplied Original Line Items"),
|
||||||
|
unapplied_url(None),
|
||||||
|
self.__active_report == ReportType.UNAPPLIED,
|
||||||
|
fa_icon="fa-solid fa-link-slash")
|
||||||
|
return OptionLink(gettext("Unapplied Original Line Items"),
|
||||||
|
unapplied_url(account),
|
||||||
|
self.__active_report == ReportType.UNAPPLIED,
|
||||||
|
fa_icon="fa-solid fa-link-slash")
|
||||||
|
|
||||||
def __iter__(self) -> t.Iterator[OptionLink]:
|
def __iter__(self) -> t.Iterator[OptionLink]:
|
||||||
"""Returns the iteration of the reports.
|
"""Returns the iteration of the reports.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
@ -34,5 +34,7 @@ class ReportType(Enum):
|
|||||||
"""The income statement."""
|
"""The income statement."""
|
||||||
BALANCE_SHEET: str = "balance-sheet"
|
BALANCE_SHEET: str = "balance-sheet"
|
||||||
"""The balance sheet."""
|
"""The balance sheet."""
|
||||||
|
UNAPPLIED: str = "unapplied"
|
||||||
|
"""The unapplied original line items."""
|
||||||
SEARCH: str = "search"
|
SEARCH: str = "search"
|
||||||
"""The search."""
|
"""The search."""
|
||||||
|
67
src/accounting/report/utils/unapplied.py
Normal file
67
src/accounting/report/utils/unapplied.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/7
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The unapplied original line item utilities.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from accounting import db
|
||||||
|
from accounting.models import Account, JournalEntryLineItem
|
||||||
|
from accounting.utils.cast import be
|
||||||
|
from accounting.utils.offset_alias import offset_alias
|
||||||
|
|
||||||
|
|
||||||
|
def get_accounts_with_unapplied() -> list[Account]:
|
||||||
|
"""Returns the accounts with unapplied original line items.
|
||||||
|
|
||||||
|
:return: The accounts with unapplied original line items.
|
||||||
|
"""
|
||||||
|
offset: sa.Alias = offset_alias()
|
||||||
|
net_balance: sa.Label \
|
||||||
|
= (JournalEntryLineItem.amount
|
||||||
|
+ sa.func.sum(sa.case(
|
||||||
|
(be(offset.c.is_debit == JournalEntryLineItem.is_debit),
|
||||||
|
offset.c.amount),
|
||||||
|
else_=-offset.c.amount))).label("net_balance")
|
||||||
|
select_unapplied: sa.Select \
|
||||||
|
= sa.select(JournalEntryLineItem.id)\
|
||||||
|
.join(Account)\
|
||||||
|
.join(offset, be(JournalEntryLineItem.id
|
||||||
|
== offset.c.original_line_item_id),
|
||||||
|
isouter=True)\
|
||||||
|
.filter(Account.is_need_offset,
|
||||||
|
sa.or_(sa.and_(Account.base_code.startswith("2"),
|
||||||
|
sa.not_(JournalEntryLineItem.is_debit)),
|
||||||
|
sa.and_(Account.base_code.startswith("1"),
|
||||||
|
JournalEntryLineItem.is_debit)))\
|
||||||
|
.group_by(JournalEntryLineItem.id)\
|
||||||
|
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
|
||||||
|
|
||||||
|
count_func: sa.Label \
|
||||||
|
= sa.func.count(JournalEntryLineItem.id).label("count")
|
||||||
|
select: sa.Select = sa.select(Account.id, count_func)\
|
||||||
|
.join(JournalEntryLineItem, isouter=True)\
|
||||||
|
.filter(JournalEntryLineItem.id.in_(select_unapplied))\
|
||||||
|
.group_by(Account.id)\
|
||||||
|
.having(count_func > 0)
|
||||||
|
counts: dict[int, int] \
|
||||||
|
= {x.id: x.count for x in db.session.execute(select)}
|
||||||
|
accounts: list[Account] = Account.query.filter(Account.id.in_(counts))\
|
||||||
|
.order_by(Account.base_code, Account.no).all()
|
||||||
|
for account in accounts:
|
||||||
|
account.count = counts[account.id]
|
||||||
|
return accounts
|
@ -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) \
|
||||||
@ -46,15 +47,16 @@ def ledger_url(currency: Currency, account: Account, period: Period) \
|
|||||||
:param period: The period.
|
:param period: The period.
|
||||||
:return: The URL of the ledger.
|
:return: The URL of the ledger.
|
||||||
"""
|
"""
|
||||||
if period.is_default:
|
if currency.code == default_currency_code() \
|
||||||
return url_for("accounting.report.ledger-default",
|
and account.code == Account.CASH_CODE \
|
||||||
currency=currency, account=account)
|
and period.is_default:
|
||||||
return url_for("accounting.report.ledger",
|
return url_for("accounting-report.ledger-default")
|
||||||
|
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 +66,10 @@ 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:
|
return url_for("accounting-report.income-expenses",
|
||||||
return url_for("accounting.report.income-expenses-default",
|
|
||||||
currency=currency, account=account)
|
|
||||||
return url_for("accounting.report.income-expenses",
|
|
||||||
currency=currency, account=account,
|
currency=currency, account=account,
|
||||||
period=period)
|
period=period)
|
||||||
|
|
||||||
@ -82,10 +81,9 @@ def trial_balance_url(currency: Currency, period: Period) -> str:
|
|||||||
:param period: The period.
|
:param period: The period.
|
||||||
:return: The URL of the trial balance.
|
:return: The URL of the trial balance.
|
||||||
"""
|
"""
|
||||||
if period.is_default:
|
if currency.code == default_currency_code() and period.is_default:
|
||||||
return url_for("accounting.report.trial-balance-default",
|
return url_for("accounting-report.trial-balance-default")
|
||||||
currency=currency)
|
return url_for("accounting-report.trial-balance",
|
||||||
return url_for("accounting.report.trial-balance",
|
|
||||||
currency=currency, period=period)
|
currency=currency, period=period)
|
||||||
|
|
||||||
|
|
||||||
@ -96,10 +94,9 @@ def income_statement_url(currency: Currency, period: Period) -> str:
|
|||||||
:param period: The period.
|
:param period: The period.
|
||||||
:return: The URL of the income statement.
|
:return: The URL of the income statement.
|
||||||
"""
|
"""
|
||||||
if period.is_default:
|
if currency.code == default_currency_code() and period.is_default:
|
||||||
return url_for("accounting.report.income-statement-default",
|
return url_for("accounting-report.income-statement-default")
|
||||||
currency=currency)
|
return url_for("accounting-report.income-statement",
|
||||||
return url_for("accounting.report.income-statement",
|
|
||||||
currency=currency, period=period)
|
currency=currency, period=period)
|
||||||
|
|
||||||
|
|
||||||
@ -110,8 +107,19 @@ def balance_sheet_url(currency: Currency, period: Period) -> str:
|
|||||||
:param period: The period.
|
:param period: The period.
|
||||||
:return: The URL of the balance sheet.
|
:return: The URL of the balance sheet.
|
||||||
"""
|
"""
|
||||||
if period.is_default:
|
if currency.code == default_currency_code() and period.is_default:
|
||||||
return url_for("accounting.report.balance-sheet-default",
|
return url_for("accounting-report.balance-sheet-default")
|
||||||
currency=currency)
|
return url_for("accounting-report.balance-sheet",
|
||||||
return url_for("accounting.report.balance-sheet",
|
|
||||||
currency=currency, period=period)
|
currency=currency, period=period)
|
||||||
|
|
||||||
|
|
||||||
|
def unapplied_url(account: Account | None) -> str:
|
||||||
|
"""Returns the URL of the unapplied original line items.
|
||||||
|
|
||||||
|
:param account: The account, or None to list the accounts with unapplied
|
||||||
|
original line items.
|
||||||
|
:return: The URL of the unapplied original line items.
|
||||||
|
"""
|
||||||
|
if account is None:
|
||||||
|
return url_for("accounting-report.unapplied-default")
|
||||||
|
return url_for("accounting-report.unapplied", account=account)
|
||||||
|
@ -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,16 @@ 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 .reports.unapplied import UnappliedOriginalLineItems
|
||||||
|
from .reports.unapplied_accounts import AccountsWithUnappliedOriginalLineItems
|
||||||
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")
|
||||||
|
|
||||||
@ -41,10 +44,7 @@ def get_default_report() -> str | Response:
|
|||||||
|
|
||||||
:return: The income and expenses log in the default period.
|
:return: The income and expenses log in the default period.
|
||||||
"""
|
"""
|
||||||
return __get_income_expenses(
|
return get_default_income_expenses()
|
||||||
db.session.get(Currency, default_currency_code()),
|
|
||||||
default_ie_account(),
|
|
||||||
get_period())
|
|
||||||
|
|
||||||
|
|
||||||
@bp.get("journal", endpoint="journal-default")
|
@bp.get("journal", endpoint="journal-default")
|
||||||
@ -80,17 +80,15 @@ def __get_journal(period: Period) -> str | Response:
|
|||||||
return report.html()
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
@bp.get("ledger/<currency:currency>/<account:account>",
|
@bp.get("ledger", endpoint="ledger-default")
|
||||||
endpoint="ledger-default")
|
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def get_default_ledger(currency: Currency, account: Account) -> str | Response:
|
def get_default_ledger() -> str | Response:
|
||||||
"""Returns the ledger in the default period.
|
"""Returns the ledger in the default currency, cash, and default period.
|
||||||
|
|
||||||
:param currency: The currency.
|
:return: The ledger in the default currency, cash, and default period.
|
||||||
:param account: The account.
|
|
||||||
:return: The ledger in the default period.
|
|
||||||
"""
|
"""
|
||||||
return __get_ledger(currency, account, get_period())
|
return __get_ledger(db.session.get(Currency, default_currency_code()),
|
||||||
|
Account.cash(), get_period())
|
||||||
|
|
||||||
|
|
||||||
@bp.get("ledger/<currency:currency>/<account:account>/<period:period>",
|
@bp.get("ledger/<currency:currency>/<account:account>/<period:period>",
|
||||||
@ -123,26 +121,23 @@ def __get_ledger(currency: Currency, account: Account, period: Period) \
|
|||||||
return report.html()
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
@bp.get("income-expenses/<currency:currency>/<ieAccount:account>",
|
@bp.get("income-expenses", 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() -> str | Response:
|
||||||
account: IncomeExpensesAccount) \
|
|
||||||
-> str | Response:
|
|
||||||
"""Returns the income and expenses log in the default period.
|
"""Returns the income and expenses log in the default period.
|
||||||
|
|
||||||
:param currency: The currency.
|
|
||||||
:param account: The account.
|
|
||||||
:return: The income and expenses log in the default period.
|
:return: The income and expenses log in the default period.
|
||||||
"""
|
"""
|
||||||
return __get_income_expenses(currency, account, get_period())
|
return __get_income_expenses(
|
||||||
|
db.session.get(Currency, default_currency_code()),
|
||||||
|
options.default_ie_account,
|
||||||
|
get_period())
|
||||||
|
|
||||||
|
|
||||||
@bp.get(
|
@bp.get("income-expenses/<currency:currency>/<currentAccount:account>/"
|
||||||
"income-expenses/<currency:currency>/<ieAccount:account>/<period:period>",
|
"<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 +149,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.
|
||||||
|
|
||||||
@ -169,16 +164,15 @@ def __get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
|
|||||||
return report.html()
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
@bp.get("trial-balance/<currency:currency>",
|
@bp.get("trial-balance", endpoint="trial-balance-default")
|
||||||
endpoint="trial-balance-default")
|
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def get_default_trial_balance(currency: Currency) -> str | Response:
|
def get_default_trial_balance() -> str | Response:
|
||||||
"""Returns the trial balance in the default period.
|
"""Returns the trial balance in the default period.
|
||||||
|
|
||||||
:param currency: The currency.
|
|
||||||
:return: The trial balance in the default period.
|
:return: The trial balance in the default period.
|
||||||
"""
|
"""
|
||||||
return __get_trial_balance(currency, get_period())
|
return __get_trial_balance(
|
||||||
|
db.session.get(Currency, default_currency_code()), get_period())
|
||||||
|
|
||||||
|
|
||||||
@bp.get("trial-balance/<currency:currency>/<period:period>",
|
@bp.get("trial-balance/<currency:currency>/<period:period>",
|
||||||
@ -207,16 +201,15 @@ def __get_trial_balance(currency: Currency, period: Period) -> str | Response:
|
|||||||
return report.html()
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
@bp.get("income-statement/<currency:currency>",
|
@bp.get("income-statement", endpoint="income-statement-default")
|
||||||
endpoint="income-statement-default")
|
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def get_default_income_statement(currency: Currency) -> str | Response:
|
def get_default_income_statement() -> str | Response:
|
||||||
"""Returns the income statement in the default period.
|
"""Returns the income statement in the default period.
|
||||||
|
|
||||||
:param currency: The currency.
|
|
||||||
:return: The income statement in the default period.
|
:return: The income statement in the default period.
|
||||||
"""
|
"""
|
||||||
return __get_income_statement(currency, get_period())
|
return __get_income_statement(
|
||||||
|
db.session.get(Currency, default_currency_code()), get_period())
|
||||||
|
|
||||||
|
|
||||||
@bp.get("income-statement/<currency:currency>/<period:period>",
|
@bp.get("income-statement/<currency:currency>/<period:period>",
|
||||||
@ -246,16 +239,15 @@ def __get_income_statement(currency: Currency, period: Period) \
|
|||||||
return report.html()
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
@bp.get("balance-sheet/<currency:currency>",
|
@bp.get("balance-sheet", endpoint="balance-sheet-default")
|
||||||
endpoint="balance-sheet-default")
|
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def get_default_balance_sheet(currency: Currency) -> str | Response:
|
def get_default_balance_sheet() -> str | Response:
|
||||||
"""Returns the balance sheet in the default period.
|
"""Returns the balance sheet in the default period.
|
||||||
|
|
||||||
:param currency: The currency.
|
|
||||||
:return: The balance sheet in the default period.
|
:return: The balance sheet in the default period.
|
||||||
"""
|
"""
|
||||||
return __get_balance_sheet(currency, get_period())
|
return __get_balance_sheet(
|
||||||
|
db.session.get(Currency, default_currency_code()), get_period())
|
||||||
|
|
||||||
|
|
||||||
@bp.get("balance-sheet/<currency:currency>/<period:period>",
|
@bp.get("balance-sheet/<currency:currency>/<period:period>",
|
||||||
@ -286,6 +278,34 @@ def __get_balance_sheet(currency: Currency, period: Period) \
|
|||||||
return report.html()
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("unapplied", endpoint="unapplied-default")
|
||||||
|
@has_permission(can_view)
|
||||||
|
def get_default_unapplied() -> str | Response:
|
||||||
|
"""Returns the accounts with unapplied original line items.
|
||||||
|
|
||||||
|
:return: The accounts with unapplied original line items.
|
||||||
|
"""
|
||||||
|
report: AccountsWithUnappliedOriginalLineItems \
|
||||||
|
= AccountsWithUnappliedOriginalLineItems()
|
||||||
|
if "as" in request.args and request.args["as"] == "csv":
|
||||||
|
return report.csv()
|
||||||
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("unapplied/<needOffsetAccount:account>", endpoint="unapplied")
|
||||||
|
@has_permission(can_view)
|
||||||
|
def get_unapplied(account: Account) -> str | Response:
|
||||||
|
"""Returns the unapplied original line items.
|
||||||
|
|
||||||
|
:param account: The Account.
|
||||||
|
:return: The unapplied original line items.
|
||||||
|
"""
|
||||||
|
report: UnappliedOriginalLineItems = UnappliedOriginalLineItems(account)
|
||||||
|
if "as" in request.args and request.args["as"] == "csv":
|
||||||
|
return report.csv()
|
||||||
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
@bp.get("search", endpoint="search")
|
@bp.get("search", endpoint="search")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def search() -> str | Response:
|
def search() -> str | Response:
|
||||||
|
@ -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;
|
||||||
@ -209,11 +209,23 @@ a.accounting-report-table-row {
|
|||||||
.accounting-report-table-body .accounting-amount {
|
.accounting-report-table-body .accounting-amount {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
.accounting-report-table-body .accounting-report-table-row {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
.accounting-report-table-body .accounting-report-table-row:nth-child(2n+1) {
|
.accounting-report-table-body .accounting-report-table-row:nth-child(2n+1) {
|
||||||
background-color: #f2f2f2;
|
background-color: #ecedee;
|
||||||
}
|
}
|
||||||
.accounting-report-table-body .accounting-report-table-row:hover {
|
.accounting-report-table-body .accounting-report-table-row:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.075);
|
background-color: #e5e6e7;
|
||||||
|
}
|
||||||
|
.accounting-report-table-body .accounting-report-table-row-danger {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
}
|
||||||
|
.accounting-report-table-body .accounting-report-table-row-danger:nth-child(2n+1) {
|
||||||
|
background-color: #eccccf;
|
||||||
|
}
|
||||||
|
.accounting-report-table-body .accounting-report-table-row-danger:hover {
|
||||||
|
background-color: #e5c7ca;
|
||||||
}
|
}
|
||||||
.accounting-journal-table .accounting-report-table-row {
|
.accounting-journal-table .accounting-report-table-row {
|
||||||
grid-template-columns: 1fr 1fr 2fr 4fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 2fr 4fr 1fr 1fr;
|
||||||
@ -309,12 +321,56 @@ a.accounting-report-table-row {
|
|||||||
.accounting-balance-sheet-total .accounting-amount, .accounting-balance-sheet-subtotal, .accounting-amount {
|
.accounting-balance-sheet-total .accounting-amount, .accounting-balance-sheet-subtotal, .accounting-amount {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
.accounting-unapplied-table .accounting-report-table-row {
|
||||||
|
grid-template-columns: 1fr 1fr 5fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
.accounting-unapplied-account-table .accounting-report-table-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.accounting-unapplied-account-table .accounting-report-table-header .accounting-report-table-row {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
/* The accounting report */
|
/* The accounting report */
|
||||||
.accounting-mobile-journal-credit {
|
.accounting-mobile-journal-credit {
|
||||||
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 unmatched offsets */
|
||||||
|
.accounting-unmatched-offset-pair-list {
|
||||||
|
height: 20rem;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
/* 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 +395,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.#isShowMore) {
|
||||||
|
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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user