Compare commits
38 Commits
b28d446d07
...
f3548a2327
Author | SHA1 | Date | |
---|---|---|---|
f3548a2327 | |||
79883d6940 | |||
b2bc993416 | |||
453b3f0da5 | |||
63ae3f0746 | |||
da4cc6489f | |||
1102a3a4f3 | |||
1402a12f04 | |||
f049b5d7ee | |||
14ed4ca354 | |||
535ff96ab3 | |||
57482f81fc | |||
a31ce3c400 | |||
319f0aed90 | |||
826dcf0f86 | |||
b2411aee74 | |||
731acdced0 | |||
35b3bca1e6 | |||
3c413497ae | |||
1b5e516413 | |||
20cb5cecc4 | |||
08dc24605d | |||
bb7e9e94ee | |||
2680a1c872 | |||
20a7ce591c | |||
474e844ed9 | |||
b34955f2fb | |||
2bd0f0f14d | |||
8b77d9ff93 | |||
a9c7360020 | |||
d02c87602b | |||
9f966643b5 | |||
5746e2a3d6 | |||
d5c2231794 | |||
fc8e257a10 | |||
2e9bf382fb | |||
de48c848da | |||
9cdcc828a7 |
@ -36,6 +36,14 @@ accounting.transaction.query module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.transaction.summary\_helper module
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.transaction.summary_helper
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.transaction.template module
|
accounting.transaction.template module
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
|
@ -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.0.0'
|
release = '0.4.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
|
||||||
@ -28,5 +28,5 @@ exclude_patterns = []
|
|||||||
# -- Options for HTML output -------------------------------------------------
|
# -- Options for HTML output -------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
|
|
||||||
html_theme = 'nature'
|
html_theme = 'sphinx_rtd_theme'
|
||||||
html_static_path = ['_static']
|
html_static_path = ['_static']
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
name = mia-accounting-flask
|
name = mia-accounting-flask
|
||||||
version = 0.3.1
|
version = 0.4.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.
|
||||||
|
@ -203,25 +203,6 @@ class Account(db.Model):
|
|||||||
return
|
return
|
||||||
self.l10n.append(AccountL10n(locale=current_locale, title=value))
|
self.l10n.append(AccountL10n(locale=current_locale, title=value))
|
||||||
|
|
||||||
@property
|
|
||||||
def is_in_use(self) -> bool:
|
|
||||||
"""Returns whether the account is in use.
|
|
||||||
|
|
||||||
:return: True if the account is in use, or False otherwise.
|
|
||||||
"""
|
|
||||||
if not hasattr(self, "__is_in_use"):
|
|
||||||
setattr(self, "__is_in_use", len(self.entries) > 0)
|
|
||||||
return getattr(self, "__is_in_use")
|
|
||||||
|
|
||||||
@is_in_use.setter
|
|
||||||
def is_in_use(self, is_in_use: bool) -> None:
|
|
||||||
"""Sets whether the account is in use.
|
|
||||||
|
|
||||||
:param is_in_use: True if the account is in use, or False otherwise.
|
|
||||||
:return: None.
|
|
||||||
"""
|
|
||||||
setattr(self, "__is_in_use", is_in_use)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_by_code(cls, code: str) -> t.Self | None:
|
def find_by_code(cls, code: str) -> t.Self | None:
|
||||||
"""Finds an account by its code.
|
"""Finds an account by its code.
|
||||||
|
@ -46,9 +46,9 @@ function initializeBaseAccountSelector() {
|
|||||||
const btnClear = document.getElementById("accounting-btn-clear-base");
|
const btnClear = document.getElementById("accounting-btn-clear-base");
|
||||||
selector.addEventListener("show.bs.modal", function () {
|
selector.addEventListener("show.bs.modal", function () {
|
||||||
base.classList.add("accounting-not-empty");
|
base.classList.add("accounting-not-empty");
|
||||||
options.forEach(function (item) {
|
for (const option of options) {
|
||||||
item.classList.remove("active");
|
option.classList.remove("active");
|
||||||
});
|
}
|
||||||
const selected = document.getElementById("accounting-base-option-" + baseCode.value);
|
const selected = document.getElementById("accounting-base-option-" + baseCode.value);
|
||||||
if (selected !== null) {
|
if (selected !== null) {
|
||||||
selected.classList.add("active");
|
selected.classList.add("active");
|
||||||
@ -59,7 +59,7 @@ function initializeBaseAccountSelector() {
|
|||||||
base.classList.remove("accounting-not-empty");
|
base.classList.remove("accounting-not-empty");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
options.forEach(function (option) {
|
for (const option of options) {
|
||||||
option.onclick = function () {
|
option.onclick = function () {
|
||||||
baseCode.value = option.dataset.code;
|
baseCode.value = option.dataset.code;
|
||||||
baseContent.innerText = option.dataset.content;
|
baseContent.innerText = option.dataset.content;
|
||||||
@ -69,7 +69,7 @@ function initializeBaseAccountSelector() {
|
|||||||
validateBase();
|
validateBase();
|
||||||
bootstrap.Modal.getInstance(selector).hide();
|
bootstrap.Modal.getInstance(selector).hide();
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
btnClear.onclick = function () {
|
btnClear.onclick = function () {
|
||||||
baseCode.value = "";
|
baseCode.value = "";
|
||||||
baseContent.innerText = "";
|
baseContent.innerText = "";
|
||||||
@ -94,15 +94,15 @@ function initializeBaseAccountQuery() {
|
|||||||
const queryNoResult = document.getElementById("accounting-base-option-no-result");
|
const queryNoResult = document.getElementById("accounting-base-option-no-result");
|
||||||
query.addEventListener("input", function () {
|
query.addEventListener("input", function () {
|
||||||
if (query.value === "") {
|
if (query.value === "") {
|
||||||
options.forEach(function (option) {
|
for (const option of options) {
|
||||||
option.classList.remove("d-none");
|
option.classList.remove("d-none");
|
||||||
});
|
}
|
||||||
optionList.classList.remove("d-none");
|
optionList.classList.remove("d-none");
|
||||||
queryNoResult.classList.add("d-none");
|
queryNoResult.classList.add("d-none");
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let hasAnyMatched = false;
|
let hasAnyMatched = false;
|
||||||
options.forEach(function (option) {
|
for (const option of options) {
|
||||||
const queryValues = JSON.parse(option.dataset.queryValues);
|
const queryValues = JSON.parse(option.dataset.queryValues);
|
||||||
let isMatched = false;
|
let isMatched = false;
|
||||||
for (const queryValue of queryValues) {
|
for (const queryValue of queryValues) {
|
||||||
@ -117,7 +117,7 @@ function initializeBaseAccountQuery() {
|
|||||||
} else {
|
} else {
|
||||||
option.classList.add("d-none");
|
option.classList.add("d-none");
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
if (!hasAnyMatched) {
|
if (!hasAnyMatched) {
|
||||||
optionList.classList.add("d-none");
|
optionList.classList.add("d-none");
|
||||||
queryNoResult.classList.remove("d-none");
|
queryNoResult.classList.remove("d-none");
|
||||||
|
254
src/accounting/static/js/account-selector.js
Normal file
254
src/accounting/static/js/account-selector.js
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
/* The Mia! Accounting Flask Project
|
||||||
|
* transaction-transfer-form.js: The JavaScript for the transfer transaction 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Initializes the page JavaScript.
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
AccountSelector.initialize();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The account selector.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class AccountSelector {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The entry type
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
#entryType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The prefix of the HTML ID and class
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
#prefix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an account selector.
|
||||||
|
*
|
||||||
|
* @param modal {HTMLFormElement} the account selector modal
|
||||||
|
*/
|
||||||
|
constructor(modal) {
|
||||||
|
this.#entryType = modal.dataset.entryType;
|
||||||
|
this.#prefix = "accounting-account-selector-" + modal.dataset.entryType;
|
||||||
|
this.#init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the account selector.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#init() {
|
||||||
|
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
|
||||||
|
const formAccount = document.getElementById("accounting-entry-form-account");
|
||||||
|
const more = document.getElementById(this.#prefix + "-more");
|
||||||
|
const btnClear = document.getElementById(this.#prefix + "-btn-clear");
|
||||||
|
const options = Array.from(document.getElementsByClassName(this.#prefix + "-option"));
|
||||||
|
const selector1 = this
|
||||||
|
more.onclick = function () {
|
||||||
|
more.classList.add("d-none");
|
||||||
|
selector1.#filterAccountOptions();
|
||||||
|
};
|
||||||
|
this.#initializeAccountQuery();
|
||||||
|
btnClear.onclick = function () {
|
||||||
|
formAccountControl.classList.remove("accounting-not-empty");
|
||||||
|
formAccount.innerText = "";
|
||||||
|
formAccount.dataset.code = "";
|
||||||
|
formAccount.dataset.text = "";
|
||||||
|
validateJournalEntryAccount();
|
||||||
|
};
|
||||||
|
for (const option of options) {
|
||||||
|
option.onclick = function () {
|
||||||
|
formAccountControl.classList.add("accounting-not-empty");
|
||||||
|
formAccount.innerText = option.dataset.content;
|
||||||
|
formAccount.dataset.code = option.dataset.code;
|
||||||
|
formAccount.dataset.text = option.dataset.content;
|
||||||
|
validateJournalEntryAccount();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the query on the account options.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#initializeAccountQuery() {
|
||||||
|
const query = document.getElementById(this.#prefix + "-query");
|
||||||
|
const helper = this;
|
||||||
|
query.addEventListener("input", function () {
|
||||||
|
helper.#filterAccountOptions();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the account options.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#filterAccountOptions() {
|
||||||
|
const query = document.getElementById(this.#prefix + "-query");
|
||||||
|
const optionList = document.getElementById(this.#prefix + "-option-list");
|
||||||
|
if (optionList === null) {
|
||||||
|
console.log(this.#prefix + "-option-list");
|
||||||
|
}
|
||||||
|
const options = Array.from(document.getElementsByClassName(this.#prefix + "-option"));
|
||||||
|
const more = document.getElementById(this.#prefix + "-more");
|
||||||
|
const queryNoResult = document.getElementById(this.#prefix + "-option-no-result");
|
||||||
|
const codesInUse = this.#getAccountCodeUsedInForm();
|
||||||
|
let shouldAnyShow = false;
|
||||||
|
for (const option of options) {
|
||||||
|
const shouldShow = this.#shouldAccountOptionShow(option, more, codesInUse, query);
|
||||||
|
if (shouldShow) {
|
||||||
|
option.classList.remove("d-none");
|
||||||
|
shouldAnyShow = true;
|
||||||
|
} else {
|
||||||
|
option.classList.add("d-none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!shouldAnyShow && more.classList.contains("d-none")) {
|
||||||
|
optionList.classList.add("d-none");
|
||||||
|
queryNoResult.classList.remove("d-none");
|
||||||
|
} else {
|
||||||
|
optionList.classList.remove("d-none");
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
#getAccountCodeUsedInForm() {
|
||||||
|
const accountCodes = Array.from(document.getElementsByClassName("accounting-" + this.#prefix + "-account-code"));
|
||||||
|
const formAccount = document.getElementById("accounting-entry-form-account");
|
||||||
|
const inUse = [formAccount.dataset.code];
|
||||||
|
for (const accountCode of accountCodes) {
|
||||||
|
inUse.push(accountCode.value);
|
||||||
|
}
|
||||||
|
return inUse
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether an account option should show.
|
||||||
|
*
|
||||||
|
* @param option {HTMLLIElement} the account option
|
||||||
|
* @param more {HTMLLIElement} the more account 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 account option should show, or false otherwise
|
||||||
|
*/
|
||||||
|
#shouldAccountOptionShow(option, more, inUse, query) {
|
||||||
|
const isQueryMatched = function () {
|
||||||
|
if (query.value === "") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const queryValues = JSON.parse(option.dataset.queryValues);
|
||||||
|
for (const queryValue of queryValues) {
|
||||||
|
if (queryValue.includes(query.value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const isMoreMatched = function () {
|
||||||
|
if (more.classList.contains("d-none")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return option.classList.contains("accounting-account-in-use") || inUse.includes(option.dataset.code);
|
||||||
|
};
|
||||||
|
return isMoreMatched() && isQueryMatched();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the account selector when it is shown.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
initShow() {
|
||||||
|
const formAccount = document.getElementById("accounting-entry-form-account");
|
||||||
|
const query = document.getElementById(this.#prefix + "-query")
|
||||||
|
const more = document.getElementById(this.#prefix + "-more");
|
||||||
|
const options = Array.from(document.getElementsByClassName(this.#prefix + "-option"));
|
||||||
|
const btnClear = document.getElementById(this.#prefix + "-btn-clear");
|
||||||
|
query.value = "";
|
||||||
|
more.classList.remove("d-none");
|
||||||
|
this.#filterAccountOptions();
|
||||||
|
for (const option of options) {
|
||||||
|
if (option.dataset.code === formAccount.dataset.code) {
|
||||||
|
option.classList.add("active");
|
||||||
|
} else {
|
||||||
|
option.classList.remove("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (formAccount.dataset.code === "") {
|
||||||
|
btnClear.classList.add("btn-secondary");
|
||||||
|
btnClear.classList.remove("btn-danger");
|
||||||
|
btnClear.disabled = true;
|
||||||
|
} else {
|
||||||
|
btnClear.classList.add("btn-danger");
|
||||||
|
btnClear.classList.remove("btn-secondary");
|
||||||
|
btnClear.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The account selectors.
|
||||||
|
* @type {{debit: AccountSelector, credit: AccountSelector}}
|
||||||
|
*/
|
||||||
|
static #selectors = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the account selectors.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
static initialize() {
|
||||||
|
const modals = Array.from(document.getElementsByClassName("accounting-account-selector-modal"));
|
||||||
|
for (const modal of modals) {
|
||||||
|
const selector = new AccountSelector(modal);
|
||||||
|
this.#selectors[selector.#entryType] = selector;
|
||||||
|
}
|
||||||
|
this.#initializeTransactionForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the transaction form.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
static #initializeTransactionForm() {
|
||||||
|
const entryForm = document.getElementById("accounting-entry-form");
|
||||||
|
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
|
||||||
|
const selectors = this.#selectors;
|
||||||
|
formAccountControl.onclick = function () {
|
||||||
|
selectors[entryForm.dataset.entryType].initShow();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Initializes the account selector for the journal entry form.
|
||||||
|
*x
|
||||||
|
*/
|
||||||
|
static initializeJournalEntryForm() {
|
||||||
|
const entryForm = document.getElementById("accounting-entry-form");
|
||||||
|
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
|
||||||
|
formAccountControl.dataset.bsTarget = "#accounting-account-selector-" + entryForm.dataset.entryType + "-modal";
|
||||||
|
}
|
||||||
|
}
|
@ -65,9 +65,9 @@ function validateForm() {
|
|||||||
*/
|
*/
|
||||||
function submitFormIfAllAsyncValid() {
|
function submitFormIfAllAsyncValid() {
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
Object.keys(isAsyncValid).forEach(function (key) {
|
for (const key of Object.keys(isAsyncValid)) {
|
||||||
isValid = isAsyncValid[key] && isValid;
|
isValid = isAsyncValid[key] && isValid;
|
||||||
});
|
}
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
document.getElementById("accounting-form").submit()
|
document.getElementById("accounting-form").submit()
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ function initializeDragAndDropReordering(list, onReorder) {
|
|||||||
function initializeMouseDragAndDropReordering(list, onReorder) {
|
function initializeMouseDragAndDropReordering(list, onReorder) {
|
||||||
const items = Array.from(list.children);
|
const items = Array.from(list.children);
|
||||||
let dragged = null;
|
let dragged = null;
|
||||||
items.forEach(function (item) {
|
for (const item of items) {
|
||||||
item.draggable = true;
|
item.draggable = true;
|
||||||
item.addEventListener("dragstart", function () {
|
item.addEventListener("dragstart", function () {
|
||||||
dragged = item;
|
dragged = item;
|
||||||
@ -56,7 +56,7 @@ function initializeMouseDragAndDropReordering(list, onReorder) {
|
|||||||
dragged.classList.remove("accounting-dragged");
|
dragged.classList.remove("accounting-dragged");
|
||||||
dragged = null;
|
dragged = null;
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -68,7 +68,7 @@ function initializeMouseDragAndDropReordering(list, onReorder) {
|
|||||||
*/
|
*/
|
||||||
function initializeTouchDragAndDropReordering(list, onReorder) {
|
function initializeTouchDragAndDropReordering(list, onReorder) {
|
||||||
const items = Array.from(list.children);
|
const items = Array.from(list.children);
|
||||||
items.forEach(function (item) {
|
for (const item of items) {
|
||||||
item.addEventListener("touchstart", function () {
|
item.addEventListener("touchstart", function () {
|
||||||
item.classList.add("accounting-dragged");
|
item.classList.add("accounting-dragged");
|
||||||
});
|
});
|
||||||
@ -81,7 +81,7 @@ function initializeTouchDragAndDropReordering(list, onReorder) {
|
|||||||
item.addEventListener("touchend", function () {
|
item.addEventListener("touchend", function () {
|
||||||
item.classList.remove("accounting-dragged");
|
item.classList.remove("accounting-dragged");
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
827
src/accounting/static/js/summary-helper.js
Normal file
827
src/accounting/static/js/summary-helper.js
Normal file
@ -0,0 +1,827 @@
|
|||||||
|
/* The Mia! Accounting Flask Project
|
||||||
|
* summary-helper.js: The JavaScript for the summary helper
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Initializes the page JavaScript.
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
SummaryHelper.initialize();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A summary helper.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class SummaryHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The entry type, either "debit" or "credit"
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
#entryType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The prefix of the HTML ID and class
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
#prefix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default tab ID
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
#defaultTabId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a summary helper.
|
||||||
|
*
|
||||||
|
* @param form {HTMLFormElement} the summary helper form
|
||||||
|
*/
|
||||||
|
constructor(form) {
|
||||||
|
this.#entryType = form.dataset.entryType;
|
||||||
|
this.#prefix = "accounting-summary-helper-" + form.dataset.entryType;
|
||||||
|
this.#defaultTabId = form.dataset.defaultTabId;
|
||||||
|
this.#init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the summary helper.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#init() {
|
||||||
|
const helper = this;
|
||||||
|
const summary = document.getElementById(this.#prefix + "-summary");
|
||||||
|
const tabs = Array.from(document.getElementsByClassName(this.#prefix + "-tab"));
|
||||||
|
for (const tab of tabs) {
|
||||||
|
tab.onclick = function () {
|
||||||
|
helper.#switchToTab(tab.dataset.tabId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#initializeGeneralTagHelper();
|
||||||
|
this.#initializeGeneralTripHelper();
|
||||||
|
this.#initializeBusTripHelper();
|
||||||
|
this.#initializeNumberHelper();
|
||||||
|
this.#initializeSuggestedAccounts();
|
||||||
|
this.#initializeSubmission();
|
||||||
|
summary.onchange = function () {
|
||||||
|
summary.value = summary.value.trim();
|
||||||
|
helper.#parseAndPopulate();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches to a tab.
|
||||||
|
*
|
||||||
|
* @param tabId {string} the tab ID.
|
||||||
|
*/
|
||||||
|
#switchToTab(tabId) {
|
||||||
|
const tabs = Array.from(document.getElementsByClassName(this.#prefix + "-tab"));
|
||||||
|
const pages = Array.from(document.getElementsByClassName(this.#prefix + "-page"));
|
||||||
|
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-" + tabId + "-btn-tag"));
|
||||||
|
for (const tab of tabs) {
|
||||||
|
if (tab.dataset.tabId === tabId) {
|
||||||
|
tab.classList.add("active");
|
||||||
|
tab.ariaCurrent = "page";
|
||||||
|
} else {
|
||||||
|
tab.classList.remove("active");
|
||||||
|
tab.ariaCurrent = "false";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const page of pages) {
|
||||||
|
if (page.dataset.tabId === tabId) {
|
||||||
|
page.classList.remove("d-none");
|
||||||
|
page.ariaCurrent = "page";
|
||||||
|
} else {
|
||||||
|
page.classList.add("d-none");
|
||||||
|
page.ariaCurrent = "false";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let selectedBtnTag = null;
|
||||||
|
for (const tagButton of tagButtons) {
|
||||||
|
if (tagButton.classList.contains("btn-primary")) {
|
||||||
|
selectedBtnTag = tagButton;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#filterSuggestedAccounts(selectedBtnTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the general tag helper.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#initializeGeneralTagHelper() {
|
||||||
|
const summary = document.getElementById(this.#prefix + "-summary");
|
||||||
|
const tag = document.getElementById(this.#prefix + "-general-tag");
|
||||||
|
const helper = this;
|
||||||
|
const updateSummary = function () {
|
||||||
|
const pos = summary.value.indexOf("—");
|
||||||
|
const prefix = tag.value === ""? "": tag.value + "—";
|
||||||
|
if (pos === -1) {
|
||||||
|
summary.value = prefix + summary.value;
|
||||||
|
} else {
|
||||||
|
summary.value = prefix + summary.value.substring(pos + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#initializeTagButtons("general", tag, updateSummary);
|
||||||
|
tag.onchange = function () {
|
||||||
|
helper.#onTagInputChange("general", tag, updateSummary);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the general trip helper.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#initializeGeneralTripHelper() {
|
||||||
|
const summary = document.getElementById(this.#prefix + "-summary");
|
||||||
|
const tag = document.getElementById(this.#prefix + "-travel-tag");
|
||||||
|
const from = document.getElementById(this.#prefix + "-travel-from");
|
||||||
|
const directionButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-direction"))
|
||||||
|
const to = document.getElementById(this.#prefix + "-travel-to");
|
||||||
|
const helper = this;
|
||||||
|
const updateSummary = function () {
|
||||||
|
let direction;
|
||||||
|
for (const directionButton of directionButtons) {
|
||||||
|
if (directionButton.classList.contains("btn-primary")) {
|
||||||
|
direction = directionButton.dataset.arrow;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
summary.value = tag.value + "—" + from.value + direction + to.value;
|
||||||
|
};
|
||||||
|
this.#initializeTagButtons("travel", tag, updateSummary);
|
||||||
|
tag.onchange = function () {
|
||||||
|
helper.#onTagInputChange("travel", tag, updateSummary);
|
||||||
|
helper.#validateGeneralTripTag();
|
||||||
|
};
|
||||||
|
from.onchange = function () {
|
||||||
|
updateSummary();
|
||||||
|
helper.#validateGeneralTripFrom();
|
||||||
|
};
|
||||||
|
for (const directionButton of directionButtons) {
|
||||||
|
directionButton.onclick = function () {
|
||||||
|
for (const otherButton of directionButtons) {
|
||||||
|
otherButton.classList.remove("btn-primary");
|
||||||
|
otherButton.classList.add("btn-outline-primary");
|
||||||
|
}
|
||||||
|
directionButton.classList.remove("btn-outline-primary");
|
||||||
|
directionButton.classList.add("btn-primary");
|
||||||
|
updateSummary();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
to.onchange = function () {
|
||||||
|
updateSummary();
|
||||||
|
helper.#validateGeneralTripTo();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the bus trip helper.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#initializeBusTripHelper() {
|
||||||
|
const summary = document.getElementById(this.#prefix + "-summary");
|
||||||
|
const tag = document.getElementById(this.#prefix + "-bus-tag");
|
||||||
|
const route = document.getElementById(this.#prefix + "-bus-route");
|
||||||
|
const from = document.getElementById(this.#prefix + "-bus-from");
|
||||||
|
const to = document.getElementById(this.#prefix + "-bus-to");
|
||||||
|
const helper = this;
|
||||||
|
const updateSummary = function () {
|
||||||
|
summary.value = tag.value + "—" + route.value + "—" + from.value + "→" + to.value;
|
||||||
|
};
|
||||||
|
this.#initializeTagButtons("bus", tag, updateSummary);
|
||||||
|
tag.onchange = function () {
|
||||||
|
helper.#onTagInputChange("bus", tag, updateSummary);
|
||||||
|
helper.#validateBusTripTag();
|
||||||
|
};
|
||||||
|
route.onchange = function () {
|
||||||
|
updateSummary();
|
||||||
|
helper.#validateBusTripRoute();
|
||||||
|
};
|
||||||
|
from.onchange = function () {
|
||||||
|
updateSummary();
|
||||||
|
helper.#validateBusTripFrom();
|
||||||
|
};
|
||||||
|
to.onchange = function () {
|
||||||
|
updateSummary();
|
||||||
|
helper.#validateBusTripTo();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the tag buttons.
|
||||||
|
*
|
||||||
|
* @param tabId {string} the tab ID
|
||||||
|
* @param tag {HTMLInputElement} the tag input
|
||||||
|
* @param updateSummary {function(): void} the callback to update the summary
|
||||||
|
*/
|
||||||
|
#initializeTagButtons(tabId, tag, updateSummary) {
|
||||||
|
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-" + tabId + "-btn-tag"));
|
||||||
|
const helper = this;
|
||||||
|
for (const tagButton of tagButtons) {
|
||||||
|
tagButton.onclick = function () {
|
||||||
|
for (const otherButton of tagButtons) {
|
||||||
|
otherButton.classList.remove("btn-primary");
|
||||||
|
otherButton.classList.add("btn-outline-primary");
|
||||||
|
}
|
||||||
|
tagButton.classList.remove("btn-outline-primary");
|
||||||
|
tagButton.classList.add("btn-primary");
|
||||||
|
tag.value = tagButton.dataset.value;
|
||||||
|
helper.#filterSuggestedAccounts(tagButton);
|
||||||
|
updateSummary();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The callback when the tag input is changed.
|
||||||
|
*
|
||||||
|
* @param tabId {string} the tab ID
|
||||||
|
* @param tag {HTMLInputElement} the tag input
|
||||||
|
* @param updateSummary {function(): void} the callback to update the summary
|
||||||
|
*/
|
||||||
|
#onTagInputChange(tabId, tag, updateSummary) {
|
||||||
|
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-" + tabId + "-btn-tag"));
|
||||||
|
let isMatched = false;
|
||||||
|
for (const tagButton of tagButtons) {
|
||||||
|
if (tagButton.dataset.value === tag.value) {
|
||||||
|
tagButton.classList.remove("btn-outline-primary");
|
||||||
|
tagButton.classList.add("btn-primary");
|
||||||
|
this.#filterSuggestedAccounts(tagButton);
|
||||||
|
isMatched = true;
|
||||||
|
} else {
|
||||||
|
tagButton.classList.remove("btn-primary");
|
||||||
|
tagButton.classList.add("btn-outline-primary");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isMatched) {
|
||||||
|
this.#filterSuggestedAccounts(null);
|
||||||
|
}
|
||||||
|
updateSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the suggested accounts.
|
||||||
|
*
|
||||||
|
* @param tagButton {HTMLButtonElement|null} the tag button
|
||||||
|
*/
|
||||||
|
#filterSuggestedAccounts(tagButton) {
|
||||||
|
const accountButtons = Array.from(document.getElementsByClassName(this.#prefix + "-account"));
|
||||||
|
if (tagButton === null) {
|
||||||
|
for (const accountButton of accountButtons) {
|
||||||
|
accountButton.classList.add("d-none");
|
||||||
|
accountButton.classList.remove("btn-primary");
|
||||||
|
accountButton.classList.add("btn-outline-primary");
|
||||||
|
this.#selectAccount(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const suggested = JSON.parse(tagButton.dataset.accounts);
|
||||||
|
for (const accountButton of accountButtons) {
|
||||||
|
if (suggested.includes(accountButton.dataset.code)) {
|
||||||
|
accountButton.classList.remove("d-none");
|
||||||
|
} else {
|
||||||
|
accountButton.classList.add("d-none");
|
||||||
|
}
|
||||||
|
this.#selectAccount(suggested[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the number helper.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#initializeNumberHelper() {
|
||||||
|
const summary = document.getElementById(this.#prefix + "-summary");
|
||||||
|
const number = document.getElementById(this.#prefix + "-number");
|
||||||
|
number.onchange = function () {
|
||||||
|
const found = summary.value.match(/^(.+)×(\d+)$/);
|
||||||
|
if (found !== null) {
|
||||||
|
summary.value = found[1];
|
||||||
|
}
|
||||||
|
if (number.value > 1) {
|
||||||
|
summary.value = summary.value + "×" + String(number.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the suggested accounts.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#initializeSuggestedAccounts() {
|
||||||
|
const accountButtons = Array.from(document.getElementsByClassName(this.#prefix + "-account"));
|
||||||
|
const helper = this;
|
||||||
|
for (const accountButton of accountButtons) {
|
||||||
|
accountButton.onclick = function () {
|
||||||
|
helper.#selectAccount(accountButton.dataset.code);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a suggested account.
|
||||||
|
*
|
||||||
|
* @param selectedCode {string|null} the account code, or null to deselect the account
|
||||||
|
*/
|
||||||
|
#selectAccount(selectedCode) {
|
||||||
|
const form = document.getElementById(this.#prefix);
|
||||||
|
if (selectedCode === null) {
|
||||||
|
form.dataset.selectedAccountCode = "";
|
||||||
|
form.dataset.selectedAccountText = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const accountButtons = Array.from(document.getElementsByClassName(this.#prefix + "-account"));
|
||||||
|
for (const accountButton of accountButtons) {
|
||||||
|
if (accountButton.dataset.code === selectedCode) {
|
||||||
|
accountButton.classList.remove("btn-outline-primary");
|
||||||
|
accountButton.classList.add("btn-primary");
|
||||||
|
form.dataset.selectedAccountCode = accountButton.dataset.code;
|
||||||
|
form.dataset.selectedAccountText = accountButton.dataset.text;
|
||||||
|
} else {
|
||||||
|
accountButton.classList.remove("btn-primary");
|
||||||
|
accountButton.classList.add("btn-outline-primary");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the summary submission
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#initializeSubmission() {
|
||||||
|
const form = document.getElementById(this.#prefix);
|
||||||
|
const helper = this;
|
||||||
|
form.onsubmit = function () {
|
||||||
|
if (helper.#validate()) {
|
||||||
|
helper.#submit();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the form.
|
||||||
|
*
|
||||||
|
* @return {boolean} true if valid, or false otherwise
|
||||||
|
*/
|
||||||
|
#validate() {
|
||||||
|
const tabs = Array.from(document.getElementsByClassName(this.#prefix + "-tab"));
|
||||||
|
let isValid = true;
|
||||||
|
for (const tab of tabs) {
|
||||||
|
if (tab.classList.contains("active")) {
|
||||||
|
switch (tab.dataset.tabId) {
|
||||||
|
case "general":
|
||||||
|
isValid = this.#validateGeneralTag() && isValid;
|
||||||
|
break;
|
||||||
|
case "travel":
|
||||||
|
isValid = this.#validateGeneralTrip() && isValid;
|
||||||
|
break;
|
||||||
|
case "bus":
|
||||||
|
isValid = this.#validateBusTrip() && isValid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a general tag.
|
||||||
|
*
|
||||||
|
* @return {boolean} true if valid, or false otherwise
|
||||||
|
*/
|
||||||
|
#validateGeneralTag() {
|
||||||
|
const field = document.getElementById(this.#prefix + "-general-tag");
|
||||||
|
const error = document.getElementById(this.#prefix + "-general-tag-error");
|
||||||
|
field.value = field.value.trim();
|
||||||
|
field.classList.remove("is-invalid");
|
||||||
|
error.innerText = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a general trip.
|
||||||
|
*
|
||||||
|
* @return {boolean} true if valid, or false otherwise
|
||||||
|
*/
|
||||||
|
#validateGeneralTrip() {
|
||||||
|
let isValid = true;
|
||||||
|
isValid = this.#validateGeneralTripTag() && isValid;
|
||||||
|
isValid = this.#validateGeneralTripFrom() && isValid;
|
||||||
|
isValid = this.#validateGeneralTripTo() && isValid;
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the tag of a general trip.
|
||||||
|
*
|
||||||
|
* @return {boolean} true if valid, or false otherwise
|
||||||
|
*/
|
||||||
|
#validateGeneralTripTag() {
|
||||||
|
const field = document.getElementById(this.#prefix + "-travel-tag");
|
||||||
|
const error = document.getElementById(this.#prefix + "-travel-tag-error");
|
||||||
|
field.value = field.value.trim();
|
||||||
|
if (field.value === "") {
|
||||||
|
field.classList.add("is-invalid");
|
||||||
|
error.innerText = A_("Please fill in the tag.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
field.classList.remove("is-invalid");
|
||||||
|
error.innerText = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the origin of a general trip.
|
||||||
|
*
|
||||||
|
* @return {boolean} true if valid, or false otherwise
|
||||||
|
*/
|
||||||
|
#validateGeneralTripFrom() {
|
||||||
|
const field = document.getElementById(this.#prefix + "-travel-from");
|
||||||
|
const error = document.getElementById(this.#prefix + "-travel-from-error");
|
||||||
|
field.value = field.value.trim();
|
||||||
|
if (field.value === "") {
|
||||||
|
field.classList.add("is-invalid");
|
||||||
|
error.innerText = A_("Please fill in the origin.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
field.classList.remove("is-invalid");
|
||||||
|
error.innerText = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the destination of a general trip.
|
||||||
|
*
|
||||||
|
* @return {boolean} true if valid, or false otherwise
|
||||||
|
*/
|
||||||
|
#validateGeneralTripTo() {
|
||||||
|
const field = document.getElementById(this.#prefix + "-travel-to");
|
||||||
|
const error = document.getElementById(this.#prefix + "-travel-to-error");
|
||||||
|
field.value = field.value.trim();
|
||||||
|
if (field.value === "") {
|
||||||
|
field.classList.add("is-invalid");
|
||||||
|
error.innerText = A_("Please fill in the destination.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
field.classList.remove("is-invalid");
|
||||||
|
error.innerText = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a bus trip.
|
||||||
|
*
|
||||||
|
* @return {boolean} true if valid, or false otherwise
|
||||||
|
*/
|
||||||
|
#validateBusTrip() {
|
||||||
|
let isValid = true;
|
||||||
|
isValid = this.#validateBusTripTag() && isValid;
|
||||||
|
isValid = this.#validateBusTripRoute() && isValid;
|
||||||
|
isValid = this.#validateBusTripFrom() && isValid;
|
||||||
|
isValid = this.#validateBusTripTo() && isValid;
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the tag of a bus trip.
|
||||||
|
*
|
||||||
|
* @return {boolean} true if valid, or false otherwise
|
||||||
|
*/
|
||||||
|
#validateBusTripTag() {
|
||||||
|
const field = document.getElementById(this.#prefix + "-bus-tag");
|
||||||
|
const error = document.getElementById(this.#prefix + "-bus-tag-error");
|
||||||
|
field.value = field.value.trim();
|
||||||
|
if (field.value === "") {
|
||||||
|
field.classList.add("is-invalid");
|
||||||
|
error.innerText = A_("Please fill in the tag.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
field.classList.remove("is-invalid");
|
||||||
|
error.innerText = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the route of a bus trip.
|
||||||
|
*
|
||||||
|
* @return {boolean} true if valid, or false otherwise
|
||||||
|
*/
|
||||||
|
#validateBusTripRoute() {
|
||||||
|
const field = document.getElementById(this.#prefix + "-bus-route");
|
||||||
|
const error = document.getElementById(this.#prefix + "-bus-route-error");
|
||||||
|
field.value = field.value.trim();
|
||||||
|
if (field.value === "") {
|
||||||
|
field.classList.add("is-invalid");
|
||||||
|
error.innerText = A_("Please fill in the route.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
field.classList.remove("is-invalid");
|
||||||
|
error.innerText = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the origin of a bus trip.
|
||||||
|
*
|
||||||
|
* @return {boolean} true if valid, or false otherwise
|
||||||
|
*/
|
||||||
|
#validateBusTripFrom() {
|
||||||
|
const field = document.getElementById(this.#prefix + "-bus-from");
|
||||||
|
const error = document.getElementById(this.#prefix + "-bus-from-error");
|
||||||
|
field.value = field.value.trim();
|
||||||
|
if (field.value === "") {
|
||||||
|
field.classList.add("is-invalid");
|
||||||
|
error.innerText = A_("Please fill in the origin.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
field.classList.remove("is-invalid");
|
||||||
|
error.innerText = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the destination of a bus trip.
|
||||||
|
*
|
||||||
|
* @return {boolean} true if valid, or false otherwise
|
||||||
|
*/
|
||||||
|
#validateBusTripTo() {
|
||||||
|
const field = document.getElementById(this.#prefix + "-bus-to");
|
||||||
|
const error = document.getElementById(this.#prefix + "-bus-to-error");
|
||||||
|
field.value = field.value.trim();
|
||||||
|
if (field.value === "") {
|
||||||
|
field.classList.add("is-invalid");
|
||||||
|
error.innerText = A_("Please fill in the destination.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
field.classList.remove("is-invalid");
|
||||||
|
error.innerText = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the summary.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#submit() {
|
||||||
|
const form = document.getElementById(this.#prefix);
|
||||||
|
const summary = document.getElementById(this.#prefix + "-summary");
|
||||||
|
const formSummaryControl = document.getElementById("accounting-entry-form-summary-control");
|
||||||
|
const formSummary = document.getElementById("accounting-entry-form-summary");
|
||||||
|
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
|
||||||
|
const formAccount = document.getElementById("accounting-entry-form-account");
|
||||||
|
const helperModal = document.getElementById(this.#prefix + "-modal");
|
||||||
|
const entryModal = document.getElementById("accounting-entry-form-modal");
|
||||||
|
if (summary.value === "") {
|
||||||
|
formSummaryControl.classList.remove("accounting-not-empty");
|
||||||
|
} else {
|
||||||
|
formSummaryControl.classList.add("accounting-not-empty");
|
||||||
|
}
|
||||||
|
if (form.dataset.selectedAccountCode !== "") {
|
||||||
|
formAccountControl.classList.add("accounting-not-empty");
|
||||||
|
formAccount.dataset.code = form.dataset.selectedAccountCode;
|
||||||
|
formAccount.dataset.text = form.dataset.selectedAccountText;
|
||||||
|
formAccount.innerText = form.dataset.selectedAccountText;
|
||||||
|
}
|
||||||
|
formSummary.dataset.value = summary.value;
|
||||||
|
formSummary.innerText = summary.value;
|
||||||
|
bootstrap.Modal.getInstance(helperModal).hide();
|
||||||
|
bootstrap.Modal.getOrCreateInstance(entryModal).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the summary helper when it is shown.
|
||||||
|
*
|
||||||
|
* @param isNew {boolean} true for adding a new journal entry, or false otherwise
|
||||||
|
*/
|
||||||
|
initShow(isNew) {
|
||||||
|
const formSummary = document.getElementById("accounting-entry-form-summary");
|
||||||
|
const summary = document.getElementById(this.#prefix + "-summary");
|
||||||
|
const closeButtons = Array.from(document.getElementsByClassName(this.#prefix + "-close"));
|
||||||
|
for (const closeButton of closeButtons) {
|
||||||
|
if (isNew) {
|
||||||
|
closeButton.dataset.bsTarget = "#" + this.#prefix + "-modal";
|
||||||
|
} else {
|
||||||
|
closeButton.dataset.bsTarget = "#accounting-entry-form-modal";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#reset();
|
||||||
|
if (!isNew) {
|
||||||
|
summary.value = formSummary.dataset.value;
|
||||||
|
this.#parseAndPopulate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the summary helper.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#reset() {
|
||||||
|
const inputs = Array.from(document.getElementsByClassName(this.#prefix + "-input"));
|
||||||
|
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-btn-tag"));
|
||||||
|
const directionButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-direction"));
|
||||||
|
for (const input of inputs) {
|
||||||
|
input.value = "";
|
||||||
|
input.classList.remove("is-invalid");
|
||||||
|
}
|
||||||
|
for (const tagButton of tagButtons) {
|
||||||
|
tagButton.classList.remove("btn-primary");
|
||||||
|
tagButton.classList.add("btn-outline-primary");
|
||||||
|
}
|
||||||
|
for (const directionButton of directionButtons) {
|
||||||
|
if (directionButton.classList.contains("accounting-default")) {
|
||||||
|
directionButton.classList.remove("btn-outline-primary");
|
||||||
|
directionButton.classList.add("btn-primary");
|
||||||
|
} else {
|
||||||
|
directionButton.classList.add("btn-outline-primary");
|
||||||
|
directionButton.classList.remove("btn-primary");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#filterSuggestedAccounts(null);
|
||||||
|
this.#switchToTab(this.#defaultTabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the summary input and populates the summary helper.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#parseAndPopulate() {
|
||||||
|
const summary = document.getElementById(this.#prefix + "-summary");
|
||||||
|
const pos = summary.value.indexOf("—");
|
||||||
|
if (pos === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let found;
|
||||||
|
found = summary.value.match(/^([^—]+)—([^—]+)—([^—→]+)→(.+?)(?:×(\d+))?$/);
|
||||||
|
if (found !== null) {
|
||||||
|
return this.#populateBusTrip(found[1], found[2], found[3], found[4], found[5]);
|
||||||
|
}
|
||||||
|
found = summary.value.match(/^([^—]+)—([^—→↔]+)([→↔])(.+?)(?:×(\d+))?$/);
|
||||||
|
if (found !== null) {
|
||||||
|
return this.#populateGeneralTrip(found[1], found[2], found[3], found[4], found[5]);
|
||||||
|
}
|
||||||
|
found = summary.value.match(/^([^—]+)—.+?(?:×(\d+)?)?$/);
|
||||||
|
if (found !== null) {
|
||||||
|
return this.#populateGeneralTag(found[1], found[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates a bus trip.
|
||||||
|
*
|
||||||
|
* @param tagName {string} the tag name
|
||||||
|
* @param routeName {string} the route name or route number
|
||||||
|
* @param fromName {string} the name of the origin
|
||||||
|
* @param toName {string} the name of the destination
|
||||||
|
* @param numberStr {string|undefined} the number of items, if any
|
||||||
|
*/
|
||||||
|
#populateBusTrip(tagName, routeName, fromName, toName, numberStr) {
|
||||||
|
const tag = document.getElementById(this.#prefix + "-bus-tag");
|
||||||
|
const route = document.getElementById(this.#prefix + "-bus-route");
|
||||||
|
const from = document.getElementById(this.#prefix + "-bus-from");
|
||||||
|
const to = document.getElementById(this.#prefix + "-bus-to");
|
||||||
|
const number = document.getElementById(this.#prefix + "-number");
|
||||||
|
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-bus-btn-tag"));
|
||||||
|
tag.value = tagName;
|
||||||
|
route.value = routeName;
|
||||||
|
from.value = fromName;
|
||||||
|
to.value = toName;
|
||||||
|
if (numberStr !== undefined) {
|
||||||
|
number.value = parseInt(numberStr);
|
||||||
|
}
|
||||||
|
for (const tagButton of tagButtons) {
|
||||||
|
if (tagButton.dataset.value === tagName) {
|
||||||
|
tagButton.classList.remove("btn-outline-primary");
|
||||||
|
tagButton.classList.add("btn-primary");
|
||||||
|
this.#filterSuggestedAccounts(tagButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#switchToTab("bus");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates a general trip.
|
||||||
|
*
|
||||||
|
* @param tagName {string} the tag name
|
||||||
|
* @param fromName {string} the name of the origin
|
||||||
|
* @param direction {string} the direction arrow, either "→" or "↔"
|
||||||
|
* @param toName {string} the name of the destination
|
||||||
|
* @param numberStr {string|undefined} the number of items, if any
|
||||||
|
*/
|
||||||
|
#populateGeneralTrip(tagName, fromName, direction, toName, numberStr) {
|
||||||
|
const tag = document.getElementById(this.#prefix + "-travel-tag");
|
||||||
|
const from = document.getElementById(this.#prefix + "-travel-from");
|
||||||
|
const to = document.getElementById(this.#prefix + "-travel-to");
|
||||||
|
const number = document.getElementById(this.#prefix + "-number");
|
||||||
|
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-btn-tag"));
|
||||||
|
const directionButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-direction"));
|
||||||
|
tag.value = tagName;
|
||||||
|
from.value = fromName;
|
||||||
|
for (const directionButton of directionButtons) {
|
||||||
|
if (directionButton.dataset.arrow === direction) {
|
||||||
|
directionButton.classList.remove("btn-outline-primary");
|
||||||
|
directionButton.classList.add("btn-primary");
|
||||||
|
} else {
|
||||||
|
directionButton.classList.add("btn-outline-primary");
|
||||||
|
directionButton.classList.remove("btn-primary");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
to.value = toName;
|
||||||
|
if (numberStr !== undefined) {
|
||||||
|
number.value = parseInt(numberStr);
|
||||||
|
}
|
||||||
|
for (const tagButton of tagButtons) {
|
||||||
|
if (tagButton.dataset.value === tagName) {
|
||||||
|
tagButton.classList.remove("btn-outline-primary");
|
||||||
|
tagButton.classList.add("btn-primary");
|
||||||
|
this.#filterSuggestedAccounts(tagButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#switchToTab("travel");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates a general tag.
|
||||||
|
*
|
||||||
|
* @param tagName {string} the tag name
|
||||||
|
* @param numberStr {string|undefined} the number of items, if any
|
||||||
|
*/
|
||||||
|
#populateGeneralTag(tagName, numberStr) {
|
||||||
|
const tag = document.getElementById(this.#prefix + "-general-tag");
|
||||||
|
const number = document.getElementById(this.#prefix + "-number");
|
||||||
|
const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-general-btn-tag"));
|
||||||
|
tag.value = tagName;
|
||||||
|
if (numberStr !== undefined) {
|
||||||
|
number.value = parseInt(numberStr);
|
||||||
|
}
|
||||||
|
for (const tagButton of tagButtons) {
|
||||||
|
if (tagButton.dataset.value === tagName) {
|
||||||
|
tagButton.classList.remove("btn-outline-primary");
|
||||||
|
tagButton.classList.add("btn-primary");
|
||||||
|
this.#filterSuggestedAccounts(tagButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#switchToTab("general");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The summary helpers.
|
||||||
|
* @type {{debit: SummaryHelper, credit: SummaryHelper}}
|
||||||
|
*/
|
||||||
|
static #helpers = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the summary helpers.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
static initialize() {
|
||||||
|
const forms = Array.from(document.getElementsByClassName("accounting-summary-helper"));
|
||||||
|
for (const form of forms) {
|
||||||
|
const helper = new SummaryHelper(form);
|
||||||
|
this.#helpers[helper.#entryType] = helper;
|
||||||
|
}
|
||||||
|
this.#initializeTransactionForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the transaction form.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
static #initializeTransactionForm() {
|
||||||
|
const entryForm = document.getElementById("accounting-entry-form");
|
||||||
|
const formSummaryControl = document.getElementById("accounting-entry-form-summary-control");
|
||||||
|
const helpers = this.#helpers;
|
||||||
|
formSummaryControl.onclick = function () {
|
||||||
|
helpers[entryForm.dataset.entryType].initShow(false);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the summary helper for a new journal entry.
|
||||||
|
*
|
||||||
|
* @param entryType {string} the entry type, either "debit" or "credit"
|
||||||
|
*/
|
||||||
|
static initializeNewJournalEntry(entryType) {
|
||||||
|
this.#helpers[entryType].initShow(true);
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,6 @@
|
|||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
initializeCurrencyForms();
|
initializeCurrencyForms();
|
||||||
initializeJournalEntries();
|
initializeJournalEntries();
|
||||||
initializeAccountSelectors();
|
|
||||||
initializeFormValidation();
|
initializeFormValidation();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,12 +78,12 @@ function initializeCurrencyForms() {
|
|||||||
btnNew.onclick = function () {
|
btnNew.onclick = function () {
|
||||||
const currencies = Array.from(document.getElementsByClassName("accounting-currency"));
|
const currencies = Array.from(document.getElementsByClassName("accounting-currency"));
|
||||||
let maxIndex = 0;
|
let maxIndex = 0;
|
||||||
currencies.forEach(function (currency) {
|
for (const currency of currencies) {
|
||||||
const index = parseInt(currency.dataset.index);
|
const index = parseInt(currency.dataset.index);
|
||||||
if (maxIndex < index) {
|
if (maxIndex < index) {
|
||||||
maxIndex = index;
|
maxIndex = index;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
const newIndex = String(maxIndex + 1);
|
const newIndex = String(maxIndex + 1);
|
||||||
const html = form.dataset.currencyTemplate
|
const html = form.dataset.currencyTemplate
|
||||||
.replaceAll("CURRENCY_INDEX", escapeHtml(newIndex));
|
.replaceAll("CURRENCY_INDEX", escapeHtml(newIndex));
|
||||||
@ -122,9 +121,9 @@ function initializeBtnDeleteCurrency(button) {
|
|||||||
function resetDeleteCurrencyButtons() {
|
function resetDeleteCurrencyButtons() {
|
||||||
const buttons = Array.from(document.getElementsByClassName("accounting-btn-delete-currency"));
|
const buttons = Array.from(document.getElementsByClassName("accounting-btn-delete-currency"));
|
||||||
if (buttons.length > 1) {
|
if (buttons.length > 1) {
|
||||||
buttons.forEach(function (button) {
|
for (const button of buttons) {
|
||||||
button.classList.remove("d-none");
|
button.classList.remove("d-none");
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
buttons[0].classList.add("d-none");
|
buttons[0].classList.add("d-none");
|
||||||
}
|
}
|
||||||
@ -157,6 +156,7 @@ function initializeNewEntryButton(button) {
|
|||||||
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
|
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
|
||||||
const formAccount = document.getElementById("accounting-entry-form-account");
|
const formAccount = document.getElementById("accounting-entry-form-account");
|
||||||
const formAccountError = document.getElementById("accounting-entry-form-account-error")
|
const formAccountError = document.getElementById("accounting-entry-form-account-error")
|
||||||
|
const formSummaryControl = document.getElementById("accounting-entry-form-summary-control");
|
||||||
const formSummary = document.getElementById("accounting-entry-form-summary");
|
const formSummary = document.getElementById("accounting-entry-form-summary");
|
||||||
const formSummaryError = document.getElementById("accounting-entry-form-summary-error");
|
const formSummaryError = document.getElementById("accounting-entry-form-summary-error");
|
||||||
const formAmount = document.getElementById("accounting-entry-form-amount");
|
const formAmount = document.getElementById("accounting-entry-form-amount");
|
||||||
@ -165,19 +165,23 @@ function initializeNewEntryButton(button) {
|
|||||||
entryForm.dataset.currencyIndex = button.dataset.currencyIndex;
|
entryForm.dataset.currencyIndex = button.dataset.currencyIndex;
|
||||||
entryForm.dataset.entryType = button.dataset.entryType;
|
entryForm.dataset.entryType = button.dataset.entryType;
|
||||||
entryForm.dataset.entryIndex = button.dataset.entryIndex;
|
entryForm.dataset.entryIndex = button.dataset.entryIndex;
|
||||||
formAccountControl.classList.remove("accounting-not-empty")
|
formAccountControl.classList.remove("accounting-not-empty");
|
||||||
formAccountControl.classList.remove("is-invalid");
|
formAccountControl.classList.remove("is-invalid");
|
||||||
formAccountControl.dataset.bsTarget = button.dataset.accountModal;
|
|
||||||
formAccount.innerText = "";
|
formAccount.innerText = "";
|
||||||
formAccount.dataset.code = "";
|
formAccount.dataset.code = "";
|
||||||
formAccount.dataset.text = "";
|
formAccount.dataset.text = "";
|
||||||
formAccountError.innerText = "";
|
formAccountError.innerText = "";
|
||||||
formSummary.value = "";
|
formSummaryControl.dataset.bsTarget = "#accounting-summary-helper-" + button.dataset.entryType + "-modal";
|
||||||
formSummary.classList.remove("is-invalid");
|
formSummaryControl.classList.remove("accounting-not-empty");
|
||||||
|
formSummaryControl.classList.remove("is-invalid");
|
||||||
|
formSummary.dataset.value = "";
|
||||||
|
formSummary.innerText = ""
|
||||||
formSummaryError.innerText = ""
|
formSummaryError.innerText = ""
|
||||||
formAmount.value = "";
|
formAmount.value = "";
|
||||||
formAmount.classList.remove("is-invalid");
|
formAmount.classList.remove("is-invalid");
|
||||||
formAmountError.innerText = "";
|
formAmountError.innerText = "";
|
||||||
|
AccountSelector.initializeJournalEntryForm();
|
||||||
|
SummaryHelper.initializeNewJournalEntry(button.dataset.entryType);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,6 +213,7 @@ function initializeJournalEntry(entry) {
|
|||||||
const control = document.getElementById(entry.dataset.prefix + "-control");
|
const control = document.getElementById(entry.dataset.prefix + "-control");
|
||||||
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
|
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
|
||||||
const formAccount = document.getElementById("accounting-entry-form-account");
|
const formAccount = document.getElementById("accounting-entry-form-account");
|
||||||
|
const formSummaryControl = document.getElementById("accounting-entry-form-summary-control");
|
||||||
const formSummary = document.getElementById("accounting-entry-form-summary");
|
const formSummary = document.getElementById("accounting-entry-form-summary");
|
||||||
const formAmount = document.getElementById("accounting-entry-form-amount");
|
const formAmount = document.getElementById("accounting-entry-form-amount");
|
||||||
control.onclick = function () {
|
control.onclick = function () {
|
||||||
@ -220,12 +225,19 @@ function initializeJournalEntry(entry) {
|
|||||||
} else {
|
} else {
|
||||||
formAccountControl.classList.add("accounting-not-empty");
|
formAccountControl.classList.add("accounting-not-empty");
|
||||||
}
|
}
|
||||||
formAccountControl.dataset.bsTarget = entry.dataset.accountModal;
|
|
||||||
formAccount.innerText = accountCode.dataset.text;
|
formAccount.innerText = accountCode.dataset.text;
|
||||||
formAccount.dataset.code = accountCode.value;
|
formAccount.dataset.code = accountCode.value;
|
||||||
formAccount.dataset.text = accountCode.dataset.text;
|
formAccount.dataset.text = accountCode.dataset.text;
|
||||||
formSummary.value = summary.value;
|
formSummaryControl.dataset.bsTarget = "#accounting-summary-helper-" + entry.dataset.entryType + "-modal";
|
||||||
|
if (summary.value === "") {
|
||||||
|
formSummaryControl.classList.remove("accounting-not-empty");
|
||||||
|
} else {
|
||||||
|
formSummaryControl.classList.add("accounting-not-empty");
|
||||||
|
}
|
||||||
|
formSummary.dataset.value = summary.value;
|
||||||
|
formSummary.innerText = summary.value;
|
||||||
formAmount.value = amount.value;
|
formAmount.value = amount.value;
|
||||||
|
AccountSelector.initializeJournalEntryForm();
|
||||||
validateJournalEntryForm();
|
validateJournalEntryForm();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -237,38 +249,8 @@ function initializeJournalEntry(entry) {
|
|||||||
*/
|
*/
|
||||||
function initializeJournalEntryFormModal() {
|
function initializeJournalEntryFormModal() {
|
||||||
const entryForm = document.getElementById("accounting-entry-form");
|
const entryForm = document.getElementById("accounting-entry-form");
|
||||||
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
|
|
||||||
const formAccount = document.getElementById("accounting-entry-form-account");
|
|
||||||
const formSummary = document.getElementById("accounting-entry-form-summary");
|
|
||||||
const formAmount = document.getElementById("accounting-entry-form-amount");
|
const formAmount = document.getElementById("accounting-entry-form-amount");
|
||||||
const modal = document.getElementById("accounting-entry-form-modal");
|
const modal = document.getElementById("accounting-entry-form-modal");
|
||||||
formAccountControl.onclick = function () {
|
|
||||||
const prefix = "accounting-" + entryForm.dataset.entryType + "-account";
|
|
||||||
const query = document.getElementById(prefix + "-selector-query")
|
|
||||||
const more = document.getElementById(prefix + "-more");
|
|
||||||
const options = Array.from(document.getElementsByClassName(prefix + "-option"));
|
|
||||||
const btnClear = document.getElementById(prefix + "-btn-clear");
|
|
||||||
query.value = "";
|
|
||||||
more.classList.remove("d-none");
|
|
||||||
filterAccountOptions(prefix);
|
|
||||||
options.forEach(function (option) {
|
|
||||||
if (option.dataset.code === formAccount.dataset.code) {
|
|
||||||
option.classList.add("active");
|
|
||||||
} else {
|
|
||||||
option.classList.remove("active");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (formAccount.dataset.code === "") {
|
|
||||||
btnClear.classList.add("btn-secondary");
|
|
||||||
btnClear.classList.remove("btn-danger");
|
|
||||||
btnClear.disabled = true;
|
|
||||||
} else {
|
|
||||||
btnClear.classList.add("btn-danger");
|
|
||||||
btnClear.classList.remove("btn-secondary");
|
|
||||||
btnClear.disabled = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
formSummary.onchange = validateJournalEntrySummary;
|
|
||||||
formAmount.onchange = validateJournalEntryAmount;
|
formAmount.onchange = validateJournalEntryAmount;
|
||||||
entryForm.onsubmit = function () {
|
entryForm.onsubmit = function () {
|
||||||
if (validateJournalEntryForm()) {
|
if (validateJournalEntryForm()) {
|
||||||
@ -297,7 +279,6 @@ function validateJournalEntryForm() {
|
|||||||
* Validates the account in the journal entry form modal.
|
* Validates the account in the journal entry form modal.
|
||||||
*
|
*
|
||||||
* @return {boolean} true if valid, or false otherwise
|
* @return {boolean} true if valid, or false otherwise
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
function validateJournalEntryAccount() {
|
function validateJournalEntryAccount() {
|
||||||
const field = document.getElementById("accounting-entry-form-account");
|
const field = document.getElementById("accounting-entry-form-account");
|
||||||
@ -320,10 +301,9 @@ function validateJournalEntryAccount() {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function validateJournalEntrySummary() {
|
function validateJournalEntrySummary() {
|
||||||
const field = document.getElementById("accounting-entry-form-summary");
|
const control = document.getElementById("accounting-entry-form-summary-control");
|
||||||
const error = document.getElementById("accounting-entry-form-summary-error");
|
const error = document.getElementById("accounting-entry-form-summary-error");
|
||||||
field.value = field.value.trim();
|
control.classList.remove("is-invalid");
|
||||||
field.classList.remove("is-invalid");
|
|
||||||
error.innerText = "";
|
error.innerText = "";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -366,12 +346,12 @@ function saveJournalEntryForm() {
|
|||||||
const entries = Array.from(document.getElementsByClassName("accounting-currency-" + currencyIndex + "-" + entryType));
|
const entries = Array.from(document.getElementsByClassName("accounting-currency-" + currencyIndex + "-" + entryType));
|
||||||
const entryList = document.getElementById("accounting-currency-" + currencyIndex + "-" + entryType + "-list")
|
const entryList = document.getElementById("accounting-currency-" + currencyIndex + "-" + entryType + "-list")
|
||||||
let maxIndex = 0;
|
let maxIndex = 0;
|
||||||
entries.forEach(function (entry) {
|
for (const entry of entries) {
|
||||||
const index = parseInt(entry.dataset.entryIndex);
|
const index = parseInt(entry.dataset.entryIndex);
|
||||||
if (maxIndex < index) {
|
if (maxIndex < index) {
|
||||||
maxIndex = index;
|
maxIndex = index;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
entryIndex = String(maxIndex + 1);
|
entryIndex = String(maxIndex + 1);
|
||||||
const html = form.dataset.entryTemplate
|
const html = form.dataset.entryTemplate
|
||||||
.replaceAll("CURRENCY_INDEX", escapeHtml(currencyIndex))
|
.replaceAll("CURRENCY_INDEX", escapeHtml(currencyIndex))
|
||||||
@ -393,8 +373,8 @@ function saveJournalEntryForm() {
|
|||||||
accountCode.value = formAccount.dataset.code;
|
accountCode.value = formAccount.dataset.code;
|
||||||
accountCode.dataset.text = formAccount.dataset.text;
|
accountCode.dataset.text = formAccount.dataset.text;
|
||||||
accountText.innerText = formAccount.dataset.text;
|
accountText.innerText = formAccount.dataset.text;
|
||||||
summary.value = formSummary.value;
|
summary.value = formSummary.dataset.value;
|
||||||
summaryText.innerText = formSummary.value;
|
summaryText.innerText = formSummary.dataset.value;
|
||||||
amount.value = formAmount.value;
|
amount.value = formAmount.value;
|
||||||
amountText.innerText = formatDecimal(new Decimal(formAmount.value));
|
amountText.innerText = formatDecimal(new Decimal(formAmount.value));
|
||||||
if (entryForm.dataset.entryIndex === "new") {
|
if (entryForm.dataset.entryIndex === "new") {
|
||||||
@ -436,9 +416,9 @@ function initializeDeleteJournalEntryButton(button) {
|
|||||||
function resetDeleteJournalEntryButtons(sameClass) {
|
function resetDeleteJournalEntryButtons(sameClass) {
|
||||||
const buttons = Array.from(document.getElementsByClassName(sameClass));
|
const buttons = Array.from(document.getElementsByClassName(sameClass));
|
||||||
if (buttons.length > 1) {
|
if (buttons.length > 1) {
|
||||||
buttons.forEach(function (button) {
|
for (const button of buttons) {
|
||||||
button.classList.remove("d-none");
|
button.classList.remove("d-none");
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
buttons[0].classList.add("d-none");
|
buttons[0].classList.add("d-none");
|
||||||
}
|
}
|
||||||
@ -456,147 +436,14 @@ function updateBalance(currencyIndex, entryType) {
|
|||||||
const amounts = Array.from(document.getElementsByClassName(prefix + "-amount"));
|
const amounts = Array.from(document.getElementsByClassName(prefix + "-amount"));
|
||||||
const totalText = document.getElementById(prefix + "-total");
|
const totalText = document.getElementById(prefix + "-total");
|
||||||
let total = new Decimal("0");
|
let total = new Decimal("0");
|
||||||
amounts.forEach(function (amount) {
|
for (const amount of amounts) {
|
||||||
if (amount.value !== "") {
|
if (amount.value !== "") {
|
||||||
total = total.plus(new Decimal(amount.value));
|
total = total.plus(new Decimal(amount.value));
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
totalText.innerText = formatDecimal(total);
|
totalText.innerText = formatDecimal(total);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the account selectors.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function initializeAccountSelectors() {
|
|
||||||
const selectors = Array.from(document.getElementsByClassName("accounting-selector-modal"));
|
|
||||||
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
|
|
||||||
const formAccount = document.getElementById("accounting-entry-form-account");
|
|
||||||
selectors.forEach(function (selector) {
|
|
||||||
const more = document.getElementById(selector.dataset.prefix + "-more");
|
|
||||||
const btnClear = document.getElementById(selector.dataset.prefix + "-btn-clear");
|
|
||||||
const options = Array.from(document.getElementsByClassName(selector.dataset.prefix + "-option"));
|
|
||||||
more.onclick = function () {
|
|
||||||
more.classList.add("d-none");
|
|
||||||
filterAccountOptions(selector.dataset.prefix);
|
|
||||||
};
|
|
||||||
initializeAccountQuery(selector);
|
|
||||||
btnClear.onclick = function () {
|
|
||||||
formAccountControl.classList.remove("accounting-not-empty");
|
|
||||||
formAccount.innerText = "";
|
|
||||||
formAccount.dataset.code = "";
|
|
||||||
formAccount.dataset.text = "";
|
|
||||||
validateJournalEntryAccount();
|
|
||||||
};
|
|
||||||
options.forEach(function (option) {
|
|
||||||
option.onclick = function () {
|
|
||||||
formAccountControl.classList.add("accounting-not-empty");
|
|
||||||
formAccount.innerText = option.dataset.content;
|
|
||||||
formAccount.dataset.code = option.dataset.code;
|
|
||||||
formAccount.dataset.text = option.dataset.content;
|
|
||||||
validateJournalEntryAccount();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the query on the account options.
|
|
||||||
*
|
|
||||||
* @param selector {HTMLDivElement} the selector modal
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function initializeAccountQuery(selector) {
|
|
||||||
const query = document.getElementById(selector.dataset.prefix + "-selector-query");
|
|
||||||
query.addEventListener("input", function () {
|
|
||||||
filterAccountOptions(selector.dataset.prefix);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters the account options.
|
|
||||||
*
|
|
||||||
* @param prefix {string} the HTML ID and class prefix
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function filterAccountOptions(prefix) {
|
|
||||||
const query = document.getElementById(prefix + "-selector-query");
|
|
||||||
const optionList = document.getElementById(prefix + "-option-list");
|
|
||||||
if (optionList === null) {
|
|
||||||
console.log(prefix + "-option-list");
|
|
||||||
}
|
|
||||||
const options = Array.from(document.getElementsByClassName(prefix + "-option"));
|
|
||||||
const more = document.getElementById(prefix + "-more");
|
|
||||||
const queryNoResult = document.getElementById(prefix + "-option-no-result");
|
|
||||||
const codesInUse = getAccountCodeUsedInForm();
|
|
||||||
let shouldAnyShow = false;
|
|
||||||
options.forEach(function (option) {
|
|
||||||
const shouldShow = shouldAccountOptionShow(option, more, codesInUse, query);
|
|
||||||
if (shouldShow) {
|
|
||||||
option.classList.remove("d-none");
|
|
||||||
shouldAnyShow = true;
|
|
||||||
} else {
|
|
||||||
option.classList.add("d-none");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!shouldAnyShow && more.classList.contains("d-none")) {
|
|
||||||
optionList.classList.add("d-none");
|
|
||||||
queryNoResult.classList.remove("d-none");
|
|
||||||
} else {
|
|
||||||
optionList.classList.remove("d-none");
|
|
||||||
queryNoResult.classList.add("d-none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether an account option should show.
|
|
||||||
*
|
|
||||||
* @param option {HTMLLIElement} the account option
|
|
||||||
* @param more {HTMLLIElement} the more account 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 account option should show, or false otherwise
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function shouldAccountOptionShow(option, more, inUse, query) {
|
|
||||||
const isQueryMatched = function () {
|
|
||||||
if (query.value === "") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const queryValues = JSON.parse(option.dataset.queryValues);
|
|
||||||
for (const queryValue of queryValues) {
|
|
||||||
if (queryValue.includes(query.value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
const isMoreMatched = function () {
|
|
||||||
if (more.classList.contains("d-none")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return option.classList.contains("accounting-account-in-use") || inUse.includes(option.dataset.code);
|
|
||||||
};
|
|
||||||
return isMoreMatched() && isQueryMatched();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the account codes that are used in the form.
|
|
||||||
*
|
|
||||||
* @return {string[]} the account codes that are used in the form
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function getAccountCodeUsedInForm() {
|
|
||||||
const accountCodes = Array.from(document.getElementsByClassName("accounting-account-code"));
|
|
||||||
const formAccount = document.getElementById("accounting-entry-form-account");
|
|
||||||
const inUse = [formAccount.dataset.code];
|
|
||||||
accountCodes.forEach(function (accountCode) {
|
|
||||||
inUse.push(accountCode.value);
|
|
||||||
});
|
|
||||||
return inUse
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the form validation.
|
* Initializes the form validation.
|
||||||
*
|
*
|
||||||
@ -655,9 +502,9 @@ function validateCurrencies() {
|
|||||||
const currencies = Array.from(document.getElementsByClassName("accounting-currency"));
|
const currencies = Array.from(document.getElementsByClassName("accounting-currency"));
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
isValid = validateCurrenciesReal() && isValid;
|
isValid = validateCurrenciesReal() && isValid;
|
||||||
currencies.forEach(function (currency) {
|
for (const currency of currencies) {
|
||||||
isValid = validateCurrency(currency) && isValid;
|
isValid = validateCurrency(currency) && isValid;
|
||||||
});
|
}
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -718,9 +565,9 @@ function validateJournalEntries(currency, entryType) {
|
|||||||
const entries = Array.from(document.getElementsByClassName("accounting-currency-" + currencyIndex + "-" + entryType));
|
const entries = Array.from(document.getElementsByClassName("accounting-currency-" + currencyIndex + "-" + entryType));
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
isValid = validateJournalEntriesReal(currencyIndex, entryType) && isValid;
|
isValid = validateJournalEntriesReal(currencyIndex, entryType) && isValid;
|
||||||
entries.forEach(function (entry) {
|
for (const entry of entries) {
|
||||||
isValid = validateJournalEntry(entry) && isValid;
|
isValid = validateJournalEntry(entry) && isValid;
|
||||||
})
|
}
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -791,17 +638,17 @@ function validateBalance(currency) {
|
|||||||
const creditAmounts = Array.from(document.getElementsByClassName(prefix + "-credit-amount"));
|
const creditAmounts = Array.from(document.getElementsByClassName(prefix + "-credit-amount"));
|
||||||
if (debit !== null && credit !== null) {
|
if (debit !== null && credit !== null) {
|
||||||
let debitTotal = new Decimal("0");
|
let debitTotal = new Decimal("0");
|
||||||
debitAmounts.forEach(function (amount) {
|
for (const amount of debitAmounts) {
|
||||||
if (amount.value !== "") {
|
if (amount.value !== "") {
|
||||||
debitTotal = debitTotal.plus(new Decimal(amount.value));
|
debitTotal = debitTotal.plus(new Decimal(amount.value));
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
let creditTotal = new Decimal("0");
|
let creditTotal = new Decimal("0");
|
||||||
creditAmounts.forEach(function (amount) {
|
for (const amount of creditAmounts) {
|
||||||
if (amount.value !== "") {
|
if (amount.value !== "") {
|
||||||
creditTotal = creditTotal.plus(new Decimal(amount.value));
|
creditTotal = creditTotal.plus(new Decimal(amount.value));
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
if (!debitTotal.equals(creditTotal)) {
|
if (!debitTotal.equals(creditTotal)) {
|
||||||
control.classList.add("is-invalid");
|
control.classList.add("is-invalid");
|
||||||
error.innerText = A_("The totals of the debit and credit amounts do not match.");
|
error.innerText = A_("The totals of the debit and credit amounts do not match.");
|
||||||
|
@ -70,7 +70,7 @@ First written: 2023/2/25
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="debit" data-entry-index="new" data-account-modal="#accounting-debit-account-selector-modal" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">
|
<button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="debit" data-entry-index="new" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -45,6 +45,12 @@ First written: 2023/2/25
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block account_selector_modals %}
|
{% block form_modals %}
|
||||||
{% include "accounting/transaction/include/debit-account-modal.html" %}
|
{% with summary_helper = form.summary_helper.debit %}
|
||||||
|
{% include "accounting/transaction/include/summary-helper-modal.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
{% with entry_type = "debit",
|
||||||
|
account_options = form.debit_account_options %}
|
||||||
|
{% include "accounting/transaction/include/account-selector-modal.html" %}
|
||||||
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
account-selector-modal.html: The modal 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/25
|
||||||
|
#}
|
||||||
|
<div id="accounting-account-selector-{{ entry_type }}-modal" class="modal fade accounting-account-selector-modal" data-entry-type="{{ entry_type }}" tabindex="-1" aria-labelledby="accounting-account-selector-{{ entry_type }}-modal-label" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id="accounting-account-selector-{{ entry_type }}-modal-label">{{ A_("Select Account") }}</h1>
|
||||||
|
<button type="button" class="btn-close" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal" aria-label="{{ A_("Close") }}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<input id="accounting-account-selector-{{ entry_type }}-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
||||||
|
<label class="input-group-text" for="accounting-account-selector-{{ entry_type }}-query">
|
||||||
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
|
{{ A_("Search") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul id="accounting-account-selector-{{ entry_type }}-option-list" class="list-group accounting-selector-list">
|
||||||
|
{% for account in account_options %}
|
||||||
|
<li id="accounting-account-selector-{{ entry_type }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ entry_type }}-option {% if account.is_in_use %} accounting-account-in-use {% endif %}" data-code="{{ account.code }}" data-content="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">
|
||||||
|
{{ account }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
<li id="accounting-account-selector-{{ entry_type }}-more" class="list-group-item accounting-clickable">{{ A_("More…") }}</li>
|
||||||
|
</ul>
|
||||||
|
<p id="accounting-account-selector-{{ entry_type }}-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">{{ A_("Cancel") }}</button>
|
||||||
|
<button id="accounting-account-selector-{{ entry_type }}-btn-clear" type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">{{ A_("Clear") }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,54 +0,0 @@
|
|||||||
{#
|
|
||||||
The Mia! Accounting Flask Project
|
|
||||||
credit-modals.html: The modals for the credit journal entry sub-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/25
|
|
||||||
#}
|
|
||||||
<div id="accounting-credit-account-selector-modal" class="modal fade accounting-selector-modal" data-prefix="accounting-credit-account" tabindex="-1" aria-labelledby="accounting-credit-account-selector-modal-label" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h1 class="modal-title fs-5" id="accounting-credit-account-selector-modal-label">{{ A_("Select Credit Account") }}</h1>
|
|
||||||
<button type="button" class="btn-close" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal" aria-label="{{ A_("Close") }}"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="input-group mb-2">
|
|
||||||
<input id="accounting-credit-account-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
|
||||||
<label class="input-group-text" for="accounting-credit-account-selector-query">
|
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
|
||||||
{{ A_("Search") }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul id="accounting-credit-account-option-list" class="list-group accounting-selector-list">
|
|
||||||
{% for account in form.credit_account_options %}
|
|
||||||
<li id="accounting-credit-account-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-credit-account-option {% if account.is_in_use %} accounting-account-in-use {% endif %}" data-code="{{ account.code }}" data-content="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">
|
|
||||||
{{ account }}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
<li id="accounting-credit-account-more" class="list-group-item accounting-clickable">{{ A_("More…") }}</li>
|
|
||||||
</ul>
|
|
||||||
<p id="accounting-credit-account-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">{{ A_("Cancel") }}</button>
|
|
||||||
<button id="accounting-credit-account-btn-clear" type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">{{ A_("Clear") }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,54 +0,0 @@
|
|||||||
{#
|
|
||||||
The Mia! Accounting Flask Project
|
|
||||||
credit-modals.html: The modals for the debit journal entry sub-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/25
|
|
||||||
#}
|
|
||||||
<div id="accounting-debit-account-selector-modal" class="modal fade accounting-selector-modal" data-prefix="accounting-debit-account" tabindex="-1" aria-labelledby="accounting-debit-account-selector-modal-label" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h1 class="modal-title fs-5" id="accounting-debit-account-selector-modal-label">{{ A_("Select Debit Account") }}</h1>
|
|
||||||
<button type="button" class="btn-close" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal" aria-label="{{ A_("Close") }}"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="input-group mb-2">
|
|
||||||
<input id="accounting-debit-account-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
|
||||||
<label class="input-group-text" for="accounting-debit-account-selector-query">
|
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
|
||||||
{{ A_("Search") }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul id="accounting-debit-account-option-list" class="list-group accounting-selector-list">
|
|
||||||
{% for account in form.debit_account_options %}
|
|
||||||
<li id="accounting-debit-account-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-debit-account-option {% if account.is_in_use %} accounting-account-in-use {% endif %}" data-code="{{ account.code }}" data-content="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">
|
|
||||||
{{ account }}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
<li id="accounting-debit-account-more" class="list-group-item accounting-clickable">{{ A_("More…") }}</li>
|
|
||||||
</ul>
|
|
||||||
<p id="accounting-debit-account-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">{{ A_("Cancel") }}</button>
|
|
||||||
<button id="accounting-debit-account-btn-clear" type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">{{ A_("Clear") }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -36,9 +36,11 @@ First written: 2023/2/25
|
|||||||
<div id="accounting-entry-form-account-error" class="invalid-feedback"></div>
|
<div id="accounting-entry-form-account-error" class="invalid-feedback"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="mb-3">
|
||||||
<input id="accounting-entry-form-summary" class="form-control" type="text" value="" placeholder=" ">
|
<div id="accounting-entry-form-summary-control" class="form-control accounting-clickable accounting-material-text-field" data-bs-toggle="modal" data-bs-target="">
|
||||||
<label for="accounting-entry-form-summary">{{ A_("Summary") }}</label>
|
<label class="form-label" for="accounting-entry-form-summary">{{ A_("Summary") }}</label>
|
||||||
|
<div id="accounting-entry-form-summary" data-value=""></div>
|
||||||
|
</div>
|
||||||
<div id="accounting-entry-form-summary-error" class="invalid-feedback"></div>
|
<div id="accounting-entry-form-summary-error" class="invalid-feedback"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -20,12 +20,12 @@ 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 }}-{{ entry_type }}-{{ entry_index }}" class="list-group-item list-group-item-action d-flex justify-content-between accounting-entry accounting-currency-{{ currency_index }}-{{ entry_type }}" data-currency-index="{{ currency_index }}" data-entry-type="{{ entry_type }}" data-entry-index="{{ entry_index }}" data-account-modal="#accounting-{{ entry_type }}-account-selector-modal" data-prefix="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}">
|
<li id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}" class="list-group-item list-group-item-action d-flex justify-content-between accounting-entry accounting-currency-{{ currency_index }}-{{ entry_type }}" data-currency-index="{{ currency_index }}" data-entry-type="{{ entry_type }}" data-entry-index="{{ entry_index }}" data-prefix="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}">
|
||||||
{% if entry_id %}
|
{% if entry_id %}
|
||||||
<input type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-eid" value="{{ entry_id }}">
|
<input type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-eid" value="{{ entry_id }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-no" type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-no" value="{{ entry_index }}">
|
<input id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-no" type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-no" value="{{ entry_index }}">
|
||||||
<input id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-account-code" class="accounting-account-code" type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-account_code" value="{{ account_code_data }}" data-text="{{ account_text }}">
|
<input id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-account-code" class="accounting-{{ entry_type }}-account-code" type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-account_code" value="{{ account_code_data }}" data-text="{{ account_text }}">
|
||||||
<input id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-summary" type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-summary" value="{{ summary_data }}">
|
<input id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-summary" type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-summary" value="{{ summary_data }}">
|
||||||
<input id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-amount" class="accounting-currency-{{ currency_index }}-{{ entry_type }}-amount" type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-amount" value="{{ amount_data }}">
|
<input id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-amount" class="accounting-currency-{{ currency_index }}-{{ entry_type }}-amount" type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-amount" value="{{ amount_data }}">
|
||||||
<div class="accounting-entry-content">
|
<div class="accounting-entry-content">
|
||||||
|
@ -24,6 +24,8 @@ First written: 2023/2/26
|
|||||||
{% block accounting_scripts %}
|
{% block accounting_scripts %}
|
||||||
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/transaction-form.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/transaction-form.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/account-selector.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/summary-helper.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -85,6 +87,6 @@ First written: 2023/2/26
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% include "accounting/transaction/include/entry-form-modal.html" %}
|
{% include "accounting/transaction/include/entry-form-modal.html" %}
|
||||||
{% block account_selector_modals %}{% endblock %}
|
{% block form_modals %}{% endblock %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -0,0 +1,181 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
entry-form-modal.html: The modal of the summary helper
|
||||||
|
|
||||||
|
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
|
||||||
|
#}
|
||||||
|
<form id="accounting-summary-helper-{{ summary_helper.type }}" class="accounting-summary-helper" data-entry-type="{{ summary_helper.type }}" data-default-tab-id="general" data-selected-account-code="" data-selected-account-text="">
|
||||||
|
<div id="accounting-summary-helper-{{ summary_helper.type }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-modal-label" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id="accounting-summary-helper-{{ summary_helper.type }}-modal-label">
|
||||||
|
<label for="accounting-summary-helper-{{ summary_helper.type }}-summary">{{ A_("Summary") }}</label>
|
||||||
|
</h1>
|
||||||
|
<button class="btn-close accounting-summary-helper-{{ summary_helper.type }}-close" type="button" data-bs-toggle="modal" data-bs-target="" aria-label="{{ A_("Close") }}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<input id="accounting-summary-helper-{{ summary_helper.type }}-summary" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-modal-label">
|
||||||
|
</div>
|
||||||
|
<ul class="nav nav-tabs mb-2">
|
||||||
|
<li class="nav-item">
|
||||||
|
<span id="accounting-summary-helper-{{ summary_helper.type }}-tab-general" class="nav-link active accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="page" data-tab-id="general">
|
||||||
|
{{ A_("General") }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<span id="accounting-summary-helper-{{ summary_helper.type }}-tab-travel" class="nav-link accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="false" data-tab-id="travel">
|
||||||
|
{{ A_("Travel") }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<span id="accounting-summary-helper-{{ summary_helper.type }}-tab-bus" class="nav-link accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="false" data-tab-id="bus">
|
||||||
|
{{ A_("Bus") }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<span id="accounting-summary-helper-{{ summary_helper.type }}-tab-regular" class="nav-link accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="false" data-tab-id="regular">
|
||||||
|
{{ A_("Regular") }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<span id="accounting-summary-helper-{{ summary_helper.type }}-tab-number" class="nav-link accounting-clickable accounting-summary-helper-{{ summary_helper.type }}-tab" aria-current="false" data-tab-id="number">
|
||||||
|
{{ A_("Number") }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{# A general summary with a tag #}
|
||||||
|
<div class="accounting-summary-helper-{{ summary_helper.type }}-page" aria-current="page" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-tab-general" data-tab-id="general">
|
||||||
|
<div class="form-floating mb-2">
|
||||||
|
<input id="accounting-summary-helper-{{ summary_helper.type }}-general-tag" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
|
||||||
|
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-general-tag">{{ A_("Tag") }}</label>
|
||||||
|
<div id="accounting-summary-helper-{{ summary_helper.type }}-general-tag-error" class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% for tag in summary_helper.general.tags %}
|
||||||
|
<button class="btn btn-outline-primary accounting-summary-helper-{{ summary_helper.type }}-btn-tag accounting-summary-helper-{{ summary_helper.type }}-general-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
|
||||||
|
{{ tag }}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# A general trip with the origin and distination #}
|
||||||
|
<div class="accounting-summary-helper-{{ summary_helper.type }}-page d-none" aria-current="false" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-tab-travel" data-tab-id="travel">
|
||||||
|
<div class="form-floating mb-2">
|
||||||
|
<input id="accounting-summary-helper-{{ summary_helper.type }}-travel-tag" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
|
||||||
|
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-travel-tag">{{ A_("Tag") }}</label>
|
||||||
|
<div id="accounting-summary-helper-{{ summary_helper.type }}-travel-tag-error" class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% for tag in summary_helper.travel.tags %}
|
||||||
|
<button class="btn btn-outline-primary accounting-summary-helper-{{ summary_helper.type }}-btn-tag accounting-summary-helper-{{ summary_helper.type }}-travel-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
|
||||||
|
{{ tag }}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-2">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input id="accounting-summary-helper-{{ summary_helper.type }}-travel-from" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
|
||||||
|
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-travel-from">{{ A_("From") }}</label>
|
||||||
|
<div id="accounting-summary-helper-{{ summary_helper.type }}-travel-from-error" class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group-vertical ms-1 me-1">
|
||||||
|
<button class="btn btn-primary accounting-summary-helper-{{ summary_helper.type }}-travel-direction accounting-default" type="button" tabindex="-1" data-arrow="→">→</button>
|
||||||
|
<button class="btn btn-outline-primary accounting-summary-helper-{{ summary_helper.type }}-travel-direction" type="button" tabindex="-1" data-arrow="↔">↔</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input id="accounting-summary-helper-{{ summary_helper.type }}-travel-to" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
|
||||||
|
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-travel-to">{{ A_("To") }}</label>
|
||||||
|
<div id="accounting-summary-helper-{{ summary_helper.type }}-travel-to-error" class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# A bus trip with the route name or route number, the origin and distination #}
|
||||||
|
<div class="accounting-summary-helper-{{ summary_helper.type }}-page d-none" aria-current="false" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-tab-bus" data-tab-id="bus">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<div class="form-floating me-2">
|
||||||
|
<input id="accounting-summary-helper-{{ summary_helper.type }}-bus-tag" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
|
||||||
|
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-bus-tag">{{ A_("Tag") }}</label>
|
||||||
|
<div id="accounting-summary-helper-{{ summary_helper.type }}-bus-tag-error" class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input id="accounting-summary-helper-{{ summary_helper.type }}-bus-route" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
|
||||||
|
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-bus-route">{{ A_("Route") }}</label>
|
||||||
|
<div id="accounting-summary-helper-{{ summary_helper.type }}-bus-route-error" class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% for tag in summary_helper.bus.tags %}
|
||||||
|
<button class="btn btn-outline-primary accounting-summary-helper-{{ summary_helper.type }}-btn-tag accounting-summary-helper-{{ summary_helper.type }}-bus-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
|
||||||
|
{{ tag }}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-2">
|
||||||
|
<div class="form-floating me-2">
|
||||||
|
<input id="accounting-summary-helper-{{ summary_helper.type }}-bus-from" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
|
||||||
|
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-bus-from">{{ A_("From") }}</label>
|
||||||
|
<div id="accounting-summary-helper-{{ summary_helper.type }}-bus-from-error" class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input id="accounting-summary-helper-{{ summary_helper.type }}-bus-to" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="text" value="" placeholder=" ">
|
||||||
|
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-bus-to">{{ A_("To") }}</label>
|
||||||
|
<div id="accounting-summary-helper-{{ summary_helper.type }}-bus-to-error" class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# A regular income/payment #}
|
||||||
|
<div class="accounting-summary-helper-{{ summary_helper.type }}-page d-none" aria-current="false" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-tab-regular" data-tab-id="regular">
|
||||||
|
{# TODO: To be done #}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# The number of items #}
|
||||||
|
<div class="accounting-summary-helper-{{ summary_helper.type }}-page d-none" aria-current="false" aria-labelledby="accounting-summary-helper-{{ summary_helper.type }}-tab-number" data-tab-id="number">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input id="accounting-summary-helper-{{ summary_helper.type }}-number" class="form-control accounting-summary-helper-{{ summary_helper.type }}-input" type="number" min="1" value="" placeholder=" ">
|
||||||
|
<label class="form-label" for="accounting-summary-helper-{{ summary_helper.type }}-number">{{ A_("The number of items") }}</label>
|
||||||
|
<div id="accounting-summary-helper-{{ summary_helper.type }}-number-error" class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# The suggested accounts #}
|
||||||
|
<div class="mt-3">
|
||||||
|
{% for account in summary_helper.accounts %}
|
||||||
|
<button class="btn btn-outline-primary d-none accounting-summary-helper-{{ summary_helper.type }}-account" type="button" data-code="{{ account.code }}" data-text="{{ account }}">
|
||||||
|
{{ account }}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary accounting-summary-helper-{{ summary_helper.type }}-close" type="button" data-bs-toggle="modal" data-bs-target="">{{ A_("Cancel") }}</button>
|
||||||
|
<button id="accounting-summary-helper-{{ summary_helper.type }}-btn-save" type="submit" class="btn btn-primary">{{ A_("Save") }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -70,7 +70,7 @@ First written: 2023/2/25
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="credit" data-entry-index="new" data-account-modal="#accounting-credit-account-selector-modal" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">
|
<button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="credit" data-entry-index="new" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -45,6 +45,12 @@ First written: 2023/2/25
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block account_selector_modals %}
|
{% block form_modals %}
|
||||||
{% include "accounting/transaction/include/credit-account-modal.html" %}
|
{% with summary_helper = form.summary_helper.credit %}
|
||||||
|
{% include "accounting/transaction/include/summary-helper-modal.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
{% with entry_type = "credit",
|
||||||
|
account_options = form.credit_account_options %}
|
||||||
|
{% include "accounting/transaction/include/account-selector-modal.html" %}
|
||||||
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -57,7 +57,7 @@ First written: 2023/2/25
|
|||||||
account_text = entry_form.account_text,
|
account_text = entry_form.account_text,
|
||||||
summary_data = "" if entry_form.summary.data is none else entry_form.summary.data,
|
summary_data = "" if entry_form.summary.data is none else entry_form.summary.data,
|
||||||
summary_errors = entry_form.summary.errors,
|
summary_errors = entry_form.summary.errors,
|
||||||
amount_data = "" if entry_form.amount.data is none else entry_form.amount.data,
|
amount_data = "" if entry_form.amount.data is none else entry_form.amount.data|accounting_txn_format_amount_input,
|
||||||
amount_errors = entry_form.amount.errors,
|
amount_errors = entry_form.amount.errors,
|
||||||
amount_text = entry_form.amount.data|accounting_txn_format_amount,
|
amount_text = entry_form.amount.data|accounting_txn_format_amount,
|
||||||
entry_errors = entry_form.all_errors %}
|
entry_errors = entry_form.all_errors %}
|
||||||
@ -72,7 +72,7 @@ First written: 2023/2/25
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="debit" data-entry-index="new" data-account-modal="#accounting-debit-account-selector-modal" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">
|
<button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="debit" data-entry-index="new" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</button>
|
</button>
|
||||||
@ -112,7 +112,7 @@ First written: 2023/2/25
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="credit" data-entry-index="new" data-account-modal="#accounting-credit-account-selector-modal" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">
|
<button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="credit" data-entry-index="new" data-bs-toggle="modal" data-bs-target="#accounting-entry-form-modal">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -49,7 +49,19 @@ First written: 2023/2/25
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block account_selector_modals %}
|
{% block form_modals %}
|
||||||
{% include "accounting/transaction/include/debit-account-modal.html" %}
|
{% with summary_helper = form.summary_helper.debit %}
|
||||||
{% include "accounting/transaction/include/credit-account-modal.html" %}
|
{% include "accounting/transaction/include/summary-helper-modal.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
{% with summary_helper = form.summary_helper.credit %}
|
||||||
|
{% include "accounting/transaction/include/summary-helper-modal.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
{% with entry_type = "debit",
|
||||||
|
account_options = form.debit_account_options %}
|
||||||
|
{% include "accounting/transaction/include/account-selector-modal.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
{% with entry_type = "credit",
|
||||||
|
account_options = form.credit_account_options %}
|
||||||
|
{% include "accounting/transaction/include/account-selector-modal.html" %}
|
||||||
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The transaction type dispatcher.
|
"""The view dispatcher for different transaction types.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
import typing as t
|
||||||
|
@ -37,6 +37,7 @@ from accounting import db
|
|||||||
from accounting.locale import lazy_gettext
|
from accounting.locale import lazy_gettext
|
||||||
from accounting.models import Transaction, Account, JournalEntry, \
|
from accounting.models import Transaction, Account, JournalEntry, \
|
||||||
TransactionCurrency, Currency
|
TransactionCurrency, Currency
|
||||||
|
from accounting.transaction.summary_helper import SummaryHelper
|
||||||
from accounting.utils.random_id import new_id
|
from accounting.utils.random_id import new_id
|
||||||
from accounting.utils.strip_text import strip_text, strip_multiline_text
|
from accounting.utils.strip_text import strip_text, strip_multiline_text
|
||||||
from accounting.utils.user import get_current_user_pk
|
from accounting.utils.user import get_current_user_pk
|
||||||
@ -114,6 +115,35 @@ class IsDebitAccount:
|
|||||||
"This account is not for debit entries."))
|
"This account is not for debit entries."))
|
||||||
|
|
||||||
|
|
||||||
|
class AccountOption:
|
||||||
|
"""An account option."""
|
||||||
|
|
||||||
|
def __init__(self, account: Account):
|
||||||
|
"""Constructs an account option.
|
||||||
|
|
||||||
|
:param account: The account.
|
||||||
|
"""
|
||||||
|
self.__account: Account = account
|
||||||
|
self.id: str = account.id
|
||||||
|
self.code: str = account.code
|
||||||
|
self.is_in_use: bool = False
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Returns the string representation of the account option.
|
||||||
|
|
||||||
|
:return: The string representation of the account option.
|
||||||
|
"""
|
||||||
|
return str(self.__account)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def query_values(self) -> list[str]:
|
||||||
|
"""Returns the values to be queried.
|
||||||
|
|
||||||
|
:return: The values to be queried.
|
||||||
|
"""
|
||||||
|
return self.__account.query_values
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryForm(FlaskForm):
|
class JournalEntryForm(FlaskForm):
|
||||||
"""The base form to create or edit a journal entry."""
|
"""The base form to create or edit a journal entry."""
|
||||||
eid = IntegerField()
|
eid = IntegerField()
|
||||||
@ -320,49 +350,54 @@ class TransactionForm(FlaskForm):
|
|||||||
obj.no = count + 1
|
obj.no = count + 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def debit_account_options(self) -> list[Account]:
|
def debit_account_options(self) -> list[AccountOption]:
|
||||||
"""The selectable debit accounts.
|
"""The selectable debit accounts.
|
||||||
|
|
||||||
:return: The selectable debit accounts.
|
:return: The selectable debit accounts.
|
||||||
"""
|
"""
|
||||||
accounts: list[Account] = Account.debit()
|
accounts: list[AccountOption] \
|
||||||
in_use: set[int] = self.__get_in_use_account_id()
|
= [AccountOption(x) for x in Account.debit()]
|
||||||
|
in_use: set[int] = set(db.session.scalars(
|
||||||
|
sa.select(JournalEntry.account_id)
|
||||||
|
.filter(JournalEntry.is_debit)
|
||||||
|
.group_by(JournalEntry.account_id)).all())
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
account.is_in_use = account.id in in_use
|
account.is_in_use = account.id in in_use
|
||||||
return accounts
|
return accounts
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def credit_account_options(self) -> list[Account]:
|
def credit_account_options(self) -> list[AccountOption]:
|
||||||
"""The selectable credit accounts.
|
"""The selectable credit accounts.
|
||||||
|
|
||||||
:return: The selectable credit accounts.
|
:return: The selectable credit accounts.
|
||||||
"""
|
"""
|
||||||
accounts: list[Account] = Account.credit()
|
accounts: list[AccountOption] \
|
||||||
in_use: set[int] = self.__get_in_use_account_id()
|
= [AccountOption(x) for x in Account.credit()]
|
||||||
|
in_use: set[int] = set(db.session.scalars(
|
||||||
|
sa.select(JournalEntry.account_id)
|
||||||
|
.filter(sa.not_(JournalEntry.is_debit))
|
||||||
|
.group_by(JournalEntry.account_id)).all())
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
account.is_in_use = account.id in in_use
|
account.is_in_use = account.id in in_use
|
||||||
return accounts
|
return accounts
|
||||||
|
|
||||||
def __get_in_use_account_id(self) -> set[int]:
|
|
||||||
"""Returns the ID of the accounts that are in use.
|
|
||||||
|
|
||||||
:return: The ID of the accounts that are in use.
|
|
||||||
"""
|
|
||||||
if self.__in_use_account_id is None:
|
|
||||||
self.__in_use_account_id = set(db.session.scalars(
|
|
||||||
sa.select(JournalEntry.account_id)
|
|
||||||
.group_by(JournalEntry.account_id)).all())
|
|
||||||
return self.__in_use_account_id
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def currencies_errors(self) -> list[str | LazyString]:
|
def currencies_errors(self) -> list[str | LazyString]:
|
||||||
"""Returns the currency errors, without the errors in their sub-forms.
|
"""Returns the currency errors, without the errors in their sub-forms.
|
||||||
|
|
||||||
:return:
|
:return: The currency errors, without the errors in their sub-forms.
|
||||||
"""
|
"""
|
||||||
return [x for x in self.currencies.errors
|
return [x for x in self.currencies.errors
|
||||||
if isinstance(x, str) or isinstance(x, LazyString)]
|
if isinstance(x, str) or isinstance(x, LazyString)]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def summary_helper(self) -> SummaryHelper:
|
||||||
|
"""Returns the summary helper.
|
||||||
|
|
||||||
|
:return: The summary helper.
|
||||||
|
"""
|
||||||
|
return SummaryHelper()
|
||||||
|
|
||||||
|
|
||||||
T = t.TypeVar("T", bound=TransactionForm)
|
T = t.TypeVar("T", bound=TransactionForm)
|
||||||
"""A transaction form variant."""
|
"""A transaction form variant."""
|
||||||
|
255
src/accounting/transaction/summary_helper.py
Normal file
255
src/accounting/transaction/summary_helper.py
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
# The Mia! Accounting Flask Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/27
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
"""The summary helper.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from accounting import db
|
||||||
|
from accounting.models import Account, JournalEntry
|
||||||
|
|
||||||
|
|
||||||
|
class SummaryAccount:
|
||||||
|
"""An account for a summary tag."""
|
||||||
|
|
||||||
|
def __init__(self, account: Account, freq: int):
|
||||||
|
"""Constructs an account for a summary tag.
|
||||||
|
|
||||||
|
:param account: The account.
|
||||||
|
:param freq: The frequency of the tag with the account.
|
||||||
|
"""
|
||||||
|
self.account: Account = account
|
||||||
|
"""The account."""
|
||||||
|
self.id: int = account.id
|
||||||
|
"""The account ID."""
|
||||||
|
self.code: str = account.code
|
||||||
|
"""The account code."""
|
||||||
|
self.freq: int = freq
|
||||||
|
"""The frequency of the tag with the account."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Returns the string representation of the account.
|
||||||
|
|
||||||
|
:return: The string representation of the account.
|
||||||
|
"""
|
||||||
|
return str(self.account)
|
||||||
|
|
||||||
|
def add_freq(self, freq: int) -> None:
|
||||||
|
"""Adds the frequency of an account.
|
||||||
|
|
||||||
|
:param freq: The frequency of the tag name with the account.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
self.freq = self.freq + freq
|
||||||
|
|
||||||
|
|
||||||
|
class SummaryTag:
|
||||||
|
"""A summary tag."""
|
||||||
|
|
||||||
|
def __init__(self, name: str):
|
||||||
|
"""Constructs a summary tag.
|
||||||
|
|
||||||
|
:param name: The tag name.
|
||||||
|
"""
|
||||||
|
self.name: str = name
|
||||||
|
"""The tag name."""
|
||||||
|
self.__account_dict: dict[int, SummaryAccount] = {}
|
||||||
|
"""The accounts that come with the tag, in the order of their
|
||||||
|
frequency."""
|
||||||
|
self.freq: int = 0
|
||||||
|
"""The frequency of the tag."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Returns the string representation of the tag.
|
||||||
|
|
||||||
|
:return: The string representation of the tag.
|
||||||
|
"""
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def add_account(self, account: Account, freq: int):
|
||||||
|
"""Adds an account.
|
||||||
|
|
||||||
|
:param account: The associated account.
|
||||||
|
:param freq: The frequency of the tag name with the account.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
self.__account_dict[account.id] = SummaryAccount(account, freq)
|
||||||
|
self.freq = self.freq + freq
|
||||||
|
|
||||||
|
@property
|
||||||
|
def accounts(self) -> list[SummaryAccount]:
|
||||||
|
"""Returns the accounts by the order of their frequencies.
|
||||||
|
|
||||||
|
:return: The accounts by the order of their frequencies.
|
||||||
|
"""
|
||||||
|
return sorted(self.__account_dict.values(), key=lambda x: -x.freq)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_codes(self) -> list[str]:
|
||||||
|
"""Returns the account codes by the order of their frequencies.
|
||||||
|
|
||||||
|
:return: The account codes by the order of their frequencies.
|
||||||
|
"""
|
||||||
|
return [x.code for x in self.accounts]
|
||||||
|
|
||||||
|
|
||||||
|
class SummaryType:
|
||||||
|
"""A summary type"""
|
||||||
|
|
||||||
|
def __init__(self, type_id: t.Literal["general", "travel", "bus"]):
|
||||||
|
"""Constructs a summary type.
|
||||||
|
|
||||||
|
:param type_id: The type ID, either "general", "travel", or "bus".
|
||||||
|
"""
|
||||||
|
self.id: t.Literal["general", "travel", "bus"] = type_id
|
||||||
|
"""The type ID."""
|
||||||
|
self.__tag_dict: dict[str, SummaryTag] = {}
|
||||||
|
"""A dictionary from the tag name to their corresponding tag."""
|
||||||
|
|
||||||
|
def add_tag(self, name: str, account: Account, freq: int) -> None:
|
||||||
|
"""Adds a tag.
|
||||||
|
|
||||||
|
:param name: The tag name.
|
||||||
|
:param account: The associated account.
|
||||||
|
:param freq: The frequency of the tag name with the account.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
if name not in self.__tag_dict:
|
||||||
|
self.__tag_dict[name] = SummaryTag(name)
|
||||||
|
self.__tag_dict[name].add_account(account, freq)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tags(self) -> list[SummaryTag]:
|
||||||
|
"""Returns the tags by the order of their frequencies.
|
||||||
|
|
||||||
|
:return: The tags by the order of their frequencies.
|
||||||
|
"""
|
||||||
|
return sorted(self.__tag_dict.values(), key=lambda x: -x.freq)
|
||||||
|
|
||||||
|
|
||||||
|
class SummaryEntryType:
|
||||||
|
"""A summary type"""
|
||||||
|
|
||||||
|
def __init__(self, entry_type_id: t.Literal["debit", "credit"]):
|
||||||
|
"""Constructs a summary entry type.
|
||||||
|
|
||||||
|
:param entry_type_id: The entry type ID, either "debit" or "credit".
|
||||||
|
"""
|
||||||
|
self.type: t.Literal["debit", "credit"] = entry_type_id
|
||||||
|
"""The entry type."""
|
||||||
|
self.general: SummaryType = SummaryType("general")
|
||||||
|
"""The general tags."""
|
||||||
|
self.travel: SummaryType = SummaryType("travel")
|
||||||
|
"""The travel tags."""
|
||||||
|
self.bus: SummaryType = SummaryType("bus")
|
||||||
|
"""The bus tags."""
|
||||||
|
self.__type_dict: dict[t.Literal["general", "travel", "bus"],
|
||||||
|
SummaryType] \
|
||||||
|
= {x.id: x for x in {self.general, self.travel, self.bus}}
|
||||||
|
"""A dictionary from the type ID to the corresponding tags."""
|
||||||
|
|
||||||
|
def add_tag(self, tag_type: t.Literal["general", "travel", "bus"],
|
||||||
|
name: str, account: Account, freq: int) -> None:
|
||||||
|
"""Adds a tag.
|
||||||
|
|
||||||
|
:param tag_type: The tag type, either "general", "travel", or "bus".
|
||||||
|
:param name: The name.
|
||||||
|
:param account: The associated account.
|
||||||
|
:param freq: The frequency of the tag name with the account.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
self.__type_dict[tag_type].add_tag(name, account, freq)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def accounts(self) -> list[SummaryAccount]:
|
||||||
|
"""Returns the suggested accounts of all tags in the summary helper in
|
||||||
|
the entry type, in their frequency order.
|
||||||
|
|
||||||
|
:return: The suggested accounts of all tags, in their frequency order.
|
||||||
|
"""
|
||||||
|
accounts: dict[int, SummaryAccount] = {}
|
||||||
|
freq: dict[int, int] = {}
|
||||||
|
for tag_type in self.__type_dict.values():
|
||||||
|
for tag in tag_type.tags:
|
||||||
|
for account in tag.accounts:
|
||||||
|
accounts[account.id] = account
|
||||||
|
if account.id not in freq:
|
||||||
|
freq[account.id] = 0
|
||||||
|
freq[account.id] \
|
||||||
|
= freq[account.id] + account.freq
|
||||||
|
return [accounts[y] for y in sorted(freq.keys(),
|
||||||
|
key=lambda x: -freq[x])]
|
||||||
|
|
||||||
|
|
||||||
|
class SummaryHelper:
|
||||||
|
"""The summary helper."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Constructs the summary helper."""
|
||||||
|
self.debit: SummaryEntryType = SummaryEntryType("debit")
|
||||||
|
"""The debit tags."""
|
||||||
|
self.credit: SummaryEntryType = SummaryEntryType("credit")
|
||||||
|
"""The credit tags."""
|
||||||
|
entry_type: sa.Label = sa.case((JournalEntry.is_debit, "debit"),
|
||||||
|
else_="credit").label("entry_type")
|
||||||
|
tag_type: sa.Label = sa.case(
|
||||||
|
(JournalEntry.summary.like("_%—_%—_%→_%"), "bus"),
|
||||||
|
(sa.or_(JournalEntry.summary.like("_%—_%→_%"),
|
||||||
|
JournalEntry.summary.like("_%—_%↔_%")), "travel"),
|
||||||
|
else_="general").label("tag_type")
|
||||||
|
tag: sa.Label = get_prefix(JournalEntry.summary, "—").label("tag")
|
||||||
|
select: sa.Select = sa.Select(entry_type, tag_type, tag,
|
||||||
|
JournalEntry.account_id,
|
||||||
|
sa.func.count().label("freq"))\
|
||||||
|
.filter(JournalEntry.summary.is_not(None),
|
||||||
|
JournalEntry.summary.like("_%—_%"))\
|
||||||
|
.group_by(entry_type, tag_type, tag, JournalEntry.account_id)
|
||||||
|
result: list[sa.Row] = db.session.execute(select).all()
|
||||||
|
accounts: dict[int, Account] \
|
||||||
|
= {x.id: x for x in Account.query
|
||||||
|
.filter(Account.id.in_({x.account_id for x in result})).all()}
|
||||||
|
entry_type_dict: dict[t.Literal["debit", "credit"], SummaryEntryType] \
|
||||||
|
= {x.type: x for x in {self.debit, self.credit}}
|
||||||
|
for row in result:
|
||||||
|
entry_type_dict[row.entry_type].add_tag(
|
||||||
|
row.tag_type, row.tag, accounts[row.account_id], row.freq)
|
||||||
|
|
||||||
|
|
||||||
|
def get_prefix(string: str | sa.Column, separator: str | sa.Column) \
|
||||||
|
-> sa.Function:
|
||||||
|
"""Returns the SQL function to find the prefix of a string.
|
||||||
|
|
||||||
|
:param string: The string.
|
||||||
|
:param separator: The separator.
|
||||||
|
:return: The position of the substring, starting from 1.
|
||||||
|
"""
|
||||||
|
return sa.func.substr(string, 0, get_position(string, separator))
|
||||||
|
|
||||||
|
|
||||||
|
def get_position(string: str | sa.Column, substring: str | sa.Column) \
|
||||||
|
-> sa.Function:
|
||||||
|
"""Returns the SQL function to find the position of a substring.
|
||||||
|
|
||||||
|
:param string: The string.
|
||||||
|
:param substring: The substring.
|
||||||
|
:return: The position of the substring, starting from 1.
|
||||||
|
"""
|
||||||
|
if db.engine.name == "postgresql":
|
||||||
|
return sa.func.strpos(string, substring)
|
||||||
|
return sa.func.instr(string, substring)
|
@ -8,8 +8,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Mia! Accounting Flask 0.0.0\n"
|
"Project-Id-Version: Mia! Accounting Flask 0.0.0\n"
|
||||||
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
|
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
|
||||||
"POT-Creation-Date: 2023-02-27 18:59+0800\n"
|
"POT-Creation-Date: 2023-03-01 00:51+0800\n"
|
||||||
"PO-Revision-Date: 2023-02-27 18:59+0800\n"
|
"PO-Revision-Date: 2023-03-01 00:51+0800\n"
|
||||||
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
|
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
|
||||||
"Language: zh_Hant\n"
|
"Language: zh_Hant\n"
|
||||||
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
|
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
|
||||||
@ -126,33 +126,52 @@ msgstr "貨幣刪掉了"
|
|||||||
msgid "Please fill in the title."
|
msgid "Please fill in the title."
|
||||||
msgstr "請填上標題。"
|
msgstr "請填上標題。"
|
||||||
|
|
||||||
#: src/accounting/static/js/transaction-form.js:308
|
#: src/accounting/static/js/summary-helper.js:441
|
||||||
#: src/accounting/static/js/transaction-form.js:764
|
#: src/accounting/static/js/summary-helper.js:512
|
||||||
#: src/accounting/transaction/forms.py:46
|
msgid "Please fill in the tag."
|
||||||
|
msgstr "請填上標籤。"
|
||||||
|
|
||||||
|
#: src/accounting/static/js/summary-helper.js:460
|
||||||
|
#: src/accounting/static/js/summary-helper.js:550
|
||||||
|
msgid "Please fill in the origin."
|
||||||
|
msgstr "請填上起點。"
|
||||||
|
|
||||||
|
#: src/accounting/static/js/summary-helper.js:479
|
||||||
|
#: src/accounting/static/js/summary-helper.js:569
|
||||||
|
msgid "Please fill in the destination."
|
||||||
|
msgstr "請填上終點。"
|
||||||
|
|
||||||
|
#: src/accounting/static/js/summary-helper.js:531
|
||||||
|
msgid "Please fill in the route."
|
||||||
|
msgstr "請填上路線名稱。"
|
||||||
|
|
||||||
|
#: src/accounting/static/js/transaction-form.js:289
|
||||||
|
#: src/accounting/static/js/transaction-form.js:611
|
||||||
|
#: src/accounting/transaction/forms.py:47
|
||||||
msgid "Please select the account."
|
msgid "Please select the account."
|
||||||
msgstr "請選擇科目。"
|
msgstr "請選擇科目。"
|
||||||
|
|
||||||
#: src/accounting/static/js/transaction-form.js:344
|
#: src/accounting/static/js/transaction-form.js:324
|
||||||
#: src/accounting/static/js/transaction-form.js:769
|
#: src/accounting/static/js/transaction-form.js:616
|
||||||
msgid "Please fill in the amount."
|
msgid "Please fill in the amount."
|
||||||
msgstr "請填上金額。"
|
msgstr "請填上金額。"
|
||||||
|
|
||||||
#: src/accounting/static/js/transaction-form.js:641
|
#: src/accounting/static/js/transaction-form.js:488
|
||||||
msgid "Please fill in the date."
|
msgid "Please fill in the date."
|
||||||
msgstr "請填上日期。"
|
msgstr "請填上日期。"
|
||||||
|
|
||||||
#: src/accounting/static/js/transaction-form.js:676
|
#: src/accounting/static/js/transaction-form.js:523
|
||||||
#: src/accounting/transaction/forms.py:56
|
#: src/accounting/transaction/forms.py:57
|
||||||
msgid "Please add some currencies."
|
msgid "Please add some currencies."
|
||||||
msgstr "請加上貨幣。"
|
msgstr "請加上貨幣。"
|
||||||
|
|
||||||
#: src/accounting/static/js/transaction-form.js:742
|
#: src/accounting/static/js/transaction-form.js:589
|
||||||
#: src/accounting/transaction/forms.py:77
|
#: src/accounting/transaction/forms.py:78
|
||||||
msgid "Please add some journal entries."
|
msgid "Please add some journal entries."
|
||||||
msgstr "請加上分錄。"
|
msgstr "請加上分錄。"
|
||||||
|
|
||||||
#: src/accounting/static/js/transaction-form.js:807
|
#: src/accounting/static/js/transaction-form.js:654
|
||||||
#: src/accounting/transaction/forms.py:670
|
#: src/accounting/transaction/forms.py:672
|
||||||
msgid "The totals of the debit and credit amounts do not match."
|
msgid "The totals of the debit and credit amounts do not match."
|
||||||
msgstr "借方貸方合計不符。 "
|
msgstr "借方貸方合計不符。 "
|
||||||
|
|
||||||
@ -167,7 +186,7 @@ msgstr "新增科目"
|
|||||||
#: src/accounting/templates/accounting/currency/detail.html:31
|
#: src/accounting/templates/accounting/currency/detail.html:31
|
||||||
#: src/accounting/templates/accounting/currency/include/form.html:33
|
#: src/accounting/templates/accounting/currency/include/form.html:33
|
||||||
#: src/accounting/templates/accounting/transaction/include/detail.html:31
|
#: src/accounting/templates/accounting/transaction/include/detail.html:31
|
||||||
#: src/accounting/templates/accounting/transaction/include/form.html:34
|
#: src/accounting/templates/accounting/transaction/include/form.html:36
|
||||||
#: src/accounting/templates/accounting/transaction/order.html:36
|
#: src/accounting/templates/accounting/transaction/order.html:36
|
||||||
msgid "Back"
|
msgid "Back"
|
||||||
msgstr "回上頁"
|
msgstr "回上頁"
|
||||||
@ -196,10 +215,10 @@ msgstr "科目刪除確認"
|
|||||||
#: src/accounting/templates/accounting/account/detail.html:70
|
#: src/accounting/templates/accounting/account/detail.html:70
|
||||||
#: src/accounting/templates/accounting/account/include/form.html:91
|
#: src/accounting/templates/accounting/account/include/form.html:91
|
||||||
#: src/accounting/templates/accounting/currency/detail.html:66
|
#: src/accounting/templates/accounting/currency/detail.html:66
|
||||||
#: src/accounting/templates/accounting/transaction/include/credit-account-modal.html:27
|
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:27
|
||||||
#: src/accounting/templates/accounting/transaction/include/debit-account-modal.html:27
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/detail.html:71
|
#: src/accounting/templates/accounting/transaction/include/detail.html:71
|
||||||
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:28
|
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:28
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:30
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr "關閉"
|
msgstr "關閉"
|
||||||
|
|
||||||
@ -210,10 +229,10 @@ msgstr "你確定要刪掉這個科目嗎?"
|
|||||||
#: src/accounting/templates/accounting/account/detail.html:76
|
#: src/accounting/templates/accounting/account/detail.html:76
|
||||||
#: src/accounting/templates/accounting/account/include/form.html:112
|
#: src/accounting/templates/accounting/account/include/form.html:112
|
||||||
#: src/accounting/templates/accounting/currency/detail.html:72
|
#: src/accounting/templates/accounting/currency/detail.html:72
|
||||||
#: src/accounting/templates/accounting/transaction/include/credit-account-modal.html:49
|
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:49
|
||||||
#: src/accounting/templates/accounting/transaction/include/debit-account-modal.html:49
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/detail.html:77
|
#: src/accounting/templates/accounting/transaction/include/detail.html:77
|
||||||
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:52
|
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:54
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:175
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "取消"
|
msgstr "取消"
|
||||||
|
|
||||||
@ -255,7 +274,7 @@ msgstr "科目管理"
|
|||||||
#: src/accounting/templates/accounting/account/list.html:32
|
#: src/accounting/templates/accounting/account/list.html:32
|
||||||
#: src/accounting/templates/accounting/currency/list.html:32
|
#: src/accounting/templates/accounting/currency/list.html:32
|
||||||
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:75
|
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:75
|
||||||
#: src/accounting/templates/accounting/transaction/include/form.html:60
|
#: src/accounting/templates/accounting/transaction/include/form.html:62
|
||||||
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:75
|
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:75
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:37
|
#: src/accounting/templates/accounting/transaction/list.html:37
|
||||||
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:77
|
#: src/accounting/templates/accounting/transaction/transfer/include/form-currency-item.html:77
|
||||||
@ -276,8 +295,7 @@ msgstr "桌機版檢索"
|
|||||||
#: src/accounting/templates/accounting/base-account/list.html:34
|
#: src/accounting/templates/accounting/base-account/list.html:34
|
||||||
#: src/accounting/templates/accounting/currency/list.html:40
|
#: src/accounting/templates/accounting/currency/list.html:40
|
||||||
#: src/accounting/templates/accounting/currency/list.html:52
|
#: src/accounting/templates/accounting/currency/list.html:52
|
||||||
#: src/accounting/templates/accounting/transaction/include/credit-account-modal.html:34
|
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:34
|
||||||
#: src/accounting/templates/accounting/transaction/include/debit-account-modal.html:34
|
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:62
|
#: src/accounting/templates/accounting/transaction/list.html:62
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:74
|
#: src/accounting/templates/accounting/transaction/list.html:74
|
||||||
msgid "Search"
|
msgid "Search"
|
||||||
@ -294,8 +312,7 @@ msgstr "行動版檢索"
|
|||||||
#: src/accounting/templates/accounting/account/order.html:81
|
#: src/accounting/templates/accounting/account/order.html:81
|
||||||
#: src/accounting/templates/accounting/base-account/list.html:51
|
#: src/accounting/templates/accounting/base-account/list.html:51
|
||||||
#: src/accounting/templates/accounting/currency/list.html:77
|
#: src/accounting/templates/accounting/currency/list.html:77
|
||||||
#: src/accounting/templates/accounting/transaction/include/credit-account-modal.html:46
|
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:46
|
||||||
#: src/accounting/templates/accounting/transaction/include/debit-account-modal.html:46
|
|
||||||
#: src/accounting/templates/accounting/transaction/list.html:93
|
#: src/accounting/templates/accounting/transaction/list.html:93
|
||||||
#: src/accounting/templates/accounting/transaction/order.html:80
|
#: src/accounting/templates/accounting/transaction/order.html:80
|
||||||
msgid "There is no data."
|
msgid "There is no data."
|
||||||
@ -309,8 +326,9 @@ msgstr "%(base)s下的科目"
|
|||||||
#: src/accounting/templates/accounting/account/include/form.html:75
|
#: src/accounting/templates/accounting/account/include/form.html:75
|
||||||
#: src/accounting/templates/accounting/account/order.html:62
|
#: src/accounting/templates/accounting/account/order.html:62
|
||||||
#: src/accounting/templates/accounting/currency/include/form.html:57
|
#: src/accounting/templates/accounting/currency/include/form.html:57
|
||||||
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:53
|
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:55
|
||||||
#: src/accounting/templates/accounting/transaction/include/form.html:76
|
#: src/accounting/templates/accounting/transaction/include/form.html:78
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:176
|
||||||
#: src/accounting/templates/accounting/transaction/order.html:61
|
#: src/accounting/templates/accounting/transaction/order.html:61
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr "儲存"
|
msgstr "儲存"
|
||||||
@ -337,8 +355,7 @@ msgstr "選擇基本科目"
|
|||||||
|
|
||||||
#: src/accounting/templates/accounting/account/include/form.html:114
|
#: src/accounting/templates/accounting/account/include/form.html:114
|
||||||
#: src/accounting/templates/accounting/account/include/form.html:116
|
#: src/accounting/templates/accounting/account/include/form.html:116
|
||||||
#: src/accounting/templates/accounting/transaction/include/credit-account-modal.html:50
|
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:50
|
||||||
#: src/accounting/templates/accounting/transaction/include/debit-account-modal.html:50
|
|
||||||
msgid "Clear"
|
msgid "Clear"
|
||||||
msgstr "清除"
|
msgstr "清除"
|
||||||
|
|
||||||
@ -432,7 +449,7 @@ msgstr "改轉帳"
|
|||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/expense/detail.html:37
|
#: src/accounting/templates/accounting/transaction/expense/detail.html:37
|
||||||
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:45
|
#: src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html:45
|
||||||
#: src/accounting/templates/accounting/transaction/include/form.html:52
|
#: src/accounting/templates/accounting/transaction/include/form.html:54
|
||||||
#: src/accounting/templates/accounting/transaction/income/detail.html:37
|
#: src/accounting/templates/accounting/transaction/income/detail.html:37
|
||||||
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:45
|
#: src/accounting/templates/accounting/transaction/income/include/form-currency-item.html:45
|
||||||
msgid "Content"
|
msgid "Content"
|
||||||
@ -462,6 +479,14 @@ msgstr "編輯%(txn)s"
|
|||||||
msgid "Currency"
|
msgid "Currency"
|
||||||
msgstr "貨幣"
|
msgstr "貨幣"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:26
|
||||||
|
msgid "Select Account"
|
||||||
|
msgstr "選擇科目"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/account-selector-modal.html:44
|
||||||
|
msgid "More…"
|
||||||
|
msgstr "更多…"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/add-new-material-fab.html:26
|
#: src/accounting/templates/accounting/transaction/include/add-new-material-fab.html:26
|
||||||
msgid "Cash expense"
|
msgid "Cash expense"
|
||||||
msgstr "現金支出"
|
msgstr "現金支出"
|
||||||
@ -470,19 +495,6 @@ msgstr "現金支出"
|
|||||||
msgid "Cash income"
|
msgid "Cash income"
|
||||||
msgstr "現金收入"
|
msgstr "現金收入"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/credit-account-modal.html:26
|
|
||||||
msgid "Select Credit Account"
|
|
||||||
msgstr "選擇貸方科目科目"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/credit-account-modal.html:44
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/debit-account-modal.html:44
|
|
||||||
msgid "More…"
|
|
||||||
msgstr "更多…"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/debit-account-modal.html:26
|
|
||||||
msgid "Select Debit Account"
|
|
||||||
msgstr "選擇借方科目"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/detail.html:70
|
#: src/accounting/templates/accounting/transaction/include/detail.html:70
|
||||||
msgid "Delete Transaction Confirmation"
|
msgid "Delete Transaction Confirmation"
|
||||||
msgstr "傳票刪除確認"
|
msgstr "傳票刪除確認"
|
||||||
@ -500,21 +512,66 @@ msgid "Account"
|
|||||||
msgstr "科目"
|
msgstr "科目"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:41
|
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:41
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:28
|
||||||
msgid "Summary"
|
msgid "Summary"
|
||||||
msgstr "摘要"
|
msgstr "摘要"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:47
|
#: src/accounting/templates/accounting/transaction/include/entry-form-modal.html:49
|
||||||
msgid "Amount"
|
msgid "Amount"
|
||||||
msgstr "金額"
|
msgstr "金額"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/form.html:46
|
#: src/accounting/templates/accounting/transaction/include/form.html:48
|
||||||
msgid "Date"
|
msgid "Date"
|
||||||
msgstr "日期"
|
msgstr "日期"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/include/form.html:69
|
#: src/accounting/templates/accounting/transaction/include/form.html:71
|
||||||
msgid "Note"
|
msgid "Note"
|
||||||
msgstr "備註"
|
msgstr "備註"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:39
|
||||||
|
msgid "General"
|
||||||
|
msgstr "一般"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:44
|
||||||
|
msgid "Travel"
|
||||||
|
msgstr "差旅"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:49
|
||||||
|
msgid "Bus"
|
||||||
|
msgstr "公車"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:54
|
||||||
|
msgid "Regular"
|
||||||
|
msgstr "帳單"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:59
|
||||||
|
msgid "Number"
|
||||||
|
msgstr "數量"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:67
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:84
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:119
|
||||||
|
msgid "Tag"
|
||||||
|
msgstr "標籤"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:99
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:140
|
||||||
|
msgid "From"
|
||||||
|
msgstr "從"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:108
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:145
|
||||||
|
msgid "To"
|
||||||
|
msgstr "至"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:124
|
||||||
|
msgid "Route"
|
||||||
|
msgstr "路線"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/transaction/include/summary-helper-modal.html:160
|
||||||
|
msgid "The number of items"
|
||||||
|
msgstr "數量"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/transaction/income/create.html:24
|
#: src/accounting/templates/accounting/transaction/income/create.html:24
|
||||||
msgid "Add a New Cash Income Transaction"
|
msgid "Add a New Cash Income Transaction"
|
||||||
msgstr "新增現金收入傳票"
|
msgstr "新增現金收入傳票"
|
||||||
@ -533,27 +590,27 @@ msgstr "借方"
|
|||||||
msgid "Credit"
|
msgid "Credit"
|
||||||
msgstr "貸方"
|
msgstr "貸方"
|
||||||
|
|
||||||
#: src/accounting/transaction/forms.py:44
|
#: src/accounting/transaction/forms.py:45
|
||||||
msgid "Please select the currency."
|
msgid "Please select the currency."
|
||||||
msgstr "請選擇貨幣。"
|
msgstr "請選擇貨幣。"
|
||||||
|
|
||||||
#: src/accounting/transaction/forms.py:67
|
#: src/accounting/transaction/forms.py:68
|
||||||
msgid "The currency does not exist."
|
msgid "The currency does not exist."
|
||||||
msgstr "沒有這個貨幣。"
|
msgstr "沒有這個貨幣。"
|
||||||
|
|
||||||
#: src/accounting/transaction/forms.py:88
|
#: src/accounting/transaction/forms.py:89
|
||||||
msgid "The account does not exist."
|
msgid "The account does not exist."
|
||||||
msgstr "沒有這個科目。"
|
msgstr "沒有這個科目。"
|
||||||
|
|
||||||
#: src/accounting/transaction/forms.py:99
|
#: src/accounting/transaction/forms.py:100
|
||||||
msgid "Please fill in a positive amount."
|
msgid "Please fill in a positive amount."
|
||||||
msgstr "金額請填正數。"
|
msgstr "金額請填正數。"
|
||||||
|
|
||||||
#: src/accounting/transaction/forms.py:113
|
#: src/accounting/transaction/forms.py:114
|
||||||
msgid "This account is not for debit entries."
|
msgid "This account is not for debit entries."
|
||||||
msgstr "科目不是借方科目。"
|
msgstr "科目不是借方科目。"
|
||||||
|
|
||||||
#: src/accounting/transaction/forms.py:200
|
#: src/accounting/transaction/forms.py:201
|
||||||
msgid "This account is not for credit entries."
|
msgid "This account is not for credit entries."
|
||||||
msgstr "科目不是貸方科目。"
|
msgstr "科目不是貸方科目。"
|
||||||
|
|
||||||
|
327
tests/test_summary_helper.py
Normal file
327
tests/test_summary_helper.py
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
# The Mia! Accounting Flask Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/28
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
"""The test for the summary helper.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from click.testing import Result
|
||||||
|
from flask import Flask
|
||||||
|
from flask.testing import FlaskCliRunner
|
||||||
|
|
||||||
|
from test_site import create_app
|
||||||
|
from testlib import get_client
|
||||||
|
from testlib_txn import Accounts, NEXT_URI, add_txn
|
||||||
|
|
||||||
|
|
||||||
|
class SummeryHelperTestCase(unittest.TestCase):
|
||||||
|
"""The summary helper test case."""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
"""Sets up the test.
|
||||||
|
This is run once per test.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
self.app: Flask = create_app(is_testing=True)
|
||||||
|
|
||||||
|
runner: FlaskCliRunner = self.app.test_cli_runner()
|
||||||
|
with self.app.app_context():
|
||||||
|
from accounting.models import BaseAccount, Transaction, \
|
||||||
|
JournalEntry
|
||||||
|
result: Result
|
||||||
|
result = runner.invoke(args="init-db")
|
||||||
|
self.assertEqual(result.exit_code, 0)
|
||||||
|
if BaseAccount.query.first() is None:
|
||||||
|
result = runner.invoke(args="accounting-init-base")
|
||||||
|
self.assertEqual(result.exit_code, 0)
|
||||||
|
result = runner.invoke(args=["accounting-init-currencies",
|
||||||
|
"-u", "editor"])
|
||||||
|
self.assertEqual(result.exit_code, 0)
|
||||||
|
result = runner.invoke(args=["accounting-init-accounts",
|
||||||
|
"-u", "editor"])
|
||||||
|
self.assertEqual(result.exit_code, 0)
|
||||||
|
Transaction.query.delete()
|
||||||
|
JournalEntry.query.delete()
|
||||||
|
|
||||||
|
self.client, self.csrf_token = get_client(self.app, "editor")
|
||||||
|
|
||||||
|
def test_summary_helper(self) -> None:
|
||||||
|
"""Test the summary helper.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
from accounting.transaction.summary_helper import SummaryHelper
|
||||||
|
for form in get_form_data(self.csrf_token):
|
||||||
|
add_txn(self.client, form)
|
||||||
|
with self.app.app_context():
|
||||||
|
helper: SummaryHelper = SummaryHelper()
|
||||||
|
|
||||||
|
# Debit-General
|
||||||
|
self.assertEqual(len(helper.debit.general.tags), 2)
|
||||||
|
self.assertEqual(helper.debit.general.tags[0].name, "Lunch")
|
||||||
|
self.assertEqual(len(helper.debit.general.tags[0].accounts), 2)
|
||||||
|
self.assertEqual(helper.debit.general.tags[0].accounts[0].code,
|
||||||
|
Accounts.MEAL)
|
||||||
|
self.assertEqual(helper.debit.general.tags[0].accounts[1].code,
|
||||||
|
Accounts.PAYABLE)
|
||||||
|
self.assertEqual(helper.debit.general.tags[1].name, "Dinner")
|
||||||
|
self.assertEqual(len(helper.debit.general.tags[1].accounts), 2)
|
||||||
|
self.assertEqual(helper.debit.general.tags[1].accounts[0].code,
|
||||||
|
Accounts.MEAL)
|
||||||
|
self.assertEqual(helper.debit.general.tags[1].accounts[1].code,
|
||||||
|
Accounts.PAYABLE)
|
||||||
|
|
||||||
|
# Debit-Travel
|
||||||
|
self.assertEqual(len(helper.debit.travel.tags), 3)
|
||||||
|
self.assertEqual(helper.debit.travel.tags[0].name, "Bike")
|
||||||
|
self.assertEqual(len(helper.debit.travel.tags[0].accounts), 1)
|
||||||
|
self.assertEqual(helper.debit.travel.tags[0].accounts[0].code,
|
||||||
|
Accounts.TRAVEL)
|
||||||
|
self.assertEqual(helper.debit.travel.tags[1].name, "Taxi")
|
||||||
|
self.assertEqual(len(helper.debit.travel.tags[1].accounts), 1)
|
||||||
|
self.assertEqual(helper.debit.travel.tags[1].accounts[0].code,
|
||||||
|
Accounts.TRAVEL)
|
||||||
|
self.assertEqual(helper.debit.travel.tags[2].name, "Airplane")
|
||||||
|
self.assertEqual(len(helper.debit.travel.tags[2].accounts), 1)
|
||||||
|
self.assertEqual(helper.debit.travel.tags[2].accounts[0].code,
|
||||||
|
Accounts.TRAVEL)
|
||||||
|
|
||||||
|
# Debit-Bus
|
||||||
|
self.assertEqual(len(helper.debit.bus.tags), 2)
|
||||||
|
self.assertEqual(helper.debit.bus.tags[0].name, "Train")
|
||||||
|
self.assertEqual(len(helper.debit.bus.tags[0].accounts), 1)
|
||||||
|
self.assertEqual(helper.debit.bus.tags[0].accounts[0].code,
|
||||||
|
Accounts.TRAVEL)
|
||||||
|
self.assertEqual(helper.debit.bus.tags[1].name, "Bus")
|
||||||
|
self.assertEqual(len(helper.debit.bus.tags[1].accounts), 1)
|
||||||
|
self.assertEqual(helper.debit.bus.tags[1].accounts[0].code,
|
||||||
|
Accounts.TRAVEL)
|
||||||
|
|
||||||
|
# Credit-General
|
||||||
|
self.assertEqual(len(helper.credit.general.tags), 2)
|
||||||
|
self.assertEqual(helper.credit.general.tags[0].name, "Lunch")
|
||||||
|
self.assertEqual(len(helper.credit.general.tags[0].accounts), 3)
|
||||||
|
self.assertEqual(helper.credit.general.tags[0].accounts[0].code,
|
||||||
|
Accounts.PAYABLE)
|
||||||
|
self.assertEqual(helper.credit.general.tags[0].accounts[1].code,
|
||||||
|
Accounts.BANK)
|
||||||
|
self.assertEqual(helper.credit.general.tags[0].accounts[2].code,
|
||||||
|
Accounts.CASH)
|
||||||
|
self.assertEqual(helper.credit.general.tags[1].name, "Dinner")
|
||||||
|
self.assertEqual(len(helper.credit.general.tags[1].accounts), 2)
|
||||||
|
self.assertEqual(helper.credit.general.tags[1].accounts[0].code,
|
||||||
|
Accounts.BANK)
|
||||||
|
self.assertEqual(helper.credit.general.tags[1].accounts[1].code,
|
||||||
|
Accounts.PAYABLE)
|
||||||
|
|
||||||
|
# Credit-Travel
|
||||||
|
self.assertEqual(len(helper.credit.travel.tags), 2)
|
||||||
|
self.assertEqual(helper.credit.travel.tags[0].name, "Bike")
|
||||||
|
self.assertEqual(len(helper.credit.travel.tags[0].accounts), 2)
|
||||||
|
self.assertEqual(helper.credit.travel.tags[0].accounts[0].code,
|
||||||
|
Accounts.PAYABLE)
|
||||||
|
self.assertEqual(helper.credit.travel.tags[0].accounts[1].code,
|
||||||
|
Accounts.PREPAID)
|
||||||
|
self.assertEqual(helper.credit.travel.tags[1].name, "Taxi")
|
||||||
|
self.assertEqual(len(helper.credit.travel.tags[1].accounts), 2)
|
||||||
|
self.assertEqual(helper.credit.travel.tags[1].accounts[0].code,
|
||||||
|
Accounts.PAYABLE)
|
||||||
|
self.assertEqual(helper.credit.travel.tags[1].accounts[1].code,
|
||||||
|
Accounts.CASH)
|
||||||
|
|
||||||
|
# Credit-Bus
|
||||||
|
self.assertEqual(len(helper.credit.bus.tags), 2)
|
||||||
|
self.assertEqual(helper.credit.bus.tags[0].name, "Train")
|
||||||
|
self.assertEqual(len(helper.credit.bus.tags[0].accounts), 2)
|
||||||
|
self.assertEqual(helper.credit.bus.tags[0].accounts[0].code,
|
||||||
|
Accounts.PREPAID)
|
||||||
|
self.assertEqual(helper.credit.bus.tags[0].accounts[1].code,
|
||||||
|
Accounts.PAYABLE)
|
||||||
|
self.assertEqual(helper.credit.bus.tags[1].name, "Bus")
|
||||||
|
self.assertEqual(len(helper.credit.bus.tags[1].accounts), 1)
|
||||||
|
self.assertEqual(helper.credit.bus.tags[1].accounts[0].code,
|
||||||
|
Accounts.PREPAID)
|
||||||
|
|
||||||
|
|
||||||
|
def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
||||||
|
"""Returns the form data for multiple transaction forms.
|
||||||
|
|
||||||
|
:param csrf_token: The CSRF token.
|
||||||
|
:return: A list of the form data.
|
||||||
|
"""
|
||||||
|
txn_date: str = date.today().isoformat()
|
||||||
|
return [{"csrf_token": csrf_token,
|
||||||
|
"next": NEXT_URI,
|
||||||
|
"date": txn_date,
|
||||||
|
"currency-0-code": "USD",
|
||||||
|
"currency-0-credit-0-account_code": Accounts.SERVICE,
|
||||||
|
"currency-0-credit-0-summary": " Salary ",
|
||||||
|
"currency-0-credit-0-amount": "2500"},
|
||||||
|
{"csrf_token": csrf_token,
|
||||||
|
"next": NEXT_URI,
|
||||||
|
"date": txn_date,
|
||||||
|
"currency-0-code": "USD",
|
||||||
|
"currency-0-debit-0-account_code": Accounts.MEAL,
|
||||||
|
"currency-0-debit-0-summary": " Lunch—Fish ",
|
||||||
|
"currency-0-debit-0-amount": "4.7",
|
||||||
|
"currency-0-credit-0-account_code": Accounts.BANK,
|
||||||
|
"currency-0-credit-0-summary": " Lunch—Fish ",
|
||||||
|
"currency-0-credit-0-amount": "4.7",
|
||||||
|
"currency-0-debit-1-account_code": Accounts.MEAL,
|
||||||
|
"currency-0-debit-1-summary": " Lunch—Fries ",
|
||||||
|
"currency-0-debit-1-amount": "2.15",
|
||||||
|
"currency-0-credit-1-account_code": Accounts.PAYABLE,
|
||||||
|
"currency-0-credit-1-summary": " Lunch—Fries ",
|
||||||
|
"currency-0-credit-1-amount": "2.15",
|
||||||
|
"currency-0-debit-2-account_code": Accounts.MEAL,
|
||||||
|
"currency-0-debit-2-summary": " Dinner—Hamburger ",
|
||||||
|
"currency-0-debit-2-amount": "4.25",
|
||||||
|
"currency-0-credit-2-account_code": Accounts.BANK,
|
||||||
|
"currency-0-credit-2-summary": " Dinner—Hamburger ",
|
||||||
|
"currency-0-credit-2-amount": "4.25"},
|
||||||
|
{"csrf_token": csrf_token,
|
||||||
|
"next": NEXT_URI,
|
||||||
|
"date": txn_date,
|
||||||
|
"currency-0-code": "USD",
|
||||||
|
"currency-0-debit-0-account_code": Accounts.MEAL,
|
||||||
|
"currency-0-debit-0-summary": " Lunch—Salad ",
|
||||||
|
"currency-0-debit-0-amount": "4.99",
|
||||||
|
"currency-0-credit-0-account_code": Accounts.CASH,
|
||||||
|
"currency-0-credit-0-summary": " Lunch—Salad ",
|
||||||
|
"currency-0-credit-0-amount": "4.99",
|
||||||
|
"currency-0-debit-1-account_code": Accounts.MEAL,
|
||||||
|
"currency-0-debit-1-summary": " Dinner—Steak ",
|
||||||
|
"currency-0-debit-1-amount": "8.28",
|
||||||
|
"currency-0-credit-1-account_code": Accounts.PAYABLE,
|
||||||
|
"currency-0-credit-1-summary": " Dinner—Steak ",
|
||||||
|
"currency-0-credit-1-amount": "8.28"},
|
||||||
|
{"csrf_token": csrf_token,
|
||||||
|
"next": NEXT_URI,
|
||||||
|
"date": txn_date,
|
||||||
|
"currency-0-code": "USD",
|
||||||
|
"currency-0-debit-0-account_code": Accounts.MEAL,
|
||||||
|
"currency-0-debit-0-summary": " Lunch—Pizza ",
|
||||||
|
"currency-0-debit-0-amount": "5.49",
|
||||||
|
"currency-0-credit-0-account_code": Accounts.PAYABLE,
|
||||||
|
"currency-0-credit-0-summary": " Lunch—Pizza ",
|
||||||
|
"currency-0-credit-0-amount": "5.49",
|
||||||
|
"currency-0-debit-1-account_code": Accounts.MEAL,
|
||||||
|
"currency-0-debit-1-summary": " Lunch—Noodles ",
|
||||||
|
"currency-0-debit-1-amount": "7.47",
|
||||||
|
"currency-0-credit-1-account_code": Accounts.PAYABLE,
|
||||||
|
"currency-0-credit-1-summary": " Lunch—Noodles ",
|
||||||
|
"currency-0-credit-1-amount": "7.47"},
|
||||||
|
{"csrf_token": csrf_token,
|
||||||
|
"next": NEXT_URI,
|
||||||
|
"date": txn_date,
|
||||||
|
"currency-0-code": "USD",
|
||||||
|
"currency-0-debit-0-account_code": Accounts.TRAVEL,
|
||||||
|
"currency-0-debit-0-summary": " Airplane—Lake City↔Hill Town ",
|
||||||
|
"currency-0-debit-0-amount": "800"},
|
||||||
|
{"csrf_token": csrf_token,
|
||||||
|
"next": NEXT_URI,
|
||||||
|
"date": txn_date,
|
||||||
|
"currency-0-code": "USD",
|
||||||
|
"currency-0-debit-0-account_code": Accounts.TRAVEL,
|
||||||
|
"currency-0-debit-0-summary": " Bus—323—Downtown→Museum ",
|
||||||
|
"currency-0-debit-0-amount": "2.5",
|
||||||
|
"currency-0-credit-0-account_code": Accounts.PREPAID,
|
||||||
|
"currency-0-credit-0-summary": " Bus—323—Downtown→Museum ",
|
||||||
|
"currency-0-credit-0-amount": "2.5",
|
||||||
|
"currency-0-debit-1-account_code": Accounts.TRAVEL,
|
||||||
|
"currency-0-debit-1-summary": " Train—Blue—Museum→Central ",
|
||||||
|
"currency-0-debit-1-amount": "3.2",
|
||||||
|
"currency-0-credit-1-account_code": Accounts.PREPAID,
|
||||||
|
"currency-0-credit-1-summary": " Train—Blue—Museum→Central ",
|
||||||
|
"currency-0-credit-1-amount": "3.2",
|
||||||
|
"currency-0-debit-2-account_code": Accounts.TRAVEL,
|
||||||
|
"currency-0-debit-2-summary": " Train—Green—Central→Mall ",
|
||||||
|
"currency-0-debit-2-amount": "3.2",
|
||||||
|
"currency-0-credit-2-account_code": Accounts.PREPAID,
|
||||||
|
"currency-0-credit-2-summary": " Train—Green—Central→Mall ",
|
||||||
|
"currency-0-credit-2-amount": "3.2",
|
||||||
|
"currency-0-debit-3-account_code": Accounts.TRAVEL,
|
||||||
|
"currency-0-debit-3-summary": " Train—Red—Mall→Museum ",
|
||||||
|
"currency-0-debit-3-amount": "4.4",
|
||||||
|
"currency-0-credit-3-account_code": Accounts.PAYABLE,
|
||||||
|
"currency-0-credit-3-summary": " Train—Red—Mall→Museum ",
|
||||||
|
"currency-0-credit-3-amount": "4.4"},
|
||||||
|
{"csrf_token": csrf_token,
|
||||||
|
"next": NEXT_URI,
|
||||||
|
"date": txn_date,
|
||||||
|
"currency-0-code": "USD",
|
||||||
|
"currency-0-debit-0-account_code": Accounts.TRAVEL,
|
||||||
|
"currency-0-debit-0-summary": " Taxi—Museum→Office ",
|
||||||
|
"currency-0-debit-0-amount": "15.5",
|
||||||
|
"currency-0-credit-0-account_code": Accounts.CASH,
|
||||||
|
"currency-0-credit-0-summary": " Taxi—Museum→Office ",
|
||||||
|
"currency-0-credit-0-amount": "15.5",
|
||||||
|
"currency-0-debit-1-account_code": Accounts.TRAVEL,
|
||||||
|
"currency-0-debit-1-summary": " Taxi—Office→Restaurant ",
|
||||||
|
"currency-0-debit-1-amount": "12",
|
||||||
|
"currency-0-credit-1-account_code": Accounts.PAYABLE,
|
||||||
|
"currency-0-credit-1-summary": " Taxi—Office→Restaurant ",
|
||||||
|
"currency-0-credit-1-amount": "12",
|
||||||
|
"currency-0-debit-2-account_code": Accounts.TRAVEL,
|
||||||
|
"currency-0-debit-2-summary": " Taxi—Restaurant→City Hall ",
|
||||||
|
"currency-0-debit-2-amount": "8",
|
||||||
|
"currency-0-credit-2-account_code": Accounts.PAYABLE,
|
||||||
|
"currency-0-credit-2-summary": " Taxi—Restaurant→City Hall ",
|
||||||
|
"currency-0-credit-2-amount": "8",
|
||||||
|
"currency-0-debit-3-account_code": Accounts.TRAVEL,
|
||||||
|
"currency-0-debit-3-summary": " Bike—City Hall→Office ",
|
||||||
|
"currency-0-debit-3-amount": "3.5",
|
||||||
|
"currency-0-credit-3-account_code": Accounts.PAYABLE,
|
||||||
|
"currency-0-credit-3-summary": " Bike—City Hall→Office ",
|
||||||
|
"currency-0-credit-3-amount": "3.5",
|
||||||
|
"currency-0-debit-4-account_code": Accounts.TRAVEL,
|
||||||
|
"currency-0-debit-4-summary": " Bike—Restaurant→Office ",
|
||||||
|
"currency-0-debit-4-amount": "4",
|
||||||
|
"currency-0-credit-4-account_code": Accounts.PAYABLE,
|
||||||
|
"currency-0-credit-4-summary": " Bike—Restaurant→Office ",
|
||||||
|
"currency-0-credit-4-amount": "4",
|
||||||
|
"currency-0-debit-5-account_code": Accounts.TRAVEL,
|
||||||
|
"currency-0-debit-5-summary": " Bike—Office→Theatre ",
|
||||||
|
"currency-0-debit-5-amount": "1.5",
|
||||||
|
"currency-0-credit-5-account_code": Accounts.PAYABLE,
|
||||||
|
"currency-0-credit-5-summary": " Bike—Office→Theatre ",
|
||||||
|
"currency-0-credit-5-amount": "1.5",
|
||||||
|
"currency-0-debit-6-account_code": Accounts.TRAVEL,
|
||||||
|
"currency-0-debit-6-summary": " Bike—Theatre→Home ",
|
||||||
|
"currency-0-debit-6-amount": "5.5",
|
||||||
|
"currency-0-credit-6-account_code": Accounts.PREPAID,
|
||||||
|
"currency-0-credit-6-summary": " Bike—Theatre→Home ",
|
||||||
|
"currency-0-credit-6-amount": "5.5"},
|
||||||
|
{"csrf_token": csrf_token,
|
||||||
|
"next": NEXT_URI,
|
||||||
|
"date": txn_date,
|
||||||
|
"currency-0-code": "USD",
|
||||||
|
"currency-0-debit-0-account_code": Accounts.PAYABLE,
|
||||||
|
"currency-0-debit-0-summary": " Dinner—Steak ",
|
||||||
|
"currency-0-debit-0-amount": "8.28",
|
||||||
|
"currency-0-credit-0-account_code": Accounts.BANK,
|
||||||
|
"currency-0-credit-0-summary": " Dinner—Steak ",
|
||||||
|
"currency-0-credit-0-amount": "8.28",
|
||||||
|
"currency-0-debit-1-account_code": Accounts.PAYABLE,
|
||||||
|
"currency-0-debit-1-summary": " Lunch—Pizza ",
|
||||||
|
"currency-0-debit-1-amount": "5.49",
|
||||||
|
"currency-0-credit-1-account_code": Accounts.BANK,
|
||||||
|
"currency-0-credit-1-summary": " Lunch—Pizza ",
|
||||||
|
"currency-0-credit-1-amount": "5.49"}]
|
||||||
|
|
@ -31,7 +31,7 @@ from testlib import get_client
|
|||||||
from testlib_txn import Accounts, get_add_form, get_unchanged_update_form, \
|
from testlib_txn import Accounts, get_add_form, get_unchanged_update_form, \
|
||||||
get_update_form, match_txn_detail, set_negative_amount, \
|
get_update_form, match_txn_detail, set_negative_amount, \
|
||||||
remove_debit_in_a_currency, remove_credit_in_a_currency, NEXT_URI, \
|
remove_debit_in_a_currency, remove_credit_in_a_currency, NEXT_URI, \
|
||||||
NON_EMPTY_NOTE, EMPTY_NOTE
|
NON_EMPTY_NOTE, EMPTY_NOTE, add_txn
|
||||||
|
|
||||||
PREFIX: str = "/accounting/transactions"
|
PREFIX: str = "/accounting/transactions"
|
||||||
"""The URL prefix of the transaction management."""
|
"""The URL prefix of the transaction management."""
|
||||||
@ -75,7 +75,7 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "nobody")
|
client, csrf_token = get_client(self.app, "nobody")
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
add_form: dict[str, str] = self.__get_add_form()
|
add_form: dict[str, str] = self.__get_add_form()
|
||||||
add_form["csrf_token"] = csrf_token
|
add_form["csrf_token"] = csrf_token
|
||||||
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
@ -110,7 +110,7 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "viewer")
|
client, csrf_token = get_client(self.app, "viewer")
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
add_form: dict[str, str] = self.__get_add_form()
|
add_form: dict[str, str] = self.__get_add_form()
|
||||||
add_form["csrf_token"] = csrf_token
|
add_form["csrf_token"] = csrf_token
|
||||||
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
@ -144,7 +144,7 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
add_form: dict[str, str] = self.__get_add_form()
|
add_form: dict[str, str] = self.__get_add_form()
|
||||||
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
@ -326,7 +326,7 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Transaction, TransactionCurrency
|
from accounting.models import Transaction, TransactionCurrency
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
||||||
edit_uri: str = f"{PREFIX}/{txn_id}/edit?next=%2F_next"
|
edit_uri: str = f"{PREFIX}/{txn_id}/edit?next=%2F_next"
|
||||||
update_uri: str = f"{PREFIX}/{txn_id}/update"
|
update_uri: str = f"{PREFIX}/{txn_id}/update"
|
||||||
@ -479,7 +479,7 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Transaction
|
from accounting.models import Transaction
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
||||||
update_uri: str = f"{PREFIX}/{txn_id}/update"
|
update_uri: str = f"{PREFIX}/{txn_id}/update"
|
||||||
txn: Transaction
|
txn: Transaction
|
||||||
@ -513,7 +513,7 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Transaction
|
from accounting.models import Transaction
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
editor_username, editor2_username = "editor", "editor2"
|
editor_username, editor2_username = "editor", "editor2"
|
||||||
client, csrf_token = get_client(self.app, editor2_username)
|
client, csrf_token = get_client(self.app, editor2_username)
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
||||||
@ -542,7 +542,7 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
||||||
delete_uri: str = f"{PREFIX}/{txn_id}/delete"
|
delete_uri: str = f"{PREFIX}/{txn_id}/delete"
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
@ -562,17 +562,6 @@ class CashIncomeTransactionTestCase(unittest.TestCase):
|
|||||||
"next": NEXT_URI})
|
"next": NEXT_URI})
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
def __add_txn(self) -> int:
|
|
||||||
"""Adds a transaction.
|
|
||||||
|
|
||||||
:return: The newly-added transaction ID..
|
|
||||||
"""
|
|
||||||
store_uri: str = f"{PREFIX}/store/income"
|
|
||||||
form: dict[str, str] = self.__get_add_form()
|
|
||||||
response: httpx.Response = self.client.post(store_uri, data=form)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
return match_txn_detail(response.headers["Location"])
|
|
||||||
|
|
||||||
def __get_add_form(self) -> dict[str, str]:
|
def __get_add_form(self) -> dict[str, str]:
|
||||||
"""Returns the form data to add a new transaction.
|
"""Returns the form data to add a new transaction.
|
||||||
|
|
||||||
@ -647,7 +636,7 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "nobody")
|
client, csrf_token = get_client(self.app, "nobody")
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
add_form: dict[str, str] = self.__get_add_form()
|
add_form: dict[str, str] = self.__get_add_form()
|
||||||
add_form["csrf_token"] = csrf_token
|
add_form["csrf_token"] = csrf_token
|
||||||
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
@ -682,7 +671,7 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "viewer")
|
client, csrf_token = get_client(self.app, "viewer")
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
add_form: dict[str, str] = self.__get_add_form()
|
add_form: dict[str, str] = self.__get_add_form()
|
||||||
add_form["csrf_token"] = csrf_token
|
add_form["csrf_token"] = csrf_token
|
||||||
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
@ -716,7 +705,7 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
add_form: dict[str, str] = self.__get_add_form()
|
add_form: dict[str, str] = self.__get_add_form()
|
||||||
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
@ -901,7 +890,7 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Transaction, TransactionCurrency
|
from accounting.models import Transaction, TransactionCurrency
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
||||||
edit_uri: str = f"{PREFIX}/{txn_id}/edit?next=%2F_next"
|
edit_uri: str = f"{PREFIX}/{txn_id}/edit?next=%2F_next"
|
||||||
update_uri: str = f"{PREFIX}/{txn_id}/update"
|
update_uri: str = f"{PREFIX}/{txn_id}/update"
|
||||||
@ -1058,7 +1047,7 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Transaction
|
from accounting.models import Transaction
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
||||||
update_uri: str = f"{PREFIX}/{txn_id}/update"
|
update_uri: str = f"{PREFIX}/{txn_id}/update"
|
||||||
txn: Transaction
|
txn: Transaction
|
||||||
@ -1092,7 +1081,7 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Transaction
|
from accounting.models import Transaction
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
editor_username, editor2_username = "editor", "editor2"
|
editor_username, editor2_username = "editor", "editor2"
|
||||||
client, csrf_token = get_client(self.app, editor2_username)
|
client, csrf_token = get_client(self.app, editor2_username)
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
||||||
@ -1121,7 +1110,7 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
||||||
delete_uri: str = f"{PREFIX}/{txn_id}/delete"
|
delete_uri: str = f"{PREFIX}/{txn_id}/delete"
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
@ -1141,17 +1130,6 @@ class CashExpenseTransactionTestCase(unittest.TestCase):
|
|||||||
"next": NEXT_URI})
|
"next": NEXT_URI})
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
def __add_txn(self) -> int:
|
|
||||||
"""Adds a transaction.
|
|
||||||
|
|
||||||
:return: The newly-added transaction ID..
|
|
||||||
"""
|
|
||||||
store_uri: str = f"{PREFIX}/store/expense"
|
|
||||||
form: dict[str, str] = self.__get_add_form()
|
|
||||||
response: httpx.Response = self.client.post(store_uri, data=form)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
return match_txn_detail(response.headers["Location"])
|
|
||||||
|
|
||||||
def __get_add_form(self) -> dict[str, str]:
|
def __get_add_form(self) -> dict[str, str]:
|
||||||
"""Returns the form data to add a new transaction.
|
"""Returns the form data to add a new transaction.
|
||||||
|
|
||||||
@ -1226,7 +1204,7 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "nobody")
|
client, csrf_token = get_client(self.app, "nobody")
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
add_form: dict[str, str] = self.__get_add_form()
|
add_form: dict[str, str] = self.__get_add_form()
|
||||||
add_form["csrf_token"] = csrf_token
|
add_form["csrf_token"] = csrf_token
|
||||||
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
@ -1261,7 +1239,7 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "viewer")
|
client, csrf_token = get_client(self.app, "viewer")
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
add_form: dict[str, str] = self.__get_add_form()
|
add_form: dict[str, str] = self.__get_add_form()
|
||||||
add_form["csrf_token"] = csrf_token
|
add_form["csrf_token"] = csrf_token
|
||||||
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
@ -1295,7 +1273,7 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
add_form: dict[str, str] = self.__get_add_form()
|
add_form: dict[str, str] = self.__get_add_form()
|
||||||
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
update_form: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
@ -1507,7 +1485,7 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Transaction, TransactionCurrency
|
from accounting.models import Transaction, TransactionCurrency
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
||||||
edit_uri: str = f"{PREFIX}/{txn_id}/edit?next=%2F_next"
|
edit_uri: str = f"{PREFIX}/{txn_id}/edit?next=%2F_next"
|
||||||
update_uri: str = f"{PREFIX}/{txn_id}/update"
|
update_uri: str = f"{PREFIX}/{txn_id}/update"
|
||||||
@ -1698,7 +1676,7 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Transaction
|
from accounting.models import Transaction
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
||||||
update_uri: str = f"{PREFIX}/{txn_id}/update"
|
update_uri: str = f"{PREFIX}/{txn_id}/update"
|
||||||
txn: Transaction
|
txn: Transaction
|
||||||
@ -1732,7 +1710,7 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Transaction
|
from accounting.models import Transaction
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
editor_username, editor2_username = "editor", "editor2"
|
editor_username, editor2_username = "editor", "editor2"
|
||||||
client, csrf_token = get_client(self.app, editor2_username)
|
client, csrf_token = get_client(self.app, editor2_username)
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
||||||
@ -1762,7 +1740,7 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Transaction, TransactionCurrency
|
from accounting.models import Transaction, TransactionCurrency
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?as=income&next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?as=income&next=%2F_next"
|
||||||
update_uri: str = f"{PREFIX}/{txn_id}/update?as=income"
|
update_uri: str = f"{PREFIX}/{txn_id}/update?as=income"
|
||||||
form_0: dict[str, str] = self.__get_update_form(txn_id)
|
form_0: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
@ -1861,7 +1839,7 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Transaction, TransactionCurrency
|
from accounting.models import Transaction, TransactionCurrency
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?as=expense&next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?as=expense&next=%2F_next"
|
||||||
update_uri: str = f"{PREFIX}/{txn_id}/update?as=expense"
|
update_uri: str = f"{PREFIX}/{txn_id}/update?as=expense"
|
||||||
form_0: dict[str, str] = self.__get_update_form(txn_id)
|
form_0: dict[str, str] = self.__get_update_form(txn_id)
|
||||||
@ -1963,7 +1941,7 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
txn_id: int = self.__add_txn()
|
txn_id: int = add_txn(self.client, self.__get_add_form())
|
||||||
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
detail_uri: str = f"{PREFIX}/{txn_id}?next=%2F_next"
|
||||||
delete_uri: str = f"{PREFIX}/{txn_id}/delete"
|
delete_uri: str = f"{PREFIX}/{txn_id}/delete"
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
@ -1983,17 +1961,6 @@ class TransferTransactionTestCase(unittest.TestCase):
|
|||||||
"next": NEXT_URI})
|
"next": NEXT_URI})
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
def __add_txn(self) -> int:
|
|
||||||
"""Adds a transaction.
|
|
||||||
|
|
||||||
:return: The newly-added transaction ID..
|
|
||||||
"""
|
|
||||||
store_uri: str = f"{PREFIX}/store/transfer"
|
|
||||||
form: dict[str, str] = self.__get_add_form()
|
|
||||||
response: httpx.Response = self.client.post(store_uri, data=form)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
return match_txn_detail(response.headers["Location"])
|
|
||||||
|
|
||||||
def __get_add_form(self) -> dict[str, str]:
|
def __get_add_form(self) -> dict[str, str]:
|
||||||
"""Returns the form data to add a new transaction.
|
"""Returns the form data to add a new transaction.
|
||||||
|
|
||||||
@ -2062,11 +2029,11 @@ class TransactionReorderTestCase(unittest.TestCase):
|
|||||||
from accounting.models import Transaction
|
from accounting.models import Transaction
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
id_1: int = self.__add_income_txn()
|
id_1: int = add_txn(self.client, self.__get_add_income_form())
|
||||||
id_2: int = self.__add_expense_txn()
|
id_2: int = add_txn(self.client, self.__get_add_expense_form())
|
||||||
id_3: int = self.__add_transfer_txn()
|
id_3: int = add_txn(self.client, self.__get_add_transfer_form())
|
||||||
id_4: int = self.__add_income_txn()
|
id_4: int = add_txn(self.client, self.__get_add_income_form())
|
||||||
id_5: int = self.__add_expense_txn()
|
id_5: int = add_txn(self.client, self.__get_add_expense_form())
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.app.app_context():
|
||||||
txn_1: Transaction = db.session.get(Transaction, id_1)
|
txn_1: Transaction = db.session.get(Transaction, id_1)
|
||||||
@ -2108,11 +2075,11 @@ class TransactionReorderTestCase(unittest.TestCase):
|
|||||||
from accounting.models import Transaction
|
from accounting.models import Transaction
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
id_1: int = self.__add_income_txn()
|
id_1: int = add_txn(self.client, self.__get_add_income_form())
|
||||||
id_2: int = self.__add_expense_txn()
|
id_2: int = add_txn(self.client, self.__get_add_expense_form())
|
||||||
id_3: int = self.__add_transfer_txn()
|
id_3: int = add_txn(self.client, self.__get_add_transfer_form())
|
||||||
id_4: int = self.__add_income_txn()
|
id_4: int = add_txn(self.client, self.__get_add_income_form())
|
||||||
id_5: int = self.__add_expense_txn()
|
id_5: int = add_txn(self.client, self.__get_add_expense_form())
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.app.app_context():
|
||||||
txn_date: date = db.session.get(Transaction, id_1).date
|
txn_date: date = db.session.get(Transaction, id_1).date
|
||||||
@ -2160,17 +2127,6 @@ class TransactionReorderTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(db.session.get(Transaction, id_4).no, 1)
|
self.assertEqual(db.session.get(Transaction, id_4).no, 1)
|
||||||
self.assertEqual(db.session.get(Transaction, id_5).no, 5)
|
self.assertEqual(db.session.get(Transaction, id_5).no, 5)
|
||||||
|
|
||||||
def __add_income_txn(self) -> int:
|
|
||||||
"""Adds a transaction.
|
|
||||||
|
|
||||||
:return: The newly-added transaction ID..
|
|
||||||
"""
|
|
||||||
store_uri: str = f"{PREFIX}/store/income"
|
|
||||||
form: dict[str, str] = self.__get_add_income_form()
|
|
||||||
response: httpx.Response = self.client.post(store_uri, data=form)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
return match_txn_detail(response.headers["Location"])
|
|
||||||
|
|
||||||
def __get_add_income_form(self) -> dict[str, str]:
|
def __get_add_income_form(self) -> dict[str, str]:
|
||||||
"""Returns the form data to add a new transaction.
|
"""Returns the form data to add a new transaction.
|
||||||
|
|
||||||
@ -2180,17 +2136,6 @@ class TransactionReorderTestCase(unittest.TestCase):
|
|||||||
form = {x: form[x] for x in form if "-debit-" not in x}
|
form = {x: form[x] for x in form if "-debit-" not in x}
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def __add_expense_txn(self) -> int:
|
|
||||||
"""Adds a transaction.
|
|
||||||
|
|
||||||
:return: The newly-added transaction ID..
|
|
||||||
"""
|
|
||||||
store_uri: str = f"{PREFIX}/store/expense"
|
|
||||||
form: dict[str, str] = self.__get_add_expense_form()
|
|
||||||
response: httpx.Response = self.client.post(store_uri, data=form)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
return match_txn_detail(response.headers["Location"])
|
|
||||||
|
|
||||||
def __get_add_expense_form(self) -> dict[str, str]:
|
def __get_add_expense_form(self) -> dict[str, str]:
|
||||||
"""Returns the form data to add a new transaction.
|
"""Returns the form data to add a new transaction.
|
||||||
|
|
||||||
@ -2214,17 +2159,6 @@ class TransactionReorderTestCase(unittest.TestCase):
|
|||||||
form = {x: form[x] for x in form if "-credit-" not in x}
|
form = {x: form[x] for x in form if "-credit-" not in x}
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def __add_transfer_txn(self) -> int:
|
|
||||||
"""Adds a transaction.
|
|
||||||
|
|
||||||
:return: The newly-added transaction ID..
|
|
||||||
"""
|
|
||||||
store_uri: str = f"{PREFIX}/store/transfer"
|
|
||||||
form: dict[str, str] = self.__get_add_transfer_form()
|
|
||||||
response: httpx.Response = self.client.post(store_uri, data=form)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
return match_txn_detail(response.headers["Location"])
|
|
||||||
|
|
||||||
def __get_add_transfer_form(self) -> dict[str, str]:
|
def __get_add_transfer_form(self) -> dict[str, str]:
|
||||||
"""Returns the form data to add a new transaction.
|
"""Returns the form data to add a new transaction.
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ from decimal import Decimal
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from secrets import randbelow
|
from secrets import randbelow
|
||||||
|
|
||||||
|
import httpx
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from test_site import db
|
from test_site import db
|
||||||
@ -38,11 +39,14 @@ class Accounts:
|
|||||||
"""The shortcuts to the common accounts."""
|
"""The shortcuts to the common accounts."""
|
||||||
CASH: str = "1111-001"
|
CASH: str = "1111-001"
|
||||||
BANK: str = "1113-001"
|
BANK: str = "1113-001"
|
||||||
|
PREPAID: str = "1258-001"
|
||||||
|
PAYABLE: str = "2141-001"
|
||||||
SALES: str = "4111-001"
|
SALES: str = "4111-001"
|
||||||
SERVICE: str = "4611-001"
|
SERVICE: str = "4611-001"
|
||||||
AGENCY: str = "4711-001"
|
AGENCY: str = "4711-001"
|
||||||
OFFICE: str = "5153-001"
|
OFFICE: str = "6153-001"
|
||||||
TRAVEL: str = "5154-001"
|
TRAVEL: str = "6154-001"
|
||||||
|
MEAL: str = "6172-001"
|
||||||
INTEREST: str = "4111-001"
|
INTEREST: str = "4111-001"
|
||||||
DONATION: str = "7481-001"
|
DONATION: str = "7481-001"
|
||||||
RENT: str = "7482-001"
|
RENT: str = "7482-001"
|
||||||
@ -381,6 +385,25 @@ def __get_currency_prefix(form: dict[str, str], code: str) -> str:
|
|||||||
return m.group(1)
|
return m.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def add_txn(client: httpx.Client, form: dict[str, str]) -> int:
|
||||||
|
"""Adds a transfer transaction.
|
||||||
|
|
||||||
|
:param client: The client.
|
||||||
|
:param form: The form data.
|
||||||
|
:return: The newly-added transaction ID.
|
||||||
|
"""
|
||||||
|
prefix: str = "/accounting/transactions"
|
||||||
|
txn_type: str = "transfer"
|
||||||
|
if len({x for x in form if "-debit-" in x}) == 0:
|
||||||
|
txn_type = "income"
|
||||||
|
elif len({x for x in form if "-credit-" in x}) == 0:
|
||||||
|
txn_type = "expense"
|
||||||
|
store_uri = f"{prefix}/store/{txn_type}"
|
||||||
|
response: httpx.Response = client.post(store_uri, data=form)
|
||||||
|
assert response.status_code == 302
|
||||||
|
return match_txn_detail(response.headers["Location"])
|
||||||
|
|
||||||
|
|
||||||
def match_txn_detail(location: str) -> int:
|
def match_txn_detail(location: str) -> int:
|
||||||
"""Validates if the redirect location is the transaction detail, and
|
"""Validates if the redirect location is the transaction detail, and
|
||||||
returns the transaction ID on success.
|
returns the transaction ID on success.
|
||||||
|
Loading…
Reference in New Issue
Block a user