Renamed "journal entry" to "voucher line item", and "entry type" to "side".
This commit is contained in:
409
src/accounting/static/js/original-line-item-selector.js
Normal file
409
src/accounting/static/js/original-line-item-selector.js
Normal file
@ -0,0 +1,409 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
* original-line-item-selector.js: The JavaScript for the original line item 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";
|
||||
|
||||
/**
|
||||
* The original line item selector.
|
||||
*
|
||||
*/
|
||||
class OriginalLineItemSelector {
|
||||
|
||||
/**
|
||||
* The line item editor
|
||||
* @type {VoucherLineItemEditor}
|
||||
*/
|
||||
lineItemEditor;
|
||||
|
||||
/**
|
||||
* The prefix of the HTML ID and class
|
||||
* @type {string}
|
||||
*/
|
||||
#prefix = "accounting-original-line-item-selector";
|
||||
|
||||
/**
|
||||
* 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 {OriginalLineItem[]}
|
||||
*/
|
||||
#options;
|
||||
|
||||
/**
|
||||
* The options by their ID
|
||||
* @type {Object.<string, OriginalLineItem>}
|
||||
*/
|
||||
#optionById;
|
||||
|
||||
/**
|
||||
* The currency code
|
||||
* @type {string}
|
||||
*/
|
||||
#currencyCode;
|
||||
|
||||
/**
|
||||
* The side, either "credit" or "debit"
|
||||
*/
|
||||
#side;
|
||||
|
||||
/**
|
||||
* Constructs an original line item selector.
|
||||
*
|
||||
* @param lineItemEditor {VoucherLineItemEditor} the line item editor
|
||||
*/
|
||||
constructor(lineItemEditor) {
|
||||
this.lineItemEditor = lineItemEditor;
|
||||
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 OriginalLineItem(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 line item.
|
||||
*
|
||||
* @param currentLineItem {LineItemSubForm} the line item sub-form that is currently editing
|
||||
* @param form {VoucherForm} the voucher form
|
||||
* @param originalLineItemId {string} the ID of the original line item
|
||||
* @return {Decimal} the net balance of the original line item
|
||||
*/
|
||||
getNetBalance(currentLineItem, form, originalLineItemId) {
|
||||
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 (amount !== null) {
|
||||
otherOffset = otherOffset.plus(amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.#optionById[originalLineItemId].bareNetBalance.minus(otherOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the net balances, subtracting the offset amounts on the form but the currently editing line item
|
||||
*
|
||||
*/
|
||||
#updateNetBalances() {
|
||||
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();
|
||||
if (otherOriginalLineItemId === null || amount === null) {
|
||||
continue;
|
||||
}
|
||||
if (!(otherOriginalLineItemId in otherOffsets)) {
|
||||
otherOffsets[otherOriginalLineItemId] = new Decimal("0");
|
||||
}
|
||||
otherOffsets[otherOriginalLineItemId] = otherOffsets[otherOriginalLineItemId].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.#side, this.#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 line item selector is shown.
|
||||
*
|
||||
*/
|
||||
onOpen() {
|
||||
this.#currencyCode = this.lineItemEditor.getCurrencyCode();
|
||||
this.#side = this.lineItemEditor.side;
|
||||
for (const option of this.#options) {
|
||||
option.setActive(option.id === this.lineItemEditor.originalLineItemId);
|
||||
}
|
||||
this.#query.value = "";
|
||||
this.#updateNetBalances();
|
||||
this.#filterOptions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An original line item.
|
||||
*
|
||||
*/
|
||||
class OriginalLineItem {
|
||||
|
||||
/**
|
||||
* The original line item selector
|
||||
* @type {OriginalLineItemSelector}
|
||||
*/
|
||||
#selector;
|
||||
|
||||
/**
|
||||
* The element
|
||||
* @type {HTMLLIElement}
|
||||
*/
|
||||
#element;
|
||||
|
||||
/**
|
||||
* The ID
|
||||
* @type {string}
|
||||
*/
|
||||
id;
|
||||
|
||||
/**
|
||||
* The date
|
||||
* @type {string}
|
||||
*/
|
||||
date;
|
||||
|
||||
/**
|
||||
* The side, either "debit" or "credit"
|
||||
* @type {string}
|
||||
*/
|
||||
#side;
|
||||
|
||||
/**
|
||||
* 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 line item
|
||||
* @type {string}
|
||||
*/
|
||||
text;
|
||||
|
||||
/**
|
||||
* The values to query against
|
||||
* @type {string[][]}
|
||||
*/
|
||||
#queryValues;
|
||||
|
||||
/**
|
||||
* Constructs an original line item.
|
||||
*
|
||||
* @param selector {OriginalLineItemSelector} the original line item 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.#side = element.dataset.side;
|
||||
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-line-item-selector-option-" + this.id + "-net-balance");
|
||||
this.text = element.dataset.text;
|
||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||
this.#element.onclick = () => this.#selector.lineItemEditor.saveOriginalLineItem(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 side {string} the side, either "debit" or "credit"
|
||||
* @param currencyCode {string} the currency code
|
||||
* @param query {string|null} the query term
|
||||
*/
|
||||
isMatched(side, currencyCode, query = null) {
|
||||
return this.netBalance.greaterThan(0)
|
||||
&& this.date <= this.#selector.lineItemEditor.form.getDate()
|
||||
&& this.#isSideMatches(side)
|
||||
&& this.#currencyCode === currencyCode
|
||||
&& this.#isQueryMatches(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the original line item matches the debit or credit side.
|
||||
*
|
||||
* @param side {string} the side, either "debit" or credit
|
||||
* @return {boolean} true if the option matches, or false otherwise
|
||||
*/
|
||||
#isSideMatches(side) {
|
||||
return (side === "debit" && this.#side === "credit")
|
||||
|| (side === "credit" && this.#side === "debit");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the original line item 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");
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user