Added the summary helper for the transaction form.
This commit is contained in:
parent
fc8e257a10
commit
d5c2231794
531
src/accounting/static/js/summary-helper.js
Normal file
531
src/accounting/static/js/summary-helper.js
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
/* 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
|
||||||
|
* @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 tabs = Array.from(document.getElementsByClassName(this.#prefix + "-tab"));
|
||||||
|
tabs.forEach(function (tab) {
|
||||||
|
tab.onclick = function () {
|
||||||
|
helper.#switchToTab(tab.dataset.tabId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.#initializeGeneralTagHelper();
|
||||||
|
this.#initializeGeneralTripHelper();
|
||||||
|
this.#initializeBusTripHelper();
|
||||||
|
this.#initializeNumberHelper();
|
||||||
|
this.#initializeSubmission();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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"));
|
||||||
|
tabs.forEach(function (tab) {
|
||||||
|
if (tab.dataset.tabId === tabId) {
|
||||||
|
tab.classList.add("active");
|
||||||
|
tab.ariaCurrent = "page";
|
||||||
|
} else {
|
||||||
|
tab.classList.remove("active");
|
||||||
|
tab.ariaCurrent = "false";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pages.forEach(function (page) {
|
||||||
|
if (page.dataset.tabId === tabId) {
|
||||||
|
page.classList.remove("d-none");
|
||||||
|
page.ariaCurrent = "page";
|
||||||
|
} else {
|
||||||
|
page.classList.add("d-none");
|
||||||
|
page.ariaCurrent = "false";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the general tag helper.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#initializeGeneralTagHelper() {
|
||||||
|
const buttons = Array.from(document.getElementsByClassName(this.#prefix + "-general-btn-tag"));
|
||||||
|
const summary = document.getElementById(this.#prefix + "-summary");
|
||||||
|
const tag = document.getElementById(this.#prefix + "-general-tag");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buttons.forEach(function (button) {
|
||||||
|
button.onclick = function () {
|
||||||
|
buttons.forEach(function (otherButton) {
|
||||||
|
otherButton.classList.remove("btn-primary");
|
||||||
|
otherButton.classList.add("btn-outline-primary");
|
||||||
|
});
|
||||||
|
button.classList.remove("btn-outline-primary");
|
||||||
|
button.classList.add("btn-primary");
|
||||||
|
tag.value = button.dataset.value;
|
||||||
|
updateSummary();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
tag.onchange = function () {
|
||||||
|
buttons.forEach(function (button) {
|
||||||
|
if (button.dataset.value === tag.value) {
|
||||||
|
button.classList.remove("btn-outline-primary");
|
||||||
|
button.classList.add("btn-primary");
|
||||||
|
} else {
|
||||||
|
button.classList.remove("btn-primary");
|
||||||
|
button.classList.add("btn-outline-primary");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateSummary();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the general trip helper.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#initializeGeneralTripHelper() {
|
||||||
|
const buttons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-btn-tag"));
|
||||||
|
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 updateSummary = function () {
|
||||||
|
let direction;
|
||||||
|
for (const button of directionButtons) {
|
||||||
|
if (button.classList.contains("btn-primary")) {
|
||||||
|
direction = button.dataset.arrow;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
summary.value = tag.value + "—" + from.value + direction + to.value;
|
||||||
|
};
|
||||||
|
buttons.forEach(function (button) {
|
||||||
|
button.onclick = function () {
|
||||||
|
buttons.forEach(function (otherButton) {
|
||||||
|
otherButton.classList.remove("btn-primary");
|
||||||
|
otherButton.classList.add("btn-outline-primary");
|
||||||
|
});
|
||||||
|
button.classList.remove("btn-outline-primary");
|
||||||
|
button.classList.add("btn-primary");
|
||||||
|
tag.value = button.dataset.value;
|
||||||
|
updateSummary();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
tag.onchange = function () {
|
||||||
|
buttons.forEach(function (button) {
|
||||||
|
if (button.dataset.value === tag.value) {
|
||||||
|
button.classList.remove("btn-outline-primary");
|
||||||
|
button.classList.add("btn-primary");
|
||||||
|
} else {
|
||||||
|
button.classList.remove("btn-primary");
|
||||||
|
button.classList.add("btn-outline-primary");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateSummary();
|
||||||
|
};
|
||||||
|
from.onchange = updateSummary;
|
||||||
|
directionButtons.forEach(function (button) {
|
||||||
|
button.onclick = function () {
|
||||||
|
directionButtons.forEach(function (otherButton) {
|
||||||
|
otherButton.classList.remove("btn-primary");
|
||||||
|
otherButton.classList.add("btn-outline-primary");
|
||||||
|
});
|
||||||
|
button.classList.remove("btn-outline-primary");
|
||||||
|
button.classList.add("btn-primary");
|
||||||
|
updateSummary();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
to.onchange = updateSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the bus trip helper.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#initializeBusTripHelper() {
|
||||||
|
const buttons = Array.from(document.getElementsByClassName(this.#prefix + "-bus-btn-tag"));
|
||||||
|
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 updateSummary = function () {
|
||||||
|
summary.value = tag.value + "—" + route.value + "—" + from.value + "→" + to.value;
|
||||||
|
};
|
||||||
|
buttons.forEach(function (button) {
|
||||||
|
button.onclick = function () {
|
||||||
|
buttons.forEach(function (otherButton) {
|
||||||
|
otherButton.classList.remove("btn-primary");
|
||||||
|
otherButton.classList.add("btn-outline-primary");
|
||||||
|
});
|
||||||
|
button.classList.remove("btn-outline-primary");
|
||||||
|
button.classList.add("btn-primary");
|
||||||
|
tag.value = button.dataset.value;
|
||||||
|
updateSummary();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
tag.onchange = function () {
|
||||||
|
buttons.forEach(function (button) {
|
||||||
|
if (button.dataset.value === tag.value) {
|
||||||
|
button.classList.remove("btn-outline-primary");
|
||||||
|
button.classList.add("btn-primary");
|
||||||
|
} else {
|
||||||
|
button.classList.remove("btn-primary");
|
||||||
|
button.classList.add("btn-outline-primary");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateSummary();
|
||||||
|
};
|
||||||
|
route.onchange = updateSummary;
|
||||||
|
from.onchange = updateSummary;
|
||||||
|
to.onchange = updateSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the summary.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#submit() {
|
||||||
|
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 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");
|
||||||
|
}
|
||||||
|
formSummary.dataset.value = summary.value;
|
||||||
|
formSummary.innerText = summary.value;
|
||||||
|
bootstrap.Modal.getInstance(helperModal).hide();
|
||||||
|
bootstrap.Modal.getOrCreateInstance(entryModal).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the summary help when it is shown.
|
||||||
|
*
|
||||||
|
* @param isNew {boolean} true for adding a new journal entry, or false otherwise
|
||||||
|
*/
|
||||||
|
initShow(isNew) {
|
||||||
|
const closeButtons = Array.from(document.getElementsByClassName(this.#prefix + "-close"));
|
||||||
|
closeButtons.forEach(function (button) {
|
||||||
|
if (isNew) {
|
||||||
|
button.dataset.bsTarget = "";
|
||||||
|
} else {
|
||||||
|
button.dataset.bsTarget = "#accounting-entry-form-modal";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.#reset();
|
||||||
|
if (!isNew) {
|
||||||
|
this.#populate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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"));
|
||||||
|
inputs.forEach(function (input) {
|
||||||
|
input.value = "";
|
||||||
|
input.classList.remove("is-invalid");
|
||||||
|
});
|
||||||
|
tagButtons.forEach(function (btnTag) {
|
||||||
|
btnTag.classList.remove("btn-primary");
|
||||||
|
btnTag.classList.add("btn-outline-primary");
|
||||||
|
});
|
||||||
|
directionButtons.forEach(function (btnDirection) {
|
||||||
|
if (btnDirection.classList.contains("accounting-default")) {
|
||||||
|
btnDirection.classList.remove("btn-outline-primary");
|
||||||
|
btnDirection.classList.add("btn-primary");
|
||||||
|
} else {
|
||||||
|
btnDirection.classList.add("btn-outline-primary");
|
||||||
|
btnDirection.classList.remove("btn-primary");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.#switchToTab(this.#defaultTabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates the summary helper from the journal entry form.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#populate() {
|
||||||
|
const formSummary = document.getElementById("accounting-entry-form-summary");
|
||||||
|
const summary = document.getElementById(this.#prefix + "-summary");
|
||||||
|
summary.value = formSummary.dataset.value;
|
||||||
|
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 buttons = 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);
|
||||||
|
}
|
||||||
|
buttons.forEach(function (button) {
|
||||||
|
if (button.dataset.value === tagName) {
|
||||||
|
button.classList.remove("btn-outline-primary");
|
||||||
|
button.classList.add("btn-primary");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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 buttons = 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;
|
||||||
|
directionButtons.forEach(function (btnDirection) {
|
||||||
|
if (btnDirection.dataset.arrow === direction) {
|
||||||
|
btnDirection.classList.remove("btn-outline-primary");
|
||||||
|
btnDirection.classList.add("btn-primary");
|
||||||
|
} else {
|
||||||
|
btnDirection.classList.add("btn-outline-primary");
|
||||||
|
btnDirection.classList.remove("btn-primary");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
to.value = toName;
|
||||||
|
if (numberStr !== undefined) {
|
||||||
|
number.value = parseInt(numberStr);
|
||||||
|
}
|
||||||
|
buttons.forEach(function (button) {
|
||||||
|
if (button.dataset.value === tagName) {
|
||||||
|
button.classList.remove("btn-outline-primary");
|
||||||
|
button.classList.add("btn-primary");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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 buttons = Array.from(document.getElementsByClassName(this.#prefix + "-general-btn-tag"));
|
||||||
|
tag.value = tagName;
|
||||||
|
if (numberStr !== undefined) {
|
||||||
|
number.value = parseInt(numberStr);
|
||||||
|
}
|
||||||
|
buttons.forEach(function (button) {
|
||||||
|
if (button.dataset.value === tagName) {
|
||||||
|
button.classList.remove("btn-outline-primary");
|
||||||
|
button.classList.add("btn-primary");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -157,6 +157,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 +166,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;
|
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 = "";
|
||||||
|
SummaryHelper.initializeNewJournalEntry(button.dataset.entryType);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,6 +214,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 () {
|
||||||
@ -224,7 +230,14 @@ function initializeJournalEntry(entry) {
|
|||||||
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;
|
||||||
validateJournalEntryForm();
|
validateJournalEntryForm();
|
||||||
};
|
};
|
||||||
@ -239,7 +252,6 @@ 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 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 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 () {
|
formAccountControl.onclick = function () {
|
||||||
@ -268,7 +280,6 @@ function initializeJournalEntryFormModal() {
|
|||||||
btnClear.disabled = false;
|
btnClear.disabled = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
formSummary.onchange = validateJournalEntrySummary;
|
|
||||||
formAmount.onchange = validateJournalEntryAmount;
|
formAmount.onchange = validateJournalEntryAmount;
|
||||||
entryForm.onsubmit = function () {
|
entryForm.onsubmit = function () {
|
||||||
if (validateJournalEntryForm()) {
|
if (validateJournalEntryForm()) {
|
||||||
@ -320,10 +331,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;
|
||||||
}
|
}
|
||||||
@ -393,8 +403,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") {
|
||||||
|
@ -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-account-modal="#accounting-debit-account-selector-modal" data-bs-toggle="modal" data-bs-target="#accounting-summary-helper-debit-modal">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ 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/summary-helper.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -85,6 +86,9 @@ First written: 2023/2/26
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% include "accounting/transaction/include/entry-form-modal.html" %}
|
{% include "accounting/transaction/include/entry-form-modal.html" %}
|
||||||
|
{% for summary_helper in form.summary_helper.types %}
|
||||||
|
{% include "accounting/transaction/include/summary-helper-modal.html" %}
|
||||||
|
{% endfor %}
|
||||||
{% block account_selector_modals %}{% endblock %}
|
{% block account_selector_modals %}{% endblock %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -0,0 +1,171 @@
|
|||||||
|
{#
|
||||||
|
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">
|
||||||
|
<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 type="button" class="btn-close accounting-summary-helper-{{ summary_helper.type }}-close" 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 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 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 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 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 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" 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>
|
||||||
|
|
||||||
|
<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 }}">
|
||||||
|
{{ 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" 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 }}">
|
||||||
|
{{ 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" data-arrow="→">→</button>
|
||||||
|
<button class="btn btn-outline-primary accounting-summary-helper-{{ summary_helper.type }}-travel-direction" type="button" 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" 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 }}">
|
||||||
|
{{ 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" 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" 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>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary accounting-summary-helper-{{ summary_helper.type }}-close" 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-account-modal="#accounting-credit-account-selector-modal" data-bs-toggle="modal" data-bs-target="#accounting-summary-helper-credit-modal">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -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-account-modal="#accounting-debit-account-selector-modal" data-bs-toggle="modal" data-bs-target="#accounting-summary-helper-debit-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-account-modal="#accounting-credit-account-selector-modal" data-bs-toggle="modal" data-bs-target="#accounting-summary-helper-credit-modal">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -364,6 +364,14 @@ class TransactionForm(FlaskForm):
|
|||||||
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."""
|
||||||
|
227
src/accounting/transaction/summary_helper.py
Normal file
227
src/accounting/transaction/summary_helper.py
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
# 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.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)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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."""
|
||||||
|
self.types: set[SummaryEntryType] = {self.debit, self.credit}
|
||||||
|
"""The tags categorized by the entry types."""
|
||||||
|
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.types}
|
||||||
|
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)
|
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"}]
|
||||||
|
|
@ -39,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 = "6153-001"
|
OFFICE: str = "6153-001"
|
||||||
TRAVEL: str = "6154-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"
|
||||||
|
Loading…
Reference in New Issue
Block a user