Compare commits

...

27 Commits

Author SHA1 Message Date
d8afadda02 Advanced to version 0.8.0. 2023-03-22 01:52:24 +08:00
c8e1270d8f Updated the translation. 2023-03-22 01:50:18 +08:00
2a78799404 Revised the page to reorder the journal entries in a same day. 2023-03-22 01:47:11 +08:00
863d7a9368 Simplified the "can_delete" pseudo property of the JournalEntry data model. SQLAlchemy caches the query result. There is no need to cache the result again. 2023-03-22 01:02:09 +08:00
6fd37b21d9 Fixed so that the journal entries that has offset cannot be deleted. 2023-03-22 00:59:43 +08:00
bbf3ee3320 Added the limitation so that the default currency and the currencies in use cannot be deleted. 2023-03-22 00:37:39 +08:00
b60cc7902d Revised the test_delete test in the AccountTestCase test case. 2023-03-22 00:37:26 +08:00
623313b58a Renamed the constants to be upper-cased in test_account.py. 2023-03-22 00:37:26 +08:00
d0d2d77a2e Added the limitation so that essential accounts, like cash, and the accounts in use, cannot be deleted. 2023-03-22 00:37:26 +08:00
494faeffea Revised the toolbar of the reports to fit better in desktop browsers. 2023-03-21 23:16:47 +08:00
871a5fd1d8 Changed the "settings" button to "edit" in the account, currency, and journal entry detail pages. 2023-03-21 23:10:33 +08:00
e615ad2690 Revised the style of the toolbar buttons for better layout on mobile devices. Hid the "Back" button on mobile devices for better layout and saving spaces. 2023-03-21 23:07:05 +08:00
da92a0b42c Replaced the BABEL_DEFAULT_LOCALE configuration variable with the default_locale from the Flask-Babel instance, to get rid of the dependency to the specific configuration variable. 2023-03-21 22:34:44 +08:00
678d0aa773 Fixed the CSS version of Tempus-Dominus in the base template of the test site. 2023-03-21 21:22:48 +08:00
9248ba7e3b Removed the redundant Flask App context from the default_currency_code Jinja2 global and the default_ie_account_code function. They are always under the Flask app context. 2023-03-21 21:17:10 +08:00
446087b212 Added the ACCOUNTING_DEFAULT_CURRENCY and ACCOUNTING_DEFAULT_IE_ACCOUNT configuration to the test site configuration, for demonstration. 2023-03-21 21:15:14 +08:00
a42e7d13a2 Renamed the configuration DEFAULT_CURRENCY, DEFAULT_IE_ACCOUNT, and RECURRING to "ACCOUNTING_DEFAULT_CURRENCY", "ACCOUNTING_DEFAULT_IE_ACCOUNT", and "ACCOUNTING_RECURRING", respectively. 2023-03-21 21:13:03 +08:00
a82f5091f1 Revised the styles of the buttons in the description editor. 2023-03-21 19:50:57 +08:00
3455827c09 Added the recurring transactions. 2023-03-21 19:45:56 +08:00
5dccf99a55 Renamed "regular" to "recurring" in the description editor. 2023-03-21 17:48:19 +08:00
8818b46e01 Moved the tag initialization from the constructor to the __init_tags method in the DescriptionEditor class. 2023-03-21 17:32:07 +08:00
2f3ad99467 Removed redundant code in the templates of the journal entry form. 2023-03-21 11:54:45 +08:00
592910187b Added the common form-debit-credit.html template to reduce the duplicated code for the currency sub-forms in the transaction form. 2023-03-21 11:47:05 +08:00
cb7a0d377f Added the common form-currency.html template to reduce the duplicated code for the currency sub-forms in the transaction form. 2023-03-21 11:26:28 +08:00
79175285f8 Changed "to transfer" to "as transfer", and updated its Font Awesome icon in the toolbar of the journal entries. 2023-03-21 11:04:45 +08:00
fef474977c Adjust the location of the Material Design floating action buttons for mobile screen. 2023-03-21 10:57:08 +08:00
fa1a55cd3d Adjusted the style for the mobile toolbar for Firefox on Android with large font size. 2023-03-21 10:56:21 +08:00
39 changed files with 1067 additions and 678 deletions

View File

@ -13,7 +13,7 @@ sys.path.insert(0, os.path.abspath('../../src/'))
project = 'Mia! Accounting Flask' project = 'Mia! Accounting Flask'
copyright = '2023, imacat' copyright = '2023, imacat'
author = 'imacat' author = 'imacat'
release = '0.7.0' release = '0.8.0'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@ -17,7 +17,7 @@
[metadata] [metadata]
name = mia-accounting-flask name = mia-accounting-flask
version = 0.7.0 version = 0.8.0
author = imacat author = imacat
author_email = imacat@mail.imacat.idv.tw author_email = imacat@mail.imacat.idv.tw
description = The Mia! Accounting Flask project. description = The Mia! Accounting Flask project.

View File

@ -157,6 +157,9 @@ def delete_account(account: Account) -> redirect:
:return: The redirection to the account list on success, or the account :return: The redirection to the account list on success, or the account
detail on error. detail on error.
""" """
if not account.can_delete:
flash(s(lazy_gettext("The account cannot be deleted.")), "error")
return redirect(inherit_next(__get_detail_uri(account)))
account.delete() account.delete()
sort_accounts_in(account.base_code, account.id) sort_accounts_in(account.base_code, account.id)
db.session.commit() db.session.commit()

View File

@ -160,6 +160,9 @@ def delete_currency(currency: Currency) -> redirect:
:return: The redirection to the currency list on success, or the currency :return: The redirection to the currency list on success, or the currency
detail on error. detail on error.
""" """
if not currency.can_delete:
flash(s(lazy_gettext("The currency cannot be deleted.")), "error")
return redirect(inherit_next(__get_detail_uri(currency)))
currency.delete() currency.delete()
db.session.commit() db.session.commit()
flash(s(lazy_gettext("The currency is deleted successfully.")), "success") flash(s(lazy_gettext("The currency is deleted successfully.")), "success")

View File

@ -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])]
@ -206,6 +237,14 @@ class DescriptionEditor:
"""The debit tags.""" """The debit tags."""
self.credit: DescriptionDebitCredit = DescriptionDebitCredit("credit") self.credit: DescriptionDebitCredit = DescriptionDebitCredit("credit")
"""The credit tags.""" """The credit tags."""
self.__init_tags()
self.__init_recurring()
def __init_tags(self):
"""Initializes the tags.
:return: None.
"""
debit_credit: sa.Label = sa.case( debit_credit: sa.Label = sa.case(
(JournalEntryLineItem.is_debit, "debit"), (JournalEntryLineItem.is_debit, "debit"),
else_="credit").label("debit_credit") else_="credit").label("debit_credit")
@ -236,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 "ACCOUNTING_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["ACCOUNTING_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:

View File

@ -175,6 +175,9 @@ def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
:return: The redirection to the journal entry list on success, or the :return: The redirection to the journal entry list on success, or the
journal entry detail on error. journal entry detail on error.
""" """
if not journal_entry.can_delete:
flash(s(lazy_gettext("The journal entry cannot be deleted.")), "error")
return redirect(inherit_next(__get_detail_uri(journal_entry)))
journal_entry.delete() journal_entry.delete()
sort_journal_entries_in(journal_entry.date, journal_entry.id) sort_journal_entries_in(journal_entry.date, journal_entry.id)
db.session.commit() db.session.commit()

View File

@ -25,8 +25,8 @@ from datetime import date
from decimal import Decimal from decimal import Decimal
import sqlalchemy as sa import sqlalchemy as sa
from flask import current_app from babel import Locale
from flask_babel import get_locale from flask_babel import get_locale, get_babel
from sqlalchemy import text from sqlalchemy import text
from accounting import db from accounting import db
@ -61,11 +61,11 @@ class BaseAccount(db.Model):
:return: The title in the current locale. :return: The title in the current locale.
""" """
current_locale = str(get_locale()) current_locale: Locale = get_locale()
if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]: if current_locale == get_babel().instance.default_locale:
return self.title_l10n return self.title_l10n
for l10n in self.l10n: for l10n in self.l10n:
if l10n.locale == current_locale: if l10n.locale == str(current_locale):
return l10n.title return l10n.title
return self.title_l10n return self.title_l10n
@ -171,11 +171,11 @@ class Account(db.Model):
:return: The title in the current locale. :return: The title in the current locale.
""" """
current_locale = str(get_locale()) current_locale: Locale = get_locale()
if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]: if current_locale == get_babel().instance.default_locale:
return self.title_l10n return self.title_l10n
for l10n in self.l10n: for l10n in self.l10n:
if l10n.locale == current_locale: if l10n.locale == str(current_locale):
return l10n.title return l10n.title
return self.title_l10n return self.title_l10n
@ -189,15 +189,15 @@ class Account(db.Model):
if self.title_l10n is None: if self.title_l10n is None:
self.title_l10n = value self.title_l10n = value
return return
current_locale = str(get_locale()) current_locale: Locale = get_locale()
if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]: if current_locale == get_babel().instance.default_locale:
self.title_l10n = value self.title_l10n = value
return return
for l10n in self.l10n: for l10n in self.l10n:
if l10n.locale == current_locale: if l10n.locale == str(current_locale):
l10n.title = value l10n.title = value
return return
self.l10n.append(AccountL10n(locale=current_locale, title=value)) self.l10n.append(AccountL10n(locale=str(current_locale), title=value))
@property @property
def is_real(self) -> bool: def is_real(self) -> bool:
@ -236,6 +236,16 @@ class Account(db.Model):
return True return True
return False return False
@property
def can_delete(self) -> bool:
"""Returns whether the account can be deleted.
:return: True if the account can be deleted, or False otherwise.
"""
if self.code in {"1111-001", "3351-001", "3353-001"}:
return False
return len(self.line_items) == 0
def delete(self) -> None: def delete(self) -> None:
"""Deletes this account. """Deletes this account.
@ -381,11 +391,11 @@ class Currency(db.Model):
:return: The name in the current locale. :return: The name in the current locale.
""" """
current_locale = str(get_locale()) current_locale: Locale = get_locale()
if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]: if current_locale == get_babel().instance.default_locale:
return self.name_l10n return self.name_l10n
for l10n in self.l10n: for l10n in self.l10n:
if l10n.locale == current_locale: if l10n.locale == str(current_locale):
return l10n.name return l10n.name
return self.name_l10n return self.name_l10n
@ -399,15 +409,15 @@ class Currency(db.Model):
if self.name_l10n is None: if self.name_l10n is None:
self.name_l10n = value self.name_l10n = value
return return
current_locale = str(get_locale()) current_locale: Locale = get_locale()
if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]: if current_locale == get_babel().instance.default_locale:
self.name_l10n = value self.name_l10n = value
return return
for l10n in self.l10n: for l10n in self.l10n:
if l10n.locale == current_locale: if l10n.locale == str(current_locale):
l10n.name = value l10n.name = value
return return
self.l10n.append(CurrencyL10n(locale=current_locale, name=value)) self.l10n.append(CurrencyL10n(locale=str(current_locale), name=value))
@property @property
def is_modified(self) -> bool: def is_modified(self) -> bool:
@ -422,6 +432,17 @@ class Currency(db.Model):
return True return True
return False return False
@property
def can_delete(self) -> bool:
"""Returns whether the currency can be deleted.
:return: True if the currency can be deleted, or False otherwise.
"""
from accounting.template_globals import default_currency_code
if self.code == default_currency_code():
return False
return len(self.line_items) == 0
def delete(self) -> None: def delete(self) -> None:
"""Deletes the currency. """Deletes the currency.
@ -597,14 +618,10 @@ class JournalEntry(db.Model):
:return: True if the journal entry can be deleted, or False otherwise. :return: True if the journal entry can be deleted, or False otherwise.
""" """
if not hasattr(self, "__can_delete"): for line_item in self.line_items:
def has_offset() -> bool: if len(line_item.offsets) > 0:
for line_item in self.line_items:
if len(line_item.offsets) > 0:
return True
return False return False
setattr(self, "__can_delete", not has_offset()) return True
return getattr(self, "__can_delete")
def delete(self) -> None: def delete(self) -> None:
"""Deletes the journal entry. """Deletes the journal entry.

View File

@ -71,8 +71,8 @@ def default_ie_account_code() -> str:
:return: The default account code for the income and expenses log. :return: The default account code for the income and expenses log.
""" """
with current_app.app_context(): return current_app.config.get("ACCOUNTING_DEFAULT_IE_ACCOUNT",
return current_app.config.get("DEFAULT_IE_ACCOUNT", Account.CASH_CODE) Account.CASH_CODE)
def default_ie_account() -> IncomeExpensesAccount: def default_ie_account() -> IncomeExpensesAccount:

View File

@ -72,11 +72,14 @@
} }
} }
@media(max-width:767px) { @media(max-width:767px) {
.accounting-toolbar {
width: 100%;
justify-content: space-evenly;
}
.accounting-toolbar > .btn:not(form), .accounting-toolbar > .btn-group > .btn { .accounting-toolbar > .btn:not(form), .accounting-toolbar > .btn-group > .btn {
height: 3.2rem; height: 3.2rem;
width: 3.2rem; width: 3.2rem;
border-radius: 50%; border-radius: 50%;
margin-left: 1rem;
} }
.accounting-toolbar > a.btn, .accounting-toolbar > .btn-group > a.btn { .accounting-toolbar > a.btn, .accounting-toolbar > .btn-group > a.btn {
padding-top: 0.7rem; padding-top: 0.7rem;
@ -315,6 +318,22 @@ a.accounting-report-table-row {
padding-left: 1rem; padding-left: 1rem;
} }
/* The description editor */
.accounting-description-editor-buttons .btn {
margin-bottom: 0.3rem;
}
/* The order of the journal entries in a same day */
.accounting-journal-entry-order-item, .accounting-journal-entry-order-item:hover {
color: inherit;
text-decoration: none;
}
.accounting-journal-entry-order-item-currency {
margin-left: 0.5rem;
border-top: thin solid lightgray;
margin-top: 0.2rem;
}
/* The Material Design text field (floating form control in Bootstrap) */ /* The Material Design text field (floating form control in Bootstrap) */
.accounting-material-text-field { .accounting-material-text-field {
position: relative; position: relative;
@ -339,7 +358,7 @@ a.accounting-report-table-row {
.accounting-material-fab { .accounting-material-fab {
position: fixed; position: fixed;
right: 2rem; right: 2rem;
bottom: 1rem; bottom: 2rem;
z-index: 10; z-index: 10;
flex-direction: column-reverse; flex-direction: column-reverse;
} }

View File

@ -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
@ -102,7 +102,7 @@ class DescriptionEditor {
/** /**
* The tab planes * The tab planes
* @type {{general: GeneralTagTab, travel: GeneralTripTab, bus: BusTripTab, regular: RegularPaymentTab, annotation: AnnotationTab}} * @type {{general: GeneralTagTab, travel: GeneralTripTab, bus: BusTripTab, recurring: RecurringTransactionTab, annotation: AnnotationTab}}
*/ */
tabPlanes = {}; tabPlanes = {};
@ -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);
@ -125,14 +125,14 @@ class DescriptionEditor {
// noinspection JSValidateTypes // noinspection JSValidateTypes
this.#accountButtons = Array.from(document.getElementsByClassName(this.prefix + "-account")); this.#accountButtons = Array.from(document.getElementsByClassName(this.prefix + "-account"));
for (const cls of [GeneralTagTab, GeneralTripTab, BusTripTab, RegularPaymentTab, AnnotationTab]) { for (const cls of [GeneralTagTab, GeneralTripTab, BusTripTab, RecurringTransactionTab, AnnotationTab]) {
const tab = new cls(this); const tab = new cls(this);
this.tabPlanes[tab.tabId()] = tab; this.tabPlanes[tab.tabId()] = tab;
} }
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;
} }
@ -985,17 +966,23 @@ class BusTripTab extends TagTabPlane {
} }
/** /**
* The regular payment tab plane. * The recurring transaction tab plane.
* *
* @private * @private
*/ */
class RegularPaymentTab extends TabPlane { class RecurringTransactionTab extends TabPlane {
/** /**
* The payment buttons * The month names
* @type {string[]}
*/
#monthNames;
/**
* The buttons of the recurring items
* @type {HTMLButtonElement[]} * @type {HTMLButtonElement[]}
*/ */
#payments; #itemButtons;
// noinspection JSValidateTypes // noinspection JSValidateTypes
/** /**
@ -1006,8 +993,44 @@ class RegularPaymentTab 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.#payments = Array.from(document.getElementsByClassName(this.prefix + "-payment")); 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]);
} }
/** /**
@ -1017,7 +1040,7 @@ class RegularPaymentTab extends TabPlane {
* @abstract * @abstract
*/ */
tabId() { tabId() {
return "regular"; return "recurring";
}; };
/** /**
@ -1026,9 +1049,9 @@ class RegularPaymentTab extends TabPlane {
* @override * @override
*/ */
reset() { reset() {
for (const payment of this.#payments) { for (const itemButton of this.#itemButtons) {
payment.classList.remove("btn-primary"); itemButton.classList.remove("btn-primary");
payment.classList.add("btn-outline-primary"); itemButton.classList.add("btn-outline-primary");
} }
} }
@ -1039,9 +1062,32 @@ class RegularPaymentTab 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.
* *

View File

@ -35,5 +35,4 @@ def default_currency_code() -> str:
:return: The default currency code. :return: The default currency code.
""" """
with current_app.app_context(): return current_app.config.get("ACCOUNTING_DEFAULT_CURRENCY", "USD")
return current_app.config.get("DEFAULT_CURRENCY", "USD")

View File

@ -25,26 +25,33 @@ First written: 2023/1/31
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="mb-3 accounting-toolbar">
<a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}"> <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}
</a> </a>
{% if accounting_can_edit() %} {% if accounting_can_edit() %}
<a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}"> <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
<i class="fa-solid fa-gear"></i> <i class="fa-solid fa-pen-to-square"></i>
{{ A_("Settings") }} {{ A_("Edit") }}
</a> </a>
{% endif %} {% endif %}
<a class="btn btn-primary" href="{{ url_for("accounting.account.order", base=obj.base)|accounting_append_next }}"> <a class="btn btn-primary" href="{{ url_for("accounting.account.order", base=obj.base)|accounting_append_next }}">
<i class="fa-solid fa-bars-staggered"></i> <i class="fa-solid fa-bars-staggered"></i>
{{ A_("Order") }} <span class="d-none d-md-inline">{{ A_("Order") }}</span>
</a> </a>
{% if accounting_can_edit() %} {% if accounting_can_edit() %}
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal"> {% if obj.can_delete %}
<i class="fa-solid fa-trash"></i> <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
{{ A_("Delete") }} <i class="fa-solid fa-trash"></i>
</button> <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
</button>
{% else %}
<button class="btn btn-secondary" type="button" disabled="disabled">
<i class="fa-solid fa-trash"></i>
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
</button>
{% endif %}
{% endif %} {% endif %}
</div> </div>
@ -56,7 +63,7 @@ First written: 2023/1/31
</div> </div>
{% endif %} {% endif %}
{% if accounting_can_edit() %} {% if accounting_can_edit() and obj.can_delete %}
<form action="{{ url_for("accounting.account.delete", account=obj) }}" method="post"> <form action="{{ url_for("accounting.account.delete", account=obj) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if request.args.next %} {% if request.args.next %}

View File

@ -27,7 +27,7 @@ First written: 2023/2/1
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="mb-3 d-none d-md-block">
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}"> <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}

View File

@ -30,7 +30,7 @@ First written: 2023/2/2
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="mb-3 d-none d-md-block">
<a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}"> <a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}

View File

@ -25,7 +25,7 @@ First written: 2023/2/1
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="mb-3 d-none d-md-block">
<a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}"> <a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}

View File

@ -25,22 +25,29 @@ First written: 2023/2/6
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="mb-3 accounting-toolbar">
<a class="btn btn-primary" href="{{ url_for("accounting.currency.list")|accounting_or_next }}"> <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.currency.list")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}
</a> </a>
{% if accounting_can_edit() %} {% if accounting_can_edit() %}
<a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.currency.edit", currency=obj)|accounting_inherit_next }}"> <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.currency.edit", currency=obj)|accounting_inherit_next }}">
<i class="fa-solid fa-gear"></i> <i class="fa-solid fa-pen-to-square"></i>
{{ A_("Settings") }} {{ A_("Edit") }}
</a> </a>
{% endif %} {% endif %}
{% if accounting_can_edit() %} {% if accounting_can_edit() %}
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal"> {% if obj.can_delete %}
<i class="fa-solid fa-trash"></i> <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
{{ A_("Delete") }} <i class="fa-solid fa-trash"></i>
</button> <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
</button>
{% else %}
<button class="btn btn-secondary" type="button" disabled="disabled">
<i class="fa-solid fa-trash"></i>
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
</button>
{% endif %}
{% endif %} {% endif %}
</div> </div>
@ -52,7 +59,7 @@ First written: 2023/2/6
</div> </div>
{% endif %} {% endif %}
{% if accounting_can_edit() %} {% if accounting_can_edit() and obj.can_delete %}
<form action="{{ url_for("accounting.currency.delete", currency=obj) }}" method="post"> <form action="{{ url_for("accounting.currency.delete", currency=obj) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if request.args.next %} {% if request.args.next %}

View File

@ -27,7 +27,7 @@ First written: 2023/2/6
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="mb-3 d-none d-md-block">
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}"> <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}

View File

@ -21,10 +21,10 @@ First written: 2023/2/26
#} #}
{% extends "accounting/journal-entry/include/detail.html" %} {% extends "accounting/journal-entry/include/detail.html" %}
{% block to_transfer %} {% block as_trasfer %}
<a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_journal_entry_to_transfer|accounting_inherit_next }}"> <a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_journal_entry_to_transfer|accounting_inherit_next }}">
<i class="fa-solid fa-bars-staggered"></i> <i class="fa-solid fa-table-columns"></i>
{{ A_("To Transfer") }} <span class="d-none d-md-inline">{{ A_("As Transfer") }}</span>
</a> </a>
{% endblock %} {% endblock %}

View File

@ -19,57 +19,15 @@ form-currency.html: The currency sub-form in the cash disbursement journal entry
Author: imacat@mail.imacat.idv.tw (imacat) Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/25 First written: 2023/2/25
#} #}
<div id="accounting-currency-{{ currency_index }}" class="mb-3 accounting-currency" data-index="{{ currency_index }}"> {% extends "accounting/journal-entry/include/form-currency.html" %}
<input id="accounting-currency-{{ currency_index }}-no" type="hidden" name="currency-{{ currency_index }}-no" value="{{ currency_index }}">
<input id="accounting-currency-{{ currency_index }}-code" type="hidden" name="currency-{{ currency_index }}-code" value="{{ currency_code_data }}">
<div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}">
<div class="d-flex justify-content-between mt-2 mb-3">
<div class="form-floating accounting-currency-content">
<select id="accounting-currency-{{ currency_index }}-code-select" class="form-select {% if currency_code_errors %} is-invalid {% endif %}" name="currency-{{ currency_index }}-code" {% if currency_code_is_locked %} disabled="disabled" {% endif %}>
{% for currency in accounting_currency_options() %}
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
{% endfor %}
</select>
<label class="form-label" for="accounting-currency-{{ currency_index }}-code-select">{{ A_("Currency") }}</label>
<div id="accounting-currency-{{ currency_index }}-code-error" class="invalid-feedback">{% if currency_code_errors %}{{ currency_code_errors[0] }}{% endif %}</div>
</div>
<div> {% block line_items %}
<button id="accounting-currency-{{ currency_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_currency_form %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}"> {% with currency_index = currency_index,
<i class="fas fa-minus"></i> debit_credit = "debit",
</button> line_item_forms = debit_forms,
</div> header = A_("Content"),
</div> debit_credit_total = debit_total,
debit_credit_errors = debit_errors %}
<div class="mb-3"> {% include "accounting/journal-entry/include/form-debit-credit.html" %}
<div id="accounting-currency-{{ currency_index }}-debit" class="form-control accounting-material-text-field accounting-not-empty {% if debit_errors %} is-invalid {% endif %}"> {% endwith %}
<label class="form-label" for="accounting-currency-{{ currency_index }}-debit">{{ A_("Content") }}</label> {% endblock %}
<ul id="accounting-currency-{{ currency_index }}-debit-list" class="list-group accounting-line-item-list">
{% for line_item_form in debit_forms %}
{% with currency_index = currency_index,
debit_credit = "debit",
line_item_index = loop.index,
only_one_line_item_form = debit_forms|length == 1,
form = line_item_form.form %}
{% include "accounting/journal-entry/include/form-line-item.html" %}
{% endwith %}
{% endfor %}
</ul>
<div class="d-flex justify-content-between mb-2">
<div>{{ A_("Total") }}</div>
<div><span id="accounting-currency-{{ currency_index }}-debit-total" class="badge rounded-pill bg-primary">{{ debit_total }}</span></div>
</div>
<div>
<button id="accounting-currency-{{ currency_index }}-debit-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="debit" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
<i class="fas fa-plus"></i>
{{ A_("New") }}
</button>
</div>
</div>
<div id="accounting-currency-{{ currency_index }}-debit-error" class="invalid-feedback">{% if debit_errors %}{{ debit_errors[0] }}{% endif %}</div>
</div>
</div>
<div id="accounting-currency-{{ currency_index }}-error" class="invalid-feedback">{% if currency_errors %}{{ currency_errors[0] }}{% endif %}</div>
</div>

View File

@ -32,7 +32,7 @@ First written: 2023/2/25
currency_code_is_locked = currency_form.is_code_locked, currency_code_is_locked = currency_form.is_code_locked,
debit_forms = currency_form.debit, debit_forms = currency_form.debit,
debit_errors = currency_form.debit_errors, debit_errors = currency_form.debit_errors,
debit_total = currency_form.form.debit_total|accounting_format_amount %} debit_total = currency_form.debit_total|accounting_format_amount %}
{% include "accounting/journal-entry/disbursement/include/form-currency.html" %} {% include "accounting/journal-entry/disbursement/include/form-currency.html" %}
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}

View File

@ -55,8 +55,8 @@ First written: 2023/2/28
</span> </span>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<span id="accounting-description-editor-{{ description_editor.debit_credit }}-regular-tab" class="nav-link accounting-clickable" aria-current="false"> <span id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab" class="nav-link accounting-clickable" aria-current="false">
{{ A_("Regular") }} {{ A_("Recurring") }}
</span> </span>
</li> </li>
<li class="nav-item"> <li class="nav-item">
@ -74,7 +74,7 @@ First written: 2023/2/28
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag-error" class="invalid-feedback"></div> <div id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag-error" class="invalid-feedback"></div>
</div> </div>
<div> <div class="accounting-description-editor-buttons">
{% for tag in description_editor.general.tags %} {% for tag in description_editor.general.tags %}
<button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-general-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}"> <button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-general-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
{{ tag }} {{ tag }}
@ -91,7 +91,7 @@ First written: 2023/2/28
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag-error" class="invalid-feedback"></div> <div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag-error" class="invalid-feedback"></div>
</div> </div>
<div> <div class="accounting-description-editor-buttons">
{% for tag in description_editor.travel.tags %} {% for tag in description_editor.travel.tags %}
<button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-travel-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}"> <button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-travel-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
{{ tag }} {{ tag }}
@ -99,7 +99,7 @@ First written: 2023/2/28
{% endfor %} {% endfor %}
</div> </div>
<div class="d-flex justify-content-between mt-2"> <div class="d-flex justify-content-between">
<div class="form-floating"> <div class="form-floating">
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from" class="form-control" type="text" value="" placeholder=" "> <input id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from" class="form-control" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from">{{ A_("From") }}</label> <label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from">{{ A_("From") }}</label>
@ -132,7 +132,7 @@ First written: 2023/2/28
</div> </div>
</div> </div>
<div> <div class="accounting-description-editor-buttons">
{% for tag in description_editor.bus.tags %} {% for tag in description_editor.bus.tags %}
<button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-bus-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}"> <button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-bus-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
{{ tag }} {{ tag }}
@ -140,7 +140,7 @@ First written: 2023/2/28
{% endfor %} {% endfor %}
</div> </div>
<div class="d-flex justify-content-between mt-2"> <div class="d-flex justify-content-between">
<div class="form-floating me-2"> <div class="form-floating me-2">
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from" class="form-control" type="text" value="" placeholder=" "> <input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from" class="form-control" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from">{{ A_("From") }}</label> <label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from">{{ A_("From") }}</label>
@ -154,9 +154,15 @@ First written: 2023/2/28
</div> </div>
</div> </div>
{# A regular income or payment #} {# A recurring transaction #}
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-regular-page" class="d-none" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-regular-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 #} <div class="accounting-description-editor-buttons">
{% 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> </div>
{# The annotation #} {# The annotation #}

View File

@ -25,32 +25,32 @@ First written: 2023/2/26
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="mb-3 accounting-toolbar">
<a class="btn btn-primary" href="{{ url_for("accounting.report.default")|accounting_or_next }}"> <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.report.default")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}
</a> </a>
{% if accounting_can_edit() %} {% if accounting_can_edit() %}
<a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_inherit_next }}"> <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_inherit_next }}">
<i class="fa-solid fa-gear"></i> <i class="fa-solid fa-pen-to-square"></i>
{{ A_("Settings") }} {{ A_("Edit") }}
</a> </a>
{% endif %} {% endif %}
<a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.order", journal_entry_date=obj.date)|accounting_append_next }}"> <a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.order", journal_entry_date=obj.date)|accounting_append_next }}">
<i class="fa-solid fa-bars-staggered"></i> <i class="fa-solid fa-bars-staggered"></i>
{{ A_("Order") }} <span class="d-none d-md-inline">{{ A_("Order") }}</span>
</a> </a>
{% if accounting_can_edit() %} {% if accounting_can_edit() %}
{% block to_transfer %}{% endblock %} {% block as_trasfer %}{% endblock %}
{% if obj.can_delete %} {% if obj.can_delete %}
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal"> <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-trash"></i>
{{ A_("Delete") }} <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
</button> </button>
{% else %} {% else %}
<button class="btn btn-secondary" type="button" disabled="disabled"> <button class="btn btn-secondary" type="button" disabled="disabled">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-trash"></i>
{{ A_("Delete") }} <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
</button> </button>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@ -0,0 +1,47 @@
{#
The Mia! Accounting Flask Project
form-currency.html: The currency sub-form in the journal entry form
Copyright (c) 2023 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/21
#}
<div id="accounting-currency-{{ currency_index }}" class="mb-3 accounting-currency" data-index="{{ currency_index }}">
<input id="accounting-currency-{{ currency_index }}-no" type="hidden" name="currency-{{ currency_index }}-no" value="{{ currency_index }}">
<input id="accounting-currency-{{ currency_index }}-code" type="hidden" name="currency-{{ currency_index }}-code" value="{{ currency_code_data }}">
<div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}">
<div class="d-flex justify-content-between mt-2 mb-3">
<div class="form-floating accounting-currency-content">
<select id="accounting-currency-{{ currency_index }}-code-select" class="form-select {% if currency_code_errors %} is-invalid {% endif %}" {% if currency_code_is_locked %} disabled="disabled" {% endif %}>
{% for currency in accounting_currency_options() %}
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
{% endfor %}
</select>
<label class="form-label" for="accounting-currency-{{ currency_index }}-code-select">{{ A_("Currency") }}</label>
<div id="accounting-currency-{{ currency_index }}-code-error" class="invalid-feedback">{% if currency_code_errors %}{{ currency_code_errors[0] }}{% endif %}</div>
</div>
<div>
<button id="accounting-currency-{{ currency_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_currency_form %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}">
<i class="fas fa-minus"></i>
</button>
</div>
</div>
{% block line_items %}{% endblock %}
</div>
<div id="accounting-currency-{{ currency_index }}-error" class="invalid-feedback">{% if currency_errors %}{{ currency_errors[0] }}{% endif %}</div>
</div>

View File

@ -0,0 +1,49 @@
{#
The Mia! Accounting Flask Project
form-debit-credit.html: The debit or credit line items in the journal entry form
Copyright (c) 2023 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/21
#}
<div class="mb-2">
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}" class="form-control accounting-material-text-field accounting-not-empty {% if debit_errors %} is-invalid {% endif %}">
<label class="form-label" for="accounting-currency-{{ currency_index }}-{{ debit_credit }}">{{ header }}</label>
<ul id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-list" class="list-group accounting-line-item-list">
{% for line_item_form in line_item_forms %}
{% with currency_index = currency_index,
line_item_index = loop.index,
only_one_line_item_form = line_item_forms|length == 1,
form = line_item_form.form %}
{% include "accounting/journal-entry/include/form-line-item.html" %}
{% endwith %}
{% endfor %}
</ul>
<div class="d-flex justify-content-between mb-2">
<div>{{ A_("Total") }}</div>
<div><span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-total" class="badge rounded-pill bg-primary">{{ debit_credit_total }}</span></div>
</div>
<div>
<button id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="{{ debit_credit }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
<i class="fas fa-plus"></i>
{{ A_("New") }}
</button>
</div>
</div>
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-error" class="invalid-feedback">{% if debit_credit_errors %}{{ debit_credit_errors[0] }}{% endif %}</div>
</div>

View File

@ -32,10 +32,10 @@ First written: 2023/2/26
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="mb-3 d-none d-md-block">
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}"> <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} <span class="d-none d-md-inline">{{ A_("Back") }}</span>
</a> </a>
</div> </div>

View File

@ -0,0 +1,57 @@
{#
The Mia! Accounting Flask Project
order-journal-entry.html: The journal entry in the journal entry order page
Copyright (c) 2023 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/21
#}
<a class="small w-100 accounting-journal-entry-order-item" href="{{ url_for("accounting.journal-entry.detail", journal_entry=journal_entry)|accounting_append_next }}">
<div>
{{ journal_entry.date|accounting_format_date }}
{% if journal_entry.is_cash_disbursement %}
{{ A_("Cash Disbursement") }}
{% elif journal_entry.is_cash_receipt %}
{{ A_("Cash Receipt") }}
{% else %}
{{ A_("Transfer") }}
{% endif %}
</div>
{% for currency in journal_entry.currencies %}
<div class="d-flex justify-content-between accounting-journal-entry-order-item-currency">
<div>
{% if not journal_entry.is_cash_receipt %}
{% for line_item in currency.debit %}
<div>{{ line_item.description|accounting_default }}</div>
{% endfor %}
{% endif %}
{% if not journal_entry.is_cash_disbursement %}
{% for line_item in currency.credit %}
<div class="accounting-mobile-journal-credit">{{ line_item.description|accounting_default }}</div>
{% endfor %}
{% endif %}
</div>
<div>
<span class="badge bg-info rounded-pill">
{% if currency.code != accounting_default_currency_code() %}
{{ currency.code }}
{% endif %}
{{ currency.debit_total|accounting_format_amount }}
</span>
</div>
</div>
{% endfor %}
</a>

View File

@ -30,7 +30,7 @@ First written: 2023/2/26
{% block content %} {% block content %}
<div class="btn-group mb-3"> <div class="mb-3 d-none d-md-block">
<a class="btn btn-primary" href="{{ url_for("accounting.report.default")|accounting_or_next }}"> <a class="btn btn-primary" href="{{ url_for("accounting.report.default")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i> <i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }} {{ A_("Back") }}
@ -47,9 +47,9 @@ First written: 2023/2/26
{% for item in list %} {% for item in list %}
<li class="list-group-item d-flex justify-content-between" data-id="{{ item.id }}"> <li class="list-group-item d-flex justify-content-between" data-id="{{ item.id }}">
<input id="accounting-order-{{ item.id }}-no" type="hidden" name="{{ item.id }}-no" value="{{ loop.index }}"> <input id="accounting-order-{{ item.id }}-no" type="hidden" name="{{ item.id }}-no" value="{{ loop.index }}">
<div> {% with journal_entry = item %}
{{ item }} {% include "accounting/journal-entry/include/order-journal-entry.html" %}
</div> {% endwith %}
<i class="fa-solid fa-bars"></i> <i class="fa-solid fa-bars"></i>
</li> </li>
{% endfor %} {% endfor %}
@ -72,7 +72,9 @@ First written: 2023/2/26
<ul class="list-group mb-3"> <ul class="list-group mb-3">
{% for item in list %} {% for item in list %}
<li class="list-group-item"> <li class="list-group-item">
{{ item }} {% with journal_entry = item %}
{% include "accounting/journal-entry/include/order-journal-entry.html" %}
{% endwith %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -21,10 +21,10 @@ First written: 2023/2/26
#} #}
{% extends "accounting/journal-entry/include/detail.html" %} {% extends "accounting/journal-entry/include/detail.html" %}
{% block to_transfer %} {% block as_trasfer %}
<a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_journal_entry_to_transfer|accounting_inherit_next }}"> <a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_journal_entry_to_transfer|accounting_inherit_next }}">
<i class="fa-solid fa-bars-staggered"></i> <i class="fa-solid fa-bars-staggered"></i>
{{ A_("To Transfer") }} <span class="d-none d-md-inline">{{ A_("As Transfer") }}</span>
</a> </a>
{% endblock %} {% endblock %}

View File

@ -19,57 +19,15 @@ form-currency.html: The currency sub-form in the cash receipt journal entry form
Author: imacat@mail.imacat.idv.tw (imacat) Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/25 First written: 2023/2/25
#} #}
<div id="accounting-currency-{{ currency_index }}" class="mb-3 accounting-currency" data-index="{{ currency_index }}"> {% extends "accounting/journal-entry/include/form-currency.html" %}
<input id="accounting-currency-{{ currency_index }}-no" type="hidden" name="currency-{{ currency_index }}-no" value="{{ currency_index }}">
<input id="accounting-currency-{{ currency_index }}-code" type="hidden" name="currency-{{ currency_index }}-code" value="{{ currency_code_data }}">
<div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}">
<div class="d-flex justify-content-between mt-2 mb-3">
<div class="form-floating accounting-currency-content">
<select id="accounting-currency-{{ currency_index }}-code-select" class="form-select {% if currency_code_errors %} is-invalid {% endif %}" name="currency-{{ currency_index }}-code" {% if currency_code_is_locked %} disabled="disabled" {% endif %}>
{% for currency in accounting_currency_options() %}
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
{% endfor %}
</select>
<label class="form-label" for="accounting-currency-{{ currency_index }}-code-select">{{ A_("Currency") }}</label>
<div id="accounting-currency-{{ currency_index }}-code-error" class="invalid-feedback">{% if currency_code_errors %}{{ currency_code_errors[0] }}{% endif %}</div>
</div>
<div> {% block line_items %}
<button id="accounting-currency-{{ currency_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_currency_form %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}"> {% with currency_index = currency_index,
<i class="fas fa-minus"></i> debit_credit = "credit",
</button> line_item_forms = credit_forms,
</div> header = A_("Content"),
</div> debit_credit_total = credit_total,
debit_credit_errors = credit_errors %}
<div class="mb-3"> {% include "accounting/journal-entry/include/form-debit-credit.html" %}
<div id="accounting-currency-{{ currency_index }}-credit" class="form-control accounting-material-text-field accounting-not-empty {% if credit_errors %} is-invalid {% endif %}"> {% endwith %}
<label class="form-label" for="accounting-currency-{{ currency_index }}-credit">{{ A_("Content") }}</label> {% endblock %}
<ul id="accounting-currency-{{ currency_index }}-credit-list" class="list-group accounting-line-item-list">
{% for line_item_form in credit_forms %}
{% with currency_index = currency_index,
debit_credit = "credit",
line_item_index = loop.index,
only_one_line_item_form = credit_forms|length == 1,
form = line_item_form.form %}
{% include "accounting/journal-entry/include/form-line-item.html" %}
{% endwith %}
{% endfor %}
</ul>
<div class="d-flex justify-content-between mb-2">
<div>{{ A_("Total") }}</div>
<div><span id="accounting-currency-{{ currency_index }}-credit-total" class="badge rounded-pill bg-primary">{{ credit_total }}</span></div>
</div>
<div>
<button id="accounting-currency-{{ currency_index }}-credit-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="credit" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
<i class="fas fa-plus"></i>
{{ A_("New") }}
</button>
</div>
</div>
<div id="accounting-currency-{{ currency_index }}-credit-error" class="invalid-feedback">{% if credit_errors %}{{ credit_errors[0] }}{% endif %}</div>
</div>
</div>
<div id="accounting-currency-{{ currency_index }}-error" class="invalid-feedback">{% if currency_errors %}{{ currency_errors[0] }}{% endif %}</div>
</div>

View File

@ -32,7 +32,7 @@ First written: 2023/2/25
currency_code_is_locked = currency_form.is_code_locked, currency_code_is_locked = currency_form.is_code_locked,
credit_forms = currency_form.credit, credit_forms = currency_form.credit,
credit_errors = currency_form.credit_errors, credit_errors = currency_form.credit_errors,
credit_total = currency_form.form.credit_total|accounting_format_amount %} credit_total = currency_form.credit_total|accounting_format_amount %}
{% include "accounting/journal-entry/receipt/include/form-currency.html" %} {% include "accounting/journal-entry/receipt/include/form-currency.html" %}
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}

View File

@ -19,91 +19,30 @@ form-currency.html: The currency sub-form in the transfer journal entry form
Author: imacat@mail.imacat.idv.tw (imacat) Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/25 First written: 2023/2/25
#} #}
<div id="accounting-currency-{{ currency_index }}" class="mb-3 accounting-currency" data-index="{{ currency_index }}"> {% extends "accounting/journal-entry/include/form-currency.html" %}
<input id="accounting-currency-{{ currency_index }}-no" type="hidden" name="currency-{{ currency_index }}-no" value="{{ currency_index }}">
<input id="accounting-currency-{{ currency_index }}-code" type="hidden" name="currency-{{ currency_index }}-code" value="{{ currency_code_data }}">
<div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}">
<div class="d-flex justify-content-between mt-2 mb-3">
<div class="form-floating accounting-currency-content">
<select id="accounting-currency-{{ currency_index }}-code-select" class="form-select {% if currency_code_errors %} is-invalid {% endif %}" {% if currency_code_is_locked %} disabled="disabled" {% endif %}>
{% for currency in accounting_currency_options() %}
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
{% endfor %}
</select>
<label class="form-label" for="accounting-currency-{{ currency_index }}-code-select">{{ A_("Currency") }}</label>
<div id="accounting-currency-{{ currency_index }}-code-error" class="invalid-feedback">{% if currency_code_errors %}{{ currency_code_errors[0] }}{% endif %}</div>
</div>
<div> {% block line_items %}
<button id="accounting-currency-{{ currency_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_currency_form %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}"> <div class="row">
<i class="fas fa-minus"></i> <div class="col-sm-6">
</button> {% with currency_index = currency_index,
</div> debit_credit = "debit",
</div> line_item_forms = debit_forms,
header = A_("Debit"),
<div class="row"> debit_credit_total = debit_total,
{# The debit line items #} debit_credit_errors = debit_errors %}
<div class="col-sm-6 mb-3"> {% include "accounting/journal-entry/include/form-debit-credit.html" %}
<div id="accounting-currency-{{ currency_index }}-debit" class="form-control accounting-material-text-field accounting-not-empty {% if debit_errors %} is-invalid {% endif %}"> {% endwith %}
<label class="form-label" for="accounting-currency-{{ currency_index }}-debit">{{ A_("Debit") }}</label> </div>
<ul id="accounting-currency-{{ currency_index }}-debit-list" class="list-group accounting-line-item-list">
{% for line_item_form in debit_forms %} <div class="col-sm-6">
{% with currency_index = currency_index, {% with currency_index = currency_index,
debit_credit = "debit", debit_credit = "credit",
line_item_index = loop.index, line_item_forms = credit_forms,
only_one_line_item_form = debit_forms|length == 1, header = A_("Credit"),
form = line_item_form.form %} debit_credit_total = credit_total,
{% include "accounting/journal-entry/include/form-line-item.html" %} debit_credit_errors = credit_errors %}
{% endwith %} {% include "accounting/journal-entry/include/form-debit-credit.html" %}
{% endfor %} {% endwith %}
</ul>
<div class="d-flex justify-content-between mb-2">
<div>{{ A_("Total") }}</div>
<div><span id="accounting-currency-{{ currency_index }}-debit-total" class="badge rounded-pill bg-primary">{{ debit_total }}</span></div>
</div>
<div>
<button id="accounting-currency-{{ currency_index }}-debit-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="debit" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
<i class="fas fa-plus"></i>
{{ A_("New") }}
</button>
</div>
</div>
<div id="accounting-currency-{{ currency_index }}-debit-error" class="invalid-feedback">{% if debit_errors %}{{ debit_errors[0] }}{% endif %}</div>
</div>
{# The credit line items #}
<div class="col-sm-6 mb-3">
<div id="accounting-currency-{{ currency_index }}-credit" class="form-control accounting-material-text-field accounting-not-empty {% if credit_errors %} is-invalid {% endif %}">
<label class="form-label" for="accounting-currency-{{ currency_index }}-credit">{{ A_("Credit") }}</label>
<ul id="accounting-currency-{{ currency_index }}-credit-list" class="list-group accounting-line-item-list">
{% for line_item_form in credit_forms %}
{% with currency_index = currency_index,
debit_credit = "credit",
line_item_index = loop.index,
only_one_line_item_form = credit_forms|length == 1,
form = line_item_form.form %}
{% include "accounting/journal-entry/include/form-line-item.html" %}
{% endwith %}
{% endfor %}
</ul>
<div class="d-flex justify-content-between mb-2">
<div>{{ A_("Total") }}</div>
<div><span id="accounting-currency-{{ currency_index }}-credit-total" class="badge rounded-pill bg-primary">{{ credit_total }}</span></div>
</div>
<div>
<button id="accounting-currency-{{ currency_index }}-credit-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="credit" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
<i class="fas fa-plus"></i>
{{ A_("New") }}
</button>
</div>
</div>
<div id="accounting-currency-{{ currency_index }}-credit-error" class="invalid-feedback">{% if credit_errors %}{{ credit_errors[0] }}{% endif %}</div>
</div>
</div>
</div> </div>
<div id="accounting-currency-{{ currency_index }}-error" class="invalid-feedback">{% if currency_errors %}{{ currency_errors[0] }}{% endif %}</div>
</div> </div>
{% endblock %}

View File

@ -32,10 +32,10 @@ First written: 2023/2/25
currency_code_is_locked = currency_form.is_code_locked, currency_code_is_locked = currency_form.is_code_locked,
debit_forms = currency_form.debit, debit_forms = currency_form.debit,
debit_errors = currency_form.debit_errors, debit_errors = currency_form.debit_errors,
debit_total = currency_form.form.debit_total|accounting_format_amount, debit_total = currency_form.debit_total|accounting_format_amount,
credit_forms = currency_form.credit, credit_forms = currency_form.credit,
credit_errors = currency_form.credit_errors, credit_errors = currency_form.credit_errors,
credit_total = currency_form.form.credit_total|accounting_format_amount %} credit_total = currency_form.credit_total|accounting_format_amount %}
{% include "accounting/journal-entry/transfer/include/form-currency.html" %} {% include "accounting/journal-entry/transfer/include/form-currency.html" %}
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}

View File

@ -45,11 +45,11 @@ First written: 2023/3/8
</div> </div>
{% endif %} {% endif %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <button id="accounting-choose-report" class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-book"></i> <i class="fa-solid fa-book"></i>
<span class="d-none d-md-inline">{{ report.report_chooser.current_report }}</span> <span class="d-none d-md-inline">{{ A_("Report") }}</span>
</button> </button>
<ul class="dropdown-menu" aria-label="{{ A_("Report") }}"> <ul class="dropdown-menu" aria-labelledby="accounting-choose-report">
{% for report in report.report_chooser %} {% for report in report.report_chooser %}
<li> <li>
<a class="dropdown-item {% if report.is_active %} active {% endif %}" href="{{ report.url }}"> <a class="dropdown-item {% if report.is_active %} active {% endif %}" href="{{ report.url }}">
@ -68,11 +68,11 @@ First written: 2023/3/8
</div> </div>
{% if use_currency_chooser %} {% if use_currency_chooser %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <button id="accounting-choose-currency" class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-money-bill-wave"></i> <i class="fa-solid fa-money-bill-wave"></i>
<span class="d-none d-md-inline">{{ report.currency.name|title }}</span> <span class="d-none d-md-inline">{{ A_("Currency") }}</span>
</button> </button>
<ul class="dropdown-menu" aria-label="{{ A_("Currency") }}"> <ul class="dropdown-menu" aria-labelledby="accounting-choose-currency">
{% for currency in report.currency_options %} {% for currency in report.currency_options %}
<li> <li>
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}"> <a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
@ -85,11 +85,11 @@ First written: 2023/3/8
{% endif %} {% endif %}
{% if use_account_chooser %} {% if use_account_chooser %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <button id="accounting-choose-account" class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-clipboard"></i> <i class="fa-solid fa-clipboard"></i>
<span class="d-none d-md-inline">{{ report.account.title|title }}</span> <span class="d-none d-md-inline">{{ A_("Account") }}</span>
</button> </button>
<ul class="dropdown-menu" aria-label="{{ A_("Account") }}"> <ul class="dropdown-menu" aria-labelledby="accounting-choose-account">
{% for account in report.account_options %} {% for account in report.account_options %}
<li> <li>
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}"> <a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
@ -103,7 +103,7 @@ First written: 2023/3/8
{% if use_period_chooser %} {% if use_period_chooser %}
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal"> <button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
<i class="fa-solid fa-calendar-day"></i> <i class="fa-solid fa-calendar-day"></i>
<span class="d-none d-md-inline">{{ report.period.desc|title }}</span> <span class="d-none d-md-inline">{{ A_("Period") }}</span>
</button> </button>
{% endif %} {% endif %}
{% if report.has_data %} {% if report.has_data %}

View File

@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Mia! Accounting Flask 0.0.0\n" "Project-Id-Version: Mia! Accounting Flask 0.0.0\n"
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n" "Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
"POT-Creation-Date: 2023-03-20 23:51+0800\n" "POT-Creation-Date: 2023-03-22 01:47+0800\n"
"PO-Revision-Date: 2023-03-20 23:51+0800\n" "PO-Revision-Date: 2023-03-22 01:47+0800\n"
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n" "Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
"Language: zh_Hant\n" "Language: zh_Hant\n"
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n" "Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
@ -19,22 +19,22 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.12.1\n" "Generated-By: Babel 2.12.1\n"
#: src/accounting/models.py:538 #: src/accounting/models.py:559
#, python-format #, python-format
msgid "Cash Disbursement Journal Entry#%(id)s" msgid "Cash Disbursement Journal Entry#%(id)s"
msgstr "現金支出傳票#%(id)s" msgstr "現金支出傳票#%(id)s"
#: src/accounting/models.py:541 #: src/accounting/models.py:562
#, python-format #, python-format
msgid "Cash Receipt Journal Entry#%(id)s" msgid "Cash Receipt Journal Entry#%(id)s"
msgstr "現金收入傳票#%(id)s" msgstr "現金收入傳票#%(id)s"
#: src/accounting/models.py:542 #: src/accounting/models.py:563
#, python-format #, python-format
msgid "Transfer Journal Entry#%(id)s" msgid "Transfer Journal Entry#%(id)s"
msgstr "轉帳傳票#%(id)s" msgstr "轉帳傳票#%(id)s"
#: src/accounting/models.py:679 #: src/accounting/models.py:696
#, python-format #, python-format
msgid "%(date)s %(description)s %(amount)s" msgid "%(date)s %(description)s %(amount)s"
msgstr "%(date)s %(description)s %(amount)s" msgstr "%(date)s %(description)s %(amount)s"
@ -86,7 +86,7 @@ msgstr "請填上標題。"
#: src/accounting/account/queries.py:50 #: src/accounting/account/queries.py:50
#: src/accounting/report/reports/search.py:101 #: src/accounting/report/reports/search.py:101
#: src/accounting/templates/accounting/account/detail.html:90 #: src/accounting/templates/accounting/account/detail.html:97
#: src/accounting/templates/accounting/account/list.html:62 #: src/accounting/templates/accounting/account/list.html:62
msgid "Needs Offset" msgid "Needs Offset"
msgstr "需抵銷" msgstr "需抵銷"
@ -103,17 +103,21 @@ msgstr "科目未異動。"
msgid "The account is updated successfully." msgid "The account is updated successfully."
msgstr "科目存好了。" msgstr "科目存好了。"
#: src/accounting/account/views.py:163 #: src/accounting/account/views.py:161
msgid "The account cannot be deleted."
msgstr "科目不可刪除。"
#: src/accounting/account/views.py:166
msgid "The account is deleted successfully." msgid "The account is deleted successfully."
msgstr "科目刪掉了" msgstr "科目刪掉了"
#: src/accounting/account/views.py:190 #: src/accounting/account/views.py:193
#: src/accounting/journal_entry/views.py:213 #: src/accounting/journal_entry/views.py:216
msgid "The order was not modified." msgid "The order was not modified."
msgstr "順序未異動。" msgstr "順序未異動。"
#: src/accounting/account/views.py:193 #: src/accounting/account/views.py:196
#: src/accounting/journal_entry/views.py:216 #: src/accounting/journal_entry/views.py:219
msgid "The order is updated successfully." msgid "The order is updated successfully."
msgstr "順序存好了。" msgstr "順序存好了。"
@ -154,7 +158,11 @@ msgstr "貨幣未異動。"
msgid "The currency is updated successfully." msgid "The currency is updated successfully."
msgstr "貨幣存好了。" msgstr "貨幣存好了。"
#: src/accounting/currency/views.py:165 #: src/accounting/currency/views.py:164
msgid "The currency cannot be deleted."
msgstr "貨幣不可刪除。"
#: src/accounting/currency/views.py:168
msgid "The currency is deleted successfully." msgid "The currency is deleted successfully."
msgstr "貨幣刪掉了" msgstr "貨幣刪掉了"
@ -170,7 +178,11 @@ msgstr "傳票未異動。"
msgid "The journal entry is updated successfully." msgid "The journal entry is updated successfully."
msgstr "傳票存好了。" msgstr "傳票存好了。"
#: src/accounting/journal_entry/views.py:181 #: src/accounting/journal_entry/views.py:179
msgid "The journal entry cannot be deleted."
msgstr "傳票不可刪除。"
#: src/accounting/journal_entry/views.py:184
msgid "The journal entry is deleted successfully." msgid "The journal entry is deleted successfully."
msgstr "傳票刪掉了" msgstr "傳票刪掉了"
@ -358,13 +370,10 @@ msgstr "全部"
#: src/accounting/report/reports/ledger.py:380 #: src/accounting/report/reports/ledger.py:380
#: src/accounting/report/reports/trial_balance.py:228 #: src/accounting/report/reports/trial_balance.py:228
#: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:43 #: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:43
#: src/accounting/templates/accounting/journal-entry/disbursement/include/form-currency.html:60 #: src/accounting/templates/accounting/journal-entry/include/form-debit-credit.html:37
#: src/accounting/templates/accounting/journal-entry/receipt/detail.html:43 #: src/accounting/templates/accounting/journal-entry/receipt/detail.html:43
#: src/accounting/templates/accounting/journal-entry/receipt/include/form-currency.html:60
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:39 #: src/accounting/templates/accounting/journal-entry/transfer/detail.html:39
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:55 #: src/accounting/templates/accounting/journal-entry/transfer/detail.html:55
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:62
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:93
#: src/accounting/templates/accounting/report/balance-sheet.html:59 #: src/accounting/templates/accounting/report/balance-sheet.html:59
#: src/accounting/templates/accounting/report/balance-sheet.html:71 #: src/accounting/templates/accounting/report/balance-sheet.html:71
#: src/accounting/templates/accounting/report/balance-sheet.html:81 #: src/accounting/templates/accounting/report/balance-sheet.html:81
@ -399,7 +408,7 @@ msgstr "日期"
#: src/accounting/report/reports/journal.py:156 #: src/accounting/report/reports/journal.py:156
#: src/accounting/report/reports/trial_balance.py:224 #: src/accounting/report/reports/trial_balance.py:224
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:57 #: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:57
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:92 #: src/accounting/templates/accounting/report/include/toolbar-buttons.html:90
#: src/accounting/templates/accounting/report/income-expenses.html:56 #: src/accounting/templates/accounting/report/income-expenses.html:56
#: src/accounting/templates/accounting/report/journal.html:55 #: src/accounting/templates/accounting/report/journal.html:55
#: src/accounting/templates/accounting/report/search.html:52 #: src/accounting/templates/accounting/report/search.html:52
@ -439,7 +448,7 @@ msgstr "餘額"
#: src/accounting/report/reports/income_expenses.py:416 #: src/accounting/report/reports/income_expenses.py:416
#: src/accounting/report/reports/journal.py:158 #: src/accounting/report/reports/journal.py:158
#: src/accounting/report/reports/ledger.py:368 #: src/accounting/report/reports/ledger.py:368
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:172 #: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:178
#: src/accounting/templates/accounting/journal-entry/include/form.html:73 #: src/accounting/templates/accounting/journal-entry/include/form.html:73
msgid "Note" msgid "Note"
msgstr "備註" msgstr "備註"
@ -475,10 +484,8 @@ msgid "Amount"
msgstr "金額" msgstr "金額"
#: src/accounting/report/reports/journal.py:155 #: src/accounting/report/reports/journal.py:155
#: src/accounting/templates/accounting/journal-entry/disbursement/include/form-currency.html:33 #: src/accounting/templates/accounting/journal-entry/include/form-currency.html:33
#: src/accounting/templates/accounting/journal-entry/receipt/include/form-currency.html:33 #: src/accounting/templates/accounting/report/include/toolbar-buttons.html:73
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:33
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:75
#: src/accounting/templates/accounting/report/journal.html:54 #: src/accounting/templates/accounting/report/journal.html:54
#: src/accounting/templates/accounting/report/search.html:51 #: src/accounting/templates/accounting/report/search.html:51
msgid "Currency" msgid "Currency"
@ -488,7 +495,7 @@ msgstr "貨幣"
#: src/accounting/report/reports/ledger.py:367 #: src/accounting/report/reports/ledger.py:367
#: src/accounting/report/reports/trial_balance.py:224 #: src/accounting/report/reports/trial_balance.py:224
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:33 #: src/accounting/templates/accounting/journal-entry/transfer/detail.html:33
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:48 #: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:30
#: src/accounting/templates/accounting/report/journal.html:57 #: src/accounting/templates/accounting/report/journal.html:57
#: src/accounting/templates/accounting/report/ledger.html:57 #: src/accounting/templates/accounting/report/ledger.html:57
#: src/accounting/templates/accounting/report/search.html:54 #: src/accounting/templates/accounting/report/search.html:54
@ -500,7 +507,7 @@ msgstr "借方"
#: src/accounting/report/reports/ledger.py:367 #: src/accounting/report/reports/ledger.py:367
#: src/accounting/report/reports/trial_balance.py:225 #: src/accounting/report/reports/trial_balance.py:225
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:49 #: src/accounting/templates/accounting/journal-entry/transfer/detail.html:49
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:79 #: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:41
#: src/accounting/templates/accounting/report/journal.html:58 #: src/accounting/templates/accounting/report/journal.html:58
#: src/accounting/templates/accounting/report/ledger.html:58 #: src/accounting/templates/accounting/report/ledger.html:58
#: src/accounting/templates/accounting/report/search.html:55 #: src/accounting/templates/accounting/report/search.html:55
@ -555,25 +562,73 @@ msgstr "資產負債表"
msgid "Please fill in the title." msgid "Please fill in the title."
msgstr "請填上標題。" msgstr "請填上標題。"
#: src/accounting/static/js/description-editor.js:767 #: src/accounting/static/js/description-editor.js:756
#: src/accounting/static/js/description-editor.js:953 #: src/accounting/static/js/description-editor.js:934
msgid "Please fill in the tag." msgid "Please fill in the tag."
msgstr "請填上標籤。" msgstr "請填上標籤。"
#: src/accounting/static/js/description-editor.js:777 #: src/accounting/static/js/description-editor.js:766
#: src/accounting/static/js/description-editor.js:973 #: src/accounting/static/js/description-editor.js:954
msgid "Please fill in the origin." msgid "Please fill in the origin."
msgstr "請填上起點。" msgstr "請填上起點。"
#: src/accounting/static/js/description-editor.js:787 #: src/accounting/static/js/description-editor.js:776
#: src/accounting/static/js/description-editor.js:983 #: src/accounting/static/js/description-editor.js:964
msgid "Please fill in the destination." msgid "Please fill in the destination."
msgstr "請填上終點。" msgstr "請填上終點。"
#: src/accounting/static/js/description-editor.js:963 #: src/accounting/static/js/description-editor.js:944
msgid "Please fill in the route." msgid "Please fill in the route."
msgstr "請填上路線名稱。" msgstr "請填上路線名稱。"
#: src/accounting/static/js/description-editor.js:998
msgid "January"
msgstr "一月"
#: src/accounting/static/js/description-editor.js:998
msgid "February"
msgstr "二月"
#: src/accounting/static/js/description-editor.js:998
msgid "March"
msgstr "三月"
#: src/accounting/static/js/description-editor.js:998
msgid "April"
msgstr "四月"
#: src/accounting/static/js/description-editor.js:999
msgid "May"
msgstr "五月"
#: src/accounting/static/js/description-editor.js:999
msgid "June"
msgstr "六月"
#: src/accounting/static/js/description-editor.js:999
msgid "July"
msgstr "七月"
#: src/accounting/static/js/description-editor.js:999
msgid "August"
msgstr "八月"
#: src/accounting/static/js/description-editor.js:1000
msgid "September"
msgstr "九月"
#: src/accounting/static/js/description-editor.js:1000
msgid "October"
msgstr "十月"
#: src/accounting/static/js/description-editor.js:1000
msgid "November"
msgstr "十一月"
#: src/accounting/static/js/description-editor.js:1000
msgid "December"
msgstr "十二月"
#: src/accounting/static/js/journal-entry-form.js:985 #: src/accounting/static/js/journal-entry-form.js:985
#: src/accounting/static/js/journal-entry-line-item-editor.js:449 #: src/accounting/static/js/journal-entry-line-item-editor.js:449
msgid "Please fill in the amount." msgid "Please fill in the amount."
@ -629,28 +684,30 @@ msgstr "回上頁"
#: src/accounting/templates/accounting/account/detail.html:36 #: src/accounting/templates/accounting/account/detail.html:36
#: src/accounting/templates/accounting/currency/detail.html:36 #: src/accounting/templates/accounting/currency/detail.html:36
#: src/accounting/templates/accounting/journal-entry/include/detail.html:36 #: src/accounting/templates/accounting/journal-entry/include/detail.html:36
msgid "Settings" msgid "Edit"
msgstr "設定" msgstr "編輯"
#: src/accounting/templates/accounting/account/detail.html:41 #: src/accounting/templates/accounting/account/detail.html:41
#: src/accounting/templates/accounting/journal-entry/include/detail.html:41 #: src/accounting/templates/accounting/journal-entry/include/detail.html:41
msgid "Order" msgid "Order"
msgstr "次序" msgstr "次序"
#: src/accounting/templates/accounting/account/detail.html:46 #: src/accounting/templates/accounting/account/detail.html:47
#: src/accounting/templates/accounting/currency/detail.html:42 #: src/accounting/templates/accounting/account/detail.html:52
#: src/accounting/templates/accounting/currency/detail.html:43
#: src/accounting/templates/accounting/currency/detail.html:48
#: src/accounting/templates/accounting/journal-entry/include/detail.html:48 #: src/accounting/templates/accounting/journal-entry/include/detail.html:48
#: src/accounting/templates/accounting/journal-entry/include/detail.html:53 #: src/accounting/templates/accounting/journal-entry/include/detail.html:53
msgid "Delete" msgid "Delete"
msgstr "刪除" msgstr "刪除"
#: src/accounting/templates/accounting/account/detail.html:69 #: src/accounting/templates/accounting/account/detail.html:76
msgid "Confirm Delete Account" msgid "Confirm Delete Account"
msgstr "刪除科目確認" msgstr "確認刪除科目"
#: src/accounting/templates/accounting/account/detail.html:70 #: src/accounting/templates/accounting/account/detail.html:77
#: src/accounting/templates/accounting/account/include/form.html:91 #: src/accounting/templates/accounting/account/include/form.html:91
#: src/accounting/templates/accounting/currency/detail.html:66 #: src/accounting/templates/accounting/currency/detail.html:73
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:27 #: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:27
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:30 #: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:30
#: src/accounting/templates/accounting/journal-entry/include/detail.html:78 #: src/accounting/templates/accounting/journal-entry/include/detail.html:78
@ -661,36 +718,36 @@ msgstr "刪除科目確認"
msgid "Close" msgid "Close"
msgstr "關閉" msgstr "關閉"
#: src/accounting/templates/accounting/account/detail.html:73 #: src/accounting/templates/accounting/account/detail.html:80
msgid "Do you really want to delete this account?" msgid "Do you really want to delete this account?"
msgstr "你確定要刪掉這個科目嗎?" msgstr "你確定要刪掉這個科目嗎?"
#: src/accounting/templates/accounting/account/detail.html:76 #: src/accounting/templates/accounting/account/detail.html:83
#: src/accounting/templates/accounting/account/include/form.html:112 #: src/accounting/templates/accounting/account/include/form.html:112
#: src/accounting/templates/accounting/currency/detail.html:72 #: src/accounting/templates/accounting/currency/detail.html:79
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:49 #: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:49
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:187 #: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:193
#: src/accounting/templates/accounting/journal-entry/include/detail.html:84 #: src/accounting/templates/accounting/journal-entry/include/detail.html:84
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:70 #: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:70
#: src/accounting/templates/accounting/report/include/search-modal.html:37 #: src/accounting/templates/accounting/report/include/search-modal.html:37
msgid "Cancel" msgid "Cancel"
msgstr "取消" msgstr "取消"
#: src/accounting/templates/accounting/account/detail.html:77 #: src/accounting/templates/accounting/account/detail.html:84
#: src/accounting/templates/accounting/currency/detail.html:73 #: src/accounting/templates/accounting/currency/detail.html:80
#: src/accounting/templates/accounting/journal-entry/include/detail.html:85 #: src/accounting/templates/accounting/journal-entry/include/detail.html:85
#: src/accounting/templates/accounting/report/include/period-chooser.html:141 #: src/accounting/templates/accounting/report/include/period-chooser.html:141
msgid "Confirm" msgid "Confirm"
msgstr "確定" msgstr "確定"
#: src/accounting/templates/accounting/account/detail.html:94 #: src/accounting/templates/accounting/account/detail.html:101
#: src/accounting/templates/accounting/currency/detail.html:85 #: src/accounting/templates/accounting/currency/detail.html:92
#: src/accounting/templates/accounting/journal-entry/include/detail.html:114 #: src/accounting/templates/accounting/journal-entry/include/detail.html:114
msgid "Created" msgid "Created"
msgstr "建檔" msgstr "建檔"
#: src/accounting/templates/accounting/account/detail.html:95 #: src/accounting/templates/accounting/account/detail.html:102
#: src/accounting/templates/accounting/currency/detail.html:86 #: src/accounting/templates/accounting/currency/detail.html:93
#: src/accounting/templates/accounting/journal-entry/include/detail.html:115 #: src/accounting/templates/accounting/journal-entry/include/detail.html:115
msgid "Updated" msgid "Updated"
msgstr "更新" msgstr "更新"
@ -714,11 +771,8 @@ msgstr "科目管理"
#: src/accounting/templates/accounting/account/list.html:32 #: src/accounting/templates/accounting/account/list.html:32
#: src/accounting/templates/accounting/currency/list.html:32 #: src/accounting/templates/accounting/currency/list.html:32
#: src/accounting/templates/accounting/journal-entry/disbursement/include/form-currency.html:67 #: src/accounting/templates/accounting/journal-entry/include/form-debit-credit.html:44
#: src/accounting/templates/accounting/journal-entry/include/form.html:64 #: src/accounting/templates/accounting/journal-entry/include/form.html:64
#: src/accounting/templates/accounting/journal-entry/receipt/include/form-currency.html:67
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:69
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:100
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:26 #: src/accounting/templates/accounting/report/include/toolbar-buttons.html:26
msgid "New" msgid "New"
msgstr "新增" msgstr "新增"
@ -730,7 +784,7 @@ msgstr "新增"
#: src/accounting/templates/accounting/currency/list.html:65 #: src/accounting/templates/accounting/currency/list.html:65
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:46 #: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:46
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:51 #: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:51
#: src/accounting/templates/accounting/journal-entry/order.html:80 #: src/accounting/templates/accounting/journal-entry/order.html:82
#: src/accounting/templates/accounting/report/balance-sheet.html:110 #: src/accounting/templates/accounting/report/balance-sheet.html:110
#: src/accounting/templates/accounting/report/income-expenses.html:113 #: src/accounting/templates/accounting/report/income-expenses.html:113
#: src/accounting/templates/accounting/report/income-statement.html:96 #: src/accounting/templates/accounting/report/income-statement.html:96
@ -749,7 +803,7 @@ msgstr "%(base)s下的科目"
#: src/accounting/templates/accounting/account/include/form.html:75 #: src/accounting/templates/accounting/account/include/form.html:75
#: src/accounting/templates/accounting/account/order.html:62 #: src/accounting/templates/accounting/account/order.html:62
#: src/accounting/templates/accounting/currency/include/form.html:57 #: src/accounting/templates/accounting/currency/include/form.html:57
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:188 #: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:194
#: src/accounting/templates/accounting/journal-entry/include/form.html:80 #: src/accounting/templates/accounting/journal-entry/include/form.html:80
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:71 #: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:71
#: src/accounting/templates/accounting/journal-entry/order.html:61 #: src/accounting/templates/accounting/journal-entry/order.html:61
@ -790,11 +844,11 @@ msgstr "基本科目管理"
msgid "Add a New Currency" msgid "Add a New Currency"
msgstr "新增貨幣" msgstr "新增貨幣"
#: src/accounting/templates/accounting/currency/detail.html:65 #: src/accounting/templates/accounting/currency/detail.html:72
msgid "Confirm Delete Currency" msgid "Confirm Delete Currency"
msgstr "刪除貨幣確認" msgstr "確認刪除貨幣"
#: src/accounting/templates/accounting/currency/detail.html:69 #: src/accounting/templates/accounting/currency/detail.html:76
msgid "Do you really want to delete this currency?" msgid "Do you really want to delete this currency?"
msgstr "你確定要刪掉這個貨幣嗎?" msgstr "你確定要刪掉這個貨幣嗎?"
@ -850,14 +904,14 @@ msgstr "新增現金支出傳票"
#: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:27 #: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:27
#: src/accounting/templates/accounting/journal-entry/receipt/detail.html:27 #: src/accounting/templates/accounting/journal-entry/receipt/detail.html:27
msgid "To Transfer" msgid "As Transfer"
msgstr "改轉帳" msgstr "改轉帳"
#: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:37 #: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:37
#: src/accounting/templates/accounting/journal-entry/disbursement/include/form-currency.html:46 #: src/accounting/templates/accounting/journal-entry/disbursement/include/form-currency.html:28
#: src/accounting/templates/accounting/journal-entry/include/form.html:56 #: src/accounting/templates/accounting/journal-entry/include/form.html:56
#: src/accounting/templates/accounting/journal-entry/receipt/detail.html:37 #: src/accounting/templates/accounting/journal-entry/receipt/detail.html:37
#: src/accounting/templates/accounting/journal-entry/receipt/include/form-currency.html:46 #: src/accounting/templates/accounting/journal-entry/receipt/include/form-currency.html:28
msgid "Content" msgid "Content"
msgstr "內容" msgstr "內容"
@ -893,8 +947,8 @@ msgid "Bus"
msgstr "公車" msgstr "公車"
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:59 #: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:59
msgid "Regular" msgid "Recurring"
msgstr "帳單" msgstr "常用"
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:64 #: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:64
msgid "Annotation" msgid "Annotation"
@ -922,7 +976,7 @@ msgstr "至"
msgid "Route" msgid "Route"
msgstr "路線" msgstr "路線"
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:166 #: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:172
msgid "The Number of Items" msgid "The Number of Items"
msgstr "數量" msgstr "數量"
@ -948,7 +1002,7 @@ msgstr "未抵銷"
#: src/accounting/templates/accounting/journal-entry/include/detail.html:77 #: src/accounting/templates/accounting/journal-entry/include/detail.html:77
msgid "Confirm Delete Journal Entry" msgid "Confirm Delete Journal Entry"
msgstr "刪除傳票確認" msgstr "確認刪除傳票"
#: src/accounting/templates/accounting/journal-entry/include/detail.html:81 #: src/accounting/templates/accounting/journal-entry/include/detail.html:81
msgid "Do you really want to delete this journal entry?" msgid "Do you really want to delete this journal entry?"
@ -962,6 +1016,24 @@ msgstr "分錄內容"
msgid "Original Line Item" msgid "Original Line Item"
msgstr "原始分錄" msgstr "原始分錄"
#: src/accounting/templates/accounting/journal-entry/include/order-journal-entry.html:26
#: src/accounting/templates/accounting/report/include/add-journal-entry-material-fab.html:26
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:31
msgid "Cash Disbursement"
msgstr "現金支出"
#: src/accounting/templates/accounting/journal-entry/include/order-journal-entry.html:28
#: src/accounting/templates/accounting/report/include/add-journal-entry-material-fab.html:29
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:36
msgid "Cash Receipt"
msgstr "現金收入"
#: src/accounting/templates/accounting/journal-entry/include/order-journal-entry.html:30
#: src/accounting/templates/accounting/report/include/add-journal-entry-material-fab.html:32
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:41
msgid "Transfer"
msgstr "轉帳"
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:26 #: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:26
msgid "Select Original Line Item" msgid "Select Original Line Item"
msgstr "選擇原始分錄" msgstr "選擇原始分錄"
@ -1007,21 +1079,6 @@ msgstr "%(period)s%(currency)s%(account)s分類帳"
msgid "Trial Balance of %(currency)s %(period)s" msgid "Trial Balance of %(currency)s %(period)s"
msgstr "%(period)s%(currency)s試算表" msgstr "%(period)s%(currency)s試算表"
#: src/accounting/templates/accounting/report/include/add-journal-entry-material-fab.html:26
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:31
msgid "Cash Disbursement"
msgstr "現金支出"
#: src/accounting/templates/accounting/report/include/add-journal-entry-material-fab.html:29
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:36
msgid "Cash Receipt"
msgstr "現金收入"
#: src/accounting/templates/accounting/report/include/add-journal-entry-material-fab.html:32
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:41
msgid "Transfer"
msgstr "轉帳"
#: src/accounting/templates/accounting/report/include/period-chooser.html:26 #: src/accounting/templates/accounting/report/include/period-chooser.html:26
msgid "Period Chooser" msgid "Period Chooser"
msgstr "選擇日期範圍" msgstr "選擇日期範圍"
@ -1042,10 +1099,14 @@ msgstr "日"
msgid "Custom" msgid "Custom"
msgstr "自訂" msgstr "自訂"
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:52 #: src/accounting/templates/accounting/report/include/toolbar-buttons.html:50
msgid "Report" msgid "Report"
msgstr "報表" msgstr "報表"
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:106
msgid "Period"
msgstr "期間"
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:112 #: src/accounting/templates/accounting/report/include/toolbar-buttons.html:112
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:117 #: src/accounting/templates/accounting/report/include/toolbar-buttons.html:117
msgid "Download" msgid "Download"

View File

@ -18,7 +18,7 @@
""" """
import unittest import unittest
from datetime import timedelta from datetime import timedelta, date
import httpx import httpx
import sqlalchemy as sa import sqlalchemy as sa
@ -28,6 +28,7 @@ from flask.testing import FlaskCliRunner
from test_site import db from test_site import db
from testlib import create_test_app, get_client, set_locale from testlib import create_test_app, get_client, set_locale
from testlib_journal_entry import add_journal_entry
NEXT_URI: str = "/_next" NEXT_URI: str = "/_next"
"""The next URI.""" """The next URI."""
@ -53,13 +54,15 @@ class AccountData:
"""The code.""" """The code."""
cash: AccountData = AccountData("1111", 1, "Cash") CASH: AccountData = AccountData("1111", 1, "Cash")
"""The cash account.""" """The cash account."""
bank: AccountData = AccountData("1113", 1, "Bank") PETTY: AccountData = AccountData("1112", 1, "Bank")
"""The petty cash account."""
BANK: AccountData = AccountData("1113", 1, "Bank")
"""The bank account.""" """The bank account."""
stock: AccountData = AccountData("1121", 1, "Stock") STOCK: AccountData = AccountData("1121", 1, "Stock")
"""The stock account.""" """The stock account."""
loan: AccountData = AccountData("2112", 1, "Loan") LOAN: AccountData = AccountData("2112", 1, "Loan")
"""The loan account.""" """The loan account."""
PREFIX: str = "/accounting/accounts" PREFIX: str = "/accounting/accounts"
"""The URL prefix for the account management.""" """The URL prefix for the account management."""
@ -147,19 +150,19 @@ class AccountTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/store", response = self.client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": cash.base_code, "base_code": CASH.base_code,
"title": cash.title}) "title": CASH.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{cash.code}") f"{PREFIX}/{CASH.code}")
response = self.client.post(f"{PREFIX}/store", response = self.client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": bank.base_code, "base_code": BANK.base_code,
"title": bank.title}) "title": BANK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{bank.code}") f"{PREFIX}/{BANK.code}")
def test_nobody(self) -> None: def test_nobody(self) -> None:
"""Test the permission as nobody. """Test the permission as nobody.
@ -173,7 +176,7 @@ class AccountTestCase(unittest.TestCase):
response = client.get(PREFIX) response = client.get(PREFIX)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{cash.code}") response = client.get(f"{PREFIX}/{CASH.code}")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/create") response = client.get(f"{PREFIX}/create")
@ -181,33 +184,33 @@ class AccountTestCase(unittest.TestCase):
response = client.post(f"{PREFIX}/store", response = client.post(f"{PREFIX}/store",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"base_code": stock.base_code, "base_code": STOCK.base_code,
"title": stock.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{cash.code}/edit") response = client.get(f"{PREFIX}/{CASH.code}/edit")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.post(f"{PREFIX}/{cash.code}/update", response = client.post(f"{PREFIX}/{CASH.code}/update",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"base_code": cash.base_code, "base_code": CASH.base_code,
"title": f"{cash.title}-2"}) "title": f"{CASH.title}-2"})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.post(f"{PREFIX}/{cash.code}/delete", response = client.post(f"{PREFIX}/{BANK.code}/delete",
data={"csrf_token": csrf_token}) data={"csrf_token": csrf_token})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/bases/{cash.base_code}") response = client.get(f"{PREFIX}/bases/{CASH.base_code}")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
with self.app.app_context(): with self.app.app_context():
bank_id: int = Account.find_by_code(bank.code).id cash_id: int = Account.find_by_code(CASH.code).id
response = client.post(f"{PREFIX}/bases/{bank.base_code}", response = client.post(f"{PREFIX}/bases/{CASH.base_code}",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"next": NEXT_URI, "next": NEXT_URI,
f"{bank_id}-no": "5"}) f"{cash_id}-no": "5"})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_viewer(self) -> None: def test_viewer(self) -> None:
@ -222,7 +225,7 @@ class AccountTestCase(unittest.TestCase):
response = client.get(PREFIX) response = client.get(PREFIX)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/{cash.code}") response = client.get(f"{PREFIX}/{CASH.code}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/create") response = client.get(f"{PREFIX}/create")
@ -230,33 +233,33 @@ class AccountTestCase(unittest.TestCase):
response = client.post(f"{PREFIX}/store", response = client.post(f"{PREFIX}/store",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"base_code": stock.base_code, "base_code": STOCK.base_code,
"title": stock.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{cash.code}/edit") response = client.get(f"{PREFIX}/{CASH.code}/edit")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.post(f"{PREFIX}/{cash.code}/update", response = client.post(f"{PREFIX}/{CASH.code}/update",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"base_code": cash.base_code, "base_code": CASH.base_code,
"title": f"{cash.title}-2"}) "title": f"{CASH.title}-2"})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.post(f"{PREFIX}/{cash.code}/delete", response = client.post(f"{PREFIX}/{BANK.code}/delete",
data={"csrf_token": csrf_token}) data={"csrf_token": csrf_token})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/bases/{cash.base_code}") response = client.get(f"{PREFIX}/bases/{CASH.base_code}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
with self.app.app_context(): with self.app.app_context():
bank_id: int = Account.find_by_code(bank.code).id cash_id: int = Account.find_by_code(CASH.code).id
response = client.post(f"{PREFIX}/bases/{bank.base_code}", response = client.post(f"{PREFIX}/bases/{CASH.base_code}",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"next": NEXT_URI, "next": NEXT_URI,
f"{bank_id}-no": "5"}) f"{cash_id}-no": "5"})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_editor(self) -> None: def test_editor(self) -> None:
@ -270,7 +273,7 @@ class AccountTestCase(unittest.TestCase):
response = self.client.get(PREFIX) response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{cash.code}") response = self.client.get(f"{PREFIX}/{CASH.code}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/create") response = self.client.get(f"{PREFIX}/create")
@ -278,37 +281,37 @@ class AccountTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/store", response = self.client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": stock.base_code, "base_code": STOCK.base_code,
"title": stock.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{stock.code}") f"{PREFIX}/{STOCK.code}")
response = self.client.get(f"{PREFIX}/{cash.code}/edit") response = self.client.get(f"{PREFIX}/{CASH.code}/edit")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(f"{PREFIX}/{cash.code}/update", response = self.client.post(f"{PREFIX}/{CASH.code}/update",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": cash.base_code, "base_code": CASH.base_code,
"title": f"{cash.title}-2"}) "title": f"{CASH.title}-2"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{cash.code}") self.assertEqual(response.headers["Location"], f"{PREFIX}/{CASH.code}")
response = self.client.post(f"{PREFIX}/{cash.code}/delete", response = self.client.post(f"{PREFIX}/{BANK.code}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], PREFIX) self.assertEqual(response.headers["Location"], PREFIX)
response = self.client.get(f"{PREFIX}/bases/{cash.base_code}") response = self.client.get(f"{PREFIX}/bases/{CASH.base_code}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
with self.app.app_context(): with self.app.app_context():
bank_id: int = Account.find_by_code(bank.code).id cash_id: int = Account.find_by_code(CASH.code).id
response = self.client.post(f"{PREFIX}/bases/{bank.base_code}", response = self.client.post(f"{PREFIX}/bases/{CASH.base_code}",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI, "next": NEXT_URI,
f"{bank_id}-no": "5"}) f"{cash_id}-no": "5"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
@ -320,31 +323,31 @@ class AccountTestCase(unittest.TestCase):
from accounting.models import Account from accounting.models import Account
create_uri: str = f"{PREFIX}/create" create_uri: str = f"{PREFIX}/create"
store_uri: str = f"{PREFIX}/store" store_uri: str = f"{PREFIX}/store"
detail_uri: str = f"{PREFIX}/{stock.code}" detail_uri: str = f"{PREFIX}/{STOCK.code}"
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.app.app_context():
self.assertEqual({x.code for x in Account.query.all()}, self.assertEqual({x.code for x in Account.query.all()},
{cash.code, bank.code}) {CASH.code, BANK.code})
# Missing CSRF token # Missing CSRF token
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"base_code": stock.base_code, data={"base_code": STOCK.base_code,
"title": stock.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
# CSRF token mismatch # CSRF token mismatch
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": f"{self.csrf_token}-2", data={"csrf_token": f"{self.csrf_token}-2",
"base_code": stock.base_code, "base_code": STOCK.base_code,
"title": stock.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
# Empty base account code # Empty base account code
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": " ", "base_code": " ",
"title": stock.title}) "title": STOCK.title})
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)
@ -352,7 +355,7 @@ class AccountTestCase(unittest.TestCase):
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": "9999", "base_code": "9999",
"title": stock.title}) "title": STOCK.title})
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)
@ -360,14 +363,14 @@ class AccountTestCase(unittest.TestCase):
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": "1", "base_code": "1",
"title": stock.title}) "title": STOCK.title})
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)
# Empty name # Empty name
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": stock.base_code, "base_code": STOCK.base_code,
"title": " "}) "title": " "})
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)
@ -376,7 +379,7 @@ class AccountTestCase(unittest.TestCase):
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": "6172", "base_code": "6172",
"title": stock.title, "title": STOCK.title,
"is_need_offset": "yes"}) "is_need_offset": "yes"})
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)
@ -384,43 +387,43 @@ class AccountTestCase(unittest.TestCase):
# 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,
"base_code": f" {stock.base_code} ", "base_code": f" {STOCK.base_code} ",
"title": f" {stock.title} "}) "title": f" {STOCK.title} "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
# Success under the same base # Success under the same base
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": stock.base_code, "base_code": STOCK.base_code,
"title": stock.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{stock.base_code}-002") f"{PREFIX}/{STOCK.base_code}-002")
# Success under the same base, with order in a mess. # Success under the same base, with order in a mess.
with self.app.app_context(): with self.app.app_context():
stock_2: Account = Account.find_by_code(f"{stock.base_code}-002") stock_2: Account = Account.find_by_code(f"{STOCK.base_code}-002")
stock_2.no = 66 stock_2.no = 66
db.session.commit() db.session.commit()
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": stock.base_code, "base_code": STOCK.base_code,
"title": stock.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{stock.base_code}-003") f"{PREFIX}/{STOCK.base_code}-003")
with self.app.app_context(): with self.app.app_context():
self.assertEqual({x.code for x in Account.query.all()}, self.assertEqual({x.code for x in Account.query.all()},
{cash.code, bank.code, stock.code, {CASH.code, BANK.code, STOCK.code,
f"{stock.base_code}-002", f"{STOCK.base_code}-002",
f"{stock.base_code}-003"}) f"{STOCK.base_code}-003"})
account: Account = Account.find_by_code(stock.code) account: Account = Account.find_by_code(STOCK.code)
self.assertEqual(account.base_code, stock.base_code) self.assertEqual(account.base_code, STOCK.base_code)
self.assertEqual(account.title_l10n, stock.title) self.assertEqual(account.title_l10n, STOCK.title)
def test_basic_update(self) -> None: def test_basic_update(self) -> None:
"""Tests the basic rules to update a user. """Tests the basic rules to update a user.
@ -428,30 +431,30 @@ class AccountTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import Account from accounting.models import Account
detail_uri: str = f"{PREFIX}/{cash.code}" detail_uri: str = f"{PREFIX}/{CASH.code}"
edit_uri: str = f"{PREFIX}/{cash.code}/edit" edit_uri: str = f"{PREFIX}/{CASH.code}/edit"
update_uri: str = f"{PREFIX}/{cash.code}/update" update_uri: str = f"{PREFIX}/{CASH.code}/update"
detail_c_uri: str = f"{PREFIX}/{stock.code}" detail_c_uri: str = f"{PREFIX}/{STOCK.code}"
response: httpx.Response response: httpx.Response
# Success, with spaces to be stripped # Success, with spaces to be stripped
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": f" {cash.base_code} ", "base_code": f" {CASH.base_code} ",
"title": f" {cash.title}-1 "}) "title": f" {CASH.title}-1 "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
account: Account = Account.find_by_code(cash.code) account: Account = Account.find_by_code(CASH.code)
self.assertEqual(account.base_code, cash.base_code) self.assertEqual(account.base_code, CASH.base_code)
self.assertEqual(account.title_l10n, f"{cash.title}-1") self.assertEqual(account.title_l10n, f"{CASH.title}-1")
# Empty base account code # Empty base account code
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": " ", "base_code": " ",
"title": stock.title}) "title": STOCK.title})
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)
@ -459,7 +462,7 @@ class AccountTestCase(unittest.TestCase):
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": "9999", "base_code": "9999",
"title": stock.title}) "title": STOCK.title})
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)
@ -467,14 +470,14 @@ class AccountTestCase(unittest.TestCase):
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": "1", "base_code": "1",
"title": stock.title}) "title": STOCK.title})
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)
# Empty name # Empty name
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": stock.base_code, "base_code": STOCK.base_code,
"title": " "}) "title": " "})
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)
@ -483,7 +486,7 @@ class AccountTestCase(unittest.TestCase):
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": "6172", "base_code": "6172",
"title": stock.title, "title": STOCK.title,
"is_need_offset": "yes"}) "is_need_offset": "yes"})
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)
@ -491,8 +494,8 @@ class AccountTestCase(unittest.TestCase):
# 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,
"base_code": stock.base_code, "base_code": STOCK.base_code,
"title": stock.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_c_uri) self.assertEqual(response.headers["Location"], detail_c_uri)
@ -508,20 +511,20 @@ class AccountTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import Account from accounting.models import Account
detail_uri: str = f"{PREFIX}/{cash.code}" detail_uri: str = f"{PREFIX}/{CASH.code}"
update_uri: str = f"{PREFIX}/{cash.code}/update" update_uri: str = f"{PREFIX}/{CASH.code}/update"
account: Account account: Account
response: httpx.Response response: httpx.Response
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": f" {cash.base_code} ", "base_code": f" {CASH.base_code} ",
"title": f" {cash.title} "}) "title": f" {CASH.title} "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
account = Account.find_by_code(cash.code) account = Account.find_by_code(CASH.code)
self.assertIsNotNone(account) self.assertIsNotNone(account)
account.created_at \ account.created_at \
= account.created_at - timedelta(seconds=5) = account.created_at - timedelta(seconds=5)
@ -530,13 +533,13 @@ class AccountTestCase(unittest.TestCase):
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": cash.base_code, "base_code": CASH.base_code,
"title": stock.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
account = Account.find_by_code(cash.code) account = Account.find_by_code(CASH.code)
self.assertIsNotNone(account) self.assertIsNotNone(account)
self.assertLess(account.created_at, self.assertLess(account.created_at,
account.updated_at) account.updated_at)
@ -549,25 +552,25 @@ class AccountTestCase(unittest.TestCase):
from accounting.models import Account from accounting.models import Account
editor_username, editor2_username = "editor", "editor2" editor_username, editor2_username = "editor", "editor2"
client, csrf_token = get_client(self.app, editor2_username) client, csrf_token = get_client(self.app, editor2_username)
detail_uri: str = f"{PREFIX}/{cash.code}" detail_uri: str = f"{PREFIX}/{CASH.code}"
update_uri: str = f"{PREFIX}/{cash.code}/update" update_uri: str = f"{PREFIX}/{CASH.code}/update"
account: Account account: Account
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.app.app_context():
account = Account.find_by_code(cash.code) account = Account.find_by_code(CASH.code)
self.assertEqual(account.created_by.username, editor_username) self.assertEqual(account.created_by.username, editor_username)
self.assertEqual(account.updated_by.username, editor_username) self.assertEqual(account.updated_by.username, editor_username)
response = client.post(update_uri, response = client.post(update_uri,
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"base_code": cash.base_code, "base_code": CASH.base_code,
"title": f"{cash.title}-2"}) "title": f"{CASH.title}-2"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
account = Account.find_by_code(cash.code) account = Account.find_by_code(CASH.code)
self.assertEqual(account.created_by.username, self.assertEqual(account.created_by.username,
editor_username) editor_username)
self.assertEqual(account.updated_by.username, self.assertEqual(account.updated_by.username,
@ -579,60 +582,60 @@ class AccountTestCase(unittest.TestCase):
:return: None :return: None
""" """
from accounting.models import Account from accounting.models import Account
detail_uri: str = f"{PREFIX}/{cash.code}" detail_uri: str = f"{PREFIX}/{CASH.code}"
update_uri: str = f"{PREFIX}/{cash.code}/update" update_uri: str = f"{PREFIX}/{CASH.code}/update"
account: Account account: Account
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.app.app_context():
account = Account.find_by_code(cash.code) account = Account.find_by_code(CASH.code)
self.assertEqual(account.title_l10n, cash.title) self.assertEqual(account.title_l10n, CASH.title)
self.assertEqual(account.l10n, []) self.assertEqual(account.l10n, [])
set_locale(self.client, self.csrf_token, "zh_Hant") set_locale(self.client, self.csrf_token, "zh_Hant")
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": cash.base_code, "base_code": CASH.base_code,
"title": f"{cash.title}-zh_Hant"}) "title": f"{CASH.title}-zh_Hant"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
account = Account.find_by_code(cash.code) account = Account.find_by_code(CASH.code)
self.assertEqual(account.title_l10n, cash.title) self.assertEqual(account.title_l10n, CASH.title)
self.assertEqual({(x.locale, x.title) for x in account.l10n}, self.assertEqual({(x.locale, x.title) for x in account.l10n},
{("zh_Hant", f"{cash.title}-zh_Hant")}) {("zh_Hant", f"{CASH.title}-zh_Hant")})
set_locale(self.client, self.csrf_token, "en") set_locale(self.client, self.csrf_token, "en")
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": cash.base_code, "base_code": CASH.base_code,
"title": f"{cash.title}-2"}) "title": f"{CASH.title}-2"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
account = Account.find_by_code(cash.code) account = Account.find_by_code(CASH.code)
self.assertEqual(account.title_l10n, f"{cash.title}-2") self.assertEqual(account.title_l10n, f"{CASH.title}-2")
self.assertEqual({(x.locale, x.title) for x in account.l10n}, self.assertEqual({(x.locale, x.title) for x in account.l10n},
{("zh_Hant", f"{cash.title}-zh_Hant")}) {("zh_Hant", f"{CASH.title}-zh_Hant")})
set_locale(self.client, self.csrf_token, "zh_Hant") set_locale(self.client, self.csrf_token, "zh_Hant")
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"base_code": cash.base_code, "base_code": CASH.base_code,
"title": f"{cash.title}-zh_Hant-2"}) "title": f"{CASH.title}-zh_Hant-2"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
account = Account.find_by_code(cash.code) account = Account.find_by_code(CASH.code)
self.assertEqual(account.title_l10n, f"{cash.title}-2") self.assertEqual(account.title_l10n, f"{CASH.title}-2")
self.assertEqual({(x.locale, x.title) for x in account.l10n}, self.assertEqual({(x.locale, x.title) for x in account.l10n},
{("zh_Hant", f"{cash.title}-zh_Hant-2")}) {("zh_Hant", f"{CASH.title}-zh_Hant-2")})
def test_delete(self) -> None: def test_delete(self) -> None:
"""Tests to delete a currency. """Tests to delete a currency.
@ -640,15 +643,51 @@ class AccountTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import Account from accounting.models import Account
detail_uri: str = f"{PREFIX}/{cash.code}" detail_uri: str = f"{PREFIX}/{PETTY.code}"
delete_uri: str = f"{PREFIX}/{cash.code}/delete" delete_uri: str = f"{PREFIX}/{PETTY.code}/delete"
list_uri: str = PREFIX list_uri: str = PREFIX
response: httpx.Response response: httpx.Response
response = self.client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token,
"base_code": PETTY.base_code,
"title": PETTY.title})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri)
response = self.client.post("/accounting/currencies/store",
data={"csrf_token": self.csrf_token,
"code": "USD",
"name": "US Dollars"})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"],
"/accounting/currencies/USD")
add_journal_entry(self.client,
form={"csrf_token": self.csrf_token,
"next": NEXT_URI,
"date": date.today().isoformat(),
"currency-1-code": "USD",
"currency-1-credit-1-account_code": BANK.code,
"currency-1-credit-1-amount": "20"})
with self.app.app_context(): with self.app.app_context():
self.assertEqual({x.code for x in Account.query.all()}, self.assertEqual({x.code for x in Account.query.all()},
{cash.code, bank.code}) {CASH.code, PETTY.code, BANK.code})
# Cannot delete the cash account
response = self.client.post(f"{PREFIX}/{CASH.code}/delete",
data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{CASH.code}")
# Cannot delete the account that is in use
response = self.client.post(f"{PREFIX}/{BANK.code}/delete",
data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{BANK.code}")
# Success
response = self.client.get(detail_uri) response = self.client.get(detail_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(delete_uri, response = self.client.post(delete_uri,
@ -658,7 +697,7 @@ class AccountTestCase(unittest.TestCase):
with self.app.app_context(): with self.app.app_context():
self.assertEqual({x.code for x in Account.query.all()}, self.assertEqual({x.code for x in Account.query.all()},
{bank.code}) {CASH.code, BANK.code})
response = self.client.get(detail_uri) response = self.client.get(detail_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)

View File

@ -20,7 +20,7 @@
import csv import csv
import typing as t import typing as t
import unittest import unittest
from datetime import timedelta from datetime import timedelta, date
import httpx import httpx
from click.testing import Result from click.testing import Result
@ -29,6 +29,7 @@ from flask.testing import FlaskCliRunner
from test_site import db from test_site import db
from testlib import create_test_app, get_client, set_locale from testlib import create_test_app, get_client, set_locale
from testlib_journal_entry import add_journal_entry, NEXT_URI
class CurrencyData: class CurrencyData:
@ -46,14 +47,14 @@ class CurrencyData:
"""The name.""" """The name."""
zza: CurrencyData = CurrencyData("ZZA", "Testing Dollar #A") USD: CurrencyData = CurrencyData("USD", "US Dollar")
"""The first test currency.""" """The US dollars."""
zzb: CurrencyData = CurrencyData("ZZB", "Testing Dollar #B") EUR: CurrencyData = CurrencyData("EUR", "Euro")
"""The second test currency.""" """The European dollars."""
zzc: CurrencyData = CurrencyData("ZZC", "Testing Dollar #C") TWD: CurrencyData = CurrencyData("TWD", "Taiwan dollars")
"""The third test currency.""" """The Taiwan dollars."""
zzd: CurrencyData = CurrencyData("ZZD", "Testing Dollar #D") JPY: CurrencyData = CurrencyData("JPY", "Japanese yen")
"""The fourth test currency.""" """The Japanese yen."""
PREFIX: str = "/accounting/currencies" PREFIX: str = "/accounting/currencies"
"""The URL prefix for the currency management.""" """The URL prefix for the currency management."""
@ -140,17 +141,17 @@ class CurrencyTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/store", response = self.client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": zza.code, "code": USD.code,
"name": zza.name}) "name": USD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{zza.code}") self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}")
response = self.client.post(f"{PREFIX}/store", response = self.client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": zzb.code, "code": EUR.code,
"name": zzb.name}) "name": EUR.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{zzb.code}") self.assertEqual(response.headers["Location"], f"{PREFIX}/{EUR.code}")
def test_nobody(self) -> None: def test_nobody(self) -> None:
"""Test the permission as nobody. """Test the permission as nobody.
@ -163,7 +164,7 @@ class CurrencyTestCase(unittest.TestCase):
response = client.get(PREFIX) response = client.get(PREFIX)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{zza.code}") response = client.get(f"{PREFIX}/{USD.code}")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/create") response = client.get(f"{PREFIX}/create")
@ -171,20 +172,20 @@ class CurrencyTestCase(unittest.TestCase):
response = client.post(f"{PREFIX}/store", response = client.post(f"{PREFIX}/store",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"code": zzc.code, "code": TWD.code,
"name": zzc.name}) "name": TWD.name})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{zza.code}/edit") response = client.get(f"{PREFIX}/{USD.code}/edit")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.post(f"{PREFIX}/{zza.code}/update", response = client.post(f"{PREFIX}/{USD.code}/update",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"code": zzd.code, "code": JPY.code,
"name": zzd.name}) "name": JPY.name})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.post(f"{PREFIX}/{zzb.code}/delete", response = client.post(f"{PREFIX}/{EUR.code}/delete",
data={"csrf_token": csrf_token}) data={"csrf_token": csrf_token})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
@ -199,7 +200,7 @@ class CurrencyTestCase(unittest.TestCase):
response = client.get(PREFIX) response = client.get(PREFIX)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/{zza.code}") response = client.get(f"{PREFIX}/{USD.code}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/create") response = client.get(f"{PREFIX}/create")
@ -207,20 +208,20 @@ class CurrencyTestCase(unittest.TestCase):
response = client.post(f"{PREFIX}/store", response = client.post(f"{PREFIX}/store",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"code": zzc.code, "code": TWD.code,
"name": zzc.name}) "name": TWD.name})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{zza.code}/edit") response = client.get(f"{PREFIX}/{USD.code}/edit")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.post(f"{PREFIX}/{zza.code}/update", response = client.post(f"{PREFIX}/{USD.code}/update",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"code": zzd.code, "code": JPY.code,
"name": zzd.name}) "name": JPY.name})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.post(f"{PREFIX}/{zzb.code}/delete", response = client.post(f"{PREFIX}/{EUR.code}/delete",
data={"csrf_token": csrf_token}) data={"csrf_token": csrf_token})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
@ -234,7 +235,7 @@ class CurrencyTestCase(unittest.TestCase):
response = self.client.get(PREFIX) response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{zza.code}") response = self.client.get(f"{PREFIX}/{USD.code}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/create") response = self.client.get(f"{PREFIX}/create")
@ -242,22 +243,22 @@ class CurrencyTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/store", response = self.client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": zzc.code, "code": TWD.code,
"name": zzc.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{zzc.code}") self.assertEqual(response.headers["Location"], f"{PREFIX}/{TWD.code}")
response = self.client.get(f"{PREFIX}/{zza.code}/edit") response = self.client.get(f"{PREFIX}/{USD.code}/edit")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(f"{PREFIX}/{zza.code}/update", response = self.client.post(f"{PREFIX}/{USD.code}/update",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": zzd.code, "code": JPY.code,
"name": zzd.name}) "name": JPY.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{zzd.code}") self.assertEqual(response.headers["Location"], f"{PREFIX}/{JPY.code}")
response = self.client.post(f"{PREFIX}/{zzb.code}/delete", response = self.client.post(f"{PREFIX}/{EUR.code}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], PREFIX) self.assertEqual(response.headers["Location"], PREFIX)
@ -270,31 +271,31 @@ class CurrencyTestCase(unittest.TestCase):
from accounting.models import Currency from accounting.models import Currency
create_uri: str = f"{PREFIX}/create" create_uri: str = f"{PREFIX}/create"
store_uri: str = f"{PREFIX}/store" store_uri: str = f"{PREFIX}/store"
detail_uri: str = f"{PREFIX}/{zzc.code}" detail_uri: str = f"{PREFIX}/{TWD.code}"
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.app.app_context():
self.assertEqual({x.code for x in Currency.query.all()}, self.assertEqual({x.code for x in Currency.query.all()},
{zza.code, zzb.code}) {USD.code, EUR.code})
# Missing CSRF token # Missing CSRF token
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"code": zzc.code, data={"code": TWD.code,
"name": zzc.name}) "name": TWD.name})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
# CSRF token mismatch # CSRF token mismatch
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": f"{self.csrf_token}-2", data={"csrf_token": f"{self.csrf_token}-2",
"code": zzc.code, "code": TWD.code,
"name": zzc.name}) "name": TWD.name})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
# Empty code # Empty code
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": " ", "code": " ",
"name": zzc.name}) "name": TWD.name})
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)
@ -302,7 +303,7 @@ class CurrencyTestCase(unittest.TestCase):
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": " create ", "code": " create ",
"name": zzc.name}) "name": TWD.name})
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)
@ -310,14 +311,14 @@ class CurrencyTestCase(unittest.TestCase):
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": " zzc ", "code": " zzc ",
"name": zzc.name}) "name": TWD.name})
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)
# Empty name # Empty name
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": zzc.code, "code": TWD.code,
"name": " "}) "name": " "})
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)
@ -325,26 +326,26 @@ class CurrencyTestCase(unittest.TestCase):
# 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,
"code": f" {zzc.code} ", "code": f" {TWD.code} ",
"name": f" {zzc.name} "}) "name": f" {TWD.name} "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
# Duplicated code # Duplicated code
response = self.client.post(store_uri, response = self.client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": zzc.code, "code": TWD.code,
"name": zzc.name}) "name": TWD.name})
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)
with self.app.app_context(): with self.app.app_context():
self.assertEqual({x.code for x in Currency.query.all()}, self.assertEqual({x.code for x in Currency.query.all()},
{zza.code, zzb.code, zzc.code}) {USD.code, EUR.code, TWD.code})
currency: Currency = db.session.get(Currency, zzc.code) currency: Currency = db.session.get(Currency, TWD.code)
self.assertEqual(currency.code, zzc.code) self.assertEqual(currency.code, TWD.code)
self.assertEqual(currency.name_l10n, zzc.name) self.assertEqual(currency.name_l10n, TWD.name)
def test_basic_update(self) -> None: def test_basic_update(self) -> None:
"""Tests the basic rules to update a user. """Tests the basic rules to update a user.
@ -352,30 +353,30 @@ class CurrencyTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import Currency from accounting.models import Currency
detail_uri: str = f"{PREFIX}/{zza.code}" detail_uri: str = f"{PREFIX}/{USD.code}"
edit_uri: str = f"{PREFIX}/{zza.code}/edit" edit_uri: str = f"{PREFIX}/{USD.code}/edit"
update_uri: str = f"{PREFIX}/{zza.code}/update" update_uri: str = f"{PREFIX}/{USD.code}/update"
detail_c_uri: str = f"{PREFIX}/{zzc.code}" detail_c_uri: str = f"{PREFIX}/{TWD.code}"
response: httpx.Response response: httpx.Response
# Success, with spaces to be stripped # Success, with spaces to be stripped
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": f" {zza.code} ", "code": f" {USD.code} ",
"name": f" {zza.name}-1 "}) "name": f" {USD.name}-1 "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
currency: Currency = db.session.get(Currency, zza.code) currency: Currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.code, zza.code) self.assertEqual(currency.code, USD.code)
self.assertEqual(currency.name_l10n, f"{zza.name}-1") self.assertEqual(currency.name_l10n, f"{USD.name}-1")
# Empty code # Empty code
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": " ", "code": " ",
"name": zzc.name}) "name": TWD.name})
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)
@ -383,7 +384,7 @@ class CurrencyTestCase(unittest.TestCase):
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": " create ", "code": " create ",
"name": zzc.name}) "name": TWD.name})
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)
@ -391,14 +392,14 @@ class CurrencyTestCase(unittest.TestCase):
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": "abc/def", "code": "abc/def",
"name": zzc.name}) "name": TWD.name})
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)
# Empty name # Empty name
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": zzc.code, "code": TWD.code,
"name": " "}) "name": " "})
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)
@ -406,16 +407,16 @@ class CurrencyTestCase(unittest.TestCase):
# Duplicated code # Duplicated code
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": zzb.code, "code": EUR.code,
"name": zzc.name}) "name": TWD.name})
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)
# Change code # Change code
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": zzc.code, "code": TWD.code,
"name": zzc.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_c_uri) self.assertEqual(response.headers["Location"], detail_c_uri)
@ -431,20 +432,20 @@ class CurrencyTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import Currency from accounting.models import Currency
detail_uri: str = f"{PREFIX}/{zza.code}" detail_uri: str = f"{PREFIX}/{USD.code}"
update_uri: str = f"{PREFIX}/{zza.code}/update" update_uri: str = f"{PREFIX}/{USD.code}/update"
currency: Currency | None currency: Currency | None
response: httpx.Response response: httpx.Response
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": f" {zza.code} ", "code": f" {USD.code} ",
"name": f" {zza.name} "}) "name": f" {USD.name} "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
currency = db.session.get(Currency, zza.code) currency = db.session.get(Currency, USD.code)
self.assertIsNotNone(currency) self.assertIsNotNone(currency)
currency.created_at \ currency.created_at \
= currency.created_at - timedelta(seconds=5) = currency.created_at - timedelta(seconds=5)
@ -453,13 +454,13 @@ class CurrencyTestCase(unittest.TestCase):
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": zza.code, "code": USD.code,
"name": zzc.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
currency = db.session.get(Currency, zza.code) currency = db.session.get(Currency, USD.code)
self.assertIsNotNone(currency) self.assertIsNotNone(currency)
self.assertLess(currency.created_at, self.assertLess(currency.created_at,
currency.updated_at) currency.updated_at)
@ -472,25 +473,25 @@ class CurrencyTestCase(unittest.TestCase):
from accounting.models import Currency from accounting.models import Currency
editor_username, editor2_username = "editor", "editor2" editor_username, editor2_username = "editor", "editor2"
client, csrf_token = get_client(self.app, editor2_username) client, csrf_token = get_client(self.app, editor2_username)
detail_uri: str = f"{PREFIX}/{zza.code}" detail_uri: str = f"{PREFIX}/{USD.code}"
update_uri: str = f"{PREFIX}/{zza.code}/update" update_uri: str = f"{PREFIX}/{USD.code}/update"
currency: Currency currency: Currency
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.app.app_context():
currency = db.session.get(Currency, zza.code) currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.created_by.username, editor_username) self.assertEqual(currency.created_by.username, editor_username)
self.assertEqual(currency.updated_by.username, editor_username) self.assertEqual(currency.updated_by.username, editor_username)
response = client.post(update_uri, response = client.post(update_uri,
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"code": zza.code, "code": USD.code,
"name": f"{zza.name}-2"}) "name": f"{USD.name}-2"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
currency = db.session.get(Currency, zza.code) currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.created_by.username, editor_username) self.assertEqual(currency.created_by.username, editor_username)
self.assertEqual(currency.updated_by.username, editor2_username) self.assertEqual(currency.updated_by.username, editor2_username)
@ -502,14 +503,14 @@ class CurrencyTestCase(unittest.TestCase):
response: httpx.Response response: httpx.Response
response = self.client.get( response = self.client.get(
f"/accounting/api/currencies/exists-code?q={zza.code}") f"/accounting/api/currencies/exists-code?q={USD.code}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = response.json() data = response.json()
self.assertEqual(set(data.keys()), {"exists"}) self.assertEqual(set(data.keys()), {"exists"})
self.assertTrue(data["exists"]) self.assertTrue(data["exists"])
response = self.client.get( response = self.client.get(
f"/accounting/api/currencies/exists-code?q={zza.code}-1") f"/accounting/api/currencies/exists-code?q={USD.code}-1")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = response.json() data = response.json()
self.assertEqual(set(data.keys()), {"exists"}) self.assertEqual(set(data.keys()), {"exists"})
@ -521,60 +522,60 @@ class CurrencyTestCase(unittest.TestCase):
:return: None :return: None
""" """
from accounting.models import Currency from accounting.models import Currency
detail_uri: str = f"{PREFIX}/{zza.code}" detail_uri: str = f"{PREFIX}/{USD.code}"
update_uri: str = f"{PREFIX}/{zza.code}/update" update_uri: str = f"{PREFIX}/{USD.code}/update"
currency: Currency currency: Currency
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.app.app_context():
currency = db.session.get(Currency, zza.code) currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.name_l10n, zza.name) self.assertEqual(currency.name_l10n, USD.name)
self.assertEqual(currency.l10n, []) self.assertEqual(currency.l10n, [])
set_locale(self.client, self.csrf_token, "zh_Hant") set_locale(self.client, self.csrf_token, "zh_Hant")
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": zza.code, "code": USD.code,
"name": f"{zza.name}-zh_Hant"}) "name": f"{USD.name}-zh_Hant"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
currency = db.session.get(Currency, zza.code) currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.name_l10n, zza.name) self.assertEqual(currency.name_l10n, USD.name)
self.assertEqual({(x.locale, x.name) for x in currency.l10n}, self.assertEqual({(x.locale, x.name) for x in currency.l10n},
{("zh_Hant", f"{zza.name}-zh_Hant")}) {("zh_Hant", f"{USD.name}-zh_Hant")})
set_locale(self.client, self.csrf_token, "en") set_locale(self.client, self.csrf_token, "en")
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": zza.code, "code": USD.code,
"name": f"{zza.name}-2"}) "name": f"{USD.name}-2"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
currency = db.session.get(Currency, zza.code) currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.name_l10n, f"{zza.name}-2") self.assertEqual(currency.name_l10n, f"{USD.name}-2")
self.assertEqual({(x.locale, x.name) for x in currency.l10n}, self.assertEqual({(x.locale, x.name) for x in currency.l10n},
{("zh_Hant", f"{zza.name}-zh_Hant")}) {("zh_Hant", f"{USD.name}-zh_Hant")})
set_locale(self.client, self.csrf_token, "zh_Hant") set_locale(self.client, self.csrf_token, "zh_Hant")
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"code": zza.code, "code": USD.code,
"name": f"{zza.name}-zh_Hant-2"}) "name": f"{USD.name}-zh_Hant-2"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
currency = db.session.get(Currency, zza.code) currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.name_l10n, f"{zza.name}-2") self.assertEqual(currency.name_l10n, f"{USD.name}-2")
self.assertEqual({(x.locale, x.name) for x in currency.l10n}, self.assertEqual({(x.locale, x.name) for x in currency.l10n},
{("zh_Hant", f"{zza.name}-zh_Hant-2")}) {("zh_Hant", f"{USD.name}-zh_Hant-2")})
def test_delete(self) -> None: def test_delete(self) -> None:
"""Tests to delete a currency. """Tests to delete a currency.
@ -582,15 +583,58 @@ class CurrencyTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import Currency from accounting.models import Currency
detail_uri: str = f"{PREFIX}/{zza.code}" detail_uri: str = f"{PREFIX}/{JPY.code}"
delete_uri: str = f"{PREFIX}/{zza.code}/delete" delete_uri: str = f"{PREFIX}/{JPY.code}/delete"
list_uri: str = PREFIX list_uri: str = PREFIX
response: httpx.Response response: httpx.Response
runner: FlaskCliRunner = self.app.test_cli_runner()
with self.app.app_context():
from accounting.models import BaseAccount
if BaseAccount.query.first() is None:
result = runner.invoke(args="accounting-init-base")
self.assertEqual(result.exit_code, 0)
response = self.client.post("/accounting/accounts/store",
data={"csrf_token": self.csrf_token,
"base_code": "1111",
"title": "Cash"})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"],
"/accounting/accounts/1111-001")
response = self.client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token,
"code": JPY.code,
"name": JPY.name})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri)
add_journal_entry(self.client,
form={"csrf_token": self.csrf_token,
"next": NEXT_URI,
"date": date.today().isoformat(),
"currency-1-code": EUR.code,
"currency-1-credit-1-account_code": "1111-001",
"currency-1-credit-1-amount": "20"})
with self.app.app_context(): with self.app.app_context():
self.assertEqual({x.code for x in Currency.query.all()}, self.assertEqual({x.code for x in Currency.query.all()},
{zza.code, zzb.code}) {USD.code, EUR.code, JPY.code})
# Cannot delete the default currency
response = self.client.post(f"{PREFIX}/{USD.code}/delete",
data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}")
# Cannot delete the account that is in use
response = self.client.post(f"{PREFIX}/{EUR.code}/delete",
data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{EUR.code}")
# Success
response = self.client.get(detail_uri) response = self.client.get(detail_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(delete_uri, response = self.client.post(delete_uri,
@ -600,7 +644,7 @@ class CurrencyTestCase(unittest.TestCase):
with self.app.app_context(): with self.app.app_context():
self.assertEqual({x.code for x in Currency.query.all()}, self.assertEqual({x.code for x in Currency.query.all()},
{zzb.code}) {USD.code, EUR.code})
response = self.client.get(detail_uri) response = self.client.get(detail_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)

View File

@ -569,12 +569,43 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
journal_entry_id: int \ from accounting.models import JournalEntry, JournalEntryLineItem
journal_entry_id_1: int \
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = f"{PREFIX}/{journal_entry_id_1}?next=%2F_next"
delete_uri: str = f"{PREFIX}/{journal_entry_id}/delete" delete_uri: str = f"{PREFIX}/{journal_entry_id_1}/delete"
response: httpx.Response response: httpx.Response
form: dict[str, str] = self.__get_add_form()
key: str = [x for x in form if x.endswith("-account_code")][0]
form[key] = Accounts.PAYABLE
journal_entry_id_2: int = add_journal_entry(self.client, form)
with self.app.app_context():
journal_entry: JournalEntry | None \
= db.session.get(JournalEntry, journal_entry_id_2)
self.assertIsNotNone(journal_entry)
line_item: JournalEntryLineItem \
= [x for x in journal_entry.line_items
if x.account_code == Accounts.PAYABLE][0]
add_journal_entry(
self.client,
form={"csrf_token": self.csrf_token,
"next": NEXT_URI,
"date": date.today().isoformat(),
"currency-1-code": line_item.currency_code,
"currency-1-debit-1-original_line_item_id": line_item.id,
"currency-1-debit-1-account_code": line_item.account_code,
"currency-1-debit-1-amount": "1"})
# Cannot delete the journal entry that is in use
response = self.client.post(f"{PREFIX}/{journal_entry_id_2}/delete",
data={"csrf_token": self.csrf_token,
"next": NEXT_URI})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"],
f"{PREFIX}/{journal_entry_id_2}?next=%2F_next")
# Success
response = self.client.get(detail_uri) response = self.client.get(detail_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(delete_uri, response = self.client.post(delete_uri,

View File

@ -50,6 +50,18 @@ 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|简体中文",
"ACCOUNTING_DEFAULT_CURRENCY": "USD",
"ACCOUNTING_DEFAULT_IE_ACCOUNT": "1111-001",
"ACCOUNTING_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

View File

@ -27,7 +27,7 @@ First written: 2023/1/27
<meta name="author" content="{{ "imacat" }}" /> <meta name="author" content="{{ "imacat" }}" />
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" crossorigin="anonymous"> <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.1/css/all.min.css" crossorigin="anonymous"> <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.1/css/all.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.2.10/dist/css/tempus-dominus.min.css" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.4.3/dist/css/tempus-dominus.min.css" crossorigin="anonymous">
{% block styles %}{% endblock %} {% block styles %}{% endblock %}
<script src="{{ url_for("babel_catalog") }}"></script> <script src="{{ url_for("babel_catalog") }}"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>