Applied the summary helper and JavaScripts to the transaction form, so that the transaction form works in the accounting application.
This commit is contained in:
parent
2c6256b497
commit
b2ce0eff54
36
accounting/static/accounting/css/summary-helper.css
Normal file
36
accounting/static/accounting/css/summary-helper.css
Normal file
@ -0,0 +1,36 @@
|
||||
/* The Mia Website
|
||||
* summary-helper.css: The style sheet for the summary helper
|
||||
*/
|
||||
|
||||
/* Copyright (c) 2019-2020 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: 2020/4/3
|
||||
*/
|
||||
|
||||
.summary-container {
|
||||
padding: 0 0 1em 0;
|
||||
}
|
||||
.summary-tab-content {
|
||||
padding-top: 1em;
|
||||
}
|
||||
.summary-categories-known {
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.summary-categories-known .btn-summary-helper {
|
||||
margin: 0.1em;
|
||||
}
|
@ -21,7 +21,7 @@
|
||||
* First written: 2019/9/17
|
||||
*/
|
||||
|
||||
.subject-line {
|
||||
.account-line {
|
||||
font-size: 0.833em;
|
||||
}
|
||||
.amount {
|
||||
|
63
accounting/static/accounting/js/regular-payments.js
Normal file
63
accounting/static/accounting/js/regular-payments.js
Normal file
@ -0,0 +1,63 @@
|
||||
/* The Mia Website
|
||||
* regular-payments.js: The JavaScript for the regular payments
|
||||
*/
|
||||
|
||||
/* Copyright (c) 2019-2020 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: 2020/4/4
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the regular payment data.
|
||||
*
|
||||
* @returns {{debits: [], credits: []}}
|
||||
*/
|
||||
function getRegularPayments() {
|
||||
const today = new Date($("#txn-date").get(0).value);
|
||||
const thisMonth = today.getMonth() + 1;
|
||||
const lastMonth = (thisMonth + 10) % 12 + 1;
|
||||
let regular = {
|
||||
debit: [],
|
||||
credit: [],
|
||||
};
|
||||
regular.debit.push({
|
||||
title: "共同生活基金",
|
||||
summary: "共同生活基金" + thisMonth + "月",
|
||||
account: "62651",
|
||||
});
|
||||
regular.debit.push({
|
||||
title: "電話費",
|
||||
summary: "電話費" + lastMonth + "月",
|
||||
account: "62562",
|
||||
});
|
||||
regular.debit.push({
|
||||
title: "健保",
|
||||
summary: "健保" + lastMonth + "月",
|
||||
account: "62621",
|
||||
});
|
||||
regular.debit.push({
|
||||
title: "國民年金",
|
||||
summary: "國民年金" + lastMonth + "月",
|
||||
account: "13141",
|
||||
});
|
||||
regular.credit.push({
|
||||
title: "薪水",
|
||||
summary: lastMonth + "月份薪水",
|
||||
account: "46116",
|
||||
});
|
||||
return regular;
|
||||
}
|
542
accounting/static/accounting/js/summary-helper.js
Normal file
542
accounting/static/accounting/js/summary-helper.js
Normal file
@ -0,0 +1,542 @@
|
||||
/* The Mia Website
|
||||
* summary-helper.js: The JavaScript for the summary helper
|
||||
*/
|
||||
|
||||
/* Copyright (c) 2019-2020 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: 2020/4/3
|
||||
*/
|
||||
|
||||
// Initializes the summary helper JavaScript.
|
||||
$(function () {
|
||||
loadSummaryCategoryData();
|
||||
$("#summary-helper-form")
|
||||
.on("submit", function () {
|
||||
return false;
|
||||
});
|
||||
$(".record-summary")
|
||||
.attr("data-toggle", "modal")
|
||||
.attr("data-target", "#summary-modal")
|
||||
.on("click", function () {
|
||||
startSummaryHelper(this);
|
||||
});
|
||||
$("#summary-summary")
|
||||
.on("change", function () {
|
||||
this.value = this.value.trim();
|
||||
parseSummaryForHelper(this.value);
|
||||
});
|
||||
$(".summary-tab")
|
||||
.on("click", function () {
|
||||
switchSummaryTab(this);
|
||||
});
|
||||
// The general categories
|
||||
$("#summary-general-category")
|
||||
.on("change", function () {
|
||||
setSummaryGeneralCategoryButtons(this.value);
|
||||
setGeneralCategorySummary();
|
||||
setSummaryAccount("general", this.value);
|
||||
});
|
||||
// The travel routes
|
||||
$("#summary-travel-category")
|
||||
.on("change", function () {
|
||||
setSummaryTravelCategoryButtons(this.value);
|
||||
setSummaryAccount("travel", this.value);
|
||||
});
|
||||
$(".summary-travel-part")
|
||||
.on("change", function () {
|
||||
this.value = this.value.trim();
|
||||
setTravelSummary();
|
||||
});
|
||||
$(".btn-summary-travel-direction")
|
||||
.on("click", function () {
|
||||
$("#summary-travel-direction").get(0).value = this.innerText;
|
||||
setSummaryTravelDirectionButtons(this.innerText);
|
||||
setTravelSummary();
|
||||
});
|
||||
// The bus routes
|
||||
$("#summary-bus-category")
|
||||
.on("change", function () {
|
||||
setSummaryBusCategoryButtons(this.value);
|
||||
setSummaryAccount("bus", this.value);
|
||||
});
|
||||
$(".summary-bus-part")
|
||||
.on("change", function () {
|
||||
this.value = this.value.trim();
|
||||
setBusSummary();
|
||||
});
|
||||
$("#summary-count")
|
||||
.on("change", function () {
|
||||
updateSummaryCount();
|
||||
});
|
||||
$("#summary-confirm")
|
||||
.on("click", function () {
|
||||
applySummaryToAccountingRecord();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The known categories
|
||||
* @type {object}
|
||||
* @private
|
||||
*/
|
||||
let summaryCategories = null;
|
||||
|
||||
/**
|
||||
* The known categories and their corresponding accounts
|
||||
* @type {object}
|
||||
* @private
|
||||
*/
|
||||
let summaryAccounts = null;
|
||||
|
||||
/**
|
||||
* The account that corresponds to this category
|
||||
* @type {null|string}
|
||||
* @private
|
||||
*/
|
||||
let summaryAccount = null;
|
||||
|
||||
/**
|
||||
* Loads the summary category data.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function loadSummaryCategoryData() {
|
||||
const data = JSON.parse($("#summary-categories").val());
|
||||
summaryCategories = {};
|
||||
summaryAccounts = {};
|
||||
["debit", "credit"].forEach(function (type) {
|
||||
summaryCategories[type] = {};
|
||||
summaryAccounts[type] = {};
|
||||
["general", "travel", "bus"].forEach(function (format) {
|
||||
summaryCategories[type][format] = [];
|
||||
summaryAccounts[type][format] = {};
|
||||
if (type + "-" + format in data) {
|
||||
data[type + "-" + format]
|
||||
.forEach(function (item) {
|
||||
summaryCategories[type][format].push(item[0]);
|
||||
summaryAccounts[type][format][item[0]] = item[1];
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the summary helper.
|
||||
*
|
||||
* @param {HTMLInputElement} summary the summary input element
|
||||
*/
|
||||
function startSummaryHelper(summary) {
|
||||
const type = summary.id.substring(0, summary.id.indexOf("-"));
|
||||
const no = parseInt(summary.id.substring(type.length + 1, summary.id.indexOf("-", type.length + 1)));
|
||||
$("#summary-record").get(0).value = type + "-" + no;
|
||||
$("#summary-summary").get(0).value = summary.value;
|
||||
// Loads the know summary categories into the summary helper
|
||||
loadKnownSummaryCategories(type);
|
||||
// Parses the summary and sets up the summary helper
|
||||
parseSummaryForHelper(summary.value);
|
||||
// Focus on the summary input
|
||||
setTimeout(function () {
|
||||
$("#summary-summary").get(0).focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the known summary categories into the summary helper.
|
||||
*
|
||||
* @param {string} type the record type, either "debit" or "credit"
|
||||
* @private
|
||||
*/
|
||||
function loadKnownSummaryCategories(type) {
|
||||
["general", "travel", "bus"].forEach(function (format) {
|
||||
const knownCategories = $("#summary-" + format + "-categories-known");
|
||||
knownCategories.html("");
|
||||
summaryCategories[type][format].forEach(function (item) {
|
||||
knownCategories.append(
|
||||
$("<span/>")
|
||||
.addClass("btn btn-outline-primary")
|
||||
.addClass("btn-summary-helper")
|
||||
.addClass("btn-summary-" + format + "-category")
|
||||
.text(item));
|
||||
});
|
||||
});
|
||||
|
||||
// The regular payments
|
||||
const regularPayments = getRegularPayments();
|
||||
["debit", "credit"].forEach(function (type) {
|
||||
summaryCategories[type].regular = [];
|
||||
summaryAccounts[type].regular = {};
|
||||
regularPayments[type].forEach(function (item) {
|
||||
summaryCategories[type].regular.push(item);
|
||||
summaryAccounts[type].regular[item.title] = item.account;
|
||||
});
|
||||
});
|
||||
const regularPaymentButtons = $("#summary-regular-payments");
|
||||
regularPaymentButtons.html("");
|
||||
summaryCategories[type].regular.forEach(function (item) {
|
||||
regularPaymentButtons.append(
|
||||
$("<span/>")
|
||||
.attr("title", item.summary)
|
||||
.addClass("btn btn-outline-primary")
|
||||
.addClass("btn-summary-helper")
|
||||
.addClass("btn-summary-regular")
|
||||
.text(item.title));
|
||||
});
|
||||
|
||||
$(".btn-summary-general-category")
|
||||
.on("click", function () {
|
||||
$("#summary-general-category").get(0).value = this.innerText;
|
||||
setSummaryGeneralCategoryButtons(this.innerText);
|
||||
setGeneralCategorySummary();
|
||||
setSummaryAccount("general", this.innerText);
|
||||
});
|
||||
$(".btn-summary-travel-category")
|
||||
.on("click", function () {
|
||||
$("#summary-travel-category").get(0).value = this.innerText;
|
||||
setSummaryTravelCategoryButtons(this.innerText);
|
||||
setTravelSummary();
|
||||
setSummaryAccount("travel", this.innerText);
|
||||
});
|
||||
$(".btn-summary-bus-category")
|
||||
.on("click", function () {
|
||||
$("#summary-bus-category").get(0).value = this.innerText;
|
||||
setSummaryBusCategoryButtons(this.innerText);
|
||||
setBusSummary();
|
||||
setSummaryAccount("bus", this.innerText);
|
||||
});
|
||||
$(".btn-summary-regular")
|
||||
.on("click", function () {
|
||||
$("#summary-summary").get(0).value = this.title;
|
||||
setSummaryRegularPaymentButtons(this.innerText);
|
||||
setSummaryAccount("regular", this.innerText);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the summary and sets up the summary helper.
|
||||
*
|
||||
* @param {string} summary the summary
|
||||
*/
|
||||
function parseSummaryForHelper(summary) {
|
||||
// Parses the summary and sets up the category helpers.
|
||||
parseSummaryForCategoryHelpers(summary);
|
||||
// The number of items
|
||||
const pos = summary.lastIndexOf("×");
|
||||
let count = 1;
|
||||
if (pos !== -1) {
|
||||
count = parseInt(summary.substr(pos + 1));
|
||||
}
|
||||
if (count === 0) {
|
||||
count = 1;
|
||||
}
|
||||
$("#summary-count").get(0).value = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the summary and sets up the category helpers.
|
||||
*
|
||||
* @param {string} summary the summary
|
||||
*/
|
||||
function parseSummaryForCategoryHelpers(summary) {
|
||||
$(".btn-summary-helper")
|
||||
.removeClass("btn-primary")
|
||||
.addClass("btn-outline-primary");
|
||||
$("#btn-summary-one-way")
|
||||
.removeClass("btn-outline-primary")
|
||||
.addClass("btn-primary");
|
||||
$(".summary-helper-input").each(function () {
|
||||
this.classList.remove("is-invalid");
|
||||
if (this.id === "summary-travel-direction") {
|
||||
this.value = $("#btn-summary-one-way").text();
|
||||
} else {
|
||||
this.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
// A bus route
|
||||
const matchBus = summary.match(/^(.+)—(.+)—(.+)→(.+?)(?:×[0-9]+)?$/);
|
||||
if (matchBus !== null) {
|
||||
$("#summary-bus-category").get(0).value = matchBus[1];
|
||||
setSummaryBusCategoryButtons(matchBus[1]);
|
||||
setSummaryAccount("bus", matchBus[1]);
|
||||
$("#summary-bus-route").get(0).value = matchBus[2];
|
||||
$("#summary-bus-from").get(0).value = matchBus[3];
|
||||
$("#summary-bus-to").get(0).value = matchBus[4];
|
||||
switchSummaryTab($("#summary-tab-bus").get(0));
|
||||
return;
|
||||
}
|
||||
|
||||
// A general travel route
|
||||
const matchTravel = summary.match(/^(.+)—(.+)([→|↔])(.+?)(?:×[0-9]+)?$/);
|
||||
if (matchTravel !== null) {
|
||||
$("#summary-travel-category").get(0).value = matchTravel[1];
|
||||
setSummaryTravelCategoryButtons(matchTravel[1]);
|
||||
setSummaryAccount("travel", matchTravel[1]);
|
||||
$("#summary-travel-from").get(0).value = matchTravel[2];
|
||||
$("#summary-travel-direction").get(0).value = matchTravel[3];
|
||||
setSummaryTravelDirectionButtons(matchTravel[3]);
|
||||
$("#summary-travel-to").get(0).value = matchTravel[4];
|
||||
switchSummaryTab($("#summary-tab-travel").get(0));
|
||||
return;
|
||||
}
|
||||
|
||||
// A general category
|
||||
const generalCategoryTab = $("#summary-tab-category").get(0);
|
||||
const matchCategory = summary.match(/^(.+)—.+(?:×[0-9]+)?$/);
|
||||
if (matchCategory !== null) {
|
||||
$("#summary-general-category").get(0).value = matchCategory[1];
|
||||
setSummaryGeneralCategoryButtons(matchCategory[1]);
|
||||
setSummaryAccount("general", matchCategory[1]);
|
||||
switchSummaryTab(generalCategoryTab);
|
||||
return;
|
||||
}
|
||||
|
||||
// A general summary text
|
||||
setSummaryGeneralCategoryButtons(null);
|
||||
setSummaryAccount("general", null);
|
||||
switchSummaryTab(generalCategoryTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the summary helper to tab.
|
||||
*
|
||||
* @param {HTMLElement} tab the navigation tab corresponding to a type
|
||||
* of helper
|
||||
* @private
|
||||
*/
|
||||
function switchSummaryTab(tab) {
|
||||
const tabName = tab.id.substr("summary-tab-".length); // "summary-tab-"
|
||||
$(".summary-tab-content").each(function () {
|
||||
if (this.id === "summary-tab-content-" + tabName) {
|
||||
this.classList.remove("d-none");
|
||||
} else {
|
||||
this.classList.add("d-none");
|
||||
}
|
||||
});
|
||||
$(".summary-tab").each(function () {
|
||||
if (this.id === tab.id) {
|
||||
this.classList.add("active");
|
||||
} else {
|
||||
this.classList.remove("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the known general category buttons.
|
||||
*
|
||||
* @param {string} category the general category
|
||||
*/
|
||||
function setSummaryGeneralCategoryButtons(category) {
|
||||
$(".btn-summary-general-category").each(function () {
|
||||
if (this.innerText === category) {
|
||||
this.classList.remove("btn-outline-primary");
|
||||
this.classList.add("btn-primary");
|
||||
} else {
|
||||
this.classList.add("btn-outline-primary");
|
||||
this.classList.remove("btn-primary");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the summary of a general category.
|
||||
*
|
||||
*/
|
||||
function setGeneralCategorySummary() {
|
||||
const summary = $("#summary-summary").get(0);
|
||||
const dashPos = summary.value.indexOf("—");
|
||||
if (dashPos !== -1) {
|
||||
summary.value = summary.value.substring(dashPos + 1);
|
||||
}
|
||||
const category = $("#summary-general-category").get(0).value;
|
||||
if (category !== "") {
|
||||
summary.value = category + "—" + summary.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the known travel category buttons.
|
||||
*
|
||||
* @param {string} category the travel category
|
||||
*/
|
||||
function setSummaryTravelCategoryButtons(category) {
|
||||
$(".btn-summary-travel-category").each(function () {
|
||||
if (this.innerText === category) {
|
||||
this.classList.remove("btn-outline-primary");
|
||||
this.classList.add("btn-primary");
|
||||
} else {
|
||||
this.classList.add("btn-outline-primary");
|
||||
this.classList.remove("btn-primary");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the summary of a general travel.
|
||||
*
|
||||
*/
|
||||
function setTravelSummary() {
|
||||
$(".summary-travel-part").each(function () {
|
||||
if (this.value === "") {
|
||||
this.classList.add("is-invalid");
|
||||
} else {
|
||||
this.classList.remove("is-invalid");
|
||||
}
|
||||
});
|
||||
let summary = $("#summary-travel-category").get(0).value
|
||||
+ "—" + $("#summary-travel-from").get(0).value
|
||||
+ $("#summary-travel-direction").get(0).value
|
||||
+ $("#summary-travel-to").get(0).value;
|
||||
const count = parseInt($("#summary-count").get(0).value);
|
||||
if (count !== 1) {
|
||||
summary = summary + "×" + count;
|
||||
}
|
||||
$("#summary-summary").get(0).value = summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the known summary travel direction buttons.
|
||||
*
|
||||
* @param {string} direction the known summary travel direction
|
||||
*/
|
||||
function setSummaryTravelDirectionButtons(direction) {
|
||||
$(".btn-summary-travel-direction").each(function () {
|
||||
if (this.innerText === direction) {
|
||||
this.classList.remove("btn-outline-primary");
|
||||
this.classList.add("btn-primary");
|
||||
} else {
|
||||
this.classList.add("btn-outline-primary");
|
||||
this.classList.remove("btn-primary");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the known bus category buttons.
|
||||
*
|
||||
* @param {string} category the bus category
|
||||
*/
|
||||
function setSummaryBusCategoryButtons(category) {
|
||||
$(".btn-summary-bus-category").each(function () {
|
||||
if (this.innerText === category) {
|
||||
this.classList.remove("btn-outline-primary");
|
||||
this.classList.add("btn-primary");
|
||||
} else {
|
||||
this.classList.add("btn-outline-primary");
|
||||
this.classList.remove("btn-primary");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the summary of a bus travel.
|
||||
*
|
||||
*/
|
||||
function setBusSummary() {
|
||||
$(".summary-bus-part").each(function () {
|
||||
if (this.value === "") {
|
||||
this.classList.add("is-invalid");
|
||||
} else {
|
||||
this.classList.remove("is-invalid");
|
||||
}
|
||||
});
|
||||
let summary = $("#summary-bus-category").get(0).value
|
||||
+ "—" + $("#summary-bus-route").get(0).value
|
||||
+ "—" + $("#summary-bus-from").get(0).value
|
||||
+ "→" + $("#summary-bus-to").get(0).value;
|
||||
const count = parseInt($("#summary-count").get(0).value);
|
||||
if (count !== 1) {
|
||||
summary = summary + "×" + count;
|
||||
}
|
||||
$("#summary-summary").get(0).value = summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the regular payment buttons.
|
||||
*
|
||||
* @param {string} category the regular payment
|
||||
*/
|
||||
function setSummaryRegularPaymentButtons(category) {
|
||||
$(".btn-summary-regular").each(function () {
|
||||
if (this.innerText === category) {
|
||||
this.classList.remove("btn-outline-primary");
|
||||
this.classList.add("btn-primary");
|
||||
} else {
|
||||
this.classList.add("btn-outline-primary");
|
||||
this.classList.remove("btn-primary");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the account for this summary category.
|
||||
*
|
||||
* @param {string} format the category format, either "general",
|
||||
* "travel", or "bus".
|
||||
* @param {string} category the category
|
||||
*/
|
||||
function setSummaryAccount(format, category) {
|
||||
const recordId = $("#summary-record").get(0).value;
|
||||
const type = recordId.substring(0, recordId.indexOf("-"));
|
||||
if (category in summaryAccounts[type][format]) {
|
||||
summaryAccount = summaryAccounts[type][format][category];
|
||||
} else {
|
||||
summaryAccount = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the count.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function updateSummaryCount() {
|
||||
const count = parseInt($("#summary-count").val());
|
||||
const summary = $("#summary-summary").get(0);
|
||||
const pos = summary.value.lastIndexOf("×");
|
||||
if (pos === -1) {
|
||||
if (count !== 1) {
|
||||
summary.value = summary.value + "×" + count;
|
||||
}
|
||||
} else {
|
||||
const content = summary.value.substring(0, pos);
|
||||
if (count === 1) {
|
||||
summary.value = content;
|
||||
} else {
|
||||
summary.value = content + "×" + count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the summary to the accounting record.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function applySummaryToAccountingRecord() {
|
||||
const recordId = $("#summary-record").get(0).value;
|
||||
const summary = $("#" + recordId + "-summary").get(0);
|
||||
summary.value = $("#summary-summary").get(0).value.trim();
|
||||
const account = $("#" + recordId + "-account").get(0);
|
||||
if (summaryAccount !== null && account.value === "") {
|
||||
account.value = summaryAccount;
|
||||
}
|
||||
setTimeout(function () {
|
||||
summary.blur();
|
||||
}, 100);
|
||||
}
|
703
accounting/static/accounting/js/transaction-form.js
Normal file
703
accounting/static/accounting/js/transaction-form.js
Normal file
@ -0,0 +1,703 @@
|
||||
/* The Mia Website
|
||||
* transaction-form.js: The JavaScript for the transaction form
|
||||
*/
|
||||
|
||||
/* Copyright (c) 2019-2020 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: 2019/9/19
|
||||
*/
|
||||
|
||||
// Initializes the page JavaScript.
|
||||
$(function () {
|
||||
getAccountOptions();
|
||||
resetRecordButtons();
|
||||
$("#txn-date")
|
||||
.on("blur", function () {
|
||||
validateDate();
|
||||
});
|
||||
$(".record-account")
|
||||
.on("focus", function () {
|
||||
removeBlankOption(this);
|
||||
})
|
||||
.on("blur", function () {
|
||||
validateAccount(this);
|
||||
});
|
||||
$(".record-summary")
|
||||
.on("blur", function () {
|
||||
validateSummary(this);
|
||||
});
|
||||
$(".record-amount")
|
||||
.on("blur", function () {
|
||||
validateAmount(this);
|
||||
})
|
||||
.on("change", function () {
|
||||
updateTotalAmount(this);
|
||||
validateBalance();
|
||||
});
|
||||
$("#txn-note")
|
||||
.on("blur", function () {
|
||||
validateNote();
|
||||
});
|
||||
$("#txn-form")
|
||||
.on("submit", function () {
|
||||
return validateForm();
|
||||
});
|
||||
$(".btn-new")
|
||||
.on("click", function () {
|
||||
addNewRecord(this);
|
||||
});
|
||||
$(".btn-del-record")
|
||||
.on("click", function () {
|
||||
deleteRecord(this);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The localized messages
|
||||
* @type {Array.}
|
||||
* @private
|
||||
*/
|
||||
let l10n = null;
|
||||
|
||||
/**
|
||||
* Returns the localization of a message.
|
||||
*
|
||||
* @param {string} key the message key
|
||||
* @returns {string} the localized message
|
||||
* @private
|
||||
*/
|
||||
function __(key) {
|
||||
if (l10n === null) {
|
||||
l10n = JSON.parse($("#l10n-messages").val());
|
||||
}
|
||||
if (key in l10n) {
|
||||
return l10n[key];
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this is a transfer transaction.
|
||||
*
|
||||
* @returns {boolean} true if this is a transfer transaction, or false
|
||||
* otherwise
|
||||
* @private
|
||||
*/
|
||||
function isTransfer() {
|
||||
return $("#debit-records").length > 0 && $("#credit-records").length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The account options
|
||||
* @type {Array.}
|
||||
* @private
|
||||
*/
|
||||
let accountOptions;
|
||||
|
||||
/**
|
||||
* Obtains the account options.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function getAccountOptions() {
|
||||
const request = new XMLHttpRequest();
|
||||
request.onreadystatechange = function() {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
accountOptions = JSON.parse(this.responseText);
|
||||
$(".record-account").each(function () {
|
||||
initializeAccountOptions(this);
|
||||
});
|
||||
}
|
||||
};
|
||||
request.open("GET", $("#account-option-url").val(), true);
|
||||
request.send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the account options.
|
||||
*
|
||||
* @param {HTMLSelectElement} account the account select element
|
||||
* @private
|
||||
*/
|
||||
function initializeAccountOptions(account) {
|
||||
const jAccount = $(account);
|
||||
const type = account.id.substring(0, account.id.indexOf("-"));
|
||||
const selectedAccount = account.value;
|
||||
let isCash = false;
|
||||
if (type === "debit") {
|
||||
isCash = ($(".credit-record").length === 0);
|
||||
} else if (type === "credit") {
|
||||
isCash = ($(".debit-record").length === 0);
|
||||
}
|
||||
jAccount.html("");
|
||||
if (selectedAccount === "") {
|
||||
jAccount.append($("<option/>"));
|
||||
}
|
||||
const headerInUse = $("<option/>")
|
||||
.attr("disabled", "disabled")
|
||||
.text(accountOptions["header_in_use"]);
|
||||
jAccount.append(headerInUse);
|
||||
accountOptions[type + "_in_use"].forEach(function (item) {
|
||||
// Skips the cash account on cash transactions.
|
||||
if (item["code"] === 1111 && isCash) {
|
||||
return;
|
||||
}
|
||||
const option = $("<option/>")
|
||||
.attr("value", item["code"])
|
||||
.text(item["code"] + " " + item["title"]);
|
||||
if (String(item["code"]) === selectedAccount) {
|
||||
option.attr("selected", "selected");
|
||||
}
|
||||
jAccount.append(option);
|
||||
});
|
||||
const headerNotInUse = $("<option/>")
|
||||
.attr("disabled", "disabled")
|
||||
.text(accountOptions["header_not_in_use"]);
|
||||
jAccount.append(headerNotInUse);
|
||||
accountOptions[type + "_not_in_use"].forEach(function (item) {
|
||||
const option = $("<option/>")
|
||||
.attr("value", item["code"])
|
||||
.text(item["code"] + " " + item["title"]);
|
||||
if (String(item["code"]) === selectedAccount) {
|
||||
option.attr("selected", "selected");
|
||||
}
|
||||
jAccount.append(option);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the dummy blank option.
|
||||
*
|
||||
* @param {HTMLSelectElement} select the select element
|
||||
* @private
|
||||
*/
|
||||
function removeBlankOption(select) {
|
||||
$(select).children().each(function () {
|
||||
if (this.value === "" && !this.disabled) {
|
||||
$(this).remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the total amount.
|
||||
*
|
||||
* @param {HTMLButtonElement|HTMLInputElement} element the amount
|
||||
* element that
|
||||
* changed, or the
|
||||
* button that
|
||||
* was hit to
|
||||
* delete a record
|
||||
* @private
|
||||
*/
|
||||
function updateTotalAmount(element) {
|
||||
const type = element.id.substring(0, element.id.indexOf("-"));
|
||||
let total = 0;
|
||||
$("." + type + "-to-sum").each(function () {
|
||||
if (this.value !== "") {
|
||||
total += parseInt(this.value);
|
||||
}
|
||||
});
|
||||
total = String(total);
|
||||
while (total.match(/^[1-9][0-9]*[0-9]{3}/)) {
|
||||
total = total.replace(/^([1-9][0-9]*)([0-9]{3})/, "$1,$2");
|
||||
}
|
||||
$("#" + type + "-total").text(total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new accounting record.
|
||||
*
|
||||
* @param {HTMLButtonElement} button the button element that was hit
|
||||
* to add a new record
|
||||
* @private
|
||||
*/
|
||||
function addNewRecord(button) {
|
||||
const type = button.id.substring(0, button.id.indexOf("-"));
|
||||
// Finds the new number that is the maximum number plus 1.
|
||||
let newNo = 0;
|
||||
$("." + type + "-record").each(function () {
|
||||
const no = parseInt(this.id.substring(type.length + 1));
|
||||
if (newNo < no) {
|
||||
newNo = no;
|
||||
}
|
||||
});
|
||||
newNo++;
|
||||
|
||||
// Inserts a new table row for the new accounting record.
|
||||
insertNewRecord(type, newNo);
|
||||
// Resets the order of the records.
|
||||
resetRecordOrders(type);
|
||||
// Resets the sort and delete buttons for the records.
|
||||
resetRecordButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new accounting record.
|
||||
*
|
||||
* @param {string} type the record type, either "debit" or "credit"
|
||||
* @param {number} newNo the number of this new accounting record
|
||||
* @private
|
||||
*/
|
||||
function insertNewRecord(type, newNo) {
|
||||
if (isTransfer()) {
|
||||
insertNewTransferRecord(type, newNo);
|
||||
} else {
|
||||
insertNewNonTransferRecord(type, newNo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new accounting record for a transfer transaction.
|
||||
*
|
||||
* @param {string} type the record type, either "debit" or "credit"
|
||||
* @param {number} newNo the number of this new accounting record
|
||||
* @private
|
||||
*/
|
||||
function insertNewTransferRecord(type, newNo) {
|
||||
const divAccount = createAccountBlock(type, newNo)
|
||||
.addClass("col-sm-12");
|
||||
const divAccountRow = $("<div/>")
|
||||
.addClass("row")
|
||||
.append(divAccount);
|
||||
|
||||
const divSummary = createSummaryBlock(type, newNo)
|
||||
.addClass("col-lg-8");
|
||||
const divAmount = createAmountBlock(type, newNo)
|
||||
.addClass("col-lg-4");
|
||||
const divSummaryAmountRow = $("<div/>")
|
||||
.addClass("row")
|
||||
.append(divSummary, divAmount);
|
||||
|
||||
const divContent = $("<div/>")
|
||||
.append(divAccountRow)
|
||||
.append(divSummaryAmountRow);
|
||||
|
||||
const divBtnGroup = createActionButtonBlock(
|
||||
type, newNo, type + "-" + newNo + "-delete")
|
||||
.addClass("btn-group-vertical");
|
||||
const divActions = $("<div/>")
|
||||
.append(divBtnGroup);
|
||||
|
||||
$("<li/>")
|
||||
.attr("id", type + "-" + newNo)
|
||||
.addClass("list-group-item")
|
||||
.addClass("d-flex")
|
||||
.addClass(type + "-record")
|
||||
.append(divContent, divActions)
|
||||
.appendTo("#" + type + "-records");
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new accounting record for a non-transfer transaction.
|
||||
*
|
||||
* @param {string} type the record type, either "debit" or "credit"
|
||||
* @param {number} newNo the number of this new accounting record
|
||||
* @private
|
||||
*/
|
||||
function insertNewNonTransferRecord(type, newNo) {
|
||||
const divAccount = createAccountBlock(type, newNo)
|
||||
.addClass("col-lg-6");
|
||||
|
||||
const divSummary = createSummaryBlock(type, newNo)
|
||||
.addClass("col-sm-8");
|
||||
const divAmount = createAmountBlock(type, newNo)
|
||||
.addClass("col-sm-4");
|
||||
const divSummaryAmountRow = $("<div/>")
|
||||
.addClass("row")
|
||||
.append(divSummary, divAmount);
|
||||
const divSummaryAmount = $("<div/>")
|
||||
.addClass("col-lg-6")
|
||||
.append(divSummaryAmountRow);
|
||||
|
||||
const divContent = $("<div/>")
|
||||
.addClass("row")
|
||||
.append(divAccount, divSummaryAmount);
|
||||
|
||||
const divBtnGroup = createActionButtonBlock(
|
||||
type, newNo, type + "-" + newNo + "-delete")
|
||||
.addClass("btn-group")
|
||||
.addClass("d-none d-lg-flex");
|
||||
const divBtnGroupVertical = createActionButtonBlock(
|
||||
type, newNo, type + "-" + newNo + "-m-delete")
|
||||
.addClass("btn-group-vertical")
|
||||
.addClass("d-lg-none");
|
||||
const divActions = $("<div/>")
|
||||
.append(divBtnGroup, divBtnGroupVertical);
|
||||
|
||||
$("<li/>")
|
||||
.attr("id", type + "-" + newNo)
|
||||
.addClass("list-group-item")
|
||||
.addClass("d-flex")
|
||||
.addClass("justify-content-between")
|
||||
.addClass(type + "-record")
|
||||
.append(divContent, divActions)
|
||||
.appendTo("#" + type + "-records");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a new <div></div> account block.
|
||||
*
|
||||
* @param {string} type the record type, either "debit" or "credit"
|
||||
* @param {number} newNo the number of this new accounting record
|
||||
* @returns {JQuery<HTMLElement>} the new <div></div> account block
|
||||
* @private
|
||||
*/
|
||||
function createAccountBlock(type, newNo) {
|
||||
const order = $("<input/>")
|
||||
.attr("id", type + "-" + newNo + "-ord")
|
||||
.attr("type", "hidden")
|
||||
.attr("name", type + "-" + newNo + "-ord")
|
||||
.addClass(type + "-ord");
|
||||
const account = $("<select/>")
|
||||
.attr("id", type + "-" + newNo + "-account")
|
||||
.attr("name", type + "-" + newNo + "-account")
|
||||
.addClass("form-control")
|
||||
.addClass("record-account")
|
||||
.addClass(type + "-account")
|
||||
.on("focus", function () {
|
||||
removeBlankOption(this);
|
||||
})
|
||||
.on("blur", function () {
|
||||
validateAccount(this);
|
||||
})
|
||||
.each(function () {
|
||||
initializeAccountOptions(this);
|
||||
});
|
||||
const accountError = $("<div/>")
|
||||
.attr("id", type + "-" + newNo + "-account-error")
|
||||
.addClass("invalid-feedback");
|
||||
return $("<div/>")
|
||||
.append(order, account, accountError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a new <div></div> summary block.
|
||||
*
|
||||
* @param {string} type the record type, either "debit" or "credit"
|
||||
* @param {number} newNo the number of this new accounting record
|
||||
* @returns {JQuery<HTMLElement>} the new <div></div> summary block
|
||||
* @private
|
||||
*/
|
||||
function createSummaryBlock(type, newNo) {
|
||||
const summary = $("<input/>")
|
||||
.attr("id", type + "-" + newNo + "-summary")
|
||||
.attr("type", "text")
|
||||
.attr("name", type + "-" + newNo + "-summary")
|
||||
.addClass("form-control")
|
||||
.addClass("record-summary")
|
||||
.on("blur", function () {
|
||||
validateSummary(this);
|
||||
});
|
||||
if (typeof startSummaryHelper === "function") {
|
||||
summary
|
||||
.attr("data-toggle", "modal")
|
||||
.attr("data-target", "#summary-modal")
|
||||
.on("click", function () {
|
||||
startSummaryHelper(this);
|
||||
});
|
||||
}
|
||||
const summaryError = $("<div/>")
|
||||
.attr("id", type + "-" + newNo + "-summary-error")
|
||||
.addClass("invalid-feedback");
|
||||
return $("<div/>")
|
||||
.append(summary, summaryError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a new <div></div> amount block.
|
||||
*
|
||||
* @param {string} type the record type, either "debit" or "credit"
|
||||
* @param {number} newNo the number of this new accounting record
|
||||
* @returns {JQuery<HTMLElement>} the new <div></div> amount block
|
||||
* @private
|
||||
*/
|
||||
function createAmountBlock(type, newNo) {
|
||||
const amount = $("<input/>")
|
||||
.attr("id", type + "-" + newNo + "-amount")
|
||||
.attr("type", "number")
|
||||
.attr("name", type + "-" + newNo + "-amount")
|
||||
.attr("min", 1)
|
||||
.attr("required", "required")
|
||||
.addClass("form-control")
|
||||
.addClass("record-amount")
|
||||
.addClass(type + "-to-sum")
|
||||
.on("blur", function () {
|
||||
validateAmount(this);
|
||||
})
|
||||
.on("change", function () {
|
||||
updateTotalAmount(this);
|
||||
validateBalance();
|
||||
});
|
||||
const amountError = $("<div/>")
|
||||
.attr("id", type + "-" + newNo + "-amount-error")
|
||||
.addClass("invalid-feedback");
|
||||
return $("<div/>")
|
||||
.append(amount, amountError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a new <div></div> action button block.
|
||||
*
|
||||
* @param {string} type the record type, either "debit" or "credit"
|
||||
* @param {number} newNo the number of this new accounting record
|
||||
* @param {string} btnDelId the ID of the delete button
|
||||
* @returns {JQuery<HTMLElement>} the new <div></div> button block
|
||||
* @private
|
||||
*/
|
||||
function createActionButtonBlock(type, newNo, btnDelId) {
|
||||
const btnSort = $("<button/>")
|
||||
.attr("type", "button")
|
||||
.addClass("btn btn-outline-secondary")
|
||||
.addClass("btn-sort-" + type)
|
||||
.append($("<i/>").addClass("fas fa-sort"));
|
||||
const btnDelete = $("<button/>")
|
||||
.attr("id", btnDelId)
|
||||
.attr("type", "button")
|
||||
.addClass("btn btn-danger")
|
||||
.addClass("btn-del-record")
|
||||
.addClass("btn-del-" + type)
|
||||
.on("click", function () {
|
||||
deleteRecord(this);
|
||||
})
|
||||
.append($("<i/>").addClass("fas fa-trash"));
|
||||
return $("<div/>")
|
||||
.addClass("btn-actions-" + type)
|
||||
.append(btnSort, btnDelete);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a record.
|
||||
*
|
||||
* @param {HTMLButtonElement} button the button element that was hit
|
||||
* to delete this record
|
||||
* @private
|
||||
*/
|
||||
function deleteRecord(button) {
|
||||
const type = button.id.substring(0, button.id.indexOf("-"));
|
||||
const no = parseInt(button.id.substring(type.length + 1, button.id.indexOf("-", type.length + 1)));
|
||||
$("#" + type + "-" + no).remove();
|
||||
resetRecordOrders(type);
|
||||
resetRecordButtons();
|
||||
updateTotalAmount(button);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the order of the records according to their appearance.
|
||||
*
|
||||
* @param {string} type the record type, either "debit" or "credit".
|
||||
* @private
|
||||
*/
|
||||
function resetRecordOrders(type) {
|
||||
const sorted = $("#" + type + "-records").sortable("toArray");
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
$("#" + sorted[i] + "-ord")[0].value = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the sort and delete buttons for the records.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function resetRecordButtons() {
|
||||
["debit", "credit"].forEach(function (type) {
|
||||
const records = $("." + type + "-record");
|
||||
if (records.length > 1) {
|
||||
$("#" + type + "-records").sortable({
|
||||
classes: {
|
||||
"ui-sortable-helper": "list-group-item-secondary",
|
||||
},
|
||||
cursor: "move",
|
||||
cancel: "input, select",
|
||||
stop: function () {
|
||||
resetRecordOrders(type);
|
||||
},
|
||||
}).sortable("enable");
|
||||
$(".btn-actions-" + type).removeClass("invisible");
|
||||
} else if (records.length === 1) {
|
||||
$("#" + type + "-records").sortable().sortable("disable");
|
||||
$(".btn-actions-" + type).addClass("invisible");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*******************
|
||||
* Form Validation *
|
||||
*******************/
|
||||
|
||||
/**
|
||||
* Validates the form.
|
||||
*
|
||||
* @returns {boolean} true if the validation succeed, or false
|
||||
* otherwise
|
||||
* @private
|
||||
*/
|
||||
function validateForm() {
|
||||
let isValidated = true;
|
||||
isValidated = isValidated && validateDate();
|
||||
$(".record-account").each(function () {
|
||||
isValidated = isValidated && validateAccount(this);
|
||||
});
|
||||
$(".record-summary").each(function () {
|
||||
isValidated = isValidated && validateSummary(this);
|
||||
});
|
||||
$(".record-amount").each(function () {
|
||||
isValidated = isValidated && validateAmount(this);
|
||||
});
|
||||
if (isTransfer()) {
|
||||
isValidated = isValidated && validateBalance();
|
||||
}
|
||||
isValidated = isValidated && validateNote();
|
||||
return isValidated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the date column.
|
||||
*
|
||||
* @returns {boolean} true if the validation succeed, or false
|
||||
* otherwise
|
||||
* @private
|
||||
*/
|
||||
function validateDate() {
|
||||
const date = $("#txn-date")[0];
|
||||
const errorMessage = $("#txn-date-error");
|
||||
if (date.value === "") {
|
||||
date.classList.add("is-invalid");
|
||||
errorMessage.text(__("Please fill in the date."));
|
||||
return false;
|
||||
}
|
||||
date.classList.remove("is-invalid");
|
||||
errorMessage.text("");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the account column.
|
||||
*
|
||||
* @param {HTMLSelectElement} account the account selection element
|
||||
* @returns {boolean} true if the validation succeed, or false
|
||||
* otherwise
|
||||
* @private
|
||||
*/
|
||||
function validateAccount(account) {
|
||||
const errorMessage = $("#" + account.id + "-error");
|
||||
if (account.value === "") {
|
||||
account.classList.add("is-invalid");
|
||||
errorMessage.text(__("Please select the account."));
|
||||
return false;
|
||||
}
|
||||
account.classList.remove("is-invalid");
|
||||
errorMessage.text("");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the summary column.
|
||||
*
|
||||
* @param {HTMLInputElement} summary the summary input element
|
||||
* @returns {boolean} true if the validation succeed, or false
|
||||
* otherwise
|
||||
* @private
|
||||
*/
|
||||
function validateSummary(summary) {
|
||||
const errorMessage = $("#" + summary.id + "-error");
|
||||
summary.value = summary.value.trim();
|
||||
if (summary.value.length > 128) {
|
||||
summary.classList.add("is-invalid");
|
||||
errorMessage.text(__("This summary is too long (max. 128 characters)."));
|
||||
return false;
|
||||
}
|
||||
summary.classList.remove("is-invalid");
|
||||
errorMessage.text("");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the amount column.
|
||||
*
|
||||
* @param {HTMLInputElement} amount the amount input element
|
||||
* @returns {boolean} true if the validation succeed, or false
|
||||
* otherwise
|
||||
* @private
|
||||
*/
|
||||
function validateAmount(amount) {
|
||||
const errorMessage = $("#" + amount.id + "-error");
|
||||
amount.value = amount.value.trim();
|
||||
if (amount.value === "") {
|
||||
amount.classList.add("is-invalid");
|
||||
errorMessage.text(__("Please fill in the amount."));
|
||||
return false;
|
||||
}
|
||||
amount.classList.remove("is-invalid");
|
||||
errorMessage.text("");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the balance between debit and credit records
|
||||
*
|
||||
* @returns {boolean} true if the validation succeed, or false
|
||||
* otherwise
|
||||
* @private
|
||||
*/
|
||||
function validateBalance() {
|
||||
const balanceRows = $(".balance-row");
|
||||
const errorMessages = $(".balance-error");
|
||||
let debitTotal = 0;
|
||||
$(".debit-to-sum").each(function () {
|
||||
if (this.value !== "") {
|
||||
debitTotal += parseInt(this.value);
|
||||
}
|
||||
});
|
||||
let creditTotal = 0;
|
||||
$(".credit-to-sum").each(function () {
|
||||
if (this.value !== "") {
|
||||
creditTotal += parseInt(this.value);
|
||||
}
|
||||
});
|
||||
if (debitTotal !== creditTotal) {
|
||||
balanceRows.addClass("is-invalid");
|
||||
errorMessages.text(__("The sum of debit and credit are inconsistent."))
|
||||
return false;
|
||||
}
|
||||
balanceRows.removeClass("is-invalid");
|
||||
errorMessages.text("");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the note column.
|
||||
*
|
||||
* @returns {boolean} true if the validation succeed, or false
|
||||
* otherwise
|
||||
* @private
|
||||
*/
|
||||
function validateNote() {
|
||||
const note = $("#txn-note")[0];
|
||||
const errorMessage = $("#txn-note-error");
|
||||
note.value = note.value.trim();
|
||||
if (note.value.length > 128) {
|
||||
note.classList.add("is-invalid");
|
||||
errorMessage.text(__("This note is too long (max. 128 characters)."));
|
||||
return false;
|
||||
}
|
||||
note.classList.remove("is-invalid");
|
||||
errorMessage.text("");
|
||||
return true;
|
||||
}
|
164
accounting/templates/accounting/include/summary-helper.html
Normal file
164
accounting/templates/accounting/include/summary-helper.html
Normal file
@ -0,0 +1,164 @@
|
||||
{% comment %}
|
||||
The Mia Accounting Application
|
||||
summary-helper.html: The view of the summary-helper dialog
|
||||
|
||||
Copyright (c) 2020 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: 2020/4/3
|
||||
{% endcomment %}
|
||||
{% load i18n %}
|
||||
|
||||
<!-- the summary helper dialog -->
|
||||
<!-- The Modal -->
|
||||
<form id="summary-helper-form" action="" method="get">
|
||||
<input id="summary-record" type="hidden" value="" />
|
||||
<div class="modal" id="summary-modal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<label for="summary-summary">
|
||||
<i class="fas fa-edit"></i>
|
||||
{% trans "Summary" context "Accounting|" as text %}{{ text|force_escape }}
|
||||
</label>
|
||||
</h4>
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal body -->
|
||||
<div class="modal-body">
|
||||
<div class="summary-container">
|
||||
<input id="summary-summary" class="form-control" value="" />
|
||||
</div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<span id="summary-tab-category" class="summary-tab nav-link active">{% trans "General" context "Accounting|Summary|" as text %}{{ text|force_escape }}</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span id="summary-tab-travel" class="summary-tab nav-link">{% trans "Travel" context "Accounting|Summary|" as text %}{{ text|force_escape }}</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span id="summary-tab-bus" class="summary-tab nav-link">{% trans "Bus" context "Accounting|Summary|" as text %}{{ text|force_escape }}</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span id="summary-tab-regular" class="summary-tab nav-link">{% trans "Regular" context "Accounting|Summary|" as text %}{{ text|force_escape }}</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span id="summary-tab-count" class="summary-tab nav-link">{% trans "Count" context "Accounting|Summary|" as text %}{{ text|force_escape }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- A general category -->
|
||||
<div id="summary-tab-content-category" class="summary-tab-content">
|
||||
<div class="row">
|
||||
<label class="col-sm-2 col-form-label" for="summary-general-category">{% trans "Category:" context "Accounting|Summary|" as text %}{{ text|force_escape }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="summary-general-category" class="form-control summary-helper-input" type="text" value="" />
|
||||
<div id="summary-general-categories-known" class="summary-categories-known"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- A general travel route -->
|
||||
<div id="summary-tab-content-travel" class="summary-tab-content d-none">
|
||||
<div class="row">
|
||||
<label class="col-sm-2 col-form-label" for="summary-travel-category">{% trans "Category:" context "Accounting|Summary|" as text %}{{ text|force_escape }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="summary-travel-category" class="form-control summary-helper-input summary-travel-part" type="text" value="" />
|
||||
<div id="summary-travel-categories-known" class="summary-categories-known"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-sm-2 col-form-label" for="summary-travel-from">{% trans "From:" context "Accounting|Summary|" as text %}{{ text|force_escape }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="summary-travel-from" class="form-control summary-helper-input summary-travel-part" type="text" value="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-sm-2 col-form-label" for="summary-travel-direction">{% trans "Direction:" context "Accounting|Summary|" as text %}{{ text|force_escape }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="summary-travel-direction" class="summary-helper-input" type="hidden" value="" />
|
||||
<span id="btn-summary-one-way" class="btn btn-outline-primary btn-summary-helper btn-summary-travel-direction"><%="→"%></span>
|
||||
<span class="btn btn-outline-primary btn-summary-helper btn-summary-travel-direction"><%="↔"%></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-sm-2 col-form-label" for="summary-travel-to">{% trans "To:" context "Accounting|Summary|" as text %}{{ text|force_escape }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="summary-travel-to" class="form-control summary-helper-input summary-travel-part" type="text" value="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- A bus route -->
|
||||
<div id="summary-tab-content-bus" class="summary-tab-content d-none">
|
||||
<div class="row">
|
||||
<label class="col-sm-2 col-form-label" for="summary-bus-category">{% trans "Category:" context "Accounting|Summary|" as text %}{{ text|force_escape }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="summary-bus-category" class="form-control summary-helper-input summary-bus-part" type="text" value="" />
|
||||
<div id="summary-bus-categories-known" class="summary-categories-known"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-sm-2 col-form-label" for="summary-bus-route">{% trans "Route:" context "Accounting|Summary|" as text %}{{ text|force_escape }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="summary-bus-route" class="form-control summary-helper-input summary-bus-part" type="text" value="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-sm-2 col-form-label" for="summary-bus-from">{% trans "From:" context "Accounting|Summary|" as text %}{{ text|force_escape }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="summary-bus-from" class="form-control summary-helper-input summary-bus-part" type="text" value="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-sm-2 col-form-label" for="summary-bus-to">{% trans "To:" context "Accounting|Summary|" as text %}{{ text|force_escape }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="summary-bus-to" class="form-control summary-helper-input summary-bus-part" type="text" value="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Regular payments -->
|
||||
<div id="summary-tab-content-regular" class="summary-tab-content d-none">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div id="summary-regular-payments" class="summary-categories-known"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="summary-tab-content-count" class="summary-tab-content d-none">
|
||||
<div class="row">
|
||||
<label class="col-sm-2 col-form-label" for="summary-count">{% trans "Count:" context "Accounting|Summary|" as text %}{{ text|force_escape }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="summary-count" class="form-control summary-helper-input" type="number" min="1" value="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal footer -->
|
||||
<div class="modal-footer">
|
||||
<button id="summary-confirm" class="btn btn-danger" type="submit" data-dismiss="modal">{{ _("Confirm") }}</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -28,10 +28,18 @@ First written: 2020/7/23
|
||||
{% block settings %}
|
||||
{% trans "Cash Expense Transaction" context "Accounting|" as title %}
|
||||
{% setvar "title" title %}
|
||||
{% setvar "use_jqueryui" True %}
|
||||
{% static "accounting/css/transactions.css" as file %}{% add_css file %}
|
||||
{% static "accounting/css/summary-helper.css" as file %}{% add_css file %}
|
||||
{% static "accounting/js/transaction-form.js" as file %}{% add_js file %}
|
||||
{% static "accounting/js/regular-payments.js" as file %}{% add_js file %}
|
||||
{% static "accounting/js/summary-helper.js" as file %}{% add_js file %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include "accounting/include/summary-helper.html" %}
|
||||
|
||||
<div class="btn-group btn-actions">
|
||||
<a class="btn btn-primary" role="button" href="{% if item.transaction %}{% url_keep_return "accounting:transactions.show" "expense" item.transaction %}{% elif request.GET.r %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
|
||||
<i class="fas fa-chevron-circle-left"></i>
|
||||
|
@ -28,10 +28,18 @@ First written: 2020/7/23
|
||||
{% block settings %}
|
||||
{% trans "Cash Income Transaction" context "Accounting|" as title %}
|
||||
{% setvar "title" title %}
|
||||
{% setvar "use_jqueryui" True %}
|
||||
{% static "accounting/css/transactions.css" as file %}{% add_css file %}
|
||||
{% static "accounting/css/summary-helper.css" as file %}{% add_css file %}
|
||||
{% static "accounting/js/transaction-form.js" as file %}{% add_js file %}
|
||||
{% static "accounting/js/regular-payments.js" as file %}{% add_js file %}
|
||||
{% static "accounting/js/summary-helper.js" as file %}{% add_js file %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include "accounting/include/summary-helper.html" %}
|
||||
|
||||
<div class="btn-group btn-actions">
|
||||
<a class="btn btn-primary" role="button" href="{% if item.transaction %}{% url_keep_return "accounting:transactions.show" "income" item.transaction %}{% elif request.GET.r %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
|
||||
<i class="fas fa-chevron-circle-left"></i>
|
||||
|
@ -28,10 +28,18 @@ First written: 2020/7/23
|
||||
{% block settings %}
|
||||
{% trans "Transfer Transaction" context "Accounting|" as title %}
|
||||
{% setvar "title" title %}
|
||||
{% setvar "use_jqueryui" True %}
|
||||
{% static "accounting/css/transactions.css" as file %}{% add_css file %}
|
||||
{% static "accounting/css/summary-helper.css" as file %}{% add_css file %}
|
||||
{% static "accounting/js/transaction-form.js" as file %}{% add_js file %}
|
||||
{% static "accounting/js/regular-payments.js" as file %}{% add_js file %}
|
||||
{% static "accounting/js/summary-helper.js" as file %}{% add_js file %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include "accounting/include/summary-helper.html" %}
|
||||
|
||||
<div class="btn-group btn-actions">
|
||||
<a class="btn btn-primary" role="button" href="{% if item.transaction %}{% url_keep_return "accounting:transactions.show" "transfer" item.transaction %}{% elif request.GET.r %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
|
||||
<i class="fas fa-chevron-circle-left"></i>
|
||||
|
@ -18,6 +18,7 @@
|
||||
"""The utilities of the accounting application.
|
||||
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
|
||||
@ -374,7 +375,11 @@ def fill_txn_from_post(txn_type, txn, post):
|
||||
txn (Transaction): The transaction.
|
||||
post (dict): The POSTed data.
|
||||
"""
|
||||
txn.date = post["date"]
|
||||
m = re.match("^([0-9]{4})-([0-9]{2})-([0-9]{2})$", post["date"])
|
||||
txn.date = datetime.date(
|
||||
int(m.group(1)),
|
||||
int(m.group(2)),
|
||||
int(m.group(3)))
|
||||
if "notes" in post:
|
||||
txn.notes = post["notes"]
|
||||
else:
|
||||
|
Loading…
Reference in New Issue
Block a user