Added the recurring transactions.
This commit is contained in:
parent
5dccf99a55
commit
3455827c09
@ -17,9 +17,11 @@
|
|||||||
"""The description editor.
|
"""The description editor.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import re
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.models import Account, JournalEntryLineItem
|
from accounting.models import Account, JournalEntryLineItem
|
||||||
@ -143,6 +145,29 @@ class DescriptionType:
|
|||||||
return sorted(self.__tag_dict.values(), key=lambda x: -x.freq)
|
return sorted(self.__tag_dict.values(), key=lambda x: -x.freq)
|
||||||
|
|
||||||
|
|
||||||
|
class DescriptionRecurring:
|
||||||
|
"""A recurring transaction."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, template: str, account: Account):
|
||||||
|
"""Constructs a recurring transaction.
|
||||||
|
|
||||||
|
:param name: The name.
|
||||||
|
:param template: The template.
|
||||||
|
:param account: The account.
|
||||||
|
"""
|
||||||
|
self.name: str = name
|
||||||
|
self.template: str = template
|
||||||
|
self.account: DescriptionAccount = DescriptionAccount(account, 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_codes(self) -> list[str]:
|
||||||
|
"""Returns the account codes by the order of their frequencies.
|
||||||
|
|
||||||
|
:return: The account codes by the order of their frequencies.
|
||||||
|
"""
|
||||||
|
return [self.account.code]
|
||||||
|
|
||||||
|
|
||||||
class DescriptionDebitCredit:
|
class DescriptionDebitCredit:
|
||||||
"""The description on debit or credit."""
|
"""The description on debit or credit."""
|
||||||
|
|
||||||
@ -163,6 +188,8 @@ class DescriptionDebitCredit:
|
|||||||
DescriptionType] \
|
DescriptionType] \
|
||||||
= {x.id: x for x in {self.general, self.travel, self.bus}}
|
= {x.id: x for x in {self.general, self.travel, self.bus}}
|
||||||
"""A dictionary from the type ID to the corresponding tags."""
|
"""A dictionary from the type ID to the corresponding tags."""
|
||||||
|
self.recurring: list[DescriptionRecurring] = []
|
||||||
|
"""The recurring transactions."""
|
||||||
|
|
||||||
def add_tag(self, tag_type: t.Literal["general", "travel", "bus"],
|
def add_tag(self, tag_type: t.Literal["general", "travel", "bus"],
|
||||||
name: str, account: Account, freq: int) -> None:
|
name: str, account: Account, freq: int) -> None:
|
||||||
@ -193,6 +220,10 @@ class DescriptionDebitCredit:
|
|||||||
freq[account.id] = 0
|
freq[account.id] = 0
|
||||||
freq[account.id] \
|
freq[account.id] \
|
||||||
= freq[account.id] + account.freq
|
= freq[account.id] + account.freq
|
||||||
|
for recurring in self.recurring:
|
||||||
|
accounts[recurring.account.id] = recurring.account
|
||||||
|
if recurring.account.id not in freq:
|
||||||
|
freq[recurring.account.id] = 0
|
||||||
return [accounts[y] for y in sorted(freq.keys(),
|
return [accounts[y] for y in sorted(freq.keys(),
|
||||||
key=lambda x: -freq[x])]
|
key=lambda x: -freq[x])]
|
||||||
|
|
||||||
@ -207,6 +238,7 @@ class DescriptionEditor:
|
|||||||
self.credit: DescriptionDebitCredit = DescriptionDebitCredit("credit")
|
self.credit: DescriptionDebitCredit = DescriptionDebitCredit("credit")
|
||||||
"""The credit tags."""
|
"""The credit tags."""
|
||||||
self.__init_tags()
|
self.__init_tags()
|
||||||
|
self.__init_recurring()
|
||||||
|
|
||||||
def __init_tags(self):
|
def __init_tags(self):
|
||||||
"""Initializes the tags.
|
"""Initializes the tags.
|
||||||
@ -243,6 +275,49 @@ class DescriptionEditor:
|
|||||||
debit_credit_dict[row.debit_credit].add_tag(
|
debit_credit_dict[row.debit_credit].add_tag(
|
||||||
row.tag_type, row.tag, accounts[row.account_id], row.freq)
|
row.tag_type, row.tag, accounts[row.account_id], row.freq)
|
||||||
|
|
||||||
|
def __init_recurring(self) -> None:
|
||||||
|
"""Initializes the recurring transactions.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
if "RECURRING" not in current_app.config:
|
||||||
|
return
|
||||||
|
data: list[tuple[t.Literal["debit", "credit"], str, str, str]] \
|
||||||
|
= [x.split("|")
|
||||||
|
for x in current_app.config["RECURRING"].split(",")]
|
||||||
|
debit_credit_dict: dict[t.Literal["debit", "credit"],
|
||||||
|
DescriptionDebitCredit] \
|
||||||
|
= {x.debit_credit: x for x in {self.debit, self.credit}}
|
||||||
|
accounts: dict[str, Account] \
|
||||||
|
= self.__get_accounts({x[1] for x in data})
|
||||||
|
for row in data:
|
||||||
|
debit_credit_dict[row[0]].recurring.append(
|
||||||
|
DescriptionRecurring(row[2], row[3], accounts[row[1]]))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __get_accounts(codes: set[str]) -> dict[str, Account]:
|
||||||
|
"""Finds and returns the accounts by codes.
|
||||||
|
|
||||||
|
:param codes: The account codes.
|
||||||
|
:return: The account.
|
||||||
|
"""
|
||||||
|
def get_condition(code0: str) -> sa.BinaryExpression:
|
||||||
|
m: re.Match = re.match(r"^(\d{4})-(\d{3})$", code0)
|
||||||
|
assert m is not None,\
|
||||||
|
f"Malformed account code \"{code0}\" for regular transactions."
|
||||||
|
return sa.and_(Account.base_code == m.group(1),
|
||||||
|
Account.no == int(m.group(2)))
|
||||||
|
|
||||||
|
conditions: list[sa.BinaryExpression] \
|
||||||
|
= [get_condition(x) for x in codes]
|
||||||
|
accounts: dict[str, Account] \
|
||||||
|
= {x.code: x for x in
|
||||||
|
Account.query.filter(sa.or_(*conditions)).all()}
|
||||||
|
for code in codes:
|
||||||
|
assert code in accounts,\
|
||||||
|
f"Unknown account \"{code}\" for regular transactions."
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
|
||||||
def get_prefix(string: str | sa.Column, separator: str | sa.Column) \
|
def get_prefix(string: str | sa.Column, separator: str | sa.Column) \
|
||||||
-> sa.Function:
|
-> sa.Function:
|
||||||
|
@ -32,7 +32,7 @@ class DescriptionEditor {
|
|||||||
* The line item editor
|
* The line item editor
|
||||||
* @type {JournalEntryLineItemEditor}
|
* @type {JournalEntryLineItemEditor}
|
||||||
*/
|
*/
|
||||||
#lineItemEditor;
|
lineItemEditor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The description editor form
|
* The description editor form
|
||||||
@ -113,7 +113,7 @@ class DescriptionEditor {
|
|||||||
* @param debitCredit {string} either "debit" or "credit"
|
* @param debitCredit {string} either "debit" or "credit"
|
||||||
*/
|
*/
|
||||||
constructor(lineItemEditor, debitCredit) {
|
constructor(lineItemEditor, debitCredit) {
|
||||||
this.#lineItemEditor = lineItemEditor;
|
this.lineItemEditor = lineItemEditor;
|
||||||
this.debitCredit = debitCredit;
|
this.debitCredit = debitCredit;
|
||||||
this.prefix = "accounting-description-editor-" + debitCredit;
|
this.prefix = "accounting-description-editor-" + debitCredit;
|
||||||
this.#form = document.getElementById(this.prefix);
|
this.#form = document.getElementById(this.prefix);
|
||||||
@ -132,7 +132,7 @@ class DescriptionEditor {
|
|||||||
this.currentTab = this.tabPlanes.general;
|
this.currentTab = this.tabPlanes.general;
|
||||||
this.#initializeSuggestedAccounts();
|
this.#initializeSuggestedAccounts();
|
||||||
this.description.onchange = () => this.#onDescriptionChange();
|
this.description.onchange = () => this.#onDescriptionChange();
|
||||||
this.#offsetButton.onclick = () => this.#lineItemEditor.originalLineItemSelector.onOpen();
|
this.#offsetButton.onclick = () => this.lineItemEditor.originalLineItemSelector.onOpen();
|
||||||
this.#form.onsubmit = () => {
|
this.#form.onsubmit = () => {
|
||||||
if (this.currentTab.validate()) {
|
if (this.currentTab.validate()) {
|
||||||
this.#submit();
|
this.#submit();
|
||||||
@ -147,7 +147,7 @@ class DescriptionEditor {
|
|||||||
*/
|
*/
|
||||||
#onDescriptionChange() {
|
#onDescriptionChange() {
|
||||||
this.description.value = this.description.value.trim();
|
this.description.value = this.description.value.trim();
|
||||||
for (const tabPlane of [this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) {
|
for (const tabPlane of [this.tabPlanes.recurring, this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) {
|
||||||
if (tabPlane.populate()) {
|
if (tabPlane.populate()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -158,27 +158,31 @@ class DescriptionEditor {
|
|||||||
/**
|
/**
|
||||||
* Filters the suggested accounts.
|
* Filters the suggested accounts.
|
||||||
*
|
*
|
||||||
* @param tagButton {HTMLButtonElement|null} the tag button
|
* @param tagButton {HTMLButtonElement} the tag button
|
||||||
*/
|
*/
|
||||||
filterSuggestedAccounts(tagButton) {
|
filterSuggestedAccounts(tagButton) {
|
||||||
for (const accountButton of this.#accountButtons) {
|
this.clearSuggestedAccounts();
|
||||||
accountButton.classList.add("d-none");
|
|
||||||
}
|
|
||||||
if (tagButton === null) {
|
|
||||||
this.#selectAccount(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const suggested = JSON.parse(tagButton.dataset.accounts);
|
const suggested = JSON.parse(tagButton.dataset.accounts);
|
||||||
let selectedAccountButton = null;
|
|
||||||
for (const accountButton of this.#accountButtons) {
|
for (const accountButton of this.#accountButtons) {
|
||||||
if (suggested.includes(accountButton.dataset.code)) {
|
if (suggested.includes(accountButton.dataset.code)) {
|
||||||
accountButton.classList.remove("d-none");
|
accountButton.classList.remove("d-none");
|
||||||
if (accountButton.dataset.code === suggested[0]) {
|
if (accountButton.dataset.code === suggested[0]) {
|
||||||
selectedAccountButton = accountButton;
|
this.#selectAccount(accountButton);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.#selectAccount(selectedAccountButton);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the suggested accounts.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
clearSuggestedAccounts() {
|
||||||
|
for (const accountButton of this.#accountButtons) {
|
||||||
|
accountButton.classList.add("d-none");
|
||||||
|
}
|
||||||
|
this.#selectAccount(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -215,9 +219,9 @@ class DescriptionEditor {
|
|||||||
#submit() {
|
#submit() {
|
||||||
bootstrap.Modal.getOrCreateInstance(this.#modal).hide();
|
bootstrap.Modal.getOrCreateInstance(this.#modal).hide();
|
||||||
if (this.#selectedAccount !== null) {
|
if (this.#selectedAccount !== null) {
|
||||||
this.#lineItemEditor.saveDescriptionWithAccount(this.description.value, this.#selectedAccount.dataset.code, this.#selectedAccount.dataset.text, this.#selectedAccount.classList.contains("accounting-account-is-need-offset"));
|
this.lineItemEditor.saveDescriptionWithAccount(this.description.value, this.#selectedAccount.dataset.code, this.#selectedAccount.dataset.text, this.#selectedAccount.classList.contains("accounting-account-is-need-offset"));
|
||||||
} else {
|
} else {
|
||||||
this.#lineItemEditor.saveDescription(this.description.value);
|
this.lineItemEditor.saveDescription(this.description.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,7 +231,7 @@ class DescriptionEditor {
|
|||||||
*/
|
*/
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.#reset();
|
this.#reset();
|
||||||
this.description.value = this.#lineItemEditor.description === null? "": this.#lineItemEditor.description;
|
this.description.value = this.lineItemEditor.description === null? "": this.lineItemEditor.description;
|
||||||
this.#onDescriptionChange();
|
this.#onDescriptionChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,7 +422,7 @@ class TagTabPlane extends TabPlane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isMatched) {
|
if (!isMatched) {
|
||||||
this.editor.filterSuggestedAccounts(null);
|
this.editor.clearSuggestedAccounts();
|
||||||
}
|
}
|
||||||
this.validateTag();
|
this.validateTag();
|
||||||
}
|
}
|
||||||
@ -436,14 +440,13 @@ class TagTabPlane extends TabPlane {
|
|||||||
*/
|
*/
|
||||||
switchToMe() {
|
switchToMe() {
|
||||||
super.switchToMe();
|
super.switchToMe();
|
||||||
let selectedTagButton = null;
|
|
||||||
for (const tagButton of this.tagButtons) {
|
for (const tagButton of this.tagButtons) {
|
||||||
if (tagButton.classList.contains("btn-primary")) {
|
if (tagButton.classList.contains("btn-primary")) {
|
||||||
selectedTagButton = tagButton;
|
this.editor.filterSuggestedAccounts(tagButton);
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.editor.filterSuggestedAccounts(selectedTagButton);
|
this.editor.clearSuggestedAccounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -561,13 +564,6 @@ class GeneralTagTab extends TagTabPlane {
|
|||||||
this.tag.value = found[1];
|
this.tag.value = found[1];
|
||||||
this.onTagChange();
|
this.onTagChange();
|
||||||
}
|
}
|
||||||
for (const tagButton of this.tagButtons) {
|
|
||||||
if (tagButton.dataset.value === this.tag.value) {
|
|
||||||
tagButton.classList.remove("btn-outline-primary");
|
|
||||||
tagButton.classList.add("btn-primary");
|
|
||||||
this.editor.filterSuggestedAccounts(tagButton);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.switchToMe();
|
this.switchToMe();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -732,13 +728,6 @@ class GeneralTripTab extends TagTabPlane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.#to.value = found[4];
|
this.#to.value = found[4];
|
||||||
for (const tagButton of this.tagButtons) {
|
|
||||||
if (tagButton.dataset.value === this.tag.value) {
|
|
||||||
tagButton.classList.remove("btn-outline-primary");
|
|
||||||
tagButton.classList.add("btn-primary");
|
|
||||||
this.editor.filterSuggestedAccounts(tagButton);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.switchToMe();
|
this.switchToMe();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -917,14 +906,6 @@ class BusTripTab extends TagTabPlane {
|
|||||||
this.#route.value = found[2];
|
this.#route.value = found[2];
|
||||||
this.#from.value = found[3];
|
this.#from.value = found[3];
|
||||||
this.#to.value = found[4];
|
this.#to.value = found[4];
|
||||||
for (const tagButton of this.tagButtons) {
|
|
||||||
if (tagButton.dataset.value === this.tag.value) {
|
|
||||||
tagButton.classList.remove("btn-outline-primary");
|
|
||||||
tagButton.classList.add("btn-primary");
|
|
||||||
this.editor.filterSuggestedAccounts(tagButton);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.switchToMe();
|
this.switchToMe();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -992,10 +973,16 @@ class BusTripTab extends TagTabPlane {
|
|||||||
class RecurringTransactionTab extends TabPlane {
|
class RecurringTransactionTab extends TabPlane {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The transaction buttons
|
* The month names
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
#monthNames;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The buttons of the recurring items
|
||||||
* @type {HTMLButtonElement[]}
|
* @type {HTMLButtonElement[]}
|
||||||
*/
|
*/
|
||||||
#transactions;
|
#itemButtons;
|
||||||
|
|
||||||
// noinspection JSValidateTypes
|
// noinspection JSValidateTypes
|
||||||
/**
|
/**
|
||||||
@ -1006,8 +993,44 @@ class RecurringTransactionTab extends TabPlane {
|
|||||||
*/
|
*/
|
||||||
constructor(editor) {
|
constructor(editor) {
|
||||||
super(editor);
|
super(editor);
|
||||||
|
this.#monthNames = [
|
||||||
|
"",
|
||||||
|
A_("January"), A_("February"), A_("March"), A_("April"),
|
||||||
|
A_("May"), A_("June"), A_("July"), A_("August"),
|
||||||
|
A_("September"), A_("October"), A_("November"), A_("December"),
|
||||||
|
];
|
||||||
// noinspection JSValidateTypes
|
// noinspection JSValidateTypes
|
||||||
this.#transactions = Array.from(document.getElementsByClassName(this.prefix + "-transaction"));
|
this.#itemButtons = Array.from(document.getElementsByClassName(this.prefix + "-item"));
|
||||||
|
for (const itemButton of this.#itemButtons) {
|
||||||
|
itemButton.onclick = () => {
|
||||||
|
this.reset();
|
||||||
|
itemButton.classList.add("btn-primary");
|
||||||
|
itemButton.classList.remove("btn-outline-primary");
|
||||||
|
this.editor.description.value = this.#getDescription(itemButton);
|
||||||
|
this.editor.filterSuggestedAccounts(itemButton);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the description for a recurring item.
|
||||||
|
*
|
||||||
|
* @param itemButton {HTMLButtonElement} the recurring item
|
||||||
|
* @return {string} the description of the recurring item
|
||||||
|
*/
|
||||||
|
#getDescription(itemButton) {
|
||||||
|
const today = new Date(this.editor.lineItemEditor.form.getDate());
|
||||||
|
const thisMonth = today.getMonth() + 1;
|
||||||
|
const lastMonth = (thisMonth + 10) % 12 + 1;
|
||||||
|
const lastBimonthlyFrom = ((thisMonth + thisMonth % 2 + 8) % 12 + 1);
|
||||||
|
const lastBimonthlyTo = ((thisMonth + thisMonth % 2 + 9) % 12 + 1);
|
||||||
|
return itemButton.dataset.template
|
||||||
|
.replaceAll("{this_month_number}", String(thisMonth))
|
||||||
|
.replaceAll("{this_month_name}", this.#monthNames[thisMonth])
|
||||||
|
.replaceAll("{last_month_number}", String(lastMonth))
|
||||||
|
.replaceAll("{last_month_name}", this.#monthNames[lastMonth])
|
||||||
|
.replaceAll("{last_bimonthly_number}", String(lastBimonthlyFrom) + "–" + String(lastBimonthlyTo))
|
||||||
|
.replaceAll("{last_bimonthly_name}", this.#monthNames[lastBimonthlyFrom] + "–" + this.#monthNames[lastBimonthlyTo]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1026,9 +1049,9 @@ class RecurringTransactionTab extends TabPlane {
|
|||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
reset() {
|
reset() {
|
||||||
for (const transaction of this.#transactions) {
|
for (const itemButton of this.#itemButtons) {
|
||||||
transaction.classList.remove("btn-primary");
|
itemButton.classList.remove("btn-primary");
|
||||||
transaction.classList.add("btn-outline-primary");
|
itemButton.classList.add("btn-outline-primary");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1039,9 +1062,32 @@ class RecurringTransactionTab extends TabPlane {
|
|||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
populate() {
|
populate() {
|
||||||
|
for (const itemButton of this.#itemButtons) {
|
||||||
|
if (this.#getDescription(itemButton) === this.editor.description.value) {
|
||||||
|
itemButton.classList.add("btn-primary");
|
||||||
|
itemButton.classList.remove("btn-outline-primary");
|
||||||
|
this.switchToMe();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches to the tab plane.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
switchToMe() {
|
||||||
|
super.switchToMe();
|
||||||
|
for (const itemButton of this.#itemButtons) {
|
||||||
|
if (itemButton.classList.contains("btn-primary")) {
|
||||||
|
this.editor.filterSuggestedAccounts(itemButton);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.editor.clearSuggestedAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the input in the tab plane.
|
* Validates the input in the tab plane.
|
||||||
*
|
*
|
||||||
|
@ -156,7 +156,11 @@ First written: 2023/2/28
|
|||||||
|
|
||||||
{# A recurring transaction #}
|
{# A recurring transaction #}
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-page" class="d-none" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab">
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-page" class="d-none" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab">
|
||||||
{# TODO: To be done #}
|
{% for recurring in description_editor.recurring %}
|
||||||
|
<button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-recurring-item" type="button" tabindex="-1" data-template="{{ recurring.template }}" data-accounts="{{ recurring.account_codes|tojson|forceescape }}">
|
||||||
|
{{ recurring.name }}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# The annotation #}
|
{# The annotation #}
|
||||||
|
@ -50,6 +50,16 @@ def create_app(is_testing: bool = False) -> Flask:
|
|||||||
"SQLALCHEMY_DATABASE_URI": db_uri,
|
"SQLALCHEMY_DATABASE_URI": db_uri,
|
||||||
"BABEL_DEFAULT_LOCALE": "en",
|
"BABEL_DEFAULT_LOCALE": "en",
|
||||||
"ALL_LINGUAS": "zh_Hant|正體中文,en|English,zh_Hans|简体中文",
|
"ALL_LINGUAS": "zh_Hant|正體中文,en|English,zh_Hans|简体中文",
|
||||||
|
"RECURRING": (
|
||||||
|
"debit|1314-001|Pension|Pension for {last_month_name},"
|
||||||
|
"debit|6262-001|Health insurance"
|
||||||
|
"|Health insurance for {last_month_name},"
|
||||||
|
"debit|6261-001|Electricity bill"
|
||||||
|
"|Electricity bill for {last_bimonthly_name},"
|
||||||
|
"debit|6261-001|Water bill|Water bill for {last_bimonthly_name},"
|
||||||
|
"debit|6261-001|Gas bill|Gas bill for {last_bimonthly_name},"
|
||||||
|
"debit|6261-001|Phone bill|Phone bill for {last_month_name},"
|
||||||
|
"credit|4611-001|Payroll|Payroll for {last_month_name}"),
|
||||||
})
|
})
|
||||||
if is_testing:
|
if is_testing:
|
||||||
app.config["TESTING"] = True
|
app.config["TESTING"] = True
|
||||||
|
Loading…
Reference in New Issue
Block a user