Compare commits
	
		
			30 Commits
		
	
	
		
			v0.9.0
			...
			7c512b1c15
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | 
@@ -760,7 +760,7 @@ class JournalEntryLineItem(db.Model):
 | 
			
		||||
        setattr(self, "__net_balance", net_balance)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def query_values(self) -> tuple[list[str], list[str]]:
 | 
			
		||||
    def query_values(self) -> list[str]:
 | 
			
		||||
        """Returns the values to be queried.
 | 
			
		||||
 | 
			
		||||
        :return: The values to be queried.
 | 
			
		||||
@@ -772,17 +772,16 @@ class JournalEntryLineItem(db.Model):
 | 
			
		||||
 | 
			
		||||
        journal_entry_day: date = self.journal_entry.date
 | 
			
		||||
        description: str = "" if self.description is None else self.description
 | 
			
		||||
        return ([description],
 | 
			
		||||
                [str(journal_entry_day.year),
 | 
			
		||||
                 "{}/{}".format(journal_entry_day.year,
 | 
			
		||||
                                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)])
 | 
			
		||||
        return [description,
 | 
			
		||||
                str(journal_entry_day.year),
 | 
			
		||||
                "{}/{}".format(journal_entry_day.year,
 | 
			
		||||
                               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)]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Option(db.Model):
 | 
			
		||||
 
 | 
			
		||||
@@ -114,10 +114,19 @@ class AccountForm {
 | 
			
		||||
        };
 | 
			
		||||
        this.#baseControl.onclick = () => {
 | 
			
		||||
            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.
 | 
			
		||||
     *
 | 
			
		||||
@@ -129,15 +138,14 @@ class AccountForm {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the base account.
 | 
			
		||||
     * Saves the selected base account.
 | 
			
		||||
     *
 | 
			
		||||
     * @param code {string} the base account code
 | 
			
		||||
     * @param text {string} the text for the base account
 | 
			
		||||
     * @param account {BaseAccountOption} the selected base account
 | 
			
		||||
     */
 | 
			
		||||
    setBaseAccount(code, text) {
 | 
			
		||||
        this.#baseCode.value = code;
 | 
			
		||||
        this.#base.innerText = text;
 | 
			
		||||
        if (["1", "2", "3"].includes(code.substring(0, 1))) {
 | 
			
		||||
    saveBaseAccount(account) {
 | 
			
		||||
        this.#baseCode.value = account.code;
 | 
			
		||||
        this.#base.innerText = account.text;
 | 
			
		||||
        if (["1", "2", "3"].includes(account.code.substring(0, 1))) {
 | 
			
		||||
            this.#isNeedOffsetControl.classList.remove("d-none");
 | 
			
		||||
            this.#isNeedOffset.disabled = false;
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -225,7 +233,7 @@ class BaseAccountSelector {
 | 
			
		||||
     * The account form
 | 
			
		||||
     * @type {AccountForm}
 | 
			
		||||
     */
 | 
			
		||||
    #form;
 | 
			
		||||
    form;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The selector modal
 | 
			
		||||
@@ -253,7 +261,7 @@ class BaseAccountSelector {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The options
 | 
			
		||||
     * @type {HTMLLIElement[]}
 | 
			
		||||
     * @type {BaseAccountOption[]}
 | 
			
		||||
     */
 | 
			
		||||
    #options;
 | 
			
		||||
 | 
			
		||||
@@ -269,83 +277,54 @@ class BaseAccountSelector {
 | 
			
		||||
     * @param form {AccountForm} the form
 | 
			
		||||
     */
 | 
			
		||||
    constructor(form) {
 | 
			
		||||
        this.#form = form;
 | 
			
		||||
        this.#modal = document.getElementById("accounting-base-selector-modal");
 | 
			
		||||
        this.#query = document.getElementById("accounting-base-selector-query");
 | 
			
		||||
        this.#optionList = document.getElementById("accounting-base-selector-option-list");
 | 
			
		||||
        // noinspection JSValidateTypes
 | 
			
		||||
        this.#options = Array.from(document.getElementsByClassName("accounting-base-selector-option"));
 | 
			
		||||
        this.#clearButton = document.getElementById("accounting-base-selector-clear");
 | 
			
		||||
        this.#queryNoResult = document.getElementById("accounting-base-selector-option-no-result");
 | 
			
		||||
        this.#modal.addEventListener("hidden.bs.modal", () => {
 | 
			
		||||
            this.#form.onBaseAccountSelectorClosed();
 | 
			
		||||
        });
 | 
			
		||||
        for (const option of this.#options) {
 | 
			
		||||
            option.onclick = () => {
 | 
			
		||||
                this.#form.setBaseAccount(option.dataset.code, option.dataset.text);
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        this.#clearButton.onclick = () => {
 | 
			
		||||
            this.#form.clearBaseAccount();
 | 
			
		||||
        };
 | 
			
		||||
        this.#initializeBaseAccountQuery();
 | 
			
		||||
        this.form = form;
 | 
			
		||||
        const prefix = "accounting-base-selector";
 | 
			
		||||
        this.#modal = document.getElementById(`${prefix}-modal`);
 | 
			
		||||
        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 BaseAccountOption(this, element));
 | 
			
		||||
        this.#clearButton = document.getElementById(`${prefix}-clear`);
 | 
			
		||||
 | 
			
		||||
        this.#modal.addEventListener("hidden.bs.modal", () => this.form.onBaseAccountSelectorClosed());
 | 
			
		||||
        this.#query.oninput = () => this.#filterOptions();
 | 
			
		||||
        this.#clearButton.onclick = () => this.form.clearBaseAccount();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initializes the query.
 | 
			
		||||
     * Filters the options.
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    #initializeBaseAccountQuery() {
 | 
			
		||||
        this.#query.addEventListener("input", () => {
 | 
			
		||||
            if (this.#query.value === "") {
 | 
			
		||||
                for (const option of this.#options) {
 | 
			
		||||
                    option.classList.remove("d-none");
 | 
			
		||||
                }
 | 
			
		||||
                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");
 | 
			
		||||
    #filterOptions() {
 | 
			
		||||
        let isAnyMatched = false;
 | 
			
		||||
        for (const option of this.#options) {
 | 
			
		||||
            if (option.isMatched(this.#query.value)) {
 | 
			
		||||
                option.setShown(true);
 | 
			
		||||
                isAnyMatched = true;
 | 
			
		||||
            } else {
 | 
			
		||||
                this.#optionList.classList.remove("d-none");
 | 
			
		||||
                this.#queryNoResult.classList.add("d-none");
 | 
			
		||||
                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");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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) {
 | 
			
		||||
            if (option.dataset.code === baseCode) {
 | 
			
		||||
                option.classList.add("active");
 | 
			
		||||
            } else {
 | 
			
		||||
                option.classList.remove("active");
 | 
			
		||||
            }
 | 
			
		||||
            option.setActive(option.code === this.form.baseCode);
 | 
			
		||||
        }
 | 
			
		||||
        if (baseCode === "") {
 | 
			
		||||
        if (this.form.baseCode === null) {
 | 
			
		||||
            this.#clearButton.classList.add("btn-secondary")
 | 
			
		||||
            this.#clearButton.classList.remove("btn-danger");
 | 
			
		||||
            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;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The prefix of the HTML ID and class
 | 
			
		||||
     * The prefix of the HTML ID and class names
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    prefix;
 | 
			
		||||
@@ -278,7 +278,7 @@ class TabPlane {
 | 
			
		||||
    editor;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The prefix of the HTML ID and classes
 | 
			
		||||
     * The prefix of the HTML ID and class names
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    prefix;
 | 
			
		||||
@@ -984,7 +984,6 @@ class RecurringTransactionTab extends TabPlane {
 | 
			
		||||
     */
 | 
			
		||||
    #itemButtons;
 | 
			
		||||
 | 
			
		||||
    // noinspection JSValidateTypes
 | 
			
		||||
    /**
 | 
			
		||||
     * Constructs a tab plane.
 | 
			
		||||
     *
 | 
			
		||||
@@ -1019,7 +1018,7 @@ class RecurringTransactionTab extends TabPlane {
 | 
			
		||||
     * @return {string} the description of the recurring item
 | 
			
		||||
     */
 | 
			
		||||
    #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 lastMonth = (thisMonth + 10) % 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() {
 | 
			
		||||
        this.#element = document.getElementById("accounting-form");
 | 
			
		||||
        this.lineItemTemplate = this.#element.dataset.lineItemTemplate;
 | 
			
		||||
        this.lineItemEditor = new JournalEntryLineItemEditor(this);
 | 
			
		||||
        this.#date = document.getElementById("accounting-date");
 | 
			
		||||
        this.#dateError = document.getElementById("accounting-date-error");
 | 
			
		||||
        this.#currencyControl = document.getElementById("accounting-currencies");
 | 
			
		||||
@@ -121,7 +122,6 @@ class JournalEntryForm {
 | 
			
		||||
        this.#addCurrencyButton = document.getElementById("accounting-add-currency");
 | 
			
		||||
        this.#note = document.getElementById("accounting-note");
 | 
			
		||||
        this.#noteError = document.getElementById("accounting-note-error");
 | 
			
		||||
        this.lineItemEditor = new JournalEntryLineItemEditor(this);
 | 
			
		||||
 | 
			
		||||
        this.#addCurrencyButton.onclick = () => {
 | 
			
		||||
            const newIndex = 1 + (this.#currencies.length === 0? 0: Math.max(...this.#currencies.map((currency) => currency.index)));
 | 
			
		||||
@@ -159,7 +159,7 @@ class JournalEntryForm {
 | 
			
		||||
     */
 | 
			
		||||
    #resetDeleteCurrencyButtons() {
 | 
			
		||||
        if (this.#currencies.length === 1) {
 | 
			
		||||
            this.#currencies[0].deleteButton.classList.add("d-none");
 | 
			
		||||
            this.#currencies[0].setDeleteButtonShown(false);
 | 
			
		||||
        } else {
 | 
			
		||||
            for (const currency of this.#currencies) {
 | 
			
		||||
                let isAnyLineItemMatched = false;
 | 
			
		||||
@@ -169,11 +169,7 @@ class JournalEntryForm {
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (isAnyLineItemMatched) {
 | 
			
		||||
                    currency.deleteButton.classList.add("d-none");
 | 
			
		||||
                } else {
 | 
			
		||||
                    currency.deleteButton.classList.remove("d-none");
 | 
			
		||||
                }
 | 
			
		||||
                currency.setDeleteButtonShown(!isAnyLineItemMatched);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -184,10 +180,8 @@ class JournalEntryForm {
 | 
			
		||||
     */
 | 
			
		||||
    #initializeDragAndDropReordering() {
 | 
			
		||||
        initializeDragAndDropReordering(this.#currencyList, () => {
 | 
			
		||||
            const currencyId = Array.from(this.#currencyList.children).map((currency) => currency.id);
 | 
			
		||||
            this.#currencies.sort((a, b) => currencyId.indexOf(a.element.id) - currencyId.indexOf(b.element.id));
 | 
			
		||||
            for (let i = 0; i < this.#currencies.length; i++) {
 | 
			
		||||
                this.#currencies[i].no.value = String(i + 1);
 | 
			
		||||
            for (const currency of this.#currencies) {
 | 
			
		||||
                currency.resetNo();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
@@ -213,7 +207,7 @@ class JournalEntryForm {
 | 
			
		||||
     * @return {string[]} the account codes used in the form
 | 
			
		||||
     */
 | 
			
		||||
    getAccountCodesUsed(debitCredit) {
 | 
			
		||||
        return this.getLineItems(debitCredit).map((lineItem) => lineItem.getAccountCode())
 | 
			
		||||
        return this.getLineItems(debitCredit).map((lineItem) => lineItem.accountCode)
 | 
			
		||||
            .filter((code) => code !== null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -222,7 +216,7 @@ class JournalEntryForm {
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string} the date
 | 
			
		||||
     */
 | 
			
		||||
    getDate() {
 | 
			
		||||
    get date() {
 | 
			
		||||
        return this.#date.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -233,7 +227,7 @@ class JournalEntryForm {
 | 
			
		||||
    updateMinDate() {
 | 
			
		||||
        let lastOriginalLineItemDate = null;
 | 
			
		||||
        for (const lineItem of this.getLineItems()) {
 | 
			
		||||
            const date = lineItem.getOriginalLineItemDate();
 | 
			
		||||
            const date = lineItem.originalLineItemDate;
 | 
			
		||||
            if (date !== null) {
 | 
			
		||||
                if (lastOriginalLineItemDate === null || lastOriginalLineItemDate < date) {
 | 
			
		||||
                    lastOriginalLineItemDate = date;
 | 
			
		||||
@@ -349,7 +343,7 @@ class CurrencySubForm {
 | 
			
		||||
     * The element
 | 
			
		||||
     * @type {HTMLDivElement}
 | 
			
		||||
     */
 | 
			
		||||
    element;
 | 
			
		||||
    #element;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The journal entry form
 | 
			
		||||
@@ -363,12 +357,6 @@ class CurrencySubForm {
 | 
			
		||||
     */
 | 
			
		||||
    index;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The prefix of the HTML ID and class
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    #prefix;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The control
 | 
			
		||||
     * @type {HTMLDivElement}
 | 
			
		||||
@@ -385,7 +373,7 @@ class CurrencySubForm {
 | 
			
		||||
     * The number
 | 
			
		||||
     * @type {HTMLInputElement}
 | 
			
		||||
     */
 | 
			
		||||
    no;
 | 
			
		||||
    #no;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The currency code
 | 
			
		||||
@@ -403,7 +391,7 @@ class CurrencySubForm {
 | 
			
		||||
     * The button to delete the currency
 | 
			
		||||
     * @type {HTMLButtonElement}
 | 
			
		||||
     */
 | 
			
		||||
    deleteButton;
 | 
			
		||||
    #deleteButton;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The debit sub-form
 | 
			
		||||
@@ -424,36 +412,58 @@ class CurrencySubForm {
 | 
			
		||||
     * @param element {HTMLDivElement} the currency sub-form element
 | 
			
		||||
     */
 | 
			
		||||
    constructor(form, element) {
 | 
			
		||||
        this.element = element;
 | 
			
		||||
        this.#element = element;
 | 
			
		||||
        this.form = form;
 | 
			
		||||
        this.index = parseInt(this.element.dataset.index);
 | 
			
		||||
        this.#prefix = "accounting-currency-" + String(this.index);
 | 
			
		||||
        this.#control = document.getElementById(this.#prefix + "-control");
 | 
			
		||||
        this.#error = document.getElementById(this.#prefix + "-error");
 | 
			
		||||
        this.no = document.getElementById(this.#prefix + "-no");
 | 
			
		||||
        this.#code = document.getElementById(this.#prefix + "-code");
 | 
			
		||||
        this.#codeSelect = document.getElementById(this.#prefix + "-code-select");
 | 
			
		||||
        this.deleteButton = document.getElementById(this.#prefix + "-delete");
 | 
			
		||||
        const debitElement = document.getElementById(this.#prefix + "-debit");
 | 
			
		||||
        this.index = parseInt(this.#element.dataset.index);
 | 
			
		||||
        const prefix = "accounting-currency-" + String(this.index);
 | 
			
		||||
        this.#control = document.getElementById(prefix + "-control");
 | 
			
		||||
        this.#error = document.getElementById(prefix + "-error");
 | 
			
		||||
        this.#no = document.getElementById(prefix + "-no");
 | 
			
		||||
        this.#code = document.getElementById(prefix + "-code");
 | 
			
		||||
        this.#codeSelect = document.getElementById(prefix + "-code-select");
 | 
			
		||||
        this.#deleteButton = document.getElementById(prefix + "-delete");
 | 
			
		||||
        const debitElement = document.getElementById(prefix + "-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.#codeSelect.onchange = () => this.#code.value = this.#codeSelect.value;
 | 
			
		||||
        this.deleteButton.onclick = () => {
 | 
			
		||||
            this.element.parentElement.removeChild(this.element);
 | 
			
		||||
        this.#deleteButton.onclick = () => {
 | 
			
		||||
            this.#element.parentElement.removeChild(this.#element);
 | 
			
		||||
            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.
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string} the currency code
 | 
			
		||||
     */
 | 
			
		||||
    getCurrencyCode() {
 | 
			
		||||
    get currencyCode() {
 | 
			
		||||
        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.
 | 
			
		||||
     *
 | 
			
		||||
@@ -479,7 +489,7 @@ class CurrencySubForm {
 | 
			
		||||
    updateCodeSelectorStatus() {
 | 
			
		||||
        let isEnabled = true;
 | 
			
		||||
        for (const lineItem of this.getLineItems()) {
 | 
			
		||||
            if (lineItem.getOriginalLineItemId() !== null) {
 | 
			
		||||
            if (lineItem.originalLineItemId !== null) {
 | 
			
		||||
                isEnabled = false;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
@@ -511,7 +521,7 @@ class CurrencySubForm {
 | 
			
		||||
     */
 | 
			
		||||
    validateBalance() {
 | 
			
		||||
        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.#error.innerText = A_("The totals of the debit and credit amounts do not match.");
 | 
			
		||||
                return false;
 | 
			
		||||
@@ -541,6 +551,12 @@ class DebitCreditSubForm {
 | 
			
		||||
     */
 | 
			
		||||
    #element;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The content
 | 
			
		||||
     * @type {HTMLDivElement}
 | 
			
		||||
     */
 | 
			
		||||
    #content;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The currencyIndex
 | 
			
		||||
     * @type {number}
 | 
			
		||||
@@ -554,7 +570,7 @@ class DebitCreditSubForm {
 | 
			
		||||
    debitCredit;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The prefix of the HTML ID and class
 | 
			
		||||
     * The prefix of the HTML ID and class names
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    #prefix;
 | 
			
		||||
@@ -602,24 +618,36 @@ class DebitCreditSubForm {
 | 
			
		||||
        this.#currencyIndex = currency.index;
 | 
			
		||||
        this.debitCredit = debitCredit;
 | 
			
		||||
        this.#prefix = "accounting-currency-" + String(this.#currencyIndex) + "-" + debitCredit;
 | 
			
		||||
        this.#content = document.getElementById(this.#prefix + "-content");
 | 
			
		||||
        this.#error = document.getElementById(this.#prefix + "-error");
 | 
			
		||||
        this.#lineItemList = document.getElementById(this.#prefix + "-list");
 | 
			
		||||
        // noinspection JSValidateTypes
 | 
			
		||||
        this.lineItems = Array.from(document.getElementsByClassName(this.#prefix)).map((element) => new LineItemSubForm(this, element));
 | 
			
		||||
        this.#total = document.getElementById(this.#prefix + "-total");
 | 
			
		||||
        this.#addLineItemButton = document.getElementById(this.#prefix + "-add-line-item");
 | 
			
		||||
 | 
			
		||||
        this.#resetContent();
 | 
			
		||||
        this.#addLineItemButton.onclick = () => this.currency.form.lineItemEditor.onAddNew(this);
 | 
			
		||||
        this.#resetDeleteLineItemButtons();
 | 
			
		||||
        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
 | 
			
		||||
     *
 | 
			
		||||
     * @returns {LineItemSubForm} the newly-added line item sub-form
 | 
			
		||||
     */
 | 
			
		||||
    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
 | 
			
		||||
            .replaceAll("CURRENCY_INDEX", escapeHtml(String(this.#currencyIndex)))
 | 
			
		||||
            .replaceAll("DEBIT_CREDIT", escapeHtml(this.debitCredit))
 | 
			
		||||
@@ -627,6 +655,7 @@ class DebitCreditSubForm {
 | 
			
		||||
        this.#lineItemList.insertAdjacentHTML("beforeend", html);
 | 
			
		||||
        const lineItem = new LineItemSubForm(this, document.getElementById(this.#prefix + "-" + String(newIndex)));
 | 
			
		||||
        this.lineItems.push(lineItem);
 | 
			
		||||
        this.#resetContent();
 | 
			
		||||
        this.#resetDeleteLineItemButtons();
 | 
			
		||||
        this.#initializeDragAndDropReordering();
 | 
			
		||||
        this.validate();
 | 
			
		||||
@@ -644,6 +673,7 @@ class DebitCreditSubForm {
 | 
			
		||||
        this.updateTotal();
 | 
			
		||||
        this.currency.updateCodeSelectorStatus();
 | 
			
		||||
        this.currency.form.updateMinDate();
 | 
			
		||||
        this.#resetContent();
 | 
			
		||||
        this.#resetDeleteLineItemButtons();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -653,27 +683,48 @@ class DebitCreditSubForm {
 | 
			
		||||
     */
 | 
			
		||||
    #resetDeleteLineItemButtons() {
 | 
			
		||||
        if (this.lineItems.length === 1) {
 | 
			
		||||
            this.lineItems[0].deleteButton.classList.add("d-none");
 | 
			
		||||
            this.lineItems[0].setDeleteButtonShown(false);
 | 
			
		||||
        } else {
 | 
			
		||||
            for (const lineItem of this.lineItems) {
 | 
			
		||||
                if (lineItem.isMatched) {
 | 
			
		||||
                    lineItem.deleteButton.classList.add("d-none");
 | 
			
		||||
                } else {
 | 
			
		||||
                    lineItem.deleteButton.classList.remove("d-none");
 | 
			
		||||
                }
 | 
			
		||||
                lineItem.setDeleteButtonShown(!lineItem.isMatched);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     *
 | 
			
		||||
     * @return {Decimal} the total amount
 | 
			
		||||
     */
 | 
			
		||||
    getTotal() {
 | 
			
		||||
    get total() {
 | 
			
		||||
        let total = new Decimal("0");
 | 
			
		||||
        for (const lineItem of this.lineItems) {
 | 
			
		||||
            const amount = lineItem.getAmount();
 | 
			
		||||
            const amount = lineItem.amount;
 | 
			
		||||
            if (amount !== null) {
 | 
			
		||||
                total = total.plus(amount);
 | 
			
		||||
            }
 | 
			
		||||
@@ -686,7 +737,7 @@ class DebitCreditSubForm {
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    updateTotal() {
 | 
			
		||||
        this.#total.innerText = formatDecimal(this.getTotal());
 | 
			
		||||
        this.#total.innerText = formatDecimal(this.total);
 | 
			
		||||
        this.currency.validateBalance();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -696,10 +747,8 @@ class DebitCreditSubForm {
 | 
			
		||||
     */
 | 
			
		||||
    #initializeDragAndDropReordering() {
 | 
			
		||||
        initializeDragAndDropReordering(this.#lineItemList, () => {
 | 
			
		||||
            const lineItemId = Array.from(this.#lineItemList.children).map((lineItem) => lineItem.id);
 | 
			
		||||
            this.lineItems.sort((a, b) => lineItemId.indexOf(a.element.id) - lineItemId.indexOf(b.element.id));
 | 
			
		||||
            for (let i = 0; i < this.lineItems.length; i++) {
 | 
			
		||||
                this.lineItems[i].no.value = String(i + 1);
 | 
			
		||||
            for (const lineItem of this.lineItems) {
 | 
			
		||||
                lineItem.resetNo();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
@@ -751,7 +800,7 @@ class LineItemSubForm {
 | 
			
		||||
     * The element
 | 
			
		||||
     * @type {HTMLLIElement}
 | 
			
		||||
     */
 | 
			
		||||
    element;
 | 
			
		||||
    #element;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Either "debit" or "credit"
 | 
			
		||||
@@ -763,7 +812,7 @@ class LineItemSubForm {
 | 
			
		||||
     * The line item index
 | 
			
		||||
     * @type {number}
 | 
			
		||||
     */
 | 
			
		||||
    lineItemIndex;
 | 
			
		||||
    index;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether this is an original line item with offsets
 | 
			
		||||
@@ -771,12 +820,6 @@ class LineItemSubForm {
 | 
			
		||||
     */
 | 
			
		||||
    isMatched;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The prefix of the HTML ID and class
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    #prefix;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The control
 | 
			
		||||
     * @type {HTMLDivElement}
 | 
			
		||||
@@ -793,7 +836,7 @@ class LineItemSubForm {
 | 
			
		||||
     * The number
 | 
			
		||||
     * @type {HTMLInputElement}
 | 
			
		||||
     */
 | 
			
		||||
    no;
 | 
			
		||||
    #no;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The account code
 | 
			
		||||
@@ -853,7 +896,7 @@ class LineItemSubForm {
 | 
			
		||||
     * The button to delete line item
 | 
			
		||||
     * @type {HTMLButtonElement}
 | 
			
		||||
     */
 | 
			
		||||
    deleteButton;
 | 
			
		||||
    #deleteButton;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Constructs the line item sub-form.
 | 
			
		||||
@@ -863,38 +906,47 @@ class LineItemSubForm {
 | 
			
		||||
     */
 | 
			
		||||
    constructor(debitCredit, element) {
 | 
			
		||||
        this.debitCreditSubForm = debitCredit;
 | 
			
		||||
        this.element = element;
 | 
			
		||||
        this.#element = element;
 | 
			
		||||
        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.#prefix = "accounting-currency-" + element.dataset.currencyIndex + "-" + this.debitCredit + "-" + this.lineItemIndex;
 | 
			
		||||
        this.#control = document.getElementById(this.#prefix + "-control");
 | 
			
		||||
        this.#error = document.getElementById(this.#prefix + "-error");
 | 
			
		||||
        this.no = document.getElementById(this.#prefix + "-no");
 | 
			
		||||
        this.#accountCode = document.getElementById(this.#prefix + "-account-code");
 | 
			
		||||
        this.#accountText = document.getElementById(this.#prefix + "-account-text");
 | 
			
		||||
        this.#description = document.getElementById(this.#prefix + "-description");
 | 
			
		||||
        this.#descriptionText = document.getElementById(this.#prefix + "-description-text");
 | 
			
		||||
        this.#originalLineItemId = document.getElementById(this.#prefix + "-original-line-item-id");
 | 
			
		||||
        this.#originalLineItemText = document.getElementById(this.#prefix + "-original-line-item-text");
 | 
			
		||||
        this.#offsets = document.getElementById(this.#prefix + "-offsets");
 | 
			
		||||
        this.#amount = document.getElementById(this.#prefix + "-amount");
 | 
			
		||||
        this.#amountText = document.getElementById(this.#prefix + "-amount-text");
 | 
			
		||||
        this.deleteButton = document.getElementById(this.#prefix + "-delete");
 | 
			
		||||
        const prefix = "accounting-currency-" + element.dataset.currencyIndex + "-" + this.debitCredit + "-" + String(this.index);
 | 
			
		||||
        this.#control = document.getElementById(prefix + "-control");
 | 
			
		||||
        this.#error = document.getElementById(prefix + "-error");
 | 
			
		||||
        this.#no = document.getElementById(prefix + "-no");
 | 
			
		||||
        this.#accountCode = document.getElementById(prefix + "-account-code");
 | 
			
		||||
        this.#accountText = document.getElementById(prefix + "-account-text");
 | 
			
		||||
        this.#description = document.getElementById(prefix + "-description");
 | 
			
		||||
        this.#descriptionText = document.getElementById(prefix + "-description-text");
 | 
			
		||||
        this.#originalLineItemId = document.getElementById(prefix + "-original-line-item-id");
 | 
			
		||||
        this.#originalLineItemText = document.getElementById(prefix + "-original-line-item-text");
 | 
			
		||||
        this.#offsets = document.getElementById(prefix + "-offsets");
 | 
			
		||||
        this.#amount = document.getElementById(prefix + "-amount");
 | 
			
		||||
        this.#amountText = document.getElementById(prefix + "-amount-text");
 | 
			
		||||
        this.#deleteButton = document.getElementById(prefix + "-delete");
 | 
			
		||||
        this.#control.onclick = () => this.debitCreditSubForm.currency.form.lineItemEditor.onEdit(this);
 | 
			
		||||
        this.deleteButton.onclick = () => {
 | 
			
		||||
            this.element.parentElement.removeChild(this.element);
 | 
			
		||||
        this.#deleteButton.onclick = () => {
 | 
			
		||||
            this.#element.parentElement.removeChild(this.#element);
 | 
			
		||||
            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.
 | 
			
		||||
     *
 | 
			
		||||
     * @return {boolean} true if the line item needs offset, or false otherwise
 | 
			
		||||
     */
 | 
			
		||||
    isNeedOffset() {
 | 
			
		||||
        return "isNeedOffset" in this.element.dataset;
 | 
			
		||||
    get isNeedOffset() {
 | 
			
		||||
        return "isNeedOffset" in this.#element.dataset;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -902,7 +954,7 @@ class LineItemSubForm {
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string|null} the ID of the original line item
 | 
			
		||||
     */
 | 
			
		||||
    getOriginalLineItemId() {
 | 
			
		||||
    get originalLineItemId() {
 | 
			
		||||
        return this.#originalLineItemId.value === ""? null: this.#originalLineItemId.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -911,7 +963,7 @@ class LineItemSubForm {
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string|null} the date of the original line item
 | 
			
		||||
     */
 | 
			
		||||
    getOriginalLineItemDate() {
 | 
			
		||||
    get originalLineItemDate() {
 | 
			
		||||
        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
 | 
			
		||||
     */
 | 
			
		||||
    getOriginalLineItemText() {
 | 
			
		||||
    get originalLineItemText() {
 | 
			
		||||
        return this.#originalLineItemId.dataset.text === ""? null: this.#originalLineItemId.dataset.text;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -929,7 +981,7 @@ class LineItemSubForm {
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string|null} the description
 | 
			
		||||
     */
 | 
			
		||||
    getDescription() {
 | 
			
		||||
    get description() {
 | 
			
		||||
        return this.#description.value === ""? null: this.#description.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -938,7 +990,7 @@ class LineItemSubForm {
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string|null} the account code
 | 
			
		||||
     */
 | 
			
		||||
    getAccountCode() {
 | 
			
		||||
    get accountCode() {
 | 
			
		||||
        return this.#accountCode.value === ""? null: this.#accountCode.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -947,7 +999,7 @@ class LineItemSubForm {
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string|null} the account text
 | 
			
		||||
     */
 | 
			
		||||
    getAccountText() {
 | 
			
		||||
    get accountText() {
 | 
			
		||||
        return this.#accountCode.dataset.text === ""? null: this.#accountCode.dataset.text;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -956,7 +1008,7 @@ class LineItemSubForm {
 | 
			
		||||
     *
 | 
			
		||||
     * @return {Decimal|null} the amount
 | 
			
		||||
     */
 | 
			
		||||
    getAmount() {
 | 
			
		||||
    get amount() {
 | 
			
		||||
        return this.#amount.value === ""? null: new Decimal(this.#amount.value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -965,10 +1017,23 @@ class LineItemSubForm {
 | 
			
		||||
     *
 | 
			
		||||
     * @return {Decimal|null} the minimal amount
 | 
			
		||||
     */
 | 
			
		||||
    getAmountMin() {
 | 
			
		||||
    get amountMin() {
 | 
			
		||||
        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.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,7 @@ class JournalEntryLineItemEditor {
 | 
			
		||||
     * The bootstrap modal
 | 
			
		||||
     * @type {HTMLDivElement}
 | 
			
		||||
     */
 | 
			
		||||
    #modal;
 | 
			
		||||
    modal;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Either "debit" or "credit"
 | 
			
		||||
@@ -53,7 +53,7 @@ class JournalEntryLineItemEditor {
 | 
			
		||||
    debitCredit;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The prefix of the HTML ID and class
 | 
			
		||||
     * The prefix of the HTML ID and class names
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    #prefix = "accounting-line-item-editor"
 | 
			
		||||
@@ -190,12 +190,6 @@ class JournalEntryLineItemEditor {
 | 
			
		||||
     */
 | 
			
		||||
    description = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The amount
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    amount = "";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The description editors
 | 
			
		||||
     * @type {{debit: DescriptionEditor, credit: DescriptionEditor}}
 | 
			
		||||
@@ -204,7 +198,7 @@ class JournalEntryLineItemEditor {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The account selectors
 | 
			
		||||
     * @type {{debit: AccountSelector, credit: AccountSelector}}
 | 
			
		||||
     * @type {{debit: JournalEntryAccountSelector, credit: JournalEntryAccountSelector}}
 | 
			
		||||
     */
 | 
			
		||||
    #accountSelectors;
 | 
			
		||||
 | 
			
		||||
@@ -222,7 +216,7 @@ class JournalEntryLineItemEditor {
 | 
			
		||||
    constructor(form) {
 | 
			
		||||
        this.form = form;
 | 
			
		||||
        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.#originalLineItemControl = document.getElementById(this.#prefix + "-original-line-item-control");
 | 
			
		||||
        this.#originalLineItemText = document.getElementById(this.#prefix + "-original-line-item");
 | 
			
		||||
@@ -237,8 +231,9 @@ class JournalEntryLineItemEditor {
 | 
			
		||||
        this.#amountInput = document.getElementById(this.#prefix + "-amount");
 | 
			
		||||
        this.#amountError = document.getElementById(this.#prefix + "-amount-error");
 | 
			
		||||
        this.#descriptionEditors = DescriptionEditor.getInstances(this);
 | 
			
		||||
        this.#accountSelectors = AccountSelector.getInstances(this);
 | 
			
		||||
        this.#accountSelectors = JournalEntryAccountSelector.getInstances(this);
 | 
			
		||||
        this.originalLineItemSelector = new OriginalLineItemSelector(this);
 | 
			
		||||
 | 
			
		||||
        this.#originalLineItemControl.onclick = () => this.originalLineItemSelector.onOpen()
 | 
			
		||||
        this.#originalLineItemDelete.onclick = () => this.clearOriginalLineItem();
 | 
			
		||||
        this.#descriptionControl.onclick = () => this.#descriptionEditors[this.debitCredit].onOpen();
 | 
			
		||||
@@ -249,12 +244,30 @@ class JournalEntryLineItemEditor {
 | 
			
		||||
                if (this.lineItem === null) {
 | 
			
		||||
                    this.lineItem = this.#debitCreditSubForm.addLineItem();
 | 
			
		||||
                }
 | 
			
		||||
                this.amount = this.#amountInput.value;
 | 
			
		||||
                this.lineItem.save(this);
 | 
			
		||||
                bootstrap.Modal.getInstance(this.#modal).hide();
 | 
			
		||||
                bootstrap.Modal.getInstance(this.modal).hide();
 | 
			
		||||
            }
 | 
			
		||||
            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 = "";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the currency code.
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string} the currency code
 | 
			
		||||
     */
 | 
			
		||||
    getCurrencyCode() {
 | 
			
		||||
        return this.#debitCreditSubForm.currency.getCurrencyCode();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Saves the description from the description editor.
 | 
			
		||||
     *
 | 
			
		||||
@@ -331,7 +335,7 @@ class JournalEntryLineItemEditor {
 | 
			
		||||
        this.description = description === ""? null: description;
 | 
			
		||||
        this.#descriptionText.innerText = description;
 | 
			
		||||
        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 text {string} the account text
 | 
			
		||||
     * @param isNeedOffset {boolean} true if the line items in the account need offset or false otherwise
 | 
			
		||||
     * @param account {JournalEntryAccountOption} the selected account
 | 
			
		||||
     */
 | 
			
		||||
    saveAccount(code, text, isNeedOffset) {
 | 
			
		||||
        this.isNeedOffset = isNeedOffset;
 | 
			
		||||
    saveAccount(account) {
 | 
			
		||||
        this.isNeedOffset = account.isNeedOffset;
 | 
			
		||||
        this.#accountControl.classList.add("accounting-not-empty");
 | 
			
		||||
        this.accountCode = code;
 | 
			
		||||
        this.accountText = text;
 | 
			
		||||
        this.#accountText.innerText = text;
 | 
			
		||||
        this.accountCode = account.code;
 | 
			
		||||
        this.accountText = account.text;
 | 
			
		||||
        this.#accountText.innerText = account.text;
 | 
			
		||||
        this.#validateAccount();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -520,10 +522,10 @@ class JournalEntryLineItemEditor {
 | 
			
		||||
        this.lineItem = lineItem;
 | 
			
		||||
        this.#debitCreditSubForm = lineItem.debitCreditSubForm;
 | 
			
		||||
        this.debitCredit = this.#debitCreditSubForm.debitCredit;
 | 
			
		||||
        this.isNeedOffset = lineItem.isNeedOffset();
 | 
			
		||||
        this.originalLineItemId = lineItem.getOriginalLineItemId();
 | 
			
		||||
        this.originalLineItemDate = lineItem.getOriginalLineItemDate();
 | 
			
		||||
        this.originalLineItemText = lineItem.getOriginalLineItemText();
 | 
			
		||||
        this.isNeedOffset = lineItem.isNeedOffset;
 | 
			
		||||
        this.originalLineItemId = lineItem.originalLineItemId;
 | 
			
		||||
        this.originalLineItemDate = lineItem.originalLineItemDate;
 | 
			
		||||
        this.originalLineItemText = lineItem.originalLineItemText;
 | 
			
		||||
        this.#originalLineItemText.innerText = this.originalLineItemText;
 | 
			
		||||
        if (this.originalLineItemId === null) {
 | 
			
		||||
            this.#originalLineItemContainer.classList.add("d-none");
 | 
			
		||||
@@ -533,25 +535,25 @@ class JournalEntryLineItemEditor {
 | 
			
		||||
            this.#originalLineItemControl.classList.add("accounting-not-empty");
 | 
			
		||||
        }
 | 
			
		||||
        this.#setEnableDescriptionAccount(!lineItem.isMatched && this.originalLineItemId === null);
 | 
			
		||||
        this.description = lineItem.getDescription();
 | 
			
		||||
        this.description = lineItem.description;
 | 
			
		||||
        if (this.description === null) {
 | 
			
		||||
            this.#descriptionControl.classList.remove("accounting-not-empty");
 | 
			
		||||
        } else {
 | 
			
		||||
            this.#descriptionControl.classList.add("accounting-not-empty");
 | 
			
		||||
        }
 | 
			
		||||
        this.#descriptionText.innerText = this.description === null? "": this.description;
 | 
			
		||||
        if (lineItem.getAccountCode() === null) {
 | 
			
		||||
        if (lineItem.accountCode === null) {
 | 
			
		||||
            this.#accountControl.classList.remove("accounting-not-empty");
 | 
			
		||||
        } else {
 | 
			
		||||
            this.#accountControl.classList.add("accounting-not-empty");
 | 
			
		||||
        }
 | 
			
		||||
        this.accountCode = lineItem.getAccountCode();
 | 
			
		||||
        this.accountText = lineItem.getAccountText();
 | 
			
		||||
        this.accountCode = lineItem.accountCode;
 | 
			
		||||
        this.accountText = lineItem.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();
 | 
			
		||||
        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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -197,7 +197,7 @@ class RecurringExpenseIncomeSubForm {
 | 
			
		||||
    editor;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The prefix of HTML ID and class
 | 
			
		||||
     * The prefix of the HTML ID and class names
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    #prefix;
 | 
			
		||||
@@ -313,10 +313,8 @@ class RecurringExpenseIncomeSubForm {
 | 
			
		||||
     */
 | 
			
		||||
    #initializeDragAndDropReordering() {
 | 
			
		||||
        initializeDragAndDropReordering(this.#itemList, () => {
 | 
			
		||||
            const itemId = Array.from(this.#itemList.children).map((item) => item.id);
 | 
			
		||||
            this.#items.sort((a, b) => itemId.indexOf(a.element.id) - itemId.indexOf(b.element.id));
 | 
			
		||||
            for (let i = 0; i < this.#items.length; i++) {
 | 
			
		||||
                this.#items[i].no.value = String(i + 1);
 | 
			
		||||
            for (const item of this.#items) {
 | 
			
		||||
                item.resetNo();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
@@ -365,7 +363,7 @@ class RecurringItemSubForm {
 | 
			
		||||
     * The element
 | 
			
		||||
     * @type {HTMLLIElement}
 | 
			
		||||
     */
 | 
			
		||||
    element;
 | 
			
		||||
    #element;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The item index
 | 
			
		||||
@@ -386,10 +384,10 @@ class RecurringItemSubForm {
 | 
			
		||||
    #error;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The number
 | 
			
		||||
     * The order number
 | 
			
		||||
     * @type {HTMLInputElement}
 | 
			
		||||
     */
 | 
			
		||||
    no;
 | 
			
		||||
    #no;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The name input
 | 
			
		||||
@@ -441,12 +439,12 @@ class RecurringItemSubForm {
 | 
			
		||||
     */
 | 
			
		||||
    constructor(expenseIncomeSubForm, element) {
 | 
			
		||||
        this.#expenseIncomeSubForm = expenseIncomeSubForm
 | 
			
		||||
        this.element = element;
 | 
			
		||||
        this.#element = element;
 | 
			
		||||
        this.itemIndex = parseInt(element.dataset.itemIndex);
 | 
			
		||||
        const prefix = "accounting-recurring-" + expenseIncomeSubForm.expenseIncome + "-" + element.dataset.itemIndex;
 | 
			
		||||
        this.#control = document.getElementById(prefix + "-control");
 | 
			
		||||
        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.#nameText = document.getElementById(prefix + "-name-text");
 | 
			
		||||
        this.#accountCode = document.getElementById(prefix + "-account-code");
 | 
			
		||||
@@ -457,17 +455,26 @@ class RecurringItemSubForm {
 | 
			
		||||
 | 
			
		||||
        this.#control.onclick = () => this.#expenseIncomeSubForm.editor.onEdit(this);
 | 
			
		||||
        this.deleteButton.onclick = () => {
 | 
			
		||||
            this.element.parentElement.removeChild(this.element);
 | 
			
		||||
            this.#element.parentElement.removeChild(this.#element);
 | 
			
		||||
            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.
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string|null} the name
 | 
			
		||||
     */
 | 
			
		||||
    getName() {
 | 
			
		||||
    get name() {
 | 
			
		||||
        return this.#name.value === ""? null: this.#name.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -476,7 +483,7 @@ class RecurringItemSubForm {
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string|null} the account code
 | 
			
		||||
     */
 | 
			
		||||
    getAccountCode() {
 | 
			
		||||
    get accountCode() {
 | 
			
		||||
        return this.#accountCode.value === ""? null: this.#accountCode.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -485,7 +492,7 @@ class RecurringItemSubForm {
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string|null} the account text
 | 
			
		||||
     */
 | 
			
		||||
    getAccountText() {
 | 
			
		||||
    get accountText() {
 | 
			
		||||
        return this.#accountCode.dataset.text === ""? null: this.#accountCode.dataset.text;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -494,7 +501,7 @@ class RecurringItemSubForm {
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string|null} the description template
 | 
			
		||||
     */
 | 
			
		||||
    getDescriptionTemplate() {
 | 
			
		||||
    get descriptionTemplate() {
 | 
			
		||||
        return this.#descriptionTemplate.value === ""? null: this.#descriptionTemplate.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -504,12 +511,12 @@ class RecurringItemSubForm {
 | 
			
		||||
     * @param editor {RecurringItemEditor} the recurring item 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.#accountCode.value = editor.accountCode;
 | 
			
		||||
        this.#accountCode.dataset.text = 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.validate();
 | 
			
		||||
    }
 | 
			
		||||
@@ -677,7 +684,7 @@ class RecurringItemEditor {
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string|null} the name
 | 
			
		||||
     */
 | 
			
		||||
    getName() {
 | 
			
		||||
    get name() {
 | 
			
		||||
        return this.#name.value === ""? null: this.#name.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -686,7 +693,7 @@ class RecurringItemEditor {
 | 
			
		||||
     *
 | 
			
		||||
     * @return {string|null} the description template
 | 
			
		||||
     */
 | 
			
		||||
    getDescriptionTemplate() {
 | 
			
		||||
    get descriptionTemplate() {
 | 
			
		||||
        return this.#descriptionTemplate.value === ""? null: this.#descriptionTemplate.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -742,16 +749,16 @@ class RecurringItemEditor {
 | 
			
		||||
     */
 | 
			
		||||
    onEdit(item) {
 | 
			
		||||
        this.#item = item;
 | 
			
		||||
        this.#name.value = item.getName() === null? "": item.getName();
 | 
			
		||||
        this.accountCode = item.getAccountCode();
 | 
			
		||||
        this.accountText = item.getAccountText();
 | 
			
		||||
        this.#name.value = item.name === null? "": item.name;
 | 
			
		||||
        this.accountCode = item.accountCode;
 | 
			
		||||
        this.accountText = item.accountText;
 | 
			
		||||
        if (this.accountText === null) {
 | 
			
		||||
            this.#accountControl.classList.remove("accounting-not-empty");
 | 
			
		||||
        } else {
 | 
			
		||||
            this.#accountControl.classList.add("accounting-not-empty");
 | 
			
		||||
        }
 | 
			
		||||
        this.#accountContainer.innerText = item.getAccountText() == null? "": item.getAccountText();
 | 
			
		||||
        this.#descriptionTemplate.value = item.getDescriptionTemplate() === null? "": item.getDescriptionTemplate();
 | 
			
		||||
        this.#accountContainer.innerText = this.accountText === null? "": this.accountText;
 | 
			
		||||
        this.#descriptionTemplate.value = item.descriptionTemplate === null? "": item.descriptionTemplate;
 | 
			
		||||
        this.#validate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -891,16 +898,16 @@ class RecurringAccountSelector {
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    #filterOptions() {
 | 
			
		||||
        let hasAnyMatched = false;
 | 
			
		||||
        let isAnyMatched = false;
 | 
			
		||||
        for (const option of this.#options) {
 | 
			
		||||
            if (option.isMatched(this.#query.value)) {
 | 
			
		||||
                option.setShown(true);
 | 
			
		||||
                hasAnyMatched = true;
 | 
			
		||||
                isAnyMatched = true;
 | 
			
		||||
            } else {
 | 
			
		||||
                option.setShown(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (!hasAnyMatched) {
 | 
			
		||||
        if (!isAnyMatched) {
 | 
			
		||||
            this.#optionList.classList.add("d-none");
 | 
			
		||||
            this.#queryNoResult.classList.remove("d-none");
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -916,7 +923,6 @@ class RecurringAccountSelector {
 | 
			
		||||
    onOpen() {
 | 
			
		||||
        this.#query.value = "";
 | 
			
		||||
        this.#filterOptions();
 | 
			
		||||
        console.log(this.editor.accountCode);
 | 
			
		||||
        for (const option of this.#options) {
 | 
			
		||||
            option.setActive(option.code === this.editor.accountCode);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ class OriginalLineItemSelector {
 | 
			
		||||
    lineItemEditor;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The prefix of the HTML ID and class
 | 
			
		||||
     * The prefix of the HTML ID and class names
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    #prefix = "accounting-original-line-item-selector";
 | 
			
		||||
@@ -96,9 +96,7 @@ class OriginalLineItemSelector {
 | 
			
		||||
        for (const option of this.#options) {
 | 
			
		||||
            this.#optionById[option.id] = option;
 | 
			
		||||
        }
 | 
			
		||||
        this.#query.addEventListener("input", () => {
 | 
			
		||||
            this.#filterOptions();
 | 
			
		||||
        });
 | 
			
		||||
        this.#query.oninput = () => this.#filterOptions();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -113,8 +111,8 @@ class OriginalLineItemSelector {
 | 
			
		||||
        const otherLineItems = form.getLineItems().filter((lineItem) => lineItem !== currentLineItem);
 | 
			
		||||
        let otherOffset = new Decimal(0);
 | 
			
		||||
        for (const otherLineItem of otherLineItems) {
 | 
			
		||||
            if (otherLineItem.getOriginalLineItemId() === originalLineItemId) {
 | 
			
		||||
                const amount = otherLineItem.getAmount();
 | 
			
		||||
            if (otherLineItem.originalLineItemId === originalLineItemId) {
 | 
			
		||||
                const amount = otherLineItem.amount;
 | 
			
		||||
                if (amount !== null) {
 | 
			
		||||
                    otherOffset = otherOffset.plus(amount);
 | 
			
		||||
                }
 | 
			
		||||
@@ -131,8 +129,8 @@ class OriginalLineItemSelector {
 | 
			
		||||
        const otherLineItems = this.lineItemEditor.form.getLineItems().filter((lineItem) => lineItem !== this.lineItemEditor.lineItem);
 | 
			
		||||
        const otherOffsets = {}
 | 
			
		||||
        for (const otherLineItem of otherLineItems) {
 | 
			
		||||
            const otherOriginalLineItemId = otherLineItem.getOriginalLineItemId();
 | 
			
		||||
            const amount = otherLineItem.getAmount();
 | 
			
		||||
            const otherOriginalLineItemId = otherLineItem.originalLineItemId;
 | 
			
		||||
            const amount = otherLineItem.amount;
 | 
			
		||||
            if (otherOriginalLineItemId === null || amount === null) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
@@ -155,16 +153,16 @@ class OriginalLineItemSelector {
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    #filterOptions() {
 | 
			
		||||
        let hasAnyMatched = false;
 | 
			
		||||
        let isAnyMatched = false;
 | 
			
		||||
        for (const option of this.#options) {
 | 
			
		||||
            if (option.isMatched(this.#debitCredit, this.#currencyCode, this.#query.value)) {
 | 
			
		||||
                option.setShown(true);
 | 
			
		||||
                hasAnyMatched = true;
 | 
			
		||||
                isAnyMatched = true;
 | 
			
		||||
            } else {
 | 
			
		||||
                option.setShown(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (!hasAnyMatched) {
 | 
			
		||||
        if (!isAnyMatched) {
 | 
			
		||||
            this.#optionList.classList.add("d-none");
 | 
			
		||||
            this.#queryNoResult.classList.remove("d-none");
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -178,7 +176,7 @@ class OriginalLineItemSelector {
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    onOpen() {
 | 
			
		||||
        this.#currencyCode = this.lineItemEditor.getCurrencyCode();
 | 
			
		||||
        this.#currencyCode = this.lineItemEditor.currencyCode;
 | 
			
		||||
        this.#debitCredit = this.lineItemEditor.debitCredit;
 | 
			
		||||
        for (const option of this.#options) {
 | 
			
		||||
            option.setActive(option.id === this.lineItemEditor.originalLineItemId);
 | 
			
		||||
@@ -275,7 +273,7 @@ class OriginalLineItem {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The values to query against
 | 
			
		||||
     * @type {string[][]}
 | 
			
		||||
     * @type {string[]}
 | 
			
		||||
     */
 | 
			
		||||
    #queryValues;
 | 
			
		||||
 | 
			
		||||
@@ -341,10 +339,10 @@ class OriginalLineItem {
 | 
			
		||||
     */
 | 
			
		||||
    isMatched(debitCredit, currencyCode, query = null) {
 | 
			
		||||
        return this.netBalance.greaterThan(0)
 | 
			
		||||
            && this.date <= this.#selector.lineItemEditor.form.getDate()
 | 
			
		||||
            && this.#isDebitCreditMatches(debitCredit)
 | 
			
		||||
            && this.date <= this.#selector.lineItemEditor.form.date
 | 
			
		||||
            && this.#isDebitCreditMatched(debitCredit)
 | 
			
		||||
            && this.#currencyCode === currencyCode
 | 
			
		||||
            && this.#isQueryMatches(query);
 | 
			
		||||
            && this.#isQueryMatched(query);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -353,34 +351,43 @@ class OriginalLineItem {
 | 
			
		||||
     * @param debitCredit {string} either "debit" or credit
 | 
			
		||||
     * @return {boolean} true if the option matches, or false otherwise
 | 
			
		||||
     */
 | 
			
		||||
    #isDebitCreditMatches(debitCredit) {
 | 
			
		||||
    #isDebitCreditMatched(debitCredit) {
 | 
			
		||||
        return (debitCredit === "debit" && this.#debitCredit === "credit")
 | 
			
		||||
            || (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
 | 
			
		||||
     * @return {boolean} true if the option matches, or false otherwise
 | 
			
		||||
     */
 | 
			
		||||
    #isQueryMatches(query) {
 | 
			
		||||
    #isQueryMatched(query) {
 | 
			
		||||
        if (query === "") {
 | 
			
		||||
            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())) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        for (const queryValue of this.#queryValues[1]) {
 | 
			
		||||
            if (queryValue === query) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        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.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -33,12 +33,6 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
 */
 | 
			
		||||
class PeriodChooser {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The prefix of the HTML ID and class
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    prefix;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The modal of the period chooser
 | 
			
		||||
     * @type {HTMLDivElement}
 | 
			
		||||
@@ -56,8 +50,8 @@ class PeriodChooser {
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.prefix = "accounting-period-chooser";
 | 
			
		||||
        this.modal = document.getElementById(this.prefix + "-modal");
 | 
			
		||||
        const prefix = "accounting-period-chooser";
 | 
			
		||||
        this.modal = document.getElementById(prefix + "-modal");
 | 
			
		||||
        for (const cls of [MonthTab, YearTab, DayTab, CustomTab]) {
 | 
			
		||||
            const tab = new cls(this);
 | 
			
		||||
            this.tabPlanes[tab.tabId()] = tab;
 | 
			
		||||
@@ -94,7 +88,7 @@ class TabPlane {
 | 
			
		||||
    chooser;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The prefix of the HTML ID and class
 | 
			
		||||
     * The prefix of the HTML ID and class names
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    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">
 | 
			
		||||
          {% 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 }}
 | 
			
		||||
            </li>
 | 
			
		||||
          {% endfor %}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,29 +20,31 @@ Author: imacat@mail.imacat.idv.tw (imacat)
 | 
			
		||||
First written: 2023/3/21
 | 
			
		||||
#}
 | 
			
		||||
<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>
 | 
			
		||||
    <ul id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-list" class="list-group accounting-line-item-list">
 | 
			
		||||
      {% for line_item_form in line_item_forms %}
 | 
			
		||||
        {% with currency_index = currency_index,
 | 
			
		||||
                line_item_index = loop.index,
 | 
			
		||||
                only_one_line_item_form = line_item_forms|length == 1,
 | 
			
		||||
                form = line_item_form.form %}
 | 
			
		||||
          {% include "accounting/journal-entry/include/form-line-item.html" %}
 | 
			
		||||
        {% endwith %}
 | 
			
		||||
      {% endfor %}
 | 
			
		||||
    </ul>
 | 
			
		||||
    <div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-content" class="mt-2 {% if not line_item_forms %} d-none {% endif %}">
 | 
			
		||||
      <ul id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-list" class="list-group accounting-line-item-list">
 | 
			
		||||
        {% for line_item_form in line_item_forms %}
 | 
			
		||||
          {% with currency_index = currency_index,
 | 
			
		||||
                  line_item_index = loop.index,
 | 
			
		||||
                  only_one_line_item_form = line_item_forms|length == 1,
 | 
			
		||||
                  form = line_item_form.form %}
 | 
			
		||||
            {% include "accounting/journal-entry/include/form-line-item.html" %}
 | 
			
		||||
          {% endwith %}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
      </ul>
 | 
			
		||||
 | 
			
		||||
    <div class="d-flex justify-content-between mb-2">
 | 
			
		||||
      <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>
 | 
			
		||||
      <div class="d-flex justify-content-between mt-2 mb-2">
 | 
			
		||||
        <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>
 | 
			
		||||
 | 
			
		||||
    <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">
 | 
			
		||||
        <i class="fas fa-plus"></i>
 | 
			
		||||
        {{ A_("New") }}
 | 
			
		||||
      </button>
 | 
			
		||||
      <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">
 | 
			
		||||
          <i class="fas fa-plus"></i>
 | 
			
		||||
          {{ A_("New") }}
 | 
			
		||||
        </button>
 | 
			
		||||
      </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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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/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/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/description-editor.js") }}"></script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,12 +27,9 @@ from flask import Flask
 | 
			
		||||
from flask.testing import FlaskCliRunner
 | 
			
		||||
 | 
			
		||||
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: str = "/_next"
 | 
			
		||||
"""The next URI."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AccountData:
 | 
			
		||||
    """The account data."""
 | 
			
		||||
 
 | 
			
		||||
@@ -28,8 +28,8 @@ from flask import Flask
 | 
			
		||||
from flask.testing import FlaskCliRunner
 | 
			
		||||
 | 
			
		||||
from test_site import db
 | 
			
		||||
from testlib import create_test_app, get_client, set_locale
 | 
			
		||||
from testlib_journal_entry import add_journal_entry, NEXT_URI
 | 
			
		||||
from testlib import NEXT_URI, create_test_app, get_client, set_locale
 | 
			
		||||
from testlib_journal_entry import add_journal_entry
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CurrencyData:
 | 
			
		||||
 
 | 
			
		||||
@@ -24,8 +24,8 @@ from click.testing import Result
 | 
			
		||||
from flask import Flask
 | 
			
		||||
from flask.testing import FlaskCliRunner
 | 
			
		||||
 | 
			
		||||
from testlib import create_test_app, get_client
 | 
			
		||||
from testlib_journal_entry import Accounts, NEXT_URI, add_journal_entry
 | 
			
		||||
from testlib import NEXT_URI, Accounts, create_test_app, get_client
 | 
			
		||||
from testlib_journal_entry import add_journal_entry
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DescriptionEditorTestCase(unittest.TestCase):
 | 
			
		||||
 
 | 
			
		||||
@@ -27,9 +27,9 @@ from flask import Flask
 | 
			
		||||
from flask.testing import FlaskCliRunner
 | 
			
		||||
 | 
			
		||||
from test_site import db
 | 
			
		||||
from testlib import create_test_app, get_client
 | 
			
		||||
from testlib_journal_entry import NEXT_URI, NON_EMPTY_NOTE, EMPTY_NOTE, \
 | 
			
		||||
    Accounts, get_add_form, get_unchanged_update_form, get_update_form, \
 | 
			
		||||
from testlib import NEXT_URI, Accounts, create_test_app, get_client
 | 
			
		||||
from testlib_journal_entry import NON_EMPTY_NOTE, EMPTY_NOTE, \
 | 
			
		||||
    get_add_form, get_unchanged_update_form, get_update_form, \
 | 
			
		||||
    match_journal_entry_detail, set_negative_amount, \
 | 
			
		||||
    remove_debit_in_a_currency, remove_credit_in_a_currency, add_journal_entry
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,8 +26,8 @@ from flask import Flask
 | 
			
		||||
from flask.testing import FlaskCliRunner
 | 
			
		||||
 | 
			
		||||
from test_site import db
 | 
			
		||||
from testlib import create_test_app, get_client
 | 
			
		||||
from testlib_journal_entry import Accounts, match_journal_entry_detail
 | 
			
		||||
from testlib import Accounts, create_test_app, get_client
 | 
			
		||||
from testlib_journal_entry import match_journal_entry_detail
 | 
			
		||||
from testlib_offset import TestData, JournalEntryLineItemData, \
 | 
			
		||||
    JournalEntryData, CurrencyData
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,8 +26,7 @@ from flask import Flask
 | 
			
		||||
from flask.testing import FlaskCliRunner
 | 
			
		||||
 | 
			
		||||
from test_site import db
 | 
			
		||||
from testlib import create_test_app, get_client
 | 
			
		||||
from testlib_journal_entry import NEXT_URI, Accounts
 | 
			
		||||
from testlib import NEXT_URI, Accounts, create_test_app, get_client
 | 
			
		||||
from testlib_offset import TestData
 | 
			
		||||
 | 
			
		||||
PREFIX: str = "/accounting/options"
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,33 @@ from test_site import create_app
 | 
			
		||||
 | 
			
		||||
TEST_SERVER: str = "https://testserver"
 | 
			
		||||
"""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:
 | 
			
		||||
@@ -57,7 +84,6 @@ def get_csrf_token(client: httpx.Client) -> str:
 | 
			
		||||
    return client.get("/.csrf-token").text
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_client(app: Flask, username: str) -> tuple[httpx.Client, str]:
 | 
			
		||||
    """Returns a user client.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,40 +26,14 @@ import httpx
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
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."
 | 
			
		||||
"""The stripped content of an non-empty note."""
 | 
			
		||||
EMPTY_NOTE: str = " \n\n  "
 | 
			
		||||
"""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]:
 | 
			
		||||
    """Returns the form data to add a new journal entry.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,8 +26,8 @@ import httpx
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
from test_site import db
 | 
			
		||||
from testlib_journal_entry import Accounts, match_journal_entry_detail, \
 | 
			
		||||
    NEXT_URI
 | 
			
		||||
from testlib import NEXT_URI, Accounts
 | 
			
		||||
from testlib_journal_entry import match_journal_entry_detail
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class JournalEntryLineItemData:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user