Compare commits
No commits in common. "c50b9a2000eead0061f04ea1d5438b5475ab721d" and "9cd9e90be0ef4b4fe1e7f077395d5d53c1718cb2" have entirely different histories.
c50b9a2000
...
9cd9e90be0
@ -58,25 +58,12 @@ def init_app(app: Flask, user_utils: AbstractUserUtils,
|
|||||||
template_folder="templates",
|
template_folder="templates",
|
||||||
static_folder="static")
|
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
|
from . import locale
|
||||||
locale.init_app(app, bp)
|
locale.init_app(app, bp)
|
||||||
|
|
||||||
from .utils import permission
|
from .utils import permission
|
||||||
permission.init_app(bp, can_view_func, can_edit_func)
|
permission.init_app(bp, can_view_func, can_edit_func)
|
||||||
|
|
||||||
from .utils import next_uri
|
|
||||||
next_uri.init_app(bp)
|
|
||||||
|
|
||||||
from . import base_account
|
from . import base_account
|
||||||
base_account.init_app(app, bp)
|
base_account.init_app(app, bp)
|
||||||
|
|
||||||
@ -89,4 +76,7 @@ def init_app(app: Flask, user_utils: AbstractUserUtils,
|
|||||||
from . import transaction
|
from . import transaction
|
||||||
transaction.init_app(app, bp)
|
transaction.init_app(app, bp)
|
||||||
|
|
||||||
|
from .utils import next_uri
|
||||||
|
next_uri.init_app(bp)
|
||||||
|
|
||||||
app.register_blueprint(bp)
|
app.register_blueprint(bp)
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The queries for the account management.
|
"""The account query.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
@ -33,7 +33,7 @@ from accounting.utils.pagination import Pagination
|
|||||||
from accounting.utils.permission import can_view, has_permission, can_edit
|
from accounting.utils.permission import can_view, has_permission, can_edit
|
||||||
from accounting.utils.user import get_current_user_pk
|
from accounting.utils.user import get_current_user_pk
|
||||||
from .forms import AccountForm, sort_accounts_in, AccountReorderForm
|
from .forms import AccountForm, sort_accounts_in, AccountReorderForm
|
||||||
from .queries import get_account_query
|
from .query import get_account_query
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("account", __name__)
|
bp: Blueprint = Blueprint("account", __name__)
|
||||||
"""The view blueprint for the account management."""
|
"""The view blueprint for the account management."""
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The queries for the base account management.
|
"""The base account query.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
@ -34,7 +34,7 @@ def list_accounts() -> str:
|
|||||||
|
|
||||||
:return: The account list.
|
:return: The account list.
|
||||||
"""
|
"""
|
||||||
from .queries import get_base_account_query
|
from .query import get_base_account_query
|
||||||
accounts: list[BaseAccount] = get_base_account_query()
|
accounts: list[BaseAccount] = get_base_account_query()
|
||||||
pagination: Pagination = Pagination[BaseAccount](accounts)
|
pagination: Pagination = Pagination[BaseAccount](accounts)
|
||||||
return render_template("accounting/base-account/list.html",
|
return render_template("accounting/base-account/list.html",
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The queries for the currency management.
|
"""The currency query.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
@ -47,7 +47,7 @@ def list_currencies() -> str:
|
|||||||
|
|
||||||
:return: The currency list.
|
:return: The currency list.
|
||||||
"""
|
"""
|
||||||
from .queries import get_currency_query
|
from .query import get_currency_query
|
||||||
currencies: list[Currency] = get_currency_query()
|
currencies: list[Currency] = get_currency_query()
|
||||||
pagination: Pagination = Pagination[Currency](currencies)
|
pagination: Pagination = Pagination[Currency](currencies)
|
||||||
return render_template("accounting/currency/list.html",
|
return render_template("accounting/currency/list.html",
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
initializeBaseAccountSelector();
|
initializeBaseAccountSelector();
|
||||||
document.getElementById("accounting-base-code")
|
document.getElementById("accounting-base-code")
|
||||||
.onchange = validateBase;
|
.onchange = validateBase;
|
||||||
@ -44,7 +44,7 @@ function initializeBaseAccountSelector() {
|
|||||||
const baseContent = document.getElementById("accounting-base-content");
|
const baseContent = document.getElementById("accounting-base-content");
|
||||||
const options = Array.from(document.getElementsByClassName("accounting-base-option"));
|
const options = Array.from(document.getElementsByClassName("accounting-base-option"));
|
||||||
const btnClear = document.getElementById("accounting-btn-clear-base");
|
const btnClear = document.getElementById("accounting-btn-clear-base");
|
||||||
selector.addEventListener("show.bs.modal", () => {
|
selector.addEventListener("show.bs.modal", function () {
|
||||||
base.classList.add("accounting-not-empty");
|
base.classList.add("accounting-not-empty");
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
option.classList.remove("active");
|
option.classList.remove("active");
|
||||||
@ -54,13 +54,13 @@ function initializeBaseAccountSelector() {
|
|||||||
selected.classList.add("active");
|
selected.classList.add("active");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
selector.addEventListener("hidden.bs.modal", () => {
|
selector.addEventListener("hidden.bs.modal", function () {
|
||||||
if (baseCode.value === "") {
|
if (baseCode.value === "") {
|
||||||
base.classList.remove("accounting-not-empty");
|
base.classList.remove("accounting-not-empty");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
option.onclick = () => {
|
option.onclick = function () {
|
||||||
baseCode.value = option.dataset.code;
|
baseCode.value = option.dataset.code;
|
||||||
baseContent.innerText = option.dataset.content;
|
baseContent.innerText = option.dataset.content;
|
||||||
btnClear.classList.add("btn-danger");
|
btnClear.classList.add("btn-danger");
|
||||||
@ -70,7 +70,7 @@ function initializeBaseAccountSelector() {
|
|||||||
bootstrap.Modal.getInstance(selector).hide();
|
bootstrap.Modal.getInstance(selector).hide();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
btnClear.onclick = () => {
|
btnClear.onclick = function () {
|
||||||
baseCode.value = "";
|
baseCode.value = "";
|
||||||
baseContent.innerText = "";
|
baseContent.innerText = "";
|
||||||
btnClear.classList.add("btn-secondary")
|
btnClear.classList.add("btn-secondary")
|
||||||
@ -92,7 +92,7 @@ function initializeBaseAccountQuery() {
|
|||||||
const optionList = document.getElementById("accounting-base-option-list");
|
const optionList = document.getElementById("accounting-base-option-list");
|
||||||
const options = Array.from(document.getElementsByClassName("accounting-base-option"));
|
const options = Array.from(document.getElementsByClassName("accounting-base-option"));
|
||||||
const queryNoResult = document.getElementById("accounting-base-option-no-result");
|
const queryNoResult = document.getElementById("accounting-base-option-no-result");
|
||||||
query.addEventListener("input", () => {
|
query.addEventListener("input", function () {
|
||||||
if (query.value === "") {
|
if (query.value === "") {
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
option.classList.remove("d-none");
|
option.classList.remove("d-none");
|
||||||
|
@ -22,10 +22,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const list = document.getElementById("accounting-order-list");
|
const list = document.getElementById("accounting-order-list");
|
||||||
if (list !== null) {
|
if (list !== null) {
|
||||||
const onReorder = () => {
|
const onReorder = function () {
|
||||||
const accounts = Array.from(list.children);
|
const accounts = Array.from(list.children);
|
||||||
for (let i = 0; i < accounts.length; i++) {
|
for (let i = 0; i < accounts.length; i++) {
|
||||||
const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no");
|
const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no");
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
AccountSelector.initialize();
|
AccountSelector.initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -65,12 +65,13 @@ class AccountSelector {
|
|||||||
const more = document.getElementById(this.#prefix + "-more");
|
const more = document.getElementById(this.#prefix + "-more");
|
||||||
const btnClear = document.getElementById(this.#prefix + "-btn-clear");
|
const btnClear = document.getElementById(this.#prefix + "-btn-clear");
|
||||||
const options = Array.from(document.getElementsByClassName(this.#prefix + "-option"));
|
const options = Array.from(document.getElementsByClassName(this.#prefix + "-option"));
|
||||||
more.onclick = () => {
|
const selector1 = this
|
||||||
|
more.onclick = function () {
|
||||||
more.classList.add("d-none");
|
more.classList.add("d-none");
|
||||||
this.#filterAccountOptions();
|
selector1.#filterAccountOptions();
|
||||||
};
|
};
|
||||||
this.#initializeAccountQuery();
|
this.#initializeAccountQuery();
|
||||||
btnClear.onclick = () => {
|
btnClear.onclick = function () {
|
||||||
formAccountControl.classList.remove("accounting-not-empty");
|
formAccountControl.classList.remove("accounting-not-empty");
|
||||||
formAccount.innerText = "";
|
formAccount.innerText = "";
|
||||||
formAccount.dataset.code = "";
|
formAccount.dataset.code = "";
|
||||||
@ -78,7 +79,7 @@ class AccountSelector {
|
|||||||
validateJournalEntryAccount();
|
validateJournalEntryAccount();
|
||||||
};
|
};
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
option.onclick = () => {
|
option.onclick = function () {
|
||||||
formAccountControl.classList.add("accounting-not-empty");
|
formAccountControl.classList.add("accounting-not-empty");
|
||||||
formAccount.innerText = option.dataset.content;
|
formAccount.innerText = option.dataset.content;
|
||||||
formAccount.dataset.code = option.dataset.code;
|
formAccount.dataset.code = option.dataset.code;
|
||||||
@ -94,8 +95,9 @@ class AccountSelector {
|
|||||||
*/
|
*/
|
||||||
#initializeAccountQuery() {
|
#initializeAccountQuery() {
|
||||||
const query = document.getElementById(this.#prefix + "-query");
|
const query = document.getElementById(this.#prefix + "-query");
|
||||||
query.addEventListener("input", () => {
|
const helper = this;
|
||||||
this.#filterAccountOptions();
|
query.addEventListener("input", function () {
|
||||||
|
helper.#filterAccountOptions();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +159,7 @@ class AccountSelector {
|
|||||||
* @return {boolean} true if the account option should show, or false otherwise
|
* @return {boolean} true if the account option should show, or false otherwise
|
||||||
*/
|
*/
|
||||||
#shouldAccountOptionShow(option, more, inUse, query) {
|
#shouldAccountOptionShow(option, more, inUse, query) {
|
||||||
const isQueryMatched = () => {
|
const isQueryMatched = function () {
|
||||||
if (query.value === "") {
|
if (query.value === "") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -169,7 +171,7 @@ class AccountSelector {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
const isMoreMatched = () => {
|
const isMoreMatched = function () {
|
||||||
if (more.classList.contains("d-none")) {
|
if (more.classList.contains("d-none")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -235,7 +237,10 @@ class AccountSelector {
|
|||||||
static #initializeTransactionForm() {
|
static #initializeTransactionForm() {
|
||||||
const entryForm = document.getElementById("accounting-entry-form");
|
const entryForm = document.getElementById("accounting-entry-form");
|
||||||
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
|
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
|
||||||
formAccountControl.onclick = () => this.#selectors[entryForm.dataset.entryType].initShow();
|
const selectors = this.#selectors;
|
||||||
|
formAccountControl.onclick = function () {
|
||||||
|
selectors[entryForm.dataset.entryType].initShow();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Initializes the account selector for the journal entry form.
|
* Initializes the account selector for the journal entry form.
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
document.getElementById("accounting-code")
|
document.getElementById("accounting-code")
|
||||||
.onchange = validateCode;
|
.onchange = validateCode;
|
||||||
document.getElementById("accounting-name")
|
document.getElementById("accounting-name")
|
||||||
|
@ -44,15 +44,15 @@ function initializeMouseDragAndDropReordering(list, onReorder) {
|
|||||||
let dragged = null;
|
let dragged = null;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
item.draggable = true;
|
item.draggable = true;
|
||||||
item.addEventListener("dragstart", () => {
|
item.addEventListener("dragstart", function () {
|
||||||
dragged = item;
|
dragged = item;
|
||||||
dragged.classList.add("accounting-dragged");
|
dragged.classList.add("accounting-dragged");
|
||||||
});
|
});
|
||||||
item.addEventListener("dragover", () => {
|
item.addEventListener("dragover", function () {
|
||||||
onDragOver(dragged, item);
|
onDragOver(dragged, item);
|
||||||
onReorder();
|
onReorder();
|
||||||
});
|
});
|
||||||
item.addEventListener("dragend", () => {
|
item.addEventListener("dragend", function () {
|
||||||
dragged.classList.remove("accounting-dragged");
|
dragged.classList.remove("accounting-dragged");
|
||||||
dragged = null;
|
dragged = null;
|
||||||
});
|
});
|
||||||
@ -69,16 +69,16 @@ function initializeMouseDragAndDropReordering(list, onReorder) {
|
|||||||
function initializeTouchDragAndDropReordering(list, onReorder) {
|
function initializeTouchDragAndDropReordering(list, onReorder) {
|
||||||
const items = Array.from(list.children);
|
const items = Array.from(list.children);
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
item.addEventListener("touchstart", () => {
|
item.addEventListener("touchstart", function () {
|
||||||
item.classList.add("accounting-dragged");
|
item.classList.add("accounting-dragged");
|
||||||
});
|
});
|
||||||
item.addEventListener("touchmove", (event) => {
|
item.addEventListener("touchmove", function (event) {
|
||||||
const touch = event.targetTouches[0];
|
const touch = event.targetTouches[0];
|
||||||
const target = document.elementFromPoint(touch.pageX, touch.pageY);
|
const target = document.elementFromPoint(touch.pageX, touch.pageY);
|
||||||
onDragOver(item, target);
|
onDragOver(item, target);
|
||||||
onReorder();
|
onReorder();
|
||||||
});
|
});
|
||||||
item.addEventListener("touchend", () => {
|
item.addEventListener("touchend", function () {
|
||||||
item.classList.remove("accounting-dragged");
|
item.classList.remove("accounting-dragged");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
initializeMaterialFabSpeedDial();
|
initializeMaterialFabSpeedDial();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
function initializeMaterialFabSpeedDial() {
|
function initializeMaterialFabSpeedDial() {
|
||||||
const btnFab = document.getElementById("accounting-btn-material-fab-speed-dial");
|
const btnFab = document.getElementById("accounting-btn-material-fab-speed-dial");
|
||||||
const fab = document.getElementById(btnFab.dataset.target);
|
const fab = document.getElementById(btnFab.dataset.target);
|
||||||
btnFab.onclick = () => {
|
btnFab.onclick = function () {
|
||||||
if (fab.classList.contains("show")) {
|
if (fab.classList.contains("show")) {
|
||||||
fab.classList.remove("show");
|
fab.classList.remove("show");
|
||||||
} else {
|
} else {
|
||||||
|
File diff suppressed because it is too large
Load Diff
827
src/accounting/static/js/summary-helper.js
Normal file
827
src/accounting/static/js/summary-helper.js
Normal file
@ -0,0 +1,827 @@
|
|||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
initializeCurrencyForms();
|
initializeCurrencyForms();
|
||||||
initializeJournalEntries();
|
initializeJournalEntries();
|
||||||
initializeFormValidation();
|
initializeFormValidation();
|
||||||
@ -68,14 +68,14 @@ function initializeCurrencyForms() {
|
|||||||
const btnNew = document.getElementById("accounting-btn-new-currency");
|
const btnNew = document.getElementById("accounting-btn-new-currency");
|
||||||
const currencyList = document.getElementById("accounting-currency-list");
|
const currencyList = document.getElementById("accounting-currency-list");
|
||||||
const deleteButtons = Array.from(document.getElementsByClassName("accounting-btn-delete-currency"));
|
const deleteButtons = Array.from(document.getElementsByClassName("accounting-btn-delete-currency"));
|
||||||
const onReorder = () => {
|
const onReorder = function () {
|
||||||
const currencies = Array.from(currencyList.children);
|
const currencies = Array.from(currencyList.children);
|
||||||
for (let i = 0; i < currencies.length; i++) {
|
for (let i = 0; i < currencies.length; i++) {
|
||||||
const no = document.getElementById(currencies[i].dataset.prefix + "-no");
|
const no = document.getElementById(currencies[i].dataset.prefix + "-no");
|
||||||
no.value = String(i + 1);
|
no.value = String(i + 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
btnNew.onclick = () => {
|
btnNew.onclick = function () {
|
||||||
const currencies = Array.from(document.getElementsByClassName("accounting-currency"));
|
const currencies = Array.from(document.getElementsByClassName("accounting-currency"));
|
||||||
let maxIndex = 0;
|
let maxIndex = 0;
|
||||||
for (const currency of currencies) {
|
for (const currency of currencies) {
|
||||||
@ -107,7 +107,7 @@ function initializeCurrencyForms() {
|
|||||||
*/
|
*/
|
||||||
function initializeBtnDeleteCurrency(button) {
|
function initializeBtnDeleteCurrency(button) {
|
||||||
const target = document.getElementById(button.dataset.target);
|
const target = document.getElementById(button.dataset.target);
|
||||||
button.onclick = () => {
|
button.onclick = function () {
|
||||||
target.parentElement.removeChild(target);
|
target.parentElement.removeChild(target);
|
||||||
resetDeleteCurrencyButtons();
|
resetDeleteCurrencyButtons();
|
||||||
};
|
};
|
||||||
@ -161,7 +161,7 @@ function initializeNewEntryButton(button) {
|
|||||||
const formSummaryError = document.getElementById("accounting-entry-form-summary-error");
|
const formSummaryError = document.getElementById("accounting-entry-form-summary-error");
|
||||||
const formAmount = document.getElementById("accounting-entry-form-amount");
|
const formAmount = document.getElementById("accounting-entry-form-amount");
|
||||||
const formAmountError = document.getElementById("accounting-entry-form-amount-error");
|
const formAmountError = document.getElementById("accounting-entry-form-amount-error");
|
||||||
button.onclick = () => {
|
button.onclick = function () {
|
||||||
entryForm.dataset.currencyIndex = button.dataset.currencyIndex;
|
entryForm.dataset.currencyIndex = button.dataset.currencyIndex;
|
||||||
entryForm.dataset.entryType = button.dataset.entryType;
|
entryForm.dataset.entryType = button.dataset.entryType;
|
||||||
entryForm.dataset.entryIndex = button.dataset.entryIndex;
|
entryForm.dataset.entryIndex = button.dataset.entryIndex;
|
||||||
@ -171,7 +171,7 @@ function initializeNewEntryButton(button) {
|
|||||||
formAccount.dataset.code = "";
|
formAccount.dataset.code = "";
|
||||||
formAccount.dataset.text = "";
|
formAccount.dataset.text = "";
|
||||||
formAccountError.innerText = "";
|
formAccountError.innerText = "";
|
||||||
formSummaryControl.dataset.bsTarget = "#accounting-summary-editor-" + button.dataset.entryType + "-modal";
|
formSummaryControl.dataset.bsTarget = "#accounting-summary-helper-" + button.dataset.entryType + "-modal";
|
||||||
formSummaryControl.classList.remove("accounting-not-empty");
|
formSummaryControl.classList.remove("accounting-not-empty");
|
||||||
formSummaryControl.classList.remove("is-invalid");
|
formSummaryControl.classList.remove("is-invalid");
|
||||||
formSummary.dataset.value = "";
|
formSummary.dataset.value = "";
|
||||||
@ -181,7 +181,7 @@ function initializeNewEntryButton(button) {
|
|||||||
formAmount.classList.remove("is-invalid");
|
formAmount.classList.remove("is-invalid");
|
||||||
formAmountError.innerText = "";
|
formAmountError.innerText = "";
|
||||||
AccountSelector.initializeJournalEntryForm();
|
AccountSelector.initializeJournalEntryForm();
|
||||||
SummaryEditor.initializeNewJournalEntry(button.dataset.entryType);
|
SummaryHelper.initializeNewJournalEntry(button.dataset.entryType);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,7 +191,7 @@ function initializeNewEntryButton(button) {
|
|||||||
* @param entryList {HTMLUListElement} the journal entry list.
|
* @param entryList {HTMLUListElement} the journal entry list.
|
||||||
*/
|
*/
|
||||||
function initializeJournalEntryListReorder(entryList) {
|
function initializeJournalEntryListReorder(entryList) {
|
||||||
initializeDragAndDropReordering(entryList, () => {
|
initializeDragAndDropReordering(entryList, function () {
|
||||||
const entries = Array.from(entryList.children);
|
const entries = Array.from(entryList.children);
|
||||||
for (let i = 0; i < entries.length; i++) {
|
for (let i = 0; i < entries.length; i++) {
|
||||||
const no = document.getElementById(entries[i].dataset.prefix + "-no");
|
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 formSummaryControl = document.getElementById("accounting-entry-form-summary-control");
|
||||||
const formSummary = document.getElementById("accounting-entry-form-summary");
|
const formSummary = document.getElementById("accounting-entry-form-summary");
|
||||||
const formAmount = document.getElementById("accounting-entry-form-amount");
|
const formAmount = document.getElementById("accounting-entry-form-amount");
|
||||||
control.onclick = () => {
|
control.onclick = function () {
|
||||||
entryForm.dataset.currencyIndex = entry.dataset.currencyIndex;
|
entryForm.dataset.currencyIndex = entry.dataset.currencyIndex;
|
||||||
entryForm.dataset.entryType = entry.dataset.entryType;
|
entryForm.dataset.entryType = entry.dataset.entryType;
|
||||||
entryForm.dataset.entryIndex = entry.dataset.entryIndex;
|
entryForm.dataset.entryIndex = entry.dataset.entryIndex;
|
||||||
@ -228,7 +228,7 @@ function initializeJournalEntry(entry) {
|
|||||||
formAccount.innerText = accountCode.dataset.text;
|
formAccount.innerText = accountCode.dataset.text;
|
||||||
formAccount.dataset.code = accountCode.value;
|
formAccount.dataset.code = accountCode.value;
|
||||||
formAccount.dataset.text = accountCode.dataset.text;
|
formAccount.dataset.text = accountCode.dataset.text;
|
||||||
formSummaryControl.dataset.bsTarget = "#accounting-summary-editor-" + entry.dataset.entryType + "-modal";
|
formSummaryControl.dataset.bsTarget = "#accounting-summary-helper-" + entry.dataset.entryType + "-modal";
|
||||||
if (summary.value === "") {
|
if (summary.value === "") {
|
||||||
formSummaryControl.classList.remove("accounting-not-empty");
|
formSummaryControl.classList.remove("accounting-not-empty");
|
||||||
} else {
|
} else {
|
||||||
@ -252,7 +252,7 @@ function initializeJournalEntryFormModal() {
|
|||||||
const formAmount = document.getElementById("accounting-entry-form-amount");
|
const formAmount = document.getElementById("accounting-entry-form-amount");
|
||||||
const modal = document.getElementById("accounting-entry-form-modal");
|
const modal = document.getElementById("accounting-entry-form-modal");
|
||||||
formAmount.onchange = validateJournalEntryAmount;
|
formAmount.onchange = validateJournalEntryAmount;
|
||||||
entryForm.onsubmit = () => {
|
entryForm.onsubmit = function () {
|
||||||
if (validateJournalEntryForm()) {
|
if (validateJournalEntryForm()) {
|
||||||
saveJournalEntryForm();
|
saveJournalEntryForm();
|
||||||
bootstrap.Modal.getInstance(modal).hide();
|
bootstrap.Modal.getInstance(modal).hide();
|
||||||
@ -398,7 +398,7 @@ function initializeDeleteJournalEntryButton(button) {
|
|||||||
const currencyIndex = target.dataset.currencyIndex;
|
const currencyIndex = target.dataset.currencyIndex;
|
||||||
const entryType = target.dataset.entryType;
|
const entryType = target.dataset.entryType;
|
||||||
const currency = document.getElementById("accounting-currency-" + currencyIndex);
|
const currency = document.getElementById("accounting-currency-" + currencyIndex);
|
||||||
button.onclick = () => {
|
button.onclick = function () {
|
||||||
target.parentElement.removeChild(target);
|
target.parentElement.removeChild(target);
|
||||||
resetDeleteJournalEntryButtons(button.dataset.sameClass);
|
resetDeleteJournalEntryButtons(button.dataset.sameClass);
|
||||||
updateBalance(currencyIndex, entryType);
|
updateBalance(currencyIndex, entryType);
|
||||||
|
@ -22,10 +22,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Initializes the page JavaScript.
|
// Initializes the page JavaScript.
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const list = document.getElementById("accounting-order-list");
|
const list = document.getElementById("accounting-order-list");
|
||||||
if (list !== null) {
|
if (list !== null) {
|
||||||
const onReorder = () => {
|
const onReorder = function () {
|
||||||
const accounts = Array.from(list.children);
|
const accounts = Array.from(list.children);
|
||||||
for (let i = 0; i < accounts.length; i++) {
|
for (let i = 0; i < accounts.length; i++) {
|
||||||
const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no");
|
const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no");
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
# 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)
|
|
@ -1,39 +0,0 @@
|
|||||||
# 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")
|
|
@ -44,14 +44,14 @@ First written: 2023/2/26
|
|||||||
<div>{{ entry.summary }}</div>
|
<div>{{ entry.summary }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>{{ entry.amount|accounting_format_amount }}</div>
|
<div>{{ entry.amount|accounting_txn_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
|
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div>{{ A_("Total") }}</div>
|
<div>{{ A_("Total") }}</div>
|
||||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
<div>{{ currency.debit_total|accounting_txn_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -25,7 +25,7 @@ First written: 2023/2/25
|
|||||||
<div class="d-flex justify-content-between mt-2 mb-3">
|
<div class="d-flex justify-content-between mt-2 mb-3">
|
||||||
<div class="form-floating accounting-currency-content">
|
<div class="form-floating accounting-currency-content">
|
||||||
<select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code">
|
<select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code">
|
||||||
{% for currency in accounting_currency_options() %}
|
{% for currency in accounting_txn_currency_options() %}
|
||||||
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
|
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
@ -57,7 +57,7 @@ First written: 2023/2/25
|
|||||||
summary_errors = entry_form.summary.errors,
|
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_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_errors = entry_form.amount.errors,
|
||||||
amount_text = entry_form.amount.data|accounting_format_amount,
|
amount_text = entry_form.amount.data|accounting_txn_format_amount,
|
||||||
entry_errors = entry_form.all_errors %}
|
entry_errors = entry_form.all_errors %}
|
||||||
{% include "accounting/transaction/include/form-entry-item.html" %}
|
{% include "accounting/transaction/include/form-entry-item.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -31,14 +31,14 @@ First written: 2023/2/25
|
|||||||
currency_code_errors = currency_form.code.errors,
|
currency_code_errors = currency_form.code.errors,
|
||||||
debit_forms = currency_form.debit,
|
debit_forms = currency_form.debit,
|
||||||
debit_errors = currency_form.debit_errors,
|
debit_errors = currency_form.debit_errors,
|
||||||
debit_total = currency_form.form.debit_total|accounting_format_amount %}
|
debit_total = currency_form.form.debit_total|accounting_txn_format_amount %}
|
||||||
{% include "accounting/transaction/expense/include/form-currency-item.html" %}
|
{% include "accounting/transaction/expense/include/form-currency-item.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with currency_index = 1,
|
{% with currency_index = 1,
|
||||||
only_one_currency_form = True,
|
only_one_currency_form = True,
|
||||||
currency_code_data = accounting_default_currency_code(),
|
currency_code_data = accounting_txn_default_currency_code(),
|
||||||
debit_total = "-" %}
|
debit_total = "-" %}
|
||||||
{% include "accounting/transaction/expense/include/form-currency-item.html" %}
|
{% include "accounting/transaction/expense/include/form-currency-item.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
@ -46,8 +46,8 @@ First written: 2023/2/25
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block form_modals %}
|
{% block form_modals %}
|
||||||
{% with summary_editor = form.summary_editor.debit %}
|
{% with summary_helper = form.summary_helper.debit %}
|
||||||
{% include "accounting/transaction/include/summary-editor-modal.html" %}
|
{% include "accounting/transaction/include/summary-helper-modal.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% with entry_type = "debit",
|
{% with entry_type = "debit",
|
||||||
account_options = form.debit_account_options %}
|
account_options = form.debit_account_options %}
|
||||||
|
@ -89,7 +89,7 @@ First written: 2023/2/26
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{{ obj.date|accounting_format_date }}
|
{{ obj.date|accounting_txn_format_date }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block transaction_currencies %}{% endblock %}
|
{% block transaction_currencies %}{% endblock %}
|
||||||
|
@ -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/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/transaction-form.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/account-selector.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/account-selector.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/summary-editor.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/summary-helper.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -1,190 +0,0 @@
|
|||||||
{#
|
|
||||||
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="→">→</button>
|
|
||||||
<button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.type }}-travel-direction" type="button" tabindex="-1" data-arrow="↔">↔</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>
|
|
@ -0,0 +1,181 @@
|
|||||||
|
{#
|
||||||
|
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="→">→</button>
|
||||||
|
<button class="btn btn-outline-primary accounting-summary-helper-{{ summary_helper.type }}-travel-direction" type="button" tabindex="-1" data-arrow="↔">↔</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>
|
@ -44,14 +44,14 @@ First written: 2023/2/26
|
|||||||
<div>{{ entry.summary }}</div>
|
<div>{{ entry.summary }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>{{ entry.amount|accounting_format_amount }}</div>
|
<div>{{ entry.amount|accounting_txn_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
|
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div>{{ A_("Total") }}</div>
|
<div>{{ A_("Total") }}</div>
|
||||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
<div>{{ currency.debit_total|accounting_txn_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -25,7 +25,7 @@ First written: 2023/2/25
|
|||||||
<div class="d-flex justify-content-between mt-2 mb-3">
|
<div class="d-flex justify-content-between mt-2 mb-3">
|
||||||
<div class="form-floating accounting-currency-content">
|
<div class="form-floating accounting-currency-content">
|
||||||
<select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code">
|
<select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code">
|
||||||
{% for currency in accounting_currency_options() %}
|
{% for currency in accounting_txn_currency_options() %}
|
||||||
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
|
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
@ -57,7 +57,7 @@ First written: 2023/2/25
|
|||||||
summary_errors = entry_form.summary.errors,
|
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_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_errors = entry_form.amount.errors,
|
||||||
amount_text = entry_form.amount.data|accounting_format_amount,
|
amount_text = entry_form.amount.data|accounting_txn_format_amount,
|
||||||
entry_errors = entry_form.all_errors %}
|
entry_errors = entry_form.all_errors %}
|
||||||
{% include "accounting/transaction/include/form-entry-item.html" %}
|
{% include "accounting/transaction/include/form-entry-item.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -31,14 +31,14 @@ First written: 2023/2/25
|
|||||||
currency_code_errors = currency_form.code.errors,
|
currency_code_errors = currency_form.code.errors,
|
||||||
credit_forms = currency_form.credit,
|
credit_forms = currency_form.credit,
|
||||||
credit_errors = currency_form.credit_errors,
|
credit_errors = currency_form.credit_errors,
|
||||||
credit_total = currency_form.form.credit_total|accounting_format_amount %}
|
credit_total = currency_form.form.credit_total|accounting_txn_format_amount %}
|
||||||
{% include "accounting/transaction/income/include/form-currency-item.html" %}
|
{% include "accounting/transaction/income/include/form-currency-item.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with currency_index = 1,
|
{% with currency_index = 1,
|
||||||
only_one_currency_form = True,
|
only_one_currency_form = True,
|
||||||
currency_code_data = accounting_default_currency_code(),
|
currency_code_data = accounting_txn_default_currency_code(),
|
||||||
credit_total = "-" %}
|
credit_total = "-" %}
|
||||||
{% include "accounting/transaction/income/include/form-currency-item.html" %}
|
{% include "accounting/transaction/income/include/form-currency-item.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
@ -46,8 +46,8 @@ First written: 2023/2/25
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block form_modals %}
|
{% block form_modals %}
|
||||||
{% with summary_editor = form.summary_editor.credit %}
|
{% with summary_helper = form.summary_helper.credit %}
|
||||||
{% include "accounting/transaction/include/summary-editor-modal.html" %}
|
{% include "accounting/transaction/include/summary-helper-modal.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% with entry_type = "credit",
|
{% with entry_type = "credit",
|
||||||
account_options = form.credit_account_options %}
|
account_options = form.credit_account_options %}
|
||||||
|
@ -85,7 +85,7 @@ First written: 2023/2/18
|
|||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{% for item in list %}
|
{% for item in list %}
|
||||||
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.transaction.detail", txn=item)|accounting_append_next }}">
|
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.transaction.detail", txn=item)|accounting_append_next }}">
|
||||||
{{ item.date|accounting_format_date }} {{ item }}
|
{{ item.date|accounting_txn_format_date }} {{ item }}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,14 +40,14 @@ First written: 2023/2/26
|
|||||||
<div>{{ entry.summary }}</div>
|
<div>{{ entry.summary }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>{{ entry.amount|accounting_format_amount }}</div>
|
<div>{{ entry.amount|accounting_txn_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
|
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div>{{ A_("Total") }}</div>
|
<div>{{ A_("Total") }}</div>
|
||||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
<div>{{ currency.debit_total|accounting_txn_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -66,14 +66,14 @@ First written: 2023/2/26
|
|||||||
<div>{{ entry.summary }}</div>
|
<div>{{ entry.summary }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>{{ entry.amount|accounting_format_amount }}</div>
|
<div>{{ entry.amount|accounting_txn_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
|
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div>{{ A_("Total") }}</div>
|
<div>{{ A_("Total") }}</div>
|
||||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
<div>{{ currency.debit_total|accounting_txn_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -25,7 +25,7 @@ First written: 2023/2/25
|
|||||||
<div class="d-flex justify-content-between mt-2 mb-3">
|
<div class="d-flex justify-content-between mt-2 mb-3">
|
||||||
<div class="form-floating accounting-currency-content">
|
<div class="form-floating accounting-currency-content">
|
||||||
<select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code">
|
<select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code">
|
||||||
{% for currency in accounting_currency_options() %}
|
{% for currency in accounting_txn_currency_options() %}
|
||||||
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
|
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
@ -59,7 +59,7 @@ First written: 2023/2/25
|
|||||||
summary_errors = entry_form.summary.errors,
|
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_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_errors = entry_form.amount.errors,
|
||||||
amount_text = entry_form.amount.data|accounting_format_amount,
|
amount_text = entry_form.amount.data|accounting_txn_format_amount,
|
||||||
entry_errors = entry_form.all_errors %}
|
entry_errors = entry_form.all_errors %}
|
||||||
{% include "accounting/transaction/include/form-entry-item.html" %}
|
{% include "accounting/transaction/include/form-entry-item.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
@ -99,7 +99,7 @@ First written: 2023/2/25
|
|||||||
summary_errors = entry_form.summary.errors,
|
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_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_errors = entry_form.amount.errors,
|
||||||
amount_text = entry_form.amount.data|accounting_format_amount,
|
amount_text = entry_form.amount.data|accounting_txn_format_amount,
|
||||||
entry_errors = entry_form.all_errors %}
|
entry_errors = entry_form.all_errors %}
|
||||||
{% include "accounting/transaction/include/form-entry-item.html" %}
|
{% include "accounting/transaction/include/form-entry-item.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -31,17 +31,17 @@ First written: 2023/2/25
|
|||||||
currency_code_errors = currency_form.code.errors,
|
currency_code_errors = currency_form.code.errors,
|
||||||
debit_forms = currency_form.debit,
|
debit_forms = currency_form.debit,
|
||||||
debit_errors = currency_form.debit_errors,
|
debit_errors = currency_form.debit_errors,
|
||||||
debit_total = currency_form.form.debit_total|accounting_format_amount,
|
debit_total = currency_form.form.debit_total|accounting_txn_format_amount,
|
||||||
credit_forms = currency_form.credit,
|
credit_forms = currency_form.credit,
|
||||||
credit_errors = currency_form.credit_errors,
|
credit_errors = currency_form.credit_errors,
|
||||||
credit_total = currency_form.form.credit_total|accounting_format_amount %}
|
credit_total = currency_form.form.credit_total|accounting_txn_format_amount %}
|
||||||
{% include "accounting/transaction/transfer/include/form-currency-item.html" %}
|
{% include "accounting/transaction/transfer/include/form-currency-item.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with currency_index = 1,
|
{% with currency_index = 1,
|
||||||
only_one_currency_form = True,
|
only_one_currency_form = True,
|
||||||
currency_code_data = accounting_default_currency_code(),
|
currency_code_data = accounting_txn_default_currency_code(),
|
||||||
debit_total = "-",
|
debit_total = "-",
|
||||||
credit_total = "-" %}
|
credit_total = "-" %}
|
||||||
{% include "accounting/transaction/transfer/include/form-currency-item.html" %}
|
{% include "accounting/transaction/transfer/include/form-currency-item.html" %}
|
||||||
@ -50,11 +50,11 @@ First written: 2023/2/25
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block form_modals %}
|
{% block form_modals %}
|
||||||
{% with summary_editor = form.summary_editor.debit %}
|
{% with summary_helper = form.summary_helper.debit %}
|
||||||
{% include "accounting/transaction/include/summary-editor-modal.html" %}
|
{% include "accounting/transaction/include/summary-helper-modal.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% with summary_editor = form.summary_editor.credit %}
|
{% with summary_helper = form.summary_helper.credit %}
|
||||||
{% include "accounting/transaction/include/summary-editor-modal.html" %}
|
{% include "accounting/transaction/include/summary-helper-modal.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% with entry_type = "debit",
|
{% with entry_type = "debit",
|
||||||
account_options = form.debit_account_options %}
|
account_options = form.debit_account_options %}
|
||||||
|
@ -26,7 +26,7 @@ from flask_wtf import FlaskForm
|
|||||||
from accounting.models import Transaction
|
from accounting.models import Transaction
|
||||||
from .forms import TransactionForm, IncomeTransactionForm, \
|
from .forms import TransactionForm, IncomeTransactionForm, \
|
||||||
ExpenseTransactionForm, TransferTransactionForm
|
ExpenseTransactionForm, TransferTransactionForm
|
||||||
from accounting.template_globals import default_currency_code
|
from .template import default_currency_code
|
||||||
|
|
||||||
|
|
||||||
class TransactionType(ABC):
|
class TransactionType(ABC):
|
||||||
|
@ -37,7 +37,7 @@ from accounting import db
|
|||||||
from accounting.locale import lazy_gettext
|
from accounting.locale import lazy_gettext
|
||||||
from accounting.models import Transaction, Account, JournalEntry, \
|
from accounting.models import Transaction, Account, JournalEntry, \
|
||||||
TransactionCurrency, Currency
|
TransactionCurrency, Currency
|
||||||
from accounting.transaction.summary_editor import SummaryEditor
|
from accounting.transaction.summary_helper import SummaryHelper
|
||||||
from accounting.utils.random_id import new_id
|
from accounting.utils.random_id import new_id
|
||||||
from accounting.utils.strip_text import strip_text, strip_multiline_text
|
from accounting.utils.strip_text import strip_text, strip_multiline_text
|
||||||
from accounting.utils.user import get_current_user_pk
|
from accounting.utils.user import get_current_user_pk
|
||||||
@ -391,12 +391,12 @@ class TransactionForm(FlaskForm):
|
|||||||
if isinstance(x, str) or isinstance(x, LazyString)]
|
if isinstance(x, str) or isinstance(x, LazyString)]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def summary_editor(self) -> SummaryEditor:
|
def summary_helper(self) -> SummaryHelper:
|
||||||
"""Returns the summary editor.
|
"""Returns the summary helper.
|
||||||
|
|
||||||
:return: The summary editor.
|
:return: The summary helper.
|
||||||
"""
|
"""
|
||||||
return SummaryEditor()
|
return SummaryHelper()
|
||||||
|
|
||||||
|
|
||||||
T = t.TypeVar("T", bound=TransactionForm)
|
T = t.TypeVar("T", bound=TransactionForm)
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The queries for the transaction management.
|
"""The transaction query.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
@ -14,7 +14,7 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The summary editor.
|
"""The summary helper.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
import typing as t
|
||||||
@ -178,7 +178,7 @@ class SummaryEntryType:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def accounts(self) -> list[SummaryAccount]:
|
def accounts(self) -> list[SummaryAccount]:
|
||||||
"""Returns the suggested accounts of all tags in the summary editor in
|
"""Returns the suggested accounts of all tags in the summary helper in
|
||||||
the entry type, in their frequency order.
|
the entry type, in their frequency order.
|
||||||
|
|
||||||
:return: The suggested accounts of all tags, 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])]
|
key=lambda x: -freq[x])]
|
||||||
|
|
||||||
|
|
||||||
class SummaryEditor:
|
class SummaryHelper:
|
||||||
"""The summary editor."""
|
"""The summary helper."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Constructs the summary editor."""
|
"""Constructs the summary helper."""
|
||||||
self.debit: SummaryEntryType = SummaryEntryType("debit")
|
self.debit: SummaryEntryType = SummaryEntryType("debit")
|
||||||
"""The debit tags."""
|
"""The debit tags."""
|
||||||
self.credit: SummaryEntryType = SummaryEntryType("credit")
|
self.credit: SummaryEntryType = SummaryEntryType("credit")
|
@ -14,15 +14,20 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The template filters for the transaction management.
|
"""The template filters and globals for the transaction management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from datetime import date, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from html import escape
|
from html import escape
|
||||||
from urllib.parse import ParseResult, urlparse, parse_qsl, urlencode, \
|
from urllib.parse import ParseResult, urlparse, parse_qsl, urlencode, \
|
||||||
urlunparse
|
urlunparse
|
||||||
|
|
||||||
from flask import request
|
from flask import request, current_app
|
||||||
|
from flask_babel import get_locale
|
||||||
|
|
||||||
|
from accounting.locale import gettext
|
||||||
|
from accounting.models import Currency
|
||||||
|
|
||||||
|
|
||||||
def with_type(uri: str) -> str:
|
def with_type(uri: str) -> str:
|
||||||
@ -57,6 +62,19 @@ def to_transfer(uri: str) -> str:
|
|||||||
return urlunparse(parts)
|
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:
|
def format_amount_input(value: Decimal) -> str:
|
||||||
"""Format an amount for an input value.
|
"""Format an amount for an input value.
|
||||||
|
|
||||||
@ -68,6 +86,36 @@ def format_amount_input(value: Decimal) -> str:
|
|||||||
return str(whole) + str(frac)[1:]
|
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:
|
def text2html(value: str) -> str:
|
||||||
"""Converts plain text into HTML.
|
"""Converts plain text into HTML.
|
||||||
|
|
||||||
@ -78,3 +126,20 @@ def text2html(value: str) -> str:
|
|||||||
s = s.replace("\n", "<br>")
|
s = s.replace("\n", "<br>")
|
||||||
s = s.replace(" ", " ")
|
s = s.replace(" ", " ")
|
||||||
return s
|
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")
|
@ -35,17 +35,23 @@ from accounting.utils.permission import has_permission, can_view, can_edit
|
|||||||
from accounting.utils.user import get_current_user_pk
|
from accounting.utils.user import get_current_user_pk
|
||||||
from .dispatcher import TransactionType, get_txn_type, TXN_TYPE_OBJ
|
from .dispatcher import TransactionType, get_txn_type, TXN_TYPE_OBJ
|
||||||
from .forms import sort_transactions_in, TransactionReorderForm
|
from .forms import sort_transactions_in, TransactionReorderForm
|
||||||
from .queries import get_transaction_query
|
from .query import get_transaction_query
|
||||||
from .template_filters import with_type, to_transfer, format_amount_input, \
|
from .template import with_type, to_transfer, format_amount, \
|
||||||
text2html
|
format_amount_input, format_date, text2html, currency_options, \
|
||||||
|
default_currency_code
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("transaction", __name__)
|
bp: Blueprint = Blueprint("transaction", __name__)
|
||||||
"""The view blueprint for the transaction management."""
|
"""The view blueprint for the transaction management."""
|
||||||
bp.add_app_template_filter(with_type, "accounting_txn_with_type")
|
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(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,
|
bp.add_app_template_filter(format_amount_input,
|
||||||
"accounting_txn_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_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")
|
@bp.get("", endpoint="list")
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The test for the summary editor.
|
"""The test for the summary helper.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import unittest
|
import unittest
|
||||||
@ -29,8 +29,8 @@ from testlib import get_client
|
|||||||
from testlib_txn import Accounts, NEXT_URI, add_txn
|
from testlib_txn import Accounts, NEXT_URI, add_txn
|
||||||
|
|
||||||
|
|
||||||
class SummeryEditorTestCase(unittest.TestCase):
|
class SummeryHelperTestCase(unittest.TestCase):
|
||||||
"""The summary editor test case."""
|
"""The summary helper test case."""
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
"""Sets up the test.
|
"""Sets up the test.
|
||||||
@ -61,101 +61,101 @@ class SummeryEditorTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
self.client, self.csrf_token = get_client(self.app, "editor")
|
self.client, self.csrf_token = get_client(self.app, "editor")
|
||||||
|
|
||||||
def test_summary_editor(self) -> None:
|
def test_summary_helper(self) -> None:
|
||||||
"""Test the summary editor.
|
"""Test the summary helper.
|
||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.transaction.summary_editor import SummaryEditor
|
from accounting.transaction.summary_helper import SummaryHelper
|
||||||
for form in get_form_data(self.csrf_token):
|
for form in get_form_data(self.csrf_token):
|
||||||
add_txn(self.client, form)
|
add_txn(self.client, form)
|
||||||
with self.app.app_context():
|
with self.app.app_context():
|
||||||
editor: SummaryEditor = SummaryEditor()
|
helper: SummaryHelper = SummaryHelper()
|
||||||
|
|
||||||
# Debit-General
|
# Debit-General
|
||||||
self.assertEqual(len(editor.debit.general.tags), 2)
|
self.assertEqual(len(helper.debit.general.tags), 2)
|
||||||
self.assertEqual(editor.debit.general.tags[0].name, "Lunch")
|
self.assertEqual(helper.debit.general.tags[0].name, "Lunch")
|
||||||
self.assertEqual(len(editor.debit.general.tags[0].accounts), 2)
|
self.assertEqual(len(helper.debit.general.tags[0].accounts), 2)
|
||||||
self.assertEqual(editor.debit.general.tags[0].accounts[0].code,
|
self.assertEqual(helper.debit.general.tags[0].accounts[0].code,
|
||||||
Accounts.MEAL)
|
Accounts.MEAL)
|
||||||
self.assertEqual(editor.debit.general.tags[0].accounts[1].code,
|
self.assertEqual(helper.debit.general.tags[0].accounts[1].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PAYABLE)
|
||||||
self.assertEqual(editor.debit.general.tags[1].name, "Dinner")
|
self.assertEqual(helper.debit.general.tags[1].name, "Dinner")
|
||||||
self.assertEqual(len(editor.debit.general.tags[1].accounts), 2)
|
self.assertEqual(len(helper.debit.general.tags[1].accounts), 2)
|
||||||
self.assertEqual(editor.debit.general.tags[1].accounts[0].code,
|
self.assertEqual(helper.debit.general.tags[1].accounts[0].code,
|
||||||
Accounts.MEAL)
|
Accounts.MEAL)
|
||||||
self.assertEqual(editor.debit.general.tags[1].accounts[1].code,
|
self.assertEqual(helper.debit.general.tags[1].accounts[1].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PAYABLE)
|
||||||
|
|
||||||
# Debit-Travel
|
# Debit-Travel
|
||||||
self.assertEqual(len(editor.debit.travel.tags), 3)
|
self.assertEqual(len(helper.debit.travel.tags), 3)
|
||||||
self.assertEqual(editor.debit.travel.tags[0].name, "Bike")
|
self.assertEqual(helper.debit.travel.tags[0].name, "Bike")
|
||||||
self.assertEqual(len(editor.debit.travel.tags[0].accounts), 1)
|
self.assertEqual(len(helper.debit.travel.tags[0].accounts), 1)
|
||||||
self.assertEqual(editor.debit.travel.tags[0].accounts[0].code,
|
self.assertEqual(helper.debit.travel.tags[0].accounts[0].code,
|
||||||
Accounts.TRAVEL)
|
Accounts.TRAVEL)
|
||||||
self.assertEqual(editor.debit.travel.tags[1].name, "Taxi")
|
self.assertEqual(helper.debit.travel.tags[1].name, "Taxi")
|
||||||
self.assertEqual(len(editor.debit.travel.tags[1].accounts), 1)
|
self.assertEqual(len(helper.debit.travel.tags[1].accounts), 1)
|
||||||
self.assertEqual(editor.debit.travel.tags[1].accounts[0].code,
|
self.assertEqual(helper.debit.travel.tags[1].accounts[0].code,
|
||||||
Accounts.TRAVEL)
|
Accounts.TRAVEL)
|
||||||
self.assertEqual(editor.debit.travel.tags[2].name, "Airplane")
|
self.assertEqual(helper.debit.travel.tags[2].name, "Airplane")
|
||||||
self.assertEqual(len(editor.debit.travel.tags[2].accounts), 1)
|
self.assertEqual(len(helper.debit.travel.tags[2].accounts), 1)
|
||||||
self.assertEqual(editor.debit.travel.tags[2].accounts[0].code,
|
self.assertEqual(helper.debit.travel.tags[2].accounts[0].code,
|
||||||
Accounts.TRAVEL)
|
Accounts.TRAVEL)
|
||||||
|
|
||||||
# Debit-Bus
|
# Debit-Bus
|
||||||
self.assertEqual(len(editor.debit.bus.tags), 2)
|
self.assertEqual(len(helper.debit.bus.tags), 2)
|
||||||
self.assertEqual(editor.debit.bus.tags[0].name, "Train")
|
self.assertEqual(helper.debit.bus.tags[0].name, "Train")
|
||||||
self.assertEqual(len(editor.debit.bus.tags[0].accounts), 1)
|
self.assertEqual(len(helper.debit.bus.tags[0].accounts), 1)
|
||||||
self.assertEqual(editor.debit.bus.tags[0].accounts[0].code,
|
self.assertEqual(helper.debit.bus.tags[0].accounts[0].code,
|
||||||
Accounts.TRAVEL)
|
Accounts.TRAVEL)
|
||||||
self.assertEqual(editor.debit.bus.tags[1].name, "Bus")
|
self.assertEqual(helper.debit.bus.tags[1].name, "Bus")
|
||||||
self.assertEqual(len(editor.debit.bus.tags[1].accounts), 1)
|
self.assertEqual(len(helper.debit.bus.tags[1].accounts), 1)
|
||||||
self.assertEqual(editor.debit.bus.tags[1].accounts[0].code,
|
self.assertEqual(helper.debit.bus.tags[1].accounts[0].code,
|
||||||
Accounts.TRAVEL)
|
Accounts.TRAVEL)
|
||||||
|
|
||||||
# Credit-General
|
# Credit-General
|
||||||
self.assertEqual(len(editor.credit.general.tags), 2)
|
self.assertEqual(len(helper.credit.general.tags), 2)
|
||||||
self.assertEqual(editor.credit.general.tags[0].name, "Lunch")
|
self.assertEqual(helper.credit.general.tags[0].name, "Lunch")
|
||||||
self.assertEqual(len(editor.credit.general.tags[0].accounts), 3)
|
self.assertEqual(len(helper.credit.general.tags[0].accounts), 3)
|
||||||
self.assertEqual(editor.credit.general.tags[0].accounts[0].code,
|
self.assertEqual(helper.credit.general.tags[0].accounts[0].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PAYABLE)
|
||||||
self.assertEqual(editor.credit.general.tags[0].accounts[1].code,
|
self.assertEqual(helper.credit.general.tags[0].accounts[1].code,
|
||||||
Accounts.BANK)
|
Accounts.BANK)
|
||||||
self.assertEqual(editor.credit.general.tags[0].accounts[2].code,
|
self.assertEqual(helper.credit.general.tags[0].accounts[2].code,
|
||||||
Accounts.CASH)
|
Accounts.CASH)
|
||||||
self.assertEqual(editor.credit.general.tags[1].name, "Dinner")
|
self.assertEqual(helper.credit.general.tags[1].name, "Dinner")
|
||||||
self.assertEqual(len(editor.credit.general.tags[1].accounts), 2)
|
self.assertEqual(len(helper.credit.general.tags[1].accounts), 2)
|
||||||
self.assertEqual(editor.credit.general.tags[1].accounts[0].code,
|
self.assertEqual(helper.credit.general.tags[1].accounts[0].code,
|
||||||
Accounts.BANK)
|
Accounts.BANK)
|
||||||
self.assertEqual(editor.credit.general.tags[1].accounts[1].code,
|
self.assertEqual(helper.credit.general.tags[1].accounts[1].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PAYABLE)
|
||||||
|
|
||||||
# Credit-Travel
|
# Credit-Travel
|
||||||
self.assertEqual(len(editor.credit.travel.tags), 2)
|
self.assertEqual(len(helper.credit.travel.tags), 2)
|
||||||
self.assertEqual(editor.credit.travel.tags[0].name, "Bike")
|
self.assertEqual(helper.credit.travel.tags[0].name, "Bike")
|
||||||
self.assertEqual(len(editor.credit.travel.tags[0].accounts), 2)
|
self.assertEqual(len(helper.credit.travel.tags[0].accounts), 2)
|
||||||
self.assertEqual(editor.credit.travel.tags[0].accounts[0].code,
|
self.assertEqual(helper.credit.travel.tags[0].accounts[0].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PAYABLE)
|
||||||
self.assertEqual(editor.credit.travel.tags[0].accounts[1].code,
|
self.assertEqual(helper.credit.travel.tags[0].accounts[1].code,
|
||||||
Accounts.PREPAID)
|
Accounts.PREPAID)
|
||||||
self.assertEqual(editor.credit.travel.tags[1].name, "Taxi")
|
self.assertEqual(helper.credit.travel.tags[1].name, "Taxi")
|
||||||
self.assertEqual(len(editor.credit.travel.tags[1].accounts), 2)
|
self.assertEqual(len(helper.credit.travel.tags[1].accounts), 2)
|
||||||
self.assertEqual(editor.credit.travel.tags[1].accounts[0].code,
|
self.assertEqual(helper.credit.travel.tags[1].accounts[0].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PAYABLE)
|
||||||
self.assertEqual(editor.credit.travel.tags[1].accounts[1].code,
|
self.assertEqual(helper.credit.travel.tags[1].accounts[1].code,
|
||||||
Accounts.CASH)
|
Accounts.CASH)
|
||||||
|
|
||||||
# Credit-Bus
|
# Credit-Bus
|
||||||
self.assertEqual(len(editor.credit.bus.tags), 2)
|
self.assertEqual(len(helper.credit.bus.tags), 2)
|
||||||
self.assertEqual(editor.credit.bus.tags[0].name, "Train")
|
self.assertEqual(helper.credit.bus.tags[0].name, "Train")
|
||||||
self.assertEqual(len(editor.credit.bus.tags[0].accounts), 2)
|
self.assertEqual(len(helper.credit.bus.tags[0].accounts), 2)
|
||||||
self.assertEqual(editor.credit.bus.tags[0].accounts[0].code,
|
self.assertEqual(helper.credit.bus.tags[0].accounts[0].code,
|
||||||
Accounts.PREPAID)
|
Accounts.PREPAID)
|
||||||
self.assertEqual(editor.credit.bus.tags[0].accounts[1].code,
|
self.assertEqual(helper.credit.bus.tags[0].accounts[1].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PAYABLE)
|
||||||
self.assertEqual(editor.credit.bus.tags[1].name, "Bus")
|
self.assertEqual(helper.credit.bus.tags[1].name, "Bus")
|
||||||
self.assertEqual(len(editor.credit.bus.tags[1].accounts), 1)
|
self.assertEqual(len(helper.credit.bus.tags[1].accounts), 1)
|
||||||
self.assertEqual(editor.credit.bus.tags[1].accounts[0].code,
|
self.assertEqual(helper.credit.bus.tags[1].accounts[0].code,
|
||||||
Accounts.PREPAID)
|
Accounts.PREPAID)
|
||||||
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user