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