Added to track the net balance and offset of the original entries.

This commit is contained in:
2023-03-17 22:32:01 +08:00
parent 40e329d37f
commit d88b3ac770
38 changed files with 3103 additions and 183 deletions

View File

@ -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);
}
}