Compare commits
6 Commits
7c512b1c15
...
v0.9.1
Author | SHA1 | Date | |
---|---|---|---|
bf2c7bb785 | |||
93ba086548 | |||
5c4f6017b8 | |||
cb16b2f0ff | |||
d2f11e8779 | |||
4ccaf01b3c |
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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)]
|
||||
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
@ -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 #}
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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"))
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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"
|
||||
|
Reference in New Issue
Block a user