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
|
* First written: 2019/9/17
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.subject-line {
|
.account-line {
|
||||||
font-size: 0.833em;
|
font-size: 0.833em;
|
||||||
}
|
}
|
||||||
.amount {
|
.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 %}
|
{% block settings %}
|
||||||
{% trans "Cash Expense Transaction" context "Accounting|" as title %}
|
{% trans "Cash Expense Transaction" context "Accounting|" as title %}
|
||||||
{% setvar "title" 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
{% include "accounting/include/summary-helper.html" %}
|
||||||
|
|
||||||
<div class="btn-group btn-actions">
|
<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 %}">
|
<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>
|
<i class="fas fa-chevron-circle-left"></i>
|
||||||
|
@ -28,10 +28,18 @@ First written: 2020/7/23
|
|||||||
{% block settings %}
|
{% block settings %}
|
||||||
{% trans "Cash Income Transaction" context "Accounting|" as title %}
|
{% trans "Cash Income Transaction" context "Accounting|" as title %}
|
||||||
{% setvar "title" 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
{% include "accounting/include/summary-helper.html" %}
|
||||||
|
|
||||||
<div class="btn-group btn-actions">
|
<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 %}">
|
<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>
|
<i class="fas fa-chevron-circle-left"></i>
|
||||||
|
@ -28,10 +28,18 @@ First written: 2020/7/23
|
|||||||
{% block settings %}
|
{% block settings %}
|
||||||
{% trans "Transfer Transaction" context "Accounting|" as title %}
|
{% trans "Transfer Transaction" context "Accounting|" as title %}
|
||||||
{% setvar "title" 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
{% include "accounting/include/summary-helper.html" %}
|
||||||
|
|
||||||
<div class="btn-group btn-actions">
|
<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 %}">
|
<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>
|
<i class="fas fa-chevron-circle-left"></i>
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"""The utilities of the accounting application.
|
"""The utilities of the accounting application.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import datetime
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -374,7 +375,11 @@ def fill_txn_from_post(txn_type, txn, post):
|
|||||||
txn (Transaction): The transaction.
|
txn (Transaction): The transaction.
|
||||||
post (dict): The POSTed data.
|
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:
|
if "notes" in post:
|
||||||
txn.notes = post["notes"]
|
txn.notes = post["notes"]
|
||||||
else:
|
else:
|
||||||
|
Loading…
Reference in New Issue
Block a user