Compare commits

..

No commits in common. "d8afadda0272d8191259effd214cc2fe6b955a89" and "2253ec7e6d635165b69294de9c28ee229795736f" have entirely different histories.

39 changed files with 677 additions and 1066 deletions

View File

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

View File

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

View File

@ -157,9 +157,6 @@ def delete_account(account: Account) -> redirect:
:return: The redirection to the account list on success, or the account
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()
sort_accounts_in(account.base_code, account.id)
db.session.commit()

View File

@ -160,9 +160,6 @@ def delete_currency(currency: Currency) -> redirect:
:return: The redirection to the currency list on success, or the currency
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()
db.session.commit()
flash(s(lazy_gettext("The currency is deleted successfully.")), "success")

View File

@ -17,11 +17,9 @@
"""The description editor.
"""
import re
import typing as t
import sqlalchemy as sa
from flask import current_app
from accounting import db
from accounting.models import Account, JournalEntryLineItem
@ -145,29 +143,6 @@ class DescriptionType:
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:
"""The description on debit or credit."""
@ -188,8 +163,6 @@ class DescriptionDebitCredit:
DescriptionType] \
= {x.id: x for x in {self.general, self.travel, self.bus}}
"""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"],
name: str, account: Account, freq: int) -> None:
@ -220,10 +193,6 @@ class DescriptionDebitCredit:
freq[account.id] = 0
freq[account.id] \
= 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(),
key=lambda x: -freq[x])]
@ -237,14 +206,6 @@ class DescriptionEditor:
"""The debit tags."""
self.credit: DescriptionDebitCredit = DescriptionDebitCredit("credit")
"""The credit tags."""
self.__init_tags()
self.__init_recurring()
def __init_tags(self):
"""Initializes the tags.
:return: None.
"""
debit_credit: sa.Label = sa.case(
(JournalEntryLineItem.is_debit, "debit"),
else_="credit").label("debit_credit")
@ -275,49 +236,6 @@ class DescriptionEditor:
debit_credit_dict[row.debit_credit].add_tag(
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) \
-> sa.Function:

View File

@ -175,9 +175,6 @@ def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
:return: The redirection to the journal entry list on success, or the
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()
sort_journal_entries_in(journal_entry.date, journal_entry.id)
db.session.commit()

View File

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

View File

@ -72,14 +72,11 @@
}
}
@media(max-width:767px) {
.accounting-toolbar {
width: 100%;
justify-content: space-evenly;
}
.accounting-toolbar > .btn:not(form), .accounting-toolbar > .btn-group > .btn {
height: 3.2rem;
width: 3.2rem;
border-radius: 50%;
margin-left: 1rem;
}
.accounting-toolbar > a.btn, .accounting-toolbar > .btn-group > a.btn {
padding-top: 0.7rem;
@ -318,22 +315,6 @@ a.accounting-report-table-row {
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) */
.accounting-material-text-field {
position: relative;
@ -358,7 +339,7 @@ a.accounting-report-table-row {
.accounting-material-fab {
position: fixed;
right: 2rem;
bottom: 2rem;
bottom: 1rem;
z-index: 10;
flex-direction: column-reverse;
}

View File

@ -32,7 +32,7 @@ class DescriptionEditor {
* The line item editor
* @type {JournalEntryLineItemEditor}
*/
lineItemEditor;
#lineItemEditor;
/**
* The description editor form
@ -102,7 +102,7 @@ class DescriptionEditor {
/**
* The tab planes
* @type {{general: GeneralTagTab, travel: GeneralTripTab, bus: BusTripTab, recurring: RecurringTransactionTab, annotation: AnnotationTab}}
* @type {{general: GeneralTagTab, travel: GeneralTripTab, bus: BusTripTab, regular: RegularPaymentTab, annotation: AnnotationTab}}
*/
tabPlanes = {};
@ -113,7 +113,7 @@ class DescriptionEditor {
* @param debitCredit {string} either "debit" or "credit"
*/
constructor(lineItemEditor, debitCredit) {
this.lineItemEditor = lineItemEditor;
this.#lineItemEditor = lineItemEditor;
this.debitCredit = debitCredit;
this.prefix = "accounting-description-editor-" + debitCredit;
this.#form = document.getElementById(this.prefix);
@ -125,14 +125,14 @@ class DescriptionEditor {
// noinspection JSValidateTypes
this.#accountButtons = Array.from(document.getElementsByClassName(this.prefix + "-account"));
for (const cls of [GeneralTagTab, GeneralTripTab, BusTripTab, RecurringTransactionTab, AnnotationTab]) {
for (const cls of [GeneralTagTab, GeneralTripTab, BusTripTab, RegularPaymentTab, AnnotationTab]) {
const tab = new cls(this);
this.tabPlanes[tab.tabId()] = tab;
}
this.currentTab = this.tabPlanes.general;
this.#initializeSuggestedAccounts();
this.description.onchange = () => this.#onDescriptionChange();
this.#offsetButton.onclick = () => this.lineItemEditor.originalLineItemSelector.onOpen();
this.#offsetButton.onclick = () => this.#lineItemEditor.originalLineItemSelector.onOpen();
this.#form.onsubmit = () => {
if (this.currentTab.validate()) {
this.#submit();
@ -147,7 +147,7 @@ class DescriptionEditor {
*/
#onDescriptionChange() {
this.description.value = this.description.value.trim();
for (const tabPlane of [this.tabPlanes.recurring, this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) {
for (const tabPlane of [this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) {
if (tabPlane.populate()) {
break;
}
@ -158,31 +158,27 @@ class DescriptionEditor {
/**
* Filters the suggested accounts.
*
* @param tagButton {HTMLButtonElement} the tag button
* @param tagButton {HTMLButtonElement|null} the tag button
*/
filterSuggestedAccounts(tagButton) {
this.clearSuggestedAccounts();
for (const accountButton of this.#accountButtons) {
accountButton.classList.add("d-none");
}
if (tagButton === null) {
this.#selectAccount(null);
return;
}
const suggested = JSON.parse(tagButton.dataset.accounts);
let selectedAccountButton = null;
for (const accountButton of this.#accountButtons) {
if (suggested.includes(accountButton.dataset.code)) {
accountButton.classList.remove("d-none");
if (accountButton.dataset.code === suggested[0]) {
this.#selectAccount(accountButton);
return;
selectedAccountButton = accountButton;
}
}
}
}
/**
* Clears the suggested accounts.
*
*/
clearSuggestedAccounts() {
for (const accountButton of this.#accountButtons) {
accountButton.classList.add("d-none");
}
this.#selectAccount(null);
this.#selectAccount(selectedAccountButton);
}
/**
@ -219,9 +215,9 @@ class DescriptionEditor {
#submit() {
bootstrap.Modal.getOrCreateInstance(this.#modal).hide();
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 {
this.lineItemEditor.saveDescription(this.description.value);
this.#lineItemEditor.saveDescription(this.description.value);
}
}
@ -231,7 +227,7 @@ class DescriptionEditor {
*/
onOpen() {
this.#reset();
this.description.value = this.lineItemEditor.description === null? "": this.lineItemEditor.description;
this.description.value = this.#lineItemEditor.description === null? "": this.#lineItemEditor.description;
this.#onDescriptionChange();
}
@ -422,7 +418,7 @@ class TagTabPlane extends TabPlane {
}
}
if (!isMatched) {
this.editor.clearSuggestedAccounts();
this.editor.filterSuggestedAccounts(null);
}
this.validateTag();
}
@ -440,13 +436,14 @@ class TagTabPlane extends TabPlane {
*/
switchToMe() {
super.switchToMe();
let selectedTagButton = null;
for (const tagButton of this.tagButtons) {
if (tagButton.classList.contains("btn-primary")) {
this.editor.filterSuggestedAccounts(tagButton);
return;
selectedTagButton = tagButton;
break;
}
}
this.editor.clearSuggestedAccounts();
this.editor.filterSuggestedAccounts(selectedTagButton);
}
/**
@ -564,6 +561,13 @@ class GeneralTagTab extends TagTabPlane {
this.tag.value = found[1];
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();
return true;
}
@ -728,6 +732,13 @@ class GeneralTripTab extends TagTabPlane {
}
}
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();
return true;
}
@ -906,6 +917,14 @@ class BusTripTab extends TagTabPlane {
this.#route.value = found[2];
this.#from.value = found[3];
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();
return true;
}
@ -966,23 +985,17 @@ class BusTripTab extends TagTabPlane {
}
/**
* The recurring transaction tab plane.
* The regular payment tab plane.
*
* @private
*/
class RecurringTransactionTab extends TabPlane {
class RegularPaymentTab extends TabPlane {
/**
* The month names
* @type {string[]}
*/
#monthNames;
/**
* The buttons of the recurring items
* The payment buttons
* @type {HTMLButtonElement[]}
*/
#itemButtons;
#payments;
// noinspection JSValidateTypes
/**
@ -993,44 +1006,8 @@ class RecurringTransactionTab extends TabPlane {
*/
constructor(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
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]);
this.#payments = Array.from(document.getElementsByClassName(this.prefix + "-payment"));
}
/**
@ -1040,7 +1017,7 @@ class RecurringTransactionTab extends TabPlane {
* @abstract
*/
tabId() {
return "recurring";
return "regular";
};
/**
@ -1049,9 +1026,9 @@ class RecurringTransactionTab extends TabPlane {
* @override
*/
reset() {
for (const itemButton of this.#itemButtons) {
itemButton.classList.remove("btn-primary");
itemButton.classList.add("btn-outline-primary");
for (const payment of this.#payments) {
payment.classList.remove("btn-primary");
payment.classList.add("btn-outline-primary");
}
}
@ -1062,32 +1039,9 @@ class RecurringTransactionTab extends TabPlane {
* @override
*/
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;
}
/**
* 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.
*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,15 +19,57 @@ form-currency.html: The currency sub-form in the cash disbursement journal entry
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/25
#}
{% extends "accounting/journal-entry/include/form-currency.html" %}
<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 %}" 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>
{% block line_items %}
<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>
<div class="mb-3">
<div id="accounting-currency-{{ currency_index }}-debit" 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">{{ A_("Content") }}</label>
<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_forms = debit_forms,
header = A_("Content"),
debit_credit_total = debit_total,
debit_credit_errors = debit_errors %}
{% include "accounting/journal-entry/include/form-debit-credit.html" %}
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 %}
{% endblock %}
{% 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,
debit_forms = currency_form.debit,
debit_errors = currency_form.debit_errors,
debit_total = currency_form.debit_total|accounting_format_amount %}
debit_total = currency_form.form.debit_total|accounting_format_amount %}
{% include "accounting/journal-entry/disbursement/include/form-currency.html" %}
{% endwith %}
{% endfor %}

View File

@ -55,8 +55,8 @@ First written: 2023/2/28
</span>
</li>
<li class="nav-item">
<span id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab" class="nav-link accounting-clickable" aria-current="false">
{{ A_("Recurring") }}
<span id="accounting-description-editor-{{ description_editor.debit_credit }}-regular-tab" class="nav-link accounting-clickable" aria-current="false">
{{ A_("Regular") }}
</span>
</li>
<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>
<div class="accounting-description-editor-buttons">
<div>
{% 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 }}">
{{ 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>
<div class="accounting-description-editor-buttons">
<div>
{% 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 }}">
{{ tag }}
@ -99,7 +99,7 @@ First written: 2023/2/28
{% endfor %}
</div>
<div class="d-flex justify-content-between">
<div class="d-flex justify-content-between mt-2">
<div class="form-floating">
<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>
@ -132,7 +132,7 @@ First written: 2023/2/28
</div>
</div>
<div class="accounting-description-editor-buttons">
<div>
{% 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 }}">
{{ tag }}
@ -140,7 +140,7 @@ First written: 2023/2/28
{% endfor %}
</div>
<div class="d-flex justify-content-between">
<div class="d-flex justify-content-between mt-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=" ">
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from">{{ A_("From") }}</label>
@ -154,15 +154,9 @@ First written: 2023/2/28
</div>
</div>
{# A recurring transaction #}
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-page" class="d-none" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab">
<div 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>
{# A regular income or payment #}
<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">
{# TODO: To be done #}
</div>
{# The annotation #}

View File

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

View File

@ -1,47 +0,0 @@
{#
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

@ -1,49 +0,0 @@
{#
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 %}
<div class="mb-3 d-none d-md-block">
<div class="btn-group mb-3">
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
<i class="fa-solid fa-circle-chevron-left"></i>
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
{{ A_("Back") }}
</a>
</div>

View File

@ -1,57 +0,0 @@
{#
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 %}
<div class="mb-3 d-none d-md-block">
<div class="btn-group mb-3">
<a class="btn btn-primary" href="{{ url_for("accounting.report.default")|accounting_or_next }}">
<i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }}
@ -47,9 +47,9 @@ First written: 2023/2/26
{% for item in list %}
<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 }}">
{% with journal_entry = item %}
{% include "accounting/journal-entry/include/order-journal-entry.html" %}
{% endwith %}
<div>
{{ item }}
</div>
<i class="fa-solid fa-bars"></i>
</li>
{% endfor %}
@ -72,9 +72,7 @@ First written: 2023/2/26
<ul class="list-group mb-3">
{% for item in list %}
<li class="list-group-item">
{% with journal_entry = item %}
{% include "accounting/journal-entry/include/order-journal-entry.html" %}
{% endwith %}
{{ item }}
</li>
{% endfor %}
</ul>

View File

@ -21,10 +21,10 @@ First written: 2023/2/26
#}
{% extends "accounting/journal-entry/include/detail.html" %}
{% block as_trasfer %}
{% block to_transfer %}
<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>
<span class="d-none d-md-inline">{{ A_("As Transfer") }}</span>
{{ A_("To Transfer") }}
</a>
{% endblock %}

View File

@ -19,15 +19,57 @@ form-currency.html: The currency sub-form in the cash receipt journal entry form
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/25
#}
{% extends "accounting/journal-entry/include/form-currency.html" %}
<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 %}" 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>
{% block line_items %}
<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>
<div class="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_("Content") }}</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_forms = credit_forms,
header = A_("Content"),
debit_credit_total = credit_total,
debit_credit_errors = credit_errors %}
{% include "accounting/journal-entry/include/form-debit-credit.html" %}
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 %}
{% endblock %}
{% 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,
credit_forms = currency_form.credit,
credit_errors = currency_form.credit_errors,
credit_total = currency_form.credit_total|accounting_format_amount %}
credit_total = currency_form.form.credit_total|accounting_format_amount %}
{% include "accounting/journal-entry/receipt/include/form-currency.html" %}
{% endwith %}
{% endfor %}

View File

@ -19,30 +19,91 @@ form-currency.html: The currency sub-form in the transfer journal entry form
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/25
#}
{% extends "accounting/journal-entry/include/form-currency.html" %}
<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 %}
<div class="row">
<div class="col-sm-6">
{# The debit line items #}
<div class="col-sm-6 mb-3">
<div id="accounting-currency-{{ currency_index }}-debit" 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">{{ A_("Debit") }}</label>
<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_forms = debit_forms,
header = A_("Debit"),
debit_credit_total = debit_total,
debit_credit_errors = debit_errors %}
{% include "accounting/journal-entry/include/form-debit-credit.html" %}
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 class="col-sm-6">
<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_forms = credit_forms,
header = A_("Credit"),
debit_credit_total = credit_total,
debit_credit_errors = credit_errors %}
{% include "accounting/journal-entry/include/form-debit-credit.html" %}
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>
{% endblock %}
<div id="accounting-currency-{{ currency_index }}-credit-error" class="invalid-feedback">{% if credit_errors %}{{ credit_errors[0] }}{% endif %}</div>
</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,10 +32,10 @@ First written: 2023/2/25
currency_code_is_locked = currency_form.is_code_locked,
debit_forms = currency_form.debit,
debit_errors = currency_form.debit_errors,
debit_total = currency_form.debit_total|accounting_format_amount,
debit_total = currency_form.form.debit_total|accounting_format_amount,
credit_forms = currency_form.credit,
credit_errors = currency_form.credit_errors,
credit_total = currency_form.credit_total|accounting_format_amount %}
credit_total = currency_form.form.credit_total|accounting_format_amount %}
{% include "accounting/journal-entry/transfer/include/form-currency.html" %}
{% endwith %}
{% endfor %}

View File

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

View File

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

View File

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

View File

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

View File

@ -569,43 +569,12 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
:return: None.
"""
from accounting.models import JournalEntry, JournalEntryLineItem
journal_entry_id_1: int \
journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{journal_entry_id_1}?next=%2F_next"
delete_uri: str = f"{PREFIX}/{journal_entry_id_1}/delete"
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next"
delete_uri: str = f"{PREFIX}/{journal_entry_id}/delete"
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)
self.assertEqual(response.status_code, 200)
response = self.client.post(delete_uri,

View File

@ -50,18 +50,6 @@ def create_app(is_testing: bool = False) -> Flask:
"SQLALCHEMY_DATABASE_URI": db_uri,
"BABEL_DEFAULT_LOCALE": "en",
"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:
app.config["TESTING"] = True

View File

@ -27,7 +27,7 @@ First written: 2023/1/27
<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/@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.4.3/dist/css/tempus-dominus.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">
{% block styles %}{% endblock %}
<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>