Compare commits
35 Commits
d5a9e1af18
...
v0.10.0
Author | SHA1 | Date | |
---|---|---|---|
a17395b43e | |||
17c8d9d1a9 | |||
fa94cd407e | |||
9a704c8185 | |||
8286c0c6d8 | |||
f7efacad75 | |||
9263ae0274 | |||
78a9d7794c | |||
f3ae37a409 | |||
ddc1081252 | |||
202d51a032 | |||
562bc47be7 | |||
f3d43a66cc | |||
c3fc6d9a87 | |||
e1a0380628 | |||
f2a2fcdd32 | |||
ab29166f1e | |||
8033921181 | |||
08732c1e66 | |||
4adc464d3d | |||
2f9d2e36cb | |||
5bb10bf6ba | |||
06e7b6ddff | |||
20e1982984 | |||
a70720be50 | |||
cb6de08152 | |||
211821b4d7 | |||
0faca49540 | |||
14e79df571 | |||
04fbb725d2 | |||
a1d6844e52 | |||
94391b02a6 | |||
1cb8a7563e | |||
63f0f28948 | |||
3431922f12 |
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,7 +32,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
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);
|
||||||
|
@ -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 = {};
|
||||||
|
|
||||||
@ -118,20 +142,19 @@ class DescriptionEditor {
|
|||||||
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
|
||||||
@ -364,7 +559,7 @@ class TabPlane {
|
|||||||
* @abstract
|
* @abstract
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class TagTabPlane extends TabPlane {
|
class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tag input
|
* The tag input
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -1005,8 +1200,8 @@ class RecurringTransactionTab extends TabPlane {
|
|||||||
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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -705,15 +701,14 @@ class DebitCreditSubForm {
|
|||||||
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.
|
||||||
*
|
*
|
||||||
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
@ -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;
|
||||||
@ -596,3 +573,4 @@ class JournalEntryLineItemEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
|
@ -181,7 +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 period = `${date.year}-${`0${date.month + 1}`.slice(-2)}`;
|
const zeroPaddedMonth = `0${date.month + 1}`.slice(-2)
|
||||||
|
const period = `${date.year}-${zeroPaddedMonth}`;
|
||||||
window.location = chooser.modal.dataset.urlTemplate
|
window.location = chooser.modal.dataset.urlTemplate
|
||||||
.replaceAll("PERIOD", period);
|
.replaceAll("PERIOD", period);
|
||||||
});
|
});
|
||||||
|
@ -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 %}
|
||||||
|
@ -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">
|
||||||
|
@ -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")
|
||||||
|
@ -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]:
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user