Compare commits
22 Commits
v0.5.0
...
8e5377a416
Author | SHA1 | Date | |
---|---|---|---|
8e5377a416 | |||
4299fd6fbd | |||
1d6a53f7cd | |||
bb2993b0c0 | |||
f6946c1165 | |||
8e219d8006 | |||
53565eb9e6 | |||
965e78d8ad | |||
74b81d3e23 | |||
a0fba6387f | |||
d28bdf2064 | |||
edf0c00e34 | |||
107d161379 | |||
f2c184f769 | |||
b45986ecfc | |||
a2c2452ec5 | |||
5194258b48 | |||
3fe7eb41ac | |||
7fb9e2f0a1 | |||
1d443f7b76 | |||
6ad4fba9cd | |||
3dda6531b5 |
@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
from secrets import randbelow
|
from secrets import randbelow
|
||||||
|
|
||||||
import click
|
import click
|
||||||
@ -93,14 +92,36 @@ def init_accounts_command(username: str) -> None:
|
|||||||
data: list[AccountData] = []
|
data: list[AccountData] = []
|
||||||
for base in bases_to_add:
|
for base in bases_to_add:
|
||||||
l10n: dict[str, str] = {x.locale: x.title for x in base.l10n}
|
l10n: dict[str, str] = {x.locale: x.title for x in base.l10n}
|
||||||
is_offset_needed: bool = True if re.match("^[12]1[34]", base.code) \
|
is_offset_needed: bool = __is_offset_needed(base.code)
|
||||||
else False
|
|
||||||
data.append((get_new_id(), base.code, 1, base.title_l10n,
|
data.append((get_new_id(), base.code, 1, base.title_l10n,
|
||||||
l10n["zh_Hant"], l10n["zh_Hans"], is_offset_needed))
|
l10n["zh_Hant"], l10n["zh_Hans"], is_offset_needed))
|
||||||
__add_accounting_accounts(data, creator_pk)
|
__add_accounting_accounts(data, creator_pk)
|
||||||
click.echo(F"{len(data)} added. Accounting accounts initialized.")
|
click.echo(F"{len(data)} added. Accounting accounts initialized.")
|
||||||
|
|
||||||
|
|
||||||
|
def __is_offset_needed(base_code: str) -> bool:
|
||||||
|
"""Checks that whether entries in the account need offset.
|
||||||
|
|
||||||
|
:param base_code: The code of the base account.
|
||||||
|
:return: True if entries in the account need offset, or False otherwise.
|
||||||
|
"""
|
||||||
|
# Assets
|
||||||
|
if base_code[0] == "1":
|
||||||
|
if base_code[:3] in {"113", "114", "118", "184"}:
|
||||||
|
return True
|
||||||
|
if base_code in {"1411", "1421", "1431", "1441", "1511", "1521",
|
||||||
|
"1581", "1611", "1851", ""}:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
# Liabilities
|
||||||
|
if base_code[0] == "2":
|
||||||
|
if base_code in {"2111", "2114", "2284", "2293"}:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
# Only assets and liabilities need offset
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def __add_accounting_accounts(data: list[AccountData], creator_pk: int)\
|
def __add_accounting_accounts(data: list[AccountData], creator_pk: int)\
|
||||||
-> None:
|
-> None:
|
||||||
"""Adds the accounts.
|
"""Adds the accounts.
|
||||||
|
@ -53,6 +53,21 @@ class BaseAccountAvailable:
|
|||||||
"The base account is not available."))
|
"The base account is not available."))
|
||||||
|
|
||||||
|
|
||||||
|
class NoOffsetNominalAccount:
|
||||||
|
"""The validator to check nominal account is not to be offset."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: BooleanField) -> None:
|
||||||
|
if not field.data:
|
||||||
|
return
|
||||||
|
if not isinstance(form, AccountForm):
|
||||||
|
return
|
||||||
|
if form.base_code.data is None:
|
||||||
|
return
|
||||||
|
if form.base_code.data[0] not in {"1", "2"}:
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"A nominal account does not need offset."))
|
||||||
|
|
||||||
|
|
||||||
class AccountForm(FlaskForm):
|
class AccountForm(FlaskForm):
|
||||||
"""The form to create or edit an account."""
|
"""The form to create or edit an account."""
|
||||||
base_code = StringField(
|
base_code = StringField(
|
||||||
@ -66,7 +81,8 @@ class AccountForm(FlaskForm):
|
|||||||
filters=[strip_text],
|
filters=[strip_text],
|
||||||
validators=[DataRequired(lazy_gettext("Please fill in the title"))])
|
validators=[DataRequired(lazy_gettext("Please fill in the title"))])
|
||||||
"""The title."""
|
"""The title."""
|
||||||
is_offset_needed = BooleanField()
|
is_offset_needed = BooleanField(
|
||||||
|
validators=[NoOffsetNominalAccount()])
|
||||||
"""Whether the the entries of this account need offset."""
|
"""Whether the the entries of this account need offset."""
|
||||||
|
|
||||||
def populate_obj(self, obj: Account) -> None:
|
def populate_obj(self, obj: Account) -> None:
|
||||||
@ -87,7 +103,10 @@ class AccountForm(FlaskForm):
|
|||||||
obj.base_code = self.base_code.data
|
obj.base_code = self.base_code.data
|
||||||
obj.no = count + 1
|
obj.no = count + 1
|
||||||
obj.title = self.title.data
|
obj.title = self.title.data
|
||||||
|
if self.base_code.data[0] in {"1", "2"}:
|
||||||
obj.is_offset_needed = self.is_offset_needed.data
|
obj.is_offset_needed = self.is_offset_needed.data
|
||||||
|
else:
|
||||||
|
obj.is_offset_needed = False
|
||||||
if is_new:
|
if is_new:
|
||||||
current_user_pk: int = get_current_user_pk()
|
current_user_pk: int = get_current_user_pk()
|
||||||
obj.created_by_id = current_user_pk
|
obj.created_by_id = current_user_pk
|
||||||
|
@ -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"),
|
||||||
|
@ -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()
|
||||||
|
@ -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."""
|
||||||
|
@ -43,9 +43,11 @@ function initializeBaseAccountSelector() {
|
|||||||
const base = document.getElementById("accounting-base");
|
const base = document.getElementById("accounting-base");
|
||||||
const baseCode = document.getElementById("accounting-base-code");
|
const baseCode = document.getElementById("accounting-base-code");
|
||||||
const baseContent = document.getElementById("accounting-base-content");
|
const baseContent = document.getElementById("accounting-base-content");
|
||||||
|
const isOffsetNeededControl = document.getElementById("accounting-is-offset-needed-control");
|
||||||
|
const isOffsetNeeded = document.getElementById("accounting-is-offset-needed");
|
||||||
const options = Array.from(document.getElementsByClassName("accounting-base-option"));
|
const options = Array.from(document.getElementsByClassName("accounting-base-option"));
|
||||||
const btnClear = document.getElementById("accounting-btn-clear-base");
|
const btnClear = document.getElementById("accounting-btn-clear-base");
|
||||||
selector.addEventListener("show.bs.modal", () => {
|
base.onclick = () => {
|
||||||
base.classList.add("accounting-not-empty");
|
base.classList.add("accounting-not-empty");
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
option.classList.remove("active");
|
option.classList.remove("active");
|
||||||
@ -54,7 +56,7 @@ function initializeBaseAccountSelector() {
|
|||||||
if (selected !== null) {
|
if (selected !== null) {
|
||||||
selected.classList.add("active");
|
selected.classList.add("active");
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
selector.addEventListener("hidden.bs.modal", () => {
|
selector.addEventListener("hidden.bs.modal", () => {
|
||||||
if (baseCode.value === "") {
|
if (baseCode.value === "") {
|
||||||
base.classList.remove("accounting-not-empty");
|
base.classList.remove("accounting-not-empty");
|
||||||
@ -64,6 +66,14 @@ function initializeBaseAccountSelector() {
|
|||||||
option.onclick = () => {
|
option.onclick = () => {
|
||||||
baseCode.value = option.dataset.code;
|
baseCode.value = option.dataset.code;
|
||||||
baseContent.innerText = option.dataset.content;
|
baseContent.innerText = option.dataset.content;
|
||||||
|
if (["1", "2"].includes(option.dataset.content.substring(0, 1))) {
|
||||||
|
isOffsetNeededControl.classList.remove("d-none");
|
||||||
|
isOffsetNeeded.disabled = false;
|
||||||
|
} else {
|
||||||
|
isOffsetNeededControl.classList.add("d-none");
|
||||||
|
isOffsetNeeded.disabled = true;
|
||||||
|
isOffsetNeeded.checked = false;
|
||||||
|
}
|
||||||
btnClear.classList.add("btn-danger");
|
btnClear.classList.add("btn-danger");
|
||||||
btnClear.classList.remove("btn-secondary")
|
btnClear.classList.remove("btn-secondary")
|
||||||
btnClear.disabled = false;
|
btnClear.disabled = false;
|
||||||
|
@ -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";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ First written: 2023/2/1
|
|||||||
<div id="accounting-title-error" class="invalid-feedback">{% if form.title.errors %}{{ form.title.errors[0] }}{% endif %}</div>
|
<div id="accounting-title-error" class="invalid-feedback">{% if form.title.errors %}{{ form.title.errors[0] }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check form-switch mb-3">
|
<div id="accounting-is-offset-needed-control" class="form-check form-switch mb-3 {% if form.base_code.data[0] not in ["1", "2"] %} d-none {% endif %}">
|
||||||
<input id="accounting-is-offset-needed" class="form-check-input" type="checkbox" name="is_offset_needed" value="1" {% if form.is_offset_needed.data %} checked="checked" {% endif %}>
|
<input id="accounting-is-offset-needed" class="form-check-input" type="checkbox" name="is_offset_needed" value="1" {% if form.is_offset_needed.data %} checked="checked" {% endif %}>
|
||||||
<label class="form-check-label" for="accounting-is-offset-needed">
|
<label class="form-check-label" for="accounting-is-offset-needed">
|
||||||
{{ A_("The entries in the account need offset.") }}
|
{{ A_("The entries in the account need offset.") }}
|
||||||
|
@ -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,
|
||||||
|
22
src/accounting/transaction/forms/__init__.py
Normal file
22
src/accounting/transaction/forms/__init__.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# 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 forms for the transaction management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from .reorder import sort_transactions_in, TransactionReorderForm
|
||||||
|
from .transaction import TransactionForm, IncomeTransactionForm, \
|
||||||
|
ExpenseTransactionForm, TransferTransactionForm
|
207
src/accounting/transaction/forms/currency.py
Normal file
207
src/accounting/transaction/forms/currency.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
# 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 currency sub-forms for the transaction management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask_babel import LazyString
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, ValidationError, FieldList, IntegerField, \
|
||||||
|
BooleanField, FormField
|
||||||
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
|
from accounting import db
|
||||||
|
from accounting.locale import lazy_gettext
|
||||||
|
from accounting.models import Currency
|
||||||
|
from accounting.utils.strip_text import strip_text
|
||||||
|
from .journal_entry import CreditEntryForm, DebitEntryForm
|
||||||
|
|
||||||
|
CURRENCY_REQUIRED: DataRequired = DataRequired(
|
||||||
|
lazy_gettext("Please select the currency."))
|
||||||
|
"""The validator to check if the currency code is empty."""
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencyExists:
|
||||||
|
"""The validator to check if the account exists."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
|
if field.data is None:
|
||||||
|
return
|
||||||
|
if db.session.get(Currency, field.data) is None:
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"The currency does not exist."))
|
||||||
|
|
||||||
|
|
||||||
|
class NeedSomeJournalEntries:
|
||||||
|
"""The validator to check if there is any journal entry sub-form."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: FieldList) -> None:
|
||||||
|
if len(field) == 0:
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"Please add some journal entries."))
|
||||||
|
|
||||||
|
|
||||||
|
class IsBalanced:
|
||||||
|
"""The validator to check that the total amount of the debit and credit
|
||||||
|
entries are equal."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: BooleanField) -> None:
|
||||||
|
if not isinstance(form, TransferCurrencyForm):
|
||||||
|
return
|
||||||
|
if len(form.debit) == 0 or len(form.credit) == 0:
|
||||||
|
return
|
||||||
|
if form.debit_total != form.credit_total:
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"The totals of the debit and credit amounts do not match."))
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencyForm(FlaskForm):
|
||||||
|
"""The form to create or edit a currency in a transaction."""
|
||||||
|
no = IntegerField()
|
||||||
|
"""The order in the transaction."""
|
||||||
|
code = StringField()
|
||||||
|
"""The currency code."""
|
||||||
|
whole_form = BooleanField()
|
||||||
|
"""The pseudo field for the whole form validators."""
|
||||||
|
|
||||||
|
|
||||||
|
class IncomeCurrencyForm(CurrencyForm):
|
||||||
|
"""The form to create or edit a currency in a cash income transaction."""
|
||||||
|
no = IntegerField()
|
||||||
|
"""The order in the transaction."""
|
||||||
|
code = StringField(
|
||||||
|
filters=[strip_text],
|
||||||
|
validators=[CURRENCY_REQUIRED,
|
||||||
|
CurrencyExists()])
|
||||||
|
"""The currency code."""
|
||||||
|
credit = FieldList(FormField(CreditEntryForm),
|
||||||
|
validators=[NeedSomeJournalEntries()])
|
||||||
|
"""The credit entries."""
|
||||||
|
whole_form = BooleanField()
|
||||||
|
"""The pseudo field for the whole form validators."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def credit_total(self) -> Decimal:
|
||||||
|
"""Returns the total amount of the credit journal entries.
|
||||||
|
|
||||||
|
:return: The total amount of the credit journal entries.
|
||||||
|
"""
|
||||||
|
return sum([x.amount.data for x in self.credit
|
||||||
|
if x.amount.data is not None])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def credit_errors(self) -> list[str | LazyString]:
|
||||||
|
"""Returns the credit journal entry errors, without the errors in their
|
||||||
|
sub-forms.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return [x for x in self.credit.errors
|
||||||
|
if isinstance(x, str) or isinstance(x, LazyString)]
|
||||||
|
|
||||||
|
|
||||||
|
class ExpenseCurrencyForm(CurrencyForm):
|
||||||
|
"""The form to create or edit a currency in a cash expense transaction."""
|
||||||
|
no = IntegerField()
|
||||||
|
"""The order in the transaction."""
|
||||||
|
code = StringField(
|
||||||
|
filters=[strip_text],
|
||||||
|
validators=[CURRENCY_REQUIRED,
|
||||||
|
CurrencyExists()])
|
||||||
|
"""The currency code."""
|
||||||
|
debit = FieldList(FormField(DebitEntryForm),
|
||||||
|
validators=[NeedSomeJournalEntries()])
|
||||||
|
"""The debit entries."""
|
||||||
|
whole_form = BooleanField()
|
||||||
|
"""The pseudo field for the whole form validators."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def debit_total(self) -> Decimal:
|
||||||
|
"""Returns the total amount of the debit journal entries.
|
||||||
|
|
||||||
|
:return: The total amount of the debit journal entries.
|
||||||
|
"""
|
||||||
|
return sum([x.amount.data for x in self.debit
|
||||||
|
if x.amount.data is not None])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def debit_errors(self) -> list[str | LazyString]:
|
||||||
|
"""Returns the debit journal entry errors, without the errors in their
|
||||||
|
sub-forms.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return [x for x in self.debit.errors
|
||||||
|
if isinstance(x, str) or isinstance(x, LazyString)]
|
||||||
|
|
||||||
|
|
||||||
|
class TransferCurrencyForm(CurrencyForm):
|
||||||
|
"""The form to create or edit a currency in a transfer transaction."""
|
||||||
|
no = IntegerField()
|
||||||
|
"""The order in the transaction."""
|
||||||
|
code = StringField(
|
||||||
|
filters=[strip_text],
|
||||||
|
validators=[CURRENCY_REQUIRED,
|
||||||
|
CurrencyExists()])
|
||||||
|
"""The currency code."""
|
||||||
|
debit = FieldList(FormField(DebitEntryForm),
|
||||||
|
validators=[NeedSomeJournalEntries()])
|
||||||
|
"""The debit entries."""
|
||||||
|
credit = FieldList(FormField(CreditEntryForm),
|
||||||
|
validators=[NeedSomeJournalEntries()])
|
||||||
|
"""The credit entries."""
|
||||||
|
whole_form = BooleanField(validators=[IsBalanced()])
|
||||||
|
"""The pseudo field for the whole form validators."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def debit_total(self) -> Decimal:
|
||||||
|
"""Returns the total amount of the debit journal entries.
|
||||||
|
|
||||||
|
:return: The total amount of the debit journal entries.
|
||||||
|
"""
|
||||||
|
return sum([x.amount.data for x in self.debit
|
||||||
|
if x.amount.data is not None])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def credit_total(self) -> Decimal:
|
||||||
|
"""Returns the total amount of the credit journal entries.
|
||||||
|
|
||||||
|
:return: The total amount of the credit journal entries.
|
||||||
|
"""
|
||||||
|
return sum([x.amount.data for x in self.credit
|
||||||
|
if x.amount.data is not None])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def debit_errors(self) -> list[str | LazyString]:
|
||||||
|
"""Returns the debit journal entry errors, without the errors in their
|
||||||
|
sub-forms.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return [x for x in self.debit.errors
|
||||||
|
if isinstance(x, str) or isinstance(x, LazyString)]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def credit_errors(self) -> list[str | LazyString]:
|
||||||
|
"""Returns the credit journal entry errors, without the errors in their
|
||||||
|
sub-forms.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return [x for x in self.credit.errors
|
||||||
|
if isinstance(x, str) or isinstance(x, LazyString)]
|
196
src/accounting/transaction/forms/journal_entry.py
Normal file
196
src/accounting/transaction/forms/journal_entry.py
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
# 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 journal entry sub-forms for the transaction management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from flask_babel import LazyString
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, ValidationError, DecimalField, IntegerField
|
||||||
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
|
from accounting.locale import lazy_gettext
|
||||||
|
from accounting.models import Account, JournalEntry
|
||||||
|
from accounting.utils.random_id import new_id
|
||||||
|
from accounting.utils.strip_text import strip_text
|
||||||
|
from accounting.utils.user import get_current_user_pk
|
||||||
|
|
||||||
|
ACCOUNT_REQUIRED: DataRequired = DataRequired(
|
||||||
|
lazy_gettext("Please select the account."))
|
||||||
|
"""The validator to check if the account code is empty."""
|
||||||
|
|
||||||
|
|
||||||
|
class AccountExists:
|
||||||
|
"""The validator to check if the account exists."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
|
if field.data is None:
|
||||||
|
return
|
||||||
|
if Account.find_by_code(field.data) is None:
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"The account does not exist."))
|
||||||
|
|
||||||
|
|
||||||
|
class PositiveAmount:
|
||||||
|
"""The validator to check if the amount is positive."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: DecimalField) -> None:
|
||||||
|
if field.data is None:
|
||||||
|
return
|
||||||
|
if field.data <= 0:
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"Please fill in a positive amount."))
|
||||||
|
|
||||||
|
|
||||||
|
class IsDebitAccount:
|
||||||
|
"""The validator to check if the account is for debit journal entries."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
|
if field.data is None:
|
||||||
|
return
|
||||||
|
if re.match(r"^(?:[1235689]|7[5678])", field.data) \
|
||||||
|
and not field.data.startswith("3351-") \
|
||||||
|
and not field.data.startswith("3353-"):
|
||||||
|
return
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"This account is not for debit entries."))
|
||||||
|
|
||||||
|
|
||||||
|
class IsCreditAccount:
|
||||||
|
"""The validator to check if the account is for credit journal entries."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
|
if field.data is None:
|
||||||
|
return
|
||||||
|
if re.match(r"^(?:[123489]|7[1234])", field.data) \
|
||||||
|
and not field.data.startswith("3351-") \
|
||||||
|
and not field.data.startswith("3353-"):
|
||||||
|
return
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"This account is not for credit entries."))
|
||||||
|
|
||||||
|
|
||||||
|
class JournalEntryForm(FlaskForm):
|
||||||
|
"""The base form to create or edit a journal entry."""
|
||||||
|
eid = IntegerField()
|
||||||
|
"""The existing journal entry ID."""
|
||||||
|
no = IntegerField()
|
||||||
|
"""The order in the currency."""
|
||||||
|
account_code = StringField()
|
||||||
|
"""The account code."""
|
||||||
|
amount = DecimalField()
|
||||||
|
"""The amount."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_text(self) -> str:
|
||||||
|
"""Returns the text representation of the account.
|
||||||
|
|
||||||
|
:return: The text representation of the account.
|
||||||
|
"""
|
||||||
|
if self.account_code.data is None:
|
||||||
|
return ""
|
||||||
|
account: Account | None = Account.find_by_code(self.account_code.data)
|
||||||
|
if account is None:
|
||||||
|
return ""
|
||||||
|
return str(account)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_errors(self) -> list[str | LazyString]:
|
||||||
|
"""Returns all the errors of the form.
|
||||||
|
|
||||||
|
:return: All the errors of the form.
|
||||||
|
"""
|
||||||
|
all_errors: list[str | LazyString] = []
|
||||||
|
for key in self.errors:
|
||||||
|
if key != "csrf_token":
|
||||||
|
all_errors.extend(self.errors[key])
|
||||||
|
return all_errors
|
||||||
|
|
||||||
|
|
||||||
|
class DebitEntryForm(JournalEntryForm):
|
||||||
|
"""The form to create or edit a debit journal entry."""
|
||||||
|
eid = IntegerField()
|
||||||
|
"""The existing journal entry ID."""
|
||||||
|
no = IntegerField()
|
||||||
|
"""The order in the currency."""
|
||||||
|
account_code = StringField(
|
||||||
|
filters=[strip_text],
|
||||||
|
validators=[ACCOUNT_REQUIRED,
|
||||||
|
AccountExists(),
|
||||||
|
IsDebitAccount()])
|
||||||
|
"""The account code."""
|
||||||
|
summary = StringField(filters=[strip_text])
|
||||||
|
"""The summary."""
|
||||||
|
amount = DecimalField(validators=[PositiveAmount()])
|
||||||
|
"""The amount."""
|
||||||
|
|
||||||
|
def populate_obj(self, obj: JournalEntry) -> None:
|
||||||
|
"""Populates the form data into a journal entry object.
|
||||||
|
|
||||||
|
:param obj: The journal entry object.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
is_new: bool = obj.id is None
|
||||||
|
if is_new:
|
||||||
|
obj.id = new_id(JournalEntry)
|
||||||
|
obj.account_id = Account.find_by_code(self.account_code.data).id
|
||||||
|
obj.summary = self.summary.data
|
||||||
|
obj.is_debit = True
|
||||||
|
obj.amount = self.amount.data
|
||||||
|
if is_new:
|
||||||
|
current_user_pk: int = get_current_user_pk()
|
||||||
|
obj.created_by_id = current_user_pk
|
||||||
|
obj.updated_by_id = current_user_pk
|
||||||
|
|
||||||
|
|
||||||
|
class CreditEntryForm(JournalEntryForm):
|
||||||
|
"""The form to create or edit a credit journal entry."""
|
||||||
|
eid = IntegerField()
|
||||||
|
"""The existing journal entry ID."""
|
||||||
|
no = IntegerField()
|
||||||
|
"""The order in the currency."""
|
||||||
|
account_code = StringField(
|
||||||
|
filters=[strip_text],
|
||||||
|
validators=[ACCOUNT_REQUIRED,
|
||||||
|
AccountExists(),
|
||||||
|
IsCreditAccount()])
|
||||||
|
"""The account code."""
|
||||||
|
summary = StringField(filters=[strip_text])
|
||||||
|
"""The summary."""
|
||||||
|
amount = DecimalField(validators=[PositiveAmount()])
|
||||||
|
"""The amount."""
|
||||||
|
|
||||||
|
def populate_obj(self, obj: JournalEntry) -> None:
|
||||||
|
"""Populates the form data into a journal entry object.
|
||||||
|
|
||||||
|
:param obj: The journal entry object.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
is_new: bool = obj.id is None
|
||||||
|
if is_new:
|
||||||
|
obj.id = new_id(JournalEntry)
|
||||||
|
obj.account_id = Account.find_by_code(self.account_code.data).id
|
||||||
|
obj.summary = self.summary.data
|
||||||
|
obj.is_debit = False
|
||||||
|
obj.amount = self.amount.data
|
||||||
|
if is_new:
|
||||||
|
current_user_pk: int = get_current_user_pk()
|
||||||
|
obj.created_by_id = current_user_pk
|
||||||
|
obj.updated_by_id = current_user_pk
|
91
src/accounting/transaction/forms/reorder.py
Normal file
91
src/accounting/transaction/forms/reorder.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# 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 reorder forms for the transaction management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from flask import request
|
||||||
|
|
||||||
|
from accounting import db
|
||||||
|
from accounting.models import Transaction
|
||||||
|
|
||||||
|
|
||||||
|
def sort_transactions_in(txn_date: date, exclude: int) -> None:
|
||||||
|
"""Sorts the transactions under a date after changing the date or deleting
|
||||||
|
a transaction.
|
||||||
|
|
||||||
|
:param txn_date: The date of the transaction.
|
||||||
|
:param exclude: The transaction ID to exclude.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
transactions: list[Transaction] = Transaction.query\
|
||||||
|
.filter(Transaction.date == txn_date,
|
||||||
|
Transaction.id != exclude)\
|
||||||
|
.order_by(Transaction.no).all()
|
||||||
|
for i in range(len(transactions)):
|
||||||
|
if transactions[i].no != i + 1:
|
||||||
|
transactions[i].no = i + 1
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionReorderForm:
|
||||||
|
"""The form to reorder the transactions."""
|
||||||
|
|
||||||
|
def __init__(self, txn_date: date):
|
||||||
|
"""Constructs the form to reorder the transactions in a day.
|
||||||
|
|
||||||
|
:param txn_date: The date.
|
||||||
|
"""
|
||||||
|
self.date: date = txn_date
|
||||||
|
self.is_modified: bool = False
|
||||||
|
|
||||||
|
def save_order(self) -> None:
|
||||||
|
"""Saves the order of the account.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
transactions: list[Transaction] = Transaction.query\
|
||||||
|
.filter(Transaction.date == self.date).all()
|
||||||
|
|
||||||
|
# Collects the specified order.
|
||||||
|
orders: dict[Transaction, int] = {}
|
||||||
|
for txn in transactions:
|
||||||
|
if f"{txn.id}-no" in request.form:
|
||||||
|
try:
|
||||||
|
orders[txn] = int(request.form[f"{txn.id}-no"])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Missing and invalid orders are appended to the end.
|
||||||
|
missing: list[Transaction] \
|
||||||
|
= [x for x in transactions if x not in orders]
|
||||||
|
if len(missing) > 0:
|
||||||
|
next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
|
||||||
|
for txn in missing:
|
||||||
|
orders[txn] = next_no
|
||||||
|
|
||||||
|
# Sort by the specified order first, and their original order.
|
||||||
|
transactions.sort(key=lambda x: (orders[x], x.no))
|
||||||
|
|
||||||
|
# Update the orders.
|
||||||
|
with db.session.no_autoflush:
|
||||||
|
for i in range(len(transactions)):
|
||||||
|
if transactions[i].no != i + 1:
|
||||||
|
transactions[i].no = i + 1
|
||||||
|
self.is_modified = True
|
@ -14,38 +14,35 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The forms for the transaction management.
|
"""The transaction forms for the transaction management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
|
||||||
import typing as t
|
import typing as t
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import date
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import request
|
|
||||||
from flask_babel import LazyString
|
from flask_babel import LazyString
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import DateField, StringField, FieldList, FormField, \
|
from wtforms import DateField, FieldList, FormField, \
|
||||||
IntegerField, TextAreaField, DecimalField, BooleanField
|
TextAreaField
|
||||||
from wtforms.validators import DataRequired, ValidationError
|
from wtforms.validators import DataRequired, ValidationError
|
||||||
|
|
||||||
from accounting import db
|
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, Currency
|
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_text, 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 .currency import CurrencyForm, IncomeCurrencyForm, ExpenseCurrencyForm, \
|
||||||
|
TransferCurrencyForm
|
||||||
|
from .journal_entry import JournalEntryForm, DebitEntryForm, CreditEntryForm
|
||||||
|
from .reorder import sort_transactions_in
|
||||||
|
|
||||||
MISSING_CURRENCY: LazyString = lazy_gettext("Please select the currency.")
|
|
||||||
"""The error message when the currency code is empty."""
|
|
||||||
MISSING_ACCOUNT: LazyString = lazy_gettext("Please select the account.")
|
|
||||||
"""The error message when the account code is empty."""
|
|
||||||
DATE_REQUIRED: DataRequired = DataRequired(
|
DATE_REQUIRED: DataRequired = DataRequired(
|
||||||
lazy_gettext("Please fill in the date."))
|
lazy_gettext("Please fill in the date."))
|
||||||
"""The validator to check if the date is empty."""
|
"""The validator to check if the date is empty."""
|
||||||
@ -61,223 +58,6 @@ class NeedSomeCurrencies:
|
|||||||
"Please add some currencies."))
|
"Please add some currencies."))
|
||||||
|
|
||||||
|
|
||||||
class CurrencyExists:
|
|
||||||
"""The validator to check if the account exists."""
|
|
||||||
|
|
||||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
|
||||||
if field.data is None:
|
|
||||||
return
|
|
||||||
if db.session.get(Currency, field.data) is None:
|
|
||||||
raise ValidationError(lazy_gettext(
|
|
||||||
"The currency does not exist."))
|
|
||||||
|
|
||||||
|
|
||||||
class NeedSomeJournalEntries:
|
|
||||||
"""The validator to check if there is any journal entry sub-form."""
|
|
||||||
|
|
||||||
def __call__(self, form: TransferCurrencyForm, field: FieldList) \
|
|
||||||
-> None:
|
|
||||||
if len(field) == 0:
|
|
||||||
raise ValidationError(lazy_gettext(
|
|
||||||
"Please add some journal entries."))
|
|
||||||
|
|
||||||
|
|
||||||
class AccountExists:
|
|
||||||
"""The validator to check if the account exists."""
|
|
||||||
|
|
||||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
|
||||||
if field.data is None:
|
|
||||||
return
|
|
||||||
if Account.find_by_code(field.data) is None:
|
|
||||||
raise ValidationError(lazy_gettext(
|
|
||||||
"The account does not exist."))
|
|
||||||
|
|
||||||
|
|
||||||
class PositiveAmount:
|
|
||||||
"""The validator to check if the amount is positive."""
|
|
||||||
|
|
||||||
def __call__(self, form: FlaskForm, field: DecimalField) -> None:
|
|
||||||
if field.data is None:
|
|
||||||
return
|
|
||||||
if field.data <= 0:
|
|
||||||
raise ValidationError(lazy_gettext(
|
|
||||||
"Please fill in a positive amount."))
|
|
||||||
|
|
||||||
|
|
||||||
class IsDebitAccount:
|
|
||||||
"""The validator to check if the account is for debit journal entries."""
|
|
||||||
|
|
||||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
|
||||||
if field.data is None:
|
|
||||||
return
|
|
||||||
if re.match(r"^(?:[1235689]|7[5678])", field.data) \
|
|
||||||
and not field.data.startswith("3351-") \
|
|
||||||
and not field.data.startswith("3353-"):
|
|
||||||
return
|
|
||||||
raise ValidationError(lazy_gettext(
|
|
||||||
"This account is not for debit entries."))
|
|
||||||
|
|
||||||
|
|
||||||
class AccountOption:
|
|
||||||
"""An account option."""
|
|
||||||
|
|
||||||
def __init__(self, account: Account):
|
|
||||||
"""Constructs an account option.
|
|
||||||
|
|
||||||
:param account: The account.
|
|
||||||
"""
|
|
||||||
self.id: str = account.id
|
|
||||||
"""The account ID."""
|
|
||||||
self.code: str = account.code
|
|
||||||
"""The account code."""
|
|
||||||
self.query_values: list[str] = account.query_values
|
|
||||||
"""The values to be queried."""
|
|
||||||
self.__str: str = str(account)
|
|
||||||
"""The string representation of the account option."""
|
|
||||||
self.is_in_use: bool = False
|
|
||||||
"""True if this account is in use, or False otherwise."""
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""Returns the string representation of the account option.
|
|
||||||
|
|
||||||
:return: The string representation of the account option.
|
|
||||||
"""
|
|
||||||
return self.__str
|
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryForm(FlaskForm):
|
|
||||||
"""The base form to create or edit a journal entry."""
|
|
||||||
eid = IntegerField()
|
|
||||||
"""The existing journal entry ID."""
|
|
||||||
no = IntegerField()
|
|
||||||
"""The order in the currency."""
|
|
||||||
account_code = StringField()
|
|
||||||
"""The account code."""
|
|
||||||
amount = DecimalField()
|
|
||||||
"""The amount."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def account_text(self) -> str:
|
|
||||||
"""Returns the text representation of the account.
|
|
||||||
|
|
||||||
:return: The text representation of the account.
|
|
||||||
"""
|
|
||||||
if self.account_code.data is None:
|
|
||||||
return ""
|
|
||||||
account: Account | None = Account.find_by_code(self.account_code.data)
|
|
||||||
if account is None:
|
|
||||||
return ""
|
|
||||||
return str(account)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def all_errors(self) -> list[str | LazyString]:
|
|
||||||
"""Returns all the errors of the form.
|
|
||||||
|
|
||||||
:return: All the errors of the form.
|
|
||||||
"""
|
|
||||||
all_errors: list[str | LazyString] = []
|
|
||||||
for key in self.errors:
|
|
||||||
if key != "csrf_token":
|
|
||||||
all_errors.extend(self.errors[key])
|
|
||||||
return all_errors
|
|
||||||
|
|
||||||
|
|
||||||
class DebitEntryForm(JournalEntryForm):
|
|
||||||
"""The form to create or edit a debit journal entry."""
|
|
||||||
eid = IntegerField()
|
|
||||||
"""The existing journal entry ID."""
|
|
||||||
no = IntegerField()
|
|
||||||
"""The order in the currency."""
|
|
||||||
account_code = StringField(
|
|
||||||
filters=[strip_text],
|
|
||||||
validators=[DataRequired(MISSING_ACCOUNT),
|
|
||||||
AccountExists(),
|
|
||||||
IsDebitAccount()])
|
|
||||||
"""The account code."""
|
|
||||||
summary = StringField(filters=[strip_text])
|
|
||||||
"""The summary."""
|
|
||||||
amount = DecimalField(validators=[PositiveAmount()])
|
|
||||||
"""The amount."""
|
|
||||||
|
|
||||||
def populate_obj(self, obj: JournalEntry) -> None:
|
|
||||||
"""Populates the form data into a journal entry object.
|
|
||||||
|
|
||||||
:param obj: The journal entry object.
|
|
||||||
:return: None.
|
|
||||||
"""
|
|
||||||
is_new: bool = obj.id is None
|
|
||||||
if is_new:
|
|
||||||
obj.id = new_id(JournalEntry)
|
|
||||||
obj.account_id = Account.find_by_code(self.account_code.data).id
|
|
||||||
obj.summary = self.summary.data
|
|
||||||
obj.is_debit = True
|
|
||||||
obj.amount = self.amount.data
|
|
||||||
if is_new:
|
|
||||||
current_user_pk: int = get_current_user_pk()
|
|
||||||
obj.created_by_id = current_user_pk
|
|
||||||
obj.updated_by_id = current_user_pk
|
|
||||||
|
|
||||||
|
|
||||||
class IsCreditAccount:
|
|
||||||
"""The validator to check if the account is for credit journal entries."""
|
|
||||||
|
|
||||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
|
||||||
if field.data is None:
|
|
||||||
return
|
|
||||||
if re.match(r"^(?:[123489]|7[1234])", field.data) \
|
|
||||||
and not field.data.startswith("3351-") \
|
|
||||||
and not field.data.startswith("3353-"):
|
|
||||||
return
|
|
||||||
raise ValidationError(lazy_gettext(
|
|
||||||
"This account is not for credit entries."))
|
|
||||||
|
|
||||||
|
|
||||||
class CreditEntryForm(JournalEntryForm):
|
|
||||||
"""The form to create or edit a credit journal entry."""
|
|
||||||
eid = IntegerField()
|
|
||||||
"""The existing journal entry ID."""
|
|
||||||
no = IntegerField()
|
|
||||||
"""The order in the currency."""
|
|
||||||
account_code = StringField(
|
|
||||||
filters=[strip_text],
|
|
||||||
validators=[DataRequired(MISSING_ACCOUNT),
|
|
||||||
AccountExists(),
|
|
||||||
IsCreditAccount()])
|
|
||||||
"""The account code."""
|
|
||||||
summary = StringField(filters=[strip_text])
|
|
||||||
"""The summary."""
|
|
||||||
amount = DecimalField(validators=[PositiveAmount()])
|
|
||||||
"""The amount."""
|
|
||||||
|
|
||||||
def populate_obj(self, obj: JournalEntry) -> None:
|
|
||||||
"""Populates the form data into a journal entry object.
|
|
||||||
|
|
||||||
:param obj: The journal entry object.
|
|
||||||
:return: None.
|
|
||||||
"""
|
|
||||||
is_new: bool = obj.id is None
|
|
||||||
if is_new:
|
|
||||||
obj.id = new_id(JournalEntry)
|
|
||||||
obj.account_id = Account.find_by_code(self.account_code.data).id
|
|
||||||
obj.summary = self.summary.data
|
|
||||||
obj.is_debit = False
|
|
||||||
obj.amount = self.amount.data
|
|
||||||
if is_new:
|
|
||||||
current_user_pk: int = get_current_user_pk()
|
|
||||||
obj.created_by_id = current_user_pk
|
|
||||||
obj.updated_by_id = current_user_pk
|
|
||||||
|
|
||||||
|
|
||||||
class CurrencyForm(FlaskForm):
|
|
||||||
"""The form to create or edit a currency in a transaction."""
|
|
||||||
no = IntegerField()
|
|
||||||
"""The order in the transaction."""
|
|
||||||
code = StringField()
|
|
||||||
"""The currency code."""
|
|
||||||
whole_form = BooleanField()
|
|
||||||
"""The pseudo field for the whole form validators."""
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionForm(FlaskForm):
|
class TransactionForm(FlaskForm):
|
||||||
"""The base form to create or edit a transaction."""
|
"""The base form to create or edit a transaction."""
|
||||||
date = DateField()
|
date = DateField()
|
||||||
@ -300,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.
|
||||||
@ -538,41 +316,6 @@ class JournalEntryCollector(t.Generic[T], ABC):
|
|||||||
ord_by_form.get(x)))
|
ord_by_form.get(x)))
|
||||||
|
|
||||||
|
|
||||||
class IncomeCurrencyForm(CurrencyForm):
|
|
||||||
"""The form to create or edit a currency in a cash income transaction."""
|
|
||||||
no = IntegerField()
|
|
||||||
"""The order in the transaction."""
|
|
||||||
code = StringField(
|
|
||||||
filters=[strip_text],
|
|
||||||
validators=[DataRequired(MISSING_CURRENCY),
|
|
||||||
CurrencyExists()])
|
|
||||||
"""The currency code."""
|
|
||||||
credit = FieldList(FormField(CreditEntryForm),
|
|
||||||
validators=[NeedSomeJournalEntries()])
|
|
||||||
"""The credit entries."""
|
|
||||||
whole_form = BooleanField()
|
|
||||||
"""The pseudo field for the whole form validators."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def credit_total(self) -> Decimal:
|
|
||||||
"""Returns the total amount of the credit journal entries.
|
|
||||||
|
|
||||||
:return: The total amount of the credit journal entries.
|
|
||||||
"""
|
|
||||||
return sum([x.amount.data for x in self.credit
|
|
||||||
if x.amount.data is not None])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def credit_errors(self) -> list[str | LazyString]:
|
|
||||||
"""Returns the credit journal entry errors, without the errors in their
|
|
||||||
sub-forms.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return [x for x in self.credit.errors
|
|
||||||
if isinstance(x, str) or isinstance(x, LazyString)]
|
|
||||||
|
|
||||||
|
|
||||||
class IncomeTransactionForm(TransactionForm):
|
class IncomeTransactionForm(TransactionForm):
|
||||||
"""The form to create or edit a cash income transaction."""
|
"""The form to create or edit a cash income transaction."""
|
||||||
date = DateField(validators=[DATE_REQUIRED])
|
date = DateField(validators=[DATE_REQUIRED])
|
||||||
@ -611,41 +354,6 @@ class IncomeTransactionForm(TransactionForm):
|
|||||||
self.collector = Collector
|
self.collector = Collector
|
||||||
|
|
||||||
|
|
||||||
class ExpenseCurrencyForm(CurrencyForm):
|
|
||||||
"""The form to create or edit a currency in a cash expense transaction."""
|
|
||||||
no = IntegerField()
|
|
||||||
"""The order in the transaction."""
|
|
||||||
code = StringField(
|
|
||||||
filters=[strip_text],
|
|
||||||
validators=[DataRequired(MISSING_CURRENCY),
|
|
||||||
CurrencyExists()])
|
|
||||||
"""The currency code."""
|
|
||||||
debit = FieldList(FormField(DebitEntryForm),
|
|
||||||
validators=[NeedSomeJournalEntries()])
|
|
||||||
"""The debit entries."""
|
|
||||||
whole_form = BooleanField()
|
|
||||||
"""The pseudo field for the whole form validators."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def debit_total(self) -> Decimal:
|
|
||||||
"""Returns the total amount of the debit journal entries.
|
|
||||||
|
|
||||||
:return: The total amount of the debit journal entries.
|
|
||||||
"""
|
|
||||||
return sum([x.amount.data for x in self.debit
|
|
||||||
if x.amount.data is not None])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def debit_errors(self) -> list[str | LazyString]:
|
|
||||||
"""Returns the debit journal entry errors, without the errors in their
|
|
||||||
sub-forms.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return [x for x in self.debit.errors
|
|
||||||
if isinstance(x, str) or isinstance(x, LazyString)]
|
|
||||||
|
|
||||||
|
|
||||||
class ExpenseTransactionForm(TransactionForm):
|
class ExpenseTransactionForm(TransactionForm):
|
||||||
"""The form to create or edit a cash expense transaction."""
|
"""The form to create or edit a cash expense transaction."""
|
||||||
date = DateField(validators=[DATE_REQUIRED])
|
date = DateField(validators=[DATE_REQUIRED])
|
||||||
@ -685,76 +393,6 @@ class ExpenseTransactionForm(TransactionForm):
|
|||||||
self.collector = Collector
|
self.collector = Collector
|
||||||
|
|
||||||
|
|
||||||
class TransferCurrencyForm(CurrencyForm):
|
|
||||||
"""The form to create or edit a currency in a transfer transaction."""
|
|
||||||
|
|
||||||
class IsBalanced:
|
|
||||||
"""The validator to check that the total amount of the debit and credit
|
|
||||||
entries are equal."""
|
|
||||||
def __call__(self, form: TransferCurrencyForm, field: BooleanField)\
|
|
||||||
-> None:
|
|
||||||
if len(form.debit) == 0 or len(form.credit) == 0:
|
|
||||||
return
|
|
||||||
if form.debit_total != form.credit_total:
|
|
||||||
raise ValidationError(lazy_gettext(
|
|
||||||
"The totals of the debit and credit amounts do not"
|
|
||||||
" match."))
|
|
||||||
|
|
||||||
no = IntegerField()
|
|
||||||
"""The order in the transaction."""
|
|
||||||
code = StringField(
|
|
||||||
filters=[strip_text],
|
|
||||||
validators=[DataRequired(MISSING_CURRENCY),
|
|
||||||
CurrencyExists()])
|
|
||||||
"""The currency code."""
|
|
||||||
debit = FieldList(FormField(DebitEntryForm),
|
|
||||||
validators=[NeedSomeJournalEntries()])
|
|
||||||
"""The debit entries."""
|
|
||||||
credit = FieldList(FormField(CreditEntryForm),
|
|
||||||
validators=[NeedSomeJournalEntries()])
|
|
||||||
"""The credit entries."""
|
|
||||||
whole_form = BooleanField(validators=[IsBalanced()])
|
|
||||||
"""The pseudo field for the whole form validators."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def debit_total(self) -> Decimal:
|
|
||||||
"""Returns the total amount of the debit journal entries.
|
|
||||||
|
|
||||||
:return: The total amount of the debit journal entries.
|
|
||||||
"""
|
|
||||||
return sum([x.amount.data for x in self.debit
|
|
||||||
if x.amount.data is not None])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def credit_total(self) -> Decimal:
|
|
||||||
"""Returns the total amount of the credit journal entries.
|
|
||||||
|
|
||||||
:return: The total amount of the credit journal entries.
|
|
||||||
"""
|
|
||||||
return sum([x.amount.data for x in self.credit
|
|
||||||
if x.amount.data is not None])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def debit_errors(self) -> list[str | LazyString]:
|
|
||||||
"""Returns the debit journal entry errors, without the errors in their
|
|
||||||
sub-forms.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return [x for x in self.debit.errors
|
|
||||||
if isinstance(x, str) or isinstance(x, LazyString)]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def credit_errors(self) -> list[str | LazyString]:
|
|
||||||
"""Returns the credit journal entry errors, without the errors in their
|
|
||||||
sub-forms.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return [x for x in self.credit.errors
|
|
||||||
if isinstance(x, str) or isinstance(x, LazyString)]
|
|
||||||
|
|
||||||
|
|
||||||
class TransferTransactionForm(TransactionForm):
|
class TransferTransactionForm(TransactionForm):
|
||||||
"""The form to create or edit a transfer transaction."""
|
"""The form to create or edit a transfer transaction."""
|
||||||
date = DateField(validators=[DATE_REQUIRED])
|
date = DateField(validators=[DATE_REQUIRED])
|
||||||
@ -795,67 +433,3 @@ class TransferTransactionForm(TransactionForm):
|
|||||||
self._credit_no = self._credit_no + 1
|
self._credit_no = self._credit_no + 1
|
||||||
|
|
||||||
self.collector = Collector
|
self.collector = Collector
|
||||||
|
|
||||||
|
|
||||||
def sort_transactions_in(txn_date: date, exclude: int) -> None:
|
|
||||||
"""Sorts the transactions under a date after changing the date or deleting
|
|
||||||
a transaction.
|
|
||||||
|
|
||||||
:param txn_date: The date of the transaction.
|
|
||||||
:param exclude: The transaction ID to exclude.
|
|
||||||
:return: None.
|
|
||||||
"""
|
|
||||||
transactions: list[Transaction] = Transaction.query\
|
|
||||||
.filter(Transaction.date == txn_date,
|
|
||||||
Transaction.id != exclude)\
|
|
||||||
.order_by(Transaction.no).all()
|
|
||||||
for i in range(len(transactions)):
|
|
||||||
if transactions[i].no != i + 1:
|
|
||||||
transactions[i].no = i + 1
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionReorderForm:
|
|
||||||
"""The form to reorder the transactions."""
|
|
||||||
|
|
||||||
def __init__(self, txn_date: date):
|
|
||||||
"""Constructs the form to reorder the transactions in a day.
|
|
||||||
|
|
||||||
:param txn_date: The date.
|
|
||||||
"""
|
|
||||||
self.date: date = txn_date
|
|
||||||
self.is_modified: bool = False
|
|
||||||
|
|
||||||
def save_order(self) -> None:
|
|
||||||
"""Saves the order of the account.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
transactions: list[Transaction] = Transaction.query\
|
|
||||||
.filter(Transaction.date == self.date).all()
|
|
||||||
|
|
||||||
# Collects the specified order.
|
|
||||||
orders: dict[Transaction, int] = {}
|
|
||||||
for txn in transactions:
|
|
||||||
if f"{txn.id}-no" in request.form:
|
|
||||||
try:
|
|
||||||
orders[txn] = int(request.form[f"{txn.id}-no"])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Missing and invalid orders are appended to the end.
|
|
||||||
missing: list[Transaction] \
|
|
||||||
= [x for x in transactions if x not in orders]
|
|
||||||
if len(missing) > 0:
|
|
||||||
next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
|
|
||||||
for txn in missing:
|
|
||||||
orders[txn] = next_no
|
|
||||||
|
|
||||||
# Sort by the specified order first, and their original order.
|
|
||||||
transactions.sort(key=lambda x: (orders[x], x.no))
|
|
||||||
|
|
||||||
# Update the orders.
|
|
||||||
with db.session.no_autoflush:
|
|
||||||
for i in range(len(transactions)):
|
|
||||||
if transactions[i].no != i + 1:
|
|
||||||
transactions[i].no = i + 1
|
|
||||||
self.is_modified = True
|
|
19
src/accounting/transaction/utils/__init__.py
Normal file
19
src/accounting/transaction/utils/__init__.py
Normal 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.
|
||||||
|
|
||||||
|
"""
|
49
src/accounting/transaction/utils/account_option.py
Normal file
49
src/accounting/transaction/utils/account_option.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# 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 account option for the transaction management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from accounting.models import Account
|
||||||
|
|
||||||
|
|
||||||
|
class AccountOption:
|
||||||
|
"""An account option."""
|
||||||
|
|
||||||
|
def __init__(self, account: Account):
|
||||||
|
"""Constructs an account option.
|
||||||
|
|
||||||
|
:param account: The account.
|
||||||
|
"""
|
||||||
|
self.id: str = account.id
|
||||||
|
"""The account ID."""
|
||||||
|
self.code: str = account.code
|
||||||
|
"""The account code."""
|
||||||
|
self.query_values: list[str] = account.query_values
|
||||||
|
"""The values to be queried."""
|
||||||
|
self.__str: str = str(account)
|
||||||
|
"""The string representation of the account option."""
|
||||||
|
self.is_in_use: bool = False
|
||||||
|
"""True if this account is in use, or False otherwise."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Returns the string representation of the account option.
|
||||||
|
|
||||||
|
:return: The string representation of the account option.
|
||||||
|
"""
|
||||||
|
return self.__str
|
@ -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 .forms import TransactionForm, IncomeTransactionForm, \
|
from accounting.transaction.forms import TransactionForm, \
|
||||||
ExpenseTransactionForm, TransferTransactionForm
|
IncomeTransactionForm, ExpenseTransactionForm, TransferTransactionForm
|
||||||
|
|
||||||
|
|
||||||
class TransactionOperator(ABC):
|
class TransactionOperator(ABC):
|
@ -34,9 +34,9 @@ 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 .forms 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."""
|
||||||
|
@ -372,6 +372,15 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
|
# A nominal account that needs offset
|
||||||
|
response = self.client.post(store_uri,
|
||||||
|
data={"csrf_token": self.csrf_token,
|
||||||
|
"base_code": "6172",
|
||||||
|
"title": stock.title,
|
||||||
|
"is_offset_needed": "yes"})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Success, with spaces to be stripped
|
# Success, with spaces to be stripped
|
||||||
response = self.client.post(store_uri,
|
response = self.client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.csrf_token,
|
||||||
@ -470,6 +479,15 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
|
# A nominal account that needs offset
|
||||||
|
response = self.client.post(update_uri,
|
||||||
|
data={"csrf_token": self.csrf_token,
|
||||||
|
"base_code": "6172",
|
||||||
|
"title": stock.title,
|
||||||
|
"is_offset_needed": "yes"})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Change the base account
|
# Change the base account
|
||||||
response = self.client.post(update_uri,
|
response = self.client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.csrf_token,
|
||||||
|
@ -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():
|
||||||
@ -79,13 +79,13 @@ class SummeryEditorTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(editor.debit.general.tags[0].accounts[0].code,
|
self.assertEqual(editor.debit.general.tags[0].accounts[0].code,
|
||||||
Accounts.MEAL)
|
Accounts.MEAL)
|
||||||
self.assertEqual(editor.debit.general.tags[0].accounts[1].code,
|
self.assertEqual(editor.debit.general.tags[0].accounts[1].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PETTY_CASH)
|
||||||
self.assertEqual(editor.debit.general.tags[1].name, "Dinner")
|
self.assertEqual(editor.debit.general.tags[1].name, "Dinner")
|
||||||
self.assertEqual(len(editor.debit.general.tags[1].accounts), 2)
|
self.assertEqual(len(editor.debit.general.tags[1].accounts), 2)
|
||||||
self.assertEqual(editor.debit.general.tags[1].accounts[0].code,
|
self.assertEqual(editor.debit.general.tags[1].accounts[0].code,
|
||||||
Accounts.MEAL)
|
Accounts.MEAL)
|
||||||
self.assertEqual(editor.debit.general.tags[1].accounts[1].code,
|
self.assertEqual(editor.debit.general.tags[1].accounts[1].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PETTY_CASH)
|
||||||
|
|
||||||
# Debit-Travel
|
# Debit-Travel
|
||||||
self.assertEqual(len(editor.debit.travel.tags), 3)
|
self.assertEqual(len(editor.debit.travel.tags), 3)
|
||||||
@ -118,7 +118,7 @@ class SummeryEditorTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(editor.credit.general.tags[0].name, "Lunch")
|
self.assertEqual(editor.credit.general.tags[0].name, "Lunch")
|
||||||
self.assertEqual(len(editor.credit.general.tags[0].accounts), 3)
|
self.assertEqual(len(editor.credit.general.tags[0].accounts), 3)
|
||||||
self.assertEqual(editor.credit.general.tags[0].accounts[0].code,
|
self.assertEqual(editor.credit.general.tags[0].accounts[0].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PETTY_CASH)
|
||||||
self.assertEqual(editor.credit.general.tags[0].accounts[1].code,
|
self.assertEqual(editor.credit.general.tags[0].accounts[1].code,
|
||||||
Accounts.BANK)
|
Accounts.BANK)
|
||||||
self.assertEqual(editor.credit.general.tags[0].accounts[2].code,
|
self.assertEqual(editor.credit.general.tags[0].accounts[2].code,
|
||||||
@ -128,20 +128,20 @@ class SummeryEditorTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(editor.credit.general.tags[1].accounts[0].code,
|
self.assertEqual(editor.credit.general.tags[1].accounts[0].code,
|
||||||
Accounts.BANK)
|
Accounts.BANK)
|
||||||
self.assertEqual(editor.credit.general.tags[1].accounts[1].code,
|
self.assertEqual(editor.credit.general.tags[1].accounts[1].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PETTY_CASH)
|
||||||
|
|
||||||
# Credit-Travel
|
# Credit-Travel
|
||||||
self.assertEqual(len(editor.credit.travel.tags), 2)
|
self.assertEqual(len(editor.credit.travel.tags), 2)
|
||||||
self.assertEqual(editor.credit.travel.tags[0].name, "Bike")
|
self.assertEqual(editor.credit.travel.tags[0].name, "Bike")
|
||||||
self.assertEqual(len(editor.credit.travel.tags[0].accounts), 2)
|
self.assertEqual(len(editor.credit.travel.tags[0].accounts), 2)
|
||||||
self.assertEqual(editor.credit.travel.tags[0].accounts[0].code,
|
self.assertEqual(editor.credit.travel.tags[0].accounts[0].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PETTY_CASH)
|
||||||
self.assertEqual(editor.credit.travel.tags[0].accounts[1].code,
|
self.assertEqual(editor.credit.travel.tags[0].accounts[1].code,
|
||||||
Accounts.PREPAID)
|
Accounts.PREPAID)
|
||||||
self.assertEqual(editor.credit.travel.tags[1].name, "Taxi")
|
self.assertEqual(editor.credit.travel.tags[1].name, "Taxi")
|
||||||
self.assertEqual(len(editor.credit.travel.tags[1].accounts), 2)
|
self.assertEqual(len(editor.credit.travel.tags[1].accounts), 2)
|
||||||
self.assertEqual(editor.credit.travel.tags[1].accounts[0].code,
|
self.assertEqual(editor.credit.travel.tags[1].accounts[0].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PETTY_CASH)
|
||||||
self.assertEqual(editor.credit.travel.tags[1].accounts[1].code,
|
self.assertEqual(editor.credit.travel.tags[1].accounts[1].code,
|
||||||
Accounts.CASH)
|
Accounts.CASH)
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ class SummeryEditorTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(editor.credit.bus.tags[0].accounts[0].code,
|
self.assertEqual(editor.credit.bus.tags[0].accounts[0].code,
|
||||||
Accounts.PREPAID)
|
Accounts.PREPAID)
|
||||||
self.assertEqual(editor.credit.bus.tags[0].accounts[1].code,
|
self.assertEqual(editor.credit.bus.tags[0].accounts[1].code,
|
||||||
Accounts.PAYABLE)
|
Accounts.PETTY_CASH)
|
||||||
self.assertEqual(editor.credit.bus.tags[1].name, "Bus")
|
self.assertEqual(editor.credit.bus.tags[1].name, "Bus")
|
||||||
self.assertEqual(len(editor.credit.bus.tags[1].accounts), 1)
|
self.assertEqual(len(editor.credit.bus.tags[1].accounts), 1)
|
||||||
self.assertEqual(editor.credit.bus.tags[1].accounts[0].code,
|
self.assertEqual(editor.credit.bus.tags[1].accounts[0].code,
|
||||||
@ -186,7 +186,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
|||||||
"currency-0-debit-1-account_code": Accounts.MEAL,
|
"currency-0-debit-1-account_code": Accounts.MEAL,
|
||||||
"currency-0-debit-1-summary": " Lunch—Fries ",
|
"currency-0-debit-1-summary": " Lunch—Fries ",
|
||||||
"currency-0-debit-1-amount": "2.15",
|
"currency-0-debit-1-amount": "2.15",
|
||||||
"currency-0-credit-1-account_code": Accounts.PAYABLE,
|
"currency-0-credit-1-account_code": Accounts.PETTY_CASH,
|
||||||
"currency-0-credit-1-summary": " Lunch—Fries ",
|
"currency-0-credit-1-summary": " Lunch—Fries ",
|
||||||
"currency-0-credit-1-amount": "2.15",
|
"currency-0-credit-1-amount": "2.15",
|
||||||
"currency-0-debit-2-account_code": Accounts.MEAL,
|
"currency-0-debit-2-account_code": Accounts.MEAL,
|
||||||
@ -208,7 +208,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
|||||||
"currency-0-debit-1-account_code": Accounts.MEAL,
|
"currency-0-debit-1-account_code": Accounts.MEAL,
|
||||||
"currency-0-debit-1-summary": " Dinner—Steak ",
|
"currency-0-debit-1-summary": " Dinner—Steak ",
|
||||||
"currency-0-debit-1-amount": "8.28",
|
"currency-0-debit-1-amount": "8.28",
|
||||||
"currency-0-credit-1-account_code": Accounts.PAYABLE,
|
"currency-0-credit-1-account_code": Accounts.PETTY_CASH,
|
||||||
"currency-0-credit-1-summary": " Dinner—Steak ",
|
"currency-0-credit-1-summary": " Dinner—Steak ",
|
||||||
"currency-0-credit-1-amount": "8.28"},
|
"currency-0-credit-1-amount": "8.28"},
|
||||||
{"csrf_token": csrf_token,
|
{"csrf_token": csrf_token,
|
||||||
@ -218,13 +218,13 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
|||||||
"currency-0-debit-0-account_code": Accounts.MEAL,
|
"currency-0-debit-0-account_code": Accounts.MEAL,
|
||||||
"currency-0-debit-0-summary": " Lunch—Pizza ",
|
"currency-0-debit-0-summary": " Lunch—Pizza ",
|
||||||
"currency-0-debit-0-amount": "5.49",
|
"currency-0-debit-0-amount": "5.49",
|
||||||
"currency-0-credit-0-account_code": Accounts.PAYABLE,
|
"currency-0-credit-0-account_code": Accounts.PETTY_CASH,
|
||||||
"currency-0-credit-0-summary": " Lunch—Pizza ",
|
"currency-0-credit-0-summary": " Lunch—Pizza ",
|
||||||
"currency-0-credit-0-amount": "5.49",
|
"currency-0-credit-0-amount": "5.49",
|
||||||
"currency-0-debit-1-account_code": Accounts.MEAL,
|
"currency-0-debit-1-account_code": Accounts.MEAL,
|
||||||
"currency-0-debit-1-summary": " Lunch—Noodles ",
|
"currency-0-debit-1-summary": " Lunch—Noodles ",
|
||||||
"currency-0-debit-1-amount": "7.47",
|
"currency-0-debit-1-amount": "7.47",
|
||||||
"currency-0-credit-1-account_code": Accounts.PAYABLE,
|
"currency-0-credit-1-account_code": Accounts.PETTY_CASH,
|
||||||
"currency-0-credit-1-summary": " Lunch—Noodles ",
|
"currency-0-credit-1-summary": " Lunch—Noodles ",
|
||||||
"currency-0-credit-1-amount": "7.47"},
|
"currency-0-credit-1-amount": "7.47"},
|
||||||
{"csrf_token": csrf_token,
|
{"csrf_token": csrf_token,
|
||||||
@ -259,7 +259,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
|||||||
"currency-0-debit-3-account_code": Accounts.TRAVEL,
|
"currency-0-debit-3-account_code": Accounts.TRAVEL,
|
||||||
"currency-0-debit-3-summary": " Train—Red—Mall→Museum ",
|
"currency-0-debit-3-summary": " Train—Red—Mall→Museum ",
|
||||||
"currency-0-debit-3-amount": "4.4",
|
"currency-0-debit-3-amount": "4.4",
|
||||||
"currency-0-credit-3-account_code": Accounts.PAYABLE,
|
"currency-0-credit-3-account_code": Accounts.PETTY_CASH,
|
||||||
"currency-0-credit-3-summary": " Train—Red—Mall→Museum ",
|
"currency-0-credit-3-summary": " Train—Red—Mall→Museum ",
|
||||||
"currency-0-credit-3-amount": "4.4"},
|
"currency-0-credit-3-amount": "4.4"},
|
||||||
{"csrf_token": csrf_token,
|
{"csrf_token": csrf_token,
|
||||||
@ -275,31 +275,31 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
|||||||
"currency-0-debit-1-account_code": Accounts.TRAVEL,
|
"currency-0-debit-1-account_code": Accounts.TRAVEL,
|
||||||
"currency-0-debit-1-summary": " Taxi—Office→Restaurant ",
|
"currency-0-debit-1-summary": " Taxi—Office→Restaurant ",
|
||||||
"currency-0-debit-1-amount": "12",
|
"currency-0-debit-1-amount": "12",
|
||||||
"currency-0-credit-1-account_code": Accounts.PAYABLE,
|
"currency-0-credit-1-account_code": Accounts.PETTY_CASH,
|
||||||
"currency-0-credit-1-summary": " Taxi—Office→Restaurant ",
|
"currency-0-credit-1-summary": " Taxi—Office→Restaurant ",
|
||||||
"currency-0-credit-1-amount": "12",
|
"currency-0-credit-1-amount": "12",
|
||||||
"currency-0-debit-2-account_code": Accounts.TRAVEL,
|
"currency-0-debit-2-account_code": Accounts.TRAVEL,
|
||||||
"currency-0-debit-2-summary": " Taxi—Restaurant→City Hall ",
|
"currency-0-debit-2-summary": " Taxi—Restaurant→City Hall ",
|
||||||
"currency-0-debit-2-amount": "8",
|
"currency-0-debit-2-amount": "8",
|
||||||
"currency-0-credit-2-account_code": Accounts.PAYABLE,
|
"currency-0-credit-2-account_code": Accounts.PETTY_CASH,
|
||||||
"currency-0-credit-2-summary": " Taxi—Restaurant→City Hall ",
|
"currency-0-credit-2-summary": " Taxi—Restaurant→City Hall ",
|
||||||
"currency-0-credit-2-amount": "8",
|
"currency-0-credit-2-amount": "8",
|
||||||
"currency-0-debit-3-account_code": Accounts.TRAVEL,
|
"currency-0-debit-3-account_code": Accounts.TRAVEL,
|
||||||
"currency-0-debit-3-summary": " Bike—City Hall→Office ",
|
"currency-0-debit-3-summary": " Bike—City Hall→Office ",
|
||||||
"currency-0-debit-3-amount": "3.5",
|
"currency-0-debit-3-amount": "3.5",
|
||||||
"currency-0-credit-3-account_code": Accounts.PAYABLE,
|
"currency-0-credit-3-account_code": Accounts.PETTY_CASH,
|
||||||
"currency-0-credit-3-summary": " Bike—City Hall→Office ",
|
"currency-0-credit-3-summary": " Bike—City Hall→Office ",
|
||||||
"currency-0-credit-3-amount": "3.5",
|
"currency-0-credit-3-amount": "3.5",
|
||||||
"currency-0-debit-4-account_code": Accounts.TRAVEL,
|
"currency-0-debit-4-account_code": Accounts.TRAVEL,
|
||||||
"currency-0-debit-4-summary": " Bike—Restaurant→Office ",
|
"currency-0-debit-4-summary": " Bike—Restaurant→Office ",
|
||||||
"currency-0-debit-4-amount": "4",
|
"currency-0-debit-4-amount": "4",
|
||||||
"currency-0-credit-4-account_code": Accounts.PAYABLE,
|
"currency-0-credit-4-account_code": Accounts.PETTY_CASH,
|
||||||
"currency-0-credit-4-summary": " Bike—Restaurant→Office ",
|
"currency-0-credit-4-summary": " Bike—Restaurant→Office ",
|
||||||
"currency-0-credit-4-amount": "4",
|
"currency-0-credit-4-amount": "4",
|
||||||
"currency-0-debit-5-account_code": Accounts.TRAVEL,
|
"currency-0-debit-5-account_code": Accounts.TRAVEL,
|
||||||
"currency-0-debit-5-summary": " Bike—Office→Theatre ",
|
"currency-0-debit-5-summary": " Bike—Office→Theatre ",
|
||||||
"currency-0-debit-5-amount": "1.5",
|
"currency-0-debit-5-amount": "1.5",
|
||||||
"currency-0-credit-5-account_code": Accounts.PAYABLE,
|
"currency-0-credit-5-account_code": Accounts.PETTY_CASH,
|
||||||
"currency-0-credit-5-summary": " Bike—Office→Theatre ",
|
"currency-0-credit-5-summary": " Bike—Office→Theatre ",
|
||||||
"currency-0-credit-5-amount": "1.5",
|
"currency-0-credit-5-amount": "1.5",
|
||||||
"currency-0-debit-6-account_code": Accounts.TRAVEL,
|
"currency-0-debit-6-account_code": Accounts.TRAVEL,
|
||||||
@ -312,13 +312,13 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
|||||||
"next": NEXT_URI,
|
"next": NEXT_URI,
|
||||||
"date": txn_date,
|
"date": txn_date,
|
||||||
"currency-0-code": "USD",
|
"currency-0-code": "USD",
|
||||||
"currency-0-debit-0-account_code": Accounts.PAYABLE,
|
"currency-0-debit-0-account_code": Accounts.PETTY_CASH,
|
||||||
"currency-0-debit-0-summary": " Dinner—Steak ",
|
"currency-0-debit-0-summary": " Dinner—Steak ",
|
||||||
"currency-0-debit-0-amount": "8.28",
|
"currency-0-debit-0-amount": "8.28",
|
||||||
"currency-0-credit-0-account_code": Accounts.BANK,
|
"currency-0-credit-0-account_code": Accounts.BANK,
|
||||||
"currency-0-credit-0-summary": " Dinner—Steak ",
|
"currency-0-credit-0-summary": " Dinner—Steak ",
|
||||||
"currency-0-credit-0-amount": "8.28",
|
"currency-0-credit-0-amount": "8.28",
|
||||||
"currency-0-debit-1-account_code": Accounts.PAYABLE,
|
"currency-0-debit-1-account_code": Accounts.PETTY_CASH,
|
||||||
"currency-0-debit-1-summary": " Lunch—Pizza ",
|
"currency-0-debit-1-summary": " Lunch—Pizza ",
|
||||||
"currency-0-debit-1-amount": "5.49",
|
"currency-0-debit-1-amount": "5.49",
|
||||||
"currency-0-credit-1-account_code": Accounts.BANK,
|
"currency-0-credit-1-account_code": Accounts.BANK,
|
||||||
|
@ -38,6 +38,7 @@ EMPTY_NOTE: str = " \n\n "
|
|||||||
class Accounts:
|
class Accounts:
|
||||||
"""The shortcuts to the common accounts."""
|
"""The shortcuts to the common accounts."""
|
||||||
CASH: str = "1111-001"
|
CASH: str = "1111-001"
|
||||||
|
PETTY_CASH: str = "1112-001"
|
||||||
BANK: str = "1113-001"
|
BANK: str = "1113-001"
|
||||||
PREPAID: str = "1258-001"
|
PREPAID: str = "1258-001"
|
||||||
PAYABLE: str = "2141-001"
|
PAYABLE: str = "2141-001"
|
||||||
|
Reference in New Issue
Block a user