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.
This commit is contained in:
parent
9c83ad97c1
commit
14638f574e
202
LICENSE
Normal file
202
LICENSE
Normal file
@ -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.
|
28
MANIFEST.in
Normal file
28
MANIFEST.in
Normal file
@ -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/*
|
50
README.rst
Normal file
50
README.rst
Normal file
@ -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
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@ -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)
|
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@ -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
|
0
docs/source/_static/.keep
Normal file
0
docs/source/_static/.keep
Normal file
0
docs/source/_templates/.keep
Normal file
0
docs/source/_templates/.keep
Normal file
53
docs/source/accounting.base_account.rst
Normal file
53
docs/source/accounting.base_account.rst
Normal file
@ -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:
|
30
docs/source/accounting.rst
Normal file
30
docs/source/accounting.rst
Normal file
@ -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:
|
37
docs/source/accounting.utils.rst
Normal file
37
docs/source/accounting.utils.rst
Normal file
@ -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:
|
32
docs/source/conf.py
Normal file
32
docs/source/conf.py
Normal file
@ -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']
|
20
docs/source/index.rst
Normal file
20
docs/source/index.rst
Normal file
@ -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`
|
7
docs/source/modules.rst
Normal file
7
docs/source/modules.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
src
|
||||||
|
===
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 4
|
||||||
|
|
||||||
|
accounting
|
20
pyproject.toml
Normal file
20
pyproject.toml
Normal file
@ -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"
|
56
setup.cfg
Normal file
56
setup.cfg
Normal file
@ -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/**
|
57
src/accounting/__init__.py
Normal file
57
src/accounting/__init__.py
Normal file
@ -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)
|
34
src/accounting/base_account/__init__.py
Normal file
34
src/accounting/base_account/__init__.py
Normal file
@ -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)
|
709
src/accounting/base_account/commands.py
Normal file
709
src/accounting/base_account/commands.py
Normal file
@ -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."""
|
73
src/accounting/base_account/models.py
Normal file
73
src/accounting/base_account/models.py
Normal file
@ -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."""
|
44
src/accounting/base_account/query.py
Normal file
44
src/accounting/base_account/query.py
Normal file
@ -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()
|
41
src/accounting/base_account/views.py
Normal file
41
src/accounting/base_account/views.py
Normal file
@ -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)
|
38
src/accounting/database.py
Normal file
38
src/accounting/database.py
Normal file
@ -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
|
114
src/accounting/locale.py
Normal file
114
src/accounting/locale.py
Normal file
@ -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
|
58
src/accounting/templates/accounting/base-account/list.html
Normal file
58
src/accounting/templates/accounting/base-account/list.html
Normal file
@ -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 %}
|
||||||
|
|
||||||
|
<form action="{{ url_for("accounting.base-account.list") }}" method="get" role="search">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<input id="query" class="form-control form-control-sm" type="search" name="q" value="{{ request.args["q"] if "q" in request.args else "" }}" placeholder=" " required="required" aria-label="Search">
|
||||||
|
<button class="input-group-text" type="submit">
|
||||||
|
<label for="query">
|
||||||
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
|
{{ A_("Search") }}
|
||||||
|
</label>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if list %}
|
||||||
|
{% include "accounting/include/pagination.html" %}
|
||||||
|
|
||||||
|
<ul class="list-group">
|
||||||
|
{% for item in list %}
|
||||||
|
<li class="list-group-item list-group-item-action">
|
||||||
|
{{ item }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ A_("There is no data.") }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
27
src/accounting/templates/accounting/base.html
Normal file
27
src/accounting/templates/accounting/base.html
Normal file
@ -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 %}
|
||||||
|
<script src="{{ url_for("accounting.babel_catalog") }}"></script>
|
||||||
|
{% block accounting_scripts %}{% endblock %}
|
||||||
|
{% endblock %}
|
37
src/accounting/templates/accounting/include/nav.html
Normal file
37
src/accounting/templates/accounting/include/nav.html
Normal file
@ -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() %}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<span class="nav-link dropdown-toggle" data-bs-toggle="dropdown">
|
||||||
|
<i class="fa-solid fa-gear"></i>
|
||||||
|
{{ A_("Accounting") }}
|
||||||
|
</span>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if request.endpoint.startswith("accounting.base-account.") %} active {% endif %}" href="{{ url_for("accounting.base-account.list") }}">
|
||||||
|
<i class="fa-solid fa-list"></i>
|
||||||
|
{{ A_("Base Accounts") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
56
src/accounting/templates/accounting/include/pagination.html
Normal file
56
src/accounting/templates/accounting/include/pagination.html
Normal file
@ -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 %}
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination">
|
||||||
|
{% for link in pagination.page_links %}
|
||||||
|
{% if link.uri is none %}
|
||||||
|
<li class="page-item disabled {% if not link.is_for_mobile %} d-none d-md-inline {% endif %}">
|
||||||
|
<span class="page-link">
|
||||||
|
{{ link.text }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item {% if not link.is_for_mobile %} d-none d-md-inline {% endif %} {% if link.is_current %} active {% endif %}">
|
||||||
|
<a class="page-link" href="{{ link.uri }}">
|
||||||
|
{{ link.text }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<li class="page-item d-none d-md-inline active dropdown">
|
||||||
|
<div class="page-link dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
{{ pagination.page_size }}
|
||||||
|
</div>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for link in pagination.page_sizes %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if link.is_current %} active {% endif %}" href="{{ link.uri }}">
|
||||||
|
{{ link.text }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
3
src/accounting/translations/babel.cfg
Normal file
3
src/accounting/translations/babel.cfg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[python: **.py]
|
||||||
|
[jinja2: **/templates/**.html]
|
||||||
|
[javascript: **/static/js/**.js]
|
@ -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 <imacat@mail.imacat.idv.tw>, 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 <imacat@mail.imacat.idv.tw>\n"
|
||||||
|
"Language: zh_Hant\n"
|
||||||
|
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\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 "下一頁"
|
||||||
|
|
21
src/accounting/utils/__init__.py
Normal file
21
src/accounting/utils/__init__.py
Normal file
@ -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.
|
||||||
|
|
||||||
|
"""
|
240
src/accounting/utils/pagination.py
Normal file
240
src/accounting/utils/pagination.py
Normal file
@ -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)
|
100
src/accounting/utils/permission.py
Normal file
100
src/accounting/utils/permission.py
Normal file
@ -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
|
44
src/accounting/utils/query.py
Normal file
44
src/accounting/utils/query.py
Normal file
@ -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
|
133
tests/babel-utils-testsite.py
Executable file
133
tests/babel-utils-testsite.py
Executable file
@ -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()
|
133
tests/babel-utils.py
Executable file
133
tests/babel-utils.py
Executable file
@ -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()
|
113
tests/test_base_account.py
Normal file
113
tests/test_base_account.py
Normal file
@ -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"], "/")
|
56
tests/testlib.py
Normal file
56
tests/testlib.py
Normal file
@ -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
|
96
tests/testsite/__init__.py
Normal file
96
tests/testsite/__init__.py
Normal file
@ -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")
|
92
tests/testsite/auth.py
Normal file
92
tests/testsite/auth.py
Normal file
@ -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
|
97
tests/testsite/locale.py
Normal file
97
tests/testsite/locale.py
Normal file
@ -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
|
134
tests/testsite/templates/base.html
Normal file
134
tests/testsite/templates/base.html
Normal file
@ -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
|
||||||
|
#}
|
||||||
|
<!doctype html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="{{ _("en") }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="author" content="{{ "imacat" }}" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.1/css/all.min.css">
|
||||||
|
{% block styles %}{% endblock %}
|
||||||
|
<script src="{{ url_for("babel_catalog") }}"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/decimal.js/9.0.0/decimal.min.js"></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg bg-body-tertiary bg-dark navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="{{ url_for("home.home") }}">
|
||||||
|
<i class="fa-solid fa-house"></i>
|
||||||
|
{{ _("Home") }}
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#collapsible-navbar" aria-controls="collapsible-navbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="collapsible-navbar" class="collapse navbar-collapse">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
{% include "/accounting/include/nav.html" %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- The right side -->
|
||||||
|
<ul class="navbar-nav d-flex">
|
||||||
|
{% if current_user() is not none %}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<span class="nav-link dropdown-toggle" data-bs-toggle="dropdown">
|
||||||
|
<i class="fa-solid fa-user"></i>
|
||||||
|
{{ current_user().username }}
|
||||||
|
</span>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<form action="{{ url_for("auth.logout") }}" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button class="btn dropdown-item" type="submit">
|
||||||
|
<i class="fa-solid fa-right-from-bracket"></i>
|
||||||
|
{{ _("Log Out") }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for("auth.login") }}">
|
||||||
|
<i class="fa-solid fa-right-to-bracket"></i>
|
||||||
|
{{ _("Log In") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<span class="nav-link dropdown-toggle" data-bs-toggle="dropdown">
|
||||||
|
<i class="fa-solid fa-language"></i>
|
||||||
|
</span>
|
||||||
|
<form action="{{ url_for("locale.set-locale") }}" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="next" value="{{ request.full_path if request.query_string else request.path }}">
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
{% for locale_code, locale_name in get_all_linguas().items() %}
|
||||||
|
<li>
|
||||||
|
<button class="dropdown-item {% if locale_code == get_locale() %} active {% endif %}" type="submit" name="locale" value="{{ locale_code }}">
|
||||||
|
{{ locale_name }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h1>{% block header %}{% endblock %}</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
{% if category == "success" %}
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% elif category == "error" %}
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<strong>{{ _("Error:") }}</strong> {{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<main class="pb-5">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
24
tests/testsite/templates/home.html
Normal file
24
tests/testsite/templates/home.html
Normal file
@ -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 %}
|
35
tests/testsite/templates/login.html
Normal file
35
tests/testsite/templates/login.html
Normal file
@ -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 %}
|
||||||
|
|
||||||
|
<form action="{{ url_for("auth.login") }}" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button class="btn btn-primary" type="submit" name="username" value="viewer">{{ _("Viewer") }}</button>
|
||||||
|
<button class="btn btn-primary" type="submit" name="username" value="editor">{{ _("Editor") }}</button>
|
||||||
|
<button class="btn btn-primary" type="submit" name="username" value="nobody">{{ _("Nobody") }}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
3
tests/testsite/translations/babel.cfg
Normal file
3
tests/testsite/translations/babel.cfg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[python: **.py]
|
||||||
|
[jinja2: **/templates/**.html]
|
||||||
|
[javascript: **/static/js/**.js]
|
54
tests/testsite/translations/zh_Hant/LC_MESSAGES/messages.po
Normal file
54
tests/testsite/translations/zh_Hant/LC_MESSAGES/messages.po
Normal file
@ -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 <imacat@mail.imacat.idv.tw>, 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 <imacat@mail.imacat.idv.tw>\n"
|
||||||
|
"Language: zh_Hant\n"
|
||||||
|
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\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 "沒有權限者"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user