diff --git a/src/accounting/static/js/summary-helper.js b/src/accounting/static/js/summary-helper.js
new file mode 100644
index 0000000..0f8d91d
--- /dev/null
+++ b/src/accounting/static/js/summary-helper.js
@@ -0,0 +1,531 @@
+/* The Mia! Accounting Flask Project
+ * summary-helper.js: The JavaScript for the summary helper
+ */
+
+/* Copyright (c) 2023 imacat.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* Author: imacat@mail.imacat.idv.tw (imacat)
+ * First written: 2023/2/28
+ */
+
+// Initializes the page JavaScript.
+document.addEventListener("DOMContentLoaded", function () {
+ SummaryHelper.initialize();
+});
+
+/**
+ * A summary helper.
+ *
+ */
+class SummaryHelper {
+
+ /**
+ * The entry type
+ * @type {string}
+ */
+ #entryType;
+
+ /**
+ * The prefix of the HTML ID and class
+ * @type {string}
+ */
+ #prefix;
+
+ /**
+ * The default tab ID
+ * @type {string}
+ */
+ #defaultTabId;
+
+ /**
+ * Constructs a summary helper.
+ *
+ * @param form {HTMLFormElement} the summary helper form
+ */
+ constructor(form) {
+ this.#entryType = form.dataset.entryType;
+ this.#prefix = "accounting-summary-helper-" + form.dataset.entryType;
+ this.#defaultTabId = form.dataset.defaultTabId;
+ this.#init();
+ }
+
+ /**
+ * Initializes the summary helper.
+ *
+ */
+ #init() {
+ const helper = this;
+ const tabs = Array.from(document.getElementsByClassName(this.#prefix + "-tab"));
+ tabs.forEach(function (tab) {
+ tab.onclick = function () {
+ helper.#switchToTab(tab.dataset.tabId);
+ }
+ });
+ this.#initializeGeneralTagHelper();
+ this.#initializeGeneralTripHelper();
+ this.#initializeBusTripHelper();
+ this.#initializeNumberHelper();
+ this.#initializeSubmission();
+ }
+
+ /**
+ * Switches to a tab.
+ *
+ * @param tabId {string} the tab ID.
+ */
+ #switchToTab(tabId) {
+ const tabs = Array.from(document.getElementsByClassName(this.#prefix + "-tab"));
+ const pages = Array.from(document.getElementsByClassName(this.#prefix + "-page"));
+ tabs.forEach(function (tab) {
+ if (tab.dataset.tabId === tabId) {
+ tab.classList.add("active");
+ tab.ariaCurrent = "page";
+ } else {
+ tab.classList.remove("active");
+ tab.ariaCurrent = "false";
+ }
+ });
+ pages.forEach(function (page) {
+ if (page.dataset.tabId === tabId) {
+ page.classList.remove("d-none");
+ page.ariaCurrent = "page";
+ } else {
+ page.classList.add("d-none");
+ page.ariaCurrent = "false";
+ }
+ });
+ }
+
+ /**
+ * Initializes the general tag helper.
+ *
+ */
+ #initializeGeneralTagHelper() {
+ const buttons = Array.from(document.getElementsByClassName(this.#prefix + "-general-btn-tag"));
+ const summary = document.getElementById(this.#prefix + "-summary");
+ const tag = document.getElementById(this.#prefix + "-general-tag");
+ const updateSummary = function () {
+ const pos = summary.value.indexOf("—");
+ const prefix = tag.value === ""? "": tag.value + "—";
+ if (pos === -1) {
+ summary.value = prefix + summary.value;
+ } else {
+ summary.value = prefix + summary.value.substring(pos + 1);
+ }
+ }
+ buttons.forEach(function (button) {
+ button.onclick = function () {
+ buttons.forEach(function (otherButton) {
+ otherButton.classList.remove("btn-primary");
+ otherButton.classList.add("btn-outline-primary");
+ });
+ button.classList.remove("btn-outline-primary");
+ button.classList.add("btn-primary");
+ tag.value = button.dataset.value;
+ updateSummary();
+ };
+ });
+ tag.onchange = function () {
+ buttons.forEach(function (button) {
+ if (button.dataset.value === tag.value) {
+ button.classList.remove("btn-outline-primary");
+ button.classList.add("btn-primary");
+ } else {
+ button.classList.remove("btn-primary");
+ button.classList.add("btn-outline-primary");
+ }
+ });
+ updateSummary();
+ };
+ }
+
+ /**
+ * Initializes the general trip helper.
+ *
+ */
+ #initializeGeneralTripHelper() {
+ const buttons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-btn-tag"));
+ const summary = document.getElementById(this.#prefix + "-summary");
+ const tag = document.getElementById(this.#prefix + "-travel-tag");
+ const from = document.getElementById(this.#prefix + "-travel-from");
+ const directionButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-direction"))
+ const to = document.getElementById(this.#prefix + "-travel-to");
+ const updateSummary = function () {
+ let direction;
+ for (const button of directionButtons) {
+ if (button.classList.contains("btn-primary")) {
+ direction = button.dataset.arrow;
+ break;
+ }
+ }
+ summary.value = tag.value + "—" + from.value + direction + to.value;
+ };
+ buttons.forEach(function (button) {
+ button.onclick = function () {
+ buttons.forEach(function (otherButton) {
+ otherButton.classList.remove("btn-primary");
+ otherButton.classList.add("btn-outline-primary");
+ });
+ button.classList.remove("btn-outline-primary");
+ button.classList.add("btn-primary");
+ tag.value = button.dataset.value;
+ updateSummary();
+ };
+ });
+ tag.onchange = function () {
+ buttons.forEach(function (button) {
+ if (button.dataset.value === tag.value) {
+ button.classList.remove("btn-outline-primary");
+ button.classList.add("btn-primary");
+ } else {
+ button.classList.remove("btn-primary");
+ button.classList.add("btn-outline-primary");
+ }
+ });
+ updateSummary();
+ };
+ from.onchange = updateSummary;
+ directionButtons.forEach(function (button) {
+ button.onclick = function () {
+ directionButtons.forEach(function (otherButton) {
+ otherButton.classList.remove("btn-primary");
+ otherButton.classList.add("btn-outline-primary");
+ });
+ button.classList.remove("btn-outline-primary");
+ button.classList.add("btn-primary");
+ updateSummary();
+ };
+ });
+ to.onchange = updateSummary;
+ }
+
+ /**
+ * Initializes the bus trip helper.
+ *
+ */
+ #initializeBusTripHelper() {
+ const buttons = Array.from(document.getElementsByClassName(this.#prefix + "-bus-btn-tag"));
+ const summary = document.getElementById(this.#prefix + "-summary");
+ const tag = document.getElementById(this.#prefix + "-bus-tag");
+ const route = document.getElementById(this.#prefix + "-bus-route");
+ const from = document.getElementById(this.#prefix + "-bus-from");
+ const to = document.getElementById(this.#prefix + "-bus-to");
+ const updateSummary = function () {
+ summary.value = tag.value + "—" + route.value + "—" + from.value + "→" + to.value;
+ };
+ buttons.forEach(function (button) {
+ button.onclick = function () {
+ buttons.forEach(function (otherButton) {
+ otherButton.classList.remove("btn-primary");
+ otherButton.classList.add("btn-outline-primary");
+ });
+ button.classList.remove("btn-outline-primary");
+ button.classList.add("btn-primary");
+ tag.value = button.dataset.value;
+ updateSummary();
+ };
+ });
+ tag.onchange = function () {
+ buttons.forEach(function (button) {
+ if (button.dataset.value === tag.value) {
+ button.classList.remove("btn-outline-primary");
+ button.classList.add("btn-primary");
+ } else {
+ button.classList.remove("btn-primary");
+ button.classList.add("btn-outline-primary");
+ }
+ });
+ updateSummary();
+ };
+ route.onchange = updateSummary;
+ from.onchange = updateSummary;
+ to.onchange = updateSummary;
+ }
+
+ /**
+ * Initializes the number helper.
+ *
+ */
+ #initializeNumberHelper() {
+ const summary = document.getElementById(this.#prefix + "-summary");
+ const number = document.getElementById(this.#prefix + "-number");
+ number.onchange = function () {
+ const found = summary.value.match(/^(.+)×(\d+)$/);
+ if (found !== null) {
+ summary.value = found[1];
+ }
+ if (number.value > 1) {
+ summary.value = summary.value + "×" + String(number.value);
+ }
+ };
+ }
+
+ /**
+ * Initializes the summary submission
+ *
+ */
+ #initializeSubmission() {
+ const form = document.getElementById(this.#prefix);
+ const helper = this;
+ form.onsubmit = function () {
+ if (helper.#validate()) {
+ helper.#submit();
+ }
+ return false;
+ };
+ }
+
+ /**
+ * Validates the form.
+ *
+ * @return {boolean} true if valid, or false otherwise
+ */
+ #validate() {
+ return true;
+ }
+
+ /**
+ * Submits the summary.
+ *
+ */
+ #submit() {
+ const summary = document.getElementById(this.#prefix + "-summary");
+ const formSummaryControl = document.getElementById("accounting-entry-form-summary-control");
+ const formSummary = document.getElementById("accounting-entry-form-summary");
+ const helperModal = document.getElementById(this.#prefix + "-modal");
+ const entryModal = document.getElementById("accounting-entry-form-modal");
+ if (summary.value === "") {
+ formSummaryControl.classList.remove("accounting-not-empty");
+ } else {
+ formSummaryControl.classList.add("accounting-not-empty");
+ }
+ formSummary.dataset.value = summary.value;
+ formSummary.innerText = summary.value;
+ bootstrap.Modal.getInstance(helperModal).hide();
+ bootstrap.Modal.getOrCreateInstance(entryModal).show();
+ }
+
+ /**
+ * Initializes the summary help when it is shown.
+ *
+ * @param isNew {boolean} true for adding a new journal entry, or false otherwise
+ */
+ initShow(isNew) {
+ const closeButtons = Array.from(document.getElementsByClassName(this.#prefix + "-close"));
+ closeButtons.forEach(function (button) {
+ if (isNew) {
+ button.dataset.bsTarget = "";
+ } else {
+ button.dataset.bsTarget = "#accounting-entry-form-modal";
+ }
+ });
+ this.#reset();
+ if (!isNew) {
+ this.#populate();
+ }
+ }
+
+ /**
+ * Resets the summary helper.
+ *
+ */
+ #reset() {
+ const inputs = Array.from(document.getElementsByClassName(this.#prefix + "-input"));
+ const tagButtons = Array.from(document.getElementsByClassName(this.#prefix + "-btn-tag"));
+ const directionButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-direction"));
+ inputs.forEach(function (input) {
+ input.value = "";
+ input.classList.remove("is-invalid");
+ });
+ tagButtons.forEach(function (btnTag) {
+ btnTag.classList.remove("btn-primary");
+ btnTag.classList.add("btn-outline-primary");
+ });
+ directionButtons.forEach(function (btnDirection) {
+ if (btnDirection.classList.contains("accounting-default")) {
+ btnDirection.classList.remove("btn-outline-primary");
+ btnDirection.classList.add("btn-primary");
+ } else {
+ btnDirection.classList.add("btn-outline-primary");
+ btnDirection.classList.remove("btn-primary");
+ }
+ });
+ this.#switchToTab(this.#defaultTabId);
+ }
+
+ /**
+ * Populates the summary helper from the journal entry form.
+ *
+ */
+ #populate() {
+ const formSummary = document.getElementById("accounting-entry-form-summary");
+ const summary = document.getElementById(this.#prefix + "-summary");
+ summary.value = formSummary.dataset.value;
+ const pos = summary.value.indexOf("—");
+ if (pos === -1) {
+ return;
+ }
+ let found;
+ found = summary.value.match(/^(.+?)—(.+?)—(.+?)→(.+?)(?:×(\d+)?)?$/);
+ if (found !== null) {
+ return this.#populateBusTrip(found[1], found[2], found[3], found[4], found[5]);
+ }
+ found = summary.value.match(/^(.+?)—(.+?)([→↔])(.+?)(?:×(\d+)?)?$/);
+ if (found !== null) {
+ return this.#populateGeneralTrip(found[1], found[2], found[3], found[4], found[5]);
+ }
+ found = summary.value.match(/^(.+?)—.+?(?:×(\d+)?)?$/);
+ if (found !== null) {
+ return this.#populateGeneralTag(found[1], found[2]);
+ }
+ }
+
+ /**
+ * Populates a bus trip.
+ *
+ * @param tagName {string} the tag name
+ * @param routeName {string} the route name or route number
+ * @param fromName {string} the name of the origin
+ * @param toName {string} the name of the destination
+ * @param numberStr {string|undefined} the number of items, if any
+ */
+ #populateBusTrip(tagName, routeName, fromName, toName, numberStr) {
+ const tag = document.getElementById(this.#prefix + "-bus-tag");
+ const route = document.getElementById(this.#prefix + "-bus-route");
+ const from = document.getElementById(this.#prefix + "-bus-from");
+ const to = document.getElementById(this.#prefix + "-bus-to");
+ const number = document.getElementById(this.#prefix + "-number");
+ const buttons = Array.from(document.getElementsByClassName(this.#prefix + "-bus-btn-tag"));
+ tag.value = tagName;
+ route.value = routeName;
+ from.value = fromName;
+ to.value = toName;
+ if (numberStr !== undefined) {
+ number.value = parseInt(numberStr);
+ }
+ buttons.forEach(function (button) {
+ if (button.dataset.value === tagName) {
+ button.classList.remove("btn-outline-primary");
+ button.classList.add("btn-primary");
+ }
+ });
+ this.#switchToTab("bus");
+ }
+
+ /**
+ * Populates a general trip.
+ *
+ * @param tagName {string} the tag name
+ * @param fromName {string} the name of the origin
+ * @param direction {string} the direction arrow, either "→" or "↔"
+ * @param toName {string} the name of the destination
+ * @param numberStr {string|undefined} the number of items, if any
+ */
+ #populateGeneralTrip(tagName, fromName, direction, toName, numberStr) {
+ const tag = document.getElementById(this.#prefix + "-travel-tag");
+ const from = document.getElementById(this.#prefix + "-travel-from");
+ const to = document.getElementById(this.#prefix + "-travel-to");
+ const number = document.getElementById(this.#prefix + "-number");
+ const buttons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-btn-tag"));
+ const directionButtons = Array.from(document.getElementsByClassName(this.#prefix + "-travel-direction"));
+ tag.value = tagName;
+ from.value = fromName;
+ directionButtons.forEach(function (btnDirection) {
+ if (btnDirection.dataset.arrow === direction) {
+ btnDirection.classList.remove("btn-outline-primary");
+ btnDirection.classList.add("btn-primary");
+ } else {
+ btnDirection.classList.add("btn-outline-primary");
+ btnDirection.classList.remove("btn-primary");
+ }
+ });
+ to.value = toName;
+ if (numberStr !== undefined) {
+ number.value = parseInt(numberStr);
+ }
+ buttons.forEach(function (button) {
+ if (button.dataset.value === tagName) {
+ button.classList.remove("btn-outline-primary");
+ button.classList.add("btn-primary");
+ }
+ });
+ this.#switchToTab("travel");
+ }
+
+ /**
+ * Populates a general tag.
+ *
+ * @param tagName {string} the tag name
+ * @param numberStr {string|undefined} the number of items, if any
+ */
+ #populateGeneralTag(tagName, numberStr) {
+ const tag = document.getElementById(this.#prefix + "-general-tag");
+ const number = document.getElementById(this.#prefix + "-number");
+ const buttons = Array.from(document.getElementsByClassName(this.#prefix + "-general-btn-tag"));
+ tag.value = tagName;
+ if (numberStr !== undefined) {
+ number.value = parseInt(numberStr);
+ }
+ buttons.forEach(function (button) {
+ if (button.dataset.value === tagName) {
+ button.classList.remove("btn-outline-primary");
+ button.classList.add("btn-primary");
+ }
+ });
+ this.#switchToTab("general");
+ }
+
+ /**
+ * The summary helpers.
+ * @type {{debit: SummaryHelper, credit: SummaryHelper}}
+ */
+ static #helpers = {}
+
+ /**
+ * Initializes the summary helpers.
+ *
+ */
+ static initialize() {
+ const forms = Array.from(document.getElementsByClassName("accounting-summary-helper"));
+ for (const form of forms) {
+ const helper = new SummaryHelper(form);
+ this.#helpers[helper.#entryType] = helper;
+ }
+ this.#initializeTransactionForm();
+ }
+
+ /**
+ * Initializes the transaction form.
+ *
+ */
+ static #initializeTransactionForm() {
+ const entryForm = document.getElementById("accounting-entry-form");
+ const formSummaryControl = document.getElementById("accounting-entry-form-summary-control");
+ const helpers = this.#helpers;
+ formSummaryControl.onclick = function () {
+ helpers[entryForm.dataset.entryType].initShow(false);
+ };
+ }
+
+ /**
+ * Initializes the summary helper for a new journal entry.
+ *
+ * @param entryType {string} the entry type, either "debit" or "credit"
+ */
+ static initializeNewJournalEntry(entryType) {
+ this.#helpers[entryType].initShow(true);
+ }
+}
diff --git a/src/accounting/static/js/transaction-form.js b/src/accounting/static/js/transaction-form.js
index 5bc183e..539266e 100644
--- a/src/accounting/static/js/transaction-form.js
+++ b/src/accounting/static/js/transaction-form.js
@@ -157,6 +157,7 @@ function initializeNewEntryButton(button) {
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
const formAccount = document.getElementById("accounting-entry-form-account");
const formAccountError = document.getElementById("accounting-entry-form-account-error")
+ const formSummaryControl = document.getElementById("accounting-entry-form-summary-control");
const formSummary = document.getElementById("accounting-entry-form-summary");
const formSummaryError = document.getElementById("accounting-entry-form-summary-error");
const formAmount = document.getElementById("accounting-entry-form-amount");
@@ -165,19 +166,23 @@ function initializeNewEntryButton(button) {
entryForm.dataset.currencyIndex = button.dataset.currencyIndex;
entryForm.dataset.entryType = button.dataset.entryType;
entryForm.dataset.entryIndex = button.dataset.entryIndex;
- formAccountControl.classList.remove("accounting-not-empty")
+ formAccountControl.classList.remove("accounting-not-empty");
formAccountControl.classList.remove("is-invalid");
formAccountControl.dataset.bsTarget = button.dataset.accountModal;
formAccount.innerText = "";
formAccount.dataset.code = "";
formAccount.dataset.text = "";
formAccountError.innerText = "";
- formSummary.value = "";
- formSummary.classList.remove("is-invalid");
+ formSummaryControl.dataset.bsTarget = "#accounting-summary-helper-" + button.dataset.entryType + "-modal";
+ formSummaryControl.classList.remove("accounting-not-empty");
+ formSummaryControl.classList.remove("is-invalid");
+ formSummary.dataset.value = "";
+ formSummary.innerText = ""
formSummaryError.innerText = ""
formAmount.value = "";
formAmount.classList.remove("is-invalid");
formAmountError.innerText = "";
+ SummaryHelper.initializeNewJournalEntry(button.dataset.entryType);
};
}
@@ -209,6 +214,7 @@ function initializeJournalEntry(entry) {
const control = document.getElementById(entry.dataset.prefix + "-control");
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
const formAccount = document.getElementById("accounting-entry-form-account");
+ const formSummaryControl = document.getElementById("accounting-entry-form-summary-control");
const formSummary = document.getElementById("accounting-entry-form-summary");
const formAmount = document.getElementById("accounting-entry-form-amount");
control.onclick = function () {
@@ -224,7 +230,14 @@ function initializeJournalEntry(entry) {
formAccount.innerText = accountCode.dataset.text;
formAccount.dataset.code = accountCode.value;
formAccount.dataset.text = accountCode.dataset.text;
- formSummary.value = summary.value;
+ formSummaryControl.dataset.bsTarget = "#accounting-summary-helper-" + entry.dataset.entryType + "-modal";
+ if (summary.value === "") {
+ formSummaryControl.classList.remove("accounting-not-empty");
+ } else {
+ formSummaryControl.classList.add("accounting-not-empty");
+ }
+ formSummary.dataset.value = summary.value;
+ formSummary.innerText = summary.value;
formAmount.value = amount.value;
validateJournalEntryForm();
};
@@ -239,7 +252,6 @@ function initializeJournalEntryFormModal() {
const entryForm = document.getElementById("accounting-entry-form");
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
const formAccount = document.getElementById("accounting-entry-form-account");
- const formSummary = document.getElementById("accounting-entry-form-summary");
const formAmount = document.getElementById("accounting-entry-form-amount");
const modal = document.getElementById("accounting-entry-form-modal");
formAccountControl.onclick = function () {
@@ -268,7 +280,6 @@ function initializeJournalEntryFormModal() {
btnClear.disabled = false;
}
};
- formSummary.onchange = validateJournalEntrySummary;
formAmount.onchange = validateJournalEntryAmount;
entryForm.onsubmit = function () {
if (validateJournalEntryForm()) {
@@ -320,10 +331,9 @@ function validateJournalEntryAccount() {
* @private
*/
function validateJournalEntrySummary() {
- const field = document.getElementById("accounting-entry-form-summary");
+ const control = document.getElementById("accounting-entry-form-summary-control");
const error = document.getElementById("accounting-entry-form-summary-error");
- field.value = field.value.trim();
- field.classList.remove("is-invalid");
+ control.classList.remove("is-invalid");
error.innerText = "";
return true;
}
@@ -393,8 +403,8 @@ function saveJournalEntryForm() {
accountCode.value = formAccount.dataset.code;
accountCode.dataset.text = formAccount.dataset.text;
accountText.innerText = formAccount.dataset.text;
- summary.value = formSummary.value;
- summaryText.innerText = formSummary.value;
+ summary.value = formSummary.dataset.value;
+ summaryText.innerText = formSummary.dataset.value;
amount.value = formAmount.value;
amountText.innerText = formatDecimal(new Decimal(formAmount.value));
if (entryForm.dataset.entryIndex === "new") {
diff --git a/src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html b/src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html
index 543360b..400e6ba 100644
--- a/src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html
+++ b/src/accounting/templates/accounting/transaction/expense/include/form-currency-item.html
@@ -70,7 +70,7 @@ First written: 2023/2/25
-
-
-
-
+
+
+
+
+
diff --git a/src/accounting/templates/accounting/transaction/include/form.html b/src/accounting/templates/accounting/transaction/include/form.html
index c09ac1b..f25fd9c 100644
--- a/src/accounting/templates/accounting/transaction/include/form.html
+++ b/src/accounting/templates/accounting/transaction/include/form.html
@@ -24,6 +24,7 @@ First written: 2023/2/26
{% block accounting_scripts %}
+
{% endblock %}
{% block content %}
@@ -85,6 +86,9 @@ First written: 2023/2/26
{% include "accounting/transaction/include/entry-form-modal.html" %}
+{% for summary_helper in form.summary_helper.types %}
+ {% include "accounting/transaction/include/summary-helper-modal.html" %}
+{% endfor %}
{% block account_selector_modals %}{% endblock %}
{% endblock %}
diff --git a/src/accounting/templates/accounting/transaction/include/summary-helper-modal.html b/src/accounting/templates/accounting/transaction/include/summary-helper-modal.html
new file mode 100644
index 0000000..0eb20db
--- /dev/null
+++ b/src/accounting/templates/accounting/transaction/include/summary-helper-modal.html
@@ -0,0 +1,171 @@
+{#
+The Mia! Accounting Flask Project
+entry-form-modal.html: The modal of the summary helper
+
+ Copyright (c) 2023 imacat.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+Author: imacat@mail.imacat.idv.tw (imacat)
+First written: 2023/2/28
+#}
+
diff --git a/src/accounting/templates/accounting/transaction/income/include/form-currency-item.html b/src/accounting/templates/accounting/transaction/income/include/form-currency-item.html
index 9569007..6e35e69 100644
--- a/src/accounting/templates/accounting/transaction/income/include/form-currency-item.html
+++ b/src/accounting/templates/accounting/transaction/income/include/form-currency-item.html
@@ -70,7 +70,7 @@ First written: 2023/2/25