Compare commits

...

29 Commits

Author SHA1 Message Date
c50b9a2000 Removed the unused tab and page classes from the templates of the summary editor. 2023-03-04 11:52:45 +08:00
af9bd14eed Removed the unused tab ID from the template of the summary editor. 2023-03-04 11:52:45 +08:00
9e1ff16e96 Fixed the aria-labelledby in the template of the summary editor. 2023-03-04 11:52:45 +08:00
f7c1fd77f2 Added blank lines and documentation to the template of the summary editor. 2023-03-04 11:52:45 +08:00
641315537d Replaced the traditional function expressions with ES6 arrow function expressions in the JavaScript for the transaction order, account order, currency form, and the speed dial for the material floating action buttons. 2023-03-04 11:52:45 +08:00
a895bd8560 Replaced the traditional function expressions with ES6 arrow function expressions in the JavaScript for the drop-and-drop reorder. 2023-03-04 11:52:44 +08:00
ca86a08f3e Replaced the traditional function expressions with ES6 arrow function expressions in the JavaScript for the account form. 2023-03-04 11:52:44 +08:00
e118422441 Replaced the traditional function expressions with ES6 arrow function expressions in the JavaScript for the account selector. 2023-03-04 11:52:44 +08:00
b3777cffbf Replaced the traditional function expressions with ES6 arrow function expressions in the JavaScript for the transaction form. 2023-03-04 11:52:44 +08:00
39c9c17007 Replaced the traditional function expressions with ES6 arrow function expressions in the JavaScript for the summary editor to avoid messing up with the "this" object. 2023-03-04 11:52:44 +08:00
3ab4eacf9f Moved the "accounting.transaction.template_globals" module to "accounting.template_globals", for the two template globals will be used in the reports beside the transaction management. 2023-03-04 07:06:03 +08:00
cff3d1b6bd Revised the code order in the init_app function in the "accounting" module. 2023-03-04 07:01:03 +08:00
f41db78831 Revised the summary helper so that when the summary is changed with the tag changed, the on-change callback is run to check the tag button status. 2023-03-04 07:01:03 +08:00
73f7d14e7b Fixed so that the values of the input fields are trimmed before composing the summary when they are changed. 2023-03-04 07:01:03 +08:00
f6ed6b10a7 Revised the summary editor to allow the "*" start character as the multiplication operation in addition to the "×" times character. 2023-03-04 07:01:03 +08:00
b5aaee4d15 Renamed the "number" tab plane to "annotation". 2023-03-04 07:01:03 +08:00
c849d6b3d4 Revised the numer tab plane in the summary editor. 2023-03-04 07:01:03 +08:00
a9908a7df4 Simplified the regular expression in the populate method of the GeneralTagTab class in the summary editor. 2023-03-04 07:01:02 +08:00
063c769158 Renamed the variables in the summary editor. 2023-03-04 07:01:02 +08:00
f8e9871300 Fixed to trim the summary when it is changed in the summary editor. 2023-03-04 07:01:02 +08:00
78a62a9575 Added a "note" field to the summary editor. 2023-03-04 07:01:02 +08:00
85fde6219e Fixed an HTML ID in the summary editor modal. 2023-03-04 07:01:02 +08:00
4eb9346d8d Renamed summary helper to summary editor. 2023-03-04 07:00:46 +08:00
11966a52ba Fixed a variable name in the #initializeAccountQuery method of the JavaScript AccountSelector class. 2023-03-04 06:57:35 +08:00
8cf81b5459 Revised the documentation of the "accounting.account.queries", "accounting.base_account.queries", "accounting.currency.queries", and "accounting.transaction.queries" modules. 2023-03-04 06:57:35 +08:00
cc958a39b3 Moved the format_amount and format_date template filters from the "accounting.transaction.template_filters" module to the "accounting.template_filters" module, and rename the filters from "accounting_txn_format_amount" and "accounting_txn_format_date" to "accounting_format_amount" and "accounting_format_date", respectively. They will not only be used in the transaction management, but also the reports. 2023-03-04 06:57:10 +08:00
9065686cc5 Split the "accounting.transaction.template" module into the "accounting.transaction.template_filters" and "accounting.transaction.template_globals" modules. 2023-03-03 18:28:59 +08:00
9a41cb10a1 Rewrote the summary helper, added the TabPlane classes so that the internal states of the summary helper is stored in the tab plane objects instead of passing the as parameters and variables. 2023-03-03 18:09:36 +08:00
6957e52d0d Renamed the "accounting.account.query", "accounting.base_account.query", "accounting.currency.query", and "accounting.transaction.query" modules to "accounting.account.queries", "accounting.base_account.queries", "accounting.currency.queries", and "accounting.transaction.queries", respectively. There will be more than one query in the next report module. 2023-03-03 01:12:36 +08:00
40 changed files with 1673 additions and 1246 deletions

View File

@ -58,12 +58,25 @@ def init_app(app: Flask, user_utils: AbstractUserUtils,
template_folder="templates",
static_folder="static")
from .template_filters import format_amount, format_date
bp.add_app_template_filter(format_amount, "accounting_format_amount")
bp.add_app_template_filter(format_date, "accounting_format_date")
from .template_globals import currency_options, default_currency_code
bp.add_app_template_global(currency_options,
"accounting_currency_options")
bp.add_app_template_global(default_currency_code,
"accounting_default_currency_code")
from . import locale
locale.init_app(app, bp)
from .utils import permission
permission.init_app(bp, can_view_func, can_edit_func)
from .utils import next_uri
next_uri.init_app(bp)
from . import base_account
base_account.init_app(app, bp)
@ -76,7 +89,4 @@ def init_app(app: Flask, user_utils: AbstractUserUtils,
from . import transaction
transaction.init_app(app, bp)
from .utils import next_uri
next_uri.init_app(bp)
app.register_blueprint(bp)

View File

@ -14,7 +14,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The account query.
"""The queries for the account management.
"""
import sqlalchemy as sa

View File

@ -33,7 +33,7 @@ from accounting.utils.pagination import Pagination
from accounting.utils.permission import can_view, has_permission, can_edit
from accounting.utils.user import get_current_user_pk
from .forms import AccountForm, sort_accounts_in, AccountReorderForm
from .query import get_account_query
from .queries import get_account_query
bp: Blueprint = Blueprint("account", __name__)
"""The view blueprint for the account management."""

View File

@ -14,7 +14,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The base account query.
"""The queries for the base account management.
"""
import sqlalchemy as sa

View File

@ -34,7 +34,7 @@ def list_accounts() -> str:
:return: The account list.
"""
from .query import get_base_account_query
from .queries import get_base_account_query
accounts: list[BaseAccount] = get_base_account_query()
pagination: Pagination = Pagination[BaseAccount](accounts)
return render_template("accounting/base-account/list.html",

View File

@ -14,7 +14,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The currency query.
"""The queries for the currency management.
"""
import sqlalchemy as sa

View File

@ -47,7 +47,7 @@ def list_currencies() -> str:
:return: The currency list.
"""
from .query import get_currency_query
from .queries import get_currency_query
currencies: list[Currency] = get_currency_query()
pagination: Pagination = Pagination[Currency](currencies)
return render_template("accounting/currency/list.html",

View File

@ -22,7 +22,7 @@
*/
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", function () {
document.addEventListener("DOMContentLoaded", () => {
initializeBaseAccountSelector();
document.getElementById("accounting-base-code")
.onchange = validateBase;
@ -44,7 +44,7 @@ function initializeBaseAccountSelector() {
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 () {
selector.addEventListener("show.bs.modal", () => {
base.classList.add("accounting-not-empty");
for (const option of options) {
option.classList.remove("active");
@ -54,13 +54,13 @@ function initializeBaseAccountSelector() {
selected.classList.add("active");
}
});
selector.addEventListener("hidden.bs.modal", function () {
selector.addEventListener("hidden.bs.modal", () => {
if (baseCode.value === "") {
base.classList.remove("accounting-not-empty");
}
});
for (const option of options) {
option.onclick = function () {
option.onclick = () => {
baseCode.value = option.dataset.code;
baseContent.innerText = option.dataset.content;
btnClear.classList.add("btn-danger");
@ -70,7 +70,7 @@ function initializeBaseAccountSelector() {
bootstrap.Modal.getInstance(selector).hide();
};
}
btnClear.onclick = function () {
btnClear.onclick = () => {
baseCode.value = "";
baseContent.innerText = "";
btnClear.classList.add("btn-secondary")
@ -92,7 +92,7 @@ function initializeBaseAccountQuery() {
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 () {
query.addEventListener("input", () => {
if (query.value === "") {
for (const option of options) {
option.classList.remove("d-none");

View File

@ -22,10 +22,10 @@
*/
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", function () {
document.addEventListener("DOMContentLoaded", () => {
const list = document.getElementById("accounting-order-list");
if (list !== null) {
const onReorder = function () {
const onReorder = () => {
const accounts = Array.from(list.children);
for (let i = 0; i < accounts.length; i++) {
const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no");

View File

@ -22,7 +22,7 @@
*/
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", function () {
document.addEventListener("DOMContentLoaded", () => {
AccountSelector.initialize();
});
@ -65,13 +65,12 @@ class AccountSelector {
const more = document.getElementById(this.#prefix + "-more");
const btnClear = document.getElementById(this.#prefix + "-btn-clear");
const options = Array.from(document.getElementsByClassName(this.#prefix + "-option"));
const selector1 = this
more.onclick = function () {
more.onclick = () => {
more.classList.add("d-none");
selector1.#filterAccountOptions();
this.#filterAccountOptions();
};
this.#initializeAccountQuery();
btnClear.onclick = function () {
btnClear.onclick = () => {
formAccountControl.classList.remove("accounting-not-empty");
formAccount.innerText = "";
formAccount.dataset.code = "";
@ -79,7 +78,7 @@ class AccountSelector {
validateJournalEntryAccount();
};
for (const option of options) {
option.onclick = function () {
option.onclick = () => {
formAccountControl.classList.add("accounting-not-empty");
formAccount.innerText = option.dataset.content;
formAccount.dataset.code = option.dataset.code;
@ -95,9 +94,8 @@ class AccountSelector {
*/
#initializeAccountQuery() {
const query = document.getElementById(this.#prefix + "-query");
const helper = this;
query.addEventListener("input", function () {
helper.#filterAccountOptions();
query.addEventListener("input", () => {
this.#filterAccountOptions();
});
}
@ -159,7 +157,7 @@ class AccountSelector {
* @return {boolean} true if the account option should show, or false otherwise
*/
#shouldAccountOptionShow(option, more, inUse, query) {
const isQueryMatched = function () {
const isQueryMatched = () => {
if (query.value === "") {
return true;
}
@ -171,7 +169,7 @@ class AccountSelector {
}
return false;
};
const isMoreMatched = function () {
const isMoreMatched = () => {
if (more.classList.contains("d-none")) {
return true;
}
@ -237,10 +235,7 @@ class AccountSelector {
static #initializeTransactionForm() {
const entryForm = document.getElementById("accounting-entry-form");
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
const selectors = this.#selectors;
formAccountControl.onclick = function () {
selectors[entryForm.dataset.entryType].initShow();
};
formAccountControl.onclick = () => this.#selectors[entryForm.dataset.entryType].initShow();
}
/**
* Initializes the account selector for the journal entry form.

View File

@ -22,7 +22,7 @@
*/
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", function () {
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("accounting-code")
.onchange = validateCode;
document.getElementById("accounting-name")

View File

@ -44,15 +44,15 @@ function initializeMouseDragAndDropReordering(list, onReorder) {
let dragged = null;
for (const item of items) {
item.draggable = true;
item.addEventListener("dragstart", function () {
item.addEventListener("dragstart", () => {
dragged = item;
dragged.classList.add("accounting-dragged");
});
item.addEventListener("dragover", function () {
item.addEventListener("dragover", () => {
onDragOver(dragged, item);
onReorder();
});
item.addEventListener("dragend", function () {
item.addEventListener("dragend", () => {
dragged.classList.remove("accounting-dragged");
dragged = null;
});
@ -69,16 +69,16 @@ function initializeMouseDragAndDropReordering(list, onReorder) {
function initializeTouchDragAndDropReordering(list, onReorder) {
const items = Array.from(list.children);
for (const item of items) {
item.addEventListener("touchstart", function () {
item.addEventListener("touchstart", () => {
item.classList.add("accounting-dragged");
});
item.addEventListener("touchmove", function (event) {
item.addEventListener("touchmove", (event) => {
const touch = event.targetTouches[0];
const target = document.elementFromPoint(touch.pageX, touch.pageY);
onDragOver(item, target);
onReorder();
});
item.addEventListener("touchend", function () {
item.addEventListener("touchend", () => {
item.classList.remove("accounting-dragged");
});
}

View File

@ -22,7 +22,7 @@
*/
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", function () {
document.addEventListener("DOMContentLoaded", () => {
initializeMaterialFabSpeedDial();
});
@ -34,7 +34,7 @@ document.addEventListener("DOMContentLoaded", function () {
function initializeMaterialFabSpeedDial() {
const btnFab = document.getElementById("accounting-btn-material-fab-speed-dial");
const fab = document.getElementById(btnFab.dataset.target);
btnFab.onclick = function () {
btnFab.onclick = () => {
if (fab.classList.contains("show")) {
fab.classList.remove("show");
} else {

File diff suppressed because it is too large Load Diff

View File

@ -1,827 +0,0 @@
/* The Mia! Accounting Flask Project
* summary-helper.js: The JavaScript for the summary helper
*/
/* 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/28
*/
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", function () {
SummaryHelper.initialize();
});
/**
* A summary helper.
*
*/
class SummaryHelper {
/**
* The entry type, either "debit" or "credit"
* @type {string}
*/
#entryType;
/**
* The prefix of the HTML ID and class
* @type {string}
*/
#prefix;
/**
* The default tab ID
* @type {string}
*/
#defaultTabId;
/**
* Constructs a summary helper.
*
* @param form {HTMLFormElement} the summary helper form
*/
constructor(form) {
this.#entryType = form.dataset.entryType;
this.#prefix = "accounting-summary-helper-" + form.dataset.entryType;
this.#defaultTabId = form.dataset.defaultTabId;
this.#init();
}
/**
* Initializes the summary helper.
*
*/
#init() {
const helper = this;
const summary = document.getElementById(this.#prefix + "-summary");
const tabs = Array.from(document.getElementsByClassName(this.#prefix + "-tab"));
for (const tab of tabs) {
tab.onclick = function () {
helper.#switchToTab(tab.dataset.tabId);
}
}
this.#initializeGeneralTagHelper();
this.#initializeGeneralTripHelper();
this.#initializeBusTripHelper();
this.#initializeNumberHelper();
this.#initializeSuggestedAccounts();
this.#initializeSubmission();
summary.onchange = function () {
summary.value = summary.value.trim();
helper.#parseAndPopulate();
};
}
/**
* Switches to a tab.
*
* @param tabId {string} the tab ID.
*/
#switchToTab(tabId) {
const tabs = Array.from(document.getElementsByClassName(this.#prefix + "-tab"));
const pages = Array.from(document.getElementsByClassName(this.#prefix + "-page"));
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-" + tabId + "-btn-tag"));
for (const tab of tabs) {
if (tab.dataset.tabId === tabId) {
tab.classList.add("active");
tab.ariaCurrent = "page";
} else {
tab.classList.remove("active");
tab.ariaCurrent = "false";
}
}
for (const page of pages) {
if (page.dataset.tabId === tabId) {
page.classList.remove("d-none");
page.ariaCurrent = "page";
} else {
page.classList.add("d-none");
page.ariaCurrent = "false";
}
}
let selectedBtnTag = null;
for (const tagButton of tagButtons) {
if (tagButton.classList.contains("btn-primary")) {
selectedBtnTag = tagButton;
break;
}
}
this.#filterSuggestedAccounts(selectedBtnTag);
}
/**
* Initializes the general tag helper.
*
*/
#initializeGeneralTagHelper() {
const summary = document.getElementById(this.#prefix + "-summary");
const tag = document.getElementById(this.#prefix + "-general-tag");
const helper = this;
const updateSummary = function () {
const pos = summary.value.indexOf("—");
const prefix = tag.value === ""? "": tag.value + "—";
if (pos === -1) {
summary.value = prefix + summary.value;
} else {
summary.value = prefix + summary.value.substring(pos + 1);
}
}
this.#initializeTagButtons("general", tag, updateSummary);
tag.onchange = function () {
helper.#onTagInputChange("general", tag, updateSummary);
};
}
/**
* Initializes the general trip helper.
*
*/
#initializeGeneralTripHelper() {
const summary = document.getElementById(this.#prefix + "-summary");
const tag = document.getElementById(this.#prefix + "-travel-tag");
const from = document.getElementById(this.#prefix + "-travel-from");
const directionButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-direction"))
const to = document.getElementById(this.#prefix + "-travel-to");
const helper = this;
const updateSummary = function () {
let direction;
for (const directionButton of directionButtons) {
if (directionButton.classList.contains("btn-primary")) {
direction = directionButton.dataset.arrow;
break;
}
}
summary.value = tag.value + "—" + from.value + direction + to.value;
};
this.#initializeTagButtons("travel", tag, updateSummary);
tag.onchange = function () {
helper.#onTagInputChange("travel", tag, updateSummary);
helper.#validateGeneralTripTag();
};
from.onchange = function () {
updateSummary();
helper.#validateGeneralTripFrom();
};
for (const directionButton of directionButtons) {
directionButton.onclick = function () {
for (const otherButton of directionButtons) {
otherButton.classList.remove("btn-primary");
otherButton.classList.add("btn-outline-primary");
}
directionButton.classList.remove("btn-outline-primary");
directionButton.classList.add("btn-primary");
updateSummary();
};
}
to.onchange = function () {
updateSummary();
helper.#validateGeneralTripTo();
};
}
/**
* Initializes the bus trip helper.
*
*/
#initializeBusTripHelper() {
const summary = document.getElementById(this.#prefix + "-summary");
const tag = document.getElementById(this.#prefix + "-bus-tag");
const route = document.getElementById(this.#prefix + "-bus-route");
const from = document.getElementById(this.#prefix + "-bus-from");
const to = document.getElementById(this.#prefix + "-bus-to");
const helper = this;
const updateSummary = function () {
summary.value = tag.value + "—" + route.value + "—" + from.value + "→" + to.value;
};
this.#initializeTagButtons("bus", tag, updateSummary);
tag.onchange = function () {
helper.#onTagInputChange("bus", tag, updateSummary);
helper.#validateBusTripTag();
};
route.onchange = function () {
updateSummary();
helper.#validateBusTripRoute();
};
from.onchange = function () {
updateSummary();
helper.#validateBusTripFrom();
};
to.onchange = function () {
updateSummary();
helper.#validateBusTripTo();
};
}
/**
* Initializes the tag buttons.
*
* @param tabId {string} the tab ID
* @param tag {HTMLInputElement} the tag input
* @param updateSummary {function(): void} the callback to update the summary
*/
#initializeTagButtons(tabId, tag, updateSummary) {
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-" + tabId + "-btn-tag"));
const helper = this;
for (const tagButton of tagButtons) {
tagButton.onclick = function () {
for (const otherButton of tagButtons) {
otherButton.classList.remove("btn-primary");
otherButton.classList.add("btn-outline-primary");
}
tagButton.classList.remove("btn-outline-primary");
tagButton.classList.add("btn-primary");
tag.value = tagButton.dataset.value;
helper.#filterSuggestedAccounts(tagButton);
updateSummary();
};
}
}
/**
* The callback when the tag input is changed.
*
* @param tabId {string} the tab ID
* @param tag {HTMLInputElement} the tag input
* @param updateSummary {function(): void} the callback to update the summary
*/
#onTagInputChange(tabId, tag, updateSummary) {
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-" + tabId + "-btn-tag"));
let isMatched = false;
for (const tagButton of tagButtons) {
if (tagButton.dataset.value === tag.value) {
tagButton.classList.remove("btn-outline-primary");
tagButton.classList.add("btn-primary");
this.#filterSuggestedAccounts(tagButton);
isMatched = true;
} else {
tagButton.classList.remove("btn-primary");
tagButton.classList.add("btn-outline-primary");
}
}
if (!isMatched) {
this.#filterSuggestedAccounts(null);
}
updateSummary();
}
/**
* Filters the suggested accounts.
*
* @param tagButton {HTMLButtonElement|null} the tag button
*/
#filterSuggestedAccounts(tagButton) {
const accountButtons = Array.from(document.getElementsByClassName(this.#prefix + "-account"));
if (tagButton === null) {
for (const accountButton of accountButtons) {
accountButton.classList.add("d-none");
accountButton.classList.remove("btn-primary");
accountButton.classList.add("btn-outline-primary");
this.#selectAccount(null);
}
return;
}
const suggested = JSON.parse(tagButton.dataset.accounts);
for (const accountButton of accountButtons) {
if (suggested.includes(accountButton.dataset.code)) {
accountButton.classList.remove("d-none");
} else {
accountButton.classList.add("d-none");
}
this.#selectAccount(suggested[0]);
}
}
/**
* Initializes the number helper.
*
*/
#initializeNumberHelper() {
const summary = document.getElementById(this.#prefix + "-summary");
const number = document.getElementById(this.#prefix + "-number");
number.onchange = function () {
const found = summary.value.match(/^(.+)×(\d+)$/);
if (found !== null) {
summary.value = found[1];
}
if (number.value > 1) {
summary.value = summary.value + "×" + String(number.value);
}
};
}
/**
* Initializes the suggested accounts.
*
*/
#initializeSuggestedAccounts() {
const accountButtons = Array.from(document.getElementsByClassName(this.#prefix + "-account"));
const helper = this;
for (const accountButton of accountButtons) {
accountButton.onclick = function () {
helper.#selectAccount(accountButton.dataset.code);
};
}
}
/**
* Select a suggested account.
*
* @param selectedCode {string|null} the account code, or null to deselect the account
*/
#selectAccount(selectedCode) {
const form = document.getElementById(this.#prefix);
if (selectedCode === null) {
form.dataset.selectedAccountCode = "";
form.dataset.selectedAccountText = "";
return;
}
const accountButtons = Array.from(document.getElementsByClassName(this.#prefix + "-account"));
for (const accountButton of accountButtons) {
if (accountButton.dataset.code === selectedCode) {
accountButton.classList.remove("btn-outline-primary");
accountButton.classList.add("btn-primary");
form.dataset.selectedAccountCode = accountButton.dataset.code;
form.dataset.selectedAccountText = accountButton.dataset.text;
} else {
accountButton.classList.remove("btn-primary");
accountButton.classList.add("btn-outline-primary");
}
}
}
/**
* Initializes the summary submission
*
*/
#initializeSubmission() {
const form = document.getElementById(this.#prefix);
const helper = this;
form.onsubmit = function () {
if (helper.#validate()) {
helper.#submit();
}
return false;
};
}
/**
* Validates the form.
*
* @return {boolean} true if valid, or false otherwise
*/
#validate() {
const tabs = Array.from(document.getElementsByClassName(this.#prefix + "-tab"));
let isValid = true;
for (const tab of tabs) {
if (tab.classList.contains("active")) {
switch (tab.dataset.tabId) {
case "general":
isValid = this.#validateGeneralTag() && isValid;
break;
case "travel":
isValid = this.#validateGeneralTrip() && isValid;
break;
case "bus":
isValid = this.#validateBusTrip() && isValid;
break;
}
}
}
return isValid;
}
/**
* Validates a general tag.
*
* @return {boolean} true if valid, or false otherwise
*/
#validateGeneralTag() {
const field = document.getElementById(this.#prefix + "-general-tag");
const error = document.getElementById(this.#prefix + "-general-tag-error");
field.value = field.value.trim();
field.classList.remove("is-invalid");
error.innerText = "";
return true;
}
/**
* Validates a general trip.
*
* @return {boolean} true if valid, or false otherwise
*/
#validateGeneralTrip() {
let isValid = true;
isValid = this.#validateGeneralTripTag() && isValid;
isValid = this.#validateGeneralTripFrom() && isValid;
isValid = this.#validateGeneralTripTo() && isValid;
return isValid;
}
/**
* Validates the tag of a general trip.
*
* @return {boolean} true if valid, or false otherwise
*/
#validateGeneralTripTag() {
const field = document.getElementById(this.#prefix + "-travel-tag");
const error = document.getElementById(this.#prefix + "-travel-tag-error");
field.value = field.value.trim();
if (field.value === "") {
field.classList.add("is-invalid");
error.innerText = A_("Please fill in the tag.");
return false;
}
field.classList.remove("is-invalid");
error.innerText = "";
return true;
}
/**
* Validates the origin of a general trip.
*
* @return {boolean} true if valid, or false otherwise
*/
#validateGeneralTripFrom() {
const field = document.getElementById(this.#prefix + "-travel-from");
const error = document.getElementById(this.#prefix + "-travel-from-error");
field.value = field.value.trim();
if (field.value === "") {
field.classList.add("is-invalid");
error.innerText = A_("Please fill in the origin.");
return false;
}
field.classList.remove("is-invalid");
error.innerText = "";
return true;
}
/**
* Validates the destination of a general trip.
*
* @return {boolean} true if valid, or false otherwise
*/
#validateGeneralTripTo() {
const field = document.getElementById(this.#prefix + "-travel-to");
const error = document.getElementById(this.#prefix + "-travel-to-error");
field.value = field.value.trim();
if (field.value === "") {
field.classList.add("is-invalid");
error.innerText = A_("Please fill in the destination.");
return false;
}
field.classList.remove("is-invalid");
error.innerText = "";
return true;
}
/**
* Validates a bus trip.
*
* @return {boolean} true if valid, or false otherwise
*/
#validateBusTrip() {
let isValid = true;
isValid = this.#validateBusTripTag() && isValid;
isValid = this.#validateBusTripRoute() && isValid;
isValid = this.#validateBusTripFrom() && isValid;
isValid = this.#validateBusTripTo() && isValid;
return isValid;
}
/**
* Validates the tag of a bus trip.
*
* @return {boolean} true if valid, or false otherwise
*/
#validateBusTripTag() {
const field = document.getElementById(this.#prefix + "-bus-tag");
const error = document.getElementById(this.#prefix + "-bus-tag-error");
field.value = field.value.trim();
if (field.value === "") {
field.classList.add("is-invalid");
error.innerText = A_("Please fill in the tag.");
return false;
}
field.classList.remove("is-invalid");
error.innerText = "";
return true;
}
/**
* Validates the route of a bus trip.
*
* @return {boolean} true if valid, or false otherwise
*/
#validateBusTripRoute() {
const field = document.getElementById(this.#prefix + "-bus-route");
const error = document.getElementById(this.#prefix + "-bus-route-error");
field.value = field.value.trim();
if (field.value === "") {
field.classList.add("is-invalid");
error.innerText = A_("Please fill in the route.");
return false;
}
field.classList.remove("is-invalid");
error.innerText = "";
return true;
}
/**
* Validates the origin of a bus trip.
*
* @return {boolean} true if valid, or false otherwise
*/
#validateBusTripFrom() {
const field = document.getElementById(this.#prefix + "-bus-from");
const error = document.getElementById(this.#prefix + "-bus-from-error");
field.value = field.value.trim();
if (field.value === "") {
field.classList.add("is-invalid");
error.innerText = A_("Please fill in the origin.");
return false;
}
field.classList.remove("is-invalid");
error.innerText = "";
return true;
}
/**
* Validates the destination of a bus trip.
*
* @return {boolean} true if valid, or false otherwise
*/
#validateBusTripTo() {
const field = document.getElementById(this.#prefix + "-bus-to");
const error = document.getElementById(this.#prefix + "-bus-to-error");
field.value = field.value.trim();
if (field.value === "") {
field.classList.add("is-invalid");
error.innerText = A_("Please fill in the destination.");
return false;
}
field.classList.remove("is-invalid");
error.innerText = "";
return true;
}
/**
* Submits the summary.
*
*/
#submit() {
const form = document.getElementById(this.#prefix);
const summary = document.getElementById(this.#prefix + "-summary");
const formSummaryControl = document.getElementById("accounting-entry-form-summary-control");
const formSummary = document.getElementById("accounting-entry-form-summary");
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
const formAccount = document.getElementById("accounting-entry-form-account");
const helperModal = document.getElementById(this.#prefix + "-modal");
const entryModal = document.getElementById("accounting-entry-form-modal");
if (summary.value === "") {
formSummaryControl.classList.remove("accounting-not-empty");
} else {
formSummaryControl.classList.add("accounting-not-empty");
}
if (form.dataset.selectedAccountCode !== "") {
formAccountControl.classList.add("accounting-not-empty");
formAccount.dataset.code = form.dataset.selectedAccountCode;
formAccount.dataset.text = form.dataset.selectedAccountText;
formAccount.innerText = form.dataset.selectedAccountText;
}
formSummary.dataset.value = summary.value;
formSummary.innerText = summary.value;
bootstrap.Modal.getInstance(helperModal).hide();
bootstrap.Modal.getOrCreateInstance(entryModal).show();
}
/**
* Initializes the summary helper when it is shown.
*
* @param isNew {boolean} true for adding a new journal entry, or false otherwise
*/
initShow(isNew) {
const formSummary = document.getElementById("accounting-entry-form-summary");
const summary = document.getElementById(this.#prefix + "-summary");
const closeButtons = Array.from(document.getElementsByClassName(this.#prefix + "-close"));
for (const closeButton of closeButtons) {
if (isNew) {
closeButton.dataset.bsTarget = "#" + this.#prefix + "-modal";
} else {
closeButton.dataset.bsTarget = "#accounting-entry-form-modal";
}
}
this.#reset();
if (!isNew) {
summary.value = formSummary.dataset.value;
this.#parseAndPopulate();
}
}
/**
* Resets the summary helper.
*
*/
#reset() {
const inputs = Array.from(document.getElementsByClassName(this.#prefix + "-input"));
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-btn-tag"));
const directionButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-direction"));
for (const input of inputs) {
input.value = "";
input.classList.remove("is-invalid");
}
for (const tagButton of tagButtons) {
tagButton.classList.remove("btn-primary");
tagButton.classList.add("btn-outline-primary");
}
for (const directionButton of directionButtons) {
if (directionButton.classList.contains("accounting-default")) {
directionButton.classList.remove("btn-outline-primary");
directionButton.classList.add("btn-primary");
} else {
directionButton.classList.add("btn-outline-primary");
directionButton.classList.remove("btn-primary");
}
}
this.#filterSuggestedAccounts(null);
this.#switchToTab(this.#defaultTabId);
}
/**
* Parses the summary input and populates the summary helper.
*
*/
#parseAndPopulate() {
const summary = document.getElementById(this.#prefix + "-summary");
const pos = summary.value.indexOf("—");
if (pos === -1) {
return;
}
let found;
found = summary.value.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:×(\d+))?$/);
if (found !== null) {
return this.#populateBusTrip(found[1], found[2], found[3], found[4], found[5]);
}
found = summary.value.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:×(\d+))?$/);
if (found !== null) {
return this.#populateGeneralTrip(found[1], found[2], found[3], found[4], found[5]);
}
found = summary.value.match(/^([^—]+)—.+?(?:×(\d+)?)?$/);
if (found !== null) {
return this.#populateGeneralTag(found[1], found[2]);
}
}
/**
* Populates a bus trip.
*
* @param tagName {string} the tag name
* @param routeName {string} the route name or route number
* @param fromName {string} the name of the origin
* @param toName {string} the name of the destination
* @param numberStr {string|undefined} the number of items, if any
*/
#populateBusTrip(tagName, routeName, fromName, toName, numberStr) {
const tag = document.getElementById(this.#prefix + "-bus-tag");
const route = document.getElementById(this.#prefix + "-bus-route");
const from = document.getElementById(this.#prefix + "-bus-from");
const to = document.getElementById(this.#prefix + "-bus-to");
const number = document.getElementById(this.#prefix + "-number");
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-bus-btn-tag"));
tag.value = tagName;
route.value = routeName;
from.value = fromName;
to.value = toName;
if (numberStr !== undefined) {
number.value = parseInt(numberStr);
}
for (const tagButton of tagButtons) {
if (tagButton.dataset.value === tagName) {
tagButton.classList.remove("btn-outline-primary");
tagButton.classList.add("btn-primary");
this.#filterSuggestedAccounts(tagButton);
}
}
this.#switchToTab("bus");
}
/**
* Populates a general trip.
*
* @param tagName {string} the tag name
* @param fromName {string} the name of the origin
* @param direction {string} the direction arrow, either "→" or "↔"
* @param toName {string} the name of the destination
* @param numberStr {string|undefined} the number of items, if any
*/
#populateGeneralTrip(tagName, fromName, direction, toName, numberStr) {
const tag = document.getElementById(this.#prefix + "-travel-tag");
const from = document.getElementById(this.#prefix + "-travel-from");
const to = document.getElementById(this.#prefix + "-travel-to");
const number = document.getElementById(this.#prefix + "-number");
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-btn-tag"));
const directionButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-direction"));
tag.value = tagName;
from.value = fromName;
for (const directionButton of directionButtons) {
if (directionButton.dataset.arrow === direction) {
directionButton.classList.remove("btn-outline-primary");
directionButton.classList.add("btn-primary");
} else {
directionButton.classList.add("btn-outline-primary");
directionButton.classList.remove("btn-primary");
}
}
to.value = toName;
if (numberStr !== undefined) {
number.value = parseInt(numberStr);
}
for (const tagButton of tagButtons) {
if (tagButton.dataset.value === tagName) {
tagButton.classList.remove("btn-outline-primary");
tagButton.classList.add("btn-primary");
this.#filterSuggestedAccounts(tagButton);
}
}
this.#switchToTab("travel");
}
/**
* Populates a general tag.
*
* @param tagName {string} the tag name
* @param numberStr {string|undefined} the number of items, if any
*/
#populateGeneralTag(tagName, numberStr) {
const tag = document.getElementById(this.#prefix + "-general-tag");
const number = document.getElementById(this.#prefix + "-number");
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-general-btn-tag"));
tag.value = tagName;
if (numberStr !== undefined) {
number.value = parseInt(numberStr);
}
for (const tagButton of tagButtons) {
if (tagButton.dataset.value === tagName) {
tagButton.classList.remove("btn-outline-primary");
tagButton.classList.add("btn-primary");
this.#filterSuggestedAccounts(tagButton);
}
}
this.#switchToTab("general");
}
/**
* The summary helpers.
* @type {{debit: SummaryHelper, credit: SummaryHelper}}
*/
static #helpers = {}
/**
* Initializes the summary helpers.
*
*/
static initialize() {
const forms = Array.from(document.getElementsByClassName("accounting-summary-helper"));
for (const form of forms) {
const helper = new SummaryHelper(form);
this.#helpers[helper.#entryType] = helper;
}
this.#initializeTransactionForm();
}
/**
* Initializes the transaction form.
*
*/
static #initializeTransactionForm() {
const entryForm = document.getElementById("accounting-entry-form");
const formSummaryControl = document.getElementById("accounting-entry-form-summary-control");
const helpers = this.#helpers;
formSummaryControl.onclick = function () {
helpers[entryForm.dataset.entryType].initShow(false);
};
}
/**
* Initializes the summary helper for a new journal entry.
*
* @param entryType {string} the entry type, either "debit" or "credit"
*/
static initializeNewJournalEntry(entryType) {
this.#helpers[entryType].initShow(true);
}
}

View File

@ -22,7 +22,7 @@
*/
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", function () {
document.addEventListener("DOMContentLoaded", () => {
initializeCurrencyForms();
initializeJournalEntries();
initializeFormValidation();
@ -68,14 +68,14 @@ function initializeCurrencyForms() {
const btnNew = document.getElementById("accounting-btn-new-currency");
const currencyList = document.getElementById("accounting-currency-list");
const deleteButtons = Array.from(document.getElementsByClassName("accounting-btn-delete-currency"));
const onReorder = function () {
const onReorder = () => {
const currencies = Array.from(currencyList.children);
for (let i = 0; i < currencies.length; i++) {
const no = document.getElementById(currencies[i].dataset.prefix + "-no");
no.value = String(i + 1);
}
};
btnNew.onclick = function () {
btnNew.onclick = () => {
const currencies = Array.from(document.getElementsByClassName("accounting-currency"));
let maxIndex = 0;
for (const currency of currencies) {
@ -107,7 +107,7 @@ function initializeCurrencyForms() {
*/
function initializeBtnDeleteCurrency(button) {
const target = document.getElementById(button.dataset.target);
button.onclick = function () {
button.onclick = () => {
target.parentElement.removeChild(target);
resetDeleteCurrencyButtons();
};
@ -161,7 +161,7 @@ function initializeNewEntryButton(button) {
const formSummaryError = document.getElementById("accounting-entry-form-summary-error");
const formAmount = document.getElementById("accounting-entry-form-amount");
const formAmountError = document.getElementById("accounting-entry-form-amount-error");
button.onclick = function () {
button.onclick = () => {
entryForm.dataset.currencyIndex = button.dataset.currencyIndex;
entryForm.dataset.entryType = button.dataset.entryType;
entryForm.dataset.entryIndex = button.dataset.entryIndex;
@ -171,7 +171,7 @@ function initializeNewEntryButton(button) {
formAccount.dataset.code = "";
formAccount.dataset.text = "";
formAccountError.innerText = "";
formSummaryControl.dataset.bsTarget = "#accounting-summary-helper-" + button.dataset.entryType + "-modal";
formSummaryControl.dataset.bsTarget = "#accounting-summary-editor-" + button.dataset.entryType + "-modal";
formSummaryControl.classList.remove("accounting-not-empty");
formSummaryControl.classList.remove("is-invalid");
formSummary.dataset.value = "";
@ -181,7 +181,7 @@ function initializeNewEntryButton(button) {
formAmount.classList.remove("is-invalid");
formAmountError.innerText = "";
AccountSelector.initializeJournalEntryForm();
SummaryHelper.initializeNewJournalEntry(button.dataset.entryType);
SummaryEditor.initializeNewJournalEntry(button.dataset.entryType);
};
}
@ -191,7 +191,7 @@ function initializeNewEntryButton(button) {
* @param entryList {HTMLUListElement} the journal entry list.
*/
function initializeJournalEntryListReorder(entryList) {
initializeDragAndDropReordering(entryList, function () {
initializeDragAndDropReordering(entryList, () => {
const entries = Array.from(entryList.children);
for (let i = 0; i < entries.length; i++) {
const no = document.getElementById(entries[i].dataset.prefix + "-no");
@ -216,7 +216,7 @@ function initializeJournalEntry(entry) {
const formSummaryControl = document.getElementById("accounting-entry-form-summary-control");
const formSummary = document.getElementById("accounting-entry-form-summary");
const formAmount = document.getElementById("accounting-entry-form-amount");
control.onclick = function () {
control.onclick = () => {
entryForm.dataset.currencyIndex = entry.dataset.currencyIndex;
entryForm.dataset.entryType = entry.dataset.entryType;
entryForm.dataset.entryIndex = entry.dataset.entryIndex;
@ -228,7 +228,7 @@ function initializeJournalEntry(entry) {
formAccount.innerText = accountCode.dataset.text;
formAccount.dataset.code = accountCode.value;
formAccount.dataset.text = accountCode.dataset.text;
formSummaryControl.dataset.bsTarget = "#accounting-summary-helper-" + entry.dataset.entryType + "-modal";
formSummaryControl.dataset.bsTarget = "#accounting-summary-editor-" + entry.dataset.entryType + "-modal";
if (summary.value === "") {
formSummaryControl.classList.remove("accounting-not-empty");
} else {
@ -252,7 +252,7 @@ function initializeJournalEntryFormModal() {
const formAmount = document.getElementById("accounting-entry-form-amount");
const modal = document.getElementById("accounting-entry-form-modal");
formAmount.onchange = validateJournalEntryAmount;
entryForm.onsubmit = function () {
entryForm.onsubmit = () => {
if (validateJournalEntryForm()) {
saveJournalEntryForm();
bootstrap.Modal.getInstance(modal).hide();
@ -398,7 +398,7 @@ function initializeDeleteJournalEntryButton(button) {
const currencyIndex = target.dataset.currencyIndex;
const entryType = target.dataset.entryType;
const currency = document.getElementById("accounting-currency-" + currencyIndex);
button.onclick = function () {
button.onclick = () => {
target.parentElement.removeChild(target);
resetDeleteJournalEntryButtons(button.dataset.sameClass);
updateBalance(currencyIndex, entryType);

View File

@ -22,10 +22,10 @@
*/
// Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", function () {
document.addEventListener("DOMContentLoaded", () => {
const list = document.getElementById("accounting-order-list");
if (list !== null) {
const onReorder = function () {
const onReorder = () => {
const accounts = Array.from(list.children);
for (let i = 0; i < accounts.length; i++) {
const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no");

View File

@ -0,0 +1,68 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The template filters.
"""
from decimal import Decimal
from datetime import date, timedelta
from flask_babel import get_locale
from accounting.locale import gettext
def format_amount(value: Decimal | None) -> str:
"""Formats an amount for readability.
:param value: The amount.
:return: The formatted amount text.
"""
if value is None or value == 0:
return "-"
whole: int = int(value)
frac: Decimal = (value - whole).normalize()
return "{:,}".format(whole) + str(frac)[1:]
def format_date(value: date) -> str:
"""Formats a date to be human-friendly.
:param value: The date.
:return: The human-friendly date text.
"""
today: date = date.today()
if value == today:
return gettext("Today")
if value == today - timedelta(days=1):
return gettext("Yesterday")
if value == today + timedelta(days=1):
return gettext("Tomorrow")
locale = str(get_locale())
if locale == "zh" or locale.startswith("zh_"):
if value == today - timedelta(days=2):
return gettext("The day before yesterday")
if value == today + timedelta(days=2):
return gettext("The day after tomorrow")
if locale == "zh" or locale.startswith("zh_"):
weekdays = ["", "", "", "", "", "", ""]
weekday = weekdays[value.weekday()]
else:
weekday = value.strftime("%a")
if value.year != today.year:
return "{}/{}/{}({})".format(
value.year, value.month, value.day, weekday)
return "{}/{}({})".format(value.month, value.day, weekday)

View File

@ -0,0 +1,39 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The template globals for the transaction management.
"""
from flask import current_app
from accounting.models import Currency
def currency_options() -> str:
"""Returns the currency options.
:return: The currency options.
"""
return Currency.query.order_by(Currency.code).all()
def default_currency_code() -> str:
"""Returns the default currency code.
:return: The default currency code.
"""
with current_app.app_context():
return current_app.config.get("DEFAULT_CURRENCY", "USD")

View File

@ -44,14 +44,14 @@ First written: 2023/2/26
<div>{{ entry.summary }}</div>
{% endif %}
</div>
<div>{{ entry.amount|accounting_txn_format_amount }}</div>
<div>{{ entry.amount|accounting_format_amount }}</div>
</div>
</li>
{% endfor %}
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
<div class="d-flex justify-content-between">
<div>{{ A_("Total") }}</div>
<div>{{ currency.debit_total|accounting_txn_format_amount }}</div>
<div>{{ currency.debit_total|accounting_format_amount }}</div>
</div>
</li>
</ul>

View File

@ -25,7 +25,7 @@ First written: 2023/2/25
<div class="d-flex justify-content-between mt-2 mb-3">
<div class="form-floating accounting-currency-content">
<select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code">
{% for currency in accounting_txn_currency_options() %}
{% for currency in accounting_currency_options() %}
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
{% endfor %}
</select>
@ -57,7 +57,7 @@ First written: 2023/2/25
summary_errors = entry_form.summary.errors,
amount_data = "" if entry_form.amount.data is none else entry_form.amount.data|accounting_txn_format_amount_input,
amount_errors = entry_form.amount.errors,
amount_text = entry_form.amount.data|accounting_txn_format_amount,
amount_text = entry_form.amount.data|accounting_format_amount,
entry_errors = entry_form.all_errors %}
{% include "accounting/transaction/include/form-entry-item.html" %}
{% endwith %}

View File

@ -31,14 +31,14 @@ First written: 2023/2/25
currency_code_errors = currency_form.code.errors,
debit_forms = currency_form.debit,
debit_errors = currency_form.debit_errors,
debit_total = currency_form.form.debit_total|accounting_txn_format_amount %}
debit_total = currency_form.form.debit_total|accounting_format_amount %}
{% include "accounting/transaction/expense/include/form-currency-item.html" %}
{% endwith %}
{% endfor %}
{% else %}
{% with currency_index = 1,
only_one_currency_form = True,
currency_code_data = accounting_txn_default_currency_code(),
currency_code_data = accounting_default_currency_code(),
debit_total = "-" %}
{% include "accounting/transaction/expense/include/form-currency-item.html" %}
{% endwith %}
@ -46,8 +46,8 @@ First written: 2023/2/25
{% endblock %}
{% block form_modals %}
{% with summary_helper = form.summary_helper.debit %}
{% include "accounting/transaction/include/summary-helper-modal.html" %}
{% with summary_editor = form.summary_editor.debit %}
{% include "accounting/transaction/include/summary-editor-modal.html" %}
{% endwith %}
{% with entry_type = "debit",
account_options = form.debit_account_options %}

View File

@ -89,7 +89,7 @@ First written: 2023/2/26
</div>
<div class="mb-3">
{{ obj.date|accounting_txn_format_date }}
{{ obj.date|accounting_format_date }}
</div>
{% block transaction_currencies %}{% endblock %}

View File

@ -25,7 +25,7 @@ First written: 2023/2/26
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/transaction-form.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/account-selector.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/summary-helper.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/summary-editor.js") }}"></script>
{% endblock %}
{% block content %}

View File

@ -0,0 +1,190 @@
{#
The Mia! Accounting Flask Project
summary-editor-modal.html: The modal of the summary editor
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/28
#}
<form id="accounting-summary-editor-{{ summary_editor.type }}" class="accounting-summary-editor" data-entry-type="{{ summary_editor.type }}">
<div id="accounting-summary-editor-{{ summary_editor.type }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-modal-label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="accounting-summary-editor-{{ summary_editor.type }}-modal-label">
<label for="accounting-summary-editor-{{ summary_editor.type }}-summary">{{ A_("Summary") }}</label>
</h1>
<button class="btn-close" type="button" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal" aria-label="{{ A_("Close") }}"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<input id="accounting-summary-editor-{{ summary_editor.type }}-summary" class="form-control" type="text" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-modal-label">
</div>
{# Tab navigation #}
<ul class="nav nav-tabs mb-2">
<li class="nav-item">
<span id="accounting-summary-editor-{{ summary_editor.type }}-general-tab" class="nav-link active accounting-clickable" aria-current="page">
{{ A_("General") }}
</span>
</li>
<li class="nav-item">
<span id="accounting-summary-editor-{{ summary_editor.type }}-travel-tab" class="nav-link accounting-clickable" aria-current="false">
{{ A_("Travel") }}
</span>
</li>
<li class="nav-item">
<span id="accounting-summary-editor-{{ summary_editor.type }}-bus-tab" class="nav-link accounting-clickable" aria-current="false">
{{ A_("Bus") }}
</span>
</li>
<li class="nav-item">
<span id="accounting-summary-editor-{{ summary_editor.type }}-regular-tab" class="nav-link accounting-clickable" aria-current="false">
{{ A_("Regular") }}
</span>
</li>
<li class="nav-item">
<span id="accounting-summary-editor-{{ summary_editor.type }}-annotation-tab" class="nav-link accounting-clickable" aria-current="false">
{{ A_("Annotation") }}
</span>
</li>
</ul>
{# A general summary with a tag #}
<div id="accounting-summary-editor-{{ summary_editor.type }}-general-page" aria-current="page" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-general-tab">
<div class="form-floating mb-2">
<input id="accounting-summary-editor-{{ summary_editor.type }}-general-tag" class="form-control" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-general-tag">{{ A_("Tag") }}</label>
<div id="accounting-summary-editor-{{ summary_editor.type }}-general-tag-error" class="invalid-feedback"></div>
</div>
<div>
{% for tag in summary_editor.general.tags %}
<button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.type }}-general-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
{{ tag }}
</button>
{% endfor %}
</div>
</div>
{# A general trip with the origin and distination #}
<div id="accounting-summary-editor-{{ summary_editor.type }}-travel-page" class="d-none" aria-current="false" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-travel-tab">
<div class="form-floating mb-2">
<input id="accounting-summary-editor-{{ summary_editor.type }}-travel-tag" class="form-control" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-travel-tag">{{ A_("Tag") }}</label>
<div id="accounting-summary-editor-{{ summary_editor.type }}-travel-tag-error" class="invalid-feedback"></div>
</div>
<div>
{% for tag in summary_editor.travel.tags %}
<button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.type }}-travel-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
{{ tag }}
</button>
{% endfor %}
</div>
<div class="d-flex justify-content-between mt-2">
<div class="form-floating">
<input id="accounting-summary-editor-{{ summary_editor.type }}-travel-from" class="form-control" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-travel-from">{{ A_("From") }}</label>
<div id="accounting-summary-editor-{{ summary_editor.type }}-travel-from-error" class="invalid-feedback"></div>
</div>
<div class="btn-group-vertical ms-1 me-1">
<button class="btn btn-primary accounting-summary-editor-{{ summary_editor.type }}-travel-direction accounting-default" type="button" tabindex="-1" data-arrow="&rarr;">&rarr;</button>
<button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.type }}-travel-direction" type="button" tabindex="-1" data-arrow="&harr;">&harr;</button>
</div>
<div class="form-floating">
<input id="accounting-summary-editor-{{ summary_editor.type }}-travel-to" class="form-control" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-travel-to">{{ A_("To") }}</label>
<div id="accounting-summary-editor-{{ summary_editor.type }}-travel-to-error" class="invalid-feedback"></div>
</div>
</div>
</div>
{# A bus trip with the route name or route number, the origin and distination #}
<div id="accounting-summary-editor-{{ summary_editor.type }}-bus-page" class="d-none" aria-current="false" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-bus-tab">
<div class="d-flex justify-content-between mb-2">
<div class="form-floating me-2">
<input id="accounting-summary-editor-{{ summary_editor.type }}-bus-tag" class="form-control" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-bus-tag">{{ A_("Tag") }}</label>
<div id="accounting-summary-editor-{{ summary_editor.type }}-bus-tag-error" class="invalid-feedback"></div>
</div>
<div class="form-floating">
<input id="accounting-summary-editor-{{ summary_editor.type }}-bus-route" class="form-control" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-bus-route">{{ A_("Route") }}</label>
<div id="accounting-summary-editor-{{ summary_editor.type }}-bus-route-error" class="invalid-feedback"></div>
</div>
</div>
<div>
{% for tag in summary_editor.bus.tags %}
<button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.type }}-bus-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
{{ tag }}
</button>
{% endfor %}
</div>
<div class="d-flex justify-content-between mt-2">
<div class="form-floating me-2">
<input id="accounting-summary-editor-{{ summary_editor.type }}-bus-from" class="form-control" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-bus-from">{{ A_("From") }}</label>
<div id="accounting-summary-editor-{{ summary_editor.type }}-bus-from-error" class="invalid-feedback"></div>
</div>
<div class="form-floating">
<input id="accounting-summary-editor-{{ summary_editor.type }}-bus-to" class="form-control" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-bus-to">{{ A_("To") }}</label>
<div id="accounting-summary-editor-{{ summary_editor.type }}-bus-to-error" class="invalid-feedback"></div>
</div>
</div>
</div>
{# A regular income or payment #}
<div id="accounting-summary-editor-{{ summary_editor.type }}-regular-page" class="d-none" aria-current="false" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-regular-tab">
{# TODO: To be done #}
</div>
{# The annotation #}
<div id="accounting-summary-editor-{{ summary_editor.type }}-annotation-page" class="d-none" aria-current="false" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-annotation-tab">
<div class="form-floating">
<input id="accounting-summary-editor-{{ summary_editor.type }}-annotation-number" class="form-control" type="number" min="1" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-annotation-number">{{ A_("The number of items") }}</label>
<div id="accounting-summary-editor-{{ summary_editor.type }}-annotation-number-error" class="invalid-feedback"></div>
</div>
<div class="form-floating mt-2">
<input id="accounting-summary-editor-{{ summary_editor.type }}-annotation-note" class="form-control" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-annotation-note">{{ A_("Note") }}</label>
<div id="accounting-summary-editor-{{ summary_editor.type }}-annotation-note-error" class="invalid-feedback"></div>
</div>
</div>
{# The suggested accounts #}
<div class="mt-3">
{% for account in summary_editor.accounts %}
<button class="btn btn-outline-primary d-none accounting-summary-editor-{{ summary_editor.type }}-account" type="button" data-code="{{ account.code }}" data-text="{{ account }}">
{{ account }}
</button>
{% endfor %}
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">{{ A_("Cancel") }}</button>
<button id="accounting-summary-editor-{{ summary_editor.type }}-btn-save" type="submit" class="btn btn-primary">{{ A_("Save") }}</button>
</div>
</div>
</div>
</div>
</form>

View File

@ -1,181 +0,0 @@
{#
The Mia! Accounting Flask Project
entry-form-modal.html: The modal of the summary helper
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/28
#}
<form id="accounting-summary-helper-{{ summary_helper.type }}" class="accounting-summary-helper" data-entry-type="{{ summary_helper.type }}" data-default-tab-id="general" data-selected-account-code="" data-selected-account-text="">
<div id="accounting-summary-helper-{{ summary_helper.type }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-modal-label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="accounting-summary-helper-{{ summary_helper.type }}-modal-label">
<label for="accounting-summary-helper-{{ summary_helper.type }}-summary">{{ A_("Summary") }}</label>
</h1>
<button class="btn-close accounting-summary-helper-{{ summary_helper.type }}-close" type="button" data-bs-toggle="modal" data-bs-target="" aria-label="{{ A_("Close") }}"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<input id="accounting-summary-helper-{{ summary_helper.type }}-summary" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-modal-label">
</div>
<ul class="nav nav-tabs mb-2">
<li class="nav-item">
<span id="accounting-summary-helper-{{ summary_helper.type }}-tab-general" class="nav-link active accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="page" data-tab-id="general">
{{ A_("General") }}
</span>
</li>
<li class="nav-item">
<span id="accounting-summary-helper-{{ summary_helper.type }}-tab-travel" class="nav-link accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="false" data-tab-id="travel">
{{ A_("Travel") }}
</span>
</li>
<li class="nav-item">
<span id="accounting-summary-helper-{{ summary_helper.type }}-tab-bus" class="nav-link accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="false" data-tab-id="bus">
{{ A_("Bus") }}
</span>
</li>
<li class="nav-item">
<span id="accounting-summary-helper-{{ summary_helper.type }}-tab-regular" class="nav-link accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="false" data-tab-id="regular">
{{ A_("Regular") }}
</span>
</li>
<li class="nav-item">
<span id="accounting-summary-helper-{{ summary_helper.type }}-tab-number" class="nav-link accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="false" data-tab-id="number">
{{ A_("Number") }}
</span>
</li>
</ul>
{# A general summary with a tag #}
<div class="accounting-summary-helper-{{ summary_helper.type }}-page" aria-current="page" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-tab-general" data-tab-id="general">
<div class="form-floating mb-2">
<input id="accounting-summary-helper-{{ summary_helper.type }}-general-tag" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-general-tag">{{ A_("Tag") }}</label>
<div id="accounting-summary-helper-{{ summary_helper.type }}-general-tag-error" class="invalid-feedback"></div>
</div>
<div>
{% for tag in summary_helper.general.tags %}
<button class="btn btn-outline-primary accounting-summary-helper-{{ summary_helper.type }}-btn-tag accounting-summary-helper-{{ summary_helper.type }}-general-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
{{ tag }}
</button>
{% endfor %}
</div>
</div>
{# A general trip with the origin and distination #}
<div class="accounting-summary-helper-{{ summary_helper.type }}-page d-none" aria-current="false" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-tab-travel" data-tab-id="travel">
<div class="form-floating mb-2">
<input id="accounting-summary-helper-{{ summary_helper.type }}-travel-tag" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-travel-tag">{{ A_("Tag") }}</label>
<div id="accounting-summary-helper-{{ summary_helper.type }}-travel-tag-error" class="invalid-feedback"></div>
</div>
<div>
{% for tag in summary_helper.travel.tags %}
<button class="btn btn-outline-primary accounting-summary-helper-{{ summary_helper.type }}-btn-tag accounting-summary-helper-{{ summary_helper.type }}-travel-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
{{ tag }}
</button>
{% endfor %}
</div>
<div class="d-flex justify-content-between mt-2">
<div class="form-floating">
<input id="accounting-summary-helper-{{ summary_helper.type }}-travel-from" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-travel-from">{{ A_("From") }}</label>
<div id="accounting-summary-helper-{{ summary_helper.type }}-travel-from-error" class="invalid-feedback"></div>
</div>
<div class="btn-group-vertical ms-1 me-1">
<button class="btn btn-primary accounting-summary-helper-{{ summary_helper.type }}-travel-direction accounting-default" type="button" tabindex="-1" data-arrow="&rarr;">&rarr;</button>
<button class="btn btn-outline-primary accounting-summary-helper-{{ summary_helper.type }}-travel-direction" type="button" tabindex="-1" data-arrow="&harr;">&harr;</button>
</div>
<div class="form-floating">
<input id="accounting-summary-helper-{{ summary_helper.type }}-travel-to" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-travel-to">{{ A_("To") }}</label>
<div id="accounting-summary-helper-{{ summary_helper.type }}-travel-to-error" class="invalid-feedback"></div>
</div>
</div>
</div>
{# A bus trip with the route name or route number, the origin and distination #}
<div class="accounting-summary-helper-{{ summary_helper.type }}-page d-none" aria-current="false" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-tab-bus" data-tab-id="bus">
<div class="d-flex justify-content-between mb-2">
<div class="form-floating me-2">
<input id="accounting-summary-helper-{{ summary_helper.type }}-bus-tag" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-bus-tag">{{ A_("Tag") }}</label>
<div id="accounting-summary-helper-{{ summary_helper.type }}-bus-tag-error" class="invalid-feedback"></div>
</div>
<div class="form-floating">
<input id="accounting-summary-helper-{{ summary_helper.type }}-bus-route" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-bus-route">{{ A_("Route") }}</label>
<div id="accounting-summary-helper-{{ summary_helper.type }}-bus-route-error" class="invalid-feedback"></div>
</div>
</div>
<div>
{% for tag in summary_helper.bus.tags %}
<button class="btn btn-outline-primary accounting-summary-helper-{{ summary_helper.type }}-btn-tag accounting-summary-helper-{{ summary_helper.type }}-bus-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
{{ tag }}
</button>
{% endfor %}
</div>
<div class="d-flex justify-content-between mt-2">
<div class="form-floating me-2">
<input id="accounting-summary-helper-{{ summary_helper.type }}-bus-from" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-bus-from">{{ A_("From") }}</label>
<div id="accounting-summary-helper-{{ summary_helper.type }}-bus-from-error" class="invalid-feedback"></div>
</div>
<div class="form-floating">
<input id="accounting-summary-helper-{{ summary_helper.type }}-bus-to" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-bus-to">{{ A_("To") }}</label>
<div id="accounting-summary-helper-{{ summary_helper.type }}-bus-to-error" class="invalid-feedback"></div>
</div>
</div>
</div>
{# A regular income/payment #}
<div class="accounting-summary-helper-{{ summary_helper.type }}-page d-none" aria-current="false" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-tab-regular" data-tab-id="regular">
{# TODO: To be done #}
</div>
{# The number of items #}
<div class="accounting-summary-helper-{{ summary_helper.type }}-page d-none" aria-current="false" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-tab-number" data-tab-id="number">
<div class="form-floating">
<input id="accounting-summary-helper-{{ summary_helper.type }}-number" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="number" min="1" value="" placeholder=" ">
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-number">{{ A_("The number of items") }}</label>
<div id="accounting-summary-helper-{{ summary_helper.type }}-number-error" class="invalid-feedback"></div>
</div>
</div>
{# The suggested accounts #}
<div class="mt-3">
{% for account in summary_helper.accounts %}
<button class="btn btn-outline-primary d-none accounting-summary-helper-{{ summary_helper.type }}-account" type="button" data-code="{{ account.code }}" data-text="{{ account }}">
{{ account }}
</button>
{% endfor %}
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary accounting-summary-helper-{{ summary_helper.type }}-close" type="button" data-bs-toggle="modal" data-bs-target="">{{ A_("Cancel") }}</button>
<button id="accounting-summary-helper-{{ summary_helper.type }}-btn-save" type="submit" class="btn btn-primary">{{ A_("Save") }}</button>
</div>
</div>
</div>
</div>
</form>

View File

@ -44,14 +44,14 @@ First written: 2023/2/26
<div>{{ entry.summary }}</div>
{% endif %}
</div>
<div>{{ entry.amount|accounting_txn_format_amount }}</div>
<div>{{ entry.amount|accounting_format_amount }}</div>
</div>
</li>
{% endfor %}
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
<div class="d-flex justify-content-between">
<div>{{ A_("Total") }}</div>
<div>{{ currency.debit_total|accounting_txn_format_amount }}</div>
<div>{{ currency.debit_total|accounting_format_amount }}</div>
</div>
</li>
</ul>

View File

@ -25,7 +25,7 @@ First written: 2023/2/25
<div class="d-flex justify-content-between mt-2 mb-3">
<div class="form-floating accounting-currency-content">
<select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code">
{% for currency in accounting_txn_currency_options() %}
{% for currency in accounting_currency_options() %}
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
{% endfor %}
</select>
@ -57,7 +57,7 @@ First written: 2023/2/25
summary_errors = entry_form.summary.errors,
amount_data = "" if entry_form.amount.data is none else entry_form.amount.data|accounting_txn_format_amount_input,
amount_errors = entry_form.amount.errors,
amount_text = entry_form.amount.data|accounting_txn_format_amount,
amount_text = entry_form.amount.data|accounting_format_amount,
entry_errors = entry_form.all_errors %}
{% include "accounting/transaction/include/form-entry-item.html" %}
{% endwith %}

View File

@ -31,14 +31,14 @@ First written: 2023/2/25
currency_code_errors = currency_form.code.errors,
credit_forms = currency_form.credit,
credit_errors = currency_form.credit_errors,
credit_total = currency_form.form.credit_total|accounting_txn_format_amount %}
credit_total = currency_form.form.credit_total|accounting_format_amount %}
{% include "accounting/transaction/income/include/form-currency-item.html" %}
{% endwith %}
{% endfor %}
{% else %}
{% with currency_index = 1,
only_one_currency_form = True,
currency_code_data = accounting_txn_default_currency_code(),
currency_code_data = accounting_default_currency_code(),
credit_total = "-" %}
{% include "accounting/transaction/income/include/form-currency-item.html" %}
{% endwith %}
@ -46,8 +46,8 @@ First written: 2023/2/25
{% endblock %}
{% block form_modals %}
{% with summary_helper = form.summary_helper.credit %}
{% include "accounting/transaction/include/summary-helper-modal.html" %}
{% with summary_editor = form.summary_editor.credit %}
{% include "accounting/transaction/include/summary-editor-modal.html" %}
{% endwith %}
{% with entry_type = "credit",
account_options = form.credit_account_options %}

View File

@ -85,7 +85,7 @@ First written: 2023/2/18
<div class="list-group">
{% for item in list %}
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.transaction.detail", txn=item)|accounting_append_next }}">
{{ item.date|accounting_txn_format_date }} {{ item }}
{{ item.date|accounting_format_date }} {{ item }}
</a>
{% endfor %}
</div>

View File

@ -40,14 +40,14 @@ First written: 2023/2/26
<div>{{ entry.summary }}</div>
{% endif %}
</div>
<div>{{ entry.amount|accounting_txn_format_amount }}</div>
<div>{{ entry.amount|accounting_format_amount }}</div>
</div>
</li>
{% endfor %}
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
<div class="d-flex justify-content-between">
<div>{{ A_("Total") }}</div>
<div>{{ currency.debit_total|accounting_txn_format_amount }}</div>
<div>{{ currency.debit_total|accounting_format_amount }}</div>
</div>
</li>
</ul>
@ -66,14 +66,14 @@ First written: 2023/2/26
<div>{{ entry.summary }}</div>
{% endif %}
</div>
<div>{{ entry.amount|accounting_txn_format_amount }}</div>
<div>{{ entry.amount|accounting_format_amount }}</div>
</div>
</li>
{% endfor %}
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
<div class="d-flex justify-content-between">
<div>{{ A_("Total") }}</div>
<div>{{ currency.debit_total|accounting_txn_format_amount }}</div>
<div>{{ currency.debit_total|accounting_format_amount }}</div>
</div>
</li>
</ul>

View File

@ -25,7 +25,7 @@ First written: 2023/2/25
<div class="d-flex justify-content-between mt-2 mb-3">
<div class="form-floating accounting-currency-content">
<select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code">
{% for currency in accounting_txn_currency_options() %}
{% for currency in accounting_currency_options() %}
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
{% endfor %}
</select>
@ -59,7 +59,7 @@ First written: 2023/2/25
summary_errors = entry_form.summary.errors,
amount_data = "" if entry_form.amount.data is none else entry_form.amount.data|accounting_txn_format_amount_input,
amount_errors = entry_form.amount.errors,
amount_text = entry_form.amount.data|accounting_txn_format_amount,
amount_text = entry_form.amount.data|accounting_format_amount,
entry_errors = entry_form.all_errors %}
{% include "accounting/transaction/include/form-entry-item.html" %}
{% endwith %}
@ -99,7 +99,7 @@ First written: 2023/2/25
summary_errors = entry_form.summary.errors,
amount_data = "" if entry_form.amount.data is none else entry_form.amount.data|accounting_txn_format_amount_input,
amount_errors = entry_form.amount.errors,
amount_text = entry_form.amount.data|accounting_txn_format_amount,
amount_text = entry_form.amount.data|accounting_format_amount,
entry_errors = entry_form.all_errors %}
{% include "accounting/transaction/include/form-entry-item.html" %}
{% endwith %}

View File

@ -31,17 +31,17 @@ First written: 2023/2/25
currency_code_errors = currency_form.code.errors,
debit_forms = currency_form.debit,
debit_errors = currency_form.debit_errors,
debit_total = currency_form.form.debit_total|accounting_txn_format_amount,
debit_total = currency_form.form.debit_total|accounting_format_amount,
credit_forms = currency_form.credit,
credit_errors = currency_form.credit_errors,
credit_total = currency_form.form.credit_total|accounting_txn_format_amount %}
credit_total = currency_form.form.credit_total|accounting_format_amount %}
{% include "accounting/transaction/transfer/include/form-currency-item.html" %}
{% endwith %}
{% endfor %}
{% else %}
{% with currency_index = 1,
only_one_currency_form = True,
currency_code_data = accounting_txn_default_currency_code(),
currency_code_data = accounting_default_currency_code(),
debit_total = "-",
credit_total = "-" %}
{% include "accounting/transaction/transfer/include/form-currency-item.html" %}
@ -50,11 +50,11 @@ First written: 2023/2/25
{% endblock %}
{% block form_modals %}
{% with summary_helper = form.summary_helper.debit %}
{% include "accounting/transaction/include/summary-helper-modal.html" %}
{% with summary_editor = form.summary_editor.debit %}
{% include "accounting/transaction/include/summary-editor-modal.html" %}
{% endwith %}
{% with summary_helper = form.summary_helper.credit %}
{% include "accounting/transaction/include/summary-helper-modal.html" %}
{% with summary_editor = form.summary_editor.credit %}
{% include "accounting/transaction/include/summary-editor-modal.html" %}
{% endwith %}
{% with entry_type = "debit",
account_options = form.debit_account_options %}

View File

@ -26,7 +26,7 @@ from flask_wtf import FlaskForm
from accounting.models import Transaction
from .forms import TransactionForm, IncomeTransactionForm, \
ExpenseTransactionForm, TransferTransactionForm
from .template import default_currency_code
from accounting.template_globals import default_currency_code
class TransactionType(ABC):

View File

@ -37,7 +37,7 @@ from accounting import db
from accounting.locale import lazy_gettext
from accounting.models import Transaction, Account, JournalEntry, \
TransactionCurrency, Currency
from accounting.transaction.summary_helper import SummaryHelper
from accounting.transaction.summary_editor import SummaryEditor
from accounting.utils.random_id import new_id
from accounting.utils.strip_text import strip_text, strip_multiline_text
from accounting.utils.user import get_current_user_pk
@ -391,12 +391,12 @@ class TransactionForm(FlaskForm):
if isinstance(x, str) or isinstance(x, LazyString)]
@property
def summary_helper(self) -> SummaryHelper:
"""Returns the summary helper.
def summary_editor(self) -> SummaryEditor:
"""Returns the summary editor.
:return: The summary helper.
:return: The summary editor.
"""
return SummaryHelper()
return SummaryEditor()
T = t.TypeVar("T", bound=TransactionForm)

View File

@ -14,7 +14,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The transaction query.
"""The queries for the transaction management.
"""
from datetime import datetime

View File

@ -14,7 +14,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The summary helper.
"""The summary editor.
"""
import typing as t
@ -178,7 +178,7 @@ class SummaryEntryType:
@property
def accounts(self) -> list[SummaryAccount]:
"""Returns the suggested accounts of all tags in the summary helper in
"""Returns the suggested accounts of all tags in the summary editor in
the entry type, in their frequency order.
:return: The suggested accounts of all tags, in their frequency order.
@ -197,11 +197,11 @@ class SummaryEntryType:
key=lambda x: -freq[x])]
class SummaryHelper:
"""The summary helper."""
class SummaryEditor:
"""The summary editor."""
def __init__(self):
"""Constructs the summary helper."""
"""Constructs the summary editor."""
self.debit: SummaryEntryType = SummaryEntryType("debit")
"""The debit tags."""
self.credit: SummaryEntryType = SummaryEntryType("credit")

View File

@ -14,20 +14,15 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The template filters and globals for the transaction management.
"""The template filters for the transaction management.
"""
from datetime import date, timedelta
from decimal import Decimal
from html import escape
from urllib.parse import ParseResult, urlparse, parse_qsl, urlencode, \
urlunparse
from flask import request, current_app
from flask_babel import get_locale
from accounting.locale import gettext
from accounting.models import Currency
from flask import request
def with_type(uri: str) -> str:
@ -62,19 +57,6 @@ def to_transfer(uri: str) -> str:
return urlunparse(parts)
def format_amount(value: Decimal | None) -> str:
"""Formats an amount for readability.
:param value: The amount.
:return: The formatted amount text.
"""
if value is None or value == 0:
return "-"
whole: int = int(value)
frac: Decimal = (value - whole).normalize()
return "{:,}".format(whole) + str(frac)[1:]
def format_amount_input(value: Decimal) -> str:
"""Format an amount for an input value.
@ -86,36 +68,6 @@ def format_amount_input(value: Decimal) -> str:
return str(whole) + str(frac)[1:]
def format_date(value: date) -> str:
"""Formats a date to be human-friendly.
:param value: The date.
:return: The human-friendly date text.
"""
today: date = date.today()
if value == today:
return gettext("Today")
if value == today - timedelta(days=1):
return gettext("Yesterday")
if value == today + timedelta(days=1):
return gettext("Tomorrow")
locale = str(get_locale())
if locale == "zh" or locale.startswith("zh_"):
if value == today - timedelta(days=2):
return gettext("The day before yesterday")
if value == today + timedelta(days=2):
return gettext("The day after tomorrow")
if locale == "zh" or locale.startswith("zh_"):
weekdays = ["", "", "", "", "", "", ""]
weekday = weekdays[value.weekday()]
else:
weekday = value.strftime("%a")
if value.year != today.year:
return "{}/{}/{}({})".format(
value.year, value.month, value.day, weekday)
return "{}/{}({})".format(value.month, value.day, weekday)
def text2html(value: str) -> str:
"""Converts plain text into HTML.
@ -126,20 +78,3 @@ def text2html(value: str) -> str:
s = s.replace("\n", "<br>")
s = s.replace(" ", " &nbsp;")
return s
def currency_options() -> str:
"""Returns the currency options.
:return: The currency options.
"""
return Currency.query.order_by(Currency.code).all()
def default_currency_code() -> str:
"""Returns the default currency code.
:return: The default currency code.
"""
with current_app.app_context():
return current_app.config.get("DEFAULT_CURRENCY", "USD")

View File

@ -35,23 +35,17 @@ from accounting.utils.permission import has_permission, can_view, can_edit
from accounting.utils.user import get_current_user_pk
from .dispatcher import TransactionType, get_txn_type, TXN_TYPE_OBJ
from .forms import sort_transactions_in, TransactionReorderForm
from .query import get_transaction_query
from .template import with_type, to_transfer, format_amount, \
format_amount_input, format_date, text2html, currency_options, \
default_currency_code
from .queries import get_transaction_query
from .template_filters import with_type, to_transfer, format_amount_input, \
text2html
bp: Blueprint = Blueprint("transaction", __name__)
"""The view blueprint for the transaction management."""
bp.add_app_template_filter(with_type, "accounting_txn_with_type")
bp.add_app_template_filter(to_transfer, "accounting_txn_to_transfer")
bp.add_app_template_filter(format_amount, "accounting_txn_format_amount")
bp.add_app_template_filter(format_amount_input,
"accounting_txn_format_amount_input")
bp.add_app_template_filter(format_date, "accounting_txn_format_date")
bp.add_app_template_filter(text2html, "accounting_txn_text2html")
bp.add_app_template_global(currency_options, "accounting_txn_currency_options")
bp.add_app_template_global(default_currency_code,
"accounting_txn_default_currency_code")
@bp.get("", endpoint="list")
@ -192,8 +186,8 @@ def show_transaction_order(txn_date: date) -> str:
:param txn_date: The date.
:return: The order of the transactions in the date.
"""
transactions: list[Transaction] = Transaction.query\
.filter(Transaction.date == txn_date)\
transactions: list[Transaction] = Transaction.query \
.filter(Transaction.date == txn_date) \
.order_by(Transaction.no).all()
return render_template("accounting/transaction/order.html",
date=txn_date, list=transactions)

View File

@ -14,7 +14,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The test for the summary helper.
"""The test for the summary editor.
"""
import unittest
@ -29,8 +29,8 @@ from testlib import get_client
from testlib_txn import Accounts, NEXT_URI, add_txn
class SummeryHelperTestCase(unittest.TestCase):
"""The summary helper test case."""
class SummeryEditorTestCase(unittest.TestCase):
"""The summary editor test case."""
def setUp(self) -> None:
"""Sets up the test.
@ -61,101 +61,101 @@ class SummeryHelperTestCase(unittest.TestCase):
self.client, self.csrf_token = get_client(self.app, "editor")
def test_summary_helper(self) -> None:
"""Test the summary helper.
def test_summary_editor(self) -> None:
"""Test the summary editor.
:return: None.
"""
from accounting.transaction.summary_helper import SummaryHelper
from accounting.transaction.summary_editor import SummaryEditor
for form in get_form_data(self.csrf_token):
add_txn(self.client, form)
with self.app.app_context():
helper: SummaryHelper = SummaryHelper()
editor: SummaryEditor = SummaryEditor()
# Debit-General
self.assertEqual(len(helper.debit.general.tags), 2)
self.assertEqual(helper.debit.general.tags[0].name, "Lunch")
self.assertEqual(len(helper.debit.general.tags[0].accounts), 2)
self.assertEqual(helper.debit.general.tags[0].accounts[0].code,
self.assertEqual(len(editor.debit.general.tags), 2)
self.assertEqual(editor.debit.general.tags[0].name, "Lunch")
self.assertEqual(len(editor.debit.general.tags[0].accounts), 2)
self.assertEqual(editor.debit.general.tags[0].accounts[0].code,
Accounts.MEAL)
self.assertEqual(helper.debit.general.tags[0].accounts[1].code,
self.assertEqual(editor.debit.general.tags[0].accounts[1].code,
Accounts.PAYABLE)
self.assertEqual(helper.debit.general.tags[1].name, "Dinner")
self.assertEqual(len(helper.debit.general.tags[1].accounts), 2)
self.assertEqual(helper.debit.general.tags[1].accounts[0].code,
self.assertEqual(editor.debit.general.tags[1].name, "Dinner")
self.assertEqual(len(editor.debit.general.tags[1].accounts), 2)
self.assertEqual(editor.debit.general.tags[1].accounts[0].code,
Accounts.MEAL)
self.assertEqual(helper.debit.general.tags[1].accounts[1].code,
self.assertEqual(editor.debit.general.tags[1].accounts[1].code,
Accounts.PAYABLE)
# Debit-Travel
self.assertEqual(len(helper.debit.travel.tags), 3)
self.assertEqual(helper.debit.travel.tags[0].name, "Bike")
self.assertEqual(len(helper.debit.travel.tags[0].accounts), 1)
self.assertEqual(helper.debit.travel.tags[0].accounts[0].code,
self.assertEqual(len(editor.debit.travel.tags), 3)
self.assertEqual(editor.debit.travel.tags[0].name, "Bike")
self.assertEqual(len(editor.debit.travel.tags[0].accounts), 1)
self.assertEqual(editor.debit.travel.tags[0].accounts[0].code,
Accounts.TRAVEL)
self.assertEqual(helper.debit.travel.tags[1].name, "Taxi")
self.assertEqual(len(helper.debit.travel.tags[1].accounts), 1)
self.assertEqual(helper.debit.travel.tags[1].accounts[0].code,
self.assertEqual(editor.debit.travel.tags[1].name, "Taxi")
self.assertEqual(len(editor.debit.travel.tags[1].accounts), 1)
self.assertEqual(editor.debit.travel.tags[1].accounts[0].code,
Accounts.TRAVEL)
self.assertEqual(helper.debit.travel.tags[2].name, "Airplane")
self.assertEqual(len(helper.debit.travel.tags[2].accounts), 1)
self.assertEqual(helper.debit.travel.tags[2].accounts[0].code,
self.assertEqual(editor.debit.travel.tags[2].name, "Airplane")
self.assertEqual(len(editor.debit.travel.tags[2].accounts), 1)
self.assertEqual(editor.debit.travel.tags[2].accounts[0].code,
Accounts.TRAVEL)
# Debit-Bus
self.assertEqual(len(helper.debit.bus.tags), 2)
self.assertEqual(helper.debit.bus.tags[0].name, "Train")
self.assertEqual(len(helper.debit.bus.tags[0].accounts), 1)
self.assertEqual(helper.debit.bus.tags[0].accounts[0].code,
self.assertEqual(len(editor.debit.bus.tags), 2)
self.assertEqual(editor.debit.bus.tags[0].name, "Train")
self.assertEqual(len(editor.debit.bus.tags[0].accounts), 1)
self.assertEqual(editor.debit.bus.tags[0].accounts[0].code,
Accounts.TRAVEL)
self.assertEqual(helper.debit.bus.tags[1].name, "Bus")
self.assertEqual(len(helper.debit.bus.tags[1].accounts), 1)
self.assertEqual(helper.debit.bus.tags[1].accounts[0].code,
self.assertEqual(editor.debit.bus.tags[1].name, "Bus")
self.assertEqual(len(editor.debit.bus.tags[1].accounts), 1)
self.assertEqual(editor.debit.bus.tags[1].accounts[0].code,
Accounts.TRAVEL)
# Credit-General
self.assertEqual(len(helper.credit.general.tags), 2)
self.assertEqual(helper.credit.general.tags[0].name, "Lunch")
self.assertEqual(len(helper.credit.general.tags[0].accounts), 3)
self.assertEqual(helper.credit.general.tags[0].accounts[0].code,
self.assertEqual(len(editor.credit.general.tags), 2)
self.assertEqual(editor.credit.general.tags[0].name, "Lunch")
self.assertEqual(len(editor.credit.general.tags[0].accounts), 3)
self.assertEqual(editor.credit.general.tags[0].accounts[0].code,
Accounts.PAYABLE)
self.assertEqual(helper.credit.general.tags[0].accounts[1].code,
self.assertEqual(editor.credit.general.tags[0].accounts[1].code,
Accounts.BANK)
self.assertEqual(helper.credit.general.tags[0].accounts[2].code,
self.assertEqual(editor.credit.general.tags[0].accounts[2].code,
Accounts.CASH)
self.assertEqual(helper.credit.general.tags[1].name, "Dinner")
self.assertEqual(len(helper.credit.general.tags[1].accounts), 2)
self.assertEqual(helper.credit.general.tags[1].accounts[0].code,
self.assertEqual(editor.credit.general.tags[1].name, "Dinner")
self.assertEqual(len(editor.credit.general.tags[1].accounts), 2)
self.assertEqual(editor.credit.general.tags[1].accounts[0].code,
Accounts.BANK)
self.assertEqual(helper.credit.general.tags[1].accounts[1].code,
self.assertEqual(editor.credit.general.tags[1].accounts[1].code,
Accounts.PAYABLE)
# Credit-Travel
self.assertEqual(len(helper.credit.travel.tags), 2)
self.assertEqual(helper.credit.travel.tags[0].name, "Bike")
self.assertEqual(len(helper.credit.travel.tags[0].accounts), 2)
self.assertEqual(helper.credit.travel.tags[0].accounts[0].code,
self.assertEqual(len(editor.credit.travel.tags), 2)
self.assertEqual(editor.credit.travel.tags[0].name, "Bike")
self.assertEqual(len(editor.credit.travel.tags[0].accounts), 2)
self.assertEqual(editor.credit.travel.tags[0].accounts[0].code,
Accounts.PAYABLE)
self.assertEqual(helper.credit.travel.tags[0].accounts[1].code,
self.assertEqual(editor.credit.travel.tags[0].accounts[1].code,
Accounts.PREPAID)
self.assertEqual(helper.credit.travel.tags[1].name, "Taxi")
self.assertEqual(len(helper.credit.travel.tags[1].accounts), 2)
self.assertEqual(helper.credit.travel.tags[1].accounts[0].code,
self.assertEqual(editor.credit.travel.tags[1].name, "Taxi")
self.assertEqual(len(editor.credit.travel.tags[1].accounts), 2)
self.assertEqual(editor.credit.travel.tags[1].accounts[0].code,
Accounts.PAYABLE)
self.assertEqual(helper.credit.travel.tags[1].accounts[1].code,
self.assertEqual(editor.credit.travel.tags[1].accounts[1].code,
Accounts.CASH)
# Credit-Bus
self.assertEqual(len(helper.credit.bus.tags), 2)
self.assertEqual(helper.credit.bus.tags[0].name, "Train")
self.assertEqual(len(helper.credit.bus.tags[0].accounts), 2)
self.assertEqual(helper.credit.bus.tags[0].accounts[0].code,
self.assertEqual(len(editor.credit.bus.tags), 2)
self.assertEqual(editor.credit.bus.tags[0].name, "Train")
self.assertEqual(len(editor.credit.bus.tags[0].accounts), 2)
self.assertEqual(editor.credit.bus.tags[0].accounts[0].code,
Accounts.PREPAID)
self.assertEqual(helper.credit.bus.tags[0].accounts[1].code,
self.assertEqual(editor.credit.bus.tags[0].accounts[1].code,
Accounts.PAYABLE)
self.assertEqual(helper.credit.bus.tags[1].name, "Bus")
self.assertEqual(len(helper.credit.bus.tags[1].accounts), 1)
self.assertEqual(helper.credit.bus.tags[1].accounts[0].code,
self.assertEqual(editor.credit.bus.tags[1].name, "Bus")
self.assertEqual(len(editor.credit.bus.tags[1].accounts), 1)
self.assertEqual(editor.credit.bus.tags[1].accounts[0].code,
Accounts.PREPAID)