Added support to sort the accounts under the same base account.

This commit is contained in:
依瑪貓 2023-02-03 09:21:07 +08:00
parent eeb05b8616
commit 5238168b2d
7 changed files with 341 additions and 21 deletions

View File

@ -18,6 +18,7 @@
""" """
import sqlalchemy as sa import sqlalchemy as sa
from flask import request
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField from wtforms import StringField, BooleanField
from wtforms.validators import DataRequired, ValidationError from wtforms.validators import DataRequired, ValidationError
@ -128,3 +129,48 @@ def sort_accounts_in(base_code: str, exclude: int) -> None:
for i in range(len(accounts)): for i in range(len(accounts)):
if accounts[i].no != i + 1: if accounts[i].no != i + 1:
accounts[i].no = i + 1 accounts[i].no = i + 1
class AccountSortForm:
"""The form to sort the accounts."""
def __init__(self, base: BaseAccount):
"""Constructs the form to sort the accounts under a base account.
:param base: The base account.
"""
self.base: BaseAccount = base
self.is_modified: bool = False
def save_order(self) -> None:
"""Saves the order of the account.
:return:
"""
accounts: list[Account] = self.base.accounts
# Collects the specified order.
orders: dict[Account, int] = {}
for account in accounts:
if f"{account.id}-no" in request.form:
try:
orders[account] = int(request.form[f"{account.id}-no"])
except ValueError:
pass
# Missing and invalid orders are appended to the end.
missing: list[Account] = [x for x in accounts if x not in orders]
if len(missing) > 0:
next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
for account in missing:
orders[account] = next_no
# Sort by the specified order first, and their original order.
accounts = sorted(accounts, key=lambda x: (orders[x], x.no, x.code))
# Update the orders.
with db.session.no_autoflush:
for i in range(len(accounts)):
if accounts[i].no != i + 1:
accounts[i].no = i + 1
self.is_modified = True

View File

@ -29,7 +29,7 @@ from accounting.models import Account, BaseAccount
from accounting.utils.next_url import inherit_next, or_next from accounting.utils.next_url import inherit_next, or_next
from accounting.utils.pagination import Pagination from accounting.utils.pagination import Pagination
from accounting.utils.permission import can_view, has_permission, can_edit from accounting.utils.permission import can_view, has_permission, can_edit
from .forms import AccountForm, sort_accounts_in from .forms import AccountForm, sort_accounts_in, AccountSortForm
bp: Blueprint = Blueprint("account", __name__) bp: Blueprint = Blueprint("account", __name__)
"""The view blueprint for the account management.""" """The view blueprint for the account management."""
@ -162,3 +162,33 @@ def delete_account(account: Account) -> redirect:
db.session.commit() db.session.commit()
flash(lazy_gettext("The account is deleted successfully."), "success") flash(lazy_gettext("The account is deleted successfully."), "success")
return redirect(or_next(url_for("accounting.account.list"))) return redirect(or_next(url_for("accounting.account.list")))
@bp.get("/sort/<baseAccount:base>", endpoint="sort-form")
@has_permission(can_edit)
def show_sort_form(base: BaseAccount) -> str:
"""Shows the form to sort the accounts under a base account.
:param base: The base account.
:return: The form to sort the accounts under the base account.
"""
return render_template("accounting/account/sort.html",
base=base)
@bp.post("/sort/<baseAccount:base>", endpoint="sort")
@has_permission(can_edit)
def sort_accounts(base: BaseAccount) -> redirect:
"""Sorts the accounts under a base account.
:param base: The base account.
:return: Sorts the accounts under the base account.
"""
form: AccountSortForm = AccountSortForm(base)
form.save_order()
if not form.is_modified:
flash(lazy_gettext("The order was not modified."), "success")
return redirect(or_next(url_for("accounting.account.list")))
db.session.commit()
flash(lazy_gettext("The order is updated successfully."), "success")
return redirect(or_next(url_for("accounting.account.list")))

View File

@ -0,0 +1,37 @@
/* The Mia! Accounting Flask Project
* account-sort.js: The JavaScript for the account sorting form
*/
/* 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/2/2
*/
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", function () {
const list = document.getElementById("sort-account-list");
const onReorder = function () {
const accounts = Array.from(list.children);
for (let i = 0; i < accounts.length; i++) {
const input = document.getElementById("sort-" + accounts[i].dataset.id + "-no");
const code = document.getElementById("sort-" + accounts[i].dataset.id + "-code");
input.value = i + 1;
code.innerText = list.dataset.baseCode + "-" + ("000" + (i + 1)).slice(-3);
}
};
initializeDragAndDropSorting(list, onReorder);
});

View File

@ -0,0 +1,108 @@
/* The Mia! Accounting Flask Project
* drag-and-drop-sorting.js: The JavaScript for the sorting with drag-and-drop
*/
/* 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/2/3
*/
/**
* Initializes the drag-and-drop sorting on a list.
*
* @param list {HTMLElement} the list to be sorted
* @param onReorder {(function())|*} The callback to reorder the items
*/
function initializeDragAndDropSorting(list, onReorder) {
initializeMouseDragAndDropSorting(list, onReorder);
initializeTouchDragAndDropSorting(list, onReorder);
}
/**
* Initializes the drag-and-drop sorting with mouse.
*
* @param list {HTMLElement} the list to be sorted
* @param onReorder {(function())|*} The callback to reorder the items
* @private
*/
function initializeMouseDragAndDropSorting(list, onReorder) {
const items = Array.from(list.children);
let dragged = null;
items.forEach(function (item) {
item.draggable = true;
item.addEventListener("dragstart", function () {
dragged = item;
dragged.classList.add("list-group-item-dark");
});
item.addEventListener("dragover", function () {
onDragOver(dragged, item);
onReorder();
});
item.addEventListener("dragend", function () {
dragged.classList.remove("list-group-item-dark");
dragged = null;
});
});
}
/**
* Initializes the drag-and-drop sorting with touch devices.
*
* @param list {HTMLElement} the list to be sorted
* @param onReorder {(function())|*} The callback to reorder the items
* @private
*/
function initializeTouchDragAndDropSorting(list, onReorder) {
const items = Array.from(list.children);
items.forEach(function (item) {
item.addEventListener("touchstart", function () {
item.classList.add("list-group-item-dark");
});
item.addEventListener("touchmove", function (event) {
const touch = event.targetTouches[0];
const target = document.elementFromPoint(touch.pageX, touch.pageY);
onDragOver(item, target);
onReorder();
});
item.addEventListener("touchend", function () {
item.classList.remove("list-group-item-dark");
});
});
}
/**
* Handles when an item is dragged over the other item.
*
* @param dragged {Element} the item that was dragged
* @param target {Element} the other item that was dragged over
*/
function onDragOver(dragged, target) {
if (target.parentElement !== dragged.parentElement || target === dragged) {
return;
}
let isBefore = false;
for (let p = target; p !== null; p = p.previousSibling) {
if (p === dragged) {
isBefore = true;
}
}
if (isBefore) {
target.parentElement.insertBefore(dragged, target.nextSibling);
} else {
target.parentElement.insertBefore(dragged, target);
}
}

View File

@ -35,6 +35,10 @@ First written: 2023/1/31
<i class="fa-solid fa-gear"></i> <i class="fa-solid fa-gear"></i>
{{ A_("Settings") }} {{ A_("Settings") }}
</a> </a>
<a class="btn btn-primary" href="{{ url_for("accounting.account.sort", base=obj.base)|append_next }}">
<i class="fa-solid fa-sort"></i>
{{ A_("Sort") }}
</a>
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#delete-modal"> <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#delete-modal">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-trash"></i>
{{ A_("Delete") }} {{ A_("Delete") }}

View File

@ -0,0 +1,75 @@
{#
The Mia! Accounting Flask Project
sort.html: The account sorting form
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/2/2
#}
{% extends "accounting/account/include/form.html" %}
{% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-sorting.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/account-sort.js") }}"></script>
{% endblock %}
{% block header %}{% block title %}{{ A_("Sort the Accounts of %(base)s", base=base) }}{% endblock %}{% endblock %}
{% block content %}
<div class="btn-group mb-3">
<a class="btn btn-primary" href="{{ url_for("accounting.account.list")|or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }}
</a>
</div>
<form action="{{ url_for("accounting.account.sort", base=base) }}" method="post">
<input id="csrf_token" 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="sort-account-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="sort-{{ account.id }}-no" type="hidden" name="{{ account.id }}-no" value="{{ loop.index }}">
<div>
<span id="sort-{{ account.id }}-code">{{ account.code }}</span>
{{ account.title }}
</div>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 15h18v-2H3v2zm0 4h18v-2H3v2zm0-8h18V9H3v2zm0-6v2h18V5H3z"/>
</svg>
</li>
{% endfor %}
</ul>
<div class="d-none d-md-block">
<button class="btn btn-primary" type="submit">
<i class="fa-solid fa-floppy-disk"></i>
{{ A_("Save") }}
</button>
</div>
<div class="d-md-none material-fab">
<button class="btn btn-primary" type="submit">
<i class="fa-solid fa-floppy-disk"></i>
</button>
</div>
</form>
{% endblock %}

View File

@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Mia! Accounting Flask 0.0.0\n" "Project-Id-Version: Mia! Accounting Flask 0.0.0\n"
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n" "Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
"POT-Creation-Date: 2023-02-01 19:51+0800\n" "POT-Creation-Date: 2023-02-03 07:40+0800\n"
"PO-Revision-Date: 2023-02-01 19:52+0800\n" "PO-Revision-Date: 2023-02-03 07:42+0800\n"
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n" "Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
"Language: zh_Hant\n" "Language: zh_Hant\n"
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n" "Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
@ -19,19 +19,25 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.11.0\n" "Generated-By: Babel 2.11.0\n"
#: src/accounting/account/forms.py:39 #: src/accounting/account/forms.py:41
msgid "The base account does not exist." msgid "The base account does not exist."
msgstr "沒有這個基本科目。" msgstr "沒有這個基本科目。"
#: src/accounting/account/forms.py:48 #: src/accounting/account/forms.py:50
#: src/accounting/static/js/account-form.js:110 #: src/accounting/static/js/account-form.js:110
msgid "Please select the base account." msgid "Please select the base account."
msgstr "請選擇基本科目。" msgstr "請選擇基本科目。"
#: src/accounting/account/forms.py:53 #: src/accounting/account/forms.py:55
msgid "Please fill in the title" msgid "Please fill in the title"
msgstr "請填上標題。" msgstr "請填上標題。"
#: src/accounting/account/query.py:50
#: src/accounting/templates/accounting/account/detail.html:88
#: src/accounting/templates/accounting/account/list.html:62
msgid "Offset needed"
msgstr "逐筆核銷"
#: src/accounting/account/views.py:88 #: src/accounting/account/views.py:88
msgid "The account is added successfully" msgid "The account is added successfully"
msgstr "科目加好了。" msgstr "科目加好了。"
@ -48,6 +54,14 @@ msgstr "科目存好了。"
msgid "The account is deleted successfully." msgid "The account is deleted successfully."
msgstr "科目刪掉了" msgstr "科目刪掉了"
#: src/accounting/account/views.py:190
msgid "The order was not modified."
msgstr "順序未異動。"
#: src/accounting/account/views.py:193
msgid "The order is updated successfully."
msgstr "順序存好了。"
#: src/accounting/static/js/account-form.js:130 #: src/accounting/static/js/account-form.js:130
msgid "Please fill in the title." msgid "Please fill in the title."
msgstr "請填上標題。" msgstr "請填上標題。"
@ -58,6 +72,7 @@ msgstr "新增科目"
#: src/accounting/templates/accounting/account/detail.html:31 #: src/accounting/templates/accounting/account/detail.html:31
#: src/accounting/templates/accounting/account/include/form.html:33 #: src/accounting/templates/accounting/account/include/form.html:33
#: src/accounting/templates/accounting/account/sort.html:35
#: src/accounting/templates/accounting/base-account/detail.html:31 #: src/accounting/templates/accounting/base-account/detail.html:31
msgid "Back" msgid "Back"
msgstr "回上頁" msgstr "回上頁"
@ -67,36 +82,35 @@ msgid "Settings"
msgstr "設定" msgstr "設定"
#: src/accounting/templates/accounting/account/detail.html:40 #: src/accounting/templates/accounting/account/detail.html:40
msgid "Sort"
msgstr "排序"
#: src/accounting/templates/accounting/account/detail.html:44
msgid "Delete" msgid "Delete"
msgstr "刪除" msgstr "刪除"
#: src/accounting/templates/accounting/account/detail.html:63 #: src/accounting/templates/accounting/account/detail.html:67
msgid "Delete Account Confirmation" msgid "Delete Account Confirmation"
msgstr "科目刪除確認" msgstr "科目刪除確認"
#: src/accounting/templates/accounting/account/detail.html:67 #: src/accounting/templates/accounting/account/detail.html:71
msgid "Do you really want to delete this account?" msgid "Do you really want to delete this account?"
msgstr "你確定要刪掉這個科目嗎?" msgstr "你確定要刪掉這個科目嗎?"
#: src/accounting/templates/accounting/account/detail.html:70 #: src/accounting/templates/accounting/account/detail.html:74
#: src/accounting/templates/accounting/account/include/form.html:111 #: src/accounting/templates/accounting/account/include/form.html:111
msgid "Cancel" msgid "Cancel"
msgstr "取消" msgstr "取消"
#: src/accounting/templates/accounting/account/detail.html:71 #: src/accounting/templates/accounting/account/detail.html:75
msgid "Confirm" msgid "Confirm"
msgstr "確定" msgstr "確定"
#: src/accounting/templates/accounting/account/detail.html:84 #: src/accounting/templates/accounting/account/detail.html:92
#: src/accounting/templates/accounting/account/list.html:62
msgid "Offset needed"
msgstr "逐筆核銷"
#: src/accounting/templates/accounting/account/detail.html:88
msgid "Created" msgid "Created"
msgstr "建檔" msgstr "建檔"
#: src/accounting/templates/accounting/account/detail.html:89 #: src/accounting/templates/accounting/account/detail.html:93
msgid "Updated" msgid "Updated"
msgstr "更新" msgstr "更新"
@ -124,6 +138,16 @@ msgstr "搜尋"
msgid "There is no data." msgid "There is no data."
msgstr "沒有資料。" msgstr "沒有資料。"
#: src/accounting/templates/accounting/account/sort.html:28
#, python-format
msgid "Sort the Accounts of %(base)s"
msgstr "%(base)s下的科目排序"
#: src/accounting/templates/accounting/account/include/form.html:75
#: src/accounting/templates/accounting/account/sort.html:67
msgid "Save"
msgstr "儲存"
#: src/accounting/templates/accounting/account/include/form.html:45 #: src/accounting/templates/accounting/account/include/form.html:45
msgid "Base account" msgid "Base account"
msgstr "基本科目" msgstr "基本科目"
@ -140,10 +164,6 @@ msgstr "標題"
msgid "The entries in the account need offsets." msgid "The entries in the account need offsets."
msgstr "帳目要逐筆核銷。" msgstr "帳目要逐筆核銷。"
#: src/accounting/templates/accounting/account/include/form.html:75
msgid "Save"
msgstr "儲存"
#: src/accounting/templates/accounting/account/include/form.html:90 #: src/accounting/templates/accounting/account/include/form.html:90
msgid "Select Base Account" msgid "Select Base Account"
msgstr "選擇基本科目" msgstr "選擇基本科目"