37 Commits

Author SHA1 Message Date
a17395b43e Advanced to version 0.10.0. 2023-04-03 22:08:02 +08:00
17c8d9d1a9 Revised the styles of the buttons of the suggested accounts in the description editor. 2023-04-03 22:07:56 +08:00
fa94cd407e Added the JavaScript setElementShown function in the journal entry form for readability. 2023-04-03 21:37:51 +08:00
9a704c8185 Revised the JavaScript account reorder code to avoid nested template literals, for readability. 2023-04-03 21:20:24 +08:00
8286c0c6d8 Revised the JavaScript MonthTab class in the period chooser to avoid nested template literals, for readability. 2023-04-03 21:19:48 +08:00
f7efacad75 Added the unauthorized method to the UserUtilityInterface interface, so that when the user has not logged in, the permission decorator can ask the user to log in instead of failing with HTTP 403 Forbidden. 2023-04-03 19:50:47 +08:00
9263ae0274 Changed the "account" property to private as "__account" in the DescriptionAccount class. 2023-04-03 19:50:47 +08:00
78a9d7794c Revised the JavaScript OriginalLineItem class to store the form instead of the selector. The selector is only used in the constructor. 2023-04-03 19:50:47 +08:00
f3ae37a409 Removed the "#selector" attribute from the JavaScript RecurringAccount class. It is only used in the constructor. There is no need to hold a reference to it. 2023-04-03 19:50:47 +08:00
ddc1081252 Removed the "#selector" attribute from the JavaScript BaseAccountOption class. It is only used in the constructor. There is no need to hold a reference to it. 2023-04-03 19:50:46 +08:00
202d51a032 Removed the "#selector" attribute from the JavaScript JournalEntryAccountOption class. It is only used in the constructor. There is no need to hold a reference to it. 2023-04-03 19:50:46 +08:00
562bc47be7 Revised the saveDescription method of the JournalEntryLineItemEditor editor to also save the isAccountConfirmed status of the DescriptionEditor editor, so that when the user selected any suggested account other than the confirmed account, the confirmed account is released from the next edit. 2023-04-03 19:50:46 +08:00
f3d43a66cc Fixed the operator in the selectAccount method of the JavaScript DescriptionEditor editor. 2023-04-03 19:50:46 +08:00
c3fc6d9a87 Revised the onOpen method of the JavaScript DescriptionEditor editor, to clear the tab planes after the confirmed account is set, so that it works in an environment where the confirmed account is already set. 2023-04-03 19:50:46 +08:00
e1a0380628 Revised the saveDescription method of the JavaScript JournalEntryLineItemEditor to accept the description editor instead of the separated description and account values. 2023-04-03 19:50:46 +08:00
f2a2fcdd32 Revised the "#onDescriptionChange" method to also reset the selected account in the JavaScript DescriptionEditor editor. 2023-04-03 19:50:46 +08:00
ab29166f1e Renamed the "#reset" method to "#resetTabPlanes" in the JavaScript DescriptionEditor, to be clear. 2023-04-03 19:50:46 +08:00
8033921181 Revised the JavaScript DescriptionEditor class so that the #reset() method is triggered by the #onDescriptionChange event, but not the onOpen event, so that user-edited description updates also clear the tab planes. 2023-04-03 19:50:45 +08:00
08732c1e66 Renamed the description attribute to #descriptionInput, and added the description getter and setter to the JavaScript DescriptionEditor editor, to hide the actual implementation of the description input. 2023-04-03 19:50:45 +08:00
4adc464d3d Merged the saveDescriptionWithAccount into the saveDescription method in the JavaScript JournalEntryLineItemEditor class. 2023-04-03 19:12:06 +08:00
2f9d2e36cb Revised the parameters of the saveDescriptionWithAccount method of the JavaScript JournalEntryLineItemEditor class to accept an DescriptionEditorAccount instance instead of the individual account values. 2023-04-03 19:12:06 +08:00
5bb10bf6ba Added the JavaScript DescriptionEditorAccount, DescriptionEditorSuggestedAccount, and DescriptionEditorConfirmedAccount classes, and revised the DescriptionEditor editor to work with these class instances instead of the HTML elements, for simplicity and readability. 2023-04-03 19:12:06 +08:00
06e7b6ddff Added the missing "is_need_offset" property to the DescriptionAccount class. 2023-04-03 19:11:10 +08:00
20e1982984 Renamed the "accounting-is-need-offset" class to "accounting-account-is-need-offset" in the line item sub-form of the journal entry form, for consistency. 2023-04-02 22:29:27 +08:00
a70720be50 Renamed the #selectedAccount attribute to #selectedAccountButton, and the filterSuggestedAccounts, #selectSuggestedAccount, clearSuggestedAccounts, #initializeSuggestedAccounts, #selectAccount, #setConfirmedAccount, and #setSuggestedAccounts methods to filterSuggestedAccountButtons, #selectSuggestedAccountButton, clearSuggestedAccountButtons, #initializeSuggestedAccountButtons, #selectAccountButton, #setConfirmedAccountButton, and #setSuggestedAccountButtons, respectively, in the JavaScript DescriptionEditor class. 2023-04-02 22:16:29 +08:00
cb6de08152 Moved the JournalEntryAccount class from journal-entry-line-item-editor.js to journal-entry-form.js. 2023-04-01 22:42:58 +08:00
211821b4d7 Added the "confirmed account" to the description editor so that it does not override the user's selected account when the user specifically selected it or already confirmed it. 2023-04-01 18:05:48 +08:00
0faca49540 Revised the save method of the JavaScript LineItemSubForm class to update whether it needs offsetting, too. 2023-04-01 00:34:29 +08:00
14e79df571 Revised the line item sub-form to store the information whether it needs offsetting as a class instead of a dataset attribute, and store it in the account code input instead of the whole element, for simplicity and readability. 2023-04-01 00:29:04 +08:00
04fbb725d2 Revised the logic to save the account in the save method of the LineItemSubForm class, since when saving from the line item editor, the account is never null. 2023-04-01 00:19:32 +08:00
a1d6844e52 Replaced the accountCode and accountText getters with the account getter in the JavaScript LineItemSubForm class. 2023-04-01 00:14:47 +08:00
94391b02a6 Added the copy() method to the JavaScript JournalEntryAccount class, and replaced the accountCode and accountText fields with the account field in the OriginalLineItem class. 2023-03-31 23:54:56 +08:00
1cb8a7563e Added the JavaScript JournalEntryAccount class, and added the account field to the JournalEntryLineItemEditor class to replace the accountCode, accountText, and isNeedOffset fields. 2023-03-31 23:33:38 +08:00
63f0f28948 Prefix the classes in the JavaScript description editor with the "DescriptionEditor". 2023-03-27 07:22:36 +08:00
3431922f12 Removed an unused import from the "accounting.models" module. 2023-03-26 01:06:19 +08:00
d5a9e1af18 Removed an unnecessary "start" variable in the constructor of the JavaScript MonthTab class. 2023-03-25 08:37:17 +08:00
73f5d63f44 Replaced string concatenations with ES6 template literals. 2023-03-25 08:37:13 +08:00
20 changed files with 597 additions and 393 deletions

View File

@ -13,7 +13,7 @@ sys.path.insert(0, os.path.abspath('../../src/'))
project = 'Mia! Accounting Flask' project = 'Mia! Accounting Flask'
copyright = '2023, imacat' copyright = '2023, imacat'
author = 'imacat' author = 'imacat'
release = '0.9.1' release = '0.10.0'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@ -17,7 +17,7 @@
[metadata] [metadata]
name = mia-accounting-flask name = mia-accounting-flask
version = 0.9.1 version = 0.10.0
author = imacat author = imacat
author_email = imacat@mail.imacat.idv.tw author_email = imacat@mail.imacat.idv.tw
description = The Mia! Accounting Flask project. description = The Mia! Accounting Flask project.

View File

@ -36,12 +36,14 @@ class DescriptionAccount:
:param account: The account. :param account: The account.
:param freq: The frequency of the tag with the account. :param freq: The frequency of the tag with the account.
""" """
self.account: Account = account self.__account: Account = account
"""The account.""" """The account."""
self.id: int = account.id self.id: int = account.id
"""The account ID.""" """The account ID."""
self.code: str = account.code self.code: str = account.code
"""The account code.""" """The account code."""
self.is_need_offset: bool = account.is_need_offset
"""Whether the journal entry line items of this account need offset."""
self.freq: int = freq self.freq: int = freq
"""The frequency of the tag with the account.""" """The frequency of the tag with the account."""
@ -50,7 +52,7 @@ class DescriptionAccount:
:return: The string representation of the account. :return: The string representation of the account.
""" """
return str(self.account) return str(self.__account)
def add_freq(self, freq: int) -> None: def add_freq(self, freq: int) -> None:
"""Adds the frequency of an account. """Adds the frequency of an account.

View File

@ -21,7 +21,6 @@ from __future__ import annotations
import re import re
import typing as t import typing as t
from datetime import date
from decimal import Decimal from decimal import Decimal
import sqlalchemy as sa import sqlalchemy as sa

View File

@ -342,12 +342,6 @@ class BaseAccountSelector {
*/ */
class BaseAccountOption { class BaseAccountOption {
/**
* The base account selector
* @type {BaseAccountSelector}
*/
#selector;
/** /**
* The element * The element
* @type {HTMLLIElement} * @type {HTMLLIElement}
@ -379,13 +373,12 @@ class BaseAccountOption {
* @param element {HTMLLIElement} the element * @param element {HTMLLIElement} the element
*/ */
constructor(selector, element) { constructor(selector, element) {
this.#selector = selector;
this.#element = element; this.#element = element;
this.code = element.dataset.code; this.code = element.dataset.code;
this.text = element.dataset.text; this.text = element.dataset.text;
this.#queryValues = JSON.parse(element.dataset.queryValues); this.#queryValues = JSON.parse(element.dataset.queryValues);
this.#element.onclick = () => this.#selector.form.saveBaseAccount(this); this.#element.onclick = () => selector.form.saveBaseAccount(this);
} }
/** /**

View File

@ -29,10 +29,11 @@ document.addEventListener("DOMContentLoaded", () => {
const onReorder = () => { const onReorder = () => {
const accounts = Array.from(list.children); const accounts = Array.from(list.children);
for (let i = 0; i < accounts.length; i++) { for (let i = 0; i < accounts.length; i++) {
const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no"); const no = document.getElementById(`accounting-order-${accounts[i].dataset.id}-no`);
const code = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-code"); const code = document.getElementById(`accounting-order-${accounts[i].dataset.id}-code`);
no.value = String(i + 1); no.value = String(i + 1);
code.innerText = list.dataset.baseCode + "-" + ("000" + (i + 1)).slice(-3); const zeroPaddedNo = `000${no.value}`.slice(-3)
code.innerText = `${list.dataset.baseCode}-${zeroPaddedNo}`;
} }
}; };
initializeDragAndDropReordering(list, onReorder); initializeDragAndDropReordering(list, onReorder);

View File

@ -128,7 +128,7 @@ class CurrencyForm {
} }
const original = this.#code.dataset.original; const original = this.#code.dataset.original;
if (original === "" || this.#code.value !== original) { if (original === "" || this.#code.value !== original) {
const response = await fetch(this.#code.dataset.existsUrl + "?q=" + encodeURIComponent(this.#code.value)); const response = await fetch(`${this.#code.dataset.existsUrl}?q=${encodeURIComponent(this.#code.value)}`);
const data = await response.json(); const data = await response.json();
if (data["exists"]) { if (data["exists"]) {
this.#code.classList.add("is-invalid"); this.#code.classList.add("is-invalid");

View File

@ -60,7 +60,7 @@ class DescriptionEditor {
/** /**
* The current tab * The current tab
* @type {TabPlane} * @type {DescriptionEditorTabPlane}
*/ */
currentTab; currentTab;
@ -68,7 +68,7 @@ class DescriptionEditor {
* The description input * The description input
* @type {HTMLInputElement} * @type {HTMLInputElement}
*/ */
description; #descriptionInput;
/** /**
* The button to the original line item selector * The button to the original line item selector
@ -89,20 +89,44 @@ class DescriptionEditor {
note; note;
/** /**
* The account buttons * The placeholder of the confirmed account
* @type {HTMLButtonElement[]} * @type {DescriptionEditorConfirmedAccount}
*/ */
#accountButtons; #confirmedAccountPlaceholder;
/** /**
* The selected account button * All the suggested accounts
* @type {HTMLButtonElement|null} * @type {DescriptionEditorSuggestedAccount[]}
*/ */
#selectedAccount = null; #allSuggestedAccounts;
/**
* The current suggested accounts
* @type {DescriptionEditorSuggestedAccount[]}
*/
#currentSuggestedAccounts;
/**
* The account that the user specified or confirmed
* @type {DescriptionEditorConfirmedAccount|null}
*/
#confirmedAccount = null;
/**
* Whether the user has confirmed the account
* @type {boolean}
*/
isAccountConfirmed = false;
/**
* The selected account.
* @type {DescriptionEditorAccount|null}
*/
selectedAccount = null;
/** /**
* The tab planes * The tab planes
* @type {{general: GeneralTagTab, travel: GeneralTripTab, bus: BusTripTab, recurring: RecurringTransactionTab, annotation: AnnotationTab}} * @type {{general: DescriptionEditorGeneralTagTab, travel: DescriptionEditorGeneralTripTab, bus: DescriptionEditorBusTripTab, recurring: DescriptionEditorRecurringTab, annotation: DescriptionEditorAnnotationTab}}
*/ */
tabPlanes = {}; tabPlanes = {};
@ -115,23 +139,22 @@ class DescriptionEditor {
constructor(lineItemEditor, debitCredit) { constructor(lineItemEditor, debitCredit) {
this.lineItemEditor = lineItemEditor; this.lineItemEditor = lineItemEditor;
this.debitCredit = debitCredit; this.debitCredit = debitCredit;
this.prefix = "accounting-description-editor-" + debitCredit; this.prefix = `accounting-description-editor-${debitCredit}`;
this.#form = document.getElementById(this.prefix); this.#form = document.getElementById(this.prefix);
this.#modal = document.getElementById(this.prefix + "-modal"); this.#modal = document.getElementById(`${this.prefix}-modal`);
this.description = document.getElementById(this.prefix + "-description"); this.#descriptionInput = document.getElementById(`${this.prefix}-description`);
this.#offsetButton = document.getElementById(this.prefix + "-offset"); this.#offsetButton = document.getElementById(`${this.prefix}-offset`);
this.number = document.getElementById(this.prefix + "-annotation-number"); this.number = document.getElementById(`${this.prefix}-annotation-number`);
this.note = document.getElementById(this.prefix + "-annotation-note"); this.note = document.getElementById(`${this.prefix}-annotation-note`);
// noinspection JSValidateTypes this.#confirmedAccountPlaceholder = new DescriptionEditorConfirmedAccount(this, document.getElementById(`${this.prefix}-account-confirmed`));
this.#accountButtons = Array.from(document.getElementsByClassName(this.prefix + "-account")); this.#allSuggestedAccounts = Array.from(document.getElementsByClassName(`${this.prefix}-account`)).map((button) => new DescriptionEditorSuggestedAccount(this, button));
for (const cls of [GeneralTagTab, GeneralTripTab, BusTripTab, RecurringTransactionTab, AnnotationTab]) { for (const cls of [DescriptionEditorGeneralTagTab, DescriptionEditorGeneralTripTab, DescriptionEditorBusTripTab, DescriptionEditorRecurringTab, DescriptionEditorAnnotationTab]) {
const tab = new cls(this); const tab = new cls(this);
this.tabPlanes[tab.tabId()] = tab; this.tabPlanes[tab.tabId()] = tab;
} }
this.currentTab = this.tabPlanes.general; this.currentTab = this.tabPlanes.general;
this.#initializeSuggestedAccounts(); this.#descriptionInput.onchange = () => this.#onDescriptionChange();
this.description.onchange = () => this.#onDescriptionChange();
this.#offsetButton.onclick = () => this.lineItemEditor.originalLineItemSelector.onOpen(); this.#offsetButton.onclick = () => this.lineItemEditor.originalLineItemSelector.onOpen();
this.#form.onsubmit = () => { this.#form.onsubmit = () => {
if (this.currentTab.validate()) { if (this.currentTab.validate()) {
@ -141,12 +164,44 @@ class DescriptionEditor {
}; };
} }
/**
* Returns the description.
*
* @return {string} the description
*/
get description() {
return this.#descriptionInput.value;
}
/**
* Sets the description.
*
* @param description {string} the description
*/
set description(description) {
this.#descriptionInput.value = description;
}
/**
* Returns the current account options.
*
* @return {DescriptionEditorAccount[]} the current account options.
*/
get #currentAccountOptions() {
if (this.#confirmedAccount === null) {
return this.#currentSuggestedAccounts;
}
return [this.#confirmedAccount].concat(this.#currentSuggestedAccounts);
}
/** /**
* The callback when the description input is changed. * The callback when the description input is changed.
* *
*/ */
#onDescriptionChange() { #onDescriptionChange() {
this.description.value = this.description.value.trim(); this.#resetTabPlanes();
this.selectedAccount = null;
this.description = this.description.trim();
for (const tabPlane of [this.tabPlanes.recurring, this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) { for (const tabPlane of [this.tabPlanes.recurring, this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) {
if (tabPlane.populate()) { if (tabPlane.populate()) {
break; break;
@ -156,20 +211,49 @@ class DescriptionEditor {
} }
/** /**
* Filters the suggested accounts. * Resets the tab planes.
*
*/
#resetTabPlanes() {
for (const tabPlane of Object.values(this.tabPlanes)) {
tabPlane.reset();
}
this.tabPlanes.general.switchToMe();
}
/**
* Updates the current suggested accounts.
* *
* @param tagButton {HTMLButtonElement} the tag button * @param tagButton {HTMLButtonElement} the tag button
*/ */
filterSuggestedAccounts(tagButton) { updateCurrentSuggestedAccounts(tagButton) {
this.clearSuggestedAccounts(); this.clearSuggestedAccounts();
const suggested = JSON.parse(tagButton.dataset.accounts); const suggestedAccountCodes = JSON.parse(tagButton.dataset.accounts);
for (const accountButton of this.#accountButtons) { this.#currentSuggestedAccounts = this.#allSuggestedAccounts.filter((account) => {
if (suggested.includes(accountButton.dataset.code)) { if (this.#confirmedAccount !== null && account.code === this.#confirmedAccount.code) {
accountButton.classList.remove("d-none"); return false;
if (accountButton.dataset.code === suggested[0]) { }
this.#selectAccount(accountButton); return suggestedAccountCodes.includes(account.code);
return; });
} for (const account of this.#currentSuggestedAccounts) {
account.setShown(true);
}
this.#selectSuggestedAccount(suggestedAccountCodes[0]);
}
/**
* Selects the suggested account.
*
* @param code {string} the code of the most-frequent suggested account
*/
#selectSuggestedAccount(code) {
if (this.isAccountConfirmed) {
return;
}
for (const account of this.#currentAccountOptions) {
if (account.code === code) {
this.selectAccount(account);
return;
} }
} }
} }
@ -179,37 +263,29 @@ class DescriptionEditor {
* *
*/ */
clearSuggestedAccounts() { clearSuggestedAccounts() {
for (const accountButton of this.#accountButtons) { for (const account of this.#allSuggestedAccounts) {
accountButton.classList.add("d-none"); account.setShown(false);
account.setActive(false);
} }
this.#selectAccount(null); this.#currentSuggestedAccounts = [];
} }
/** /**
* Initializes the suggested accounts. * Select an account.
* *
* @param selectedAccount {DescriptionEditorAccount|null} the account, or null to deselect the account
*/ */
#initializeSuggestedAccounts() { selectAccount(selectedAccount) {
for (const accountButton of this.#accountButtons) { for (const account of this.#currentAccountOptions) {
accountButton.onclick = () => this.#selectAccount(accountButton); account.setActive(false);
} }
} if (selectedAccount !== null) {
selectedAccount.setActive(true);
/**
* Select a suggested account.
*
* @param selectedAccountButton {HTMLButtonElement|null} the account button, or null to deselect the account
*/
#selectAccount(selectedAccountButton) {
for (const accountButton of this.#accountButtons) {
accountButton.classList.remove("btn-primary");
accountButton.classList.add("btn-outline-primary");
} }
if (selectedAccountButton !== null) { this.selectedAccount = selectedAccount;
selectedAccountButton.classList.remove("btn-outline-primary"); if (this.selectedAccount !== null) {
selectedAccountButton.classList.add("btn-primary"); this.isAccountConfirmed &&= this.selectedAccount.isConfirmedAccount;
} }
this.#selectedAccount = selectedAccountButton;
} }
/** /**
@ -218,11 +294,7 @@ class DescriptionEditor {
*/ */
#submit() { #submit() {
bootstrap.Modal.getOrCreateInstance(this.#modal).hide(); bootstrap.Modal.getOrCreateInstance(this.#modal).hide();
if (this.#selectedAccount !== null) { this.lineItemEditor.saveDescription(this);
this.lineItemEditor.saveDescriptionWithAccount(this.description.value, this.#selectedAccount.dataset.code, this.#selectedAccount.dataset.text, this.#selectedAccount.classList.contains("accounting-account-is-need-offset"));
} else {
this.lineItemEditor.saveDescription(this.description.value);
}
} }
/** /**
@ -230,21 +302,27 @@ class DescriptionEditor {
* *
*/ */
onOpen() { onOpen() {
this.#reset(); this.description = this.lineItemEditor.description === null? "": this.lineItemEditor.description;
this.description.value = this.lineItemEditor.description === null? "": this.lineItemEditor.description; this.#setConfirmedAccount();
this.#onDescriptionChange(); this.#onDescriptionChange();
if (this.isAccountConfirmed) {
this.selectAccount(this.#confirmedAccount);
}
} }
/** /**
* Resets the description editor. * Sets the confirmed account.
* *
*/ */
#reset() { #setConfirmedAccount() {
this.description.value = ""; this.isAccountConfirmed = this.lineItemEditor.isAccountConfirmed;
for (const tabPlane of Object.values(this.tabPlanes)) { this.#confirmedAccountPlaceholder.setShown(this.isAccountConfirmed);
tabPlane.reset(); if (this.isAccountConfirmed) {
this.#confirmedAccountPlaceholder.initializeFrom(this.lineItemEditor.account);
this.#confirmedAccount = this.#confirmedAccountPlaceholder;
} else {
this.#confirmedAccount = null;
} }
this.tabPlanes.general.switchToMe();
} }
/** /**
@ -263,13 +341,130 @@ class DescriptionEditor {
} }
} }
/**
* An account option in the description editor.
*
*/
class DescriptionEditorAccount extends JournalEntryAccount {
/**
* The account button
* @type {HTMLButtonElement}
*/
#element;
/**
* Whether this is the account specified or confirmed by the user
* @type {boolean}
*/
isConfirmedAccount = false;
/**
* Constructs an account option in the description editor.
*
* @param editor {DescriptionEditor} the description editor
* @param code {string} the account code
* @param text {string} the account text
* @param isNeedOffset {boolean} true if the line items in the account needs offset, or false otherwise
* @param button {HTMLButtonElement} the account button
*/
constructor(editor, code, text, isNeedOffset, button) {
super(code, text, isNeedOffset);
this.#element = button;
this.#element.onclick = () => editor.selectAccount(this);
}
/**
* 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("btn-primary");
this.#element.classList.remove("btn-outline-primary");
} else {
this.#element.classList.remove("btn-primary");
this.#element.classList.add("btn-outline-primary");
}
}
/**
* Sets the content of the account button.
*
*/
resetContent() {
this.#element.innerText = this.text;
}
}
/**
* A suggested account.
*
*/
class DescriptionEditorSuggestedAccount extends DescriptionEditorAccount {
/**
* Constructs a suggested account.
*
* @param editor {DescriptionEditor} the description editor
* @param button {HTMLButtonElement} the account button
*/
constructor(editor, button) {
super(editor, button.dataset.code, button.dataset.text, button.classList.contains("accounting-account-is-need-offset"), button);
}
}
/**
* The account option that is specified or confirmed by the user.
*
*/
class DescriptionEditorConfirmedAccount extends DescriptionEditorAccount {
/**
* Constructs the account option that is specified or confirmed by the user.
*
* @param editor {DescriptionEditor} the description editor
* @param button {HTMLButtonElement} the account button
*/
constructor(editor, button) {
super(editor, "", "", false, button);
this.isConfirmedAccount = true;
}
/**
* Initializes the confirmed account from the line item editor.
*
* @param account {JournalEntryAccount} the confirmed account from the line item editor
*/
initializeFrom(account) {
this.code = account.code;
this.text = account.text;
this.isNeedOffset = account.isNeedOffset;
this.resetContent();
}
}
/** /**
* A tab plane. * A tab plane.
* *
* @abstract * @abstract
* @private * @private
*/ */
class TabPlane { class DescriptionEditorTabPlane {
/** /**
* The parent description editor * The parent description editor
@ -302,9 +497,9 @@ class TabPlane {
*/ */
constructor(editor) { constructor(editor) {
this.editor = editor; this.editor = editor;
this.prefix = this.editor.prefix + "-" + this.tabId(); this.prefix = `${this.editor.prefix}-${this.tabId()}`;
this.#tab = document.getElementById(this.prefix + "-tab"); this.#tab = document.getElementById(`${this.prefix}-tab`);
this.#page = document.getElementById(this.prefix + "-page"); this.#page = document.getElementById(`${this.prefix}-page`);
this.#tab.onclick = () => this.switchToMe(); this.#tab.onclick = () => this.switchToMe();
} }
@ -364,7 +559,7 @@ class TabPlane {
* @abstract * @abstract
* @private * @private
*/ */
class TagTabPlane extends TabPlane { class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
/** /**
* The tag input * The tag input
@ -392,10 +587,10 @@ class TagTabPlane extends TabPlane {
*/ */
constructor(editor) { constructor(editor) {
super(editor); super(editor);
this.tag = document.getElementById(this.prefix + "-tag"); this.tag = document.getElementById(`${this.prefix}-tag`);
this.tagError = document.getElementById(this.prefix + "-tag-error"); this.tagError = document.getElementById(`${this.prefix}-tag-error`);
// noinspection JSValidateTypes // noinspection JSValidateTypes
this.tagButtons = Array.from(document.getElementsByClassName(this.prefix + "-btn-tag")); this.tagButtons = Array.from(document.getElementsByClassName(`${this.prefix}-btn-tag`));
this.initializeTagButtons(); this.initializeTagButtons();
this.tag.onchange = () => { this.tag.onchange = () => {
this.onTagChange(); this.onTagChange();
@ -414,7 +609,7 @@ class TagTabPlane extends TabPlane {
if (tagButton.dataset.value === this.tag.value) { if (tagButton.dataset.value === this.tag.value) {
tagButton.classList.remove("btn-outline-primary"); tagButton.classList.remove("btn-outline-primary");
tagButton.classList.add("btn-primary"); tagButton.classList.add("btn-primary");
this.editor.filterSuggestedAccounts(tagButton); this.editor.updateCurrentSuggestedAccounts(tagButton);
isMatched = true; isMatched = true;
} else { } else {
tagButton.classList.remove("btn-primary"); tagButton.classList.remove("btn-primary");
@ -442,7 +637,7 @@ class TagTabPlane extends TabPlane {
super.switchToMe(); super.switchToMe();
for (const tagButton of this.tagButtons) { for (const tagButton of this.tagButtons) {
if (tagButton.classList.contains("btn-primary")) { if (tagButton.classList.contains("btn-primary")) {
this.editor.filterSuggestedAccounts(tagButton); this.editor.updateCurrentSuggestedAccounts(tagButton);
return; return;
} }
} }
@ -463,7 +658,7 @@ class TagTabPlane extends TabPlane {
tagButton.classList.remove("btn-outline-primary"); tagButton.classList.remove("btn-outline-primary");
tagButton.classList.add("btn-primary"); tagButton.classList.add("btn-primary");
this.tag.value = tagButton.dataset.value; this.tag.value = tagButton.dataset.value;
this.editor.filterSuggestedAccounts(tagButton); this.editor.updateCurrentSuggestedAccounts(tagButton);
this.updateDescription(); this.updateDescription();
}; };
} }
@ -522,7 +717,7 @@ class TagTabPlane extends TabPlane {
* *
* @private * @private
*/ */
class GeneralTagTab extends TagTabPlane { class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane {
/** /**
* The tab ID * The tab ID
@ -540,12 +735,12 @@ class GeneralTagTab extends TagTabPlane {
* @override * @override
*/ */
updateDescription() { updateDescription() {
const pos = this.editor.description.value.indexOf("—"); const pos = this.editor.description.indexOf("—");
const prefix = this.tag.value === ""? "": this.tag.value + "—"; const prefix = this.tag.value === ""? "": `${this.tag.value}`;
if (pos === -1) { if (pos === -1) {
this.editor.description.value = prefix + this.editor.description.value; this.editor.description = `${prefix}${this.editor.description}`;
} else { } else {
this.editor.description.value = prefix + this.editor.description.value.substring(pos + 1); this.editor.description = `${prefix}${this.editor.description.substring(pos + 1)}`;
} }
} }
@ -556,7 +751,7 @@ class GeneralTagTab extends TagTabPlane {
* @override * @override
*/ */
populate() { populate() {
const found = this.editor.description.value.match(/^([^—]+)—/); const found = this.editor.description.match(/^([^—]+)—/);
if (found === null) { if (found === null) {
return false; return false;
} }
@ -583,7 +778,7 @@ class GeneralTagTab extends TagTabPlane {
* *
* @private * @private
*/ */
class GeneralTripTab extends TagTabPlane { class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
/** /**
* The origin * The origin
@ -623,12 +818,12 @@ class GeneralTripTab extends TagTabPlane {
*/ */
constructor(editor) { constructor(editor) {
super(editor); super(editor);
this.#from = document.getElementById(this.prefix + "-from"); this.#from = document.getElementById(`${this.prefix}-from`);
this.#fromError = document.getElementById(this.prefix + "-from-error"); this.#fromError = document.getElementById(`${this.prefix}-from-error`);
this.#to = document.getElementById(this.prefix + "-to"); this.#to = document.getElementById(`${this.prefix}-to`);
this.#toError = document.getElementById(this.prefix + "-to-error") this.#toError = document.getElementById(`${this.prefix}-to-error`)
// noinspection JSValidateTypes // noinspection JSValidateTypes
this.#directionButtons = Array.from(document.getElementsByClassName(this.prefix + "-direction")); this.#directionButtons = Array.from(document.getElementsByClassName(`${this.prefix}-direction`));
this.#from.onchange = () => { this.#from.onchange = () => {
this.#from.value = this.#from.value.trim(); this.#from.value = this.#from.value.trim();
this.updateDescription(); this.updateDescription();
@ -675,7 +870,7 @@ class GeneralTripTab extends TagTabPlane {
break; break;
} }
} }
this.editor.description.value = this.tag.value + "—" + this.#from.value + direction + this.#to.value; this.editor.description = `${this.tag.value}${this.#from.value}${direction}${this.#to.value}`;
} }
/** /**
@ -709,7 +904,7 @@ class GeneralTripTab extends TagTabPlane {
* @override * @override
*/ */
populate() { populate() {
const found = this.editor.description.value.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/); const found = this.editor.description.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
if (found === null) { if (found === null) {
return false; return false;
} }
@ -782,7 +977,7 @@ class GeneralTripTab extends TagTabPlane {
* *
* @private * @private
*/ */
class BusTripTab extends TagTabPlane { class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
/** /**
* The route * The route
@ -828,12 +1023,12 @@ class BusTripTab extends TagTabPlane {
*/ */
constructor(editor) { constructor(editor) {
super(editor); super(editor);
this.#route = document.getElementById(this.prefix + "-route"); this.#route = document.getElementById(`${this.prefix}-route`);
this.#routeError = document.getElementById(this.prefix + "-route-error"); this.#routeError = document.getElementById(`${this.prefix}-route-error`);
this.#from = document.getElementById(this.prefix + "-from"); this.#from = document.getElementById(`${this.prefix}-from`);
this.#fromError = document.getElementById(this.prefix + "-from-error"); this.#fromError = document.getElementById(`${this.prefix}-from-error`);
this.#to = document.getElementById(this.prefix + "-to"); this.#to = document.getElementById(`${this.prefix}-to`);
this.#toError = document.getElementById(this.prefix + "-to-error") this.#toError = document.getElementById(`${this.prefix}-to-error`)
this.#route.onchange = () => { this.#route.onchange = () => {
this.#route.value = this.#route.value.trim(); this.#route.value = this.#route.value.trim();
this.updateDescription(); this.updateDescription();
@ -867,7 +1062,7 @@ class BusTripTab extends TagTabPlane {
* @override * @override
*/ */
updateDescription() { updateDescription() {
this.editor.description.value = this.tag.value + "—" + this.#route.value + "—" + this.#from.value + "→" + this.#to.value; this.editor.description = `${this.tag.value}${this.#route.value}${this.#from.value}${this.#to.value}`;
} }
/** /**
@ -895,7 +1090,7 @@ class BusTripTab extends TagTabPlane {
* @override * @override
*/ */
populate() { populate() {
const found = this.editor.description.value.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/); const found = this.editor.description.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
if (found === null) { if (found === null) {
return false; return false;
} }
@ -970,7 +1165,7 @@ class BusTripTab extends TagTabPlane {
* *
* @private * @private
*/ */
class RecurringTransactionTab extends TabPlane { class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
/** /**
* The month names * The month names
@ -999,14 +1194,14 @@ class RecurringTransactionTab extends TabPlane {
A_("September"), A_("October"), A_("November"), A_("December"), A_("September"), A_("October"), A_("November"), A_("December"),
]; ];
// noinspection JSValidateTypes // noinspection JSValidateTypes
this.#itemButtons = Array.from(document.getElementsByClassName(this.prefix + "-item")); this.#itemButtons = Array.from(document.getElementsByClassName(`${this.prefix}-item`));
for (const itemButton of this.#itemButtons) { for (const itemButton of this.#itemButtons) {
itemButton.onclick = () => { itemButton.onclick = () => {
this.reset(); this.reset();
itemButton.classList.add("btn-primary"); itemButton.classList.add("btn-primary");
itemButton.classList.remove("btn-outline-primary"); itemButton.classList.remove("btn-outline-primary");
this.editor.description.value = this.#getDescription(itemButton); this.editor.description = this.#getDescription(itemButton);
this.editor.filterSuggestedAccounts(itemButton); this.editor.updateCurrentSuggestedAccounts(itemButton);
}; };
} }
} }
@ -1028,8 +1223,8 @@ class RecurringTransactionTab extends TabPlane {
.replaceAll("{this_month_name}", this.#monthNames[thisMonth]) .replaceAll("{this_month_name}", this.#monthNames[thisMonth])
.replaceAll("{last_month_number}", String(lastMonth)) .replaceAll("{last_month_number}", String(lastMonth))
.replaceAll("{last_month_name}", this.#monthNames[lastMonth]) .replaceAll("{last_month_name}", this.#monthNames[lastMonth])
.replaceAll("{last_bimonthly_number}", String(lastBimonthlyFrom) + "" + String(lastBimonthlyTo)) .replaceAll("{last_bimonthly_number}", `${String(lastBimonthlyFrom)}${String(lastBimonthlyTo)}`)
.replaceAll("{last_bimonthly_name}", this.#monthNames[lastBimonthlyFrom] + "" + this.#monthNames[lastBimonthlyTo]); .replaceAll("{last_bimonthly_name}", `${this.#monthNames[lastBimonthlyFrom]}${this.#monthNames[lastBimonthlyTo]}`);
} }
/** /**
@ -1062,7 +1257,7 @@ class RecurringTransactionTab extends TabPlane {
*/ */
populate() { populate() {
for (const itemButton of this.#itemButtons) { for (const itemButton of this.#itemButtons) {
if (this.#getDescription(itemButton) === this.editor.description.value) { if (this.#getDescription(itemButton) === this.editor.description) {
itemButton.classList.add("btn-primary"); itemButton.classList.add("btn-primary");
itemButton.classList.remove("btn-outline-primary"); itemButton.classList.remove("btn-outline-primary");
this.switchToMe(); this.switchToMe();
@ -1080,7 +1275,7 @@ class RecurringTransactionTab extends TabPlane {
super.switchToMe(); super.switchToMe();
for (const itemButton of this.#itemButtons) { for (const itemButton of this.#itemButtons) {
if (itemButton.classList.contains("btn-primary")) { if (itemButton.classList.contains("btn-primary")) {
this.editor.filterSuggestedAccounts(itemButton); this.editor.updateCurrentSuggestedAccounts(itemButton);
return; return;
} }
} }
@ -1103,7 +1298,7 @@ class RecurringTransactionTab extends TabPlane {
* *
* @private * @private
*/ */
class AnnotationTab extends TabPlane { class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane {
/** /**
* Constructs a tab plane. * Constructs a tab plane.
@ -1136,15 +1331,15 @@ class AnnotationTab extends TabPlane {
* @override * @override
*/ */
updateDescription() { updateDescription() {
const found = this.editor.description.value.match(/^(.*?)(?:[*×]\d+)?(?:\([^()]+\))?$/); const found = this.editor.description.match(/^(.*?)(?:[*×]\d+)?(?:\([^()]+\))?$/);
if (found !== null) { if (found !== null) {
this.editor.description.value = found[1]; this.editor.description = found[1];
} }
if (parseInt(this.editor.number.value) > 1) { if (parseInt(this.editor.number.value) > 1) {
this.editor.description.value = this.editor.description.value + "×" + this.editor.number.value; this.editor.description = `${this.editor.description}×${this.editor.number.value}`;
} }
if (this.editor.note.value !== "") { if (this.editor.note.value !== "") {
this.editor.description.value = this.editor.description.value + "(" + this.editor.note.value + ")"; this.editor.description = `${this.editor.description}(${this.editor.note.value})`;
} }
} }
@ -1165,19 +1360,19 @@ class AnnotationTab extends TabPlane {
* @override * @override
*/ */
populate() { populate() {
const found = this.editor.description.value.match(/^(.*?)(?:[*×](\d+))?(?:\(([^()]+)\))?$/); const found = this.editor.description.match(/^(.*?)(?:[*×](\d+))?(?:\(([^()]+)\))?$/);
this.editor.description.value = found[1]; this.editor.description = found[1];
if (found[2] === undefined || parseInt(found[2]) === 1) { if (found[2] === undefined || parseInt(found[2]) === 1) {
this.editor.number.value = ""; this.editor.number.value = "";
} else { } else {
this.editor.number.value = found[2]; this.editor.number.value = found[2];
this.editor.description.value = this.editor.description.value + "×" + this.editor.number.value; this.editor.description = `${this.editor.description}×${this.editor.number.value}`;
} }
if (found[3] === undefined) { if (found[3] === undefined) {
this.editor.note.value = ""; this.editor.note.value = "";
} else { } else {
this.editor.note.value = found[3]; this.editor.note.value = found[3];
this.editor.description.value = this.editor.description.value + "(" + this.editor.note.value + ")"; this.editor.description = `${this.editor.description}(${this.editor.note.value})`;
} }
return true; return true;
} }

View File

@ -91,13 +91,13 @@ class JournalEntryAccountSelector {
constructor(lineItemEditor, debitCredit) { constructor(lineItemEditor, debitCredit) {
this.lineItemEditor = lineItemEditor this.lineItemEditor = lineItemEditor
this.#debitCredit = debitCredit; this.#debitCredit = debitCredit;
const prefix = "accounting-account-selector-" + debitCredit; const prefix = `accounting-account-selector-${debitCredit}`;
this.#query = document.getElementById(prefix + "-query"); this.#query = document.getElementById(`${prefix}-query`);
this.#queryNoResult = document.getElementById(prefix + "-option-no-result"); this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
this.#optionList = document.getElementById(prefix + "-option-list"); this.#optionList = document.getElementById(`${prefix}-option-list`);
this.#options = Array.from(document.getElementsByClassName(prefix + "-option")).map((element) => new JournalEntryAccountOption(this, element)); this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new JournalEntryAccountOption(this, element));
this.#more = document.getElementById(prefix + "-more"); this.#more = document.getElementById(`${prefix}-more`);
this.#clearButton = document.getElementById(prefix + "-btn-clear"); this.#clearButton = document.getElementById(`${prefix}-btn-clear`);
this.#more.onclick = () => { this.#more.onclick = () => {
this.#isShowMore = true; this.#isShowMore = true;
@ -139,8 +139,8 @@ class JournalEntryAccountSelector {
*/ */
#getCodesUsedInForm() { #getCodesUsedInForm() {
const inUse = this.lineItemEditor.form.getAccountCodesUsed(this.#debitCredit); const inUse = this.lineItemEditor.form.getAccountCodesUsed(this.#debitCredit);
if (this.lineItemEditor.accountCode !== null) { if (this.lineItemEditor.account !== null) {
inUse.push(this.lineItemEditor.accountCode); inUse.push(this.lineItemEditor.account.code);
} }
return inUse return inUse
} }
@ -155,9 +155,9 @@ class JournalEntryAccountSelector {
this.#more.classList.remove("d-none"); this.#more.classList.remove("d-none");
this.#filterOptions(); this.#filterOptions();
for (const option of this.#options) { for (const option of this.#options) {
option.setActive(option.code === this.lineItemEditor.accountCode); option.setActive(this.lineItemEditor.account !== null && option.code === this.lineItemEditor.account.code);
} }
if (this.lineItemEditor.accountCode === null) { if (this.lineItemEditor.account === null) {
this.#clearButton.classList.add("btn-secondary"); this.#clearButton.classList.add("btn-secondary");
this.#clearButton.classList.remove("btn-danger"); this.#clearButton.classList.remove("btn-danger");
this.#clearButton.disabled = true; this.#clearButton.disabled = true;
@ -190,12 +190,6 @@ class JournalEntryAccountSelector {
*/ */
class JournalEntryAccountOption { class JournalEntryAccountOption {
/**
* The account selector
* @type {JournalEntryAccountSelector}
*/
#selector;
/** /**
* The element * The element
* @type {HTMLLIElement} * @type {HTMLLIElement}
@ -239,7 +233,6 @@ class JournalEntryAccountOption {
* @param element {HTMLLIElement} the element * @param element {HTMLLIElement} the element
*/ */
constructor(selector, element) { constructor(selector, element) {
this.#selector = selector;
this.#element = element; this.#element = element;
this.code = element.dataset.code; this.code = element.dataset.code;
this.text = element.dataset.text; this.text = element.dataset.text;
@ -247,7 +240,7 @@ class JournalEntryAccountOption {
this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset"); this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset");
this.#queryValues = JSON.parse(element.dataset.queryValues); this.#queryValues = JSON.parse(element.dataset.queryValues);
this.#element.onclick = () => this.#selector.lineItemEditor.saveAccount(this); this.#element.onclick = () => selector.lineItemEditor.saveAccount(this);
} }
/** /**

View File

@ -128,7 +128,7 @@ class JournalEntryForm {
const html = this.#element.dataset.currencyTemplate const html = this.#element.dataset.currencyTemplate
.replaceAll("CURRENCY_INDEX", escapeHtml(String(newIndex))); .replaceAll("CURRENCY_INDEX", escapeHtml(String(newIndex)));
this.#currencyList.insertAdjacentHTML("beforeend", html); this.#currencyList.insertAdjacentHTML("beforeend", html);
const element = document.getElementById("accounting-currency-" + String(newIndex)); const element = document.getElementById(`accounting-currency-${String(newIndex)}`);
this.#currencies.push(new CurrencySubForm(this, element)); this.#currencies.push(new CurrencySubForm(this, element));
this.#resetDeleteCurrencyButtons(); this.#resetDeleteCurrencyButtons();
this.#initializeDragAndDropReordering(); this.#initializeDragAndDropReordering();
@ -207,8 +207,8 @@ class JournalEntryForm {
* @return {string[]} the account codes used in the form * @return {string[]} the account codes used in the form
*/ */
getAccountCodesUsed(debitCredit) { getAccountCodesUsed(debitCredit) {
return this.getLineItems(debitCredit).map((lineItem) => lineItem.accountCode) return this.getLineItems(debitCredit).filter((lineItem) => lineItem.account !== null)
.filter((code) => code !== null); .map((lineItem) => lineItem.account.code);
} }
/** /**
@ -415,16 +415,16 @@ class CurrencySubForm {
this.#element = element; this.#element = element;
this.form = form; this.form = form;
this.index = parseInt(this.#element.dataset.index); this.index = parseInt(this.#element.dataset.index);
const prefix = "accounting-currency-" + String(this.index); const prefix = `accounting-currency-${String(this.index)}`;
this.#control = document.getElementById(prefix + "-control"); this.#control = document.getElementById(`${prefix}-control`);
this.#error = document.getElementById(prefix + "-error"); this.#error = document.getElementById(`${prefix}-error`);
this.#no = document.getElementById(prefix + "-no"); this.#no = document.getElementById(`${prefix}-no`);
this.#code = document.getElementById(prefix + "-code"); this.#code = document.getElementById(`${prefix}-code`);
this.#codeSelect = document.getElementById(prefix + "-code-select"); this.#codeSelect = document.getElementById(`${prefix}-code-select`);
this.#deleteButton = document.getElementById(prefix + "-delete"); this.#deleteButton = document.getElementById(`${prefix}-delete`);
const debitElement = document.getElementById(prefix + "-debit"); const debitElement = document.getElementById(`${prefix}-debit`);
this.#debit = debitElement === null? null: new DebitCreditSubForm(this, debitElement, "debit"); this.#debit = debitElement === null? null: new DebitCreditSubForm(this, debitElement, "debit");
const creditElement = document.getElementById(prefix + "-credit"); const creditElement = document.getElementById(`${prefix}-credit`);
this.#credit = creditElement == null? null: new DebitCreditSubForm(this, creditElement, "credit"); this.#credit = creditElement == null? null: new DebitCreditSubForm(this, creditElement, "credit");
this.#codeSelect.onchange = () => this.#code.value = this.#codeSelect.value; this.#codeSelect.onchange = () => this.#code.value = this.#codeSelect.value;
this.#deleteButton.onclick = () => { this.#deleteButton.onclick = () => {
@ -457,11 +457,7 @@ class CurrencySubForm {
* @param isShown {boolean} true to show, or false otherwise * @param isShown {boolean} true to show, or false otherwise
*/ */
setDeleteButtonShown(isShown) { setDeleteButtonShown(isShown) {
if (isShown) { setElementShown(this.#deleteButton, isShown);
this.#deleteButton.classList.remove("d-none");
} else {
this.#deleteButton.classList.add("d-none");
}
} }
/** /**
@ -617,13 +613,13 @@ class DebitCreditSubForm {
this.#element = element; this.#element = element;
this.#currencyIndex = currency.index; this.#currencyIndex = currency.index;
this.debitCredit = debitCredit; this.debitCredit = debitCredit;
this.#prefix = "accounting-currency-" + String(this.#currencyIndex) + "-" + debitCredit; this.#prefix = `accounting-currency-${String(this.#currencyIndex)}-${debitCredit}`;
this.#content = document.getElementById(this.#prefix + "-content"); this.#content = document.getElementById(`${this.#prefix}-content`);
this.#error = document.getElementById(this.#prefix + "-error"); this.#error = document.getElementById(`${this.#prefix}-error`);
this.#lineItemList = document.getElementById(this.#prefix + "-list"); this.#lineItemList = document.getElementById(`${this.#prefix}-list`);
this.lineItems = Array.from(document.getElementsByClassName(this.#prefix)).map((element) => new LineItemSubForm(this, element)); this.lineItems = Array.from(document.getElementsByClassName(this.#prefix)).map((element) => new LineItemSubForm(this, element));
this.#total = document.getElementById(this.#prefix + "-total"); this.#total = document.getElementById(`${this.#prefix}-total`);
this.#addLineItemButton = document.getElementById(this.#prefix + "-add-line-item"); this.#addLineItemButton = document.getElementById(`${this.#prefix}-add-line-item`);
this.#resetContent(); this.#resetContent();
this.#addLineItemButton.onclick = () => this.currency.form.lineItemEditor.onAddNew(this); this.#addLineItemButton.onclick = () => this.currency.form.lineItemEditor.onAddNew(this);
@ -653,7 +649,7 @@ class DebitCreditSubForm {
.replaceAll("DEBIT_CREDIT", escapeHtml(this.debitCredit)) .replaceAll("DEBIT_CREDIT", escapeHtml(this.debitCredit))
.replaceAll("LINE_ITEM_INDEX", escapeHtml(String(newIndex))); .replaceAll("LINE_ITEM_INDEX", escapeHtml(String(newIndex)));
this.#lineItemList.insertAdjacentHTML("beforeend", html); this.#lineItemList.insertAdjacentHTML("beforeend", html);
const lineItem = new LineItemSubForm(this, document.getElementById(this.#prefix + "-" + String(newIndex))); const lineItem = new LineItemSubForm(this, document.getElementById(`${this.#prefix}-${String(newIndex)}`));
this.lineItems.push(lineItem); this.lineItems.push(lineItem);
this.#resetContent(); this.#resetContent();
this.#resetDeleteLineItemButtons(); this.#resetDeleteLineItemButtons();
@ -700,20 +696,19 @@ class DebitCreditSubForm {
this.#element.classList.remove("accounting-not-empty"); this.#element.classList.remove("accounting-not-empty");
this.#element.classList.add("accounting-clickable"); this.#element.classList.add("accounting-clickable");
this.#element.dataset.bsToggle = "modal" this.#element.dataset.bsToggle = "modal"
this.#element.dataset.bsTarget = "#" + this.currency.form.lineItemEditor.modal.id; this.#element.dataset.bsTarget = `#${this.currency.form.lineItemEditor.modal.id}`;
this.#element.onclick = () => { this.#element.onclick = () => {
this.#element.classList.add("accounting-not-empty"); this.#element.classList.add("accounting-not-empty");
this.currency.form.lineItemEditor.onAddNew(this); this.currency.form.lineItemEditor.onAddNew(this);
}; };
this.#content.classList.add("d-none");
} else { } else {
this.#element.classList.add("accounting-not-empty"); this.#element.classList.add("accounting-not-empty");
this.#element.classList.remove("accounting-clickable"); this.#element.classList.remove("accounting-clickable");
delete this.#element.dataset.bsToggle; delete this.#element.dataset.bsToggle;
delete this.#element.dataset.bsTarget; delete this.#element.dataset.bsTarget;
this.#element.onclick = null; this.#element.onclick = null;
this.#content.classList.remove("d-none");
} }
setElementShown(this.#content, this.lineItems.length !== 0);
} }
/** /**
@ -784,6 +779,53 @@ class DebitCreditSubForm {
} }
} }
/**
* A journal entry account.
*
*/
class JournalEntryAccount {
/**
* The account code
* @type {string}
*/
code;
/**
* The account text
* @type {string}
*/
text;
/**
* Whether the line items in the account needs offset
* @type {boolean}
*/
isNeedOffset;
/**
* Constructs a journal entry account.
*
* @param code {string} the account code
* @param text {string} the account text
* @param isNeedOffset {boolean} true if the line items in the account needs offset, or false otherwise
*/
constructor(code, text, isNeedOffset) {
this.code = code;
this.text = text;
this.isNeedOffset = isNeedOffset;
}
/**
* Returns a copy of the account.
*
* @return {JournalEntryAccount} the copy of the account
*/
copy() {
return new JournalEntryAccount(this.code, this.text, this.isNeedOffset);
}
}
/** /**
* The line item sub-form. * The line item sub-form.
* *
@ -910,20 +952,20 @@ class LineItemSubForm {
this.debitCredit = element.dataset.debitCredit; this.debitCredit = element.dataset.debitCredit;
this.index = parseInt(element.dataset.lineItemIndex); this.index = parseInt(element.dataset.lineItemIndex);
this.isMatched = element.classList.contains("accounting-matched-line-item"); this.isMatched = element.classList.contains("accounting-matched-line-item");
const prefix = "accounting-currency-" + element.dataset.currencyIndex + "-" + this.debitCredit + "-" + String(this.index); const prefix = `accounting-currency-${element.dataset.currencyIndex}-${this.debitCredit}-${String(this.index)}`;
this.#control = document.getElementById(prefix + "-control"); this.#control = document.getElementById(`${prefix}-control`);
this.#error = document.getElementById(prefix + "-error"); this.#error = document.getElementById(`${prefix}-error`);
this.#no = document.getElementById(prefix + "-no"); this.#no = document.getElementById(`${prefix}-no`);
this.#accountCode = document.getElementById(prefix + "-account-code"); this.#accountCode = document.getElementById(`${prefix}-account-code`);
this.#accountText = document.getElementById(prefix + "-account-text"); this.#accountText = document.getElementById(`${prefix}-account-text`);
this.#description = document.getElementById(prefix + "-description"); this.#description = document.getElementById(`${prefix}-description`);
this.#descriptionText = document.getElementById(prefix + "-description-text"); this.#descriptionText = document.getElementById(`${prefix}-description-text`);
this.#originalLineItemId = document.getElementById(prefix + "-original-line-item-id"); this.#originalLineItemId = document.getElementById(`${prefix}-original-line-item-id`);
this.#originalLineItemText = document.getElementById(prefix + "-original-line-item-text"); this.#originalLineItemText = document.getElementById(`${prefix}-original-line-item-text`);
this.#offsets = document.getElementById(prefix + "-offsets"); this.#offsets = document.getElementById(`${prefix}-offsets`);
this.#amount = document.getElementById(prefix + "-amount"); this.#amount = document.getElementById(`${prefix}-amount`);
this.#amountText = document.getElementById(prefix + "-amount-text"); this.#amountText = document.getElementById(`${prefix}-amount-text`);
this.#deleteButton = document.getElementById(prefix + "-delete"); this.#deleteButton = document.getElementById(`${prefix}-delete`);
this.#control.onclick = () => this.debitCreditSubForm.currency.form.lineItemEditor.onEdit(this); this.#control.onclick = () => this.debitCreditSubForm.currency.form.lineItemEditor.onEdit(this);
this.#deleteButton.onclick = () => { this.#deleteButton.onclick = () => {
this.#element.parentElement.removeChild(this.#element); this.#element.parentElement.removeChild(this.#element);
@ -940,15 +982,6 @@ class LineItemSubForm {
this.#no.value = String(siblings.indexOf(this.#element) + 1); 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
*/
get isNeedOffset() {
return "isNeedOffset" in this.#element.dataset;
}
/** /**
* Returns the ID of the original line item. * Returns the ID of the original line item.
* *
@ -986,21 +1019,12 @@ class LineItemSubForm {
} }
/** /**
* Returns the account code. * Returns the account.
* *
* @return {string|null} the account code * @return {JournalEntryAccount|null} the account
*/ */
get accountCode() { get account() {
return this.#accountCode.value === ""? null: this.#accountCode.value; return this.#accountCode.value === null? null: new JournalEntryAccount(this.#accountCode.value, this.#accountCode.dataset.text, this.#accountCode.classList.contains("accounting-account-is-need-offset"));
}
/**
* Returns the account text.
*
* @return {string|null} the account text
*/
get accountText() {
return this.#accountCode.dataset.text === ""? null: this.#accountCode.dataset.text;
} }
/** /**
@ -1027,11 +1051,7 @@ class LineItemSubForm {
* @param isShown {boolean} true to show, or false otherwise * @param isShown {boolean} true to show, or false otherwise
*/ */
setDeleteButtonShown(isShown) { setDeleteButtonShown(isShown) {
if (isShown) { setElementShown(this.#deleteButton, isShown);
this.#deleteButton.classList.remove("d-none");
} else {
this.#deleteButton.classList.add("d-none");
}
} }
/** /**
@ -1061,24 +1081,24 @@ class LineItemSubForm {
* @param editor {JournalEntryLineItemEditor} the line item editor * @param editor {JournalEntryLineItemEditor} the line item editor
*/ */
save(editor) { save(editor) {
if (editor.isNeedOffset) { setElementShown(this.#offsets, editor.account.isNeedOffset);
this.#offsets.classList.remove("d-none");
} else {
this.#offsets.classList.add("d-none");
}
this.#originalLineItemId.value = editor.originalLineItemId === null? "": editor.originalLineItemId; this.#originalLineItemId.value = editor.originalLineItemId === null? "": editor.originalLineItemId;
this.#originalLineItemId.dataset.date = editor.originalLineItemDate === null? "": editor.originalLineItemDate; this.#originalLineItemId.dataset.date = editor.originalLineItemDate === null? "": editor.originalLineItemDate;
this.#originalLineItemId.dataset.text = editor.originalLineItemText === null? "": editor.originalLineItemText; this.#originalLineItemId.dataset.text = editor.originalLineItemText === null? "": editor.originalLineItemText;
setElementShown(this.#originalLineItemText, editor.originalLineItemText !== null);
if (editor.originalLineItemText === null) { if (editor.originalLineItemText === null) {
this.#originalLineItemText.classList.add("d-none");
this.#originalLineItemText.innerText = ""; this.#originalLineItemText.innerText = "";
} else { } else {
this.#originalLineItemText.classList.remove("d-none");
this.#originalLineItemText.innerText = A_("Offset %(item)s", {item: editor.originalLineItemText}); this.#originalLineItemText.innerText = A_("Offset %(item)s", {item: editor.originalLineItemText});
} }
this.#accountCode.value = editor.accountCode === null? "": editor.accountCode; this.#accountCode.value = editor.account.code;
this.#accountCode.dataset.text = editor.accountText === null? "": editor.accountText; this.#accountCode.dataset.text = editor.account.text;
this.#accountText.innerText = editor.accountText === null? "": editor.accountText; if (editor.account.isNeedOffset) {
this.#accountCode.classList.add("accounting-account-is-need-offset");
} else {
this.#accountCode.classList.remove("accounting-account-is-need-offset");
}
this.#accountText.innerText = editor.account.text;
this.#description.value = editor.description === null? "": editor.description; this.#description.value = editor.description === null? "": editor.description;
this.#descriptionText.innerText = editor.description === null? "": editor.description; this.#descriptionText.innerText = editor.description === null? "": editor.description;
this.#amount.value = editor.amount; this.#amount.value = editor.amount;
@ -1119,3 +1139,18 @@ function formatDecimal(number) {
const whole = Number(number.minus(frac)).toLocaleString(); const whole = Number(number.minus(frac)).toLocaleString();
return whole + String(frac).substring(1); return whole + String(frac).substring(1);
} }
/**
* Sets whether an element is shown.
*
* @param element {HTMLElement} the element
* @param isShown {boolean} true to show, or false otherwise
* @private
*/
function setElementShown(element, isShown) {
if (isShown) {
element.classList.remove("d-none");
} else {
element.classList.add("d-none");
}
}

View File

@ -148,12 +148,6 @@ class JournalEntryLineItemEditor {
*/ */
#debitCreditSubForm; #debitCreditSubForm;
/**
* Whether the journal entry line item needs offset
* @type {boolean}
*/
isNeedOffset = false;
/** /**
* The ID of the original line item * The ID of the original line item
* @type {string|null} * @type {string|null}
@ -173,16 +167,16 @@ class JournalEntryLineItemEditor {
originalLineItemText = null; originalLineItemText = null;
/** /**
* The account code * The account
* @type {string|null} * @type {JournalEntryAccount|null}
*/ */
accountCode = null; account = null;
/** /**
* The account text * Whether the user has confirmed the account
* @type {string|null} * @type {boolean}
*/ */
accountText = null; isAccountConfirmed = false;
/** /**
* The description * The description
@ -216,20 +210,20 @@ class JournalEntryLineItemEditor {
constructor(form) { constructor(form) {
this.form = form; this.form = form;
this.#element = document.getElementById(this.#prefix); this.#element = document.getElementById(this.#prefix);
this.modal = document.getElementById(this.#prefix + "-modal"); this.modal = document.getElementById(`${this.#prefix}-modal`);
this.#originalLineItemContainer = document.getElementById(this.#prefix + "-original-line-item-container"); this.#originalLineItemContainer = document.getElementById(`${this.#prefix}-original-line-item-container`);
this.#originalLineItemControl = document.getElementById(this.#prefix + "-original-line-item-control"); this.#originalLineItemControl = document.getElementById(`${this.#prefix}-original-line-item-control`);
this.#originalLineItemText = document.getElementById(this.#prefix + "-original-line-item"); this.#originalLineItemText = document.getElementById(`${this.#prefix}-original-line-item`);
this.#originalLineItemError = document.getElementById(this.#prefix + "-original-line-item-error"); this.#originalLineItemError = document.getElementById(`${this.#prefix}-original-line-item-error`);
this.#originalLineItemDelete = document.getElementById(this.#prefix + "-original-line-item-delete"); this.#originalLineItemDelete = document.getElementById(`${this.#prefix}-original-line-item-delete`);
this.#descriptionControl = document.getElementById(this.#prefix + "-description-control"); this.#descriptionControl = document.getElementById(`${this.#prefix}-description-control`);
this.#descriptionText = document.getElementById(this.#prefix + "-description"); this.#descriptionText = document.getElementById(`${this.#prefix}-description`);
this.#descriptionError = document.getElementById(this.#prefix + "-description-error"); this.#descriptionError = document.getElementById(`${this.#prefix}-description-error`);
this.#accountControl = document.getElementById(this.#prefix + "-account-control"); this.#accountControl = document.getElementById(`${this.#prefix}-account-control`);
this.#accountText = document.getElementById(this.#prefix + "-account"); this.#accountText = document.getElementById(`${this.#prefix}-account`);
this.#accountError = document.getElementById(this.#prefix + "-account-error") this.#accountError = document.getElementById(`${this.#prefix}-account-error`)
this.#amountInput = document.getElementById(this.#prefix + "-amount"); this.#amountInput = document.getElementById(`${this.#prefix}-amount`);
this.#amountError = document.getElementById(this.#prefix + "-amount-error"); this.#amountError = document.getElementById(`${this.#prefix}-amount-error`);
this.#descriptionEditors = DescriptionEditor.getInstances(this); this.#descriptionEditors = DescriptionEditor.getInstances(this);
this.#accountSelectors = JournalEntryAccountSelector.getInstances(this); this.#accountSelectors = JournalEntryAccountSelector.getInstances(this);
this.originalLineItemSelector = new OriginalLineItemSelector(this); this.originalLineItemSelector = new OriginalLineItemSelector(this);
@ -276,7 +270,6 @@ class JournalEntryLineItemEditor {
* @param originalLineItem {OriginalLineItem} the original line item * @param originalLineItem {OriginalLineItem} the original line item
*/ */
saveOriginalLineItem(originalLineItem) { saveOriginalLineItem(originalLineItem) {
this.isNeedOffset = false;
this.#originalLineItemContainer.classList.remove("d-none"); this.#originalLineItemContainer.classList.remove("d-none");
this.#originalLineItemControl.classList.add("accounting-not-empty"); this.#originalLineItemControl.classList.add("accounting-not-empty");
this.originalLineItemId = originalLineItem.id; this.originalLineItemId = originalLineItem.id;
@ -292,9 +285,9 @@ class JournalEntryLineItemEditor {
this.description = originalLineItem.description === ""? null: originalLineItem.description; this.description = originalLineItem.description === ""? null: originalLineItem.description;
this.#descriptionText.innerText = originalLineItem.description; this.#descriptionText.innerText = originalLineItem.description;
this.#accountControl.classList.add("accounting-not-empty"); this.#accountControl.classList.add("accounting-not-empty");
this.accountCode = originalLineItem.accountCode; this.account = originalLineItem.account.copy();
this.accountText = originalLineItem.accountText; this.isAccountConfirmed = false;
this.#accountText.innerText = originalLineItem.accountText; this.#accountText.innerText = this.account.text;
this.#amountInput.value = String(originalLineItem.netBalance); this.#amountInput.value = String(originalLineItem.netBalance);
this.#amountInput.max = String(originalLineItem.netBalance); this.#amountInput.max = String(originalLineItem.netBalance);
this.#amountInput.min = "0"; this.#amountInput.min = "0";
@ -306,7 +299,6 @@ class JournalEntryLineItemEditor {
* *
*/ */
clearOriginalLineItem() { clearOriginalLineItem() {
this.isNeedOffset = false;
this.#originalLineItemContainer.classList.add("d-none"); this.#originalLineItemContainer.classList.add("d-none");
this.#originalLineItemControl.classList.remove("accounting-not-empty"); this.#originalLineItemControl.classList.remove("accounting-not-empty");
this.originalLineItemId = null; this.originalLineItemId = null;
@ -315,8 +307,8 @@ class JournalEntryLineItemEditor {
this.#originalLineItemText.innerText = ""; this.#originalLineItemText.innerText = "";
this.#setEnableDescriptionAccount(true); this.#setEnableDescriptionAccount(true);
this.#accountControl.classList.remove("accounting-not-empty"); this.#accountControl.classList.remove("accounting-not-empty");
this.accountCode = null; this.account = null;
this.accountText = null; this.isAccountConfirmed = false;
this.#accountText.innerText = ""; this.#accountText.innerText = "";
this.#amountInput.max = ""; this.#amountInput.max = "";
} }
@ -324,47 +316,35 @@ class JournalEntryLineItemEditor {
/** /**
* Saves the description from the description editor. * Saves the description from the description editor.
* *
* @param description {string} the description * @param editor {DescriptionEditor} the description editor
*/ */
saveDescription(description) { saveDescription(editor) {
if (description === "") { if (editor.selectedAccount !== null) {
this.#accountControl.classList.add("accounting-not-empty");
this.account = editor.selectedAccount.copy();
this.#accountText.innerText = editor.selectedAccount.text;
this.isAccountConfirmed = editor.isAccountConfirmed;
this.#validateAccount();
}
if (editor.description === "") {
this.#descriptionControl.classList.remove("accounting-not-empty"); this.#descriptionControl.classList.remove("accounting-not-empty");
} else { } else {
this.#descriptionControl.classList.add("accounting-not-empty"); this.#descriptionControl.classList.add("accounting-not-empty");
} }
this.description = description === ""? null: description; this.description = editor.description === ""? null: editor.description;
this.#descriptionText.innerText = description; this.#descriptionText.innerText = editor.description;
this.#validateDescription(); this.#validateDescription();
bootstrap.Modal.getOrCreateInstance(this.modal).show(); bootstrap.Modal.getOrCreateInstance(this.modal).show();
} }
/**
* Saves the description with the suggested account from the description editor.
*
* @param description {string} the description
* @param accountCode {string} the account code
* @param accountText {string} the account text
* @param isAccountNeedOffset {boolean} true if the line items in the account need offset, or false otherwise
*/
saveDescriptionWithAccount(description, accountCode, accountText, isAccountNeedOffset) {
this.isNeedOffset = isAccountNeedOffset;
this.#accountControl.classList.add("accounting-not-empty");
this.accountCode = accountCode;
this.accountText = accountText;
this.#accountText.innerText = accountText;
this.#validateAccount();
this.saveDescription(description)
}
/** /**
* Clears the account. * Clears the account.
* *
*/ */
clearAccount() { clearAccount() {
this.isNeedOffset = false;
this.#accountControl.classList.remove("accounting-not-empty"); this.#accountControl.classList.remove("accounting-not-empty");
this.accountCode = null; this.account = null;
this.accountText = null; this.isAccountConfirmed = false;
this.#accountText.innerText = ""; this.#accountText.innerText = "";
this.#validateAccount(); this.#validateAccount();
} }
@ -375,10 +355,9 @@ class JournalEntryLineItemEditor {
* @param account {JournalEntryAccountOption} the selected account * @param account {JournalEntryAccountOption} the selected account
*/ */
saveAccount(account) { saveAccount(account) {
this.isNeedOffset = account.isNeedOffset;
this.#accountControl.classList.add("accounting-not-empty"); this.#accountControl.classList.add("accounting-not-empty");
this.accountCode = account.code; this.account = new JournalEntryAccount(account.code, account.text, account.isNeedOffset);
this.accountText = account.text; this.isAccountConfirmed = true;
this.#accountText.innerText = account.text; this.#accountText.innerText = account.text;
this.#validateAccount(); this.#validateAccount();
} }
@ -427,7 +406,7 @@ class JournalEntryLineItemEditor {
* @return {boolean} true if valid, or false otherwise * @return {boolean} true if valid, or false otherwise
*/ */
#validateAccount() { #validateAccount() {
if (this.accountCode === null) { if (this.account === null) {
this.#accountControl.classList.add("is-invalid"); this.#accountControl.classList.add("is-invalid");
this.#accountError.innerText = A_("Please select the account."); this.#accountError.innerText = A_("Please select the account.");
return false; return false;
@ -486,7 +465,6 @@ class JournalEntryLineItemEditor {
this.lineItem = null; this.lineItem = null;
this.#debitCreditSubForm = debitCredit; this.#debitCreditSubForm = debitCredit;
this.debitCredit = this.#debitCreditSubForm.debitCredit; this.debitCredit = this.#debitCreditSubForm.debitCredit;
this.isNeedOffset = false;
this.#originalLineItemContainer.classList.add("d-none"); this.#originalLineItemContainer.classList.add("d-none");
this.#originalLineItemControl.classList.remove("accounting-not-empty"); this.#originalLineItemControl.classList.remove("accounting-not-empty");
this.#originalLineItemControl.classList.remove("is-invalid"); this.#originalLineItemControl.classList.remove("is-invalid");
@ -502,8 +480,8 @@ class JournalEntryLineItemEditor {
this.#descriptionError.innerText = "" this.#descriptionError.innerText = ""
this.#accountControl.classList.remove("accounting-not-empty"); this.#accountControl.classList.remove("accounting-not-empty");
this.#accountControl.classList.remove("is-invalid"); this.#accountControl.classList.remove("is-invalid");
this.accountCode = null; this.account = null;
this.accountText = null; this.isAccountConfirmed = false;
this.#accountText.innerText = ""; this.#accountText.innerText = "";
this.#accountError.innerText = ""; this.#accountError.innerText = "";
this.#amountInput.value = ""; this.#amountInput.value = "";
@ -522,7 +500,6 @@ class JournalEntryLineItemEditor {
this.lineItem = lineItem; this.lineItem = lineItem;
this.#debitCreditSubForm = lineItem.debitCreditSubForm; this.#debitCreditSubForm = lineItem.debitCreditSubForm;
this.debitCredit = this.#debitCreditSubForm.debitCredit; this.debitCredit = this.#debitCreditSubForm.debitCredit;
this.isNeedOffset = lineItem.isNeedOffset;
this.originalLineItemId = lineItem.originalLineItemId; this.originalLineItemId = lineItem.originalLineItemId;
this.originalLineItemDate = lineItem.originalLineItemDate; this.originalLineItemDate = lineItem.originalLineItemDate;
this.originalLineItemText = lineItem.originalLineItemText; this.originalLineItemText = lineItem.originalLineItemText;
@ -542,14 +519,14 @@ class JournalEntryLineItemEditor {
this.#descriptionControl.classList.add("accounting-not-empty"); this.#descriptionControl.classList.add("accounting-not-empty");
} }
this.#descriptionText.innerText = this.description === null? "": this.description; this.#descriptionText.innerText = this.description === null? "": this.description;
if (lineItem.accountCode === null) { this.account = lineItem.account;
this.isAccountConfirmed = true;
if (this.account === null) {
this.#accountControl.classList.remove("accounting-not-empty"); this.#accountControl.classList.remove("accounting-not-empty");
} else { } else {
this.#accountControl.classList.add("accounting-not-empty"); this.#accountControl.classList.add("accounting-not-empty");
} }
this.accountCode = lineItem.accountCode; this.#accountText.innerText = this.account.text;
this.accountText = lineItem.accountText;
this.#accountText.innerText = this.accountText;
this.#amountInput.value = lineItem.amount === null? "": String(lineItem.amount); this.#amountInput.value = lineItem.amount === null? "": String(lineItem.amount);
const maxAmount = this.#getMaxAmount(); const maxAmount = this.#getMaxAmount();
this.#amountInput.max = maxAmount === null? "": maxAmount; this.#amountInput.max = maxAmount === null? "": maxAmount;
@ -577,11 +554,11 @@ class JournalEntryLineItemEditor {
#setEnableDescriptionAccount(isEnabled) { #setEnableDescriptionAccount(isEnabled) {
if (isEnabled) { if (isEnabled) {
this.#descriptionControl.dataset.bsToggle = "modal"; this.#descriptionControl.dataset.bsToggle = "modal";
this.#descriptionControl.dataset.bsTarget = "#accounting-description-editor-" + this.#debitCreditSubForm.debitCredit + "-modal"; this.#descriptionControl.dataset.bsTarget = `#accounting-description-editor-${this.#debitCreditSubForm.debitCredit}-modal`;
this.#descriptionControl.classList.remove("accounting-disabled"); this.#descriptionControl.classList.remove("accounting-disabled");
this.#descriptionControl.classList.add("accounting-clickable"); this.#descriptionControl.classList.add("accounting-clickable");
this.#accountControl.dataset.bsToggle = "modal"; this.#accountControl.dataset.bsToggle = "modal";
this.#accountControl.dataset.bsTarget = "#accounting-account-selector-" + this.#debitCreditSubForm.debitCredit + "-modal"; this.#accountControl.dataset.bsTarget = `#accounting-account-selector-${this.#debitCreditSubForm.debitCredit}-modal`;
this.#accountControl.classList.remove("accounting-disabled"); this.#accountControl.classList.remove("accounting-disabled");
this.#accountControl.classList.add("accounting-clickable"); this.#accountControl.classList.add("accounting-clickable");
} else { } else {
@ -596,3 +573,4 @@ class JournalEntryLineItemEditor {
} }
} }
} }

View File

@ -29,7 +29,7 @@ document.addEventListener("DOMContentLoaded", () => {
const onReorder = () => { const onReorder = () => {
const accounts = Array.from(list.children); const accounts = Array.from(list.children);
for (let i = 0; i < accounts.length; i++) { for (let i = 0; i < accounts.length; i++) {
const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no"); const no = document.getElementById(`accounting-order-${accounts[i].dataset.id}-no`);
no.value = String(i + 1); no.value = String(i + 1);
} }
}; };

View File

@ -242,12 +242,12 @@ class RecurringExpenseIncomeSubForm {
this.#form = form; this.#form = form;
this.expenseIncome = expenseIncome; this.expenseIncome = expenseIncome;
this.editor = new RecurringItemEditor(this); this.editor = new RecurringItemEditor(this);
this.#prefix = "accounting-recurring-" + expenseIncome; this.#prefix = `accounting-recurring-${expenseIncome}`;
this.#element = document.getElementById(this.#prefix); this.#element = document.getElementById(this.#prefix);
this.#content = document.getElementById(this.#prefix + "-content"); this.#content = document.getElementById(`${this.#prefix}-content`);
this.#itemList = document.getElementById(this.#prefix + "-list"); this.#itemList = document.getElementById(`${this.#prefix}-list`);
this.#items = Array.from(document.getElementsByClassName(this.#prefix + "-item")).map((element) => new RecurringItemSubForm(this, element)); this.#items = Array.from(document.getElementsByClassName(`${this.#prefix}-item`)).map((element) => new RecurringItemSubForm(this, element));
this.#addButton = document.getElementById(this.#prefix + "-add"); this.#addButton = document.getElementById(`${this.#prefix}-add`);
this.#resetContent(); this.#resetContent();
this.#addButton.onclick = () => this.editor.onAddNew(); this.#addButton.onclick = () => this.editor.onAddNew();
@ -265,7 +265,7 @@ class RecurringExpenseIncomeSubForm {
.replaceAll("EXPENSE_INCOME", escapeHtml(this.expenseIncome)) .replaceAll("EXPENSE_INCOME", escapeHtml(this.expenseIncome))
.replaceAll("ITEM_INDEX", escapeHtml(String(newIndex))); .replaceAll("ITEM_INDEX", escapeHtml(String(newIndex)));
this.#itemList.insertAdjacentHTML("beforeend", html); this.#itemList.insertAdjacentHTML("beforeend", html);
const element = document.getElementById(this.#prefix + "-" + String(newIndex)) const element = document.getElementById(`${this.#prefix}-${String(newIndex)}`)
const item = new RecurringItemSubForm(this, element); const item = new RecurringItemSubForm(this, element);
this.#items.push(item); this.#items.push(item);
this.#resetContent(); this.#resetContent();
@ -294,7 +294,7 @@ class RecurringExpenseIncomeSubForm {
this.#element.classList.remove("accounting-not-empty"); this.#element.classList.remove("accounting-not-empty");
this.#element.classList.add("accounting-clickable"); this.#element.classList.add("accounting-clickable");
this.#element.dataset.bsToggle = "modal" this.#element.dataset.bsToggle = "modal"
this.#element.dataset.bsTarget = "#" + this.editor.modal.id; this.#element.dataset.bsTarget = `#${this.editor.modal.id}`;
this.#element.onclick = () => this.editor.onAddNew(); this.#element.onclick = () => this.editor.onAddNew();
this.#content.classList.add("d-none"); this.#content.classList.add("d-none");
} else { } else {
@ -441,17 +441,17 @@ class RecurringItemSubForm {
this.#expenseIncomeSubForm = expenseIncomeSubForm this.#expenseIncomeSubForm = expenseIncomeSubForm
this.#element = element; this.#element = element;
this.itemIndex = parseInt(element.dataset.itemIndex); this.itemIndex = parseInt(element.dataset.itemIndex);
const prefix = "accounting-recurring-" + expenseIncomeSubForm.expenseIncome + "-" + element.dataset.itemIndex; const prefix = `accounting-recurring-${expenseIncomeSubForm.expenseIncome}-${element.dataset.itemIndex}`;
this.#control = document.getElementById(prefix + "-control"); this.#control = document.getElementById(`${prefix}-control`);
this.#error = document.getElementById(prefix + "-error"); this.#error = document.getElementById(`${prefix}-error`);
this.#no = document.getElementById(prefix + "-no"); this.#no = document.getElementById(`${prefix}-no`);
this.#name = document.getElementById(prefix + "-name"); this.#name = document.getElementById(`${prefix}-name`);
this.#nameText = document.getElementById(prefix + "-name-text"); this.#nameText = document.getElementById(`${prefix}-name-text`);
this.#accountCode = document.getElementById(prefix + "-account-code"); this.#accountCode = document.getElementById(`${prefix}-account-code`);
this.#accountText = document.getElementById(prefix + "-account-text"); this.#accountText = document.getElementById(`${prefix}-account-text`);
this.#descriptionTemplate = document.getElementById(prefix + "-description-template"); this.#descriptionTemplate = document.getElementById(`${prefix}-description-template`);
this.#descriptionTemplateText = document.getElementById(prefix + "-description-template-text"); this.#descriptionTemplateText = document.getElementById(`${prefix}-description-template-text`);
this.deleteButton = document.getElementById(prefix + "-delete"); this.deleteButton = document.getElementById(`${prefix}-delete`);
this.#control.onclick = () => this.#expenseIncomeSubForm.editor.onEdit(this); this.#control.onclick = () => this.#expenseIncomeSubForm.editor.onEdit(this);
this.deleteButton.onclick = () => { this.deleteButton.onclick = () => {
@ -652,16 +652,16 @@ class RecurringItemEditor {
constructor(subForm) { constructor(subForm) {
this.#subForm = subForm; this.#subForm = subForm;
this.expenseIncome = subForm.expenseIncome; this.expenseIncome = subForm.expenseIncome;
const prefix = "accounting-recurring-item-editor-" + subForm.expenseIncome; const prefix = `accounting-recurring-item-editor-${subForm.expenseIncome}`;
this.#form = document.getElementById(prefix); this.#form = document.getElementById(prefix);
this.modal = document.getElementById(prefix + "-modal"); this.modal = document.getElementById(`${prefix}-modal`);
this.#name = document.getElementById(prefix + "-name"); this.#name = document.getElementById(`${prefix}-name`);
this.#nameError = document.getElementById(prefix + "-name-error"); this.#nameError = document.getElementById(`${prefix}-name-error`);
this.#accountControl = document.getElementById(prefix + "-account-control"); this.#accountControl = document.getElementById(`${prefix}-account-control`);
this.#accountContainer = document.getElementById(prefix + "-account"); this.#accountContainer = document.getElementById(`${prefix}-account`);
this.#accountError = document.getElementById(prefix + "-account-error"); this.#accountError = document.getElementById(`${prefix}-account-error`);
this.#descriptionTemplate = document.getElementById(prefix + "-description-template"); this.#descriptionTemplate = document.getElementById(`${prefix}-description-template`);
this.#descriptionTemplateError = document.getElementById(prefix + "-description-template-error"); this.#descriptionTemplateError = document.getElementById(`${prefix}-description-template-error`);
this.#accountSelector = new RecurringAccountSelector(this); this.#accountSelector = new RecurringAccountSelector(this);
this.#name.onchange = () => this.#validateName(); this.#name.onchange = () => this.#validateName();
@ -882,12 +882,12 @@ class RecurringAccountSelector {
constructor(editor) { constructor(editor) {
this.editor = editor; this.editor = editor;
this.#expenseIncome = editor.expenseIncome; this.#expenseIncome = editor.expenseIncome;
const prefix = "accounting-recurring-accounting-selector-" + editor.expenseIncome; const prefix = `accounting-recurring-accounting-selector-${editor.expenseIncome}`;
this.#query = document.getElementById(prefix + "-query"); this.#query = document.getElementById(`${prefix}-query`);
this.#queryNoResult = document.getElementById(prefix + "-option-no-result"); this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
this.#optionList = document.getElementById(prefix + "-option-list"); this.#optionList = document.getElementById(`${prefix}-option-list`);
this.#options = Array.from(document.getElementsByClassName(prefix + "-option")).map((element) => new RecurringAccount(this, element)); this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new RecurringAccount(this, element));
this.#clearButton = document.getElementById(prefix + "-clear"); this.#clearButton = document.getElementById(`${prefix}-clear`);
this.#query.oninput = () => this.#filterOptions(); this.#query.oninput = () => this.#filterOptions();
this.#clearButton.onclick = () => this.editor.clearAccount(); this.#clearButton.onclick = () => this.editor.clearAccount();
@ -944,12 +944,6 @@ class RecurringAccountSelector {
*/ */
class RecurringAccount { class RecurringAccount {
/**
* The account selector for the recurring item editor
* @type {RecurringAccountSelector}
*/
#selector;
/** /**
* The element * The element
* @type {HTMLLIElement} * @type {HTMLLIElement}
@ -981,13 +975,12 @@ class RecurringAccount {
* @param element {HTMLLIElement} the element * @param element {HTMLLIElement} the element
*/ */
constructor(selector, element) { constructor(selector, element) {
this.#selector = selector;
this.#element = element; this.#element = element;
this.code = element.dataset.code; this.code = element.dataset.code;
this.text = element.dataset.text; this.text = element.dataset.text;
this.#queryValues = JSON.parse(element.dataset.queryValues); this.#queryValues = JSON.parse(element.dataset.queryValues);
this.#element.onclick = () => this.#selector.editor.saveAccount(this); this.#element.onclick = () => selector.editor.saveAccount(this);
} }
/** /**

View File

@ -88,10 +88,10 @@ class OriginalLineItemSelector {
*/ */
constructor(lineItemEditor) { constructor(lineItemEditor) {
this.lineItemEditor = lineItemEditor; this.lineItemEditor = lineItemEditor;
this.#query = document.getElementById(this.#prefix + "-query"); this.#query = document.getElementById(`${this.#prefix}-query`);
this.#queryNoResult = document.getElementById(this.#prefix + "-option-no-result"); this.#queryNoResult = document.getElementById(`${this.#prefix}-option-no-result`);
this.#optionList = document.getElementById(this.#prefix + "-option-list"); this.#optionList = document.getElementById(`${this.#prefix}-option-list`);
this.#options = Array.from(document.getElementsByClassName(this.#prefix + "-option")).map((element) => new OriginalLineItem(this, element)); this.#options = Array.from(document.getElementsByClassName(`${this.#prefix}-option`)).map((element) => new OriginalLineItem(this, element));
this.#optionById = {}; this.#optionById = {};
for (const option of this.#options) { for (const option of this.#options) {
this.#optionById[option.id] = option; this.#optionById[option.id] = option;
@ -194,10 +194,10 @@ class OriginalLineItemSelector {
class OriginalLineItem { class OriginalLineItem {
/** /**
* The original line item selector * The journal entry form
* @type {OriginalLineItemSelector} * @type {JournalEntryForm}
*/ */
#selector; #form;
/** /**
* The element * The element
@ -230,16 +230,10 @@ class OriginalLineItem {
#currencyCode; #currencyCode;
/** /**
* The account code * The account
* @type {string} * @type {JournalEntryAccount}
*/ */
accountCode; account;
/**
* The account text
* @type {string}
*/
accountText;
/** /**
* The description * The description
@ -284,21 +278,20 @@ class OriginalLineItem {
* @param element {HTMLLIElement} the element * @param element {HTMLLIElement} the element
*/ */
constructor(selector, element) { constructor(selector, element) {
this.#selector = selector; this.#form = selector.lineItemEditor.form;
this.#element = element; this.#element = element;
this.id = element.dataset.id; this.id = element.dataset.id;
this.date = element.dataset.date; this.date = element.dataset.date;
this.#debitCredit = element.dataset.debitCredit; this.#debitCredit = element.dataset.debitCredit;
this.#currencyCode = element.dataset.currencyCode; this.#currencyCode = element.dataset.currencyCode;
this.accountCode = element.dataset.accountCode; this.account = new JournalEntryAccount(element.dataset.accountCode, element.dataset.accountText, false);
this.accountText = element.dataset.accountText;
this.description = element.dataset.description; this.description = element.dataset.description;
this.bareNetBalance = new Decimal(element.dataset.netBalance); this.bareNetBalance = new Decimal(element.dataset.netBalance);
this.netBalance = this.bareNetBalance; this.netBalance = this.bareNetBalance;
this.netBalanceText = document.getElementById("accounting-original-line-item-selector-option-" + this.id + "-net-balance"); this.netBalanceText = document.getElementById(`accounting-original-line-item-selector-option-${this.id}-net-balance`);
this.text = element.dataset.text; this.text = element.dataset.text;
this.#queryValues = JSON.parse(element.dataset.queryValues); this.#queryValues = JSON.parse(element.dataset.queryValues);
this.#element.onclick = () => this.#selector.lineItemEditor.saveOriginalLineItem(this); this.#element.onclick = () => selector.lineItemEditor.saveOriginalLineItem(this);
} }
/** /**
@ -339,7 +332,7 @@ class OriginalLineItem {
*/ */
isMatched(debitCredit, currencyCode, query = null) { isMatched(debitCredit, currencyCode, query = null) {
return this.netBalance.greaterThan(0) return this.netBalance.greaterThan(0)
&& this.date <= this.#selector.lineItemEditor.form.date && this.date <= this.#form.date
&& this.#isDebitCreditMatched(debitCredit) && this.#isDebitCreditMatched(debitCredit)
&& this.#currencyCode === currencyCode && this.#currencyCode === currencyCode
&& this.#isQueryMatched(query); && this.#isQueryMatched(query);

View File

@ -51,7 +51,7 @@ class PeriodChooser {
*/ */
constructor() { constructor() {
const prefix = "accounting-period-chooser"; const prefix = "accounting-period-chooser";
this.modal = document.getElementById(prefix + "-modal"); this.modal = document.getElementById(`${prefix}-modal`);
for (const cls of [MonthTab, YearTab, DayTab, CustomTab]) { for (const cls of [MonthTab, YearTab, DayTab, CustomTab]) {
const tab = new cls(this); const tab = new cls(this);
this.tabPlanes[tab.tabId()] = tab; this.tabPlanes[tab.tabId()] = tab;
@ -112,9 +112,9 @@ class TabPlane {
*/ */
constructor(chooser) { constructor(chooser) {
this.chooser = chooser; this.chooser = chooser;
this.prefix = "accounting-period-chooser-" + this.tabId(); this.prefix = `accounting-period-chooser-${this.tabId()}`;
this.#tab = document.getElementById(this.prefix + "-tab"); this.#tab = document.getElementById(`${this.prefix}-tab`);
this.#page = document.getElementById(this.prefix + "-page"); this.#page = document.getElementById(`${this.prefix}-page`);
this.#tab.onclick = () => this.#switchToMe(); this.#tab.onclick = () => this.#switchToMe();
} }
@ -164,12 +164,11 @@ class MonthTab extends TabPlane {
*/ */
constructor(chooser) { constructor(chooser) {
super(chooser); super(chooser);
const monthChooser = document.getElementById(this.prefix + "-chooser"); const monthChooser = document.getElementById(`${this.prefix}-chooser`);
if (monthChooser !== null) { if (monthChooser !== null) {
let start = monthChooser.dataset.start;
this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, { this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, {
restrictions: { restrictions: {
minDate: new Date(start), minDate: new Date(monthChooser.dataset.start),
}, },
display: { display: {
inline: true, inline: true,
@ -182,9 +181,8 @@ class MonthTab extends TabPlane {
}); });
monthChooser.addEventListener(tempusDominus.Namespace.events.change, (e) => { monthChooser.addEventListener(tempusDominus.Namespace.events.change, (e) => {
const date = e.detail.date; const date = e.detail.date;
const year = date.year; const zeroPaddedMonth = `0${date.month + 1}`.slice(-2)
const month = date.month + 1; const period = `${date.year}-${zeroPaddedMonth}`;
const period = month < 10? year + "-0" + month: year + "-" + month;
window.location = chooser.modal.dataset.urlTemplate window.location = chooser.modal.dataset.urlTemplate
.replaceAll("PERIOD", period); .replaceAll("PERIOD", period);
}); });
@ -244,8 +242,8 @@ class DayTab extends TabPlane {
*/ */
constructor(chooser) { constructor(chooser) {
super(chooser); super(chooser);
this.#date = document.getElementById(this.prefix + "-date"); this.#date = document.getElementById(`${this.prefix}-date`);
this.#dateError = document.getElementById(this.prefix + "-date-error"); this.#dateError = document.getElementById(`${this.prefix}-date-error`);
if (this.#date !== null) { if (this.#date !== null) {
this.#date.onchange = () => { this.#date.onchange = () => {
if (this.#validateDate()) { if (this.#validateDate()) {
@ -331,11 +329,11 @@ class CustomTab extends TabPlane {
*/ */
constructor(chooser) { constructor(chooser) {
super(chooser); super(chooser);
this.#start = document.getElementById(this.prefix + "-start"); this.#start = document.getElementById(`${this.prefix}-start`);
this.#startError = document.getElementById(this.prefix + "-start-error"); this.#startError = document.getElementById(`${this.prefix}-start-error`);
this.#end = document.getElementById(this.prefix + "-end"); this.#end = document.getElementById(`${this.prefix}-end`);
this.#endError = document.getElementById(this.prefix + "-end-error"); this.#endError = document.getElementById(`${this.prefix}-end-error`);
this.#conform = document.getElementById(this.prefix + "-confirm"); this.#conform = document.getElementById(`${this.prefix}-confirm`);
if (this.#start !== null) { if (this.#start !== null) {
this.#start.onchange = () => { this.#start.onchange = () => {
if (this.#validateStart()) { if (this.#validateStart()) {
@ -353,7 +351,7 @@ class CustomTab extends TabPlane {
isValid = this.#validateEnd() && isValid; isValid = this.#validateEnd() && isValid;
if (isValid) { if (isValid) {
window.location = chooser.modal.dataset.urlTemplate window.location = chooser.modal.dataset.urlTemplate
.replaceAll("PERIOD", this.#start.value + "-" + this.#end.value); .replaceAll("PERIOD", `${this.#start.value}-${this.#end.value}`);
} }
}; };
} }

View File

@ -182,8 +182,9 @@ First written: 2023/2/28
{# The suggested accounts #} {# The suggested accounts #}
<div class="mt-3"> <div class="mt-3">
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-account-confirmed" class="btn btn-primary mb-1 d-none" type="button"></button>
{% for account in description_editor.accounts %} {% for account in description_editor.accounts %}
<button class="btn btn-outline-primary d-none accounting-description-editor-{{ description_editor.debit_credit }}-account {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" type="button" data-code="{{ account.code }}" data-text="{{ account }}"> <button class="btn btn-outline-primary mb-1 d-none accounting-description-editor-{{ description_editor.debit_credit }}-account {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" type="button" data-code="{{ account.code }}" data-text="{{ account }}">
{{ account }} {{ account }}
</button> </button>
{% endfor %} {% endfor %}

View File

@ -20,13 +20,13 @@ Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/25 First written: 2023/2/25
#} #}
{# <ul> For SonarQube not to complain about incorrect HTML #} {# <ul> For SonarQube not to complain about incorrect HTML #}
<li id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}" class="list-group-item list-group-item-action d-flex justify-content-between accounting-currency-{{ currency_index }}-{{ debit_credit }} {% if form.offsets %} accounting-matched-line-item {% endif %}" data-currency-index="{{ currency_index }}" data-debit-credit="{{ debit_credit }}" data-line-item-index="{{ line_item_index }}" {% if form.is_need_offset %} data-is-need-offset="true" {% endif %}> <li id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}" class="list-group-item list-group-item-action d-flex justify-content-between accounting-currency-{{ currency_index }}-{{ debit_credit }} {% if form.offsets %} accounting-matched-line-item {% endif %}" data-currency-index="{{ currency_index }}" data-debit-credit="{{ debit_credit }}" data-line-item-index="{{ line_item_index }}">
{% if form.id.data %} {% if form.id.data %}
<input type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-id" value="{{ form.id.data }}"> <input type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-id" value="{{ form.id.data }}">
{% endif %} {% endif %}
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-no" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-no" value="{{ line_item_index }}"> <input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-no" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-no" value="{{ line_item_index }}">
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original-line-item-id" class="accounting-original-line-item-id" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original_line_item_id" value="{{ form.original_line_item_id.data|accounting_default }}" data-date="{{ form.original_line_item_date|accounting_default }}" data-text="{{ form.original_line_item_text|accounting_default }}"> <input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original-line-item-id" class="accounting-original-line-item-id" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original_line_item_id" value="{{ form.original_line_item_id.data|accounting_default }}" data-date="{{ form.original_line_item_date|accounting_default }}" data-text="{{ form.original_line_item_text|accounting_default }}">
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-code" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account_code" value="{{ form.account_code.data|accounting_default }}" data-text="{{ form.account_text }}"> <input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-code" class="{% if form.is_need_offset %} accounting-account-is-need-offset {% endif %}" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account_code" value="{{ form.account_code.data|accounting_default }}" data-text="{{ form.account_text }}">
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" value="{{ form.description.data|accounting_default }}"> <input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" value="{{ form.description.data|accounting_default }}">
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" value="{{ form.amount.data|accounting_journal_entry_format_amount_input }}" data-min="{{ form.offset_total|accounting_default("0") }}"> <input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" value="{{ form.amount.data|accounting_journal_entry_format_amount_input }}" data-min="{{ form.offset_total|accounting_default("0") }}">
<div class="accounting-line-item-content"> <div class="accounting-line-item-content">

View File

@ -21,7 +21,7 @@ This module should not import any other module from the application.
""" """
import typing as t import typing as t
from flask import abort, Blueprint from flask import abort, Blueprint, Response
from accounting.utils.user import get_current_user, UserUtilityInterface from accounting.utils.user import get_current_user, UserUtilityInterface
@ -49,6 +49,10 @@ def has_permission(rule: t.Callable[[], bool]) -> t.Callable:
:raise Forbidden: When the user is denied. :raise Forbidden: When the user is denied.
""" """
if not rule(): if not rule():
if get_current_user() is None:
response: Response | None = _unauthorized_func()
if response is not None:
return response
abort(403) abort(403)
return view(*args, **kwargs) return view(*args, **kwargs)
@ -66,6 +70,9 @@ data."""
__can_admin_func: t.Callable[[], bool] = lambda: True __can_admin_func: t.Callable[[], bool] = lambda: True
"""The callback that returns whether the current user can administrate the """The callback that returns whether the current user can administrate the
accounting settings.""" accounting settings."""
_unauthorized_func: t.Callable[[], Response | None] \
= lambda: Response(status=403)
"""The callback that returns the response to require the user to log in."""
def can_view() -> bool: def can_view() -> bool:
@ -111,10 +118,12 @@ def init_app(bp: Blueprint, user_utils: UserUtilityInterface) -> None:
:param user_utils: The user utilities. :param user_utils: The user utilities.
:return: None. :return: None.
""" """
global __can_view_func, __can_edit_func, __can_admin_func global __can_view_func, __can_edit_func, __can_admin_func, \
_unauthorized_func
__can_view_func = user_utils.can_view __can_view_func = user_utils.can_view
__can_edit_func = user_utils.can_edit __can_edit_func = user_utils.can_edit
__can_admin_func = user_utils.can_admin __can_admin_func = user_utils.can_admin
_unauthorized_func = user_utils.unauthorized
bp.add_app_template_global(user_utils.can_view, "accounting_can_view") bp.add_app_template_global(user_utils.can_view, "accounting_can_view")
bp.add_app_template_global(user_utils.can_edit, "accounting_can_edit") bp.add_app_template_global(user_utils.can_edit, "accounting_can_edit")
bp.add_app_template_global(user_utils.can_admin, "accounting_can_admin") bp.add_app_template_global(user_utils.can_admin, "accounting_can_admin")

View File

@ -23,7 +23,7 @@ import typing as t
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import sqlalchemy as sa import sqlalchemy as sa
from flask import g from flask import g, Response
from flask_sqlalchemy.model import Model from flask_sqlalchemy.model import Model
T = t.TypeVar("T", bound=Model) T = t.TypeVar("T", bound=Model)
@ -59,6 +59,17 @@ class UserUtilityInterface(t.Generic[T], ABC):
accounting settings, or False otherwise. accounting settings, or False otherwise.
""" """
@abstractmethod
def unauthorized(self) -> Response | None:
"""Returns the response to require the user to log in.
This may be a redirection to the login page, or an HTTP 401
Unauthorized response for HTTP Authentication. If this returns None,
an HTTP 403 Forbidden response is return to the user.
:return: The response to require the user to log in.
"""
@property @property
@abstractmethod @abstractmethod
def cls(self) -> t.Type[T]: def cls(self) -> t.Type[T]:

View File

@ -22,7 +22,7 @@ import typing as t
from secrets import token_urlsafe from secrets import token_urlsafe
import click import click
from flask import Flask, Blueprint, render_template from flask import Flask, Blueprint, render_template, redirect, Response
from flask.cli import with_appcontext from flask.cli import with_appcontext
from flask_babel_js import BabelJS from flask_babel_js import BabelJS
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
@ -82,6 +82,9 @@ def create_app(is_testing: bool = False) -> Flask:
return auth.current_user() is not None \ return auth.current_user() is not None \
and auth.current_user().username == "admin" and auth.current_user().username == "admin"
def unauthorized(self) -> Response:
return redirect("/login")
@property @property
def cls(self) -> t.Type[auth.User]: def cls(self) -> t.Type[auth.User]:
return auth.User return auth.User