6 Commits

14 changed files with 89 additions and 132 deletions

View File

@ -13,7 +13,7 @@ sys.path.insert(0, os.path.abspath('../../src/'))
project = 'Mia! Accounting Flask'
copyright = '2023, imacat'
author = 'imacat'
release = '0.9.0'
release = '0.9.1'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@ -17,7 +17,7 @@
[metadata]
name = mia-accounting-flask
version = 0.9.0
version = 0.9.1
author = imacat
author_email = imacat@mail.imacat.idv.tw
description = The Mia! Accounting Flask project.

View File

@ -770,17 +770,10 @@ class JournalEntryLineItem(db.Model):
frac: Decimal = (value - whole).normalize()
return str(whole) + str(abs(frac))[1:]
journal_entry_day: date = self.journal_entry.date
description: str = "" if self.description is None else self.description
return [description,
str(journal_entry_day.year),
"{}/{}".format(journal_entry_day.year,
journal_entry_day.month),
"{}/{}".format(journal_entry_day.month,
journal_entry_day.day),
"{}/{}/{}".format(journal_entry_day.year,
journal_entry_day.month,
journal_entry_day.day),
return ["{}/{}/{}".format(self.journal_entry.date.year,
self.journal_entry.date.month,
self.journal_entry.date.day),
"" if self.description is None else self.description,
format_amount(self.amount)]

View File

@ -38,26 +38,49 @@ First written: 2023/3/22
</a>
</div>
<div class="form-floating mb-3">
<input id="accounting-default-currency" class="form-control" value="{{ obj.default_currency_text }}" readonly="readonly">
<label class="form-label" for="accounting-default-currency">{{ A_("Default Currency") }}</label>
</div>
<table class="table table-striped table-hover table-light" aria-label="{{ A_("Settings") }}">
<tbody>
<tr>
<th scope="row">{{ A_("Default Currency") }}</th>
<td>{{ obj.default_currency_text }}</td>
</tr>
<tr>
<th scope="row">{{ A_("Default Account for the Income and Expenses Log") }}</th>
<td>{{ obj.default_ie_account_code_text }}</td>
</tr>
</tbody>
</table>
<div class="form-floating mb-3">
<input id="accounting-default-ie-account" class="form-control" value="{{ obj.default_ie_account_code_text }}" readonly="readonly">
<label class="form-label" for="accounting-default-ie-account">{{ A_("Default Account for the Income and Expenses Log") }}</label>
</div>
<h2>{{ A_("Recurring Expense") }}</h2>
{% with expense_income = "expense",
label = A_("Recurring Expense"),
recurring_items = obj.recurring.expenses %}
{% include "accounting/option/include/detail-recurring-expense-income.html" %}
{% endwith %}
{% if obj.recurring.expenses %}
<ul class="list-group mb-3 accounting-list-group-stripped accounting-list-group-hover">
{% for recurring_item in obj.recurring.expenses %}
<li class="list-group-item">
<div class="small">{{ recurring_item.account_text }}</div>
<div>{{ recurring_item.name }}</div>
<div class="small">{{ recurring_item.description_template }}</div>
</li>
{% endfor %}
</ul>
{% else %}
<p>{{ A_("There is no data.") }}</p>
{% endif %}
{% with expense_income = "income",
label = A_("Recurring Income"),
recurring_items = obj.recurring.incomes %}
{% include "accounting/option/include/detail-recurring-expense-income.html" %}
{% endwith %}
<h2>{{ A_("Recurring Income") }}</h2>
{% if obj.recurring.incomes %}
<ul class="list-group mb-3 accounting-list-group-stripped accounting-list-group-hover">
{% for recurring_item in obj.recurring.incomes %}
<li class="list-group-item">
<div class="small">{{ recurring_item.account_text }}</div>
<div>{{ recurring_item.name }}</div>
<div class="small">{{ recurring_item.description_template }}</div>
</li>
{% endfor %}
</ul>
{% else %}
<p>{{ A_("There is no data.") }}</p>
{% endif %}
{% endblock %}

View File

@ -1,31 +0,0 @@
{#
The Mia! Accounting Flask Project
detail-recurring-expense-income.html: The recurring expense or income in the option detail
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/3/22
#}
<div id="accounting-recurring-{{ expense_income }}" class="form-control mb-3 accounting-material-text-field {% if recurring_items %} accounting-not-empty {% endif %}">
<label class="form-label" for="accounting-recurring-{{ expense_income }}">{{ label }}</label>
{% if recurring_items %}
<ul class="list-group mb-2 mt-2">
{% for item in recurring_items %}
{% include "accounting/option/include/detail-recurring-item.html" %}
{% endfor %}
</ul>
{% endif %}
</div>

View File

@ -1,28 +0,0 @@
{#
The Mia! Accounting Flask Project
detail-recurring-item.html: The recurring item in the option detail
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/3/22
#}
{# <ul> For SonarQube not to complain about incorrect HTML #}
<li class="list-group-item list-group-item-action">
<div class="small">{{ item.account_text }}</div>
<div>{{ item.name }}</div>
<div class="small">{{ item.description_template }}</div>
</li>
{# </ul> For SonarQube not to complain about incorrect HTML #}

View File

@ -547,8 +547,8 @@ class AccountTestCase(unittest.TestCase):
:return: None.
"""
from accounting.models import Account
editor_username, editor2_username = "editor", "editor2"
client, csrf_token = get_client(self.app, editor2_username)
editor_username, admin_username = "editor", "admin"
client, csrf_token = get_client(self.app, admin_username)
detail_uri: str = f"{PREFIX}/{CASH.code}"
update_uri: str = f"{PREFIX}/{CASH.code}/update"
account: Account
@ -571,7 +571,7 @@ class AccountTestCase(unittest.TestCase):
self.assertEqual(account.created_by.username,
editor_username)
self.assertEqual(account.updated_by.username,
editor2_username)
admin_username)
def test_l10n(self) -> None:
"""Tests the localization.

View File

@ -471,8 +471,8 @@ class CurrencyTestCase(unittest.TestCase):
:return: None.
"""
from accounting.models import Currency
editor_username, editor2_username = "editor", "editor2"
client, csrf_token = get_client(self.app, editor2_username)
editor_username, admin_username = "editor", "admin"
client, csrf_token = get_client(self.app, admin_username)
detail_uri: str = f"{PREFIX}/{USD.code}"
update_uri: str = f"{PREFIX}/{USD.code}/update"
currency: Currency
@ -493,7 +493,7 @@ class CurrencyTestCase(unittest.TestCase):
with self.app.app_context():
currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.created_by.username, editor_username)
self.assertEqual(currency.updated_by.username, editor2_username)
self.assertEqual(currency.updated_by.username, admin_username)
def test_api_exists(self) -> None:
"""Tests the API to check if a code exists.

View File

@ -537,8 +537,8 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry
journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form())
editor_username, editor2_username = "editor", "editor2"
client, csrf_token = get_client(self.app, editor2_username)
editor_username, admin_username = "editor", "admin"
client, csrf_token = get_client(self.app, admin_username)
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next"
update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
journal_entry: JournalEntry
@ -562,7 +562,7 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
self.assertEqual(journal_entry.created_by.username,
editor_username)
self.assertEqual(journal_entry.updated_by.username,
editor2_username)
admin_username)
def test_delete(self) -> None:
"""Tests to delete a journal entry.
@ -1163,8 +1163,8 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry
journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form())
editor_username, editor2_username = "editor", "editor2"
client, csrf_token = get_client(self.app, editor2_username)
editor_username, admin_username = "editor", "admin"
client, csrf_token = get_client(self.app, admin_username)
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next"
update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
journal_entry: JournalEntry
@ -1188,7 +1188,7 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
self.assertEqual(journal_entry.created_by.username,
editor_username)
self.assertEqual(journal_entry.updated_by.username,
editor2_username)
admin_username)
def test_delete(self) -> None:
"""Tests to delete a journal entry.
@ -1837,8 +1837,8 @@ class TransferJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry
journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form())
editor_username, editor2_username = "editor", "editor2"
client, csrf_token = get_client(self.app, editor2_username)
editor_username, admin_username = "editor", "admin"
client, csrf_token = get_client(self.app, admin_username)
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next"
update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
journal_entry: JournalEntry
@ -1862,7 +1862,7 @@ class TransferJournalEntryTestCase(unittest.TestCase):
self.assertEqual(journal_entry.created_by.username,
editor_username)
self.assertEqual(journal_entry.updated_by.username,
editor2_username)
admin_username)
def test_save_as_receipt(self) -> None:
"""Tests to save a transfer journal entry as a cash receipt journal

View File

@ -67,7 +67,7 @@ class OptionTestCase(unittest.TestCase):
self.assertEqual(result.exit_code, 0)
Option.query.delete()
self.client, self.csrf_token = get_client(self.app, "editor")
self.client, self.csrf_token = get_client(self.app, "admin")
self.data: TestData = TestData(self.app, self.client, self.csrf_token)
def test_nobody(self) -> None:
@ -104,12 +104,12 @@ class OptionTestCase(unittest.TestCase):
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token))
self.assertEqual(response.status_code, 403)
def test_editor2(self) -> None:
"""Test the permission as non-administrator.
def test_editor(self) -> None:
"""Test the permission as editor.
:return: None.
"""
client, csrf_token = get_client(self.app, "editor2")
client, csrf_token = get_client(self.app, "editor")
response: httpx.Response
response = client.get(DETAIL_URI)
@ -121,7 +121,7 @@ class OptionTestCase(unittest.TestCase):
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token))
self.assertEqual(response.status_code, 403)
def test_editor(self) -> None:
def test_admin(self) -> None:
"""Test the permission as administrator.
:return: None.
@ -343,7 +343,7 @@ class OptionTestCase(unittest.TestCase):
"""
from accounting.models import Option
from accounting.utils.user import get_user_pk
editor_username, editor2_username = "editor", "editor2"
admin_username, editor_username = "admin", "editor"
option: Option | None
response: httpx.Response
@ -352,11 +352,11 @@ class OptionTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], DETAIL_URI)
with self.app.app_context():
editor2_pk: int = get_user_pk(editor2_username)
editor_pk: int = get_user_pk(editor_username)
option = db.session.get(Option, "recurring")
self.assertIsNotNone(option)
option.created_by_id = editor2_pk
option.updated_by_id = editor2_pk
option.created_by_id = editor_pk
option.updated_by_id = editor_pk
db.session.commit()
form: dict[str, str] = self.__get_form()
@ -371,8 +371,8 @@ class OptionTestCase(unittest.TestCase):
with self.app.app_context():
option = db.session.get(Option, "recurring")
self.assertIsNotNone(option)
self.assertEqual(option.created_by.username, editor2_username)
self.assertEqual(option.updated_by.username, editor_username)
self.assertEqual(option.created_by.username, editor_username)
self.assertEqual(option.updated_by.username, admin_username)
def __get_form(self, csrf_token: str | None = None) -> dict[str, str]:
"""Returns the option form.

View File

@ -72,15 +72,15 @@ def create_app(is_testing: bool = False) -> Flask:
def can_view(self) -> bool:
return auth.current_user() is not None \
and auth.current_user().username in ["viewer", "editor",
"editor2"]
"admin"]
def can_edit(self) -> bool:
return auth.current_user() is not None \
and auth.current_user().username in ["editor", "editor2"]
and auth.current_user().username in ["editor", "admin"]
def can_admin(self) -> bool:
return auth.current_user() is not None \
and auth.current_user().username == "editor"
and auth.current_user().username == "admin"
@property
def cls(self) -> t.Type[auth.User]:
@ -112,7 +112,7 @@ def init_db_command() -> None:
"""Initializes the database."""
db.create_all()
from .auth import User
for username in ["viewer", "editor", "editor2", "nobody"]:
for username in ["viewer", "editor", "admin", "nobody"]:
if User.query.filter(User.username == username).first() is None:
db.session.add(User(username=username))
db.session.commit()

View File

@ -58,8 +58,8 @@ def login() -> redirect:
:return: The redirection to the home page.
"""
if request.form.get("username") not in ["viewer", "editor", "editor2",
"nobody"]:
if request.form.get("username") not in {"viewer", "editor", "admin",
"nobody"}:
return redirect(url_for("auth.login"))
session["user"] = request.form.get("username")
return redirect(url_for("home.home"))

View File

@ -29,7 +29,7 @@ First written: 2023/1/27
<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="editor2">{{ _("Editor2") }}</button>
<button class="btn btn-primary" type="submit" name="username" value="admin">{{ _("Administrator") }}</button>
<button class="btn btn-primary" type="submit" name="username" value="nobody">{{ _("Nobody") }}</button>
</form>

View File

@ -9,8 +9,8 @@ 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-02-27 10:07+0800\n"
"PO-Revision-Date: 2023-02-27 10:08+0800\n"
"POT-Creation-Date: 2023-03-24 08:32+0800\n"
"PO-Revision-Date: 2023-03-24 08:33+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"
@ -18,27 +18,27 @@ msgstr ""
"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"
"Generated-By: Babel 2.12.1\n"
#: tests/test_site/templates/base.html:23
msgid "en"
msgstr "zh-Hant"
#: tests/test_site/templates/base.html:43
#: tests/test_site/templates/base.html:46
#: tests/test_site/templates/home.html:24
msgid "Home"
msgstr "首頁"
#: tests/test_site/templates/base.html:68
#: tests/test_site/templates/base.html:71
msgid "Log Out"
msgstr "登出"
#: tests/test_site/templates/base.html:78
#: tests/test_site/templates/base.html:81
#: tests/test_site/templates/login.html:24
msgid "Log In"
msgstr "登入"
#: tests/test_site/templates/base.html:119
#: tests/test_site/templates/base.html:122
msgid "Error:"
msgstr "錯誤:"
@ -51,8 +51,8 @@ msgid "Editor"
msgstr "記帳者"
#: tests/test_site/templates/login.html:32
msgid "Editor2"
msgstr "記帳者2"
msgid "Administrator"
msgstr "管理者"
#: tests/test_site/templates/login.html:33
msgid "Nobody"