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:
依瑪貓 2023-01-29 22:28:27 +08:00
parent 9c83ad97c1
commit 14638f574e
45 changed files with 3302 additions and 0 deletions

202
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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

View File

View File

View 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:

View 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:

View 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
View 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
View 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
View File

@ -0,0 +1,7 @@
src
===
.. toctree::
:maxdepth: 4
accounting

20
pyproject.toml Normal file
View 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
View 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/**

View 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)

View 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)

View 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."""

View 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."""

View 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()

View 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)

View 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
View 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

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View File

@ -0,0 +1,3 @@
[python: **.py]
[jinja2: **/templates/**.html]
[javascript: **/static/js/**.js]

View File

@ -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 "下一頁"

View 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.
"""

View 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)

View 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

View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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

View 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>

View 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 %}

View 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 %}

View File

@ -0,0 +1,3 @@
[python: **.py]
[jinja2: **/templates/**.html]
[javascript: **/static/js/**.js]

View 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 "沒有權限者"