19 Commits

Author SHA1 Message Date
8dc340dbf1 Advanced to version 0.2.0. 2023-02-07 20:55:00 +08:00
4b5b348270 Implemented the incremental search (search-as-you-type) in the base account selector of the account form. 2023-02-07 20:27:25 +08:00
d9585f0e53 Fixed a translated message. 2023-02-07 20:20:18 +08:00
5737d6cef4 Fixed the error message in the Javascript validateAsyncCodeIsDuplicated function in currency-form.js. 2023-02-07 20:20:16 +08:00
1d61fa93d3 Prepend all the HTML ID and class names with "accounting-" to avoid name conflict. 2023-02-07 20:20:01 +08:00
b1c7bc61c4 Renamed the can_view_accounting and can_edit_accounting template functions to accounting_can_view and accounting_can_edit, respectively. 2023-02-07 17:15:05 +08:00
708a434b5d Renamed the append_next, inherit_next, and or_next template filters to accounting_append_next, accounting_inherit_next, and accounting_or_next, to avoid name conflict. 2023-02-07 17:13:06 +08:00
8e524674a3 Added the init_app function to the "accounting.utils.next_url" module to initialize the template filters, and apply it to the init_app function of the accounting application. 2023-02-07 17:10:03 +08:00
699db20308 Revised the init_app function of the "accounting.utils.permission" module to register the "can_view" and "can_edit" functions under the blueprint instead of the whole application. 2023-02-07 17:05:27 +08:00
c3cedf714b Revised the documentation of the init_app function of the "accounting.locale", "accounting.base_account", "accounting.account", and "accounting.currency" modules. 2023-02-07 17:01:08 +08:00
c67ed4471c Fixed the permission so that the template helper also use the wrapper can_view and can_edit, that can_edit always requires the user to log in first. 2023-02-07 16:58:46 +08:00
2d3b9f68b8 Added the missing Material Floating Action Button to add a new currency for the mobile devices to the currency list. 2023-02-07 16:43:42 +08:00
f82278b48a Updated the icon of the currency management in the navigation menu. 2023-02-07 16:36:18 +08:00
85480804e7 Updated the translation. 2023-02-07 16:23:46 +08:00
9e85c14431 Changed the can_edit permission to at least require the user to log in first. 2023-02-07 16:03:13 +08:00
31dc8fab04 Changed the type hint of the "current_user" pseudo property of the AbstractUserUtils class to return None when the user has not logged in. 2023-02-07 16:00:51 +08:00
dc24af1db0 Added the get_current_user function to the "accounting.utils.user" module to retrieve the currently logged-in user and cache it in the current request. 2023-02-07 16:00:47 +08:00
59795635ee Updated the Sphinx documentation. 2023-02-07 11:41:28 +08:00
399afe56c8 Added the initial values for the database instance, the user class and the user primary key column, to allow the Sphinx documentation system to work properly. 2023-02-07 11:40:48 +08:00
34 changed files with 515 additions and 197 deletions

View File

@ -0,0 +1,53 @@
accounting.account package
==========================
Submodules
----------
accounting.account.commands module
----------------------------------
.. automodule:: accounting.account.commands
:members:
:undoc-members:
:show-inheritance:
accounting.account.converters module
------------------------------------
.. automodule:: accounting.account.converters
:members:
:undoc-members:
:show-inheritance:
accounting.account.forms module
-------------------------------
.. automodule:: accounting.account.forms
:members:
:undoc-members:
:show-inheritance:
accounting.account.query module
-------------------------------
.. automodule:: accounting.account.query
:members:
:undoc-members:
:show-inheritance:
accounting.account.views module
-------------------------------
.. automodule:: accounting.account.views
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.account
:members:
:undoc-members:
:show-inheritance:

View File

@ -12,18 +12,10 @@ accounting.base\_account.commands module
:undoc-members:
:show-inheritance:
accounting.base\_account.database module
----------------------------------------
accounting.base\_account.converters module
------------------------------------------
.. automodule:: accounting.base_account.database
:members:
:undoc-members:
:show-inheritance:
accounting.base\_account.models module
--------------------------------------
.. automodule:: accounting.base_account.models
.. automodule:: accounting.base_account.converters
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,53 @@
accounting.currency package
===========================
Submodules
----------
accounting.currency.commands module
-----------------------------------
.. automodule:: accounting.currency.commands
:members:
:undoc-members:
:show-inheritance:
accounting.currency.converters module
-------------------------------------
.. automodule:: accounting.currency.converters
:members:
:undoc-members:
:show-inheritance:
accounting.currency.forms module
--------------------------------
.. automodule:: accounting.currency.forms
:members:
:undoc-members:
:show-inheritance:
accounting.currency.query module
--------------------------------
.. automodule:: accounting.currency.query
:members:
:undoc-members:
:show-inheritance:
accounting.currency.views module
--------------------------------
.. automodule:: accounting.currency.views
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.currency
:members:
:undoc-members:
:show-inheritance:

View File

@ -7,12 +7,22 @@ Subpackages
.. toctree::
:maxdepth: 4
accounting.account
accounting.base_account
accounting.currency
accounting.utils
Submodules
----------
accounting.database module
--------------------------
.. automodule:: accounting.database
:members:
:undoc-members:
:show-inheritance:
accounting.locale module
------------------------
@ -21,6 +31,14 @@ accounting.locale module
:undoc-members:
:show-inheritance:
accounting.models module
------------------------
.. automodule:: accounting.models
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -4,6 +4,14 @@ accounting.utils package
Submodules
----------
accounting.utils.next\_url module
---------------------------------
.. automodule:: accounting.utils.next_url
:members:
:undoc-members:
:show-inheritance:
accounting.utils.pagination module
----------------------------------
@ -28,6 +36,30 @@ accounting.utils.query module
:undoc-members:
:show-inheritance:
accounting.utils.random\_id module
----------------------------------
.. automodule:: accounting.utils.random_id
:members:
:undoc-members:
:show-inheritance:
accounting.utils.strip\_text module
-----------------------------------
.. automodule:: accounting.utils.strip_text
:members:
:undoc-members:
:show-inheritance:
accounting.utils.user module
----------------------------
.. automodule:: accounting.utils.user
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------

View File

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

View File

@ -55,7 +55,7 @@ def init_app(app: Flask, user_utils: AbstractUserUtils,
locale.init_app(app, bp)
from .utils import permission
permission.init_app(app, can_view_func, can_edit_func)
permission.init_app(bp, can_view_func, can_edit_func)
from . import base_account
base_account.init_app(app, bp)
@ -66,9 +66,7 @@ def init_app(app: Flask, user_utils: AbstractUserUtils,
from . import currency
currency.init_app(app, bp)
from .utils.next_url import append_next, inherit_next, or_next
bp.add_app_template_filter(append_next, "append_next")
bp.add_app_template_filter(inherit_next, "inherit_next")
bp.add_app_template_filter(or_next, "or_next")
from .utils import next_url
next_url.init_app(bp)
app.register_blueprint(bp)

View File

@ -23,8 +23,8 @@ 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.
:param bp: The blueprint of the accounting application.
:return: None.
"""
from .converters import AccountConverter

View File

@ -23,8 +23,8 @@ 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.
:param bp: The blueprint of the accounting application.
:return: None.
"""
from .converters import BaseAccountConverter

View File

@ -23,8 +23,8 @@ 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.
:param bp: The blueprint of the accounting application.
:return: None.
"""
from .converters import CurrencyConverter

View File

@ -25,7 +25,7 @@ time.
from flask_sqlalchemy import SQLAlchemy
db: SQLAlchemy
db: SQLAlchemy = SQLAlchemy()
"""The database instance."""

View File

@ -116,8 +116,8 @@ def __babel_js_catalog_view() -> Response:
def init_app(app: Flask, bp: Blueprint) -> None:
"""Initializes the application.
:param bp: The blueprint of the accounting application.
:param app: The Flask application.
:param bp: The blueprint of the accounting application.
:return: None.
"""
bp.add_url_rule("/_jstrans.js", "babel_catalog",

View File

@ -64,6 +64,14 @@ class BaseAccount(db.Model):
return l10n.title
return self.title_l10n
@property
def query_values(self) -> list[str]:
"""Returns the values to be queried.
:return: The values to be queried.
"""
return [self.code, self.title_l10n] + [x.title for x in self.l10n]
class BaseAccountL10n(db.Model):
"""A localized base account title."""

View File

@ -21,65 +21,50 @@
* First written: 2023/2/1
*/
.clickable {
.accounting-clickable {
cursor: pointer;
}
.btn-group .btn .search-input {
.btn-group .btn .accounting-search-input {
min-height: calc(1em + .5rem + 2px);
padding: 0 0.5rem;
}
.btn-group .btn .search-label button {
.btn-group .btn .accounting-search-label button {
border: none;
background-color: transparent;
color: inherit;
padding-right: 0;
}
/** The account management */
.account {
/** The card layout */
.accounting-card {
padding: 2em 1.5em;
margin: 1em;
background-color: #E9ECEF;
border-radius: 0.3em;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.account .account-title {
.accounting-card-title {
font-size: 1.8rem;
font-weight: bolder;
}
.account .account-code {
.accounting-card-code {
font-size: 1.4rem;
color: #373b3e;
}
.list-base-selector {
/** The option selector */
.accounting-selector-list {
height: 20rem;
overflow-y: scroll;
}
/** The currency management. */
.currency {
padding: 2em 1.5em;
margin: 1em;
background-color: #E9ECEF;
border-radius: 0.3em;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.currency .currency-name {
font-size: 1.8rem;
font-weight: bolder;
}
.currency .currency-code {
font-size: 1.4rem;
color: #373b3e;
}
/* The Material Design text field (floating form control in Bootstrap) */
.material-text-field {
.accounting-material-text-field {
position: relative;
min-height: calc(3.5rem + 2px);
padding-top: 1.625rem;
}
.material-text-field > .form-label {
.accounting-material-text-field > .form-label {
position: absolute;
top: 0;
left: 0;
@ -88,27 +73,27 @@
transform-origin: 0 0;
transition: opacity .1s ease-in-out,transform .1s ease-in-out;
}
.material-text-field.not-empty > .form-label {
.accounting-material-text-field.accounting-not-empty > .form-label {
opacity: 0.65;
transform: scale(.85) translateY(-.5rem) translateX(.15rem);
}
/* The Material Design floating action buttons */
.material-fab {
.accounting-material-fab {
position: fixed;
right: 2rem;
bottom: 1rem;
z-index: 10;
flex-direction: column-reverse;
}
.material-fab .btn {
.accounting-material-fab .btn {
border-radius: 50%;
transform: scale(1.5);
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0,0,0,.12);
display: block;
margin-top: 2.5rem;
}
.material-fab .btn:hover, .material-fab .btn:focus {
.accounting-material-fab .btn:hover, .accounting-material-fab .btn:focus {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0,0,0,.12);
}

View File

@ -24,11 +24,11 @@
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", function () {
initializeBaseAccountSelector();
document.getElementById("account-base-code")
document.getElementById("accounting-base-code")
.onchange = validateBase;
document.getElementById("account-title")
document.getElementById("accounting-title")
.onchange = validateTitle;
document.getElementById("account-form")
document.getElementById("accounting-form")
.onsubmit = validateForm;
});
@ -38,25 +38,25 @@ document.addEventListener("DOMContentLoaded", function () {
* @private
*/
function initializeBaseAccountSelector() {
const selector = document.getElementById("select-base-modal");
const base = document.getElementById("account-base");
const baseCode = document.getElementById("account-base-code");
const baseContent = document.getElementById("account-base-content");
const options = Array.from(document.getElementsByClassName("list-group-item-base"));
const btnClear = document.getElementById("btn-clear-base");
const selector = document.getElementById("accounting-base-selector-model");
const base = document.getElementById("accounting-base");
const baseCode = document.getElementById("accounting-base-code");
const baseContent = document.getElementById("accounting-base-content");
const options = Array.from(document.getElementsByClassName("accounting-base-option"));
const btnClear = document.getElementById("accounting-btn-clear-base");
selector.addEventListener("show.bs.modal", function () {
base.classList.add("not-empty");
base.classList.add("accounting-not-empty");
options.forEach(function (item) {
item.classList.remove("active");
});
const selected = document.getElementById("list-group-item-base-" + baseCode.value);
const selected = document.getElementById("accounting-base-option-" + baseCode.value);
if (selected !== null) {
selected.classList.add("active");
}
});
selector.addEventListener("hidden.bs.modal", function () {
if (baseCode.value === "") {
base.classList.remove("not-empty");
base.classList.remove("accounting-not-empty");
}
});
options.forEach(function (option) {
@ -79,6 +79,54 @@ function initializeBaseAccountSelector() {
validateBase();
bootstrap.Modal.getInstance(selector).hide();
}
initializeBaseAccountQuery();
}
/**
* Initializes the query on the base account options.
*
* @private
*/
function initializeBaseAccountQuery() {
const query = document.getElementById("accounting-base-selector-query");
const optionList = document.getElementById("accounting-base-option-list");
const options = Array.from(document.getElementsByClassName("accounting-base-option"));
const queryNoResult = document.getElementById("accounting-base-option-no-result");
query.addEventListener("input", function () {
console.log(query.value);
if (query.value === "") {
options.forEach(function (option) {
option.classList.remove("d-none");
});
optionList.classList.remove("d-none");
queryNoResult.classList.add("d-none");
return
}
let hasAnyMatched = false;
options.forEach(function (option) {
const queryValues = JSON.parse(option.dataset.queryValues);
let isMatched = false;
for (let i = 0; i < queryValues.length; i++) {
if (queryValues[i].includes(query.value)) {
isMatched = true;
break;
}
}
if (isMatched) {
option.classList.remove("d-none");
hasAnyMatched = true;
} else {
option.classList.add("d-none");
}
});
if (!hasAnyMatched) {
optionList.classList.add("d-none");
queryNoResult.classList.remove("d-none");
} else {
optionList.classList.remove("d-none");
queryNoResult.classList.add("d-none");
}
});
}
/**
@ -101,9 +149,9 @@ function validateForm() {
* @private
*/
function validateBase() {
const field = document.getElementById("account-base-code");
const error = document.getElementById("account-base-code-error");
const displayField = document.getElementById("account-base");
const field = document.getElementById("accounting-base-code");
const error = document.getElementById("accounting-base-code-error");
const displayField = document.getElementById("accounting-base");
field.value = field.value.trim();
if (field.value === "") {
displayField.classList.add("is-invalid");
@ -122,8 +170,8 @@ function validateBase() {
* @private
*/
function validateTitle() {
const field = document.getElementById("account-title");
const error = document.getElementById("account-title-error");
const field = document.getElementById("accounting-title");
const error = document.getElementById("accounting-title-error");
field.value = field.value.trim();
if (field.value === "") {
field.classList.add("is-invalid");

View File

@ -23,13 +23,13 @@
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", function () {
const list = document.getElementById("account-order-list");
const list = document.getElementById("accounting-order-list");
if (list !== null) {
const onReorder = function () {
const accounts = Array.from(list.children);
for (let i = 0; i < accounts.length; i++) {
const no = document.getElementById("account-order-" + accounts[i].dataset.id + "-no");
const code = document.getElementById("account-order-" + accounts[i].dataset.id + "-code");
const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no");
const code = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-code");
no.value = String(i + 1);
code.innerText = list.dataset.baseCode + "-" + ("000" + (i + 1)).slice(-3);
}

View File

@ -23,11 +23,11 @@
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("currency-code")
document.getElementById("accounting-code")
.onchange = validateCode;
document.getElementById("currency-name")
document.getElementById("accounting-name")
.onchange = validateName;
document.getElementById("currency-form")
document.getElementById("accounting-form")
.onsubmit = validateForm;
});
@ -69,7 +69,7 @@ function submitFormIfAllAsyncValid() {
isValid = isAsyncValid[key] && isValid;
});
if (isValid) {
document.getElementById("currency-form").submit()
document.getElementById("accounting-form").submit()
}
}
@ -84,8 +84,8 @@ function validateCode(changeEvent = null) {
const key = "code";
const isSubmission = changeEvent === null;
let hasAsyncValidation = false;
const field = document.getElementById("currency-code");
const error = document.getElementById("currency-code-error");
const field = document.getElementById("accounting-code");
const error = document.getElementById("accounting-code-error");
field.value = field.value.trim();
if (field.value === "") {
field.classList.add("is-invalid");
@ -125,15 +125,15 @@ function validateCode(changeEvent = null) {
* @private
*/
function validateAsyncCodeIsDuplicated(isSubmission, key) {
const field = document.getElementById("currency-code");
const error = document.getElementById("currency-code-error");
const field = document.getElementById("accounting-code");
const error = document.getElementById("accounting-code-error");
const url = field.dataset.existsUrl;
const onLoad = function () {
if (this.status === 200) {
const result = JSON.parse(this.responseText);
if (result["exists"]) {
field.classList.add("is-invalid");
error.innerText = _("Code conflicts with another currency.");
error.innerText = A_("Code conflicts with another currency.");
if (isSubmission) {
isAsyncValid[key] = false;
}
@ -160,8 +160,8 @@ function validateAsyncCodeIsDuplicated(isSubmission, key) {
* @private
*/
function validateName() {
const field = document.getElementById("currency-name");
const error = document.getElementById("currency-name-error");
const field = document.getElementById("accounting-name");
const error = document.getElementById("accounting-name-error");
field.value = field.value.trim();
if (field.value === "") {
field.classList.add("is-invalid");

View File

@ -26,47 +26,47 @@ First written: 2023/1/31
{% block content %}
<div class="btn-group mb-3">
<a class="btn btn-primary" href="{{ url_for("accounting.account.list")|or_next }}">
<a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }}
</a>
{% if can_edit_accounting() %}
<a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.account.edit", account=obj)|inherit_next }}">
{% if accounting_can_edit() %}
<a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
<i class="fa-solid fa-gear"></i>
{{ A_("Settings") }}
</a>
{% endif %}
<a class="btn btn-primary" href="{{ url_for("accounting.account.order", base=obj.base)|append_next }}">
<a class="btn btn-primary" href="{{ url_for("accounting.account.order", base=obj.base)|accounting_append_next }}">
<i class="fa-solid fa-bars-staggered"></i>
{{ A_("Order") }}
</a>
{% if can_edit_accounting() %}
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#delete-modal">
{% if accounting_can_edit() %}
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
<i class="fa-solid fa-trash"></i>
{{ A_("Delete") }}
</button>
{% endif %}
</div>
{% if can_edit_accounting() %}
<div class="d-md-none material-fab">
<a class="btn btn-primary" href="{{ url_for("accounting.account.edit", account=obj)|inherit_next }}">
{% if accounting_can_edit() %}
<div class="d-md-none accounting-material-fab">
<a class="btn btn-primary" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
<i class="fa-solid fa-pen-to-square"></i>
</a>
</div>
{% endif %}
{% if can_edit_accounting() %}
<form id="delete-form" action="{{ url_for("accounting.account.delete", account=obj) }}" method="post">
<input id="csrf_token" type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if accounting_can_edit() %}
<form action="{{ url_for("accounting.account.delete", account=obj) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if "next" in request.args %}
<input type="hidden" name="next" value="{{ request.args["next"] }}">
{% endif %}
<div class="modal fade" id="delete-modal" tabindex="-1" aria-labelledby="delete-model-label" aria-hidden="true">
<div class="modal fade" id="accounting-delete-modal" tabindex="-1" aria-labelledby="accounting-delete-model-label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="delete-model-label">{{ A_("Delete Account Confirmation") }}</h1>
<h1 class="modal-title fs-5" id="accounting-delete-model-label">{{ A_("Delete Account Confirmation") }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
@ -82,9 +82,9 @@ First written: 2023/1/31
</form>
{% endif %}
<div class="account col-sm-6">
<div class="account-title">{{ obj.title }}</div>
<div class="account-code">{{ obj.code }}</div>
<div class="accounting-card col-sm-6">
<div class="accounting-card-title">{{ obj.title }}</div>
<div class="accounting-card-code">{{ obj.code }}</div>
{% if obj.is_offset_needed %}
<div>
<span class="badge rounded-pill bg-info">{{ A_("Offset needed") }}</span>

View File

@ -23,6 +23,6 @@ First written: 2023/2/1
{% block header %}{% block title %}{{ A_("%(account)s Settings", account=account) }}{% endblock %}{% endblock %}
{% block back_url %}{{ url_for("accounting.account.detail", account=account)|inherit_next }}{% endblock %}
{% block back_url %}{{ url_for("accounting.account.detail", account=account)|accounting_inherit_next }}{% endblock %}
{% block action_url %}{{ url_for("accounting.account.update", account=account) }}{% endblock %}

View File

@ -34,16 +34,16 @@ First written: 2023/2/1
</a>
</div>
<form id="account-form" action="{% block action_url %}{% endblock %}" method="post">
<form id="accounting-form" action="{% block action_url %}{% endblock %}" method="post">
{{ form.csrf_token }}
{% if "next" in request.args %}
<input type="hidden" name="next" value="{{ request.args["next"] }}">
{% endif %}
<div class="form-floating mb-3">
<input id="account-base-code" type="hidden" name="base_code" value="{{ "" if form.base_code.data is none else form.base_code.data }}">
<div id="account-base" class="form-control clickable material-text-field {% if form.base_code.data %} not-empty {% endif %} {% if form.base_code.errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#select-base-modal">
<label id="account-base-label" class="form-label" for="account-base">{{ A_("Base account") }}</label>
<div id="account-base-content">
<input id="accounting-base-code" type="hidden" name="base_code" value="{{ "" if form.base_code.data is none else form.base_code.data }}">
<div id="accounting-base" class="form-control accounting-clickable accounting-material-text-field {% if form.base_code.data %} accounting-not-empty {% endif %} {% if form.base_code.errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-base-selector-model">
<label class="form-label" for="accounting-base">{{ A_("Base account") }}</label>
<div id="accounting-base-content">
{% if form.base_code.data %}
{% if form.base_code.errors %}
{{ A_("(Unknown)") }}
@ -53,18 +53,18 @@ First written: 2023/2/1
{% endif %}
</div>
</div>
<div id="account-base-code-error" class="invalid-feedback">{% if form.base_code.errors %}{{ form.base_code.errors[0] }}{% endif %}</div>
<div id="accounting-base-code-error" class="invalid-feedback">{% if form.base_code.errors %}{{ form.base_code.errors[0] }}{% endif %}</div>
</div>
<div class="form-floating mb-3">
<input id="account-title" class="form-control {% if form.title.errors %} is-invalid {% endif %}" type="text" name="title" value="{{ "" if form.title.data is none else form.title.data }}" placeholder=" " required="required">
<label class="form-label" for="account-title">{{ A_("Title") }}</label>
<div id="account-title-error" class="invalid-feedback">{% if form.title.errors %}{{ form.title.errors[0] }}{% endif %}</div>
<input id="accounting-title" class="form-control {% if form.title.errors %} is-invalid {% endif %}" type="text" name="title" value="{{ "" if form.title.data is none else form.title.data }}" placeholder=" " required="required">
<label class="form-label" for="accounting-title">{{ A_("Title") }}</label>
<div id="accounting-title-error" class="invalid-feedback">{% if form.title.errors %}{{ form.title.errors[0] }}{% endif %}</div>
</div>
<div class="form-check form-switch mb-3">
<input id="account-is-offset-needed" class="form-check-input" type="checkbox" name="is_offset_needed" value="1" {% if form.is_offset_needed.data %} checked="checked" {% endif %}>
<label class="form-check-label" for="account-is-offset-needed">
<input id="accounting-is-offset-needed" class="form-check-input" type="checkbox" name="is_offset_needed" value="1" {% if form.is_offset_needed.data %} checked="checked" {% endif %}>
<label class="form-check-label" for="accounting-is-offset-needed">
{{ A_("The entries in the account need offsets.") }}
</label>
</div>
@ -76,43 +76,44 @@ First written: 2023/2/1
</button>
</div>
<div class="d-md-none material-fab">
<div class="d-md-none accounting-material-fab">
<button class="btn btn-primary" type="submit">
<i class="fa-solid fa-floppy-disk"></i>
</button>
</div>
</form>
<div class="modal fade" id="select-base-modal" tabindex="-1" aria-labelledby="select-base-model-label" aria-hidden="true">
<div class="modal fade" id="accounting-base-selector-model" tabindex="-1" aria-labelledby="accounting-base-selector-model-label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="base-selector-model-label">{{ A_("Select Base Account") }}</h1>
<h1 class="modal-title fs-5" id="accounting-base-selector-model-label">{{ A_("Select Base Account") }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="input-group mb-2">
<input id="select-base-query" class="form-control form-control-sm" type="search" placeholder=" " required="required" aria-label="Search">
<label class="input-group-text" for="select-base-query">
<input id="accounting-base-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required" aria-label="Search">
<label class="input-group-text" for="accounting-base-selector-query">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
</label>
</div>
<ul class="list-group list-base-selector">
<ul id="accounting-base-option-list" class="list-group accounting-selector-list">
{% for base in form.base_options %}
<li id="list-group-item-base-{{ base.code }}" class="list-group-item list-group-item-base clickable" data-code="{{ base.code }}" data-content="{{ base }}">
<li id="accounting-base-option-{{ base.code }}" class="list-group-item accounting-base-option accounting-clickable" data-code="{{ base.code }}" data-content="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}">
{{ base }}
</li>
{% endfor %}
</ul>
<p id="accounting-base-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
{% if form.base_code.data %}
<button id="btn-clear-base" type="button" class="btn btn-danger">{{ A_("Clear") }}</button>
<button id="accounting-btn-clear-base" type="button" class="btn btn-danger">{{ A_("Clear") }}</button>
{% else %}
<button id="btn-clear-base" type="button" class="btn btn-secondary" disabled="disabled">{{ A_("Clear") }}</button>
<button id="accounting-btn-clear-base" type="button" class="btn btn-secondary" disabled="disabled">{{ A_("Clear") }}</button>
{% endif %}
</div>
</div>

View File

@ -26,15 +26,15 @@ First written: 2023/1/30
{% block content %}
<div class="btn-group mb-2">
{% if can_edit_accounting() %}
<a class="btn btn-primary text-nowrap d-none d-md-block" href="{{ url_for("accounting.account.create")|append_next }}">
{% if accounting_can_edit() %}
<a class="btn btn-primary text-nowrap d-none d-md-block" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
<i class="fa-solid fa-plus"></i>
{{ A_("New") }}
</a>
{% endif %}
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.account.list") }}" method="get" role="search">
<input id="search-input" class="form-control form-control-sm search-input" type="search" name="q" value="{{ request.args["q"] if "q" in request.args else "" }}" placeholder=" " required="required" aria-label="Search">
<label for="search-input" class="search-label">
<input id="accounting-search" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args["q"] if "q" in request.args else "" }}" placeholder=" " required="required" aria-label="Search">
<label for="accounting-search" class="accounting-search-label">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
@ -43,9 +43,9 @@ First written: 2023/1/30
</form>
</div>
{% if can_edit_accounting() %}
<div class="d-md-none material-fab">
<a class="btn btn-primary" href="{{ url_for("accounting.account.create")|append_next }}">
{% if accounting_can_edit() %}
<div class="d-md-none accounting-material-fab">
<a class="btn btn-primary" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
<i class="fa-solid fa-plus"></i>
</a>
</div>
@ -56,7 +56,7 @@ First written: 2023/1/30
<div class="list-group">
{% for item in list %}
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.account.detail", account=item)|append_next }}">
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.account.detail", account=item)|accounting_append_next }}">
{{ item }}
{% if item.is_offset_needed %}
<span class="badge rounded-pill bg-info">{{ A_("Offset needed") }}</span>

View File

@ -31,24 +31,24 @@ First written: 2023/2/2
{% block content %}
<div class="btn-group mb-3">
<a class="btn btn-primary" href="{{ url_for("accounting.account.list")|or_next }}">
<a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }}
</a>
</div>
{% if base.accounts|length > 1 and can_edit_accounting() %}
{% if base.accounts|length > 1 and accounting_can_edit() %}
<form action="{{ url_for("accounting.account.sort", base=base) }}" method="post">
<input id="csrf_token" type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if "next" in request.args %}
<input type="hidden" name="next" value="{{ request.args["next"] }}">
{% endif %}
<ul id="account-order-list" class="list-group mb-3" data-base-code="{{ base.code }}">
<ul id="accounting-order-list" class="list-group mb-3" data-base-code="{{ base.code }}">
{% for account in base.accounts|sort(attribute="no") %}
<li class="list-group-item d-flex justify-content-between" data-id="{{ account.id }}">
<input id="account-order-{{ account.id }}-no" type="hidden" name="{{ account.id }}-no" value="{{ loop.index }}">
<input id="accounting-order-{{ account.id }}-no" type="hidden" name="{{ account.id }}-no" value="{{ loop.index }}">
<div>
<span id="account-order-{{ account.id }}-code">{{ account.code }}</span>
<span id="accounting-order-{{ account.id }}-code">{{ account.code }}</span>
{{ account.title }}
</div>
<i class="fa-solid fa-bars"></i>
@ -63,7 +63,7 @@ First written: 2023/2/2
</button>
</div>
<div class="d-md-none material-fab">
<div class="d-md-none accounting-material-fab">
<button class="btn btn-primary" type="submit">
<i class="fa-solid fa-floppy-disk"></i>
</button>

View File

@ -26,19 +26,19 @@ First written: 2023/2/1
{% block content %}
<div class="btn-group mb-3">
<a class="btn btn-primary" href="{{ url_for("accounting.account.list")|or_next }}">
<a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }}
</a>
</div>
<div class="account col-sm-6">
<div class="account-title">{{ obj.title }}</div>
<div class="account-code">{{ obj.code }}</div>
<div class="accounting-card col-sm-6">
<div class="accounting-card-title">{{ obj.title }}</div>
<div class="accounting-card-code">{{ obj.code }}</div>
{% if obj.accounts %}
<div>
{% for account in obj.accounts %}
<a class="btn btn-primary" href="{{ url_for("accounting.account.detail", account=account)|append_next }}">
<a class="btn btn-primary" href="{{ url_for("accounting.account.detail", account=account)|accounting_append_next }}">
{{ account }}
</a>
{% endfor %}

View File

@ -27,8 +27,8 @@ First written: 2023/1/26
<div class="btn-group mb-2">
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.base-account.list") }}" method="get" role="search">
<input id="search-input" class="form-control form-control-sm search-input" type="search" name="q" value="{{ request.args["q"] if "q" in request.args else "" }}" placeholder=" " required="required" aria-label="Search">
<label for="search-input" class="search-label">
<input id="accounting-search" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args["q"] if "q" in request.args else "" }}" placeholder=" " required="required" aria-label="Search">
<label for="accounting-search" class="accounting-search-label">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
@ -42,7 +42,7 @@ First written: 2023/1/26
<div class="list-group">
{% for item in list %}
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.base-account.detail", account=item)|append_next }}">
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.base-account.detail", account=item)|accounting_append_next }}">
{{ item }}
</a>
{% endfor %}

View File

@ -26,43 +26,43 @@ First written: 2023/2/6
{% block content %}
<div class="btn-group mb-3">
<a class="btn btn-primary" href="{{ url_for("accounting.currency.list")|or_next }}">
<a class="btn btn-primary" href="{{ url_for("accounting.currency.list")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }}
</a>
{% if can_edit_accounting() %}
<a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.currency.edit", currency=obj)|inherit_next }}">
{% if accounting_can_edit() %}
<a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.currency.edit", currency=obj)|accounting_inherit_next }}">
<i class="fa-solid fa-gear"></i>
{{ A_("Settings") }}
</a>
{% endif %}
{% if can_edit_accounting() %}
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#delete-modal">
{% if accounting_can_edit() %}
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
<i class="fa-solid fa-trash"></i>
{{ A_("Delete") }}
</button>
{% endif %}
</div>
{% if can_edit_accounting() %}
<div class="d-md-none material-fab">
<a class="btn btn-primary" href="{{ url_for("accounting.currency.edit", currency=obj)|inherit_next }}">
{% if accounting_can_edit() %}
<div class="d-md-none accounting-material-fab">
<a class="btn btn-primary" href="{{ url_for("accounting.currency.edit", currency=obj)|accounting_inherit_next }}">
<i class="fa-solid fa-pen-to-square"></i>
</a>
</div>
{% endif %}
{% if can_edit_accounting() %}
<form id="delete-form" action="{{ url_for("accounting.currency.delete", currency=obj) }}" method="post">
<input id="csrf_token" type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if accounting_can_edit() %}
<form action="{{ url_for("accounting.currency.delete", currency=obj) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if "next" in request.args %}
<input type="hidden" name="next" value="{{ request.args["next"] }}">
{% endif %}
<div class="modal fade" id="delete-modal" tabindex="-1" aria-labelledby="delete-model-label" aria-hidden="true">
<div class="modal fade" id="accounting-delete-modal" tabindex="-1" aria-labelledby="accounting-delete-model-label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="delete-model-label">{{ A_("Delete Currency Confirmation") }}</h1>
<h1 class="modal-title fs-5" id="accounting-delete-model-label">{{ A_("Delete Currency Confirmation") }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
@ -78,9 +78,9 @@ First written: 2023/2/6
</form>
{% endif %}
<div class="currency col-sm-6">
<div class="currency-name">{{ obj.name }}</div>
<div class="currency-code">{{ obj.code }}</div>
<div class="accounting-card col-sm-6">
<div class="accounting-card-title">{{ obj.name }}</div>
<div class="accounting-card-code">{{ obj.code }}</div>
<div class="small text-secondary fst-italic">
<div>{{ A_("Created") }} {{ obj.created_at }} {{ obj.created_by }}</div>
<div>{{ A_("Updated") }} {{ obj.updated_at }} {{ obj.updated_by }}</div>

View File

@ -23,7 +23,7 @@ First written: 2023/2/6
{% block header %}{% block title %}{{ A_("%(currency)s Settings", currency=currency) }}{% endblock %}{% endblock %}
{% block back_url %}{{ url_for("accounting.currency.detail", currency=currency)|inherit_next }}{% endblock %}
{% block back_url %}{{ url_for("accounting.currency.detail", currency=currency)|accounting_inherit_next }}{% endblock %}
{% block action_url %}{{ url_for("accounting.currency.update", currency=currency) }}{% endblock %}

View File

@ -34,21 +34,21 @@ First written: 2023/2/6
</a>
</div>
<form id="currency-form" action="{% block action_url %}{% endblock %}" method="post">
<form id="accounting-form" action="{% block action_url %}{% endblock %}" method="post">
{{ form.csrf_token }}
{% if "next" in request.args %}
<input type="hidden" name="next" value="{{ request.args["next"] }}">
{% endif %}
<div class="form-floating mb-3">
<input id="currency-code" class="form-control {% if form.code.errors %} is-invalid {% endif %}" type="text" name="code" value="{{ "" if form.code.data is none else form.code.data }}" placeholder=" " required="required" data-exists-url="{{ url_for("accounting.currency-api.exists") }}" data-original="{% block original_code %}{% endblock %}" data-blocklist="{{ form.CODE_BLOCKLIST|tojson|forceescape }}">
<label class="form-label" for="currency-code">{{ A_("Code") }}</label>
<div id="currency-code-error" class="invalid-feedback">{% if form.code.errors %}{{ form.code.errors[0] }}{% endif %}</div>
<input id="accounting-code" class="form-control {% if form.code.errors %} is-invalid {% endif %}" type="text" name="code" value="{{ "" if form.code.data is none else form.code.data }}" placeholder=" " required="required" data-exists-url="{{ url_for("accounting.currency-api.exists") }}" data-original="{% block original_code %}{% endblock %}" data-blocklist="{{ form.CODE_BLOCKLIST|tojson|forceescape }}">
<label class="form-label" for="accounting-code">{{ A_("Code") }}</label>
<div id="accounting-code-error" class="invalid-feedback">{% if form.code.errors %}{{ form.code.errors[0] }}{% endif %}</div>
</div>
<div class="form-floating mb-3">
<input id="currency-name" class="form-control {% if form.name.errors %} is-invalid {% endif %}" type="text" name="name" value="{{ "" if form.name.data is none else form.name.data }}" placeholder=" " required="required">
<label class="form-label" for="currency-name">{{ A_("Name") }}</label>
<div id="currency-name-error" class="invalid-feedback">{% if form.name.errors %}{{ form.name.errors[0] }}{% endif %}</div>
<input id="accounting-name" class="form-control {% if form.name.errors %} is-invalid {% endif %}" type="text" name="name" value="{{ "" if form.name.data is none else form.name.data }}" placeholder=" " required="required">
<label class="form-label" for="accounting-name">{{ A_("Name") }}</label>
<div id="accounting-name-error" class="invalid-feedback">{% if form.name.errors %}{{ form.name.errors[0] }}{% endif %}</div>
</div>
<div class="d-none d-md-block">
@ -58,7 +58,7 @@ First written: 2023/2/6
</button>
</div>
<div class="d-md-none material-fab">
<div class="d-md-none accounting-material-fab">
<button class="btn btn-primary" type="submit">
<i class="fa-solid fa-floppy-disk"></i>
</button>

View File

@ -26,15 +26,15 @@ First written: 2023/2/6
{% block content %}
<div class="btn-group mb-2">
{% if can_edit_accounting() %}
<a class="btn btn-primary text-nowrap d-none d-md-block" href="{{ url_for("accounting.currency.create")|append_next }}">
{% if accounting_can_edit() %}
<a class="btn btn-primary text-nowrap d-none d-md-block" href="{{ url_for("accounting.currency.create")|accounting_append_next }}">
<i class="fa-solid fa-plus"></i>
{{ A_("New") }}
</a>
{% endif %}
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.currency.list") }}" method="get" role="search">
<input id="search-input" class="form-control form-control-sm search-input" type="search" name="q" value="{{ request.args["q"] if "q" in request.args else "" }}" placeholder=" " required="required" aria-label="Search">
<label for="search-input" class="search-label">
<input id="accounting-search" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args["q"] if "q" in request.args else "" }}" placeholder=" " required="required" aria-label="Search">
<label for="accounting-search" class="accounting-search-label">
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
{{ A_("Search") }}
@ -43,12 +43,20 @@ First written: 2023/2/6
</form>
</div>
{% if accounting_can_edit() %}
<div class="d-md-none accounting-material-fab">
<a class="btn btn-primary" href="{{ url_for("accounting.currency.create")|accounting_append_next }}">
<i class="fa-solid fa-plus"></i>
</a>
</div>
{% endif %}
{% if list %}
{% include "accounting/include/pagination.html" %}
<div class="list-group">
{% for item in list %}
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.currency.detail", currency=item)|append_next }}">
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.currency.detail", currency=item)|accounting_append_next }}">
{{ item }}
</a>
{% endfor %}

View File

@ -19,7 +19,7 @@ nav.html: The navigation menu for the accounting application.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/1/26
#}
{% if can_view_accounting() %}
{% if accounting_can_view() %}
<li class="nav-item dropdown">
<span class="nav-link dropdown-toggle" data-bs-toggle="dropdown">
<i class="fa-solid fa-gear"></i>
@ -40,7 +40,7 @@ First written: 2023/1/26
</li>
<li>
<a class="dropdown-item {% if request.endpoint.startswith("accounting.currency.") %} active {% endif %}" href="{{ url_for("accounting.currency.list") }}">
<i class="fa-solid fa-list"></i>
<i class="fa-solid fa-money-bill-wave"></i>
{{ A_("Currencies") }}
</a>
</li>

View File

@ -8,8 +8,8 @@ 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-02-06 09:47+0800\n"
"PO-Revision-Date: 2023-02-06 09:48+0800\n"
"POT-Creation-Date: 2023-02-07 16:22+0800\n"
"PO-Revision-Date: 2023-02-07 18:04+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"
@ -23,12 +23,16 @@ msgstr ""
msgid "The base account does not exist."
msgstr "沒有這個基本科目。"
#: src/accounting/account/forms.py:50
#: src/accounting/account/forms.py:52
msgid "The base account is not available."
msgstr "不能選這個基本科目。"
#: src/accounting/account/forms.py:61
#: src/accounting/static/js/account-form.js:110
msgid "Please select the base account."
msgstr "請選擇基本科目。"
#: src/accounting/account/forms.py:55
#: src/accounting/account/forms.py:67
msgid "Please fill in the title"
msgstr "請填上標題。"
@ -50,18 +54,59 @@ msgstr "科目未異動。"
msgid "The account is updated successfully."
msgstr "科目存好了。"
#: src/accounting/account/views.py:167
#: src/accounting/account/views.py:165
msgid "The account is deleted successfully."
msgstr "科目刪掉了"
#: src/accounting/account/views.py:194
#: src/accounting/account/views.py:192
msgid "The order was not modified."
msgstr "順序未異動。"
#: src/accounting/account/views.py:197
#: src/accounting/account/views.py:195
msgid "The order is updated successfully."
msgstr "順序存好了。"
#: src/accounting/currency/forms.py:47
#: src/accounting/static/js/currency-form.js:136
msgid "Code conflicts with another currency."
msgstr "代碼與其它貨幣重複。"
#: src/accounting/currency/forms.py:52
#: src/accounting/static/js/currency-form.js:92
msgid "Please fill in the code."
msgstr "請填上代碼。"
#: src/accounting/currency/forms.py:54
#: src/accounting/static/js/currency-form.js:103
msgid "Code can only be composed of 3 upper-cased letters."
msgstr "代碼限為三個大寫英文字母。"
#: src/accounting/currency/forms.py:57
#: src/accounting/static/js/currency-form.js:98
msgid "This code is not available."
msgstr "不能用這個代碼。"
#: src/accounting/currency/forms.py:63
#: src/accounting/static/js/currency-form.js:168
msgid "Please fill in the name."
msgstr "請填上名稱。"
#: src/accounting/currency/views.py:90
msgid "The currency is added successfully"
msgstr "貨幣加好了。"
#: src/accounting/currency/views.py:146
msgid "The currency was not modified."
msgstr "貨幣未異動。"
#: src/accounting/currency/views.py:151
msgid "The currency is updated successfully."
msgstr "貨幣存好了。"
#: src/accounting/currency/views.py:167
msgid "The currency is deleted successfully."
msgstr "貨幣刪掉了"
#: src/accounting/static/js/account-form.js:130
msgid "Please fill in the title."
msgstr "請填上標題。"
@ -74,10 +119,13 @@ msgstr "新增科目"
#: src/accounting/templates/accounting/account/include/form.html:33
#: src/accounting/templates/accounting/account/order.html:36
#: src/accounting/templates/accounting/base-account/detail.html:31
#: src/accounting/templates/accounting/currency/detail.html:31
#: src/accounting/templates/accounting/currency/include/form.html:33
msgid "Back"
msgstr "回上頁"
#: src/accounting/templates/accounting/account/detail.html:36
#: src/accounting/templates/accounting/currency/detail.html:36
msgid "Settings"
msgstr "設定"
@ -86,6 +134,7 @@ msgid "Order"
msgstr "次序"
#: src/accounting/templates/accounting/account/detail.html:46
#: src/accounting/templates/accounting/currency/detail.html:42
msgid "Delete"
msgstr "刪除"
@ -99,18 +148,22 @@ msgstr "你確定要刪掉這個科目嗎?"
#: src/accounting/templates/accounting/account/detail.html:76
#: src/accounting/templates/accounting/account/include/form.html:111
#: src/accounting/templates/accounting/currency/detail.html:72
msgid "Cancel"
msgstr "取消"
#: src/accounting/templates/accounting/account/detail.html:77
#: src/accounting/templates/accounting/currency/detail.html:73
msgid "Confirm"
msgstr "確定"
#: src/accounting/templates/accounting/account/detail.html:94
#: src/accounting/templates/accounting/currency/detail.html:85
msgid "Created"
msgstr "建檔"
#: src/accounting/templates/accounting/account/detail.html:95
#: src/accounting/templates/accounting/currency/detail.html:86
msgid "Updated"
msgstr "更新"
@ -121,6 +174,7 @@ msgstr "%(account)s設定"
#: src/accounting/templates/accounting/account/list.html:24
#: src/accounting/templates/accounting/base-account/list.html:24
#: src/accounting/templates/accounting/currency/list.html:24
#, python-format
msgid "Search Result for \"%(query)s\""
msgstr "「%(query)s」搜尋結果"
@ -130,18 +184,21 @@ msgid "Account Management"
msgstr "科目管理"
#: src/accounting/templates/accounting/account/list.html:32
#: src/accounting/templates/accounting/currency/list.html:32
msgid "New"
msgstr "新增"
#: src/accounting/templates/accounting/account/include/form.html:98
#: src/accounting/templates/accounting/account/list.html:40
#: src/accounting/templates/accounting/base-account/list.html:34
#: src/accounting/templates/accounting/currency/list.html:40
msgid "Search"
msgstr "搜尋"
#: src/accounting/templates/accounting/account/list.html:68
#: src/accounting/templates/accounting/account/order.html:81
#: src/accounting/templates/accounting/base-account/list.html:51
#: src/accounting/templates/accounting/currency/list.html:57
msgid "There is no data."
msgstr "沒有資料。"
@ -152,6 +209,7 @@ msgstr "%(base)s下的科目"
#: src/accounting/templates/accounting/account/include/form.html:75
#: src/accounting/templates/accounting/account/order.html:62
#: src/accounting/templates/accounting/currency/include/form.html:57
msgid "Save"
msgstr "儲存"
@ -184,6 +242,35 @@ msgstr "清除"
msgid "Base Account Managements"
msgstr "基本科目管理"
#: src/accounting/templates/accounting/currency/create.html:24
msgid "Add a New Currency"
msgstr "新增貨幣"
#: src/accounting/templates/accounting/currency/detail.html:65
msgid "Delete Currency Confirmation"
msgstr "貨幣刪除確認"
#: src/accounting/templates/accounting/currency/detail.html:69
msgid "Do you really want to delete this currency?"
msgstr "你確定要刪掉這個貨幣嗎?"
#: src/accounting/templates/accounting/currency/edit.html:24
#, python-format
msgid "%(currency)s Settings"
msgstr "%(currency)s設定"
#: src/accounting/templates/accounting/currency/list.html:24
msgid "Currency Management"
msgstr "貨幣管理"
#: src/accounting/templates/accounting/currency/include/form.html:44
msgid "Code"
msgstr "代碼"
#: src/accounting/templates/accounting/currency/include/form.html:50
msgid "Name"
msgstr "名稱"
#: src/accounting/templates/accounting/include/nav.html:26
msgid "Accounting"
msgstr "記帳"
@ -196,6 +283,10 @@ msgstr "科目"
msgid "Base Accounts"
msgstr "基本科目"
#: src/accounting/templates/accounting/include/nav.html:44
msgid "Currencies"
msgstr "貨幣"
#: src/accounting/utils/pagination.py:206
msgctxt "Pagination|"
msgid "Previous"

View File

@ -22,7 +22,7 @@ This module should not import any other module from the application.
from urllib.parse import urlparse, parse_qsl, ParseResult, urlencode, \
urlunparse
from flask import request
from flask import request, Blueprint
def append_next(uri: str) -> str:
@ -73,3 +73,14 @@ def __set_next(uri: str, next_uri: str) -> str:
parts: list[str] = list(uri_p)
parts[4] = urlencode(params)
return urlunparse(parts)
def init_app(bp: Blueprint) -> None:
"""Initializes the application.
:param bp: The blueprint of the accounting application.
:return: None.
"""
bp.add_app_template_filter(append_next, "accounting_append_next")
bp.add_app_template_filter(inherit_next, "accounting_inherit_next")
bp.add_app_template_filter(or_next, "accounting_or_next")

View File

@ -21,7 +21,9 @@ This module should not import any other module from the application.
"""
import typing as t
from flask import Flask, abort
from flask import abort, Blueprint
from accounting.utils.user import get_current_user
def has_permission(rule: t.Callable[[], bool]) -> t.Callable:
@ -75,17 +77,22 @@ def can_view() -> bool:
def can_edit() -> bool:
"""Returns whether the current user can edit the account data.
The user has to log in.
:return: True if the current user can edit the accounting data, or False
otherwise.
"""
if get_current_user() is None:
return False
return __can_edit_func()
def init_app(app: Flask, can_view_func: t.Callable[[], bool] | None = None,
def init_app(bp: Blueprint,
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 bp: The blueprint 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
@ -97,5 +104,5 @@ def init_app(app: Flask, can_view_func: t.Callable[[], bool] | None = 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
app.jinja_env.globals["can_edit_accounting"] = __can_edit_func
bp.add_app_template_global(can_view, "accounting_can_view")
bp.add_app_template_global(can_edit, "accounting_can_edit")

View File

@ -23,6 +23,7 @@ import typing as t
from abc import ABC, abstractmethod
import sqlalchemy as sa
from flask import g
from flask_sqlalchemy.model import Model
T = t.TypeVar("T", bound=Model)
@ -49,10 +50,11 @@ class AbstractUserUtils(t.Generic[T], ABC):
@property
@abstractmethod
def current_user(self) -> T:
"""Returns the current user.
def current_user(self) -> T | None:
"""Returns the currently logged-in user.
:return: The current user.
:return: The currently logged-in user, or None if the user has not
logged in
"""
@abstractmethod
@ -72,9 +74,9 @@ class AbstractUserUtils(t.Generic[T], ABC):
__user_utils: AbstractUserUtils
"""The user utilities."""
user_cls: t.Type[Model]
user_cls: t.Type[Model] = Model
"""The user class."""
user_pk_column: sa.Column
user_pk_column: sa.Column = sa.Column(sa.Integer)
"""The primary key column of the user class."""
@ -95,7 +97,7 @@ def get_current_user_pk() -> int:
:return: The primary key value of the currently logged-in user.
"""
return __user_utils.get_pk(__user_utils.current_user)
return __user_utils.get_pk(get_current_user())
def has_user(username: str) -> bool:
@ -114,3 +116,14 @@ def get_user_pk(username: str) -> int:
:return: The primary key value of the user by the username.
"""
return __user_utils.get_pk(__user_utils.get_by_username(username))
def get_current_user() -> user_cls | None:
"""Returns the currently logged-in user. The result is cached in the
current request.
:return: The currently logged-in user.
"""
if not hasattr(g, "_accounting_user"):
setattr(g, "_accounting_user", __user_utils.current_user)
return getattr(g, "_accounting_user")

View File

@ -80,7 +80,7 @@ def create_app(is_testing: bool = False) -> Flask:
return auth.User.id
@property
def current_user(self) -> auth.User:
def current_user(self) -> auth.User | None:
return auth.current_user()
def get_by_username(self, username: str) -> auth.User | None: