Compare commits
63 Commits
d5a9e1af18
...
v1.0.1
Author | SHA1 | Date | |
---|---|---|---|
86c5b91697 | |||
9168840e64 | |||
21b9cfa8b8 | |||
b0b3b3acb1 | |||
cb1d254cf0 | |||
eb9ad57e72 | |||
ec26f8ef4d | |||
7ed29115ed | |||
95955197ac | |||
d5a0f79e4b | |||
6aa655aa64 | |||
6e532af26e | |||
fa1818d124 | |||
f21ecc2aa9 | |||
5ae1ab95ae | |||
7a5b3b78fc | |||
7df4051452 | |||
85084c68fd | |||
0185c16654 | |||
7dd007f3cf | |||
38b8a028d5 | |||
213981a8b2 | |||
a4d1789b58 | |||
91620d7db2 | |||
02fcabb0ce | |||
4c2dcc5070 | |||
c9166fda4d | |||
3a0f0873e2 | |||
a17395b43e | |||
17c8d9d1a9 | |||
fa94cd407e | |||
9a704c8185 | |||
8286c0c6d8 | |||
f7efacad75 | |||
9263ae0274 | |||
78a9d7794c | |||
f3ae37a409 | |||
ddc1081252 | |||
202d51a032 | |||
562bc47be7 | |||
f3d43a66cc | |||
c3fc6d9a87 | |||
e1a0380628 | |||
f2a2fcdd32 | |||
ab29166f1e | |||
8033921181 | |||
08732c1e66 | |||
4adc464d3d | |||
2f9d2e36cb | |||
5bb10bf6ba | |||
06e7b6ddff | |||
20e1982984 | |||
a70720be50 | |||
cb6de08152 | |||
211821b4d7 | |||
0faca49540 | |||
14e79df571 | |||
04fbb725d2 | |||
a1d6844e52 | |||
94391b02a6 | |||
1cb8a7563e | |||
63f0f28948 | |||
3431922f12 |
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
|
||||
|
||||
# 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
|
||||
|
||||
# Copyright (c) 2022-2023 imacat.
|
||||
@ -15,17 +15,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
recursive-include src/accounting/static *
|
||||
exclude src/accounting/static/js/dummy.js
|
||||
include src/accounting/translations/*
|
||||
include src/accounting/translations/*/LC_MESSAGES/*
|
||||
include docs/*
|
||||
include docs/source/*
|
||||
include docs/source/_static/*
|
||||
include docs/source/_templates/*
|
||||
include tests/*
|
||||
recursive-include src/accounting/templates *
|
||||
recursive-include src/accounting/translations *
|
||||
recursive-include src/accounting/data *
|
||||
recursive-include docs *
|
||||
recursive-exclude docs/build *
|
||||
recursive-include tests *
|
||||
exclude tests/test_temp.py
|
||||
include tests/test_site/*
|
||||
include tests/test_site/static/*
|
||||
include tests/test_site/templates/*
|
||||
include tests/test_site/translations/*
|
||||
include tests/test_site/translations/*/LC_MESSAGES/*
|
||||
recursive-exclude tests *.pyc
|
||||
recursive-exclude tests/instance *
|
||||
|
193
README.rst
193
README.rst
@ -1,24 +1,178 @@
|
||||
=====================
|
||||
Mia! Accounting Flask
|
||||
=====================
|
||||
===============
|
||||
Mia! Accounting
|
||||
===============
|
||||
|
||||
|
||||
Description
|
||||
===========
|
||||
|
||||
This is the Mia! Accounting Flask project. It is an accounting
|
||||
module for the Flask_ applications.
|
||||
*Mia! Accounting* is an accounting module for Flask_ applications.
|
||||
It implements `double-entry bookkeeping`_, and generates the following
|
||||
accounting reports:
|
||||
|
||||
* Trial balance
|
||||
* Income statement
|
||||
* Balance sheet
|
||||
|
||||
In addition, *Mia! Accounting* tracks offsets for unpaid payables and
|
||||
receivables.
|
||||
|
||||
You may try the `live demonstration`_.
|
||||
|
||||
|
||||
Install
|
||||
=======
|
||||
Installation
|
||||
============
|
||||
|
||||
Install the latest source from the
|
||||
`Mia! Accounting Flask repository`_.
|
||||
Install *Mia! Accounting* with ``pip``:
|
||||
|
||||
::
|
||||
|
||||
pip install git+https://gitea.imacat.idv.tw/imacat/mia-accounting-flask.git
|
||||
pip install mia-accounting
|
||||
|
||||
You may also download the from the `PyPI project page`_ or the
|
||||
`release page`_ on the `Git repository`_.
|
||||
|
||||
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
You need a running Flask application with database user login.
|
||||
The primary key of the user data model must be integer.
|
||||
|
||||
The following front-end JavaScript libraries must be loaded. You may
|
||||
download it locally or use CDN_.
|
||||
|
||||
* Bootstrap_ 5.2.3 or above
|
||||
* FontAwesome_ 6.2.1 or above
|
||||
* `Decimal.js`_ 6.4.3 or above
|
||||
* `Tempus-Dominus`_ 6.4.3 or above
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
You need to pass the Flask *app* and an implementation of
|
||||
`UserUtilityInterface`_ to the `init_app`_ function.
|
||||
``UserUtilityInterface`` contains everything *Mia! Accounting* needs.
|
||||
|
||||
The following is an example configuration for *Mia! Accounting*.
|
||||
|
||||
::
|
||||
|
||||
from flask import Response, redirect
|
||||
from .auth import current_user()
|
||||
from .modules import User
|
||||
|
||||
def create_app(test_config=None) -> Flask:
|
||||
app: Flask = Flask(__name__)
|
||||
|
||||
... (Configuration of SQLAlchemy, CSRF, Babel_JS, ... etc) ...
|
||||
|
||||
import accounting
|
||||
|
||||
class UserUtils(accounting.UserUtilityInterface[User]):
|
||||
|
||||
def can_view(self) -> bool:
|
||||
return True
|
||||
|
||||
def can_edit(self) -> bool:
|
||||
return "editor" in current_user().roles
|
||||
|
||||
def can_admin(self) -> bool:
|
||||
return current_user().is_admin
|
||||
|
||||
def unauthorized(self) -> Response:
|
||||
return redirect("/login")
|
||||
|
||||
@property
|
||||
def cls(self) -> t.Type[User]:
|
||||
return User
|
||||
|
||||
@property
|
||||
def pk_column(self) -> Column:
|
||||
return User.id
|
||||
|
||||
@property
|
||||
def current_user(self) -> User | None:
|
||||
return current_user()
|
||||
|
||||
def get_by_username(self, username: str) -> User | None:
|
||||
return User.query.filter(User.username == username).first()
|
||||
|
||||
def get_pk(self, user: User) -> int:
|
||||
return user.id
|
||||
|
||||
accounting.init_app(app, UserUtils())
|
||||
|
||||
... (Any other configuration) ...
|
||||
|
||||
return app
|
||||
|
||||
|
||||
Database Initialization
|
||||
=======================
|
||||
|
||||
After the configuration, you need to run
|
||||
`flask_sqlalchemy.SQLAlchemy.create_all`_ to create the
|
||||
database tables that *Mia! Accounting* uses.
|
||||
|
||||
*Mia! Accounting* adds three console commands:
|
||||
|
||||
* ``accounting-init-base``
|
||||
* ``accounting-init-accounts``
|
||||
* ``accounting-init-currencies``
|
||||
|
||||
After database tables are created, run
|
||||
``accounting-init-base`` first, and then the other two commands.
|
||||
|
||||
::
|
||||
|
||||
% flask --app myapp accounting-init-base
|
||||
% flask --app myapp accounting-init-accounts
|
||||
% flask --app myapp accounting-init-currencies
|
||||
|
||||
|
||||
Navigation Menu
|
||||
===============
|
||||
|
||||
Include the navigation menu in the `Bootstrap navigation bar`_ in your
|
||||
base template:
|
||||
|
||||
::
|
||||
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary bg-dark navbar-dark">
|
||||
<div class="container-fluid">
|
||||
...
|
||||
<div id="collapsible-navbar" class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
...
|
||||
{% include "accounting/include/nav.html" %}
|
||||
...
|
||||
</ul>
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
Check your Flask application and see how it works.
|
||||
|
||||
|
||||
Test Site and Live Demonstration
|
||||
================================
|
||||
|
||||
You may find a working example in the `test site`_ in the
|
||||
`source distribution`_. It is the simplest website that works with
|
||||
*Mia! Accounting*. It is used in the automatic tests. It is the same
|
||||
code run for `live demonstration`_.
|
||||
|
||||
If you do not have a running Flask application, you may start with the
|
||||
test site.
|
||||
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
Refer to the `documentation on Read the Docs`_.
|
||||
|
||||
|
||||
Copyright
|
||||
@ -46,5 +200,22 @@ Authors
|
||||
| imacat@mail.imacat.idv.tw
|
||||
| 2023/1/27
|
||||
|
||||
|
||||
.. _Flask: https://flask.palletsprojects.com
|
||||
.. _Mia! Accounting Flask repository: https://gitea.imacat.idv.tw/imacat/mia-accounting-flask
|
||||
.. _double-entry bookkeeping: https://en.wikipedia.org/wiki/Double-entry_bookkeeping
|
||||
.. _live demonstration: https://accounting.imacat.idv.tw
|
||||
.. _PyPI project page: https://pypi.org/project/mia-accounting
|
||||
.. _release page: https://github.com/imacat/mia-accounting/releases
|
||||
.. _Git repository: https://github.com/imacat/mia-accounting
|
||||
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
|
||||
.. _Bootstrap: https://getbootstrap.com
|
||||
.. _FontAwesome: https://fontawesome.com
|
||||
.. _Decimal.js: https://mikemcl.github.io/decimal.js
|
||||
.. _Tempus-Dominus: https://getdatepicker.com
|
||||
.. _UserUtilityInterface: https://mia-accounting.readthedocs.io/en/latest/accounting.utils.html#accounting.utils.user.UserUtilityInterface
|
||||
.. _init_app: https://mia-accounting.readthedocs.io/en/latest/accounting.html#accounting.init_app
|
||||
.. _flask_sqlalchemy.SQLAlchemy.create_all: https://flask-sqlalchemy.palletsprojects.com/en/3.0.x/api/#flask_sqlalchemy.SQLAlchemy.create_all
|
||||
.. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/
|
||||
.. _test site: https://github.com/imacat/mia-accounting/tree/main/tests/test_site
|
||||
.. _source distribution: https://pypi.org/project/mia-accounting/#files
|
||||
.. _documentation on Read the Docs: https://mia-accounting.readthedocs.io
|
||||
|
@ -10,10 +10,10 @@ sys.path.insert(0, os.path.abspath('../../src/'))
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = 'Mia! Accounting Flask'
|
||||
project = 'Mia! Accounting'
|
||||
copyright = '2023, imacat'
|
||||
author = 'imacat'
|
||||
release = '0.9.1'
|
||||
release = '1.0.1'
|
||||
|
||||
# -- 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.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to Mia! Accounting Flask's documentation!
|
||||
=================================================
|
||||
Welcome to Mia! Accounting's documentation!
|
||||
===========================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
intro
|
||||
accounting
|
||||
examples
|
||||
history
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
138
docs/source/intro.rst
Normal file
138
docs/source/intro.rst
Normal file
@ -0,0 +1,138 @@
|
||||
Introduction
|
||||
============
|
||||
|
||||
*Mia! Accounting* is an accounting module for Flask_ applications.
|
||||
It implements `double-entry bookkeeping`_, and generates the following
|
||||
accounting reports:
|
||||
|
||||
* Trial balance
|
||||
* Income statement
|
||||
* Balance sheet
|
||||
|
||||
In addition, *Mia! Accounting* tracks offsets for unpaid payables and
|
||||
receivables.
|
||||
|
||||
You may try the `live demonstration`_.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Install *Mia! Accounting* with ``pip``:
|
||||
|
||||
::
|
||||
|
||||
pip install mia-accounting
|
||||
|
||||
You may also download the from the `PyPI project page`_ or the
|
||||
`release page`_ on the `Git repository`_.
|
||||
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
You need a running Flask application with database user login.
|
||||
The primary key of the user data model must be integer.
|
||||
|
||||
The following front-end JavaScript libraries must be loaded. You may
|
||||
download it locally or use CDN_.
|
||||
|
||||
* Bootstrap_ 5.2.3 or above
|
||||
* FontAwesome_ 6.2.1 or above
|
||||
* `Decimal.js`_ 6.4.3 or above
|
||||
* `Tempus-Dominus`_ 6.4.3 or above
|
||||
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
You need to pass the Flask *app* and an implementation of
|
||||
:py:class:`accounting.utils.user.UserUtilityInterface` to the
|
||||
:py:func:`accounting.init_app` function. ``UserUtilityInterface``
|
||||
contains everything *Mia! Accounting* needs.
|
||||
|
||||
See an example in :ref:`example-userutils`.
|
||||
|
||||
|
||||
Database Initialization
|
||||
-----------------------
|
||||
|
||||
After the configuration, you need to run
|
||||
`flask_sqlalchemy.SQLAlchemy.create_all`_ to create the
|
||||
database tables that *Mia! Accounting* uses.
|
||||
|
||||
*Mia! Accounting* adds three console commands:
|
||||
|
||||
* ``accounting-init-base``
|
||||
* ``accounting-init-accounts``
|
||||
* ``accounting-init-currencies``
|
||||
|
||||
After database tables are created, run
|
||||
``accounting-init-base`` first, and then the other two commands.
|
||||
|
||||
::
|
||||
|
||||
% flask --app myapp accounting-init-base
|
||||
% flask --app myapp accounting-init-accounts
|
||||
% flask --app myapp accounting-init-currencies
|
||||
|
||||
|
||||
Navigation Menu
|
||||
---------------
|
||||
|
||||
Include the navigation menu in the `Bootstrap navigation bar`_ in your
|
||||
base template:
|
||||
|
||||
::
|
||||
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary bg-dark navbar-dark">
|
||||
<div class="container-fluid">
|
||||
...
|
||||
<div id="collapsible-navbar" class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
...
|
||||
{% include "accounting/include/nav.html" %}
|
||||
...
|
||||
</ul>
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
Check your Flask application and see how it works.
|
||||
|
||||
|
||||
Test Site and Live Demonstration
|
||||
--------------------------------
|
||||
|
||||
You may find a working example in the `test site`_ in the
|
||||
`source distribution`_. It is the simplest website that works with
|
||||
*Mia! Accounting*. It is used in the automatic tests. It is the same
|
||||
code run for `live demonstration`_.
|
||||
|
||||
If you do not have a running Flask application, you may start with the
|
||||
test site.
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Refer to the `documentation on Read the Docs`_.
|
||||
|
||||
|
||||
.. _Flask: https://flask.palletsprojects.com
|
||||
.. _double-entry bookkeeping: https://en.wikipedia.org/wiki/Double-entry_bookkeeping
|
||||
.. _live demonstration: https://accounting.imacat.idv.tw
|
||||
.. _PyPI project page: https://pypi.org/project/mia-accounting
|
||||
.. _release page: https://github.com/imacat/mia-accounting/releases
|
||||
.. _Git repository: https://github.com/imacat/mia-accounting
|
||||
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
|
||||
.. _Bootstrap: https://getbootstrap.com
|
||||
.. _FontAwesome: https://fontawesome.com
|
||||
.. _Decimal.js: https://mikemcl.github.io/decimal.js
|
||||
.. _Tempus-Dominus: https://getdatepicker.com
|
||||
.. _flask_sqlalchemy.SQLAlchemy.create_all: https://flask-sqlalchemy.palletsprojects.com/en/3.0.x/api/#flask_sqlalchemy.SQLAlchemy.create_all
|
||||
.. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/
|
||||
.. _test site: https://github.com/imacat/mia-accounting/tree/main/tests/test_site
|
||||
.. _source distribution: https://pypi.org/project/mia-accounting/#files
|
||||
.. _documentation on Read the Docs: https://mia-accounting.readthedocs.io
|
@ -1,7 +1,7 @@
|
||||
# The Mia! Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
|
||||
|
||||
# Copyright (c) 2022 imacat.
|
||||
# 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.
|
||||
@ -15,6 +15,51 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
[project]
|
||||
name = "mia-accounting"
|
||||
version = "1.0.1"
|
||||
description = "A Flask accounting module."
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.11"
|
||||
authors = [
|
||||
{name = "imacat", email = "imacat@mail.imacat.idv.tw"},
|
||||
]
|
||||
keywords = ["mia", "accounting", "flask"]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Framework :: Flask",
|
||||
"Topic :: Office/Business :: Financial :: Accounting",
|
||||
]
|
||||
dependencies = [
|
||||
"flask",
|
||||
"Flask-SQLAlchemy",
|
||||
"Flask-WTF",
|
||||
"Flask-Babel >= 3",
|
||||
"Flask-Babel-JS",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
"unittest",
|
||||
"httpx",
|
||||
"OpenCC",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Documentation" = "https://mia-accounting.readthedocs.io"
|
||||
"Repository" = "https://github.com/imacat/mia-accounting"
|
||||
"Bug Tracker" = "https://github.com/imacat/mia-accounting/issues"
|
||||
"Demonstration" = "https://accounting.imacat.idv.tw"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
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.9.1
|
||||
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
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
@ -47,7 +47,6 @@ def init_app(app: Flask, user_utils: UserUtilityInterface,
|
||||
init_user_utils(user_utils)
|
||||
|
||||
bp: Blueprint = Blueprint("accounting", __name__,
|
||||
url_prefix=url_prefix,
|
||||
template_folder="templates",
|
||||
static_folder="static")
|
||||
|
||||
@ -84,9 +83,9 @@ def init_app(app: Flask, user_utils: UserUtilityInterface,
|
||||
journal_entry.init_app(app, bp)
|
||||
|
||||
from . import report
|
||||
report.init_app(app, bp)
|
||||
report.init_app(app, url_prefix)
|
||||
|
||||
from . import option
|
||||
option.init_app(bp)
|
||||
|
||||
app.register_blueprint(bp)
|
||||
app.register_blueprint(bp, url_prefix=url_prefix)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/31
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/27
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
@ -36,12 +36,14 @@ class DescriptionAccount:
|
||||
:param account: The account.
|
||||
:param freq: The frequency of the tag with the account.
|
||||
"""
|
||||
self.account: Account = account
|
||||
self.__account: Account = account
|
||||
"""The account."""
|
||||
self.id: int = account.id
|
||||
"""The account ID."""
|
||||
self.code: str = account.code
|
||||
"""The account code."""
|
||||
self.is_need_offset: bool = account.is_need_offset
|
||||
"""Whether the journal entry line items of this account need offset."""
|
||||
self.freq: int = freq
|
||||
"""The frequency of the tag with the account."""
|
||||
|
||||
@ -50,7 +52,7 @@ class DescriptionAccount:
|
||||
|
||||
:return: The string representation of the account.
|
||||
"""
|
||||
return str(self.account)
|
||||
return str(self.__account)
|
||||
|
||||
def add_freq(self, freq: int) -> None:
|
||||
"""Adds the frequency of an account.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/15
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
@ -72,11 +72,12 @@ def get_selectable_original_line_items(
|
||||
line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query\
|
||||
.filter(JournalEntryLineItem.id.in_({x for x in net_balances}))\
|
||||
.join(JournalEntry)\
|
||||
.order_by(JournalEntry.date, JournalEntryLineItem.is_debit,
|
||||
JournalEntryLineItem.no)\
|
||||
.order_by(JournalEntry.date, JournalEntry.no,
|
||||
JournalEntryLineItem.is_debit, JournalEntryLineItem.no)\
|
||||
.options(selectinload(JournalEntryLineItem.currency),
|
||||
selectinload(JournalEntryLineItem.account),
|
||||
selectinload(JournalEntryLineItem.journal_entry)).all()
|
||||
line_items.reverse()
|
||||
for line_item in line_items:
|
||||
line_item.net_balance = line_item.amount \
|
||||
if net_balances[line_item.id] is None \
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
@ -235,4 +235,4 @@ def __get_default_page_uri() -> str:
|
||||
|
||||
:return: The URI for the default page.
|
||||
"""
|
||||
return url_for("accounting.report.default")
|
||||
return url_for("accounting-report.default")
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
@ -21,7 +21,6 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
import typing as t
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
@ -17,14 +17,14 @@
|
||||
"""The report management.
|
||||
|
||||
"""
|
||||
from flask import Flask, Blueprint
|
||||
from flask import Flask
|
||||
|
||||
|
||||
def init_app(app: Flask, bp: Blueprint) -> None:
|
||||
def init_app(app: Flask, url_prefix: str) -> None:
|
||||
"""Initialize the application.
|
||||
|
||||
:param app: The Flask application.
|
||||
:param bp: The blueprint of the accounting application.
|
||||
:param url_prefix: The URL prefix of the accounting application.
|
||||
:return: None.
|
||||
"""
|
||||
from .converters import PeriodConverter, IncomeExpensesAccountConverter
|
||||
@ -32,4 +32,4 @@ def init_app(app: Flask, bp: Blueprint) -> None:
|
||||
app.url_map.converters["ieAccount"] = IncomeExpensesAccountConverter
|
||||
|
||||
from .views import bp as report_bp
|
||||
bp.register_blueprint(report_bp, url_prefix="/reports")
|
||||
app.register_blueprint(report_bp, url_prefix=url_prefix)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
@ -137,6 +137,7 @@ class AccountCollector:
|
||||
.join(JournalEntry).join(Account)\
|
||||
.filter(*conditions)\
|
||||
.group_by(Account.id, Account.base_code, Account.no)\
|
||||
.having(balance_func != 0)\
|
||||
.order_by(Account.base_code, Account.no)
|
||||
account_balances: list[sa.Row] \
|
||||
= db.session.execute(select_balance).all()
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
@ -269,6 +269,7 @@ class IncomeStatement(BaseReport):
|
||||
.join(JournalEntry).join(Account)\
|
||||
.filter(*conditions)\
|
||||
.group_by(Account.id)\
|
||||
.having(balance_func != 0)\
|
||||
.order_by(Account.base_code, Account.no)
|
||||
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
||||
accounts: dict[int, Account] \
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
@ -191,6 +191,7 @@ class TrialBalance(BaseReport):
|
||||
.join(JournalEntry).join(Account)\
|
||||
.filter(*conditions)\
|
||||
.group_by(Account.id)\
|
||||
.having(balance_func != 0)\
|
||||
.order_by(Account.base_code, Account.no)
|
||||
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
||||
accounts: dict[int, Account] \
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/6
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/5
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
@ -68,9 +68,9 @@ class ReportChooser:
|
||||
"""The title of the current report."""
|
||||
self.is_search: bool = active_report == ReportType.SEARCH
|
||||
"""Whether the current report is the search page."""
|
||||
self.__reports.append(self.__journal)
|
||||
self.__reports.append(self.__ledger)
|
||||
self.__reports.append(self.__income_expenses)
|
||||
self.__reports.append(self.__ledger)
|
||||
self.__reports.append(self.__journal)
|
||||
self.__reports.append(self.__trial_balance)
|
||||
self.__reports.append(self.__income_statement)
|
||||
self.__reports.append(self.__balance_sheet)
|
||||
@ -80,28 +80,6 @@ class ReportChooser:
|
||||
if self.is_search:
|
||||
self.current_report = gettext("Search")
|
||||
|
||||
@property
|
||||
def __journal(self) -> OptionLink:
|
||||
"""Returns the journal.
|
||||
|
||||
:return: The journal.
|
||||
"""
|
||||
return OptionLink(gettext("Journal"), journal_url(self.__period),
|
||||
self.__active_report == ReportType.JOURNAL,
|
||||
fa_icon="fa-solid fa-book")
|
||||
|
||||
@property
|
||||
def __ledger(self) -> OptionLink:
|
||||
"""Returns the ledger.
|
||||
|
||||
:return: The ledger.
|
||||
"""
|
||||
return OptionLink(gettext("Ledger"),
|
||||
ledger_url(self.__currency, self.__account,
|
||||
self.__period),
|
||||
self.__active_report == ReportType.LEDGER,
|
||||
fa_icon="fa-solid fa-clipboard")
|
||||
|
||||
@property
|
||||
def __income_expenses(self) -> OptionLink:
|
||||
"""Returns the income and expenses log.
|
||||
@ -118,6 +96,28 @@ class ReportChooser:
|
||||
self.__active_report == ReportType.INCOME_EXPENSES,
|
||||
fa_icon="fa-solid fa-money-bill-wave")
|
||||
|
||||
@property
|
||||
def __ledger(self) -> OptionLink:
|
||||
"""Returns the ledger.
|
||||
|
||||
:return: The ledger.
|
||||
"""
|
||||
return OptionLink(gettext("Ledger"),
|
||||
ledger_url(self.__currency, self.__account,
|
||||
self.__period),
|
||||
self.__active_report == ReportType.LEDGER,
|
||||
fa_icon="fa-solid fa-clipboard")
|
||||
|
||||
@property
|
||||
def __journal(self) -> OptionLink:
|
||||
"""Returns the journal.
|
||||
|
||||
:return: The journal.
|
||||
"""
|
||||
return OptionLink(gettext("Journal"), journal_url(self.__period),
|
||||
self.__active_report == ReportType.JOURNAL,
|
||||
fa_icon="fa-solid fa-book")
|
||||
|
||||
@property
|
||||
def __trial_balance(self) -> OptionLink:
|
||||
"""Returns the trial balance.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
@ -34,8 +34,8 @@ def journal_url(period: Period) \
|
||||
:return: The URL of the journal.
|
||||
"""
|
||||
if period.is_default:
|
||||
return url_for("accounting.report.journal-default")
|
||||
return url_for("accounting.report.journal", period=period)
|
||||
return url_for("accounting-report.journal-default")
|
||||
return url_for("accounting-report.journal", period=period)
|
||||
|
||||
|
||||
def ledger_url(currency: Currency, account: Account, period: Period) \
|
||||
@ -48,9 +48,9 @@ def ledger_url(currency: Currency, account: Account, period: Period) \
|
||||
:return: The URL of the ledger.
|
||||
"""
|
||||
if period.is_default:
|
||||
return url_for("accounting.report.ledger-default",
|
||||
return url_for("accounting-report.ledger-default",
|
||||
currency=currency, account=account)
|
||||
return url_for("accounting.report.ledger",
|
||||
return url_for("accounting-report.ledger",
|
||||
currency=currency, account=account,
|
||||
period=period)
|
||||
|
||||
@ -67,11 +67,11 @@ def income_expenses_url(currency: Currency, account: CurrentAccount,
|
||||
if currency.code == default_currency_code() \
|
||||
and account.code == options.default_ie_account_code \
|
||||
and period.is_default:
|
||||
return url_for("accounting.report.default")
|
||||
return url_for("accounting-report.default")
|
||||
if period.is_default:
|
||||
return url_for("accounting.report.income-expenses-default",
|
||||
return url_for("accounting-report.income-expenses-default",
|
||||
currency=currency, account=account)
|
||||
return url_for("accounting.report.income-expenses",
|
||||
return url_for("accounting-report.income-expenses",
|
||||
currency=currency, account=account,
|
||||
period=period)
|
||||
|
||||
@ -84,9 +84,9 @@ def trial_balance_url(currency: Currency, period: Period) -> str:
|
||||
:return: The URL of the trial balance.
|
||||
"""
|
||||
if period.is_default:
|
||||
return url_for("accounting.report.trial-balance-default",
|
||||
return url_for("accounting-report.trial-balance-default",
|
||||
currency=currency)
|
||||
return url_for("accounting.report.trial-balance",
|
||||
return url_for("accounting-report.trial-balance",
|
||||
currency=currency, period=period)
|
||||
|
||||
|
||||
@ -98,9 +98,9 @@ def income_statement_url(currency: Currency, period: Period) -> str:
|
||||
:return: The URL of the income statement.
|
||||
"""
|
||||
if period.is_default:
|
||||
return url_for("accounting.report.income-statement-default",
|
||||
return url_for("accounting-report.income-statement-default",
|
||||
currency=currency)
|
||||
return url_for("accounting.report.income-statement",
|
||||
return url_for("accounting-report.income-statement",
|
||||
currency=currency, period=period)
|
||||
|
||||
|
||||
@ -112,7 +112,7 @@ def balance_sheet_url(currency: Currency, period: Period) -> str:
|
||||
:return: The URL of the balance sheet.
|
||||
"""
|
||||
if period.is_default:
|
||||
return url_for("accounting.report.balance-sheet-default",
|
||||
return url_for("accounting-report.balance-sheet-default",
|
||||
currency=currency)
|
||||
return url_for("accounting.report.balance-sheet",
|
||||
return url_for("accounting-report.balance-sheet",
|
||||
currency=currency, period=period)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
@ -30,7 +30,7 @@ from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
|
||||
IncomeStatement, BalanceSheet, Search
|
||||
from .template_filters import format_amount
|
||||
|
||||
bp: Blueprint = Blueprint("report", __name__)
|
||||
bp: Blueprint = Blueprint("accounting-report", __name__)
|
||||
"""The view blueprint for the reports."""
|
||||
bp.add_app_template_filter(format_amount, "accounting_report_format_amount")
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
/* The Mia! Accounting Project
|
||||
* style.css: The style sheet for the accounting application.
|
||||
*/
|
||||
|
||||
@ -316,6 +316,10 @@ a.accounting-report-table-row {
|
||||
}
|
||||
|
||||
/* The description editor */
|
||||
.accounting-description-editor-buttons {
|
||||
max-height: 7rem;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.accounting-description-editor-buttons .btn {
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
/* The Mia! Accounting Project
|
||||
* account-form.js: The JavaScript for the account form
|
||||
*/
|
||||
|
||||
@ -342,12 +342,6 @@ class BaseAccountSelector {
|
||||
*/
|
||||
class BaseAccountOption {
|
||||
|
||||
/**
|
||||
* The base account selector
|
||||
* @type {BaseAccountSelector}
|
||||
*/
|
||||
#selector;
|
||||
|
||||
/**
|
||||
* The element
|
||||
* @type {HTMLLIElement}
|
||||
@ -379,13 +373,12 @@ class BaseAccountOption {
|
||||
* @param element {HTMLLIElement} the element
|
||||
*/
|
||||
constructor(selector, element) {
|
||||
this.#selector = selector;
|
||||
this.#element = element;
|
||||
this.code = element.dataset.code;
|
||||
this.text = element.dataset.text;
|
||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||
|
||||
this.#element.onclick = () => this.#selector.form.saveBaseAccount(this);
|
||||
this.#element.onclick = () => selector.form.saveBaseAccount(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
/* The Mia! Accounting Project
|
||||
* account-order.js: The JavaScript for the account order
|
||||
*/
|
||||
|
||||
@ -32,7 +32,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const no = document.getElementById(`accounting-order-${accounts[i].dataset.id}-no`);
|
||||
const code = document.getElementById(`accounting-order-${accounts[i].dataset.id}-code`);
|
||||
no.value = String(i + 1);
|
||||
code.innerText = `${list.dataset.baseCode}-${`000${i + 1}`.slice(-3)}`;
|
||||
const zeroPaddedNo = `000${no.value}`.slice(-3)
|
||||
code.innerText = `${list.dataset.baseCode}-${zeroPaddedNo}`;
|
||||
}
|
||||
};
|
||||
initializeDragAndDropReordering(list, onReorder);
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
/* The Mia! Accounting Project
|
||||
* currency-form.js: The JavaScript for the currency form
|
||||
*/
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
/* The Mia! Accounting Project
|
||||
* description-editor.js: The JavaScript for the description editor
|
||||
*/
|
||||
|
||||
@ -60,7 +60,7 @@ class DescriptionEditor {
|
||||
|
||||
/**
|
||||
* The current tab
|
||||
* @type {TabPlane}
|
||||
* @type {DescriptionEditorTabPlane}
|
||||
*/
|
||||
currentTab;
|
||||
|
||||
@ -68,7 +68,7 @@ class DescriptionEditor {
|
||||
* The description input
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
description;
|
||||
#descriptionInput;
|
||||
|
||||
/**
|
||||
* The button to the original line item selector
|
||||
@ -89,20 +89,44 @@ class DescriptionEditor {
|
||||
note;
|
||||
|
||||
/**
|
||||
* The account buttons
|
||||
* @type {HTMLButtonElement[]}
|
||||
* The placeholder of the confirmed account
|
||||
* @type {DescriptionEditorConfirmedAccount}
|
||||
*/
|
||||
#accountButtons;
|
||||
#confirmedAccountPlaceholder;
|
||||
|
||||
/**
|
||||
* The selected account button
|
||||
* @type {HTMLButtonElement|null}
|
||||
* All the suggested accounts
|
||||
* @type {DescriptionEditorSuggestedAccount[]}
|
||||
*/
|
||||
#selectedAccount = null;
|
||||
#allSuggestedAccounts;
|
||||
|
||||
/**
|
||||
* The current suggested accounts
|
||||
* @type {DescriptionEditorSuggestedAccount[]}
|
||||
*/
|
||||
#currentSuggestedAccounts;
|
||||
|
||||
/**
|
||||
* The account that the user specified or confirmed
|
||||
* @type {DescriptionEditorConfirmedAccount|null}
|
||||
*/
|
||||
#confirmedAccount = null;
|
||||
|
||||
/**
|
||||
* Whether the user has confirmed the account
|
||||
* @type {boolean}
|
||||
*/
|
||||
isAccountConfirmed = false;
|
||||
|
||||
/**
|
||||
* The selected account.
|
||||
* @type {DescriptionEditorAccount|null}
|
||||
*/
|
||||
selectedAccount = null;
|
||||
|
||||
/**
|
||||
* The tab planes
|
||||
* @type {{general: GeneralTagTab, travel: GeneralTripTab, bus: BusTripTab, recurring: RecurringTransactionTab, annotation: AnnotationTab}}
|
||||
* @type {{general: DescriptionEditorGeneralTagTab, travel: DescriptionEditorGeneralTripTab, bus: DescriptionEditorBusTripTab, recurring: DescriptionEditorRecurringTab, annotation: DescriptionEditorAnnotationTab}}
|
||||
*/
|
||||
tabPlanes = {};
|
||||
|
||||
@ -118,20 +142,19 @@ class DescriptionEditor {
|
||||
this.prefix = `accounting-description-editor-${debitCredit}`;
|
||||
this.#form = document.getElementById(this.prefix);
|
||||
this.#modal = document.getElementById(`${this.prefix}-modal`);
|
||||
this.description = document.getElementById(`${this.prefix}-description`);
|
||||
this.#descriptionInput = document.getElementById(`${this.prefix}-description`);
|
||||
this.#offsetButton = document.getElementById(`${this.prefix}-offset`);
|
||||
this.number = document.getElementById(`${this.prefix}-annotation-number`);
|
||||
this.note = document.getElementById(`${this.prefix}-annotation-note`);
|
||||
// noinspection JSValidateTypes
|
||||
this.#accountButtons = Array.from(document.getElementsByClassName(`${this.prefix}-account`));
|
||||
this.#confirmedAccountPlaceholder = new DescriptionEditorConfirmedAccount(this, document.getElementById(`${this.prefix}-account-confirmed`));
|
||||
this.#allSuggestedAccounts = Array.from(document.getElementsByClassName(`${this.prefix}-account`)).map((button) => new DescriptionEditorSuggestedAccount(this, button));
|
||||
|
||||
for (const cls of [GeneralTagTab, GeneralTripTab, BusTripTab, RecurringTransactionTab, AnnotationTab]) {
|
||||
for (const cls of [DescriptionEditorGeneralTagTab, DescriptionEditorGeneralTripTab, DescriptionEditorBusTripTab, DescriptionEditorRecurringTab, DescriptionEditorAnnotationTab]) {
|
||||
const tab = new cls(this);
|
||||
this.tabPlanes[tab.tabId()] = tab;
|
||||
}
|
||||
this.currentTab = this.tabPlanes.general;
|
||||
this.#initializeSuggestedAccounts();
|
||||
this.description.onchange = () => this.#onDescriptionChange();
|
||||
this.#descriptionInput.onchange = () => this.#onDescriptionChange();
|
||||
this.#offsetButton.onclick = () => this.lineItemEditor.originalLineItemSelector.onOpen();
|
||||
this.#form.onsubmit = () => {
|
||||
if (this.currentTab.validate()) {
|
||||
@ -141,12 +164,44 @@ class DescriptionEditor {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description.
|
||||
*
|
||||
* @return {string} the description
|
||||
*/
|
||||
get description() {
|
||||
return this.#descriptionInput.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description.
|
||||
*
|
||||
* @param description {string} the description
|
||||
*/
|
||||
set description(description) {
|
||||
this.#descriptionInput.value = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current account options.
|
||||
*
|
||||
* @return {DescriptionEditorAccount[]} the current account options.
|
||||
*/
|
||||
get #currentAccountOptions() {
|
||||
if (this.#confirmedAccount === null) {
|
||||
return this.#currentSuggestedAccounts;
|
||||
}
|
||||
return [this.#confirmedAccount].concat(this.#currentSuggestedAccounts);
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback when the description input is changed.
|
||||
*
|
||||
*/
|
||||
#onDescriptionChange() {
|
||||
this.description.value = this.description.value.trim();
|
||||
this.#resetTabPlanes();
|
||||
this.selectedAccount = null;
|
||||
this.description = this.description.trim();
|
||||
for (const tabPlane of [this.tabPlanes.recurring, this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) {
|
||||
if (tabPlane.populate()) {
|
||||
break;
|
||||
@ -156,20 +211,49 @@ class DescriptionEditor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the suggested accounts.
|
||||
* Resets the tab planes.
|
||||
*
|
||||
*/
|
||||
#resetTabPlanes() {
|
||||
for (const tabPlane of Object.values(this.tabPlanes)) {
|
||||
tabPlane.reset();
|
||||
}
|
||||
this.tabPlanes.general.switchToMe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current suggested accounts.
|
||||
*
|
||||
* @param tagButton {HTMLButtonElement} the tag button
|
||||
*/
|
||||
filterSuggestedAccounts(tagButton) {
|
||||
updateCurrentSuggestedAccounts(tagButton) {
|
||||
this.clearSuggestedAccounts();
|
||||
const suggested = JSON.parse(tagButton.dataset.accounts);
|
||||
for (const accountButton of this.#accountButtons) {
|
||||
if (suggested.includes(accountButton.dataset.code)) {
|
||||
accountButton.classList.remove("d-none");
|
||||
if (accountButton.dataset.code === suggested[0]) {
|
||||
this.#selectAccount(accountButton);
|
||||
return;
|
||||
}
|
||||
const suggestedAccountCodes = JSON.parse(tagButton.dataset.accounts);
|
||||
this.#currentSuggestedAccounts = this.#allSuggestedAccounts.filter((account) => {
|
||||
if (this.#confirmedAccount !== null && account.code === this.#confirmedAccount.code) {
|
||||
return false;
|
||||
}
|
||||
return suggestedAccountCodes.includes(account.code);
|
||||
});
|
||||
for (const account of this.#currentSuggestedAccounts) {
|
||||
account.setShown(true);
|
||||
}
|
||||
this.#selectSuggestedAccount(suggestedAccountCodes[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the suggested account.
|
||||
*
|
||||
* @param code {string} the code of the most-frequent suggested account
|
||||
*/
|
||||
#selectSuggestedAccount(code) {
|
||||
if (this.isAccountConfirmed) {
|
||||
return;
|
||||
}
|
||||
for (const account of this.#currentAccountOptions) {
|
||||
if (account.code === code) {
|
||||
this.selectAccount(account);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -179,37 +263,29 @@ class DescriptionEditor {
|
||||
*
|
||||
*/
|
||||
clearSuggestedAccounts() {
|
||||
for (const accountButton of this.#accountButtons) {
|
||||
accountButton.classList.add("d-none");
|
||||
for (const account of this.#allSuggestedAccounts) {
|
||||
account.setShown(false);
|
||||
account.setActive(false);
|
||||
}
|
||||
this.#selectAccount(null);
|
||||
this.#currentSuggestedAccounts = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the suggested accounts.
|
||||
* Select an account.
|
||||
*
|
||||
* @param selectedAccount {DescriptionEditorAccount|null} the account, or null to deselect the account
|
||||
*/
|
||||
#initializeSuggestedAccounts() {
|
||||
for (const accountButton of this.#accountButtons) {
|
||||
accountButton.onclick = () => this.#selectAccount(accountButton);
|
||||
selectAccount(selectedAccount) {
|
||||
for (const account of this.#currentAccountOptions) {
|
||||
account.setActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a suggested account.
|
||||
*
|
||||
* @param selectedAccountButton {HTMLButtonElement|null} the account button, or null to deselect the account
|
||||
*/
|
||||
#selectAccount(selectedAccountButton) {
|
||||
for (const accountButton of this.#accountButtons) {
|
||||
accountButton.classList.remove("btn-primary");
|
||||
accountButton.classList.add("btn-outline-primary");
|
||||
if (selectedAccount !== null) {
|
||||
selectedAccount.setActive(true);
|
||||
}
|
||||
if (selectedAccountButton !== null) {
|
||||
selectedAccountButton.classList.remove("btn-outline-primary");
|
||||
selectedAccountButton.classList.add("btn-primary");
|
||||
this.selectedAccount = selectedAccount;
|
||||
if (this.selectedAccount !== null) {
|
||||
this.isAccountConfirmed &&= this.selectedAccount.isConfirmedAccount;
|
||||
}
|
||||
this.#selectedAccount = selectedAccountButton;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -218,11 +294,7 @@ class DescriptionEditor {
|
||||
*/
|
||||
#submit() {
|
||||
bootstrap.Modal.getOrCreateInstance(this.#modal).hide();
|
||||
if (this.#selectedAccount !== null) {
|
||||
this.lineItemEditor.saveDescriptionWithAccount(this.description.value, this.#selectedAccount.dataset.code, this.#selectedAccount.dataset.text, this.#selectedAccount.classList.contains("accounting-account-is-need-offset"));
|
||||
} else {
|
||||
this.lineItemEditor.saveDescription(this.description.value);
|
||||
}
|
||||
this.lineItemEditor.saveDescription(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -230,21 +302,27 @@ class DescriptionEditor {
|
||||
*
|
||||
*/
|
||||
onOpen() {
|
||||
this.#reset();
|
||||
this.description.value = this.lineItemEditor.description === null? "": this.lineItemEditor.description;
|
||||
this.description = this.lineItemEditor.description === null? "": this.lineItemEditor.description;
|
||||
this.#setConfirmedAccount();
|
||||
this.#onDescriptionChange();
|
||||
if (this.isAccountConfirmed) {
|
||||
this.selectAccount(this.#confirmedAccount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the description editor.
|
||||
* Sets the confirmed account.
|
||||
*
|
||||
*/
|
||||
#reset() {
|
||||
this.description.value = "";
|
||||
for (const tabPlane of Object.values(this.tabPlanes)) {
|
||||
tabPlane.reset();
|
||||
#setConfirmedAccount() {
|
||||
this.isAccountConfirmed = this.lineItemEditor.isAccountConfirmed;
|
||||
this.#confirmedAccountPlaceholder.setShown(this.isAccountConfirmed);
|
||||
if (this.isAccountConfirmed) {
|
||||
this.#confirmedAccountPlaceholder.initializeFrom(this.lineItemEditor.account);
|
||||
this.#confirmedAccount = this.#confirmedAccountPlaceholder;
|
||||
} else {
|
||||
this.#confirmedAccount = null;
|
||||
}
|
||||
this.tabPlanes.general.switchToMe();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -263,13 +341,130 @@ class DescriptionEditor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An account option in the description editor.
|
||||
*
|
||||
*/
|
||||
class DescriptionEditorAccount extends JournalEntryAccount {
|
||||
|
||||
/**
|
||||
* The account button
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
#element;
|
||||
|
||||
/**
|
||||
* Whether this is the account specified or confirmed by the user
|
||||
* @type {boolean}
|
||||
*/
|
||||
isConfirmedAccount = false;
|
||||
|
||||
/**
|
||||
* Constructs an account option in the description editor.
|
||||
*
|
||||
* @param editor {DescriptionEditor} the description editor
|
||||
* @param code {string} the account code
|
||||
* @param text {string} the account text
|
||||
* @param isNeedOffset {boolean} true if the line items in the account needs offset, or false otherwise
|
||||
* @param button {HTMLButtonElement} the account button
|
||||
*/
|
||||
constructor(editor, code, text, isNeedOffset, button) {
|
||||
super(code, text, isNeedOffset);
|
||||
this.#element = button;
|
||||
this.#element.onclick = () => editor.selectAccount(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the option is shown.
|
||||
*
|
||||
* @param isShown {boolean} true to show, or false otherwise
|
||||
*/
|
||||
setShown(isShown) {
|
||||
if (isShown) {
|
||||
this.#element.classList.remove("d-none");
|
||||
} else {
|
||||
this.#element.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the option is active.
|
||||
*
|
||||
* @param isActive {boolean} true if active, or false otherwise
|
||||
*/
|
||||
setActive(isActive) {
|
||||
if (isActive) {
|
||||
this.#element.classList.add("btn-primary");
|
||||
this.#element.classList.remove("btn-outline-primary");
|
||||
} else {
|
||||
this.#element.classList.remove("btn-primary");
|
||||
this.#element.classList.add("btn-outline-primary");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content of the account button.
|
||||
*
|
||||
*/
|
||||
resetContent() {
|
||||
this.#element.innerText = this.text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A suggested account.
|
||||
*
|
||||
*/
|
||||
class DescriptionEditorSuggestedAccount extends DescriptionEditorAccount {
|
||||
|
||||
/**
|
||||
* Constructs a suggested account.
|
||||
*
|
||||
* @param editor {DescriptionEditor} the description editor
|
||||
* @param button {HTMLButtonElement} the account button
|
||||
*/
|
||||
constructor(editor, button) {
|
||||
super(editor, button.dataset.code, button.dataset.text, button.classList.contains("accounting-account-is-need-offset"), button);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The account option that is specified or confirmed by the user.
|
||||
*
|
||||
*/
|
||||
class DescriptionEditorConfirmedAccount extends DescriptionEditorAccount {
|
||||
|
||||
/**
|
||||
* Constructs the account option that is specified or confirmed by the user.
|
||||
*
|
||||
* @param editor {DescriptionEditor} the description editor
|
||||
* @param button {HTMLButtonElement} the account button
|
||||
*/
|
||||
constructor(editor, button) {
|
||||
super(editor, "", "", false, button);
|
||||
this.isConfirmedAccount = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the confirmed account from the line item editor.
|
||||
*
|
||||
* @param account {JournalEntryAccount} the confirmed account from the line item editor
|
||||
*/
|
||||
initializeFrom(account) {
|
||||
this.code = account.code;
|
||||
this.text = account.text;
|
||||
this.isNeedOffset = account.isNeedOffset;
|
||||
this.resetContent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A tab plane.
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
*/
|
||||
class TabPlane {
|
||||
class DescriptionEditorTabPlane {
|
||||
|
||||
/**
|
||||
* The parent description editor
|
||||
@ -364,7 +559,7 @@ class TabPlane {
|
||||
* @abstract
|
||||
* @private
|
||||
*/
|
||||
class TagTabPlane extends TabPlane {
|
||||
class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
|
||||
|
||||
/**
|
||||
* The tag input
|
||||
@ -414,7 +609,7 @@ class TagTabPlane extends TabPlane {
|
||||
if (tagButton.dataset.value === this.tag.value) {
|
||||
tagButton.classList.remove("btn-outline-primary");
|
||||
tagButton.classList.add("btn-primary");
|
||||
this.editor.filterSuggestedAccounts(tagButton);
|
||||
this.editor.updateCurrentSuggestedAccounts(tagButton);
|
||||
isMatched = true;
|
||||
} else {
|
||||
tagButton.classList.remove("btn-primary");
|
||||
@ -442,7 +637,7 @@ class TagTabPlane extends TabPlane {
|
||||
super.switchToMe();
|
||||
for (const tagButton of this.tagButtons) {
|
||||
if (tagButton.classList.contains("btn-primary")) {
|
||||
this.editor.filterSuggestedAccounts(tagButton);
|
||||
this.editor.updateCurrentSuggestedAccounts(tagButton);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -463,7 +658,7 @@ class TagTabPlane extends TabPlane {
|
||||
tagButton.classList.remove("btn-outline-primary");
|
||||
tagButton.classList.add("btn-primary");
|
||||
this.tag.value = tagButton.dataset.value;
|
||||
this.editor.filterSuggestedAccounts(tagButton);
|
||||
this.editor.updateCurrentSuggestedAccounts(tagButton);
|
||||
this.updateDescription();
|
||||
};
|
||||
}
|
||||
@ -522,7 +717,7 @@ class TagTabPlane extends TabPlane {
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
class GeneralTagTab extends TagTabPlane {
|
||||
class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane {
|
||||
|
||||
/**
|
||||
* The tab ID
|
||||
@ -540,12 +735,12 @@ class GeneralTagTab extends TagTabPlane {
|
||||
* @override
|
||||
*/
|
||||
updateDescription() {
|
||||
const pos = this.editor.description.value.indexOf("—");
|
||||
const pos = this.editor.description.indexOf("—");
|
||||
const prefix = this.tag.value === ""? "": `${this.tag.value}—`;
|
||||
if (pos === -1) {
|
||||
this.editor.description.value = `${prefix}${this.editor.description.value}`;
|
||||
this.editor.description = `${prefix}${this.editor.description}`;
|
||||
} else {
|
||||
this.editor.description.value = `${prefix}${this.editor.description.value.substring(pos + 1)}`;
|
||||
this.editor.description = `${prefix}${this.editor.description.substring(pos + 1)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -556,7 +751,7 @@ class GeneralTagTab extends TagTabPlane {
|
||||
* @override
|
||||
*/
|
||||
populate() {
|
||||
const found = this.editor.description.value.match(/^([^—]+)—/);
|
||||
const found = this.editor.description.match(/^([^—]+)—/);
|
||||
if (found === null) {
|
||||
return false;
|
||||
}
|
||||
@ -583,7 +778,7 @@ class GeneralTagTab extends TagTabPlane {
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
class GeneralTripTab extends TagTabPlane {
|
||||
class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
|
||||
|
||||
/**
|
||||
* The origin
|
||||
@ -675,7 +870,7 @@ class GeneralTripTab extends TagTabPlane {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.editor.description.value = `${this.tag.value}—${this.#from.value}${direction}${this.#to.value}`;
|
||||
this.editor.description = `${this.tag.value}—${this.#from.value}${direction}${this.#to.value}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -709,7 +904,7 @@ class GeneralTripTab extends TagTabPlane {
|
||||
* @override
|
||||
*/
|
||||
populate() {
|
||||
const found = this.editor.description.value.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
|
||||
const found = this.editor.description.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
|
||||
if (found === null) {
|
||||
return false;
|
||||
}
|
||||
@ -782,7 +977,7 @@ class GeneralTripTab extends TagTabPlane {
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
class BusTripTab extends TagTabPlane {
|
||||
class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
|
||||
|
||||
/**
|
||||
* The route
|
||||
@ -867,7 +1062,7 @@ class BusTripTab extends TagTabPlane {
|
||||
* @override
|
||||
*/
|
||||
updateDescription() {
|
||||
this.editor.description.value = `${this.tag.value}—${this.#route.value}—${this.#from.value}→${this.#to.value}`;
|
||||
this.editor.description = `${this.tag.value}—${this.#route.value}—${this.#from.value}→${this.#to.value}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -895,7 +1090,7 @@ class BusTripTab extends TagTabPlane {
|
||||
* @override
|
||||
*/
|
||||
populate() {
|
||||
const found = this.editor.description.value.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
|
||||
const found = this.editor.description.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
|
||||
if (found === null) {
|
||||
return false;
|
||||
}
|
||||
@ -970,7 +1165,7 @@ class BusTripTab extends TagTabPlane {
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
class RecurringTransactionTab extends TabPlane {
|
||||
class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
|
||||
|
||||
/**
|
||||
* The month names
|
||||
@ -1005,8 +1200,8 @@ class RecurringTransactionTab extends TabPlane {
|
||||
this.reset();
|
||||
itemButton.classList.add("btn-primary");
|
||||
itemButton.classList.remove("btn-outline-primary");
|
||||
this.editor.description.value = this.#getDescription(itemButton);
|
||||
this.editor.filterSuggestedAccounts(itemButton);
|
||||
this.editor.description = this.#getDescription(itemButton);
|
||||
this.editor.updateCurrentSuggestedAccounts(itemButton);
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1062,7 +1257,7 @@ class RecurringTransactionTab extends TabPlane {
|
||||
*/
|
||||
populate() {
|
||||
for (const itemButton of this.#itemButtons) {
|
||||
if (this.#getDescription(itemButton) === this.editor.description.value) {
|
||||
if (this.#getDescription(itemButton) === this.editor.description) {
|
||||
itemButton.classList.add("btn-primary");
|
||||
itemButton.classList.remove("btn-outline-primary");
|
||||
this.switchToMe();
|
||||
@ -1080,7 +1275,7 @@ class RecurringTransactionTab extends TabPlane {
|
||||
super.switchToMe();
|
||||
for (const itemButton of this.#itemButtons) {
|
||||
if (itemButton.classList.contains("btn-primary")) {
|
||||
this.editor.filterSuggestedAccounts(itemButton);
|
||||
this.editor.updateCurrentSuggestedAccounts(itemButton);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -1103,7 +1298,7 @@ class RecurringTransactionTab extends TabPlane {
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
class AnnotationTab extends TabPlane {
|
||||
class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane {
|
||||
|
||||
/**
|
||||
* Constructs a tab plane.
|
||||
@ -1136,15 +1331,15 @@ class AnnotationTab extends TabPlane {
|
||||
* @override
|
||||
*/
|
||||
updateDescription() {
|
||||
const found = this.editor.description.value.match(/^(.*?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
|
||||
const found = this.editor.description.match(/^(.*?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
|
||||
if (found !== null) {
|
||||
this.editor.description.value = found[1];
|
||||
this.editor.description = found[1];
|
||||
}
|
||||
if (parseInt(this.editor.number.value) > 1) {
|
||||
this.editor.description.value = `${this.editor.description.value}×${this.editor.number.value}`;
|
||||
this.editor.description = `${this.editor.description}×${this.editor.number.value}`;
|
||||
}
|
||||
if (this.editor.note.value !== "") {
|
||||
this.editor.description.value = `${this.editor.description.value}(${this.editor.note.value})`;
|
||||
this.editor.description = `${this.editor.description}(${this.editor.note.value})`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1165,19 +1360,19 @@ class AnnotationTab extends TabPlane {
|
||||
* @override
|
||||
*/
|
||||
populate() {
|
||||
const found = this.editor.description.value.match(/^(.*?)(?:[*×](\d+))?(?:\(([^()]+)\))?$/);
|
||||
this.editor.description.value = found[1];
|
||||
const found = this.editor.description.match(/^(.*?)(?:[*×](\d+))?(?:\(([^()]+)\))?$/);
|
||||
this.editor.description = found[1];
|
||||
if (found[2] === undefined || parseInt(found[2]) === 1) {
|
||||
this.editor.number.value = "";
|
||||
} else {
|
||||
this.editor.number.value = found[2];
|
||||
this.editor.description.value = `${this.editor.description.value}×${this.editor.number.value}`;
|
||||
this.editor.description = `${this.editor.description}×${this.editor.number.value}`;
|
||||
}
|
||||
if (found[3] === undefined) {
|
||||
this.editor.note.value = "";
|
||||
} else {
|
||||
this.editor.note.value = found[3];
|
||||
this.editor.description.value = `${this.editor.description.value}(${this.editor.note.value})`;
|
||||
this.editor.description = `${this.editor.description}(${this.editor.note.value})`;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
/* The Mia! Accounting Project
|
||||
* journal-entry-account-selector.js: The JavaScript for the account selector of the journal entry form
|
||||
*/
|
||||
|
||||
@ -139,8 +139,8 @@ class JournalEntryAccountSelector {
|
||||
*/
|
||||
#getCodesUsedInForm() {
|
||||
const inUse = this.lineItemEditor.form.getAccountCodesUsed(this.#debitCredit);
|
||||
if (this.lineItemEditor.accountCode !== null) {
|
||||
inUse.push(this.lineItemEditor.accountCode);
|
||||
if (this.lineItemEditor.account !== null) {
|
||||
inUse.push(this.lineItemEditor.account.code);
|
||||
}
|
||||
return inUse
|
||||
}
|
||||
@ -155,9 +155,9 @@ class JournalEntryAccountSelector {
|
||||
this.#more.classList.remove("d-none");
|
||||
this.#filterOptions();
|
||||
for (const option of this.#options) {
|
||||
option.setActive(option.code === this.lineItemEditor.accountCode);
|
||||
option.setActive(this.lineItemEditor.account !== null && option.code === this.lineItemEditor.account.code);
|
||||
}
|
||||
if (this.lineItemEditor.accountCode === null) {
|
||||
if (this.lineItemEditor.account === null) {
|
||||
this.#clearButton.classList.add("btn-secondary");
|
||||
this.#clearButton.classList.remove("btn-danger");
|
||||
this.#clearButton.disabled = true;
|
||||
@ -190,12 +190,6 @@ class JournalEntryAccountSelector {
|
||||
*/
|
||||
class JournalEntryAccountOption {
|
||||
|
||||
/**
|
||||
* The account selector
|
||||
* @type {JournalEntryAccountSelector}
|
||||
*/
|
||||
#selector;
|
||||
|
||||
/**
|
||||
* The element
|
||||
* @type {HTMLLIElement}
|
||||
@ -239,7 +233,6 @@ class JournalEntryAccountOption {
|
||||
* @param element {HTMLLIElement} the element
|
||||
*/
|
||||
constructor(selector, element) {
|
||||
this.#selector = selector;
|
||||
this.#element = element;
|
||||
this.code = element.dataset.code;
|
||||
this.text = element.dataset.text;
|
||||
@ -247,7 +240,7 @@ class JournalEntryAccountOption {
|
||||
this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset");
|
||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||
|
||||
this.#element.onclick = () => this.#selector.lineItemEditor.saveAccount(this);
|
||||
this.#element.onclick = () => selector.lineItemEditor.saveAccount(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
/* The Mia! Accounting Project
|
||||
* journal-entry-form.js: The JavaScript for the journal entry form
|
||||
*/
|
||||
|
||||
@ -207,8 +207,8 @@ class JournalEntryForm {
|
||||
* @return {string[]} the account codes used in the form
|
||||
*/
|
||||
getAccountCodesUsed(debitCredit) {
|
||||
return this.getLineItems(debitCredit).map((lineItem) => lineItem.accountCode)
|
||||
.filter((code) => code !== null);
|
||||
return this.getLineItems(debitCredit).filter((lineItem) => lineItem.account !== null)
|
||||
.map((lineItem) => lineItem.account.code);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -457,11 +457,7 @@ class CurrencySubForm {
|
||||
* @param isShown {boolean} true to show, or false otherwise
|
||||
*/
|
||||
setDeleteButtonShown(isShown) {
|
||||
if (isShown) {
|
||||
this.#deleteButton.classList.remove("d-none");
|
||||
} else {
|
||||
this.#deleteButton.classList.add("d-none");
|
||||
}
|
||||
setElementShown(this.#deleteButton, isShown);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -705,15 +701,14 @@ class DebitCreditSubForm {
|
||||
this.#element.classList.add("accounting-not-empty");
|
||||
this.currency.form.lineItemEditor.onAddNew(this);
|
||||
};
|
||||
this.#content.classList.add("d-none");
|
||||
} 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;
|
||||
this.#content.classList.remove("d-none");
|
||||
}
|
||||
setElementShown(this.#content, this.lineItems.length !== 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -784,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.
|
||||
*
|
||||
@ -940,15 +982,6 @@ class LineItemSubForm {
|
||||
this.#no.value = String(siblings.indexOf(this.#element) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the line item needs offset.
|
||||
*
|
||||
* @return {boolean} true if the line item needs offset, or false otherwise
|
||||
*/
|
||||
get isNeedOffset() {
|
||||
return "isNeedOffset" in this.#element.dataset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the original line item.
|
||||
*
|
||||
@ -986,21 +1019,12 @@ class LineItemSubForm {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account code.
|
||||
* Returns the account.
|
||||
*
|
||||
* @return {string|null} the account code
|
||||
* @return {JournalEntryAccount|null} the account
|
||||
*/
|
||||
get accountCode() {
|
||||
return this.#accountCode.value === ""? null: this.#accountCode.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account text.
|
||||
*
|
||||
* @return {string|null} the account text
|
||||
*/
|
||||
get accountText() {
|
||||
return this.#accountCode.dataset.text === ""? null: this.#accountCode.dataset.text;
|
||||
get account() {
|
||||
return this.#accountCode.value === null? null: new JournalEntryAccount(this.#accountCode.value, this.#accountCode.dataset.text, this.#accountCode.classList.contains("accounting-account-is-need-offset"));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1027,11 +1051,7 @@ class LineItemSubForm {
|
||||
* @param isShown {boolean} true to show, or false otherwise
|
||||
*/
|
||||
setDeleteButtonShown(isShown) {
|
||||
if (isShown) {
|
||||
this.#deleteButton.classList.remove("d-none");
|
||||
} else {
|
||||
this.#deleteButton.classList.add("d-none");
|
||||
}
|
||||
setElementShown(this.#deleteButton, isShown);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1061,24 +1081,24 @@ class LineItemSubForm {
|
||||
* @param editor {JournalEntryLineItemEditor} the line item editor
|
||||
*/
|
||||
save(editor) {
|
||||
if (editor.isNeedOffset) {
|
||||
this.#offsets.classList.remove("d-none");
|
||||
} else {
|
||||
this.#offsets.classList.add("d-none");
|
||||
}
|
||||
setElementShown(this.#offsets, editor.account.isNeedOffset);
|
||||
this.#originalLineItemId.value = editor.originalLineItemId === null? "": editor.originalLineItemId;
|
||||
this.#originalLineItemId.dataset.date = editor.originalLineItemDate === null? "": editor.originalLineItemDate;
|
||||
this.#originalLineItemId.dataset.text = editor.originalLineItemText === null? "": editor.originalLineItemText;
|
||||
setElementShown(this.#originalLineItemText, editor.originalLineItemText !== null);
|
||||
if (editor.originalLineItemText === null) {
|
||||
this.#originalLineItemText.classList.add("d-none");
|
||||
this.#originalLineItemText.innerText = "";
|
||||
} else {
|
||||
this.#originalLineItemText.classList.remove("d-none");
|
||||
this.#originalLineItemText.innerText = A_("Offset %(item)s", {item: editor.originalLineItemText});
|
||||
}
|
||||
this.#accountCode.value = editor.accountCode === null? "": editor.accountCode;
|
||||
this.#accountCode.dataset.text = editor.accountText === null? "": editor.accountText;
|
||||
this.#accountText.innerText = editor.accountText === null? "": editor.accountText;
|
||||
this.#accountCode.value = editor.account.code;
|
||||
this.#accountCode.dataset.text = editor.account.text;
|
||||
if (editor.account.isNeedOffset) {
|
||||
this.#accountCode.classList.add("accounting-account-is-need-offset");
|
||||
} else {
|
||||
this.#accountCode.classList.remove("accounting-account-is-need-offset");
|
||||
}
|
||||
this.#accountText.innerText = editor.account.text;
|
||||
this.#description.value = editor.description === null? "": editor.description;
|
||||
this.#descriptionText.innerText = editor.description === null? "": editor.description;
|
||||
this.#amount.value = editor.amount;
|
||||
@ -1119,3 +1139,18 @@ function formatDecimal(number) {
|
||||
const whole = Number(number.minus(frac)).toLocaleString();
|
||||
return whole + String(frac).substring(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether an element is shown.
|
||||
*
|
||||
* @param element {HTMLElement} the element
|
||||
* @param isShown {boolean} true to show, or false otherwise
|
||||
* @private
|
||||
*/
|
||||
function setElementShown(element, isShown) {
|
||||
if (isShown) {
|
||||
element.classList.remove("d-none");
|
||||
} else {
|
||||
element.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
@ -148,12 +148,6 @@ class JournalEntryLineItemEditor {
|
||||
*/
|
||||
#debitCreditSubForm;
|
||||
|
||||
/**
|
||||
* Whether the journal entry line item needs offset
|
||||
* @type {boolean}
|
||||
*/
|
||||
isNeedOffset = false;
|
||||
|
||||
/**
|
||||
* The ID of the original line item
|
||||
* @type {string|null}
|
||||
@ -173,16 +167,16 @@ class JournalEntryLineItemEditor {
|
||||
originalLineItemText = null;
|
||||
|
||||
/**
|
||||
* The account code
|
||||
* @type {string|null}
|
||||
* The account
|
||||
* @type {JournalEntryAccount|null}
|
||||
*/
|
||||
accountCode = null;
|
||||
account = null;
|
||||
|
||||
/**
|
||||
* The account text
|
||||
* @type {string|null}
|
||||
* Whether the user has confirmed the account
|
||||
* @type {boolean}
|
||||
*/
|
||||
accountText = null;
|
||||
isAccountConfirmed = false;
|
||||
|
||||
/**
|
||||
* The description
|
||||
@ -276,7 +270,6 @@ class JournalEntryLineItemEditor {
|
||||
* @param originalLineItem {OriginalLineItem} the original line item
|
||||
*/
|
||||
saveOriginalLineItem(originalLineItem) {
|
||||
this.isNeedOffset = false;
|
||||
this.#originalLineItemContainer.classList.remove("d-none");
|
||||
this.#originalLineItemControl.classList.add("accounting-not-empty");
|
||||
this.originalLineItemId = originalLineItem.id;
|
||||
@ -292,9 +285,9 @@ class JournalEntryLineItemEditor {
|
||||
this.description = originalLineItem.description === ""? null: originalLineItem.description;
|
||||
this.#descriptionText.innerText = originalLineItem.description;
|
||||
this.#accountControl.classList.add("accounting-not-empty");
|
||||
this.accountCode = originalLineItem.accountCode;
|
||||
this.accountText = originalLineItem.accountText;
|
||||
this.#accountText.innerText = originalLineItem.accountText;
|
||||
this.account = originalLineItem.account.copy();
|
||||
this.isAccountConfirmed = false;
|
||||
this.#accountText.innerText = this.account.text;
|
||||
this.#amountInput.value = String(originalLineItem.netBalance);
|
||||
this.#amountInput.max = String(originalLineItem.netBalance);
|
||||
this.#amountInput.min = "0";
|
||||
@ -306,7 +299,6 @@ class JournalEntryLineItemEditor {
|
||||
*
|
||||
*/
|
||||
clearOriginalLineItem() {
|
||||
this.isNeedOffset = false;
|
||||
this.#originalLineItemContainer.classList.add("d-none");
|
||||
this.#originalLineItemControl.classList.remove("accounting-not-empty");
|
||||
this.originalLineItemId = null;
|
||||
@ -315,8 +307,8 @@ class JournalEntryLineItemEditor {
|
||||
this.#originalLineItemText.innerText = "";
|
||||
this.#setEnableDescriptionAccount(true);
|
||||
this.#accountControl.classList.remove("accounting-not-empty");
|
||||
this.accountCode = null;
|
||||
this.accountText = null;
|
||||
this.account = null;
|
||||
this.isAccountConfirmed = false;
|
||||
this.#accountText.innerText = "";
|
||||
this.#amountInput.max = "";
|
||||
}
|
||||
@ -324,47 +316,35 @@ class JournalEntryLineItemEditor {
|
||||
/**
|
||||
* Saves the description from the description editor.
|
||||
*
|
||||
* @param description {string} the description
|
||||
* @param editor {DescriptionEditor} the description editor
|
||||
*/
|
||||
saveDescription(description) {
|
||||
if (description === "") {
|
||||
saveDescription(editor) {
|
||||
if (editor.selectedAccount !== null) {
|
||||
this.#accountControl.classList.add("accounting-not-empty");
|
||||
this.account = editor.selectedAccount.copy();
|
||||
this.#accountText.innerText = editor.selectedAccount.text;
|
||||
this.isAccountConfirmed = editor.isAccountConfirmed;
|
||||
this.#validateAccount();
|
||||
}
|
||||
if (editor.description === "") {
|
||||
this.#descriptionControl.classList.remove("accounting-not-empty");
|
||||
} else {
|
||||
this.#descriptionControl.classList.add("accounting-not-empty");
|
||||
}
|
||||
this.description = description === ""? null: description;
|
||||
this.#descriptionText.innerText = description;
|
||||
this.description = editor.description === ""? null: editor.description;
|
||||
this.#descriptionText.innerText = editor.description;
|
||||
this.#validateDescription();
|
||||
bootstrap.Modal.getOrCreateInstance(this.modal).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the description with the suggested account from the description editor.
|
||||
*
|
||||
* @param description {string} the description
|
||||
* @param accountCode {string} the account code
|
||||
* @param accountText {string} the account text
|
||||
* @param isAccountNeedOffset {boolean} true if the line items in the account need offset, or false otherwise
|
||||
*/
|
||||
saveDescriptionWithAccount(description, accountCode, accountText, isAccountNeedOffset) {
|
||||
this.isNeedOffset = isAccountNeedOffset;
|
||||
this.#accountControl.classList.add("accounting-not-empty");
|
||||
this.accountCode = accountCode;
|
||||
this.accountText = accountText;
|
||||
this.#accountText.innerText = accountText;
|
||||
this.#validateAccount();
|
||||
this.saveDescription(description)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the account.
|
||||
*
|
||||
*/
|
||||
clearAccount() {
|
||||
this.isNeedOffset = false;
|
||||
this.#accountControl.classList.remove("accounting-not-empty");
|
||||
this.accountCode = null;
|
||||
this.accountText = null;
|
||||
this.account = null;
|
||||
this.isAccountConfirmed = false;
|
||||
this.#accountText.innerText = "";
|
||||
this.#validateAccount();
|
||||
}
|
||||
@ -375,10 +355,9 @@ class JournalEntryLineItemEditor {
|
||||
* @param account {JournalEntryAccountOption} the selected account
|
||||
*/
|
||||
saveAccount(account) {
|
||||
this.isNeedOffset = account.isNeedOffset;
|
||||
this.#accountControl.classList.add("accounting-not-empty");
|
||||
this.accountCode = account.code;
|
||||
this.accountText = account.text;
|
||||
this.account = new JournalEntryAccount(account.code, account.text, account.isNeedOffset);
|
||||
this.isAccountConfirmed = true;
|
||||
this.#accountText.innerText = account.text;
|
||||
this.#validateAccount();
|
||||
}
|
||||
@ -427,7 +406,7 @@ class JournalEntryLineItemEditor {
|
||||
* @return {boolean} true if valid, or false otherwise
|
||||
*/
|
||||
#validateAccount() {
|
||||
if (this.accountCode === null) {
|
||||
if (this.account === null) {
|
||||
this.#accountControl.classList.add("is-invalid");
|
||||
this.#accountError.innerText = A_("Please select the account.");
|
||||
return false;
|
||||
@ -486,7 +465,6 @@ class JournalEntryLineItemEditor {
|
||||
this.lineItem = null;
|
||||
this.#debitCreditSubForm = debitCredit;
|
||||
this.debitCredit = this.#debitCreditSubForm.debitCredit;
|
||||
this.isNeedOffset = false;
|
||||
this.#originalLineItemContainer.classList.add("d-none");
|
||||
this.#originalLineItemControl.classList.remove("accounting-not-empty");
|
||||
this.#originalLineItemControl.classList.remove("is-invalid");
|
||||
@ -502,8 +480,8 @@ class JournalEntryLineItemEditor {
|
||||
this.#descriptionError.innerText = ""
|
||||
this.#accountControl.classList.remove("accounting-not-empty");
|
||||
this.#accountControl.classList.remove("is-invalid");
|
||||
this.accountCode = null;
|
||||
this.accountText = null;
|
||||
this.account = null;
|
||||
this.isAccountConfirmed = false;
|
||||
this.#accountText.innerText = "";
|
||||
this.#accountError.innerText = "";
|
||||
this.#amountInput.value = "";
|
||||
@ -522,7 +500,6 @@ class JournalEntryLineItemEditor {
|
||||
this.lineItem = lineItem;
|
||||
this.#debitCreditSubForm = lineItem.debitCreditSubForm;
|
||||
this.debitCredit = this.#debitCreditSubForm.debitCredit;
|
||||
this.isNeedOffset = lineItem.isNeedOffset;
|
||||
this.originalLineItemId = lineItem.originalLineItemId;
|
||||
this.originalLineItemDate = lineItem.originalLineItemDate;
|
||||
this.originalLineItemText = lineItem.originalLineItemText;
|
||||
@ -542,14 +519,14 @@ class JournalEntryLineItemEditor {
|
||||
this.#descriptionControl.classList.add("accounting-not-empty");
|
||||
}
|
||||
this.#descriptionText.innerText = this.description === null? "": this.description;
|
||||
if (lineItem.accountCode === null) {
|
||||
this.account = lineItem.account;
|
||||
this.isAccountConfirmed = true;
|
||||
if (this.account === null) {
|
||||
this.#accountControl.classList.remove("accounting-not-empty");
|
||||
} else {
|
||||
this.#accountControl.classList.add("accounting-not-empty");
|
||||
}
|
||||
this.accountCode = lineItem.accountCode;
|
||||
this.accountText = lineItem.accountText;
|
||||
this.#accountText.innerText = this.accountText;
|
||||
this.#accountText.innerText = this.account.text;
|
||||
this.#amountInput.value = lineItem.amount === null? "": String(lineItem.amount);
|
||||
const maxAmount = this.#getMaxAmount();
|
||||
this.#amountInput.max = maxAmount === null? "": maxAmount;
|
||||
@ -596,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
|
||||
*/
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
/* The Mia! Accounting Project
|
||||
* account-form.js: The JavaScript for the account form
|
||||
*/
|
||||
|
||||
@ -944,12 +944,6 @@ class RecurringAccountSelector {
|
||||
*/
|
||||
class RecurringAccount {
|
||||
|
||||
/**
|
||||
* The account selector for the recurring item editor
|
||||
* @type {RecurringAccountSelector}
|
||||
*/
|
||||
#selector;
|
||||
|
||||
/**
|
||||
* The element
|
||||
* @type {HTMLLIElement}
|
||||
@ -981,13 +975,12 @@ class RecurringAccount {
|
||||
* @param element {HTMLLIElement} the element
|
||||
*/
|
||||
constructor(selector, element) {
|
||||
this.#selector = selector;
|
||||
this.#element = element;
|
||||
this.code = element.dataset.code;
|
||||
this.text = element.dataset.text;
|
||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||
|
||||
this.#element.onclick = () => this.#selector.editor.saveAccount(this);
|
||||
this.#element.onclick = () => selector.editor.saveAccount(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
/* The Mia! Accounting Project
|
||||
* original-line-item-selector.js: The JavaScript for the original line item selector
|
||||
*/
|
||||
|
||||
@ -194,10 +194,10 @@ class OriginalLineItemSelector {
|
||||
class OriginalLineItem {
|
||||
|
||||
/**
|
||||
* The original line item selector
|
||||
* @type {OriginalLineItemSelector}
|
||||
* The journal entry form
|
||||
* @type {JournalEntryForm}
|
||||
*/
|
||||
#selector;
|
||||
#form;
|
||||
|
||||
/**
|
||||
* The element
|
||||
@ -230,16 +230,10 @@ class OriginalLineItem {
|
||||
#currencyCode;
|
||||
|
||||
/**
|
||||
* The account code
|
||||
* @type {string}
|
||||
* The account
|
||||
* @type {JournalEntryAccount}
|
||||
*/
|
||||
accountCode;
|
||||
|
||||
/**
|
||||
* The account text
|
||||
* @type {string}
|
||||
*/
|
||||
accountText;
|
||||
account;
|
||||
|
||||
/**
|
||||
* The description
|
||||
@ -284,21 +278,20 @@ class OriginalLineItem {
|
||||
* @param element {HTMLLIElement} the element
|
||||
*/
|
||||
constructor(selector, element) {
|
||||
this.#selector = selector;
|
||||
this.#form = selector.lineItemEditor.form;
|
||||
this.#element = element;
|
||||
this.id = element.dataset.id;
|
||||
this.date = element.dataset.date;
|
||||
this.#debitCredit = element.dataset.debitCredit;
|
||||
this.#currencyCode = element.dataset.currencyCode;
|
||||
this.accountCode = element.dataset.accountCode;
|
||||
this.accountText = element.dataset.accountText;
|
||||
this.account = new JournalEntryAccount(element.dataset.accountCode, element.dataset.accountText, false);
|
||||
this.description = element.dataset.description;
|
||||
this.bareNetBalance = new Decimal(element.dataset.netBalance);
|
||||
this.netBalance = this.bareNetBalance;
|
||||
this.netBalanceText = document.getElementById(`accounting-original-line-item-selector-option-${this.id}-net-balance`);
|
||||
this.text = element.dataset.text;
|
||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||
this.#element.onclick = () => this.#selector.lineItemEditor.saveOriginalLineItem(this);
|
||||
this.#element.onclick = () => selector.lineItemEditor.saveOriginalLineItem(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -339,7 +332,7 @@ class OriginalLineItem {
|
||||
*/
|
||||
isMatched(debitCredit, currencyCode, query = null) {
|
||||
return this.netBalance.greaterThan(0)
|
||||
&& this.date <= this.#selector.lineItemEditor.form.date
|
||||
&& this.date <= this.#form.date
|
||||
&& this.#isDebitCreditMatched(debitCredit)
|
||||
&& this.#currencyCode === currencyCode
|
||||
&& this.#isQueryMatched(query);
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
/* The Mia! Accounting Project
|
||||
* period-chooser.js: The JavaScript for the period chooser
|
||||
*/
|
||||
|
||||
@ -181,7 +181,8 @@ class MonthTab extends TabPlane {
|
||||
});
|
||||
monthChooser.addEventListener(tempusDominus.Namespace.events.change, (e) => {
|
||||
const date = e.detail.date;
|
||||
const period = `${date.year}-${`0${date.month + 1}`.slice(-2)}`;
|
||||
const zeroPaddedMonth = `0${date.month + 1}`.slice(-2)
|
||||
const period = `${date.year}-${zeroPaddedMonth}`;
|
||||
window.location = chooser.modal.dataset.urlTemplate
|
||||
.replaceAll("PERIOD", period);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# The Mia! Accounting Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
|
@ -1,5 +1,5 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
The Mia! Accounting Project
|
||||
create.html: The account creation form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
@ -1,5 +1,5 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
The Mia! Accounting Project
|
||||
detail.html: The account detail
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
@ -1,5 +1,5 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
The Mia! Accounting Project
|
||||
edit.html: The account edit form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
@ -1,5 +1,5 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
The Mia! Accounting Project
|
||||
form.html: The account form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
@ -1,5 +1,5 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
The Mia! Accounting Project
|
||||
list.html: The account list
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
@ -1,5 +1,5 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
The Mia! Accounting Project
|
||||
order.html: The order of the accounts under a same base account
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user