Compare commits
	
		
			55 Commits
		
	
	
		
			d8afadda02
			...
			52807c5322
		
	
	| 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 | 
							
								
								
									
										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.8.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.8.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)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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."""
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,10 +21,10 @@ import re
 | 
				
			|||||||
import typing as t
 | 
					import typing as t
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import sqlalchemy as sa
 | 
					import sqlalchemy as sa
 | 
				
			||||||
from flask import current_app
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.models import Account, JournalEntryLineItem
 | 
					from accounting.models import Account, JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.utils.options import options, Recurring
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DescriptionAccount:
 | 
					class DescriptionAccount:
 | 
				
			||||||
@@ -148,16 +148,16 @@ class DescriptionType:
 | 
				
			|||||||
class DescriptionRecurring:
 | 
					class DescriptionRecurring:
 | 
				
			||||||
    """A recurring transaction."""
 | 
					    """A recurring transaction."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, name: str, template: str, account: Account):
 | 
					    def __init__(self, name: str, account: Account, description_template: str):
 | 
				
			||||||
        """Constructs a recurring transaction.
 | 
					        """Constructs a recurring transaction.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param name: The name.
 | 
					        :param name: The name.
 | 
				
			||||||
        :param template: The template.
 | 
					        :param description_template: The description template.
 | 
				
			||||||
        :param account: The account.
 | 
					        :param account: The account.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.name: str = name
 | 
					        self.name: str = name
 | 
				
			||||||
        self.template: str = template
 | 
					 | 
				
			||||||
        self.account: DescriptionAccount = DescriptionAccount(account, 0)
 | 
					        self.account: DescriptionAccount = DescriptionAccount(account, 0)
 | 
				
			||||||
 | 
					        self.description_template: str = description_template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def account_codes(self) -> list[str]:
 | 
					    def account_codes(self) -> list[str]:
 | 
				
			||||||
@@ -280,19 +280,17 @@ class DescriptionEditor:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if "ACCOUNTING_RECURRING" not in current_app.config:
 | 
					        recurring: Recurring = options.recurring
 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        data: list[tuple[t.Literal["debit", "credit"], str, str, str]] \
 | 
					 | 
				
			||||||
            = [x.split("|")
 | 
					 | 
				
			||||||
               for x in current_app.config["ACCOUNTING_RECURRING"].split(",")]
 | 
					 | 
				
			||||||
        debit_credit_dict: dict[t.Literal["debit", "credit"],
 | 
					 | 
				
			||||||
                                DescriptionDebitCredit] \
 | 
					 | 
				
			||||||
            = {x.debit_credit: x for x in {self.debit, self.credit}}
 | 
					 | 
				
			||||||
        accounts: dict[str, Account] \
 | 
					        accounts: dict[str, Account] \
 | 
				
			||||||
            = self.__get_accounts({x[1] for x in data})
 | 
					            = self.__get_accounts(recurring.codes)
 | 
				
			||||||
        for row in data:
 | 
					        self.debit.recurring \
 | 
				
			||||||
            debit_credit_dict[row[0]].recurring.append(
 | 
					            = [DescriptionRecurring(x.name, accounts[x.account_code],
 | 
				
			||||||
                DescriptionRecurring(row[2], row[3], accounts[row[1]]))
 | 
					                                    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
 | 
					    @staticmethod
 | 
				
			||||||
    def __get_accounts(codes: set[str]) -> dict[str, Account]:
 | 
					    def __get_accounts(codes: set[str]) -> dict[str, Account]:
 | 
				
			||||||
@@ -301,6 +299,9 @@ class DescriptionEditor:
 | 
				
			|||||||
        :param codes: The account codes.
 | 
					        :param codes: The account codes.
 | 
				
			||||||
        :return: The account.
 | 
					        :return: The account.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        if len(codes) == 0:
 | 
				
			||||||
 | 
					            return {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        def get_condition(code0: str) -> sa.BinaryExpression:
 | 
					        def get_condition(code0: str) -> sa.BinaryExpression:
 | 
				
			||||||
            m: re.Match = re.match(r"^(\d{4})-(\d{3})$", code0)
 | 
					            m: re.Match = re.match(r"^(\d{4})-(\d{3})$", code0)
 | 
				
			||||||
            assert m is not None,\
 | 
					            assert m is not None,\
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -269,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"),
 | 
				
			||||||
@@ -290,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"),
 | 
				
			||||||
@@ -779,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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,14 +72,11 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@media(max-width:767px) {
 | 
					@media(max-width:767px) {
 | 
				
			||||||
    .accounting-toolbar {
 | 
					 | 
				
			||||||
    	width: 100%;
 | 
					 | 
				
			||||||
    	justify-content: space-evenly;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .accounting-toolbar > .btn:not(form), .accounting-toolbar > .btn-group > .btn {
 | 
					    .accounting-toolbar > .btn:not(form), .accounting-toolbar > .btn-group > .btn {
 | 
				
			||||||
        height: 3.2rem;
 | 
					        height: 3.2rem;
 | 
				
			||||||
        width: 3.2rem;
 | 
					        width: 3.2rem;
 | 
				
			||||||
        border-radius: 50%;
 | 
					        border-radius: 50%;
 | 
				
			||||||
 | 
					        margin-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;
 | 
				
			||||||
@@ -334,6 +331,14 @@ a.accounting-report-table-row {
 | 
				
			|||||||
    margin-top: 0.2rem;
 | 
					    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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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,4 +34,4 @@ def default_currency_code() -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    :return: The default currency code.
 | 
					    :return: The default currency code.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    return current_app.config.get("ACCOUNTING_DEFAULT_CURRENCY", "USD")
 | 
					    return options.default_currency_code
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,17 +26,17 @@ First written: 2023/1/31
 | 
				
			|||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="mb-3 accounting-toolbar">
 | 
					<div class="mb-3 accounting-toolbar">
 | 
				
			||||||
  <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
 | 
					  <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-pen-to-square"></i>
 | 
					      <i class="fa-solid fa-pen-to-square"></i>
 | 
				
			||||||
      {{ A_("Edit") }}
 | 
					      {{ 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>
 | 
				
			||||||
    <span class="d-none d-md-inline">{{ A_("Order") }}</span>
 | 
					    <span class="d-none d-md-inline">{{ A_("Order") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
@@ -57,7 +57,7 @@ First written: 2023/1/31
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% 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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,10 +27,10 @@ First written: 2023/2/1
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="mb-3 d-none d-md-block">
 | 
					<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="mb-3 d-none d-md-block">
 | 
					<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="mb-3 d-none d-md-block">
 | 
					<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 %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,12 +26,12 @@ First written: 2023/2/6
 | 
				
			|||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="mb-3 accounting-toolbar">
 | 
					<div class="mb-3 accounting-toolbar">
 | 
				
			||||||
  <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.currency.list")|accounting_or_next }}">
 | 
					  <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-pen-to-square"></i>
 | 
					      <i class="fa-solid fa-pen-to-square"></i>
 | 
				
			||||||
      {{ A_("Edit") }}
 | 
					      {{ A_("Edit") }}
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
@@ -53,7 +53,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.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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,10 +27,10 @@ First written: 2023/2/6
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="mb-3 d-none d-md-block">
 | 
					<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 %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,7 @@ First written: 2023/2/26
 | 
				
			|||||||
{% extends "accounting/journal-entry/include/detail.html" %}
 | 
					{% extends "accounting/journal-entry/include/detail.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block as_trasfer %}
 | 
					{% 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-table-columns"></i>
 | 
					    <i class="fa-solid fa-table-columns"></i>
 | 
				
			||||||
    <span class="d-none d-md-inline">{{ A_("As Transfer") }}</span>
 | 
					    <span class="d-none d-md-inline">{{ A_("As Transfer") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -158,7 +158,7 @@ First written: 2023/2/28
 | 
				
			|||||||
          <div id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-page" class="d-none" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab">
 | 
					          <div id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-page" class="d-none" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab">
 | 
				
			||||||
            <div class="accounting-description-editor-buttons">
 | 
					            <div class="accounting-description-editor-buttons">
 | 
				
			||||||
              {% for recurring in description_editor.recurring %}
 | 
					              {% for recurring in description_editor.recurring %}
 | 
				
			||||||
                <button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-recurring-item" type="button" tabindex="-1" data-template="{{ recurring.template }}" data-accounts="{{ recurring.account_codes|tojson|forceescape }}">
 | 
					                <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 }}
 | 
					                  {{ recurring.name }}
 | 
				
			||||||
                </button>
 | 
					                </button>
 | 
				
			||||||
              {% endfor %}
 | 
					              {% endfor %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,17 +26,17 @@ First written: 2023/2/26
 | 
				
			|||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="mb-3 accounting-toolbar">
 | 
					<div class="mb-3 accounting-toolbar">
 | 
				
			||||||
  <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.report.default")|accounting_or_next }}">
 | 
					  <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-pen-to-square"></i>
 | 
					      <i class="fa-solid fa-pen-to-square"></i>
 | 
				
			||||||
      {{ A_("Edit") }}
 | 
					      {{ 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>
 | 
				
			||||||
    <span class="d-none d-md-inline">{{ A_("Order") }}</span>
 | 
					    <span class="d-none d-md-inline">{{ A_("Order") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,7 @@ First written: 2023/2/26
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="mb-3 d-none d-md-block">
 | 
					<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>
 | 
				
			||||||
    <span class="d-none d-md-inline">{{ A_("Back") }}</span>
 | 
					    <span class="d-none d-md-inline">{{ A_("Back") }}</span>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,6 @@ 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 }}">
 | 
					<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>
 | 
					  <div>
 | 
				
			||||||
    {{ journal_entry.date|accounting_format_date }}
 | 
					 | 
				
			||||||
    {% if journal_entry.is_cash_disbursement %}
 | 
					    {% if journal_entry.is_cash_disbursement %}
 | 
				
			||||||
      {{ A_("Cash Disbursement") }}
 | 
					      {{ A_("Cash Disbursement") }}
 | 
				
			||||||
    {% elif journal_entry.is_cash_receipt %}
 | 
					    {% elif journal_entry.is_cash_receipt %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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="mb-3 d-none d-md-block">
 | 
					<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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,7 @@ First written: 2023/2/26
 | 
				
			|||||||
{% extends "accounting/journal-entry/include/detail.html" %}
 | 
					{% extends "accounting/journal-entry/include/detail.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block as_trasfer %}
 | 
					{% 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>
 | 
				
			||||||
    <span class="d-none d-md-inline">{{ A_("As Transfer") }}</span>
 | 
					    <span class="d-none d-md-inline">{{ A_("As Transfer") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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-22 01:47+0800\n"
 | 
					"POT-Creation-Date: 2023-03-23 00:45+0800\n"
 | 
				
			||||||
"PO-Revision-Date: 2023-03-22 01:47+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:559
 | 
					#: 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:562
 | 
					#: 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:563
 | 
					#: 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:696
 | 
					#: 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"
 | 
				
			||||||
@@ -141,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 "請填上名稱。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -186,28 +205,24 @@ msgstr "傳票不可刪除。"
 | 
				
			|||||||
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 "借方貸方合計不符。 "
 | 
				
			||||||
@@ -236,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 ""
 | 
				
			||||||
@@ -299,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 "全部"
 | 
				
			||||||
@@ -363,8 +412,8 @@ 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
 | 
				
			||||||
@@ -392,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
 | 
				
			||||||
@@ -404,10 +453,11 @@ 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/option/include/recurring-item-editor-modal.html:39
 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:90
 | 
					#: 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
 | 
				
			||||||
@@ -416,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
 | 
				
			||||||
@@ -428,24 +478,24 @@ 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:178
 | 
					#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:178
 | 
				
			||||||
@@ -515,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
 | 
				
			||||||
@@ -526,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
 | 
				
			||||||
@@ -678,12 +725,14 @@ 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
 | 
				
			||||||
 | 
					#: src/accounting/templates/accounting/option/detail.html:31
 | 
				
			||||||
msgid "Edit"
 | 
					msgid "Edit"
 | 
				
			||||||
msgstr "編輯"
 | 
					msgstr "編輯"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -713,6 +762,8 @@ msgstr "確認刪除科目"
 | 
				
			|||||||
#: 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"
 | 
				
			||||||
@@ -729,6 +780,8 @@ msgstr "你確定要刪掉這個科目嗎?"
 | 
				
			|||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:193
 | 
					#: 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 "取消"
 | 
				
			||||||
@@ -773,6 +826,7 @@ msgstr "科目管理"
 | 
				
			|||||||
#: src/accounting/templates/accounting/currency/list.html:32
 | 
					#: src/accounting/templates/accounting/currency/list.html:32
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/form-debit-credit.html:44
 | 
					#: src/accounting/templates/accounting/journal-entry/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/option/include/form-recurring-expense-income.html:37
 | 
				
			||||||
#: 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 "新增"
 | 
				
			||||||
@@ -785,6 +839,7 @@ msgstr "新增"
 | 
				
			|||||||
#: 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:82
 | 
					#: 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
 | 
				
			||||||
@@ -807,6 +862,8 @@ msgstr "%(base)s下的科目"
 | 
				
			|||||||
#: 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 "儲存"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -833,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 "清除"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -866,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 "名稱"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -889,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 "分頁瀏覽"
 | 
				
			||||||
@@ -923,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 "選擇科目"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1016,19 +1082,19 @@ msgstr "分錄內容"
 | 
				
			|||||||
msgid "Original Line Item"
 | 
					msgid "Original Line Item"
 | 
				
			||||||
msgstr "原始分錄"
 | 
					msgstr "原始分錄"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/order-journal-entry.html:26
 | 
					#: src/accounting/templates/accounting/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/add-journal-entry-material-fab.html:26
 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:31
 | 
					#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:31
 | 
				
			||||||
msgid "Cash Disbursement"
 | 
					msgid "Cash Disbursement"
 | 
				
			||||||
msgstr "現金支出"
 | 
					msgstr "現金支出"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/order-journal-entry.html:28
 | 
					#: 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/add-journal-entry-material-fab.html:29
 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:36
 | 
					#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:36
 | 
				
			||||||
msgid "Cash Receipt"
 | 
					msgid "Cash Receipt"
 | 
				
			||||||
msgstr "現金收入"
 | 
					msgstr "現金收入"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: src/accounting/templates/accounting/journal-entry/include/order-journal-entry.html:30
 | 
					#: 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/add-journal-entry-material-fab.html:32
 | 
				
			||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:41
 | 
					#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:41
 | 
				
			||||||
msgid "Transfer"
 | 
					msgid "Transfer"
 | 
				
			||||||
@@ -1046,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
 | 
				
			||||||
@@ -1112,6 +1240,10 @@ msgstr "期間"
 | 
				
			|||||||
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:
 | 
				
			||||||
    return current_app.config.get("ACCOUNTING_DEFAULT_IE_ACCOUNT",
 | 
					        """Returns the SQL condition for the current assets and liabilities
 | 
				
			||||||
                                  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]:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
@@ -1562,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)
 | 
				
			||||||
@@ -1728,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)
 | 
				
			||||||
@@ -1748,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)
 | 
				
			||||||
@@ -1914,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)
 | 
				
			||||||
@@ -1930,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}"}
 | 
				
			||||||
@@ -50,18 +50,6 @@ def create_app(is_testing: bool = False) -> Flask:
 | 
				
			|||||||
        "SQLALCHEMY_DATABASE_URI": db_uri,
 | 
					        "SQLALCHEMY_DATABASE_URI": db_uri,
 | 
				
			||||||
        "BABEL_DEFAULT_LOCALE": "en",
 | 
					        "BABEL_DEFAULT_LOCALE": "en",
 | 
				
			||||||
        "ALL_LINGUAS": "zh_Hant|正體中文,en|English,zh_Hans|简体中文",
 | 
					        "ALL_LINGUAS": "zh_Hant|正體中文,en|English,zh_Hans|简体中文",
 | 
				
			||||||
        "ACCOUNTING_DEFAULT_CURRENCY": "USD",
 | 
					 | 
				
			||||||
        "ACCOUNTING_DEFAULT_IE_ACCOUNT": "1111-001",
 | 
					 | 
				
			||||||
        "ACCOUNTING_RECURRING": (
 | 
					 | 
				
			||||||
            "debit|1314-001|Pension|Pension for {last_month_name},"
 | 
					 | 
				
			||||||
            "debit|6262-001|Health insurance"
 | 
					 | 
				
			||||||
            "|Health insurance for {last_month_name},"
 | 
					 | 
				
			||||||
            "debit|6261-001|Electricity bill"
 | 
					 | 
				
			||||||
            "|Electricity bill for {last_bimonthly_name},"
 | 
					 | 
				
			||||||
            "debit|6261-001|Water bill|Water bill for {last_bimonthly_name},"
 | 
					 | 
				
			||||||
            "debit|6261-001|Gas bill|Gas bill for {last_bimonthly_name},"
 | 
					 | 
				
			||||||
            "debit|6261-001|Phone bill|Phone bill for {last_month_name},"
 | 
					 | 
				
			||||||
            "credit|4611-001|Payroll|Payroll for {last_month_name}"),
 | 
					 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    if is_testing:
 | 
					    if is_testing:
 | 
				
			||||||
        app.config["TESTING"] = True
 | 
					        app.config["TESTING"] = True
 | 
				
			||||||
@@ -90,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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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