Compare commits
	
		
			36 Commits
		
	
	
		
			v0.9.0
			...
			bf2c7bb785
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| bf2c7bb785 | |||
| 93ba086548 | |||
| 5c4f6017b8 | |||
| cb16b2f0ff | |||
| d2f11e8779 | |||
| 4ccaf01b3c | |||
| 7c512b1c15 | |||
| dc432da398 | |||
| c8504bcbf5 | |||
| c865141583 | |||
| 8c1ecd6eac | |||
| e8e4100677 | |||
| 6a8773c531 | |||
| 30e0c7682c | |||
| eb5a7bef7e | |||
| 8a174d8847 | |||
| 7459afd63a | |||
| a9afc385e9 | |||
| a8be739ec7 | |||
| 0130bc58a9 | |||
| 821059fa80 | |||
| 5b4f57d0b3 | |||
| 4bfac2d545 | |||
| f105f0cf7b | |||
| 5e320729d7 | |||
| 7515032082 | |||
| 361b18e411 | |||
| 7d084e570e | |||
| cb397910f8 | |||
| 5f8b0dec98 | |||
| 8398d1e8bb | |||
| 562801692a | |||
| faee1e61c6 | |||
| 57a4177037 | |||
| fa1dedf207 | |||
| 7ed13dc0af | 
| @@ -13,7 +13,7 @@ sys.path.insert(0, os.path.abspath('../../src/')) | |||||||
| project = 'Mia! Accounting Flask' | project = 'Mia! Accounting Flask' | ||||||
| copyright = '2023, imacat' | copyright = '2023, imacat' | ||||||
| author = 'imacat' | author = 'imacat' | ||||||
| release = '0.9.0' | release = '0.9.1' | ||||||
|  |  | ||||||
| # -- General configuration --------------------------------------------------- | # -- General configuration --------------------------------------------------- | ||||||
| # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ | |||||||
|  |  | ||||||
| [metadata] | [metadata] | ||||||
| name = mia-accounting-flask | name = mia-accounting-flask | ||||||
| version = 0.9.0 | version = 0.9.1 | ||||||
| author = imacat | author = imacat | ||||||
| author_email = imacat@mail.imacat.idv.tw | author_email = imacat@mail.imacat.idv.tw | ||||||
| description = The Mia! Accounting Flask project. | description = The Mia! Accounting Flask project. | ||||||
|   | |||||||
| @@ -760,7 +760,7 @@ class JournalEntryLineItem(db.Model): | |||||||
|         setattr(self, "__net_balance", net_balance) |         setattr(self, "__net_balance", net_balance) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def query_values(self) -> tuple[list[str], list[str]]: |     def query_values(self) -> list[str]: | ||||||
|         """Returns the values to be queried. |         """Returns the values to be queried. | ||||||
|  |  | ||||||
|         :return: The values to be queried. |         :return: The values to be queried. | ||||||
| @@ -770,19 +770,11 @@ class JournalEntryLineItem(db.Model): | |||||||
|             frac: Decimal = (value - whole).normalize() |             frac: Decimal = (value - whole).normalize() | ||||||
|             return str(whole) + str(abs(frac))[1:] |             return str(whole) + str(abs(frac))[1:] | ||||||
|  |  | ||||||
|         journal_entry_day: date = self.journal_entry.date |         return ["{}/{}/{}".format(self.journal_entry.date.year, | ||||||
|         description: str = "" if self.description is None else self.description |                                   self.journal_entry.date.month, | ||||||
|         return ([description], |                                   self.journal_entry.date.day), | ||||||
|                 [str(journal_entry_day.year), |                 "" if self.description is None else self.description, | ||||||
|                  "{}/{}".format(journal_entry_day.year, |                 format_amount(self.amount)] | ||||||
|                                 journal_entry_day.month), |  | ||||||
|                  "{}/{}".format(journal_entry_day.month, |  | ||||||
|                                 journal_entry_day.day), |  | ||||||
|                  "{}/{}/{}".format(journal_entry_day.year, |  | ||||||
|                                    journal_entry_day.month, |  | ||||||
|                                    journal_entry_day.day), |  | ||||||
|                  format_amount(self.amount), |  | ||||||
|                  format_amount(self.net_balance)]) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Option(db.Model): | class Option(db.Model): | ||||||
|   | |||||||
| @@ -114,10 +114,19 @@ class AccountForm { | |||||||
|         }; |         }; | ||||||
|         this.#baseControl.onclick = () => { |         this.#baseControl.onclick = () => { | ||||||
|             this.#baseControl.classList.add("accounting-not-empty"); |             this.#baseControl.classList.add("accounting-not-empty"); | ||||||
|             this.#baseAccountSelector.onOpen(this.#baseCode.value); |             this.#baseAccountSelector.onOpen(); | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the base code. | ||||||
|  |      * | ||||||
|  |      * @return {string|null} | ||||||
|  |      */ | ||||||
|  |     get baseCode() { | ||||||
|  |         return this.#baseCode.value === ""? null: this.#baseCode.value; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The callback when the base account selector is closed. |      * The callback when the base account selector is closed. | ||||||
|      * |      * | ||||||
| @@ -129,15 +138,14 @@ class AccountForm { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Sets the base account. |      * Saves the selected base account. | ||||||
|      * |      * | ||||||
|      * @param code {string} the base account code |      * @param account {BaseAccountOption} the selected base account | ||||||
|      * @param text {string} the text for the base account |  | ||||||
|      */ |      */ | ||||||
|     setBaseAccount(code, text) { |     saveBaseAccount(account) { | ||||||
|         this.#baseCode.value = code; |         this.#baseCode.value = account.code; | ||||||
|         this.#base.innerText = text; |         this.#base.innerText = account.text; | ||||||
|         if (["1", "2", "3"].includes(code.substring(0, 1))) { |         if (["1", "2", "3"].includes(account.code.substring(0, 1))) { | ||||||
|             this.#isNeedOffsetControl.classList.remove("d-none"); |             this.#isNeedOffsetControl.classList.remove("d-none"); | ||||||
|             this.#isNeedOffset.disabled = false; |             this.#isNeedOffset.disabled = false; | ||||||
|         } else { |         } else { | ||||||
| @@ -225,7 +233,7 @@ class BaseAccountSelector { | |||||||
|      * The account form |      * The account form | ||||||
|      * @type {AccountForm} |      * @type {AccountForm} | ||||||
|      */ |      */ | ||||||
|     #form; |     form; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The selector modal |      * The selector modal | ||||||
| @@ -253,7 +261,7 @@ class BaseAccountSelector { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The options |      * The options | ||||||
|      * @type {HTMLLIElement[]} |      * @type {BaseAccountOption[]} | ||||||
|      */ |      */ | ||||||
|     #options; |     #options; | ||||||
|  |  | ||||||
| @@ -269,83 +277,54 @@ class BaseAccountSelector { | |||||||
|      * @param form {AccountForm} the form |      * @param form {AccountForm} the form | ||||||
|      */ |      */ | ||||||
|     constructor(form) { |     constructor(form) { | ||||||
|         this.#form = form; |         this.form = form; | ||||||
|         this.#modal = document.getElementById("accounting-base-selector-modal"); |         const prefix = "accounting-base-selector"; | ||||||
|         this.#query = document.getElementById("accounting-base-selector-query"); |         this.#modal = document.getElementById(`${prefix}-modal`); | ||||||
|         this.#optionList = document.getElementById("accounting-base-selector-option-list"); |         this.#query = document.getElementById(`${prefix}-query`); | ||||||
|         // noinspection JSValidateTypes |         this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`); | ||||||
|         this.#options = Array.from(document.getElementsByClassName("accounting-base-selector-option")); |         this.#optionList = document.getElementById(`${prefix}-option-list`); | ||||||
|         this.#clearButton = document.getElementById("accounting-base-selector-clear"); |         this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new BaseAccountOption(this, element)); | ||||||
|         this.#queryNoResult = document.getElementById("accounting-base-selector-option-no-result"); |         this.#clearButton = document.getElementById(`${prefix}-clear`); | ||||||
|         this.#modal.addEventListener("hidden.bs.modal", () => { |  | ||||||
|             this.#form.onBaseAccountSelectorClosed(); |         this.#modal.addEventListener("hidden.bs.modal", () => this.form.onBaseAccountSelectorClosed()); | ||||||
|         }); |         this.#query.oninput = () => this.#filterOptions(); | ||||||
|         for (const option of this.#options) { |         this.#clearButton.onclick = () => this.form.clearBaseAccount(); | ||||||
|             option.onclick = () => { |  | ||||||
|                 this.#form.setBaseAccount(option.dataset.code, option.dataset.text); |  | ||||||
|             }; |  | ||||||
|         } |  | ||||||
|         this.#clearButton.onclick = () => { |  | ||||||
|             this.#form.clearBaseAccount(); |  | ||||||
|         }; |  | ||||||
|         this.#initializeBaseAccountQuery(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Initializes the query. |      * Filters the options. | ||||||
|      * |      * | ||||||
|      */ |      */ | ||||||
|     #initializeBaseAccountQuery() { |     #filterOptions() { | ||||||
|         this.#query.addEventListener("input", () => { |         let isAnyMatched = false; | ||||||
|             if (this.#query.value === "") { |         for (const option of this.#options) { | ||||||
|                 for (const option of this.#options) { |             if (option.isMatched(this.#query.value)) { | ||||||
|                     option.classList.remove("d-none"); |                 option.setShown(true); | ||||||
|                 } |                 isAnyMatched = true; | ||||||
|                 this.#optionList.classList.remove("d-none"); |  | ||||||
|                 this.#queryNoResult.classList.add("d-none"); |  | ||||||
|                 return |  | ||||||
|             } |  | ||||||
|             let hasAnyMatched = false; |  | ||||||
|             for (const option of this.#options) { |  | ||||||
|                 const queryValues = JSON.parse(option.dataset.queryValues); |  | ||||||
|                 let isMatched = false; |  | ||||||
|                 for (const queryValue of queryValues) { |  | ||||||
|                     if (queryValue.toLowerCase().includes(this.#query.value.toLowerCase())) { |  | ||||||
|                         isMatched = true; |  | ||||||
|                         break; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 if (isMatched) { |  | ||||||
|                     option.classList.remove("d-none"); |  | ||||||
|                     hasAnyMatched = true; |  | ||||||
|                 } else { |  | ||||||
|                     option.classList.add("d-none"); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             if (!hasAnyMatched) { |  | ||||||
|                 this.#optionList.classList.add("d-none"); |  | ||||||
|                 this.#queryNoResult.classList.remove("d-none"); |  | ||||||
|             } else { |             } else { | ||||||
|                 this.#optionList.classList.remove("d-none"); |                 option.setShown(false); | ||||||
|                 this.#queryNoResult.classList.add("d-none"); |  | ||||||
|             } |             } | ||||||
|         }); |         } | ||||||
|  |         if (!isAnyMatched) { | ||||||
|  |             this.#optionList.classList.add("d-none"); | ||||||
|  |             this.#queryNoResult.classList.remove("d-none"); | ||||||
|  |         } else { | ||||||
|  |             this.#optionList.classList.remove("d-none"); | ||||||
|  |             this.#queryNoResult.classList.add("d-none"); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The callback when the base account selector is shown. |      * The callback when the base account selector is shown. | ||||||
|      * |      * | ||||||
|      * @param baseCode {string} the active base code |  | ||||||
|      */ |      */ | ||||||
|     onOpen(baseCode) { |     onOpen() { | ||||||
|  |         this.#query.value = ""; | ||||||
|  |         this.#filterOptions(); | ||||||
|         for (const option of this.#options) { |         for (const option of this.#options) { | ||||||
|             if (option.dataset.code === baseCode) { |             option.setActive(option.code === this.form.baseCode); | ||||||
|                 option.classList.add("active"); |  | ||||||
|             } else { |  | ||||||
|                 option.classList.remove("active"); |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|         if (baseCode === "") { |         if (this.form.baseCode === null) { | ||||||
|             this.#clearButton.classList.add("btn-secondary") |             this.#clearButton.classList.add("btn-secondary") | ||||||
|             this.#clearButton.classList.remove("btn-danger"); |             this.#clearButton.classList.remove("btn-danger"); | ||||||
|             this.#clearButton.disabled = true; |             this.#clearButton.disabled = true; | ||||||
| @@ -356,3 +335,100 @@ class BaseAccountSelector { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A base account option. | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | class BaseAccountOption { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The base account selector | ||||||
|  |      * @type {BaseAccountSelector} | ||||||
|  |      */ | ||||||
|  |     #selector; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The element | ||||||
|  |      * @type {HTMLLIElement} | ||||||
|  |      */ | ||||||
|  |     #element; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The account code | ||||||
|  |      * @type {string} | ||||||
|  |      */ | ||||||
|  |     code; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The account text | ||||||
|  |      * @type {string} | ||||||
|  |      */ | ||||||
|  |     text; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The values to query against | ||||||
|  |      * @type {string[]} | ||||||
|  |      */ | ||||||
|  |     #queryValues; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Constructs the account in the base account selector. | ||||||
|  |      * | ||||||
|  |      * @param selector {BaseAccountSelector} the base account selector | ||||||
|  |      * @param element {HTMLLIElement} the element | ||||||
|  |      */ | ||||||
|  |     constructor(selector, element) { | ||||||
|  |         this.#selector = selector; | ||||||
|  |         this.#element = element; | ||||||
|  |         this.code = element.dataset.code; | ||||||
|  |         this.text = element.dataset.text; | ||||||
|  |         this.#queryValues = JSON.parse(element.dataset.queryValues); | ||||||
|  |  | ||||||
|  |         this.#element.onclick = () => this.#selector.form.saveBaseAccount(this); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns whether the account matches the query. | ||||||
|  |      * | ||||||
|  |      * @param query {string} the query term | ||||||
|  |      * @return {boolean} true if the option matches, or false otherwise | ||||||
|  |      */ | ||||||
|  |     isMatched(query) { | ||||||
|  |         if (query === "") { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         for (const queryValue of this.#queryValues) { | ||||||
|  |             if (queryValue.toLowerCase().includes(query.toLowerCase())) { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Sets whether the option is shown. | ||||||
|  |      * | ||||||
|  |      * @param isShown {boolean} true to show, or false otherwise | ||||||
|  |      */ | ||||||
|  |     setShown(isShown) { | ||||||
|  |         if (isShown) { | ||||||
|  |             this.#element.classList.remove("d-none"); | ||||||
|  |         } else { | ||||||
|  |             this.#element.classList.add("d-none"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Sets whether the option is active. | ||||||
|  |      * | ||||||
|  |      * @param isActive {boolean} true if active, or false otherwise | ||||||
|  |      */ | ||||||
|  |     setActive(isActive) { | ||||||
|  |         if (isActive) { | ||||||
|  |             this.#element.classList.add("active"); | ||||||
|  |         } else { | ||||||
|  |             this.#element.classList.remove("active"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,224 +0,0 @@ | |||||||
| /* The Mia! Accounting Flask Project |  | ||||||
|  * account-selector.js: The JavaScript for the account selector |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| /*  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 |  | ||||||
|  */ |  | ||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * The account selector. |  | ||||||
|  * |  | ||||||
|  */ |  | ||||||
| class AccountSelector { |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The line item editor |  | ||||||
|      * @type {JournalEntryLineItemEditor} |  | ||||||
|      */ |  | ||||||
|     #lineItemEditor; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Either "debit" or "credit" |  | ||||||
|      * @type {string} |  | ||||||
|      */ |  | ||||||
|     #debitCredit; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The prefix of the HTML ID and class |  | ||||||
|      * @type {string} |  | ||||||
|      */ |  | ||||||
|     #prefix; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The button to clear the account |  | ||||||
|      * @type {HTMLButtonElement} |  | ||||||
|      */ |  | ||||||
|     #clearButton |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The query input |  | ||||||
|      * @type {HTMLInputElement} |  | ||||||
|      */ |  | ||||||
|     #query; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The error message when the query has no result |  | ||||||
|      * @type {HTMLParagraphElement} |  | ||||||
|      */ |  | ||||||
|     #queryNoResult; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The option list |  | ||||||
|      * @type {HTMLUListElement} |  | ||||||
|      */ |  | ||||||
|     #optionList; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The options |  | ||||||
|      * @type {HTMLLIElement[]} |  | ||||||
|      */ |  | ||||||
|     #options; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The more item to show all accounts |  | ||||||
|      * @type {HTMLLIElement} |  | ||||||
|      */ |  | ||||||
|     #more; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Constructs an account selector. |  | ||||||
|      * |  | ||||||
|      * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor |  | ||||||
|      * @param debitCredit {string} either "debit" or "credit" |  | ||||||
|      */ |  | ||||||
|     constructor(lineItemEditor, debitCredit) { |  | ||||||
|         this.#lineItemEditor = lineItemEditor |  | ||||||
|         this.#debitCredit = debitCredit; |  | ||||||
|         this.#prefix = "accounting-account-selector-" + debitCredit; |  | ||||||
|         this.#query = document.getElementById(this.#prefix + "-query"); |  | ||||||
|         this.#queryNoResult = document.getElementById(this.#prefix + "-option-no-result"); |  | ||||||
|         this.#optionList = document.getElementById(this.#prefix + "-option-list"); |  | ||||||
|         // noinspection JSValidateTypes |  | ||||||
|         this.#options = Array.from(document.getElementsByClassName(this.#prefix + "-option")); |  | ||||||
|         this.#more = document.getElementById(this.#prefix + "-more"); |  | ||||||
|         this.#clearButton = document.getElementById(this.#prefix + "-btn-clear"); |  | ||||||
|         this.#more.onclick = () => { |  | ||||||
|             this.#more.classList.add("d-none"); |  | ||||||
|             this.#filterOptions(); |  | ||||||
|         }; |  | ||||||
|         this.#clearButton.onclick = () => this.#lineItemEditor.clearAccount(); |  | ||||||
|         for (const option of this.#options) { |  | ||||||
|             option.onclick = () => this.#lineItemEditor.saveAccount(option.dataset.code, option.dataset.text, option.classList.contains("accounting-account-is-need-offset")); |  | ||||||
|         } |  | ||||||
|         this.#query.addEventListener("input", () => { |  | ||||||
|             this.#filterOptions(); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Filters the options. |  | ||||||
|      * |  | ||||||
|      */ |  | ||||||
|     #filterOptions() { |  | ||||||
|         const codesInUse = this.#getCodesUsedInForm(); |  | ||||||
|         let shouldAnyShow = false; |  | ||||||
|         for (const option of this.#options) { |  | ||||||
|             const shouldShow = this.#shouldOptionShow(option, this.#more, codesInUse, this.#query); |  | ||||||
|             if (shouldShow) { |  | ||||||
|                 option.classList.remove("d-none"); |  | ||||||
|                 shouldAnyShow = true; |  | ||||||
|             } else { |  | ||||||
|                 option.classList.add("d-none"); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         if (!shouldAnyShow && this.#more.classList.contains("d-none")) { |  | ||||||
|             this.#optionList.classList.add("d-none"); |  | ||||||
|             this.#queryNoResult.classList.remove("d-none"); |  | ||||||
|         } else { |  | ||||||
|             this.#optionList.classList.remove("d-none"); |  | ||||||
|             this.#queryNoResult.classList.add("d-none"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Returns the account codes that are used in the form. |  | ||||||
|      * |  | ||||||
|      * @return {string[]} the account codes that are used in the form |  | ||||||
|      */ |  | ||||||
|     #getCodesUsedInForm() { |  | ||||||
|         const inUse = this.#lineItemEditor.form.getAccountCodesUsed(this.#debitCredit); |  | ||||||
|         if (this.#lineItemEditor.accountCode !== null) { |  | ||||||
|             inUse.push(this.#lineItemEditor.accountCode); |  | ||||||
|         } |  | ||||||
|         return inUse |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Returns whether an option should show. |  | ||||||
|      * |  | ||||||
|      * @param option {HTMLLIElement} the option |  | ||||||
|      * @param more {HTMLLIElement} the more element |  | ||||||
|      * @param inUse {string[]} the account codes that are used in the form |  | ||||||
|      * @param query {HTMLInputElement} the query element, if any |  | ||||||
|      * @return {boolean} true if the option should show, or false otherwise |  | ||||||
|      */ |  | ||||||
|     #shouldOptionShow(option, more, inUse, query) { |  | ||||||
|         const isQueryMatched = () => { |  | ||||||
|             if (query.value === "") { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|             const queryValues = JSON.parse(option.dataset.queryValues); |  | ||||||
|             for (const queryValue of queryValues) { |  | ||||||
|                 if (queryValue.toLowerCase().includes(query.value.toLowerCase())) { |  | ||||||
|                     return true; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             return false; |  | ||||||
|         }; |  | ||||||
|         const isMoreMatched = () => { |  | ||||||
|             if (more.classList.contains("d-none")) { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|             return option.classList.contains("accounting-account-in-use") || inUse.includes(option.dataset.code); |  | ||||||
|         }; |  | ||||||
|         return isMoreMatched() && isQueryMatched(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The callback when the account selector is shown. |  | ||||||
|      * |  | ||||||
|      */ |  | ||||||
|     onOpen() { |  | ||||||
|         this.#query.value = ""; |  | ||||||
|         this.#more.classList.remove("d-none"); |  | ||||||
|         this.#filterOptions(); |  | ||||||
|         for (const option of this.#options) { |  | ||||||
|             if (option.dataset.code === this.#lineItemEditor.accountCode) { |  | ||||||
|                 option.classList.add("active"); |  | ||||||
|             } else { |  | ||||||
|                 option.classList.remove("active"); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         if (this.#lineItemEditor.accountCode === null) { |  | ||||||
|             this.#clearButton.classList.add("btn-secondary"); |  | ||||||
|             this.#clearButton.classList.remove("btn-danger"); |  | ||||||
|             this.#clearButton.disabled = true; |  | ||||||
|         } else { |  | ||||||
|             this.#clearButton.classList.add("btn-danger"); |  | ||||||
|             this.#clearButton.classList.remove("btn-secondary"); |  | ||||||
|             this.#clearButton.disabled = false; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Returns the account selector instances. |  | ||||||
|      * |  | ||||||
|      * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor |  | ||||||
|      * @return {{debit: AccountSelector, credit: AccountSelector}} |  | ||||||
|      */ |  | ||||||
|     static getInstances(lineItemEditor) { |  | ||||||
|         const selectors = {} |  | ||||||
|         const modals = Array.from(document.getElementsByClassName("accounting-account-selector")); |  | ||||||
|         for (const modal of modals) { |  | ||||||
|             selectors[modal.dataset.debitCredit] = new AccountSelector(lineItemEditor, modal.dataset.debitCredit); |  | ||||||
|         } |  | ||||||
|         return selectors; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -41,7 +41,7 @@ class DescriptionEditor { | |||||||
|     #form; |     #form; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The prefix of the HTML ID and class |      * The prefix of the HTML ID and class names | ||||||
|      * @type {string} |      * @type {string} | ||||||
|      */ |      */ | ||||||
|     prefix; |     prefix; | ||||||
| @@ -278,7 +278,7 @@ class TabPlane { | |||||||
|     editor; |     editor; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The prefix of the HTML ID and classes |      * The prefix of the HTML ID and class names | ||||||
|      * @type {string} |      * @type {string} | ||||||
|      */ |      */ | ||||||
|     prefix; |     prefix; | ||||||
| @@ -984,7 +984,6 @@ class RecurringTransactionTab extends TabPlane { | |||||||
|      */ |      */ | ||||||
|     #itemButtons; |     #itemButtons; | ||||||
|  |  | ||||||
|     // noinspection JSValidateTypes |  | ||||||
|     /** |     /** | ||||||
|      * Constructs a tab plane. |      * Constructs a tab plane. | ||||||
|      * |      * | ||||||
| @@ -1019,7 +1018,7 @@ class RecurringTransactionTab extends TabPlane { | |||||||
|      * @return {string} the description of the recurring item |      * @return {string} the description of the recurring item | ||||||
|      */ |      */ | ||||||
|     #getDescription(itemButton) { |     #getDescription(itemButton) { | ||||||
|         const today = new Date(this.editor.lineItemEditor.form.getDate()); |         const today = new Date(this.editor.lineItemEditor.form.date); | ||||||
|         const thisMonth = today.getMonth() + 1; |         const thisMonth = today.getMonth() + 1; | ||||||
|         const lastMonth = (thisMonth + 10) % 12 + 1; |         const lastMonth = (thisMonth + 10) % 12 + 1; | ||||||
|         const lastBimonthlyFrom = ((thisMonth + thisMonth % 2 + 8) % 12 + 1); |         const lastBimonthlyFrom = ((thisMonth + thisMonth % 2 + 8) % 12 + 1); | ||||||
|   | |||||||
							
								
								
									
										319
									
								
								src/accounting/static/js/journal-entry-account-selector.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										319
									
								
								src/accounting/static/js/journal-entry-account-selector.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,319 @@ | |||||||
|  | /* The Mia! Accounting Flask Project | ||||||
|  |  * journal-entry-account-selector.js: The JavaScript for the account selector of the journal entry form | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /*  Copyright (c) 2023 imacat. | ||||||
|  |  * | ||||||
|  |  *  Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |  *  you may not use this file except in compliance with the License. | ||||||
|  |  *  You may obtain a copy of the License at | ||||||
|  |  * | ||||||
|  |  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  * | ||||||
|  |  *  Unless required by applicable law or agreed to in writing, software | ||||||
|  |  *  distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |  *  See the License for the specific language governing permissions and | ||||||
|  |  *  limitations under the License. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /* Author: imacat@mail.imacat.idv.tw (imacat) | ||||||
|  |  * First written: 2023/2/28 | ||||||
|  |  */ | ||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * The account selector. | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | class JournalEntryAccountSelector { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The line item editor | ||||||
|  |      * @type {JournalEntryLineItemEditor} | ||||||
|  |      */ | ||||||
|  |     lineItemEditor; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Either "debit" or "credit" | ||||||
|  |      * @type {string} | ||||||
|  |      */ | ||||||
|  |     #debitCredit; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The button to clear the account | ||||||
|  |      * @type {HTMLButtonElement} | ||||||
|  |      */ | ||||||
|  |     #clearButton | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The query input | ||||||
|  |      * @type {HTMLInputElement} | ||||||
|  |      */ | ||||||
|  |     #query; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The error message when the query has no result | ||||||
|  |      * @type {HTMLParagraphElement} | ||||||
|  |      */ | ||||||
|  |     #queryNoResult; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The option list | ||||||
|  |      * @type {HTMLUListElement} | ||||||
|  |      */ | ||||||
|  |     #optionList; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The options | ||||||
|  |      * @type {JournalEntryAccountOption[]} | ||||||
|  |      */ | ||||||
|  |     #options; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The more item to show all accounts | ||||||
|  |      * @type {HTMLLIElement} | ||||||
|  |      */ | ||||||
|  |     #more; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Whether to show all accounts | ||||||
|  |      * @type {boolean} | ||||||
|  |      */ | ||||||
|  |     #isShowMore = false; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Constructs an account selector. | ||||||
|  |      * | ||||||
|  |      * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor | ||||||
|  |      * @param debitCredit {string} either "debit" or "credit" | ||||||
|  |      */ | ||||||
|  |     constructor(lineItemEditor, debitCredit) { | ||||||
|  |         this.lineItemEditor = lineItemEditor | ||||||
|  |         this.#debitCredit = debitCredit; | ||||||
|  |         const prefix = "accounting-account-selector-" + debitCredit; | ||||||
|  |         this.#query = document.getElementById(prefix + "-query"); | ||||||
|  |         this.#queryNoResult = document.getElementById(prefix + "-option-no-result"); | ||||||
|  |         this.#optionList = document.getElementById(prefix + "-option-list"); | ||||||
|  |         this.#options = Array.from(document.getElementsByClassName(prefix + "-option")).map((element) => new JournalEntryAccountOption(this, element)); | ||||||
|  |         this.#more = document.getElementById(prefix + "-more"); | ||||||
|  |         this.#clearButton = document.getElementById(prefix + "-btn-clear"); | ||||||
|  |  | ||||||
|  |         this.#more.onclick = () => { | ||||||
|  |             this.#isShowMore = true; | ||||||
|  |             this.#more.classList.add("d-none"); | ||||||
|  |             this.#filterOptions(); | ||||||
|  |         }; | ||||||
|  |         this.#query.oninput = () => this.#filterOptions(); | ||||||
|  |         this.#clearButton.onclick = () => this.lineItemEditor.clearAccount(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Filters the options. | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     #filterOptions() { | ||||||
|  |         const codesInUse = this.#getCodesUsedInForm(); | ||||||
|  |         let isAnyMatched = false; | ||||||
|  |         for (const option of this.#options) { | ||||||
|  |             if (option.isMatched(this.#isShowMore, codesInUse, this.#query.value)) { | ||||||
|  |                 option.setShown(true); | ||||||
|  |                 isAnyMatched = true; | ||||||
|  |             } else { | ||||||
|  |                 option.setShown(false); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (!isAnyMatched) { | ||||||
|  |             this.#optionList.classList.add("d-none"); | ||||||
|  |             this.#queryNoResult.classList.remove("d-none"); | ||||||
|  |         } else { | ||||||
|  |             this.#optionList.classList.remove("d-none"); | ||||||
|  |             this.#queryNoResult.classList.add("d-none"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the account codes that are used in the form. | ||||||
|  |      * | ||||||
|  |      * @return {string[]} the account codes that are used in the form | ||||||
|  |      */ | ||||||
|  |     #getCodesUsedInForm() { | ||||||
|  |         const inUse = this.lineItemEditor.form.getAccountCodesUsed(this.#debitCredit); | ||||||
|  |         if (this.lineItemEditor.accountCode !== null) { | ||||||
|  |             inUse.push(this.lineItemEditor.accountCode); | ||||||
|  |         } | ||||||
|  |         return inUse | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The callback when the account selector is shown. | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     onOpen() { | ||||||
|  |         this.#query.value = ""; | ||||||
|  |         this.#isShowMore = false; | ||||||
|  |         this.#more.classList.remove("d-none"); | ||||||
|  |         this.#filterOptions(); | ||||||
|  |         for (const option of this.#options) { | ||||||
|  |             option.setActive(option.code === this.lineItemEditor.accountCode); | ||||||
|  |         } | ||||||
|  |         if (this.lineItemEditor.accountCode === null) { | ||||||
|  |             this.#clearButton.classList.add("btn-secondary"); | ||||||
|  |             this.#clearButton.classList.remove("btn-danger"); | ||||||
|  |             this.#clearButton.disabled = true; | ||||||
|  |         } else { | ||||||
|  |             this.#clearButton.classList.add("btn-danger"); | ||||||
|  |             this.#clearButton.classList.remove("btn-secondary"); | ||||||
|  |             this.#clearButton.disabled = false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the account selector instances. | ||||||
|  |      * | ||||||
|  |      * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor | ||||||
|  |      * @return {{debit: JournalEntryAccountSelector, credit: JournalEntryAccountSelector}} | ||||||
|  |      */ | ||||||
|  |     static getInstances(lineItemEditor) { | ||||||
|  |         const selectors = {} | ||||||
|  |         const modals = Array.from(document.getElementsByClassName("accounting-account-selector")); | ||||||
|  |         for (const modal of modals) { | ||||||
|  |             selectors[modal.dataset.debitCredit] = new JournalEntryAccountSelector(lineItemEditor, modal.dataset.debitCredit); | ||||||
|  |         } | ||||||
|  |         return selectors; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * An account option | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | class JournalEntryAccountOption { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The account selector | ||||||
|  |      * @type {JournalEntryAccountSelector} | ||||||
|  |      */ | ||||||
|  |     #selector; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The element | ||||||
|  |      * @type {HTMLLIElement} | ||||||
|  |      */ | ||||||
|  |     #element; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The account code | ||||||
|  |      * @type {string} | ||||||
|  |      */ | ||||||
|  |     code; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The account text | ||||||
|  |      * @type {string} | ||||||
|  |      */ | ||||||
|  |     text; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Whether the account is in use | ||||||
|  |      * @type {boolean} | ||||||
|  |      */ | ||||||
|  |     #isInUse; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Whether line items in the account need offset | ||||||
|  |      * @type {boolean} | ||||||
|  |      */ | ||||||
|  |     isNeedOffset; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The values to query against | ||||||
|  |      * @type {string[]} | ||||||
|  |      */ | ||||||
|  |     #queryValues; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Constructs the account in the account selector. | ||||||
|  |      * | ||||||
|  |      * @param selector {JournalEntryAccountSelector} the account selector | ||||||
|  |      * @param element {HTMLLIElement} the element | ||||||
|  |      */ | ||||||
|  |     constructor(selector, element) { | ||||||
|  |         this.#selector = selector; | ||||||
|  |         this.#element = element; | ||||||
|  |         this.code = element.dataset.code; | ||||||
|  |         this.text = element.dataset.text; | ||||||
|  |         this.#isInUse = element.classList.contains("accounting-account-is-in-use"); | ||||||
|  |         this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset"); | ||||||
|  |         this.#queryValues = JSON.parse(element.dataset.queryValues); | ||||||
|  |  | ||||||
|  |         this.#element.onclick = () => this.#selector.lineItemEditor.saveAccount(this); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns whether the account matches the query. | ||||||
|  |      * | ||||||
|  |      * @param isShowMore {boolean} true to show all accounts, or false to show only those in use | ||||||
|  |      * @param codesInUse {string[]} the account codes that are used in the form | ||||||
|  |      * @param query {string} the query term | ||||||
|  |      * @return {boolean} true if the option matches, or false otherwise | ||||||
|  |      */ | ||||||
|  |     isMatched(isShowMore, codesInUse, query) { | ||||||
|  |         return this.#isInUseMatched(isShowMore, codesInUse) && this.#isQueryMatched(query); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns whether the account matches the "in-use" condition. | ||||||
|  |      * | ||||||
|  |      * @param isShowMore {boolean} true to show all accounts, or false to show only those in use | ||||||
|  |      * @param codesInUse {string[]} the account codes that are used in the form | ||||||
|  |      * @return {boolean} true if the option matches, or false otherwise | ||||||
|  |      */ | ||||||
|  |     #isInUseMatched(isShowMore, codesInUse) { | ||||||
|  |         return isShowMore || this.#isInUse || codesInUse.includes(this.code); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns whether the account matches the query term. | ||||||
|  |      * | ||||||
|  |      * @param query {string} the query term | ||||||
|  |      * @return {boolean} true if the option matches, or false otherwise | ||||||
|  |      */ | ||||||
|  |     #isQueryMatched(query) { | ||||||
|  |         if (query === "") { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         for (const queryValue of this.#queryValues) { | ||||||
|  |             if (queryValue.toLowerCase().includes(query.toLowerCase())) { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Sets whether the option is shown. | ||||||
|  |      * | ||||||
|  |      * @param isShown {boolean} true to show, or false otherwise | ||||||
|  |      */ | ||||||
|  |     setShown(isShown) { | ||||||
|  |         if (isShown) { | ||||||
|  |             this.#element.classList.remove("d-none"); | ||||||
|  |         } else { | ||||||
|  |             this.#element.classList.add("d-none"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Sets whether the option is active. | ||||||
|  |      * | ||||||
|  |      * @param isActive {boolean} true if active, or false otherwise | ||||||
|  |      */ | ||||||
|  |     setActive(isActive) { | ||||||
|  |         if (isActive) { | ||||||
|  |             this.#element.classList.add("active"); | ||||||
|  |         } else { | ||||||
|  |             this.#element.classList.remove("active"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -111,6 +111,7 @@ class JournalEntryForm { | |||||||
|     constructor() { |     constructor() { | ||||||
|         this.#element = document.getElementById("accounting-form"); |         this.#element = document.getElementById("accounting-form"); | ||||||
|         this.lineItemTemplate = this.#element.dataset.lineItemTemplate; |         this.lineItemTemplate = this.#element.dataset.lineItemTemplate; | ||||||
|  |         this.lineItemEditor = new JournalEntryLineItemEditor(this); | ||||||
|         this.#date = document.getElementById("accounting-date"); |         this.#date = document.getElementById("accounting-date"); | ||||||
|         this.#dateError = document.getElementById("accounting-date-error"); |         this.#dateError = document.getElementById("accounting-date-error"); | ||||||
|         this.#currencyControl = document.getElementById("accounting-currencies"); |         this.#currencyControl = document.getElementById("accounting-currencies"); | ||||||
| @@ -121,7 +122,6 @@ class JournalEntryForm { | |||||||
|         this.#addCurrencyButton = document.getElementById("accounting-add-currency"); |         this.#addCurrencyButton = document.getElementById("accounting-add-currency"); | ||||||
|         this.#note = document.getElementById("accounting-note"); |         this.#note = document.getElementById("accounting-note"); | ||||||
|         this.#noteError = document.getElementById("accounting-note-error"); |         this.#noteError = document.getElementById("accounting-note-error"); | ||||||
|         this.lineItemEditor = new JournalEntryLineItemEditor(this); |  | ||||||
|  |  | ||||||
|         this.#addCurrencyButton.onclick = () => { |         this.#addCurrencyButton.onclick = () => { | ||||||
|             const newIndex = 1 + (this.#currencies.length === 0? 0: Math.max(...this.#currencies.map((currency) => currency.index))); |             const newIndex = 1 + (this.#currencies.length === 0? 0: Math.max(...this.#currencies.map((currency) => currency.index))); | ||||||
| @@ -159,7 +159,7 @@ class JournalEntryForm { | |||||||
|      */ |      */ | ||||||
|     #resetDeleteCurrencyButtons() { |     #resetDeleteCurrencyButtons() { | ||||||
|         if (this.#currencies.length === 1) { |         if (this.#currencies.length === 1) { | ||||||
|             this.#currencies[0].deleteButton.classList.add("d-none"); |             this.#currencies[0].setDeleteButtonShown(false); | ||||||
|         } else { |         } else { | ||||||
|             for (const currency of this.#currencies) { |             for (const currency of this.#currencies) { | ||||||
|                 let isAnyLineItemMatched = false; |                 let isAnyLineItemMatched = false; | ||||||
| @@ -169,11 +169,7 @@ class JournalEntryForm { | |||||||
|                         break; |                         break; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 if (isAnyLineItemMatched) { |                 currency.setDeleteButtonShown(!isAnyLineItemMatched); | ||||||
|                     currency.deleteButton.classList.add("d-none"); |  | ||||||
|                 } else { |  | ||||||
|                     currency.deleteButton.classList.remove("d-none"); |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -184,10 +180,8 @@ class JournalEntryForm { | |||||||
|      */ |      */ | ||||||
|     #initializeDragAndDropReordering() { |     #initializeDragAndDropReordering() { | ||||||
|         initializeDragAndDropReordering(this.#currencyList, () => { |         initializeDragAndDropReordering(this.#currencyList, () => { | ||||||
|             const currencyId = Array.from(this.#currencyList.children).map((currency) => currency.id); |             for (const currency of this.#currencies) { | ||||||
|             this.#currencies.sort((a, b) => currencyId.indexOf(a.element.id) - currencyId.indexOf(b.element.id)); |                 currency.resetNo(); | ||||||
|             for (let i = 0; i < this.#currencies.length; i++) { |  | ||||||
|                 this.#currencies[i].no.value = String(i + 1); |  | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| @@ -213,7 +207,7 @@ class JournalEntryForm { | |||||||
|      * @return {string[]} the account codes used in the form |      * @return {string[]} the account codes used in the form | ||||||
|      */ |      */ | ||||||
|     getAccountCodesUsed(debitCredit) { |     getAccountCodesUsed(debitCredit) { | ||||||
|         return this.getLineItems(debitCredit).map((lineItem) => lineItem.getAccountCode()) |         return this.getLineItems(debitCredit).map((lineItem) => lineItem.accountCode) | ||||||
|             .filter((code) => code !== null); |             .filter((code) => code !== null); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -222,7 +216,7 @@ class JournalEntryForm { | |||||||
|      * |      * | ||||||
|      * @return {string} the date |      * @return {string} the date | ||||||
|      */ |      */ | ||||||
|     getDate() { |     get date() { | ||||||
|         return this.#date.value; |         return this.#date.value; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -233,7 +227,7 @@ class JournalEntryForm { | |||||||
|     updateMinDate() { |     updateMinDate() { | ||||||
|         let lastOriginalLineItemDate = null; |         let lastOriginalLineItemDate = null; | ||||||
|         for (const lineItem of this.getLineItems()) { |         for (const lineItem of this.getLineItems()) { | ||||||
|             const date = lineItem.getOriginalLineItemDate(); |             const date = lineItem.originalLineItemDate; | ||||||
|             if (date !== null) { |             if (date !== null) { | ||||||
|                 if (lastOriginalLineItemDate === null || lastOriginalLineItemDate < date) { |                 if (lastOriginalLineItemDate === null || lastOriginalLineItemDate < date) { | ||||||
|                     lastOriginalLineItemDate = date; |                     lastOriginalLineItemDate = date; | ||||||
| @@ -349,7 +343,7 @@ class CurrencySubForm { | |||||||
|      * The element |      * The element | ||||||
|      * @type {HTMLDivElement} |      * @type {HTMLDivElement} | ||||||
|      */ |      */ | ||||||
|     element; |     #element; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The journal entry form |      * The journal entry form | ||||||
| @@ -363,12 +357,6 @@ class CurrencySubForm { | |||||||
|      */ |      */ | ||||||
|     index; |     index; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The prefix of the HTML ID and class |  | ||||||
|      * @type {string} |  | ||||||
|      */ |  | ||||||
|     #prefix; |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The control |      * The control | ||||||
|      * @type {HTMLDivElement} |      * @type {HTMLDivElement} | ||||||
| @@ -385,7 +373,7 @@ class CurrencySubForm { | |||||||
|      * The number |      * The number | ||||||
|      * @type {HTMLInputElement} |      * @type {HTMLInputElement} | ||||||
|      */ |      */ | ||||||
|     no; |     #no; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The currency code |      * The currency code | ||||||
| @@ -403,7 +391,7 @@ class CurrencySubForm { | |||||||
|      * The button to delete the currency |      * The button to delete the currency | ||||||
|      * @type {HTMLButtonElement} |      * @type {HTMLButtonElement} | ||||||
|      */ |      */ | ||||||
|     deleteButton; |     #deleteButton; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The debit sub-form |      * The debit sub-form | ||||||
| @@ -424,36 +412,58 @@ class CurrencySubForm { | |||||||
|      * @param element {HTMLDivElement} the currency sub-form element |      * @param element {HTMLDivElement} the currency sub-form element | ||||||
|      */ |      */ | ||||||
|     constructor(form, element) { |     constructor(form, element) { | ||||||
|         this.element = element; |         this.#element = element; | ||||||
|         this.form = form; |         this.form = form; | ||||||
|         this.index = parseInt(this.element.dataset.index); |         this.index = parseInt(this.#element.dataset.index); | ||||||
|         this.#prefix = "accounting-currency-" + String(this.index); |         const prefix = "accounting-currency-" + String(this.index); | ||||||
|         this.#control = document.getElementById(this.#prefix + "-control"); |         this.#control = document.getElementById(prefix + "-control"); | ||||||
|         this.#error = document.getElementById(this.#prefix + "-error"); |         this.#error = document.getElementById(prefix + "-error"); | ||||||
|         this.no = document.getElementById(this.#prefix + "-no"); |         this.#no = document.getElementById(prefix + "-no"); | ||||||
|         this.#code = document.getElementById(this.#prefix + "-code"); |         this.#code = document.getElementById(prefix + "-code"); | ||||||
|         this.#codeSelect = document.getElementById(this.#prefix + "-code-select"); |         this.#codeSelect = document.getElementById(prefix + "-code-select"); | ||||||
|         this.deleteButton = document.getElementById(this.#prefix + "-delete"); |         this.#deleteButton = document.getElementById(prefix + "-delete"); | ||||||
|         const debitElement = document.getElementById(this.#prefix + "-debit"); |         const debitElement = document.getElementById(prefix + "-debit"); | ||||||
|         this.#debit = debitElement === null? null: new DebitCreditSubForm(this, debitElement, "debit"); |         this.#debit = debitElement === null? null: new DebitCreditSubForm(this, debitElement, "debit"); | ||||||
|         const creditElement = document.getElementById(this.#prefix + "-credit"); |         const creditElement = document.getElementById(prefix + "-credit"); | ||||||
|         this.#credit = creditElement == null? null: new DebitCreditSubForm(this, creditElement, "credit"); |         this.#credit = creditElement == null? null: new DebitCreditSubForm(this, creditElement, "credit"); | ||||||
|         this.#codeSelect.onchange = () => this.#code.value = this.#codeSelect.value; |         this.#codeSelect.onchange = () => this.#code.value = this.#codeSelect.value; | ||||||
|         this.deleteButton.onclick = () => { |         this.#deleteButton.onclick = () => { | ||||||
|             this.element.parentElement.removeChild(this.element); |             this.#element.parentElement.removeChild(this.#element); | ||||||
|             this.form.deleteCurrency(this); |             this.form.deleteCurrency(this); | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Reset the order number. | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     resetNo() { | ||||||
|  |         const siblings = Array.from(this.#element.parentElement.children); | ||||||
|  |         this.#no.value = String(siblings.indexOf(this.#element) + 1); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Returns the currency code. |      * Returns the currency code. | ||||||
|      * |      * | ||||||
|      * @return {string} the currency code |      * @return {string} the currency code | ||||||
|      */ |      */ | ||||||
|     getCurrencyCode() { |     get currencyCode() { | ||||||
|         return this.#code.value; |         return this.#code.value; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Sets whether the delete button is shown. | ||||||
|  |      * | ||||||
|  |      * @param isShown {boolean} true to show, or false otherwise | ||||||
|  |      */ | ||||||
|  |     setDeleteButtonShown(isShown) { | ||||||
|  |         if (isShown) { | ||||||
|  |             this.#deleteButton.classList.remove("d-none"); | ||||||
|  |         } else { | ||||||
|  |             this.#deleteButton.classList.add("d-none"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Returns all the line items in the form. |      * Returns all the line items in the form. | ||||||
|      * |      * | ||||||
| @@ -479,7 +489,7 @@ class CurrencySubForm { | |||||||
|     updateCodeSelectorStatus() { |     updateCodeSelectorStatus() { | ||||||
|         let isEnabled = true; |         let isEnabled = true; | ||||||
|         for (const lineItem of this.getLineItems()) { |         for (const lineItem of this.getLineItems()) { | ||||||
|             if (lineItem.getOriginalLineItemId() !== null) { |             if (lineItem.originalLineItemId !== null) { | ||||||
|                 isEnabled = false; |                 isEnabled = false; | ||||||
|                 break; |                 break; | ||||||
|             } |             } | ||||||
| @@ -511,7 +521,7 @@ class CurrencySubForm { | |||||||
|      */ |      */ | ||||||
|     validateBalance() { |     validateBalance() { | ||||||
|         if (this.#debit !== null && this.#credit !== null) { |         if (this.#debit !== null && this.#credit !== null) { | ||||||
|             if (!this.#debit.getTotal().equals(this.#credit.getTotal())) { |             if (!this.#debit.total.equals(this.#credit.total)) { | ||||||
|                 this.#control.classList.add("is-invalid"); |                 this.#control.classList.add("is-invalid"); | ||||||
|                 this.#error.innerText = A_("The totals of the debit and credit amounts do not match."); |                 this.#error.innerText = A_("The totals of the debit and credit amounts do not match."); | ||||||
|                 return false; |                 return false; | ||||||
| @@ -541,6 +551,12 @@ class DebitCreditSubForm { | |||||||
|      */ |      */ | ||||||
|     #element; |     #element; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The content | ||||||
|  |      * @type {HTMLDivElement} | ||||||
|  |      */ | ||||||
|  |     #content; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The currencyIndex |      * The currencyIndex | ||||||
|      * @type {number} |      * @type {number} | ||||||
| @@ -554,7 +570,7 @@ class DebitCreditSubForm { | |||||||
|     debitCredit; |     debitCredit; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The prefix of the HTML ID and class |      * The prefix of the HTML ID and class names | ||||||
|      * @type {string} |      * @type {string} | ||||||
|      */ |      */ | ||||||
|     #prefix; |     #prefix; | ||||||
| @@ -602,24 +618,36 @@ class DebitCreditSubForm { | |||||||
|         this.#currencyIndex = currency.index; |         this.#currencyIndex = currency.index; | ||||||
|         this.debitCredit = debitCredit; |         this.debitCredit = debitCredit; | ||||||
|         this.#prefix = "accounting-currency-" + String(this.#currencyIndex) + "-" + debitCredit; |         this.#prefix = "accounting-currency-" + String(this.#currencyIndex) + "-" + debitCredit; | ||||||
|  |         this.#content = document.getElementById(this.#prefix + "-content"); | ||||||
|         this.#error = document.getElementById(this.#prefix + "-error"); |         this.#error = document.getElementById(this.#prefix + "-error"); | ||||||
|         this.#lineItemList = document.getElementById(this.#prefix + "-list"); |         this.#lineItemList = document.getElementById(this.#prefix + "-list"); | ||||||
|         // noinspection JSValidateTypes |  | ||||||
|         this.lineItems = Array.from(document.getElementsByClassName(this.#prefix)).map((element) => new LineItemSubForm(this, element)); |         this.lineItems = Array.from(document.getElementsByClassName(this.#prefix)).map((element) => new LineItemSubForm(this, element)); | ||||||
|         this.#total = document.getElementById(this.#prefix + "-total"); |         this.#total = document.getElementById(this.#prefix + "-total"); | ||||||
|         this.#addLineItemButton = document.getElementById(this.#prefix + "-add-line-item"); |         this.#addLineItemButton = document.getElementById(this.#prefix + "-add-line-item"); | ||||||
|  |  | ||||||
|  |         this.#resetContent(); | ||||||
|         this.#addLineItemButton.onclick = () => this.currency.form.lineItemEditor.onAddNew(this); |         this.#addLineItemButton.onclick = () => this.currency.form.lineItemEditor.onAddNew(this); | ||||||
|         this.#resetDeleteLineItemButtons(); |         this.#resetDeleteLineItemButtons(); | ||||||
|         this.#initializeDragAndDropReordering(); |         this.#initializeDragAndDropReordering(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The callback when the line item editor is closed. | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     onLineItemEditorClosed() { | ||||||
|  |         if (this.lineItems.length === 0) { | ||||||
|  |             this.#element.classList.remove("accounting-not-empty"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Adds a new line item sub-form |      * Adds a new line item sub-form | ||||||
|      * |      * | ||||||
|      * @returns {LineItemSubForm} the newly-added line item sub-form |      * @returns {LineItemSubForm} the newly-added line item sub-form | ||||||
|      */ |      */ | ||||||
|     addLineItem() { |     addLineItem() { | ||||||
|         const newIndex = 1 + (this.lineItems.length === 0? 0: Math.max(...this.lineItems.map((lineItem) => lineItem.lineItemIndex))); |         const newIndex = 1 + (this.lineItems.length === 0? 0: Math.max(...this.lineItems.map((lineItem) => lineItem.index))); | ||||||
|         const html = this.currency.form.lineItemTemplate |         const html = this.currency.form.lineItemTemplate | ||||||
|             .replaceAll("CURRENCY_INDEX", escapeHtml(String(this.#currencyIndex))) |             .replaceAll("CURRENCY_INDEX", escapeHtml(String(this.#currencyIndex))) | ||||||
|             .replaceAll("DEBIT_CREDIT", escapeHtml(this.debitCredit)) |             .replaceAll("DEBIT_CREDIT", escapeHtml(this.debitCredit)) | ||||||
| @@ -627,6 +655,7 @@ class DebitCreditSubForm { | |||||||
|         this.#lineItemList.insertAdjacentHTML("beforeend", html); |         this.#lineItemList.insertAdjacentHTML("beforeend", html); | ||||||
|         const lineItem = new LineItemSubForm(this, document.getElementById(this.#prefix + "-" + String(newIndex))); |         const lineItem = new LineItemSubForm(this, document.getElementById(this.#prefix + "-" + String(newIndex))); | ||||||
|         this.lineItems.push(lineItem); |         this.lineItems.push(lineItem); | ||||||
|  |         this.#resetContent(); | ||||||
|         this.#resetDeleteLineItemButtons(); |         this.#resetDeleteLineItemButtons(); | ||||||
|         this.#initializeDragAndDropReordering(); |         this.#initializeDragAndDropReordering(); | ||||||
|         this.validate(); |         this.validate(); | ||||||
| @@ -644,6 +673,7 @@ class DebitCreditSubForm { | |||||||
|         this.updateTotal(); |         this.updateTotal(); | ||||||
|         this.currency.updateCodeSelectorStatus(); |         this.currency.updateCodeSelectorStatus(); | ||||||
|         this.currency.form.updateMinDate(); |         this.currency.form.updateMinDate(); | ||||||
|  |         this.#resetContent(); | ||||||
|         this.#resetDeleteLineItemButtons(); |         this.#resetDeleteLineItemButtons(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -653,27 +683,48 @@ class DebitCreditSubForm { | |||||||
|      */ |      */ | ||||||
|     #resetDeleteLineItemButtons() { |     #resetDeleteLineItemButtons() { | ||||||
|         if (this.lineItems.length === 1) { |         if (this.lineItems.length === 1) { | ||||||
|             this.lineItems[0].deleteButton.classList.add("d-none"); |             this.lineItems[0].setDeleteButtonShown(false); | ||||||
|         } else { |         } else { | ||||||
|             for (const lineItem of this.lineItems) { |             for (const lineItem of this.lineItems) { | ||||||
|                 if (lineItem.isMatched) { |                 lineItem.setDeleteButtonShown(!lineItem.isMatched); | ||||||
|                     lineItem.deleteButton.classList.add("d-none"); |  | ||||||
|                 } else { |  | ||||||
|                     lineItem.deleteButton.classList.remove("d-none"); |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Resets the layout of the content. | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     #resetContent() { | ||||||
|  |         if (this.lineItems.length === 0) { | ||||||
|  |             this.#element.classList.remove("accounting-not-empty"); | ||||||
|  |             this.#element.classList.add("accounting-clickable"); | ||||||
|  |             this.#element.dataset.bsToggle = "modal" | ||||||
|  |             this.#element.dataset.bsTarget = "#" + this.currency.form.lineItemEditor.modal.id; | ||||||
|  |             this.#element.onclick = () => { | ||||||
|  |                 this.#element.classList.add("accounting-not-empty"); | ||||||
|  |                 this.currency.form.lineItemEditor.onAddNew(this); | ||||||
|  |             }; | ||||||
|  |             this.#content.classList.add("d-none"); | ||||||
|  |         } else { | ||||||
|  |             this.#element.classList.add("accounting-not-empty"); | ||||||
|  |             this.#element.classList.remove("accounting-clickable"); | ||||||
|  |             delete this.#element.dataset.bsToggle; | ||||||
|  |             delete this.#element.dataset.bsTarget; | ||||||
|  |             this.#element.onclick = null; | ||||||
|  |             this.#content.classList.remove("d-none"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Returns the total amount. |      * Returns the total amount. | ||||||
|      * |      * | ||||||
|      * @return {Decimal} the total amount |      * @return {Decimal} the total amount | ||||||
|      */ |      */ | ||||||
|     getTotal() { |     get total() { | ||||||
|         let total = new Decimal("0"); |         let total = new Decimal("0"); | ||||||
|         for (const lineItem of this.lineItems) { |         for (const lineItem of this.lineItems) { | ||||||
|             const amount = lineItem.getAmount(); |             const amount = lineItem.amount; | ||||||
|             if (amount !== null) { |             if (amount !== null) { | ||||||
|                 total = total.plus(amount); |                 total = total.plus(amount); | ||||||
|             } |             } | ||||||
| @@ -686,7 +737,7 @@ class DebitCreditSubForm { | |||||||
|      * |      * | ||||||
|      */ |      */ | ||||||
|     updateTotal() { |     updateTotal() { | ||||||
|         this.#total.innerText = formatDecimal(this.getTotal()); |         this.#total.innerText = formatDecimal(this.total); | ||||||
|         this.currency.validateBalance(); |         this.currency.validateBalance(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -696,10 +747,8 @@ class DebitCreditSubForm { | |||||||
|      */ |      */ | ||||||
|     #initializeDragAndDropReordering() { |     #initializeDragAndDropReordering() { | ||||||
|         initializeDragAndDropReordering(this.#lineItemList, () => { |         initializeDragAndDropReordering(this.#lineItemList, () => { | ||||||
|             const lineItemId = Array.from(this.#lineItemList.children).map((lineItem) => lineItem.id); |             for (const lineItem of this.lineItems) { | ||||||
|             this.lineItems.sort((a, b) => lineItemId.indexOf(a.element.id) - lineItemId.indexOf(b.element.id)); |                 lineItem.resetNo(); | ||||||
|             for (let i = 0; i < this.lineItems.length; i++) { |  | ||||||
|                 this.lineItems[i].no.value = String(i + 1); |  | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| @@ -751,7 +800,7 @@ class LineItemSubForm { | |||||||
|      * The element |      * The element | ||||||
|      * @type {HTMLLIElement} |      * @type {HTMLLIElement} | ||||||
|      */ |      */ | ||||||
|     element; |     #element; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Either "debit" or "credit" |      * Either "debit" or "credit" | ||||||
| @@ -763,7 +812,7 @@ class LineItemSubForm { | |||||||
|      * The line item index |      * The line item index | ||||||
|      * @type {number} |      * @type {number} | ||||||
|      */ |      */ | ||||||
|     lineItemIndex; |     index; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Whether this is an original line item with offsets |      * Whether this is an original line item with offsets | ||||||
| @@ -771,12 +820,6 @@ class LineItemSubForm { | |||||||
|      */ |      */ | ||||||
|     isMatched; |     isMatched; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The prefix of the HTML ID and class |  | ||||||
|      * @type {string} |  | ||||||
|      */ |  | ||||||
|     #prefix; |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The control |      * The control | ||||||
|      * @type {HTMLDivElement} |      * @type {HTMLDivElement} | ||||||
| @@ -793,7 +836,7 @@ class LineItemSubForm { | |||||||
|      * The number |      * The number | ||||||
|      * @type {HTMLInputElement} |      * @type {HTMLInputElement} | ||||||
|      */ |      */ | ||||||
|     no; |     #no; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The account code |      * The account code | ||||||
| @@ -853,7 +896,7 @@ class LineItemSubForm { | |||||||
|      * The button to delete line item |      * The button to delete line item | ||||||
|      * @type {HTMLButtonElement} |      * @type {HTMLButtonElement} | ||||||
|      */ |      */ | ||||||
|     deleteButton; |     #deleteButton; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Constructs the line item sub-form. |      * Constructs the line item sub-form. | ||||||
| @@ -863,38 +906,47 @@ class LineItemSubForm { | |||||||
|      */ |      */ | ||||||
|     constructor(debitCredit, element) { |     constructor(debitCredit, element) { | ||||||
|         this.debitCreditSubForm = debitCredit; |         this.debitCreditSubForm = debitCredit; | ||||||
|         this.element = element; |         this.#element = element; | ||||||
|         this.debitCredit = element.dataset.debitCredit; |         this.debitCredit = element.dataset.debitCredit; | ||||||
|         this.lineItemIndex = parseInt(element.dataset.lineItemIndex); |         this.index = parseInt(element.dataset.lineItemIndex); | ||||||
|         this.isMatched = element.classList.contains("accounting-matched-line-item"); |         this.isMatched = element.classList.contains("accounting-matched-line-item"); | ||||||
|         this.#prefix = "accounting-currency-" + element.dataset.currencyIndex + "-" + this.debitCredit + "-" + this.lineItemIndex; |         const prefix = "accounting-currency-" + element.dataset.currencyIndex + "-" + this.debitCredit + "-" + String(this.index); | ||||||
|         this.#control = document.getElementById(this.#prefix + "-control"); |         this.#control = document.getElementById(prefix + "-control"); | ||||||
|         this.#error = document.getElementById(this.#prefix + "-error"); |         this.#error = document.getElementById(prefix + "-error"); | ||||||
|         this.no = document.getElementById(this.#prefix + "-no"); |         this.#no = document.getElementById(prefix + "-no"); | ||||||
|         this.#accountCode = document.getElementById(this.#prefix + "-account-code"); |         this.#accountCode = document.getElementById(prefix + "-account-code"); | ||||||
|         this.#accountText = document.getElementById(this.#prefix + "-account-text"); |         this.#accountText = document.getElementById(prefix + "-account-text"); | ||||||
|         this.#description = document.getElementById(this.#prefix + "-description"); |         this.#description = document.getElementById(prefix + "-description"); | ||||||
|         this.#descriptionText = document.getElementById(this.#prefix + "-description-text"); |         this.#descriptionText = document.getElementById(prefix + "-description-text"); | ||||||
|         this.#originalLineItemId = document.getElementById(this.#prefix + "-original-line-item-id"); |         this.#originalLineItemId = document.getElementById(prefix + "-original-line-item-id"); | ||||||
|         this.#originalLineItemText = document.getElementById(this.#prefix + "-original-line-item-text"); |         this.#originalLineItemText = document.getElementById(prefix + "-original-line-item-text"); | ||||||
|         this.#offsets = document.getElementById(this.#prefix + "-offsets"); |         this.#offsets = document.getElementById(prefix + "-offsets"); | ||||||
|         this.#amount = document.getElementById(this.#prefix + "-amount"); |         this.#amount = document.getElementById(prefix + "-amount"); | ||||||
|         this.#amountText = document.getElementById(this.#prefix + "-amount-text"); |         this.#amountText = document.getElementById(prefix + "-amount-text"); | ||||||
|         this.deleteButton = document.getElementById(this.#prefix + "-delete"); |         this.#deleteButton = document.getElementById(prefix + "-delete"); | ||||||
|         this.#control.onclick = () => this.debitCreditSubForm.currency.form.lineItemEditor.onEdit(this); |         this.#control.onclick = () => this.debitCreditSubForm.currency.form.lineItemEditor.onEdit(this); | ||||||
|         this.deleteButton.onclick = () => { |         this.#deleteButton.onclick = () => { | ||||||
|             this.element.parentElement.removeChild(this.element); |             this.#element.parentElement.removeChild(this.#element); | ||||||
|             this.debitCreditSubForm.deleteLineItem(this); |             this.debitCreditSubForm.deleteLineItem(this); | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Reset the order number. | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     resetNo() { | ||||||
|  |         const siblings = Array.from(this.#element.parentElement.children); | ||||||
|  |         this.#no.value = String(siblings.indexOf(this.#element) + 1); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Returns whether the line item needs offset. |      * Returns whether the line item needs offset. | ||||||
|      * |      * | ||||||
|      * @return {boolean} true if the line item needs offset, or false otherwise |      * @return {boolean} true if the line item needs offset, or false otherwise | ||||||
|      */ |      */ | ||||||
|     isNeedOffset() { |     get isNeedOffset() { | ||||||
|         return "isNeedOffset" in this.element.dataset; |         return "isNeedOffset" in this.#element.dataset; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -902,7 +954,7 @@ class LineItemSubForm { | |||||||
|      * |      * | ||||||
|      * @return {string|null} the ID of the original line item |      * @return {string|null} the ID of the original line item | ||||||
|      */ |      */ | ||||||
|     getOriginalLineItemId() { |     get originalLineItemId() { | ||||||
|         return this.#originalLineItemId.value === ""? null: this.#originalLineItemId.value; |         return this.#originalLineItemId.value === ""? null: this.#originalLineItemId.value; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -911,7 +963,7 @@ class LineItemSubForm { | |||||||
|      * |      * | ||||||
|      * @return {string|null} the date of the original line item |      * @return {string|null} the date of the original line item | ||||||
|      */ |      */ | ||||||
|     getOriginalLineItemDate() { |     get originalLineItemDate() { | ||||||
|         return this.#originalLineItemId.dataset.date === ""? null: this.#originalLineItemId.dataset.date; |         return this.#originalLineItemId.dataset.date === ""? null: this.#originalLineItemId.dataset.date; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -920,7 +972,7 @@ class LineItemSubForm { | |||||||
|      * |      * | ||||||
|      * @return {string|null} the text of the original line item |      * @return {string|null} the text of the original line item | ||||||
|      */ |      */ | ||||||
|     getOriginalLineItemText() { |     get originalLineItemText() { | ||||||
|         return this.#originalLineItemId.dataset.text === ""? null: this.#originalLineItemId.dataset.text; |         return this.#originalLineItemId.dataset.text === ""? null: this.#originalLineItemId.dataset.text; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -929,7 +981,7 @@ class LineItemSubForm { | |||||||
|      * |      * | ||||||
|      * @return {string|null} the description |      * @return {string|null} the description | ||||||
|      */ |      */ | ||||||
|     getDescription() { |     get description() { | ||||||
|         return this.#description.value === ""? null: this.#description.value; |         return this.#description.value === ""? null: this.#description.value; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -938,7 +990,7 @@ class LineItemSubForm { | |||||||
|      * |      * | ||||||
|      * @return {string|null} the account code |      * @return {string|null} the account code | ||||||
|      */ |      */ | ||||||
|     getAccountCode() { |     get accountCode() { | ||||||
|         return this.#accountCode.value === ""? null: this.#accountCode.value; |         return this.#accountCode.value === ""? null: this.#accountCode.value; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -947,7 +999,7 @@ class LineItemSubForm { | |||||||
|      * |      * | ||||||
|      * @return {string|null} the account text |      * @return {string|null} the account text | ||||||
|      */ |      */ | ||||||
|     getAccountText() { |     get accountText() { | ||||||
|         return this.#accountCode.dataset.text === ""? null: this.#accountCode.dataset.text; |         return this.#accountCode.dataset.text === ""? null: this.#accountCode.dataset.text; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -956,7 +1008,7 @@ class LineItemSubForm { | |||||||
|      * |      * | ||||||
|      * @return {Decimal|null} the amount |      * @return {Decimal|null} the amount | ||||||
|      */ |      */ | ||||||
|     getAmount() { |     get amount() { | ||||||
|         return this.#amount.value === ""? null: new Decimal(this.#amount.value); |         return this.#amount.value === ""? null: new Decimal(this.#amount.value); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -965,10 +1017,23 @@ class LineItemSubForm { | |||||||
|      * |      * | ||||||
|      * @return {Decimal|null} the minimal amount |      * @return {Decimal|null} the minimal amount | ||||||
|      */ |      */ | ||||||
|     getAmountMin() { |     get amountMin() { | ||||||
|         return this.#amount.dataset.min === ""? null: new Decimal(this.#amount.dataset.min); |         return this.#amount.dataset.min === ""? null: new Decimal(this.#amount.dataset.min); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Sets whether the delete button is shown. | ||||||
|  |      * | ||||||
|  |      * @param isShown {boolean} true to show, or false otherwise | ||||||
|  |      */ | ||||||
|  |     setDeleteButtonShown(isShown) { | ||||||
|  |         if (isShown) { | ||||||
|  |             this.#deleteButton.classList.remove("d-none"); | ||||||
|  |         } else { | ||||||
|  |             this.#deleteButton.classList.add("d-none"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Validates the form. |      * Validates the form. | ||||||
|      * |      * | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ class JournalEntryLineItemEditor { | |||||||
|      * The bootstrap modal |      * The bootstrap modal | ||||||
|      * @type {HTMLDivElement} |      * @type {HTMLDivElement} | ||||||
|      */ |      */ | ||||||
|     #modal; |     modal; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Either "debit" or "credit" |      * Either "debit" or "credit" | ||||||
| @@ -53,7 +53,7 @@ class JournalEntryLineItemEditor { | |||||||
|     debitCredit; |     debitCredit; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The prefix of the HTML ID and class |      * The prefix of the HTML ID and class names | ||||||
|      * @type {string} |      * @type {string} | ||||||
|      */ |      */ | ||||||
|     #prefix = "accounting-line-item-editor" |     #prefix = "accounting-line-item-editor" | ||||||
| @@ -190,12 +190,6 @@ class JournalEntryLineItemEditor { | |||||||
|      */ |      */ | ||||||
|     description = null; |     description = null; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The amount |  | ||||||
|      * @type {string} |  | ||||||
|      */ |  | ||||||
|     amount = ""; |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The description editors |      * The description editors | ||||||
|      * @type {{debit: DescriptionEditor, credit: DescriptionEditor}} |      * @type {{debit: DescriptionEditor, credit: DescriptionEditor}} | ||||||
| @@ -204,7 +198,7 @@ class JournalEntryLineItemEditor { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The account selectors |      * The account selectors | ||||||
|      * @type {{debit: AccountSelector, credit: AccountSelector}} |      * @type {{debit: JournalEntryAccountSelector, credit: JournalEntryAccountSelector}} | ||||||
|      */ |      */ | ||||||
|     #accountSelectors; |     #accountSelectors; | ||||||
|  |  | ||||||
| @@ -222,7 +216,7 @@ class JournalEntryLineItemEditor { | |||||||
|     constructor(form) { |     constructor(form) { | ||||||
|         this.form = form; |         this.form = form; | ||||||
|         this.#element = document.getElementById(this.#prefix); |         this.#element = document.getElementById(this.#prefix); | ||||||
|         this.#modal = document.getElementById(this.#prefix + "-modal"); |         this.modal = document.getElementById(this.#prefix + "-modal"); | ||||||
|         this.#originalLineItemContainer = document.getElementById(this.#prefix + "-original-line-item-container"); |         this.#originalLineItemContainer = document.getElementById(this.#prefix + "-original-line-item-container"); | ||||||
|         this.#originalLineItemControl = document.getElementById(this.#prefix + "-original-line-item-control"); |         this.#originalLineItemControl = document.getElementById(this.#prefix + "-original-line-item-control"); | ||||||
|         this.#originalLineItemText = document.getElementById(this.#prefix + "-original-line-item"); |         this.#originalLineItemText = document.getElementById(this.#prefix + "-original-line-item"); | ||||||
| @@ -237,8 +231,9 @@ class JournalEntryLineItemEditor { | |||||||
|         this.#amountInput = document.getElementById(this.#prefix + "-amount"); |         this.#amountInput = document.getElementById(this.#prefix + "-amount"); | ||||||
|         this.#amountError = document.getElementById(this.#prefix + "-amount-error"); |         this.#amountError = document.getElementById(this.#prefix + "-amount-error"); | ||||||
|         this.#descriptionEditors = DescriptionEditor.getInstances(this); |         this.#descriptionEditors = DescriptionEditor.getInstances(this); | ||||||
|         this.#accountSelectors = AccountSelector.getInstances(this); |         this.#accountSelectors = JournalEntryAccountSelector.getInstances(this); | ||||||
|         this.originalLineItemSelector = new OriginalLineItemSelector(this); |         this.originalLineItemSelector = new OriginalLineItemSelector(this); | ||||||
|  |  | ||||||
|         this.#originalLineItemControl.onclick = () => this.originalLineItemSelector.onOpen() |         this.#originalLineItemControl.onclick = () => this.originalLineItemSelector.onOpen() | ||||||
|         this.#originalLineItemDelete.onclick = () => this.clearOriginalLineItem(); |         this.#originalLineItemDelete.onclick = () => this.clearOriginalLineItem(); | ||||||
|         this.#descriptionControl.onclick = () => this.#descriptionEditors[this.debitCredit].onOpen(); |         this.#descriptionControl.onclick = () => this.#descriptionEditors[this.debitCredit].onOpen(); | ||||||
| @@ -249,12 +244,30 @@ class JournalEntryLineItemEditor { | |||||||
|                 if (this.lineItem === null) { |                 if (this.lineItem === null) { | ||||||
|                     this.lineItem = this.#debitCreditSubForm.addLineItem(); |                     this.lineItem = this.#debitCreditSubForm.addLineItem(); | ||||||
|                 } |                 } | ||||||
|                 this.amount = this.#amountInput.value; |  | ||||||
|                 this.lineItem.save(this); |                 this.lineItem.save(this); | ||||||
|                 bootstrap.Modal.getInstance(this.#modal).hide(); |                 bootstrap.Modal.getInstance(this.modal).hide(); | ||||||
|             } |             } | ||||||
|             return false; |             return false; | ||||||
|         }; |         }; | ||||||
|  |         this.modal.addEventListener("hidden.bs.modal", () => this.#debitCreditSubForm.onLineItemEditorClosed()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the amount. | ||||||
|  |      * | ||||||
|  |      * @return {string} the amount | ||||||
|  |      */ | ||||||
|  |     get amount() { | ||||||
|  |         return this.#amountInput.value; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the currency code. | ||||||
|  |      * | ||||||
|  |      * @return {string} the currency code | ||||||
|  |      */ | ||||||
|  |     get currencyCode() { | ||||||
|  |         return this.#debitCreditSubForm.currency.currencyCode; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -308,15 +321,6 @@ class JournalEntryLineItemEditor { | |||||||
|         this.#amountInput.max = ""; |         this.#amountInput.max = ""; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Returns the currency code. |  | ||||||
|      * |  | ||||||
|      * @return {string} the currency code |  | ||||||
|      */ |  | ||||||
|     getCurrencyCode() { |  | ||||||
|         return this.#debitCreditSubForm.currency.getCurrencyCode(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Saves the description from the description editor. |      * Saves the description from the description editor. | ||||||
|      * |      * | ||||||
| @@ -331,7 +335,7 @@ class JournalEntryLineItemEditor { | |||||||
|         this.description = description === ""? null: description; |         this.description = description === ""? null: description; | ||||||
|         this.#descriptionText.innerText = description; |         this.#descriptionText.innerText = description; | ||||||
|         this.#validateDescription(); |         this.#validateDescription(); | ||||||
|         bootstrap.Modal.getOrCreateInstance(this.#modal).show(); |         bootstrap.Modal.getOrCreateInstance(this.modal).show(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -366,18 +370,16 @@ class JournalEntryLineItemEditor { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Sets the account. |      * Saves the selected account. | ||||||
|      * |      * | ||||||
|      * @param code {string} the account code |      * @param account {JournalEntryAccountOption} the selected account | ||||||
|      * @param text {string} the account text |  | ||||||
|      * @param isNeedOffset {boolean} true if the line items in the account need offset or false otherwise |  | ||||||
|      */ |      */ | ||||||
|     saveAccount(code, text, isNeedOffset) { |     saveAccount(account) { | ||||||
|         this.isNeedOffset = isNeedOffset; |         this.isNeedOffset = account.isNeedOffset; | ||||||
|         this.#accountControl.classList.add("accounting-not-empty"); |         this.#accountControl.classList.add("accounting-not-empty"); | ||||||
|         this.accountCode = code; |         this.accountCode = account.code; | ||||||
|         this.accountText = text; |         this.accountText = account.text; | ||||||
|         this.#accountText.innerText = text; |         this.#accountText.innerText = account.text; | ||||||
|         this.#validateAccount(); |         this.#validateAccount(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -520,10 +522,10 @@ class JournalEntryLineItemEditor { | |||||||
|         this.lineItem = lineItem; |         this.lineItem = lineItem; | ||||||
|         this.#debitCreditSubForm = lineItem.debitCreditSubForm; |         this.#debitCreditSubForm = lineItem.debitCreditSubForm; | ||||||
|         this.debitCredit = this.#debitCreditSubForm.debitCredit; |         this.debitCredit = this.#debitCreditSubForm.debitCredit; | ||||||
|         this.isNeedOffset = lineItem.isNeedOffset(); |         this.isNeedOffset = lineItem.isNeedOffset; | ||||||
|         this.originalLineItemId = lineItem.getOriginalLineItemId(); |         this.originalLineItemId = lineItem.originalLineItemId; | ||||||
|         this.originalLineItemDate = lineItem.getOriginalLineItemDate(); |         this.originalLineItemDate = lineItem.originalLineItemDate; | ||||||
|         this.originalLineItemText = lineItem.getOriginalLineItemText(); |         this.originalLineItemText = lineItem.originalLineItemText; | ||||||
|         this.#originalLineItemText.innerText = this.originalLineItemText; |         this.#originalLineItemText.innerText = this.originalLineItemText; | ||||||
|         if (this.originalLineItemId === null) { |         if (this.originalLineItemId === null) { | ||||||
|             this.#originalLineItemContainer.classList.add("d-none"); |             this.#originalLineItemContainer.classList.add("d-none"); | ||||||
| @@ -533,25 +535,25 @@ class JournalEntryLineItemEditor { | |||||||
|             this.#originalLineItemControl.classList.add("accounting-not-empty"); |             this.#originalLineItemControl.classList.add("accounting-not-empty"); | ||||||
|         } |         } | ||||||
|         this.#setEnableDescriptionAccount(!lineItem.isMatched && this.originalLineItemId === null); |         this.#setEnableDescriptionAccount(!lineItem.isMatched && this.originalLineItemId === null); | ||||||
|         this.description = lineItem.getDescription(); |         this.description = lineItem.description; | ||||||
|         if (this.description === null) { |         if (this.description === null) { | ||||||
|             this.#descriptionControl.classList.remove("accounting-not-empty"); |             this.#descriptionControl.classList.remove("accounting-not-empty"); | ||||||
|         } else { |         } else { | ||||||
|             this.#descriptionControl.classList.add("accounting-not-empty"); |             this.#descriptionControl.classList.add("accounting-not-empty"); | ||||||
|         } |         } | ||||||
|         this.#descriptionText.innerText = this.description === null? "": this.description; |         this.#descriptionText.innerText = this.description === null? "": this.description; | ||||||
|         if (lineItem.getAccountCode() === null) { |         if (lineItem.accountCode === null) { | ||||||
|             this.#accountControl.classList.remove("accounting-not-empty"); |             this.#accountControl.classList.remove("accounting-not-empty"); | ||||||
|         } else { |         } else { | ||||||
|             this.#accountControl.classList.add("accounting-not-empty"); |             this.#accountControl.classList.add("accounting-not-empty"); | ||||||
|         } |         } | ||||||
|         this.accountCode = lineItem.getAccountCode(); |         this.accountCode = lineItem.accountCode; | ||||||
|         this.accountText = lineItem.getAccountText(); |         this.accountText = lineItem.accountText; | ||||||
|         this.#accountText.innerText = this.accountText; |         this.#accountText.innerText = this.accountText; | ||||||
|         this.#amountInput.value = lineItem.getAmount() === null? "": String(lineItem.getAmount()); |         this.#amountInput.value = lineItem.amount === null? "": String(lineItem.amount); | ||||||
|         const maxAmount = this.#getMaxAmount(); |         const maxAmount = this.#getMaxAmount(); | ||||||
|         this.#amountInput.max = maxAmount === null? "": maxAmount; |         this.#amountInput.max = maxAmount === null? "": maxAmount; | ||||||
|         this.#amountInput.min = lineItem.getAmountMin() === null? "": String(lineItem.getAmountMin()); |         this.#amountInput.min = lineItem.amountMin === null? "": String(lineItem.amountMin); | ||||||
|         this.#validate(); |         this.#validate(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -197,7 +197,7 @@ class RecurringExpenseIncomeSubForm { | |||||||
|     editor; |     editor; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The prefix of HTML ID and class |      * The prefix of the HTML ID and class names | ||||||
|      * @type {string} |      * @type {string} | ||||||
|      */ |      */ | ||||||
|     #prefix; |     #prefix; | ||||||
| @@ -313,10 +313,8 @@ class RecurringExpenseIncomeSubForm { | |||||||
|      */ |      */ | ||||||
|     #initializeDragAndDropReordering() { |     #initializeDragAndDropReordering() { | ||||||
|         initializeDragAndDropReordering(this.#itemList, () => { |         initializeDragAndDropReordering(this.#itemList, () => { | ||||||
|             const itemId = Array.from(this.#itemList.children).map((item) => item.id); |             for (const item of this.#items) { | ||||||
|             this.#items.sort((a, b) => itemId.indexOf(a.element.id) - itemId.indexOf(b.element.id)); |                 item.resetNo(); | ||||||
|             for (let i = 0; i < this.#items.length; i++) { |  | ||||||
|                 this.#items[i].no.value = String(i + 1); |  | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| @@ -365,7 +363,7 @@ class RecurringItemSubForm { | |||||||
|      * The element |      * The element | ||||||
|      * @type {HTMLLIElement} |      * @type {HTMLLIElement} | ||||||
|      */ |      */ | ||||||
|     element; |     #element; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The item index |      * The item index | ||||||
| @@ -386,10 +384,10 @@ class RecurringItemSubForm { | |||||||
|     #error; |     #error; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The number |      * The order number | ||||||
|      * @type {HTMLInputElement} |      * @type {HTMLInputElement} | ||||||
|      */ |      */ | ||||||
|     no; |     #no; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The name input |      * The name input | ||||||
| @@ -441,12 +439,12 @@ class RecurringItemSubForm { | |||||||
|      */ |      */ | ||||||
|     constructor(expenseIncomeSubForm, element) { |     constructor(expenseIncomeSubForm, element) { | ||||||
|         this.#expenseIncomeSubForm = expenseIncomeSubForm |         this.#expenseIncomeSubForm = expenseIncomeSubForm | ||||||
|         this.element = element; |         this.#element = element; | ||||||
|         this.itemIndex = parseInt(element.dataset.itemIndex); |         this.itemIndex = parseInt(element.dataset.itemIndex); | ||||||
|         const prefix = "accounting-recurring-" + expenseIncomeSubForm.expenseIncome + "-" + element.dataset.itemIndex; |         const prefix = "accounting-recurring-" + expenseIncomeSubForm.expenseIncome + "-" + element.dataset.itemIndex; | ||||||
|         this.#control = document.getElementById(prefix + "-control"); |         this.#control = document.getElementById(prefix + "-control"); | ||||||
|         this.#error = document.getElementById(prefix + "-error"); |         this.#error = document.getElementById(prefix + "-error"); | ||||||
|         this.no = document.getElementById(prefix + "-no"); |         this.#no = document.getElementById(prefix + "-no"); | ||||||
|         this.#name = document.getElementById(prefix + "-name"); |         this.#name = document.getElementById(prefix + "-name"); | ||||||
|         this.#nameText = document.getElementById(prefix + "-name-text"); |         this.#nameText = document.getElementById(prefix + "-name-text"); | ||||||
|         this.#accountCode = document.getElementById(prefix + "-account-code"); |         this.#accountCode = document.getElementById(prefix + "-account-code"); | ||||||
| @@ -457,17 +455,26 @@ class RecurringItemSubForm { | |||||||
|  |  | ||||||
|         this.#control.onclick = () => this.#expenseIncomeSubForm.editor.onEdit(this); |         this.#control.onclick = () => this.#expenseIncomeSubForm.editor.onEdit(this); | ||||||
|         this.deleteButton.onclick = () => { |         this.deleteButton.onclick = () => { | ||||||
|             this.element.parentElement.removeChild(this.element); |             this.#element.parentElement.removeChild(this.#element); | ||||||
|             this.#expenseIncomeSubForm.deleteItem(this); |             this.#expenseIncomeSubForm.deleteItem(this); | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Reset the order number. | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     resetNo() { | ||||||
|  |         const siblings = Array.from(this.#element.parentElement.children); | ||||||
|  |         this.#no.value = String(siblings.indexOf(this.#element) + 1); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Returns the name. |      * Returns the name. | ||||||
|      * |      * | ||||||
|      * @return {string|null} the name |      * @return {string|null} the name | ||||||
|      */ |      */ | ||||||
|     getName() { |     get name() { | ||||||
|         return this.#name.value === ""? null: this.#name.value; |         return this.#name.value === ""? null: this.#name.value; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -476,7 +483,7 @@ class RecurringItemSubForm { | |||||||
|      * |      * | ||||||
|      * @return {string|null} the account code |      * @return {string|null} the account code | ||||||
|      */ |      */ | ||||||
|     getAccountCode() { |     get accountCode() { | ||||||
|         return this.#accountCode.value === ""? null: this.#accountCode.value; |         return this.#accountCode.value === ""? null: this.#accountCode.value; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -485,7 +492,7 @@ class RecurringItemSubForm { | |||||||
|      * |      * | ||||||
|      * @return {string|null} the account text |      * @return {string|null} the account text | ||||||
|      */ |      */ | ||||||
|     getAccountText() { |     get accountText() { | ||||||
|         return this.#accountCode.dataset.text === ""? null: this.#accountCode.dataset.text; |         return this.#accountCode.dataset.text === ""? null: this.#accountCode.dataset.text; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -494,7 +501,7 @@ class RecurringItemSubForm { | |||||||
|      * |      * | ||||||
|      * @return {string|null} the description template |      * @return {string|null} the description template | ||||||
|      */ |      */ | ||||||
|     getDescriptionTemplate() { |     get descriptionTemplate() { | ||||||
|         return this.#descriptionTemplate.value === ""? null: this.#descriptionTemplate.value; |         return this.#descriptionTemplate.value === ""? null: this.#descriptionTemplate.value; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -504,12 +511,12 @@ class RecurringItemSubForm { | |||||||
|      * @param editor {RecurringItemEditor} the recurring item editor |      * @param editor {RecurringItemEditor} the recurring item editor | ||||||
|      */ |      */ | ||||||
|     save(editor) { |     save(editor) { | ||||||
|         this.#name.value = editor.getName() === null? "": editor.getName(); |         this.#name.value = editor.name === null? "": editor.name; | ||||||
|         this.#nameText.innerText = this.#name.value; |         this.#nameText.innerText = this.#name.value; | ||||||
|         this.#accountCode.value = editor.accountCode; |         this.#accountCode.value = editor.accountCode; | ||||||
|         this.#accountCode.dataset.text = editor.accountText; |         this.#accountCode.dataset.text = editor.accountText; | ||||||
|         this.#accountText.innerText = editor.accountText; |         this.#accountText.innerText = editor.accountText; | ||||||
|         this.#descriptionTemplate.value = editor.getDescriptionTemplate() === null? "": editor.getDescriptionTemplate(); |         this.#descriptionTemplate.value = editor.descriptionTemplate === null? "": editor.descriptionTemplate; | ||||||
|         this.#descriptionTemplateText.innerText = this.#descriptionTemplate.value; |         this.#descriptionTemplateText.innerText = this.#descriptionTemplate.value; | ||||||
|         this.validate(); |         this.validate(); | ||||||
|     } |     } | ||||||
| @@ -677,7 +684,7 @@ class RecurringItemEditor { | |||||||
|      * |      * | ||||||
|      * @return {string|null} the name |      * @return {string|null} the name | ||||||
|      */ |      */ | ||||||
|     getName() { |     get name() { | ||||||
|         return this.#name.value === ""? null: this.#name.value; |         return this.#name.value === ""? null: this.#name.value; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -686,7 +693,7 @@ class RecurringItemEditor { | |||||||
|      * |      * | ||||||
|      * @return {string|null} the description template |      * @return {string|null} the description template | ||||||
|      */ |      */ | ||||||
|     getDescriptionTemplate() { |     get descriptionTemplate() { | ||||||
|         return this.#descriptionTemplate.value === ""? null: this.#descriptionTemplate.value; |         return this.#descriptionTemplate.value === ""? null: this.#descriptionTemplate.value; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -742,16 +749,16 @@ class RecurringItemEditor { | |||||||
|      */ |      */ | ||||||
|     onEdit(item) { |     onEdit(item) { | ||||||
|         this.#item = item; |         this.#item = item; | ||||||
|         this.#name.value = item.getName() === null? "": item.getName(); |         this.#name.value = item.name === null? "": item.name; | ||||||
|         this.accountCode = item.getAccountCode(); |         this.accountCode = item.accountCode; | ||||||
|         this.accountText = item.getAccountText(); |         this.accountText = item.accountText; | ||||||
|         if (this.accountText === null) { |         if (this.accountText === null) { | ||||||
|             this.#accountControl.classList.remove("accounting-not-empty"); |             this.#accountControl.classList.remove("accounting-not-empty"); | ||||||
|         } else { |         } else { | ||||||
|             this.#accountControl.classList.add("accounting-not-empty"); |             this.#accountControl.classList.add("accounting-not-empty"); | ||||||
|         } |         } | ||||||
|         this.#accountContainer.innerText = item.getAccountText() == null? "": item.getAccountText(); |         this.#accountContainer.innerText = this.accountText === null? "": this.accountText; | ||||||
|         this.#descriptionTemplate.value = item.getDescriptionTemplate() === null? "": item.getDescriptionTemplate(); |         this.#descriptionTemplate.value = item.descriptionTemplate === null? "": item.descriptionTemplate; | ||||||
|         this.#validate(); |         this.#validate(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -891,16 +898,16 @@ class RecurringAccountSelector { | |||||||
|      * |      * | ||||||
|      */ |      */ | ||||||
|     #filterOptions() { |     #filterOptions() { | ||||||
|         let hasAnyMatched = false; |         let isAnyMatched = false; | ||||||
|         for (const option of this.#options) { |         for (const option of this.#options) { | ||||||
|             if (option.isMatched(this.#query.value)) { |             if (option.isMatched(this.#query.value)) { | ||||||
|                 option.setShown(true); |                 option.setShown(true); | ||||||
|                 hasAnyMatched = true; |                 isAnyMatched = true; | ||||||
|             } else { |             } else { | ||||||
|                 option.setShown(false); |                 option.setShown(false); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if (!hasAnyMatched) { |         if (!isAnyMatched) { | ||||||
|             this.#optionList.classList.add("d-none"); |             this.#optionList.classList.add("d-none"); | ||||||
|             this.#queryNoResult.classList.remove("d-none"); |             this.#queryNoResult.classList.remove("d-none"); | ||||||
|         } else { |         } else { | ||||||
| @@ -916,7 +923,6 @@ class RecurringAccountSelector { | |||||||
|     onOpen() { |     onOpen() { | ||||||
|         this.#query.value = ""; |         this.#query.value = ""; | ||||||
|         this.#filterOptions(); |         this.#filterOptions(); | ||||||
|         console.log(this.editor.accountCode); |  | ||||||
|         for (const option of this.#options) { |         for (const option of this.#options) { | ||||||
|             option.setActive(option.code === this.editor.accountCode); |             option.setActive(option.code === this.editor.accountCode); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ class OriginalLineItemSelector { | |||||||
|     lineItemEditor; |     lineItemEditor; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The prefix of the HTML ID and class |      * The prefix of the HTML ID and class names | ||||||
|      * @type {string} |      * @type {string} | ||||||
|      */ |      */ | ||||||
|     #prefix = "accounting-original-line-item-selector"; |     #prefix = "accounting-original-line-item-selector"; | ||||||
| @@ -96,9 +96,7 @@ class OriginalLineItemSelector { | |||||||
|         for (const option of this.#options) { |         for (const option of this.#options) { | ||||||
|             this.#optionById[option.id] = option; |             this.#optionById[option.id] = option; | ||||||
|         } |         } | ||||||
|         this.#query.addEventListener("input", () => { |         this.#query.oninput = () => this.#filterOptions(); | ||||||
|             this.#filterOptions(); |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -113,8 +111,8 @@ class OriginalLineItemSelector { | |||||||
|         const otherLineItems = form.getLineItems().filter((lineItem) => lineItem !== currentLineItem); |         const otherLineItems = form.getLineItems().filter((lineItem) => lineItem !== currentLineItem); | ||||||
|         let otherOffset = new Decimal(0); |         let otherOffset = new Decimal(0); | ||||||
|         for (const otherLineItem of otherLineItems) { |         for (const otherLineItem of otherLineItems) { | ||||||
|             if (otherLineItem.getOriginalLineItemId() === originalLineItemId) { |             if (otherLineItem.originalLineItemId === originalLineItemId) { | ||||||
|                 const amount = otherLineItem.getAmount(); |                 const amount = otherLineItem.amount; | ||||||
|                 if (amount !== null) { |                 if (amount !== null) { | ||||||
|                     otherOffset = otherOffset.plus(amount); |                     otherOffset = otherOffset.plus(amount); | ||||||
|                 } |                 } | ||||||
| @@ -131,8 +129,8 @@ class OriginalLineItemSelector { | |||||||
|         const otherLineItems = this.lineItemEditor.form.getLineItems().filter((lineItem) => lineItem !== this.lineItemEditor.lineItem); |         const otherLineItems = this.lineItemEditor.form.getLineItems().filter((lineItem) => lineItem !== this.lineItemEditor.lineItem); | ||||||
|         const otherOffsets = {} |         const otherOffsets = {} | ||||||
|         for (const otherLineItem of otherLineItems) { |         for (const otherLineItem of otherLineItems) { | ||||||
|             const otherOriginalLineItemId = otherLineItem.getOriginalLineItemId(); |             const otherOriginalLineItemId = otherLineItem.originalLineItemId; | ||||||
|             const amount = otherLineItem.getAmount(); |             const amount = otherLineItem.amount; | ||||||
|             if (otherOriginalLineItemId === null || amount === null) { |             if (otherOriginalLineItemId === null || amount === null) { | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
| @@ -155,16 +153,16 @@ class OriginalLineItemSelector { | |||||||
|      * |      * | ||||||
|      */ |      */ | ||||||
|     #filterOptions() { |     #filterOptions() { | ||||||
|         let hasAnyMatched = false; |         let isAnyMatched = false; | ||||||
|         for (const option of this.#options) { |         for (const option of this.#options) { | ||||||
|             if (option.isMatched(this.#debitCredit, this.#currencyCode, this.#query.value)) { |             if (option.isMatched(this.#debitCredit, this.#currencyCode, this.#query.value)) { | ||||||
|                 option.setShown(true); |                 option.setShown(true); | ||||||
|                 hasAnyMatched = true; |                 isAnyMatched = true; | ||||||
|             } else { |             } else { | ||||||
|                 option.setShown(false); |                 option.setShown(false); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if (!hasAnyMatched) { |         if (!isAnyMatched) { | ||||||
|             this.#optionList.classList.add("d-none"); |             this.#optionList.classList.add("d-none"); | ||||||
|             this.#queryNoResult.classList.remove("d-none"); |             this.#queryNoResult.classList.remove("d-none"); | ||||||
|         } else { |         } else { | ||||||
| @@ -178,7 +176,7 @@ class OriginalLineItemSelector { | |||||||
|      * |      * | ||||||
|      */ |      */ | ||||||
|     onOpen() { |     onOpen() { | ||||||
|         this.#currencyCode = this.lineItemEditor.getCurrencyCode(); |         this.#currencyCode = this.lineItemEditor.currencyCode; | ||||||
|         this.#debitCredit = this.lineItemEditor.debitCredit; |         this.#debitCredit = this.lineItemEditor.debitCredit; | ||||||
|         for (const option of this.#options) { |         for (const option of this.#options) { | ||||||
|             option.setActive(option.id === this.lineItemEditor.originalLineItemId); |             option.setActive(option.id === this.lineItemEditor.originalLineItemId); | ||||||
| @@ -275,7 +273,7 @@ class OriginalLineItem { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The values to query against |      * The values to query against | ||||||
|      * @type {string[][]} |      * @type {string[]} | ||||||
|      */ |      */ | ||||||
|     #queryValues; |     #queryValues; | ||||||
|  |  | ||||||
| @@ -341,10 +339,10 @@ class OriginalLineItem { | |||||||
|      */ |      */ | ||||||
|     isMatched(debitCredit, currencyCode, query = null) { |     isMatched(debitCredit, currencyCode, query = null) { | ||||||
|         return this.netBalance.greaterThan(0) |         return this.netBalance.greaterThan(0) | ||||||
|             && this.date <= this.#selector.lineItemEditor.form.getDate() |             && this.date <= this.#selector.lineItemEditor.form.date | ||||||
|             && this.#isDebitCreditMatches(debitCredit) |             && this.#isDebitCreditMatched(debitCredit) | ||||||
|             && this.#currencyCode === currencyCode |             && this.#currencyCode === currencyCode | ||||||
|             && this.#isQueryMatches(query); |             && this.#isQueryMatched(query); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -353,34 +351,43 @@ class OriginalLineItem { | |||||||
|      * @param debitCredit {string} either "debit" or credit |      * @param debitCredit {string} either "debit" or credit | ||||||
|      * @return {boolean} true if the option matches, or false otherwise |      * @return {boolean} true if the option matches, or false otherwise | ||||||
|      */ |      */ | ||||||
|     #isDebitCreditMatches(debitCredit) { |     #isDebitCreditMatched(debitCredit) { | ||||||
|         return (debitCredit === "debit" && this.#debitCredit === "credit") |         return (debitCredit === "debit" && this.#debitCredit === "credit") | ||||||
|             || (debitCredit === "credit" && this.#debitCredit === "debit"); |             || (debitCredit === "credit" && this.#debitCredit === "debit"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Returns whether the original line item matches the query. |      * Returns whether the original line item matches the query term. | ||||||
|      * |      * | ||||||
|      * @param query {string|null} the query term |      * @param query {string|null} the query term | ||||||
|      * @return {boolean} true if the option matches, or false otherwise |      * @return {boolean} true if the option matches, or false otherwise | ||||||
|      */ |      */ | ||||||
|     #isQueryMatches(query) { |     #isQueryMatched(query) { | ||||||
|         if (query === "") { |         if (query === "") { | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|         for (const queryValue of this.#queryValues[0]) { |         if (this.#getNetBalanceForQuery().includes(query.toLowerCase())) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         for (const queryValue of this.#queryValues) { | ||||||
|             if (queryValue.toLowerCase().includes(query.toLowerCase())) { |             if (queryValue.toLowerCase().includes(query.toLowerCase())) { | ||||||
|                 return true; |                 return true; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         for (const queryValue of this.#queryValues[1]) { |  | ||||||
|             if (queryValue === query) { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the net balance in the format for query match. | ||||||
|  |      * | ||||||
|  |      * @return {string} the net balance in the format for query match | ||||||
|  |      */ | ||||||
|  |     #getNetBalanceForQuery() { | ||||||
|  |         const frac = this.netBalance.modulo(1); | ||||||
|  |         const whole = Number(this.netBalance.minus(frac)); | ||||||
|  |         return String(whole) + String(frac).substring(1); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Sets whether the option is shown. |      * Sets whether the option is shown. | ||||||
|      * |      * | ||||||
|   | |||||||
| @@ -33,12 +33,6 @@ document.addEventListener("DOMContentLoaded", () => { | |||||||
|  */ |  */ | ||||||
| class PeriodChooser { | class PeriodChooser { | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The prefix of the HTML ID and class |  | ||||||
|      * @type {string} |  | ||||||
|      */ |  | ||||||
|     prefix; |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The modal of the period chooser |      * The modal of the period chooser | ||||||
|      * @type {HTMLDivElement} |      * @type {HTMLDivElement} | ||||||
| @@ -56,8 +50,8 @@ class PeriodChooser { | |||||||
|      * |      * | ||||||
|      */ |      */ | ||||||
|     constructor() { |     constructor() { | ||||||
|         this.prefix = "accounting-period-chooser"; |         const prefix = "accounting-period-chooser"; | ||||||
|         this.modal = document.getElementById(this.prefix + "-modal"); |         this.modal = document.getElementById(prefix + "-modal"); | ||||||
|         for (const cls of [MonthTab, YearTab, DayTab, CustomTab]) { |         for (const cls of [MonthTab, YearTab, DayTab, CustomTab]) { | ||||||
|             const tab = new cls(this); |             const tab = new cls(this); | ||||||
|             this.tabPlanes[tab.tabId()] = tab; |             this.tabPlanes[tab.tabId()] = tab; | ||||||
| @@ -94,7 +88,7 @@ class TabPlane { | |||||||
|     chooser; |     chooser; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The prefix of the HTML ID and class |      * The prefix of the HTML ID and class names | ||||||
|      * @type {string} |      * @type {string} | ||||||
|      */ |      */ | ||||||
|     prefix; |     prefix; | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ First written: 2023/2/25 | |||||||
|  |  | ||||||
|         <ul id="accounting-account-selector-{{ debit_credit }}-option-list" class="list-group accounting-selector-list"> |         <ul id="accounting-account-selector-{{ debit_credit }}-option-list" class="list-group accounting-selector-list"> | ||||||
|           {% for account in account_options %} |           {% for account in account_options %} | ||||||
|             <li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal"> |             <li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-is-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal"> | ||||||
|               {{ account }} |               {{ account }} | ||||||
|             </li> |             </li> | ||||||
|           {% endfor %} |           {% endfor %} | ||||||
|   | |||||||
| @@ -20,29 +20,31 @@ Author: imacat@mail.imacat.idv.tw (imacat) | |||||||
| First written: 2023/3/21 | First written: 2023/3/21 | ||||||
| #} | #} | ||||||
| <div class="mb-2"> | <div class="mb-2"> | ||||||
|   <div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}" class="form-control accounting-material-text-field accounting-not-empty {% if debit_errors %} is-invalid {% endif %}"> |   <div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}" class="form-control accounting-material-text-field {% if line_item_forms %} accounting-not-empty {% else %} accounting-clickable {% endif %} {% if debit_errors %} is-invalid {% endif %}"> | ||||||
|     <label class="form-label" for="accounting-currency-{{ currency_index }}-{{ debit_credit }}">{{ header }}</label> |     <label class="form-label" for="accounting-currency-{{ currency_index }}-{{ debit_credit }}">{{ header }}</label> | ||||||
|     <ul id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-list" class="list-group accounting-line-item-list"> |     <div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-content" class="mt-2 {% if not line_item_forms %} d-none {% endif %}"> | ||||||
|       {% for line_item_form in line_item_forms %} |       <ul id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-list" class="list-group accounting-line-item-list"> | ||||||
|         {% with currency_index = currency_index, |         {% for line_item_form in line_item_forms %} | ||||||
|                 line_item_index = loop.index, |           {% with currency_index = currency_index, | ||||||
|                 only_one_line_item_form = line_item_forms|length == 1, |                   line_item_index = loop.index, | ||||||
|                 form = line_item_form.form %} |                   only_one_line_item_form = line_item_forms|length == 1, | ||||||
|           {% include "accounting/journal-entry/include/form-line-item.html" %} |                   form = line_item_form.form %} | ||||||
|         {% endwith %} |             {% include "accounting/journal-entry/include/form-line-item.html" %} | ||||||
|       {% endfor %} |           {% endwith %} | ||||||
|     </ul> |         {% endfor %} | ||||||
|  |       </ul> | ||||||
|  |  | ||||||
|     <div class="d-flex justify-content-between mb-2"> |       <div class="d-flex justify-content-between mt-2 mb-2"> | ||||||
|       <div>{{ A_("Total") }}</div> |         <div>{{ A_("Total") }}</div> | ||||||
|       <div><span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-total" class="badge rounded-pill bg-primary">{{ debit_credit_total }}</span></div> |         <div><span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-total" class="badge rounded-pill bg-primary">{{ debit_credit_total }}</span></div> | ||||||
|     </div> |       </div> | ||||||
|  |  | ||||||
|     <div> |       <div> | ||||||
|       <button id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="{{ debit_credit }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal"> |         <button id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="{{ debit_credit }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal"> | ||||||
|         <i class="fas fa-plus"></i> |           <i class="fas fa-plus"></i> | ||||||
|         {{ A_("New") }} |           {{ A_("New") }} | ||||||
|       </button> |         </button> | ||||||
|  |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|   <div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-error" class="invalid-feedback">{% if debit_credit_errors %}{{ debit_credit_errors[0] }}{% endif %}</div> |   <div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-error" class="invalid-feedback">{% if debit_credit_errors %}{{ debit_credit_errors[0] }}{% endif %}</div> | ||||||
|   | |||||||
| @@ -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/journal-entry-form.js") }}"></script> |   <script src="{{ url_for("accounting.static", filename="js/journal-entry-form.js") }}"></script> | ||||||
|   <script src="{{ url_for("accounting.static", filename="js/journal-entry-line-item-editor.js") }}"></script> |   <script src="{{ url_for("accounting.static", filename="js/journal-entry-line-item-editor.js") }}"></script> | ||||||
|   <script src="{{ url_for("accounting.static", filename="js/account-selector.js") }}"></script> |   <script src="{{ url_for("accounting.static", filename="js/journal-entry-account-selector.js") }}"></script> | ||||||
|   <script src="{{ url_for("accounting.static", filename="js/original-line-item-selector.js") }}"></script> |   <script src="{{ url_for("accounting.static", filename="js/original-line-item-selector.js") }}"></script> | ||||||
|   <script src="{{ url_for("accounting.static", filename="js/description-editor.js") }}"></script> |   <script src="{{ url_for("accounting.static", filename="js/description-editor.js") }}"></script> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|   | |||||||
| @@ -38,26 +38,49 @@ First written: 2023/3/22 | |||||||
|   </a> |   </a> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <div class="form-floating mb-3"> | <table class="table table-striped table-hover table-light" aria-label="{{ A_("Settings") }}"> | ||||||
|   <input id="accounting-default-currency" class="form-control" value="{{ obj.default_currency_text }}" readonly="readonly"> | <tbody> | ||||||
|   <label class="form-label" for="accounting-default-currency">{{ A_("Default Currency") }}</label> | <tr> | ||||||
| </div> |   <th scope="row">{{ A_("Default Currency") }}</th> | ||||||
|  |   <td>{{ obj.default_currency_text }}</td> | ||||||
|  | </tr> | ||||||
|  | <tr> | ||||||
|  |   <th scope="row">{{ A_("Default Account for the Income and Expenses Log") }}</th> | ||||||
|  |   <td>{{ obj.default_ie_account_code_text }}</td> | ||||||
|  | </tr> | ||||||
|  | </tbody> | ||||||
|  | </table> | ||||||
|  |  | ||||||
| <div class="form-floating mb-3"> | <h2>{{ A_("Recurring Expense") }}</h2> | ||||||
|   <input id="accounting-default-ie-account" class="form-control" value="{{ obj.default_ie_account_code_text }}" readonly="readonly"> |  | ||||||
|   <label class="form-label" for="accounting-default-ie-account">{{ A_("Default Account for the Income and Expenses Log") }}</label> |  | ||||||
| </div> |  | ||||||
|  |  | ||||||
| {% with expense_income = "expense", | {% if obj.recurring.expenses %} | ||||||
|         label = A_("Recurring Expense"), |   <ul class="list-group mb-3 accounting-list-group-stripped accounting-list-group-hover"> | ||||||
|         recurring_items = obj.recurring.expenses %} |     {% for recurring_item in obj.recurring.expenses %} | ||||||
|   {% include "accounting/option/include/detail-recurring-expense-income.html" %} |       <li class="list-group-item"> | ||||||
| {% endwith %} |         <div class="small">{{ recurring_item.account_text }}</div> | ||||||
|  |         <div>{{ recurring_item.name }}</div> | ||||||
|  |         <div class="small">{{ recurring_item.description_template }}</div> | ||||||
|  |       </li> | ||||||
|  |     {% endfor %} | ||||||
|  |   </ul> | ||||||
|  | {% else %} | ||||||
|  |   <p>{{ A_("There is no data.") }}</p> | ||||||
|  | {% endif %} | ||||||
|  |  | ||||||
| {% with expense_income = "income", | <h2>{{ A_("Recurring Income") }}</h2> | ||||||
|         label = A_("Recurring Income"), |  | ||||||
|         recurring_items = obj.recurring.incomes %} | {% if obj.recurring.incomes %} | ||||||
|   {% include "accounting/option/include/detail-recurring-expense-income.html" %} |   <ul class="list-group mb-3 accounting-list-group-stripped accounting-list-group-hover"> | ||||||
| {% endwith %} |     {% for recurring_item in obj.recurring.incomes %} | ||||||
|  |       <li class="list-group-item"> | ||||||
|  |         <div class="small">{{ recurring_item.account_text }}</div> | ||||||
|  |         <div>{{ recurring_item.name }}</div> | ||||||
|  |         <div class="small">{{ recurring_item.description_template }}</div> | ||||||
|  |       </li> | ||||||
|  |     {% endfor %} | ||||||
|  |   </ul> | ||||||
|  | {% else %} | ||||||
|  |   <p>{{ A_("There is no data.") }}</p> | ||||||
|  | {% endif %} | ||||||
|  |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
|   | |||||||
| @@ -1,31 +0,0 @@ | |||||||
| {# |  | ||||||
| The Mia! Accounting Flask Project |  | ||||||
| detail-recurring-expense-income.html: The recurring expense or income in the option detail |  | ||||||
|  |  | ||||||
|  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/3/22 |  | ||||||
| #} |  | ||||||
| <div id="accounting-recurring-{{ expense_income }}" class="form-control mb-3 accounting-material-text-field {% if recurring_items %} accounting-not-empty {% endif %}"> |  | ||||||
|   <label class="form-label" for="accounting-recurring-{{ expense_income }}">{{ label }}</label> |  | ||||||
|   {% if recurring_items %} |  | ||||||
|     <ul class="list-group mb-2 mt-2"> |  | ||||||
|       {% for item in recurring_items %} |  | ||||||
|         {% include "accounting/option/include/detail-recurring-item.html" %} |  | ||||||
|       {% endfor %} |  | ||||||
|     </ul> |  | ||||||
|   {% endif %} |  | ||||||
| </div> |  | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| {# |  | ||||||
| The Mia! Accounting Flask Project |  | ||||||
| detail-recurring-item.html: The recurring item in the option detail |  | ||||||
|  |  | ||||||
|  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/3/22 |  | ||||||
| #} |  | ||||||
| {# <ul> For SonarQube not to complain about incorrect HTML #} |  | ||||||
| <li class="list-group-item list-group-item-action"> |  | ||||||
|   <div class="small">{{ item.account_text }}</div> |  | ||||||
|   <div>{{ item.name }}</div> |  | ||||||
|   <div class="small">{{ item.description_template }}</div> |  | ||||||
| </li> |  | ||||||
| {# </ul> For SonarQube not to complain about incorrect HTML #} |  | ||||||
| @@ -27,12 +27,9 @@ from flask import Flask | |||||||
| from flask.testing import FlaskCliRunner | from flask.testing import FlaskCliRunner | ||||||
|  |  | ||||||
| from test_site import db | from test_site import db | ||||||
| from testlib import create_test_app, get_client, set_locale | from testlib import NEXT_URI, create_test_app, get_client, set_locale | ||||||
| from testlib_journal_entry import add_journal_entry | from testlib_journal_entry import add_journal_entry | ||||||
|  |  | ||||||
| NEXT_URI: str = "/_next" |  | ||||||
| """The next URI.""" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AccountData: | class AccountData: | ||||||
|     """The account data.""" |     """The account data.""" | ||||||
| @@ -550,8 +547,8 @@ class AccountTestCase(unittest.TestCase): | |||||||
|         :return: None. |         :return: None. | ||||||
|         """ |         """ | ||||||
|         from accounting.models import Account |         from accounting.models import Account | ||||||
|         editor_username, editor2_username = "editor", "editor2" |         editor_username, admin_username = "editor", "admin" | ||||||
|         client, csrf_token = get_client(self.app, editor2_username) |         client, csrf_token = get_client(self.app, admin_username) | ||||||
|         detail_uri: str = f"{PREFIX}/{CASH.code}" |         detail_uri: str = f"{PREFIX}/{CASH.code}" | ||||||
|         update_uri: str = f"{PREFIX}/{CASH.code}/update" |         update_uri: str = f"{PREFIX}/{CASH.code}/update" | ||||||
|         account: Account |         account: Account | ||||||
| @@ -574,7 +571,7 @@ class AccountTestCase(unittest.TestCase): | |||||||
|             self.assertEqual(account.created_by.username, |             self.assertEqual(account.created_by.username, | ||||||
|                              editor_username) |                              editor_username) | ||||||
|             self.assertEqual(account.updated_by.username, |             self.assertEqual(account.updated_by.username, | ||||||
|                              editor2_username) |                              admin_username) | ||||||
|  |  | ||||||
|     def test_l10n(self) -> None: |     def test_l10n(self) -> None: | ||||||
|         """Tests the localization. |         """Tests the localization. | ||||||
|   | |||||||
| @@ -28,8 +28,8 @@ from flask import Flask | |||||||
| from flask.testing import FlaskCliRunner | from flask.testing import FlaskCliRunner | ||||||
|  |  | ||||||
| from test_site import db | from test_site import db | ||||||
| from testlib import create_test_app, get_client, set_locale | from testlib import NEXT_URI, create_test_app, get_client, set_locale | ||||||
| from testlib_journal_entry import add_journal_entry, NEXT_URI | from testlib_journal_entry import add_journal_entry | ||||||
|  |  | ||||||
|  |  | ||||||
| class CurrencyData: | class CurrencyData: | ||||||
| @@ -471,8 +471,8 @@ class CurrencyTestCase(unittest.TestCase): | |||||||
|         :return: None. |         :return: None. | ||||||
|         """ |         """ | ||||||
|         from accounting.models import Currency |         from accounting.models import Currency | ||||||
|         editor_username, editor2_username = "editor", "editor2" |         editor_username, admin_username = "editor", "admin" | ||||||
|         client, csrf_token = get_client(self.app, editor2_username) |         client, csrf_token = get_client(self.app, admin_username) | ||||||
|         detail_uri: str = f"{PREFIX}/{USD.code}" |         detail_uri: str = f"{PREFIX}/{USD.code}" | ||||||
|         update_uri: str = f"{PREFIX}/{USD.code}/update" |         update_uri: str = f"{PREFIX}/{USD.code}/update" | ||||||
|         currency: Currency |         currency: Currency | ||||||
| @@ -493,7 +493,7 @@ class CurrencyTestCase(unittest.TestCase): | |||||||
|         with self.app.app_context(): |         with self.app.app_context(): | ||||||
|             currency = db.session.get(Currency, USD.code) |             currency = db.session.get(Currency, USD.code) | ||||||
|             self.assertEqual(currency.created_by.username, editor_username) |             self.assertEqual(currency.created_by.username, editor_username) | ||||||
|             self.assertEqual(currency.updated_by.username, editor2_username) |             self.assertEqual(currency.updated_by.username, admin_username) | ||||||
|  |  | ||||||
|     def test_api_exists(self) -> None: |     def test_api_exists(self) -> None: | ||||||
|         """Tests the API to check if a code exists. |         """Tests the API to check if a code exists. | ||||||
|   | |||||||
| @@ -24,8 +24,8 @@ from click.testing import Result | |||||||
| from flask import Flask | from flask import Flask | ||||||
| from flask.testing import FlaskCliRunner | from flask.testing import FlaskCliRunner | ||||||
|  |  | ||||||
| from testlib import create_test_app, get_client | from testlib import NEXT_URI, Accounts, create_test_app, get_client | ||||||
| from testlib_journal_entry import Accounts, NEXT_URI, add_journal_entry | from testlib_journal_entry import add_journal_entry | ||||||
|  |  | ||||||
|  |  | ||||||
| class DescriptionEditorTestCase(unittest.TestCase): | class DescriptionEditorTestCase(unittest.TestCase): | ||||||
|   | |||||||
| @@ -27,9 +27,9 @@ from flask import Flask | |||||||
| from flask.testing import FlaskCliRunner | from flask.testing import FlaskCliRunner | ||||||
|  |  | ||||||
| from test_site import db | from test_site import db | ||||||
| from testlib import create_test_app, get_client | from testlib import NEXT_URI, Accounts, create_test_app, get_client | ||||||
| from testlib_journal_entry import NEXT_URI, NON_EMPTY_NOTE, EMPTY_NOTE, \ | from testlib_journal_entry import NON_EMPTY_NOTE, EMPTY_NOTE, \ | ||||||
|     Accounts, get_add_form, get_unchanged_update_form, get_update_form, \ |     get_add_form, get_unchanged_update_form, get_update_form, \ | ||||||
|     match_journal_entry_detail, set_negative_amount, \ |     match_journal_entry_detail, set_negative_amount, \ | ||||||
|     remove_debit_in_a_currency, remove_credit_in_a_currency, add_journal_entry |     remove_debit_in_a_currency, remove_credit_in_a_currency, add_journal_entry | ||||||
|  |  | ||||||
| @@ -537,8 +537,8 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase): | |||||||
|         from accounting.models import JournalEntry |         from accounting.models import JournalEntry | ||||||
|         journal_entry_id: int \ |         journal_entry_id: int \ | ||||||
|             = add_journal_entry(self.client, self.__get_add_form()) |             = add_journal_entry(self.client, self.__get_add_form()) | ||||||
|         editor_username, editor2_username = "editor", "editor2" |         editor_username, admin_username = "editor", "admin" | ||||||
|         client, csrf_token = get_client(self.app, editor2_username) |         client, csrf_token = get_client(self.app, admin_username) | ||||||
|         detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" |         detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" | ||||||
|         update_uri: str = f"{PREFIX}/{journal_entry_id}/update" |         update_uri: str = f"{PREFIX}/{journal_entry_id}/update" | ||||||
|         journal_entry: JournalEntry |         journal_entry: JournalEntry | ||||||
| @@ -562,7 +562,7 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase): | |||||||
|             self.assertEqual(journal_entry.created_by.username, |             self.assertEqual(journal_entry.created_by.username, | ||||||
|                              editor_username) |                              editor_username) | ||||||
|             self.assertEqual(journal_entry.updated_by.username, |             self.assertEqual(journal_entry.updated_by.username, | ||||||
|                              editor2_username) |                              admin_username) | ||||||
|  |  | ||||||
|     def test_delete(self) -> None: |     def test_delete(self) -> None: | ||||||
|         """Tests to delete a journal entry. |         """Tests to delete a journal entry. | ||||||
| @@ -1163,8 +1163,8 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase): | |||||||
|         from accounting.models import JournalEntry |         from accounting.models import JournalEntry | ||||||
|         journal_entry_id: int \ |         journal_entry_id: int \ | ||||||
|             = add_journal_entry(self.client, self.__get_add_form()) |             = add_journal_entry(self.client, self.__get_add_form()) | ||||||
|         editor_username, editor2_username = "editor", "editor2" |         editor_username, admin_username = "editor", "admin" | ||||||
|         client, csrf_token = get_client(self.app, editor2_username) |         client, csrf_token = get_client(self.app, admin_username) | ||||||
|         detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" |         detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" | ||||||
|         update_uri: str = f"{PREFIX}/{journal_entry_id}/update" |         update_uri: str = f"{PREFIX}/{journal_entry_id}/update" | ||||||
|         journal_entry: JournalEntry |         journal_entry: JournalEntry | ||||||
| @@ -1188,7 +1188,7 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase): | |||||||
|             self.assertEqual(journal_entry.created_by.username, |             self.assertEqual(journal_entry.created_by.username, | ||||||
|                              editor_username) |                              editor_username) | ||||||
|             self.assertEqual(journal_entry.updated_by.username, |             self.assertEqual(journal_entry.updated_by.username, | ||||||
|                              editor2_username) |                              admin_username) | ||||||
|  |  | ||||||
|     def test_delete(self) -> None: |     def test_delete(self) -> None: | ||||||
|         """Tests to delete a journal entry. |         """Tests to delete a journal entry. | ||||||
| @@ -1837,8 +1837,8 @@ class TransferJournalEntryTestCase(unittest.TestCase): | |||||||
|         from accounting.models import JournalEntry |         from accounting.models import JournalEntry | ||||||
|         journal_entry_id: int \ |         journal_entry_id: int \ | ||||||
|             = add_journal_entry(self.client, self.__get_add_form()) |             = add_journal_entry(self.client, self.__get_add_form()) | ||||||
|         editor_username, editor2_username = "editor", "editor2" |         editor_username, admin_username = "editor", "admin" | ||||||
|         client, csrf_token = get_client(self.app, editor2_username) |         client, csrf_token = get_client(self.app, admin_username) | ||||||
|         detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" |         detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" | ||||||
|         update_uri: str = f"{PREFIX}/{journal_entry_id}/update" |         update_uri: str = f"{PREFIX}/{journal_entry_id}/update" | ||||||
|         journal_entry: JournalEntry |         journal_entry: JournalEntry | ||||||
| @@ -1862,7 +1862,7 @@ class TransferJournalEntryTestCase(unittest.TestCase): | |||||||
|             self.assertEqual(journal_entry.created_by.username, |             self.assertEqual(journal_entry.created_by.username, | ||||||
|                              editor_username) |                              editor_username) | ||||||
|             self.assertEqual(journal_entry.updated_by.username, |             self.assertEqual(journal_entry.updated_by.username, | ||||||
|                              editor2_username) |                              admin_username) | ||||||
|  |  | ||||||
|     def test_save_as_receipt(self) -> None: |     def test_save_as_receipt(self) -> None: | ||||||
|         """Tests to save a transfer journal entry as a cash receipt journal |         """Tests to save a transfer journal entry as a cash receipt journal | ||||||
|   | |||||||
| @@ -26,8 +26,8 @@ from flask import Flask | |||||||
| from flask.testing import FlaskCliRunner | from flask.testing import FlaskCliRunner | ||||||
|  |  | ||||||
| from test_site import db | from test_site import db | ||||||
| from testlib import create_test_app, get_client | from testlib import Accounts, create_test_app, get_client | ||||||
| from testlib_journal_entry import Accounts, match_journal_entry_detail | from testlib_journal_entry import match_journal_entry_detail | ||||||
| from testlib_offset import TestData, JournalEntryLineItemData, \ | from testlib_offset import TestData, JournalEntryLineItemData, \ | ||||||
|     JournalEntryData, CurrencyData |     JournalEntryData, CurrencyData | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,8 +26,7 @@ from flask import Flask | |||||||
| from flask.testing import FlaskCliRunner | from flask.testing import FlaskCliRunner | ||||||
|  |  | ||||||
| from test_site import db | from test_site import db | ||||||
| from testlib import create_test_app, get_client | from testlib import NEXT_URI, Accounts, create_test_app, get_client | ||||||
| from testlib_journal_entry import NEXT_URI, Accounts |  | ||||||
| from testlib_offset import TestData | from testlib_offset import TestData | ||||||
|  |  | ||||||
| PREFIX: str = "/accounting/options" | PREFIX: str = "/accounting/options" | ||||||
| @@ -68,7 +67,7 @@ class OptionTestCase(unittest.TestCase): | |||||||
|             self.assertEqual(result.exit_code, 0) |             self.assertEqual(result.exit_code, 0) | ||||||
|             Option.query.delete() |             Option.query.delete() | ||||||
|  |  | ||||||
|         self.client, self.csrf_token = get_client(self.app, "editor") |         self.client, self.csrf_token = get_client(self.app, "admin") | ||||||
|         self.data: TestData = TestData(self.app, self.client, self.csrf_token) |         self.data: TestData = TestData(self.app, self.client, self.csrf_token) | ||||||
|  |  | ||||||
|     def test_nobody(self) -> None: |     def test_nobody(self) -> None: | ||||||
| @@ -105,12 +104,12 @@ class OptionTestCase(unittest.TestCase): | |||||||
|         response = client.post(UPDATE_URI, data=self.__get_form(csrf_token)) |         response = client.post(UPDATE_URI, data=self.__get_form(csrf_token)) | ||||||
|         self.assertEqual(response.status_code, 403) |         self.assertEqual(response.status_code, 403) | ||||||
|  |  | ||||||
|     def test_editor2(self) -> None: |     def test_editor(self) -> None: | ||||||
|         """Test the permission as non-administrator. |         """Test the permission as editor. | ||||||
|  |  | ||||||
|         :return: None. |         :return: None. | ||||||
|         """ |         """ | ||||||
|         client, csrf_token = get_client(self.app, "editor2") |         client, csrf_token = get_client(self.app, "editor") | ||||||
|         response: httpx.Response |         response: httpx.Response | ||||||
|  |  | ||||||
|         response = client.get(DETAIL_URI) |         response = client.get(DETAIL_URI) | ||||||
| @@ -122,7 +121,7 @@ class OptionTestCase(unittest.TestCase): | |||||||
|         response = client.post(UPDATE_URI, data=self.__get_form(csrf_token)) |         response = client.post(UPDATE_URI, data=self.__get_form(csrf_token)) | ||||||
|         self.assertEqual(response.status_code, 403) |         self.assertEqual(response.status_code, 403) | ||||||
|  |  | ||||||
|     def test_editor(self) -> None: |     def test_admin(self) -> None: | ||||||
|         """Test the permission as administrator. |         """Test the permission as administrator. | ||||||
|  |  | ||||||
|         :return: None. |         :return: None. | ||||||
| @@ -344,7 +343,7 @@ class OptionTestCase(unittest.TestCase): | |||||||
|         """ |         """ | ||||||
|         from accounting.models import Option |         from accounting.models import Option | ||||||
|         from accounting.utils.user import get_user_pk |         from accounting.utils.user import get_user_pk | ||||||
|         editor_username, editor2_username = "editor", "editor2" |         admin_username, editor_username = "admin", "editor" | ||||||
|         option: Option | None |         option: Option | None | ||||||
|         response: httpx.Response |         response: httpx.Response | ||||||
|  |  | ||||||
| @@ -353,11 +352,11 @@ class OptionTestCase(unittest.TestCase): | |||||||
|         self.assertEqual(response.headers["Location"], DETAIL_URI) |         self.assertEqual(response.headers["Location"], DETAIL_URI) | ||||||
|  |  | ||||||
|         with self.app.app_context(): |         with self.app.app_context(): | ||||||
|             editor2_pk: int = get_user_pk(editor2_username) |             editor_pk: int = get_user_pk(editor_username) | ||||||
|             option = db.session.get(Option, "recurring") |             option = db.session.get(Option, "recurring") | ||||||
|             self.assertIsNotNone(option) |             self.assertIsNotNone(option) | ||||||
|             option.created_by_id = editor2_pk |             option.created_by_id = editor_pk | ||||||
|             option.updated_by_id = editor2_pk |             option.updated_by_id = editor_pk | ||||||
|             db.session.commit() |             db.session.commit() | ||||||
|  |  | ||||||
|         form: dict[str, str] = self.__get_form() |         form: dict[str, str] = self.__get_form() | ||||||
| @@ -372,8 +371,8 @@ class OptionTestCase(unittest.TestCase): | |||||||
|         with self.app.app_context(): |         with self.app.app_context(): | ||||||
|             option = db.session.get(Option, "recurring") |             option = db.session.get(Option, "recurring") | ||||||
|             self.assertIsNotNone(option) |             self.assertIsNotNone(option) | ||||||
|             self.assertEqual(option.created_by.username, editor2_username) |             self.assertEqual(option.created_by.username, editor_username) | ||||||
|             self.assertEqual(option.updated_by.username, editor_username) |             self.assertEqual(option.updated_by.username, admin_username) | ||||||
|  |  | ||||||
|     def __get_form(self, csrf_token: str | None = None) -> dict[str, str]: |     def __get_form(self, csrf_token: str | None = None) -> dict[str, str]: | ||||||
|         """Returns the option form. |         """Returns the option form. | ||||||
|   | |||||||
| @@ -72,15 +72,15 @@ def create_app(is_testing: bool = False) -> Flask: | |||||||
|         def can_view(self) -> bool: |         def can_view(self) -> bool: | ||||||
|             return auth.current_user() is not None \ |             return auth.current_user() is not None \ | ||||||
|                 and auth.current_user().username in ["viewer", "editor", |                 and auth.current_user().username in ["viewer", "editor", | ||||||
|                                                      "editor2"] |                                                      "admin"] | ||||||
|  |  | ||||||
|         def can_edit(self) -> bool: |         def can_edit(self) -> bool: | ||||||
|             return auth.current_user() is not None \ |             return auth.current_user() is not None \ | ||||||
|                 and auth.current_user().username in ["editor", "editor2"] |                 and auth.current_user().username in ["editor", "admin"] | ||||||
|  |  | ||||||
|         def can_admin(self) -> bool: |         def can_admin(self) -> bool: | ||||||
|             return auth.current_user() is not None \ |             return auth.current_user() is not None \ | ||||||
|                 and auth.current_user().username == "editor" |                 and auth.current_user().username == "admin" | ||||||
|  |  | ||||||
|         @property |         @property | ||||||
|         def cls(self) -> t.Type[auth.User]: |         def cls(self) -> t.Type[auth.User]: | ||||||
| @@ -112,7 +112,7 @@ def init_db_command() -> None: | |||||||
|     """Initializes the database.""" |     """Initializes the database.""" | ||||||
|     db.create_all() |     db.create_all() | ||||||
|     from .auth import User |     from .auth import User | ||||||
|     for username in ["viewer", "editor", "editor2", "nobody"]: |     for username in ["viewer", "editor", "admin", "nobody"]: | ||||||
|         if User.query.filter(User.username == username).first() is None: |         if User.query.filter(User.username == username).first() is None: | ||||||
|             db.session.add(User(username=username)) |             db.session.add(User(username=username)) | ||||||
|     db.session.commit() |     db.session.commit() | ||||||
|   | |||||||
| @@ -58,8 +58,8 @@ def login() -> redirect: | |||||||
|  |  | ||||||
|     :return: The redirection to the home page. |     :return: The redirection to the home page. | ||||||
|     """ |     """ | ||||||
|     if request.form.get("username") not in ["viewer", "editor", "editor2", |     if request.form.get("username") not in {"viewer", "editor", "admin", | ||||||
|                                             "nobody"]: |                                             "nobody"}: | ||||||
|         return redirect(url_for("auth.login")) |         return redirect(url_for("auth.login")) | ||||||
|     session["user"] = request.form.get("username") |     session["user"] = request.form.get("username") | ||||||
|     return redirect(url_for("home.home")) |     return redirect(url_for("home.home")) | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ First written: 2023/1/27 | |||||||
|   <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> |   <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | ||||||
|   <button class="btn btn-primary" type="submit" name="username" value="viewer">{{ _("Viewer") }}</button> |   <button class="btn btn-primary" type="submit" name="username" value="viewer">{{ _("Viewer") }}</button> | ||||||
|   <button class="btn btn-primary" type="submit" name="username" value="editor">{{ _("Editor") }}</button> |   <button class="btn btn-primary" type="submit" name="username" value="editor">{{ _("Editor") }}</button> | ||||||
|   <button class="btn btn-primary" type="submit" name="username" value="editor2">{{ _("Editor2") }}</button> |   <button class="btn btn-primary" type="submit" name="username" value="admin">{{ _("Administrator") }}</button> | ||||||
|   <button class="btn btn-primary" type="submit" name="username" value="nobody">{{ _("Nobody") }}</button> |   <button class="btn btn-primary" type="submit" name="username" value="nobody">{{ _("Nobody") }}</button> | ||||||
| </form> | </form> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,8 +9,8 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: Mia! Accounting Flask Demonstration 0.0.0\n" | "Project-Id-Version: Mia! Accounting Flask Demonstration 0.0.0\n" | ||||||
| "Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n" | "Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n" | ||||||
| "POT-Creation-Date: 2023-02-27 10:07+0800\n" | "POT-Creation-Date: 2023-03-24 08:32+0800\n" | ||||||
| "PO-Revision-Date: 2023-02-27 10:08+0800\n" | "PO-Revision-Date: 2023-03-24 08:33+0800\n" | ||||||
| "Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n" | "Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n" | ||||||
| "Language: zh_Hant\n" | "Language: zh_Hant\n" | ||||||
| "Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n" | "Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n" | ||||||
| @@ -18,27 +18,27 @@ msgstr "" | |||||||
| "MIME-Version: 1.0\n" | "MIME-Version: 1.0\n" | ||||||
| "Content-Type: text/plain; charset=utf-8\n" | "Content-Type: text/plain; charset=utf-8\n" | ||||||
| "Content-Transfer-Encoding: 8bit\n" | "Content-Transfer-Encoding: 8bit\n" | ||||||
| "Generated-By: Babel 2.11.0\n" | "Generated-By: Babel 2.12.1\n" | ||||||
|  |  | ||||||
| #: tests/test_site/templates/base.html:23 | #: tests/test_site/templates/base.html:23 | ||||||
| msgid "en" | msgid "en" | ||||||
| msgstr "zh-Hant" | msgstr "zh-Hant" | ||||||
|  |  | ||||||
| #: tests/test_site/templates/base.html:43 | #: tests/test_site/templates/base.html:46 | ||||||
| #: tests/test_site/templates/home.html:24 | #: tests/test_site/templates/home.html:24 | ||||||
| msgid "Home" | msgid "Home" | ||||||
| msgstr "首頁" | msgstr "首頁" | ||||||
|  |  | ||||||
| #: tests/test_site/templates/base.html:68 | #: tests/test_site/templates/base.html:71 | ||||||
| msgid "Log Out" | msgid "Log Out" | ||||||
| msgstr "登出" | msgstr "登出" | ||||||
|  |  | ||||||
| #: tests/test_site/templates/base.html:78 | #: tests/test_site/templates/base.html:81 | ||||||
| #: tests/test_site/templates/login.html:24 | #: tests/test_site/templates/login.html:24 | ||||||
| msgid "Log In" | msgid "Log In" | ||||||
| msgstr "登入" | msgstr "登入" | ||||||
|  |  | ||||||
| #: tests/test_site/templates/base.html:119 | #: tests/test_site/templates/base.html:122 | ||||||
| msgid "Error:" | msgid "Error:" | ||||||
| msgstr "錯誤:" | msgstr "錯誤:" | ||||||
|  |  | ||||||
| @@ -51,8 +51,8 @@ msgid "Editor" | |||||||
| msgstr "記帳者" | msgstr "記帳者" | ||||||
|  |  | ||||||
| #: tests/test_site/templates/login.html:32 | #: tests/test_site/templates/login.html:32 | ||||||
| msgid "Editor2" | msgid "Administrator" | ||||||
| msgstr "記帳者2" | msgstr "管理者" | ||||||
|  |  | ||||||
| #: tests/test_site/templates/login.html:33 | #: tests/test_site/templates/login.html:33 | ||||||
| msgid "Nobody" | msgid "Nobody" | ||||||
|   | |||||||
| @@ -26,6 +26,33 @@ from test_site import create_app | |||||||
|  |  | ||||||
| TEST_SERVER: str = "https://testserver" | TEST_SERVER: str = "https://testserver" | ||||||
| """The test server URI.""" | """The test server URI.""" | ||||||
|  | NEXT_URI: str = "/_next" | ||||||
|  | """The next URI.""" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Accounts: | ||||||
|  |     """The shortcuts to the common accounts.""" | ||||||
|  |     CASH: str = "1111-001" | ||||||
|  |     PETTY_CASH: str = "1112-001" | ||||||
|  |     BANK: str = "1113-001" | ||||||
|  |     NOTES_RECEIVABLE: str = "1131-001" | ||||||
|  |     RECEIVABLE: str = "1141-001" | ||||||
|  |     PREPAID: str = "1258-001" | ||||||
|  |     NOTES_PAYABLE: str = "2131-001" | ||||||
|  |     PAYABLE: str = "2141-001" | ||||||
|  |     SALES: str = "4111-001" | ||||||
|  |     SERVICE: str = "4611-001" | ||||||
|  |     AGENCY: str = "4711-001" | ||||||
|  |     RENT_EXPENSE: str = "6252-001" | ||||||
|  |     OFFICE: str = "6253-001" | ||||||
|  |     TRAVEL: str = "6254-001" | ||||||
|  |     POSTAGE: str = "6256-001" | ||||||
|  |     UTILITIES: str = "6261-001" | ||||||
|  |     INSURANCE: str = "6262-001" | ||||||
|  |     MEAL: str = "6272-001" | ||||||
|  |     INTEREST: str = "7111-001" | ||||||
|  |     DONATION: str = "7481-001" | ||||||
|  |     RENT_INCOME: str = "7482-001" | ||||||
|  |  | ||||||
|  |  | ||||||
| def create_test_app() -> Flask: | def create_test_app() -> Flask: | ||||||
| @@ -57,7 +84,6 @@ def get_csrf_token(client: httpx.Client) -> str: | |||||||
|     return client.get("/.csrf-token").text |     return client.get("/.csrf-token").text | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_client(app: Flask, username: str) -> tuple[httpx.Client, str]: | def get_client(app: Flask, username: str) -> tuple[httpx.Client, str]: | ||||||
|     """Returns a user client. |     """Returns a user client. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,40 +26,14 @@ import httpx | |||||||
| from flask import Flask | from flask import Flask | ||||||
|  |  | ||||||
| from test_site import db | from test_site import db | ||||||
|  | from testlib import NEXT_URI, Accounts | ||||||
|  |  | ||||||
| NEXT_URI: str = "/_next" |  | ||||||
| """The next URI.""" |  | ||||||
| NON_EMPTY_NOTE: str = "  This is \n\na test." | NON_EMPTY_NOTE: str = "  This is \n\na test." | ||||||
| """The stripped content of an non-empty note.""" | """The stripped content of an non-empty note.""" | ||||||
| EMPTY_NOTE: str = " \n\n  " | EMPTY_NOTE: str = " \n\n  " | ||||||
| """The empty note content.""" | """The empty note content.""" | ||||||
|  |  | ||||||
|  |  | ||||||
| class Accounts: |  | ||||||
|     """The shortcuts to the common accounts.""" |  | ||||||
|     CASH: str = "1111-001" |  | ||||||
|     PETTY_CASH: str = "1112-001" |  | ||||||
|     BANK: str = "1113-001" |  | ||||||
|     NOTES_RECEIVABLE: str = "1131-001" |  | ||||||
|     RECEIVABLE: str = "1141-001" |  | ||||||
|     PREPAID: str = "1258-001" |  | ||||||
|     NOTES_PAYABLE: str = "2131-001" |  | ||||||
|     PAYABLE: str = "2141-001" |  | ||||||
|     SALES: str = "4111-001" |  | ||||||
|     SERVICE: str = "4611-001" |  | ||||||
|     AGENCY: str = "4711-001" |  | ||||||
|     RENT_EXPENSE: str = "6252-001" |  | ||||||
|     OFFICE: str = "6253-001" |  | ||||||
|     TRAVEL: str = "6254-001" |  | ||||||
|     POSTAGE: str = "6256-001" |  | ||||||
|     UTILITIES: str = "6261-001" |  | ||||||
|     INSURANCE: str = "6262-001" |  | ||||||
|     MEAL: str = "6272-001" |  | ||||||
|     INTEREST: str = "7111-001" |  | ||||||
|     DONATION: str = "7481-001" |  | ||||||
|     RENT_INCOME: str = "7482-001" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_add_form(csrf_token: str) -> dict[str, str]: | def get_add_form(csrf_token: str) -> dict[str, str]: | ||||||
|     """Returns the form data to add a new journal entry. |     """Returns the form data to add a new journal entry. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,8 +26,8 @@ import httpx | |||||||
| from flask import Flask | from flask import Flask | ||||||
|  |  | ||||||
| from test_site import db | from test_site import db | ||||||
| from testlib_journal_entry import Accounts, match_journal_entry_detail, \ | from testlib import NEXT_URI, Accounts | ||||||
|     NEXT_URI | from testlib_journal_entry import match_journal_entry_detail | ||||||
|  |  | ||||||
|  |  | ||||||
| class JournalEntryLineItemData: | class JournalEntryLineItemData: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user