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 formAccount = document.getElementById("accounting-entry-form-account");
|
||||
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 formSummaryError = document.getElementById("accounting-entry-form-summary-error");
|
||||
const formAmount = document.getElementById("accounting-entry-form-amount");
|
||||
@ -165,19 +166,23 @@ function initializeNewEntryButton(button) {
|
||||
entryForm.dataset.currencyIndex = button.dataset.currencyIndex;
|
||||
entryForm.dataset.entryType = button.dataset.entryType;
|
||||
entryForm.dataset.entryIndex = button.dataset.entryIndex;
|
||||
formAccountControl.classList.remove("accounting-not-empty")
|
||||
formAccountControl.classList.remove("accounting-not-empty");
|
||||
formAccountControl.classList.remove("is-invalid");
|
||||
formAccountControl.dataset.bsTarget = button.dataset.accountModal;
|
||||
formAccount.innerText = "";
|
||||
formAccount.dataset.code = "";
|
||||
formAccount.dataset.text = "";
|
||||
formAccountError.innerText = "";
|
||||
formSummary.value = "";
|
||||
formSummary.classList.remove("is-invalid");
|
||||
formSummaryControl.dataset.bsTarget = "#accounting-summary-helper-" + button.dataset.entryType + "-modal";
|
||||
formSummaryControl.classList.remove("accounting-not-empty");
|
||||
formSummaryControl.classList.remove("is-invalid");
|
||||
formSummary.dataset.value = "";
|
||||
formSummary.innerText = ""
|
||||
formSummaryError.innerText = ""
|
||||
formAmount.value = "";
|
||||
formAmount.classList.remove("is-invalid");
|
||||
formAmountError.innerText = "";
|
||||
SummaryHelper.initializeNewJournalEntry(button.dataset.entryType);
|
||||
};
|
||||
}
|
||||
|
||||
@ -209,6 +214,7 @@ function initializeJournalEntry(entry) {
|
||||
const control = document.getElementById(entry.dataset.prefix + "-control");
|
||||
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
|
||||
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 formAmount = document.getElementById("accounting-entry-form-amount");
|
||||
control.onclick = function () {
|
||||
@ -224,7 +230,14 @@ function initializeJournalEntry(entry) {
|
||||
formAccount.innerText = accountCode.dataset.text;
|
||||
formAccount.dataset.code = accountCode.value;
|
||||
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;
|
||||
validateJournalEntryForm();
|
||||
};
|
||||
@ -239,7 +252,6 @@ function initializeJournalEntryFormModal() {
|
||||
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 modal = document.getElementById("accounting-entry-form-modal");
|
||||
formAccountControl.onclick = function () {
|
||||
@ -268,7 +280,6 @@ function initializeJournalEntryFormModal() {
|
||||
btnClear.disabled = false;
|
||||
}
|
||||
};
|
||||
formSummary.onchange = validateJournalEntrySummary;
|
||||
formAmount.onchange = validateJournalEntryAmount;
|
||||
entryForm.onsubmit = function () {
|
||||
if (validateJournalEntryForm()) {
|
||||
@ -320,10 +331,9 @@ function validateJournalEntryAccount() {
|
||||
* @private
|
||||
*/
|
||||
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");
|
||||
field.value = field.value.trim();
|
||||
field.classList.remove("is-invalid");
|
||||
control.classList.remove("is-invalid");
|
||||
error.innerText = "";
|
||||
return true;
|
||||
}
|
||||
@ -393,8 +403,8 @@ function saveJournalEntryForm() {
|
||||
accountCode.value = formAccount.dataset.code;
|
||||
accountCode.dataset.text = formAccount.dataset.text;
|
||||
accountText.innerText = formAccount.dataset.text;
|
||||
summary.value = formSummary.value;
|
||||
summaryText.innerText = formSummary.value;
|
||||
summary.value = formSummary.dataset.value;
|
||||
summaryText.innerText = formSummary.dataset.value;
|
||||
amount.value = formAmount.value;
|
||||
amountText.innerText = formatDecimal(new Decimal(formAmount.value));
|
||||
if (entryForm.dataset.entryIndex === "new") {
|
||||
|
@ -70,7 +70,7 @@ First written: 2023/2/25
|
||||
</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>
|
||||
{{ A_("New") }}
|
||||
</button>
|
||||
|
@ -36,9 +36,11 @@ First written: 2023/2/25
|
||||
<div id="accounting-entry-form-account-error" class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input id="accounting-entry-form-summary" class="form-control" type="text" value="" placeholder=" ">
|
||||
<label for="accounting-entry-form-summary">{{ A_("Summary") }}</label>
|
||||
<div class="mb-3">
|
||||
<div id="accounting-entry-form-summary-control" class="form-control accounting-clickable accounting-material-text-field" data-bs-toggle="modal" data-bs-target="">
|
||||
<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>
|
||||
|
||||
|
@ -24,6 +24,7 @@ First written: 2023/2/26
|
||||
{% 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/transaction-form.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/summary-helper.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -85,6 +86,9 @@ First written: 2023/2/26
|
||||
</form>
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% 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>
|
||||
<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>
|
||||
{{ A_("New") }}
|
||||
</button>
|
||||
|
@ -72,7 +72,7 @@ First written: 2023/2/25
|
||||
</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>
|
||||
{{ A_("New") }}
|
||||
</button>
|
||||
@ -112,7 +112,7 @@ First written: 2023/2/25
|
||||
</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>
|
||||
{{ A_("New") }}
|
||||
</button>
|
||||
|
@ -364,6 +364,14 @@ class TransactionForm(FlaskForm):
|
||||
return [x for x in self.currencies.errors
|
||||
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)
|
||||
"""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."""
|
||||
CASH: str = "1111-001"
|
||||
BANK: str = "1113-001"
|
||||
PREPAID: str = "1258-001"
|
||||
PAYABLE: str = "2141-001"
|
||||
SALES: str = "4111-001"
|
||||
SERVICE: str = "4611-001"
|
||||
AGENCY: str = "4711-001"
|
||||
OFFICE: str = "6153-001"
|
||||
TRAVEL: str = "6154-001"
|
||||
MEAL: str = "6172-001"
|
||||
INTEREST: str = "4111-001"
|
||||
DONATION: str = "7481-001"
|
||||
RENT: str = "7482-001"
|
||||
|
Loading…
Reference in New Issue
Block a user