From 14638f574ed61da192a4ac4831965938eb133bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BE=9D=E7=91=AA=E8=B2=93?= Date: Sun, 29 Jan 2023 22:28:27 +0800 Subject: [PATCH] Added the initial application with the main account list, the pagination, the query, the permission, the localization, the documentation, the test case, and a test demonstration site. --- LICENSE | 202 +++++ MANIFEST.in | 28 + README.rst | 50 ++ docs/Makefile | 20 + docs/make.bat | 35 + docs/source/_static/.keep | 0 docs/source/_templates/.keep | 0 docs/source/accounting.base_account.rst | 53 ++ docs/source/accounting.rst | 30 + docs/source/accounting.utils.rst | 37 + docs/source/conf.py | 32 + docs/source/index.rst | 20 + docs/source/modules.rst | 7 + pyproject.toml | 20 + setup.cfg | 56 ++ src/accounting/__init__.py | 57 ++ src/accounting/base_account/__init__.py | 34 + src/accounting/base_account/commands.py | 709 ++++++++++++++++++ src/accounting/base_account/models.py | 73 ++ src/accounting/base_account/query.py | 44 ++ src/accounting/base_account/views.py | 41 + src/accounting/database.py | 38 + src/accounting/locale.py | 114 +++ .../accounting/base-account/list.html | 58 ++ src/accounting/templates/accounting/base.html | 27 + .../templates/accounting/include/nav.html | 37 + .../accounting/include/pagination.html | 56 ++ src/accounting/translations/babel.cfg | 3 + .../zh_Hant/LC_MESSAGES/accounting.po | 46 ++ src/accounting/utils/__init__.py | 21 + src/accounting/utils/pagination.py | 240 ++++++ src/accounting/utils/permission.py | 100 +++ src/accounting/utils/query.py | 44 ++ tests/babel-utils-testsite.py | 133 ++++ tests/babel-utils.py | 133 ++++ tests/test_base_account.py | 113 +++ tests/testlib.py | 56 ++ tests/testsite/__init__.py | 96 +++ tests/testsite/auth.py | 92 +++ tests/testsite/locale.py | 97 +++ tests/testsite/templates/base.html | 134 ++++ tests/testsite/templates/home.html | 24 + tests/testsite/templates/login.html | 35 + tests/testsite/translations/babel.cfg | 3 + .../zh_Hant/LC_MESSAGES/messages.po | 54 ++ 45 files changed, 3302 insertions(+) create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/_static/.keep create mode 100644 docs/source/_templates/.keep create mode 100644 docs/source/accounting.base_account.rst create mode 100644 docs/source/accounting.rst create mode 100644 docs/source/accounting.utils.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 docs/source/modules.rst create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 src/accounting/__init__.py create mode 100644 src/accounting/base_account/__init__.py create mode 100644 src/accounting/base_account/commands.py create mode 100644 src/accounting/base_account/models.py create mode 100644 src/accounting/base_account/query.py create mode 100644 src/accounting/base_account/views.py create mode 100644 src/accounting/database.py create mode 100644 src/accounting/locale.py create mode 100644 src/accounting/templates/accounting/base-account/list.html create mode 100644 src/accounting/templates/accounting/base.html create mode 100644 src/accounting/templates/accounting/include/nav.html create mode 100644 src/accounting/templates/accounting/include/pagination.html create mode 100644 src/accounting/translations/babel.cfg create mode 100644 src/accounting/translations/zh_Hant/LC_MESSAGES/accounting.po create mode 100644 src/accounting/utils/__init__.py create mode 100644 src/accounting/utils/pagination.py create mode 100644 src/accounting/utils/permission.py create mode 100644 src/accounting/utils/query.py create mode 100755 tests/babel-utils-testsite.py create mode 100755 tests/babel-utils.py create mode 100644 tests/test_base_account.py create mode 100644 tests/testlib.py create mode 100644 tests/testsite/__init__.py create mode 100644 tests/testsite/auth.py create mode 100644 tests/testsite/locale.py create mode 100644 tests/testsite/templates/base.html create mode 100644 tests/testsite/templates/home.html create mode 100644 tests/testsite/templates/login.html create mode 100644 tests/testsite/translations/babel.cfg create mode 100644 tests/testsite/translations/zh_Hant/LC_MESSAGES/messages.po diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..afb6b5d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,28 @@ +# 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. + +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/* +include tests/testsite/* +include tests/testsite/templates/* +include tests/testsite/translations/* +include tests/testsite/translations/*/LC_MESSAGES/* diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..8eaf9bd --- /dev/null +++ b/README.rst @@ -0,0 +1,50 @@ +===================== +Mia! Accounting Flask +===================== + + +Description +=========== + +This is the Mia! Accounting Flask project. It is an accounting +module for the Flask_ applications. + + +Install +======= + +Install the latest source from the +`Mia! Accounting Flask repository`_. + +:: + + pip install git+https://gitea.imacat.idv.tw/imacat/mia-accounting-flask.git + + +Copyright +========= + + 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. + + +Authors +======= + +| imacat +| 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 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/_static/.keep b/docs/source/_static/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/source/_templates/.keep b/docs/source/_templates/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/source/accounting.base_account.rst b/docs/source/accounting.base_account.rst new file mode 100644 index 0000000..5c7cb44 --- /dev/null +++ b/docs/source/accounting.base_account.rst @@ -0,0 +1,53 @@ +accounting.base\_account package +================================ + +Submodules +---------- + +accounting.base\_account.commands module +---------------------------------------- + +.. automodule:: accounting.base_account.commands + :members: + :undoc-members: + :show-inheritance: + +accounting.base\_account.database module +---------------------------------------- + +.. automodule:: accounting.base_account.database + :members: + :undoc-members: + :show-inheritance: + +accounting.base\_account.models module +-------------------------------------- + +.. automodule:: accounting.base_account.models + :members: + :undoc-members: + :show-inheritance: + +accounting.base\_account.query module +------------------------------------- + +.. automodule:: accounting.base_account.query + :members: + :undoc-members: + :show-inheritance: + +accounting.base\_account.views module +------------------------------------- + +.. automodule:: accounting.base_account.views + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: accounting.base_account + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/accounting.rst b/docs/source/accounting.rst new file mode 100644 index 0000000..b806e9c --- /dev/null +++ b/docs/source/accounting.rst @@ -0,0 +1,30 @@ +accounting package +================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + accounting.base_account + accounting.utils + +Submodules +---------- + +accounting.locale module +------------------------ + +.. automodule:: accounting.locale + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: accounting + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/accounting.utils.rst b/docs/source/accounting.utils.rst new file mode 100644 index 0000000..1a2d854 --- /dev/null +++ b/docs/source/accounting.utils.rst @@ -0,0 +1,37 @@ +accounting.utils package +======================== + +Submodules +---------- + +accounting.utils.pagination module +---------------------------------- + +.. automodule:: accounting.utils.pagination + :members: + :undoc-members: + :show-inheritance: + +accounting.utils.permission module +---------------------------------- + +.. automodule:: accounting.utils.permission + :members: + :undoc-members: + :show-inheritance: + +accounting.utils.query module +----------------------------- + +.. automodule:: accounting.utils.query + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: accounting.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..7d79f32 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,32 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html +import os +import sys + +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' +copyright = '2023, imacat' +author = 'imacat' +release = '0.0.0' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ['sphinx.ext.autodoc'] + +templates_path = ['_templates'] +exclude_patterns = [] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'nature' +html_static_path = ['_static'] diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..3cf6286 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,20 @@ +.. Mia! Accounting Flask 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! +================================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/modules.rst b/docs/source/modules.rst new file mode 100644 index 0000000..ffaa717 --- /dev/null +++ b/docs/source/modules.rst @@ -0,0 +1,7 @@ +src +=== + +.. toctree:: + :maxdepth: 4 + + accounting diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3ca8701 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +# The Mia! Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21 + +# Copyright (c) 2022 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. + +[build-system] +requires = ["setuptools>=42"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..95781f8 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,56 @@ +# 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.0.0 +author = imacat +author_email = imacat@mail.imacat.idv.tw +description = The Mia! Accounting Flask project. +long_description = file: README.rst +long_description_content_type = text/x-rst +url = https://github.com/imacat/mia-accounting-flask +project_urls = + Bug Tracker = https://github.com/imacat/mia-accounting-flask/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Framework :: Flask + Topic :: Office/Business :: Financial :: Accounting + +[options] +package_dir = + = src +python_requires = >=3.10 +install_requires = + flask + Flask-SQLAlchemy + Flask-WTF + Flask-Babel >= 3 + Flask-Babel-JS +tests_require = + unittest + httpx + OpenCC + +[options.package_data] +accounting = + templates/** + translations/*/LC_MESSAGES/*.mo +accounting.base_account = + templates/** diff --git a/src/accounting/__init__.py b/src/accounting/__init__.py new file mode 100644 index 0000000..203dc2f --- /dev/null +++ b/src/accounting/__init__.py @@ -0,0 +1,57 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The accounting application. + +""" +import typing as t + +from flask import Flask, Blueprint + + +def init_app(app: Flask, url_prefix: str = "/accounting", + can_view_func: t.Callable[[], bool] | None = None, + can_edit_func: t.Callable[[], bool] | None = None) -> None: + """Initialize the application. + + :param app: The Flask application. + :param url_prefix: The URL prefix of the accounting application. + :param can_view_func: A callback that returns whether the current user can + view the accounting data. + :param can_edit_func: A callback that returns whether the current user can + edit the accounting data. + :return: None. + """ + # The database instance must be set before loading everything + # in the application. + from .database import set_db + set_db(app.extensions["sqlalchemy"]) + + bp: Blueprint = Blueprint("accounting", __name__, + url_prefix=url_prefix, + template_folder="templates", + static_folder="static") + + from . import locale + locale.init_app(app, bp) + + from .utils import permission + permission.init_app(app, can_view_func, can_edit_func) + + from . import base_account + base_account.init_app(app, bp) + + app.register_blueprint(bp) diff --git a/src/accounting/base_account/__init__.py b/src/accounting/base_account/__init__.py new file mode 100644 index 0000000..c401932 --- /dev/null +++ b/src/accounting/base_account/__init__.py @@ -0,0 +1,34 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The base account management. + +""" +from flask import Flask, Blueprint + + +def init_app(app: Flask, bp: Blueprint) -> None: + """Initialize the application. + + :param bp: The blueprint of the accounting application. + :param app: The Flask application. + :return: None. + """ + from .views import bp as base_account_bp + bp.register_blueprint(base_account_bp, url_prefix="/base-accounts") + + from .commands import init_base_accounts_command + app.cli.add_command(init_base_accounts_command) diff --git a/src/accounting/base_account/commands.py b/src/accounting/base_account/commands.py new file mode 100644 index 0000000..a940297 --- /dev/null +++ b/src/accounting/base_account/commands.py @@ -0,0 +1,709 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The console commands for the base account management. + +""" +import click +from flask.cli import with_appcontext + +from accounting.database import db +from .models import BaseAccount, BaseAccountL10n + +BaseAccountData = tuple[int, str, str, str] +"""The format of the base account data, as a list of (code, English, +Traditional Chinese, Simplified Chinese) tuples.""" + + +@click.command("accounting-init-base") +@with_appcontext +def init_base_accounts_command() -> None: + """Initializes the base accounts.""" + if BaseAccount.query.first() is not None: + click.echo("Base accounts already exist.") + raise click.Abort + + db.session.bulk_save_objects( + [BaseAccount(code=str(x[0]), title_l10n=x[1]) for x in DATA]) + db.session.bulk_save_objects( + [BaseAccountL10n(account_code=x[0], locale=y[0], title=y[1]) + for x in DATA for y in (("zh_Hant", x[2]), ("zh_Hans", x[3]))]) + db.session.commit() + click.echo("Base accounts initialized.") + + +DATA: list[BaseAccountData] = [ + (1, "assets", "資產", "资产"), + (2, "liabilities", "負債", "负债"), + (3, "owners’ equity", "業主權益", "业主权益"), + (4, "operating revenue", "營業收入", "营业收入"), + (5, "operating costs", "營業成本", "营业成本"), + (6, "operating expenses", "營業費用", "营业费用"), + (7, "non-operating revenue and expenses, other income (expense)", + "營業外收入及費用", "营业外收入及费用"), + (8, "income tax expense (or benefit)", "所得稅費用(或利益)", + "所得税费用(或利益)"), + (9, "nonrecurring gain or loss", "非經常營業損益", "非经常营业损益"), + (11, "current assets", "流動資產", "流动资产"), + (12, "current assets", "流動資產", "流动资产"), + (13, "funds and long-term investments", "基金及長期投資", "基金及长期投资"), + (14, "property , plant, and equipment", "固定資產", "固定资产"), + (15, "property , plant, and equipment", "固定資產", "固定资产"), + (16, "depletable assets", "遞耗資產", "递耗资产"), + (17, "intangible assets", "無形資產", "无形资产"), + (18, "other assets", "其他資產", "其他资产"), + (21, "current liabilities", "流動負債", "流动负债"), + (22, "current liabilities", "流動負債", "流动负债"), + (23, "long-term liabilities", "長期負債", "长期负债"), + (28, "other liabilities", "其他負債", "其他负债"), + (31, "capital", "資本", "资本"), + (32, "additional paid-in capital", "資本公積", "资本公积"), + (33, "retained earnings (accumulated deficit)", "保留盈餘(或累積虧損)", + "保留盈余(或累积亏损)"), + (34, "equity adjustments", "權益調整", "权益调整"), + (35, "treasury stock", "庫藏股", "库藏股"), + (36, "minority interest", "少數股權", "少数股权"), + (41, "sales revenue", "銷貨收入", "销货收入"), + (46, "service revenue", "勞務收入", "劳务收入"), + (47, "agency revenue", "業務收入", "业务收入"), + (48, "other operating revenue", "其他營業收入", "其他营业收入"), + (51, "cost of goods sold", "銷貨成本", "销货成本"), + (56, "service costs", "勞務成本", "劳务成本"), + (57, "agency costs", "業務成本", "业务成本"), + (58, "other operating costs", "其他營業成本", "其他营业成本"), + (61, "selling expenses", "推銷費用", "推销费用"), + (62, "general & administrative expenses", "管理及總務費用", "管理及总务费用"), + (63, "research and development expenses", "研究發展費用", "研究发展费用"), + (71, "non-operating revenue", "營業外收入", "营业外收入"), + (72, "non-operating revenue", "營業外收入", "营业外收入"), + (73, "non-operating revenue", "營業外收入", "营业外收入"), + (74, "non-operating revenue", "營業外收入", "营业外收入"), + (75, "non-operating expenses", "營業外費用", "营业外费用"), + (76, "non-operating expenses", "營業外費用", "营业外费用"), + (77, "non-operating expenses", "營業外費用", "营业外费用"), + (78, "non-operating expenses", "營業外費用", "营业外费用"), + (81, "income tax expense (or benefit)", "所得稅費用(或利益)", + "所得税费用(或利益)"), + (91, "gain (loss) from discontinued operations", "停業部門損益", + "停业部门损益"), + (92, "extraordinary gain or loss", "非常損益", "非常损益"), + (93, "cumulative effect of changes in accounting principles", + "會計原則變動累積影響數", "会计原则变动累积影响数"), + (94, "minority interest income", "少數股權淨利", "少数股权净利"), + (111, "cash and cash equivalents", "現金及約當現金", "现金及约当现金"), + (112, "short-term investments", "短期投資", "短期投资"), + (113, "notes receivable", "應收票據", "应收票据"), + (114, "accounts receivable", "應收帳款", "应收帐款"), + (118, "other receivables", "其他應收款", "其他应收款"), + (121, "inventories", "存貨", "存货"), + (122, "inventories", "存貨", "存货"), + (125, "prepaid expenses", "預付費用", "预付费用"), + (126, "prepayments", "預付款項", "预付款项"), + (128, "other current assets", "其他流動資產", "其他流动资产"), + (129, "other current assets", "其他流動資產", "其他流动资产"), + (131, "funds", "基金", "基金"), + (132, "long-term investments", "長期投資", "长期投资"), + (141, "land", "土地", "土地"), + (142, "land improvements", "土地改良物", "土地改良物"), + (143, "buildings", "房屋及建物", "房屋及建物"), + (144, "machinery and equipment", "機(器)具及設備", "机(器)具及设备"), + (145, "machinery and equipment", "機(器)具及設備", "机(器)具及设备"), + (146, "machinery and equipment", "機(器)具及設備", "机(器)具及设备"), + (151, "leased assets", "租賃資產", "租赁资产"), + (152, "leasehold improvements", "租賃權益改良", "租赁权益改良"), + (156, "construction in progress and prepayments for equipment", + "未完工程及預付購置設備款", "未完工程及预付购置设备款"), + (158, "miscellaneous property, plant, and equipment", "雜項固定資產", + "杂项固定资产"), + (161, "depletable assets", "遞耗資產", "递耗资产"), + (171, "trademarks", "商標權", "商标权"), + (172, "patents", "專利權", "专利权"), + (173, "franchise", "特許權", "特许权"), + (174, "copyright", "著作權", "著作权"), + (175, "computer software", "電腦軟體", "电脑软体"), + (176, "goodwill", "商譽", "商誉"), + (177, "organization costs", "開辦費", "开办费"), + (178, "other intangibles", "其他無形資產", "其他无形资产"), + (181, "deferred assets", "遞延資產", "递延资产"), + (182, "idle assets", "閒置資產", "闲置资产"), + (184, "long-term notes , accounts and overdue receivables", + "長期應收票據及款項與催收帳款", "长期应收票据及款项与催收帐款"), + (185, "assets leased to others", "出租資產", "出租资产"), + (186, "refundable deposit", "存出保證金", "存出保证金"), + (188, "miscellaneous assets", "雜項資產", "杂项资产"), + (211, "short-term borrowings (debt)", "短期借款", "短期借款"), + (212, "short-term notes and bills payable", "應付短期票券", "应付短期票券"), + (213, "notes payable", "應付票據", "应付票据"), + (214, "accounts pay able", "應付帳款", "应付帐款"), + (216, "income taxes payable", "應付所得稅", "应付所得税"), + (217, "accrued expenses", "應付費用", "应付费用"), + (218, "other payables", "其他應付款", "其他应付款"), + (219, "other payables", "其他應付款", "其他应付款"), + (226, "advance receipts", "預收款項", "预收款项"), + (227, "long-term liabilities -current portion", + "一年或一營業週期內到期長期負債", "一年或一营业周期内到期长期负债"), + (228, "other current liabilities", "其他流動負債", + "其他流动负债"), + (229, "other current liabilities", "其他流動負債", + "其他流动负债"), + (231, "corporate bonds payable", "應付公司債", "应付公司债"), + (232, "long-term loans payable", "長期借款", "长期借款"), + (233, "long-term notes and accounts payable", "長期應付票據及款項", + "长期应付票据及款项"), + (234, "accrued liabilities for land value increment tax", + "估計應付土地增值稅", "估计应付土地增值税"), + (235, "accrued pension liabilities", "應計退休金負債", "应计退休金负债"), + (238, "other long-term liabilities", "其他長期負債", "其他长期负债"), + (281, "deferred liabilities", "遞延負債", "递延负债"), + (286, "deposits received", "存入保證金", "存入保证金"), + (288, "miscellaneous liabilities", "雜項負債", "杂项负债"), + (311, "capital", "資本(或股本)", "资本(或股本)"), + (321, "paid-in capital in excess of par", "股票溢價", "股票溢价"), + (323, "capital surplus from assets revaluation", "資產重估增值準備", + "资产重估增值准备"), + (324, "capital surplus from gain on disposal of assets", "處分資產溢價公積", + "处分资产溢价公积"), + (325, "capital surplus from business combination", "合併公積", "合并公积"), + (326, "donated surplus", "受贈公積", "受赠公积"), + (328, "other additional paid-in capital", "其他資本公積", "其他资本公积"), + (331, "legal reserve", "法定盈餘公積", "法定盈余公积"), + (332, "special reserve", "特別盈餘公積", "特别盈余公积"), + (335, "retained earnings-unappropriated (or accumulated deficit)", + "未分配盈餘(或累積虧損)", "未分配盈余(或累积亏损)"), + (341, + "unrealized loss on market value decline of long-term equity investments", + "長期股權投資未實現跌價損失", "长期股权投资未实现跌价损失"), + (342, "cumulative translation adjustment", "累積換算調整數", "累积换算调整数"), + (343, "net loss not recognized as pension cost", "未認列為退休金成本之淨損失", + "未认列为退休金成本之净损失"), + (351, "treasury stock", "庫藏股", "库藏股"), + (361, "minority interest", "少數股權", "少数股权"), + (411, "sales revenue", "銷貨收入", "销货收入"), + (417, "sales return", "銷貨退回", "销货退回"), + (419, "sales allowances", "銷貨折讓", "销货折让"), + (461, "service revenue", "勞務收入", "劳务收入"), + (471, "agency revenue", "業務收入", "业务收入"), + (488, "other operating revenue", "其他營業收入—其他", "其他营业收入—其他"), + (511, "cost of goods sold", "銷貨成本", "销货成本"), + (512, "purchases", "進貨", "进货"), + (513, "materials purchased", "進料", "进料"), + (514, "direct labor", "直接人工", "直接人工"), + (515, "manufacturing overhead", "製造費用", "制造费用"), + (516, "manufacturing overhead", "製造費用", "制造费用"), + (517, "manufacturing overhead", "製造費用", "制造费用"), + (518, "manufacturing overhead", "製造費用", "制造费用"), + (561, "service costs", "勞務成本", "劳务成本"), + (571, "agency costs", "業務成本", "业务成本"), + (588, "other operating costs-other", "其他營業成本—其他", "其他营业成本—其他"), + (615, "selling expenses", "推銷費用", "推销费用"), + (616, "selling expenses", "推銷費用", "推销费用"), + (617, "selling expenses", "推銷費用", "推销费用"), + (618, "selling expenses", "推銷費用", "推销费用"), + (625, "general & administrative expenses", "管理及總務費用", "管理及总务费用"), + (626, "general & administrative expenses", "管理及總務費用", "管理及总务费用"), + (627, "general & administrative expenses", "管理及總務費用", "管理及总务费用"), + (628, "general & administrative expenses", "管理及總務費用", "管理及总务费用"), + (635, "research and development expenses", "研究發展費用", "研究发展费用"), + (636, "research and development expenses", "研究發展費用", "研究发展费用"), + (637, "research and development expenses", "研究發展費用", "研究发展费用"), + (638, "research and development expenses", "研究發展費用", "研究发展费用"), + (711, "interest revenue", "利息收入", "利息收入"), + (712, "investment income", "投資收益", "投资收益"), + (713, "foreign exchange gain", "兌換利益", "兑换利益"), + (714, "gain on disposal of investments", "處分投資收益", "处分投资收益"), + (715, "gain on disposal of assets", "處分資產溢價收入", "处分资产溢价收入"), + (748, "other non-operating revenue", "其他營業外收入", "其他营业外收入"), + (751, "interest expense", "利息費用", "利息费用"), + (752, "investment loss", "投資損失", "投资损失"), + (753, "foreign exchange loss", "兌換損失", "兑换损失"), + (754, "loss on disposal of investments", "處分投資損失", "处分投资损失"), + (755, "loss on disposal of assets", "處分資產損失", "处分资产损失"), + (788, "other non-operating expenses", "其他營業外費用", "其他营业外费用"), + (811, "income tax expense (or benefit)", "所得稅費用(或利益)", + "所得税费用(或利益)"), + (911, "income (loss) from operations of discontinued segments", + "停業部門損益—停業前營業損益", "停业部门损益—停业前营业损益"), + (912, "gain (loss) from disposal of discontinued segments", + "停業部門損益—處分損益", "停业部门损益—处分损益"), + (921, "extraordinary gain or loss", "非常損益", "非常损益"), + (931, "cumulative effect of changes in accounting principles", + "會計原則變動累積影響數", "会计原则变动累积影响数"), + (941, "minority interest income", "少數股權淨利", "少数股权净利"), + (1111, "cash on hand", "庫存現金", "库存现金"), + (1112, "petty cash/revolving funds", "零用金/週轉金", "零用金/周转金"), + (1113, "cash in banks", "銀行存款", "银行存款"), + (1116, "cash in transit", "在途現金", "在途现金"), + (1117, "cash equivalents", "約當現金", "约当现金"), + (1118, "other cash and cash equivalents", "其他現金及約當現金", + "其他现金及约当现金"), + (1121, "short-term investments – stock", "短期投資—股票", "短期投资—股票"), + (1122, "short-term investments – short-term notes and bills", + "短期投資—短期票券", "短期投资—短期票券"), + (1123, "short-term investments – government bonds", "短期投資—政府債券", + "短期投资—政府债券"), + (1124, "short-term investments – beneficiary certificates", + "短期投資—受益憑證", "短期投资—受益凭证"), + (1125, "short-term investments – corporate bonds", "短期投資—公司債", + "短期投资—公司债"), + (1128, "short-term investments – other", "短期投資—其他", "短期投资—其他"), + (1129, "allowance for reduction of short-term investment to market", + "備抵短期投資跌價損失", "备抵短期投资跌价损失"), + (1131, "notes receivable", "應收票據", "应收票据"), + (1132, "discounted notes receivable", "應收票據貼現", "应收票据贴现"), + (1137, "notes receivable – related parties", "應收票據—關係人", + "应收票据—关系人"), + (1138, "other notes receivable", "其他應收票據", "其他应收票据"), + (1139, "allowance for uncollectible accounts – notes receivable", + "備抵呆帳-應收票據", "备抵呆帐-应收票据"), + (1141, "accounts receivable", "應收帳款", "应收帐款"), + (1142, "installment accounts receivable", "應收分期帳款", + "应收分期帐款"), + (1147, "accounts receivable – related parties", "應收帳款—關係人", + "应收帐款—关系人"), + (1149, "allowance for uncollectible accounts – accounts receivable", + "備抵呆帳-應收帳款", "备抵呆帐-应收帐款"), + (1181, "forward exchange contract receivable", "應收出售遠匯款", + "应收出售远汇款"), + (1182, "forward exchange contract receivable – foreign currencies", + "應收遠匯款—外幣", "应收远汇款—外币"), + (1183, "discount on forward ex-change contract", "買賣遠匯折價", + "买卖远汇折价"), + (1184, "earned revenue receivable", "應收收益", "应收收益"), + (1185, "income tax refund receivable", "應收退稅款", "应收退税款"), + (1187, "other receivables – related parties", "其他應收款—關係人", + "其他应收款—关系人"), + (1188, "other receivables – other", "其他應收款—其他", "其他应收款—其他"), + (1189, "allowance for uncollectible accounts – other receivables", + "備抵呆帳—其他應收款", "备抵呆帐—其他应收款"), + (1211, "merchandise inventory", "商品存貨", "商品存货"), + (1212, "consigned goods", "寄銷商品", "寄销商品"), + (1213, "goods in transit", "在途商品", "在途商品"), + (1219, "allowance for reduction of inventory to market", "備抵存貨跌價損失", + "备抵存货跌价损失"), + (1221, "finished goods", "製成品", "制成品"), + (1222, "consigned finished goods", "寄銷製成品", "寄销制成品"), + (1223, "by-products", "副產品", "副产品"), + (1224, "work in process", "在製品", "在制品"), + (1225, "work in process – outsourced", "委外加工", "委外加工"), + (1226, "raw materials", "原料", "原料"), + (1227, "supplies", "物料", "物料"), + (1228, "materials and supplies in transit", "在途原物料", "在途原物料"), + (1229, "allowance for reduction of inventory to market", "備抵存貨跌價損失", + "备抵存货跌价损失"), + (1251, "prepaid payroll", "預付薪資", "预付薪资"), + (1252, "prepaid rents", "預付租金", "预付租金"), + (1253, "prepaid insurance", "預付保險費", "预付保险费"), + (1254, "office supplies", "用品盤存", "用品盘存"), + (1255, "prepaid income tax", "預付所得稅", "预付所得税"), + (1258, "other prepaid expenses", "其他預付費用", "其他预付费用"), + (1261, "prepayment for purchases", "預付貨款", "预付货款"), + (1268, "other prepayments", "其他預付款項", "其他预付款项"), + (1281, "VAT paid ( or input tax)", "進項稅額", "进项税额"), + (1282, "excess VAT paid (or overpaid VAT)", "留抵稅額", "留抵税额"), + (1283, "temporary payments", "暫付款", "暂付款"), + (1284, "payment on behalf of others", "代付款", "代付款"), + (1285, "advances to employees", "員工借支", "员工借支"), + (1286, "refundable deposits", "存出保證金", "存出保证金"), + (1287, "certificate of deposit-restricted", "受限制存款", "受限制存款"), + (1291, "deferred income tax assets", "遞延所得稅資產", "递延所得税资产"), + (1292, "deferred foreign exchange losses", "遞延兌換損失", "递延兑换损失"), + (1293, "owners’ (stockholders’) current account", "業主(股東)往來", + "业主(股东)往来"), + (1294, "current account with others", "同業往來", "同业往来"), + (1298, "other current assets – other", "其他流動資產—其他", + "其他流动资产—其他"), + (1311, "redemption fund (or sinking fund)", "償債基金", "偿债基金"), + (1312, "fund for improvement and expansion", "改良及擴充基金", + "改良及扩充基金"), + (1313, "contingency fund", "意外損失準備基金", "意外损失准备基金"), + (1314, "pension fund", "退休基金", "退休基金"), + (1318, "other funds", "其他基金", "其他基金"), + (1321, "long-term equity investments", "長期股權投資", "长期股权投资"), + (1322, "long-term bond investments", "長期債券投資", "长期债券投资"), + (1323, "long-term real estate in-vestments", "長期不動產投資", + "长期不动产投资"), + (1324, "cash surrender value of life insurance", "人壽保險現金解約價值", + "人寿保险现金解约价值"), + (1328, "other long-term investments", "其他長期投資", "其他长期投资"), + (1329, + "allowance for excess of cost over market value of long-term investments", + "備抵長期投資跌價損失", "备抵长期投资跌价损失"), + (1411, "land", "土地", "土地"), + (1418, "land – revaluation increments", "土地—重估增值", "土地—重估增值"), + (1421, "land improvements", "土地改良物", "土地改良物"), + (1428, "land improvements – revaluation increments", "土地改良物—重估增值", + "土地改良物—重估增值"), + (1429, "accumulated depreciation – land improvements", "累積折舊—土地改良物", + "累积折旧—土地改良物"), + (1431, "buildings", "房屋及建物", "房屋及建物"), + (1438, "buildings –revaluation increments", "房屋及建物—重估增值", + "房屋及建物—重估增值"), + (1439, "accumulated depreciation – buildings", "累積折舊—房屋及建物", + "累积折旧—房屋及建物"), + (1441, "machinery", "機(器)具", "机(器)具"), + (1448, "machinery – revaluation increments", "機(器)具—重估增值", + "机(器)具—重估增值"), + (1449, "accumulated depreciation – machinery", "累積折舊—機(器)具", + "累积折旧—机(器)具"), + (1511, "leased assets", "租賃資產", "租赁资产"), + (1519, "accumulated depreciation – leased assets", "累積折舊—租賃資產", + "累积折旧—租赁资产"), + (1521, "leasehold improvements", "租賃權益改良", "租赁权益改良"), + (1529, "accumulated depreciation – leasehold improvements", + "累積折舊—租賃權益改良", "累积折旧—租赁权益改良"), + (1561, "construction in progress", "未完工程", "未完工程"), + (1562, "prepayment for equipment", "預付購置設備款", "预付购置设备款"), + (1581, "miscellaneous property, plant, and equipment", "雜項固定資產", + "杂项固定资产"), + (1588, + "miscellaneous property, plant, and equipment – revaluation increments", + "雜項固定資產—重估增值", "杂项固定资产—重估增值"), + (1589, + "accumulated depreciation – miscellaneous property, plant, and equipment", + "累積折舊—雜項固定資產", "累积折旧—杂项固定资产"), + (1611, "natural resources", "天然資源", "天然资源"), + (1618, "natural resources –revaluation increments", "天然資源—重估增值", + "天然资源—重估增值"), + (1619, "accumulated depletion – natural resources", "累積折耗—天然資源", + "累积折耗—天然资源"), + (1711, "trademarks", "商標權", "商标权"), + (1721, "patents", "專利權", "专利权"), + (1731, "franchise", "特許權", "特许权"), + (1741, "copyright", "著作權", "著作权"), + (1751, "computer software cost", "電腦軟體", "电脑软体"), + (1761, "goodwill", "商譽", "商誉"), + (1771, "organization costs", "開辦費", "开办费"), + (1781, "deferred pension costs", "遞延退休金成本", "递延退休金成本"), + (1782, "leasehold improvements", "租賃權益改良", "租赁权益改良"), + (1788, "other intangible assets – other", "其他無形資產—其他", + "其他无形资产—其他"), + (1811, "deferred bond issuance costs", "債券發行成本", "债券发行成本"), + (1812, "long-term prepaid rent", "長期預付租金", "长期预付租金"), + (1813, "long-term prepaid insurance", "長期預付保險費", "长期预付保险费"), + (1814, "deferred income tax assets", "遞延所得稅資產", "递延所得税资产"), + (1815, "prepaid pension cost", "預付退休金", "预付退休金"), + (1818, "other deferred assets", "其他遞延資產", "其他递延资产"), + (1821, "idle assets", "閒置資產", "闲置资产"), + (1841, "long-term notes receivable", "長期應收票據", "长期应收票据"), + (1842, "long-term accounts receivable", "長期應收帳款", "长期应收帐款"), + (1843, "overdue receivables", "催收帳款", "催收帐款"), + (1847, + "long-term notes, accounts and overdue receivables – related parties", + "長期應收票據及款項與催收帳款—關係人", "长期应收票据及款项与催收帐款—关系人"), + (1848, "other long-term receivables", "其他長期應收款項", "其他长期应收款项"), + (1849, + "allowance for uncollectible accounts – long-term notes, accounts and" + " overdue receivables", + "備抵呆帳—長期應收票據及款項與催收帳款", "备抵呆帐—长期应收票据及款项与催收帐款"), + (1851, "assets leased to others", "出租資產", "出租资产"), + (1858, "assets leased to others – incremental value from revaluation", + "出租資產—重估增值", "出租资产—重估增值"), + (1859, "accumulated depreciation – assets leased to others", + "累積折舊—出租資產", "累积折旧—出租资产"), + (1861, "refundable deposits", "存出保證金", "存出保证金"), + (1881, "certificate of deposit – restricted", "受限制存款", "受限制存款"), + (1888, "miscellaneous assets – other", "雜項資產—其他", "杂项资产—其他"), + (2111, "bank overdraft", "銀行透支", "银行透支"), + (2112, "bank loan", "銀行借款", "银行借款"), + (2114, "short-term borrowings – owners", "短期借款—業主", "短期借款—业主"), + (2115, "short-term borrowings – employees", "短期借款—員工", "短期借款—员工"), + (2117, "short-term borrowings – related parties", "短期借款—關係人", + "短期借款—关系人"), + (2118, "short-term borrowings – other", "短期借款—其他", "短期借款—其他"), + (2121, "commercial paper payable", "應付商業本票", "应付商业本票"), + (2122, "bank acceptance", "銀行承兌匯票", "银行承兑汇票"), + (2128, "other short-term notes and bills payable", "其他應付短期票券", + "其他应付短期票券"), + (2129, "discount on short-term notes and bills payable", "應付短期票券折價", + "应付短期票券折价"), + (2131, "notes payable", "應付票據", "应付票据"), + (2137, "notes payable – related parties", "應付票據—關係人", + "应付票据—关系人"), + (2138, "other notes payable", "其他應付票據", "其他应付票据"), + (2141, "accounts payable", "應付帳款", "应付帐款"), + (2147, "accounts payable – related parties", "應付帳款—關係人", + "应付帐款—关系人"), + (2161, "income tax payable", "應付所得稅", "应付所得税"), + (2171, "accrued payroll", "應付薪工", "应付薪工"), + (2172, "accrued rent payable", "應付租金", "应付租金"), + (2173, "accrued interest payable", "應付利息", "应付利息"), + (2174, "accrued VAT payable", "應付營業稅", "应付营业税"), + (2175, "accrued taxes payable – other", "應付稅捐—其他", "应付税捐—其他"), + (2178, "other accrued expenses payable", "其他應付費用", "其他应付费用"), + (2181, "forward exchange contract payable", "應付購入遠匯款", "应付购入远汇款"), + (2182, "forward exchange contract payable – foreign currencies", + "應付遠匯款—外幣", "应付远汇款—外币"), + (2183, "premium on forward exchange contract", "買賣遠匯溢價", "买卖远汇溢价"), + (2184, "payables on land and building purchased", "應付土地房屋款", + "应付土地房屋款"), + (2185, "Payables on equipment", "應付設備款", "应付设备款"), + (2187, "other payables – related parties", "其他應付款—關係人", + "其他应付款—关系人"), + (2191, "dividend payable", "應付股利", "应付股利"), + (2192, "bonus payable", "應付紅利", "应付红利"), + (2193, "compensation payable to directors and supervisors", "應付董監事酬勞", + "应付董监事酬劳"), + (2198, "other payables – other", "其他應付款—其他", "其他应付款—其他"), + (2261, "sales revenue received in advance", "預收貨款", "预收货款"), + (2262, "revenue received in advance", "預收收入", "预收收入"), + (2268, "other advance receipts", "其他預收款", "其他预收款"), + (2271, "corporate bonds payable – current portion", + "一年或一營業週期內到期公司債", "一年或一营业周期内到期公司债"), + (2272, "long-term loans payable – current portion", + "一年或一營業週期內到期長期借款", "一年或一营业周期内到期长期借款"), + (2273, + "long-term notes and accounts payable due within one year or one" + " operating cycle", + "一年或一營業週期內到期長期應付票據及款項", + "一年或一营业周期内到期长期应付票据及款项"), + (2277, + "long-term notes and accounts payables to related parties – current" + " portion", + "一年或一營業週期內到期長期應付票據及款項—關係人", + "一年或一营业周期内到期长期应付票据及款项—关系人"), + (2278, "other long-term liabilities – current portion", + "其他一年或一營業週期內到期長期負債", "其他一年或一营业周期内到期长期负债"), + (2281, "VAT received (or output tax)", "銷項稅額", "销项税额"), + (2283, "temporary receipts", "暫收款", "暂收款"), + (2284, "receipts under custody", "代收款", "代收款"), + (2285, "estimated warranty liabilities", "估計售後服務/保固負債", + "估计售后服务/保固负债"), + (2291, "deferred income tax liabilities", "遞延所得稅負債", "递延所得税负债"), + (2292, "deferred foreign exchange gain", "遞延兌換利益", "递延兑换利益"), + (2293, "owners’ current account", "業主(股東)往來", "业主(股东)往来"), + (2294, "current account with others", "同業往來", "同业往来"), + (2298, "other current liabilities – others", "其他流動負債—其他", + "其他流动负债—其他"), + (2311, "corporate bonds payable", "應付公司債", "应付公司债"), + (2319, "premium (discount) on corporate bonds payable", + "應付公司債溢(折)價", "应付公司债溢(折)价"), + (2321, "long-term loans payable – bank", "長期銀行借款", "长期银行借款"), + (2324, "long-term loans payable – owners", "長期借款—業主", "长期借款—业主"), + (2325, "long-term loans payable – employees", "長期借款—員工", + "长期借款—员工"), + (2327, "long-term loans payable – related parties", "長期借款—關係人", + "长期借款—关系人"), + (2328, "long-term loans payable – other", "長期借款—其他", "长期借款—其他"), + (2331, "long-term notes payable", "長期應付票據", "长期应付票据"), + (2332, "long-term accounts pay-able", "長期應付帳款", "长期应付帐款"), + (2333, "long-term capital lease liabilities", "長期應付租賃負債", + "长期应付租赁负债"), + (2337, "Long-term notes and accounts payable – related parties", + "長期應付票據及款項—關係人", "长期应付票据及款项—关系人"), + (2338, "other long-term payables", "其他長期應付款項", "其他长期应付款项"), + (2341, "estimated accrued land value incremental tax pay-able", + "估計應付土地增值稅", "估计应付土地增值税"), + (2351, "accrued pension liabilities", "應計退休金負債", "应计退休金负债"), + (2388, "other long-term liabilities – other", "其他長期負債—其他", + "其他长期负债—其他"), + (2811, "deferred revenue", "遞延收入", "递延收入"), + (2814, "deferred income tax liabilities", "遞延所得稅負債", "递延所得税负债"), + (2818, "other deferred liabilities", "其他遞延負債", "其他递延负债"), + (2861, "guarantee deposit received", "存入保證金", "存入保证金"), + (2888, "miscellaneous liabilities – other", "雜項負債—其他", "杂项负债—其他"), + (3111, "capital – common stock", "普通股股本", "普通股股本"), + (3112, "capital – preferred stock", "特別股股本", "特别股股本"), + (3113, "capital collected in advance", "預收股本", "预收股本"), + (3114, "stock dividends to be distributed", "待分配股票股利", + "待分配股票股利"), + (3115, "capital", "資本", "资本"), + (3211, "paid-in capital in excess of par- common stock", "普通股股票溢價", + "普通股股票溢价"), + (3212, "paid-in capital in excess of par- preferred stock", "特別股股票溢價", + "特别股股票溢价"), + (3231, "capital surplus from assets revaluation", "資產重估增值準備", + "资产重估增值准备"), + (3241, "capital surplus from gain on disposal of assets", "處分資產溢價公積", + "处分资产溢价公积"), + (3251, "capital surplus from business combination", "合併公積", "合并公积"), + (3261, "donated surplus", "受贈公積", "受赠公积"), + (3281, "additional paid-in capital from investee under equity method", + "權益法長期股權投資資本公積", "权益法长期股权投资资本公积"), + (3282, "additional paid-in capital – treasury stock trans-actions", + "資本公積—庫藏股票交易", "资本公积—库藏股票交易"), + (3311, "legal reserve", "法定盈餘公積", "法定盈余公积"), + (3321, "contingency reserve", "意外損失準備", "意外损失准备"), + (3322, "improvement and expansion reserve", "改良擴充準備", "改良扩充准备"), + (3323, "special reserve for redemption of liabilities", "償債準備", + "偿债准备"), + (3328, "other special reserve", "其他特別盈餘公積", "其他特别盈余公积"), + (3351, "accumulated profit or loss", "累積盈虧", "累积盈亏"), + (3352, "prior period adjustments", "前期損益調整", "前期损益调整"), + (3353, "net income or loss for current period", "本期損益", "本期损益"), + (3411, + "unrealized loss on market value decline of long-term equity investments", + "長期股權投資未實現跌價損失", "长期股权投资未实现跌价损失"), + (3421, "cumulative translation adjustments", "累積換算調整數", + "累积换算调整数"), + (3431, "net loss not recognized as pension costs", + "未認列為退休金成本之淨損失", "未认列为退休金成本之净损失"), + (3511, "treasury stock", "庫藏股", "库藏股"), + (3611, "minority interest", "少數股權", "少数股权"), + (4111, "sales revenue", "銷貨收入", "销货收入"), + (4112, "installment sales revenue", "分期付款銷貨收入", "分期付款销货收入"), + (4171, "sales return", "銷貨退回", "销货退回"), + (4191, "sales discounts and allowances", "銷貨折讓", "销货折让"), + (4611, "service revenue", "勞務收入", "劳务收入"), + (4711, "agency revenue", "業務收入", "业务收入"), + (4888, "other operating revenue – other", "其他營業收入—其他", + "其他营业收入—其他"), + (5111, "cost of goods sold", "銷貨成本", "销货成本"), + (5112, "installment cost of goods sold", "分期付款銷貨成本", + "分期付款销货成本"), + (5121, "purchases", "進貨", "进货"), + (5122, "purchase expenses", "進貨費用", "进货费用"), + (5123, "purchase returns", "進貨退出", "进货退出"), + (5124, "charges on purchased merchandise", "進貨折讓", "进货折让"), + (5131, "material purchased", "進料", "进料"), + (5132, "charges on purchased material", "進料費用", "进料费用"), + (5133, "material purchase returns", "進料退出", "进料退出"), + (5134, "material purchase allowances", "進料折讓", "进料折让"), + (5141, "direct labor", "直接人工", "直接人工"), + (5151, "indirect labor", "間接人工", "间接人工"), + (5152, "rent expense, rent", "租金支出", "租金支出"), + (5153, "office supplies (expense)", "文具用品", "文具用品"), + (5154, "travelling expense, travel", "旅費", "旅费"), + (5155, "shipping expenses, freight", "運費", "运费"), + (5156, "postage (expenses)", "郵電費", "邮电费"), + (5157, "repair (s) and maintenance (expense )", "修繕費", "修缮费"), + (5158, "packing expenses", "包裝費", "包装费"), + (5161, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"), + (5162, "insurance (expense)", "保險費", "保险费"), + (5163, "manufacturing overhead – outsourced", "加工費", "加工费"), + (5166, "taxes", "稅捐", "税捐"), + (5168, "depreciation expense", "折舊", "折旧"), + (5169, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"), + (5172, "meal (expenses)", "伙食費", "伙食费"), + (5173, "employee benefits/welfare", "職工福利", "职工福利"), + (5176, "training (expense)", "訓練費", "训练费"), + (5177, "indirect materials", "間接材料", "间接材料"), + (5188, "other manufacturing expenses", "其他製造費用", "其他制造费用"), + (5611, "service costs", "勞務成本", "劳务成本"), + (5711, "agency costs", "業務成本", "业务成本"), + (5888, "other operating costs – other", "其他營業成本—其他", + "其他营业成本—其他"), + (6151, "payroll expense", "薪資支出", "薪资支出"), + (6152, "rent expense, rent", "租金支出", "租金支出"), + (6153, "office supplies (expense)", "文具用品", "文具用品"), + (6154, "travelling expense, travel", "旅費", "旅费"), + (6155, "shipping expenses, freight", "運費", "运费"), + (6156, "postage (expenses)", "郵電費", "邮电费"), + (6157, "repair (s) and maintenance (expense)", "修繕費", "修缮费"), + (6159, "advertisement expense, advertisement", "廣告費", "广告费"), + (6161, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"), + (6162, "insurance (expense)", "保險費", "保险费"), + (6164, "entertainment (expense)", "交際費", "交际费"), + (6165, "donation (expense)", "捐贈", "捐赠"), + (6166, "taxes", "稅捐", "税捐"), + (6167, "loss on uncollectible accounts", "呆帳損失", "呆帐损失"), + (6168, "depreciation expense", "折舊", "折旧"), + (6169, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"), + (6172, "meal (expenses)", "伙食費", "伙食费"), + (6173, "employee benefits/welfare", "職工福利", "职工福利"), + (6175, "commission (expense)", "佣金支出", "佣金支出"), + (6176, "training (expense)", "訓練費", "训练费"), + (6188, "other selling expenses", "其他推銷費用", "其他推销费用"), + (6251, "payroll expense", "薪資支出", "薪资支出"), + (6252, "rent expense, rent", "租金支出", "租金支出"), + (6253, "office supplies", "文具用品", "文具用品"), + (6254, "travelling expense, travel", "旅費", "旅费"), + (6255, "shipping expenses,freight", "運費", "运费"), + (6256, "postage (expenses)", "郵電費", "邮电费"), + (6257, "repair (s) and maintenance (expense)", "修繕費", "修缮费"), + (6259, "advertisement expense, advertisement", "廣告費", "广告费"), + (6261, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"), + (6262, "insurance (expense)", "保險費", "保险费"), + (6264, "entertainment (expense)", "交際費", "交际费"), + (6265, "donation (expense)", "捐贈", "捐赠"), + (6266, "taxes", "稅捐", "税捐"), + (6267, "loss on uncollectible accounts", "呆帳損失", "呆帐损失"), + (6268, "depreciation expense", "折舊", "折旧"), + (6269, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"), + (6271, "loss on export sales", "外銷損失", "外销损失"), + (6272, "meal (expenses)", "伙食費", "伙食费"), + (6273, "employee benefits/welfare", "職工福利", "职工福利"), + (6274, "research and development expense", "研究發展費用", "研究发展费用"), + (6275, "commission (expense)", "佣金支出", "佣金支出"), + (6276, "training (expense)", "訓練費", "训练费"), + (6278, "professional service fees", "勞務費", "劳务费"), + (6288, "other general and administrative expenses", "其他管理及總務費用", + "其他管理及总务费用"), + (6351, "payroll expense", "薪資支出", "薪资支出"), + (6352, "rent expense, rent", "租金支出", "租金支出"), + (6353, "office supplies", "文具用品", "文具用品"), + (6354, "travelling expense, travel", "旅費", "旅费"), + (6355, "shipping expenses, freight", "運費", "运费"), + (6356, "postage (expenses)", "郵電費", "邮电费"), + (6357, "repair (s) and maintenance (expense)", "修繕費", "修缮费"), + (6361, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"), + (6362, "insurance (expense)", "保險費", "保险费"), + (6364, "entertainment (expense)", "交際費", "交际费"), + (6366, "taxes", "稅捐", "税捐"), + (6368, "depreciation expense", "折舊", "折旧"), + (6369, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"), + (6372, "meal (expenses)", "伙食費", "伙食费"), + (6373, "employee benefits/welfare", "職工福利", "职工福利"), + (6376, "training (expense)", "訓練費", "训练费"), + (6378, "other research and development expenses", "其他研究發展費用", + "其他研究发展费用"), + (7111, "interest revenue/income", "利息收入", "利息收入"), + (7121, "investment income recognized under equity method", + "權益法認列之投資收益", "权益法认列之投资收益"), + (7122, "dividends income", "股利收入", "股利收入"), + (7123, "gain on market price recovery of short-term investment", + "短期投資市價回升利益", "短期投资市价回升利益"), + (7131, "foreign exchange gain", "兌換利益", "兑换利益"), + (7141, "gain on disposal of investments", "處分投資收益", "处分投资收益"), + (7151, "gain on disposal of assets", "處分資產溢價收入", "处分资产溢价收入"), + (7481, "donation income", "捐贈收入", "捐赠收入"), + (7482, "rent revenue/income", "租金收入", "租金收入"), + (7483, "commission revenue/income", "佣金收入", "佣金收入"), + (7484, "revenue from sale of scraps", "出售下腳及廢料收入", + "出售下脚及废料收入"), + (7485, "gain on physical inventory", "存貨盤盈", "存货盘盈"), + (7486, "gain from price recovery of inventory", "存貨跌價回升利益", + "存货跌价回升利益"), + (7487, "gain on reversal of bad debts", "壞帳轉回利益", "坏帐转回利益"), + (7488, "other non-operating revenue – other items", "其他營業外收入—其他", + "其他营业外收入—其他"), + (7511, "interest expense", "利息費用", "利息费用"), + (7521, "investment loss recognized under equity method", + "權益法認列之投資損失", "权益法认列之投资损失"), + (7523, "unrealized loss on reduction of short-term investments to market", + "短期投資未實現跌價損失", "短期投资未实现跌价损失"), + (7531, "foreign exchange loss", "兌換損失", "兑换损失"), + (7541, "loss on disposal of investments", "處分投資損失", "处分投资损失"), + (7551, "loss on disposal of assets", "處分資產損失", "处分资产损失"), + (7881, "loss on work stoppages", "停工損失", "停工损失"), + (7882, "casualty loss", "災害損失", "灾害损失"), + (7885, "loss on physical inventory", "存貨盤損", "存货盘损"), + (7886, + "loss for market price decline and obsolete and slow-moving inventories", + "存貨跌價及呆滯損失", "存货跌价及呆滞损失"), + (7888, "other non-operating expenses – other", "其他營業外費用—其他", + "其他营业外费用—其他"), + (8111, "income tax expense ( or benefit)", "所得稅費用(或利益)", + "所得税费用(或利益)"), + (9111, "income (loss) from operations of discontinued segment", + "停業部門損益—停業前營業損益", "停业部门损益—停业前营业损益"), + (9121, "gain (loss) from disposal of discontinued segment", + "停業部門損益—處分損益", "停业部门损益—处分损益"), + (9211, "extraordinary gain or loss", "非常損益", "非常损益"), + (9311, "cumulative effect of changes in accounting principles", + "會計原則變動累積影響數", "会计原则变动累积影响数"), + (9411, "minority interest income", "少數股權淨利", "少数股权净利"), +] +"""The base account data.""" diff --git a/src/accounting/base_account/models.py b/src/accounting/base_account/models.py new file mode 100644 index 0000000..8b37ac9 --- /dev/null +++ b/src/accounting/base_account/models.py @@ -0,0 +1,73 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The data models for the base account management. + +""" +from flask import current_app +from flask_babel import get_locale + +from accounting.database import db + + +class BaseAccount(db.Model): + """A base account.""" + __tablename__ = "accounting_base_accounts" + """The table name.""" + code = db.Column(db.String, nullable=False, primary_key=True) + """The code.""" + title_l10n = db.Column("title", db.String, nullable=False) + """The title.""" + l10n = db.relationship("BaseAccountL10n", back_populates="account", + lazy=False) + """The localized titles.""" + + def __str__(self) -> str: + """Returns the string representation of the base account. + + :return: The string representation of the base account. + """ + return F"{self.code} {self.title}" + + @property + def title(self) -> str: + """Returns the title in the current locale. + + :return: The title in the current locale. + """ + current_locale = str(get_locale()) + if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]: + return self.title_l10n + for l10n in self.l10n: + if l10n.locale == current_locale: + return l10n.title + return self.title_l10n + + +class BaseAccountL10n(db.Model): + """A localized base account title.""" + __tablename__ = "accounting_base_accounts_l10n" + """The table name.""" + account_code = db.Column(db.String, db.ForeignKey(BaseAccount.code, + ondelete="CASCADE"), + nullable=False, primary_key=True) + """The code of the account.""" + account = db.relationship(BaseAccount, back_populates="l10n") + """The account.""" + locale = db.Column(db.String, nullable=False, primary_key=True) + """The locale.""" + title = db.Column(db.String, nullable=False) + """The localized title.""" diff --git a/src/accounting/base_account/query.py b/src/accounting/base_account/query.py new file mode 100644 index 0000000..5fc04f3 --- /dev/null +++ b/src/accounting/base_account/query.py @@ -0,0 +1,44 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The base account query. + +""" +import sqlalchemy as sa +from flask import request + +from accounting.utils.query import parse_query_keywords +from .models import BaseAccount, BaseAccountL10n + + +def get_base_account_query() -> list[BaseAccount]: + """Returns the base accounts, optionally filtered by the query. + + :return: The base accounts. + """ + keywords: list[str] = parse_query_keywords(request.args.get("q")) + if len(keywords) == 0: + return BaseAccount.query.order_by(BaseAccount.code).all() + conditions: list[sa.BinaryExpression] = [] + for k in keywords: + l10n: list[BaseAccountL10n] = BaseAccountL10n.query\ + .filter(BaseAccountL10n.title.contains(k)).all() + l10n_matches: set[str] = {x.account_code for x in l10n} + conditions.append(sa.or_(BaseAccount.code.contains(k), + BaseAccount.title_l10n.contains(k), + BaseAccount.code.in_(l10n_matches))) + return BaseAccount.query.filter(*conditions)\ + .order_by(BaseAccount.code).all() diff --git a/src/accounting/base_account/views.py b/src/accounting/base_account/views.py new file mode 100644 index 0000000..2e2bfb4 --- /dev/null +++ b/src/accounting/base_account/views.py @@ -0,0 +1,41 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The views for the base account management. + +""" +from flask import Blueprint, render_template + +from accounting.utils.pagination import Pagination +from accounting.utils.permission import has_permission, can_view + +bp: Blueprint = Blueprint("base-account", __name__) +"""The view blueprint for the base account management.""" + + +@bp.get("", endpoint="list") +@has_permission(can_view) +def list_accounts() -> str: + """Lists the base accounts. + + :return: The account list. + """ + from .models import BaseAccount + from .query import get_base_account_query + accounts: list[BaseAccount] = get_base_account_query() + pagination: Pagination = Pagination[BaseAccount](accounts) + return render_template("accounting/base-account/list.html", + list=pagination.list, pagination=pagination) diff --git a/src/accounting/database.py b/src/accounting/database.py new file mode 100644 index 0000000..24ad4d8 --- /dev/null +++ b/src/accounting/database.py @@ -0,0 +1,38 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The database instance factory for the base account management. + +This is to overcome the problem that the database instance needs to be +initialized at compile time, but as a submodule it is only available at run +time. + +""" +from flask_sqlalchemy import SQLAlchemy + +db: SQLAlchemy +"""The database instance.""" + + +def set_db(new_db: SQLAlchemy) -> None: + """Sets the database instance. + + :param new_db: The database instance. + :return: None. + """ + global db + db = new_db diff --git a/src/accounting/locale.py b/src/accounting/locale.py new file mode 100644 index 0000000..380446e --- /dev/null +++ b/src/accounting/locale.py @@ -0,0 +1,114 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The localization for the accounting application. + +""" +import json +from pathlib import Path + +from flask import Flask, Response, Blueprint +from flask_babel import LazyString, Domain +from flask_babel_js import JAVASCRIPT, c2js + +translation_dir: Path = Path(__file__).parent / "translations" +domain: Domain = Domain(translation_directories=[translation_dir], + domain="accounting") + + +def gettext(string, **variables) -> str: + """A replacement of the Babel gettext() function.. + + :param string: The message to translate. + :param variables: The variable substitution. + :return: The translated message. + """ + return domain.gettext(string, **variables) + + +def lazy_gettext(string, **variables) -> LazyString: + """A replacement of the Babel lazy_gettext() function.. + + :param string: The message to translate. + :param variables: The variable substitution. + :return: The translated message. + """ + return domain.lazy_gettext(string, **variables) + + +def __babel_js_catalog_view() -> Response: + """A tweaked view taken from Flask-Babel-JS that returns the messages and + with the A_() function instead of _(). + + :return: The response. + """ + js = [ + """"use strict"; + +(function() { + var babel = {}; + babel.catalog = """ + ] + + translations = domain.get_translations() + # Here used to be an isinstance check for NullTranslations, but the + # translation object that is "merged" by flask-babel is seen as an + # instance of NullTranslations. + catalog = translations._catalog.copy() + + # copy()ing the catalog here because we're modifying the original copy. + for key, value in catalog.copy().items(): + if isinstance(key, tuple): + text, plural = key + if text not in catalog: + catalog[text] = {} + + catalog[text][plural] = value + del catalog[key] + + js.append(json.dumps(catalog, indent=4)) + + js.append(";\n") + js.append(JAVASCRIPT) + + metadata = translations.gettext("") + if metadata: + for m in metadata.splitlines(): + if m.lower().startswith("plural-forms:"): + js.append(" babel.plural = ") + js.append(c2js(m.lower().split("plural=")[1].rstrip(';'))) + + js.append(""" + + window.A_ = babel.gettext; +})(); +""") + + resp = Response("".join(js)) + resp.headers["Content-Type"] = "text/javascript" + return resp + + +def init_app(app: Flask, bp: Blueprint) -> None: + """Initializes the application. + + :param bp: The blueprint of the accounting application. + :param app: The Flask application. + :return: None. + """ + bp.add_url_rule("/_jstrans.js", "babel_catalog", + __babel_js_catalog_view) + app.jinja_env.globals["A_"] = domain.gettext diff --git a/src/accounting/templates/accounting/base-account/list.html b/src/accounting/templates/accounting/base-account/list.html new file mode 100644 index 0000000..715ec5d --- /dev/null +++ b/src/accounting/templates/accounting/base-account/list.html @@ -0,0 +1,58 @@ +{# +The Mia! Accounting Flask Project +list.html: The base account list + + Copyright (c) 2023 imacat. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Author: imacat@mail.imacat.idv.tw (imacat) +First written: 2023/1/26 +#} +{% extends "accounting/base.html" %} + +{% block header %}{% block title %}{{ A_("Base Accounts") }}{% endblock %}{% endblock %} + +{% block content %} + +
+
+
+
+ + +
+
+
+
+ +{% if list %} + {% include "accounting/include/pagination.html" %} + + +{% else %} +

{{ A_("There is no data.") }}

+{% endif %} + +{% endblock %} diff --git a/src/accounting/templates/accounting/base.html b/src/accounting/templates/accounting/base.html new file mode 100644 index 0000000..1c13acf --- /dev/null +++ b/src/accounting/templates/accounting/base.html @@ -0,0 +1,27 @@ +{# +The Mia! Accounting Flask Project +base.html: The application-wide base template. + + Copyright (c) 2023 imacat. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Author: imacat@mail.imacat.idv.tw (imacat) +First written: 2023/1/27 +#} +{% extends "base.html" %} + +{% block scripts %} + + {% block accounting_scripts %}{% endblock %} +{% endblock %} diff --git a/src/accounting/templates/accounting/include/nav.html b/src/accounting/templates/accounting/include/nav.html new file mode 100644 index 0000000..39cac80 --- /dev/null +++ b/src/accounting/templates/accounting/include/nav.html @@ -0,0 +1,37 @@ +{# +The Mia! Accounting Flask Project +nav.html: The navigation menu for the accounting application. + + Copyright (c) 2023 imacat. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Author: imacat@mail.imacat.idv.tw (imacat) +First written: 2023/1/26 +#} +{% if can_view_accounting() %} + +{% endif %} diff --git a/src/accounting/templates/accounting/include/pagination.html b/src/accounting/templates/accounting/include/pagination.html new file mode 100644 index 0000000..b73a5fd --- /dev/null +++ b/src/accounting/templates/accounting/include/pagination.html @@ -0,0 +1,56 @@ +{# +The Mia! Accounting Flask Project +pagination.html: The pagination navigation bar. + + Copyright (c) 2023 imacat. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Author: imacat@mail.imacat.idv.tw (imacat) +First written: 2023/1/26 +#} +{% if pagination.is_needed %} + +{% endif %} diff --git a/src/accounting/translations/babel.cfg b/src/accounting/translations/babel.cfg new file mode 100644 index 0000000..3135aad --- /dev/null +++ b/src/accounting/translations/babel.cfg @@ -0,0 +1,3 @@ +[python: **.py] +[jinja2: **/templates/**.html] +[javascript: **/static/js/**.js] diff --git a/src/accounting/translations/zh_Hant/LC_MESSAGES/accounting.po b/src/accounting/translations/zh_Hant/LC_MESSAGES/accounting.po new file mode 100644 index 0000000..ca0d2c0 --- /dev/null +++ b/src/accounting/translations/zh_Hant/LC_MESSAGES/accounting.po @@ -0,0 +1,46 @@ +# Chinese (Traditional) translations for the Mia! Accounting Flask project. +# Copyright (C) 2023 imacat +# This file is distributed under the same license as the Mia! Accounting +# Flask project. +# imacat , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: Mia! Accounting Flask 0.0.0\n" +"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n" +"POT-Creation-Date: 2023-01-28 13:37+0800\n" +"PO-Revision-Date: 2023-01-28 13:37+0800\n" +"Last-Translator: imacat \n" +"Language: zh_Hant\n" +"Language-Team: zh_Hant \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.11.0\n" + +#: src/accounting/base_account/templates/accounting/base-account/list.html:24 +#: src/accounting/templates/accounting/include/nav.html:32 +msgid "Base Accounts" +msgstr "基本科目" + +#: src/accounting/base_account/templates/accounting/base-account/list.html:35 +msgid "Search" +msgstr "搜尋" + +#: src/accounting/base_account/templates/accounting/base-account/list.html:53 +msgid "There is no data." +msgstr "沒有資料。" + +#: src/accounting/templates/accounting/include/nav.html:26 +msgid "Accounting" +msgstr "記帳" + +#: src/accounting/utils/pagination.py:146 +msgid "Previous" +msgstr "前一頁" + +#: src/accounting/utils/pagination.py:194 +msgid "Next" +msgstr "下一頁" + diff --git a/src/accounting/utils/__init__.py b/src/accounting/utils/__init__.py new file mode 100644 index 0000000..3a7fb57 --- /dev/null +++ b/src/accounting/utils/__init__.py @@ -0,0 +1,21 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The independent utilities. + +This module should not import any other module from the application. + +""" diff --git a/src/accounting/utils/pagination.py b/src/accounting/utils/pagination.py new file mode 100644 index 0000000..a5b133b --- /dev/null +++ b/src/accounting/utils/pagination.py @@ -0,0 +1,240 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The pagination utilities. + +This module should not import any other module from the application. + +""" +import typing as t +from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse, \ + ParseResult + +from flask import request + +from accounting.locale import gettext + + +class PageLink: + """A link in the pagination.""" + + def __init__(self, text: str, uri: str | None = None, + is_current: bool = False, is_for_mobile: bool = False): + """Constructs the link. + + :param text: The link text. + :param uri: The link URI, or None if there is no link. + :param is_current: True if the page is the current page, or False + otherwise. + :param is_for_mobile: True if the page should be shown on small + screens, or False otherwise. + """ + self.text: str = text + """The link text""" + self.uri: str | None = uri + """The link URI, or None if there is no link.""" + self.is_current: bool = is_current + """Whether the link is the current page.""" + self.is_for_mobile: bool = is_for_mobile + """Whether the link should be shown on mobile screens.""" + + +T = t.TypeVar("T") + + +class Pagination(t.Generic[T]): + """The pagination utilities""" + AVAILABLE_PAGE_SIZES: list[int] = [10, 100, 200] + """The available page sizes.""" + DEFAULT_PAGE_SIZE: int = 10 + """The default page size.""" + + def __init__(self, items: list[T], is_reversed: bool = False): + """Constructs the pagination. + + :param items: The items. + :param is_reversed: True if the default page is the last page, or False + otherwise. + """ + self.__items: list[T] = items + """All the items.""" + self.__is_reversed: bool = is_reversed + """Whether the default page is the last page.""" + self.page_size: int = int(request.args.get("page-size", + self.DEFAULT_PAGE_SIZE)) + """The number of items in a page.""" + self.__total_pages: int = 0 if len(items) == 0 \ + else int((len(items) - 1) / self.page_size) + 1 + """The total number of pages.""" + self.is_needed: bool = self.__total_pages > 1 + """Whether there should be pagination.""" + self.__default_page_no: int = 0 + """The default page number.""" + self.page_no: int = 0 + """The current page number.""" + self.list: list[T] = [] + """The items shown in the list""" + if self.__total_pages > 0: + self.__set_list() + self.__current_uri: str = request.full_path if request.query_string \ + else request.path + """The current URI.""" + self.__base_uri_params: tuple[list[str], list[tuple[str, str]]] \ + = self.__get_base_uri_params() + """The base URI parameters.""" + self.page_links: list[PageLink] = self.__get_page_links() + """The pagination links.""" + self.page_sizes: list[PageLink] = self.__get_page_sizes() + """The links to switch the number of items in a page.""" + + def __set_list(self) -> None: + """Sets the items to show in the list. + + :return: None. + """ + self.__default_page_no = self.__total_pages if self.__is_reversed \ + else 1 + self.page_no = int(request.args.get("page-no", + self.__default_page_no)) + if self.page_no < 1: + self.page_no = 1 + if self.page_no > self.__total_pages: + self.page_no = self.__total_pages + lower_bound: int = (self.page_no - 1) * self.page_size + upper_bound: int = lower_bound + self.page_size + if upper_bound > len(self.__items): + upper_bound = len(self.__items) + self.list = self.__items[lower_bound:upper_bound] + + def __get_base_uri_params(self) -> tuple[list[str], list[tuple[str, str]]]: + """Returns the base URI and its parameters, with the "page-no" and + "page-size" parameters removed. + + :return: The URI parts and the cleaned-up query parameters. + """ + uri_p: ParseResult = urlparse(self.__current_uri) + params: list[tuple[str, str]] = parse_qsl(uri_p.query) + params = [x for x in params if x[0] not in ["page-no", "page-size"]] + parts: list[str] = list(uri_p) + return parts, params + + def __get_page_links(self) -> list[PageLink]: + """Returns the page links in the pagination navigation. + + :return: The page links in the pagination navigation. + """ + if self.__total_pages < 2: + return [] + uri: str | None + links: list[PageLink] = [] + + # The previous page. + uri = None if self.page_no == 1 else self.__uri_page(self.page_no - 1) + links.append(PageLink(gettext("Previous"), uri, is_for_mobile=True)) + + # The first page. + if self.page_no > 1: + links.append(PageLink("1", self.__uri_page(1))) + + # The eclipse of the previous pages. + if self.page_no - 3 == 2: + links.append(PageLink(str(self.page_no - 3), + self.__uri_page(self.page_no - 3))) + elif self.page_no - 3 > 2: + links.append(PageLink("…")) + + # The previous two pages. + if self.page_no - 2 > 1: + links.append(PageLink(str(self.page_no - 2), + self.__uri_page(self.page_no - 2))) + if self.page_no - 1 > 1: + links.append(PageLink(str(self.page_no - 1), + self.__uri_page(self.page_no - 1))) + + # The current page. + links.append(PageLink(str(self.page_no), self.__uri_page(self.page_no), + is_current=True)) + + # The next two pages. + if self.page_no + 1 < self.__total_pages: + links.append(PageLink(str(self.page_no + 1), + self.__uri_page(self.page_no + 1))) + if self.page_no + 2 < self.__total_pages: + links.append(PageLink(str(self.page_no + 2), + self.__uri_page(self.page_no + 2))) + + # The eclipse of the next pages. + if self.page_no + 3 == self.__total_pages - 1: + links.append(PageLink(str(self.page_no + 3), + self.__uri_page(self.page_no + 3))) + elif self.page_no + 3 < self.__total_pages - 1: + links.append(PageLink("…")) + + # The last page. + if self.page_no < self.__total_pages: + links.append(PageLink(str(self.__total_pages), + self.__uri_page(self.__total_pages))) + + # The next page. + uri = None if self.page_no == self.__total_pages \ + else self.__uri_page(self.page_no + 1) + links.append(PageLink(gettext("Next"), uri, is_for_mobile=True)) + + return links + + def __uri_page(self, page_no: int) -> str: + """Returns the URI of a page. + + :param page_no: The page number. + :return: The URI of the page. + """ + params: list[tuple[str, str]] = [] + if page_no != self.__default_page_no: + params.append(("page-no", str(page_no))) + if self.page_size != self.DEFAULT_PAGE_SIZE: + params.append(("page-size", str(self.page_size))) + return self.__uri_set_params(params) + + def __get_page_sizes(self) -> list[PageLink]: + """Returns the available page sizes. + + :return: The available page sizes. + """ + return [PageLink(str(x), self.__uri_size(x), + is_current=x == self.page_size) + for x in self.AVAILABLE_PAGE_SIZES] + + def __uri_size(self, page_size: int) -> str: + """Returns the URI of a page size. + + :param page_size: The page size. + :return: The URI of the page size. + """ + if page_size == self.page_size: + return self.__current_uri + return self.__uri_set_params([("page-size", str(page_size))]) + + def __uri_set_params(self, params: list[tuple[str, str]]) -> str: + """Returns the URI with the query parameters set. + + :param params: The query parameters. + :return: The URI with the query parameters set. + """ + cur_params: list[tuple[str, str]] = self.__base_uri_params[1].copy() + cur_params.extend(params) + parts: list[str] = self.__base_uri_params[0].copy() + parts[4] = urlencode(cur_params) + return urlunparse(parts) diff --git a/src/accounting/utils/permission.py b/src/accounting/utils/permission.py new file mode 100644 index 0000000..170270b --- /dev/null +++ b/src/accounting/utils/permission.py @@ -0,0 +1,100 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The permissions. + +This module should not import any other module from the application. + +""" +import typing as t + +from flask import Flask, abort + + +def has_permission(rule: t.Callable[[], bool]) -> t.Callable: + """The permission decorator to check whether the current user is allowed. + + :param rule: The permission rule. + :return: The view decorator. + """ + + def decorator(view: t.Callable) -> t.Callable: + """The view decorator to decorate a view with permission tests. + + :param view: The view. + :return: The decorated view. + """ + + def decorated_view(*args, **kwargs): + """The decorated view that tests against a permission rule. + + :param args: The arguments of the view. + :param kwargs: The keyword arguments of the view. + :return: The response of the view. + :raise Forbidden: When the user is denied. + """ + if not rule(): + abort(403) + return view(*args, **kwargs) + + return decorated_view + + return decorator + + +__can_view_func: t.Callable[[], bool] = lambda: True +"""The callback that returns whether the current user can view the accounting +data.""" +__can_edit_func: t.Callable[[], bool] = lambda: True +"""The callback that returns whether the current user can edit the accounting +data.""" + + +def can_view() -> bool: + """Returns whether the current user can view the account data. + + :return: True if the current user can view the accounting data, or False + otherwise. + """ + return __can_view_func() + + +def can_edit() -> bool: + """Returns whether the current user can edit the account data. + + :return: True if the current user can edit the accounting data, or False + otherwise. + """ + return __can_edit_func() + + +def init_app(app: Flask, can_view_func: t.Callable[[], bool] | None = None, + can_edit_func: t.Callable[[], bool] | None = None) -> None: + """Initializes the application. + + :param app: The Flask application. + :param can_view_func: A callback that returns whether the current user can + view the accounting data. + :param can_edit_func: A callback that returns whether the current user can + edit the accounting data. + :return: None. + """ + global __can_view_func, __can_edit_func + if can_view_func is not None: + __can_view_func = can_view_func + if can_edit_func is not None: + __can_edit_func = can_edit_func + app.jinja_env.globals["can_view_accounting"] = __can_view_func diff --git a/src/accounting/utils/query.py b/src/accounting/utils/query.py new file mode 100644 index 0000000..aea01f9 --- /dev/null +++ b/src/accounting/utils/query.py @@ -0,0 +1,44 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The query keyword parser. + +This module should not import any other module from the application. + +""" +import re + + +def parse_query_keywords(q: str | None) -> list[str]: + """Returns the query keywords by the query parameter. + + :param q: The query parameter. + :return: The query keywords. + """ + if q is None: + return [] + q = q.strip() + if q == "": + return [] + keywords: list[str] = [] + while q is not None: + m: re.Match = re.match(r"(?:\"([^\"]+)\"|(\S+))(?:\s+(.+)|)$", q) + if m.group(1) is not None: + keywords.append(m.group(1)) + else: + keywords.append(m.group(2)) + q = m.group(3) + return keywords diff --git a/tests/babel-utils-testsite.py b/tests/babel-utils-testsite.py new file mode 100755 index 0000000..04aeb2a --- /dev/null +++ b/tests/babel-utils-testsite.py @@ -0,0 +1,133 @@ +#! env python3 +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/28 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The translation management utilities for the test site. + +""" +import os +import re +from pathlib import Path +from time import strftime + +import click +from babel.messages.frontend import CommandLineInterface +from opencc import OpenCC + +root_dir: Path = Path(__file__).parent.parent +translation_dir: Path = root_dir / "tests" / "testsite" / "translations" +domain: str = "messages" + + +@click.group() +def main() -> None: + """Manages the message translation.""" + + +@click.command("extract") +def babel_extract() -> None: + """Extracts the messages for translation.""" + os.chdir(root_dir) + cfg: Path = translation_dir / "babel.cfg" + pot: Path = translation_dir / f"{domain}.pot" + zh_hant: Path = translation_dir / "zh_Hant" / "LC_MESSAGES"\ + / f"{domain}.po" + zh_hans: Path = translation_dir / "zh_Hans" / "LC_MESSAGES"\ + / f"{domain}.po" + CommandLineInterface().run([ + "pybabel", "extract", "-F", str(cfg), "-k", "lazy_gettext", "-k", "A_", + "-o", str(pot), str(Path("tests") / "testsite")]) + if not zh_hant.exists(): + zh_hant.touch() + if not zh_hans.exists(): + zh_hans.touch() + CommandLineInterface().run([ + "pybabel", "update", "-i", str(pot), "-D", domain, + "-d", translation_dir]) + + +@click.command("compile") +def babel_compile() -> None: + """Compiles the translated messages.""" + __convert_chinese() + __update_rev_date() + CommandLineInterface().run([ + "pybabel", "compile", "-D", domain, "-d", translation_dir]) + + +def __convert_chinese() -> None: + """Updates the Simplified Chinese translation according to the Traditional + Chinese translation. + + :return: None. + """ + cc: OpenCC = OpenCC("tw2sp") + zh_hant: Path = translation_dir / "zh_Hant" / "LC_MESSAGES"\ + / f"{domain}.po" + zh_hans: Path = translation_dir / "zh_Hans" / "LC_MESSAGES"\ + / f"{domain}.po" + now: str = strftime("%Y-%m-%d %H:%M%z") + with open(zh_hant, "r") as f: + content: str = f.read() + content = cc.convert(content) + content = re.sub(r"^# Chinese \\(Traditional\\) translations ", + "# Chinese (Simplified) translations ", content) + content = re.sub(r"\n\"PO-Revision-Date: [^\n]*\"\n", + f"\n\"PO-Revision-Date: {now}\\\\n\"\n", + content) + content = content.replace("\n\"Language-Team: zh_Hant", + "\n\"Language-Team: zh_Hans") + content = content.replace("\n\"Language: zh_Hant\\n\"\n", + "\n\"Language: zh_Hans\\n\"\n") + content = content.replace("\nmsgstr \"zh-Hant\"\n", + "\nmsgstr \"zh-Hans\"\n") + zh_hans.parent.mkdir(exist_ok=True) + with open(zh_hans, "w") as f: + f.write(content) + + +def __update_rev_date() -> None: + """Updates the revision dates in the PO files. + + :return: None. + """ + for language_dir in translation_dir.glob("*"): + po_file: Path = language_dir / "LC_MESSAGES" / f"{domain}.po" + if po_file.is_file(): + __update_file_rev_date(po_file) + + +def __update_file_rev_date(file: Path) -> None: + """Updates the revision date of a PO file + + :param file: The PO file. + :return: None. + """ + now = strftime("%Y-%m-%d %H:%M%z") + with open(file, "r+") as f: + content = f.read() + content = re.sub(r"\n\"PO-Revision-Date: [^\n]*\"\n", + f"\n\"PO-Revision-Date: {now}\\\\n\"\n", + content) + f.seek(0) + f.write(content) + + +main.add_command(babel_extract) +main.add_command(babel_compile) + +if __name__ == '__main__': + main() diff --git a/tests/babel-utils.py b/tests/babel-utils.py new file mode 100755 index 0000000..300af69 --- /dev/null +++ b/tests/babel-utils.py @@ -0,0 +1,133 @@ +#! env python3 +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/28 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The translation management utilities. + +""" +import os +import re +from pathlib import Path +from time import strftime + +import click +from babel.messages.frontend import CommandLineInterface +from opencc import OpenCC + +root_dir: Path = Path(__file__).parent.parent +translation_dir: Path = root_dir / "src" / "accounting" / "translations" +domain: str = "accounting" + + +@click.group() +def main() -> None: + """Manages the message translation.""" + + +@click.command("extract") +def babel_extract() -> None: + """Extracts the messages for translation.""" + os.chdir(root_dir) + cfg: Path = translation_dir / "babel.cfg" + pot: Path = translation_dir / f"{domain}.pot" + zh_hant: Path = translation_dir / "zh_Hant" / "LC_MESSAGES"\ + / f"{domain}.po" + zh_hans: Path = translation_dir / "zh_Hans" / "LC_MESSAGES"\ + / f"{domain}.po" + CommandLineInterface().run([ + "pybabel", "extract", "-F", str(cfg), "-k", "lazy_gettext", "-k", "A_", + "-o", str(pot), "src"]) + if not zh_hant.exists(): + zh_hant.touch() + if not zh_hans.exists(): + zh_hans.touch() + CommandLineInterface().run([ + "pybabel", "update", "-i", str(pot), "-D", domain, + "-d", translation_dir]) + + +@click.command("compile") +def babel_compile() -> None: + """Compiles the translated messages.""" + __convert_chinese() + __update_rev_date() + CommandLineInterface().run([ + "pybabel", "compile", "-D", domain, "-d", translation_dir]) + + +def __convert_chinese() -> None: + """Updates the Simplified Chinese translation according to the Traditional + Chinese translation. + + :return: None. + """ + cc: OpenCC = OpenCC("tw2sp") + zh_hant: Path = translation_dir / "zh_Hant" / "LC_MESSAGES"\ + / f"{domain}.po" + zh_hans: Path = translation_dir / "zh_Hans" / "LC_MESSAGES"\ + / f"{domain}.po" + now: str = strftime("%Y-%m-%d %H:%M%z") + with open(zh_hant, "r") as f: + content: str = f.read() + content = cc.convert(content) + content = re.sub(r"^# Chinese \\(Traditional\\) translations ", + "# Chinese (Simplified) translations ", content) + content = re.sub(r"\n\"PO-Revision-Date: [^\n]*\"\n", + f"\n\"PO-Revision-Date: {now}\\\\n\"\n", + content) + content = content.replace("\n\"Language-Team: zh_Hant", + "\n\"Language-Team: zh_Hans") + content = content.replace("\n\"Language: zh_Hant\\n\"\n", + "\n\"Language: zh_Hans\\n\"\n") + content = content.replace("\nmsgstr \"zh-Hant\"\n", + "\nmsgstr \"zh-Hans\"\n") + zh_hans.parent.mkdir(exist_ok=True) + with open(zh_hans, "w") as f: + f.write(content) + + +def __update_rev_date() -> None: + """Updates the revision dates in the PO files. + + :return: None. + """ + for language_dir in translation_dir.glob("*"): + po_file: Path = language_dir / "LC_MESSAGES" / f"{domain}.po" + if po_file.is_file(): + __update_file_rev_date(po_file) + + +def __update_file_rev_date(file: Path) -> None: + """Updates the revision date of a PO file + + :param file: The PO file. + :return: None. + """ + now = strftime("%Y-%m-%d %H:%M%z") + with open(file, "r+") as f: + content = f.read() + content = re.sub(r"\n\"PO-Revision-Date: [^\n]*\"\n", + f"\n\"PO-Revision-Date: {now}\\\\n\"\n", + content) + f.seek(0) + f.write(content) + + +main.add_command(babel_extract) +main.add_command(babel_compile) + +if __name__ == '__main__': + main() diff --git a/tests/test_base_account.py b/tests/test_base_account.py new file mode 100644 index 0000000..7f74c9a --- /dev/null +++ b/tests/test_base_account.py @@ -0,0 +1,113 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The test for the base account management. + +""" +import unittest + +import httpx +from click.testing import Result +from flask import Flask +from flask.testing import FlaskCliRunner + +from testlib import get_csrf_token +from testsite import create_app + + +class BaseAccountTestCase(unittest.TestCase): + """The base account test case.""" + + def setUp(self) -> None: + """Sets up the test. + This is run once per test. + + :return: None. + """ + self.app: Flask = create_app(is_testing=True) + + runner: FlaskCliRunner = self.app.test_cli_runner() + with self.app.app_context(): + result: Result = runner.invoke(args="init-db") + self.assertEqual(result.exit_code, 0) + self.client: httpx.Client = httpx.Client(app=self.app, + base_url="https://testserver") + self.client.headers["Referer"] = "https://testserver" + self.csrf_token: str = get_csrf_token(self, self.client, "/login") + + def test_init(self) -> None: + """Tests the "accounting-init-base" console command. + + :return: None. + """ + from accounting.base_account.models import BaseAccount, BaseAccountL10n + runner: FlaskCliRunner = self.app.test_cli_runner() + result: Result = runner.invoke(args="accounting-init-base") + self.assertEqual(result.exit_code, 0) + with self.app.app_context(): + accounts: list[BaseAccount] = BaseAccount.query.all() + l10n: list[BaseAccountL10n] = BaseAccountL10n.query.all() + self.assertEqual(len(accounts), 527) + self.assertEqual(len(l10n), 527 * 2) + l10n_keys: set[str] = {f"{x.account_code}-{x.locale}" for x in l10n} + for account in accounts: + self.assertIn(f"{account.code}-zh_Hant", l10n_keys) + self.assertIn(f"{account.code}-zh_Hant", l10n_keys) + + list_uri: str = "/accounting/base-accounts" + response: httpx.Response + + self.__logout() + response = self.client.get(list_uri) + self.assertEqual(response.status_code, 403) + + self.__logout() + self.__login_as("viewer") + response = self.client.get(list_uri) + self.assertEqual(response.status_code, 200) + + self.__logout() + self.__login_as("editor") + response = self.client.get(list_uri) + self.assertEqual(response.status_code, 200) + + self.__logout() + self.__login_as("nobody") + response = self.client.get(list_uri) + self.assertEqual(response.status_code, 403) + + def __logout(self) -> None: + """Logs out the currently logged-in user. + + :return: None. + """ + response: httpx.Response = self.client.post( + "/logout", data={"csrf_token": self.csrf_token}) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.headers["Location"], "/") + + def __login_as(self, username: str) -> None: + """Logs in as a specific user. + + :param username: The username. + :return: None. + """ + response: httpx.Response = self.client.post( + "/login", data={"csrf_token": self.csrf_token, + "username": username}) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.headers["Location"], "/") diff --git a/tests/testlib.py b/tests/testlib.py new file mode 100644 index 0000000..e00a5e6 --- /dev/null +++ b/tests/testlib.py @@ -0,0 +1,56 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/27 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The common test libraries. + +""" +from html.parser import HTMLParser +from unittest import TestCase + +import httpx + + +def get_csrf_token(test_case: TestCase, client: httpx.Client, uri: str) -> str: + """Returns the CSRF token from a form in a URI. + + :param test_case: The test case. + :param client: The httpx client. + :param uri: The URI. + :return: The CSRF token. + """ + + class CsrfParser(HTMLParser): + """The CSRF token parser.""" + + def __init__(self): + """Constructs the CSRF token parser.""" + super().__init__() + self.csrf_token: str | None = None + """The CSRF token.""" + + def handle_starttag(self, tag: str, + attrs: list[tuple[str, str | None]]) -> None: + """Handles when a start tag is found.""" + attrs_dict: dict[str, str] = dict(attrs) + if attrs_dict.get("name") == "csrf_token": + self.csrf_token = attrs_dict["value"] + + response: httpx.Response = client.get(uri) + test_case.assertEqual(response.status_code, 200) + parser: CsrfParser = CsrfParser() + parser.feed(response.text) + test_case.assertIsNotNone(parser.csrf_token) + return parser.csrf_token diff --git a/tests/testsite/__init__.py b/tests/testsite/__init__.py new file mode 100644 index 0000000..52c514b --- /dev/null +++ b/tests/testsite/__init__.py @@ -0,0 +1,96 @@ +# The Mia! Accounting Flask Demonstration Website. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/27 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The Mia! Accounting Flask demonstration website. + +""" +import typing as t +from secrets import token_urlsafe + +import click +from flask import Flask, Blueprint, render_template +from flask.cli import with_appcontext +from flask_babel_js import BabelJS +from flask_sqlalchemy import SQLAlchemy +from flask_wtf import CSRFProtect + +bp: Blueprint = Blueprint("home", __name__) +babel_js: BabelJS = BabelJS() +csrf: CSRFProtect = CSRFProtect() +db: SQLAlchemy = SQLAlchemy() + + +def create_app(is_testing: bool = False) -> Flask: + """Create and configure the application. + + :param is_testing: True if we are running for testing, or False otherwise. + :return: The application. + """ + import accounting + + app: Flask = Flask(__name__) + db_uri: str = "sqlite:///" if is_testing else "sqlite:///local.sqlite" + app.config.from_mapping({ + "SECRET_KEY": token_urlsafe(32), + "SQLALCHEMY_DATABASE_URI": db_uri, + "BABEL_DEFAULT_LOCALE": "en", + "ALL_LINGUAS": "zh_Hant|正體中文,en|English,zh_Hans|简体中文", + }) + if is_testing: + app.config["TESTING"] = True + + babel_js.init_app(app) + csrf.init_app(app) + db.init_app(app) + + app.register_blueprint(bp, url_prefix="/") + app.cli.add_command(init_db_command) + + from . import locale + locale.init_app(app) + + from . import auth + auth.init_app(app) + + can_view: t.Callable[[], bool] = lambda: auth.current_user() is not None \ + and auth.current_user().username in ["viewer", "editor"] + can_edit: t.Callable[[], bool] = lambda: auth.current_user() is not None \ + and auth.current_user().username == "editor" + accounting.init_app(app, can_view_func=can_view, can_edit_func=can_edit) + + return app + + +@click.command("init-db") +@with_appcontext +def init_db_command() -> None: + """Initializes the database.""" + db.create_all() + from .auth import User + for username in ["viewer", "editor", "nobody"]: + if User.query.filter(User.username == username).first() is None: + db.session.add(User(username=username)) + db.session.commit() + click.echo("Database initialized successfully.") + + +@bp.get("/", endpoint="home") +def get_home() -> str: + """Returns the home page. + + :return: The home page. + """ + return render_template("home.html") diff --git a/tests/testsite/auth.py b/tests/testsite/auth.py new file mode 100644 index 0000000..bd732fa --- /dev/null +++ b/tests/testsite/auth.py @@ -0,0 +1,92 @@ +# The Mia! Accounting Flask Demonstration Website. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/27 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The authentication for the Mia! Accounting Flask demonstration website. + +""" +from flask import Blueprint, render_template, Flask, redirect, url_for, \ + session, request, g + +from . import db + +bp: Blueprint = Blueprint("auth", __name__, url_prefix="/") + + +class User(db.Model): + """A user.""" + __tablename__ = "users" + """The table name.""" + id = db.Column(db.Integer, nullable=False, primary_key=True, + autoincrement=True) + """The ID""" + username = db.Column(db.String, nullable=False, unique=True) + """The username.""" + + +@bp.get("login", endpoint="login-form") +def show_login_form() -> str: + """Shows the login form. + + :return: The login form. + """ + return render_template("login.html") + + +@bp.post("login", endpoint="login") +def login() -> redirect: + """Logs in the user. + + :return: The redirection to the home page. + """ + if request.form.get("username") not in ["viewer", "editor", "nobody"]: + return redirect(url_for("auth.login")) + session["user"] = request.form.get("username") + return redirect(url_for("home.home")) + + +@bp.post("logout", endpoint="logout") +def logout() -> redirect: + """Logs out the user. + + :return: The redirection to the home page. + """ + if "user" in session: + del session["user"] + return redirect(url_for("home.home")) + + +def current_user() -> User | None: + """Returns the current user. + + :return: The current user, or None if the user did not log in. + """ + if not hasattr(g, "user"): + if "user" not in session: + g.user = None + else: + g.user = User.query.filter( + User.username == session["user"]).first() + return g.user + + +def init_app(app: Flask) -> None: + """Initialize the localization. + + :param app: The Flask application. + :return: None. + """ + app.register_blueprint(bp) + app.jinja_env.globals["current_user"] = current_user diff --git a/tests/testsite/locale.py b/tests/testsite/locale.py new file mode 100644 index 0000000..70ca9e7 --- /dev/null +++ b/tests/testsite/locale.py @@ -0,0 +1,97 @@ +# The Mia! Accounting Flask Demonstration Website. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/2 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The localization for the Mia! Accounting Flask demonstration website. + +""" +from babel import Locale +from flask import request, session, current_app, Blueprint, Response, \ + redirect, url_for, Flask +from flask_babel import Babel +from werkzeug.datastructures import LanguageAccept + +bp: Blueprint = Blueprint("locale", __name__, url_prefix="/") + + +def get_locale(): + """Returns the locale of the user + + :return: The locale of the user. + """ + all_linguas: dict[str, str] = get_all_linguas() + if "locale" in session and session["locale"] in all_linguas: + return session["locale"] + return __fix_accept_language(request.accept_languages)\ + .best_match(all_linguas.keys()) + + +def __fix_accept_language(accept: LanguageAccept) -> LanguageAccept: + """Fixes the accept-language so that territory variants may be matched to + script variants. For example, zh_TW, zh_HK to zh_Hant, and zh_CN, zh_SG to + zh_Hans. This is to solve the issue that Flask only recognizes the script + variants, like zh_Hant and zh_Hans. + + :param accept: The original HTTP accept languages. + :return: The fixed HTTP accept languages + """ + accept_list: list[tuple[str, float]] = list(accept) + to_add: list[tuple[str, float]] = [] + for pair in accept_list: + locale: Locale = Locale.parse(pair[0].replace("-", "_")) + if locale.script is not None: + tag: str = f"{locale.language}-{locale.script}" + if tag not in accept: + to_add.append((tag, pair[1])) + accept_list.extend(to_add) + return LanguageAccept(accept_list) + + +@bp.post("/locale", endpoint="set-locale") +def set_locale() -> Response: + """Sets the locale for the user. + + :return: The response. + """ + all_linguas: dict[str, str] = get_all_linguas() + if "locale" in request.form and request.form["locale"] in all_linguas: + session["locale"] = request.form["locale"] + if "next" in request.form: + return redirect(request.form["next"]) + return redirect(url_for("home.home")) + + +def get_all_linguas() -> dict[str, str]: + """Returns all the available languages. + + :return: All the available languages, as a dictionary of the language code + and their local names. + """ + return {y[0]: y[1] for y in + [x.split("|") for x in + current_app.config["ALL_LINGUAS"].split(",")]} + + +def init_app(app: Flask) -> None: + """Initialize the localization. + + :param app: The Flask application. + :return: None. + """ + babel = Babel() + babel.init_app(app, locale_selector=get_locale) + app.register_blueprint(bp) + app.jinja_env.globals["get_locale"] = get_locale + app.jinja_env.globals["get_all_linguas"] = get_all_linguas diff --git a/tests/testsite/templates/base.html b/tests/testsite/templates/base.html new file mode 100644 index 0000000..4678192 --- /dev/null +++ b/tests/testsite/templates/base.html @@ -0,0 +1,134 @@ +{# +The Mia! Accounting Flask Demonstration Website +base.html: The side-wide layout template + + Copyright (c) 2023 imacat. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Author: imacat@mail.imacat.idv.tw (imacat) +First written: 2023/1/27 +#} + + + + + + + + + {% block styles %}{% endblock %} + + + + {% block scripts %}{% endblock %} + {% block title %}{% endblock %} + + + + + +
+ +

{% block header %}{% endblock %}

+ +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + {% if category == "success" %} + + {% elif category == "error" %} + + {% endif %} + {% endfor %} + {% endif %} +{% endwith %} + +
+ {% block content %}{% endblock %} +
+ +
+ + + diff --git a/tests/testsite/templates/home.html b/tests/testsite/templates/home.html new file mode 100644 index 0000000..a850001 --- /dev/null +++ b/tests/testsite/templates/home.html @@ -0,0 +1,24 @@ +{# +The Mia! Accounting Flask Demonstration Website +home.html: The home page. + + Copyright (c) 2023 imacat. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Author: imacat@mail.imacat.idv.tw (imacat) +First written: 2023/1/27 +#} +{% extends "base.html" %} + +{% block header %}{% block title %}{{ _("Home") }}{% endblock %}{% endblock %} diff --git a/tests/testsite/templates/login.html b/tests/testsite/templates/login.html new file mode 100644 index 0000000..0b9db75 --- /dev/null +++ b/tests/testsite/templates/login.html @@ -0,0 +1,35 @@ +{# +The Mia! Accounting Flask Demonstration Website +login.html: The login page. + + Copyright (c) 2023 imacat. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Author: imacat@mail.imacat.idv.tw (imacat) +First written: 2023/1/27 +#} +{% extends "base.html" %} + +{% block header %}{% block title %}{{ _("Log In") }}{% endblock %}{% endblock %} + +{% block content %} + +
+ + + + +
+ +{% endblock %} diff --git a/tests/testsite/translations/babel.cfg b/tests/testsite/translations/babel.cfg new file mode 100644 index 0000000..3135aad --- /dev/null +++ b/tests/testsite/translations/babel.cfg @@ -0,0 +1,3 @@ +[python: **.py] +[jinja2: **/templates/**.html] +[javascript: **/static/js/**.js] diff --git a/tests/testsite/translations/zh_Hant/LC_MESSAGES/messages.po b/tests/testsite/translations/zh_Hant/LC_MESSAGES/messages.po new file mode 100644 index 0000000..8371664 --- /dev/null +++ b/tests/testsite/translations/zh_Hant/LC_MESSAGES/messages.po @@ -0,0 +1,54 @@ +# Chinese (Traditional) translations for the Mia! Accounting Flask +# Demonstration website. +# Copyright (C) 2023 imacat +# This file is distributed under the same license as the Mia! Accounting +# Flask Demonstration project. +# imacat , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: Mia! Accounting Flask Demonstration 0.0.0\n" +"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n" +"POT-Creation-Date: 2023-01-28 13:42+0800\n" +"PO-Revision-Date: 2023-01-28 13:42+0800\n" +"Last-Translator: imacat \n" +"Language: zh_Hant\n" +"Language-Team: zh_Hant \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.11.0\n" + +#: tests/testsite/templates/base.html:23 +msgid "en" +msgstr "zh-Hant" + +#: tests/testsite/templates/base.html:43 tests/testsite/templates/home.html:24 +msgid "Home" +msgstr "首頁" + +#: tests/testsite/templates/base.html:68 +msgid "Log Out" +msgstr "" + +#: tests/testsite/templates/base.html:78 tests/testsite/templates/login.html:24 +msgid "Log In" +msgstr "登入" + +#: tests/testsite/templates/base.html:119 +msgid "Error:" +msgstr "錯誤:" + +#: tests/testsite/templates/login.html:30 +msgid "Viewer" +msgstr "讀報表者" + +#: tests/testsite/templates/login.html:31 +msgid "Editor" +msgstr "記帳者" + +#: tests/testsite/templates/login.html:32 +msgid "Nobody" +msgstr "沒有權限者" +