Compare commits
	
		
			27 Commits
		
	
	
		
			v0.7.0
			...
			d8afadda02
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d8afadda02 | |||
| c8e1270d8f | |||
| 2a78799404 | |||
| 863d7a9368 | |||
| 6fd37b21d9 | |||
| bbf3ee3320 | |||
| b60cc7902d | |||
| 623313b58a | |||
| d0d2d77a2e | |||
| 494faeffea | |||
| 871a5fd1d8 | |||
| e615ad2690 | |||
| da92a0b42c | |||
| 678d0aa773 | |||
| 9248ba7e3b | |||
| 446087b212 | |||
| a42e7d13a2 | |||
| a82f5091f1 | |||
| 3455827c09 | |||
| 5dccf99a55 | |||
| 8818b46e01 | |||
| 2f3ad99467 | |||
| 592910187b | |||
| cb7a0d377f | |||
| 79175285f8 | |||
| fef474977c | |||
| fa1a55cd3d | 
@@ -13,7 +13,7 @@ sys.path.insert(0, os.path.abspath('../../src/'))
 | 
				
			|||||||
project = 'Mia! Accounting Flask'
 | 
					project = 'Mia! Accounting Flask'
 | 
				
			||||||
copyright = '2023, imacat'
 | 
					copyright = '2023, imacat'
 | 
				
			||||||
author = 'imacat'
 | 
					author = 'imacat'
 | 
				
			||||||
release = '0.7.0'
 | 
					release = '0.8.0'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# -- General configuration ---------------------------------------------------
 | 
					# -- General configuration ---------------------------------------------------
 | 
				
			||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 | 
					# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[metadata]
 | 
					[metadata]
 | 
				
			||||||
name = mia-accounting-flask
 | 
					name = mia-accounting-flask
 | 
				
			||||||
version = 0.7.0
 | 
					version = 0.8.0
 | 
				
			||||||
author = imacat
 | 
					author = imacat
 | 
				
			||||||
author_email = imacat@mail.imacat.idv.tw
 | 
					author_email = imacat@mail.imacat.idv.tw
 | 
				
			||||||
description = The Mia! Accounting Flask project.
 | 
					description = The Mia! Accounting Flask project.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -157,6 +157,9 @@ def delete_account(account: Account) -> redirect:
 | 
				
			|||||||
    :return: The redirection to the account list on success, or the account
 | 
					    :return: The redirection to the account list on success, or the account
 | 
				
			||||||
        detail on error.
 | 
					        detail on error.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    if not account.can_delete:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The account cannot be deleted.")), "error")
 | 
				
			||||||
 | 
					        return redirect(inherit_next(__get_detail_uri(account)))
 | 
				
			||||||
    account.delete()
 | 
					    account.delete()
 | 
				
			||||||
    sort_accounts_in(account.base_code, account.id)
 | 
					    sort_accounts_in(account.base_code, account.id)
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -160,6 +160,9 @@ def delete_currency(currency: Currency) -> redirect:
 | 
				
			|||||||
    :return: The redirection to the currency list on success, or the currency
 | 
					    :return: The redirection to the currency list on success, or the currency
 | 
				
			||||||
        detail on error.
 | 
					        detail on error.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    if not currency.can_delete:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The currency cannot be deleted.")), "error")
 | 
				
			||||||
 | 
					        return redirect(inherit_next(__get_detail_uri(currency)))
 | 
				
			||||||
    currency.delete()
 | 
					    currency.delete()
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
    flash(s(lazy_gettext("The currency is deleted successfully.")), "success")
 | 
					    flash(s(lazy_gettext("The currency is deleted successfully.")), "success")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,9 +17,11 @@
 | 
				
			|||||||
"""The description editor.
 | 
					"""The description editor.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
import typing as t
 | 
					import typing as t
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import sqlalchemy as sa
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask import current_app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.models import Account, JournalEntryLineItem
 | 
					from accounting.models import Account, JournalEntryLineItem
 | 
				
			||||||
@@ -143,6 +145,29 @@ class DescriptionType:
 | 
				
			|||||||
        return sorted(self.__tag_dict.values(), key=lambda x: -x.freq)
 | 
					        return sorted(self.__tag_dict.values(), key=lambda x: -x.freq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DescriptionRecurring:
 | 
				
			||||||
 | 
					    """A recurring transaction."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, name: str, template: str, account: Account):
 | 
				
			||||||
 | 
					        """Constructs a recurring transaction.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param name: The name.
 | 
				
			||||||
 | 
					        :param template: The template.
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.name: str = name
 | 
				
			||||||
 | 
					        self.template: str = template
 | 
				
			||||||
 | 
					        self.account: DescriptionAccount = DescriptionAccount(account, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def account_codes(self) -> list[str]:
 | 
				
			||||||
 | 
					        """Returns the account codes by the order of their frequencies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The account codes by the order of their frequencies.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [self.account.code]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DescriptionDebitCredit:
 | 
					class DescriptionDebitCredit:
 | 
				
			||||||
    """The description on debit or credit."""
 | 
					    """The description on debit or credit."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -163,6 +188,8 @@ class DescriptionDebitCredit:
 | 
				
			|||||||
                               DescriptionType] \
 | 
					                               DescriptionType] \
 | 
				
			||||||
            = {x.id: x for x in {self.general, self.travel, self.bus}}
 | 
					            = {x.id: x for x in {self.general, self.travel, self.bus}}
 | 
				
			||||||
        """A dictionary from the type ID to the corresponding tags."""
 | 
					        """A dictionary from the type ID to the corresponding tags."""
 | 
				
			||||||
 | 
					        self.recurring: list[DescriptionRecurring] = []
 | 
				
			||||||
 | 
					        """The recurring transactions."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_tag(self, tag_type: t.Literal["general", "travel", "bus"],
 | 
					    def add_tag(self, tag_type: t.Literal["general", "travel", "bus"],
 | 
				
			||||||
                name: str, account: Account, freq: int) -> None:
 | 
					                name: str, account: Account, freq: int) -> None:
 | 
				
			||||||
@@ -193,6 +220,10 @@ class DescriptionDebitCredit:
 | 
				
			|||||||
                        freq[account.id] = 0
 | 
					                        freq[account.id] = 0
 | 
				
			||||||
                    freq[account.id] \
 | 
					                    freq[account.id] \
 | 
				
			||||||
                        = freq[account.id] + account.freq
 | 
					                        = freq[account.id] + account.freq
 | 
				
			||||||
 | 
					        for recurring in self.recurring:
 | 
				
			||||||
 | 
					            accounts[recurring.account.id] = recurring.account
 | 
				
			||||||
 | 
					            if recurring.account.id not in freq:
 | 
				
			||||||
 | 
					                freq[recurring.account.id] = 0
 | 
				
			||||||
        return [accounts[y] for y in sorted(freq.keys(),
 | 
					        return [accounts[y] for y in sorted(freq.keys(),
 | 
				
			||||||
                                            key=lambda x: -freq[x])]
 | 
					                                            key=lambda x: -freq[x])]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -206,6 +237,14 @@ class DescriptionEditor:
 | 
				
			|||||||
        """The debit tags."""
 | 
					        """The debit tags."""
 | 
				
			||||||
        self.credit: DescriptionDebitCredit = DescriptionDebitCredit("credit")
 | 
					        self.credit: DescriptionDebitCredit = DescriptionDebitCredit("credit")
 | 
				
			||||||
        """The credit tags."""
 | 
					        """The credit tags."""
 | 
				
			||||||
 | 
					        self.__init_tags()
 | 
				
			||||||
 | 
					        self.__init_recurring()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init_tags(self):
 | 
				
			||||||
 | 
					        """Initializes the tags.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
        debit_credit: sa.Label = sa.case(
 | 
					        debit_credit: sa.Label = sa.case(
 | 
				
			||||||
            (JournalEntryLineItem.is_debit, "debit"),
 | 
					            (JournalEntryLineItem.is_debit, "debit"),
 | 
				
			||||||
            else_="credit").label("debit_credit")
 | 
					            else_="credit").label("debit_credit")
 | 
				
			||||||
@@ -236,6 +275,49 @@ class DescriptionEditor:
 | 
				
			|||||||
            debit_credit_dict[row.debit_credit].add_tag(
 | 
					            debit_credit_dict[row.debit_credit].add_tag(
 | 
				
			||||||
                row.tag_type, row.tag, accounts[row.account_id], row.freq)
 | 
					                row.tag_type, row.tag, accounts[row.account_id], row.freq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init_recurring(self) -> None:
 | 
				
			||||||
 | 
					        """Initializes the recurring transactions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if "ACCOUNTING_RECURRING" not in current_app.config:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        data: list[tuple[t.Literal["debit", "credit"], str, str, str]] \
 | 
				
			||||||
 | 
					            = [x.split("|")
 | 
				
			||||||
 | 
					               for x in current_app.config["ACCOUNTING_RECURRING"].split(",")]
 | 
				
			||||||
 | 
					        debit_credit_dict: dict[t.Literal["debit", "credit"],
 | 
				
			||||||
 | 
					                                DescriptionDebitCredit] \
 | 
				
			||||||
 | 
					            = {x.debit_credit: x for x in {self.debit, self.credit}}
 | 
				
			||||||
 | 
					        accounts: dict[str, Account] \
 | 
				
			||||||
 | 
					            = self.__get_accounts({x[1] for x in data})
 | 
				
			||||||
 | 
					        for row in data:
 | 
				
			||||||
 | 
					            debit_credit_dict[row[0]].recurring.append(
 | 
				
			||||||
 | 
					                DescriptionRecurring(row[2], row[3], accounts[row[1]]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def __get_accounts(codes: set[str]) -> dict[str, Account]:
 | 
				
			||||||
 | 
					        """Finds and returns the accounts by codes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param codes: The account codes.
 | 
				
			||||||
 | 
					        :return: The account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        def get_condition(code0: str) -> sa.BinaryExpression:
 | 
				
			||||||
 | 
					            m: re.Match = re.match(r"^(\d{4})-(\d{3})$", code0)
 | 
				
			||||||
 | 
					            assert m is not None,\
 | 
				
			||||||
 | 
					                f"Malformed account code \"{code0}\" for regular transactions."
 | 
				
			||||||
 | 
					            return sa.and_(Account.base_code == m.group(1),
 | 
				
			||||||
 | 
					                           Account.no == int(m.group(2)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [get_condition(x) for x in codes]
 | 
				
			||||||
 | 
					        accounts: dict[str, Account] \
 | 
				
			||||||
 | 
					            = {x.code: x for x in
 | 
				
			||||||
 | 
					               Account.query.filter(sa.or_(*conditions)).all()}
 | 
				
			||||||
 | 
					        for code in codes:
 | 
				
			||||||
 | 
					            assert code in accounts,\
 | 
				
			||||||
 | 
					                f"Unknown account \"{code}\" for regular transactions."
 | 
				
			||||||
 | 
					        return accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_prefix(string: str | sa.Column, separator: str | sa.Column) \
 | 
					def get_prefix(string: str | sa.Column, separator: str | sa.Column) \
 | 
				
			||||||
        -> sa.Function:
 | 
					        -> sa.Function:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -175,6 +175,9 @@ def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
 | 
				
			|||||||
    :return: The redirection to the journal entry list on success, or the
 | 
					    :return: The redirection to the journal entry list on success, or the
 | 
				
			||||||
        journal entry detail on error.
 | 
					        journal entry detail on error.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    if not journal_entry.can_delete:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The journal entry cannot be deleted.")), "error")
 | 
				
			||||||
 | 
					        return redirect(inherit_next(__get_detail_uri(journal_entry)))
 | 
				
			||||||
    journal_entry.delete()
 | 
					    journal_entry.delete()
 | 
				
			||||||
    sort_journal_entries_in(journal_entry.date, journal_entry.id)
 | 
					    sort_journal_entries_in(journal_entry.date, journal_entry.id)
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,8 +25,8 @@ from datetime import date
 | 
				
			|||||||
from decimal import Decimal
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import sqlalchemy as sa
 | 
					import sqlalchemy as sa
 | 
				
			||||||
from flask import current_app
 | 
					from babel import Locale
 | 
				
			||||||
from flask_babel import get_locale
 | 
					from flask_babel import get_locale, get_babel
 | 
				
			||||||
from sqlalchemy import text
 | 
					from sqlalchemy import text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
@@ -61,11 +61,11 @@ class BaseAccount(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The title in the current locale.
 | 
					        :return: The title in the current locale.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            return self.title_l10n
 | 
					            return self.title_l10n
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                return l10n.title
 | 
					                return l10n.title
 | 
				
			||||||
        return self.title_l10n
 | 
					        return self.title_l10n
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -171,11 +171,11 @@ class Account(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The title in the current locale.
 | 
					        :return: The title in the current locale.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            return self.title_l10n
 | 
					            return self.title_l10n
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                return l10n.title
 | 
					                return l10n.title
 | 
				
			||||||
        return self.title_l10n
 | 
					        return self.title_l10n
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -189,15 +189,15 @@ class Account(db.Model):
 | 
				
			|||||||
        if self.title_l10n is None:
 | 
					        if self.title_l10n is None:
 | 
				
			||||||
            self.title_l10n = value
 | 
					            self.title_l10n = value
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            self.title_l10n = value
 | 
					            self.title_l10n = value
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                l10n.title = value
 | 
					                l10n.title = value
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
        self.l10n.append(AccountL10n(locale=current_locale, title=value))
 | 
					        self.l10n.append(AccountL10n(locale=str(current_locale), title=value))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def is_real(self) -> bool:
 | 
					    def is_real(self) -> bool:
 | 
				
			||||||
@@ -236,6 +236,16 @@ class Account(db.Model):
 | 
				
			|||||||
                return True
 | 
					                return True
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def can_delete(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether the account can be deleted.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if the account can be deleted, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.code in {"1111-001", "3351-001", "3353-001"}:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        return len(self.line_items) == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self) -> None:
 | 
					    def delete(self) -> None:
 | 
				
			||||||
        """Deletes this account.
 | 
					        """Deletes this account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -381,11 +391,11 @@ class Currency(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The name in the current locale.
 | 
					        :return: The name in the current locale.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            return self.name_l10n
 | 
					            return self.name_l10n
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                return l10n.name
 | 
					                return l10n.name
 | 
				
			||||||
        return self.name_l10n
 | 
					        return self.name_l10n
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -399,15 +409,15 @@ class Currency(db.Model):
 | 
				
			|||||||
        if self.name_l10n is None:
 | 
					        if self.name_l10n is None:
 | 
				
			||||||
            self.name_l10n = value
 | 
					            self.name_l10n = value
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            self.name_l10n = value
 | 
					            self.name_l10n = value
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                l10n.name = value
 | 
					                l10n.name = value
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
        self.l10n.append(CurrencyL10n(locale=current_locale, name=value))
 | 
					        self.l10n.append(CurrencyL10n(locale=str(current_locale), name=value))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def is_modified(self) -> bool:
 | 
					    def is_modified(self) -> bool:
 | 
				
			||||||
@@ -422,6 +432,17 @@ class Currency(db.Model):
 | 
				
			|||||||
                return True
 | 
					                return True
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def can_delete(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether the currency can be deleted.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if the currency can be deleted, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        from accounting.template_globals import default_currency_code
 | 
				
			||||||
 | 
					        if self.code == default_currency_code():
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        return len(self.line_items) == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self) -> None:
 | 
					    def delete(self) -> None:
 | 
				
			||||||
        """Deletes the currency.
 | 
					        """Deletes the currency.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -597,14 +618,10 @@ class JournalEntry(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: True if the journal entry can be deleted, or False otherwise.
 | 
					        :return: True if the journal entry can be deleted, or False otherwise.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if not hasattr(self, "__can_delete"):
 | 
					        for line_item in self.line_items:
 | 
				
			||||||
            def has_offset() -> bool:
 | 
					            if len(line_item.offsets) > 0:
 | 
				
			||||||
                for line_item in self.line_items:
 | 
					 | 
				
			||||||
                    if len(line_item.offsets) > 0:
 | 
					 | 
				
			||||||
                        return True
 | 
					 | 
				
			||||||
                return False
 | 
					                return False
 | 
				
			||||||
            setattr(self, "__can_delete", not has_offset())
 | 
					        return True
 | 
				
			||||||
        return getattr(self, "__can_delete")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self) -> None:
 | 
					    def delete(self) -> None:
 | 
				
			||||||
        """Deletes the journal entry.
 | 
					        """Deletes the journal entry.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -71,8 +71,8 @@ def default_ie_account_code() -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    :return: The default account code for the income and expenses log.
 | 
					    :return: The default account code for the income and expenses log.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    with current_app.app_context():
 | 
					    return current_app.config.get("ACCOUNTING_DEFAULT_IE_ACCOUNT",
 | 
				
			||||||
        return current_app.config.get("DEFAULT_IE_ACCOUNT", Account.CASH_CODE)
 | 
					                                  Account.CASH_CODE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def default_ie_account() -> IncomeExpensesAccount:
 | 
					def default_ie_account() -> IncomeExpensesAccount:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,11 +72,14 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@media(max-width:767px) {
 | 
					@media(max-width:767px) {
 | 
				
			||||||
 | 
					    .accounting-toolbar {
 | 
				
			||||||
 | 
					    	width: 100%;
 | 
				
			||||||
 | 
					    	justify-content: space-evenly;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    .accounting-toolbar > .btn:not(form), .accounting-toolbar > .btn-group > .btn {
 | 
					    .accounting-toolbar > .btn:not(form), .accounting-toolbar > .btn-group > .btn {
 | 
				
			||||||
        height: 3.2rem;
 | 
					        height: 3.2rem;
 | 
				
			||||||
        width: 3.2rem;
 | 
					        width: 3.2rem;
 | 
				
			||||||
        border-radius: 50%;
 | 
					        border-radius: 50%;
 | 
				
			||||||
        margin-left: 1rem;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    .accounting-toolbar > a.btn, .accounting-toolbar > .btn-group > a.btn {
 | 
					    .accounting-toolbar > a.btn, .accounting-toolbar > .btn-group > a.btn {
 | 
				
			||||||
        padding-top: 0.7rem;
 | 
					        padding-top: 0.7rem;
 | 
				
			||||||
@@ -315,6 +318,22 @@ a.accounting-report-table-row {
 | 
				
			|||||||
    padding-left: 1rem;
 | 
					    padding-left: 1rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* The description editor */
 | 
				
			||||||
 | 
					.accounting-description-editor-buttons .btn {
 | 
				
			||||||
 | 
					    margin-bottom: 0.3rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* The order of the journal entries in a same day */
 | 
				
			||||||
 | 
					.accounting-journal-entry-order-item, .accounting-journal-entry-order-item:hover {
 | 
				
			||||||
 | 
					    color: inherit;
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-journal-entry-order-item-currency {
 | 
				
			||||||
 | 
					    margin-left: 0.5rem;
 | 
				
			||||||
 | 
					    border-top: thin solid lightgray;
 | 
				
			||||||
 | 
					    margin-top: 0.2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* The Material Design text field (floating form control in Bootstrap) */
 | 
					/* The Material Design text field (floating form control in Bootstrap) */
 | 
				
			||||||
.accounting-material-text-field {
 | 
					.accounting-material-text-field {
 | 
				
			||||||
    position: relative;
 | 
					    position: relative;
 | 
				
			||||||
@@ -339,7 +358,7 @@ a.accounting-report-table-row {
 | 
				
			|||||||
.accounting-material-fab {
 | 
					.accounting-material-fab {
 | 
				
			||||||
    position: fixed;
 | 
					    position: fixed;
 | 
				
			||||||
    right: 2rem;
 | 
					    right: 2rem;
 | 
				
			||||||
    bottom: 1rem;
 | 
					    bottom: 2rem;
 | 
				
			||||||
    z-index: 10;
 | 
					    z-index: 10;
 | 
				
			||||||
    flex-direction: column-reverse;
 | 
					    flex-direction: column-reverse;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,7 @@ class DescriptionEditor {
 | 
				
			|||||||
     * The line item editor
 | 
					     * The line item editor
 | 
				
			||||||
     * @type {JournalEntryLineItemEditor}
 | 
					     * @type {JournalEntryLineItemEditor}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #lineItemEditor;
 | 
					    lineItemEditor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The description editor form
 | 
					     * The description editor form
 | 
				
			||||||
@@ -102,7 +102,7 @@ class DescriptionEditor {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The tab planes
 | 
					     * The tab planes
 | 
				
			||||||
     * @type {{general: GeneralTagTab, travel: GeneralTripTab, bus: BusTripTab, regular: RegularPaymentTab, annotation: AnnotationTab}}
 | 
					     * @type {{general: GeneralTagTab, travel: GeneralTripTab, bus: BusTripTab, recurring: RecurringTransactionTab, annotation: AnnotationTab}}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    tabPlanes = {};
 | 
					    tabPlanes = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -113,7 +113,7 @@ class DescriptionEditor {
 | 
				
			|||||||
     * @param debitCredit {string} either "debit" or "credit"
 | 
					     * @param debitCredit {string} either "debit" or "credit"
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(lineItemEditor, debitCredit) {
 | 
					    constructor(lineItemEditor, debitCredit) {
 | 
				
			||||||
        this.#lineItemEditor = lineItemEditor;
 | 
					        this.lineItemEditor = lineItemEditor;
 | 
				
			||||||
        this.debitCredit = debitCredit;
 | 
					        this.debitCredit = debitCredit;
 | 
				
			||||||
        this.prefix = "accounting-description-editor-" + debitCredit;
 | 
					        this.prefix = "accounting-description-editor-" + debitCredit;
 | 
				
			||||||
        this.#form = document.getElementById(this.prefix);
 | 
					        this.#form = document.getElementById(this.prefix);
 | 
				
			||||||
@@ -125,14 +125,14 @@ class DescriptionEditor {
 | 
				
			|||||||
        // noinspection JSValidateTypes
 | 
					        // noinspection JSValidateTypes
 | 
				
			||||||
        this.#accountButtons = Array.from(document.getElementsByClassName(this.prefix + "-account"));
 | 
					        this.#accountButtons = Array.from(document.getElementsByClassName(this.prefix + "-account"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const cls of [GeneralTagTab, GeneralTripTab, BusTripTab, RegularPaymentTab, AnnotationTab]) {
 | 
					        for (const cls of [GeneralTagTab, GeneralTripTab, BusTripTab, RecurringTransactionTab, AnnotationTab]) {
 | 
				
			||||||
            const tab = new cls(this);
 | 
					            const tab = new cls(this);
 | 
				
			||||||
            this.tabPlanes[tab.tabId()] = tab;
 | 
					            this.tabPlanes[tab.tabId()] = tab;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.currentTab = this.tabPlanes.general;
 | 
					        this.currentTab = this.tabPlanes.general;
 | 
				
			||||||
        this.#initializeSuggestedAccounts();
 | 
					        this.#initializeSuggestedAccounts();
 | 
				
			||||||
        this.description.onchange = () => this.#onDescriptionChange();
 | 
					        this.description.onchange = () => this.#onDescriptionChange();
 | 
				
			||||||
        this.#offsetButton.onclick = () => this.#lineItemEditor.originalLineItemSelector.onOpen();
 | 
					        this.#offsetButton.onclick = () => this.lineItemEditor.originalLineItemSelector.onOpen();
 | 
				
			||||||
        this.#form.onsubmit = () => {
 | 
					        this.#form.onsubmit = () => {
 | 
				
			||||||
            if (this.currentTab.validate()) {
 | 
					            if (this.currentTab.validate()) {
 | 
				
			||||||
                this.#submit();
 | 
					                this.#submit();
 | 
				
			||||||
@@ -147,7 +147,7 @@ class DescriptionEditor {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    #onDescriptionChange() {
 | 
					    #onDescriptionChange() {
 | 
				
			||||||
        this.description.value = this.description.value.trim();
 | 
					        this.description.value = this.description.value.trim();
 | 
				
			||||||
        for (const tabPlane of [this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) {
 | 
					        for (const tabPlane of [this.tabPlanes.recurring, this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) {
 | 
				
			||||||
            if (tabPlane.populate()) {
 | 
					            if (tabPlane.populate()) {
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -158,27 +158,31 @@ class DescriptionEditor {
 | 
				
			|||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Filters the suggested accounts.
 | 
					     * Filters the suggested accounts.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param tagButton {HTMLButtonElement|null} the tag button
 | 
					     * @param tagButton {HTMLButtonElement} the tag button
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    filterSuggestedAccounts(tagButton) {
 | 
					    filterSuggestedAccounts(tagButton) {
 | 
				
			||||||
        for (const accountButton of this.#accountButtons) {
 | 
					        this.clearSuggestedAccounts();
 | 
				
			||||||
            accountButton.classList.add("d-none");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (tagButton === null) {
 | 
					 | 
				
			||||||
            this.#selectAccount(null);
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        const suggested = JSON.parse(tagButton.dataset.accounts);
 | 
					        const suggested = JSON.parse(tagButton.dataset.accounts);
 | 
				
			||||||
        let selectedAccountButton = null;
 | 
					 | 
				
			||||||
        for (const accountButton of this.#accountButtons) {
 | 
					        for (const accountButton of this.#accountButtons) {
 | 
				
			||||||
            if (suggested.includes(accountButton.dataset.code)) {
 | 
					            if (suggested.includes(accountButton.dataset.code)) {
 | 
				
			||||||
                accountButton.classList.remove("d-none");
 | 
					                accountButton.classList.remove("d-none");
 | 
				
			||||||
                if (accountButton.dataset.code === suggested[0]) {
 | 
					                if (accountButton.dataset.code === suggested[0]) {
 | 
				
			||||||
                    selectedAccountButton = accountButton;
 | 
					                    this.#selectAccount(accountButton);
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.#selectAccount(selectedAccountButton);
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Clears the suggested accounts.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    clearSuggestedAccounts() {
 | 
				
			||||||
 | 
					        for (const accountButton of this.#accountButtons) {
 | 
				
			||||||
 | 
					            accountButton.classList.add("d-none");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.#selectAccount(null);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -215,9 +219,9 @@ class DescriptionEditor {
 | 
				
			|||||||
    #submit() {
 | 
					    #submit() {
 | 
				
			||||||
        bootstrap.Modal.getOrCreateInstance(this.#modal).hide();
 | 
					        bootstrap.Modal.getOrCreateInstance(this.#modal).hide();
 | 
				
			||||||
        if (this.#selectedAccount !== null) {
 | 
					        if (this.#selectedAccount !== null) {
 | 
				
			||||||
            this.#lineItemEditor.saveDescriptionWithAccount(this.description.value, this.#selectedAccount.dataset.code, this.#selectedAccount.dataset.text, this.#selectedAccount.classList.contains("accounting-account-is-need-offset"));
 | 
					            this.lineItemEditor.saveDescriptionWithAccount(this.description.value, this.#selectedAccount.dataset.code, this.#selectedAccount.dataset.text, this.#selectedAccount.classList.contains("accounting-account-is-need-offset"));
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            this.#lineItemEditor.saveDescription(this.description.value);
 | 
					            this.lineItemEditor.saveDescription(this.description.value);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -227,7 +231,7 @@ class DescriptionEditor {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    onOpen() {
 | 
					    onOpen() {
 | 
				
			||||||
        this.#reset();
 | 
					        this.#reset();
 | 
				
			||||||
        this.description.value = this.#lineItemEditor.description === null? "": this.#lineItemEditor.description;
 | 
					        this.description.value = this.lineItemEditor.description === null? "": this.lineItemEditor.description;
 | 
				
			||||||
        this.#onDescriptionChange();
 | 
					        this.#onDescriptionChange();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -418,7 +422,7 @@ class TagTabPlane extends TabPlane {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (!isMatched) {
 | 
					        if (!isMatched) {
 | 
				
			||||||
            this.editor.filterSuggestedAccounts(null);
 | 
					            this.editor.clearSuggestedAccounts();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.validateTag();
 | 
					        this.validateTag();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -436,14 +440,13 @@ class TagTabPlane extends TabPlane {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    switchToMe() {
 | 
					    switchToMe() {
 | 
				
			||||||
        super.switchToMe();
 | 
					        super.switchToMe();
 | 
				
			||||||
        let selectedTagButton = null;
 | 
					 | 
				
			||||||
        for (const tagButton of this.tagButtons) {
 | 
					        for (const tagButton of this.tagButtons) {
 | 
				
			||||||
            if (tagButton.classList.contains("btn-primary")) {
 | 
					            if (tagButton.classList.contains("btn-primary")) {
 | 
				
			||||||
                selectedTagButton = tagButton;
 | 
					                this.editor.filterSuggestedAccounts(tagButton);
 | 
				
			||||||
                break;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.editor.filterSuggestedAccounts(selectedTagButton);
 | 
					        this.editor.clearSuggestedAccounts();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -561,13 +564,6 @@ class GeneralTagTab extends TagTabPlane {
 | 
				
			|||||||
            this.tag.value = found[1];
 | 
					            this.tag.value = found[1];
 | 
				
			||||||
            this.onTagChange();
 | 
					            this.onTagChange();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        for (const tagButton of this.tagButtons) {
 | 
					 | 
				
			||||||
            if (tagButton.dataset.value === this.tag.value) {
 | 
					 | 
				
			||||||
                tagButton.classList.remove("btn-outline-primary");
 | 
					 | 
				
			||||||
                tagButton.classList.add("btn-primary");
 | 
					 | 
				
			||||||
                this.editor.filterSuggestedAccounts(tagButton);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this.switchToMe();
 | 
					        this.switchToMe();
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -732,13 +728,6 @@ class GeneralTripTab extends TagTabPlane {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.#to.value = found[4];
 | 
					        this.#to.value = found[4];
 | 
				
			||||||
        for (const tagButton of this.tagButtons) {
 | 
					 | 
				
			||||||
            if (tagButton.dataset.value === this.tag.value) {
 | 
					 | 
				
			||||||
                tagButton.classList.remove("btn-outline-primary");
 | 
					 | 
				
			||||||
                tagButton.classList.add("btn-primary");
 | 
					 | 
				
			||||||
                this.editor.filterSuggestedAccounts(tagButton);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this.switchToMe();
 | 
					        this.switchToMe();
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -917,14 +906,6 @@ class BusTripTab extends TagTabPlane {
 | 
				
			|||||||
        this.#route.value = found[2];
 | 
					        this.#route.value = found[2];
 | 
				
			||||||
        this.#from.value = found[3];
 | 
					        this.#from.value = found[3];
 | 
				
			||||||
        this.#to.value = found[4];
 | 
					        this.#to.value = found[4];
 | 
				
			||||||
        for (const tagButton of this.tagButtons) {
 | 
					 | 
				
			||||||
            if (tagButton.dataset.value === this.tag.value) {
 | 
					 | 
				
			||||||
                tagButton.classList.remove("btn-outline-primary");
 | 
					 | 
				
			||||||
                tagButton.classList.add("btn-primary");
 | 
					 | 
				
			||||||
                this.editor.filterSuggestedAccounts(tagButton);
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this.switchToMe();
 | 
					        this.switchToMe();
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -985,17 +966,23 @@ class BusTripTab extends TagTabPlane {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The regular payment tab plane.
 | 
					 * The recurring transaction tab plane.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @private
 | 
					 * @private
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class RegularPaymentTab extends TabPlane {
 | 
					class RecurringTransactionTab extends TabPlane {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The payment buttons
 | 
					     * The month names
 | 
				
			||||||
 | 
					     * @type {string[]}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #monthNames;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The buttons of the recurring items
 | 
				
			||||||
     * @type {HTMLButtonElement[]}
 | 
					     * @type {HTMLButtonElement[]}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #payments;
 | 
					    #itemButtons;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // noinspection JSValidateTypes
 | 
					    // noinspection JSValidateTypes
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -1006,8 +993,44 @@ class RegularPaymentTab extends TabPlane {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(editor) {
 | 
					    constructor(editor) {
 | 
				
			||||||
        super(editor);
 | 
					        super(editor);
 | 
				
			||||||
 | 
					        this.#monthNames = [
 | 
				
			||||||
 | 
					            "",
 | 
				
			||||||
 | 
					            A_("January"), A_("February"), A_("March"), A_("April"),
 | 
				
			||||||
 | 
					            A_("May"), A_("June"), A_("July"), A_("August"),
 | 
				
			||||||
 | 
					            A_("September"), A_("October"), A_("November"), A_("December"),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
        // noinspection JSValidateTypes
 | 
					        // noinspection JSValidateTypes
 | 
				
			||||||
        this.#payments = Array.from(document.getElementsByClassName(this.prefix + "-payment"));
 | 
					        this.#itemButtons = Array.from(document.getElementsByClassName(this.prefix + "-item"));
 | 
				
			||||||
 | 
					        for (const itemButton of this.#itemButtons) {
 | 
				
			||||||
 | 
					            itemButton.onclick = () => {
 | 
				
			||||||
 | 
					                this.reset();
 | 
				
			||||||
 | 
					                itemButton.classList.add("btn-primary");
 | 
				
			||||||
 | 
					                itemButton.classList.remove("btn-outline-primary");
 | 
				
			||||||
 | 
					                this.editor.description.value = this.#getDescription(itemButton);
 | 
				
			||||||
 | 
					                this.editor.filterSuggestedAccounts(itemButton);
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns the description for a recurring item.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param itemButton {HTMLButtonElement} the recurring item
 | 
				
			||||||
 | 
					     * @return {string} the description of the recurring item
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #getDescription(itemButton) {
 | 
				
			||||||
 | 
					        const today = new Date(this.editor.lineItemEditor.form.getDate());
 | 
				
			||||||
 | 
					        const thisMonth = today.getMonth() + 1;
 | 
				
			||||||
 | 
					        const lastMonth = (thisMonth + 10) % 12 + 1;
 | 
				
			||||||
 | 
					        const lastBimonthlyFrom = ((thisMonth + thisMonth % 2 + 8) % 12 + 1);
 | 
				
			||||||
 | 
					        const lastBimonthlyTo = ((thisMonth + thisMonth % 2 + 9) % 12 + 1);
 | 
				
			||||||
 | 
					        return itemButton.dataset.template
 | 
				
			||||||
 | 
					            .replaceAll("{this_month_number}", String(thisMonth))
 | 
				
			||||||
 | 
					            .replaceAll("{this_month_name}", this.#monthNames[thisMonth])
 | 
				
			||||||
 | 
					            .replaceAll("{last_month_number}", String(lastMonth))
 | 
				
			||||||
 | 
					            .replaceAll("{last_month_name}", this.#monthNames[lastMonth])
 | 
				
			||||||
 | 
					            .replaceAll("{last_bimonthly_number}", String(lastBimonthlyFrom) + "–" + String(lastBimonthlyTo))
 | 
				
			||||||
 | 
					            .replaceAll("{last_bimonthly_name}", this.#monthNames[lastBimonthlyFrom] + "–" + this.#monthNames[lastBimonthlyTo]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -1017,7 +1040,7 @@ class RegularPaymentTab extends TabPlane {
 | 
				
			|||||||
     * @abstract
 | 
					     * @abstract
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    tabId() {
 | 
					    tabId() {
 | 
				
			||||||
        return "regular";
 | 
					        return "recurring";
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -1026,9 +1049,9 @@ class RegularPaymentTab extends TabPlane {
 | 
				
			|||||||
     * @override
 | 
					     * @override
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    reset() {
 | 
					    reset() {
 | 
				
			||||||
        for (const payment of this.#payments) {
 | 
					        for (const itemButton of this.#itemButtons) {
 | 
				
			||||||
            payment.classList.remove("btn-primary");
 | 
					            itemButton.classList.remove("btn-primary");
 | 
				
			||||||
            payment.classList.add("btn-outline-primary");
 | 
					            itemButton.classList.add("btn-outline-primary");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1039,9 +1062,32 @@ class RegularPaymentTab extends TabPlane {
 | 
				
			|||||||
     * @override
 | 
					     * @override
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    populate() {
 | 
					    populate() {
 | 
				
			||||||
 | 
					        for (const itemButton of this.#itemButtons) {
 | 
				
			||||||
 | 
					            if (this.#getDescription(itemButton) === this.editor.description.value) {
 | 
				
			||||||
 | 
					                itemButton.classList.add("btn-primary");
 | 
				
			||||||
 | 
					                itemButton.classList.remove("btn-outline-primary");
 | 
				
			||||||
 | 
					                this.switchToMe();
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Switches to the tab plane.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    switchToMe() {
 | 
				
			||||||
 | 
					        super.switchToMe();
 | 
				
			||||||
 | 
					        for (const itemButton of this.#itemButtons) {
 | 
				
			||||||
 | 
					            if (itemButton.classList.contains("btn-primary")) {
 | 
				
			||||||
 | 
					                this.editor.filterSuggestedAccounts(itemButton);
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.editor.clearSuggestedAccounts();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Validates the input in the tab plane.
 | 
					     * Validates the input in the tab plane.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,5 +35,4 @@ def default_currency_code() -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    :return: The default currency code.
 | 
					    :return: The default currency code.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    with current_app.app_context():
 | 
					    return current_app.config.get("ACCOUNTING_DEFAULT_CURRENCY", "USD")
 | 
				
			||||||
        return current_app.config.get("DEFAULT_CURRENCY", "USD")
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,26 +25,33 @@ First written: 2023/1/31
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group mb-3">
 | 
					<div class="mb-3 accounting-toolbar">
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
 | 
					  <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
					    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
				
			||||||
    {{ A_("Back") }}
 | 
					    {{ A_("Back") }}
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
  {% if accounting_can_edit() %}
 | 
					  {% if accounting_can_edit() %}
 | 
				
			||||||
    <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
 | 
					    <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
 | 
				
			||||||
      <i class="fa-solid fa-gear"></i>
 | 
					      <i class="fa-solid fa-pen-to-square"></i>
 | 
				
			||||||
      {{ A_("Settings") }}
 | 
					      {{ A_("Edit") }}
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.account.order", base=obj.base)|accounting_append_next }}">
 | 
					  <a class="btn btn-primary" href="{{ url_for("accounting.account.order", base=obj.base)|accounting_append_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-bars-staggered"></i>
 | 
					    <i class="fa-solid fa-bars-staggered"></i>
 | 
				
			||||||
    {{ A_("Order") }}
 | 
					    <span class="d-none d-md-inline">{{ A_("Order") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
  {% if accounting_can_edit() %}
 | 
					  {% if accounting_can_edit() %}
 | 
				
			||||||
    <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
 | 
					    {% if obj.can_delete %}
 | 
				
			||||||
      <i class="fa-solid fa-trash"></i>
 | 
					      <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
 | 
				
			||||||
      {{ A_("Delete") }}
 | 
					        <i class="fa-solid fa-trash"></i>
 | 
				
			||||||
    </button>
 | 
					        <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    {% else %}
 | 
				
			||||||
 | 
					      <button class="btn btn-secondary" type="button" disabled="disabled">
 | 
				
			||||||
 | 
					        <i class="fa-solid fa-trash"></i>
 | 
				
			||||||
 | 
					        <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,7 +63,7 @@ First written: 2023/1/31
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% if accounting_can_edit() %}
 | 
					{% if accounting_can_edit() and obj.can_delete %}
 | 
				
			||||||
  <form action="{{ url_for("accounting.account.delete", account=obj) }}" method="post">
 | 
					  <form action="{{ url_for("accounting.account.delete", account=obj) }}" method="post">
 | 
				
			||||||
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
					    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
				
			||||||
    {% if request.args.next %}
 | 
					    {% if request.args.next %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,7 +27,7 @@ First written: 2023/2/1
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group mb-3">
 | 
					<div class="mb-3 d-none d-md-block">
 | 
				
			||||||
  <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
 | 
					  <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
 | 
				
			||||||
    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
					    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
				
			||||||
    {{ A_("Back") }}
 | 
					    {{ A_("Back") }}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,7 +30,7 @@ First written: 2023/2/2
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group mb-3">
 | 
					<div class="mb-3 d-none d-md-block">
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
 | 
					  <a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
					    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
				
			||||||
    {{ A_("Back") }}
 | 
					    {{ A_("Back") }}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,7 @@ First written: 2023/2/1
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group mb-3">
 | 
					<div class="mb-3 d-none d-md-block">
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
 | 
					  <a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
					    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
				
			||||||
    {{ A_("Back") }}
 | 
					    {{ A_("Back") }}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,22 +25,29 @@ First written: 2023/2/6
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group mb-3">
 | 
					<div class="mb-3 accounting-toolbar">
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.currency.list")|accounting_or_next }}">
 | 
					  <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.currency.list")|accounting_or_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
					    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
				
			||||||
    {{ A_("Back") }}
 | 
					    {{ A_("Back") }}
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
  {% if accounting_can_edit() %}
 | 
					  {% if accounting_can_edit() %}
 | 
				
			||||||
    <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.currency.edit", currency=obj)|accounting_inherit_next }}">
 | 
					    <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.currency.edit", currency=obj)|accounting_inherit_next }}">
 | 
				
			||||||
      <i class="fa-solid fa-gear"></i>
 | 
					      <i class="fa-solid fa-pen-to-square"></i>
 | 
				
			||||||
      {{ A_("Settings") }}
 | 
					      {{ A_("Edit") }}
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
  {% if accounting_can_edit() %}
 | 
					  {% if accounting_can_edit() %}
 | 
				
			||||||
    <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
 | 
					    {% if obj.can_delete %}
 | 
				
			||||||
      <i class="fa-solid fa-trash"></i>
 | 
					      <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
 | 
				
			||||||
      {{ A_("Delete") }}
 | 
					        <i class="fa-solid fa-trash"></i>
 | 
				
			||||||
    </button>
 | 
					        <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    {% else %}
 | 
				
			||||||
 | 
					      <button class="btn btn-secondary" type="button" disabled="disabled">
 | 
				
			||||||
 | 
					        <i class="fa-solid fa-trash"></i>
 | 
				
			||||||
 | 
					        <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -52,7 +59,7 @@ First written: 2023/2/6
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% if accounting_can_edit() %}
 | 
					{% if accounting_can_edit() and obj.can_delete %}
 | 
				
			||||||
  <form action="{{ url_for("accounting.currency.delete", currency=obj) }}" method="post">
 | 
					  <form action="{{ url_for("accounting.currency.delete", currency=obj) }}" method="post">
 | 
				
			||||||
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
					    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
				
			||||||
    {% if request.args.next %}
 | 
					    {% if request.args.next %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,7 +27,7 @@ First written: 2023/2/6
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group mb-3">
 | 
					<div class="mb-3 d-none d-md-block">
 | 
				
			||||||
  <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
 | 
					  <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
 | 
				
			||||||
    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
					    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
				
			||||||
    {{ A_("Back") }}
 | 
					    {{ A_("Back") }}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,10 +21,10 @@ First written: 2023/2/26
 | 
				
			|||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/journal-entry/include/detail.html" %}
 | 
					{% extends "accounting/journal-entry/include/detail.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block to_transfer %}
 | 
					{% block as_trasfer %}
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_journal_entry_to_transfer|accounting_inherit_next }}">
 | 
					  <a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_journal_entry_to_transfer|accounting_inherit_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-bars-staggered"></i>
 | 
					    <i class="fa-solid fa-table-columns"></i>
 | 
				
			||||||
    {{ A_("To Transfer") }}
 | 
					    <span class="d-none d-md-inline">{{ A_("As Transfer") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,57 +19,15 @@ form-currency.html: The currency sub-form in the cash disbursement journal entry
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/25
 | 
					First written: 2023/2/25
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
<div id="accounting-currency-{{ currency_index }}" class="mb-3 accounting-currency" data-index="{{ currency_index }}">
 | 
					{% extends "accounting/journal-entry/include/form-currency.html" %}
 | 
				
			||||||
  <input id="accounting-currency-{{ currency_index }}-no" type="hidden" name="currency-{{ currency_index }}-no" value="{{ currency_index }}">
 | 
					 | 
				
			||||||
  <input id="accounting-currency-{{ currency_index }}-code" type="hidden" name="currency-{{ currency_index }}-code" value="{{ currency_code_data }}">
 | 
					 | 
				
			||||||
  <div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}">
 | 
					 | 
				
			||||||
    <div class="d-flex justify-content-between mt-2 mb-3">
 | 
					 | 
				
			||||||
      <div class="form-floating accounting-currency-content">
 | 
					 | 
				
			||||||
        <select id="accounting-currency-{{ currency_index }}-code-select" class="form-select {% if currency_code_errors %} is-invalid {% endif %}" name="currency-{{ currency_index }}-code" {% if currency_code_is_locked %} disabled="disabled" {% endif %}>
 | 
					 | 
				
			||||||
          {% for currency in accounting_currency_options() %}
 | 
					 | 
				
			||||||
            <option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
 | 
					 | 
				
			||||||
          {% endfor %}
 | 
					 | 
				
			||||||
        </select>
 | 
					 | 
				
			||||||
        <label class="form-label" for="accounting-currency-{{ currency_index }}-code-select">{{ A_("Currency") }}</label>
 | 
					 | 
				
			||||||
        <div id="accounting-currency-{{ currency_index }}-code-error" class="invalid-feedback">{% if currency_code_errors %}{{ currency_code_errors[0] }}{% endif %}</div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div>
 | 
					{% block line_items %}
 | 
				
			||||||
        <button id="accounting-currency-{{ currency_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_currency_form %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}">
 | 
					  {% with currency_index = currency_index,
 | 
				
			||||||
          <i class="fas fa-minus"></i>
 | 
					          debit_credit = "debit",
 | 
				
			||||||
        </button>
 | 
					          line_item_forms = debit_forms,
 | 
				
			||||||
      </div>
 | 
					          header = A_("Content"),
 | 
				
			||||||
    </div>
 | 
					          debit_credit_total = debit_total,
 | 
				
			||||||
 | 
					          debit_credit_errors = debit_errors %}
 | 
				
			||||||
    <div class="mb-3">
 | 
					    {% include "accounting/journal-entry/include/form-debit-credit.html" %}
 | 
				
			||||||
      <div id="accounting-currency-{{ currency_index }}-debit" class="form-control accounting-material-text-field accounting-not-empty {% if debit_errors %} is-invalid {% endif %}">
 | 
					  {% endwith %}
 | 
				
			||||||
        <label class="form-label" for="accounting-currency-{{ currency_index }}-debit">{{ A_("Content") }}</label>
 | 
					{% endblock %}
 | 
				
			||||||
        <ul id="accounting-currency-{{ currency_index }}-debit-list" class="list-group accounting-line-item-list">
 | 
					 | 
				
			||||||
          {% for line_item_form in debit_forms %}
 | 
					 | 
				
			||||||
            {% with currency_index = currency_index,
 | 
					 | 
				
			||||||
                    debit_credit = "debit",
 | 
					 | 
				
			||||||
                    line_item_index = loop.index,
 | 
					 | 
				
			||||||
                    only_one_line_item_form = debit_forms|length == 1,
 | 
					 | 
				
			||||||
                    form = line_item_form.form %}
 | 
					 | 
				
			||||||
              {% include "accounting/journal-entry/include/form-line-item.html" %}
 | 
					 | 
				
			||||||
            {% endwith %}
 | 
					 | 
				
			||||||
          {% endfor %}
 | 
					 | 
				
			||||||
        </ul>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div class="d-flex justify-content-between mb-2">
 | 
					 | 
				
			||||||
          <div>{{ A_("Total") }}</div>
 | 
					 | 
				
			||||||
          <div><span id="accounting-currency-{{ currency_index }}-debit-total" class="badge rounded-pill bg-primary">{{ debit_total }}</span></div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div>
 | 
					 | 
				
			||||||
          <button id="accounting-currency-{{ currency_index }}-debit-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="debit" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
 | 
					 | 
				
			||||||
            <i class="fas fa-plus"></i>
 | 
					 | 
				
			||||||
            {{ A_("New") }}
 | 
					 | 
				
			||||||
          </button>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div id="accounting-currency-{{ currency_index }}-debit-error" class="invalid-feedback">{% if debit_errors %}{{ debit_errors[0] }}{% endif %}</div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
  <div id="accounting-currency-{{ currency_index }}-error" class="invalid-feedback">{% if currency_errors %}{{ currency_errors[0] }}{% endif %}</div>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,7 @@ First written: 2023/2/25
 | 
				
			|||||||
              currency_code_is_locked = currency_form.is_code_locked,
 | 
					              currency_code_is_locked = currency_form.is_code_locked,
 | 
				
			||||||
              debit_forms = currency_form.debit,
 | 
					              debit_forms = currency_form.debit,
 | 
				
			||||||
              debit_errors = currency_form.debit_errors,
 | 
					              debit_errors = currency_form.debit_errors,
 | 
				
			||||||
              debit_total = currency_form.form.debit_total|accounting_format_amount %}
 | 
					              debit_total = currency_form.debit_total|accounting_format_amount %}
 | 
				
			||||||
        {% include "accounting/journal-entry/disbursement/include/form-currency.html" %}
 | 
					        {% include "accounting/journal-entry/disbursement/include/form-currency.html" %}
 | 
				
			||||||
      {% endwith %}
 | 
					      {% endwith %}
 | 
				
			||||||
    {% endfor %}
 | 
					    {% endfor %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,8 +55,8 @@ First written: 2023/2/28
 | 
				
			|||||||
              </span>
 | 
					              </span>
 | 
				
			||||||
            </li>
 | 
					            </li>
 | 
				
			||||||
            <li class="nav-item">
 | 
					            <li class="nav-item">
 | 
				
			||||||
              <span id="accounting-description-editor-{{ description_editor.debit_credit }}-regular-tab" class="nav-link accounting-clickable" aria-current="false">
 | 
					              <span id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab" class="nav-link accounting-clickable" aria-current="false">
 | 
				
			||||||
                {{ A_("Regular") }}
 | 
					                {{ A_("Recurring") }}
 | 
				
			||||||
              </span>
 | 
					              </span>
 | 
				
			||||||
            </li>
 | 
					            </li>
 | 
				
			||||||
            <li class="nav-item">
 | 
					            <li class="nav-item">
 | 
				
			||||||
@@ -74,7 +74,7 @@ First written: 2023/2/28
 | 
				
			|||||||
              <div id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag-error" class="invalid-feedback"></div>
 | 
					              <div id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag-error" class="invalid-feedback"></div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div>
 | 
					            <div class="accounting-description-editor-buttons">
 | 
				
			||||||
              {% for tag in description_editor.general.tags %}
 | 
					              {% for tag in description_editor.general.tags %}
 | 
				
			||||||
                <button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-general-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
 | 
					                <button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-general-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
 | 
				
			||||||
                  {{ tag }}
 | 
					                  {{ tag }}
 | 
				
			||||||
@@ -91,7 +91,7 @@ First written: 2023/2/28
 | 
				
			|||||||
              <div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag-error" class="invalid-feedback"></div>
 | 
					              <div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag-error" class="invalid-feedback"></div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div>
 | 
					            <div class="accounting-description-editor-buttons">
 | 
				
			||||||
              {% for tag in description_editor.travel.tags %}
 | 
					              {% for tag in description_editor.travel.tags %}
 | 
				
			||||||
                <button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-travel-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
 | 
					                <button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-travel-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
 | 
				
			||||||
                  {{ tag }}
 | 
					                  {{ tag }}
 | 
				
			||||||
@@ -99,7 +99,7 @@ First written: 2023/2/28
 | 
				
			|||||||
              {% endfor %}
 | 
					              {% endfor %}
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div class="d-flex justify-content-between mt-2">
 | 
					            <div class="d-flex justify-content-between">
 | 
				
			||||||
              <div class="form-floating">
 | 
					              <div class="form-floating">
 | 
				
			||||||
                <input id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from" class="form-control" type="text" value="" placeholder=" ">
 | 
					                <input id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from" class="form-control" type="text" value="" placeholder=" ">
 | 
				
			||||||
                <label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from">{{ A_("From") }}</label>
 | 
					                <label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from">{{ A_("From") }}</label>
 | 
				
			||||||
@@ -132,7 +132,7 @@ First written: 2023/2/28
 | 
				
			|||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div>
 | 
					            <div class="accounting-description-editor-buttons">
 | 
				
			||||||
              {% for tag in description_editor.bus.tags %}
 | 
					              {% for tag in description_editor.bus.tags %}
 | 
				
			||||||
                <button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-bus-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
 | 
					                <button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-bus-btn-tag" type="button" tabindex="-1" data-value="{{ tag.name }}" data-accounts="{{ tag.account_codes|tojson|forceescape }}">
 | 
				
			||||||
                  {{ tag }}
 | 
					                  {{ tag }}
 | 
				
			||||||
@@ -140,7 +140,7 @@ First written: 2023/2/28
 | 
				
			|||||||
              {% endfor %}
 | 
					              {% endfor %}
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div class="d-flex justify-content-between mt-2">
 | 
					            <div class="d-flex justify-content-between">
 | 
				
			||||||
              <div class="form-floating me-2">
 | 
					              <div class="form-floating me-2">
 | 
				
			||||||
                <input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from" class="form-control" type="text" value="" placeholder=" ">
 | 
					                <input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from" class="form-control" type="text" value="" placeholder=" ">
 | 
				
			||||||
                <label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from">{{ A_("From") }}</label>
 | 
					                <label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from">{{ A_("From") }}</label>
 | 
				
			||||||
@@ -154,9 +154,15 @@ First written: 2023/2/28
 | 
				
			|||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          {# A regular income or payment #}
 | 
					          {# A recurring transaction #}
 | 
				
			||||||
          <div id="accounting-description-editor-{{ description_editor.debit_credit }}-regular-page" class="d-none" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-regular-tab">
 | 
					          <div id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-page" class="d-none" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab">
 | 
				
			||||||
            {# TODO: To be done #}
 | 
					            <div class="accounting-description-editor-buttons">
 | 
				
			||||||
 | 
					              {% for recurring in description_editor.recurring %}
 | 
				
			||||||
 | 
					                <button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-recurring-item" type="button" tabindex="-1" data-template="{{ recurring.template }}" data-accounts="{{ recurring.account_codes|tojson|forceescape }}">
 | 
				
			||||||
 | 
					                  {{ recurring.name }}
 | 
				
			||||||
 | 
					                </button>
 | 
				
			||||||
 | 
					              {% endfor %}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          {# The annotation #}
 | 
					          {# The annotation #}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,32 +25,32 @@ First written: 2023/2/26
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group mb-3">
 | 
					<div class="mb-3 accounting-toolbar">
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.report.default")|accounting_or_next }}">
 | 
					  <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.report.default")|accounting_or_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
					    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
				
			||||||
    {{ A_("Back") }}
 | 
					    {{ A_("Back") }}
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
  {% if accounting_can_edit() %}
 | 
					  {% if accounting_can_edit() %}
 | 
				
			||||||
    <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_inherit_next }}">
 | 
					    <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_inherit_next }}">
 | 
				
			||||||
      <i class="fa-solid fa-gear"></i>
 | 
					      <i class="fa-solid fa-pen-to-square"></i>
 | 
				
			||||||
      {{ A_("Settings") }}
 | 
					      {{ A_("Edit") }}
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.order", journal_entry_date=obj.date)|accounting_append_next }}">
 | 
					  <a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.order", journal_entry_date=obj.date)|accounting_append_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-bars-staggered"></i>
 | 
					    <i class="fa-solid fa-bars-staggered"></i>
 | 
				
			||||||
    {{ A_("Order") }}
 | 
					    <span class="d-none d-md-inline">{{ A_("Order") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
  {% if accounting_can_edit() %}
 | 
					  {% if accounting_can_edit() %}
 | 
				
			||||||
    {% block to_transfer %}{% endblock %}
 | 
					    {% block as_trasfer %}{% endblock %}
 | 
				
			||||||
    {% if obj.can_delete %}
 | 
					    {% if obj.can_delete %}
 | 
				
			||||||
      <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
 | 
					      <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
 | 
				
			||||||
        <i class="fa-solid fa-trash"></i>
 | 
					        <i class="fa-solid fa-trash"></i>
 | 
				
			||||||
        {{ A_("Delete") }}
 | 
					        <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    {% else %}
 | 
					    {% else %}
 | 
				
			||||||
      <button class="btn btn-secondary" type="button" disabled="disabled">
 | 
					      <button class="btn btn-secondary" type="button" disabled="disabled">
 | 
				
			||||||
        <i class="fa-solid fa-trash"></i>
 | 
					        <i class="fa-solid fa-trash"></i>
 | 
				
			||||||
        {{ A_("Delete") }}
 | 
					        <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					{#
 | 
				
			||||||
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
 | 
					form-currency.html: The currency sub-form in the journal entry form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					 you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					 You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					 distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					 See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					 limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
 | 
					First written: 2023/3/21
 | 
				
			||||||
 | 
					#}
 | 
				
			||||||
 | 
					<div id="accounting-currency-{{ currency_index }}" class="mb-3 accounting-currency" data-index="{{ currency_index }}">
 | 
				
			||||||
 | 
					  <input id="accounting-currency-{{ currency_index }}-no" type="hidden" name="currency-{{ currency_index }}-no" value="{{ currency_index }}">
 | 
				
			||||||
 | 
					  <input id="accounting-currency-{{ currency_index }}-code" type="hidden" name="currency-{{ currency_index }}-code" value="{{ currency_code_data }}">
 | 
				
			||||||
 | 
					  <div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}">
 | 
				
			||||||
 | 
					    <div class="d-flex justify-content-between mt-2 mb-3">
 | 
				
			||||||
 | 
					      <div class="form-floating accounting-currency-content">
 | 
				
			||||||
 | 
					        <select id="accounting-currency-{{ currency_index }}-code-select" class="form-select {% if currency_code_errors %} is-invalid {% endif %}" {% if currency_code_is_locked %} disabled="disabled" {% endif %}>
 | 
				
			||||||
 | 
					          {% for currency in accounting_currency_options() %}
 | 
				
			||||||
 | 
					            <option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
 | 
				
			||||||
 | 
					          {% endfor %}
 | 
				
			||||||
 | 
					        </select>
 | 
				
			||||||
 | 
					        <label class="form-label" for="accounting-currency-{{ currency_index }}-code-select">{{ A_("Currency") }}</label>
 | 
				
			||||||
 | 
					        <div id="accounting-currency-{{ currency_index }}-code-error" class="invalid-feedback">{% if currency_code_errors %}{{ currency_code_errors[0] }}{% endif %}</div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div>
 | 
				
			||||||
 | 
					        <button id="accounting-currency-{{ currency_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_currency_form %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}">
 | 
				
			||||||
 | 
					          <i class="fas fa-minus"></i>
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {% block line_items %}{% endblock %}
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <div id="accounting-currency-{{ currency_index }}-error" class="invalid-feedback">{% if currency_errors %}{{ currency_errors[0] }}{% endif %}</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					{#
 | 
				
			||||||
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
 | 
					form-debit-credit.html: The debit or credit line items in the journal entry form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					 you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					 You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					 distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					 See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					 limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
 | 
					First written: 2023/3/21
 | 
				
			||||||
 | 
					#}
 | 
				
			||||||
 | 
					<div class="mb-2">
 | 
				
			||||||
 | 
					  <div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}" class="form-control accounting-material-text-field accounting-not-empty {% if debit_errors %} is-invalid {% endif %}">
 | 
				
			||||||
 | 
					    <label class="form-label" for="accounting-currency-{{ currency_index }}-{{ debit_credit }}">{{ header }}</label>
 | 
				
			||||||
 | 
					    <ul id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-list" class="list-group accounting-line-item-list">
 | 
				
			||||||
 | 
					      {% for line_item_form in line_item_forms %}
 | 
				
			||||||
 | 
					        {% with currency_index = currency_index,
 | 
				
			||||||
 | 
					                line_item_index = loop.index,
 | 
				
			||||||
 | 
					                only_one_line_item_form = line_item_forms|length == 1,
 | 
				
			||||||
 | 
					                form = line_item_form.form %}
 | 
				
			||||||
 | 
					          {% include "accounting/journal-entry/include/form-line-item.html" %}
 | 
				
			||||||
 | 
					        {% endwith %}
 | 
				
			||||||
 | 
					      {% endfor %}
 | 
				
			||||||
 | 
					    </ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="d-flex justify-content-between mb-2">
 | 
				
			||||||
 | 
					      <div>{{ A_("Total") }}</div>
 | 
				
			||||||
 | 
					      <div><span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-total" class="badge rounded-pill bg-primary">{{ debit_credit_total }}</span></div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					      <button id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="{{ debit_credit }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
 | 
				
			||||||
 | 
					        <i class="fas fa-plus"></i>
 | 
				
			||||||
 | 
					        {{ A_("New") }}
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-error" class="invalid-feedback">{% if debit_credit_errors %}{{ debit_credit_errors[0] }}{% endif %}</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -32,10 +32,10 @@ First written: 2023/2/26
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group mb-3">
 | 
					<div class="mb-3 d-none d-md-block">
 | 
				
			||||||
  <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
 | 
					  <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
 | 
				
			||||||
    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
					    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
				
			||||||
    {{ A_("Back") }}
 | 
					    <span class="d-none d-md-inline">{{ A_("Back") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					{#
 | 
				
			||||||
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
 | 
					order-journal-entry.html: The journal entry in the journal entry order page
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					 you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					 You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					 distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					 See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					 limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
 | 
					First written: 2023/3/21
 | 
				
			||||||
 | 
					#}
 | 
				
			||||||
 | 
					<a class="small w-100 accounting-journal-entry-order-item" href="{{ url_for("accounting.journal-entry.detail", journal_entry=journal_entry)|accounting_append_next }}">
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    {{ journal_entry.date|accounting_format_date }}
 | 
				
			||||||
 | 
					    {% if journal_entry.is_cash_disbursement %}
 | 
				
			||||||
 | 
					      {{ A_("Cash Disbursement") }}
 | 
				
			||||||
 | 
					    {% elif journal_entry.is_cash_receipt %}
 | 
				
			||||||
 | 
					      {{ A_("Cash Receipt") }}
 | 
				
			||||||
 | 
					    {% else %}
 | 
				
			||||||
 | 
					      {{ A_("Transfer") }}
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  {% for currency in journal_entry.currencies %}
 | 
				
			||||||
 | 
					    <div class="d-flex justify-content-between accounting-journal-entry-order-item-currency">
 | 
				
			||||||
 | 
					      <div>
 | 
				
			||||||
 | 
					        {% if not journal_entry.is_cash_receipt %}
 | 
				
			||||||
 | 
					          {% for line_item in currency.debit %}
 | 
				
			||||||
 | 
					            <div>{{ line_item.description|accounting_default }}</div>
 | 
				
			||||||
 | 
					          {% endfor %}
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					        {% if not journal_entry.is_cash_disbursement %}
 | 
				
			||||||
 | 
					          {% for line_item in currency.credit %}
 | 
				
			||||||
 | 
					            <div class="accounting-mobile-journal-credit">{{ line_item.description|accounting_default }}</div>
 | 
				
			||||||
 | 
					          {% endfor %}
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div>
 | 
				
			||||||
 | 
					        <span class="badge bg-info rounded-pill">
 | 
				
			||||||
 | 
					          {% if currency.code != accounting_default_currency_code() %}
 | 
				
			||||||
 | 
					            {{ currency.code }}
 | 
				
			||||||
 | 
					          {% endif %}
 | 
				
			||||||
 | 
					          {{ currency.debit_total|accounting_format_amount }}
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  {% endfor %}
 | 
				
			||||||
 | 
					</a>
 | 
				
			||||||
@@ -30,7 +30,7 @@ First written: 2023/2/26
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group mb-3">
 | 
					<div class="mb-3 d-none d-md-block">
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.report.default")|accounting_or_next }}">
 | 
					  <a class="btn btn-primary" href="{{ url_for("accounting.report.default")|accounting_or_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
					    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
				
			||||||
    {{ A_("Back") }}
 | 
					    {{ A_("Back") }}
 | 
				
			||||||
@@ -47,9 +47,9 @@ First written: 2023/2/26
 | 
				
			|||||||
      {% for item in list %}
 | 
					      {% for item in list %}
 | 
				
			||||||
        <li class="list-group-item d-flex justify-content-between" data-id="{{ item.id }}">
 | 
					        <li class="list-group-item d-flex justify-content-between" data-id="{{ item.id }}">
 | 
				
			||||||
          <input id="accounting-order-{{ item.id }}-no" type="hidden" name="{{ item.id }}-no" value="{{ loop.index }}">
 | 
					          <input id="accounting-order-{{ item.id }}-no" type="hidden" name="{{ item.id }}-no" value="{{ loop.index }}">
 | 
				
			||||||
          <div>
 | 
					          {% with journal_entry = item %}
 | 
				
			||||||
            {{ item }}
 | 
					            {% include "accounting/journal-entry/include/order-journal-entry.html" %}
 | 
				
			||||||
          </div>
 | 
					          {% endwith %}
 | 
				
			||||||
          <i class="fa-solid fa-bars"></i>
 | 
					          <i class="fa-solid fa-bars"></i>
 | 
				
			||||||
        </li>
 | 
					        </li>
 | 
				
			||||||
      {% endfor %}
 | 
					      {% endfor %}
 | 
				
			||||||
@@ -72,7 +72,9 @@ First written: 2023/2/26
 | 
				
			|||||||
  <ul class="list-group mb-3">
 | 
					  <ul class="list-group mb-3">
 | 
				
			||||||
    {% for item in list %}
 | 
					    {% for item in list %}
 | 
				
			||||||
      <li class="list-group-item">
 | 
					      <li class="list-group-item">
 | 
				
			||||||
        {{ item }}
 | 
					        {% with journal_entry = item %}
 | 
				
			||||||
 | 
					          {% include "accounting/journal-entry/include/order-journal-entry.html" %}
 | 
				
			||||||
 | 
					        {% endwith %}
 | 
				
			||||||
      </li>
 | 
					      </li>
 | 
				
			||||||
    {% endfor %}
 | 
					    {% endfor %}
 | 
				
			||||||
  </ul>
 | 
					  </ul>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,10 +21,10 @@ First written: 2023/2/26
 | 
				
			|||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/journal-entry/include/detail.html" %}
 | 
					{% extends "accounting/journal-entry/include/detail.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block to_transfer %}
 | 
					{% block as_trasfer %}
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_journal_entry_to_transfer|accounting_inherit_next }}">
 | 
					  <a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_journal_entry_to_transfer|accounting_inherit_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-bars-staggered"></i>
 | 
					    <i class="fa-solid fa-bars-staggered"></i>
 | 
				
			||||||
    {{ A_("To Transfer") }}
 | 
					    <span class="d-none d-md-inline">{{ A_("As Transfer") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,57 +19,15 @@ form-currency.html: The currency sub-form in the cash receipt journal entry form
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/25
 | 
					First written: 2023/2/25
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
<div id="accounting-currency-{{ currency_index }}" class="mb-3 accounting-currency" data-index="{{ currency_index }}">
 | 
					{% extends "accounting/journal-entry/include/form-currency.html" %}
 | 
				
			||||||
  <input id="accounting-currency-{{ currency_index }}-no" type="hidden" name="currency-{{ currency_index }}-no" value="{{ currency_index }}">
 | 
					 | 
				
			||||||
  <input id="accounting-currency-{{ currency_index }}-code" type="hidden" name="currency-{{ currency_index }}-code" value="{{ currency_code_data }}">
 | 
					 | 
				
			||||||
  <div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}">
 | 
					 | 
				
			||||||
    <div class="d-flex justify-content-between mt-2 mb-3">
 | 
					 | 
				
			||||||
      <div class="form-floating accounting-currency-content">
 | 
					 | 
				
			||||||
        <select id="accounting-currency-{{ currency_index }}-code-select" class="form-select {% if currency_code_errors %} is-invalid {% endif %}" name="currency-{{ currency_index }}-code" {% if currency_code_is_locked %} disabled="disabled" {% endif %}>
 | 
					 | 
				
			||||||
          {% for currency in accounting_currency_options() %}
 | 
					 | 
				
			||||||
            <option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
 | 
					 | 
				
			||||||
          {% endfor %}
 | 
					 | 
				
			||||||
        </select>
 | 
					 | 
				
			||||||
        <label class="form-label" for="accounting-currency-{{ currency_index }}-code-select">{{ A_("Currency") }}</label>
 | 
					 | 
				
			||||||
        <div id="accounting-currency-{{ currency_index }}-code-error" class="invalid-feedback">{% if currency_code_errors %}{{ currency_code_errors[0] }}{% endif %}</div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div>
 | 
					{% block line_items %}
 | 
				
			||||||
        <button id="accounting-currency-{{ currency_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_currency_form %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}">
 | 
					  {% with currency_index = currency_index,
 | 
				
			||||||
          <i class="fas fa-minus"></i>
 | 
					          debit_credit = "credit",
 | 
				
			||||||
        </button>
 | 
					          line_item_forms = credit_forms,
 | 
				
			||||||
      </div>
 | 
					          header = A_("Content"),
 | 
				
			||||||
    </div>
 | 
					          debit_credit_total = credit_total,
 | 
				
			||||||
 | 
					          debit_credit_errors = credit_errors %}
 | 
				
			||||||
    <div class="mb-3">
 | 
					    {% include "accounting/journal-entry/include/form-debit-credit.html" %}
 | 
				
			||||||
      <div id="accounting-currency-{{ currency_index }}-credit" class="form-control accounting-material-text-field accounting-not-empty {% if credit_errors %} is-invalid {% endif %}">
 | 
					  {% endwith %}
 | 
				
			||||||
        <label class="form-label" for="accounting-currency-{{ currency_index }}-credit">{{ A_("Content") }}</label>
 | 
					{% endblock %}
 | 
				
			||||||
        <ul id="accounting-currency-{{ currency_index }}-credit-list" class="list-group accounting-line-item-list">
 | 
					 | 
				
			||||||
          {% for line_item_form in credit_forms %}
 | 
					 | 
				
			||||||
            {% with currency_index = currency_index,
 | 
					 | 
				
			||||||
                    debit_credit = "credit",
 | 
					 | 
				
			||||||
                    line_item_index = loop.index,
 | 
					 | 
				
			||||||
                    only_one_line_item_form = credit_forms|length == 1,
 | 
					 | 
				
			||||||
                    form = line_item_form.form %}
 | 
					 | 
				
			||||||
              {% include "accounting/journal-entry/include/form-line-item.html" %}
 | 
					 | 
				
			||||||
            {% endwith %}
 | 
					 | 
				
			||||||
          {% endfor %}
 | 
					 | 
				
			||||||
        </ul>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div class="d-flex justify-content-between mb-2">
 | 
					 | 
				
			||||||
          <div>{{ A_("Total") }}</div>
 | 
					 | 
				
			||||||
          <div><span id="accounting-currency-{{ currency_index }}-credit-total" class="badge rounded-pill bg-primary">{{ credit_total }}</span></div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div>
 | 
					 | 
				
			||||||
          <button id="accounting-currency-{{ currency_index }}-credit-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="credit" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
 | 
					 | 
				
			||||||
            <i class="fas fa-plus"></i>
 | 
					 | 
				
			||||||
            {{ A_("New") }}
 | 
					 | 
				
			||||||
          </button>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div id="accounting-currency-{{ currency_index }}-credit-error" class="invalid-feedback">{% if credit_errors %}{{ credit_errors[0] }}{% endif %}</div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
  <div id="accounting-currency-{{ currency_index }}-error" class="invalid-feedback">{% if currency_errors %}{{ currency_errors[0] }}{% endif %}</div>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,7 @@ First written: 2023/2/25
 | 
				
			|||||||
              currency_code_is_locked = currency_form.is_code_locked,
 | 
					              currency_code_is_locked = currency_form.is_code_locked,
 | 
				
			||||||
              credit_forms = currency_form.credit,
 | 
					              credit_forms = currency_form.credit,
 | 
				
			||||||
              credit_errors = currency_form.credit_errors,
 | 
					              credit_errors = currency_form.credit_errors,
 | 
				
			||||||
              credit_total = currency_form.form.credit_total|accounting_format_amount %}
 | 
					              credit_total = currency_form.credit_total|accounting_format_amount %}
 | 
				
			||||||
        {% include "accounting/journal-entry/receipt/include/form-currency.html" %}
 | 
					        {% include "accounting/journal-entry/receipt/include/form-currency.html" %}
 | 
				
			||||||
      {% endwith %}
 | 
					      {% endwith %}
 | 
				
			||||||
    {% endfor %}
 | 
					    {% endfor %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,91 +19,30 @@ form-currency.html: The currency sub-form in the transfer journal entry form
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/25
 | 
					First written: 2023/2/25
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
<div id="accounting-currency-{{ currency_index }}" class="mb-3 accounting-currency" data-index="{{ currency_index }}">
 | 
					{% extends "accounting/journal-entry/include/form-currency.html" %}
 | 
				
			||||||
  <input id="accounting-currency-{{ currency_index }}-no" type="hidden" name="currency-{{ currency_index }}-no" value="{{ currency_index }}">
 | 
					 | 
				
			||||||
  <input id="accounting-currency-{{ currency_index }}-code" type="hidden" name="currency-{{ currency_index }}-code" value="{{ currency_code_data }}">
 | 
					 | 
				
			||||||
  <div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}">
 | 
					 | 
				
			||||||
    <div class="d-flex justify-content-between mt-2 mb-3">
 | 
					 | 
				
			||||||
      <div class="form-floating accounting-currency-content">
 | 
					 | 
				
			||||||
        <select id="accounting-currency-{{ currency_index }}-code-select" class="form-select {% if currency_code_errors %} is-invalid {% endif %}" {% if currency_code_is_locked %} disabled="disabled" {% endif %}>
 | 
					 | 
				
			||||||
          {% for currency in accounting_currency_options() %}
 | 
					 | 
				
			||||||
            <option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
 | 
					 | 
				
			||||||
          {% endfor %}
 | 
					 | 
				
			||||||
        </select>
 | 
					 | 
				
			||||||
        <label class="form-label" for="accounting-currency-{{ currency_index }}-code-select">{{ A_("Currency") }}</label>
 | 
					 | 
				
			||||||
        <div id="accounting-currency-{{ currency_index }}-code-error" class="invalid-feedback">{% if currency_code_errors %}{{ currency_code_errors[0] }}{% endif %}</div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div>
 | 
					{% block line_items %}
 | 
				
			||||||
        <button id="accounting-currency-{{ currency_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_currency_form %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}">
 | 
					<div class="row">
 | 
				
			||||||
          <i class="fas fa-minus"></i>
 | 
					  <div class="col-sm-6">
 | 
				
			||||||
        </button>
 | 
					    {% with currency_index = currency_index,
 | 
				
			||||||
      </div>
 | 
					            debit_credit = "debit",
 | 
				
			||||||
    </div>
 | 
					            line_item_forms = debit_forms,
 | 
				
			||||||
 | 
					            header = A_("Debit"),
 | 
				
			||||||
    <div class="row">
 | 
					            debit_credit_total = debit_total,
 | 
				
			||||||
      {# The debit line items #}
 | 
					            debit_credit_errors = debit_errors %}
 | 
				
			||||||
      <div class="col-sm-6 mb-3">
 | 
					      {% include "accounting/journal-entry/include/form-debit-credit.html" %}
 | 
				
			||||||
        <div id="accounting-currency-{{ currency_index }}-debit" class="form-control accounting-material-text-field accounting-not-empty {% if debit_errors %} is-invalid {% endif %}">
 | 
					    {% endwith %}
 | 
				
			||||||
          <label class="form-label" for="accounting-currency-{{ currency_index }}-debit">{{ A_("Debit") }}</label>
 | 
					  </div>
 | 
				
			||||||
          <ul id="accounting-currency-{{ currency_index }}-debit-list" class="list-group accounting-line-item-list">
 | 
					
 | 
				
			||||||
            {% for line_item_form in debit_forms %}
 | 
					  <div class="col-sm-6">
 | 
				
			||||||
              {% with currency_index = currency_index,
 | 
					    {% with currency_index = currency_index,
 | 
				
			||||||
                      debit_credit = "debit",
 | 
					            debit_credit = "credit",
 | 
				
			||||||
                      line_item_index = loop.index,
 | 
					            line_item_forms = credit_forms,
 | 
				
			||||||
                      only_one_line_item_form = debit_forms|length == 1,
 | 
					            header = A_("Credit"),
 | 
				
			||||||
                      form = line_item_form.form %}
 | 
					            debit_credit_total = credit_total,
 | 
				
			||||||
                {% include "accounting/journal-entry/include/form-line-item.html" %}
 | 
					            debit_credit_errors = credit_errors %}
 | 
				
			||||||
              {% endwith %}
 | 
					      {% include "accounting/journal-entry/include/form-debit-credit.html" %}
 | 
				
			||||||
            {% endfor %}
 | 
					    {% endwith %}
 | 
				
			||||||
          </ul>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div class="d-flex justify-content-between mb-2">
 | 
					 | 
				
			||||||
            <div>{{ A_("Total") }}</div>
 | 
					 | 
				
			||||||
            <div><span id="accounting-currency-{{ currency_index }}-debit-total" class="badge rounded-pill bg-primary">{{ debit_total }}</span></div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div>
 | 
					 | 
				
			||||||
            <button id="accounting-currency-{{ currency_index }}-debit-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="debit" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
 | 
					 | 
				
			||||||
              <i class="fas fa-plus"></i>
 | 
					 | 
				
			||||||
              {{ A_("New") }}
 | 
					 | 
				
			||||||
            </button>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div id="accounting-currency-{{ currency_index }}-debit-error" class="invalid-feedback">{% if debit_errors %}{{ debit_errors[0] }}{% endif %}</div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      {# The credit line items #}
 | 
					 | 
				
			||||||
      <div class="col-sm-6 mb-3">
 | 
					 | 
				
			||||||
        <div id="accounting-currency-{{ currency_index }}-credit" class="form-control accounting-material-text-field accounting-not-empty {% if credit_errors %} is-invalid {% endif %}">
 | 
					 | 
				
			||||||
          <label class="form-label" for="accounting-currency-{{ currency_index }}-credit">{{ A_("Credit") }}</label>
 | 
					 | 
				
			||||||
          <ul id="accounting-currency-{{ currency_index }}-credit-list" class="list-group accounting-line-item-list">
 | 
					 | 
				
			||||||
            {% for line_item_form in credit_forms %}
 | 
					 | 
				
			||||||
              {% with currency_index = currency_index,
 | 
					 | 
				
			||||||
                      debit_credit = "credit",
 | 
					 | 
				
			||||||
                      line_item_index = loop.index,
 | 
					 | 
				
			||||||
                      only_one_line_item_form = credit_forms|length == 1,
 | 
					 | 
				
			||||||
                      form = line_item_form.form %}
 | 
					 | 
				
			||||||
                {% include "accounting/journal-entry/include/form-line-item.html" %}
 | 
					 | 
				
			||||||
              {% endwith %}
 | 
					 | 
				
			||||||
            {% endfor %}
 | 
					 | 
				
			||||||
          </ul>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div class="d-flex justify-content-between mb-2">
 | 
					 | 
				
			||||||
            <div>{{ A_("Total") }}</div>
 | 
					 | 
				
			||||||
            <div><span id="accounting-currency-{{ currency_index }}-credit-total" class="badge rounded-pill bg-primary">{{ credit_total }}</span></div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div>
 | 
					 | 
				
			||||||
            <button id="accounting-currency-{{ currency_index }}-credit-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="credit" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
 | 
					 | 
				
			||||||
              <i class="fas fa-plus"></i>
 | 
					 | 
				
			||||||
              {{ A_("New") }}
 | 
					 | 
				
			||||||
            </button>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div id="accounting-currency-{{ currency_index }}-credit-error" class="invalid-feedback">{% if credit_errors %}{{ credit_errors[0] }}{% endif %}</div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <div id="accounting-currency-{{ currency_index }}-error" class="invalid-feedback">{% if currency_errors %}{{ currency_errors[0] }}{% endif %}</div>
 | 
					 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,10 +32,10 @@ First written: 2023/2/25
 | 
				
			|||||||
              currency_code_is_locked = currency_form.is_code_locked,
 | 
					              currency_code_is_locked = currency_form.is_code_locked,
 | 
				
			||||||
              debit_forms = currency_form.debit,
 | 
					              debit_forms = currency_form.debit,
 | 
				
			||||||
              debit_errors = currency_form.debit_errors,
 | 
					              debit_errors = currency_form.debit_errors,
 | 
				
			||||||
              debit_total = currency_form.form.debit_total|accounting_format_amount,
 | 
					              debit_total = currency_form.debit_total|accounting_format_amount,
 | 
				
			||||||
              credit_forms = currency_form.credit,
 | 
					              credit_forms = currency_form.credit,
 | 
				
			||||||
              credit_errors = currency_form.credit_errors,
 | 
					              credit_errors = currency_form.credit_errors,
 | 
				
			||||||
              credit_total = currency_form.form.credit_total|accounting_format_amount %}
 | 
					              credit_total = currency_form.credit_total|accounting_format_amount %}
 | 
				
			||||||
        {% include "accounting/journal-entry/transfer/include/form-currency.html" %}
 | 
					        {% include "accounting/journal-entry/transfer/include/form-currency.html" %}
 | 
				
			||||||
      {% endwith %}
 | 
					      {% endwith %}
 | 
				
			||||||
    {% endfor %}
 | 
					    {% endfor %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -45,11 +45,11 @@ First written: 2023/3/8
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
<div class="btn-group" role="group">
 | 
					<div class="btn-group" role="group">
 | 
				
			||||||
  <button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
 | 
					  <button id="accounting-choose-report" class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
 | 
				
			||||||
    <i class="fa-solid fa-book"></i>
 | 
					    <i class="fa-solid fa-book"></i>
 | 
				
			||||||
    <span class="d-none d-md-inline">{{ report.report_chooser.current_report }}</span>
 | 
					    <span class="d-none d-md-inline">{{ A_("Report") }}</span>
 | 
				
			||||||
  </button>
 | 
					  </button>
 | 
				
			||||||
  <ul class="dropdown-menu" aria-label="{{ A_("Report") }}">
 | 
					  <ul class="dropdown-menu" aria-labelledby="accounting-choose-report">
 | 
				
			||||||
    {% for report in report.report_chooser %}
 | 
					    {% for report in report.report_chooser %}
 | 
				
			||||||
      <li>
 | 
					      <li>
 | 
				
			||||||
        <a class="dropdown-item {% if report.is_active %} active {% endif %}" href="{{ report.url }}">
 | 
					        <a class="dropdown-item {% if report.is_active %} active {% endif %}" href="{{ report.url }}">
 | 
				
			||||||
@@ -68,11 +68,11 @@ First written: 2023/3/8
 | 
				
			|||||||
</div>
 | 
					</div>
 | 
				
			||||||
{% if use_currency_chooser %}
 | 
					{% if use_currency_chooser %}
 | 
				
			||||||
  <div class="btn-group" role="group">
 | 
					  <div class="btn-group" role="group">
 | 
				
			||||||
    <button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
 | 
					    <button id="accounting-choose-currency" class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
 | 
				
			||||||
      <i class="fa-solid fa-money-bill-wave"></i>
 | 
					      <i class="fa-solid fa-money-bill-wave"></i>
 | 
				
			||||||
      <span class="d-none d-md-inline">{{ report.currency.name|title }}</span>
 | 
					      <span class="d-none d-md-inline">{{ A_("Currency") }}</span>
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
    <ul class="dropdown-menu" aria-label="{{ A_("Currency") }}">
 | 
					    <ul class="dropdown-menu" aria-labelledby="accounting-choose-currency">
 | 
				
			||||||
      {% for currency in report.currency_options %}
 | 
					      {% for currency in report.currency_options %}
 | 
				
			||||||
        <li>
 | 
					        <li>
 | 
				
			||||||
          <a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
 | 
					          <a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
 | 
				
			||||||
@@ -85,11 +85,11 @@ First written: 2023/3/8
 | 
				
			|||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
{% if use_account_chooser %}
 | 
					{% if use_account_chooser %}
 | 
				
			||||||
  <div class="btn-group" role="group">
 | 
					  <div class="btn-group" role="group">
 | 
				
			||||||
    <button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
 | 
					    <button id="accounting-choose-account" class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
 | 
				
			||||||
      <i class="fa-solid fa-clipboard"></i>
 | 
					      <i class="fa-solid fa-clipboard"></i>
 | 
				
			||||||
      <span class="d-none d-md-inline">{{ report.account.title|title }}</span>
 | 
					      <span class="d-none d-md-inline">{{ A_("Account") }}</span>
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
    <ul class="dropdown-menu" aria-label="{{ A_("Account") }}">
 | 
					    <ul class="dropdown-menu" aria-labelledby="accounting-choose-account">
 | 
				
			||||||
      {% for account in report.account_options %}
 | 
					      {% for account in report.account_options %}
 | 
				
			||||||
        <li>
 | 
					        <li>
 | 
				
			||||||
          <a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
 | 
					          <a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
 | 
				
			||||||
@@ -103,7 +103,7 @@ First written: 2023/3/8
 | 
				
			|||||||
{% if use_period_chooser %}
 | 
					{% if use_period_chooser %}
 | 
				
			||||||
  <button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
 | 
					  <button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
 | 
				
			||||||
    <i class="fa-solid fa-calendar-day"></i>
 | 
					    <i class="fa-solid fa-calendar-day"></i>
 | 
				
			||||||
    <span class="d-none d-md-inline">{{ report.period.desc|title }}</span>
 | 
					    <span class="d-none d-md-inline">{{ A_("Period") }}</span>
 | 
				
			||||||
  </button>
 | 
					  </button>
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
{% if report.has_data %}
 | 
					{% if report.has_data %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,8 +8,8 @@ msgid ""
 | 
				
			|||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Project-Id-Version: Mia! Accounting Flask 0.0.0\n"
 | 
					"Project-Id-Version: Mia! Accounting Flask 0.0.0\n"
 | 
				
			||||||
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
 | 
					"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
 | 
				
			||||||
"POT-Creation-Date: 2023-03-20 23:51+0800\n"
 | 
					"POT-Creation-Date: 2023-03-22 01:47+0800\n"
 | 
				
			||||||
"PO-Revision-Date: 2023-03-20 23:51+0800\n"
 | 
					"PO-Revision-Date: 2023-03-22 01:47+0800\n"
 | 
				
			||||||
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
 | 
					"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
 | 
				
			||||||
"Language: zh_Hant\n"
 | 
					"Language: zh_Hant\n"
 | 
				
			||||||
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
 | 
					"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
 | 
				
			||||||
@@ -19,22 +19,22 @@ msgstr ""
 | 
				
			|||||||
"Content-Transfer-Encoding: 8bit\n"
 | 
					"Content-Transfer-Encoding: 8bit\n"
 | 
				
			||||||
"Generated-By: Babel 2.12.1\n"
 | 
					"Generated-By: Babel 2.12.1\n"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/models.py:538
 | 
					#: src/accounting/models.py:559
 | 
				
			||||||
#, python-format
 | 
					#, python-format
 | 
				
			||||||
msgid "Cash Disbursement Journal Entry#%(id)s"
 | 
					msgid "Cash Disbursement Journal Entry#%(id)s"
 | 
				
			||||||
msgstr "現金支出傳票#%(id)s"
 | 
					msgstr "現金支出傳票#%(id)s"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/models.py:541
 | 
					#: src/accounting/models.py:562
 | 
				
			||||||
#, python-format
 | 
					#, python-format
 | 
				
			||||||
msgid "Cash Receipt Journal Entry#%(id)s"
 | 
					msgid "Cash Receipt Journal Entry#%(id)s"
 | 
				
			||||||
msgstr "現金收入傳票#%(id)s"
 | 
					msgstr "現金收入傳票#%(id)s"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/models.py:542
 | 
					#: src/accounting/models.py:563
 | 
				
			||||||
#, python-format
 | 
					#, python-format
 | 
				
			||||||
msgid "Transfer Journal Entry#%(id)s"
 | 
					msgid "Transfer Journal Entry#%(id)s"
 | 
				
			||||||
msgstr "轉帳傳票#%(id)s"
 | 
					msgstr "轉帳傳票#%(id)s"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/models.py:679
 | 
					#: src/accounting/models.py:696
 | 
				
			||||||
#, python-format
 | 
					#, python-format
 | 
				
			||||||
msgid "%(date)s %(description)s %(amount)s"
 | 
					msgid "%(date)s %(description)s %(amount)s"
 | 
				
			||||||
msgstr "%(date)s %(description)s %(amount)s"
 | 
					msgstr "%(date)s %(description)s %(amount)s"
 | 
				
			||||||
@@ -86,7 +86,7 @@ msgstr "請填上標題。"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#: src/accounting/account/queries.py:50
 | 
					#: src/accounting/account/queries.py:50
 | 
				
			||||||
#: src/accounting/report/reports/search.py:101
 | 
					#: src/accounting/report/reports/search.py:101
 | 
				
			||||||
#: src/accounting/templates/accounting/account/detail.html:90
 | 
					#: src/accounting/templates/accounting/account/detail.html:97
 | 
				
			||||||
#: src/accounting/templates/accounting/account/list.html:62
 | 
					#: src/accounting/templates/accounting/account/list.html:62
 | 
				
			||||||
msgid "Needs Offset"
 | 
					msgid "Needs Offset"
 | 
				
			||||||
msgstr "需抵銷"
 | 
					msgstr "需抵銷"
 | 
				
			||||||
@@ -103,17 +103,21 @@ msgstr "科目未異動。"
 | 
				
			|||||||
msgid "The account is updated successfully."
 | 
					msgid "The account is updated successfully."
 | 
				
			||||||
msgstr "科目存好了。"
 | 
					msgstr "科目存好了。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/account/views.py:163
 | 
					#: src/accounting/account/views.py:161
 | 
				
			||||||
 | 
					msgid "The account cannot be deleted."
 | 
				
			||||||
 | 
					msgstr "科目不可刪除。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/account/views.py:166
 | 
				
			||||||
msgid "The account is deleted successfully."
 | 
					msgid "The account is deleted successfully."
 | 
				
			||||||
msgstr "科目刪掉了"
 | 
					msgstr "科目刪掉了"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/account/views.py:190
 | 
					#: src/accounting/account/views.py:193
 | 
				
			||||||
#: src/accounting/journal_entry/views.py:213
 | 
					#: src/accounting/journal_entry/views.py:216
 | 
				
			||||||
msgid "The order was not modified."
 | 
					msgid "The order was not modified."
 | 
				
			||||||
msgstr "順序未異動。"
 | 
					msgstr "順序未異動。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/account/views.py:193
 | 
					#: src/accounting/account/views.py:196
 | 
				
			||||||
#: src/accounting/journal_entry/views.py:216
 | 
					#: src/accounting/journal_entry/views.py:219
 | 
				
			||||||
msgid "The order is updated successfully."
 | 
					msgid "The order is updated successfully."
 | 
				
			||||||
msgstr "順序存好了。"
 | 
					msgstr "順序存好了。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -154,7 +158,11 @@ msgstr "貨幣未異動。"
 | 
				
			|||||||
msgid "The currency is updated successfully."
 | 
					msgid "The currency is updated successfully."
 | 
				
			||||||
msgstr "貨幣存好了。"
 | 
					msgstr "貨幣存好了。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/currency/views.py:165
 | 
					#: src/accounting/currency/views.py:164
 | 
				
			||||||
 | 
					msgid "The currency cannot be deleted."
 | 
				
			||||||
 | 
					msgstr "貨幣不可刪除。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/currency/views.py:168
 | 
				
			||||||
msgid "The currency is deleted successfully."
 | 
					msgid "The currency is deleted successfully."
 | 
				
			||||||
msgstr "貨幣刪掉了"
 | 
					msgstr "貨幣刪掉了"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -170,7 +178,11 @@ msgstr "傳票未異動。"
 | 
				
			|||||||
msgid "The journal entry is updated successfully."
 | 
					msgid "The journal entry is updated successfully."
 | 
				
			||||||
msgstr "傳票存好了。"
 | 
					msgstr "傳票存好了。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/journal_entry/views.py:181
 | 
					#: src/accounting/journal_entry/views.py:179
 | 
				
			||||||
 | 
					msgid "The journal entry cannot be deleted."
 | 
				
			||||||
 | 
					msgstr "傳票不可刪除。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/journal_entry/views.py:184
 | 
				
			||||||
msgid "The journal entry is deleted successfully."
 | 
					msgid "The journal entry is deleted successfully."
 | 
				
			||||||
msgstr "傳票刪掉了"
 | 
					msgstr "傳票刪掉了"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -358,13 +370,10 @@ msgstr "全部"
 | 
				
			|||||||
#: src/accounting/report/reports/ledger.py:380
 | 
					#: src/accounting/report/reports/ledger.py:380
 | 
				
			||||||
#: src/accounting/report/reports/trial_balance.py:228
 | 
					#: src/accounting/report/reports/trial_balance.py:228
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:43
 | 
					#: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:43
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/disbursement/include/form-currency.html:60
 | 
					#: src/accounting/templates/accounting/journal-entry/include/form-debit-credit.html:37
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/receipt/detail.html:43
 | 
					#: src/accounting/templates/accounting/journal-entry/receipt/detail.html:43
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/receipt/include/form-currency.html:60
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:39
 | 
					#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:39
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:55
 | 
					#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:55
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:62
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:93
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:59
 | 
					#: src/accounting/templates/accounting/report/balance-sheet.html:59
 | 
				
			||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:71
 | 
					#: src/accounting/templates/accounting/report/balance-sheet.html:71
 | 
				
			||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:81
 | 
					#: src/accounting/templates/accounting/report/balance-sheet.html:81
 | 
				
			||||||
@@ -399,7 +408,7 @@ msgstr "日期"
 | 
				
			|||||||
#: src/accounting/report/reports/journal.py:156
 | 
					#: src/accounting/report/reports/journal.py:156
 | 
				
			||||||
#: src/accounting/report/reports/trial_balance.py:224
 | 
					#: src/accounting/report/reports/trial_balance.py:224
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:57
 | 
					#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:57
 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:92
 | 
					#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:90
 | 
				
			||||||
#: src/accounting/templates/accounting/report/income-expenses.html:56
 | 
					#: src/accounting/templates/accounting/report/income-expenses.html:56
 | 
				
			||||||
#: src/accounting/templates/accounting/report/journal.html:55
 | 
					#: src/accounting/templates/accounting/report/journal.html:55
 | 
				
			||||||
#: src/accounting/templates/accounting/report/search.html:52
 | 
					#: src/accounting/templates/accounting/report/search.html:52
 | 
				
			||||||
@@ -439,7 +448,7 @@ msgstr "餘額"
 | 
				
			|||||||
#: src/accounting/report/reports/income_expenses.py:416
 | 
					#: src/accounting/report/reports/income_expenses.py:416
 | 
				
			||||||
#: src/accounting/report/reports/journal.py:158
 | 
					#: src/accounting/report/reports/journal.py:158
 | 
				
			||||||
#: src/accounting/report/reports/ledger.py:368
 | 
					#: src/accounting/report/reports/ledger.py:368
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:172
 | 
					#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:178
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/form.html:73
 | 
					#: src/accounting/templates/accounting/journal-entry/include/form.html:73
 | 
				
			||||||
msgid "Note"
 | 
					msgid "Note"
 | 
				
			||||||
msgstr "備註"
 | 
					msgstr "備註"
 | 
				
			||||||
@@ -475,10 +484,8 @@ msgid "Amount"
 | 
				
			|||||||
msgstr "金額"
 | 
					msgstr "金額"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/report/reports/journal.py:155
 | 
					#: src/accounting/report/reports/journal.py:155
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/disbursement/include/form-currency.html:33
 | 
					#: src/accounting/templates/accounting/journal-entry/include/form-currency.html:33
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/receipt/include/form-currency.html:33
 | 
					#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:73
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:33
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:75
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/report/journal.html:54
 | 
					#: src/accounting/templates/accounting/report/journal.html:54
 | 
				
			||||||
#: src/accounting/templates/accounting/report/search.html:51
 | 
					#: src/accounting/templates/accounting/report/search.html:51
 | 
				
			||||||
msgid "Currency"
 | 
					msgid "Currency"
 | 
				
			||||||
@@ -488,7 +495,7 @@ msgstr "貨幣"
 | 
				
			|||||||
#: src/accounting/report/reports/ledger.py:367
 | 
					#: src/accounting/report/reports/ledger.py:367
 | 
				
			||||||
#: src/accounting/report/reports/trial_balance.py:224
 | 
					#: src/accounting/report/reports/trial_balance.py:224
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:33
 | 
					#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:33
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:48
 | 
					#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:30
 | 
				
			||||||
#: src/accounting/templates/accounting/report/journal.html:57
 | 
					#: src/accounting/templates/accounting/report/journal.html:57
 | 
				
			||||||
#: src/accounting/templates/accounting/report/ledger.html:57
 | 
					#: src/accounting/templates/accounting/report/ledger.html:57
 | 
				
			||||||
#: src/accounting/templates/accounting/report/search.html:54
 | 
					#: src/accounting/templates/accounting/report/search.html:54
 | 
				
			||||||
@@ -500,7 +507,7 @@ msgstr "借方"
 | 
				
			|||||||
#: src/accounting/report/reports/ledger.py:367
 | 
					#: src/accounting/report/reports/ledger.py:367
 | 
				
			||||||
#: src/accounting/report/reports/trial_balance.py:225
 | 
					#: src/accounting/report/reports/trial_balance.py:225
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:49
 | 
					#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:49
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:79
 | 
					#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:41
 | 
				
			||||||
#: src/accounting/templates/accounting/report/journal.html:58
 | 
					#: src/accounting/templates/accounting/report/journal.html:58
 | 
				
			||||||
#: src/accounting/templates/accounting/report/ledger.html:58
 | 
					#: src/accounting/templates/accounting/report/ledger.html:58
 | 
				
			||||||
#: src/accounting/templates/accounting/report/search.html:55
 | 
					#: src/accounting/templates/accounting/report/search.html:55
 | 
				
			||||||
@@ -555,25 +562,73 @@ msgstr "資產負債表"
 | 
				
			|||||||
msgid "Please fill in the title."
 | 
					msgid "Please fill in the title."
 | 
				
			||||||
msgstr "請填上標題。"
 | 
					msgstr "請填上標題。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/static/js/description-editor.js:767
 | 
					#: src/accounting/static/js/description-editor.js:756
 | 
				
			||||||
#: src/accounting/static/js/description-editor.js:953
 | 
					#: src/accounting/static/js/description-editor.js:934
 | 
				
			||||||
msgid "Please fill in the tag."
 | 
					msgid "Please fill in the tag."
 | 
				
			||||||
msgstr "請填上標籤。"
 | 
					msgstr "請填上標籤。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/static/js/description-editor.js:777
 | 
					#: src/accounting/static/js/description-editor.js:766
 | 
				
			||||||
#: src/accounting/static/js/description-editor.js:973
 | 
					#: src/accounting/static/js/description-editor.js:954
 | 
				
			||||||
msgid "Please fill in the origin."
 | 
					msgid "Please fill in the origin."
 | 
				
			||||||
msgstr "請填上起點。"
 | 
					msgstr "請填上起點。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/static/js/description-editor.js:787
 | 
					#: src/accounting/static/js/description-editor.js:776
 | 
				
			||||||
#: src/accounting/static/js/description-editor.js:983
 | 
					#: src/accounting/static/js/description-editor.js:964
 | 
				
			||||||
msgid "Please fill in the destination."
 | 
					msgid "Please fill in the destination."
 | 
				
			||||||
msgstr "請填上終點。"
 | 
					msgstr "請填上終點。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/static/js/description-editor.js:963
 | 
					#: src/accounting/static/js/description-editor.js:944
 | 
				
			||||||
msgid "Please fill in the route."
 | 
					msgid "Please fill in the route."
 | 
				
			||||||
msgstr "請填上路線名稱。"
 | 
					msgstr "請填上路線名稱。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/static/js/description-editor.js:998
 | 
				
			||||||
 | 
					msgid "January"
 | 
				
			||||||
 | 
					msgstr "一月"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/static/js/description-editor.js:998
 | 
				
			||||||
 | 
					msgid "February"
 | 
				
			||||||
 | 
					msgstr "二月"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/static/js/description-editor.js:998
 | 
				
			||||||
 | 
					msgid "March"
 | 
				
			||||||
 | 
					msgstr "三月"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/static/js/description-editor.js:998
 | 
				
			||||||
 | 
					msgid "April"
 | 
				
			||||||
 | 
					msgstr "四月"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/static/js/description-editor.js:999
 | 
				
			||||||
 | 
					msgid "May"
 | 
				
			||||||
 | 
					msgstr "五月"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/static/js/description-editor.js:999
 | 
				
			||||||
 | 
					msgid "June"
 | 
				
			||||||
 | 
					msgstr "六月"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/static/js/description-editor.js:999
 | 
				
			||||||
 | 
					msgid "July"
 | 
				
			||||||
 | 
					msgstr "七月"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/static/js/description-editor.js:999
 | 
				
			||||||
 | 
					msgid "August"
 | 
				
			||||||
 | 
					msgstr "八月"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/static/js/description-editor.js:1000
 | 
				
			||||||
 | 
					msgid "September"
 | 
				
			||||||
 | 
					msgstr "九月"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/static/js/description-editor.js:1000
 | 
				
			||||||
 | 
					msgid "October"
 | 
				
			||||||
 | 
					msgstr "十月"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/static/js/description-editor.js:1000
 | 
				
			||||||
 | 
					msgid "November"
 | 
				
			||||||
 | 
					msgstr "十一月"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/static/js/description-editor.js:1000
 | 
				
			||||||
 | 
					msgid "December"
 | 
				
			||||||
 | 
					msgstr "十二月"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/static/js/journal-entry-form.js:985
 | 
					#: src/accounting/static/js/journal-entry-form.js:985
 | 
				
			||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:449
 | 
					#: src/accounting/static/js/journal-entry-line-item-editor.js:449
 | 
				
			||||||
msgid "Please fill in the amount."
 | 
					msgid "Please fill in the amount."
 | 
				
			||||||
@@ -629,28 +684,30 @@ msgstr "回上頁"
 | 
				
			|||||||
#: src/accounting/templates/accounting/account/detail.html:36
 | 
					#: src/accounting/templates/accounting/account/detail.html:36
 | 
				
			||||||
#: src/accounting/templates/accounting/currency/detail.html:36
 | 
					#: src/accounting/templates/accounting/currency/detail.html:36
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:36
 | 
					#: src/accounting/templates/accounting/journal-entry/include/detail.html:36
 | 
				
			||||||
msgid "Settings"
 | 
					msgid "Edit"
 | 
				
			||||||
msgstr "設定"
 | 
					msgstr "編輯"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/account/detail.html:41
 | 
					#: src/accounting/templates/accounting/account/detail.html:41
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:41
 | 
					#: src/accounting/templates/accounting/journal-entry/include/detail.html:41
 | 
				
			||||||
msgid "Order"
 | 
					msgid "Order"
 | 
				
			||||||
msgstr "次序"
 | 
					msgstr "次序"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/account/detail.html:46
 | 
					#: src/accounting/templates/accounting/account/detail.html:47
 | 
				
			||||||
#: src/accounting/templates/accounting/currency/detail.html:42
 | 
					#: src/accounting/templates/accounting/account/detail.html:52
 | 
				
			||||||
 | 
					#: src/accounting/templates/accounting/currency/detail.html:43
 | 
				
			||||||
 | 
					#: src/accounting/templates/accounting/currency/detail.html:48
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:48
 | 
					#: src/accounting/templates/accounting/journal-entry/include/detail.html:48
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:53
 | 
					#: src/accounting/templates/accounting/journal-entry/include/detail.html:53
 | 
				
			||||||
msgid "Delete"
 | 
					msgid "Delete"
 | 
				
			||||||
msgstr "刪除"
 | 
					msgstr "刪除"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/account/detail.html:69
 | 
					#: src/accounting/templates/accounting/account/detail.html:76
 | 
				
			||||||
msgid "Confirm Delete Account"
 | 
					msgid "Confirm Delete Account"
 | 
				
			||||||
msgstr "刪除科目確認"
 | 
					msgstr "確認刪除科目"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/account/detail.html:70
 | 
					#: src/accounting/templates/accounting/account/detail.html:77
 | 
				
			||||||
#: src/accounting/templates/accounting/account/include/form.html:91
 | 
					#: src/accounting/templates/accounting/account/include/form.html:91
 | 
				
			||||||
#: src/accounting/templates/accounting/currency/detail.html:66
 | 
					#: src/accounting/templates/accounting/currency/detail.html:73
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:27
 | 
					#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:27
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:30
 | 
					#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:30
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:78
 | 
					#: src/accounting/templates/accounting/journal-entry/include/detail.html:78
 | 
				
			||||||
@@ -661,36 +718,36 @@ msgstr "刪除科目確認"
 | 
				
			|||||||
msgid "Close"
 | 
					msgid "Close"
 | 
				
			||||||
msgstr "關閉"
 | 
					msgstr "關閉"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/account/detail.html:73
 | 
					#: src/accounting/templates/accounting/account/detail.html:80
 | 
				
			||||||
msgid "Do you really want to delete this account?"
 | 
					msgid "Do you really want to delete this account?"
 | 
				
			||||||
msgstr "你確定要刪掉這個科目嗎?"
 | 
					msgstr "你確定要刪掉這個科目嗎?"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/account/detail.html:76
 | 
					#: src/accounting/templates/accounting/account/detail.html:83
 | 
				
			||||||
#: src/accounting/templates/accounting/account/include/form.html:112
 | 
					#: src/accounting/templates/accounting/account/include/form.html:112
 | 
				
			||||||
#: src/accounting/templates/accounting/currency/detail.html:72
 | 
					#: src/accounting/templates/accounting/currency/detail.html:79
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:49
 | 
					#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:49
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:187
 | 
					#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:193
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:84
 | 
					#: src/accounting/templates/accounting/journal-entry/include/detail.html:84
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:70
 | 
					#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:70
 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:37
 | 
					#: src/accounting/templates/accounting/report/include/search-modal.html:37
 | 
				
			||||||
msgid "Cancel"
 | 
					msgid "Cancel"
 | 
				
			||||||
msgstr "取消"
 | 
					msgstr "取消"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/account/detail.html:77
 | 
					#: src/accounting/templates/accounting/account/detail.html:84
 | 
				
			||||||
#: src/accounting/templates/accounting/currency/detail.html:73
 | 
					#: src/accounting/templates/accounting/currency/detail.html:80
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:85
 | 
					#: src/accounting/templates/accounting/journal-entry/include/detail.html:85
 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:141
 | 
					#: src/accounting/templates/accounting/report/include/period-chooser.html:141
 | 
				
			||||||
msgid "Confirm"
 | 
					msgid "Confirm"
 | 
				
			||||||
msgstr "確定"
 | 
					msgstr "確定"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/account/detail.html:94
 | 
					#: src/accounting/templates/accounting/account/detail.html:101
 | 
				
			||||||
#: src/accounting/templates/accounting/currency/detail.html:85
 | 
					#: src/accounting/templates/accounting/currency/detail.html:92
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:114
 | 
					#: src/accounting/templates/accounting/journal-entry/include/detail.html:114
 | 
				
			||||||
msgid "Created"
 | 
					msgid "Created"
 | 
				
			||||||
msgstr "建檔"
 | 
					msgstr "建檔"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/account/detail.html:95
 | 
					#: src/accounting/templates/accounting/account/detail.html:102
 | 
				
			||||||
#: src/accounting/templates/accounting/currency/detail.html:86
 | 
					#: src/accounting/templates/accounting/currency/detail.html:93
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:115
 | 
					#: src/accounting/templates/accounting/journal-entry/include/detail.html:115
 | 
				
			||||||
msgid "Updated"
 | 
					msgid "Updated"
 | 
				
			||||||
msgstr "更新"
 | 
					msgstr "更新"
 | 
				
			||||||
@@ -714,11 +771,8 @@ msgstr "科目管理"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/account/list.html:32
 | 
					#: src/accounting/templates/accounting/account/list.html:32
 | 
				
			||||||
#: src/accounting/templates/accounting/currency/list.html:32
 | 
					#: src/accounting/templates/accounting/currency/list.html:32
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/disbursement/include/form-currency.html:67
 | 
					#: src/accounting/templates/accounting/journal-entry/include/form-debit-credit.html:44
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/form.html:64
 | 
					#: src/accounting/templates/accounting/journal-entry/include/form.html:64
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/receipt/include/form-currency.html:67
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:69
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:100
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:26
 | 
					#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:26
 | 
				
			||||||
msgid "New"
 | 
					msgid "New"
 | 
				
			||||||
msgstr "新增"
 | 
					msgstr "新增"
 | 
				
			||||||
@@ -730,7 +784,7 @@ msgstr "新增"
 | 
				
			|||||||
#: src/accounting/templates/accounting/currency/list.html:65
 | 
					#: src/accounting/templates/accounting/currency/list.html:65
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:46
 | 
					#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:46
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:51
 | 
					#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:51
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/order.html:80
 | 
					#: src/accounting/templates/accounting/journal-entry/order.html:82
 | 
				
			||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:110
 | 
					#: src/accounting/templates/accounting/report/balance-sheet.html:110
 | 
				
			||||||
#: src/accounting/templates/accounting/report/income-expenses.html:113
 | 
					#: src/accounting/templates/accounting/report/income-expenses.html:113
 | 
				
			||||||
#: src/accounting/templates/accounting/report/income-statement.html:96
 | 
					#: src/accounting/templates/accounting/report/income-statement.html:96
 | 
				
			||||||
@@ -749,7 +803,7 @@ msgstr "%(base)s下的科目"
 | 
				
			|||||||
#: src/accounting/templates/accounting/account/include/form.html:75
 | 
					#: src/accounting/templates/accounting/account/include/form.html:75
 | 
				
			||||||
#: src/accounting/templates/accounting/account/order.html:62
 | 
					#: src/accounting/templates/accounting/account/order.html:62
 | 
				
			||||||
#: src/accounting/templates/accounting/currency/include/form.html:57
 | 
					#: src/accounting/templates/accounting/currency/include/form.html:57
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:188
 | 
					#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:194
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/form.html:80
 | 
					#: src/accounting/templates/accounting/journal-entry/include/form.html:80
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:71
 | 
					#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:71
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/order.html:61
 | 
					#: src/accounting/templates/accounting/journal-entry/order.html:61
 | 
				
			||||||
@@ -790,11 +844,11 @@ msgstr "基本科目管理"
 | 
				
			|||||||
msgid "Add a New Currency"
 | 
					msgid "Add a New Currency"
 | 
				
			||||||
msgstr "新增貨幣"
 | 
					msgstr "新增貨幣"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/currency/detail.html:65
 | 
					#: src/accounting/templates/accounting/currency/detail.html:72
 | 
				
			||||||
msgid "Confirm Delete Currency"
 | 
					msgid "Confirm Delete Currency"
 | 
				
			||||||
msgstr "刪除貨幣確認"
 | 
					msgstr "確認刪除貨幣"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/currency/detail.html:69
 | 
					#: src/accounting/templates/accounting/currency/detail.html:76
 | 
				
			||||||
msgid "Do you really want to delete this currency?"
 | 
					msgid "Do you really want to delete this currency?"
 | 
				
			||||||
msgstr "你確定要刪掉這個貨幣嗎?"
 | 
					msgstr "你確定要刪掉這個貨幣嗎?"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -850,14 +904,14 @@ msgstr "新增現金支出傳票"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:27
 | 
					#: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:27
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/receipt/detail.html:27
 | 
					#: src/accounting/templates/accounting/journal-entry/receipt/detail.html:27
 | 
				
			||||||
msgid "To Transfer"
 | 
					msgid "As Transfer"
 | 
				
			||||||
msgstr "改轉帳"
 | 
					msgstr "改轉帳"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:37
 | 
					#: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:37
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/disbursement/include/form-currency.html:46
 | 
					#: src/accounting/templates/accounting/journal-entry/disbursement/include/form-currency.html:28
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/form.html:56
 | 
					#: src/accounting/templates/accounting/journal-entry/include/form.html:56
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/receipt/detail.html:37
 | 
					#: src/accounting/templates/accounting/journal-entry/receipt/detail.html:37
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/receipt/include/form-currency.html:46
 | 
					#: src/accounting/templates/accounting/journal-entry/receipt/include/form-currency.html:28
 | 
				
			||||||
msgid "Content"
 | 
					msgid "Content"
 | 
				
			||||||
msgstr "內容"
 | 
					msgstr "內容"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -893,8 +947,8 @@ msgid "Bus"
 | 
				
			|||||||
msgstr "公車"
 | 
					msgstr "公車"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:59
 | 
					#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:59
 | 
				
			||||||
msgid "Regular"
 | 
					msgid "Recurring"
 | 
				
			||||||
msgstr "帳單"
 | 
					msgstr "常用"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:64
 | 
					#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:64
 | 
				
			||||||
msgid "Annotation"
 | 
					msgid "Annotation"
 | 
				
			||||||
@@ -922,7 +976,7 @@ msgstr "至"
 | 
				
			|||||||
msgid "Route"
 | 
					msgid "Route"
 | 
				
			||||||
msgstr "路線"
 | 
					msgstr "路線"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:166
 | 
					#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:172
 | 
				
			||||||
msgid "The Number of Items"
 | 
					msgid "The Number of Items"
 | 
				
			||||||
msgstr "數量"
 | 
					msgstr "數量"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -948,7 +1002,7 @@ msgstr "未抵銷"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:77
 | 
					#: src/accounting/templates/accounting/journal-entry/include/detail.html:77
 | 
				
			||||||
msgid "Confirm Delete Journal Entry"
 | 
					msgid "Confirm Delete Journal Entry"
 | 
				
			||||||
msgstr "刪除傳票確認"
 | 
					msgstr "確認刪除傳票"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:81
 | 
					#: src/accounting/templates/accounting/journal-entry/include/detail.html:81
 | 
				
			||||||
msgid "Do you really want to delete this journal entry?"
 | 
					msgid "Do you really want to delete this journal entry?"
 | 
				
			||||||
@@ -962,6 +1016,24 @@ msgstr "分錄內容"
 | 
				
			|||||||
msgid "Original Line Item"
 | 
					msgid "Original Line Item"
 | 
				
			||||||
msgstr "原始分錄"
 | 
					msgstr "原始分錄"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/templates/accounting/journal-entry/include/order-journal-entry.html:26
 | 
				
			||||||
 | 
					#: src/accounting/templates/accounting/report/include/add-journal-entry-material-fab.html:26
 | 
				
			||||||
 | 
					#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:31
 | 
				
			||||||
 | 
					msgid "Cash Disbursement"
 | 
				
			||||||
 | 
					msgstr "現金支出"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/templates/accounting/journal-entry/include/order-journal-entry.html:28
 | 
				
			||||||
 | 
					#: src/accounting/templates/accounting/report/include/add-journal-entry-material-fab.html:29
 | 
				
			||||||
 | 
					#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:36
 | 
				
			||||||
 | 
					msgid "Cash Receipt"
 | 
				
			||||||
 | 
					msgstr "現金收入"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/templates/accounting/journal-entry/include/order-journal-entry.html:30
 | 
				
			||||||
 | 
					#: src/accounting/templates/accounting/report/include/add-journal-entry-material-fab.html:32
 | 
				
			||||||
 | 
					#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:41
 | 
				
			||||||
 | 
					msgid "Transfer"
 | 
				
			||||||
 | 
					msgstr "轉帳"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:26
 | 
					#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:26
 | 
				
			||||||
msgid "Select Original Line Item"
 | 
					msgid "Select Original Line Item"
 | 
				
			||||||
msgstr "選擇原始分錄"
 | 
					msgstr "選擇原始分錄"
 | 
				
			||||||
@@ -1007,21 +1079,6 @@ msgstr "%(period)s%(currency)s%(account)s分類帳"
 | 
				
			|||||||
msgid "Trial Balance of %(currency)s %(period)s"
 | 
					msgid "Trial Balance of %(currency)s %(period)s"
 | 
				
			||||||
msgstr "%(period)s%(currency)s試算表"
 | 
					msgstr "%(period)s%(currency)s試算表"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/add-journal-entry-material-fab.html:26
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:31
 | 
					 | 
				
			||||||
msgid "Cash Disbursement"
 | 
					 | 
				
			||||||
msgstr "現金支出"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/add-journal-entry-material-fab.html:29
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:36
 | 
					 | 
				
			||||||
msgid "Cash Receipt"
 | 
					 | 
				
			||||||
msgstr "現金收入"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/add-journal-entry-material-fab.html:32
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:41
 | 
					 | 
				
			||||||
msgid "Transfer"
 | 
					 | 
				
			||||||
msgstr "轉帳"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:26
 | 
					#: src/accounting/templates/accounting/report/include/period-chooser.html:26
 | 
				
			||||||
msgid "Period Chooser"
 | 
					msgid "Period Chooser"
 | 
				
			||||||
msgstr "選擇日期範圍"
 | 
					msgstr "選擇日期範圍"
 | 
				
			||||||
@@ -1042,10 +1099,14 @@ msgstr "日"
 | 
				
			|||||||
msgid "Custom"
 | 
					msgid "Custom"
 | 
				
			||||||
msgstr "自訂"
 | 
					msgstr "自訂"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:52
 | 
					#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:50
 | 
				
			||||||
msgid "Report"
 | 
					msgid "Report"
 | 
				
			||||||
msgstr "報表"
 | 
					msgstr "報表"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:106
 | 
				
			||||||
 | 
					msgid "Period"
 | 
				
			||||||
 | 
					msgstr "期間"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:112
 | 
					#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:112
 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:117
 | 
					#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:117
 | 
				
			||||||
msgid "Download"
 | 
					msgid "Download"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
import unittest
 | 
					import unittest
 | 
				
			||||||
from datetime import timedelta
 | 
					from datetime import timedelta, date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import httpx
 | 
					import httpx
 | 
				
			||||||
import sqlalchemy as sa
 | 
					import sqlalchemy as sa
 | 
				
			||||||
@@ -28,6 +28,7 @@ from flask.testing import FlaskCliRunner
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from test_site import db
 | 
					from test_site import db
 | 
				
			||||||
from testlib import create_test_app, get_client, set_locale
 | 
					from testlib import create_test_app, get_client, set_locale
 | 
				
			||||||
 | 
					from testlib_journal_entry import add_journal_entry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
NEXT_URI: str = "/_next"
 | 
					NEXT_URI: str = "/_next"
 | 
				
			||||||
"""The next URI."""
 | 
					"""The next URI."""
 | 
				
			||||||
@@ -53,13 +54,15 @@ class AccountData:
 | 
				
			|||||||
        """The code."""
 | 
					        """The code."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
cash: AccountData = AccountData("1111", 1, "Cash")
 | 
					CASH: AccountData = AccountData("1111", 1, "Cash")
 | 
				
			||||||
"""The cash account."""
 | 
					"""The cash account."""
 | 
				
			||||||
bank: AccountData = AccountData("1113", 1, "Bank")
 | 
					PETTY: AccountData = AccountData("1112", 1, "Bank")
 | 
				
			||||||
 | 
					"""The petty cash account."""
 | 
				
			||||||
 | 
					BANK: AccountData = AccountData("1113", 1, "Bank")
 | 
				
			||||||
"""The bank account."""
 | 
					"""The bank account."""
 | 
				
			||||||
stock: AccountData = AccountData("1121", 1, "Stock")
 | 
					STOCK: AccountData = AccountData("1121", 1, "Stock")
 | 
				
			||||||
"""The stock account."""
 | 
					"""The stock account."""
 | 
				
			||||||
loan: AccountData = AccountData("2112", 1, "Loan")
 | 
					LOAN: AccountData = AccountData("2112", 1, "Loan")
 | 
				
			||||||
"""The loan account."""
 | 
					"""The loan account."""
 | 
				
			||||||
PREFIX: str = "/accounting/accounts"
 | 
					PREFIX: str = "/accounting/accounts"
 | 
				
			||||||
"""The URL prefix for the account management."""
 | 
					"""The URL prefix for the account management."""
 | 
				
			||||||
@@ -147,19 +150,19 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(f"{PREFIX}/store",
 | 
					        response = self.client.post(f"{PREFIX}/store",
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": cash.base_code,
 | 
					                                          "base_code": CASH.base_code,
 | 
				
			||||||
                                          "title": cash.title})
 | 
					                                          "title": CASH.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"],
 | 
					        self.assertEqual(response.headers["Location"],
 | 
				
			||||||
                         f"{PREFIX}/{cash.code}")
 | 
					                         f"{PREFIX}/{CASH.code}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(f"{PREFIX}/store",
 | 
					        response = self.client.post(f"{PREFIX}/store",
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": bank.base_code,
 | 
					                                          "base_code": BANK.base_code,
 | 
				
			||||||
                                          "title": bank.title})
 | 
					                                          "title": BANK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"],
 | 
					        self.assertEqual(response.headers["Location"],
 | 
				
			||||||
                         f"{PREFIX}/{bank.code}")
 | 
					                         f"{PREFIX}/{BANK.code}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_nobody(self) -> None:
 | 
					    def test_nobody(self) -> None:
 | 
				
			||||||
        """Test the permission as nobody.
 | 
					        """Test the permission as nobody.
 | 
				
			||||||
@@ -173,7 +176,7 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = client.get(PREFIX)
 | 
					        response = client.get(PREFIX)
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/{cash.code}")
 | 
					        response = client.get(f"{PREFIX}/{CASH.code}")
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/create")
 | 
					        response = client.get(f"{PREFIX}/create")
 | 
				
			||||||
@@ -181,33 +184,33 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/store",
 | 
					        response = client.post(f"{PREFIX}/store",
 | 
				
			||||||
                               data={"csrf_token": csrf_token,
 | 
					                               data={"csrf_token": csrf_token,
 | 
				
			||||||
                                     "base_code": stock.base_code,
 | 
					                                     "base_code": STOCK.base_code,
 | 
				
			||||||
                                     "title": stock.title})
 | 
					                                     "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/{cash.code}/edit")
 | 
					        response = client.get(f"{PREFIX}/{CASH.code}/edit")
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/{cash.code}/update",
 | 
					        response = client.post(f"{PREFIX}/{CASH.code}/update",
 | 
				
			||||||
                               data={"csrf_token": csrf_token,
 | 
					                               data={"csrf_token": csrf_token,
 | 
				
			||||||
                                     "base_code": cash.base_code,
 | 
					                                     "base_code": CASH.base_code,
 | 
				
			||||||
                                     "title": f"{cash.title}-2"})
 | 
					                                     "title": f"{CASH.title}-2"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/{cash.code}/delete",
 | 
					        response = client.post(f"{PREFIX}/{BANK.code}/delete",
 | 
				
			||||||
                               data={"csrf_token": csrf_token})
 | 
					                               data={"csrf_token": csrf_token})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/bases/{cash.base_code}")
 | 
					        response = client.get(f"{PREFIX}/bases/{CASH.base_code}")
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            bank_id: int = Account.find_by_code(bank.code).id
 | 
					            cash_id: int = Account.find_by_code(CASH.code).id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/bases/{bank.base_code}",
 | 
					        response = client.post(f"{PREFIX}/bases/{CASH.base_code}",
 | 
				
			||||||
                               data={"csrf_token": csrf_token,
 | 
					                               data={"csrf_token": csrf_token,
 | 
				
			||||||
                                     "next": NEXT_URI,
 | 
					                                     "next": NEXT_URI,
 | 
				
			||||||
                                     f"{bank_id}-no": "5"})
 | 
					                                     f"{cash_id}-no": "5"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_viewer(self) -> None:
 | 
					    def test_viewer(self) -> None:
 | 
				
			||||||
@@ -222,7 +225,7 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = client.get(PREFIX)
 | 
					        response = client.get(PREFIX)
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/{cash.code}")
 | 
					        response = client.get(f"{PREFIX}/{CASH.code}")
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/create")
 | 
					        response = client.get(f"{PREFIX}/create")
 | 
				
			||||||
@@ -230,33 +233,33 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/store",
 | 
					        response = client.post(f"{PREFIX}/store",
 | 
				
			||||||
                               data={"csrf_token": csrf_token,
 | 
					                               data={"csrf_token": csrf_token,
 | 
				
			||||||
                                     "base_code": stock.base_code,
 | 
					                                     "base_code": STOCK.base_code,
 | 
				
			||||||
                                     "title": stock.title})
 | 
					                                     "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/{cash.code}/edit")
 | 
					        response = client.get(f"{PREFIX}/{CASH.code}/edit")
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/{cash.code}/update",
 | 
					        response = client.post(f"{PREFIX}/{CASH.code}/update",
 | 
				
			||||||
                               data={"csrf_token": csrf_token,
 | 
					                               data={"csrf_token": csrf_token,
 | 
				
			||||||
                                     "base_code": cash.base_code,
 | 
					                                     "base_code": CASH.base_code,
 | 
				
			||||||
                                     "title": f"{cash.title}-2"})
 | 
					                                     "title": f"{CASH.title}-2"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/{cash.code}/delete",
 | 
					        response = client.post(f"{PREFIX}/{BANK.code}/delete",
 | 
				
			||||||
                               data={"csrf_token": csrf_token})
 | 
					                               data={"csrf_token": csrf_token})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/bases/{cash.base_code}")
 | 
					        response = client.get(f"{PREFIX}/bases/{CASH.base_code}")
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            bank_id: int = Account.find_by_code(bank.code).id
 | 
					            cash_id: int = Account.find_by_code(CASH.code).id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/bases/{bank.base_code}",
 | 
					        response = client.post(f"{PREFIX}/bases/{CASH.base_code}",
 | 
				
			||||||
                               data={"csrf_token": csrf_token,
 | 
					                               data={"csrf_token": csrf_token,
 | 
				
			||||||
                                     "next": NEXT_URI,
 | 
					                                     "next": NEXT_URI,
 | 
				
			||||||
                                     f"{bank_id}-no": "5"})
 | 
					                                     f"{cash_id}-no": "5"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_editor(self) -> None:
 | 
					    def test_editor(self) -> None:
 | 
				
			||||||
@@ -270,7 +273,7 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = self.client.get(PREFIX)
 | 
					        response = self.client.get(PREFIX)
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.get(f"{PREFIX}/{cash.code}")
 | 
					        response = self.client.get(f"{PREFIX}/{CASH.code}")
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.get(f"{PREFIX}/create")
 | 
					        response = self.client.get(f"{PREFIX}/create")
 | 
				
			||||||
@@ -278,37 +281,37 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(f"{PREFIX}/store",
 | 
					        response = self.client.post(f"{PREFIX}/store",
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": stock.base_code,
 | 
					                                          "base_code": STOCK.base_code,
 | 
				
			||||||
                                          "title": stock.title})
 | 
					                                          "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"],
 | 
					        self.assertEqual(response.headers["Location"],
 | 
				
			||||||
                         f"{PREFIX}/{stock.code}")
 | 
					                         f"{PREFIX}/{STOCK.code}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.get(f"{PREFIX}/{cash.code}/edit")
 | 
					        response = self.client.get(f"{PREFIX}/{CASH.code}/edit")
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(f"{PREFIX}/{cash.code}/update",
 | 
					        response = self.client.post(f"{PREFIX}/{CASH.code}/update",
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": cash.base_code,
 | 
					                                          "base_code": CASH.base_code,
 | 
				
			||||||
                                          "title": f"{cash.title}-2"})
 | 
					                                          "title": f"{CASH.title}-2"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], f"{PREFIX}/{cash.code}")
 | 
					        self.assertEqual(response.headers["Location"], f"{PREFIX}/{CASH.code}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(f"{PREFIX}/{cash.code}/delete",
 | 
					        response = self.client.post(f"{PREFIX}/{BANK.code}/delete",
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token})
 | 
					                                    data={"csrf_token": self.csrf_token})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], PREFIX)
 | 
					        self.assertEqual(response.headers["Location"], PREFIX)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.get(f"{PREFIX}/bases/{cash.base_code}")
 | 
					        response = self.client.get(f"{PREFIX}/bases/{CASH.base_code}")
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            bank_id: int = Account.find_by_code(bank.code).id
 | 
					            cash_id: int = Account.find_by_code(CASH.code).id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(f"{PREFIX}/bases/{bank.base_code}",
 | 
					        response = self.client.post(f"{PREFIX}/bases/{CASH.base_code}",
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "next": NEXT_URI,
 | 
					                                          "next": NEXT_URI,
 | 
				
			||||||
                                          f"{bank_id}-no": "5"})
 | 
					                                          f"{cash_id}-no": "5"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], NEXT_URI)
 | 
					        self.assertEqual(response.headers["Location"], NEXT_URI)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -320,31 +323,31 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        from accounting.models import Account
 | 
					        from accounting.models import Account
 | 
				
			||||||
        create_uri: str = f"{PREFIX}/create"
 | 
					        create_uri: str = f"{PREFIX}/create"
 | 
				
			||||||
        store_uri: str = f"{PREFIX}/store"
 | 
					        store_uri: str = f"{PREFIX}/store"
 | 
				
			||||||
        detail_uri: str = f"{PREFIX}/{stock.code}"
 | 
					        detail_uri: str = f"{PREFIX}/{STOCK.code}"
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            self.assertEqual({x.code for x in Account.query.all()},
 | 
					            self.assertEqual({x.code for x in Account.query.all()},
 | 
				
			||||||
                             {cash.code, bank.code})
 | 
					                             {CASH.code, BANK.code})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Missing CSRF token
 | 
					        # Missing CSRF token
 | 
				
			||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"base_code": stock.base_code,
 | 
					                                    data={"base_code": STOCK.base_code,
 | 
				
			||||||
                                          "title": stock.title})
 | 
					                                          "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 400)
 | 
					        self.assertEqual(response.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # CSRF token mismatch
 | 
					        # CSRF token mismatch
 | 
				
			||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": f"{self.csrf_token}-2",
 | 
					                                    data={"csrf_token": f"{self.csrf_token}-2",
 | 
				
			||||||
                                          "base_code": stock.base_code,
 | 
					                                          "base_code": STOCK.base_code,
 | 
				
			||||||
                                          "title": stock.title})
 | 
					                                          "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 400)
 | 
					        self.assertEqual(response.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Empty base account code
 | 
					        # Empty base account code
 | 
				
			||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": " ",
 | 
					                                          "base_code": " ",
 | 
				
			||||||
                                          "title": stock.title})
 | 
					                                          "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -352,7 +355,7 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": "9999",
 | 
					                                          "base_code": "9999",
 | 
				
			||||||
                                          "title": stock.title})
 | 
					                                          "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -360,14 +363,14 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": "1",
 | 
					                                          "base_code": "1",
 | 
				
			||||||
                                          "title": stock.title})
 | 
					                                          "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Empty name
 | 
					        # Empty name
 | 
				
			||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": stock.base_code,
 | 
					                                          "base_code": STOCK.base_code,
 | 
				
			||||||
                                          "title": " "})
 | 
					                                          "title": " "})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
@@ -376,7 +379,7 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": "6172",
 | 
					                                          "base_code": "6172",
 | 
				
			||||||
                                          "title": stock.title,
 | 
					                                          "title": STOCK.title,
 | 
				
			||||||
                                          "is_need_offset": "yes"})
 | 
					                                          "is_need_offset": "yes"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
@@ -384,43 +387,43 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        # Success, with spaces to be stripped
 | 
					        # Success, with spaces to be stripped
 | 
				
			||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": f" {stock.base_code} ",
 | 
					                                          "base_code": f" {STOCK.base_code} ",
 | 
				
			||||||
                                          "title": f" {stock.title} "})
 | 
					                                          "title": f" {STOCK.title} "})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Success under the same base
 | 
					        # Success under the same base
 | 
				
			||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": stock.base_code,
 | 
					                                          "base_code": STOCK.base_code,
 | 
				
			||||||
                                          "title": stock.title})
 | 
					                                          "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"],
 | 
					        self.assertEqual(response.headers["Location"],
 | 
				
			||||||
                         f"{PREFIX}/{stock.base_code}-002")
 | 
					                         f"{PREFIX}/{STOCK.base_code}-002")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Success under the same base, with order in a mess.
 | 
					        # Success under the same base, with order in a mess.
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            stock_2: Account = Account.find_by_code(f"{stock.base_code}-002")
 | 
					            stock_2: Account = Account.find_by_code(f"{STOCK.base_code}-002")
 | 
				
			||||||
            stock_2.no = 66
 | 
					            stock_2.no = 66
 | 
				
			||||||
            db.session.commit()
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": stock.base_code,
 | 
					                                          "base_code": STOCK.base_code,
 | 
				
			||||||
                                          "title": stock.title})
 | 
					                                          "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"],
 | 
					        self.assertEqual(response.headers["Location"],
 | 
				
			||||||
                         f"{PREFIX}/{stock.base_code}-003")
 | 
					                         f"{PREFIX}/{STOCK.base_code}-003")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            self.assertEqual({x.code for x in Account.query.all()},
 | 
					            self.assertEqual({x.code for x in Account.query.all()},
 | 
				
			||||||
                             {cash.code, bank.code, stock.code,
 | 
					                             {CASH.code, BANK.code, STOCK.code,
 | 
				
			||||||
                              f"{stock.base_code}-002",
 | 
					                              f"{STOCK.base_code}-002",
 | 
				
			||||||
                              f"{stock.base_code}-003"})
 | 
					                              f"{STOCK.base_code}-003"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            account: Account = Account.find_by_code(stock.code)
 | 
					            account: Account = Account.find_by_code(STOCK.code)
 | 
				
			||||||
            self.assertEqual(account.base_code, stock.base_code)
 | 
					            self.assertEqual(account.base_code, STOCK.base_code)
 | 
				
			||||||
            self.assertEqual(account.title_l10n, stock.title)
 | 
					            self.assertEqual(account.title_l10n, STOCK.title)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_basic_update(self) -> None:
 | 
					    def test_basic_update(self) -> None:
 | 
				
			||||||
        """Tests the basic rules to update a user.
 | 
					        """Tests the basic rules to update a user.
 | 
				
			||||||
@@ -428,30 +431,30 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Account
 | 
					        from accounting.models import Account
 | 
				
			||||||
        detail_uri: str = f"{PREFIX}/{cash.code}"
 | 
					        detail_uri: str = f"{PREFIX}/{CASH.code}"
 | 
				
			||||||
        edit_uri: str = f"{PREFIX}/{cash.code}/edit"
 | 
					        edit_uri: str = f"{PREFIX}/{CASH.code}/edit"
 | 
				
			||||||
        update_uri: str = f"{PREFIX}/{cash.code}/update"
 | 
					        update_uri: str = f"{PREFIX}/{CASH.code}/update"
 | 
				
			||||||
        detail_c_uri: str = f"{PREFIX}/{stock.code}"
 | 
					        detail_c_uri: str = f"{PREFIX}/{STOCK.code}"
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Success, with spaces to be stripped
 | 
					        # Success, with spaces to be stripped
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": f" {cash.base_code} ",
 | 
					                                          "base_code": f" {CASH.base_code} ",
 | 
				
			||||||
                                          "title": f" {cash.title}-1 "})
 | 
					                                          "title": f" {CASH.title}-1 "})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            account: Account = Account.find_by_code(cash.code)
 | 
					            account: Account = Account.find_by_code(CASH.code)
 | 
				
			||||||
            self.assertEqual(account.base_code, cash.base_code)
 | 
					            self.assertEqual(account.base_code, CASH.base_code)
 | 
				
			||||||
            self.assertEqual(account.title_l10n, f"{cash.title}-1")
 | 
					            self.assertEqual(account.title_l10n, f"{CASH.title}-1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Empty base account code
 | 
					        # Empty base account code
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": " ",
 | 
					                                          "base_code": " ",
 | 
				
			||||||
                                          "title": stock.title})
 | 
					                                          "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -459,7 +462,7 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": "9999",
 | 
					                                          "base_code": "9999",
 | 
				
			||||||
                                          "title": stock.title})
 | 
					                                          "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -467,14 +470,14 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": "1",
 | 
					                                          "base_code": "1",
 | 
				
			||||||
                                          "title": stock.title})
 | 
					                                          "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Empty name
 | 
					        # Empty name
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": stock.base_code,
 | 
					                                          "base_code": STOCK.base_code,
 | 
				
			||||||
                                          "title": " "})
 | 
					                                          "title": " "})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
@@ -483,7 +486,7 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": "6172",
 | 
					                                          "base_code": "6172",
 | 
				
			||||||
                                          "title": stock.title,
 | 
					                                          "title": STOCK.title,
 | 
				
			||||||
                                          "is_need_offset": "yes"})
 | 
					                                          "is_need_offset": "yes"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
@@ -491,8 +494,8 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        # Change the base account
 | 
					        # Change the base account
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": stock.base_code,
 | 
					                                          "base_code": STOCK.base_code,
 | 
				
			||||||
                                          "title": stock.title})
 | 
					                                          "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_c_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_c_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -508,20 +511,20 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Account
 | 
					        from accounting.models import Account
 | 
				
			||||||
        detail_uri: str = f"{PREFIX}/{cash.code}"
 | 
					        detail_uri: str = f"{PREFIX}/{CASH.code}"
 | 
				
			||||||
        update_uri: str = f"{PREFIX}/{cash.code}/update"
 | 
					        update_uri: str = f"{PREFIX}/{CASH.code}/update"
 | 
				
			||||||
        account: Account
 | 
					        account: Account
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": f" {cash.base_code} ",
 | 
					                                          "base_code": f" {CASH.base_code} ",
 | 
				
			||||||
                                          "title": f" {cash.title} "})
 | 
					                                          "title": f" {CASH.title} "})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            account = Account.find_by_code(cash.code)
 | 
					            account = Account.find_by_code(CASH.code)
 | 
				
			||||||
            self.assertIsNotNone(account)
 | 
					            self.assertIsNotNone(account)
 | 
				
			||||||
            account.created_at \
 | 
					            account.created_at \
 | 
				
			||||||
                = account.created_at - timedelta(seconds=5)
 | 
					                = account.created_at - timedelta(seconds=5)
 | 
				
			||||||
@@ -530,13 +533,13 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": cash.base_code,
 | 
					                                          "base_code": CASH.base_code,
 | 
				
			||||||
                                          "title": stock.title})
 | 
					                                          "title": STOCK.title})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            account = Account.find_by_code(cash.code)
 | 
					            account = Account.find_by_code(CASH.code)
 | 
				
			||||||
            self.assertIsNotNone(account)
 | 
					            self.assertIsNotNone(account)
 | 
				
			||||||
            self.assertLess(account.created_at,
 | 
					            self.assertLess(account.created_at,
 | 
				
			||||||
                            account.updated_at)
 | 
					                            account.updated_at)
 | 
				
			||||||
@@ -549,25 +552,25 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        from accounting.models import Account
 | 
					        from accounting.models import Account
 | 
				
			||||||
        editor_username, editor2_username = "editor", "editor2"
 | 
					        editor_username, editor2_username = "editor", "editor2"
 | 
				
			||||||
        client, csrf_token = get_client(self.app, editor2_username)
 | 
					        client, csrf_token = get_client(self.app, editor2_username)
 | 
				
			||||||
        detail_uri: str = f"{PREFIX}/{cash.code}"
 | 
					        detail_uri: str = f"{PREFIX}/{CASH.code}"
 | 
				
			||||||
        update_uri: str = f"{PREFIX}/{cash.code}/update"
 | 
					        update_uri: str = f"{PREFIX}/{CASH.code}/update"
 | 
				
			||||||
        account: Account
 | 
					        account: Account
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            account = Account.find_by_code(cash.code)
 | 
					            account = Account.find_by_code(CASH.code)
 | 
				
			||||||
            self.assertEqual(account.created_by.username, editor_username)
 | 
					            self.assertEqual(account.created_by.username, editor_username)
 | 
				
			||||||
            self.assertEqual(account.updated_by.username, editor_username)
 | 
					            self.assertEqual(account.updated_by.username, editor_username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.post(update_uri,
 | 
					        response = client.post(update_uri,
 | 
				
			||||||
                               data={"csrf_token": csrf_token,
 | 
					                               data={"csrf_token": csrf_token,
 | 
				
			||||||
                                     "base_code": cash.base_code,
 | 
					                                     "base_code": CASH.base_code,
 | 
				
			||||||
                                     "title": f"{cash.title}-2"})
 | 
					                                     "title": f"{CASH.title}-2"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            account = Account.find_by_code(cash.code)
 | 
					            account = Account.find_by_code(CASH.code)
 | 
				
			||||||
            self.assertEqual(account.created_by.username,
 | 
					            self.assertEqual(account.created_by.username,
 | 
				
			||||||
                             editor_username)
 | 
					                             editor_username)
 | 
				
			||||||
            self.assertEqual(account.updated_by.username,
 | 
					            self.assertEqual(account.updated_by.username,
 | 
				
			||||||
@@ -579,60 +582,60 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        :return: None
 | 
					        :return: None
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Account
 | 
					        from accounting.models import Account
 | 
				
			||||||
        detail_uri: str = f"{PREFIX}/{cash.code}"
 | 
					        detail_uri: str = f"{PREFIX}/{CASH.code}"
 | 
				
			||||||
        update_uri: str = f"{PREFIX}/{cash.code}/update"
 | 
					        update_uri: str = f"{PREFIX}/{CASH.code}/update"
 | 
				
			||||||
        account: Account
 | 
					        account: Account
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            account = Account.find_by_code(cash.code)
 | 
					            account = Account.find_by_code(CASH.code)
 | 
				
			||||||
            self.assertEqual(account.title_l10n, cash.title)
 | 
					            self.assertEqual(account.title_l10n, CASH.title)
 | 
				
			||||||
            self.assertEqual(account.l10n, [])
 | 
					            self.assertEqual(account.l10n, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        set_locale(self.client, self.csrf_token, "zh_Hant")
 | 
					        set_locale(self.client, self.csrf_token, "zh_Hant")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": cash.base_code,
 | 
					                                          "base_code": CASH.base_code,
 | 
				
			||||||
                                          "title": f"{cash.title}-zh_Hant"})
 | 
					                                          "title": f"{CASH.title}-zh_Hant"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            account = Account.find_by_code(cash.code)
 | 
					            account = Account.find_by_code(CASH.code)
 | 
				
			||||||
            self.assertEqual(account.title_l10n, cash.title)
 | 
					            self.assertEqual(account.title_l10n, CASH.title)
 | 
				
			||||||
            self.assertEqual({(x.locale, x.title) for x in account.l10n},
 | 
					            self.assertEqual({(x.locale, x.title) for x in account.l10n},
 | 
				
			||||||
                             {("zh_Hant", f"{cash.title}-zh_Hant")})
 | 
					                             {("zh_Hant", f"{CASH.title}-zh_Hant")})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        set_locale(self.client, self.csrf_token, "en")
 | 
					        set_locale(self.client, self.csrf_token, "en")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": cash.base_code,
 | 
					                                          "base_code": CASH.base_code,
 | 
				
			||||||
                                          "title": f"{cash.title}-2"})
 | 
					                                          "title": f"{CASH.title}-2"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            account = Account.find_by_code(cash.code)
 | 
					            account = Account.find_by_code(CASH.code)
 | 
				
			||||||
            self.assertEqual(account.title_l10n, f"{cash.title}-2")
 | 
					            self.assertEqual(account.title_l10n, f"{CASH.title}-2")
 | 
				
			||||||
            self.assertEqual({(x.locale, x.title) for x in account.l10n},
 | 
					            self.assertEqual({(x.locale, x.title) for x in account.l10n},
 | 
				
			||||||
                             {("zh_Hant", f"{cash.title}-zh_Hant")})
 | 
					                             {("zh_Hant", f"{CASH.title}-zh_Hant")})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        set_locale(self.client, self.csrf_token, "zh_Hant")
 | 
					        set_locale(self.client, self.csrf_token, "zh_Hant")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "base_code": cash.base_code,
 | 
					                                          "base_code": CASH.base_code,
 | 
				
			||||||
                                          "title": f"{cash.title}-zh_Hant-2"})
 | 
					                                          "title": f"{CASH.title}-zh_Hant-2"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            account = Account.find_by_code(cash.code)
 | 
					            account = Account.find_by_code(CASH.code)
 | 
				
			||||||
            self.assertEqual(account.title_l10n, f"{cash.title}-2")
 | 
					            self.assertEqual(account.title_l10n, f"{CASH.title}-2")
 | 
				
			||||||
            self.assertEqual({(x.locale, x.title) for x in account.l10n},
 | 
					            self.assertEqual({(x.locale, x.title) for x in account.l10n},
 | 
				
			||||||
                             {("zh_Hant", f"{cash.title}-zh_Hant-2")})
 | 
					                             {("zh_Hant", f"{CASH.title}-zh_Hant-2")})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_delete(self) -> None:
 | 
					    def test_delete(self) -> None:
 | 
				
			||||||
        """Tests to delete a currency.
 | 
					        """Tests to delete a currency.
 | 
				
			||||||
@@ -640,15 +643,51 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Account
 | 
					        from accounting.models import Account
 | 
				
			||||||
        detail_uri: str = f"{PREFIX}/{cash.code}"
 | 
					        detail_uri: str = f"{PREFIX}/{PETTY.code}"
 | 
				
			||||||
        delete_uri: str = f"{PREFIX}/{cash.code}/delete"
 | 
					        delete_uri: str = f"{PREFIX}/{PETTY.code}/delete"
 | 
				
			||||||
        list_uri: str = PREFIX
 | 
					        list_uri: str = PREFIX
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.client.post(f"{PREFIX}/store",
 | 
				
			||||||
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
 | 
					                                          "base_code": PETTY.base_code,
 | 
				
			||||||
 | 
					                                          "title": PETTY.title})
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.client.post("/accounting/currencies/store",
 | 
				
			||||||
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
 | 
					                                          "code": "USD",
 | 
				
			||||||
 | 
					                                          "name": "US Dollars"})
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
 | 
					        self.assertEqual(response.headers["Location"],
 | 
				
			||||||
 | 
					                         "/accounting/currencies/USD")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        add_journal_entry(self.client,
 | 
				
			||||||
 | 
					                          form={"csrf_token": self.csrf_token,
 | 
				
			||||||
 | 
					                                "next": NEXT_URI,
 | 
				
			||||||
 | 
					                                "date": date.today().isoformat(),
 | 
				
			||||||
 | 
					                                "currency-1-code": "USD",
 | 
				
			||||||
 | 
					                                "currency-1-credit-1-account_code": BANK.code,
 | 
				
			||||||
 | 
					                                "currency-1-credit-1-amount": "20"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            self.assertEqual({x.code for x in Account.query.all()},
 | 
					            self.assertEqual({x.code for x in Account.query.all()},
 | 
				
			||||||
                             {cash.code, bank.code})
 | 
					                             {CASH.code, PETTY.code, BANK.code})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Cannot delete the cash account
 | 
				
			||||||
 | 
					        response = self.client.post(f"{PREFIX}/{CASH.code}/delete",
 | 
				
			||||||
 | 
					                                    data={"csrf_token": self.csrf_token})
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
 | 
					        self.assertEqual(response.headers["Location"], f"{PREFIX}/{CASH.code}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Cannot delete the account that is in use
 | 
				
			||||||
 | 
					        response = self.client.post(f"{PREFIX}/{BANK.code}/delete",
 | 
				
			||||||
 | 
					                                    data={"csrf_token": self.csrf_token})
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
 | 
					        self.assertEqual(response.headers["Location"], f"{PREFIX}/{BANK.code}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Success
 | 
				
			||||||
        response = self.client.get(detail_uri)
 | 
					        response = self.client.get(detail_uri)
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
        response = self.client.post(delete_uri,
 | 
					        response = self.client.post(delete_uri,
 | 
				
			||||||
@@ -658,7 +697,7 @@ class AccountTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            self.assertEqual({x.code for x in Account.query.all()},
 | 
					            self.assertEqual({x.code for x in Account.query.all()},
 | 
				
			||||||
                             {bank.code})
 | 
					                             {CASH.code, BANK.code})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.get(detail_uri)
 | 
					        response = self.client.get(detail_uri)
 | 
				
			||||||
        self.assertEqual(response.status_code, 404)
 | 
					        self.assertEqual(response.status_code, 404)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,7 +20,7 @@
 | 
				
			|||||||
import csv
 | 
					import csv
 | 
				
			||||||
import typing as t
 | 
					import typing as t
 | 
				
			||||||
import unittest
 | 
					import unittest
 | 
				
			||||||
from datetime import timedelta
 | 
					from datetime import timedelta, date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import httpx
 | 
					import httpx
 | 
				
			||||||
from click.testing import Result
 | 
					from click.testing import Result
 | 
				
			||||||
@@ -29,6 +29,7 @@ from flask.testing import FlaskCliRunner
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from test_site import db
 | 
					from test_site import db
 | 
				
			||||||
from testlib import create_test_app, get_client, set_locale
 | 
					from testlib import create_test_app, get_client, set_locale
 | 
				
			||||||
 | 
					from testlib_journal_entry import add_journal_entry, NEXT_URI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CurrencyData:
 | 
					class CurrencyData:
 | 
				
			||||||
@@ -46,14 +47,14 @@ class CurrencyData:
 | 
				
			|||||||
        """The name."""
 | 
					        """The name."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
zza: CurrencyData = CurrencyData("ZZA", "Testing Dollar #A")
 | 
					USD: CurrencyData = CurrencyData("USD", "US Dollar")
 | 
				
			||||||
"""The first test currency."""
 | 
					"""The US dollars."""
 | 
				
			||||||
zzb: CurrencyData = CurrencyData("ZZB", "Testing Dollar #B")
 | 
					EUR: CurrencyData = CurrencyData("EUR", "Euro")
 | 
				
			||||||
"""The second test currency."""
 | 
					"""The European dollars."""
 | 
				
			||||||
zzc: CurrencyData = CurrencyData("ZZC", "Testing Dollar #C")
 | 
					TWD: CurrencyData = CurrencyData("TWD", "Taiwan dollars")
 | 
				
			||||||
"""The third test currency."""
 | 
					"""The Taiwan dollars."""
 | 
				
			||||||
zzd: CurrencyData = CurrencyData("ZZD", "Testing Dollar #D")
 | 
					JPY: CurrencyData = CurrencyData("JPY", "Japanese yen")
 | 
				
			||||||
"""The fourth test currency."""
 | 
					"""The Japanese yen."""
 | 
				
			||||||
PREFIX: str = "/accounting/currencies"
 | 
					PREFIX: str = "/accounting/currencies"
 | 
				
			||||||
"""The URL prefix for the currency management."""
 | 
					"""The URL prefix for the currency management."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -140,17 +141,17 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(f"{PREFIX}/store",
 | 
					        response = self.client.post(f"{PREFIX}/store",
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": zza.code,
 | 
					                                          "code": USD.code,
 | 
				
			||||||
                                          "name": zza.name})
 | 
					                                          "name": USD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], f"{PREFIX}/{zza.code}")
 | 
					        self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(f"{PREFIX}/store",
 | 
					        response = self.client.post(f"{PREFIX}/store",
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": zzb.code,
 | 
					                                          "code": EUR.code,
 | 
				
			||||||
                                          "name": zzb.name})
 | 
					                                          "name": EUR.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], f"{PREFIX}/{zzb.code}")
 | 
					        self.assertEqual(response.headers["Location"], f"{PREFIX}/{EUR.code}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_nobody(self) -> None:
 | 
					    def test_nobody(self) -> None:
 | 
				
			||||||
        """Test the permission as nobody.
 | 
					        """Test the permission as nobody.
 | 
				
			||||||
@@ -163,7 +164,7 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = client.get(PREFIX)
 | 
					        response = client.get(PREFIX)
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/{zza.code}")
 | 
					        response = client.get(f"{PREFIX}/{USD.code}")
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/create")
 | 
					        response = client.get(f"{PREFIX}/create")
 | 
				
			||||||
@@ -171,20 +172,20 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/store",
 | 
					        response = client.post(f"{PREFIX}/store",
 | 
				
			||||||
                               data={"csrf_token": csrf_token,
 | 
					                               data={"csrf_token": csrf_token,
 | 
				
			||||||
                                     "code": zzc.code,
 | 
					                                     "code": TWD.code,
 | 
				
			||||||
                                     "name": zzc.name})
 | 
					                                     "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/{zza.code}/edit")
 | 
					        response = client.get(f"{PREFIX}/{USD.code}/edit")
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/{zza.code}/update",
 | 
					        response = client.post(f"{PREFIX}/{USD.code}/update",
 | 
				
			||||||
                               data={"csrf_token": csrf_token,
 | 
					                               data={"csrf_token": csrf_token,
 | 
				
			||||||
                                     "code": zzd.code,
 | 
					                                     "code": JPY.code,
 | 
				
			||||||
                                     "name": zzd.name})
 | 
					                                     "name": JPY.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/{zzb.code}/delete",
 | 
					        response = client.post(f"{PREFIX}/{EUR.code}/delete",
 | 
				
			||||||
                               data={"csrf_token": csrf_token})
 | 
					                               data={"csrf_token": csrf_token})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -199,7 +200,7 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = client.get(PREFIX)
 | 
					        response = client.get(PREFIX)
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/{zza.code}")
 | 
					        response = client.get(f"{PREFIX}/{USD.code}")
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/create")
 | 
					        response = client.get(f"{PREFIX}/create")
 | 
				
			||||||
@@ -207,20 +208,20 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/store",
 | 
					        response = client.post(f"{PREFIX}/store",
 | 
				
			||||||
                               data={"csrf_token": csrf_token,
 | 
					                               data={"csrf_token": csrf_token,
 | 
				
			||||||
                                     "code": zzc.code,
 | 
					                                     "code": TWD.code,
 | 
				
			||||||
                                     "name": zzc.name})
 | 
					                                     "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.get(f"{PREFIX}/{zza.code}/edit")
 | 
					        response = client.get(f"{PREFIX}/{USD.code}/edit")
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/{zza.code}/update",
 | 
					        response = client.post(f"{PREFIX}/{USD.code}/update",
 | 
				
			||||||
                               data={"csrf_token": csrf_token,
 | 
					                               data={"csrf_token": csrf_token,
 | 
				
			||||||
                                     "code": zzd.code,
 | 
					                                     "code": JPY.code,
 | 
				
			||||||
                                     "name": zzd.name})
 | 
					                                     "name": JPY.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.post(f"{PREFIX}/{zzb.code}/delete",
 | 
					        response = client.post(f"{PREFIX}/{EUR.code}/delete",
 | 
				
			||||||
                               data={"csrf_token": csrf_token})
 | 
					                               data={"csrf_token": csrf_token})
 | 
				
			||||||
        self.assertEqual(response.status_code, 403)
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -234,7 +235,7 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = self.client.get(PREFIX)
 | 
					        response = self.client.get(PREFIX)
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.get(f"{PREFIX}/{zza.code}")
 | 
					        response = self.client.get(f"{PREFIX}/{USD.code}")
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.get(f"{PREFIX}/create")
 | 
					        response = self.client.get(f"{PREFIX}/create")
 | 
				
			||||||
@@ -242,22 +243,22 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(f"{PREFIX}/store",
 | 
					        response = self.client.post(f"{PREFIX}/store",
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": zzc.code,
 | 
					                                          "code": TWD.code,
 | 
				
			||||||
                                          "name": zzc.name})
 | 
					                                          "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], f"{PREFIX}/{zzc.code}")
 | 
					        self.assertEqual(response.headers["Location"], f"{PREFIX}/{TWD.code}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.get(f"{PREFIX}/{zza.code}/edit")
 | 
					        response = self.client.get(f"{PREFIX}/{USD.code}/edit")
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(f"{PREFIX}/{zza.code}/update",
 | 
					        response = self.client.post(f"{PREFIX}/{USD.code}/update",
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": zzd.code,
 | 
					                                          "code": JPY.code,
 | 
				
			||||||
                                          "name": zzd.name})
 | 
					                                          "name": JPY.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], f"{PREFIX}/{zzd.code}")
 | 
					        self.assertEqual(response.headers["Location"], f"{PREFIX}/{JPY.code}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(f"{PREFIX}/{zzb.code}/delete",
 | 
					        response = self.client.post(f"{PREFIX}/{EUR.code}/delete",
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token})
 | 
					                                    data={"csrf_token": self.csrf_token})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], PREFIX)
 | 
					        self.assertEqual(response.headers["Location"], PREFIX)
 | 
				
			||||||
@@ -270,31 +271,31 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        from accounting.models import Currency
 | 
					        from accounting.models import Currency
 | 
				
			||||||
        create_uri: str = f"{PREFIX}/create"
 | 
					        create_uri: str = f"{PREFIX}/create"
 | 
				
			||||||
        store_uri: str = f"{PREFIX}/store"
 | 
					        store_uri: str = f"{PREFIX}/store"
 | 
				
			||||||
        detail_uri: str = f"{PREFIX}/{zzc.code}"
 | 
					        detail_uri: str = f"{PREFIX}/{TWD.code}"
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            self.assertEqual({x.code for x in Currency.query.all()},
 | 
					            self.assertEqual({x.code for x in Currency.query.all()},
 | 
				
			||||||
                             {zza.code, zzb.code})
 | 
					                             {USD.code, EUR.code})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Missing CSRF token
 | 
					        # Missing CSRF token
 | 
				
			||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"code": zzc.code,
 | 
					                                    data={"code": TWD.code,
 | 
				
			||||||
                                          "name": zzc.name})
 | 
					                                          "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 400)
 | 
					        self.assertEqual(response.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # CSRF token mismatch
 | 
					        # CSRF token mismatch
 | 
				
			||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": f"{self.csrf_token}-2",
 | 
					                                    data={"csrf_token": f"{self.csrf_token}-2",
 | 
				
			||||||
                                          "code": zzc.code,
 | 
					                                          "code": TWD.code,
 | 
				
			||||||
                                          "name": zzc.name})
 | 
					                                          "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 400)
 | 
					        self.assertEqual(response.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Empty code
 | 
					        # Empty code
 | 
				
			||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": " ",
 | 
					                                          "code": " ",
 | 
				
			||||||
                                          "name": zzc.name})
 | 
					                                          "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -302,7 +303,7 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": " create ",
 | 
					                                          "code": " create ",
 | 
				
			||||||
                                          "name": zzc.name})
 | 
					                                          "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -310,14 +311,14 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": " zzc ",
 | 
					                                          "code": " zzc ",
 | 
				
			||||||
                                          "name": zzc.name})
 | 
					                                          "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Empty name
 | 
					        # Empty name
 | 
				
			||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": zzc.code,
 | 
					                                          "code": TWD.code,
 | 
				
			||||||
                                          "name": " "})
 | 
					                                          "name": " "})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
@@ -325,26 +326,26 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        # Success, with spaces to be stripped
 | 
					        # Success, with spaces to be stripped
 | 
				
			||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": f" {zzc.code} ",
 | 
					                                          "code": f" {TWD.code} ",
 | 
				
			||||||
                                          "name": f" {zzc.name} "})
 | 
					                                          "name": f" {TWD.name} "})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Duplicated code
 | 
					        # Duplicated code
 | 
				
			||||||
        response = self.client.post(store_uri,
 | 
					        response = self.client.post(store_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": zzc.code,
 | 
					                                          "code": TWD.code,
 | 
				
			||||||
                                          "name": zzc.name})
 | 
					                                          "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            self.assertEqual({x.code for x in Currency.query.all()},
 | 
					            self.assertEqual({x.code for x in Currency.query.all()},
 | 
				
			||||||
                             {zza.code, zzb.code, zzc.code})
 | 
					                             {USD.code, EUR.code, TWD.code})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            currency: Currency = db.session.get(Currency, zzc.code)
 | 
					            currency: Currency = db.session.get(Currency, TWD.code)
 | 
				
			||||||
            self.assertEqual(currency.code, zzc.code)
 | 
					            self.assertEqual(currency.code, TWD.code)
 | 
				
			||||||
            self.assertEqual(currency.name_l10n, zzc.name)
 | 
					            self.assertEqual(currency.name_l10n, TWD.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_basic_update(self) -> None:
 | 
					    def test_basic_update(self) -> None:
 | 
				
			||||||
        """Tests the basic rules to update a user.
 | 
					        """Tests the basic rules to update a user.
 | 
				
			||||||
@@ -352,30 +353,30 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Currency
 | 
					        from accounting.models import Currency
 | 
				
			||||||
        detail_uri: str = f"{PREFIX}/{zza.code}"
 | 
					        detail_uri: str = f"{PREFIX}/{USD.code}"
 | 
				
			||||||
        edit_uri: str = f"{PREFIX}/{zza.code}/edit"
 | 
					        edit_uri: str = f"{PREFIX}/{USD.code}/edit"
 | 
				
			||||||
        update_uri: str = f"{PREFIX}/{zza.code}/update"
 | 
					        update_uri: str = f"{PREFIX}/{USD.code}/update"
 | 
				
			||||||
        detail_c_uri: str = f"{PREFIX}/{zzc.code}"
 | 
					        detail_c_uri: str = f"{PREFIX}/{TWD.code}"
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Success, with spaces to be stripped
 | 
					        # Success, with spaces to be stripped
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": f" {zza.code} ",
 | 
					                                          "code": f" {USD.code} ",
 | 
				
			||||||
                                          "name": f" {zza.name}-1 "})
 | 
					                                          "name": f" {USD.name}-1 "})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            currency: Currency = db.session.get(Currency, zza.code)
 | 
					            currency: Currency = db.session.get(Currency, USD.code)
 | 
				
			||||||
            self.assertEqual(currency.code, zza.code)
 | 
					            self.assertEqual(currency.code, USD.code)
 | 
				
			||||||
            self.assertEqual(currency.name_l10n, f"{zza.name}-1")
 | 
					            self.assertEqual(currency.name_l10n, f"{USD.name}-1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Empty code
 | 
					        # Empty code
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": " ",
 | 
					                                          "code": " ",
 | 
				
			||||||
                                          "name": zzc.name})
 | 
					                                          "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -383,7 +384,7 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": " create ",
 | 
					                                          "code": " create ",
 | 
				
			||||||
                                          "name": zzc.name})
 | 
					                                          "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -391,14 +392,14 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": "abc/def",
 | 
					                                          "code": "abc/def",
 | 
				
			||||||
                                          "name": zzc.name})
 | 
					                                          "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Empty name
 | 
					        # Empty name
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": zzc.code,
 | 
					                                          "code": TWD.code,
 | 
				
			||||||
                                          "name": " "})
 | 
					                                          "name": " "})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
@@ -406,16 +407,16 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        # Duplicated code
 | 
					        # Duplicated code
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": zzb.code,
 | 
					                                          "code": EUR.code,
 | 
				
			||||||
                                          "name": zzc.name})
 | 
					                                          "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Change code
 | 
					        # Change code
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": zzc.code,
 | 
					                                          "code": TWD.code,
 | 
				
			||||||
                                          "name": zzc.name})
 | 
					                                          "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_c_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_c_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -431,20 +432,20 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Currency
 | 
					        from accounting.models import Currency
 | 
				
			||||||
        detail_uri: str = f"{PREFIX}/{zza.code}"
 | 
					        detail_uri: str = f"{PREFIX}/{USD.code}"
 | 
				
			||||||
        update_uri: str = f"{PREFIX}/{zza.code}/update"
 | 
					        update_uri: str = f"{PREFIX}/{USD.code}/update"
 | 
				
			||||||
        currency: Currency | None
 | 
					        currency: Currency | None
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": f" {zza.code} ",
 | 
					                                          "code": f" {USD.code} ",
 | 
				
			||||||
                                          "name": f" {zza.name} "})
 | 
					                                          "name": f" {USD.name} "})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            currency = db.session.get(Currency, zza.code)
 | 
					            currency = db.session.get(Currency, USD.code)
 | 
				
			||||||
            self.assertIsNotNone(currency)
 | 
					            self.assertIsNotNone(currency)
 | 
				
			||||||
            currency.created_at \
 | 
					            currency.created_at \
 | 
				
			||||||
                = currency.created_at - timedelta(seconds=5)
 | 
					                = currency.created_at - timedelta(seconds=5)
 | 
				
			||||||
@@ -453,13 +454,13 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": zza.code,
 | 
					                                          "code": USD.code,
 | 
				
			||||||
                                          "name": zzc.name})
 | 
					                                          "name": TWD.name})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            currency = db.session.get(Currency, zza.code)
 | 
					            currency = db.session.get(Currency, USD.code)
 | 
				
			||||||
            self.assertIsNotNone(currency)
 | 
					            self.assertIsNotNone(currency)
 | 
				
			||||||
            self.assertLess(currency.created_at,
 | 
					            self.assertLess(currency.created_at,
 | 
				
			||||||
                            currency.updated_at)
 | 
					                            currency.updated_at)
 | 
				
			||||||
@@ -472,25 +473,25 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        from accounting.models import Currency
 | 
					        from accounting.models import Currency
 | 
				
			||||||
        editor_username, editor2_username = "editor", "editor2"
 | 
					        editor_username, editor2_username = "editor", "editor2"
 | 
				
			||||||
        client, csrf_token = get_client(self.app, editor2_username)
 | 
					        client, csrf_token = get_client(self.app, editor2_username)
 | 
				
			||||||
        detail_uri: str = f"{PREFIX}/{zza.code}"
 | 
					        detail_uri: str = f"{PREFIX}/{USD.code}"
 | 
				
			||||||
        update_uri: str = f"{PREFIX}/{zza.code}/update"
 | 
					        update_uri: str = f"{PREFIX}/{USD.code}/update"
 | 
				
			||||||
        currency: Currency
 | 
					        currency: Currency
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            currency = db.session.get(Currency, zza.code)
 | 
					            currency = db.session.get(Currency, USD.code)
 | 
				
			||||||
            self.assertEqual(currency.created_by.username, editor_username)
 | 
					            self.assertEqual(currency.created_by.username, editor_username)
 | 
				
			||||||
            self.assertEqual(currency.updated_by.username, editor_username)
 | 
					            self.assertEqual(currency.updated_by.username, editor_username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = client.post(update_uri,
 | 
					        response = client.post(update_uri,
 | 
				
			||||||
                               data={"csrf_token": csrf_token,
 | 
					                               data={"csrf_token": csrf_token,
 | 
				
			||||||
                                     "code": zza.code,
 | 
					                                     "code": USD.code,
 | 
				
			||||||
                                     "name": f"{zza.name}-2"})
 | 
					                                     "name": f"{USD.name}-2"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            currency = db.session.get(Currency, zza.code)
 | 
					            currency = db.session.get(Currency, USD.code)
 | 
				
			||||||
            self.assertEqual(currency.created_by.username, editor_username)
 | 
					            self.assertEqual(currency.created_by.username, editor_username)
 | 
				
			||||||
            self.assertEqual(currency.updated_by.username, editor2_username)
 | 
					            self.assertEqual(currency.updated_by.username, editor2_username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -502,14 +503,14 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.get(
 | 
					        response = self.client.get(
 | 
				
			||||||
            f"/accounting/api/currencies/exists-code?q={zza.code}")
 | 
					            f"/accounting/api/currencies/exists-code?q={USD.code}")
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
        data = response.json()
 | 
					        data = response.json()
 | 
				
			||||||
        self.assertEqual(set(data.keys()), {"exists"})
 | 
					        self.assertEqual(set(data.keys()), {"exists"})
 | 
				
			||||||
        self.assertTrue(data["exists"])
 | 
					        self.assertTrue(data["exists"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.get(
 | 
					        response = self.client.get(
 | 
				
			||||||
            f"/accounting/api/currencies/exists-code?q={zza.code}-1")
 | 
					            f"/accounting/api/currencies/exists-code?q={USD.code}-1")
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
        data = response.json()
 | 
					        data = response.json()
 | 
				
			||||||
        self.assertEqual(set(data.keys()), {"exists"})
 | 
					        self.assertEqual(set(data.keys()), {"exists"})
 | 
				
			||||||
@@ -521,60 +522,60 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        :return: None
 | 
					        :return: None
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Currency
 | 
					        from accounting.models import Currency
 | 
				
			||||||
        detail_uri: str = f"{PREFIX}/{zza.code}"
 | 
					        detail_uri: str = f"{PREFIX}/{USD.code}"
 | 
				
			||||||
        update_uri: str = f"{PREFIX}/{zza.code}/update"
 | 
					        update_uri: str = f"{PREFIX}/{USD.code}/update"
 | 
				
			||||||
        currency: Currency
 | 
					        currency: Currency
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            currency = db.session.get(Currency, zza.code)
 | 
					            currency = db.session.get(Currency, USD.code)
 | 
				
			||||||
            self.assertEqual(currency.name_l10n, zza.name)
 | 
					            self.assertEqual(currency.name_l10n, USD.name)
 | 
				
			||||||
            self.assertEqual(currency.l10n, [])
 | 
					            self.assertEqual(currency.l10n, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        set_locale(self.client, self.csrf_token, "zh_Hant")
 | 
					        set_locale(self.client, self.csrf_token, "zh_Hant")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": zza.code,
 | 
					                                          "code": USD.code,
 | 
				
			||||||
                                          "name": f"{zza.name}-zh_Hant"})
 | 
					                                          "name": f"{USD.name}-zh_Hant"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            currency = db.session.get(Currency, zza.code)
 | 
					            currency = db.session.get(Currency, USD.code)
 | 
				
			||||||
            self.assertEqual(currency.name_l10n, zza.name)
 | 
					            self.assertEqual(currency.name_l10n, USD.name)
 | 
				
			||||||
            self.assertEqual({(x.locale, x.name) for x in currency.l10n},
 | 
					            self.assertEqual({(x.locale, x.name) for x in currency.l10n},
 | 
				
			||||||
                             {("zh_Hant", f"{zza.name}-zh_Hant")})
 | 
					                             {("zh_Hant", f"{USD.name}-zh_Hant")})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        set_locale(self.client, self.csrf_token, "en")
 | 
					        set_locale(self.client, self.csrf_token, "en")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": zza.code,
 | 
					                                          "code": USD.code,
 | 
				
			||||||
                                          "name": f"{zza.name}-2"})
 | 
					                                          "name": f"{USD.name}-2"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            currency = db.session.get(Currency, zza.code)
 | 
					            currency = db.session.get(Currency, USD.code)
 | 
				
			||||||
            self.assertEqual(currency.name_l10n, f"{zza.name}-2")
 | 
					            self.assertEqual(currency.name_l10n, f"{USD.name}-2")
 | 
				
			||||||
            self.assertEqual({(x.locale, x.name) for x in currency.l10n},
 | 
					            self.assertEqual({(x.locale, x.name) for x in currency.l10n},
 | 
				
			||||||
                             {("zh_Hant", f"{zza.name}-zh_Hant")})
 | 
					                             {("zh_Hant", f"{USD.name}-zh_Hant")})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        set_locale(self.client, self.csrf_token, "zh_Hant")
 | 
					        set_locale(self.client, self.csrf_token, "zh_Hant")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(update_uri,
 | 
					        response = self.client.post(update_uri,
 | 
				
			||||||
                                    data={"csrf_token": self.csrf_token,
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
                                          "code": zza.code,
 | 
					                                          "code": USD.code,
 | 
				
			||||||
                                          "name": f"{zza.name}-zh_Hant-2"})
 | 
					                                          "name": f"{USD.name}-zh_Hant-2"})
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], detail_uri)
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            currency = db.session.get(Currency, zza.code)
 | 
					            currency = db.session.get(Currency, USD.code)
 | 
				
			||||||
            self.assertEqual(currency.name_l10n, f"{zza.name}-2")
 | 
					            self.assertEqual(currency.name_l10n, f"{USD.name}-2")
 | 
				
			||||||
            self.assertEqual({(x.locale, x.name) for x in currency.l10n},
 | 
					            self.assertEqual({(x.locale, x.name) for x in currency.l10n},
 | 
				
			||||||
                             {("zh_Hant", f"{zza.name}-zh_Hant-2")})
 | 
					                             {("zh_Hant", f"{USD.name}-zh_Hant-2")})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_delete(self) -> None:
 | 
					    def test_delete(self) -> None:
 | 
				
			||||||
        """Tests to delete a currency.
 | 
					        """Tests to delete a currency.
 | 
				
			||||||
@@ -582,15 +583,58 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Currency
 | 
					        from accounting.models import Currency
 | 
				
			||||||
        detail_uri: str = f"{PREFIX}/{zza.code}"
 | 
					        detail_uri: str = f"{PREFIX}/{JPY.code}"
 | 
				
			||||||
        delete_uri: str = f"{PREFIX}/{zza.code}/delete"
 | 
					        delete_uri: str = f"{PREFIX}/{JPY.code}/delete"
 | 
				
			||||||
        list_uri: str = PREFIX
 | 
					        list_uri: str = PREFIX
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        runner: FlaskCliRunner = self.app.test_cli_runner()
 | 
				
			||||||
 | 
					        with self.app.app_context():
 | 
				
			||||||
 | 
					            from accounting.models import BaseAccount
 | 
				
			||||||
 | 
					            if BaseAccount.query.first() is None:
 | 
				
			||||||
 | 
					                result = runner.invoke(args="accounting-init-base")
 | 
				
			||||||
 | 
					                self.assertEqual(result.exit_code, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.client.post("/accounting/accounts/store",
 | 
				
			||||||
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
 | 
					                                          "base_code": "1111",
 | 
				
			||||||
 | 
					                                          "title": "Cash"})
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
 | 
					        self.assertEqual(response.headers["Location"],
 | 
				
			||||||
 | 
					                         "/accounting/accounts/1111-001")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.client.post(f"{PREFIX}/store",
 | 
				
			||||||
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
 | 
					                                          "code": JPY.code,
 | 
				
			||||||
 | 
					                                          "name": JPY.name})
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
 | 
					        self.assertEqual(response.headers["Location"], detail_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        add_journal_entry(self.client,
 | 
				
			||||||
 | 
					                          form={"csrf_token": self.csrf_token,
 | 
				
			||||||
 | 
					                                "next": NEXT_URI,
 | 
				
			||||||
 | 
					                                "date": date.today().isoformat(),
 | 
				
			||||||
 | 
					                                "currency-1-code": EUR.code,
 | 
				
			||||||
 | 
					                                "currency-1-credit-1-account_code": "1111-001",
 | 
				
			||||||
 | 
					                                "currency-1-credit-1-amount": "20"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            self.assertEqual({x.code for x in Currency.query.all()},
 | 
					            self.assertEqual({x.code for x in Currency.query.all()},
 | 
				
			||||||
                             {zza.code, zzb.code})
 | 
					                             {USD.code, EUR.code, JPY.code})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Cannot delete the default currency
 | 
				
			||||||
 | 
					        response = self.client.post(f"{PREFIX}/{USD.code}/delete",
 | 
				
			||||||
 | 
					                                    data={"csrf_token": self.csrf_token})
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
 | 
					        self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Cannot delete the account that is in use
 | 
				
			||||||
 | 
					        response = self.client.post(f"{PREFIX}/{EUR.code}/delete",
 | 
				
			||||||
 | 
					                                    data={"csrf_token": self.csrf_token})
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
 | 
					        self.assertEqual(response.headers["Location"], f"{PREFIX}/{EUR.code}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Success
 | 
				
			||||||
        response = self.client.get(detail_uri)
 | 
					        response = self.client.get(detail_uri)
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
        response = self.client.post(delete_uri,
 | 
					        response = self.client.post(delete_uri,
 | 
				
			||||||
@@ -600,7 +644,7 @@ class CurrencyTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            self.assertEqual({x.code for x in Currency.query.all()},
 | 
					            self.assertEqual({x.code for x in Currency.query.all()},
 | 
				
			||||||
                             {zzb.code})
 | 
					                             {USD.code, EUR.code})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.get(detail_uri)
 | 
					        response = self.client.get(detail_uri)
 | 
				
			||||||
        self.assertEqual(response.status_code, 404)
 | 
					        self.assertEqual(response.status_code, 404)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -569,12 +569,43 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        journal_entry_id: int \
 | 
					        from accounting.models import JournalEntry, JournalEntryLineItem
 | 
				
			||||||
 | 
					        journal_entry_id_1: int \
 | 
				
			||||||
            = add_journal_entry(self.client, self.__get_add_form())
 | 
					            = add_journal_entry(self.client, self.__get_add_form())
 | 
				
			||||||
        detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next"
 | 
					        detail_uri: str = f"{PREFIX}/{journal_entry_id_1}?next=%2F_next"
 | 
				
			||||||
        delete_uri: str = f"{PREFIX}/{journal_entry_id}/delete"
 | 
					        delete_uri: str = f"{PREFIX}/{journal_entry_id_1}/delete"
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        form: dict[str, str] = self.__get_add_form()
 | 
				
			||||||
 | 
					        key: str = [x for x in form if x.endswith("-account_code")][0]
 | 
				
			||||||
 | 
					        form[key] = Accounts.PAYABLE
 | 
				
			||||||
 | 
					        journal_entry_id_2: int = add_journal_entry(self.client, form)
 | 
				
			||||||
 | 
					        with self.app.app_context():
 | 
				
			||||||
 | 
					            journal_entry: JournalEntry | None \
 | 
				
			||||||
 | 
					                = db.session.get(JournalEntry, journal_entry_id_2)
 | 
				
			||||||
 | 
					            self.assertIsNotNone(journal_entry)
 | 
				
			||||||
 | 
					            line_item: JournalEntryLineItem \
 | 
				
			||||||
 | 
					                = [x for x in journal_entry.line_items
 | 
				
			||||||
 | 
					                   if x.account_code == Accounts.PAYABLE][0]
 | 
				
			||||||
 | 
					        add_journal_entry(
 | 
				
			||||||
 | 
					            self.client,
 | 
				
			||||||
 | 
					            form={"csrf_token": self.csrf_token,
 | 
				
			||||||
 | 
					                  "next": NEXT_URI,
 | 
				
			||||||
 | 
					                  "date": date.today().isoformat(),
 | 
				
			||||||
 | 
					                  "currency-1-code": line_item.currency_code,
 | 
				
			||||||
 | 
					                  "currency-1-debit-1-original_line_item_id": line_item.id,
 | 
				
			||||||
 | 
					                  "currency-1-debit-1-account_code": line_item.account_code,
 | 
				
			||||||
 | 
					                  "currency-1-debit-1-amount": "1"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Cannot delete the journal entry that is in use
 | 
				
			||||||
 | 
					        response = self.client.post(f"{PREFIX}/{journal_entry_id_2}/delete",
 | 
				
			||||||
 | 
					                                    data={"csrf_token": self.csrf_token,
 | 
				
			||||||
 | 
					                                          "next": NEXT_URI})
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
 | 
					        self.assertEqual(response.headers["Location"],
 | 
				
			||||||
 | 
					                         f"{PREFIX}/{journal_entry_id_2}?next=%2F_next")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Success
 | 
				
			||||||
        response = self.client.get(detail_uri)
 | 
					        response = self.client.get(detail_uri)
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
        response = self.client.post(delete_uri,
 | 
					        response = self.client.post(delete_uri,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -50,6 +50,18 @@ def create_app(is_testing: bool = False) -> Flask:
 | 
				
			|||||||
        "SQLALCHEMY_DATABASE_URI": db_uri,
 | 
					        "SQLALCHEMY_DATABASE_URI": db_uri,
 | 
				
			||||||
        "BABEL_DEFAULT_LOCALE": "en",
 | 
					        "BABEL_DEFAULT_LOCALE": "en",
 | 
				
			||||||
        "ALL_LINGUAS": "zh_Hant|正體中文,en|English,zh_Hans|简体中文",
 | 
					        "ALL_LINGUAS": "zh_Hant|正體中文,en|English,zh_Hans|简体中文",
 | 
				
			||||||
 | 
					        "ACCOUNTING_DEFAULT_CURRENCY": "USD",
 | 
				
			||||||
 | 
					        "ACCOUNTING_DEFAULT_IE_ACCOUNT": "1111-001",
 | 
				
			||||||
 | 
					        "ACCOUNTING_RECURRING": (
 | 
				
			||||||
 | 
					            "debit|1314-001|Pension|Pension for {last_month_name},"
 | 
				
			||||||
 | 
					            "debit|6262-001|Health insurance"
 | 
				
			||||||
 | 
					            "|Health insurance for {last_month_name},"
 | 
				
			||||||
 | 
					            "debit|6261-001|Electricity bill"
 | 
				
			||||||
 | 
					            "|Electricity bill for {last_bimonthly_name},"
 | 
				
			||||||
 | 
					            "debit|6261-001|Water bill|Water bill for {last_bimonthly_name},"
 | 
				
			||||||
 | 
					            "debit|6261-001|Gas bill|Gas bill for {last_bimonthly_name},"
 | 
				
			||||||
 | 
					            "debit|6261-001|Phone bill|Phone bill for {last_month_name},"
 | 
				
			||||||
 | 
					            "credit|4611-001|Payroll|Payroll for {last_month_name}"),
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    if is_testing:
 | 
					    if is_testing:
 | 
				
			||||||
        app.config["TESTING"] = True
 | 
					        app.config["TESTING"] = True
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,7 +27,7 @@ First written: 2023/1/27
 | 
				
			|||||||
  <meta name="author" content="{{ "imacat" }}" />
 | 
					  <meta name="author" content="{{ "imacat" }}" />
 | 
				
			||||||
  <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" crossorigin="anonymous">
 | 
					  <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" crossorigin="anonymous">
 | 
				
			||||||
  <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.1/css/all.min.css" crossorigin="anonymous">
 | 
					  <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.1/css/all.min.css" crossorigin="anonymous">
 | 
				
			||||||
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.2.10/dist/css/tempus-dominus.min.css" crossorigin="anonymous">
 | 
					  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.4.3/dist/css/tempus-dominus.min.css" crossorigin="anonymous">
 | 
				
			||||||
  {% block styles %}{% endblock %}
 | 
					  {% block styles %}{% endblock %}
 | 
				
			||||||
  <script src="{{ url_for("babel_catalog") }}"></script>
 | 
					  <script src="{{ url_for("babel_catalog") }}"></script>
 | 
				
			||||||
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
 | 
					  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user