Compare commits
	
		
			29 Commits
		
	
	
		
			9cd9e90be0
			...
			c50b9a2000
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c50b9a2000 | |||
| af9bd14eed | |||
| 9e1ff16e96 | |||
| f7c1fd77f2 | |||
| 641315537d | |||
| a895bd8560 | |||
| ca86a08f3e | |||
| e118422441 | |||
| b3777cffbf | |||
| 39c9c17007 | |||
| 3ab4eacf9f | |||
| cff3d1b6bd | |||
| f41db78831 | |||
| 73f7d14e7b | |||
| f6ed6b10a7 | |||
| b5aaee4d15 | |||
| c849d6b3d4 | |||
| a9908a7df4 | |||
| 063c769158 | |||
| f8e9871300 | |||
| 78a62a9575 | |||
| 85fde6219e | |||
| 4eb9346d8d | |||
| 11966a52ba | |||
| 8cf81b5459 | |||
| cc958a39b3 | |||
| 9065686cc5 | |||
| 9a41cb10a1 | |||
| 6957e52d0d | 
| @@ -58,12 +58,25 @@ def init_app(app: Flask, user_utils: AbstractUserUtils, | ||||
|                               template_folder="templates", | ||||
|                               static_folder="static") | ||||
|  | ||||
|     from .template_filters import format_amount, format_date | ||||
|     bp.add_app_template_filter(format_amount, "accounting_format_amount") | ||||
|     bp.add_app_template_filter(format_date, "accounting_format_date") | ||||
|  | ||||
|     from .template_globals import currency_options, default_currency_code | ||||
|     bp.add_app_template_global(currency_options, | ||||
|                                "accounting_currency_options") | ||||
|     bp.add_app_template_global(default_currency_code, | ||||
|                                "accounting_default_currency_code") | ||||
|  | ||||
|     from . import locale | ||||
|     locale.init_app(app, bp) | ||||
|  | ||||
|     from .utils import permission | ||||
|     permission.init_app(bp, can_view_func, can_edit_func) | ||||
|  | ||||
|     from .utils import next_uri | ||||
|     next_uri.init_app(bp) | ||||
|  | ||||
|     from . import base_account | ||||
|     base_account.init_app(app, bp) | ||||
|  | ||||
| @@ -76,7 +89,4 @@ def init_app(app: Flask, user_utils: AbstractUserUtils, | ||||
|     from . import transaction | ||||
|     transaction.init_app(app, bp) | ||||
|  | ||||
|     from .utils import next_uri | ||||
|     next_uri.init_app(bp) | ||||
|  | ||||
|     app.register_blueprint(bp) | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
| #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| #  See the License for the specific language governing permissions and | ||||
| #  limitations under the License. | ||||
| """The account query. | ||||
| """The queries for the account management. | ||||
| 
 | ||||
| """ | ||||
| import sqlalchemy as sa | ||||
| @@ -33,7 +33,7 @@ from accounting.utils.pagination import Pagination | ||||
| from accounting.utils.permission import can_view, has_permission, can_edit | ||||
| from accounting.utils.user import get_current_user_pk | ||||
| from .forms import AccountForm, sort_accounts_in, AccountReorderForm | ||||
| from .query import get_account_query | ||||
| from .queries import get_account_query | ||||
|  | ||||
| bp: Blueprint = Blueprint("account", __name__) | ||||
| """The view blueprint for the account management.""" | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
| #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| #  See the License for the specific language governing permissions and | ||||
| #  limitations under the License. | ||||
| """The base account query. | ||||
| """The queries for the base account management. | ||||
| 
 | ||||
| """ | ||||
| import sqlalchemy as sa | ||||
| @@ -34,7 +34,7 @@ def list_accounts() -> str: | ||||
|  | ||||
|     :return: The account list. | ||||
|     """ | ||||
|     from .query import get_base_account_query | ||||
|     from .queries import get_base_account_query | ||||
|     accounts: list[BaseAccount] = get_base_account_query() | ||||
|     pagination: Pagination = Pagination[BaseAccount](accounts) | ||||
|     return render_template("accounting/base-account/list.html", | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
| #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| #  See the License for the specific language governing permissions and | ||||
| #  limitations under the License. | ||||
| """The currency query. | ||||
| """The queries for the currency management. | ||||
| 
 | ||||
| """ | ||||
| import sqlalchemy as sa | ||||
| @@ -47,7 +47,7 @@ def list_currencies() -> str: | ||||
|  | ||||
|     :return: The currency list. | ||||
|     """ | ||||
|     from .query import get_currency_query | ||||
|     from .queries import get_currency_query | ||||
|     currencies: list[Currency] = get_currency_query() | ||||
|     pagination: Pagination = Pagination[Currency](currencies) | ||||
|     return render_template("accounting/currency/list.html", | ||||
|   | ||||
| @@ -22,7 +22,7 @@ | ||||
|  */ | ||||
|  | ||||
| // Initializes the page JavaScript. | ||||
| document.addEventListener("DOMContentLoaded", function () { | ||||
| document.addEventListener("DOMContentLoaded", () => { | ||||
|     initializeBaseAccountSelector(); | ||||
|     document.getElementById("accounting-base-code") | ||||
|         .onchange = validateBase; | ||||
| @@ -44,7 +44,7 @@ function initializeBaseAccountSelector() { | ||||
|     const baseContent = document.getElementById("accounting-base-content"); | ||||
|     const options = Array.from(document.getElementsByClassName("accounting-base-option")); | ||||
|     const btnClear = document.getElementById("accounting-btn-clear-base"); | ||||
|     selector.addEventListener("show.bs.modal", function () { | ||||
|     selector.addEventListener("show.bs.modal", () => { | ||||
|         base.classList.add("accounting-not-empty"); | ||||
|         for (const option of options) { | ||||
|             option.classList.remove("active"); | ||||
| @@ -54,13 +54,13 @@ function initializeBaseAccountSelector() { | ||||
|             selected.classList.add("active"); | ||||
|         } | ||||
|     }); | ||||
|     selector.addEventListener("hidden.bs.modal", function () { | ||||
|     selector.addEventListener("hidden.bs.modal", () => { | ||||
|         if (baseCode.value === "") { | ||||
|             base.classList.remove("accounting-not-empty"); | ||||
|         } | ||||
|     }); | ||||
|     for (const option of options) { | ||||
|         option.onclick = function () { | ||||
|         option.onclick = () => { | ||||
|             baseCode.value = option.dataset.code; | ||||
|             baseContent.innerText = option.dataset.content; | ||||
|             btnClear.classList.add("btn-danger"); | ||||
| @@ -70,7 +70,7 @@ function initializeBaseAccountSelector() { | ||||
|             bootstrap.Modal.getInstance(selector).hide(); | ||||
|         }; | ||||
|     } | ||||
|     btnClear.onclick = function () { | ||||
|     btnClear.onclick = () => { | ||||
|         baseCode.value = ""; | ||||
|         baseContent.innerText = ""; | ||||
|         btnClear.classList.add("btn-secondary") | ||||
| @@ -92,7 +92,7 @@ function initializeBaseAccountQuery() { | ||||
|     const optionList = document.getElementById("accounting-base-option-list"); | ||||
|     const options = Array.from(document.getElementsByClassName("accounting-base-option")); | ||||
|     const queryNoResult = document.getElementById("accounting-base-option-no-result"); | ||||
|     query.addEventListener("input", function () { | ||||
|     query.addEventListener("input", () => { | ||||
|         if (query.value === "") { | ||||
|             for (const option of options) { | ||||
|                 option.classList.remove("d-none"); | ||||
|   | ||||
| @@ -22,10 +22,10 @@ | ||||
|  */ | ||||
|  | ||||
| // Initializes the page JavaScript. | ||||
| document.addEventListener("DOMContentLoaded", function () { | ||||
| document.addEventListener("DOMContentLoaded", () => { | ||||
|     const list = document.getElementById("accounting-order-list"); | ||||
|     if (list !== null) { | ||||
|         const onReorder = function () { | ||||
|         const onReorder = () => { | ||||
|             const accounts = Array.from(list.children); | ||||
|             for (let i = 0; i < accounts.length; i++) { | ||||
|                 const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no"); | ||||
|   | ||||
| @@ -22,7 +22,7 @@ | ||||
|  */ | ||||
|  | ||||
| // Initializes the page JavaScript. | ||||
| document.addEventListener("DOMContentLoaded", function () { | ||||
| document.addEventListener("DOMContentLoaded", () => { | ||||
|     AccountSelector.initialize(); | ||||
| }); | ||||
|  | ||||
| @@ -65,13 +65,12 @@ class AccountSelector { | ||||
|         const more = document.getElementById(this.#prefix + "-more"); | ||||
|         const btnClear = document.getElementById(this.#prefix + "-btn-clear"); | ||||
|         const options = Array.from(document.getElementsByClassName(this.#prefix + "-option")); | ||||
|         const selector1 = this | ||||
|         more.onclick = function () { | ||||
|         more.onclick = () => { | ||||
|             more.classList.add("d-none"); | ||||
|             selector1.#filterAccountOptions(); | ||||
|             this.#filterAccountOptions(); | ||||
|         }; | ||||
|         this.#initializeAccountQuery(); | ||||
|         btnClear.onclick = function () { | ||||
|         btnClear.onclick = () => { | ||||
|             formAccountControl.classList.remove("accounting-not-empty"); | ||||
|             formAccount.innerText = ""; | ||||
|             formAccount.dataset.code = ""; | ||||
| @@ -79,7 +78,7 @@ class AccountSelector { | ||||
|             validateJournalEntryAccount(); | ||||
|         }; | ||||
|         for (const option of options) { | ||||
|             option.onclick = function () { | ||||
|             option.onclick = () => { | ||||
|                 formAccountControl.classList.add("accounting-not-empty"); | ||||
|                 formAccount.innerText = option.dataset.content; | ||||
|                 formAccount.dataset.code = option.dataset.code; | ||||
| @@ -95,9 +94,8 @@ class AccountSelector { | ||||
|      */ | ||||
|     #initializeAccountQuery() { | ||||
|         const query = document.getElementById(this.#prefix + "-query"); | ||||
|         const helper = this; | ||||
|         query.addEventListener("input", function () { | ||||
|             helper.#filterAccountOptions(); | ||||
|         query.addEventListener("input", () => { | ||||
|             this.#filterAccountOptions(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| @@ -159,7 +157,7 @@ class AccountSelector { | ||||
|      * @return {boolean} true if the account option should show, or false otherwise | ||||
|      */ | ||||
|     #shouldAccountOptionShow(option, more, inUse, query) { | ||||
|         const isQueryMatched = function () { | ||||
|         const isQueryMatched = () => { | ||||
|             if (query.value === "") { | ||||
|                 return true; | ||||
|             } | ||||
| @@ -171,7 +169,7 @@ class AccountSelector { | ||||
|             } | ||||
|             return false; | ||||
|         }; | ||||
|         const isMoreMatched = function () { | ||||
|         const isMoreMatched = () => { | ||||
|             if (more.classList.contains("d-none")) { | ||||
|                 return true; | ||||
|             } | ||||
| @@ -237,10 +235,7 @@ class AccountSelector { | ||||
|     static #initializeTransactionForm() { | ||||
|         const entryForm = document.getElementById("accounting-entry-form"); | ||||
|         const formAccountControl = document.getElementById("accounting-entry-form-account-control"); | ||||
|         const selectors = this.#selectors; | ||||
|         formAccountControl.onclick = function () { | ||||
|             selectors[entryForm.dataset.entryType].initShow(); | ||||
|         }; | ||||
|         formAccountControl.onclick = () => this.#selectors[entryForm.dataset.entryType].initShow(); | ||||
|     } | ||||
|     /** | ||||
|      * Initializes the account selector for the journal entry form. | ||||
|   | ||||
| @@ -22,7 +22,7 @@ | ||||
|  */ | ||||
|  | ||||
| // Initializes the page JavaScript. | ||||
| document.addEventListener("DOMContentLoaded", function () { | ||||
| document.addEventListener("DOMContentLoaded", () => { | ||||
|     document.getElementById("accounting-code") | ||||
|         .onchange = validateCode; | ||||
|     document.getElementById("accounting-name") | ||||
|   | ||||
| @@ -44,15 +44,15 @@ function initializeMouseDragAndDropReordering(list, onReorder) { | ||||
|     let dragged = null; | ||||
|     for (const item of items) { | ||||
|         item.draggable = true; | ||||
|         item.addEventListener("dragstart", function () { | ||||
|         item.addEventListener("dragstart", () => { | ||||
|             dragged = item; | ||||
|             dragged.classList.add("accounting-dragged"); | ||||
|         }); | ||||
|         item.addEventListener("dragover", function () { | ||||
|         item.addEventListener("dragover", () => { | ||||
|             onDragOver(dragged, item); | ||||
|             onReorder(); | ||||
|         }); | ||||
|         item.addEventListener("dragend", function () { | ||||
|         item.addEventListener("dragend", () => { | ||||
|             dragged.classList.remove("accounting-dragged"); | ||||
|             dragged = null; | ||||
|         }); | ||||
| @@ -69,16 +69,16 @@ function initializeMouseDragAndDropReordering(list, onReorder) { | ||||
| function initializeTouchDragAndDropReordering(list, onReorder) { | ||||
|     const items = Array.from(list.children); | ||||
|     for (const item of items) { | ||||
|         item.addEventListener("touchstart", function () { | ||||
|         item.addEventListener("touchstart", () => { | ||||
|             item.classList.add("accounting-dragged"); | ||||
|         }); | ||||
|         item.addEventListener("touchmove", function (event) { | ||||
|         item.addEventListener("touchmove", (event) => { | ||||
|             const touch = event.targetTouches[0]; | ||||
|             const target = document.elementFromPoint(touch.pageX, touch.pageY); | ||||
|             onDragOver(item, target); | ||||
|             onReorder(); | ||||
|         }); | ||||
|         item.addEventListener("touchend", function () { | ||||
|         item.addEventListener("touchend", () => { | ||||
|             item.classList.remove("accounting-dragged"); | ||||
|         }); | ||||
|     } | ||||
|   | ||||
| @@ -22,7 +22,7 @@ | ||||
|  */ | ||||
|  | ||||
| // Initializes the page JavaScript. | ||||
| document.addEventListener("DOMContentLoaded", function () { | ||||
| document.addEventListener("DOMContentLoaded", () => { | ||||
|     initializeMaterialFabSpeedDial(); | ||||
| }); | ||||
|  | ||||
| @@ -34,7 +34,7 @@ document.addEventListener("DOMContentLoaded", function () { | ||||
| function initializeMaterialFabSpeedDial() { | ||||
|     const btnFab = document.getElementById("accounting-btn-material-fab-speed-dial"); | ||||
|     const fab = document.getElementById(btnFab.dataset.target); | ||||
|     btnFab.onclick = function () { | ||||
|     btnFab.onclick = () => { | ||||
|         if (fab.classList.contains("show")) { | ||||
|             fab.classList.remove("show"); | ||||
|         } else { | ||||
|   | ||||
							
								
								
									
										1204
									
								
								src/accounting/static/js/summary-editor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1204
									
								
								src/accounting/static/js/summary-editor.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,827 +0,0 @@ | ||||
| /* The Mia! Accounting Flask Project | ||||
|  * summary-helper.js: The JavaScript for the summary helper | ||||
|  */ | ||||
|  | ||||
| /*  Copyright (c) 2023 imacat. | ||||
|  * | ||||
|  *  Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  *  you may not use this file except in compliance with the License. | ||||
|  *  You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  *  Unless required by applicable law or agreed to in writing, software | ||||
|  *  distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  *  See the License for the specific language governing permissions and | ||||
|  *  limitations under the License. | ||||
|  */ | ||||
|  | ||||
| /* Author: imacat@mail.imacat.idv.tw (imacat) | ||||
|  * First written: 2023/2/28 | ||||
|  */ | ||||
|  | ||||
| // Initializes the page JavaScript. | ||||
| document.addEventListener("DOMContentLoaded", function () { | ||||
|     SummaryHelper.initialize(); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * A summary helper. | ||||
|  * | ||||
|  */ | ||||
| class SummaryHelper { | ||||
|  | ||||
|     /** | ||||
|      * The entry type, either "debit" or "credit" | ||||
|      * @type {string} | ||||
|      */ | ||||
|     #entryType; | ||||
|  | ||||
|     /** | ||||
|      * The prefix of the HTML ID and class | ||||
|      * @type {string} | ||||
|      */ | ||||
|     #prefix; | ||||
|  | ||||
|     /** | ||||
|      * The default tab ID | ||||
|      * @type {string} | ||||
|      */ | ||||
|     #defaultTabId; | ||||
|  | ||||
|     /** | ||||
|      * Constructs a summary helper. | ||||
|      * | ||||
|      * @param form {HTMLFormElement} the summary helper form | ||||
|      */ | ||||
|     constructor(form) { | ||||
|         this.#entryType = form.dataset.entryType; | ||||
|         this.#prefix = "accounting-summary-helper-" + form.dataset.entryType; | ||||
|         this.#defaultTabId = form.dataset.defaultTabId; | ||||
|         this.#init(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the summary helper. | ||||
|      * | ||||
|      */ | ||||
|     #init() { | ||||
|         const helper = this; | ||||
|         const summary = document.getElementById(this.#prefix + "-summary"); | ||||
|         const tabs = Array.from(document.getElementsByClassName(this.#prefix + "-tab")); | ||||
|         for (const tab of tabs) { | ||||
|             tab.onclick = function () { | ||||
|                 helper.#switchToTab(tab.dataset.tabId); | ||||
|             } | ||||
|         } | ||||
|         this.#initializeGeneralTagHelper(); | ||||
|         this.#initializeGeneralTripHelper(); | ||||
|         this.#initializeBusTripHelper(); | ||||
|         this.#initializeNumberHelper(); | ||||
|         this.#initializeSuggestedAccounts(); | ||||
|         this.#initializeSubmission(); | ||||
|         summary.onchange = function () { | ||||
|             summary.value = summary.value.trim(); | ||||
|             helper.#parseAndPopulate(); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Switches to a tab. | ||||
|      * | ||||
|      * @param tabId {string} the tab ID. | ||||
|      */ | ||||
|     #switchToTab(tabId) { | ||||
|         const tabs = Array.from(document.getElementsByClassName(this.#prefix + "-tab")); | ||||
|         const pages = Array.from(document.getElementsByClassName(this.#prefix + "-page")); | ||||
|         const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-" + tabId + "-btn-tag")); | ||||
|         for (const tab of tabs) { | ||||
|             if (tab.dataset.tabId === tabId) { | ||||
|                 tab.classList.add("active"); | ||||
|                 tab.ariaCurrent = "page"; | ||||
|             } else { | ||||
|                 tab.classList.remove("active"); | ||||
|                 tab.ariaCurrent = "false"; | ||||
|             } | ||||
|         } | ||||
|         for (const page of pages) { | ||||
|             if (page.dataset.tabId === tabId) { | ||||
|                 page.classList.remove("d-none"); | ||||
|                 page.ariaCurrent = "page"; | ||||
|             } else { | ||||
|                 page.classList.add("d-none"); | ||||
|                 page.ariaCurrent = "false"; | ||||
|             } | ||||
|         } | ||||
|         let selectedBtnTag = null; | ||||
|         for (const tagButton of tagButtons) { | ||||
|             if (tagButton.classList.contains("btn-primary")) { | ||||
|                 selectedBtnTag = tagButton; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         this.#filterSuggestedAccounts(selectedBtnTag); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the general tag helper. | ||||
|      * | ||||
|      */ | ||||
|     #initializeGeneralTagHelper() { | ||||
|         const summary = document.getElementById(this.#prefix + "-summary"); | ||||
|         const tag = document.getElementById(this.#prefix + "-general-tag"); | ||||
|         const helper = this; | ||||
|         const updateSummary = function () { | ||||
|             const pos = summary.value.indexOf("—"); | ||||
|             const prefix = tag.value === ""? "": tag.value + "—"; | ||||
|             if (pos === -1) { | ||||
|                 summary.value = prefix + summary.value; | ||||
|             } else { | ||||
|                 summary.value = prefix + summary.value.substring(pos + 1); | ||||
|             } | ||||
|         } | ||||
|         this.#initializeTagButtons("general", tag, updateSummary); | ||||
|         tag.onchange = function () { | ||||
|             helper.#onTagInputChange("general", tag, updateSummary); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the general trip helper. | ||||
|      * | ||||
|      */ | ||||
|     #initializeGeneralTripHelper() { | ||||
|         const summary = document.getElementById(this.#prefix + "-summary"); | ||||
|         const tag = document.getElementById(this.#prefix + "-travel-tag"); | ||||
|         const from = document.getElementById(this.#prefix + "-travel-from"); | ||||
|         const directionButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-direction")) | ||||
|         const to = document.getElementById(this.#prefix + "-travel-to"); | ||||
|         const helper = this; | ||||
|         const updateSummary = function () { | ||||
|             let direction; | ||||
|             for (const directionButton of directionButtons) { | ||||
|                 if (directionButton.classList.contains("btn-primary")) { | ||||
|                     direction = directionButton.dataset.arrow; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|             summary.value = tag.value + "—" + from.value + direction + to.value; | ||||
|         }; | ||||
|         this.#initializeTagButtons("travel", tag, updateSummary); | ||||
|         tag.onchange = function () { | ||||
|             helper.#onTagInputChange("travel", tag, updateSummary); | ||||
|             helper.#validateGeneralTripTag(); | ||||
|         }; | ||||
|         from.onchange = function () { | ||||
|             updateSummary(); | ||||
|             helper.#validateGeneralTripFrom(); | ||||
|         }; | ||||
|         for (const directionButton of directionButtons) { | ||||
|             directionButton.onclick = function () { | ||||
|                 for (const otherButton of directionButtons) { | ||||
|                     otherButton.classList.remove("btn-primary"); | ||||
|                     otherButton.classList.add("btn-outline-primary"); | ||||
|                 } | ||||
|                 directionButton.classList.remove("btn-outline-primary"); | ||||
|                 directionButton.classList.add("btn-primary"); | ||||
|                 updateSummary(); | ||||
|             }; | ||||
|         } | ||||
|         to.onchange = function () { | ||||
|             updateSummary(); | ||||
|             helper.#validateGeneralTripTo(); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the bus trip helper. | ||||
|      * | ||||
|      */ | ||||
|     #initializeBusTripHelper() { | ||||
|         const summary = document.getElementById(this.#prefix + "-summary"); | ||||
|         const tag = document.getElementById(this.#prefix + "-bus-tag"); | ||||
|         const route = document.getElementById(this.#prefix + "-bus-route"); | ||||
|         const from = document.getElementById(this.#prefix + "-bus-from"); | ||||
|         const to = document.getElementById(this.#prefix + "-bus-to"); | ||||
|         const helper = this; | ||||
|         const updateSummary = function () { | ||||
|             summary.value = tag.value + "—" + route.value + "—" + from.value + "→" + to.value; | ||||
|         }; | ||||
|         this.#initializeTagButtons("bus", tag, updateSummary); | ||||
|         tag.onchange = function () { | ||||
|             helper.#onTagInputChange("bus", tag, updateSummary); | ||||
|             helper.#validateBusTripTag(); | ||||
|         }; | ||||
|         route.onchange = function () { | ||||
|             updateSummary(); | ||||
|             helper.#validateBusTripRoute(); | ||||
|         }; | ||||
|         from.onchange = function () { | ||||
|             updateSummary(); | ||||
|             helper.#validateBusTripFrom(); | ||||
|         }; | ||||
|         to.onchange = function () { | ||||
|             updateSummary(); | ||||
|             helper.#validateBusTripTo(); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the tag buttons. | ||||
|      * | ||||
|      * @param tabId {string} the tab ID | ||||
|      * @param tag {HTMLInputElement} the tag input | ||||
|      * @param updateSummary {function(): void} the callback to update the summary | ||||
|      */ | ||||
|     #initializeTagButtons(tabId, tag, updateSummary) { | ||||
|         const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-" + tabId + "-btn-tag")); | ||||
|         const helper = this; | ||||
|         for (const tagButton of tagButtons) { | ||||
|             tagButton.onclick = function () { | ||||
|                 for (const otherButton of tagButtons) { | ||||
|                     otherButton.classList.remove("btn-primary"); | ||||
|                     otherButton.classList.add("btn-outline-primary"); | ||||
|                 } | ||||
|                 tagButton.classList.remove("btn-outline-primary"); | ||||
|                 tagButton.classList.add("btn-primary"); | ||||
|                 tag.value = tagButton.dataset.value; | ||||
|                 helper.#filterSuggestedAccounts(tagButton); | ||||
|                 updateSummary(); | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * The callback when the tag input is changed. | ||||
|      * | ||||
|      * @param tabId {string} the tab ID | ||||
|      * @param tag {HTMLInputElement} the tag input | ||||
|      * @param updateSummary {function(): void} the callback to update the summary | ||||
|      */ | ||||
|     #onTagInputChange(tabId, tag, updateSummary) { | ||||
|         const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-" + tabId + "-btn-tag")); | ||||
|         let isMatched = false; | ||||
|         for (const tagButton of tagButtons) { | ||||
|             if (tagButton.dataset.value === tag.value) { | ||||
|                 tagButton.classList.remove("btn-outline-primary"); | ||||
|                 tagButton.classList.add("btn-primary"); | ||||
|                 this.#filterSuggestedAccounts(tagButton); | ||||
|                 isMatched = true; | ||||
|             } else { | ||||
|                 tagButton.classList.remove("btn-primary"); | ||||
|                 tagButton.classList.add("btn-outline-primary"); | ||||
|             } | ||||
|         } | ||||
|         if (!isMatched) { | ||||
|             this.#filterSuggestedAccounts(null); | ||||
|         } | ||||
|         updateSummary(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Filters the suggested accounts. | ||||
|      * | ||||
|      * @param tagButton {HTMLButtonElement|null} the tag button | ||||
|      */ | ||||
|     #filterSuggestedAccounts(tagButton) { | ||||
|         const accountButtons = Array.from(document.getElementsByClassName(this.#prefix + "-account")); | ||||
|         if (tagButton === null) { | ||||
|             for (const accountButton of accountButtons) { | ||||
|                 accountButton.classList.add("d-none"); | ||||
|                 accountButton.classList.remove("btn-primary"); | ||||
|                 accountButton.classList.add("btn-outline-primary"); | ||||
|                 this.#selectAccount(null); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|         const suggested = JSON.parse(tagButton.dataset.accounts); | ||||
|         for (const accountButton of accountButtons) { | ||||
|             if (suggested.includes(accountButton.dataset.code)) { | ||||
|                 accountButton.classList.remove("d-none"); | ||||
|             } else { | ||||
|                 accountButton.classList.add("d-none"); | ||||
|             } | ||||
|             this.#selectAccount(suggested[0]); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the number helper. | ||||
|      * | ||||
|      */ | ||||
|     #initializeNumberHelper() { | ||||
|         const summary = document.getElementById(this.#prefix + "-summary"); | ||||
|         const number = document.getElementById(this.#prefix + "-number"); | ||||
|         number.onchange = function () { | ||||
|             const found = summary.value.match(/^(.+)×(\d+)$/); | ||||
|             if (found !== null) { | ||||
|                 summary.value = found[1]; | ||||
|             } | ||||
|             if (number.value > 1) { | ||||
|                 summary.value = summary.value + "×" + String(number.value); | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the suggested accounts. | ||||
|      * | ||||
|      */ | ||||
|     #initializeSuggestedAccounts() { | ||||
|         const accountButtons = Array.from(document.getElementsByClassName(this.#prefix + "-account")); | ||||
|         const helper = this; | ||||
|         for (const accountButton of accountButtons) { | ||||
|             accountButton.onclick = function () { | ||||
|                 helper.#selectAccount(accountButton.dataset.code); | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Select a suggested account. | ||||
|      * | ||||
|      * @param selectedCode {string|null} the account code, or null to deselect the account | ||||
|      */ | ||||
|     #selectAccount(selectedCode) { | ||||
|         const form = document.getElementById(this.#prefix); | ||||
|         if (selectedCode === null) { | ||||
|             form.dataset.selectedAccountCode = ""; | ||||
|             form.dataset.selectedAccountText = ""; | ||||
|             return; | ||||
|         } | ||||
|         const accountButtons = Array.from(document.getElementsByClassName(this.#prefix + "-account")); | ||||
|         for (const accountButton of accountButtons) { | ||||
|             if (accountButton.dataset.code === selectedCode) { | ||||
|                 accountButton.classList.remove("btn-outline-primary"); | ||||
|                 accountButton.classList.add("btn-primary"); | ||||
|                 form.dataset.selectedAccountCode = accountButton.dataset.code; | ||||
|                 form.dataset.selectedAccountText = accountButton.dataset.text; | ||||
|             } else { | ||||
|                 accountButton.classList.remove("btn-primary"); | ||||
|                 accountButton.classList.add("btn-outline-primary"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the summary submission | ||||
|      * | ||||
|      */ | ||||
|     #initializeSubmission() { | ||||
|         const form = document.getElementById(this.#prefix); | ||||
|         const helper = this; | ||||
|         form.onsubmit = function () { | ||||
|             if (helper.#validate()) { | ||||
|                 helper.#submit(); | ||||
|             } | ||||
|             return false; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates the form. | ||||
|      * | ||||
|      * @return {boolean} true if valid, or false otherwise | ||||
|      */ | ||||
|     #validate() { | ||||
|         const tabs = Array.from(document.getElementsByClassName(this.#prefix + "-tab")); | ||||
|         let isValid = true; | ||||
|         for (const tab of tabs) { | ||||
|             if (tab.classList.contains("active")) { | ||||
|                 switch (tab.dataset.tabId) { | ||||
|                     case "general": | ||||
|                         isValid = this.#validateGeneralTag() && isValid; | ||||
|                         break; | ||||
|                     case "travel": | ||||
|                         isValid = this.#validateGeneralTrip() && isValid; | ||||
|                         break; | ||||
|                     case "bus": | ||||
|                         isValid = this.#validateBusTrip() && isValid; | ||||
|                         break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return isValid; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates a general tag. | ||||
|      * | ||||
|      * @return {boolean} true if valid, or false otherwise | ||||
|      */ | ||||
|     #validateGeneralTag() { | ||||
|         const field = document.getElementById(this.#prefix + "-general-tag"); | ||||
|         const error = document.getElementById(this.#prefix + "-general-tag-error"); | ||||
|         field.value = field.value.trim(); | ||||
|         field.classList.remove("is-invalid"); | ||||
|         error.innerText = ""; | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates a general trip. | ||||
|      * | ||||
|      * @return {boolean} true if valid, or false otherwise | ||||
|      */ | ||||
|     #validateGeneralTrip() { | ||||
|         let isValid = true; | ||||
|         isValid = this.#validateGeneralTripTag() && isValid; | ||||
|         isValid = this.#validateGeneralTripFrom() && isValid; | ||||
|         isValid = this.#validateGeneralTripTo() && isValid; | ||||
|         return isValid; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates the tag of a general trip. | ||||
|      * | ||||
|      * @return {boolean} true if valid, or false otherwise | ||||
|      */ | ||||
|     #validateGeneralTripTag() { | ||||
|         const field = document.getElementById(this.#prefix + "-travel-tag"); | ||||
|         const error = document.getElementById(this.#prefix + "-travel-tag-error"); | ||||
|         field.value = field.value.trim(); | ||||
|         if (field.value === "") { | ||||
|             field.classList.add("is-invalid"); | ||||
|             error.innerText = A_("Please fill in the tag."); | ||||
|             return false; | ||||
|         } | ||||
|         field.classList.remove("is-invalid"); | ||||
|         error.innerText = ""; | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates the origin of a general trip. | ||||
|      * | ||||
|      * @return {boolean} true if valid, or false otherwise | ||||
|      */ | ||||
|     #validateGeneralTripFrom() { | ||||
|         const field = document.getElementById(this.#prefix + "-travel-from"); | ||||
|         const error = document.getElementById(this.#prefix + "-travel-from-error"); | ||||
|         field.value = field.value.trim(); | ||||
|         if (field.value === "") { | ||||
|             field.classList.add("is-invalid"); | ||||
|             error.innerText = A_("Please fill in the origin."); | ||||
|             return false; | ||||
|         } | ||||
|         field.classList.remove("is-invalid"); | ||||
|         error.innerText = ""; | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates the destination of a general trip. | ||||
|      * | ||||
|      * @return {boolean} true if valid, or false otherwise | ||||
|      */ | ||||
|     #validateGeneralTripTo() { | ||||
|         const field = document.getElementById(this.#prefix + "-travel-to"); | ||||
|         const error = document.getElementById(this.#prefix + "-travel-to-error"); | ||||
|         field.value = field.value.trim(); | ||||
|         if (field.value === "") { | ||||
|             field.classList.add("is-invalid"); | ||||
|             error.innerText = A_("Please fill in the destination."); | ||||
|             return false; | ||||
|         } | ||||
|         field.classList.remove("is-invalid"); | ||||
|         error.innerText = ""; | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates a bus trip. | ||||
|      * | ||||
|      * @return {boolean} true if valid, or false otherwise | ||||
|      */ | ||||
|     #validateBusTrip() { | ||||
|         let isValid = true; | ||||
|         isValid = this.#validateBusTripTag() && isValid; | ||||
|         isValid = this.#validateBusTripRoute() && isValid; | ||||
|         isValid = this.#validateBusTripFrom() && isValid; | ||||
|         isValid = this.#validateBusTripTo() && isValid; | ||||
|         return isValid; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates the tag of a bus trip. | ||||
|      * | ||||
|      * @return {boolean} true if valid, or false otherwise | ||||
|      */ | ||||
|     #validateBusTripTag() { | ||||
|         const field = document.getElementById(this.#prefix + "-bus-tag"); | ||||
|         const error = document.getElementById(this.#prefix + "-bus-tag-error"); | ||||
|         field.value = field.value.trim(); | ||||
|         if (field.value === "") { | ||||
|             field.classList.add("is-invalid"); | ||||
|             error.innerText = A_("Please fill in the tag."); | ||||
|             return false; | ||||
|         } | ||||
|         field.classList.remove("is-invalid"); | ||||
|         error.innerText = ""; | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates the route of a bus trip. | ||||
|      * | ||||
|      * @return {boolean} true if valid, or false otherwise | ||||
|      */ | ||||
|     #validateBusTripRoute() { | ||||
|         const field = document.getElementById(this.#prefix + "-bus-route"); | ||||
|         const error = document.getElementById(this.#prefix + "-bus-route-error"); | ||||
|         field.value = field.value.trim(); | ||||
|         if (field.value === "") { | ||||
|             field.classList.add("is-invalid"); | ||||
|             error.innerText = A_("Please fill in the route."); | ||||
|             return false; | ||||
|         } | ||||
|         field.classList.remove("is-invalid"); | ||||
|         error.innerText = ""; | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates the origin of a bus trip. | ||||
|      * | ||||
|      * @return {boolean} true if valid, or false otherwise | ||||
|      */ | ||||
|     #validateBusTripFrom() { | ||||
|         const field = document.getElementById(this.#prefix + "-bus-from"); | ||||
|         const error = document.getElementById(this.#prefix + "-bus-from-error"); | ||||
|         field.value = field.value.trim(); | ||||
|         if (field.value === "") { | ||||
|             field.classList.add("is-invalid"); | ||||
|             error.innerText = A_("Please fill in the origin."); | ||||
|             return false; | ||||
|         } | ||||
|         field.classList.remove("is-invalid"); | ||||
|         error.innerText = ""; | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates the destination of a bus trip. | ||||
|      * | ||||
|      * @return {boolean} true if valid, or false otherwise | ||||
|      */ | ||||
|     #validateBusTripTo() { | ||||
|         const field = document.getElementById(this.#prefix + "-bus-to"); | ||||
|         const error = document.getElementById(this.#prefix + "-bus-to-error"); | ||||
|         field.value = field.value.trim(); | ||||
|         if (field.value === "") { | ||||
|             field.classList.add("is-invalid"); | ||||
|             error.innerText = A_("Please fill in the destination."); | ||||
|             return false; | ||||
|         } | ||||
|         field.classList.remove("is-invalid"); | ||||
|         error.innerText = ""; | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Submits the summary. | ||||
|      * | ||||
|      */ | ||||
|     #submit() { | ||||
|         const form = document.getElementById(this.#prefix); | ||||
|         const summary = document.getElementById(this.#prefix + "-summary"); | ||||
|         const formSummaryControl = document.getElementById("accounting-entry-form-summary-control"); | ||||
|         const formSummary = document.getElementById("accounting-entry-form-summary"); | ||||
|         const formAccountControl = document.getElementById("accounting-entry-form-account-control"); | ||||
|         const formAccount = document.getElementById("accounting-entry-form-account"); | ||||
|         const helperModal = document.getElementById(this.#prefix + "-modal"); | ||||
|         const entryModal = document.getElementById("accounting-entry-form-modal"); | ||||
|         if (summary.value === "") { | ||||
|             formSummaryControl.classList.remove("accounting-not-empty"); | ||||
|         } else { | ||||
|             formSummaryControl.classList.add("accounting-not-empty"); | ||||
|         } | ||||
|         if (form.dataset.selectedAccountCode !== "") { | ||||
|             formAccountControl.classList.add("accounting-not-empty"); | ||||
|             formAccount.dataset.code = form.dataset.selectedAccountCode; | ||||
|             formAccount.dataset.text = form.dataset.selectedAccountText; | ||||
|             formAccount.innerText = form.dataset.selectedAccountText; | ||||
|         } | ||||
|         formSummary.dataset.value = summary.value; | ||||
|         formSummary.innerText = summary.value; | ||||
|         bootstrap.Modal.getInstance(helperModal).hide(); | ||||
|         bootstrap.Modal.getOrCreateInstance(entryModal).show(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the summary helper when it is shown. | ||||
|      * | ||||
|      * @param isNew {boolean} true for adding a new journal entry, or false otherwise | ||||
|      */ | ||||
|     initShow(isNew) { | ||||
|         const formSummary = document.getElementById("accounting-entry-form-summary"); | ||||
|         const summary = document.getElementById(this.#prefix + "-summary"); | ||||
|         const closeButtons = Array.from(document.getElementsByClassName(this.#prefix + "-close")); | ||||
|         for (const closeButton of closeButtons) { | ||||
|             if (isNew) { | ||||
|                 closeButton.dataset.bsTarget = "#" + this.#prefix + "-modal"; | ||||
|             } else { | ||||
|                 closeButton.dataset.bsTarget = "#accounting-entry-form-modal"; | ||||
|             } | ||||
|         } | ||||
|         this.#reset(); | ||||
|         if (!isNew) { | ||||
|             summary.value = formSummary.dataset.value; | ||||
|             this.#parseAndPopulate(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Resets the summary helper. | ||||
|      * | ||||
|      */ | ||||
|     #reset() { | ||||
|         const inputs = Array.from(document.getElementsByClassName(this.#prefix + "-input")); | ||||
|         const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-btn-tag")); | ||||
|         const directionButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-direction")); | ||||
|         for (const input of inputs) { | ||||
|             input.value = ""; | ||||
|             input.classList.remove("is-invalid"); | ||||
|         } | ||||
|         for (const tagButton of tagButtons) { | ||||
|             tagButton.classList.remove("btn-primary"); | ||||
|             tagButton.classList.add("btn-outline-primary"); | ||||
|         } | ||||
|         for (const directionButton of directionButtons) { | ||||
|             if (directionButton.classList.contains("accounting-default")) { | ||||
|                 directionButton.classList.remove("btn-outline-primary"); | ||||
|                 directionButton.classList.add("btn-primary"); | ||||
|             } else { | ||||
|                 directionButton.classList.add("btn-outline-primary"); | ||||
|                 directionButton.classList.remove("btn-primary"); | ||||
|             } | ||||
|         } | ||||
|         this.#filterSuggestedAccounts(null); | ||||
|         this.#switchToTab(this.#defaultTabId); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Parses the summary input and populates the summary helper. | ||||
|      * | ||||
|      */ | ||||
|     #parseAndPopulate() { | ||||
|         const summary = document.getElementById(this.#prefix + "-summary"); | ||||
|         const pos = summary.value.indexOf("—"); | ||||
|         if (pos === -1) { | ||||
|             return; | ||||
|         } | ||||
|         let found; | ||||
|         found = summary.value.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:×(\d+))?$/); | ||||
|         if (found !== null) { | ||||
|             return this.#populateBusTrip(found[1], found[2], found[3], found[4], found[5]); | ||||
|         } | ||||
|         found = summary.value.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:×(\d+))?$/); | ||||
|         if (found !== null) { | ||||
|             return this.#populateGeneralTrip(found[1], found[2], found[3], found[4], found[5]); | ||||
|         } | ||||
|         found = summary.value.match(/^([^—]+)—.+?(?:×(\d+)?)?$/); | ||||
|         if (found !== null) { | ||||
|             return this.#populateGeneralTag(found[1], found[2]); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Populates a bus trip. | ||||
|      * | ||||
|      * @param tagName {string} the tag name | ||||
|      * @param routeName {string} the route name or route number | ||||
|      * @param fromName {string} the name of the origin | ||||
|      * @param toName {string} the name of the destination | ||||
|      * @param numberStr {string|undefined} the number of items, if any | ||||
|      */ | ||||
|     #populateBusTrip(tagName, routeName, fromName, toName, numberStr) { | ||||
|         const tag = document.getElementById(this.#prefix + "-bus-tag"); | ||||
|         const route = document.getElementById(this.#prefix + "-bus-route"); | ||||
|         const from = document.getElementById(this.#prefix + "-bus-from"); | ||||
|         const to = document.getElementById(this.#prefix + "-bus-to"); | ||||
|         const number = document.getElementById(this.#prefix + "-number"); | ||||
|         const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-bus-btn-tag")); | ||||
|         tag.value = tagName; | ||||
|         route.value = routeName; | ||||
|         from.value = fromName; | ||||
|         to.value = toName; | ||||
|         if (numberStr !== undefined) { | ||||
|             number.value = parseInt(numberStr); | ||||
|         } | ||||
|         for (const tagButton of tagButtons) { | ||||
|             if (tagButton.dataset.value === tagName) { | ||||
|                 tagButton.classList.remove("btn-outline-primary"); | ||||
|                 tagButton.classList.add("btn-primary"); | ||||
|                 this.#filterSuggestedAccounts(tagButton); | ||||
|             } | ||||
|         } | ||||
|         this.#switchToTab("bus"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Populates a general trip. | ||||
|      * | ||||
|      * @param tagName {string} the tag name | ||||
|      * @param fromName {string} the name of the origin | ||||
|      * @param direction {string} the direction arrow, either "→" or "↔" | ||||
|      * @param toName {string} the name of the destination | ||||
|      * @param numberStr {string|undefined} the number of items, if any | ||||
|      */ | ||||
|     #populateGeneralTrip(tagName, fromName, direction, toName, numberStr) { | ||||
|         const tag = document.getElementById(this.#prefix + "-travel-tag"); | ||||
|         const from = document.getElementById(this.#prefix + "-travel-from"); | ||||
|         const to = document.getElementById(this.#prefix + "-travel-to"); | ||||
|         const number = document.getElementById(this.#prefix + "-number"); | ||||
|         const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-btn-tag")); | ||||
|         const directionButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-direction")); | ||||
|         tag.value = tagName; | ||||
|         from.value = fromName; | ||||
|         for (const directionButton of directionButtons) { | ||||
|             if (directionButton.dataset.arrow === direction) { | ||||
|                 directionButton.classList.remove("btn-outline-primary"); | ||||
|                 directionButton.classList.add("btn-primary"); | ||||
|             } else { | ||||
|                 directionButton.classList.add("btn-outline-primary"); | ||||
|                 directionButton.classList.remove("btn-primary"); | ||||
|             } | ||||
|         } | ||||
|         to.value = toName; | ||||
|         if (numberStr !== undefined) { | ||||
|             number.value = parseInt(numberStr); | ||||
|         } | ||||
|         for (const tagButton of tagButtons) { | ||||
|             if (tagButton.dataset.value === tagName) { | ||||
|                 tagButton.classList.remove("btn-outline-primary"); | ||||
|                 tagButton.classList.add("btn-primary"); | ||||
|                 this.#filterSuggestedAccounts(tagButton); | ||||
|             } | ||||
|         } | ||||
|         this.#switchToTab("travel"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Populates a general tag. | ||||
|      * | ||||
|      * @param tagName {string} the tag name | ||||
|      * @param numberStr {string|undefined} the number of items, if any | ||||
|      */ | ||||
|     #populateGeneralTag(tagName, numberStr) { | ||||
|         const tag = document.getElementById(this.#prefix + "-general-tag"); | ||||
|         const number = document.getElementById(this.#prefix + "-number"); | ||||
|         const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-general-btn-tag")); | ||||
|         tag.value = tagName; | ||||
|         if (numberStr !== undefined) { | ||||
|             number.value = parseInt(numberStr); | ||||
|         } | ||||
|         for (const tagButton of tagButtons) { | ||||
|             if (tagButton.dataset.value === tagName) { | ||||
|                 tagButton.classList.remove("btn-outline-primary"); | ||||
|                 tagButton.classList.add("btn-primary"); | ||||
|                 this.#filterSuggestedAccounts(tagButton); | ||||
|             } | ||||
|         } | ||||
|         this.#switchToTab("general"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * The summary helpers. | ||||
|      * @type {{debit: SummaryHelper, credit: SummaryHelper}} | ||||
|      */ | ||||
|     static #helpers = {} | ||||
|  | ||||
|     /** | ||||
|      * Initializes the summary helpers. | ||||
|      * | ||||
|      */ | ||||
|     static initialize() { | ||||
|         const forms = Array.from(document.getElementsByClassName("accounting-summary-helper")); | ||||
|         for (const form of forms) { | ||||
|             const helper = new SummaryHelper(form); | ||||
|             this.#helpers[helper.#entryType] = helper; | ||||
|         } | ||||
|         this.#initializeTransactionForm(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the transaction form. | ||||
|      * | ||||
|      */ | ||||
|     static #initializeTransactionForm() { | ||||
|         const entryForm = document.getElementById("accounting-entry-form"); | ||||
|         const formSummaryControl = document.getElementById("accounting-entry-form-summary-control"); | ||||
|         const helpers = this.#helpers; | ||||
|         formSummaryControl.onclick = function () { | ||||
|             helpers[entryForm.dataset.entryType].initShow(false); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the summary helper for a new journal entry. | ||||
|      * | ||||
|      * @param entryType {string} the entry type, either "debit" or "credit" | ||||
|      */ | ||||
|     static initializeNewJournalEntry(entryType) { | ||||
|         this.#helpers[entryType].initShow(true); | ||||
|     } | ||||
| } | ||||
| @@ -22,7 +22,7 @@ | ||||
|  */ | ||||
|  | ||||
| // Initializes the page JavaScript. | ||||
| document.addEventListener("DOMContentLoaded", function () { | ||||
| document.addEventListener("DOMContentLoaded", () => { | ||||
|     initializeCurrencyForms(); | ||||
|     initializeJournalEntries(); | ||||
|     initializeFormValidation(); | ||||
| @@ -68,14 +68,14 @@ function initializeCurrencyForms() { | ||||
|     const btnNew = document.getElementById("accounting-btn-new-currency"); | ||||
|     const currencyList = document.getElementById("accounting-currency-list"); | ||||
|     const deleteButtons = Array.from(document.getElementsByClassName("accounting-btn-delete-currency")); | ||||
|     const onReorder = function () { | ||||
|     const onReorder = () => { | ||||
|         const currencies = Array.from(currencyList.children); | ||||
|         for (let i = 0; i < currencies.length; i++) { | ||||
|             const no = document.getElementById(currencies[i].dataset.prefix + "-no"); | ||||
|             no.value = String(i + 1); | ||||
|         } | ||||
|     }; | ||||
|     btnNew.onclick = function () { | ||||
|     btnNew.onclick = () => { | ||||
|         const currencies = Array.from(document.getElementsByClassName("accounting-currency")); | ||||
|         let maxIndex = 0; | ||||
|         for (const currency of currencies) { | ||||
| @@ -107,7 +107,7 @@ function initializeCurrencyForms() { | ||||
|  */ | ||||
| function initializeBtnDeleteCurrency(button) { | ||||
|     const target = document.getElementById(button.dataset.target); | ||||
|     button.onclick = function () { | ||||
|     button.onclick = () => { | ||||
|         target.parentElement.removeChild(target); | ||||
|         resetDeleteCurrencyButtons(); | ||||
|     }; | ||||
| @@ -161,7 +161,7 @@ function initializeNewEntryButton(button) { | ||||
|     const formSummaryError = document.getElementById("accounting-entry-form-summary-error"); | ||||
|     const formAmount = document.getElementById("accounting-entry-form-amount"); | ||||
|     const formAmountError = document.getElementById("accounting-entry-form-amount-error"); | ||||
|     button.onclick = function () { | ||||
|     button.onclick = () => { | ||||
|         entryForm.dataset.currencyIndex = button.dataset.currencyIndex; | ||||
|         entryForm.dataset.entryType = button.dataset.entryType; | ||||
|         entryForm.dataset.entryIndex = button.dataset.entryIndex; | ||||
| @@ -171,7 +171,7 @@ function initializeNewEntryButton(button) { | ||||
|         formAccount.dataset.code = ""; | ||||
|         formAccount.dataset.text = ""; | ||||
|         formAccountError.innerText = ""; | ||||
|         formSummaryControl.dataset.bsTarget = "#accounting-summary-helper-" + button.dataset.entryType + "-modal"; | ||||
|         formSummaryControl.dataset.bsTarget = "#accounting-summary-editor-" + button.dataset.entryType + "-modal"; | ||||
|         formSummaryControl.classList.remove("accounting-not-empty"); | ||||
|         formSummaryControl.classList.remove("is-invalid"); | ||||
|         formSummary.dataset.value = ""; | ||||
| @@ -181,7 +181,7 @@ function initializeNewEntryButton(button) { | ||||
|         formAmount.classList.remove("is-invalid"); | ||||
|         formAmountError.innerText = ""; | ||||
|         AccountSelector.initializeJournalEntryForm(); | ||||
|         SummaryHelper.initializeNewJournalEntry(button.dataset.entryType); | ||||
|         SummaryEditor.initializeNewJournalEntry(button.dataset.entryType); | ||||
|     }; | ||||
| } | ||||
|  | ||||
| @@ -191,7 +191,7 @@ function initializeNewEntryButton(button) { | ||||
|  * @param entryList {HTMLUListElement} the journal entry list. | ||||
|  */ | ||||
| function initializeJournalEntryListReorder(entryList) { | ||||
|     initializeDragAndDropReordering(entryList, function () { | ||||
|     initializeDragAndDropReordering(entryList, () => { | ||||
|         const entries = Array.from(entryList.children); | ||||
|         for (let i = 0; i < entries.length; i++) { | ||||
|             const no = document.getElementById(entries[i].dataset.prefix + "-no"); | ||||
| @@ -216,7 +216,7 @@ function initializeJournalEntry(entry) { | ||||
|     const formSummaryControl = document.getElementById("accounting-entry-form-summary-control"); | ||||
|     const formSummary = document.getElementById("accounting-entry-form-summary"); | ||||
|     const formAmount = document.getElementById("accounting-entry-form-amount"); | ||||
|     control.onclick = function () { | ||||
|     control.onclick = () => { | ||||
|         entryForm.dataset.currencyIndex = entry.dataset.currencyIndex; | ||||
|         entryForm.dataset.entryType = entry.dataset.entryType; | ||||
|         entryForm.dataset.entryIndex = entry.dataset.entryIndex; | ||||
| @@ -228,7 +228,7 @@ function initializeJournalEntry(entry) { | ||||
|         formAccount.innerText = accountCode.dataset.text; | ||||
|         formAccount.dataset.code = accountCode.value; | ||||
|         formAccount.dataset.text = accountCode.dataset.text; | ||||
|         formSummaryControl.dataset.bsTarget = "#accounting-summary-helper-" + entry.dataset.entryType + "-modal"; | ||||
|         formSummaryControl.dataset.bsTarget = "#accounting-summary-editor-" + entry.dataset.entryType + "-modal"; | ||||
|         if (summary.value === "") { | ||||
|             formSummaryControl.classList.remove("accounting-not-empty"); | ||||
|         } else { | ||||
| @@ -252,7 +252,7 @@ function initializeJournalEntryFormModal() { | ||||
|     const formAmount = document.getElementById("accounting-entry-form-amount"); | ||||
|     const modal = document.getElementById("accounting-entry-form-modal"); | ||||
|     formAmount.onchange = validateJournalEntryAmount; | ||||
|     entryForm.onsubmit = function () { | ||||
|     entryForm.onsubmit = () => { | ||||
|         if (validateJournalEntryForm()) { | ||||
|             saveJournalEntryForm(); | ||||
|             bootstrap.Modal.getInstance(modal).hide(); | ||||
| @@ -398,7 +398,7 @@ function initializeDeleteJournalEntryButton(button) { | ||||
|     const currencyIndex = target.dataset.currencyIndex; | ||||
|     const entryType = target.dataset.entryType; | ||||
|     const currency = document.getElementById("accounting-currency-" + currencyIndex); | ||||
|     button.onclick = function () { | ||||
|     button.onclick = () => { | ||||
|         target.parentElement.removeChild(target); | ||||
|         resetDeleteJournalEntryButtons(button.dataset.sameClass); | ||||
|         updateBalance(currencyIndex, entryType); | ||||
|   | ||||
| @@ -22,10 +22,10 @@ | ||||
|  */ | ||||
|  | ||||
| // Initializes the page JavaScript. | ||||
| document.addEventListener("DOMContentLoaded", function () { | ||||
| document.addEventListener("DOMContentLoaded", () => { | ||||
|     const list = document.getElementById("accounting-order-list"); | ||||
|     if (list !== null) { | ||||
|         const onReorder = function () { | ||||
|         const onReorder = () => { | ||||
|             const accounts = Array.from(list.children); | ||||
|             for (let i = 0; i < accounts.length; i++) { | ||||
|                 const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no"); | ||||
|   | ||||
							
								
								
									
										68
									
								
								src/accounting/template_filters.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/accounting/template_filters.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| # The Mia! Accounting Flask Project. | ||||
| # Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25 | ||||
|  | ||||
| #  Copyright (c) 2023 imacat. | ||||
| # | ||||
| #  Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| #  you may not use this file except in compliance with the License. | ||||
| #  You may obtain a copy of the License at | ||||
| # | ||||
| #      http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| #  Unless required by applicable law or agreed to in writing, software | ||||
| #  distributed under the License is distributed on an "AS IS" BASIS, | ||||
| #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| #  See the License for the specific language governing permissions and | ||||
| #  limitations under the License. | ||||
| """The template filters. | ||||
|  | ||||
| """ | ||||
| from decimal import Decimal | ||||
| from datetime import date, timedelta | ||||
|  | ||||
| from flask_babel import get_locale | ||||
|  | ||||
| from accounting.locale import gettext | ||||
|  | ||||
|  | ||||
| def format_amount(value: Decimal | None) -> str: | ||||
|     """Formats an amount for readability. | ||||
|  | ||||
|     :param value: The amount. | ||||
|     :return: The formatted amount text. | ||||
|     """ | ||||
|     if value is None or value == 0: | ||||
|         return "-" | ||||
|     whole: int = int(value) | ||||
|     frac: Decimal = (value - whole).normalize() | ||||
|     return "{:,}".format(whole) + str(frac)[1:] | ||||
|  | ||||
|  | ||||
| def format_date(value: date) -> str: | ||||
|     """Formats a date to be human-friendly. | ||||
|  | ||||
|     :param value: The date. | ||||
|     :return: The human-friendly date text. | ||||
|     """ | ||||
|     today: date = date.today() | ||||
|     if value == today: | ||||
|         return gettext("Today") | ||||
|     if value == today - timedelta(days=1): | ||||
|         return gettext("Yesterday") | ||||
|     if value == today + timedelta(days=1): | ||||
|         return gettext("Tomorrow") | ||||
|     locale = str(get_locale()) | ||||
|     if locale == "zh" or locale.startswith("zh_"): | ||||
|         if value == today - timedelta(days=2): | ||||
|             return gettext("The day before yesterday") | ||||
|         if value == today + timedelta(days=2): | ||||
|             return gettext("The day after tomorrow") | ||||
|     if locale == "zh" or locale.startswith("zh_"): | ||||
|         weekdays = ["一", "二", "三", "四", "五", "六", "日"] | ||||
|         weekday = weekdays[value.weekday()] | ||||
|     else: | ||||
|         weekday = value.strftime("%a") | ||||
|     if value.year != today.year: | ||||
|         return "{}/{}/{}({})".format( | ||||
|             value.year, value.month, value.day, weekday) | ||||
|     return "{}/{}({})".format(value.month, value.day, weekday) | ||||
							
								
								
									
										39
									
								
								src/accounting/template_globals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/accounting/template_globals.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| # The Mia! Accounting Flask Project. | ||||
| # Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3 | ||||
|  | ||||
| #  Copyright (c) 2023 imacat. | ||||
| # | ||||
| #  Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| #  you may not use this file except in compliance with the License. | ||||
| #  You may obtain a copy of the License at | ||||
| # | ||||
| #      http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| #  Unless required by applicable law or agreed to in writing, software | ||||
| #  distributed under the License is distributed on an "AS IS" BASIS, | ||||
| #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| #  See the License for the specific language governing permissions and | ||||
| #  limitations under the License. | ||||
| """The template globals for the transaction management. | ||||
|  | ||||
| """ | ||||
| from flask import current_app | ||||
|  | ||||
| from accounting.models import Currency | ||||
|  | ||||
|  | ||||
| def currency_options() -> str: | ||||
|     """Returns the currency options. | ||||
|  | ||||
|     :return: The currency options. | ||||
|     """ | ||||
|     return Currency.query.order_by(Currency.code).all() | ||||
|  | ||||
|  | ||||
| def default_currency_code() -> str: | ||||
|     """Returns the default currency code. | ||||
|  | ||||
|     :return: The default currency code. | ||||
|     """ | ||||
|     with current_app.app_context(): | ||||
|         return current_app.config.get("DEFAULT_CURRENCY", "USD") | ||||
| @@ -44,14 +44,14 @@ First written: 2023/2/26 | ||||
|                   <div>{{ entry.summary }}</div> | ||||
|                 {% endif %} | ||||
|               </div> | ||||
|               <div>{{ entry.amount|accounting_txn_format_amount }}</div> | ||||
|               <div>{{ entry.amount|accounting_format_amount }}</div> | ||||
|             </div> | ||||
|           </li> | ||||
|         {% endfor %} | ||||
|         <li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total"> | ||||
|           <div class="d-flex justify-content-between"> | ||||
|             <div>{{ A_("Total") }}</div> | ||||
|             <div>{{ currency.debit_total|accounting_txn_format_amount }}</div> | ||||
|             <div>{{ currency.debit_total|accounting_format_amount }}</div> | ||||
|           </div> | ||||
|         </li> | ||||
|       </ul> | ||||
|   | ||||
| @@ -25,7 +25,7 @@ First written: 2023/2/25 | ||||
|     <div class="d-flex justify-content-between mt-2 mb-3"> | ||||
|       <div class="form-floating accounting-currency-content"> | ||||
|         <select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code"> | ||||
|           {% for currency in accounting_txn_currency_options() %} | ||||
|           {% for currency in accounting_currency_options() %} | ||||
|             <option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option> | ||||
|           {% endfor %} | ||||
|         </select> | ||||
| @@ -57,7 +57,7 @@ First written: 2023/2/25 | ||||
|                     summary_errors = entry_form.summary.errors, | ||||
|                     amount_data = "" if entry_form.amount.data is none else entry_form.amount.data|accounting_txn_format_amount_input, | ||||
|                     amount_errors = entry_form.amount.errors, | ||||
|                     amount_text = entry_form.amount.data|accounting_txn_format_amount, | ||||
|                     amount_text = entry_form.amount.data|accounting_format_amount, | ||||
|                     entry_errors = entry_form.all_errors %} | ||||
|               {% include "accounting/transaction/include/form-entry-item.html" %} | ||||
|             {% endwith %} | ||||
|   | ||||
| @@ -31,14 +31,14 @@ First written: 2023/2/25 | ||||
|               currency_code_errors = currency_form.code.errors, | ||||
|               debit_forms = currency_form.debit, | ||||
|               debit_errors = currency_form.debit_errors, | ||||
|               debit_total = currency_form.form.debit_total|accounting_txn_format_amount %} | ||||
|               debit_total = currency_form.form.debit_total|accounting_format_amount %} | ||||
|         {% include "accounting/transaction/expense/include/form-currency-item.html" %} | ||||
|       {% endwith %} | ||||
|     {% endfor %} | ||||
|   {% else %} | ||||
|     {% with currency_index = 1, | ||||
|             only_one_currency_form = True, | ||||
|             currency_code_data = accounting_txn_default_currency_code(), | ||||
|             currency_code_data = accounting_default_currency_code(), | ||||
|             debit_total = "-" %} | ||||
|       {% include "accounting/transaction/expense/include/form-currency-item.html" %} | ||||
|     {% endwith %} | ||||
| @@ -46,8 +46,8 @@ First written: 2023/2/25 | ||||
| {% endblock %} | ||||
|  | ||||
| {% block form_modals %} | ||||
|   {% with summary_helper = form.summary_helper.debit %} | ||||
|     {% include "accounting/transaction/include/summary-helper-modal.html" %} | ||||
|   {% with summary_editor = form.summary_editor.debit %} | ||||
|     {% include "accounting/transaction/include/summary-editor-modal.html" %} | ||||
|   {% endwith %} | ||||
|   {% with entry_type = "debit", | ||||
|           account_options = form.debit_account_options %} | ||||
|   | ||||
| @@ -89,7 +89,7 @@ First written: 2023/2/26 | ||||
|   </div> | ||||
|  | ||||
|   <div class="mb-3"> | ||||
|     {{ obj.date|accounting_txn_format_date }} | ||||
|     {{ obj.date|accounting_format_date }} | ||||
|   </div> | ||||
|  | ||||
|   {% block transaction_currencies %}{% endblock %} | ||||
|   | ||||
| @@ -25,7 +25,7 @@ First written: 2023/2/26 | ||||
|   <script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script> | ||||
|   <script src="{{ url_for("accounting.static", filename="js/transaction-form.js") }}"></script> | ||||
|   <script src="{{ url_for("accounting.static", filename="js/account-selector.js") }}"></script> | ||||
|   <script src="{{ url_for("accounting.static", filename="js/summary-helper.js") }}"></script> | ||||
|   <script src="{{ url_for("accounting.static", filename="js/summary-editor.js") }}"></script> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
|   | ||||
| @@ -0,0 +1,190 @@ | ||||
| {# | ||||
| The Mia! Accounting Flask Project | ||||
| summary-editor-modal.html: The modal of the summary editor | ||||
|  | ||||
|  Copyright (c) 2023 imacat. | ||||
|  | ||||
|  Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  you may not use this file except in compliance with the License. | ||||
|  You may obtain a copy of the License at | ||||
|  | ||||
|      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|  Unless required by applicable law or agreed to in writing, software | ||||
|  distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  See the License for the specific language governing permissions and | ||||
|  limitations under the License. | ||||
|  | ||||
| Author: imacat@mail.imacat.idv.tw (imacat) | ||||
| First written: 2023/2/28 | ||||
| #} | ||||
| <form id="accounting-summary-editor-{{ summary_editor.type }}" class="accounting-summary-editor" data-entry-type="{{ summary_editor.type }}"> | ||||
|   <div id="accounting-summary-editor-{{ summary_editor.type }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-modal-label" aria-hidden="true"> | ||||
|     <div class="modal-dialog"> | ||||
|       <div class="modal-content"> | ||||
|         <div class="modal-header"> | ||||
|           <h1 class="modal-title fs-5" id="accounting-summary-editor-{{ summary_editor.type }}-modal-label"> | ||||
|             <label for="accounting-summary-editor-{{ summary_editor.type }}-summary">{{ A_("Summary") }}</label> | ||||
|           </h1> | ||||
|           <button class="btn-close" type="button" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal" aria-label="{{ A_("Close") }}"></button> | ||||
|         </div> | ||||
|         <div class="modal-body"> | ||||
|           <div class="mb-3"> | ||||
|             <input id="accounting-summary-editor-{{ summary_editor.type }}-summary" class="form-control" type="text" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-modal-label"> | ||||
|           </div> | ||||
|  | ||||
|           {# Tab navigation #} | ||||
|           <ul class="nav nav-tabs mb-2"> | ||||
|             <li class="nav-item"> | ||||
|               <span id="accounting-summary-editor-{{ summary_editor.type }}-general-tab" class="nav-link active accounting-clickable" aria-current="page"> | ||||
|                 {{ A_("General") }} | ||||
|               </span> | ||||
|             </li> | ||||
|             <li class="nav-item"> | ||||
|               <span id="accounting-summary-editor-{{ summary_editor.type }}-travel-tab" class="nav-link accounting-clickable" aria-current="false"> | ||||
|                 {{ A_("Travel") }} | ||||
|               </span> | ||||
|             </li> | ||||
|             <li class="nav-item"> | ||||
|               <span id="accounting-summary-editor-{{ summary_editor.type }}-bus-tab" class="nav-link accounting-clickable" aria-current="false"> | ||||
|                 {{ A_("Bus") }} | ||||
|               </span> | ||||
|             </li> | ||||
|             <li class="nav-item"> | ||||
|               <span id="accounting-summary-editor-{{ summary_editor.type }}-regular-tab" class="nav-link accounting-clickable" aria-current="false"> | ||||
|                 {{ A_("Regular") }} | ||||
|               </span> | ||||
|             </li> | ||||
|             <li class="nav-item"> | ||||
|               <span id="accounting-summary-editor-{{ summary_editor.type }}-annotation-tab" class="nav-link accounting-clickable" aria-current="false"> | ||||
|                 {{ A_("Annotation") }} | ||||
|               </span> | ||||
|             </li> | ||||
|           </ul> | ||||
|  | ||||
|           {# A general summary with a tag #} | ||||
|           <div id="accounting-summary-editor-{{ summary_editor.type }}-general-page" aria-current="page" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-general-tab"> | ||||
|             <div class="form-floating mb-2"> | ||||
|               <input id="accounting-summary-editor-{{ summary_editor.type }}-general-tag" class="form-control" type="text" value="" placeholder=" "> | ||||
|               <label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-general-tag">{{ A_("Tag") }}</label> | ||||
|               <div id="accounting-summary-editor-{{ summary_editor.type }}-general-tag-error" class="invalid-feedback"></div> | ||||
|             </div> | ||||
|  | ||||
|             <div> | ||||
|               {% for tag in summary_editor.general.tags %} | ||||
|                 <button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.type }}-general-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}"> | ||||
|                   {{ tag }} | ||||
|                 </button> | ||||
|               {% endfor %} | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|           {# A general trip with the origin and distination #} | ||||
|           <div id="accounting-summary-editor-{{ summary_editor.type }}-travel-page" class="d-none" aria-current="false" aria-labelledby="accounting-summary-editor-{{ summary_editor.type }}-travel-tab"> | ||||
|             <div class="form-floating mb-2"> | ||||
|               <input id="accounting-summary-editor-{{ summary_editor.type }}-travel-tag" class="form-control" type="text" value="" placeholder=" "> | ||||
|               <label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-travel-tag">{{ A_("Tag") }}</label> | ||||
|               <div id="accounting-summary-editor-{{ summary_editor.type }}-travel-tag-error" class="invalid-feedback"></div> | ||||
|             </div> | ||||
|  | ||||
|             <div> | ||||
|               {% for tag in summary_editor.travel.tags %} | ||||
|                 <button class="btn btn-outline-primary accounting-summary-editor-{{ summary_editor.type }}-travel-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}"> | ||||
|                   {{ tag }} | ||||
|                 </button> | ||||
|               {% endfor %} | ||||
|             </div> | ||||
|  | ||||
|             <div class="d-flex justify-content-between mt-2"> | ||||
|               <div class="form-floating"> | ||||
|                 <input id="accounting-summary-editor-{{ summary_editor.type }}-travel-from" class="form-control" type="text" value="" placeholder=" "> | ||||
|                 <label class="form-label" for="accounting-summary-editor-{{ summary_editor.type }}-travel-from">{{ A_("From") }}</label> | ||||
|                 <div id="accounting-summary-editor-{{ summary_editor.type }}-travel-from-error" class="invalid-feedback"></div> | ||||
|               </div> | ||||
|               <div class="btn-group-vertical ms-1 me-1"> | ||||
|                 <button class="btn btn-primary accounting-summary-editor-{{ summary_editor.type }}-travel-direction accounting-default" type="button" tabindex="-1" data-arrow="→">→</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> | ||||
| @@ -1,181 +0,0 @@ | ||||
| {# | ||||
| The Mia! Accounting Flask Project | ||||
| entry-form-modal.html: The modal of the summary helper | ||||
|  | ||||
|  Copyright (c) 2023 imacat. | ||||
|  | ||||
|  Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  you may not use this file except in compliance with the License. | ||||
|  You may obtain a copy of the License at | ||||
|  | ||||
|      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|  Unless required by applicable law or agreed to in writing, software | ||||
|  distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  See the License for the specific language governing permissions and | ||||
|  limitations under the License. | ||||
|  | ||||
| Author: imacat@mail.imacat.idv.tw (imacat) | ||||
| First written: 2023/2/28 | ||||
| #} | ||||
| <form id="accounting-summary-helper-{{ summary_helper.type }}" class="accounting-summary-helper" data-entry-type="{{ summary_helper.type }}" data-default-tab-id="general" data-selected-account-code="" data-selected-account-text=""> | ||||
|   <div id="accounting-summary-helper-{{ summary_helper.type }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-modal-label" aria-hidden="true"> | ||||
|     <div class="modal-dialog"> | ||||
|       <div class="modal-content"> | ||||
|         <div class="modal-header"> | ||||
|           <h1 class="modal-title fs-5" id="accounting-summary-helper-{{ summary_helper.type }}-modal-label"> | ||||
|             <label for="accounting-summary-helper-{{ summary_helper.type }}-summary">{{ A_("Summary") }}</label> | ||||
|           </h1> | ||||
|           <button class="btn-close accounting-summary-helper-{{ summary_helper.type }}-close" type="button" data-bs-toggle="modal" data-bs-target="" aria-label="{{ A_("Close") }}"></button> | ||||
|         </div> | ||||
|         <div class="modal-body"> | ||||
|           <div class="mb-3"> | ||||
|             <input id="accounting-summary-helper-{{ summary_helper.type }}-summary" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-modal-label"> | ||||
|           </div> | ||||
|           <ul class="nav nav-tabs mb-2"> | ||||
|             <li class="nav-item"> | ||||
|               <span id="accounting-summary-helper-{{ summary_helper.type }}-tab-general" class="nav-link active accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="page" data-tab-id="general"> | ||||
|                 {{ A_("General") }} | ||||
|               </span> | ||||
|             </li> | ||||
|             <li class="nav-item"> | ||||
|               <span id="accounting-summary-helper-{{ summary_helper.type }}-tab-travel" class="nav-link accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="false" data-tab-id="travel"> | ||||
|                 {{ A_("Travel") }} | ||||
|               </span> | ||||
|             </li> | ||||
|             <li class="nav-item"> | ||||
|               <span id="accounting-summary-helper-{{ summary_helper.type }}-tab-bus" class="nav-link accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="false" data-tab-id="bus"> | ||||
|                 {{ A_("Bus") }} | ||||
|               </span> | ||||
|             </li> | ||||
|             <li class="nav-item"> | ||||
|               <span id="accounting-summary-helper-{{ summary_helper.type }}-tab-regular" class="nav-link accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="false" data-tab-id="regular"> | ||||
|                 {{ A_("Regular") }} | ||||
|               </span> | ||||
|             </li> | ||||
|             <li class="nav-item"> | ||||
|               <span id="accounting-summary-helper-{{ summary_helper.type }}-tab-number" class="nav-link accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="false" data-tab-id="number"> | ||||
|                 {{ A_("Number") }} | ||||
|               </span> | ||||
|             </li> | ||||
|           </ul> | ||||
|           {# A general summary with a tag #} | ||||
|           <div class="accounting-summary-helper-{{ summary_helper.type }}-page" aria-current="page" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-tab-general" data-tab-id="general"> | ||||
|             <div class="form-floating mb-2"> | ||||
|               <input id="accounting-summary-helper-{{ summary_helper.type }}-general-tag" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" "> | ||||
|               <label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-general-tag">{{ A_("Tag") }}</label> | ||||
|               <div id="accounting-summary-helper-{{ summary_helper.type }}-general-tag-error" class="invalid-feedback"></div> | ||||
|             </div> | ||||
|  | ||||
|             <div> | ||||
|               {% for tag in summary_helper.general.tags %} | ||||
|                 <button class="btn btn-outline-primary accounting-summary-helper-{{ summary_helper.type }}-btn-tag accounting-summary-helper-{{ summary_helper.type }}-general-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}"> | ||||
|                   {{ tag }} | ||||
|                 </button> | ||||
|               {% endfor %} | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|           {# A general trip with the origin and distination #} | ||||
|           <div class="accounting-summary-helper-{{ summary_helper.type }}-page d-none" aria-current="false" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-tab-travel" data-tab-id="travel"> | ||||
|             <div class="form-floating mb-2"> | ||||
|               <input id="accounting-summary-helper-{{ summary_helper.type }}-travel-tag" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" "> | ||||
|               <label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-travel-tag">{{ A_("Tag") }}</label> | ||||
|               <div id="accounting-summary-helper-{{ summary_helper.type }}-travel-tag-error" class="invalid-feedback"></div> | ||||
|             </div> | ||||
|  | ||||
|             <div> | ||||
|               {% for tag in summary_helper.travel.tags %} | ||||
|                 <button class="btn btn-outline-primary accounting-summary-helper-{{ summary_helper.type }}-btn-tag accounting-summary-helper-{{ summary_helper.type }}-travel-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}"> | ||||
|                   {{ tag }} | ||||
|                 </button> | ||||
|               {% endfor %} | ||||
|             </div> | ||||
|  | ||||
|             <div class="d-flex justify-content-between mt-2"> | ||||
|               <div class="form-floating"> | ||||
|                 <input id="accounting-summary-helper-{{ summary_helper.type }}-travel-from" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" "> | ||||
|                 <label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-travel-from">{{ A_("From") }}</label> | ||||
|                 <div id="accounting-summary-helper-{{ summary_helper.type }}-travel-from-error" class="invalid-feedback"></div> | ||||
|               </div> | ||||
|               <div class="btn-group-vertical ms-1 me-1"> | ||||
|                 <button class="btn btn-primary accounting-summary-helper-{{ summary_helper.type }}-travel-direction accounting-default" type="button" tabindex="-1" data-arrow="→">→</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> | ||||
|                 {% endif %} | ||||
|               </div> | ||||
|               <div>{{ entry.amount|accounting_txn_format_amount }}</div> | ||||
|               <div>{{ entry.amount|accounting_format_amount }}</div> | ||||
|             </div> | ||||
|           </li> | ||||
|         {% endfor %} | ||||
|         <li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total"> | ||||
|           <div class="d-flex justify-content-between"> | ||||
|             <div>{{ A_("Total") }}</div> | ||||
|             <div>{{ currency.debit_total|accounting_txn_format_amount }}</div> | ||||
|             <div>{{ currency.debit_total|accounting_format_amount }}</div> | ||||
|           </div> | ||||
|         </li> | ||||
|       </ul> | ||||
|   | ||||
| @@ -25,7 +25,7 @@ First written: 2023/2/25 | ||||
|     <div class="d-flex justify-content-between mt-2 mb-3"> | ||||
|       <div class="form-floating accounting-currency-content"> | ||||
|         <select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code"> | ||||
|           {% for currency in accounting_txn_currency_options() %} | ||||
|           {% for currency in accounting_currency_options() %} | ||||
|             <option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option> | ||||
|           {% endfor %} | ||||
|         </select> | ||||
| @@ -57,7 +57,7 @@ First written: 2023/2/25 | ||||
|                     summary_errors = entry_form.summary.errors, | ||||
|                     amount_data = "" if entry_form.amount.data is none else entry_form.amount.data|accounting_txn_format_amount_input, | ||||
|                     amount_errors = entry_form.amount.errors, | ||||
|                     amount_text = entry_form.amount.data|accounting_txn_format_amount, | ||||
|                     amount_text = entry_form.amount.data|accounting_format_amount, | ||||
|                     entry_errors = entry_form.all_errors %} | ||||
|               {% include "accounting/transaction/include/form-entry-item.html" %} | ||||
|             {% endwith %} | ||||
|   | ||||
| @@ -31,14 +31,14 @@ First written: 2023/2/25 | ||||
|               currency_code_errors = currency_form.code.errors, | ||||
|               credit_forms = currency_form.credit, | ||||
|               credit_errors = currency_form.credit_errors, | ||||
|               credit_total = currency_form.form.credit_total|accounting_txn_format_amount %} | ||||
|               credit_total = currency_form.form.credit_total|accounting_format_amount %} | ||||
|         {% include "accounting/transaction/income/include/form-currency-item.html" %} | ||||
|       {% endwith %} | ||||
|     {% endfor %} | ||||
|   {% else %} | ||||
|     {% with currency_index = 1, | ||||
|             only_one_currency_form = True, | ||||
|             currency_code_data = accounting_txn_default_currency_code(), | ||||
|             currency_code_data = accounting_default_currency_code(), | ||||
|             credit_total = "-" %} | ||||
|       {% include "accounting/transaction/income/include/form-currency-item.html" %} | ||||
|     {% endwith %} | ||||
| @@ -46,8 +46,8 @@ First written: 2023/2/25 | ||||
| {% endblock %} | ||||
|  | ||||
| {% block form_modals %} | ||||
|   {% with summary_helper = form.summary_helper.credit %} | ||||
|     {% include "accounting/transaction/include/summary-helper-modal.html" %} | ||||
|   {% with summary_editor = form.summary_editor.credit %} | ||||
|     {% include "accounting/transaction/include/summary-editor-modal.html" %} | ||||
|   {% endwith %} | ||||
|   {% with entry_type = "credit", | ||||
|           account_options = form.credit_account_options %} | ||||
|   | ||||
| @@ -85,7 +85,7 @@ First written: 2023/2/18 | ||||
|   <div class="list-group"> | ||||
|   {% for item in list %} | ||||
|     <a class="list-group-item list-group-item-action" href="{{ url_for("accounting.transaction.detail", txn=item)|accounting_append_next }}"> | ||||
|       {{ item.date|accounting_txn_format_date }} {{ item }} | ||||
|       {{ item.date|accounting_format_date }} {{ item }} | ||||
|     </a> | ||||
|   {% endfor %} | ||||
|   </div> | ||||
|   | ||||
| @@ -40,14 +40,14 @@ First written: 2023/2/26 | ||||
|                       <div>{{ entry.summary }}</div> | ||||
|                     {% endif %} | ||||
|                   </div> | ||||
|                   <div>{{ entry.amount|accounting_txn_format_amount }}</div> | ||||
|                   <div>{{ entry.amount|accounting_format_amount }}</div> | ||||
|                 </div> | ||||
|               </li> | ||||
|             {% endfor %} | ||||
|             <li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total"> | ||||
|               <div class="d-flex justify-content-between"> | ||||
|                 <div>{{ A_("Total") }}</div> | ||||
|                 <div>{{ currency.debit_total|accounting_txn_format_amount }}</div> | ||||
|                 <div>{{ currency.debit_total|accounting_format_amount }}</div> | ||||
|               </div> | ||||
|             </li> | ||||
|           </ul> | ||||
| @@ -66,14 +66,14 @@ First written: 2023/2/26 | ||||
|                       <div>{{ entry.summary }}</div> | ||||
|                     {% endif %} | ||||
|                   </div> | ||||
|                   <div>{{ entry.amount|accounting_txn_format_amount }}</div> | ||||
|                   <div>{{ entry.amount|accounting_format_amount }}</div> | ||||
|                 </div> | ||||
|               </li> | ||||
|             {% endfor %} | ||||
|             <li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total"> | ||||
|               <div class="d-flex justify-content-between"> | ||||
|                 <div>{{ A_("Total") }}</div> | ||||
|                 <div>{{ currency.debit_total|accounting_txn_format_amount }}</div> | ||||
|                 <div>{{ currency.debit_total|accounting_format_amount }}</div> | ||||
|               </div> | ||||
|             </li> | ||||
|           </ul> | ||||
|   | ||||
| @@ -25,7 +25,7 @@ First written: 2023/2/25 | ||||
|     <div class="d-flex justify-content-between mt-2 mb-3"> | ||||
|       <div class="form-floating accounting-currency-content"> | ||||
|         <select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code"> | ||||
|           {% for currency in accounting_txn_currency_options() %} | ||||
|           {% for currency in accounting_currency_options() %} | ||||
|             <option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option> | ||||
|           {% endfor %} | ||||
|         </select> | ||||
| @@ -59,7 +59,7 @@ First written: 2023/2/25 | ||||
|                       summary_errors = entry_form.summary.errors, | ||||
|                       amount_data = "" if entry_form.amount.data is none else entry_form.amount.data|accounting_txn_format_amount_input, | ||||
|                       amount_errors = entry_form.amount.errors, | ||||
|                       amount_text = entry_form.amount.data|accounting_txn_format_amount, | ||||
|                       amount_text = entry_form.amount.data|accounting_format_amount, | ||||
|                       entry_errors = entry_form.all_errors %} | ||||
|                 {% include "accounting/transaction/include/form-entry-item.html" %} | ||||
|               {% endwith %} | ||||
| @@ -99,7 +99,7 @@ First written: 2023/2/25 | ||||
|                       summary_errors = entry_form.summary.errors, | ||||
|                       amount_data = "" if entry_form.amount.data is none else entry_form.amount.data|accounting_txn_format_amount_input, | ||||
|                       amount_errors = entry_form.amount.errors, | ||||
|                       amount_text = entry_form.amount.data|accounting_txn_format_amount, | ||||
|                       amount_text = entry_form.amount.data|accounting_format_amount, | ||||
|                       entry_errors = entry_form.all_errors %} | ||||
|                 {% include "accounting/transaction/include/form-entry-item.html" %} | ||||
|               {% endwith %} | ||||
|   | ||||
| @@ -31,17 +31,17 @@ First written: 2023/2/25 | ||||
|               currency_code_errors = currency_form.code.errors, | ||||
|               debit_forms = currency_form.debit, | ||||
|               debit_errors = currency_form.debit_errors, | ||||
|               debit_total = currency_form.form.debit_total|accounting_txn_format_amount, | ||||
|               debit_total = currency_form.form.debit_total|accounting_format_amount, | ||||
|               credit_forms = currency_form.credit, | ||||
|               credit_errors = currency_form.credit_errors, | ||||
|               credit_total = currency_form.form.credit_total|accounting_txn_format_amount %} | ||||
|               credit_total = currency_form.form.credit_total|accounting_format_amount %} | ||||
|         {% include "accounting/transaction/transfer/include/form-currency-item.html" %} | ||||
|       {% endwith %} | ||||
|     {% endfor %} | ||||
|   {% else %} | ||||
|     {% with currency_index = 1, | ||||
|             only_one_currency_form = True, | ||||
|             currency_code_data = accounting_txn_default_currency_code(), | ||||
|             currency_code_data = accounting_default_currency_code(), | ||||
|             debit_total = "-", | ||||
|             credit_total = "-" %} | ||||
|       {% include "accounting/transaction/transfer/include/form-currency-item.html" %} | ||||
| @@ -50,11 +50,11 @@ First written: 2023/2/25 | ||||
| {% endblock %} | ||||
|  | ||||
| {% block form_modals %} | ||||
|   {% with summary_helper = form.summary_helper.debit %} | ||||
|     {% include "accounting/transaction/include/summary-helper-modal.html" %} | ||||
|   {% with summary_editor = form.summary_editor.debit %} | ||||
|     {% include "accounting/transaction/include/summary-editor-modal.html" %} | ||||
|   {% endwith %} | ||||
|   {% with summary_helper = form.summary_helper.credit %} | ||||
|     {% include "accounting/transaction/include/summary-helper-modal.html" %} | ||||
|   {% with summary_editor = form.summary_editor.credit %} | ||||
|     {% include "accounting/transaction/include/summary-editor-modal.html" %} | ||||
|   {% endwith %} | ||||
|   {% with entry_type = "debit", | ||||
|           account_options = form.debit_account_options %} | ||||
|   | ||||
| @@ -26,7 +26,7 @@ from flask_wtf import FlaskForm | ||||
| from accounting.models import Transaction | ||||
| from .forms import TransactionForm, IncomeTransactionForm, \ | ||||
|     ExpenseTransactionForm, TransferTransactionForm | ||||
| from .template import default_currency_code | ||||
| from accounting.template_globals import default_currency_code | ||||
|  | ||||
|  | ||||
| class TransactionType(ABC): | ||||
|   | ||||
| @@ -37,7 +37,7 @@ from accounting import db | ||||
| from accounting.locale import lazy_gettext | ||||
| from accounting.models import Transaction, Account, JournalEntry, \ | ||||
|     TransactionCurrency, Currency | ||||
| from accounting.transaction.summary_helper import SummaryHelper | ||||
| from accounting.transaction.summary_editor import SummaryEditor | ||||
| from accounting.utils.random_id import new_id | ||||
| from accounting.utils.strip_text import strip_text, strip_multiline_text | ||||
| from accounting.utils.user import get_current_user_pk | ||||
| @@ -391,12 +391,12 @@ class TransactionForm(FlaskForm): | ||||
|                 if isinstance(x, str) or isinstance(x, LazyString)] | ||||
|  | ||||
|     @property | ||||
|     def summary_helper(self) -> SummaryHelper: | ||||
|         """Returns the summary helper. | ||||
|     def summary_editor(self) -> SummaryEditor: | ||||
|         """Returns the summary editor. | ||||
|  | ||||
|         :return: The summary helper. | ||||
|         :return: The summary editor. | ||||
|         """ | ||||
|         return SummaryHelper() | ||||
|         return SummaryEditor() | ||||
|  | ||||
|  | ||||
| T = t.TypeVar("T", bound=TransactionForm) | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
| #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| #  See the License for the specific language governing permissions and | ||||
| #  limitations under the License. | ||||
| """The transaction query. | ||||
| """The queries for the transaction management. | ||||
| 
 | ||||
| """ | ||||
| from datetime import datetime | ||||
| @@ -14,7 +14,7 @@ | ||||
| #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| #  See the License for the specific language governing permissions and | ||||
| #  limitations under the License. | ||||
| """The summary helper. | ||||
| """The summary editor. | ||||
| 
 | ||||
| """ | ||||
| import typing as t | ||||
| @@ -178,7 +178,7 @@ class SummaryEntryType: | ||||
| 
 | ||||
|     @property | ||||
|     def accounts(self) -> list[SummaryAccount]: | ||||
|         """Returns the suggested accounts of all tags in the summary helper in | ||||
|         """Returns the suggested accounts of all tags in the summary editor in | ||||
|         the entry type, in their frequency order. | ||||
| 
 | ||||
|         :return: The suggested accounts of all tags, in their frequency order. | ||||
| @@ -197,11 +197,11 @@ class SummaryEntryType: | ||||
|                                             key=lambda x: -freq[x])] | ||||
| 
 | ||||
| 
 | ||||
| class SummaryHelper: | ||||
|     """The summary helper.""" | ||||
| class SummaryEditor: | ||||
|     """The summary editor.""" | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         """Constructs the summary helper.""" | ||||
|         """Constructs the summary editor.""" | ||||
|         self.debit: SummaryEntryType = SummaryEntryType("debit") | ||||
|         """The debit tags.""" | ||||
|         self.credit: SummaryEntryType = SummaryEntryType("credit") | ||||
| @@ -14,20 +14,15 @@ | ||||
| #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| #  See the License for the specific language governing permissions and | ||||
| #  limitations under the License. | ||||
| """The template filters and globals for the transaction management. | ||||
| """The template filters for the transaction management. | ||||
| 
 | ||||
| """ | ||||
| from datetime import date, timedelta | ||||
| from decimal import Decimal | ||||
| from html import escape | ||||
| from urllib.parse import ParseResult, urlparse, parse_qsl, urlencode, \ | ||||
|     urlunparse | ||||
| 
 | ||||
| from flask import request, current_app | ||||
| from flask_babel import get_locale | ||||
| 
 | ||||
| from accounting.locale import gettext | ||||
| from accounting.models import Currency | ||||
| from flask import request | ||||
| 
 | ||||
| 
 | ||||
| def with_type(uri: str) -> str: | ||||
| @@ -62,19 +57,6 @@ def to_transfer(uri: str) -> str: | ||||
|     return urlunparse(parts) | ||||
| 
 | ||||
| 
 | ||||
| def format_amount(value: Decimal | None) -> str: | ||||
|     """Formats an amount for readability. | ||||
| 
 | ||||
|     :param value: The amount. | ||||
|     :return: The formatted amount text. | ||||
|     """ | ||||
|     if value is None or value == 0: | ||||
|         return "-" | ||||
|     whole: int = int(value) | ||||
|     frac: Decimal = (value - whole).normalize() | ||||
|     return "{:,}".format(whole) + str(frac)[1:] | ||||
| 
 | ||||
| 
 | ||||
| def format_amount_input(value: Decimal) -> str: | ||||
|     """Format an amount for an input value. | ||||
| 
 | ||||
| @@ -86,36 +68,6 @@ def format_amount_input(value: Decimal) -> str: | ||||
|     return str(whole) + str(frac)[1:] | ||||
| 
 | ||||
| 
 | ||||
| def format_date(value: date) -> str: | ||||
|     """Formats a date to be human-friendly. | ||||
| 
 | ||||
|     :param value: The date. | ||||
|     :return: The human-friendly date text. | ||||
|     """ | ||||
|     today: date = date.today() | ||||
|     if value == today: | ||||
|         return gettext("Today") | ||||
|     if value == today - timedelta(days=1): | ||||
|         return gettext("Yesterday") | ||||
|     if value == today + timedelta(days=1): | ||||
|         return gettext("Tomorrow") | ||||
|     locale = str(get_locale()) | ||||
|     if locale == "zh" or locale.startswith("zh_"): | ||||
|         if value == today - timedelta(days=2): | ||||
|             return gettext("The day before yesterday") | ||||
|         if value == today + timedelta(days=2): | ||||
|             return gettext("The day after tomorrow") | ||||
|     if locale == "zh" or locale.startswith("zh_"): | ||||
|         weekdays = ["一", "二", "三", "四", "五", "六", "日"] | ||||
|         weekday = weekdays[value.weekday()] | ||||
|     else: | ||||
|         weekday = value.strftime("%a") | ||||
|     if value.year != today.year: | ||||
|         return "{}/{}/{}({})".format( | ||||
|             value.year, value.month, value.day, weekday) | ||||
|     return "{}/{}({})".format(value.month, value.day, weekday) | ||||
| 
 | ||||
| 
 | ||||
| def text2html(value: str) -> str: | ||||
|     """Converts plain text into HTML. | ||||
| 
 | ||||
| @@ -126,20 +78,3 @@ def text2html(value: str) -> str: | ||||
|     s = s.replace("\n", "<br>") | ||||
|     s = s.replace("  ", "  ") | ||||
|     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,23 +35,17 @@ from accounting.utils.permission import has_permission, can_view, can_edit | ||||
| from accounting.utils.user import get_current_user_pk | ||||
| from .dispatcher import TransactionType, get_txn_type, TXN_TYPE_OBJ | ||||
| from .forms import sort_transactions_in, TransactionReorderForm | ||||
| from .query import get_transaction_query | ||||
| from .template import with_type, to_transfer, format_amount, \ | ||||
|     format_amount_input, format_date, text2html, currency_options, \ | ||||
|     default_currency_code | ||||
| from .queries import get_transaction_query | ||||
| from .template_filters import with_type, to_transfer, format_amount_input, \ | ||||
|     text2html | ||||
|  | ||||
| bp: Blueprint = Blueprint("transaction", __name__) | ||||
| """The view blueprint for the transaction management.""" | ||||
| bp.add_app_template_filter(with_type, "accounting_txn_with_type") | ||||
| bp.add_app_template_filter(to_transfer, "accounting_txn_to_transfer") | ||||
| bp.add_app_template_filter(format_amount, "accounting_txn_format_amount") | ||||
| bp.add_app_template_filter(format_amount_input, | ||||
|                            "accounting_txn_format_amount_input") | ||||
| bp.add_app_template_filter(format_date, "accounting_txn_format_date") | ||||
| bp.add_app_template_filter(text2html, "accounting_txn_text2html") | ||||
| bp.add_app_template_global(currency_options, "accounting_txn_currency_options") | ||||
| bp.add_app_template_global(default_currency_code, | ||||
|                            "accounting_txn_default_currency_code") | ||||
|  | ||||
|  | ||||
| @bp.get("", endpoint="list") | ||||
| @@ -192,8 +186,8 @@ def show_transaction_order(txn_date: date) -> str: | ||||
|     :param txn_date: The date. | ||||
|     :return: The order of the transactions in the date. | ||||
|     """ | ||||
|     transactions: list[Transaction] = Transaction.query\ | ||||
|         .filter(Transaction.date == txn_date)\ | ||||
|     transactions: list[Transaction] = Transaction.query \ | ||||
|         .filter(Transaction.date == txn_date) \ | ||||
|         .order_by(Transaction.no).all() | ||||
|     return render_template("accounting/transaction/order.html", | ||||
|                            date=txn_date, list=transactions) | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
| #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| #  See the License for the specific language governing permissions and | ||||
| #  limitations under the License. | ||||
| """The test for the summary helper. | ||||
| """The test for the summary editor. | ||||
| 
 | ||||
| """ | ||||
| import unittest | ||||
| @@ -29,8 +29,8 @@ from testlib import get_client | ||||
| from testlib_txn import Accounts, NEXT_URI, add_txn | ||||
| 
 | ||||
| 
 | ||||
| class SummeryHelperTestCase(unittest.TestCase): | ||||
|     """The summary helper test case.""" | ||||
| class SummeryEditorTestCase(unittest.TestCase): | ||||
|     """The summary editor test case.""" | ||||
| 
 | ||||
|     def setUp(self) -> None: | ||||
|         """Sets up the test. | ||||
| @@ -61,101 +61,101 @@ class SummeryHelperTestCase(unittest.TestCase): | ||||
| 
 | ||||
|         self.client, self.csrf_token = get_client(self.app, "editor") | ||||
| 
 | ||||
|     def test_summary_helper(self) -> None: | ||||
|         """Test the summary helper. | ||||
|     def test_summary_editor(self) -> None: | ||||
|         """Test the summary editor. | ||||
| 
 | ||||
|         :return: None. | ||||
|         """ | ||||
|         from accounting.transaction.summary_helper import SummaryHelper | ||||
|         from accounting.transaction.summary_editor import SummaryEditor | ||||
|         for form in get_form_data(self.csrf_token): | ||||
|             add_txn(self.client, form) | ||||
|         with self.app.app_context(): | ||||
|             helper: SummaryHelper = SummaryHelper() | ||||
|             editor: SummaryEditor = SummaryEditor() | ||||
| 
 | ||||
|         # Debit-General | ||||
|         self.assertEqual(len(helper.debit.general.tags), 2) | ||||
|         self.assertEqual(helper.debit.general.tags[0].name, "Lunch") | ||||
|         self.assertEqual(len(helper.debit.general.tags[0].accounts), 2) | ||||
|         self.assertEqual(helper.debit.general.tags[0].accounts[0].code, | ||||
|         self.assertEqual(len(editor.debit.general.tags), 2) | ||||
|         self.assertEqual(editor.debit.general.tags[0].name, "Lunch") | ||||
|         self.assertEqual(len(editor.debit.general.tags[0].accounts), 2) | ||||
|         self.assertEqual(editor.debit.general.tags[0].accounts[0].code, | ||||
|                          Accounts.MEAL) | ||||
|         self.assertEqual(helper.debit.general.tags[0].accounts[1].code, | ||||
|         self.assertEqual(editor.debit.general.tags[0].accounts[1].code, | ||||
|                          Accounts.PAYABLE) | ||||
|         self.assertEqual(helper.debit.general.tags[1].name, "Dinner") | ||||
|         self.assertEqual(len(helper.debit.general.tags[1].accounts), 2) | ||||
|         self.assertEqual(helper.debit.general.tags[1].accounts[0].code, | ||||
|         self.assertEqual(editor.debit.general.tags[1].name, "Dinner") | ||||
|         self.assertEqual(len(editor.debit.general.tags[1].accounts), 2) | ||||
|         self.assertEqual(editor.debit.general.tags[1].accounts[0].code, | ||||
|                          Accounts.MEAL) | ||||
|         self.assertEqual(helper.debit.general.tags[1].accounts[1].code, | ||||
|         self.assertEqual(editor.debit.general.tags[1].accounts[1].code, | ||||
|                          Accounts.PAYABLE) | ||||
| 
 | ||||
|         # Debit-Travel | ||||
|         self.assertEqual(len(helper.debit.travel.tags), 3) | ||||
|         self.assertEqual(helper.debit.travel.tags[0].name, "Bike") | ||||
|         self.assertEqual(len(helper.debit.travel.tags[0].accounts), 1) | ||||
|         self.assertEqual(helper.debit.travel.tags[0].accounts[0].code, | ||||
|         self.assertEqual(len(editor.debit.travel.tags), 3) | ||||
|         self.assertEqual(editor.debit.travel.tags[0].name, "Bike") | ||||
|         self.assertEqual(len(editor.debit.travel.tags[0].accounts), 1) | ||||
|         self.assertEqual(editor.debit.travel.tags[0].accounts[0].code, | ||||
|                          Accounts.TRAVEL) | ||||
|         self.assertEqual(helper.debit.travel.tags[1].name, "Taxi") | ||||
|         self.assertEqual(len(helper.debit.travel.tags[1].accounts), 1) | ||||
|         self.assertEqual(helper.debit.travel.tags[1].accounts[0].code, | ||||
|         self.assertEqual(editor.debit.travel.tags[1].name, "Taxi") | ||||
|         self.assertEqual(len(editor.debit.travel.tags[1].accounts), 1) | ||||
|         self.assertEqual(editor.debit.travel.tags[1].accounts[0].code, | ||||
|                          Accounts.TRAVEL) | ||||
|         self.assertEqual(helper.debit.travel.tags[2].name, "Airplane") | ||||
|         self.assertEqual(len(helper.debit.travel.tags[2].accounts), 1) | ||||
|         self.assertEqual(helper.debit.travel.tags[2].accounts[0].code, | ||||
|         self.assertEqual(editor.debit.travel.tags[2].name, "Airplane") | ||||
|         self.assertEqual(len(editor.debit.travel.tags[2].accounts), 1) | ||||
|         self.assertEqual(editor.debit.travel.tags[2].accounts[0].code, | ||||
|                          Accounts.TRAVEL) | ||||
| 
 | ||||
|         # Debit-Bus | ||||
|         self.assertEqual(len(helper.debit.bus.tags), 2) | ||||
|         self.assertEqual(helper.debit.bus.tags[0].name, "Train") | ||||
|         self.assertEqual(len(helper.debit.bus.tags[0].accounts), 1) | ||||
|         self.assertEqual(helper.debit.bus.tags[0].accounts[0].code, | ||||
|         self.assertEqual(len(editor.debit.bus.tags), 2) | ||||
|         self.assertEqual(editor.debit.bus.tags[0].name, "Train") | ||||
|         self.assertEqual(len(editor.debit.bus.tags[0].accounts), 1) | ||||
|         self.assertEqual(editor.debit.bus.tags[0].accounts[0].code, | ||||
|                          Accounts.TRAVEL) | ||||
|         self.assertEqual(helper.debit.bus.tags[1].name, "Bus") | ||||
|         self.assertEqual(len(helper.debit.bus.tags[1].accounts), 1) | ||||
|         self.assertEqual(helper.debit.bus.tags[1].accounts[0].code, | ||||
|         self.assertEqual(editor.debit.bus.tags[1].name, "Bus") | ||||
|         self.assertEqual(len(editor.debit.bus.tags[1].accounts), 1) | ||||
|         self.assertEqual(editor.debit.bus.tags[1].accounts[0].code, | ||||
|                          Accounts.TRAVEL) | ||||
| 
 | ||||
|         # Credit-General | ||||
|         self.assertEqual(len(helper.credit.general.tags), 2) | ||||
|         self.assertEqual(helper.credit.general.tags[0].name, "Lunch") | ||||
|         self.assertEqual(len(helper.credit.general.tags[0].accounts), 3) | ||||
|         self.assertEqual(helper.credit.general.tags[0].accounts[0].code, | ||||
|         self.assertEqual(len(editor.credit.general.tags), 2) | ||||
|         self.assertEqual(editor.credit.general.tags[0].name, "Lunch") | ||||
|         self.assertEqual(len(editor.credit.general.tags[0].accounts), 3) | ||||
|         self.assertEqual(editor.credit.general.tags[0].accounts[0].code, | ||||
|                          Accounts.PAYABLE) | ||||
|         self.assertEqual(helper.credit.general.tags[0].accounts[1].code, | ||||
|         self.assertEqual(editor.credit.general.tags[0].accounts[1].code, | ||||
|                          Accounts.BANK) | ||||
|         self.assertEqual(helper.credit.general.tags[0].accounts[2].code, | ||||
|         self.assertEqual(editor.credit.general.tags[0].accounts[2].code, | ||||
|                          Accounts.CASH) | ||||
|         self.assertEqual(helper.credit.general.tags[1].name, "Dinner") | ||||
|         self.assertEqual(len(helper.credit.general.tags[1].accounts), 2) | ||||
|         self.assertEqual(helper.credit.general.tags[1].accounts[0].code, | ||||
|         self.assertEqual(editor.credit.general.tags[1].name, "Dinner") | ||||
|         self.assertEqual(len(editor.credit.general.tags[1].accounts), 2) | ||||
|         self.assertEqual(editor.credit.general.tags[1].accounts[0].code, | ||||
|                          Accounts.BANK) | ||||
|         self.assertEqual(helper.credit.general.tags[1].accounts[1].code, | ||||
|         self.assertEqual(editor.credit.general.tags[1].accounts[1].code, | ||||
|                          Accounts.PAYABLE) | ||||
| 
 | ||||
|         # Credit-Travel | ||||
|         self.assertEqual(len(helper.credit.travel.tags), 2) | ||||
|         self.assertEqual(helper.credit.travel.tags[0].name, "Bike") | ||||
|         self.assertEqual(len(helper.credit.travel.tags[0].accounts), 2) | ||||
|         self.assertEqual(helper.credit.travel.tags[0].accounts[0].code, | ||||
|         self.assertEqual(len(editor.credit.travel.tags), 2) | ||||
|         self.assertEqual(editor.credit.travel.tags[0].name, "Bike") | ||||
|         self.assertEqual(len(editor.credit.travel.tags[0].accounts), 2) | ||||
|         self.assertEqual(editor.credit.travel.tags[0].accounts[0].code, | ||||
|                          Accounts.PAYABLE) | ||||
|         self.assertEqual(helper.credit.travel.tags[0].accounts[1].code, | ||||
|         self.assertEqual(editor.credit.travel.tags[0].accounts[1].code, | ||||
|                          Accounts.PREPAID) | ||||
|         self.assertEqual(helper.credit.travel.tags[1].name, "Taxi") | ||||
|         self.assertEqual(len(helper.credit.travel.tags[1].accounts), 2) | ||||
|         self.assertEqual(helper.credit.travel.tags[1].accounts[0].code, | ||||
|         self.assertEqual(editor.credit.travel.tags[1].name, "Taxi") | ||||
|         self.assertEqual(len(editor.credit.travel.tags[1].accounts), 2) | ||||
|         self.assertEqual(editor.credit.travel.tags[1].accounts[0].code, | ||||
|                          Accounts.PAYABLE) | ||||
|         self.assertEqual(helper.credit.travel.tags[1].accounts[1].code, | ||||
|         self.assertEqual(editor.credit.travel.tags[1].accounts[1].code, | ||||
|                          Accounts.CASH) | ||||
| 
 | ||||
|         # Credit-Bus | ||||
|         self.assertEqual(len(helper.credit.bus.tags), 2) | ||||
|         self.assertEqual(helper.credit.bus.tags[0].name, "Train") | ||||
|         self.assertEqual(len(helper.credit.bus.tags[0].accounts), 2) | ||||
|         self.assertEqual(helper.credit.bus.tags[0].accounts[0].code, | ||||
|         self.assertEqual(len(editor.credit.bus.tags), 2) | ||||
|         self.assertEqual(editor.credit.bus.tags[0].name, "Train") | ||||
|         self.assertEqual(len(editor.credit.bus.tags[0].accounts), 2) | ||||
|         self.assertEqual(editor.credit.bus.tags[0].accounts[0].code, | ||||
|                          Accounts.PREPAID) | ||||
|         self.assertEqual(helper.credit.bus.tags[0].accounts[1].code, | ||||
|         self.assertEqual(editor.credit.bus.tags[0].accounts[1].code, | ||||
|                          Accounts.PAYABLE) | ||||
|         self.assertEqual(helper.credit.bus.tags[1].name, "Bus") | ||||
|         self.assertEqual(len(helper.credit.bus.tags[1].accounts), 1) | ||||
|         self.assertEqual(helper.credit.bus.tags[1].accounts[0].code, | ||||
|         self.assertEqual(editor.credit.bus.tags[1].name, "Bus") | ||||
|         self.assertEqual(len(editor.credit.bus.tags[1].accounts), 1) | ||||
|         self.assertEqual(editor.credit.bus.tags[1].accounts[0].code, | ||||
|                          Accounts.PREPAID) | ||||
| 
 | ||||
| 
 | ||||
		Reference in New Issue
	
	Block a user