Compare commits
82 Commits
Author | SHA1 | Date | |
---|---|---|---|
52807c5322 | |||
231a71feea | |||
4902eecae0 | |||
889e4c058e | |||
7262a6cb42 | |||
c4ff4ecb3d | |||
2859f628ea | |||
e0355b2af1 | |||
b4d390c33a | |||
a4ab8a761c | |||
907ce6d06e | |||
7e1388735e | |||
6f773dd837 | |||
87fa5aa6bc | |||
35e05b3708 | |||
7ccc96bda0 | |||
283758ebe9 | |||
b673c7aeaf | |||
0ad2ac53dd | |||
7e90ec5a8f | |||
7755365467 | |||
979eea606a | |||
5a9e08f2c4 | |||
68c810d492 | |||
5f88260507 | |||
779d89f8c4 | |||
5d4bf4361b | |||
10170d613d | |||
c885c08c37 | |||
e2a4340f2a | |||
9728ff30e0 | |||
a4644ede5f | |||
8f477dd6f1 | |||
44ac53f15c | |||
5edb5465c5 | |||
067afdb165 | |||
37a4c26f86 | |||
89e43830b4 | |||
671dbfb692 | |||
2014344d25 | |||
f9c39709c8 | |||
b394c58ec6 | |||
0af3e2785b | |||
7066f75e72 | |||
619540da49 | |||
567004f7d9 | |||
761d5a5824 | |||
fa3cdace7f | |||
656762850c | |||
e2325f08d0 | |||
855356084e | |||
7aaeb32a3d | |||
b376cf1580 | |||
ccbdc779ac | |||
61ee08fda2 | |||
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 |
29
docs/source/accounting.option.rst
Normal file
29
docs/source/accounting.option.rst
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
accounting.option package
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
accounting.option.forms module
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.option.forms
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.option.views module
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.option.views
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.option
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
@ -28,14 +28,6 @@ accounting.report.utils.csv\_export module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.report.utils.ie\_account module
|
|
||||||
------------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: accounting.report.utils.ie_account
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
accounting.report.utils.option\_link module
|
accounting.report.utils.option\_link module
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
|
|
||||||
|
@ -11,12 +11,21 @@ Subpackages
|
|||||||
accounting.base_account
|
accounting.base_account
|
||||||
accounting.currency
|
accounting.currency
|
||||||
accounting.journal_entry
|
accounting.journal_entry
|
||||||
|
accounting.option
|
||||||
accounting.report
|
accounting.report
|
||||||
accounting.utils
|
accounting.utils
|
||||||
|
|
||||||
Submodules
|
Submodules
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
accounting.forms module
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.forms
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.locale module
|
accounting.locale module
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
|
@ -12,6 +12,14 @@ accounting.utils.cast module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.utils.current\_account module
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.utils.current_account
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.utils.flash\_errors module
|
accounting.utils.flash\_errors module
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
@ -36,6 +44,14 @@ accounting.utils.next\_uri module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.utils.options module
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.utils.options
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.utils.pagination module
|
accounting.utils.pagination module
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
|
@ -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.9.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.9.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.
|
||||||
|
@ -86,4 +86,7 @@ def init_app(app: Flask, user_utils: UserUtilityInterface,
|
|||||||
from . import report
|
from . import report
|
||||||
report.init_app(app, bp)
|
report.init_app(app, bp)
|
||||||
|
|
||||||
|
from . import option
|
||||||
|
option.init_app(bp)
|
||||||
|
|
||||||
app.register_blueprint(bp)
|
app.register_blueprint(bp)
|
||||||
|
@ -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")
|
||||||
|
96
src/accounting/forms.py
Normal file
96
src/accounting/forms.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# The Mia! Accounting Flask Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The forms.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
from flask_babel import LazyString
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, ValidationError
|
||||||
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
|
from accounting import db
|
||||||
|
from accounting.locale import lazy_gettext
|
||||||
|
from accounting.models import Currency, Account
|
||||||
|
|
||||||
|
|
||||||
|
ACCOUNT_REQUIRED: DataRequired = DataRequired(
|
||||||
|
lazy_gettext("Please select the account."))
|
||||||
|
"""The validator to check if the account code is empty."""
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencyExists:
|
||||||
|
"""The validator to check if the account exists."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
|
if field.data is None:
|
||||||
|
return
|
||||||
|
if db.session.get(Currency, field.data) is None:
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"The currency does not exist."))
|
||||||
|
|
||||||
|
|
||||||
|
class AccountExists:
|
||||||
|
"""The validator to check if the account exists."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
|
if field.data is None:
|
||||||
|
return
|
||||||
|
if Account.find_by_code(field.data) is None:
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"The account does not exist."))
|
||||||
|
|
||||||
|
|
||||||
|
class IsDebitAccount:
|
||||||
|
"""The validator to check if the account is for debit line items."""
|
||||||
|
|
||||||
|
def __init__(self, message: str | LazyString):
|
||||||
|
"""Constructs the validator.
|
||||||
|
|
||||||
|
:param message: The error message.
|
||||||
|
"""
|
||||||
|
self.__message: str | LazyString = message
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
|
if field.data is None:
|
||||||
|
return
|
||||||
|
if re.match(r"^(?:[1235689]|7[5678])", field.data) \
|
||||||
|
and not field.data.startswith("3351-") \
|
||||||
|
and not field.data.startswith("3353-"):
|
||||||
|
return
|
||||||
|
raise ValidationError(self.__message)
|
||||||
|
|
||||||
|
|
||||||
|
class IsCreditAccount:
|
||||||
|
"""The validator to check if the account is for credit line items."""
|
||||||
|
|
||||||
|
def __init__(self, message: str | LazyString):
|
||||||
|
"""Constructs the validator.
|
||||||
|
|
||||||
|
:param message: The error message.
|
||||||
|
"""
|
||||||
|
self.__message: str | LazyString = message
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
|
if field.data is None:
|
||||||
|
return
|
||||||
|
if re.match(r"^(?:[123489]|7[1234])", field.data) \
|
||||||
|
and not field.data.startswith("3351-") \
|
||||||
|
and not field.data.startswith("3353-"):
|
||||||
|
return
|
||||||
|
raise ValidationError(self.__message)
|
@ -27,29 +27,20 @@ from wtforms import StringField, ValidationError, FieldList, IntegerField, \
|
|||||||
from wtforms.validators import DataRequired
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.locale import lazy_gettext
|
from accounting.forms import CurrencyExists
|
||||||
from accounting.models import Currency, JournalEntryLineItem
|
|
||||||
from accounting.journal_entry.utils.offset_alias import offset_alias
|
from accounting.journal_entry.utils.offset_alias import offset_alias
|
||||||
|
from accounting.locale import lazy_gettext
|
||||||
|
from accounting.models import JournalEntryLineItem
|
||||||
from accounting.utils.cast import be
|
from accounting.utils.cast import be
|
||||||
from accounting.utils.strip_text import strip_text
|
from accounting.utils.strip_text import strip_text
|
||||||
from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
|
from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
|
||||||
|
|
||||||
|
|
||||||
CURRENCY_REQUIRED: DataRequired = DataRequired(
|
CURRENCY_REQUIRED: DataRequired = DataRequired(
|
||||||
lazy_gettext("Please select the currency."))
|
lazy_gettext("Please select the currency."))
|
||||||
"""The validator to check if the currency code is empty."""
|
"""The validator to check if the currency code is empty."""
|
||||||
|
|
||||||
|
|
||||||
class CurrencyExists:
|
|
||||||
"""The validator to check if the account exists."""
|
|
||||||
|
|
||||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
|
||||||
if field.data is None:
|
|
||||||
return
|
|
||||||
if db.session.get(Currency, field.data) is None:
|
|
||||||
raise ValidationError(lazy_gettext(
|
|
||||||
"The currency does not exist."))
|
|
||||||
|
|
||||||
|
|
||||||
class SameCurrencyAsOriginalLineItems:
|
class SameCurrencyAsOriginalLineItems:
|
||||||
"""The validator to check if the currency is the same as the
|
"""The validator to check if the currency is the same as the
|
||||||
original line items."""
|
original line items."""
|
||||||
|
@ -220,7 +220,7 @@ class JournalEntryForm(FlaskForm):
|
|||||||
:return: The selectable debit accounts.
|
:return: The selectable debit accounts.
|
||||||
"""
|
"""
|
||||||
accounts: list[AccountOption] \
|
accounts: list[AccountOption] \
|
||||||
= [AccountOption(x) for x in Account.debit()
|
= [AccountOption(x) for x in Account.selectable_debit()
|
||||||
if not (x.code[0] == "2" and x.is_need_offset)]
|
if not (x.code[0] == "2" and x.is_need_offset)]
|
||||||
in_use: set[int] = set(db.session.scalars(
|
in_use: set[int] = set(db.session.scalars(
|
||||||
sa.select(JournalEntryLineItem.account_id)
|
sa.select(JournalEntryLineItem.account_id)
|
||||||
@ -237,7 +237,7 @@ class JournalEntryForm(FlaskForm):
|
|||||||
:return: The selectable credit accounts.
|
:return: The selectable credit accounts.
|
||||||
"""
|
"""
|
||||||
accounts: list[AccountOption] \
|
accounts: list[AccountOption] \
|
||||||
= [AccountOption(x) for x in Account.credit()
|
= [AccountOption(x) for x in Account.selectable_credit()
|
||||||
if not (x.code[0] == "1" and x.is_need_offset)]
|
if not (x.code[0] == "1" and x.is_need_offset)]
|
||||||
in_use: set[int] = set(db.session.scalars(
|
in_use: set[int] = set(db.session.scalars(
|
||||||
sa.select(JournalEntryLineItem.account_id)
|
sa.select(JournalEntryLineItem.account_id)
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
"""The line item sub-forms for the journal entry management.
|
"""The line item sub-forms for the journal entry management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import re
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
@ -26,9 +25,11 @@ from flask_babel import LazyString
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from wtforms import StringField, ValidationError, DecimalField, IntegerField
|
from wtforms import StringField, ValidationError, DecimalField, IntegerField
|
||||||
from wtforms.validators import DataRequired, Optional
|
from wtforms.validators import Optional
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
|
from accounting.forms import ACCOUNT_REQUIRED, AccountExists, IsDebitAccount, \
|
||||||
|
IsCreditAccount
|
||||||
from accounting.locale import lazy_gettext
|
from accounting.locale import lazy_gettext
|
||||||
from accounting.models import Account, JournalEntryLineItem
|
from accounting.models import Account, JournalEntryLineItem
|
||||||
from accounting.template_filters import format_amount
|
from accounting.template_filters import format_amount
|
||||||
@ -37,10 +38,6 @@ from accounting.utils.random_id import new_id
|
|||||||
from accounting.utils.strip_text import strip_text
|
from accounting.utils.strip_text import strip_text
|
||||||
from accounting.utils.user import get_current_user_pk
|
from accounting.utils.user import get_current_user_pk
|
||||||
|
|
||||||
ACCOUNT_REQUIRED: DataRequired = DataRequired(
|
|
||||||
lazy_gettext("Please select the account."))
|
|
||||||
"""The validator to check if the account code is empty."""
|
|
||||||
|
|
||||||
|
|
||||||
class OriginalLineItemExists:
|
class OriginalLineItemExists:
|
||||||
"""The validator to check if the original line item exists."""
|
"""The validator to check if the original line item exists."""
|
||||||
@ -105,45 +102,6 @@ class OriginalLineItemNotOffset:
|
|||||||
"The original line item cannot be an offset item."))
|
"The original line item cannot be an offset item."))
|
||||||
|
|
||||||
|
|
||||||
class AccountExists:
|
|
||||||
"""The validator to check if the account exists."""
|
|
||||||
|
|
||||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
|
||||||
if field.data is None:
|
|
||||||
return
|
|
||||||
if Account.find_by_code(field.data) is None:
|
|
||||||
raise ValidationError(lazy_gettext(
|
|
||||||
"The account does not exist."))
|
|
||||||
|
|
||||||
|
|
||||||
class IsDebitAccount:
|
|
||||||
"""The validator to check if the account is for debit line items."""
|
|
||||||
|
|
||||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
|
||||||
if field.data is None:
|
|
||||||
return
|
|
||||||
if re.match(r"^(?:[1235689]|7[5678])", field.data) \
|
|
||||||
and not field.data.startswith("3351-") \
|
|
||||||
and not field.data.startswith("3353-"):
|
|
||||||
return
|
|
||||||
raise ValidationError(lazy_gettext(
|
|
||||||
"This account is not for debit line items."))
|
|
||||||
|
|
||||||
|
|
||||||
class IsCreditAccount:
|
|
||||||
"""The validator to check if the account is for credit line items."""
|
|
||||||
|
|
||||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
|
||||||
if field.data is None:
|
|
||||||
return
|
|
||||||
if re.match(r"^(?:[123489]|7[1234])", field.data) \
|
|
||||||
and not field.data.startswith("3351-") \
|
|
||||||
and not field.data.startswith("3353-"):
|
|
||||||
return
|
|
||||||
raise ValidationError(lazy_gettext(
|
|
||||||
"This account is not for credit line items."))
|
|
||||||
|
|
||||||
|
|
||||||
class SameAccountAsOriginalLineItem:
|
class SameAccountAsOriginalLineItem:
|
||||||
"""The validator to check if the account is the same as the
|
"""The validator to check if the account is the same as the
|
||||||
original line item."""
|
original line item."""
|
||||||
@ -452,12 +410,14 @@ class DebitLineItemForm(LineItemForm):
|
|||||||
"""The ID of the original line item."""
|
"""The ID of the original line item."""
|
||||||
account_code = StringField(
|
account_code = StringField(
|
||||||
filters=[strip_text],
|
filters=[strip_text],
|
||||||
validators=[ACCOUNT_REQUIRED,
|
validators=[
|
||||||
AccountExists(),
|
ACCOUNT_REQUIRED,
|
||||||
IsDebitAccount(),
|
AccountExists(),
|
||||||
SameAccountAsOriginalLineItem(),
|
IsDebitAccount(lazy_gettext(
|
||||||
KeepAccountWhenHavingOffset(),
|
"This account is not for debit line items.")),
|
||||||
NotStartPayableFromDebit()])
|
SameAccountAsOriginalLineItem(),
|
||||||
|
KeepAccountWhenHavingOffset(),
|
||||||
|
NotStartPayableFromDebit()])
|
||||||
"""The account code."""
|
"""The account code."""
|
||||||
description = StringField(filters=[strip_text])
|
description = StringField(filters=[strip_text])
|
||||||
"""The description."""
|
"""The description."""
|
||||||
@ -502,12 +462,14 @@ class CreditLineItemForm(LineItemForm):
|
|||||||
"""The ID of the original line item."""
|
"""The ID of the original line item."""
|
||||||
account_code = StringField(
|
account_code = StringField(
|
||||||
filters=[strip_text],
|
filters=[strip_text],
|
||||||
validators=[ACCOUNT_REQUIRED,
|
validators=[
|
||||||
AccountExists(),
|
ACCOUNT_REQUIRED,
|
||||||
IsCreditAccount(),
|
AccountExists(),
|
||||||
SameAccountAsOriginalLineItem(),
|
IsCreditAccount(lazy_gettext(
|
||||||
KeepAccountWhenHavingOffset(),
|
"This account is not for credit line items.")),
|
||||||
NotStartReceivableFromCredit()])
|
SameAccountAsOriginalLineItem(),
|
||||||
|
KeepAccountWhenHavingOffset(),
|
||||||
|
NotStartReceivableFromCredit()])
|
||||||
"""The account code."""
|
"""The account code."""
|
||||||
description = StringField(filters=[strip_text])
|
description = StringField(filters=[strip_text])
|
||||||
"""The description."""
|
"""The description."""
|
||||||
|
@ -17,12 +17,14 @@
|
|||||||
"""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 accounting import db
|
from accounting import db
|
||||||
from accounting.models import Account, JournalEntryLineItem
|
from accounting.models import Account, JournalEntryLineItem
|
||||||
|
from accounting.utils.options import options, Recurring
|
||||||
|
|
||||||
|
|
||||||
class DescriptionAccount:
|
class DescriptionAccount:
|
||||||
@ -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, account: Account, description_template: str):
|
||||||
|
"""Constructs a recurring transaction.
|
||||||
|
|
||||||
|
:param name: The name.
|
||||||
|
:param description_template: The description template.
|
||||||
|
:param account: The account.
|
||||||
|
"""
|
||||||
|
self.name: str = name
|
||||||
|
self.account: DescriptionAccount = DescriptionAccount(account, 0)
|
||||||
|
self.description_template: str = description_template
|
||||||
|
|
||||||
|
@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,50 @@ 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.
|
||||||
|
"""
|
||||||
|
recurring: Recurring = options.recurring
|
||||||
|
accounts: dict[str, Account] \
|
||||||
|
= self.__get_accounts(recurring.codes)
|
||||||
|
self.debit.recurring \
|
||||||
|
= [DescriptionRecurring(x.name, accounts[x.account_code],
|
||||||
|
x.description_template)
|
||||||
|
for x in recurring.expenses]
|
||||||
|
self.credit.recurring \
|
||||||
|
= [DescriptionRecurring(x.name, accounts[x.account_code],
|
||||||
|
x.description_template)
|
||||||
|
for x in recurring.incomes]
|
||||||
|
|
||||||
|
@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.
|
||||||
|
"""
|
||||||
|
if len(codes) == 0:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
@ -259,13 +269,15 @@ class Account(db.Model):
|
|||||||
cls.no == int(m.group(2))).first()
|
cls.no == int(m.group(2))).first()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def debit(cls) -> list[t.Self]:
|
def selectable_debit(cls) -> list[t.Self]:
|
||||||
"""Returns the debit accounts.
|
"""Returns the selectable debit accounts.
|
||||||
|
Payable line items can not start from debit.
|
||||||
|
|
||||||
:return: The debit accounts.
|
:return: The selectable debit accounts.
|
||||||
"""
|
"""
|
||||||
return cls.query.filter(sa.or_(cls.base_code.startswith("1"),
|
return cls.query.filter(sa.or_(cls.base_code.startswith("1"),
|
||||||
cls.base_code.startswith("2"),
|
sa.and_(cls.base_code.startswith("2"),
|
||||||
|
sa.not_(cls.is_need_offset)),
|
||||||
cls.base_code.startswith("3"),
|
cls.base_code.startswith("3"),
|
||||||
cls.base_code.startswith("5"),
|
cls.base_code.startswith("5"),
|
||||||
cls.base_code.startswith("6"),
|
cls.base_code.startswith("6"),
|
||||||
@ -280,12 +292,14 @@ class Account(db.Model):
|
|||||||
.order_by(cls.base_code, cls.no).all()
|
.order_by(cls.base_code, cls.no).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def credit(cls) -> list[t.Self]:
|
def selectable_credit(cls) -> list[t.Self]:
|
||||||
"""Returns the debit accounts.
|
"""Returns the selectable debit accounts.
|
||||||
|
Receivable line items can not start from credit.
|
||||||
|
|
||||||
:return: The debit accounts.
|
:return: The selectable debit accounts.
|
||||||
"""
|
"""
|
||||||
return cls.query.filter(sa.or_(cls.base_code.startswith("1"),
|
return cls.query.filter(sa.or_(sa.and_(cls.base_code.startswith("1"),
|
||||||
|
sa.not_(cls.is_need_offset)),
|
||||||
cls.base_code.startswith("2"),
|
cls.base_code.startswith("2"),
|
||||||
cls.base_code.startswith("3"),
|
cls.base_code.startswith("3"),
|
||||||
cls.base_code.startswith("4"),
|
cls.base_code.startswith("4"),
|
||||||
@ -381,11 +395,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 +413,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 +436,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 +622,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.
|
||||||
@ -762,3 +783,33 @@ class JournalEntryLineItem(db.Model):
|
|||||||
journal_entry_day.day),
|
journal_entry_day.day),
|
||||||
format_amount(self.amount),
|
format_amount(self.amount),
|
||||||
format_amount(self.net_balance)])
|
format_amount(self.net_balance)])
|
||||||
|
|
||||||
|
|
||||||
|
class Option(db.Model):
|
||||||
|
"""An option."""
|
||||||
|
__tablename__ = "accounting_options"
|
||||||
|
"""The table name."""
|
||||||
|
name = db.Column(db.String, nullable=False, primary_key=True)
|
||||||
|
"""The name."""
|
||||||
|
value = db.Column(db.Text, nullable=False)
|
||||||
|
"""The option value."""
|
||||||
|
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
||||||
|
server_default=db.func.now())
|
||||||
|
"""The time of creation."""
|
||||||
|
created_by_id = db.Column(db.Integer,
|
||||||
|
db.ForeignKey(user_pk_column,
|
||||||
|
onupdate="CASCADE"),
|
||||||
|
nullable=False)
|
||||||
|
"""The ID of the creator."""
|
||||||
|
created_by = db.relationship(user_cls, foreign_keys=created_by_id)
|
||||||
|
"""The creator."""
|
||||||
|
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
||||||
|
server_default=db.func.now())
|
||||||
|
"""The time of last update."""
|
||||||
|
updated_by_id = db.Column(db.Integer,
|
||||||
|
db.ForeignKey(user_pk_column,
|
||||||
|
onupdate="CASCADE"),
|
||||||
|
nullable=False)
|
||||||
|
"""The ID of the updator."""
|
||||||
|
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
|
||||||
|
"""The updator."""
|
||||||
|
30
src/accounting/option/__init__.py
Normal file
30
src/accounting/option/__init__.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# The Mia! Accounting Flask Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The option management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(bp: Blueprint) -> None:
|
||||||
|
"""Initialize the application.
|
||||||
|
|
||||||
|
:param bp: The blueprint of the accounting application.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
from .views import bp as option_bp
|
||||||
|
bp.register_blueprint(option_bp, url_prefix="/options")
|
269
src/accounting/option/forms.py
Normal file
269
src/accounting/option/forms.py
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
# The Mia! Accounting Flask Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The forms for the option management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from flask import render_template
|
||||||
|
from flask_babel import LazyString
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, FieldList, FormField, IntegerField
|
||||||
|
from wtforms.validators import DataRequired, ValidationError
|
||||||
|
|
||||||
|
from accounting.forms import ACCOUNT_REQUIRED, CurrencyExists, AccountExists, \
|
||||||
|
IsDebitAccount, IsCreditAccount
|
||||||
|
from accounting.locale import lazy_gettext
|
||||||
|
from accounting.models import Account
|
||||||
|
from accounting.utils.current_account import CurrentAccount
|
||||||
|
from accounting.utils.options import Options
|
||||||
|
from accounting.utils.strip_text import strip_text
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentAccountExists:
|
||||||
|
"""The validator to check that the current account exists."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
|
if field.data is None or field.data == CurrentAccount.CURRENT_AL_CODE:
|
||||||
|
return
|
||||||
|
if Account.find_by_code(field.data) is None:
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"The account does not exist."))
|
||||||
|
|
||||||
|
|
||||||
|
class AccountNotCurrent:
|
||||||
|
"""The validator to check that the account is a current account."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
|
if field.data is None or field.data == CurrentAccount.CURRENT_AL_CODE:
|
||||||
|
return
|
||||||
|
if field.data[:2] not in {"11", "12", "21", "22"}:
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"This is not a current account."))
|
||||||
|
|
||||||
|
|
||||||
|
class NotStartPayableFromExpense:
|
||||||
|
"""The validator to check that a payable line item does not start from
|
||||||
|
expense."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
|
if field.data is None or field.data[0] != "2":
|
||||||
|
return
|
||||||
|
account: Account | None = Account.find_by_code(field.data)
|
||||||
|
if account is not None and account.is_need_offset:
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"You cannot select a payable account as expense."))
|
||||||
|
|
||||||
|
|
||||||
|
class NotStartReceivableFromIncome:
|
||||||
|
"""The validator to check that a receivable line item does not start
|
||||||
|
from income."""
|
||||||
|
|
||||||
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
|
if field.data is None or field.data[0] != "1":
|
||||||
|
return
|
||||||
|
account: Account | None = Account.find_by_code(field.data)
|
||||||
|
if account is not None and account.is_need_offset:
|
||||||
|
raise ValidationError(lazy_gettext(
|
||||||
|
"You cannot select a receivable account as income."))
|
||||||
|
|
||||||
|
|
||||||
|
class RecurringItemForm(FlaskForm):
|
||||||
|
"""The base sub-form to add or update the recurring item."""
|
||||||
|
no = IntegerField()
|
||||||
|
"""The order number of this recurring item."""
|
||||||
|
name = StringField()
|
||||||
|
"""The name of the recurring item."""
|
||||||
|
account_code = StringField()
|
||||||
|
"""The account code."""
|
||||||
|
description_template = StringField()
|
||||||
|
"""The description template."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_text(self) -> str | None:
|
||||||
|
"""Returns the account text.
|
||||||
|
|
||||||
|
:return: The account text.
|
||||||
|
"""
|
||||||
|
if self.account_code.data is None:
|
||||||
|
return None
|
||||||
|
account: Account | None = Account.find_by_code(self.account_code.data)
|
||||||
|
return None if account is None else str(account)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_errors(self) -> list[str | LazyString]:
|
||||||
|
"""Returns all the errors of the form.
|
||||||
|
|
||||||
|
:return: All the errors of the form.
|
||||||
|
"""
|
||||||
|
all_errors: list[str | LazyString] = []
|
||||||
|
for key in self.errors:
|
||||||
|
if key != "csrf_token":
|
||||||
|
all_errors.extend(self.errors[key])
|
||||||
|
return all_errors
|
||||||
|
|
||||||
|
|
||||||
|
class RecurringExpenseForm(RecurringItemForm):
|
||||||
|
"""The sub-form to add or update the recurring expenses."""
|
||||||
|
no = IntegerField()
|
||||||
|
"""The order number of this recurring item."""
|
||||||
|
name = StringField(
|
||||||
|
filters=[strip_text],
|
||||||
|
validators=[DataRequired(lazy_gettext("Please fill in the name."))])
|
||||||
|
"""The name of the recurring item."""
|
||||||
|
account_code = StringField(
|
||||||
|
filters=[strip_text],
|
||||||
|
validators=[
|
||||||
|
ACCOUNT_REQUIRED,
|
||||||
|
AccountExists(),
|
||||||
|
IsDebitAccount(lazy_gettext("This account is not for expense.")),
|
||||||
|
NotStartPayableFromExpense()])
|
||||||
|
"""The account code."""
|
||||||
|
description_template = StringField(
|
||||||
|
filters=[strip_text],
|
||||||
|
validators=[
|
||||||
|
DataRequired(lazy_gettext(
|
||||||
|
"Please fill in the description template."))])
|
||||||
|
"""The template for the line item description."""
|
||||||
|
|
||||||
|
|
||||||
|
class RecurringIncomeForm(RecurringItemForm):
|
||||||
|
"""The sub-form to add or update the recurring incomes."""
|
||||||
|
no = IntegerField()
|
||||||
|
"""The order number of this recurring item."""
|
||||||
|
name = StringField(
|
||||||
|
filters=[strip_text],
|
||||||
|
validators=[DataRequired(lazy_gettext("Please fill in the name."))])
|
||||||
|
"""The name of the recurring item."""
|
||||||
|
account_code = StringField(
|
||||||
|
filters=[strip_text],
|
||||||
|
validators=[
|
||||||
|
ACCOUNT_REQUIRED,
|
||||||
|
AccountExists(),
|
||||||
|
IsCreditAccount(lazy_gettext("This account is not for income.")),
|
||||||
|
NotStartReceivableFromIncome()])
|
||||||
|
"""The account code."""
|
||||||
|
description_template = StringField(
|
||||||
|
filters=[strip_text],
|
||||||
|
validators=[
|
||||||
|
DataRequired(lazy_gettext(
|
||||||
|
"Please fill in the description template."))])
|
||||||
|
"""The description template."""
|
||||||
|
|
||||||
|
|
||||||
|
class RecurringForm(RecurringItemForm):
|
||||||
|
"""The sub-form for the recurring expenses and incomes."""
|
||||||
|
expenses = FieldList(FormField(RecurringExpenseForm), name="expense")
|
||||||
|
"""The recurring expenses."""
|
||||||
|
incomes = FieldList(FormField(RecurringIncomeForm), name="income")
|
||||||
|
"""The recurring incomes."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def item_template(self) -> str:
|
||||||
|
"""Returns the template of a recurring item.
|
||||||
|
|
||||||
|
:return: The template of a recurring item.
|
||||||
|
"""
|
||||||
|
return render_template(
|
||||||
|
"accounting/option/include/form-recurring-item.html",
|
||||||
|
expense_income="EXPENSE_INCOME",
|
||||||
|
item_index="ITEM_INDEX",
|
||||||
|
form=RecurringItemForm())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expense_accounts(self) -> list[Account]:
|
||||||
|
"""The expense accounts.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
return Account.selectable_debit()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def income_accounts(self) -> list[Account]:
|
||||||
|
"""The income accounts.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
return Account.selectable_credit()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def as_data(self) -> dict[str, list[tuple[str, str, str]]]:
|
||||||
|
"""Returns the form data.
|
||||||
|
|
||||||
|
:return: The form data.
|
||||||
|
"""
|
||||||
|
def as_tuple(item: RecurringItemForm) -> tuple[str, str, str]:
|
||||||
|
return (item.name.data, item.account_code.data,
|
||||||
|
item.description_template.data)
|
||||||
|
|
||||||
|
expenses: list[RecurringItemForm] = [x.form for x in self.expenses]
|
||||||
|
self.__sort_item_forms(expenses)
|
||||||
|
incomes: list[RecurringItemForm] = [x.form for x in self.incomes]
|
||||||
|
self.__sort_item_forms(incomes)
|
||||||
|
return {"expense": [as_tuple(x) for x in expenses],
|
||||||
|
"income": [as_tuple(x) for x in incomes]}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __sort_item_forms(forms: list[RecurringItemForm]) -> None:
|
||||||
|
"""Sorts the recurring item sub-forms.
|
||||||
|
|
||||||
|
:param forms: The recurring item sub-forms.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
ord_by_form: dict[RecurringItemForm, int] \
|
||||||
|
= {forms[i]: i for i in range(len(forms))}
|
||||||
|
recv_no: set[int] = {x.no.data for x in forms if x.no.data is not None}
|
||||||
|
missing_recv_no: int = 100 if len(recv_no) == 0 else max(recv_no) + 100
|
||||||
|
forms.sort(key=lambda x: (x.no.data or missing_recv_no,
|
||||||
|
ord_by_form.get(x)))
|
||||||
|
|
||||||
|
|
||||||
|
class OptionForm(FlaskForm):
|
||||||
|
"""The form to update the options."""
|
||||||
|
default_currency_code = StringField(
|
||||||
|
filters=[strip_text],
|
||||||
|
validators=[
|
||||||
|
DataRequired(lazy_gettext("Please select the default currency.")),
|
||||||
|
CurrencyExists()])
|
||||||
|
"""The default currency code."""
|
||||||
|
default_ie_account_code = StringField(
|
||||||
|
filters=[strip_text],
|
||||||
|
validators=[
|
||||||
|
DataRequired(lazy_gettext(
|
||||||
|
"Please select the default account"
|
||||||
|
" for the income and expenses log.")),
|
||||||
|
CurrentAccountExists(),
|
||||||
|
AccountNotCurrent()])
|
||||||
|
"""The default account code for the income and expenses log."""
|
||||||
|
recurring = FormField(RecurringForm)
|
||||||
|
"""The recurring expenses and incomes."""
|
||||||
|
|
||||||
|
def populate_obj(self, obj: Options) -> None:
|
||||||
|
"""Populates the form data into a currency object.
|
||||||
|
|
||||||
|
:param obj: The currency object.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
obj.default_currency_code = self.default_currency_code.data
|
||||||
|
obj.default_ie_account_code = self.default_ie_account_code.data
|
||||||
|
obj.recurring_data = self.recurring.form.as_data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_accounts(self) -> list[CurrentAccount]:
|
||||||
|
"""Returns the current accounts.
|
||||||
|
|
||||||
|
:return: The current accounts.
|
||||||
|
"""
|
||||||
|
return CurrentAccount.accounts()
|
83
src/accounting/option/views.py
Normal file
83
src/accounting/option/views.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# The Mia! Accounting Flask Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The views for the option management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from urllib.parse import parse_qsl, urlencode
|
||||||
|
|
||||||
|
from flask import Blueprint, render_template, redirect, session, request, \
|
||||||
|
flash, url_for
|
||||||
|
from werkzeug.datastructures import ImmutableMultiDict
|
||||||
|
|
||||||
|
from accounting.locale import lazy_gettext
|
||||||
|
from accounting.utils.cast import s
|
||||||
|
from accounting.utils.flash_errors import flash_form_errors
|
||||||
|
from accounting.utils.next_uri import inherit_next
|
||||||
|
from accounting.utils.options import options
|
||||||
|
from accounting.utils.permission import has_permission, can_admin
|
||||||
|
from .forms import OptionForm
|
||||||
|
|
||||||
|
bp: Blueprint = Blueprint("option", __name__)
|
||||||
|
"""The view blueprint for the currency management."""
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("", endpoint="detail")
|
||||||
|
@has_permission(can_admin)
|
||||||
|
def show_options() -> str:
|
||||||
|
"""Shows the options.
|
||||||
|
|
||||||
|
:return: The options.
|
||||||
|
"""
|
||||||
|
return render_template("accounting/option/detail.html", obj=options)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("edit", endpoint="edit")
|
||||||
|
@has_permission(can_admin)
|
||||||
|
def show_option_form() -> str:
|
||||||
|
"""Shows the option form.
|
||||||
|
|
||||||
|
:return: The option form.
|
||||||
|
"""
|
||||||
|
form: OptionForm
|
||||||
|
if "form" in session:
|
||||||
|
form = OptionForm(ImmutableMultiDict(parse_qsl(session["form"])))
|
||||||
|
del session["form"]
|
||||||
|
form.validate()
|
||||||
|
else:
|
||||||
|
form = OptionForm(obj=options)
|
||||||
|
return render_template("accounting/option/form.html", form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("update", endpoint="update")
|
||||||
|
@has_permission(can_admin)
|
||||||
|
def update_options() -> redirect:
|
||||||
|
"""Updates the options.
|
||||||
|
|
||||||
|
:return: The redirection to the option form.
|
||||||
|
"""
|
||||||
|
form = OptionForm(request.form)
|
||||||
|
if not form.validate():
|
||||||
|
flash_form_errors(form)
|
||||||
|
session["form"] = urlencode(list(request.form.items()))
|
||||||
|
return redirect(inherit_next(url_for("accounting.option.edit")))
|
||||||
|
form.populate_obj(options)
|
||||||
|
if not options.is_modified:
|
||||||
|
flash(s(lazy_gettext("The settings were not modified.")), "success")
|
||||||
|
return redirect(inherit_next(url_for("accounting.option.detail")))
|
||||||
|
options.commit()
|
||||||
|
flash(s(lazy_gettext("The settings are saved successfully.")), "success")
|
||||||
|
return redirect(inherit_next(url_for("accounting.option.detail")))
|
@ -23,8 +23,8 @@ from flask import abort
|
|||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
from accounting.models import Account
|
from accounting.models import Account
|
||||||
|
from accounting.utils.current_account import CurrentAccount
|
||||||
from .period import Period, get_period
|
from .period import Period, get_period
|
||||||
from .utils.ie_account import IncomeExpensesAccount
|
|
||||||
|
|
||||||
|
|
||||||
class PeriodConverter(BaseConverter):
|
class PeriodConverter(BaseConverter):
|
||||||
@ -55,22 +55,22 @@ class IncomeExpensesAccountConverter(BaseConverter):
|
|||||||
"""The supplier converter to convert the income and expenses log pseudo
|
"""The supplier converter to convert the income and expenses log pseudo
|
||||||
account code from and to the corresponding pseudo account in the routes."""
|
account code from and to the corresponding pseudo account in the routes."""
|
||||||
|
|
||||||
def to_python(self, value: str) -> IncomeExpensesAccount:
|
def to_python(self, value: str) -> CurrentAccount:
|
||||||
"""Converts an account code to an account.
|
"""Converts an account code to an account.
|
||||||
|
|
||||||
:param value: The account code.
|
:param value: The account code.
|
||||||
:return: The corresponding account.
|
:return: The corresponding account.
|
||||||
"""
|
"""
|
||||||
if value == IncomeExpensesAccount.CURRENT_AL_CODE:
|
if value == CurrentAccount.CURRENT_AL_CODE:
|
||||||
return IncomeExpensesAccount.current_assets_and_liabilities()
|
return CurrentAccount.current_assets_and_liabilities()
|
||||||
if not re.match("^[12][12]", value):
|
if not re.match("^[12][12]", value):
|
||||||
abort(404)
|
abort(404)
|
||||||
account: Account | None = Account.find_by_code(value)
|
account: Account | None = Account.find_by_code(value)
|
||||||
if account is None:
|
if account is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
return IncomeExpensesAccount(account)
|
return CurrentAccount(account)
|
||||||
|
|
||||||
def to_url(self, value: IncomeExpensesAccount) -> str:
|
def to_url(self, value: CurrentAccount) -> str:
|
||||||
"""Converts an account to account code.
|
"""Converts an account to account code.
|
||||||
|
|
||||||
:param value: The account.
|
:param value: The account.
|
||||||
|
@ -33,12 +33,12 @@ from accounting.report.utils.base_page_params import BasePageParams
|
|||||||
from accounting.report.utils.base_report import BaseReport
|
from accounting.report.utils.base_report import BaseReport
|
||||||
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
|
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
|
||||||
period_spec
|
period_spec
|
||||||
from accounting.report.utils.ie_account import IncomeExpensesAccount
|
|
||||||
from accounting.report.utils.option_link import OptionLink
|
from accounting.report.utils.option_link import OptionLink
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
from accounting.report.utils.report_chooser import ReportChooser
|
||||||
from accounting.report.utils.report_type import ReportType
|
from accounting.report.utils.report_type import ReportType
|
||||||
from accounting.report.utils.urls import income_expenses_url
|
from accounting.report.utils.urls import income_expenses_url
|
||||||
from accounting.utils.cast import be
|
from accounting.utils.cast import be
|
||||||
|
from accounting.utils.current_account import CurrentAccount
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
|
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ class ReportLineItem:
|
|||||||
class LineItemCollector:
|
class LineItemCollector:
|
||||||
"""The line item collector."""
|
"""The line item collector."""
|
||||||
|
|
||||||
def __init__(self, currency: Currency, account: IncomeExpensesAccount,
|
def __init__(self, currency: Currency, account: CurrentAccount,
|
||||||
period: Period):
|
period: Period):
|
||||||
"""Constructs the line item collector.
|
"""Constructs the line item collector.
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ class LineItemCollector:
|
|||||||
"""
|
"""
|
||||||
self.__currency: Currency = currency
|
self.__currency: Currency = currency
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
self.__account: IncomeExpensesAccount = account
|
self.__account: CurrentAccount = account
|
||||||
"""The account."""
|
"""The account."""
|
||||||
self.__period: Period = period
|
self.__period: Period = period
|
||||||
"""The period"""
|
"""The period"""
|
||||||
@ -173,11 +173,8 @@ class LineItemCollector:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def __account_condition(self) -> sa.BinaryExpression:
|
def __account_condition(self) -> sa.BinaryExpression:
|
||||||
if self.__account.code == IncomeExpensesAccount.CURRENT_AL_CODE:
|
if self.__account.code == CurrentAccount.CURRENT_AL_CODE:
|
||||||
return sa.or_(Account.base_code.startswith("11"),
|
return CurrentAccount.sql_condition()
|
||||||
Account.base_code.startswith("12"),
|
|
||||||
Account.base_code.startswith("21"),
|
|
||||||
Account.base_code.startswith("22"))
|
|
||||||
return Account.id == self.__account.id
|
return Account.id == self.__account.id
|
||||||
|
|
||||||
def __get_total(self) -> ReportLineItem | None:
|
def __get_total(self) -> ReportLineItem | None:
|
||||||
@ -264,7 +261,7 @@ class PageParams(BasePageParams):
|
|||||||
"""The HTML page parameters."""
|
"""The HTML page parameters."""
|
||||||
|
|
||||||
def __init__(self, currency: Currency,
|
def __init__(self, currency: Currency,
|
||||||
account: IncomeExpensesAccount,
|
account: CurrentAccount,
|
||||||
period: Period,
|
period: Period,
|
||||||
has_data: bool,
|
has_data: bool,
|
||||||
pagination: Pagination[ReportLineItem],
|
pagination: Pagination[ReportLineItem],
|
||||||
@ -283,7 +280,7 @@ class PageParams(BasePageParams):
|
|||||||
"""
|
"""
|
||||||
self.currency: Currency = currency
|
self.currency: Currency = currency
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
self.account: IncomeExpensesAccount = account
|
self.account: CurrentAccount = account
|
||||||
"""The account."""
|
"""The account."""
|
||||||
self.period: Period = period
|
self.period: Period = period
|
||||||
"""The period."""
|
"""The period."""
|
||||||
@ -341,8 +338,8 @@ class PageParams(BasePageParams):
|
|||||||
|
|
||||||
:return: The account options.
|
:return: The account options.
|
||||||
"""
|
"""
|
||||||
current_al: IncomeExpensesAccount \
|
current_al: CurrentAccount \
|
||||||
= IncomeExpensesAccount.current_assets_and_liabilities()
|
= CurrentAccount.current_assets_and_liabilities()
|
||||||
options: list[OptionLink] \
|
options: list[OptionLink] \
|
||||||
= [OptionLink(str(current_al),
|
= [OptionLink(str(current_al),
|
||||||
income_expenses_url(self.currency, current_al,
|
income_expenses_url(self.currency, current_al,
|
||||||
@ -352,15 +349,12 @@ class PageParams(BasePageParams):
|
|||||||
.join(Account)\
|
.join(Account)\
|
||||||
.filter(be(JournalEntryLineItem.currency_code
|
.filter(be(JournalEntryLineItem.currency_code
|
||||||
== self.currency.code),
|
== self.currency.code),
|
||||||
sa.or_(Account.base_code.startswith("11"),
|
CurrentAccount.sql_condition())\
|
||||||
Account.base_code.startswith("12"),
|
|
||||||
Account.base_code.startswith("21"),
|
|
||||||
Account.base_code.startswith("22")))\
|
|
||||||
.group_by(JournalEntryLineItem.account_id)
|
.group_by(JournalEntryLineItem.account_id)
|
||||||
options.extend([OptionLink(str(x),
|
options.extend([OptionLink(str(x),
|
||||||
income_expenses_url(
|
income_expenses_url(
|
||||||
self.currency,
|
self.currency,
|
||||||
IncomeExpensesAccount(x),
|
CurrentAccount(x),
|
||||||
self.period),
|
self.period),
|
||||||
x.id == self.account.id)
|
x.id == self.account.id)
|
||||||
for x in Account.query.filter(Account.id.in_(in_use))
|
for x in Account.query.filter(Account.id.in_(in_use))
|
||||||
@ -371,7 +365,7 @@ class PageParams(BasePageParams):
|
|||||||
class IncomeExpenses(BaseReport):
|
class IncomeExpenses(BaseReport):
|
||||||
"""The income and expenses log."""
|
"""The income and expenses log."""
|
||||||
|
|
||||||
def __init__(self, currency: Currency, account: IncomeExpensesAccount,
|
def __init__(self, currency: Currency, account: CurrentAccount,
|
||||||
period: Period):
|
period: Period):
|
||||||
"""Constructs an income and expenses log.
|
"""Constructs an income and expenses log.
|
||||||
|
|
||||||
@ -381,7 +375,7 @@ class IncomeExpenses(BaseReport):
|
|||||||
"""
|
"""
|
||||||
self.__currency: Currency = currency
|
self.__currency: Currency = currency
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
self.__account: IncomeExpensesAccount = account
|
self.__account: CurrentAccount = account
|
||||||
"""The account."""
|
"""The account."""
|
||||||
self.__period: Period = period
|
self.__period: Period = period
|
||||||
"""The period."""
|
"""The period."""
|
||||||
|
@ -30,7 +30,7 @@ from accounting.locale import gettext
|
|||||||
from accounting.models import Currency, Account
|
from accounting.models import Currency, Account
|
||||||
from accounting.report.period import Period, get_period
|
from accounting.report.period import Period, get_period
|
||||||
from accounting.template_globals import default_currency_code
|
from accounting.template_globals import default_currency_code
|
||||||
from .ie_account import IncomeExpensesAccount
|
from accounting.utils.current_account import CurrentAccount
|
||||||
from .option_link import OptionLink
|
from .option_link import OptionLink
|
||||||
from .report_type import ReportType
|
from .report_type import ReportType
|
||||||
from .urls import journal_url, ledger_url, income_expenses_url, \
|
from .urls import journal_url, ledger_url, income_expenses_url, \
|
||||||
@ -113,7 +113,7 @@ class ReportChooser:
|
|||||||
account: Account = Account.cash()
|
account: Account = Account.cash()
|
||||||
return OptionLink(gettext("Income and Expenses Log"),
|
return OptionLink(gettext("Income and Expenses Log"),
|
||||||
income_expenses_url(self.__currency,
|
income_expenses_url(self.__currency,
|
||||||
IncomeExpensesAccount(account),
|
CurrentAccount(account),
|
||||||
self.__period),
|
self.__period),
|
||||||
self.__active_report == ReportType.INCOME_EXPENSES,
|
self.__active_report == ReportType.INCOME_EXPENSES,
|
||||||
fa_icon="fa-solid fa-money-bill-wave")
|
fa_icon="fa-solid fa-money-bill-wave")
|
||||||
|
@ -22,7 +22,8 @@ from flask import url_for
|
|||||||
from accounting.models import Currency, Account
|
from accounting.models import Currency, Account
|
||||||
from accounting.report.period import Period
|
from accounting.report.period import Period
|
||||||
from accounting.template_globals import default_currency_code
|
from accounting.template_globals import default_currency_code
|
||||||
from .ie_account import IncomeExpensesAccount, default_ie_account_code
|
from accounting.utils.current_account import CurrentAccount
|
||||||
|
from accounting.utils.options import options
|
||||||
|
|
||||||
|
|
||||||
def journal_url(period: Period) \
|
def journal_url(period: Period) \
|
||||||
@ -54,7 +55,7 @@ def ledger_url(currency: Currency, account: Account, period: Period) \
|
|||||||
period=period)
|
period=period)
|
||||||
|
|
||||||
|
|
||||||
def income_expenses_url(currency: Currency, account: IncomeExpensesAccount,
|
def income_expenses_url(currency: Currency, account: CurrentAccount,
|
||||||
period: Period) -> str:
|
period: Period) -> str:
|
||||||
"""Returns the URL of an income and expenses log.
|
"""Returns the URL of an income and expenses log.
|
||||||
|
|
||||||
@ -64,7 +65,7 @@ def income_expenses_url(currency: Currency, account: IncomeExpensesAccount,
|
|||||||
:return: The URL of the income and expenses log.
|
:return: The URL of the income and expenses log.
|
||||||
"""
|
"""
|
||||||
if currency.code == default_currency_code() \
|
if currency.code == default_currency_code() \
|
||||||
and account.code == default_ie_account_code() \
|
and account.code == options.default_ie_account_code \
|
||||||
and period.is_default:
|
and period.is_default:
|
||||||
return url_for("accounting.report.default")
|
return url_for("accounting.report.default")
|
||||||
if period.is_default:
|
if period.is_default:
|
||||||
|
@ -23,11 +23,12 @@ from accounting import db
|
|||||||
from accounting.models import Currency, Account
|
from accounting.models import Currency, Account
|
||||||
from accounting.report.period import Period, get_period
|
from accounting.report.period import Period, get_period
|
||||||
from accounting.template_globals import default_currency_code
|
from accounting.template_globals import default_currency_code
|
||||||
|
from accounting.utils.current_account import CurrentAccount
|
||||||
|
from accounting.utils.options import options
|
||||||
from accounting.utils.permission import has_permission, can_view
|
from accounting.utils.permission import has_permission, can_view
|
||||||
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
|
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
|
||||||
IncomeStatement, BalanceSheet, Search
|
IncomeStatement, BalanceSheet, Search
|
||||||
from .template_filters import format_amount
|
from .template_filters import format_amount
|
||||||
from .utils.ie_account import IncomeExpensesAccount, default_ie_account
|
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("report", __name__)
|
bp: Blueprint = Blueprint("report", __name__)
|
||||||
"""The view blueprint for the reports."""
|
"""The view blueprint for the reports."""
|
||||||
@ -43,7 +44,7 @@ def get_default_report() -> str | Response:
|
|||||||
"""
|
"""
|
||||||
return __get_income_expenses(
|
return __get_income_expenses(
|
||||||
db.session.get(Currency, default_currency_code()),
|
db.session.get(Currency, default_currency_code()),
|
||||||
default_ie_account(),
|
options.default_ie_account,
|
||||||
get_period())
|
get_period())
|
||||||
|
|
||||||
|
|
||||||
@ -126,8 +127,7 @@ def __get_ledger(currency: Currency, account: Account, period: Period) \
|
|||||||
@bp.get("income-expenses/<currency:currency>/<ieAccount:account>",
|
@bp.get("income-expenses/<currency:currency>/<ieAccount:account>",
|
||||||
endpoint="income-expenses-default")
|
endpoint="income-expenses-default")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def get_default_income_expenses(currency: Currency,
|
def get_default_income_expenses(currency: Currency, account: CurrentAccount) \
|
||||||
account: IncomeExpensesAccount) \
|
|
||||||
-> str | Response:
|
-> str | Response:
|
||||||
"""Returns the income and expenses log in the default period.
|
"""Returns the income and expenses log in the default period.
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ def get_default_income_expenses(currency: Currency,
|
|||||||
"income-expenses/<currency:currency>/<ieAccount:account>/<period:period>",
|
"income-expenses/<currency:currency>/<ieAccount:account>/<period:period>",
|
||||||
endpoint="income-expenses")
|
endpoint="income-expenses")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
|
def get_income_expenses(currency: Currency, account: CurrentAccount,
|
||||||
period: Period) -> str | Response:
|
period: Period) -> str | Response:
|
||||||
"""Returns the income and expenses log.
|
"""Returns the income and expenses log.
|
||||||
|
|
||||||
@ -154,7 +154,7 @@ def get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
|
|||||||
return __get_income_expenses(currency, account, period)
|
return __get_income_expenses(currency, account, period)
|
||||||
|
|
||||||
|
|
||||||
def __get_income_expenses(currency: Currency, account: IncomeExpensesAccount,
|
def __get_income_expenses(currency: Currency, account: CurrentAccount,
|
||||||
period: Period) -> str | Response:
|
period: Period) -> str | Response:
|
||||||
"""Returns the income and expenses log.
|
"""Returns the income and expenses log.
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@
|
|||||||
height: 3.2rem;
|
height: 3.2rem;
|
||||||
width: 3.2rem;
|
width: 3.2rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-left: 1rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
.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 +315,30 @@ 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 illustration of the description template for the recurring transactions */
|
||||||
|
.accounting-recurring-description-template-illustration p {
|
||||||
|
margin: 0.2rem 0;
|
||||||
|
}
|
||||||
|
.accounting-recurring-description-template-illustration ul {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* 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 +363,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;
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ class AccountForm {
|
|||||||
this.#isNeedOffsetControl = document.getElementById("accounting-is-need-offset-control");
|
this.#isNeedOffsetControl = document.getElementById("accounting-is-need-offset-control");
|
||||||
this.#isNeedOffset = document.getElementById("accounting-is-need-offset");
|
this.#isNeedOffset = document.getElementById("accounting-is-need-offset");
|
||||||
this.#formElement.onsubmit = () => {
|
this.#formElement.onsubmit = () => {
|
||||||
return this.#validateForm();
|
return this.#validate();
|
||||||
};
|
};
|
||||||
this.#baseControl.onclick = () => {
|
this.#baseControl.onclick = () => {
|
||||||
this.#baseControl.classList.add("accounting-not-empty");
|
this.#baseControl.classList.add("accounting-not-empty");
|
||||||
@ -163,7 +163,7 @@ class AccountForm {
|
|||||||
*
|
*
|
||||||
* @returns {boolean} true if valid, or false otherwise
|
* @returns {boolean} true if valid, or false otherwise
|
||||||
*/
|
*/
|
||||||
#validateForm() {
|
#validate() {
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
isValid = this.#validateBase() && isValid;
|
isValid = this.#validateBase() && isValid;
|
||||||
isValid = this.#validateTitle() && isValid;
|
isValid = this.#validateTitle() && isValid;
|
||||||
@ -282,7 +282,7 @@ class BaseAccountSelector {
|
|||||||
});
|
});
|
||||||
for (const option of this.#options) {
|
for (const option of this.#options) {
|
||||||
option.onclick = () => {
|
option.onclick = () => {
|
||||||
this.#form.setBaseAccount(option.dataset.code, option.dataset.content);
|
this.#form.setBaseAccount(option.dataset.code, option.dataset.text);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.#clearButton.onclick = () => {
|
this.#clearButton.onclick = () => {
|
||||||
|
@ -105,7 +105,7 @@ class AccountSelector {
|
|||||||
};
|
};
|
||||||
this.#clearButton.onclick = () => this.#lineItemEditor.clearAccount();
|
this.#clearButton.onclick = () => this.#lineItemEditor.clearAccount();
|
||||||
for (const option of this.#options) {
|
for (const option of this.#options) {
|
||||||
option.onclick = () => this.#lineItemEditor.saveAccount(option.dataset.code, option.dataset.content, option.classList.contains("accounting-account-is-need-offset"));
|
option.onclick = () => this.#lineItemEditor.saveAccount(option.dataset.code, option.dataset.text, option.classList.contains("accounting-account-is-need-offset"));
|
||||||
}
|
}
|
||||||
this.#query.addEventListener("input", () => {
|
this.#query.addEventListener("input", () => {
|
||||||
this.#filterOptions();
|
this.#filterOptions();
|
||||||
|
@ -81,7 +81,7 @@ class CurrencyForm {
|
|||||||
this.#validateName();
|
this.#validateName();
|
||||||
};
|
};
|
||||||
this.#formElement.onsubmit = () => {
|
this.#formElement.onsubmit = () => {
|
||||||
this.#validateForm().then((isValid) => {
|
this.#validate().then((isValid) => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
this.#formElement.submit();
|
this.#formElement.submit();
|
||||||
}
|
}
|
||||||
@ -95,7 +95,7 @@ class CurrencyForm {
|
|||||||
*
|
*
|
||||||
* @returns {Promise<boolean>} true if valid, or false otherwise
|
* @returns {Promise<boolean>} true if valid, or false otherwise
|
||||||
*/
|
*/
|
||||||
async #validateForm() {
|
async #validate() {
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
isValid = await this.#validateCode() && isValid;
|
isValid = await this.#validateCode() && isValid;
|
||||||
isValid = this.#validateName() && isValid;
|
isValid = this.#validateName() && isValid;
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
1030
src/accounting/static/js/option-form.js
Normal file
1030
src/accounting/static/js/option-form.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -17,9 +17,8 @@
|
|||||||
"""The template globals.
|
"""The template globals.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
from accounting.models import Currency
|
from accounting.models import Currency
|
||||||
|
from accounting.utils.options import options
|
||||||
|
|
||||||
|
|
||||||
def currency_options() -> str:
|
def currency_options() -> str:
|
||||||
@ -35,5 +34,4 @@ def default_currency_code() -> str:
|
|||||||
|
|
||||||
:return: The default currency code.
|
:return: The default currency code.
|
||||||
"""
|
"""
|
||||||
with current_app.app_context():
|
return options.default_currency_code
|
||||||
return current_app.config.get("DEFAULT_CURRENCY", "USD")
|
|
||||||
|
@ -25,38 +25,45 @@ 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" role="button" 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") }}
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</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" role="button" 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" role="button" 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>
|
||||||
|
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<a class="btn btn-primary" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
|
||||||
<i class="fa-solid fa-pen-to-square"></i>
|
<i class="fa-solid fa-pen-to-square"></i>
|
||||||
</a>
|
</a>
|
||||||
</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,10 +27,10 @@ First written: 2023/2/1
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="btn-group mb-3">
|
<div class="mb-3 accounting-toolbar">
|
||||||
<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>
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ First written: 2023/2/1
|
|||||||
|
|
||||||
<ul id="accounting-base-selector-option-list" class="list-group accounting-selector-list">
|
<ul id="accounting-base-selector-option-list" class="list-group accounting-selector-list">
|
||||||
{% for base in form.base_options %}
|
{% for base in form.base_options %}
|
||||||
<li class="list-group-item accounting-clickable accounting-base-selector-option" data-code="{{ base.code }}" data-content="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}" data-bs-dismiss="modal">
|
<li class="list-group-item accounting-clickable accounting-base-selector-option" data-code="{{ base.code }}" data-text="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}" data-bs-dismiss="modal">
|
||||||
{{ base }}
|
{{ base }}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -27,7 +27,7 @@ First written: 2023/1/30
|
|||||||
|
|
||||||
<div class="mb-2 accounting-toolbar">
|
<div class="mb-2 accounting-toolbar">
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<a class="btn btn-primary text-nowrap d-none d-md-block" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
|
<a class="btn btn-primary text-nowrap d-none d-md-block" role="button" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
|
||||||
<i class="fa-solid fa-plus"></i>
|
<i class="fa-solid fa-plus"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</a>
|
</a>
|
||||||
@ -45,7 +45,7 @@ First written: 2023/1/30
|
|||||||
|
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<a class="btn btn-primary" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
|
||||||
<i class="fa-solid fa-plus"></i>
|
<i class="fa-solid fa-plus"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,10 +30,10 @@ First written: 2023/2/2
|
|||||||
|
|
||||||
{% 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" role="button" 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") }}
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -25,10 +25,10 @@ First written: 2023/2/1
|
|||||||
|
|
||||||
{% 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" role="button" 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") }}
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ First written: 2023/2/1
|
|||||||
{% if obj.accounts %}
|
{% if obj.accounts %}
|
||||||
<div>
|
<div>
|
||||||
{% for account in obj.accounts %}
|
{% for account in obj.accounts %}
|
||||||
<a class="btn btn-primary" href="{{ url_for("accounting.account.detail", account=account)|accounting_append_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.detail", account=account)|accounting_append_next }}">
|
||||||
{{ account }}
|
{{ account }}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -25,34 +25,41 @@ 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" role="button" 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") }}
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</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" role="button" 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>
|
||||||
|
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<a class="btn btn-primary" href="{{ url_for("accounting.currency.edit", currency=obj)|accounting_inherit_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.currency.edit", currency=obj)|accounting_inherit_next }}">
|
||||||
<i class="fa-solid fa-pen-to-square"></i>
|
<i class="fa-solid fa-pen-to-square"></i>
|
||||||
</a>
|
</a>
|
||||||
</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,10 +27,10 @@ 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" 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>
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ First written: 2023/2/6
|
|||||||
|
|
||||||
<div class="mb-2 accounting-toolbar">
|
<div class="mb-2 accounting-toolbar">
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<a class="btn btn-primary text-nowrap d-none d-md-block" href="{{ url_for("accounting.currency.create")|accounting_append_next }}">
|
<a class="btn btn-primary text-nowrap d-none d-md-block" role="button" href="{{ url_for("accounting.currency.create")|accounting_append_next }}">
|
||||||
<i class="fa-solid fa-plus"></i>
|
<i class="fa-solid fa-plus"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</a>
|
</a>
|
||||||
@ -45,7 +45,7 @@ First written: 2023/2/6
|
|||||||
|
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<a class="btn btn-primary" href="{{ url_for("accounting.currency.create")|accounting_append_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.currency.create")|accounting_append_next }}">
|
||||||
<i class="fa-solid fa-plus"></i>
|
<i class="fa-solid fa-plus"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,6 +51,14 @@ First written: 2023/1/26
|
|||||||
{{ A_("Currencies") }}
|
{{ A_("Currencies") }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if accounting_can_admin() %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if request.endpoint and request.endpoint.startswith("accounting.option.") %} active {% endif %}" href="{{ url_for("accounting.option.detail") }}">
|
||||||
|
<i class="fa-solid fa-gear"></i>
|
||||||
|
{{ A_("Settings") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -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" role="button" 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 %}
|
||||||
|
@ -37,7 +37,7 @@ First written: 2023/2/25
|
|||||||
|
|
||||||
<ul id="accounting-account-selector-{{ debit_credit }}-option-list" class="list-group accounting-selector-list">
|
<ul id="accounting-account-selector-{{ debit_credit }}-option-list" class="list-group accounting-selector-list">
|
||||||
{% for account in account_options %}
|
{% for account in account_options %}
|
||||||
<li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-content="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
<li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||||
{{ account }}
|
{{ account }}
|
||||||
</li>
|
</li>
|
||||||
{% 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.description_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" role="button" 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") }}
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</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" role="button" 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" role="button" 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 %}
|
||||||
@ -58,7 +58,7 @@ First written: 2023/2/26
|
|||||||
|
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_inherit_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_inherit_next }}">
|
||||||
<i class="fa-solid fa-pen-to-square"></i>
|
<i class="fa-solid fa-pen-to-square"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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 accounting-toolbar">
|
||||||
<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,56 @@
|
|||||||
|
{#
|
||||||
|
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>
|
||||||
|
{% 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>
|
@ -26,14 +26,14 @@ First written: 2023/2/26
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/journal-entry-order.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/journal-entry-order.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Journal Entries on %(date)s", date=date) }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ A_("Journal Entries on %(date)s", date=date|accounting_format_date) }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% 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" role="button" 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") }}
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -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" role="button" 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 %}
|
||||||
|
63
src/accounting/templates/accounting/option/detail.html
Normal file
63
src/accounting/templates/accounting/option/detail.html
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
detail.html: The option detail
|
||||||
|
|
||||||
|
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/22
|
||||||
|
#}
|
||||||
|
{% extends "accounting/base.html" %}
|
||||||
|
|
||||||
|
{% block header %}{% block title %}{{ A_("Settings") }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="mb-3 accounting-toolbar">
|
||||||
|
<a class="btn btn-primary d-none d-md-inline" role="button" href="{{ url_for("accounting.option.edit")|accounting_inherit_next }}">
|
||||||
|
<i class="fa-solid fa-pen-to-square"></i>
|
||||||
|
{{ A_("Edit") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-md-none accounting-material-fab">
|
||||||
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.option.edit")|accounting_inherit_next }}">
|
||||||
|
<i class="fa-solid fa-pen-to-square"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<input id="accounting-default-currency" class="form-control" value="{{ obj.default_currency_text }}" readonly="readonly">
|
||||||
|
<label class="form-label" for="accounting-default-currency">{{ A_("Default Currency") }}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<input id="accounting-default-ie-account" class="form-control" value="{{ obj.default_ie_account_code_text }}" readonly="readonly">
|
||||||
|
<label class="form-label" for="accounting-default-ie-account">{{ A_("Default Account for the Income and Expenses Log") }}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with expense_income = "expense",
|
||||||
|
label = A_("Recurring Expense"),
|
||||||
|
recurring_items = obj.recurring.expenses %}
|
||||||
|
{% include "accounting/option/include/detail-recurring-expense-income.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with expense_income = "income",
|
||||||
|
label = A_("Recurring Income"),
|
||||||
|
recurring_items = obj.recurring.incomes %}
|
||||||
|
{% include "accounting/option/include/detail-recurring-expense-income.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% endblock %}
|
109
src/accounting/templates/accounting/option/form.html
Normal file
109
src/accounting/templates/accounting/option/form.html
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
form.html: The option 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/22
|
||||||
|
#}
|
||||||
|
{% extends "accounting/base.html" %}
|
||||||
|
|
||||||
|
{% block accounting_scripts %}
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/option-form.js") }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}{% block title %}{{ A_("Settings") }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="mb-3 accounting-toolbar">
|
||||||
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.option.detail")|accounting_inherit_next }}">
|
||||||
|
<i class="fa-solid fa-circle-chevron-left"></i>
|
||||||
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="accounting-form" action="{{ url_for("accounting.option.update") }}" method="post" data-recurring-item-template="{{ form.recurring.item_template }}">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
{% if request.args.next %}
|
||||||
|
<input type="hidden" name="next" value="{{ request.args.next }}">
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<select id="accounting-default-currency" class="form-select {% if form.default_currency_code.errors %} is-invalid {% endif %}" name="default_currency_code">
|
||||||
|
{% for currency in accounting_currency_options() %}
|
||||||
|
<option value="{{ currency.code }}" {% if currency.code == form.default_currency_code.data %} selected="selected" {% endif %}>{{ currency }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<label class="form-label" for="accounting-default-currency">{{ A_("Default Currency") }}</label>
|
||||||
|
<div id="accounting-default-currency-error" class="invalid-feedback">{% if form.default_currency_code.errors %}{{ form.default_currency_code.errors[0] }}{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<select id="accounting-default-ie-account" class="form-select {% if form.default_ie_account_code.errors %} is-invalid {% endif %}" name="default_ie_account_code">
|
||||||
|
{% for account in form.current_accounts %}
|
||||||
|
<option value="{{ account.code }}" {% if account.code == form.default_ie_account_code.data %} selected="selected" {% endif %}>{{ account }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<label class="form-label" for="accounting-default-ie-account">{{ A_("Default Account for the Income and Expenses Log") }}</label>
|
||||||
|
<div id="accounting-default-ie-account-error" class="invalid-feedback">{% if form.default_ie_account_code.errors %}{{ form.default_ie_account_code.errors[0] }}{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with expense_income = "expense",
|
||||||
|
label = A_("Recurring Expense"),
|
||||||
|
recurring_items = form.recurring.expenses %}
|
||||||
|
{% include "accounting/option/include/form-recurring-expense-income.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with expense_income = "income",
|
||||||
|
label = A_("Recurring Income"),
|
||||||
|
recurring_items = form.recurring.incomes %}
|
||||||
|
{% include "accounting/option/include/form-recurring-expense-income.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="d-none d-md-block">
|
||||||
|
<button class="btn btn-primary" type="submit">
|
||||||
|
<i class="fa-solid fa-floppy-disk"></i>
|
||||||
|
{{ A_("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-md-none accounting-material-fab">
|
||||||
|
<button class="btn btn-primary" type="submit">
|
||||||
|
<i class="fa-solid fa-floppy-disk"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% with expense_income = "expense",
|
||||||
|
title = A_("Recurring Expense") %}
|
||||||
|
{% include "accounting/option/include/recurring-item-editor-modal.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
{% with expense_income = "income",
|
||||||
|
title = A_("Recurring Income") %}
|
||||||
|
{% include "accounting/option/include/recurring-item-editor-modal.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with expense_income = "expense",
|
||||||
|
accounts = form.recurring.expense_accounts %}
|
||||||
|
{% include "accounting/option/include/recurring-account-selector-modal.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
{% with expense_income = "income",
|
||||||
|
accounts = form.recurring.income_accounts %}
|
||||||
|
{% include "accounting/option/include/recurring-account-selector-modal.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,31 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
detail-recurring-expense-income.html: The recurring expense or income in the option detail
|
||||||
|
|
||||||
|
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/22
|
||||||
|
#}
|
||||||
|
<div id="accounting-recurring-{{ expense_income }}" class="form-control mb-3 accounting-material-text-field {% if recurring_items %} accounting-not-empty {% endif %}">
|
||||||
|
<label class="form-label" for="accounting-recurring-{{ expense_income }}">{{ label }}</label>
|
||||||
|
{% if recurring_items %}
|
||||||
|
<ul class="list-group mb-2 mt-2">
|
||||||
|
{% for item in recurring_items %}
|
||||||
|
{% include "accounting/option/include/detail-recurring-item.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
@ -0,0 +1,28 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
detail-recurring-item.html: The recurring item in the option detail
|
||||||
|
|
||||||
|
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/22
|
||||||
|
#}
|
||||||
|
{# <ul> For SonarQube not to complain about incorrect HTML #}
|
||||||
|
<li class="list-group-item list-group-item-action">
|
||||||
|
<div class="small">{{ item.account_text }}</div>
|
||||||
|
<div>{{ item.name }}</div>
|
||||||
|
<div class="small">{{ item.description_template }}</div>
|
||||||
|
</li>
|
||||||
|
{# </ul> For SonarQube not to complain about incorrect HTML #}
|
@ -0,0 +1,41 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
form-recurring-expense-income.html: The recurring expense or income sub-form in the option 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/22
|
||||||
|
#}
|
||||||
|
<div id="accounting-recurring-{{ expense_income }}" class="form-control mb-3 accounting-material-text-field {% if recurring_items %} accounting-not-empty {% else %} accounting-clickable {% endif %}">
|
||||||
|
<label class="form-label" for="accounting-recurring-{{ expense_income }}">{{ label }}</label>
|
||||||
|
<div id="accounting-recurring-{{ expense_income }}-content" class="{% if not recurring_items %} d-none {% endif %}">
|
||||||
|
<ul id="accounting-recurring-{{ expense_income }}-list" class="list-group mb-2 mt-2">
|
||||||
|
{% for recurring_item in recurring_items %}
|
||||||
|
{% with form = recurring_item.form,
|
||||||
|
item_index = loop.index %}
|
||||||
|
{% include "accounting/option/include/form-recurring-item.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button id="accounting-recurring-{{ expense_income }}-add" class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-recurring-item-editor-{{ expense_income }}-modal">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
{{ A_("New") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,45 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
form-recurring-item.html: The recurring item sub-form in the option 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/22
|
||||||
|
#}
|
||||||
|
{# <ul> For SonarQube not to complain about incorrect HTML #}
|
||||||
|
<li id="accounting-recurring-{{ expense_income }}-{{ item_index }}" class="list-group-item list-group-item-action accounting-recurring-{{ expense_income }}-item" data-item-index="{{ item_index }}">
|
||||||
|
<input id="accounting-recurring-{{ expense_income }}-{{ item_index }}-no" type="hidden" name="recurring-{{ expense_income }}-{{ item_index }}-no" value="{{ item_index }}">
|
||||||
|
<input id="accounting-recurring-{{ expense_income }}-{{ item_index }}-name" type="hidden" name="recurring-{{ expense_income }}-{{ item_index }}-name" value="{{ form.name.data|accounting_default }}">
|
||||||
|
<input id="accounting-recurring-{{ expense_income }}-{{ item_index }}-account-code" type="hidden" name="recurring-{{ expense_income }}-{{ item_index }}-account_code" value="{{ form.account_code.data|accounting_default }}" data-text="{{ form.account_text|accounting_default }}">
|
||||||
|
<input id="accounting-recurring-{{ expense_income }}-{{ item_index }}-description-template" type="hidden" name="recurring-{{ expense_income }}-{{ item_index }}-description_template" value="{{ form.description_template.data|accounting_default }}">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div class="w-100">
|
||||||
|
<div id="accounting-recurring-{{ expense_income }}-{{ item_index }}-control" class="form-control accounting-clickable {% if form.all_errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-recurring-item-editor-{{ expense_income }}-modal">
|
||||||
|
<div id="accounting-recurring-{{ expense_income }}-{{ item_index }}-account-text" class="small">{{ form.account_text|accounting_default }}</div>
|
||||||
|
<div id="accounting-recurring-{{ expense_income }}-{{ item_index }}-name-text">{{ form.name.data|accounting_default }}</div>
|
||||||
|
<div id="accounting-recurring-{{ expense_income }}-{{ item_index }}-description-template-text" class="small">{{ form.description_template.data|accounting_default }}</div>
|
||||||
|
</div>
|
||||||
|
<div id="accounting-recurring-{{ expense_income }}-{{ item_index }}-error" class="invalid-feedback">{% if form.all_errors %}{{ form.all_errors[0] }}{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ms-2">
|
||||||
|
<button id="accounting-recurring-{{ expense_income }}-{{ item_index }}-delete" class="btn btn-danger rounded-circle" type="button">
|
||||||
|
<i class="fas fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{# </ul> For SonarQube not to complain about incorrect HTML #}
|
@ -0,0 +1,53 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
recurring-account-selector-modal.html: The modal of the account selector for the recurring item editor
|
||||||
|
|
||||||
|
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/22
|
||||||
|
#}
|
||||||
|
<div id="accounting-recurring-accounting-selector-{{ expense_income }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-recurring-accounting-selector-{{ expense_income }}-modal-label" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id="accounting-recurring-accounting-selector-{{ expense_income }}-modal-label">{{ A_("Select Account") }}</h1>
|
||||||
|
<button type="button" class="btn-close" data-bs-toggle="modal" data-bs-target="#accounting-recurring-item-editor-{{ expense_income }}-modal" aria-label="{{ A_("Close") }}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<input id="accounting-recurring-accounting-selector-{{ expense_income }}-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
||||||
|
<label class="input-group-text" for="accounting-recurring-accounting-selector-{{ expense_income }}-query">
|
||||||
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
|
{{ A_("Search") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul id="accounting-recurring-accounting-selector-{{ expense_income }}-option-list" class="list-group accounting-selector-list">
|
||||||
|
{% for account in accounts %}
|
||||||
|
<li id="accounting-recurring-accounting-selector-{{ expense_income }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-recurring-accounting-selector-{{ expense_income }}-option" data-code="{{ account.code }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-recurring-item-editor-{{ expense_income }}-modal">
|
||||||
|
{{ account }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p id="accounting-recurring-accounting-selector-{{ expense_income }}-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#accounting-recurring-item-editor-{{ expense_income }}-modal">{{ A_("Cancel") }}</button>
|
||||||
|
<button id="accounting-recurring-accounting-selector-{{ expense_income }}-clear" type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#accounting-recurring-item-editor-{{ expense_income }}-modal">{{ A_("Clear") }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,71 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
recurring-item-editor-modal.html: The modal of the recurring item editor
|
||||||
|
|
||||||
|
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/22
|
||||||
|
#}
|
||||||
|
<form id="accounting-recurring-item-editor-{{ expense_income }}">
|
||||||
|
<div id="accounting-recurring-item-editor-{{ expense_income }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-recurring-item-editor-{{ expense_income }}-modal-label" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id="accounting-recurring-item-editor-{{ expense_income }}-modal-label">{{ title }}</h1>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ A_("Close") }}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<input id="accounting-recurring-item-editor-{{ expense_income }}-name" class="form-control" type="text" value="" placeholder=" " required="required">
|
||||||
|
<label for="accounting-recurring-item-editor-{{ expense_income }}-name">{{ A_("Name") }}</label>
|
||||||
|
<div id="accounting-recurring-item-editor-{{ expense_income }}-name-error" class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div id="accounting-recurring-item-editor-{{ expense_income }}-account-control" class="form-control accounting-clickable accounting-material-text-field" data-bs-toggle="modal" data-bs-target="#accounting-recurring-accounting-selector-{{ expense_income }}-modal">
|
||||||
|
<label class="form-label" for="accounting-recurring-item-editor-{{ expense_income }}-account">{{ A_("Account") }}</label>
|
||||||
|
<div id="accounting-recurring-item-editor-{{ expense_income }}-account"></div>
|
||||||
|
</div>
|
||||||
|
<div id="accounting-recurring-item-editor-{{ expense_income }}-account-error" class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<input id="accounting-recurring-item-editor-{{ expense_income }}-description-template" class="form-control" type="text" value="" placeholder=" " required="required">
|
||||||
|
<label for="accounting-recurring-item-editor-{{ expense_income }}-description-template">{{ A_("Description Template") }}</label>
|
||||||
|
<div id="accounting-recurring-item-editor-{{ expense_income }}-description-template-error" class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 border-top accounting-recurring-description-template-illustration">
|
||||||
|
<p>{{ A_("Available template variables:") }}</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>{this_month_number}</code>: {{ A_("This month, as a number.") }}</li>
|
||||||
|
<li><code>{this_month_name}</code>: {{ A_("This month, in its name.") }}</li>
|
||||||
|
<li><code>{last_month_number}</code>: {{ A_("Last month, as a number.") }}</li>
|
||||||
|
<li><code>{last_month_name}</code>: {{ A_("Last month, in its name.") }}</li>
|
||||||
|
<li><code>{last_bimonthly_number}</code>: {{ A_("The previous bimonthly period, as numbers.") }}</li>
|
||||||
|
<li><code>{last_bimonthly_name}</code>: {{ A_("The previous bimonthly period, as their names.") }}</li>
|
||||||
|
</ul>
|
||||||
|
<p>{{ A_("Example:") }} <code>{{ A_("Water bill for {last_bimonthly_name}") }}</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
|
||||||
|
<button type="submit" class="btn btn-primary">{{ A_("Save") }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -22,13 +22,13 @@ First written: 2023/2/25
|
|||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<div id="accounting-material-fab-speed-dial" class="d-md-none accounting-material-fab">
|
<div id="accounting-material-fab-speed-dial" class="d-md-none accounting-material-fab">
|
||||||
<div id="accounting-material-fab-speed-dial-actions" class="d-md-none accounting-material-fab-speed-dial-group">
|
<div id="accounting-material-fab-speed-dial-actions" class="d-md-none accounting-material-fab-speed-dial-group">
|
||||||
<a class="btn rounded-pill" href="{{ url_for("accounting.journal-entry.create", journal_entry_type=report.journal_entry_types.CASH_DISBURSEMENT)|accounting_append_next }}">
|
<a class="btn rounded-pill" role="button" href="{{ url_for("accounting.journal-entry.create", journal_entry_type=report.journal_entry_types.CASH_DISBURSEMENT)|accounting_append_next }}">
|
||||||
{{ A_("Cash Disbursement") }}
|
{{ A_("Cash Disbursement") }}
|
||||||
</a>
|
</a>
|
||||||
<a class="btn rounded-pill" href="{{ url_for("accounting.journal-entry.create", journal_entry_type=report.journal_entry_types.CASH_RECEIPT)|accounting_append_next }}">
|
<a class="btn rounded-pill" role="button" href="{{ url_for("accounting.journal-entry.create", journal_entry_type=report.journal_entry_types.CASH_RECEIPT)|accounting_append_next }}">
|
||||||
{{ A_("Cash Receipt") }}
|
{{ A_("Cash Receipt") }}
|
||||||
</a>
|
</a>
|
||||||
<a class="btn rounded-pill" href="{{ url_for("accounting.journal-entry.create", journal_entry_type=report.journal_entry_types.TRANSFER)|accounting_append_next }}">
|
<a class="btn rounded-pill" role="button" href="{{ url_for("accounting.journal-entry.create", journal_entry_type=report.journal_entry_types.TRANSFER)|accounting_append_next }}">
|
||||||
{{ A_("Transfer") }}
|
{{ A_("Transfer") }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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-23 00:45+0800\n"
|
||||||
"PO-Revision-Date: 2023-03-20 23:51+0800\n"
|
"PO-Revision-Date: 2023-03-23 00:46+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,38 @@ 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/forms.py:33
|
||||||
|
#: src/accounting/static/js/journal-entry-form.js:980
|
||||||
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:430
|
||||||
|
#: src/accounting/static/js/option-form.js:530
|
||||||
|
#: src/accounting/static/js/option-form.js:796
|
||||||
|
msgid "Please select the account."
|
||||||
|
msgstr "請選擇科目。"
|
||||||
|
|
||||||
|
#: src/accounting/forms.py:44
|
||||||
|
msgid "The currency does not exist."
|
||||||
|
msgstr "沒有這個貨幣。"
|
||||||
|
|
||||||
|
#: src/accounting/forms.py:55 src/accounting/option/forms.py:42
|
||||||
|
msgid "The account does not exist."
|
||||||
|
msgstr "沒有這個科目。"
|
||||||
|
|
||||||
|
#: src/accounting/models.py:563
|
||||||
#, 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:566
|
||||||
#, 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:567
|
||||||
#, 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:700
|
||||||
#, 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 +102,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 +119,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 "順序存好了。"
|
||||||
|
|
||||||
@ -137,8 +157,11 @@ msgstr "代碼限為三個大寫英文字母。"
|
|||||||
msgid "This code is not available."
|
msgid "This code is not available."
|
||||||
msgstr "不能用這個代碼。"
|
msgstr "不能用這個代碼。"
|
||||||
|
|
||||||
#: src/accounting/currency/forms.py:62
|
#: src/accounting/currency/forms.py:62 src/accounting/option/forms.py:124
|
||||||
|
#: src/accounting/option/forms.py:148
|
||||||
#: src/accounting/static/js/currency-form.js:153
|
#: src/accounting/static/js/currency-form.js:153
|
||||||
|
#: src/accounting/static/js/option-form.js:525
|
||||||
|
#: src/accounting/static/js/option-form.js:780
|
||||||
msgid "Please fill in the name."
|
msgid "Please fill in the name."
|
||||||
msgstr "請填上名稱。"
|
msgstr "請填上名稱。"
|
||||||
|
|
||||||
@ -154,7 +177,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,32 +197,32 @@ 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 "傳票刪掉了"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:38
|
#: src/accounting/journal_entry/forms/currency.py:40
|
||||||
msgid "Please select the currency."
|
msgid "Please select the currency."
|
||||||
msgstr "請選擇貨幣。"
|
msgstr "請選擇貨幣。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:49
|
#: src/accounting/journal_entry/forms/currency.py:63
|
||||||
msgid "The currency does not exist."
|
|
||||||
msgstr "沒有這個貨幣。"
|
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:72
|
|
||||||
msgid "The currency must be the same as the original line item."
|
msgid "The currency must be the same as the original line item."
|
||||||
msgstr "貨幣需和原始分錄相同。"
|
msgstr "貨幣需和原始分錄相同。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:99
|
#: src/accounting/journal_entry/forms/currency.py:90
|
||||||
msgid "The currency must not be changed when there is offset."
|
msgid "The currency must not be changed when there is offset."
|
||||||
msgstr "抵銷過不可變更貨幣。"
|
msgstr "抵銷過不可變更貨幣。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:108
|
#: src/accounting/journal_entry/forms/currency.py:99
|
||||||
#: src/accounting/static/js/journal-entry-form.js:729
|
#: src/accounting/static/js/journal-entry-form.js:729
|
||||||
msgid "Please add some line items."
|
msgid "Please add some line items."
|
||||||
msgstr "請加上分錄。"
|
msgstr "請加上分錄。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:121
|
#: src/accounting/journal_entry/forms/currency.py:112
|
||||||
#: src/accounting/static/js/journal-entry-form.js:516
|
#: src/accounting/static/js/journal-entry-form.js:516
|
||||||
msgid "The totals of the debit and credit amounts do not match."
|
msgid "The totals of the debit and credit amounts do not match."
|
||||||
msgstr "借方貸方合計不符。 "
|
msgstr "借方貸方合計不符。 "
|
||||||
@ -224,62 +251,44 @@ msgstr "請加上貨幣。"
|
|||||||
msgid "Line items with offset cannot be deleted."
|
msgid "Line items with offset cannot be deleted."
|
||||||
msgstr "無法刪除抵銷過的分錄。"
|
msgstr "無法刪除抵銷過的分錄。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:41
|
#: src/accounting/journal_entry/forms/line_item.py:49
|
||||||
#: src/accounting/static/js/journal-entry-form.js:980
|
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:430
|
|
||||||
msgid "Please select the account."
|
|
||||||
msgstr "請選擇科目。"
|
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:52
|
|
||||||
msgid "The original line item does not exist."
|
msgid "The original line item does not exist."
|
||||||
msgstr "沒有這筆原始分錄。"
|
msgstr "沒有這筆原始分錄。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:73
|
#: src/accounting/journal_entry/forms/line_item.py:70
|
||||||
msgid "The original line item is on the same debit or credit."
|
msgid "The original line item is on the same debit or credit."
|
||||||
msgstr "原始分錄在借貸同一邊。"
|
msgstr "原始分錄在借貸同一邊。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:88
|
#: src/accounting/journal_entry/forms/line_item.py:85
|
||||||
msgid "The original line item does not need offset."
|
msgid "The original line item does not need offset."
|
||||||
msgstr "這筆原始分錄不需抵銷。"
|
msgstr "這筆原始分錄不需抵銷。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:104
|
#: src/accounting/journal_entry/forms/line_item.py:101
|
||||||
msgid "The original line item cannot be an offset item."
|
msgid "The original line item cannot be an offset item."
|
||||||
msgstr "原始分錄不可以是抵銷分錄。"
|
msgstr "原始分錄不可以是抵銷分錄。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:115
|
#: src/accounting/journal_entry/forms/line_item.py:119
|
||||||
msgid "The account does not exist."
|
|
||||||
msgstr "沒有這個科目。"
|
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:129
|
|
||||||
msgid "This account is not for debit line items."
|
|
||||||
msgstr "科目不是借方科目。"
|
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:143
|
|
||||||
msgid "This account is not for credit line items."
|
|
||||||
msgstr "科目不是貸方科目。"
|
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:161
|
|
||||||
msgid "The account must be the same as the original line item."
|
msgid "The account must be the same as the original line item."
|
||||||
msgstr "科目需和原始分錄相同。"
|
msgstr "科目需和原始分錄相同。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:179
|
#: src/accounting/journal_entry/forms/line_item.py:137
|
||||||
msgid "The account must not be changed when there is offset."
|
msgid "The account must not be changed when there is offset."
|
||||||
msgstr "抵銷過不可變更科目。"
|
msgstr "抵銷過不可變更科目。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:195
|
#: src/accounting/journal_entry/forms/line_item.py:153
|
||||||
msgid "A payable line item cannot start from debit."
|
msgid "A payable line item cannot start from debit."
|
||||||
msgstr "不可由借方新建應付款。"
|
msgstr "不可由借方新建應付款。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:211
|
#: src/accounting/journal_entry/forms/line_item.py:169
|
||||||
msgid "A receivable line item cannot start from credit."
|
msgid "A receivable line item cannot start from credit."
|
||||||
msgstr "不可由貸方新建應收款。"
|
msgstr "不可由貸方新建應收款。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:222
|
#: src/accounting/journal_entry/forms/line_item.py:180
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:455
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:455
|
||||||
msgid "Please fill in a positive amount."
|
msgid "Please fill in a positive amount."
|
||||||
msgstr "金額請填正數。"
|
msgstr "金額請填正數。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:264
|
#: src/accounting/journal_entry/forms/line_item.py:222
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:461
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:461
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -287,12 +296,64 @@ msgid ""
|
|||||||
"line item."
|
"line item."
|
||||||
msgstr "金額不可超過原始分錄凈額 %(balance)s 。"
|
msgstr "金額不可超過原始分錄凈額 %(balance)s 。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:285
|
#: src/accounting/journal_entry/forms/line_item.py:243
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:469
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:469
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The amount must not be less than the offset total %(total)s."
|
msgid "The amount must not be less than the offset total %(total)s."
|
||||||
msgstr "金額不可低於抵銷總額 %(total)s 。"
|
msgstr "金額不可低於抵銷總額 %(total)s 。"
|
||||||
|
|
||||||
|
#: src/accounting/journal_entry/forms/line_item.py:416
|
||||||
|
msgid "This account is not for debit line items."
|
||||||
|
msgstr "科目不是借方科目。"
|
||||||
|
|
||||||
|
#: src/accounting/journal_entry/forms/line_item.py:468
|
||||||
|
msgid "This account is not for credit line items."
|
||||||
|
msgstr "科目不是貸方科目。"
|
||||||
|
|
||||||
|
#: src/accounting/option/forms.py:53
|
||||||
|
msgid "This is not a current account."
|
||||||
|
msgstr "這不是流動科目。"
|
||||||
|
|
||||||
|
#: src/accounting/option/forms.py:66
|
||||||
|
msgid "You cannot select a payable account as expense."
|
||||||
|
msgstr "支出不能選應付科目。"
|
||||||
|
|
||||||
|
#: src/accounting/option/forms.py:79
|
||||||
|
msgid "You cannot select a receivable account as income."
|
||||||
|
msgstr "收入不能選應收科目。"
|
||||||
|
|
||||||
|
#: src/accounting/option/forms.py:131
|
||||||
|
msgid "This account is not for expense."
|
||||||
|
msgstr "科目不是支出科目。"
|
||||||
|
|
||||||
|
#: src/accounting/option/forms.py:137 src/accounting/option/forms.py:161
|
||||||
|
#: src/accounting/static/js/option-form.js:535
|
||||||
|
#: src/accounting/static/js/option-form.js:813
|
||||||
|
msgid "Please fill in the description template."
|
||||||
|
msgstr "請填上摘要範本。"
|
||||||
|
|
||||||
|
#: src/accounting/option/forms.py:155
|
||||||
|
msgid "This account is not for income."
|
||||||
|
msgstr "科目不是收入科目。"
|
||||||
|
|
||||||
|
#: src/accounting/option/forms.py:238
|
||||||
|
#: src/accounting/static/js/option-form.js:136
|
||||||
|
msgid "Please select the default currency."
|
||||||
|
msgstr "請選擇預設貨幣。"
|
||||||
|
|
||||||
|
#: src/accounting/option/forms.py:244
|
||||||
|
#: src/accounting/static/js/option-form.js:152
|
||||||
|
msgid "Please select the default account for the income and expenses log."
|
||||||
|
msgstr "請選擇收支帳的預設科目。"
|
||||||
|
|
||||||
|
#: src/accounting/option/views.py:79
|
||||||
|
msgid "The settings were not modified."
|
||||||
|
msgstr "設定未異動。"
|
||||||
|
|
||||||
|
#: src/accounting/option/views.py:82
|
||||||
|
msgid "The settings are saved successfully."
|
||||||
|
msgstr "設定存好了。"
|
||||||
|
|
||||||
#: src/accounting/report/period/description.py:33
|
#: src/accounting/report/period/description.py:33
|
||||||
msgid "for all time"
|
msgid "for all time"
|
||||||
msgstr "全部"
|
msgstr "全部"
|
||||||
@ -351,20 +412,17 @@ msgstr "全部"
|
|||||||
#: src/accounting/report/reports/balance_sheet.py:426
|
#: src/accounting/report/reports/balance_sheet.py:426
|
||||||
#: src/accounting/report/reports/balance_sheet.py:438
|
#: src/accounting/report/reports/balance_sheet.py:438
|
||||||
#: src/accounting/report/reports/balance_sheet.py:440
|
#: src/accounting/report/reports/balance_sheet.py:440
|
||||||
#: src/accounting/report/reports/income_expenses.py:192
|
#: src/accounting/report/reports/income_expenses.py:189
|
||||||
#: src/accounting/report/reports/income_expenses.py:429
|
#: src/accounting/report/reports/income_expenses.py:423
|
||||||
#: src/accounting/report/reports/income_statement.py:299
|
#: src/accounting/report/reports/income_statement.py:299
|
||||||
#: src/accounting/report/reports/ledger.py:171
|
#: src/accounting/report/reports/ledger.py:171
|
||||||
#: 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
|
||||||
@ -383,7 +441,7 @@ msgstr "合計"
|
|||||||
msgid "Brought forward"
|
msgid "Brought forward"
|
||||||
msgstr "前期轉入"
|
msgstr "前期轉入"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:413
|
#: src/accounting/report/reports/income_expenses.py:407
|
||||||
#: src/accounting/report/reports/journal.py:155
|
#: src/accounting/report/reports/journal.py:155
|
||||||
#: src/accounting/report/reports/ledger.py:366
|
#: src/accounting/report/reports/ledger.py:366
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form.html:50
|
#: src/accounting/templates/accounting/journal-entry/include/form.html:50
|
||||||
@ -395,11 +453,12 @@ msgstr "前期轉入"
|
|||||||
msgid "Date"
|
msgid "Date"
|
||||||
msgstr "日期"
|
msgstr "日期"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:413
|
#: src/accounting/report/reports/income_expenses.py:407
|
||||||
#: 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/option/include/recurring-item-editor-modal.html:39
|
||||||
|
#: 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
|
||||||
@ -407,7 +466,7 @@ msgstr "日期"
|
|||||||
msgid "Account"
|
msgid "Account"
|
||||||
msgstr "科目"
|
msgstr "科目"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:414
|
#: src/accounting/report/reports/income_expenses.py:408
|
||||||
#: src/accounting/report/reports/journal.py:156
|
#: src/accounting/report/reports/journal.py:156
|
||||||
#: src/accounting/report/reports/ledger.py:366
|
#: src/accounting/report/reports/ledger.py:366
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:28
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:28
|
||||||
@ -419,27 +478,27 @@ msgstr "科目"
|
|||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "摘要"
|
msgstr "摘要"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:414
|
#: src/accounting/report/reports/income_expenses.py:408
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:58
|
#: src/accounting/templates/accounting/report/income-expenses.html:58
|
||||||
msgid "Income"
|
msgid "Income"
|
||||||
msgstr "收入"
|
msgstr "收入"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:415
|
#: src/accounting/report/reports/income_expenses.py:409
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:59
|
#: src/accounting/templates/accounting/report/income-expenses.html:59
|
||||||
msgid "Expense"
|
msgid "Expense"
|
||||||
msgstr "支出"
|
msgstr "支出"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:415
|
#: src/accounting/report/reports/income_expenses.py:409
|
||||||
#: src/accounting/report/reports/ledger.py:368
|
#: src/accounting/report/reports/ledger.py:368
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:60
|
#: src/accounting/templates/accounting/report/income-expenses.html:60
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:60
|
#: src/accounting/templates/accounting/report/ledger.html:60
|
||||||
msgid "Balance"
|
msgid "Balance"
|
||||||
msgstr "餘額"
|
msgstr "餘額"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:416
|
#: src/accounting/report/reports/income_expenses.py:410
|
||||||
#: 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 +534,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 +545,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 +557,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
|
||||||
@ -508,10 +565,6 @@ msgstr "借方"
|
|||||||
msgid "Credit"
|
msgid "Credit"
|
||||||
msgstr "貸方"
|
msgstr "貸方"
|
||||||
|
|
||||||
#: src/accounting/report/utils/ie_account.py:64
|
|
||||||
msgid "current assets and liabilities"
|
|
||||||
msgstr "流動資產與負債"
|
|
||||||
|
|
||||||
#: src/accounting/report/utils/report_chooser.py:81
|
#: src/accounting/report/utils/report_chooser.py:81
|
||||||
#: src/accounting/templates/accounting/account/include/form.html:98
|
#: src/accounting/templates/accounting/account/include/form.html:98
|
||||||
#: src/accounting/templates/accounting/account/list.html:40
|
#: src/accounting/templates/accounting/account/list.html:40
|
||||||
@ -519,6 +572,7 @@ msgstr "流動資產與負債"
|
|||||||
#: src/accounting/templates/accounting/currency/list.html:40
|
#: src/accounting/templates/accounting/currency/list.html:40
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:34
|
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:34
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:34
|
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:34
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-account-selector-modal.html:34
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:27
|
#: src/accounting/templates/accounting/report/include/search-modal.html:27
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:33
|
#: src/accounting/templates/accounting/report/include/search-modal.html:33
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:38
|
#: src/accounting/templates/accounting/report/include/search-modal.html:38
|
||||||
@ -555,25 +609,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."
|
||||||
@ -623,74 +725,82 @@ msgstr "新增科目"
|
|||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:31
|
#: src/accounting/templates/accounting/journal-entry/include/detail.html:31
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form.html:38
|
#: src/accounting/templates/accounting/journal-entry/include/form.html:38
|
||||||
#: src/accounting/templates/accounting/journal-entry/order.html:36
|
#: src/accounting/templates/accounting/journal-entry/order.html:36
|
||||||
|
#: src/accounting/templates/accounting/option/form.html:36
|
||||||
msgid "Back"
|
msgid "Back"
|
||||||
msgstr "回上頁"
|
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"
|
#: src/accounting/templates/accounting/option/detail.html:31
|
||||||
msgstr "設定"
|
msgid "Edit"
|
||||||
|
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
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:28
|
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:28
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:27
|
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:27
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-account-selector-modal.html:27
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:28
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:27
|
#: src/accounting/templates/accounting/report/include/period-chooser.html:27
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:28
|
#: src/accounting/templates/accounting/report/include/search-modal.html:28
|
||||||
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/option/include/recurring-account-selector-modal.html:48
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:65
|
||||||
#: 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 +824,9 @@ 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/option/include/form-recurring-expense-income.html:37
|
||||||
#: 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 +838,8 @@ 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/option/include/recurring-account-selector-modal.html:45
|
||||||
#: 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,10 +858,12 @@ 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
|
||||||
|
#: src/accounting/templates/accounting/option/form.html:80
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:66
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr "儲存"
|
msgstr "儲存"
|
||||||
|
|
||||||
@ -779,6 +890,7 @@ msgstr "選擇基本科目"
|
|||||||
#: src/accounting/templates/accounting/account/include/form.html:114
|
#: src/accounting/templates/accounting/account/include/form.html:114
|
||||||
#: src/accounting/templates/accounting/account/include/form.html:116
|
#: src/accounting/templates/accounting/account/include/form.html:116
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:50
|
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:50
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-account-selector-modal.html:49
|
||||||
msgid "Clear"
|
msgid "Clear"
|
||||||
msgstr "清除"
|
msgstr "清除"
|
||||||
|
|
||||||
@ -790,11 +902,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 "你確定要刪掉這個貨幣嗎?"
|
||||||
|
|
||||||
@ -812,6 +924,7 @@ msgid "Code"
|
|||||||
msgstr "代碼"
|
msgstr "代碼"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/currency/include/form.html:50
|
#: src/accounting/templates/accounting/currency/include/form.html:50
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:33
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr "名稱"
|
msgstr "名稱"
|
||||||
|
|
||||||
@ -835,6 +948,12 @@ msgstr "基本科目"
|
|||||||
msgid "Currencies"
|
msgid "Currencies"
|
||||||
msgstr "貨幣"
|
msgstr "貨幣"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/include/nav.html:58
|
||||||
|
#: src/accounting/templates/accounting/option/detail.html:24
|
||||||
|
#: src/accounting/templates/accounting/option/form.html:29
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr "設定"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/include/pagination.html:23
|
#: src/accounting/templates/accounting/include/pagination.html:23
|
||||||
msgid "Page navigation"
|
msgid "Page navigation"
|
||||||
msgstr "分頁瀏覽"
|
msgstr "分頁瀏覽"
|
||||||
@ -850,14 +969,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 "內容"
|
||||||
|
|
||||||
@ -869,6 +988,7 @@ msgid "Editing %(journal_entry)s"
|
|||||||
msgstr "編輯%(journal_entry)s"
|
msgstr "編輯%(journal_entry)s"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:26
|
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:26
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-account-selector-modal.html:26
|
||||||
msgid "Select Account"
|
msgid "Select Account"
|
||||||
msgstr "選擇科目"
|
msgstr "選擇科目"
|
||||||
|
|
||||||
@ -893,8 +1013,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 +1042,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 +1068,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 +1082,24 @@ msgstr "分錄內容"
|
|||||||
msgid "Original Line Item"
|
msgid "Original Line Item"
|
||||||
msgstr "原始分錄"
|
msgstr "原始分錄"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/journal-entry/include/order-journal-entry.html:25
|
||||||
|
#: 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:27
|
||||||
|
#: 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:29
|
||||||
|
#: 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 "選擇原始分錄"
|
||||||
@ -974,6 +1112,68 @@ msgstr "新增現金收入傳票"
|
|||||||
msgid "Add a New Transfer Journal Entry"
|
msgid "Add a New Transfer Journal Entry"
|
||||||
msgstr "新增轉帳傳票"
|
msgstr "新增轉帳傳票"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/detail.html:43
|
||||||
|
#: src/accounting/templates/accounting/option/form.html:51
|
||||||
|
msgid "Default Currency"
|
||||||
|
msgstr "預設貨幣"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/detail.html:48
|
||||||
|
#: src/accounting/templates/accounting/option/form.html:61
|
||||||
|
msgid "Default Account for the Income and Expenses Log"
|
||||||
|
msgstr "收支帳預設科目"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/detail.html:52
|
||||||
|
#: src/accounting/templates/accounting/option/form.html:66
|
||||||
|
#: src/accounting/templates/accounting/option/form.html:92
|
||||||
|
msgid "Recurring Expense"
|
||||||
|
msgstr "常用支出"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/detail.html:58
|
||||||
|
#: src/accounting/templates/accounting/option/form.html:72
|
||||||
|
#: src/accounting/templates/accounting/option/form.html:96
|
||||||
|
msgid "Recurring Income"
|
||||||
|
msgstr "常用收入"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:47
|
||||||
|
msgid "Description Template"
|
||||||
|
msgstr "摘要範本"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:52
|
||||||
|
msgid "Available template variables:"
|
||||||
|
msgstr "範本變數說明:"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:54
|
||||||
|
msgid "This month, as a number."
|
||||||
|
msgstr "這個月的數字。"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:55
|
||||||
|
msgid "This month, in its name."
|
||||||
|
msgstr "這個月的名稱。"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:56
|
||||||
|
msgid "Last month, as a number."
|
||||||
|
msgstr "上個月的數字。"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:57
|
||||||
|
msgid "Last month, in its name."
|
||||||
|
msgstr "上個月的名稱。"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:58
|
||||||
|
msgid "The previous bimonthly period, as numbers."
|
||||||
|
msgstr "前個雙月期的數字。"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:59
|
||||||
|
msgid "The previous bimonthly period, as their names."
|
||||||
|
msgstr "前個雙月期的名稱。"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:61
|
||||||
|
msgid "Example:"
|
||||||
|
msgstr "範例:"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:61
|
||||||
|
msgid "Water bill for {last_bimonthly_name}"
|
||||||
|
msgstr "水費{last_bimonthly_number}月"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:29
|
#: src/accounting/templates/accounting/report/balance-sheet.html:29
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:49
|
#: src/accounting/templates/accounting/report/balance-sheet.html:49
|
||||||
#, python-format
|
#, python-format
|
||||||
@ -1007,21 +1207,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,15 +1227,23 @@ 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"
|
||||||
msgstr "下載"
|
msgstr "下載"
|
||||||
|
|
||||||
|
#: src/accounting/utils/current_account.py:65
|
||||||
|
msgid "current assets and liabilities"
|
||||||
|
msgstr "流動資產與負債"
|
||||||
|
|
||||||
#: src/accounting/utils/pagination.py:206
|
#: src/accounting/utils/pagination.py:206
|
||||||
msgctxt "Pagination|"
|
msgctxt "Pagination|"
|
||||||
msgid "Previous"
|
msgid "Previous"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# The Mia! Accounting Flask Project.
|
# The Mia! Accounting Flask Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023 imacat.
|
||||||
#
|
#
|
||||||
@ -14,28 +14,29 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The pseudo account for the income and expenses log.
|
"""The current assets and liabilities account.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
from flask import current_app
|
from accounting import db
|
||||||
|
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from accounting.models import Account
|
from accounting.models import Account
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
class IncomeExpensesAccount:
|
class CurrentAccount:
|
||||||
"""The pseudo account for the income and expenses log."""
|
"""A current assets and liabilities account."""
|
||||||
CURRENT_AL_CODE: str = "0000-000"
|
CURRENT_AL_CODE: str = "0000-000"
|
||||||
"""The account code for the current assets and liabilities."""
|
"""The account code for all current assets and liabilities."""
|
||||||
|
|
||||||
def __init__(self, account: Account | None = None):
|
def __init__(self, account: Account | None = None):
|
||||||
"""Constructs the pseudo account for the income and expenses log.
|
"""Constructs the current assets and liabilities account.
|
||||||
|
|
||||||
:param account: The actual account.
|
:param account: The actual account.
|
||||||
"""
|
"""
|
||||||
self.account: Account | None = account
|
self.account: Account | None = account
|
||||||
|
"""The actual account."""
|
||||||
self.id: int = -1 if account is None else account.id
|
self.id: int = -1 if account is None else account.id
|
||||||
"""The ID."""
|
"""The ID."""
|
||||||
self.code: str = "" if account is None else account.code
|
self.code: str = "" if account is None else account.code
|
||||||
@ -54,9 +55,9 @@ class IncomeExpensesAccount:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def current_assets_and_liabilities(cls) -> t.Self:
|
def current_assets_and_liabilities(cls) -> t.Self:
|
||||||
"""Returns the pseudo account for current assets and liabilities.
|
"""Returns the pseudo account for all current assets and liabilities.
|
||||||
|
|
||||||
:return: The pseudo account for current assets and liabilities.
|
:return: The pseudo account for all current assets and liabilities.
|
||||||
"""
|
"""
|
||||||
account: cls = cls()
|
account: cls = cls()
|
||||||
account.id = 0
|
account.id = 0
|
||||||
@ -65,22 +66,28 @@ class IncomeExpensesAccount:
|
|||||||
account.str = account.title
|
account.str = account.title
|
||||||
return account
|
return account
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def accounts(cls) -> list[t.Self]:
|
||||||
|
"""Returns the current assets and liabilities accounts.
|
||||||
|
|
||||||
def default_ie_account_code() -> str:
|
:return: The current assets and liabilities accounts.
|
||||||
"""Returns the default account code for the income and expenses log.
|
"""
|
||||||
|
accounts: list[cls] = [cls.current_assets_and_liabilities()]
|
||||||
|
accounts.extend([CurrentAccount(x)
|
||||||
|
for x in db.session.query(Account)
|
||||||
|
.filter(cls.sql_condition())
|
||||||
|
.order_by(Account.base_code, Account.no)])
|
||||||
|
return accounts
|
||||||
|
|
||||||
:return: The default account code for the income and expenses log.
|
@classmethod
|
||||||
"""
|
def sql_condition(cls) -> sa.BinaryExpression:
|
||||||
with current_app.app_context():
|
"""Returns the SQL condition for the current assets and liabilities
|
||||||
return current_app.config.get("DEFAULT_IE_ACCOUNT", Account.CASH_CODE)
|
accounts.
|
||||||
|
|
||||||
|
:return: The SQL condition for the current assets and liabilities
|
||||||
def default_ie_account() -> IncomeExpensesAccount:
|
accounts.
|
||||||
"""Returns the default account for the income and expenses log.
|
"""
|
||||||
|
return sa.or_(Account.base_code.startswith("11"),
|
||||||
:return: The default account for the income and expenses log.
|
Account.base_code.startswith("12"),
|
||||||
"""
|
Account.base_code.startswith("21"),
|
||||||
code: str = default_ie_account_code()
|
Account.base_code.startswith("22"))
|
||||||
if code == IncomeExpensesAccount.CURRENT_AL_CODE:
|
|
||||||
return IncomeExpensesAccount.current_assets_and_liabilities()
|
|
||||||
return IncomeExpensesAccount(Account.find_by_code(code))
|
|
225
src/accounting/utils/options.py
Normal file
225
src/accounting/utils/options.py
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
# The Mia! Accounting Flask Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The getter and setter for the option management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from accounting import db
|
||||||
|
from accounting.models import Option, Account, Currency
|
||||||
|
from accounting.utils.current_account import CurrentAccount
|
||||||
|
from accounting.utils.user import get_current_user_pk
|
||||||
|
|
||||||
|
|
||||||
|
class RecurringItem:
|
||||||
|
"""A recurring item."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, account_code: str,
|
||||||
|
description_template: str):
|
||||||
|
"""Constructs the recurring item.
|
||||||
|
|
||||||
|
:param name: The name.
|
||||||
|
:param account_code: The account code.
|
||||||
|
:param description_template: The description template.
|
||||||
|
"""
|
||||||
|
self.name: str = name
|
||||||
|
self.account_code: str = account_code
|
||||||
|
self.description_template: str = description_template
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_text(self) -> str:
|
||||||
|
"""Returns the account text.
|
||||||
|
|
||||||
|
:return: The account text.
|
||||||
|
"""
|
||||||
|
return str(Account.find_by_code(self.account_code))
|
||||||
|
|
||||||
|
|
||||||
|
class Recurring:
|
||||||
|
"""The recurring expenses or incomes."""
|
||||||
|
|
||||||
|
def __init__(self, data: dict[str, list[tuple[str, str, str]]]):
|
||||||
|
"""Constructs the recurring item.
|
||||||
|
|
||||||
|
:param data: The data.
|
||||||
|
"""
|
||||||
|
self.expenses: list[RecurringItem] \
|
||||||
|
= [RecurringItem(x[0], x[1], x[2]) for x in data["expense"]]
|
||||||
|
self.incomes: list[RecurringItem] \
|
||||||
|
= [RecurringItem(x[0], x[1], x[2]) for x in data["income"]]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def codes(self) -> set[str]:
|
||||||
|
"""Returns all the account codes.
|
||||||
|
|
||||||
|
:return: All the account codes.
|
||||||
|
"""
|
||||||
|
return {x.account_code for x in self.expenses + self.incomes}
|
||||||
|
|
||||||
|
|
||||||
|
class Options:
|
||||||
|
"""The options."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Constructs the options."""
|
||||||
|
self.is_modified: bool = False
|
||||||
|
"""Whether the options were modified."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_currency_code(self) -> str:
|
||||||
|
"""Returns the default currency code.
|
||||||
|
|
||||||
|
:return: The default currency code.
|
||||||
|
"""
|
||||||
|
return self.__get_option("default_currency_code", "USD")
|
||||||
|
|
||||||
|
@default_currency_code.setter
|
||||||
|
def default_currency_code(self, value: str) -> None:
|
||||||
|
"""Sets the default currency code.
|
||||||
|
|
||||||
|
:param value: The default currency code.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
self.__set_option("default_currency_code", value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_currency_text(self) -> str:
|
||||||
|
"""Returns the text of the default currency code.
|
||||||
|
|
||||||
|
:return: The text of the default currency code.
|
||||||
|
"""
|
||||||
|
return str(db.session.get(Currency, self.default_currency_code))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_ie_account_code(self) -> str:
|
||||||
|
"""Returns the default account code for the income and expenses log.
|
||||||
|
|
||||||
|
:return: The default account code for the income and expenses log.
|
||||||
|
"""
|
||||||
|
return self.__get_option("default_ie_account", Account.CASH_CODE)
|
||||||
|
|
||||||
|
@default_ie_account_code.setter
|
||||||
|
def default_ie_account_code(self, value: str) -> None:
|
||||||
|
"""Sets the default account code for the income and expenses log.
|
||||||
|
|
||||||
|
:param value: The default account code for the income and expenses log.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
self.__set_option("default_ie_account", value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_ie_account_code_text(self) -> str:
|
||||||
|
"""Returns the text of the default currency code.
|
||||||
|
|
||||||
|
:return: The text of the default currency code.
|
||||||
|
"""
|
||||||
|
code: str = self.default_ie_account_code
|
||||||
|
if code == CurrentAccount.CURRENT_AL_CODE:
|
||||||
|
return str(CurrentAccount.current_assets_and_liabilities())
|
||||||
|
return str(CurrentAccount(Account.find_by_code(code)))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_ie_account(self) -> CurrentAccount:
|
||||||
|
"""Returns the default account code for the income and expenses log.
|
||||||
|
|
||||||
|
:return: The default account code for the income and expenses log.
|
||||||
|
"""
|
||||||
|
if self.default_ie_account_code \
|
||||||
|
== CurrentAccount.CURRENT_AL_CODE:
|
||||||
|
return CurrentAccount.current_assets_and_liabilities()
|
||||||
|
return CurrentAccount(
|
||||||
|
Account.find_by_code(self.default_ie_account_code))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recurring_data(self) -> dict[str, list[tuple[str, str, str]]]:
|
||||||
|
"""Returns the data of the recurring expenses and incomes.
|
||||||
|
|
||||||
|
:return: The data of the recurring expenses and incomes.
|
||||||
|
"""
|
||||||
|
json_data: str | None = self.__get_option("recurring")
|
||||||
|
if json_data is None:
|
||||||
|
return {"expense": [], "income": []}
|
||||||
|
return json.loads(json_data)
|
||||||
|
|
||||||
|
@recurring_data.setter
|
||||||
|
def recurring_data(self,
|
||||||
|
value: dict[str, list[tuple[str, str, str]]]) -> None:
|
||||||
|
"""Sets the data of the recurring expenses and incomes.
|
||||||
|
|
||||||
|
:param value: The data of the recurring expenses and incomes.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
self.__set_option("recurring", json.dumps(value, ensure_ascii=False,
|
||||||
|
separators=(",", ":")))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recurring(self) -> Recurring:
|
||||||
|
"""Returns the recurring expenses and incomes.
|
||||||
|
|
||||||
|
:return: The recurring expenses and incomes.
|
||||||
|
"""
|
||||||
|
return Recurring(self.recurring_data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __get_option(name: str, default: str | None = None) -> str:
|
||||||
|
"""Returns the value of an option.
|
||||||
|
|
||||||
|
:param name: The name.
|
||||||
|
:param default: The default value when the value does not exist.
|
||||||
|
:return: The value.
|
||||||
|
"""
|
||||||
|
option: Option | None = db.session.get(Option, name)
|
||||||
|
if option is None:
|
||||||
|
return default
|
||||||
|
return option.value
|
||||||
|
|
||||||
|
def __set_option(self, name: str, value: str) -> None:
|
||||||
|
"""Sets the value of an option.
|
||||||
|
|
||||||
|
:param name: The name.
|
||||||
|
:param value: The value.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
option: Option | None = db.session.get(Option, name)
|
||||||
|
if option is None:
|
||||||
|
current_user_pk: int = get_current_user_pk()
|
||||||
|
db.session.add(Option(name=name,
|
||||||
|
value=value,
|
||||||
|
created_by_id=current_user_pk,
|
||||||
|
updated_by_id=current_user_pk))
|
||||||
|
self.is_modified = True
|
||||||
|
return
|
||||||
|
if option.value == value:
|
||||||
|
return
|
||||||
|
option.value = value
|
||||||
|
option.updated_by_id = get_current_user_pk()
|
||||||
|
option.updated_at = sa.func.now()
|
||||||
|
self.is_modified = True
|
||||||
|
|
||||||
|
def commit(self) -> None:
|
||||||
|
"""Commits the options to the database.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
db.session.commit()
|
||||||
|
self.is_modified = False
|
||||||
|
|
||||||
|
|
||||||
|
options: Options = Options()
|
||||||
|
"""The options."""
|
@ -63,10 +63,13 @@ data."""
|
|||||||
__can_edit_func: t.Callable[[], bool] = lambda: True
|
__can_edit_func: t.Callable[[], bool] = lambda: True
|
||||||
"""The callback that returns whether the current user can edit the accounting
|
"""The callback that returns whether the current user can edit the accounting
|
||||||
data."""
|
data."""
|
||||||
|
__can_admin_func: t.Callable[[], bool] = lambda: True
|
||||||
|
"""The callback that returns whether the current user can administrate the
|
||||||
|
accounting settings."""
|
||||||
|
|
||||||
|
|
||||||
def can_view() -> bool:
|
def can_view() -> bool:
|
||||||
"""Returns whether the current user can view the account data.
|
"""Returns whether the current user can view the accounting data.
|
||||||
|
|
||||||
:return: True if the current user can view the accounting data, or False
|
:return: True if the current user can view the accounting data, or False
|
||||||
otherwise.
|
otherwise.
|
||||||
@ -75,7 +78,7 @@ def can_view() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def can_edit() -> bool:
|
def can_edit() -> bool:
|
||||||
"""Returns whether the current user can edit the account data.
|
"""Returns whether the current user can edit the accounting data.
|
||||||
|
|
||||||
The user has to log in.
|
The user has to log in.
|
||||||
|
|
||||||
@ -87,6 +90,20 @@ def can_edit() -> bool:
|
|||||||
return __can_edit_func()
|
return __can_edit_func()
|
||||||
|
|
||||||
|
|
||||||
|
def can_admin() -> bool:
|
||||||
|
"""Returns whether the current user can administrate the accounting
|
||||||
|
settings.
|
||||||
|
|
||||||
|
The user has to log in.
|
||||||
|
|
||||||
|
:return: True if the current user can administrate the accounting settings,
|
||||||
|
or False otherwise.
|
||||||
|
"""
|
||||||
|
if get_current_user() is None:
|
||||||
|
return False
|
||||||
|
return __can_admin_func()
|
||||||
|
|
||||||
|
|
||||||
def init_app(bp: Blueprint, user_utils: UserUtilityInterface) -> None:
|
def init_app(bp: Blueprint, user_utils: UserUtilityInterface) -> None:
|
||||||
"""Initializes the application.
|
"""Initializes the application.
|
||||||
|
|
||||||
@ -94,8 +111,10 @@ def init_app(bp: Blueprint, user_utils: UserUtilityInterface) -> None:
|
|||||||
:param user_utils: The user utilities.
|
:param user_utils: The user utilities.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
global __can_view_func, __can_edit_func
|
global __can_view_func, __can_edit_func, __can_admin_func
|
||||||
__can_view_func = user_utils.can_view
|
__can_view_func = user_utils.can_view
|
||||||
__can_edit_func = user_utils.can_edit
|
__can_edit_func = user_utils.can_edit
|
||||||
|
__can_admin_func = user_utils.can_admin
|
||||||
bp.add_app_template_global(user_utils.can_view, "accounting_can_view")
|
bp.add_app_template_global(user_utils.can_view, "accounting_can_view")
|
||||||
bp.add_app_template_global(user_utils.can_edit, "accounting_can_edit")
|
bp.add_app_template_global(user_utils.can_edit, "accounting_can_edit")
|
||||||
|
bp.add_app_template_global(user_utils.can_admin, "accounting_can_admin")
|
||||||
|
@ -50,6 +50,15 @@ class UserUtilityInterface(t.Generic[T], ABC):
|
|||||||
data, or False otherwise.
|
data, or False otherwise.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def can_admin(self) -> bool:
|
||||||
|
"""Returns whether the currently logged-in user can administrate the
|
||||||
|
accounting settings.
|
||||||
|
|
||||||
|
:return: True if the currently logged-in user can administrate the
|
||||||
|
accounting settings, or False otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def cls(self) -> t.Type[T]:
|
def cls(self) -> t.Type[T]:
|
||||||
|
@ -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)
|
||||||
|
@ -309,7 +309,7 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(len(currencies[2].credit), 2)
|
self.assertEqual(len(currencies[2].credit), 2)
|
||||||
self.assertEqual(currencies[2].credit[0].no, 6)
|
self.assertEqual(currencies[2].credit[0].no, 6)
|
||||||
self.assertEqual(currencies[2].credit[0].account.code,
|
self.assertEqual(currencies[2].credit[0].account.code,
|
||||||
Accounts.RENT)
|
Accounts.RENT_INCOME)
|
||||||
self.assertEqual(currencies[2].credit[1].no, 7)
|
self.assertEqual(currencies[2].credit[1].no, 7)
|
||||||
self.assertEqual(currencies[2].credit[1].account.code,
|
self.assertEqual(currencies[2].credit[1].account.code,
|
||||||
Accounts.DONATION)
|
Accounts.DONATION)
|
||||||
@ -441,7 +441,7 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
|
|||||||
self.assertNotIn(currencies1[0].credit[1].id, old_id)
|
self.assertNotIn(currencies1[0].credit[1].id, old_id)
|
||||||
self.assertEqual(currencies1[0].credit[1].no, 2)
|
self.assertEqual(currencies1[0].credit[1].no, 2)
|
||||||
self.assertEqual(currencies1[0].credit[1].account.code,
|
self.assertEqual(currencies1[0].credit[1].account.code,
|
||||||
Accounts.RENT)
|
Accounts.RENT_INCOME)
|
||||||
|
|
||||||
self.assertEqual(currencies1[1].code, "EUR")
|
self.assertEqual(currencies1[1].code, "EUR")
|
||||||
self.assertEqual(len(currencies1[1].debit), 1)
|
self.assertEqual(len(currencies1[1].debit), 1)
|
||||||
@ -457,7 +457,7 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
|
|||||||
currencies0[2].credit[0].id)
|
currencies0[2].credit[0].id)
|
||||||
self.assertEqual(currencies1[1].credit[0].no, 3)
|
self.assertEqual(currencies1[1].credit[0].no, 3)
|
||||||
self.assertEqual(currencies1[1].credit[0].account.code,
|
self.assertEqual(currencies1[1].credit[0].account.code,
|
||||||
Accounts.RENT)
|
Accounts.RENT_INCOME)
|
||||||
self.assertEqual(currencies1[1].credit[1].id,
|
self.assertEqual(currencies1[1].credit[1].id,
|
||||||
currencies0[2].credit[1].id)
|
currencies0[2].credit[1].id)
|
||||||
self.assertEqual(currencies1[1].credit[1].no, 4)
|
self.assertEqual(currencies1[1].credit[1].no, 4)
|
||||||
@ -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,
|
||||||
@ -1531,7 +1562,7 @@ class TransferJournalEntryTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(len(currencies[2].credit), 2)
|
self.assertEqual(len(currencies[2].credit), 2)
|
||||||
self.assertEqual(currencies[2].credit[0].no, 6)
|
self.assertEqual(currencies[2].credit[0].no, 6)
|
||||||
self.assertEqual(currencies[2].credit[0].account.code,
|
self.assertEqual(currencies[2].credit[0].account.code,
|
||||||
Accounts.RENT)
|
Accounts.RENT_INCOME)
|
||||||
self.assertEqual(currencies[2].credit[1].no, 7)
|
self.assertEqual(currencies[2].credit[1].no, 7)
|
||||||
self.assertEqual(currencies[2].credit[1].account.code,
|
self.assertEqual(currencies[2].credit[1].account.code,
|
||||||
Accounts.DONATION)
|
Accounts.DONATION)
|
||||||
@ -1697,7 +1728,7 @@ class TransferJournalEntryTestCase(unittest.TestCase):
|
|||||||
self.assertNotIn(currencies1[0].credit[1].id, old_id)
|
self.assertNotIn(currencies1[0].credit[1].id, old_id)
|
||||||
self.assertEqual(currencies1[0].credit[1].no, 2)
|
self.assertEqual(currencies1[0].credit[1].no, 2)
|
||||||
self.assertEqual(currencies1[0].credit[1].account.code,
|
self.assertEqual(currencies1[0].credit[1].account.code,
|
||||||
Accounts.RENT)
|
Accounts.RENT_INCOME)
|
||||||
|
|
||||||
self.assertEqual(currencies1[1].code, "EUR")
|
self.assertEqual(currencies1[1].code, "EUR")
|
||||||
self.assertEqual(len(currencies1[1].debit), 2)
|
self.assertEqual(len(currencies1[1].debit), 2)
|
||||||
@ -1717,7 +1748,7 @@ class TransferJournalEntryTestCase(unittest.TestCase):
|
|||||||
currencies0[2].credit[0].id)
|
currencies0[2].credit[0].id)
|
||||||
self.assertEqual(currencies1[1].credit[0].no, 3)
|
self.assertEqual(currencies1[1].credit[0].no, 3)
|
||||||
self.assertEqual(currencies1[1].credit[0].account.code,
|
self.assertEqual(currencies1[1].credit[0].account.code,
|
||||||
Accounts.RENT)
|
Accounts.RENT_INCOME)
|
||||||
self.assertEqual(currencies1[1].credit[1].id,
|
self.assertEqual(currencies1[1].credit[1].id,
|
||||||
currencies0[2].credit[1].id)
|
currencies0[2].credit[1].id)
|
||||||
self.assertEqual(currencies1[1].credit[1].no, 4)
|
self.assertEqual(currencies1[1].credit[1].no, 4)
|
||||||
@ -1883,7 +1914,7 @@ class TransferJournalEntryTestCase(unittest.TestCase):
|
|||||||
self.assertNotIn(currencies1[0].credit[1].id, old_id)
|
self.assertNotIn(currencies1[0].credit[1].id, old_id)
|
||||||
self.assertEqual(currencies1[0].credit[1].no, 2)
|
self.assertEqual(currencies1[0].credit[1].no, 2)
|
||||||
self.assertEqual(currencies1[0].credit[1].account.code,
|
self.assertEqual(currencies1[0].credit[1].account.code,
|
||||||
Accounts.RENT)
|
Accounts.RENT_INCOME)
|
||||||
|
|
||||||
self.assertEqual(currencies1[1].code, "EUR")
|
self.assertEqual(currencies1[1].code, "EUR")
|
||||||
self.assertEqual(len(currencies1[1].debit), 1)
|
self.assertEqual(len(currencies1[1].debit), 1)
|
||||||
@ -1899,7 +1930,7 @@ class TransferJournalEntryTestCase(unittest.TestCase):
|
|||||||
currencies0[2].credit[0].id)
|
currencies0[2].credit[0].id)
|
||||||
self.assertEqual(currencies1[1].credit[0].no, 3)
|
self.assertEqual(currencies1[1].credit[0].no, 3)
|
||||||
self.assertEqual(currencies1[1].credit[0].account.code,
|
self.assertEqual(currencies1[1].credit[0].account.code,
|
||||||
Accounts.RENT)
|
Accounts.RENT_INCOME)
|
||||||
self.assertEqual(currencies1[1].credit[1].id,
|
self.assertEqual(currencies1[1].credit[1].id,
|
||||||
currencies0[2].credit[1].id)
|
currencies0[2].credit[1].id)
|
||||||
self.assertEqual(currencies1[1].credit[1].no, 4)
|
self.assertEqual(currencies1[1].credit[1].no, 4)
|
||||||
|
417
tests/test_option.py
Normal file
417
tests/test_option.py
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
# The Mia! Accounting Flask Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The test for the options.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from click.testing import Result
|
||||||
|
from flask import Flask
|
||||||
|
from flask.testing import FlaskCliRunner
|
||||||
|
|
||||||
|
from test_site import db
|
||||||
|
from testlib import create_test_app, get_client
|
||||||
|
from testlib_journal_entry import NEXT_URI, Accounts
|
||||||
|
from testlib_offset import TestData
|
||||||
|
|
||||||
|
PREFIX: str = "/accounting/options"
|
||||||
|
"""The URL prefix for the option management."""
|
||||||
|
DETAIL_URI: str = f"{PREFIX}?next=%2F_next"
|
||||||
|
"""THE URI for the option detail."""
|
||||||
|
EDIT_URI: str = f"{PREFIX}/edit?next=%2F_next"
|
||||||
|
"""THE URI for the form to edit the options."""
|
||||||
|
UPDATE_URI: str = f"{PREFIX}/update"
|
||||||
|
"""THE URI to update the options."""
|
||||||
|
|
||||||
|
|
||||||
|
class OptionTestCase(unittest.TestCase):
|
||||||
|
"""The option test case."""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
"""Sets up the test.
|
||||||
|
This is run once per test.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
self.app: Flask = create_test_app()
|
||||||
|
|
||||||
|
runner: FlaskCliRunner = self.app.test_cli_runner()
|
||||||
|
with self.app.app_context():
|
||||||
|
from accounting.models import BaseAccount, Option
|
||||||
|
result: Result
|
||||||
|
result = runner.invoke(args="init-db")
|
||||||
|
self.assertEqual(result.exit_code, 0)
|
||||||
|
if BaseAccount.query.first() is None:
|
||||||
|
result = runner.invoke(args="accounting-init-base")
|
||||||
|
self.assertEqual(result.exit_code, 0)
|
||||||
|
result = runner.invoke(args=["accounting-init-currencies",
|
||||||
|
"-u", "editor"])
|
||||||
|
self.assertEqual(result.exit_code, 0)
|
||||||
|
result = runner.invoke(args=["accounting-init-accounts",
|
||||||
|
"-u", "editor"])
|
||||||
|
self.assertEqual(result.exit_code, 0)
|
||||||
|
Option.query.delete()
|
||||||
|
|
||||||
|
self.client, self.csrf_token = get_client(self.app, "editor")
|
||||||
|
self.data: TestData = TestData(self.app, self.client, self.csrf_token)
|
||||||
|
|
||||||
|
def test_nobody(self) -> None:
|
||||||
|
"""Test the permission as nobody.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
client, csrf_token = get_client(self.app, "nobody")
|
||||||
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = client.get(DETAIL_URI)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
response = client.get(EDIT_URI)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_viewer(self) -> None:
|
||||||
|
"""Test the permission as viewer.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
client, csrf_token = get_client(self.app, "viewer")
|
||||||
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = client.get(DETAIL_URI)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
response = client.get(EDIT_URI)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_editor2(self) -> None:
|
||||||
|
"""Test the permission as non-administrator.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
client, csrf_token = get_client(self.app, "editor2")
|
||||||
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = client.get(DETAIL_URI)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
response = client.get(EDIT_URI)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_editor(self) -> None:
|
||||||
|
"""Test the permission as administrator.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = self.client.get(DETAIL_URI)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(EDIT_URI)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(UPDATE_URI, data=self.__get_form())
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
||||||
|
|
||||||
|
def test_set(self) -> None:
|
||||||
|
"""Test to set the options.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
from accounting.utils.options import options
|
||||||
|
form: dict[str, str]
|
||||||
|
response: httpx.Response
|
||||||
|
|
||||||
|
# Empty currency code
|
||||||
|
form = self.__get_form()
|
||||||
|
form["default_currency_code"] = " "
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], EDIT_URI)
|
||||||
|
|
||||||
|
# Non-existing currency code
|
||||||
|
form = self.__get_form()
|
||||||
|
form["default_currency_code"] = "ZZZ"
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], EDIT_URI)
|
||||||
|
|
||||||
|
# Empty current account
|
||||||
|
form = self.__get_form()
|
||||||
|
form["default_ie_account_code"] = " "
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], EDIT_URI)
|
||||||
|
|
||||||
|
# Non-existing current account
|
||||||
|
form = self.__get_form()
|
||||||
|
form["default_ie_account_code"] = "9999-999"
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], EDIT_URI)
|
||||||
|
|
||||||
|
# Not a current account
|
||||||
|
form = self.__get_form()
|
||||||
|
form["default_ie_account_code"] = Accounts.MEAL
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], EDIT_URI)
|
||||||
|
|
||||||
|
# Recurring item name empty
|
||||||
|
form = self.__get_form()
|
||||||
|
key = [x for x in form if x.endswith("-name")][0]
|
||||||
|
form[key] = " "
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], EDIT_URI)
|
||||||
|
|
||||||
|
# Recurring item account empty
|
||||||
|
form = self.__get_form()
|
||||||
|
key = [x for x in form if x.endswith("-account_code")][0]
|
||||||
|
form[key] = " "
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], EDIT_URI)
|
||||||
|
|
||||||
|
# Recurring item non-expense account
|
||||||
|
form = self.__get_form()
|
||||||
|
key = [x for x in form
|
||||||
|
if x.startswith("recurring-expense-")
|
||||||
|
and x.endswith("-account_code")][0]
|
||||||
|
form[key] = Accounts.SERVICE
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], EDIT_URI)
|
||||||
|
|
||||||
|
# Recurring item non-income account
|
||||||
|
form = self.__get_form()
|
||||||
|
key = [x for x in form
|
||||||
|
if x.startswith("recurring-income-")
|
||||||
|
and x.endswith("-account_code")][0]
|
||||||
|
form[key] = Accounts.UTILITIES
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], EDIT_URI)
|
||||||
|
|
||||||
|
# Recurring item payable expense
|
||||||
|
form = self.__get_form()
|
||||||
|
key = [x for x in form
|
||||||
|
if x.startswith("recurring-expense-")
|
||||||
|
and x.endswith("-account_code")][0]
|
||||||
|
form[key] = Accounts.PAYABLE
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], EDIT_URI)
|
||||||
|
|
||||||
|
# Recurring item receivable income
|
||||||
|
form = self.__get_form()
|
||||||
|
key = [x for x in form
|
||||||
|
if x.startswith("recurring-income-")
|
||||||
|
and x.endswith("-account_code")][0]
|
||||||
|
form[key] = Accounts.RECEIVABLE
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], EDIT_URI)
|
||||||
|
|
||||||
|
# Recurring item description template empty
|
||||||
|
form = self.__get_form()
|
||||||
|
key = [x for x in form if x.endswith("-description_template")][0]
|
||||||
|
form[key] = " "
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], EDIT_URI)
|
||||||
|
|
||||||
|
# Success, with malformed order
|
||||||
|
with self.app.app_context():
|
||||||
|
self.assertEqual(options.default_currency_code, "USD")
|
||||||
|
self.assertEqual(options.default_ie_account_code, "1111-001")
|
||||||
|
self.assertEqual(len(options.recurring.expenses), 0)
|
||||||
|
self.assertEqual(len(options.recurring.incomes), 0)
|
||||||
|
|
||||||
|
response = self.client.post(UPDATE_URI, data=self.__get_form())
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
self.assertEqual(options.default_currency_code, "EUR")
|
||||||
|
self.assertEqual(options.default_ie_account_code, "0000-000")
|
||||||
|
self.assertEqual(len(options.recurring.expenses), 4)
|
||||||
|
self.assertEqual(options.recurring.expenses[0].account_code,
|
||||||
|
Accounts.INSURANCE)
|
||||||
|
self.assertEqual(options.recurring.expenses[1].account_code,
|
||||||
|
Accounts.POSTAGE)
|
||||||
|
self.assertEqual(options.recurring.expenses[2].account_code,
|
||||||
|
Accounts.UTILITIES)
|
||||||
|
self.assertEqual(options.recurring.expenses[3].account_code,
|
||||||
|
Accounts.RENT_EXPENSE)
|
||||||
|
self.assertEqual(len(options.recurring.incomes), 2)
|
||||||
|
self.assertEqual(options.recurring.incomes[0].account_code,
|
||||||
|
Accounts.SERVICE)
|
||||||
|
self.assertEqual(options.recurring.incomes[1].account_code,
|
||||||
|
Accounts.DONATION)
|
||||||
|
|
||||||
|
# Success, with no recurring data
|
||||||
|
form = self.__get_form()
|
||||||
|
form = {x: form[x] for x in form if not x.startswith("recurring-")}
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
self.assertEqual(len(options.recurring.expenses), 0)
|
||||||
|
self.assertEqual(len(options.recurring.incomes), 0)
|
||||||
|
|
||||||
|
def test_update_not_modified(self) -> None:
|
||||||
|
"""Tests that the data is not modified.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
from accounting.models import Option
|
||||||
|
form: dict[str, str]
|
||||||
|
option: Option | None
|
||||||
|
resource: httpx.Response
|
||||||
|
|
||||||
|
response = self.client.post(UPDATE_URI, data=self.__get_form())
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
option = db.session.get(Option, "recurring")
|
||||||
|
self.assertIsNotNone(option)
|
||||||
|
timestamp: datetime = option.created_at - timedelta(seconds=5)
|
||||||
|
option.created_at = timestamp
|
||||||
|
option.updated_at = timestamp
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# The recurring setting was not modified
|
||||||
|
form = self.__get_form()
|
||||||
|
form["default_currency_code"] = "JPY"
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
option = db.session.get(Option, "recurring")
|
||||||
|
self.assertIsNotNone(option)
|
||||||
|
self.assertEqual(option.created_at, timestamp)
|
||||||
|
self.assertEqual(option.updated_at, timestamp)
|
||||||
|
|
||||||
|
# The recurring setting was modified
|
||||||
|
form = self.__get_form()
|
||||||
|
key: str = [x for x in form
|
||||||
|
if x.startswith("recurring-expense-")
|
||||||
|
and x.endswith("-account_code")][0]
|
||||||
|
form[key] = Accounts.MEAL
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
option = db.session.get(Option, "recurring")
|
||||||
|
self.assertIsNotNone(option)
|
||||||
|
self.assertLess(option.created_at, option.updated_at)
|
||||||
|
|
||||||
|
def test_created_updated_by(self) -> None:
|
||||||
|
"""Tests the created-by and updated-by record.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
from accounting.models import Option
|
||||||
|
from accounting.utils.user import get_user_pk
|
||||||
|
editor_username, editor2_username = "editor", "editor2"
|
||||||
|
option: Option | None
|
||||||
|
response: httpx.Response
|
||||||
|
|
||||||
|
response = self.client.post(UPDATE_URI, data=self.__get_form())
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
editor2_pk: int = get_user_pk(editor2_username)
|
||||||
|
option = db.session.get(Option, "recurring")
|
||||||
|
self.assertIsNotNone(option)
|
||||||
|
option.created_by_id = editor2_pk
|
||||||
|
option.updated_by_id = editor2_pk
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
form: dict[str, str] = self.__get_form()
|
||||||
|
key: str = [x for x in form
|
||||||
|
if x.startswith("recurring-expense-")
|
||||||
|
and x.endswith("-account_code")][0]
|
||||||
|
form[key] = Accounts.MEAL
|
||||||
|
response = self.client.post(UPDATE_URI, data=form)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
option = db.session.get(Option, "recurring")
|
||||||
|
self.assertIsNotNone(option)
|
||||||
|
self.assertEqual(option.created_by.username, editor2_username)
|
||||||
|
self.assertEqual(option.updated_by.username, editor_username)
|
||||||
|
|
||||||
|
def __get_form(self, csrf_token: str | None = None) -> dict[str, str]:
|
||||||
|
"""Returns the option form.
|
||||||
|
|
||||||
|
:param csrf_token: The CSRF token.
|
||||||
|
:return: The option form.
|
||||||
|
"""
|
||||||
|
if csrf_token is None:
|
||||||
|
csrf_token = self.csrf_token
|
||||||
|
return {"csrf_token": csrf_token,
|
||||||
|
"next": NEXT_URI,
|
||||||
|
"default_currency_code": "EUR",
|
||||||
|
"default_ie_account_code": "0000-000",
|
||||||
|
"recurring-expense-1-name": "Water bill",
|
||||||
|
"recurring-expense-1-account_code": Accounts.UTILITIES,
|
||||||
|
"recurring-expense-1-description_template":
|
||||||
|
"Water bill for {last_bimonthly_name}",
|
||||||
|
"recurring-expense-3-no": "16",
|
||||||
|
"recurring-expense-3-name": "Phone bill",
|
||||||
|
"recurring-expense-3-account_code": Accounts.POSTAGE,
|
||||||
|
"recurring-expense-3-description_template":
|
||||||
|
"Phone bill for {last_month_name}",
|
||||||
|
"recurring-expense-12-name": "Rent",
|
||||||
|
"recurring-expense-12-account_code": Accounts.RENT_EXPENSE,
|
||||||
|
"recurring-expense-12-description_template":
|
||||||
|
"Rent for {this_month_name}",
|
||||||
|
"recurring-expense-26-no": "7",
|
||||||
|
"recurring-expense-26-name": "Insurance",
|
||||||
|
"recurring-expense-26-account_code": Accounts.INSURANCE,
|
||||||
|
"recurring-expense-26-description_template":
|
||||||
|
"Insurance for {last_month_name}",
|
||||||
|
"recurring-income-2-no": "12",
|
||||||
|
"recurring-income-2-name": "Donation",
|
||||||
|
"recurring-income-2-account_code": Accounts.DONATION,
|
||||||
|
"recurring-income-2-description_template":
|
||||||
|
"Donation for {this_month_name}",
|
||||||
|
"recurring-income-15-no": "4",
|
||||||
|
"recurring-income-15-name": "Payroll",
|
||||||
|
"recurring-income-15-account_code": Accounts.SERVICE,
|
||||||
|
"recurring-income-15-description_template":
|
||||||
|
"Payroll for {last_month_name}"}
|
@ -78,6 +78,10 @@ def create_app(is_testing: bool = False) -> Flask:
|
|||||||
return auth.current_user() is not None \
|
return auth.current_user() is not None \
|
||||||
and auth.current_user().username in ["editor", "editor2"]
|
and auth.current_user().username in ["editor", "editor2"]
|
||||||
|
|
||||||
|
def can_admin(self) -> bool:
|
||||||
|
return auth.current_user() is not None \
|
||||||
|
and auth.current_user().username == "editor"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cls(self) -> t.Type[auth.User]:
|
def cls(self) -> t.Type[auth.User]:
|
||||||
return auth.User
|
return auth.User
|
||||||
|
@ -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>
|
||||||
|
@ -48,12 +48,16 @@ class Accounts:
|
|||||||
SALES: str = "4111-001"
|
SALES: str = "4111-001"
|
||||||
SERVICE: str = "4611-001"
|
SERVICE: str = "4611-001"
|
||||||
AGENCY: str = "4711-001"
|
AGENCY: str = "4711-001"
|
||||||
OFFICE: str = "6153-001"
|
RENT_EXPENSE: str = "6252-001"
|
||||||
TRAVEL: str = "6154-001"
|
OFFICE: str = "6253-001"
|
||||||
MEAL: str = "6172-001"
|
TRAVEL: str = "6254-001"
|
||||||
|
POSTAGE: str = "6256-001"
|
||||||
|
UTILITIES: str = "6261-001"
|
||||||
|
INSURANCE: str = "6262-001"
|
||||||
|
MEAL: str = "6272-001"
|
||||||
INTEREST: str = "7111-001"
|
INTEREST: str = "7111-001"
|
||||||
DONATION: str = "7481-001"
|
DONATION: str = "7481-001"
|
||||||
RENT: str = "7482-001"
|
RENT_INCOME: str = "7482-001"
|
||||||
|
|
||||||
|
|
||||||
def get_add_form(csrf_token: str) -> dict[str, str]:
|
def get_add_form(csrf_token: str) -> dict[str, str]:
|
||||||
@ -115,7 +119,7 @@ def get_add_form(csrf_token: str) -> dict[str, str]:
|
|||||||
"currency-16-debit-9-description": " Gas ",
|
"currency-16-debit-9-description": " Gas ",
|
||||||
"currency-16-debit-9-amount": "30000",
|
"currency-16-debit-9-amount": "30000",
|
||||||
"currency-16-credit-6-no": "6",
|
"currency-16-credit-6-no": "6",
|
||||||
"currency-16-credit-6-account_code": Accounts.RENT,
|
"currency-16-credit-6-account_code": Accounts.RENT_INCOME,
|
||||||
"currency-16-credit-6-description": " Rent ",
|
"currency-16-credit-6-description": " Rent ",
|
||||||
"currency-16-credit-6-amount": "35000",
|
"currency-16-credit-6-amount": "35000",
|
||||||
"currency-16-credit-9-account_code": Accounts.DONATION,
|
"currency-16-credit-9-account_code": Accounts.DONATION,
|
||||||
@ -349,7 +353,7 @@ def __mess_up_currencies(form: dict[str, str]) -> dict[str, str]:
|
|||||||
f"{prefix}debit-14-description": " ",
|
f"{prefix}debit-14-description": " ",
|
||||||
f"{prefix}debit-14-amount": "14.55",
|
f"{prefix}debit-14-amount": "14.55",
|
||||||
f"{prefix}credit-16-no": "7",
|
f"{prefix}credit-16-no": "7",
|
||||||
f"{prefix}credit-16-account_code": Accounts.RENT,
|
f"{prefix}credit-16-account_code": Accounts.RENT_INCOME,
|
||||||
f"{prefix}credit-16-description": " Bike ",
|
f"{prefix}credit-16-description": " Bike ",
|
||||||
f"{prefix}credit-16-amount": "19.5",
|
f"{prefix}credit-16-amount": "19.5",
|
||||||
f"{prefix}credit-22-no": "5",
|
f"{prefix}credit-22-no": "5",
|
||||||
|
Reference in New Issue
Block a user