Added to track the net balance and offset of the original entries.
This commit is contained in:
@ -31,6 +31,9 @@
|
||||
color: #141619;
|
||||
background-color: #D3D3D4;
|
||||
}
|
||||
.form-control.accounting-disabled {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
/** The toolbar */
|
||||
.accounting-toolbar {
|
||||
@ -113,6 +116,33 @@
|
||||
border-bottom: thick double slategray;
|
||||
}
|
||||
|
||||
/* Links between objects */
|
||||
.accounting-original-entry {
|
||||
border-top: thin solid darkslategray;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
.accounting-original-entry a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.accounting-original-entry a:hover {
|
||||
color: inherit;
|
||||
}
|
||||
.accounting-offset-entries {
|
||||
border-top: thin solid darkslategray;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
.accounting-offset-entries ul li {
|
||||
list-style: none;
|
||||
}
|
||||
.accounting-offset-entries ul li a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.accounting-offset-entries ul li a:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/** The option selector */
|
||||
.accounting-selector-list {
|
||||
height: 20rem;
|
||||
@ -150,6 +180,9 @@
|
||||
font-weight: bolder;
|
||||
border-top: thick double slategray;
|
||||
}
|
||||
.accounting-entry-editor-original-entry-content {
|
||||
width: calc(100% - 3rem);
|
||||
}
|
||||
|
||||
/* The report table */
|
||||
.accounting-report-table-header, .accounting-report-table-footer {
|
||||
|
@ -111,7 +111,7 @@ class AccountSelector {
|
||||
};
|
||||
for (const option of this.#options) {
|
||||
option.onclick = () => {
|
||||
this.#entryEditor.saveAccount(option.dataset.code, option.dataset.content);
|
||||
this.#entryEditor.saveAccount(option.dataset.code, option.dataset.content, option.classList.contains("accounting-account-is-offset-needed"));
|
||||
};
|
||||
}
|
||||
this.#query.addEventListener("input", () => {
|
||||
|
@ -57,6 +57,36 @@ class JournalEntryEditor {
|
||||
*/
|
||||
#prefix = "accounting-entry-editor"
|
||||
|
||||
/**
|
||||
* The container of the original entry
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#originalEntryContainer;
|
||||
|
||||
/**
|
||||
* The control of the original entry
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#originalEntryControl;
|
||||
|
||||
/**
|
||||
* The original entry
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#originalEntry;
|
||||
|
||||
/**
|
||||
* The error message of the original entry
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#originalEntryError;
|
||||
|
||||
/**
|
||||
* The delete button of the original entry
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
#originalEntryDelete;
|
||||
|
||||
/**
|
||||
* The control of the summary
|
||||
* @type {HTMLDivElement}
|
||||
@ -109,7 +139,7 @@ class JournalEntryEditor {
|
||||
* The journal entry to edit
|
||||
* @type {JournalEntrySubForm|null}
|
||||
*/
|
||||
#entry;
|
||||
entry;
|
||||
|
||||
/**
|
||||
* The debit or credit entry side sub-form
|
||||
@ -124,6 +154,11 @@ class JournalEntryEditor {
|
||||
constructor() {
|
||||
this.#element = document.getElementById(this.#prefix);
|
||||
this.#modal = document.getElementById(this.#prefix + "-modal");
|
||||
this.#originalEntryContainer = document.getElementById(this.#prefix + "-original-entry-container");
|
||||
this.#originalEntryControl = document.getElementById(this.#prefix + "-original-entry-control");
|
||||
this.#originalEntry = document.getElementById(this.#prefix + "-original-entry");
|
||||
this.#originalEntryError = document.getElementById(this.#prefix + "-original-entry-error");
|
||||
this.#originalEntryDelete = document.getElementById(this.#prefix + "-original-entry-delete");
|
||||
this.#summaryControl = document.getElementById(this.#prefix + "-summary-control");
|
||||
this.#summary = document.getElementById(this.#prefix + "-summary");
|
||||
this.#summaryError = document.getElementById(this.#prefix + "-summary-error");
|
||||
@ -131,25 +166,90 @@ class JournalEntryEditor {
|
||||
this.#account = document.getElementById(this.#prefix + "-account");
|
||||
this.#accountError = document.getElementById(this.#prefix + "-account-error")
|
||||
this.#amount = document.getElementById(this.#prefix + "-amount");
|
||||
this.#amountError = document.getElementById(this.#prefix + "-amount-error")
|
||||
this.#amountError = document.getElementById(this.#prefix + "-amount-error");
|
||||
this.#originalEntryControl.onclick = () => OriginalEntrySelector.start(this, this.#originalEntry.dataset.id);
|
||||
this.#originalEntryDelete.onclick = () => this.clearOriginalEntry();
|
||||
this.#summaryControl.onclick = () => {
|
||||
SummaryEditor.start(this, this.#summary.dataset.value);
|
||||
};
|
||||
this.#accountControl.onclick = () => {
|
||||
AccountSelector.start(this, this.entryType);
|
||||
}
|
||||
this.#amount.onchange = () => {
|
||||
this.#validateAmount();
|
||||
}
|
||||
this.#element.onsubmit = () => {
|
||||
if (this.#validate()) {
|
||||
if (this.#entry === null) {
|
||||
this.#entry = this.#side.addJournalEntry();
|
||||
if (this.entry === null) {
|
||||
this.entry = this.#side.addJournalEntry();
|
||||
}
|
||||
this.#entry.save(this.#account.dataset.code, this.#account.dataset.text, this.#summary.dataset.value, this.#amount.value);
|
||||
this.entry.save("isOriginalEntry" in this.#element.dataset,this.#originalEntry.dataset.id, this.#originalEntry.dataset.date, this.#originalEntry.dataset.text, this.#account.dataset.code, this.#account.dataset.text, this.#summary.dataset.value, this.#amount.value);
|
||||
bootstrap.Modal.getInstance(this.#modal).hide();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the original entry from the original entry selector.
|
||||
*
|
||||
* @param originalEntry {OriginalEntry} the original entry
|
||||
*/
|
||||
saveOriginalEntry(originalEntry) {
|
||||
delete this.#element.dataset.isOriginalEntry;
|
||||
this.#originalEntryContainer.classList.remove("d-none");
|
||||
this.#originalEntryControl.classList.add("accounting-not-empty");
|
||||
this.#originalEntry.dataset.id = originalEntry.id;
|
||||
this.#originalEntry.dataset.date = originalEntry.date;
|
||||
this.#originalEntry.dataset.text = originalEntry.text;
|
||||
this.#originalEntry.innerText = originalEntry.text;
|
||||
this.#setEnableSummaryAccount(false);
|
||||
if (originalEntry.summary === "") {
|
||||
this.#summaryControl.classList.remove("accounting-not-empty");
|
||||
} else {
|
||||
this.#summaryControl.classList.add("accounting-not-empty");
|
||||
}
|
||||
this.#summary.dataset.value = originalEntry.summary;
|
||||
this.#summary.innerText = originalEntry.summary;
|
||||
this.#accountControl.classList.add("accounting-not-empty");
|
||||
this.#account.dataset.code = originalEntry.accountCode;
|
||||
this.#account.dataset.text = originalEntry.accountText;
|
||||
this.#account.innerText = originalEntry.accountText;
|
||||
this.#amount.value = String(originalEntry.netBalance);
|
||||
this.#amount.max = String(originalEntry.netBalance);
|
||||
this.#amount.min = "0";
|
||||
this.#validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the original entry.
|
||||
*
|
||||
*/
|
||||
clearOriginalEntry() {
|
||||
delete this.#element.dataset.isOriginalEntry;
|
||||
this.#originalEntryContainer.classList.add("d-none");
|
||||
this.#originalEntryControl.classList.remove("accounting-not-empty");
|
||||
this.#originalEntry.dataset.id = "";
|
||||
this.#originalEntry.dataset.date = "";
|
||||
this.#originalEntry.dataset.text = "";
|
||||
this.#originalEntry.innerText = "";
|
||||
this.#setEnableSummaryAccount(true);
|
||||
this.#accountControl.classList.remove("accounting-not-empty");
|
||||
this.#account.dataset.code = "";
|
||||
this.#account.dataset.text = "";
|
||||
this.#account.innerText = "";
|
||||
this.#amount.max = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currency code.
|
||||
*
|
||||
* @return {string} the currency code
|
||||
*/
|
||||
getCurrencyCode() {
|
||||
return this.#side.currency.getCurrencyCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transaction form.
|
||||
*
|
||||
@ -172,6 +272,7 @@ class JournalEntryEditor {
|
||||
}
|
||||
this.#summary.dataset.value = summary;
|
||||
this.#summary.innerText = summary;
|
||||
this.#validateSummary();
|
||||
bootstrap.Modal.getOrCreateInstance(this.#modal).show();
|
||||
}
|
||||
|
||||
@ -181,8 +282,14 @@ class JournalEntryEditor {
|
||||
* @param summary {string} the summary
|
||||
* @param accountCode {string} the account code
|
||||
* @param accountText {string} the account text
|
||||
* @param isAccountOffsetNeeded {boolean} true if the journal entries in the account need offset, or false otherwise
|
||||
*/
|
||||
saveSummaryWithAccount(summary, accountCode, accountText) {
|
||||
saveSummaryWithAccount(summary, accountCode, accountText, isAccountOffsetNeeded) {
|
||||
if (isAccountOffsetNeeded) {
|
||||
this.#element.dataset.isOriginalEntry = "true";
|
||||
} else {
|
||||
delete this.#element.dataset.isOriginalEntry;
|
||||
}
|
||||
this.#accountControl.classList.add("accounting-not-empty");
|
||||
this.#account.dataset.code = accountCode;
|
||||
this.#account.dataset.text = accountText;
|
||||
@ -205,6 +312,7 @@ class JournalEntryEditor {
|
||||
*
|
||||
*/
|
||||
clearAccount() {
|
||||
delete this.#element.dataset.isOriginalEntry;
|
||||
this.#accountControl.classList.remove("accounting-not-empty");
|
||||
this.#account.dataset.code = "";
|
||||
this.#account.dataset.text = "";
|
||||
@ -217,8 +325,14 @@ class JournalEntryEditor {
|
||||
*
|
||||
* @param code {string} the account code
|
||||
* @param text {string} the account text
|
||||
* @param isOffsetNeeded {boolean} true if the journal entries in the account need offset or false otherwise
|
||||
*/
|
||||
saveAccount(code, text) {
|
||||
saveAccount(code, text, isOffsetNeeded) {
|
||||
if (isOffsetNeeded) {
|
||||
this.#element.dataset.isOriginalEntry = "true";
|
||||
} else {
|
||||
delete this.#element.dataset.isOriginalEntry;
|
||||
}
|
||||
this.#accountControl.classList.add("accounting-not-empty");
|
||||
this.#account.dataset.code = code;
|
||||
this.#account.dataset.text = text;
|
||||
@ -233,12 +347,25 @@ class JournalEntryEditor {
|
||||
*/
|
||||
#validate() {
|
||||
let isValid = true;
|
||||
isValid = this.#validateOriginalEntry() && isValid;
|
||||
isValid = this.#validateSummary() && isValid;
|
||||
isValid = this.#validateAccount() && isValid;
|
||||
isValid = this.#validateAmount() && isValid
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the original entry.
|
||||
*
|
||||
* @return {boolean} true if valid, or false otherwise
|
||||
* @private
|
||||
*/
|
||||
#validateOriginalEntry() {
|
||||
this.#originalEntryControl.classList.remove("is-invalid");
|
||||
this.#originalEntryError.innerText = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the summary.
|
||||
*
|
||||
@ -281,8 +408,29 @@ class JournalEntryEditor {
|
||||
this.#amountError.innerText = A_("Please fill in the amount.");
|
||||
return false;
|
||||
}
|
||||
const amount =new Decimal(this.#amount.value);
|
||||
if (amount.lessThanOrEqualTo(0)) {
|
||||
this.#amount.classList.add("is-invalid");
|
||||
this.#amountError.innerText = A_("Please fill in a positive amount.");
|
||||
return false;
|
||||
}
|
||||
if (this.#amount.max !== "") {
|
||||
if (amount.greaterThan(new Decimal(this.#amount.max))) {
|
||||
this.#amount.classList.add("is-invalid");
|
||||
this.#amountError.innerText = A_("The amount must not exceed the net balance %(balance)s of the original entry.", {balance: new Decimal(this.#amount.max)});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.#amount.min !== "") {
|
||||
const min = new Decimal(this.#amount.min);
|
||||
if (amount.lessThan(min)) {
|
||||
this.#amount.classList.add("is-invalid");
|
||||
this.#amountError.innerText = A_("The amount must not be less than the offset total %(total)s.", {total: formatDecimal(min)});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.#amount.classList.remove("is-invalid");
|
||||
this.#amount.innerText = "";
|
||||
this.#amountError.innerText = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -292,24 +440,33 @@ class JournalEntryEditor {
|
||||
* @param side {DebitCreditSideSubForm} the debit or credit side sub-form
|
||||
*/
|
||||
#onAddNew(side) {
|
||||
this.#entry = null;
|
||||
this.entry = null;
|
||||
this.#side = side;
|
||||
this.entryType = this.#side.entryType;
|
||||
this.#element.dataset.entryType = side.entryType;
|
||||
this.#summaryControl.dataset.bsTarget = "#accounting-summary-editor-" + side.entryType + "-modal";
|
||||
delete this.#element.dataset.isOriginalEntry;
|
||||
this.#originalEntryContainer.classList.add("d-none");
|
||||
this.#originalEntryControl.classList.remove("accounting-not-empty");
|
||||
this.#originalEntryControl.classList.remove("is-invalid");
|
||||
this.#originalEntry.dataset.id = "";
|
||||
this.#originalEntry.dataset.date = "";
|
||||
this.#originalEntry.dataset.text = "";
|
||||
this.#originalEntry.innerText = "";
|
||||
this.#setEnableSummaryAccount(true);
|
||||
this.#summaryControl.classList.remove("accounting-not-empty");
|
||||
this.#summaryControl.classList.remove("is-invalid");
|
||||
this.#summary.dataset.value = "";
|
||||
this.#summary.innerText = ""
|
||||
this.#summaryError.innerText = ""
|
||||
this.#accountControl.dataset.bsTarget = "#accounting-account-selector-" + side.entryType + "-modal";
|
||||
this.#accountControl.classList.remove("accounting-not-empty");
|
||||
this.#accountControl.classList.remove("is-invalid");
|
||||
this.#account.innerText = "";
|
||||
this.#account.dataset.code = "";
|
||||
this.#account.dataset.text = "";
|
||||
this.#account.innerText = "";
|
||||
this.#accountError.innerText = "";
|
||||
this.#amount.value = "";
|
||||
this.#amount.max = "";
|
||||
this.#amount.min = "0";
|
||||
this.#amount.classList.remove("is-invalid");
|
||||
this.#amountError.innerText = "";
|
||||
}
|
||||
@ -318,17 +475,37 @@ class JournalEntryEditor {
|
||||
* Edits a journal entry.
|
||||
*
|
||||
* @param entry {JournalEntrySubForm} the journal entry sub-form
|
||||
* @param originalEntryId {string} the ID of the original entry
|
||||
* @param originalEntryDate {string} the date of the original entry
|
||||
* @param originalEntryText {string} the text of the original entry
|
||||
* @param summary {string} the summary
|
||||
* @param accountCode {string} the account code
|
||||
* @param accountText {string} the account text
|
||||
* @param amount {string} the amount
|
||||
* @param amountMin {string} the minimal amount
|
||||
*/
|
||||
#onEdit(entry, summary, accountCode, accountText, amount) {
|
||||
this.#entry = entry;
|
||||
#onEdit(entry, originalEntryId, originalEntryDate, originalEntryText, summary, accountCode, accountText, amount, amountMin) {
|
||||
this.entry = entry;
|
||||
this.#side = entry.side;
|
||||
this.entryType = this.#side.entryType;
|
||||
this.#element.dataset.entryType = entry.entryType;
|
||||
this.#summaryControl.dataset.bsTarget = "#accounting-summary-editor-" + entry.entryType + "-modal";
|
||||
if (entry.isOriginalEntry()) {
|
||||
this.#element.dataset.isOriginalEntry = "true";
|
||||
} else {
|
||||
delete this.#element.dataset.isOriginalEntry;
|
||||
}
|
||||
if (originalEntryId === "") {
|
||||
this.#originalEntryContainer.classList.add("d-none");
|
||||
this.#originalEntryControl.classList.remove("accounting-not-empty");
|
||||
} else {
|
||||
this.#originalEntryContainer.classList.remove("d-none");
|
||||
this.#originalEntryControl.classList.add("accounting-not-empty");
|
||||
}
|
||||
this.#originalEntry.dataset.id = originalEntryId;
|
||||
this.#originalEntry.dataset.date = originalEntryDate;
|
||||
this.#originalEntry.dataset.text = originalEntryText;
|
||||
this.#originalEntry.innerText = originalEntryText;
|
||||
this.#setEnableSummaryAccount(!entry.isMatched && originalEntryId === "");
|
||||
if (summary === "") {
|
||||
this.#summaryControl.classList.remove("accounting-not-empty");
|
||||
} else {
|
||||
@ -336,16 +513,58 @@ class JournalEntryEditor {
|
||||
}
|
||||
this.#summary.dataset.value = summary;
|
||||
this.#summary.innerText = summary;
|
||||
this.#accountControl.dataset.bsTarget = "#accounting-account-selector-" + entry.entryType + "-modal";
|
||||
if (accountCode === "") {
|
||||
this.#accountControl.classList.remove("accounting-not-empty");
|
||||
} else {
|
||||
this.#accountControl.classList.add("accounting-not-empty");
|
||||
}
|
||||
this.#account.innerText = accountText;
|
||||
this.#account.dataset.code = accountCode;
|
||||
this.#account.dataset.text = accountText;
|
||||
this.#account.innerText = accountText;
|
||||
this.#amount.value = amount;
|
||||
const maxAmount = this.#getMaxAmount();
|
||||
this.#amount.max = maxAmount === null? "": maxAmount;
|
||||
this.#amount.min = amountMin;
|
||||
this.#validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds out the max amount.
|
||||
*
|
||||
* @return {Decimal|null} the max amount
|
||||
*/
|
||||
#getMaxAmount() {
|
||||
if (this.#originalEntry.dataset.id === "") {
|
||||
return null;
|
||||
}
|
||||
return OriginalEntrySelector.getNetBalance(this.entry, this.getTransactionForm(), this.#originalEntry.dataset.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the enable status of the summary and account.
|
||||
*
|
||||
* @param isEnabled {boolean} true to enable, or false otherwise
|
||||
*/
|
||||
#setEnableSummaryAccount(isEnabled) {
|
||||
if (isEnabled) {
|
||||
this.#summaryControl.dataset.bsToggle = "modal";
|
||||
this.#summaryControl.dataset.bsTarget = "#accounting-summary-editor-" + this.#side.entryType + "-modal";
|
||||
this.#summaryControl.classList.remove("accounting-disabled");
|
||||
this.#summaryControl.classList.add("accounting-clickable");
|
||||
this.#accountControl.dataset.bsToggle = "modal";
|
||||
this.#accountControl.dataset.bsTarget = "#accounting-account-selector-" + this.#side.entryType + "-modal";
|
||||
this.#accountControl.classList.remove("accounting-disabled");
|
||||
this.#accountControl.classList.add("accounting-clickable");
|
||||
} else {
|
||||
this.#summaryControl.dataset.bsToggle = "";
|
||||
this.#summaryControl.dataset.bsTarget = "";
|
||||
this.#summaryControl.classList.add("accounting-disabled");
|
||||
this.#summaryControl.classList.remove("accounting-clickable");
|
||||
this.#accountControl.dataset.bsToggle = "";
|
||||
this.#accountControl.dataset.bsTarget = "";
|
||||
this.#accountControl.classList.add("accounting-disabled");
|
||||
this.#accountControl.classList.remove("accounting-clickable");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -375,12 +594,16 @@ class JournalEntryEditor {
|
||||
* Edits a journal entry.
|
||||
*
|
||||
* @param entry {JournalEntrySubForm} the journal entry sub-form
|
||||
* @param originalEntryId {string} the ID of the original entry
|
||||
* @param originalEntryDate {string} the date of the original entry
|
||||
* @param originalEntryText {string} the text of the original entry
|
||||
* @param summary {string} the summary
|
||||
* @param accountCode {string} the account code
|
||||
* @param accountText {string} the account text
|
||||
* @param amount {string} the amount
|
||||
* @param amountMin {string} the minimal amount
|
||||
*/
|
||||
static edit(entry, summary, accountCode, accountText, amount) {
|
||||
this.#editor.#onEdit(entry, summary, accountCode, accountText, amount);
|
||||
static edit(entry, originalEntryId, originalEntryDate, originalEntryText, summary, accountCode, accountText, amount, amountMin) {
|
||||
this.#editor.#onEdit(entry, originalEntryId, originalEntryDate, originalEntryText, summary, accountCode, accountText, amount, amountMin);
|
||||
}
|
||||
}
|
||||
|
447
src/accounting/static/js/original-entry-selector.js
Normal file
447
src/accounting/static/js/original-entry-selector.js
Normal file
@ -0,0 +1,447 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
* original-entry-selector.js: The JavaScript for the original entry 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/3/10
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
// Initializes the page JavaScript.
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
OriginalEntrySelector.initialize();
|
||||
});
|
||||
|
||||
/**
|
||||
* The original entry selector.
|
||||
*
|
||||
*/
|
||||
class OriginalEntrySelector {
|
||||
|
||||
/**
|
||||
* The prefix of the HTML ID and class
|
||||
* @type {string}
|
||||
*/
|
||||
#prefix = "accounting-original-entry-selector";
|
||||
|
||||
/**
|
||||
* The modal of the original entry editor
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#modal;
|
||||
|
||||
/**
|
||||
* 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 {OriginalEntry[]}
|
||||
*/
|
||||
#options;
|
||||
|
||||
/**
|
||||
* The options by their ID
|
||||
* @type {Object.<string, OriginalEntry>}
|
||||
*/
|
||||
#optionById;
|
||||
|
||||
/**
|
||||
* The journal entry editor.
|
||||
* @type {JournalEntryEditor}
|
||||
*/
|
||||
entryEditor;
|
||||
|
||||
/**
|
||||
* Constructs an original entry selector.
|
||||
*
|
||||
*/
|
||||
constructor() {
|
||||
this.#modal = document.getElementById(this.#prefix + "-modal");
|
||||
this.#query = document.getElementById(this.#prefix + "-query");
|
||||
this.#queryNoResult = document.getElementById(this.#prefix + "-option-no-result");
|
||||
this.#optionList = document.getElementById(this.#prefix + "-option-list");
|
||||
this.#options = Array.from(document.getElementsByClassName(this.#prefix + "-option")).map((element) => new OriginalEntry(this, element));
|
||||
this.#optionById = {};
|
||||
for (const option of this.#options) {
|
||||
this.#optionById[option.id] = option;
|
||||
}
|
||||
this.#query.addEventListener("input", () => {
|
||||
this.#filterOptions();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the net balance for an original entry.
|
||||
*
|
||||
* @param currentEntry {JournalEntrySubForm} the journal entry sub-form that is currently editing
|
||||
* @param form {TransactionForm} the transaction form
|
||||
* @param originalEntryId {string} the ID of the original entry
|
||||
* @return {Decimal} the net balance of the original entry
|
||||
*/
|
||||
#getNetBalance(currentEntry, form, originalEntryId) {
|
||||
const otherEntries = form.getEntries().filter((entry) => entry !== currentEntry);
|
||||
let otherOffset = new Decimal(0);
|
||||
for (const otherEntry of otherEntries) {
|
||||
if (otherEntry.getOriginalEntryId() === originalEntryId) {
|
||||
const amount = otherEntry.getAmount();
|
||||
if (amount !== null) {
|
||||
otherOffset = otherOffset.plus(amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.#optionById[originalEntryId].bareNetBalance.minus(otherOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the net balances, subtracting the offset amounts on the form but the currently editing journal entry
|
||||
*
|
||||
*/
|
||||
#updateNetBalances() {
|
||||
const otherEntries = this.entryEditor.getTransactionForm().getEntries().filter((entry) => entry !== this.entryEditor.entry);
|
||||
const otherOffsets = {}
|
||||
for (const otherEntry of otherEntries) {
|
||||
const otherOriginalEntryId = otherEntry.getOriginalEntryId();
|
||||
const amount = otherEntry.getAmount();
|
||||
if (otherOriginalEntryId === null || amount === null) {
|
||||
continue;
|
||||
}
|
||||
if (!(otherOriginalEntryId in otherOffsets)) {
|
||||
otherOffsets[otherOriginalEntryId] = new Decimal("0");
|
||||
}
|
||||
otherOffsets[otherOriginalEntryId] = otherOffsets[otherOriginalEntryId].plus(amount);
|
||||
}
|
||||
for (const option of this.#options) {
|
||||
if (option.id in otherOffsets) {
|
||||
option.updateNetBalance(otherOffsets[option.id]);
|
||||
} else {
|
||||
option.resetNetBalance();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the options.
|
||||
*
|
||||
*/
|
||||
#filterOptions() {
|
||||
let hasAnyMatched = false;
|
||||
for (const option of this.#options) {
|
||||
if (option.isMatched(this.#modal.dataset.entryType, this.#modal.dataset.currencyCode, this.#query.value)) {
|
||||
option.setShown(true);
|
||||
hasAnyMatched = true;
|
||||
} else {
|
||||
option.setShown(false);
|
||||
}
|
||||
}
|
||||
if (!hasAnyMatched) {
|
||||
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 original entry selector is shown.
|
||||
*
|
||||
* @param entryEditor {JournalEntryEditor} the journal entry editor
|
||||
* @param originalEntryId {string|null} the ID of the original entry
|
||||
*/
|
||||
#onOpen(entryEditor, originalEntryId) {
|
||||
this.entryEditor = entryEditor
|
||||
this.#modal.dataset.currencyCode = entryEditor.getCurrencyCode();
|
||||
this.#modal.dataset.entryType = entryEditor.entryType;
|
||||
for (const option of this.#options) {
|
||||
option.setActive(option.id === originalEntryId);
|
||||
}
|
||||
this.#query.value = "";
|
||||
this.#updateNetBalances();
|
||||
this.#filterOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* The original entry selector.
|
||||
* @type {OriginalEntrySelector}
|
||||
*/
|
||||
static #selector;
|
||||
|
||||
/**
|
||||
* Initializes the original entry selector.
|
||||
*
|
||||
*/
|
||||
static initialize() {
|
||||
this.#selector = new OriginalEntrySelector();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the original entry selector.
|
||||
*
|
||||
* @param entryEditor {JournalEntryEditor} the journal entry editor
|
||||
* @param originalEntryId {string|null} the ID of the original entry
|
||||
*/
|
||||
static start(entryEditor, originalEntryId = null) {
|
||||
this.#selector.#onOpen(entryEditor, originalEntryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the net balance for an original entry.
|
||||
*
|
||||
* @param currentEntry {JournalEntrySubForm} the journal entry sub-form that is currently editing
|
||||
* @param form {TransactionForm} the transaction form
|
||||
* @param originalEntryId {string} the ID of the original entry
|
||||
* @return {Decimal} the net balance of the original entry
|
||||
*/
|
||||
static getNetBalance(currentEntry, form, originalEntryId) {
|
||||
return this.#selector.#getNetBalance(currentEntry, form, originalEntryId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An original entry.
|
||||
*
|
||||
*/
|
||||
class OriginalEntry {
|
||||
|
||||
/**
|
||||
* The original entry selector
|
||||
* @type {OriginalEntrySelector}
|
||||
*/
|
||||
#selector;
|
||||
|
||||
/**
|
||||
* The element
|
||||
* @type {HTMLLIElement}
|
||||
*/
|
||||
#element;
|
||||
|
||||
/**
|
||||
* The ID
|
||||
* @type {string}
|
||||
*/
|
||||
id;
|
||||
|
||||
/**
|
||||
* The date
|
||||
* @type {string}
|
||||
*/
|
||||
date;
|
||||
|
||||
/**
|
||||
* The entry type, either "debit" or "credit"
|
||||
* @type {string}
|
||||
*/
|
||||
#entryType;
|
||||
|
||||
/**
|
||||
* The currency code
|
||||
* @type {string}
|
||||
*/
|
||||
#currencyCode;
|
||||
|
||||
/**
|
||||
* The account code
|
||||
* @type {string}
|
||||
*/
|
||||
accountCode;
|
||||
|
||||
/**
|
||||
* The account text
|
||||
* @type {string}
|
||||
*/
|
||||
accountText;
|
||||
|
||||
/**
|
||||
* The summary
|
||||
* @type {string}
|
||||
*/
|
||||
summary;
|
||||
|
||||
/**
|
||||
* The net balance, without the offset amounts on the form
|
||||
* @type {Decimal}
|
||||
*/
|
||||
bareNetBalance;
|
||||
|
||||
/**
|
||||
* The net balance
|
||||
* @type {Decimal}
|
||||
*/
|
||||
netBalance;
|
||||
|
||||
/**
|
||||
* The text of the net balance
|
||||
* @type {HTMLSpanElement}
|
||||
*/
|
||||
netBalanceText;
|
||||
|
||||
/**
|
||||
* The text representation of the original entry
|
||||
* @type {string}
|
||||
*/
|
||||
text;
|
||||
|
||||
/**
|
||||
* The values to query against
|
||||
* @type {string[][]}
|
||||
*/
|
||||
#queryValues;
|
||||
|
||||
/**
|
||||
* Constructs an original entry.
|
||||
*
|
||||
* @param selector {OriginalEntrySelector} the original entry selector
|
||||
* @param element {HTMLLIElement} the element
|
||||
*/
|
||||
constructor(selector, element) {
|
||||
this.#selector = selector;
|
||||
this.#element = element;
|
||||
this.id = element.dataset.id;
|
||||
this.date = element.dataset.date;
|
||||
this.#entryType = element.dataset.entryType;
|
||||
this.#currencyCode = element.dataset.currencyCode;
|
||||
this.accountCode = element.dataset.accountCode;
|
||||
this.accountText = element.dataset.accountText;
|
||||
this.summary = element.dataset.summary;
|
||||
this.bareNetBalance = new Decimal(element.dataset.netBalance);
|
||||
this.netBalance = this.bareNetBalance;
|
||||
this.netBalanceText = document.getElementById("accounting-original-entry-selector-option-" + this.id + "-net-balance");
|
||||
this.text = element.dataset.text;
|
||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||
this.#element.onclick = () => this.#selector.entryEditor.saveOriginalEntry(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the net balance to its initial value, without the offset amounts on the form.
|
||||
*
|
||||
*/
|
||||
resetNetBalance() {
|
||||
if (this.netBalance !== this.bareNetBalance) {
|
||||
this.netBalance = this.bareNetBalance;
|
||||
this.#updateNetBalanceText();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the net balance with an offset.
|
||||
*
|
||||
* @param offset {Decimal} the offset to be added to the net balance
|
||||
*/
|
||||
updateNetBalance(offset) {
|
||||
this.netBalance = this.bareNetBalance.minus(offset);
|
||||
this.#updateNetBalanceText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the text display of the net balance.
|
||||
*
|
||||
*/
|
||||
#updateNetBalanceText() {
|
||||
this.netBalanceText.innerText = formatDecimal(this.netBalance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the original matches.
|
||||
*
|
||||
* @param entryType {string} the entry type, either "debit" or "credit"
|
||||
* @param currencyCode {string} the currency code
|
||||
* @param query {string|null} the query term
|
||||
*/
|
||||
isMatched(entryType, currencyCode, query = null) {
|
||||
return this.netBalance.greaterThan(0)
|
||||
&& this.date <= this.#selector.entryEditor.getTransactionForm().getDate()
|
||||
&& this.#isEntryTypeMatches(entryType)
|
||||
&& this.#currencyCode === currencyCode
|
||||
&& this.#isQueryMatches(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the original entry matches the entry type.
|
||||
*
|
||||
* @param entryType {string} the entry type, either "debit" or credit
|
||||
* @return {boolean} true if the option matches, or false otherwise
|
||||
*/
|
||||
#isEntryTypeMatches(entryType) {
|
||||
return (entryType === "debit" && this.#entryType === "credit")
|
||||
|| (entryType === "credit" && this.#entryType === "debit");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the original entry matches the query.
|
||||
*
|
||||
* @param query {string|null} the query term
|
||||
* @return {boolean} true if the option matches, or false otherwise
|
||||
*/
|
||||
#isQueryMatches(query) {
|
||||
if (query === "") {
|
||||
return true;
|
||||
}
|
||||
for (const queryValue of this.#queryValues[0]) {
|
||||
if (queryValue.toLowerCase().includes(query.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const queryValue of this.#queryValues[1]) {
|
||||
if (queryValue === query) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -69,6 +69,12 @@ class SummaryEditor {
|
||||
*/
|
||||
summary;
|
||||
|
||||
/**
|
||||
* The button to the original entry selector
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
#offsetButton;
|
||||
|
||||
/**
|
||||
* The number input
|
||||
* @type {HTMLInputElement}
|
||||
@ -116,6 +122,7 @@ class SummaryEditor {
|
||||
this.prefix = "accounting-summary-editor-" + form.dataset.entryType;
|
||||
this.#modal = document.getElementById(this.prefix + "-modal");
|
||||
this.summary = document.getElementById(this.prefix + "-summary");
|
||||
this.#offsetButton = document.getElementById(this.prefix + "-offset");
|
||||
this.number = document.getElementById(this.prefix + "-annotation-number");
|
||||
this.note = document.getElementById(this.prefix + "-annotation-note");
|
||||
// noinspection JSValidateTypes
|
||||
@ -128,6 +135,7 @@ class SummaryEditor {
|
||||
this.currentTab = this.tabPlanes.general;
|
||||
this.#initializeSuggestedAccounts();
|
||||
this.summary.onchange = () => this.#onSummaryChange();
|
||||
this.#offsetButton.onclick = () => OriginalEntrySelector.start(this.#entryEditor);
|
||||
this.#form.onsubmit = () => {
|
||||
if (this.currentTab.validate()) {
|
||||
this.#submit();
|
||||
@ -210,7 +218,7 @@ class SummaryEditor {
|
||||
#submit() {
|
||||
bootstrap.Modal.getOrCreateInstance(this.#modal).hide();
|
||||
if (this.#selectedAccount !== null) {
|
||||
this.#entryEditor.saveSummaryWithAccount(this.summary.value, this.#selectedAccount.dataset.code, this.#selectedAccount.dataset.text);
|
||||
this.#entryEditor.saveSummaryWithAccount(this.summary.value, this.#selectedAccount.dataset.code, this.#selectedAccount.dataset.text, this.#selectedAccount.classList.contains("accounting-account-is-offset-needed"));
|
||||
} else {
|
||||
this.#entryEditor.saveSummary(this.summary.value);
|
||||
}
|
||||
|
@ -159,7 +159,19 @@ class TransactionForm {
|
||||
this.#currencies[0].deleteButton.classList.add("d-none");
|
||||
} else {
|
||||
for (const currency of this.#currencies) {
|
||||
currency.deleteButton.classList.remove("d-none");
|
||||
const isAnyEntryMatched = () => {
|
||||
for (const entry of currency.getEntries()) {
|
||||
if (entry.isMatched) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (isAnyEntryMatched()) {
|
||||
currency.deleteButton.classList.add("d-none");
|
||||
} else {
|
||||
currency.deleteButton.classList.remove("d-none");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -178,6 +190,20 @@ class TransactionForm {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the journal entries in the form.
|
||||
*
|
||||
* @param entryType {string|null} the entry type, either "debit" or "credit", or null for both
|
||||
* @return {JournalEntrySubForm[]} all the journal entry sub-forms
|
||||
*/
|
||||
getEntries(entryType = null) {
|
||||
const entries = [];
|
||||
for (const currency of this.#currencies) {
|
||||
entries.push(...currency.getEntries(entryType));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account codes used in the form.
|
||||
*
|
||||
@ -185,11 +211,35 @@ class TransactionForm {
|
||||
* @return {string[]} the account codes used in the form
|
||||
*/
|
||||
getAccountCodesUsed(entryType) {
|
||||
let inUse = [];
|
||||
for (const currency of this.#currencies) {
|
||||
inUse = inUse.concat(currency.getAccountCodesUsed(entryType));
|
||||
return this.getEntries(entryType).map((entry) => entry.getAccountCode())
|
||||
.filter((code) => code !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the date.
|
||||
*
|
||||
* @return {string} the date
|
||||
*/
|
||||
getDate() {
|
||||
return this.#date.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the minimal date.
|
||||
*
|
||||
*/
|
||||
updateMinDate() {
|
||||
let lastOriginalEntryDate = null;
|
||||
for (const entry of this.getEntries()) {
|
||||
const date = entry.getOriginalEntryDate();
|
||||
if (date !== null) {
|
||||
if (lastOriginalEntryDate === null || lastOriginalEntryDate < date) {
|
||||
lastOriginalEntryDate = date;
|
||||
}
|
||||
}
|
||||
}
|
||||
return inUse;
|
||||
this.#date.min = lastOriginalEntryDate === null? "": lastOriginalEntryDate;
|
||||
this.#validateDate();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -218,6 +268,11 @@ class TransactionForm {
|
||||
this.#dateError.innerText = A_("Please fill in the date.");
|
||||
return false;
|
||||
}
|
||||
if (this.#date.value < this.#date.min) {
|
||||
this.#date.classList.add("is-invalid");
|
||||
this.#dateError.innerText = A_("The date cannot be earlier than the original entries.");
|
||||
return false;
|
||||
}
|
||||
this.#date.classList.remove("is-invalid");
|
||||
this.#dateError.innerText = "";
|
||||
return true;
|
||||
@ -330,6 +385,18 @@ class CurrencySubForm {
|
||||
*/
|
||||
no;
|
||||
|
||||
/**
|
||||
* The currency code
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
#code;
|
||||
|
||||
/**
|
||||
* The currency code selector
|
||||
* @type {HTMLSelectElement}
|
||||
*/
|
||||
#codeSelect;
|
||||
|
||||
/**
|
||||
* The button to delete the currency
|
||||
* @type {HTMLButtonElement}
|
||||
@ -362,11 +429,16 @@ class CurrencySubForm {
|
||||
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.#debit = debitElement === null? null: new DebitCreditSideSubForm(this, debitElement, "debit");
|
||||
const creditElement = document.getElementById(this.#prefix + "-credit");
|
||||
this.#credit = creditElement == null? null: new DebitCreditSideSubForm(this, creditElement, "credit");
|
||||
this.#codeSelect.onchange = () => {
|
||||
this.#code.value = this.#codeSelect.value;
|
||||
};
|
||||
this.deleteButton.onclick = () => {
|
||||
this.element.parentElement.removeChild(this.element);
|
||||
this.form.deleteCurrency(this);
|
||||
@ -374,18 +446,45 @@ class CurrencySubForm {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account codes used in the form.
|
||||
* Returns the currency code.
|
||||
*
|
||||
* @param entryType {string} the entry type, either "debit" or "credit"
|
||||
* @return {string[]} the account codes used in the form
|
||||
* @return {string} the currency code
|
||||
*/
|
||||
getAccountCodesUsed(entryType) {
|
||||
if (entryType === "debit") {
|
||||
return this.#debit.getAccountCodesUsed();
|
||||
} else if (entryType === "credit") {
|
||||
return this.#credit.getAccountCodesUsed();
|
||||
getCurrencyCode() {
|
||||
return this.#code.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the journal entries in the form.
|
||||
*
|
||||
* @param entryType {string|null} the entry type, either "debit" or "credit", or null for both
|
||||
* @return {JournalEntrySubForm[]} all the journal entry sub-forms
|
||||
*/
|
||||
getEntries(entryType = null) {
|
||||
const entries = []
|
||||
for (const side of [this.#debit, this.#credit]) {
|
||||
if (side !== null ) {
|
||||
if (entryType === null || side.entryType === entryType) {
|
||||
entries.push(...side.entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates whether to enable the currency code selector
|
||||
*
|
||||
*/
|
||||
updateCodeSelectorStatus() {
|
||||
let isEnabled = true;
|
||||
for (const entry of this.getEntries()) {
|
||||
if (entry.getOriginalEntryId() !== null) {
|
||||
isEnabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.#codeSelect.disabled = !isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -476,7 +575,7 @@ class DebitCreditSideSubForm {
|
||||
* The journal entry sub-forms
|
||||
* @type {JournalEntrySubForm[]}
|
||||
*/
|
||||
#entries;
|
||||
entries;
|
||||
|
||||
/**
|
||||
* The total
|
||||
@ -506,7 +605,7 @@ class DebitCreditSideSubForm {
|
||||
this.#error = document.getElementById(this.#prefix + "-error");
|
||||
this.#entryList = document.getElementById(this.#prefix + "-list");
|
||||
// noinspection JSValidateTypes
|
||||
this.#entries = Array.from(document.getElementsByClassName(this.#prefix)).map((element) => new JournalEntrySubForm(this, element));
|
||||
this.entries = Array.from(document.getElementsByClassName(this.#prefix)).map((element) => new JournalEntrySubForm(this, element));
|
||||
this.#total = document.getElementById(this.#prefix + "-total");
|
||||
this.#addEntryButton = document.getElementById(this.#prefix + "-add-entry");
|
||||
this.#addEntryButton.onclick = () => {
|
||||
@ -522,14 +621,14 @@ class DebitCreditSideSubForm {
|
||||
* @returns {JournalEntrySubForm} the newly-added journal entry sub-form
|
||||
*/
|
||||
addJournalEntry() {
|
||||
const newIndex = 1 + (this.#entries.length === 0? 0: Math.max(...this.#entries.map((entry) => entry.entryIndex)));
|
||||
const newIndex = 1 + (this.entries.length === 0? 0: Math.max(...this.entries.map((entry) => entry.entryIndex)));
|
||||
const html = this.currency.form.entryTemplate
|
||||
.replaceAll("CURRENCY_INDEX", escapeHtml(String(this.#currencyIndex)))
|
||||
.replaceAll("ENTRY_TYPE", escapeHtml(this.entryType))
|
||||
.replaceAll("ENTRY_INDEX", escapeHtml(String(newIndex)));
|
||||
this.#entryList.insertAdjacentHTML("beforeend", html);
|
||||
const entry = new JournalEntrySubForm(this, document.getElementById(this.#prefix + "-" + String(newIndex)));
|
||||
this.#entries.push(entry);
|
||||
this.entries.push(entry);
|
||||
this.#resetDeleteJournalEntryButtons();
|
||||
this.#initializeDragAndDropReordering();
|
||||
this.validate();
|
||||
@ -542,9 +641,11 @@ class DebitCreditSideSubForm {
|
||||
* @param entry {JournalEntrySubForm}
|
||||
*/
|
||||
deleteJournalEntry(entry) {
|
||||
const index = this.#entries.indexOf(entry);
|
||||
this.#entries.splice(index, 1);
|
||||
const index = this.entries.indexOf(entry);
|
||||
this.entries.splice(index, 1);
|
||||
this.updateTotal();
|
||||
this.currency.updateCodeSelectorStatus();
|
||||
this.currency.form.updateMinDate();
|
||||
this.#resetDeleteJournalEntryButtons();
|
||||
}
|
||||
|
||||
@ -553,11 +654,15 @@ class DebitCreditSideSubForm {
|
||||
*
|
||||
*/
|
||||
#resetDeleteJournalEntryButtons() {
|
||||
if (this.#entries.length === 1) {
|
||||
this.#entries[0].deleteButton.classList.add("d-none");
|
||||
if (this.entries.length === 1) {
|
||||
this.entries[0].deleteButton.classList.add("d-none");
|
||||
} else {
|
||||
for (const entry of this.#entries) {
|
||||
entry.deleteButton.classList.remove("d-none");
|
||||
for (const entry of this.entries) {
|
||||
if (entry.isMatched) {
|
||||
entry.deleteButton.classList.add("d-none");
|
||||
} else {
|
||||
entry.deleteButton.classList.remove("d-none");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -569,9 +674,10 @@ class DebitCreditSideSubForm {
|
||||
*/
|
||||
getTotal() {
|
||||
let total = new Decimal("0");
|
||||
for (const entry of this.#entries) {
|
||||
if (entry.amount.value !== "") {
|
||||
total = total.plus(new Decimal(entry.amount.value));
|
||||
for (const entry of this.entries) {
|
||||
const amount = entry.getAmount();
|
||||
if (amount !== null) {
|
||||
total = total.plus(amount);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
@ -582,17 +688,10 @@ class DebitCreditSideSubForm {
|
||||
*
|
||||
*/
|
||||
updateTotal() {
|
||||
let total = new Decimal("0");
|
||||
for (const entry of this.#entries) {
|
||||
if (entry.amount.value !== "") {
|
||||
total = total.plus(new Decimal(entry.amount.value));
|
||||
}
|
||||
}
|
||||
this.#total.innerText = formatDecimal(this.getTotal());
|
||||
this.currency.validateBalance();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the drag and drop reordering on the currency sub-forms.
|
||||
*
|
||||
@ -600,22 +699,13 @@ class DebitCreditSideSubForm {
|
||||
#initializeDragAndDropReordering() {
|
||||
initializeDragAndDropReordering(this.#entryList, () => {
|
||||
const entryId = Array.from(this.#entryList.children).map((entry) => entry.id);
|
||||
this.#entries.sort((a, b) => entryId.indexOf(a.element.id) - entryId.indexOf(b.element.id));
|
||||
for (let i = 0; i < this.#entries.length; i++) {
|
||||
this.#entries[i].no.value = String(i + 1);
|
||||
this.entries.sort((a, b) => entryId.indexOf(a.element.id) - entryId.indexOf(b.element.id));
|
||||
for (let i = 0; i < this.entries.length; i++) {
|
||||
this.entries[i].no.value = String(i + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account codes used in the form.
|
||||
*
|
||||
* @return {string[]} the account codes used in the form
|
||||
*/
|
||||
getAccountCodesUsed() {
|
||||
return this.#entries.filter((entry) => entry.getAccountCode() !== null).map((entry) => entry.getAccountCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the form.
|
||||
*
|
||||
@ -624,7 +714,7 @@ class DebitCreditSideSubForm {
|
||||
validate() {
|
||||
let isValid = true;
|
||||
isValid = this.#validateReal() && isValid;
|
||||
for (const entry of this.#entries) {
|
||||
for (const entry of this.entries) {
|
||||
isValid = entry.validate() && isValid;
|
||||
}
|
||||
return isValid;
|
||||
@ -636,7 +726,7 @@ class DebitCreditSideSubForm {
|
||||
* @returns {boolean} true if valid, or false otherwise
|
||||
*/
|
||||
#validateReal() {
|
||||
if (this.#entries.length === 0) {
|
||||
if (this.entries.length === 0) {
|
||||
this.#element.classList.add("is-invalid");
|
||||
this.#error.innerText = A_("Please add some journal entries.");
|
||||
return false;
|
||||
@ -677,6 +767,12 @@ class JournalEntrySubForm {
|
||||
*/
|
||||
entryIndex;
|
||||
|
||||
/**
|
||||
* Whether this is an original entry with offsets
|
||||
* @type {boolean}
|
||||
*/
|
||||
isMatched;
|
||||
|
||||
/**
|
||||
* The prefix of the HTML ID and class
|
||||
* @type {string}
|
||||
@ -725,11 +821,29 @@ class JournalEntrySubForm {
|
||||
*/
|
||||
#summaryText;
|
||||
|
||||
/**
|
||||
* The ID of the original entry
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
#originalEntryId;
|
||||
|
||||
/**
|
||||
* The text of the original entry
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#originalEntryText;
|
||||
|
||||
/**
|
||||
* The offset entries
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
#offsets;
|
||||
|
||||
/**
|
||||
* The amount
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
amount;
|
||||
#amount;
|
||||
|
||||
/**
|
||||
* The text display of the amount
|
||||
@ -754,6 +868,7 @@ class JournalEntrySubForm {
|
||||
this.element = element;
|
||||
this.entryType = element.dataset.entryType;
|
||||
this.entryIndex = parseInt(element.dataset.entryIndex);
|
||||
this.isMatched = element.classList.contains("accounting-matched-entry");
|
||||
this.#prefix = "accounting-currency-" + element.dataset.currencyIndex + "-" + this.entryType + "-" + this.entryIndex;
|
||||
this.#control = document.getElementById(this.#prefix + "-control");
|
||||
this.#error = document.getElementById(this.#prefix + "-error");
|
||||
@ -762,11 +877,14 @@ class JournalEntrySubForm {
|
||||
this.#accountText = document.getElementById(this.#prefix + "-account-text");
|
||||
this.#summary = document.getElementById(this.#prefix + "-summary");
|
||||
this.#summaryText = document.getElementById(this.#prefix + "-summary-text");
|
||||
this.amount = document.getElementById(this.#prefix + "-amount");
|
||||
this.#originalEntryId = document.getElementById(this.#prefix + "-original-entry-id");
|
||||
this.#originalEntryText = document.getElementById(this.#prefix + "-original-entry-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");
|
||||
this.#control.onclick = () => {
|
||||
JournalEntryEditor.edit(this, this.#summary.value, this.#accountCode.value, this.#accountCode.dataset.text, this.amount.value);
|
||||
JournalEntryEditor.edit(this, this.#originalEntryId.value, this.#originalEntryId.dataset.date, this.#originalEntryId.dataset.text, this.#summary.value, this.#accountCode.value, this.#accountCode.dataset.text, this.#amount.value, this.#amount.dataset.min);
|
||||
};
|
||||
this.deleteButton.onclick = () => {
|
||||
this.element.parentElement.removeChild(this.element);
|
||||
@ -774,6 +892,33 @@ class JournalEntrySubForm {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the entry is an original entry.
|
||||
*
|
||||
* @return {boolean} true if the entry is an original entry, or false otherwise
|
||||
*/
|
||||
isOriginalEntry() {
|
||||
return "isOriginalEntry" in this.element.dataset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the original entry.
|
||||
*
|
||||
* @return {string|null} the ID of the original entry
|
||||
*/
|
||||
getOriginalEntryId() {
|
||||
return this.#originalEntryId.value === ""? null: this.#originalEntryId.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the date of the original entry.
|
||||
*
|
||||
* @return {string|null} the date of the original entry
|
||||
*/
|
||||
getOriginalEntryDate() {
|
||||
return this.#originalEntryId.dataset.date === ""? null: this.#originalEntryId.dataset.date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account code.
|
||||
*
|
||||
@ -783,6 +928,15 @@ class JournalEntrySubForm {
|
||||
return this.#accountCode.value === ""? null: this.#accountCode.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount.
|
||||
*
|
||||
* @return {Decimal|null} the amount
|
||||
*/
|
||||
getAmount() {
|
||||
return this.#amount.value === ""? null: new Decimal(this.#amount.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the form.
|
||||
*
|
||||
@ -794,7 +948,7 @@ class JournalEntrySubForm {
|
||||
this.#error.innerText = A_("Please select the account.");
|
||||
return false;
|
||||
}
|
||||
if (this.amount.value === "") {
|
||||
if (this.#amount.value === "") {
|
||||
this.#control.classList.add("is-invalid");
|
||||
this.#error.innerText = A_("Please fill in the amount.");
|
||||
return false;
|
||||
@ -807,21 +961,42 @@ class JournalEntrySubForm {
|
||||
/**
|
||||
* Stores the data into the journal entry sub-form.
|
||||
*
|
||||
* @param isOriginalEntry {boolean} true if this is an original entry, or false otherwise
|
||||
* @param originalEntryId {string} the ID of the original entry
|
||||
* @param originalEntryDate {string} the date of the original entry
|
||||
* @param originalEntryText {string} the text of the original entry
|
||||
* @param accountCode {string} the account code
|
||||
* @param accountText {string} the account text
|
||||
* @param summary {string} the summary
|
||||
* @param amount {string} the amount
|
||||
*/
|
||||
save(accountCode, accountText, summary, amount) {
|
||||
save(isOriginalEntry, originalEntryId, originalEntryDate, originalEntryText, accountCode, accountText, summary, amount) {
|
||||
if (isOriginalEntry) {
|
||||
this.#offsets.classList.remove("d-none");
|
||||
} else {
|
||||
this.#offsets.classList.add("d-none");
|
||||
}
|
||||
this.#originalEntryId.value = originalEntryId;
|
||||
this.#originalEntryId.dataset.date = originalEntryDate;
|
||||
this.#originalEntryId.dataset.text = originalEntryText;
|
||||
if (originalEntryText === "") {
|
||||
this.#originalEntryText.classList.add("d-none");
|
||||
this.#originalEntryText.innerText = "";
|
||||
} else {
|
||||
this.#originalEntryText.classList.remove("d-none");
|
||||
this.#originalEntryText.innerText = A_("Offset %(entry)s", {entry: originalEntryText});
|
||||
}
|
||||
this.#accountCode.value = accountCode;
|
||||
this.#accountCode.dataset.text = accountText;
|
||||
this.#accountText.innerText = accountText;
|
||||
this.#summary.value = summary;
|
||||
this.#summaryText.innerText = summary;
|
||||
this.amount.value = amount;
|
||||
this.#amount.value = amount;
|
||||
this.#amountText.innerText = formatDecimal(new Decimal(amount));
|
||||
this.validate();
|
||||
this.side.updateTotal();
|
||||
this.side.currency.updateCodeSelectorStatus();
|
||||
this.side.currency.form.updateMinDate();
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user