Compare commits
21 Commits
7d084e570e
...
7c512b1c15
Author | SHA1 | Date | |
---|---|---|---|
7c512b1c15 | |||
dc432da398 | |||
c8504bcbf5 | |||
c865141583 | |||
8c1ecd6eac | |||
e8e4100677 | |||
6a8773c531 | |||
30e0c7682c | |||
eb5a7bef7e | |||
8a174d8847 | |||
7459afd63a | |||
a9afc385e9 | |||
a8be739ec7 | |||
0130bc58a9 | |||
821059fa80 | |||
5b4f57d0b3 | |||
4bfac2d545 | |||
f105f0cf7b | |||
5e320729d7 | |||
7515032082 | |||
361b18e411 |
@ -760,7 +760,7 @@ class JournalEntryLineItem(db.Model):
|
||||
setattr(self, "__net_balance", net_balance)
|
||||
|
||||
@property
|
||||
def query_values(self) -> tuple[list[str], list[str]]:
|
||||
def query_values(self) -> list[str]:
|
||||
"""Returns the values to be queried.
|
||||
|
||||
:return: The values to be queried.
|
||||
@ -772,17 +772,16 @@ class JournalEntryLineItem(db.Model):
|
||||
|
||||
journal_entry_day: date = self.journal_entry.date
|
||||
description: str = "" if self.description is None else self.description
|
||||
return ([description],
|
||||
[str(journal_entry_day.year),
|
||||
"{}/{}".format(journal_entry_day.year,
|
||||
journal_entry_day.month),
|
||||
"{}/{}".format(journal_entry_day.month,
|
||||
journal_entry_day.day),
|
||||
"{}/{}/{}".format(journal_entry_day.year,
|
||||
journal_entry_day.month,
|
||||
journal_entry_day.day),
|
||||
format_amount(self.amount),
|
||||
format_amount(self.net_balance)])
|
||||
return [description,
|
||||
str(journal_entry_day.year),
|
||||
"{}/{}".format(journal_entry_day.year,
|
||||
journal_entry_day.month),
|
||||
"{}/{}".format(journal_entry_day.month,
|
||||
journal_entry_day.day),
|
||||
"{}/{}/{}".format(journal_entry_day.year,
|
||||
journal_entry_day.month,
|
||||
journal_entry_day.day),
|
||||
format_amount(self.amount)]
|
||||
|
||||
|
||||
class Option(db.Model):
|
||||
|
@ -114,10 +114,19 @@ class AccountForm {
|
||||
};
|
||||
this.#baseControl.onclick = () => {
|
||||
this.#baseControl.classList.add("accounting-not-empty");
|
||||
this.#baseAccountSelector.onOpen(this.#baseCode.value);
|
||||
this.#baseAccountSelector.onOpen();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base code.
|
||||
*
|
||||
* @return {string|null}
|
||||
*/
|
||||
get baseCode() {
|
||||
return this.#baseCode.value === ""? null: this.#baseCode.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback when the base account selector is closed.
|
||||
*
|
||||
@ -129,15 +138,14 @@ class AccountForm {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base account.
|
||||
* Saves the selected base account.
|
||||
*
|
||||
* @param code {string} the base account code
|
||||
* @param text {string} the text for the base account
|
||||
* @param account {BaseAccountOption} the selected base account
|
||||
*/
|
||||
setBaseAccount(code, text) {
|
||||
this.#baseCode.value = code;
|
||||
this.#base.innerText = text;
|
||||
if (["1", "2", "3"].includes(code.substring(0, 1))) {
|
||||
saveBaseAccount(account) {
|
||||
this.#baseCode.value = account.code;
|
||||
this.#base.innerText = account.text;
|
||||
if (["1", "2", "3"].includes(account.code.substring(0, 1))) {
|
||||
this.#isNeedOffsetControl.classList.remove("d-none");
|
||||
this.#isNeedOffset.disabled = false;
|
||||
} else {
|
||||
@ -225,7 +233,7 @@ class BaseAccountSelector {
|
||||
* The account form
|
||||
* @type {AccountForm}
|
||||
*/
|
||||
#form;
|
||||
form;
|
||||
|
||||
/**
|
||||
* The selector modal
|
||||
@ -253,7 +261,7 @@ class BaseAccountSelector {
|
||||
|
||||
/**
|
||||
* The options
|
||||
* @type {HTMLLIElement[]}
|
||||
* @type {BaseAccountOption[]}
|
||||
*/
|
||||
#options;
|
||||
|
||||
@ -269,83 +277,54 @@ class BaseAccountSelector {
|
||||
* @param form {AccountForm} the form
|
||||
*/
|
||||
constructor(form) {
|
||||
this.#form = form;
|
||||
this.#modal = document.getElementById("accounting-base-selector-modal");
|
||||
this.#query = document.getElementById("accounting-base-selector-query");
|
||||
this.#optionList = document.getElementById("accounting-base-selector-option-list");
|
||||
// noinspection JSValidateTypes
|
||||
this.#options = Array.from(document.getElementsByClassName("accounting-base-selector-option"));
|
||||
this.#clearButton = document.getElementById("accounting-base-selector-clear");
|
||||
this.#queryNoResult = document.getElementById("accounting-base-selector-option-no-result");
|
||||
this.#modal.addEventListener("hidden.bs.modal", () => {
|
||||
this.#form.onBaseAccountSelectorClosed();
|
||||
});
|
||||
for (const option of this.#options) {
|
||||
option.onclick = () => {
|
||||
this.#form.setBaseAccount(option.dataset.code, option.dataset.text);
|
||||
};
|
||||
}
|
||||
this.#clearButton.onclick = () => {
|
||||
this.#form.clearBaseAccount();
|
||||
};
|
||||
this.#initializeBaseAccountQuery();
|
||||
this.form = form;
|
||||
const prefix = "accounting-base-selector";
|
||||
this.#modal = document.getElementById(`${prefix}-modal`);
|
||||
this.#query = document.getElementById(`${prefix}-query`);
|
||||
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
||||
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
||||
this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new BaseAccountOption(this, element));
|
||||
this.#clearButton = document.getElementById(`${prefix}-clear`);
|
||||
|
||||
this.#modal.addEventListener("hidden.bs.modal", () => this.form.onBaseAccountSelectorClosed());
|
||||
this.#query.oninput = () => this.#filterOptions();
|
||||
this.#clearButton.onclick = () => this.form.clearBaseAccount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the query.
|
||||
* Filters the options.
|
||||
*
|
||||
*/
|
||||
#initializeBaseAccountQuery() {
|
||||
this.#query.addEventListener("input", () => {
|
||||
if (this.#query.value === "") {
|
||||
for (const option of this.#options) {
|
||||
option.classList.remove("d-none");
|
||||
}
|
||||
this.#optionList.classList.remove("d-none");
|
||||
this.#queryNoResult.classList.add("d-none");
|
||||
return
|
||||
}
|
||||
let hasAnyMatched = false;
|
||||
for (const option of this.#options) {
|
||||
const queryValues = JSON.parse(option.dataset.queryValues);
|
||||
let isMatched = false;
|
||||
for (const queryValue of queryValues) {
|
||||
if (queryValue.toLowerCase().includes(this.#query.value.toLowerCase())) {
|
||||
isMatched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isMatched) {
|
||||
option.classList.remove("d-none");
|
||||
hasAnyMatched = true;
|
||||
} else {
|
||||
option.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
if (!hasAnyMatched) {
|
||||
this.#optionList.classList.add("d-none");
|
||||
this.#queryNoResult.classList.remove("d-none");
|
||||
#filterOptions() {
|
||||
let isAnyMatched = false;
|
||||
for (const option of this.#options) {
|
||||
if (option.isMatched(this.#query.value)) {
|
||||
option.setShown(true);
|
||||
isAnyMatched = true;
|
||||
} else {
|
||||
this.#optionList.classList.remove("d-none");
|
||||
this.#queryNoResult.classList.add("d-none");
|
||||
option.setShown(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!isAnyMatched) {
|
||||
this.#optionList.classList.add("d-none");
|
||||
this.#queryNoResult.classList.remove("d-none");
|
||||
} else {
|
||||
this.#optionList.classList.remove("d-none");
|
||||
this.#queryNoResult.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback when the base account selector is shown.
|
||||
*
|
||||
* @param baseCode {string} the active base code
|
||||
*/
|
||||
onOpen(baseCode) {
|
||||
onOpen() {
|
||||
this.#query.value = "";
|
||||
this.#filterOptions();
|
||||
for (const option of this.#options) {
|
||||
if (option.dataset.code === baseCode) {
|
||||
option.classList.add("active");
|
||||
} else {
|
||||
option.classList.remove("active");
|
||||
}
|
||||
option.setActive(option.code === this.form.baseCode);
|
||||
}
|
||||
if (baseCode === "") {
|
||||
if (this.form.baseCode === null) {
|
||||
this.#clearButton.classList.add("btn-secondary")
|
||||
this.#clearButton.classList.remove("btn-danger");
|
||||
this.#clearButton.disabled = true;
|
||||
@ -356,3 +335,100 @@ class BaseAccountSelector {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A base account option.
|
||||
*
|
||||
*/
|
||||
class BaseAccountOption {
|
||||
|
||||
/**
|
||||
* The base account selector
|
||||
* @type {BaseAccountSelector}
|
||||
*/
|
||||
#selector;
|
||||
|
||||
/**
|
||||
* The element
|
||||
* @type {HTMLLIElement}
|
||||
*/
|
||||
#element;
|
||||
|
||||
/**
|
||||
* The account code
|
||||
* @type {string}
|
||||
*/
|
||||
code;
|
||||
|
||||
/**
|
||||
* The account text
|
||||
* @type {string}
|
||||
*/
|
||||
text;
|
||||
|
||||
/**
|
||||
* The values to query against
|
||||
* @type {string[]}
|
||||
*/
|
||||
#queryValues;
|
||||
|
||||
/**
|
||||
* Constructs the account in the base account selector.
|
||||
*
|
||||
* @param selector {BaseAccountSelector} the base account selector
|
||||
* @param element {HTMLLIElement} the element
|
||||
*/
|
||||
constructor(selector, element) {
|
||||
this.#selector = selector;
|
||||
this.#element = element;
|
||||
this.code = element.dataset.code;
|
||||
this.text = element.dataset.text;
|
||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||
|
||||
this.#element.onclick = () => this.#selector.form.saveBaseAccount(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the account matches the query.
|
||||
*
|
||||
* @param query {string} the query term
|
||||
* @return {boolean} true if the option matches, or false otherwise
|
||||
*/
|
||||
isMatched(query) {
|
||||
if (query === "") {
|
||||
return true;
|
||||
}
|
||||
for (const queryValue of this.#queryValues) {
|
||||
if (queryValue.toLowerCase().includes(query.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the option is shown.
|
||||
*
|
||||
* @param isShown {boolean} true to show, or false otherwise
|
||||
*/
|
||||
setShown(isShown) {
|
||||
if (isShown) {
|
||||
this.#element.classList.remove("d-none");
|
||||
} else {
|
||||
this.#element.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the option is active.
|
||||
*
|
||||
* @param isActive {boolean} true if active, or false otherwise
|
||||
*/
|
||||
setActive(isActive) {
|
||||
if (isActive) {
|
||||
this.#element.classList.add("active");
|
||||
} else {
|
||||
this.#element.classList.remove("active");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,218 +0,0 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
* account-selector.js: The JavaScript for the account selector
|
||||
*/
|
||||
|
||||
/* Copyright (c) 2023 imacat.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
* First written: 2023/2/28
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The account selector.
|
||||
*
|
||||
*/
|
||||
class AccountSelector {
|
||||
|
||||
/**
|
||||
* The line item editor
|
||||
* @type {JournalEntryLineItemEditor}
|
||||
*/
|
||||
#lineItemEditor;
|
||||
|
||||
/**
|
||||
* Either "debit" or "credit"
|
||||
* @type {string}
|
||||
*/
|
||||
#debitCredit;
|
||||
|
||||
/**
|
||||
* The button to clear the account
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
#clearButton
|
||||
|
||||
/**
|
||||
* The query input
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
#query;
|
||||
|
||||
/**
|
||||
* The error message when the query has no result
|
||||
* @type {HTMLParagraphElement}
|
||||
*/
|
||||
#queryNoResult;
|
||||
|
||||
/**
|
||||
* The option list
|
||||
* @type {HTMLUListElement}
|
||||
*/
|
||||
#optionList;
|
||||
|
||||
/**
|
||||
* The options
|
||||
* @type {HTMLLIElement[]}
|
||||
*/
|
||||
#options;
|
||||
|
||||
/**
|
||||
* The more item to show all accounts
|
||||
* @type {HTMLLIElement}
|
||||
*/
|
||||
#more;
|
||||
|
||||
/**
|
||||
* Constructs an account selector.
|
||||
*
|
||||
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
|
||||
* @param debitCredit {string} either "debit" or "credit"
|
||||
*/
|
||||
constructor(lineItemEditor, debitCredit) {
|
||||
this.#lineItemEditor = lineItemEditor
|
||||
this.#debitCredit = debitCredit;
|
||||
const prefix = "accounting-account-selector-" + debitCredit;
|
||||
this.#query = document.getElementById(prefix + "-query");
|
||||
this.#queryNoResult = document.getElementById(prefix + "-option-no-result");
|
||||
this.#optionList = document.getElementById(prefix + "-option-list");
|
||||
// noinspection JSValidateTypes
|
||||
this.#options = Array.from(document.getElementsByClassName(prefix + "-option"));
|
||||
this.#more = document.getElementById(prefix + "-more");
|
||||
this.#clearButton = document.getElementById(prefix + "-btn-clear");
|
||||
this.#more.onclick = () => {
|
||||
this.#more.classList.add("d-none");
|
||||
this.#filterOptions();
|
||||
};
|
||||
this.#clearButton.onclick = () => this.#lineItemEditor.clearAccount();
|
||||
for (const option of this.#options) {
|
||||
option.onclick = () => this.#lineItemEditor.saveAccount(option.dataset.code, option.dataset.text, option.classList.contains("accounting-account-is-need-offset"));
|
||||
}
|
||||
this.#query.addEventListener("input", () => {
|
||||
this.#filterOptions();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the options.
|
||||
*
|
||||
*/
|
||||
#filterOptions() {
|
||||
const codesInUse = this.#getCodesUsedInForm();
|
||||
let shouldAnyShow = false;
|
||||
for (const option of this.#options) {
|
||||
const shouldShow = this.#shouldOptionShow(option, this.#more, codesInUse, this.#query);
|
||||
if (shouldShow) {
|
||||
option.classList.remove("d-none");
|
||||
shouldAnyShow = true;
|
||||
} else {
|
||||
option.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
if (!shouldAnyShow && this.#more.classList.contains("d-none")) {
|
||||
this.#optionList.classList.add("d-none");
|
||||
this.#queryNoResult.classList.remove("d-none");
|
||||
} else {
|
||||
this.#optionList.classList.remove("d-none");
|
||||
this.#queryNoResult.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account codes that are used in the form.
|
||||
*
|
||||
* @return {string[]} the account codes that are used in the form
|
||||
*/
|
||||
#getCodesUsedInForm() {
|
||||
const inUse = this.#lineItemEditor.form.getAccountCodesUsed(this.#debitCredit);
|
||||
if (this.#lineItemEditor.accountCode !== null) {
|
||||
inUse.push(this.#lineItemEditor.accountCode);
|
||||
}
|
||||
return inUse
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether an option should show.
|
||||
*
|
||||
* @param option {HTMLLIElement} the option
|
||||
* @param more {HTMLLIElement} the more element
|
||||
* @param inUse {string[]} the account codes that are used in the form
|
||||
* @param query {HTMLInputElement} the query element, if any
|
||||
* @return {boolean} true if the option should show, or false otherwise
|
||||
*/
|
||||
#shouldOptionShow(option, more, inUse, query) {
|
||||
const isQueryMatched = () => {
|
||||
if (query.value === "") {
|
||||
return true;
|
||||
}
|
||||
const queryValues = JSON.parse(option.dataset.queryValues);
|
||||
for (const queryValue of queryValues) {
|
||||
if (queryValue.toLowerCase().includes(query.value.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const isMoreMatched = () => {
|
||||
if (more.classList.contains("d-none")) {
|
||||
return true;
|
||||
}
|
||||
return option.classList.contains("accounting-account-in-use") || inUse.includes(option.dataset.code);
|
||||
};
|
||||
return isMoreMatched() && isQueryMatched();
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback when the account selector is shown.
|
||||
*
|
||||
*/
|
||||
onOpen() {
|
||||
this.#query.value = "";
|
||||
this.#more.classList.remove("d-none");
|
||||
this.#filterOptions();
|
||||
for (const option of this.#options) {
|
||||
if (option.dataset.code === this.#lineItemEditor.accountCode) {
|
||||
option.classList.add("active");
|
||||
} else {
|
||||
option.classList.remove("active");
|
||||
}
|
||||
}
|
||||
if (this.#lineItemEditor.accountCode === null) {
|
||||
this.#clearButton.classList.add("btn-secondary");
|
||||
this.#clearButton.classList.remove("btn-danger");
|
||||
this.#clearButton.disabled = true;
|
||||
} else {
|
||||
this.#clearButton.classList.add("btn-danger");
|
||||
this.#clearButton.classList.remove("btn-secondary");
|
||||
this.#clearButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account selector instances.
|
||||
*
|
||||
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
|
||||
* @return {{debit: AccountSelector, credit: AccountSelector}}
|
||||
*/
|
||||
static getInstances(lineItemEditor) {
|
||||
const selectors = {}
|
||||
const modals = Array.from(document.getElementsByClassName("accounting-account-selector"));
|
||||
for (const modal of modals) {
|
||||
selectors[modal.dataset.debitCredit] = new AccountSelector(lineItemEditor, modal.dataset.debitCredit);
|
||||
}
|
||||
return selectors;
|
||||
}
|
||||
}
|
@ -984,7 +984,6 @@ class RecurringTransactionTab extends TabPlane {
|
||||
*/
|
||||
#itemButtons;
|
||||
|
||||
// noinspection JSValidateTypes
|
||||
/**
|
||||
* Constructs a tab plane.
|
||||
*
|
||||
|
319
src/accounting/static/js/journal-entry-account-selector.js
Normal file
319
src/accounting/static/js/journal-entry-account-selector.js
Normal file
@ -0,0 +1,319 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
* journal-entry-account-selector.js: The JavaScript for the account selector of the journal entry form
|
||||
*/
|
||||
|
||||
/* Copyright (c) 2023 imacat.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
* First written: 2023/2/28
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The account selector.
|
||||
*
|
||||
*/
|
||||
class JournalEntryAccountSelector {
|
||||
|
||||
/**
|
||||
* The line item editor
|
||||
* @type {JournalEntryLineItemEditor}
|
||||
*/
|
||||
lineItemEditor;
|
||||
|
||||
/**
|
||||
* Either "debit" or "credit"
|
||||
* @type {string}
|
||||
*/
|
||||
#debitCredit;
|
||||
|
||||
/**
|
||||
* The button to clear the account
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
#clearButton
|
||||
|
||||
/**
|
||||
* The query input
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
#query;
|
||||
|
||||
/**
|
||||
* The error message when the query has no result
|
||||
* @type {HTMLParagraphElement}
|
||||
*/
|
||||
#queryNoResult;
|
||||
|
||||
/**
|
||||
* The option list
|
||||
* @type {HTMLUListElement}
|
||||
*/
|
||||
#optionList;
|
||||
|
||||
/**
|
||||
* The options
|
||||
* @type {JournalEntryAccountOption[]}
|
||||
*/
|
||||
#options;
|
||||
|
||||
/**
|
||||
* The more item to show all accounts
|
||||
* @type {HTMLLIElement}
|
||||
*/
|
||||
#more;
|
||||
|
||||
/**
|
||||
* Whether to show all accounts
|
||||
* @type {boolean}
|
||||
*/
|
||||
#isShowMore = false;
|
||||
|
||||
/**
|
||||
* Constructs an account selector.
|
||||
*
|
||||
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
|
||||
* @param debitCredit {string} either "debit" or "credit"
|
||||
*/
|
||||
constructor(lineItemEditor, debitCredit) {
|
||||
this.lineItemEditor = lineItemEditor
|
||||
this.#debitCredit = debitCredit;
|
||||
const prefix = "accounting-account-selector-" + debitCredit;
|
||||
this.#query = document.getElementById(prefix + "-query");
|
||||
this.#queryNoResult = document.getElementById(prefix + "-option-no-result");
|
||||
this.#optionList = document.getElementById(prefix + "-option-list");
|
||||
this.#options = Array.from(document.getElementsByClassName(prefix + "-option")).map((element) => new JournalEntryAccountOption(this, element));
|
||||
this.#more = document.getElementById(prefix + "-more");
|
||||
this.#clearButton = document.getElementById(prefix + "-btn-clear");
|
||||
|
||||
this.#more.onclick = () => {
|
||||
this.#isShowMore = true;
|
||||
this.#more.classList.add("d-none");
|
||||
this.#filterOptions();
|
||||
};
|
||||
this.#query.oninput = () => this.#filterOptions();
|
||||
this.#clearButton.onclick = () => this.lineItemEditor.clearAccount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the options.
|
||||
*
|
||||
*/
|
||||
#filterOptions() {
|
||||
const codesInUse = this.#getCodesUsedInForm();
|
||||
let isAnyMatched = false;
|
||||
for (const option of this.#options) {
|
||||
if (option.isMatched(this.#isShowMore, codesInUse, this.#query.value)) {
|
||||
option.setShown(true);
|
||||
isAnyMatched = true;
|
||||
} else {
|
||||
option.setShown(false);
|
||||
}
|
||||
}
|
||||
if (!isAnyMatched) {
|
||||
this.#optionList.classList.add("d-none");
|
||||
this.#queryNoResult.classList.remove("d-none");
|
||||
} else {
|
||||
this.#optionList.classList.remove("d-none");
|
||||
this.#queryNoResult.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account codes that are used in the form.
|
||||
*
|
||||
* @return {string[]} the account codes that are used in the form
|
||||
*/
|
||||
#getCodesUsedInForm() {
|
||||
const inUse = this.lineItemEditor.form.getAccountCodesUsed(this.#debitCredit);
|
||||
if (this.lineItemEditor.accountCode !== null) {
|
||||
inUse.push(this.lineItemEditor.accountCode);
|
||||
}
|
||||
return inUse
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback when the account selector is shown.
|
||||
*
|
||||
*/
|
||||
onOpen() {
|
||||
this.#query.value = "";
|
||||
this.#isShowMore = false;
|
||||
this.#more.classList.remove("d-none");
|
||||
this.#filterOptions();
|
||||
for (const option of this.#options) {
|
||||
option.setActive(option.code === this.lineItemEditor.accountCode);
|
||||
}
|
||||
if (this.lineItemEditor.accountCode === null) {
|
||||
this.#clearButton.classList.add("btn-secondary");
|
||||
this.#clearButton.classList.remove("btn-danger");
|
||||
this.#clearButton.disabled = true;
|
||||
} else {
|
||||
this.#clearButton.classList.add("btn-danger");
|
||||
this.#clearButton.classList.remove("btn-secondary");
|
||||
this.#clearButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account selector instances.
|
||||
*
|
||||
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
|
||||
* @return {{debit: JournalEntryAccountSelector, credit: JournalEntryAccountSelector}}
|
||||
*/
|
||||
static getInstances(lineItemEditor) {
|
||||
const selectors = {}
|
||||
const modals = Array.from(document.getElementsByClassName("accounting-account-selector"));
|
||||
for (const modal of modals) {
|
||||
selectors[modal.dataset.debitCredit] = new JournalEntryAccountSelector(lineItemEditor, modal.dataset.debitCredit);
|
||||
}
|
||||
return selectors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An account option
|
||||
*
|
||||
*/
|
||||
class JournalEntryAccountOption {
|
||||
|
||||
/**
|
||||
* The account selector
|
||||
* @type {JournalEntryAccountSelector}
|
||||
*/
|
||||
#selector;
|
||||
|
||||
/**
|
||||
* The element
|
||||
* @type {HTMLLIElement}
|
||||
*/
|
||||
#element;
|
||||
|
||||
/**
|
||||
* The account code
|
||||
* @type {string}
|
||||
*/
|
||||
code;
|
||||
|
||||
/**
|
||||
* The account text
|
||||
* @type {string}
|
||||
*/
|
||||
text;
|
||||
|
||||
/**
|
||||
* Whether the account is in use
|
||||
* @type {boolean}
|
||||
*/
|
||||
#isInUse;
|
||||
|
||||
/**
|
||||
* Whether line items in the account need offset
|
||||
* @type {boolean}
|
||||
*/
|
||||
isNeedOffset;
|
||||
|
||||
/**
|
||||
* The values to query against
|
||||
* @type {string[]}
|
||||
*/
|
||||
#queryValues;
|
||||
|
||||
/**
|
||||
* Constructs the account in the account selector.
|
||||
*
|
||||
* @param selector {JournalEntryAccountSelector} the account selector
|
||||
* @param element {HTMLLIElement} the element
|
||||
*/
|
||||
constructor(selector, element) {
|
||||
this.#selector = selector;
|
||||
this.#element = element;
|
||||
this.code = element.dataset.code;
|
||||
this.text = element.dataset.text;
|
||||
this.#isInUse = element.classList.contains("accounting-account-is-in-use");
|
||||
this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset");
|
||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||
|
||||
this.#element.onclick = () => this.#selector.lineItemEditor.saveAccount(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the account matches the query.
|
||||
*
|
||||
* @param isShowMore {boolean} true to show all accounts, or false to show only those in use
|
||||
* @param codesInUse {string[]} the account codes that are used in the form
|
||||
* @param query {string} the query term
|
||||
* @return {boolean} true if the option matches, or false otherwise
|
||||
*/
|
||||
isMatched(isShowMore, codesInUse, query) {
|
||||
return this.#isInUseMatched(isShowMore, codesInUse) && this.#isQueryMatched(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the account matches the "in-use" condition.
|
||||
*
|
||||
* @param isShowMore {boolean} true to show all accounts, or false to show only those in use
|
||||
* @param codesInUse {string[]} the account codes that are used in the form
|
||||
* @return {boolean} true if the option matches, or false otherwise
|
||||
*/
|
||||
#isInUseMatched(isShowMore, codesInUse) {
|
||||
return isShowMore || this.#isInUse || codesInUse.includes(this.code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the account matches the query term.
|
||||
*
|
||||
* @param query {string} the query term
|
||||
* @return {boolean} true if the option matches, or false otherwise
|
||||
*/
|
||||
#isQueryMatched(query) {
|
||||
if (query === "") {
|
||||
return true;
|
||||
}
|
||||
for (const queryValue of this.#queryValues) {
|
||||
if (queryValue.toLowerCase().includes(query.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the option is shown.
|
||||
*
|
||||
* @param isShown {boolean} true to show, or false otherwise
|
||||
*/
|
||||
setShown(isShown) {
|
||||
if (isShown) {
|
||||
this.#element.classList.remove("d-none");
|
||||
} else {
|
||||
this.#element.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the option is active.
|
||||
*
|
||||
* @param isActive {boolean} true if active, or false otherwise
|
||||
*/
|
||||
setActive(isActive) {
|
||||
if (isActive) {
|
||||
this.#element.classList.add("active");
|
||||
} else {
|
||||
this.#element.classList.remove("active");
|
||||
}
|
||||
}
|
||||
}
|
@ -621,7 +621,6 @@ class DebitCreditSubForm {
|
||||
this.#content = document.getElementById(this.#prefix + "-content");
|
||||
this.#error = document.getElementById(this.#prefix + "-error");
|
||||
this.#lineItemList = document.getElementById(this.#prefix + "-list");
|
||||
// noinspection JSValidateTypes
|
||||
this.lineItems = Array.from(document.getElementsByClassName(this.#prefix)).map((element) => new LineItemSubForm(this, element));
|
||||
this.#total = document.getElementById(this.#prefix + "-total");
|
||||
this.#addLineItemButton = document.getElementById(this.#prefix + "-add-line-item");
|
||||
@ -632,6 +631,16 @@ class DebitCreditSubForm {
|
||||
this.#initializeDragAndDropReordering();
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback when the line item editor is closed.
|
||||
*
|
||||
*/
|
||||
onLineItemEditorClosed() {
|
||||
if (this.lineItems.length === 0) {
|
||||
this.#element.classList.remove("accounting-not-empty");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new line item sub-form
|
||||
*
|
||||
@ -692,7 +701,10 @@ class DebitCreditSubForm {
|
||||
this.#element.classList.add("accounting-clickable");
|
||||
this.#element.dataset.bsToggle = "modal"
|
||||
this.#element.dataset.bsTarget = "#" + this.currency.form.lineItemEditor.modal.id;
|
||||
this.#element.onclick = () => this.currency.form.lineItemEditor.onAddNew(this);
|
||||
this.#element.onclick = () => {
|
||||
this.#element.classList.add("accounting-not-empty");
|
||||
this.currency.form.lineItemEditor.onAddNew(this);
|
||||
};
|
||||
this.#content.classList.add("d-none");
|
||||
} else {
|
||||
this.#element.classList.add("accounting-not-empty");
|
||||
|
@ -198,7 +198,7 @@ class JournalEntryLineItemEditor {
|
||||
|
||||
/**
|
||||
* The account selectors
|
||||
* @type {{debit: AccountSelector, credit: AccountSelector}}
|
||||
* @type {{debit: JournalEntryAccountSelector, credit: JournalEntryAccountSelector}}
|
||||
*/
|
||||
#accountSelectors;
|
||||
|
||||
@ -231,8 +231,9 @@ class JournalEntryLineItemEditor {
|
||||
this.#amountInput = document.getElementById(this.#prefix + "-amount");
|
||||
this.#amountError = document.getElementById(this.#prefix + "-amount-error");
|
||||
this.#descriptionEditors = DescriptionEditor.getInstances(this);
|
||||
this.#accountSelectors = AccountSelector.getInstances(this);
|
||||
this.#accountSelectors = JournalEntryAccountSelector.getInstances(this);
|
||||
this.originalLineItemSelector = new OriginalLineItemSelector(this);
|
||||
|
||||
this.#originalLineItemControl.onclick = () => this.originalLineItemSelector.onOpen()
|
||||
this.#originalLineItemDelete.onclick = () => this.clearOriginalLineItem();
|
||||
this.#descriptionControl.onclick = () => this.#descriptionEditors[this.debitCredit].onOpen();
|
||||
@ -248,6 +249,7 @@ class JournalEntryLineItemEditor {
|
||||
}
|
||||
return false;
|
||||
};
|
||||
this.modal.addEventListener("hidden.bs.modal", () => this.#debitCreditSubForm.onLineItemEditorClosed());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -368,18 +370,16 @@ class JournalEntryLineItemEditor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the account.
|
||||
* Saves the selected account.
|
||||
*
|
||||
* @param code {string} the account code
|
||||
* @param text {string} the account text
|
||||
* @param isNeedOffset {boolean} true if the line items in the account need offset or false otherwise
|
||||
* @param account {JournalEntryAccountOption} the selected account
|
||||
*/
|
||||
saveAccount(code, text, isNeedOffset) {
|
||||
this.isNeedOffset = isNeedOffset;
|
||||
saveAccount(account) {
|
||||
this.isNeedOffset = account.isNeedOffset;
|
||||
this.#accountControl.classList.add("accounting-not-empty");
|
||||
this.accountCode = code;
|
||||
this.accountText = text;
|
||||
this.#accountText.innerText = text;
|
||||
this.accountCode = account.code;
|
||||
this.accountText = account.text;
|
||||
this.#accountText.innerText = account.text;
|
||||
this.#validateAccount();
|
||||
}
|
||||
|
||||
|
@ -898,16 +898,16 @@ class RecurringAccountSelector {
|
||||
*
|
||||
*/
|
||||
#filterOptions() {
|
||||
let hasAnyMatched = false;
|
||||
let isAnyMatched = false;
|
||||
for (const option of this.#options) {
|
||||
if (option.isMatched(this.#query.value)) {
|
||||
option.setShown(true);
|
||||
hasAnyMatched = true;
|
||||
isAnyMatched = true;
|
||||
} else {
|
||||
option.setShown(false);
|
||||
}
|
||||
}
|
||||
if (!hasAnyMatched) {
|
||||
if (!isAnyMatched) {
|
||||
this.#optionList.classList.add("d-none");
|
||||
this.#queryNoResult.classList.remove("d-none");
|
||||
} else {
|
||||
@ -923,7 +923,6 @@ class RecurringAccountSelector {
|
||||
onOpen() {
|
||||
this.#query.value = "";
|
||||
this.#filterOptions();
|
||||
console.log(this.editor.accountCode);
|
||||
for (const option of this.#options) {
|
||||
option.setActive(option.code === this.editor.accountCode);
|
||||
}
|
||||
|
@ -96,9 +96,7 @@ class OriginalLineItemSelector {
|
||||
for (const option of this.#options) {
|
||||
this.#optionById[option.id] = option;
|
||||
}
|
||||
this.#query.addEventListener("input", () => {
|
||||
this.#filterOptions();
|
||||
});
|
||||
this.#query.oninput = () => this.#filterOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -155,16 +153,16 @@ class OriginalLineItemSelector {
|
||||
*
|
||||
*/
|
||||
#filterOptions() {
|
||||
let hasAnyMatched = false;
|
||||
let isAnyMatched = false;
|
||||
for (const option of this.#options) {
|
||||
if (option.isMatched(this.#debitCredit, this.#currencyCode, this.#query.value)) {
|
||||
option.setShown(true);
|
||||
hasAnyMatched = true;
|
||||
isAnyMatched = true;
|
||||
} else {
|
||||
option.setShown(false);
|
||||
}
|
||||
}
|
||||
if (!hasAnyMatched) {
|
||||
if (!isAnyMatched) {
|
||||
this.#optionList.classList.add("d-none");
|
||||
this.#queryNoResult.classList.remove("d-none");
|
||||
} else {
|
||||
@ -275,7 +273,7 @@ class OriginalLineItem {
|
||||
|
||||
/**
|
||||
* The values to query against
|
||||
* @type {string[][]}
|
||||
* @type {string[]}
|
||||
*/
|
||||
#queryValues;
|
||||
|
||||
@ -342,9 +340,9 @@ class OriginalLineItem {
|
||||
isMatched(debitCredit, currencyCode, query = null) {
|
||||
return this.netBalance.greaterThan(0)
|
||||
&& this.date <= this.#selector.lineItemEditor.form.date
|
||||
&& this.#isDebitCreditMatches(debitCredit)
|
||||
&& this.#isDebitCreditMatched(debitCredit)
|
||||
&& this.#currencyCode === currencyCode
|
||||
&& this.#isQueryMatches(query);
|
||||
&& this.#isQueryMatched(query);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -353,34 +351,43 @@ class OriginalLineItem {
|
||||
* @param debitCredit {string} either "debit" or credit
|
||||
* @return {boolean} true if the option matches, or false otherwise
|
||||
*/
|
||||
#isDebitCreditMatches(debitCredit) {
|
||||
#isDebitCreditMatched(debitCredit) {
|
||||
return (debitCredit === "debit" && this.#debitCredit === "credit")
|
||||
|| (debitCredit === "credit" && this.#debitCredit === "debit");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the original line item matches the query.
|
||||
* Returns whether the original line item matches the query term.
|
||||
*
|
||||
* @param query {string|null} the query term
|
||||
* @return {boolean} true if the option matches, or false otherwise
|
||||
*/
|
||||
#isQueryMatches(query) {
|
||||
#isQueryMatched(query) {
|
||||
if (query === "") {
|
||||
return true;
|
||||
}
|
||||
for (const queryValue of this.#queryValues[0]) {
|
||||
if (this.#getNetBalanceForQuery().includes(query.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
for (const queryValue of this.#queryValues) {
|
||||
if (queryValue.toLowerCase().includes(query.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const queryValue of this.#queryValues[1]) {
|
||||
if (queryValue === query) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the net balance in the format for query match.
|
||||
*
|
||||
* @return {string} the net balance in the format for query match
|
||||
*/
|
||||
#getNetBalanceForQuery() {
|
||||
const frac = this.netBalance.modulo(1);
|
||||
const whole = Number(this.netBalance.minus(frac));
|
||||
return String(whole) + String(frac).substring(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the option is shown.
|
||||
*
|
||||
|
@ -37,7 +37,7 @@ First written: 2023/2/25
|
||||
|
||||
<ul id="accounting-account-selector-{{ debit_credit }}-option-list" class="list-group accounting-selector-list">
|
||||
{% for account in account_options %}
|
||||
<li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||
<li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-is-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||
{{ account }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
@ -25,7 +25,7 @@ First written: 2023/2/26
|
||||
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/journal-entry-form.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/journal-entry-line-item-editor.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/account-selector.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/journal-entry-account-selector.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/original-line-item-selector.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/description-editor.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
@ -27,12 +27,9 @@ from flask import Flask
|
||||
from flask.testing import FlaskCliRunner
|
||||
|
||||
from test_site import db
|
||||
from testlib import create_test_app, get_client, set_locale
|
||||
from testlib import NEXT_URI, create_test_app, get_client, set_locale
|
||||
from testlib_journal_entry import add_journal_entry
|
||||
|
||||
NEXT_URI: str = "/_next"
|
||||
"""The next URI."""
|
||||
|
||||
|
||||
class AccountData:
|
||||
"""The account data."""
|
||||
|
@ -28,8 +28,8 @@ from flask import Flask
|
||||
from flask.testing import FlaskCliRunner
|
||||
|
||||
from test_site import db
|
||||
from testlib import create_test_app, get_client, set_locale
|
||||
from testlib_journal_entry import add_journal_entry, NEXT_URI
|
||||
from testlib import NEXT_URI, create_test_app, get_client, set_locale
|
||||
from testlib_journal_entry import add_journal_entry
|
||||
|
||||
|
||||
class CurrencyData:
|
||||
|
@ -24,8 +24,8 @@ from click.testing import Result
|
||||
from flask import Flask
|
||||
from flask.testing import FlaskCliRunner
|
||||
|
||||
from testlib import create_test_app, get_client
|
||||
from testlib_journal_entry import Accounts, NEXT_URI, add_journal_entry
|
||||
from testlib import NEXT_URI, Accounts, create_test_app, get_client
|
||||
from testlib_journal_entry import add_journal_entry
|
||||
|
||||
|
||||
class DescriptionEditorTestCase(unittest.TestCase):
|
||||
|
@ -27,9 +27,9 @@ from flask import Flask
|
||||
from flask.testing import FlaskCliRunner
|
||||
|
||||
from test_site import db
|
||||
from testlib import create_test_app, get_client
|
||||
from testlib_journal_entry import NEXT_URI, NON_EMPTY_NOTE, EMPTY_NOTE, \
|
||||
Accounts, get_add_form, get_unchanged_update_form, get_update_form, \
|
||||
from testlib import NEXT_URI, Accounts, create_test_app, get_client
|
||||
from testlib_journal_entry import NON_EMPTY_NOTE, EMPTY_NOTE, \
|
||||
get_add_form, get_unchanged_update_form, get_update_form, \
|
||||
match_journal_entry_detail, set_negative_amount, \
|
||||
remove_debit_in_a_currency, remove_credit_in_a_currency, add_journal_entry
|
||||
|
||||
|
@ -26,8 +26,8 @@ from flask import Flask
|
||||
from flask.testing import FlaskCliRunner
|
||||
|
||||
from test_site import db
|
||||
from testlib import create_test_app, get_client
|
||||
from testlib_journal_entry import Accounts, match_journal_entry_detail
|
||||
from testlib import Accounts, create_test_app, get_client
|
||||
from testlib_journal_entry import match_journal_entry_detail
|
||||
from testlib_offset import TestData, JournalEntryLineItemData, \
|
||||
JournalEntryData, CurrencyData
|
||||
|
||||
|
@ -26,8 +26,7 @@ from flask import Flask
|
||||
from flask.testing import FlaskCliRunner
|
||||
|
||||
from test_site import db
|
||||
from testlib import create_test_app, get_client
|
||||
from testlib_journal_entry import NEXT_URI, Accounts
|
||||
from testlib import NEXT_URI, Accounts, create_test_app, get_client
|
||||
from testlib_offset import TestData
|
||||
|
||||
PREFIX: str = "/accounting/options"
|
||||
|
@ -26,6 +26,33 @@ from test_site import create_app
|
||||
|
||||
TEST_SERVER: str = "https://testserver"
|
||||
"""The test server URI."""
|
||||
NEXT_URI: str = "/_next"
|
||||
"""The next URI."""
|
||||
|
||||
|
||||
class Accounts:
|
||||
"""The shortcuts to the common accounts."""
|
||||
CASH: str = "1111-001"
|
||||
PETTY_CASH: str = "1112-001"
|
||||
BANK: str = "1113-001"
|
||||
NOTES_RECEIVABLE: str = "1131-001"
|
||||
RECEIVABLE: str = "1141-001"
|
||||
PREPAID: str = "1258-001"
|
||||
NOTES_PAYABLE: str = "2131-001"
|
||||
PAYABLE: str = "2141-001"
|
||||
SALES: str = "4111-001"
|
||||
SERVICE: str = "4611-001"
|
||||
AGENCY: str = "4711-001"
|
||||
RENT_EXPENSE: str = "6252-001"
|
||||
OFFICE: str = "6253-001"
|
||||
TRAVEL: str = "6254-001"
|
||||
POSTAGE: str = "6256-001"
|
||||
UTILITIES: str = "6261-001"
|
||||
INSURANCE: str = "6262-001"
|
||||
MEAL: str = "6272-001"
|
||||
INTEREST: str = "7111-001"
|
||||
DONATION: str = "7481-001"
|
||||
RENT_INCOME: str = "7482-001"
|
||||
|
||||
|
||||
def create_test_app() -> Flask:
|
||||
@ -57,7 +84,6 @@ def get_csrf_token(client: httpx.Client) -> str:
|
||||
return client.get("/.csrf-token").text
|
||||
|
||||
|
||||
|
||||
def get_client(app: Flask, username: str) -> tuple[httpx.Client, str]:
|
||||
"""Returns a user client.
|
||||
|
||||
|
@ -26,40 +26,14 @@ import httpx
|
||||
from flask import Flask
|
||||
|
||||
from test_site import db
|
||||
from testlib import NEXT_URI, Accounts
|
||||
|
||||
NEXT_URI: str = "/_next"
|
||||
"""The next URI."""
|
||||
NON_EMPTY_NOTE: str = " This is \n\na test."
|
||||
"""The stripped content of an non-empty note."""
|
||||
EMPTY_NOTE: str = " \n\n "
|
||||
"""The empty note content."""
|
||||
|
||||
|
||||
class Accounts:
|
||||
"""The shortcuts to the common accounts."""
|
||||
CASH: str = "1111-001"
|
||||
PETTY_CASH: str = "1112-001"
|
||||
BANK: str = "1113-001"
|
||||
NOTES_RECEIVABLE: str = "1131-001"
|
||||
RECEIVABLE: str = "1141-001"
|
||||
PREPAID: str = "1258-001"
|
||||
NOTES_PAYABLE: str = "2131-001"
|
||||
PAYABLE: str = "2141-001"
|
||||
SALES: str = "4111-001"
|
||||
SERVICE: str = "4611-001"
|
||||
AGENCY: str = "4711-001"
|
||||
RENT_EXPENSE: str = "6252-001"
|
||||
OFFICE: str = "6253-001"
|
||||
TRAVEL: str = "6254-001"
|
||||
POSTAGE: str = "6256-001"
|
||||
UTILITIES: str = "6261-001"
|
||||
INSURANCE: str = "6262-001"
|
||||
MEAL: str = "6272-001"
|
||||
INTEREST: str = "7111-001"
|
||||
DONATION: str = "7481-001"
|
||||
RENT_INCOME: str = "7482-001"
|
||||
|
||||
|
||||
def get_add_form(csrf_token: str) -> dict[str, str]:
|
||||
"""Returns the form data to add a new journal entry.
|
||||
|
||||
|
@ -26,8 +26,8 @@ import httpx
|
||||
from flask import Flask
|
||||
|
||||
from test_site import db
|
||||
from testlib_journal_entry import Accounts, match_journal_entry_detail, \
|
||||
NEXT_URI
|
||||
from testlib import NEXT_URI, Accounts
|
||||
from testlib_journal_entry import match_journal_entry_detail
|
||||
|
||||
|
||||
class JournalEntryLineItemData:
|
||||
|
Loading…
Reference in New Issue
Block a user