Compare commits

..

13 Commits

Author SHA1 Message Date
74b81d3e23 Renamed the offset_original_id column to original_entry_id, and the offset_original relationship to original_entry in the JournalEntry data model. 2023-03-11 16:34:30 +08:00
a0fba6387f Added the order to the search report. 2023-03-11 16:34:30 +08:00
d28bdf2064 Revised the parameter order in the template of the currency sub-form of the transaction form. 2023-03-11 16:34:29 +08:00
edf0c00e34 Shortened the names of the #filterAccountOptions, #getAccountCodeUsedInForm, and #shouldAccountOptionShow methods to #filterOptions, #getCodesUsedInForm, and #shouldOptionShow, respectively, in the JavaScript AccountSelector class. 2023-03-11 16:34:29 +08:00
107d161379 Removed a debug output from the JavaScript AccountSelector class. 2023-03-11 16:34:29 +08:00
f2c184f769 Rewrote the JavaScript AccountSelector to store the page elements in the object. 2023-03-11 16:34:28 +08:00
b45986ecfc Fixed the parameter type for the JavaScript AccountSelector class. 2023-03-11 16:34:28 +08:00
a2c2452ec5 Added a missing blank line to the JavaScript AccountSelector class. 2023-03-11 16:34:28 +08:00
5194258b48 Removed the redundant #init method from the JavaScript AccountSelector class. 2023-03-11 16:34:28 +08:00
3fe7eb41ac Removed the unused "__in_use_account_id" property from the TransactionForm form. 2023-03-11 16:34:28 +08:00
7fb9e2f0a1 Added missing documentation to the OptionLink data model in the "accounting.report.utils.option_link" module. 2023-03-11 16:34:28 +08:00
1d443f7b76 Renamed the "accounting.transaction.form" module to "accounting.transaction.forms". It only contains forms now. 2023-03-11 16:34:28 +08:00
6ad4fba9cd Moved the "accounting.transaction.operators", "accounting.transaction.summary_editor" and "accounting.transaction.form.account_option" modules into the "accounting.transaction.utils" module. 2023-03-11 16:34:28 +08:00
16 changed files with 159 additions and 109 deletions

View File

@ -597,14 +597,14 @@ class JournalEntry(db.Model):
"""True for a debit entry, or False for a credit entry.""" """True for a debit entry, or False for a credit entry."""
no = db.Column(db.Integer, nullable=False) no = db.Column(db.Integer, nullable=False)
"""The entry number under the transaction and debit or credit.""" """The entry number under the transaction and debit or credit."""
offset_original_id = db.Column(db.Integer, original_entry_id = db.Column(db.Integer,
db.ForeignKey(id, onupdate="CASCADE"), db.ForeignKey(id, onupdate="CASCADE"),
nullable=True) nullable=True)
"""The ID of the original entry to offset.""" """The ID of the original entry."""
offset_original = db.relationship("JournalEntry", back_populates="offsets", original_entry = db.relationship("JournalEntry", back_populates="offsets",
remote_side=id, passive_deletes=True) remote_side=id, passive_deletes=True)
"""The original entry to offset.""" """The original entry."""
offsets = db.relationship("JournalEntry", back_populates="offset_original") offsets = db.relationship("JournalEntry", back_populates="original_entry")
"""The offset entries.""" """The offset entries."""
currency_code = db.Column(db.String, currency_code = db.Column(db.String,
db.ForeignKey(Currency.code, onupdate="CASCADE"), db.ForeignKey(Currency.code, onupdate="CASCADE"),

View File

@ -68,7 +68,10 @@ class EntryCollector:
except ArithmeticError: except ArithmeticError:
pass pass
conditions.append(sa.or_(*sub_conditions)) conditions.append(sa.or_(*sub_conditions))
return JournalEntry.query.filter(*conditions)\ return JournalEntry.query.join(Transaction).filter(*conditions)\
.order_by(Transaction.date,
JournalEntry.is_debit,
JournalEntry.no)\
.options(selectinload(JournalEntry.account), .options(selectinload(JournalEntry.account),
selectinload(JournalEntry.currency), selectinload(JournalEntry.currency),
selectinload(JournalEntry.transaction)).all() selectinload(JournalEntry.transaction)).all()

View File

@ -27,10 +27,15 @@ class OptionLink:
"""Constructs an option link. """Constructs an option link.
:param title: The title. :param title: The title.
:param url: The URI. :param url: The URL.
:param is_active: True if active, or False otherwise :param is_active: True if active, or False otherwise
:param fa_icon: The font-awesome icon, if any.
""" """
self.title: str = title self.title: str = title
"""The title."""
self.url: str = url self.url: str = url
"""The URL."""
self.is_active: bool = is_active self.is_active: bool = is_active
"""True if active, or False otherwise."""
self.fa_icon: str | None = fa_icon self.fa_icon: str | None = fa_icon
"""The font-awesome icon, if any."""

View File

@ -45,78 +45,91 @@ class AccountSelector {
*/ */
#prefix; #prefix;
/**
* The button to clear the account
* @type {HTMLButtonElement}
*/
#clearButton
/**
* The query input
* @type {HTMLInputElement}
*/
#query;
/**
* The error message when the query has no result
* @type {HTMLParagraphElement}
*/
#queryNoResult;
/**
* The option list
* @type {HTMLUListElement}
*/
#optionList;
/**
* The options
* @type {HTMLLIElement[]}
*/
#options;
/**
* The more item to show all accounts
* @type {HTMLLIElement}
*/
#more;
/** /**
* Constructs an account selector. * Constructs an account selector.
* *
* @param modal {HTMLFormElement} the account selector modal * @param modal {HTMLDivElement} the account selector modal
*/ */
constructor(modal) { constructor(modal) {
this.#entryType = modal.dataset.entryType; this.#entryType = modal.dataset.entryType;
this.#prefix = "accounting-account-selector-" + modal.dataset.entryType; this.#prefix = "accounting-account-selector-" + modal.dataset.entryType;
this.#init(); this.#query = document.getElementById(this.#prefix + "-query");
} this.#queryNoResult = document.getElementById(this.#prefix + "-option-no-result");
this.#optionList = document.getElementById(this.#prefix + "-option-list");
/** // noinspection JSValidateTypes
* Initializes the account selector. this.#options = Array.from(document.getElementsByClassName(this.#prefix + "-option"));
* this.#more = document.getElementById(this.#prefix + "-more");
*/ this.#clearButton = document.getElementById(this.#prefix + "-btn-clear");
#init() { this.#more.onclick = () => {
const formAccountControl = document.getElementById("accounting-entry-form-account-control"); this.#more.classList.add("d-none");
const formAccount = document.getElementById("accounting-entry-form-account"); this.#filterOptions();
const more = document.getElementById(this.#prefix + "-more");
const btnClear = document.getElementById(this.#prefix + "-btn-clear");
const options = Array.from(document.getElementsByClassName(this.#prefix + "-option"));
more.onclick = () => {
more.classList.add("d-none");
this.#filterAccountOptions();
}; };
this.#initializeAccountQuery(); this.#clearButton.onclick = () => {
btnClear.onclick = () => { AccountSelector.#formAccountControl.classList.remove("accounting-not-empty");
formAccountControl.classList.remove("accounting-not-empty"); AccountSelector.#formAccount.innerText = "";
formAccount.innerText = ""; AccountSelector.#formAccount.dataset.code = "";
formAccount.dataset.code = ""; AccountSelector.#formAccount.dataset.text = "";
formAccount.dataset.text = "";
validateJournalEntryAccount(); validateJournalEntryAccount();
}; };
for (const option of options) { for (const option of this.#options) {
option.onclick = () => { option.onclick = () => {
formAccountControl.classList.add("accounting-not-empty"); AccountSelector.#formAccountControl.classList.add("accounting-not-empty");
formAccount.innerText = option.dataset.content; AccountSelector.#formAccount.innerText = option.dataset.content;
formAccount.dataset.code = option.dataset.code; AccountSelector.#formAccount.dataset.code = option.dataset.code;
formAccount.dataset.text = option.dataset.content; AccountSelector.#formAccount.dataset.text = option.dataset.content;
validateJournalEntryAccount(); validateJournalEntryAccount();
}; };
} }
} this.#query.addEventListener("input", () => {
this.#filterOptions();
/**
* Initializes the query on the account options.
*
*/
#initializeAccountQuery() {
const query = document.getElementById(this.#prefix + "-query");
query.addEventListener("input", () => {
this.#filterAccountOptions();
}); });
} }
/** /**
* Filters the account options. * Filters the options.
* *
*/ */
#filterAccountOptions() { #filterOptions() {
const query = document.getElementById(this.#prefix + "-query"); const codesInUse = this.#getCodesUsedInForm();
const optionList = document.getElementById(this.#prefix + "-option-list");
if (optionList === null) {
console.log(this.#prefix + "-option-list");
}
const options = Array.from(document.getElementsByClassName(this.#prefix + "-option"));
const more = document.getElementById(this.#prefix + "-more");
const queryNoResult = document.getElementById(this.#prefix + "-option-no-result");
const codesInUse = this.#getAccountCodeUsedInForm();
let shouldAnyShow = false; let shouldAnyShow = false;
for (const option of options) { for (const option of this.#options) {
const shouldShow = this.#shouldAccountOptionShow(option, more, codesInUse, query); const shouldShow = this.#shouldOptionShow(option, this.#more, codesInUse, this.#query);
if (shouldShow) { if (shouldShow) {
option.classList.remove("d-none"); option.classList.remove("d-none");
shouldAnyShow = true; shouldAnyShow = true;
@ -124,12 +137,12 @@ class AccountSelector {
option.classList.add("d-none"); option.classList.add("d-none");
} }
} }
if (!shouldAnyShow && more.classList.contains("d-none")) { if (!shouldAnyShow && this.#more.classList.contains("d-none")) {
optionList.classList.add("d-none"); this.#optionList.classList.add("d-none");
queryNoResult.classList.remove("d-none"); this.#queryNoResult.classList.remove("d-none");
} else { } else {
optionList.classList.remove("d-none"); this.#optionList.classList.remove("d-none");
queryNoResult.classList.add("d-none"); this.#queryNoResult.classList.add("d-none");
} }
} }
@ -138,10 +151,9 @@ class AccountSelector {
* *
* @return {string[]} the account codes that are used in the form * @return {string[]} the account codes that are used in the form
*/ */
#getAccountCodeUsedInForm() { #getCodesUsedInForm() {
const accountCodes = Array.from(document.getElementsByClassName("accounting-" + this.#prefix + "-account-code")); const accountCodes = Array.from(document.getElementsByClassName("accounting-" + this.#prefix + "-account-code"));
const formAccount = document.getElementById("accounting-entry-form-account"); const inUse = [AccountSelector.#formAccount.dataset.code];
const inUse = [formAccount.dataset.code];
for (const accountCode of accountCodes) { for (const accountCode of accountCodes) {
inUse.push(accountCode.value); inUse.push(accountCode.value);
} }
@ -149,15 +161,15 @@ class AccountSelector {
} }
/** /**
* Returns whether an account option should show. * Returns whether an option should show.
* *
* @param option {HTMLLIElement} the account option * @param option {HTMLLIElement} the option
* @param more {HTMLLIElement} the more account element * @param more {HTMLLIElement} the more element
* @param inUse {string[]} the account codes that are used in the form * @param inUse {string[]} the account codes that are used in the form
* @param query {HTMLInputElement} the query element, if any * @param query {HTMLInputElement} the query element, if any
* @return {boolean} true if the account option should show, or false otherwise * @return {boolean} true if the option should show, or false otherwise
*/ */
#shouldAccountOptionShow(option, more, inUse, query) { #shouldOptionShow(option, more, inUse, query) {
const isQueryMatched = () => { const isQueryMatched = () => {
if (query.value === "") { if (query.value === "") {
return true; return true;
@ -180,33 +192,28 @@ class AccountSelector {
} }
/** /**
* Initializes the account selector when it is shown. * The callback when the account selector is shown.
* *
*/ */
initShow() { #onOpen() {
const formAccount = document.getElementById("accounting-entry-form-account"); this.#query.value = "";
const query = document.getElementById(this.#prefix + "-query") this.#more.classList.remove("d-none");
const more = document.getElementById(this.#prefix + "-more"); this.#filterOptions();
const options = Array.from(document.getElementsByClassName(this.#prefix + "-option")); for (const option of this.#options) {
const btnClear = document.getElementById(this.#prefix + "-btn-clear"); if (option.dataset.code === AccountSelector.#formAccount.dataset.code) {
query.value = "";
more.classList.remove("d-none");
this.#filterAccountOptions();
for (const option of options) {
if (option.dataset.code === formAccount.dataset.code) {
option.classList.add("active"); option.classList.add("active");
} else { } else {
option.classList.remove("active"); option.classList.remove("active");
} }
} }
if (formAccount.dataset.code === "") { if (AccountSelector.#formAccount.dataset.code === "") {
btnClear.classList.add("btn-secondary"); this.#clearButton.classList.add("btn-secondary");
btnClear.classList.remove("btn-danger"); this.#clearButton.classList.remove("btn-danger");
btnClear.disabled = true; this.#clearButton.disabled = true;
} else { } else {
btnClear.classList.add("btn-danger"); this.#clearButton.classList.add("btn-danger");
btnClear.classList.remove("btn-secondary"); this.#clearButton.classList.remove("btn-secondary");
btnClear.disabled = false; this.#clearButton.disabled = false;
} }
} }
@ -216,11 +223,32 @@ class AccountSelector {
*/ */
static #selectors = {} static #selectors = {}
/**
* The journal entry form.
* @type {HTMLFormElement}
*/
static #entryForm;
/**
* The control of the account on the journal entry form
* @type {HTMLDivElement}
*/
static #formAccountControl;
/**
* The account on the journal entry form
* @type {HTMLDivElement}
*/
static #formAccount;
/** /**
* Initializes the account selectors. * Initializes the account selectors.
* *
*/ */
static initialize() { static initialize() {
this.#entryForm = document.getElementById("accounting-entry-form");
this.#formAccountControl = document.getElementById("accounting-entry-form-account-control");
this.#formAccount = document.getElementById("accounting-entry-form-account");
const modals = Array.from(document.getElementsByClassName("accounting-account-selector-modal")); const modals = Array.from(document.getElementsByClassName("accounting-account-selector-modal"));
for (const modal of modals) { for (const modal of modals) {
const selector = new AccountSelector(modal); const selector = new AccountSelector(modal);
@ -234,17 +262,14 @@ class AccountSelector {
* *
*/ */
static #initializeTransactionForm() { static #initializeTransactionForm() {
const entryForm = document.getElementById("accounting-entry-form"); this.#formAccountControl.onclick = () => this.#selectors[this.#entryForm.dataset.entryType].#onOpen();
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
formAccountControl.onclick = () => this.#selectors[entryForm.dataset.entryType].initShow();
} }
/** /**
* Initializes the account selector for the journal entry form. * Initializes the account selector for the journal entry form.
*x *x
*/ */
static initializeJournalEntryForm() { static initializeJournalEntryForm() {
const entryForm = document.getElementById("accounting-entry-form"); this.#formAccountControl.dataset.bsTarget = "#accounting-account-selector-" + this.#entryForm.dataset.entryType + "-modal";
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
formAccountControl.dataset.bsTarget = "#accounting-account-selector-" + entryForm.dataset.entryType + "-modal";
} }
} }

View File

@ -88,10 +88,10 @@ First written: 2023/2/25
<ul id="accounting-currency-{{ currency_index }}-credit-list" class="list-group accounting-entry-list"> <ul id="accounting-currency-{{ currency_index }}-credit-list" class="list-group accounting-entry-list">
{% for entry_form in credit_forms %} {% for entry_form in credit_forms %}
{% with currency_index = currency_index, {% with currency_index = currency_index,
entry_id = entry_form.eid.data,
entry_type = "credit", entry_type = "credit",
entry_index = loop.index, entry_index = loop.index,
only_one_entry_form = debit_forms|length == 1, only_one_entry_form = debit_forms|length == 1,
entry_id = entry_form.eid.data,
account_code_data = entry_form.account_code.data|accounting_default, account_code_data = entry_form.account_code.data|accounting_default,
account_code_error = entry_form.account_code.errors, account_code_error = entry_form.account_code.errors,
account_text = entry_form.account_text, account_text = entry_form.account_text,

View File

@ -33,11 +33,11 @@ from accounting import db
from accounting.locale import lazy_gettext from accounting.locale import lazy_gettext
from accounting.models import Transaction, Account, JournalEntry, \ from accounting.models import Transaction, Account, JournalEntry, \
TransactionCurrency TransactionCurrency
from accounting.transaction.summary_editor import SummaryEditor from accounting.transaction.utils.account_option import AccountOption
from accounting.transaction.utils.summary_editor import SummaryEditor
from accounting.utils.random_id import new_id from accounting.utils.random_id import new_id
from accounting.utils.strip_text import strip_multiline_text from accounting.utils.strip_text import strip_multiline_text
from accounting.utils.user import get_current_user_pk from accounting.utils.user import get_current_user_pk
from .account_option import AccountOption
from .currency import CurrencyForm, IncomeCurrencyForm, ExpenseCurrencyForm, \ from .currency import CurrencyForm, IncomeCurrencyForm, ExpenseCurrencyForm, \
TransferCurrencyForm TransferCurrencyForm
from .journal_entry import JournalEntryForm, DebitEntryForm, CreditEntryForm from .journal_entry import JournalEntryForm, DebitEntryForm, CreditEntryForm
@ -80,8 +80,6 @@ class TransactionForm(FlaskForm):
"""The journal entry collector. The default is the base abstract """The journal entry collector. The default is the base abstract
collector only to provide the correct type. The subclass forms should collector only to provide the correct type. The subclass forms should
provide their own collectors.""" provide their own collectors."""
self.__in_use_account_id: set[int] | None = None
"""The ID of the accounts that are in use."""
def populate_obj(self, obj: Transaction) -> None: def populate_obj(self, obj: Transaction) -> None:
"""Populates the form data into a transaction object. """Populates the form data into a transaction object.

View File

@ -0,0 +1,19 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The utilities for the transaction management.
"""

View File

@ -26,8 +26,8 @@ from flask_wtf import FlaskForm
from accounting.models import Transaction from accounting.models import Transaction
from accounting.template_globals import default_currency_code from accounting.template_globals import default_currency_code
from accounting.utils.txn_types import TransactionType from accounting.utils.txn_types import TransactionType
from .form import TransactionForm, IncomeTransactionForm, \ from accounting.transaction.forms import TransactionForm, \
ExpenseTransactionForm, TransferTransactionForm IncomeTransactionForm, ExpenseTransactionForm, TransferTransactionForm
class TransactionOperator(ABC): class TransactionOperator(ABC):

View File

@ -33,10 +33,10 @@ from accounting.utils.next_uri import inherit_next, or_next
from accounting.utils.permission import has_permission, can_view, can_edit from accounting.utils.permission import has_permission, can_view, can_edit
from accounting.utils.txn_types import TransactionType from accounting.utils.txn_types import TransactionType
from accounting.utils.user import get_current_user_pk from accounting.utils.user import get_current_user_pk
from .form import sort_transactions_in, TransactionReorderForm from .forms import sort_transactions_in, TransactionReorderForm
from .operators import TransactionOperator, TXN_TYPE_TO_OP, get_txn_op
from .template_filters import with_type, to_transfer, format_amount_input, \ from .template_filters import with_type, to_transfer, format_amount_input, \
text2html text2html
from .utils.operators import TransactionOperator, TXN_TYPE_TO_OP, get_txn_op
bp: Blueprint = Blueprint("transaction", __name__) bp: Blueprint = Blueprint("transaction", __name__)
"""The view blueprint for the transaction management.""" """The view blueprint for the transaction management."""

View File

@ -66,7 +66,7 @@ class SummeryEditorTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.transaction.summary_editor import SummaryEditor from accounting.transaction.utils.summary_editor import SummaryEditor
for form in get_form_data(self.csrf_token): for form in get_form_data(self.csrf_token):
add_txn(self.client, form) add_txn(self.client, form)
with self.app.app_context(): with self.app.app_context():