Renamed "voucher" to "journal entry".
This commit is contained in:
		@@ -80,8 +80,8 @@ def init_app(app: Flask, user_utils: UserUtilityInterface,
 | 
				
			|||||||
    from . import currency
 | 
					    from . import currency
 | 
				
			||||||
    currency.init_app(app, bp)
 | 
					    currency.init_app(app, bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from . import voucher
 | 
					    from . import journal_entry
 | 
				
			||||||
    voucher.init_app(app, bp)
 | 
					    journal_entry.init_app(app, bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from . import report
 | 
					    from . import report
 | 
				
			||||||
    report.init_app(app, bp)
 | 
					    report.init_app(app, bp)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
#  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 voucher management.
 | 
					"""The journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
from flask import Flask, Blueprint
 | 
					from flask import Flask, Blueprint
 | 
				
			||||||
@@ -27,11 +27,11 @@ def init_app(app: Flask, bp: Blueprint) -> None:
 | 
				
			|||||||
    :param bp: The blueprint of the accounting application.
 | 
					    :param bp: The blueprint of the accounting application.
 | 
				
			||||||
    :return: None.
 | 
					    :return: None.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    from .converters import VoucherConverter, VoucherTypeConverter, \
 | 
					    from .converters import JournalEntryConverter, JournalEntryTypeConverter, \
 | 
				
			||||||
        DateConverter
 | 
					        DateConverter
 | 
				
			||||||
    app.url_map.converters["voucher"] = VoucherConverter
 | 
					    app.url_map.converters["journalEntry"] = JournalEntryConverter
 | 
				
			||||||
    app.url_map.converters["voucherType"] = VoucherTypeConverter
 | 
					    app.url_map.converters["journalEntryType"] = JournalEntryTypeConverter
 | 
				
			||||||
    app.url_map.converters["date"] = DateConverter
 | 
					    app.url_map.converters["date"] = DateConverter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .views import bp as voucher_bp
 | 
					    from .views import bp as journal_entry_bp
 | 
				
			||||||
    bp.register_blueprint(voucher_bp, url_prefix="/vouchers")
 | 
					    bp.register_blueprint(journal_entry_bp, url_prefix="/journal-entries")
 | 
				
			||||||
							
								
								
									
										107
									
								
								src/accounting/journal_entry/converters.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/accounting/journal_entry/converters.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  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 path converters for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask import abort
 | 
				
			||||||
 | 
					from sqlalchemy.orm import selectinload
 | 
				
			||||||
 | 
					from werkzeug.routing import BaseConverter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.models import JournalEntry, JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.utils.journal_entry_types import JournalEntryType
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JournalEntryConverter(BaseConverter):
 | 
				
			||||||
 | 
					    """The journal entry converter to convert the journal entry ID from and to
 | 
				
			||||||
 | 
					    the corresponding journal entry in the routes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_python(self, value: str) -> JournalEntry:
 | 
				
			||||||
 | 
					        """Converts a journal entry ID to a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The journal entry ID.
 | 
				
			||||||
 | 
					        :return: The corresponding journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        journal_entry: JournalEntry | None = JournalEntry.query\
 | 
				
			||||||
 | 
					            .join(JournalEntryLineItem)\
 | 
				
			||||||
 | 
					            .filter(JournalEntry.id == value)\
 | 
				
			||||||
 | 
					            .options(selectinload(JournalEntry.line_items)
 | 
				
			||||||
 | 
					                     .selectinload(JournalEntryLineItem.offsets)
 | 
				
			||||||
 | 
					                     .selectinload(JournalEntryLineItem.journal_entry))\
 | 
				
			||||||
 | 
					            .first()
 | 
				
			||||||
 | 
					        if journal_entry is None:
 | 
				
			||||||
 | 
					            abort(404)
 | 
				
			||||||
 | 
					        return journal_entry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_url(self, value: JournalEntry) -> str:
 | 
				
			||||||
 | 
					        """Converts a journal entry to its ID.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The journal entry.
 | 
				
			||||||
 | 
					        :return: The ID.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return str(value.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JournalEntryTypeConverter(BaseConverter):
 | 
				
			||||||
 | 
					    """The journal entry converter to convert the journal entry type ID from
 | 
				
			||||||
 | 
					    and to the corresponding journal entry type in the routes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_python(self, value: str) -> JournalEntryType:
 | 
				
			||||||
 | 
					        """Converts a journal entry ID to a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The journal entry ID.
 | 
				
			||||||
 | 
					        :return: The corresponding journal entry type.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        type_dict: dict[str, JournalEntryType] \
 | 
				
			||||||
 | 
					            = {x.value: x for x in JournalEntryType}
 | 
				
			||||||
 | 
					        journal_entry_type: JournalEntryType | None = type_dict.get(value)
 | 
				
			||||||
 | 
					        if journal_entry_type is None:
 | 
				
			||||||
 | 
					            abort(404)
 | 
				
			||||||
 | 
					        return journal_entry_type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_url(self, value: JournalEntryType) -> str:
 | 
				
			||||||
 | 
					        """Converts a journal entry type to its ID.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The journal entry type.
 | 
				
			||||||
 | 
					        :return: The ID.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return str(value.value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DateConverter(BaseConverter):
 | 
				
			||||||
 | 
					    """The date converter to convert the ISO date from and to the
 | 
				
			||||||
 | 
					    corresponding date in the routes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_python(self, value: str) -> date:
 | 
				
			||||||
 | 
					        """Converts an ISO date to a date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The ISO date.
 | 
				
			||||||
 | 
					        :return: The corresponding date.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            return date.fromisoformat(value)
 | 
				
			||||||
 | 
					        except ValueError:
 | 
				
			||||||
 | 
					            abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_url(self, value: date) -> str:
 | 
				
			||||||
 | 
					        """Converts a date to its ISO date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The date.
 | 
				
			||||||
 | 
					        :return: The ISO date.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return value.isoformat()
 | 
				
			||||||
@@ -14,9 +14,9 @@
 | 
				
			|||||||
#  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 forms for the voucher management.
 | 
					"""The forms for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
from .reorder import sort_vouchers_in, VoucherReorderForm
 | 
					from .reorder import sort_journal_entries_in, JournalEntryReorderForm
 | 
				
			||||||
from .voucher import VoucherForm, CashReceiptVoucherForm, \
 | 
					from .journal_entry import JournalEntryForm, CashReceiptJournalEntryForm, \
 | 
				
			||||||
    CashDisbursementVoucherForm, TransferVoucherForm
 | 
					    CashDisbursementJournalEntryForm, TransferJournalEntryForm
 | 
				
			||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
#  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 currency sub-forms for the voucher management.
 | 
					"""The currency sub-forms for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
from decimal import Decimal
 | 
					from decimal import Decimal
 | 
				
			||||||
@@ -29,7 +29,7 @@ from wtforms.validators import DataRequired
 | 
				
			|||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.locale import lazy_gettext
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
from accounting.models import Currency, JournalEntryLineItem
 | 
					from accounting.models import Currency, JournalEntryLineItem
 | 
				
			||||||
from accounting.voucher.utils.offset_alias import offset_alias
 | 
					from accounting.journal_entry.utils.offset_alias import offset_alias
 | 
				
			||||||
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
 | 
				
			||||||
@@ -123,9 +123,9 @@ class IsBalanced:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CurrencyForm(FlaskForm):
 | 
					class CurrencyForm(FlaskForm):
 | 
				
			||||||
    """The form to create or edit a currency in a voucher."""
 | 
					    """The form to create or edit a currency in a journal entry."""
 | 
				
			||||||
    no = IntegerField()
 | 
					    no = IntegerField()
 | 
				
			||||||
    """The order in the voucher."""
 | 
					    """The order in the journal entry."""
 | 
				
			||||||
    code = StringField()
 | 
					    code = StringField()
 | 
				
			||||||
    """The currency code."""
 | 
					    """The currency code."""
 | 
				
			||||||
    whole_form = BooleanField()
 | 
					    whole_form = BooleanField()
 | 
				
			||||||
@@ -169,9 +169,10 @@ class CurrencyForm(FlaskForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CashReceiptCurrencyForm(CurrencyForm):
 | 
					class CashReceiptCurrencyForm(CurrencyForm):
 | 
				
			||||||
    """The form to create or edit a currency in a cash receipt voucher."""
 | 
					    """The form to create or edit a currency in a
 | 
				
			||||||
 | 
					    cash receipt journal entry."""
 | 
				
			||||||
    no = IntegerField()
 | 
					    no = IntegerField()
 | 
				
			||||||
    """The order in the voucher."""
 | 
					    """The order in the journal entry."""
 | 
				
			||||||
    code = StringField(
 | 
					    code = StringField(
 | 
				
			||||||
        filters=[strip_text],
 | 
					        filters=[strip_text],
 | 
				
			||||||
        validators=[CURRENCY_REQUIRED,
 | 
					        validators=[CURRENCY_REQUIRED,
 | 
				
			||||||
@@ -206,9 +207,10 @@ class CashReceiptCurrencyForm(CurrencyForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CashDisbursementCurrencyForm(CurrencyForm):
 | 
					class CashDisbursementCurrencyForm(CurrencyForm):
 | 
				
			||||||
    """The form to create or edit a currency in a cash disbursement voucher."""
 | 
					    """The form to create or edit a currency in a
 | 
				
			||||||
 | 
					    cash disbursement journal entry."""
 | 
				
			||||||
    no = IntegerField()
 | 
					    no = IntegerField()
 | 
				
			||||||
    """The order in the voucher."""
 | 
					    """The order in the journal entry."""
 | 
				
			||||||
    code = StringField(
 | 
					    code = StringField(
 | 
				
			||||||
        filters=[strip_text],
 | 
					        filters=[strip_text],
 | 
				
			||||||
        validators=[CURRENCY_REQUIRED,
 | 
					        validators=[CURRENCY_REQUIRED,
 | 
				
			||||||
@@ -243,9 +245,9 @@ class CashDisbursementCurrencyForm(CurrencyForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TransferCurrencyForm(CurrencyForm):
 | 
					class TransferCurrencyForm(CurrencyForm):
 | 
				
			||||||
    """The form to create or edit a currency in a transfer voucher."""
 | 
					    """The form to create or edit a currency in a transfer journal entry."""
 | 
				
			||||||
    no = IntegerField()
 | 
					    no = IntegerField()
 | 
				
			||||||
    """The order in the voucher."""
 | 
					    """The order in the journal entry."""
 | 
				
			||||||
    code = StringField(
 | 
					    code = StringField(
 | 
				
			||||||
        filters=[strip_text],
 | 
					        filters=[strip_text],
 | 
				
			||||||
        validators=[CURRENCY_REQUIRED,
 | 
					        validators=[CURRENCY_REQUIRED,
 | 
				
			||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
#  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 voucher forms for the voucher management.
 | 
					"""The journal entry forms for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
import datetime as dt
 | 
					import datetime as dt
 | 
				
			||||||
@@ -30,19 +30,19 @@ from wtforms.validators import DataRequired, ValidationError
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.locale import lazy_gettext
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
from accounting.models import Voucher, Account, JournalEntryLineItem, \
 | 
					from accounting.models import JournalEntry, Account, JournalEntryLineItem, \
 | 
				
			||||||
    VoucherCurrency
 | 
					    JournalEntryCurrency
 | 
				
			||||||
from accounting.voucher.utils.account_option import AccountOption
 | 
					from accounting.journal_entry.utils.account_option import AccountOption
 | 
				
			||||||
from accounting.voucher.utils.original_line_items import \
 | 
					from accounting.journal_entry.utils.original_line_items import \
 | 
				
			||||||
    get_selectable_original_line_items
 | 
					    get_selectable_original_line_items
 | 
				
			||||||
from accounting.voucher.utils.description_editor import DescriptionEditor
 | 
					from accounting.journal_entry.utils.description_editor import DescriptionEditor
 | 
				
			||||||
from accounting.utils.random_id import new_id
 | 
					from accounting.utils.random_id import new_id
 | 
				
			||||||
from accounting.utils.strip_text import strip_multiline_text
 | 
					from accounting.utils.strip_text import strip_multiline_text
 | 
				
			||||||
from accounting.utils.user import get_current_user_pk
 | 
					from accounting.utils.user import get_current_user_pk
 | 
				
			||||||
from .currency import CurrencyForm, CashReceiptCurrencyForm, \
 | 
					from .currency import CurrencyForm, CashReceiptCurrencyForm, \
 | 
				
			||||||
    CashDisbursementCurrencyForm, TransferCurrencyForm
 | 
					    CashDisbursementCurrencyForm, TransferCurrencyForm
 | 
				
			||||||
from .line_item import LineItemForm, DebitLineItemForm, CreditLineItemForm
 | 
					from .line_item import LineItemForm, DebitLineItemForm, CreditLineItemForm
 | 
				
			||||||
from .reorder import sort_vouchers_in
 | 
					from .reorder import sort_journal_entries_in
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DATE_REQUIRED: DataRequired = DataRequired(
 | 
					DATE_REQUIRED: DataRequired = DataRequired(
 | 
				
			||||||
    lazy_gettext("Please fill in the date."))
 | 
					    lazy_gettext("Please fill in the date."))
 | 
				
			||||||
@@ -54,7 +54,7 @@ class NotBeforeOriginalLineItems:
 | 
				
			|||||||
    original line items."""
 | 
					    original line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __call__(self, form: FlaskForm, field: DateField) -> None:
 | 
					    def __call__(self, form: FlaskForm, field: DateField) -> None:
 | 
				
			||||||
        assert isinstance(form, VoucherForm)
 | 
					        assert isinstance(form, JournalEntryForm)
 | 
				
			||||||
        if field.data is None:
 | 
					        if field.data is None:
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        min_date: dt.date | None = form.min_date
 | 
					        min_date: dt.date | None = form.min_date
 | 
				
			||||||
@@ -69,7 +69,7 @@ class NotAfterOffsetItems:
 | 
				
			|||||||
    """The validator to check if the date is not after the offset items."""
 | 
					    """The validator to check if the date is not after the offset items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __call__(self, form: FlaskForm, field: DateField) -> None:
 | 
					    def __call__(self, form: FlaskForm, field: DateField) -> None:
 | 
				
			||||||
        assert isinstance(form, VoucherForm)
 | 
					        assert isinstance(form, JournalEntryForm)
 | 
				
			||||||
        if field.data is None:
 | 
					        if field.data is None:
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        max_date: dt.date | None = form.max_date
 | 
					        max_date: dt.date | None = form.max_date
 | 
				
			||||||
@@ -92,7 +92,7 @@ class CannotDeleteOriginalLineItemsWithOffset:
 | 
				
			|||||||
    """The validator to check the original line items with offset."""
 | 
					    """The validator to check the original line items with offset."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __call__(self, form: FlaskForm, field: FieldList) -> None:
 | 
					    def __call__(self, form: FlaskForm, field: FieldList) -> None:
 | 
				
			||||||
        assert isinstance(form, VoucherForm)
 | 
					        assert isinstance(form, JournalEntryForm)
 | 
				
			||||||
        if form.obj is None:
 | 
					        if form.obj is None:
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        existing_matched_original_line_item_id: set[int] \
 | 
					        existing_matched_original_line_item_id: set[int] \
 | 
				
			||||||
@@ -105,8 +105,8 @@ class CannotDeleteOriginalLineItemsWithOffset:
 | 
				
			|||||||
                    "Line items with offset cannot be deleted."))
 | 
					                    "Line items with offset cannot be deleted."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VoucherForm(FlaskForm):
 | 
					class JournalEntryForm(FlaskForm):
 | 
				
			||||||
    """The base form to create or edit a voucher."""
 | 
					    """The base form to create or edit a journal entry."""
 | 
				
			||||||
    date = DateField()
 | 
					    date = DateField()
 | 
				
			||||||
    """The date."""
 | 
					    """The date."""
 | 
				
			||||||
    currencies = FieldList(FormField(CurrencyForm))
 | 
					    currencies = FieldList(FormField(CurrencyForm))
 | 
				
			||||||
@@ -115,20 +115,20 @@ class VoucherForm(FlaskForm):
 | 
				
			|||||||
    """The note."""
 | 
					    """The note."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
        """Constructs a base voucher form.
 | 
					        """Constructs a base journal entry form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param args: The arguments.
 | 
					        :param args: The arguments.
 | 
				
			||||||
        :param kwargs: The keyword arguments.
 | 
					        :param kwargs: The keyword arguments.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
        self.is_modified: bool = False
 | 
					        self.is_modified: bool = False
 | 
				
			||||||
        """Whether the voucher is modified during populate_obj()."""
 | 
					        """Whether the journal entry is modified during populate_obj()."""
 | 
				
			||||||
        self.collector: t.Type[LineItemCollector] = LineItemCollector
 | 
					        self.collector: t.Type[LineItemCollector] = LineItemCollector
 | 
				
			||||||
        """The line item collector.  The default is the base abstract
 | 
					        """The line item collector.  The default is the base abstract
 | 
				
			||||||
        collector only to provide the correct type.  The subclass forms should
 | 
					        collector only to provide the correct type.  The subclass forms should
 | 
				
			||||||
        provide their own collectors."""
 | 
					        provide their own collectors."""
 | 
				
			||||||
        self.obj: Voucher | None = kwargs.get("obj")
 | 
					        self.obj: JournalEntry | None = kwargs.get("obj")
 | 
				
			||||||
        """The voucher, when editing an existing one."""
 | 
					        """The journal entry, when editing an existing one."""
 | 
				
			||||||
        self._is_need_payable: bool = False
 | 
					        self._is_need_payable: bool = False
 | 
				
			||||||
        """Whether we need the payable original line items."""
 | 
					        """Whether we need the payable original line items."""
 | 
				
			||||||
        self._is_need_receivable: bool = False
 | 
					        self._is_need_receivable: bool = False
 | 
				
			||||||
@@ -140,17 +140,17 @@ class VoucherForm(FlaskForm):
 | 
				
			|||||||
        """The original line items whose net balances were exceeded by the
 | 
					        """The original line items whose net balances were exceeded by the
 | 
				
			||||||
        amounts in the line item sub-forms."""
 | 
					        amounts in the line item sub-forms."""
 | 
				
			||||||
        for line_item in self.line_items:
 | 
					        for line_item in self.line_items:
 | 
				
			||||||
            line_item.voucher_form = self
 | 
					            line_item.journal_entry_form = self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def populate_obj(self, obj: Voucher) -> None:
 | 
					    def populate_obj(self, obj: JournalEntry) -> None:
 | 
				
			||||||
        """Populates the form data into a voucher object.
 | 
					        """Populates the form data into a journal entry object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param obj: The voucher object.
 | 
					        :param obj: The journal entry object.
 | 
				
			||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        is_new: bool = obj.id is None
 | 
					        is_new: bool = obj.id is None
 | 
				
			||||||
        if is_new:
 | 
					        if is_new:
 | 
				
			||||||
            obj.id = new_id(Voucher)
 | 
					            obj.id = new_id(JournalEntry)
 | 
				
			||||||
        self.date: DateField
 | 
					        self.date: DateField
 | 
				
			||||||
        self.__set_date(obj, self.date.data)
 | 
					        self.__set_date(obj, self.date.data)
 | 
				
			||||||
        obj.note = self.note.data
 | 
					        obj.note = self.note.data
 | 
				
			||||||
@@ -185,31 +185,31 @@ class VoucherForm(FlaskForm):
 | 
				
			|||||||
            line_items.extend(currency.line_items)
 | 
					            line_items.extend(currency.line_items)
 | 
				
			||||||
        return line_items
 | 
					        return line_items
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __set_date(self, obj: Voucher, new_date: dt.date) -> None:
 | 
					    def __set_date(self, obj: JournalEntry, new_date: dt.date) -> None:
 | 
				
			||||||
        """Sets the voucher date and number.
 | 
					        """Sets the journal entry date and number.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param obj: The voucher object.
 | 
					        :param obj: The journal entry object.
 | 
				
			||||||
        :param new_date: The new date.
 | 
					        :param new_date: The new date.
 | 
				
			||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if obj.date is None or obj.date != new_date:
 | 
					        if obj.date is None or obj.date != new_date:
 | 
				
			||||||
            if obj.date is not None:
 | 
					            if obj.date is not None:
 | 
				
			||||||
                sort_vouchers_in(obj.date, obj.id)
 | 
					                sort_journal_entries_in(obj.date, obj.id)
 | 
				
			||||||
            if self.max_date is not None and new_date == self.max_date:
 | 
					            if self.max_date is not None and new_date == self.max_date:
 | 
				
			||||||
                db_min_no: int | None = db.session.scalar(
 | 
					                db_min_no: int | None = db.session.scalar(
 | 
				
			||||||
                    sa.select(sa.func.min(Voucher.no))
 | 
					                    sa.select(sa.func.min(JournalEntry.no))
 | 
				
			||||||
                    .filter(Voucher.date == new_date))
 | 
					                    .filter(JournalEntry.date == new_date))
 | 
				
			||||||
                if db_min_no is None:
 | 
					                if db_min_no is None:
 | 
				
			||||||
                    obj.date = new_date
 | 
					                    obj.date = new_date
 | 
				
			||||||
                    obj.no = 1
 | 
					                    obj.no = 1
 | 
				
			||||||
                else:
 | 
					                else:
 | 
				
			||||||
                    obj.date = new_date
 | 
					                    obj.date = new_date
 | 
				
			||||||
                    obj.no = db_min_no - 1
 | 
					                    obj.no = db_min_no - 1
 | 
				
			||||||
                    sort_vouchers_in(new_date)
 | 
					                    sort_journal_entries_in(new_date)
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                sort_vouchers_in(new_date, obj.id)
 | 
					                sort_journal_entries_in(new_date, obj.id)
 | 
				
			||||||
                count: int = Voucher.query\
 | 
					                count: int = JournalEntry.query\
 | 
				
			||||||
                    .filter(Voucher.date == new_date).count()
 | 
					                    .filter(JournalEntry.date == new_date).count()
 | 
				
			||||||
                obj.date = new_date
 | 
					                obj.date = new_date
 | 
				
			||||||
                obj.no = count + 1
 | 
					                obj.no = count + 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -289,7 +289,7 @@ class VoucherForm(FlaskForm):
 | 
				
			|||||||
               if x.original_line_item_id.data is not None}
 | 
					               if x.original_line_item_id.data is not None}
 | 
				
			||||||
        if len(original_line_item_id) == 0:
 | 
					        if len(original_line_item_id) == 0:
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
        select: sa.Select = sa.select(sa.func.max(Voucher.date))\
 | 
					        select: sa.Select = sa.select(sa.func.max(JournalEntry.date))\
 | 
				
			||||||
            .join(JournalEntryLineItem)\
 | 
					            .join(JournalEntryLineItem)\
 | 
				
			||||||
            .filter(JournalEntryLineItem.id.in_(original_line_item_id))
 | 
					            .filter(JournalEntryLineItem.id.in_(original_line_item_id))
 | 
				
			||||||
        return db.session.scalar(select)
 | 
					        return db.session.scalar(select)
 | 
				
			||||||
@@ -302,30 +302,30 @@ class VoucherForm(FlaskForm):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        line_item_id: set[int] = {x.eid.data for x in self.line_items
 | 
					        line_item_id: set[int] = {x.eid.data for x in self.line_items
 | 
				
			||||||
                                  if x.eid.data is not None}
 | 
					                                  if x.eid.data is not None}
 | 
				
			||||||
        select: sa.Select = sa.select(sa.func.min(Voucher.date))\
 | 
					        select: sa.Select = sa.select(sa.func.min(JournalEntry.date))\
 | 
				
			||||||
            .join(JournalEntryLineItem)\
 | 
					            .join(JournalEntryLineItem)\
 | 
				
			||||||
            .filter(JournalEntryLineItem.original_line_item_id
 | 
					            .filter(JournalEntryLineItem.original_line_item_id
 | 
				
			||||||
                    .in_(line_item_id))
 | 
					                    .in_(line_item_id))
 | 
				
			||||||
        return db.session.scalar(select)
 | 
					        return db.session.scalar(select)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
T = t.TypeVar("T", bound=VoucherForm)
 | 
					T = t.TypeVar("T", bound=JournalEntryForm)
 | 
				
			||||||
"""A voucher form variant."""
 | 
					"""A journal entry form variant."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LineItemCollector(t.Generic[T], ABC):
 | 
					class LineItemCollector(t.Generic[T], ABC):
 | 
				
			||||||
    """The line item collector."""
 | 
					    """The line item collector."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, form: T, obj: Voucher):
 | 
					    def __init__(self, form: T, obj: JournalEntry):
 | 
				
			||||||
        """Constructs the line item collector.
 | 
					        """Constructs the line item collector.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param form: The voucher form.
 | 
					        :param form: The journal entry form.
 | 
				
			||||||
        :param obj: The voucher.
 | 
					        :param obj: The journal entry.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.form: T = form
 | 
					        self.form: T = form
 | 
				
			||||||
        """The voucher form."""
 | 
					        """The journal entry form."""
 | 
				
			||||||
        self.__obj: Voucher = obj
 | 
					        self.__obj: JournalEntry = obj
 | 
				
			||||||
        """The voucher object."""
 | 
					        """The journal entry object."""
 | 
				
			||||||
        self.__line_items: list[JournalEntryLineItem] = list(obj.line_items)
 | 
					        self.__line_items: list[JournalEntryLineItem] = list(obj.line_items)
 | 
				
			||||||
        """The existing line items."""
 | 
					        """The existing line items."""
 | 
				
			||||||
        self.__line_items_by_id: dict[int, JournalEntryLineItem] \
 | 
					        self.__line_items_by_id: dict[int, JournalEntryLineItem] \
 | 
				
			||||||
@@ -334,8 +334,8 @@ class LineItemCollector(t.Generic[T], ABC):
 | 
				
			|||||||
        self.__no_by_id: dict[int, int] \
 | 
					        self.__no_by_id: dict[int, int] \
 | 
				
			||||||
            = {x.id: x.no for x in self.__line_items}
 | 
					            = {x.id: x.no for x in self.__line_items}
 | 
				
			||||||
        """A dictionary from the line item number to their line items."""
 | 
					        """A dictionary from the line item number to their line items."""
 | 
				
			||||||
        self.__currencies: list[VoucherCurrency] = obj.currencies
 | 
					        self.__currencies: list[JournalEntryCurrency] = obj.currencies
 | 
				
			||||||
        """The currencies in the voucher."""
 | 
					        """The currencies in the journal entry."""
 | 
				
			||||||
        self._debit_no: int = 1
 | 
					        self._debit_no: int = 1
 | 
				
			||||||
        """The number index for the debit line items."""
 | 
					        """The number index for the debit line items."""
 | 
				
			||||||
        self._credit_no: int = 1
 | 
					        self._credit_no: int = 1
 | 
				
			||||||
@@ -378,12 +378,12 @@ class LineItemCollector(t.Generic[T], ABC):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def _make_cash_line_item(self, forms: list[LineItemForm], is_debit: bool,
 | 
					    def _make_cash_line_item(self, forms: list[LineItemForm], is_debit: bool,
 | 
				
			||||||
                             currency_code: str, no: int) -> None:
 | 
					                             currency_code: str, no: int) -> None:
 | 
				
			||||||
        """Composes the cash line item at the other debit or credit of the cash
 | 
					        """Composes the cash line item at the other debit or credit of the
 | 
				
			||||||
        voucher.
 | 
					        cash journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param forms: The line item forms in the same currency.
 | 
					        :param forms: The line item forms in the same currency.
 | 
				
			||||||
        :param is_debit: True for a cash receipt voucher, or False for a
 | 
					        :param is_debit: True for a cash receipt journal entry, or False for a
 | 
				
			||||||
            cash disbursement voucher.
 | 
					            cash disbursement journal entry.
 | 
				
			||||||
        :param currency_code: The code of the currency.
 | 
					        :param currency_code: The code of the currency.
 | 
				
			||||||
        :param no: The number of the line item.
 | 
					        :param no: The number of the line item.
 | 
				
			||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
@@ -449,8 +449,8 @@ class LineItemCollector(t.Generic[T], ABC):
 | 
				
			|||||||
                                  ord_by_form.get(x)))
 | 
					                                  ord_by_form.get(x)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CashReceiptVoucherForm(VoucherForm):
 | 
					class CashReceiptJournalEntryForm(JournalEntryForm):
 | 
				
			||||||
    """The form to create or edit a cash receipt voucher."""
 | 
					    """The form to create or edit a cash receipt journal entry."""
 | 
				
			||||||
    date = DateField(
 | 
					    date = DateField(
 | 
				
			||||||
        validators=[DATE_REQUIRED,
 | 
					        validators=[DATE_REQUIRED,
 | 
				
			||||||
                    NotBeforeOriginalLineItems(),
 | 
					                    NotBeforeOriginalLineItems(),
 | 
				
			||||||
@@ -469,8 +469,8 @@ class CashReceiptVoucherForm(VoucherForm):
 | 
				
			|||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
        self._is_need_receivable = True
 | 
					        self._is_need_receivable = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        class Collector(LineItemCollector[CashReceiptVoucherForm]):
 | 
					        class Collector(LineItemCollector[CashReceiptJournalEntryForm]):
 | 
				
			||||||
            """The line item collector for the cash receipt vouchers."""
 | 
					            """The line item collector for the cash receipt journal entries."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            def collect(self) -> None:
 | 
					            def collect(self) -> None:
 | 
				
			||||||
                currencies: list[CashReceiptCurrencyForm] \
 | 
					                currencies: list[CashReceiptCurrencyForm] \
 | 
				
			||||||
@@ -495,8 +495,8 @@ class CashReceiptVoucherForm(VoucherForm):
 | 
				
			|||||||
        self.collector = Collector
 | 
					        self.collector = Collector
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CashDisbursementVoucherForm(VoucherForm):
 | 
					class CashDisbursementJournalEntryForm(JournalEntryForm):
 | 
				
			||||||
    """The form to create or edit a cash disbursement voucher."""
 | 
					    """The form to create or edit a cash disbursement journal entry."""
 | 
				
			||||||
    date = DateField(
 | 
					    date = DateField(
 | 
				
			||||||
        validators=[DATE_REQUIRED,
 | 
					        validators=[DATE_REQUIRED,
 | 
				
			||||||
                    NotBeforeOriginalLineItems(),
 | 
					                    NotBeforeOriginalLineItems(),
 | 
				
			||||||
@@ -516,8 +516,9 @@ class CashDisbursementVoucherForm(VoucherForm):
 | 
				
			|||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
        self._is_need_payable = True
 | 
					        self._is_need_payable = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        class Collector(LineItemCollector[CashDisbursementVoucherForm]):
 | 
					        class Collector(LineItemCollector[CashDisbursementJournalEntryForm]):
 | 
				
			||||||
            """The line item collector for the cash disbursement vouchers."""
 | 
					            """The line item collector for the cash disbursement journal
 | 
				
			||||||
 | 
					            entries."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            def collect(self) -> None:
 | 
					            def collect(self) -> None:
 | 
				
			||||||
                currencies: list[CashDisbursementCurrencyForm] \
 | 
					                currencies: list[CashDisbursementCurrencyForm] \
 | 
				
			||||||
@@ -542,8 +543,8 @@ class CashDisbursementVoucherForm(VoucherForm):
 | 
				
			|||||||
        self.collector = Collector
 | 
					        self.collector = Collector
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TransferVoucherForm(VoucherForm):
 | 
					class TransferJournalEntryForm(JournalEntryForm):
 | 
				
			||||||
    """The form to create or edit a transfer voucher."""
 | 
					    """The form to create or edit a transfer journal entry."""
 | 
				
			||||||
    date = DateField(
 | 
					    date = DateField(
 | 
				
			||||||
        validators=[DATE_REQUIRED,
 | 
					        validators=[DATE_REQUIRED,
 | 
				
			||||||
                    NotBeforeOriginalLineItems(),
 | 
					                    NotBeforeOriginalLineItems(),
 | 
				
			||||||
@@ -563,8 +564,8 @@ class TransferVoucherForm(VoucherForm):
 | 
				
			|||||||
        self._is_need_payable = True
 | 
					        self._is_need_payable = True
 | 
				
			||||||
        self._is_need_receivable = True
 | 
					        self._is_need_receivable = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        class Collector(LineItemCollector[TransferVoucherForm]):
 | 
					        class Collector(LineItemCollector[TransferJournalEntryForm]):
 | 
				
			||||||
            """The line item collector for the transfer vouchers."""
 | 
					            """The line item collector for the transfer journal entries."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            def collect(self) -> None:
 | 
					            def collect(self) -> None:
 | 
				
			||||||
                currencies: list[TransferCurrencyForm] \
 | 
					                currencies: list[TransferCurrencyForm] \
 | 
				
			||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
#  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 line item sub-forms for the voucher management.
 | 
					"""The line item sub-forms for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
@@ -238,9 +238,9 @@ class NotExceedingOriginalLineItemNetBalance:
 | 
				
			|||||||
            return
 | 
					            return
 | 
				
			||||||
        is_debit: bool = isinstance(form, DebitLineItemForm)
 | 
					        is_debit: bool = isinstance(form, DebitLineItemForm)
 | 
				
			||||||
        existing_line_item_id: set[int] = set()
 | 
					        existing_line_item_id: set[int] = set()
 | 
				
			||||||
        if form.voucher_form.obj is not None:
 | 
					        if form.journal_entry_form.obj is not None:
 | 
				
			||||||
            existing_line_item_id \
 | 
					            existing_line_item_id \
 | 
				
			||||||
                = {x.id for x in form.voucher_form.obj.line_items}
 | 
					                = {x.id for x in form.journal_entry_form.obj.line_items}
 | 
				
			||||||
        offset_total_func: sa.Function = sa.func.sum(sa.case(
 | 
					        offset_total_func: sa.Function = sa.func.sum(sa.case(
 | 
				
			||||||
            (be(JournalEntryLineItem.is_debit == is_debit),
 | 
					            (be(JournalEntryLineItem.is_debit == is_debit),
 | 
				
			||||||
             JournalEntryLineItem.amount),
 | 
					             JournalEntryLineItem.amount),
 | 
				
			||||||
@@ -253,7 +253,7 @@ class NotExceedingOriginalLineItemNetBalance:
 | 
				
			|||||||
        if offset_total_but_form is None:
 | 
					        if offset_total_but_form is None:
 | 
				
			||||||
            offset_total_but_form = Decimal("0")
 | 
					            offset_total_but_form = Decimal("0")
 | 
				
			||||||
        offset_total_on_form: Decimal = sum(
 | 
					        offset_total_on_form: Decimal = sum(
 | 
				
			||||||
            [x.amount.data for x in form.voucher_form.line_items
 | 
					            [x.amount.data for x in form.journal_entry_form.line_items
 | 
				
			||||||
             if x.original_line_item_id.data == original_line_item.id
 | 
					             if x.original_line_item_id.data == original_line_item.id
 | 
				
			||||||
             and x.amount != field and x.amount.data is not None])
 | 
					             and x.amount != field and x.amount.data is not None])
 | 
				
			||||||
        net_balance: Decimal = original_line_item.amount \
 | 
					        net_balance: Decimal = original_line_item.amount \
 | 
				
			||||||
@@ -307,9 +307,9 @@ class LineItemForm(FlaskForm):
 | 
				
			|||||||
        :param kwargs: The keyword arguments.
 | 
					        :param kwargs: The keyword arguments.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
        from .voucher import VoucherForm
 | 
					        from .journal_entry import JournalEntryForm
 | 
				
			||||||
        self.voucher_form: VoucherForm | None = None
 | 
					        self.journal_entry_form: JournalEntryForm | None = None
 | 
				
			||||||
        """The source voucher form."""
 | 
					        """The source journal entry form."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def account_text(self) -> str:
 | 
					    def account_text(self) -> str:
 | 
				
			||||||
@@ -346,7 +346,7 @@ class LineItemForm(FlaskForm):
 | 
				
			|||||||
        :return: The text representation of the original line item.
 | 
					        :return: The text representation of the original line item.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return None if self.__original_line_item is None \
 | 
					        return None if self.__original_line_item is None \
 | 
				
			||||||
            else self.__original_line_item.voucher.date
 | 
					            else self.__original_line_item.journal_entry.date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def original_line_item_text(self) -> str | None:
 | 
					    def original_line_item_text(self) -> str | None:
 | 
				
			||||||
@@ -389,10 +389,11 @@ class LineItemForm(FlaskForm):
 | 
				
			|||||||
                return JournalEntryLineItem.query\
 | 
					                return JournalEntryLineItem.query\
 | 
				
			||||||
                    .filter(JournalEntryLineItem.original_line_item_id
 | 
					                    .filter(JournalEntryLineItem.original_line_item_id
 | 
				
			||||||
                            == self.eid.data)\
 | 
					                            == self.eid.data)\
 | 
				
			||||||
                    .options(selectinload(JournalEntryLineItem.voucher),
 | 
					                    .options(selectinload(JournalEntryLineItem.journal_entry),
 | 
				
			||||||
                             selectinload(JournalEntryLineItem.account),
 | 
					                             selectinload(JournalEntryLineItem.account),
 | 
				
			||||||
                             selectinload(JournalEntryLineItem.offsets)
 | 
					                             selectinload(JournalEntryLineItem.offsets)
 | 
				
			||||||
                             .selectinload(JournalEntryLineItem.voucher)).all()
 | 
					                             .selectinload(
 | 
				
			||||||
 | 
					                                 JournalEntryLineItem.journal_entry)).all()
 | 
				
			||||||
            setattr(self, "__offsets", get_offsets())
 | 
					            setattr(self, "__offsets", get_offsets())
 | 
				
			||||||
        return getattr(self, "__offsets")
 | 
					        return getattr(self, "__offsets")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										95
									
								
								src/accounting/journal_entry/forms/reorder.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/accounting/journal_entry/forms/reorder.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,95 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  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 reorder forms for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask import request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.models import JournalEntry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def sort_journal_entries_in(journal_entry_date: date,
 | 
				
			||||||
 | 
					                            exclude: int | None = None) -> None:
 | 
				
			||||||
 | 
					    """Sorts the journal entries under a date after changing the date or
 | 
				
			||||||
 | 
					    deleting a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry_date: The date of the journal entry.
 | 
				
			||||||
 | 
					    :param exclude: The journal entry ID to exclude.
 | 
				
			||||||
 | 
					    :return: None.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					        = [JournalEntry.date == journal_entry_date]
 | 
				
			||||||
 | 
					    if exclude is not None:
 | 
				
			||||||
 | 
					        conditions.append(JournalEntry.id != exclude)
 | 
				
			||||||
 | 
					    journal_entries: list[JournalEntry] = JournalEntry.query\
 | 
				
			||||||
 | 
					        .filter(*conditions)\
 | 
				
			||||||
 | 
					        .order_by(JournalEntry.no).all()
 | 
				
			||||||
 | 
					    for i in range(len(journal_entries)):
 | 
				
			||||||
 | 
					        if journal_entries[i].no != i + 1:
 | 
				
			||||||
 | 
					            journal_entries[i].no = i + 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JournalEntryReorderForm:
 | 
				
			||||||
 | 
					    """The form to reorder the journal entries."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, journal_entry_date: date):
 | 
				
			||||||
 | 
					        """Constructs the form to reorder the journal entries in a day.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry_date: The date.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.date: date = journal_entry_date
 | 
				
			||||||
 | 
					        self.is_modified: bool = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def save_order(self) -> None:
 | 
				
			||||||
 | 
					        """Saves the order of the account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        journal_entries: list[JournalEntry] = JournalEntry.query\
 | 
				
			||||||
 | 
					            .filter(JournalEntry.date == self.date).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Collects the specified order.
 | 
				
			||||||
 | 
					        orders: dict[JournalEntry, int] = {}
 | 
				
			||||||
 | 
					        for journal_entry in journal_entries:
 | 
				
			||||||
 | 
					            if f"{journal_entry.id}-no" in request.form:
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    orders[journal_entry] \
 | 
				
			||||||
 | 
					                        = int(request.form[f"{journal_entry.id}-no"])
 | 
				
			||||||
 | 
					                except ValueError:
 | 
				
			||||||
 | 
					                    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Missing and invalid orders are appended to the end.
 | 
				
			||||||
 | 
					        missing: list[JournalEntry] \
 | 
				
			||||||
 | 
					            = [x for x in journal_entries if x not in orders]
 | 
				
			||||||
 | 
					        if len(missing) > 0:
 | 
				
			||||||
 | 
					            next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
 | 
				
			||||||
 | 
					            for journal_entry in missing:
 | 
				
			||||||
 | 
					                orders[journal_entry] = next_no
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Sort by the specified order first, and their original order.
 | 
				
			||||||
 | 
					        journal_entries.sort(key=lambda x: (orders[x], x.no))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Update the orders.
 | 
				
			||||||
 | 
					        with db.session.no_autoflush:
 | 
				
			||||||
 | 
					            for i in range(len(journal_entries)):
 | 
				
			||||||
 | 
					                if journal_entries[i].no != i + 1:
 | 
				
			||||||
 | 
					                    journal_entries[i].no = i + 1
 | 
				
			||||||
 | 
					                    self.is_modified = True
 | 
				
			||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
#  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 template filters for the voucher management.
 | 
					"""The template filters for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
from decimal import Decimal
 | 
					from decimal import Decimal
 | 
				
			||||||
@@ -26,10 +26,10 @@ from flask import request
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def with_type(uri: str) -> str:
 | 
					def with_type(uri: str) -> str:
 | 
				
			||||||
    """Adds the voucher type to the URI, if it is specified.
 | 
					    """Adds the journal entry type to the URI, if it is specified.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param uri: The URI.
 | 
					    :param uri: The URI.
 | 
				
			||||||
    :return: The result URL, optionally with the voucher type added.
 | 
					    :return: The result URL, optionally with the journal entry type added.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if "as" not in request.args:
 | 
					    if "as" not in request.args:
 | 
				
			||||||
        return uri
 | 
					        return uri
 | 
				
			||||||
@@ -43,10 +43,10 @@ def with_type(uri: str) -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def to_transfer(uri: str) -> str:
 | 
					def to_transfer(uri: str) -> str:
 | 
				
			||||||
    """Adds the transfer voucher type to the URI.
 | 
					    """Adds the transfer journal entry type to the URI.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param uri: The URI.
 | 
					    :param uri: The URI.
 | 
				
			||||||
    :return: The result URL, with the transfer voucher type added.
 | 
					    :return: The result URL, with the transfer journal entry type added.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    uri_p: ParseResult = urlparse(uri)
 | 
					    uri_p: ParseResult = urlparse(uri)
 | 
				
			||||||
    params: list[tuple[str, str]] = parse_qsl(uri_p.query)
 | 
					    params: list[tuple[str, str]] = parse_qsl(uri_p.query)
 | 
				
			||||||
@@ -14,6 +14,6 @@
 | 
				
			|||||||
#  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 utilities for the voucher management.
 | 
					"""The utilities for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
#  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 account option for the voucher management.
 | 
					"""The account option for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
from accounting.models import Account
 | 
					from accounting.models import Account
 | 
				
			||||||
							
								
								
									
										336
									
								
								src/accounting/journal_entry/utils/operators.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								src/accounting/journal_entry/utils/operators.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,336 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  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 operators for different journal entry types.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import typing as t
 | 
				
			||||||
 | 
					from abc import ABC, abstractmethod
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask import render_template, request, abort
 | 
				
			||||||
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.models import JournalEntry
 | 
				
			||||||
 | 
					from accounting.template_globals import default_currency_code
 | 
				
			||||||
 | 
					from accounting.utils.journal_entry_types import JournalEntryType
 | 
				
			||||||
 | 
					from accounting.journal_entry.forms import JournalEntryForm, \
 | 
				
			||||||
 | 
					    CashReceiptJournalEntryForm, CashDisbursementJournalEntryForm, \
 | 
				
			||||||
 | 
					    TransferJournalEntryForm
 | 
				
			||||||
 | 
					from accounting.journal_entry.forms.line_item import LineItemForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JournalEntryOperator(ABC):
 | 
				
			||||||
 | 
					    """The base journal entry operator."""
 | 
				
			||||||
 | 
					    CHECK_ORDER: int = -1
 | 
				
			||||||
 | 
					    """The order when checking the journal entry operator."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def form(self) -> t.Type[JournalEntryForm]:
 | 
				
			||||||
 | 
					        """Returns the form class.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The form class.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def render_create_template(self, form: FlaskForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to create a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param form: The journal entry form.
 | 
				
			||||||
 | 
					        :return: the form to create a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def render_detail_template(self, journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the detail page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: the detail page.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def render_edit_template(self, journal_entry: JournalEntry,
 | 
				
			||||||
 | 
					                             form: FlaskForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to edit a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :param form: The form.
 | 
				
			||||||
 | 
					        :return: the form to edit a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def is_my_type(self, journal_entry: JournalEntry) -> bool:
 | 
				
			||||||
 | 
					        """Checks and returns whether the journal entry belongs to the type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: True if the journal entry belongs to the type, or False
 | 
				
			||||||
 | 
					            otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def _line_item_template(self) -> str:
 | 
				
			||||||
 | 
					        """Renders and returns the template for the line item sub-form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The template for the line item sub-form.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/include/form-line-item.html",
 | 
				
			||||||
 | 
					            currency_index="CURRENCY_INDEX",
 | 
				
			||||||
 | 
					            debit_credit="DEBIT_CREDIT",
 | 
				
			||||||
 | 
					            line_item_index="LINE_ITEM_INDEX",
 | 
				
			||||||
 | 
					            form=LineItemForm())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CashReceiptJournalEntry(JournalEntryOperator):
 | 
				
			||||||
 | 
					    """A cash receipt journal entry."""
 | 
				
			||||||
 | 
					    CHECK_ORDER: int = 2
 | 
				
			||||||
 | 
					    """The order when checking the journal entry operator."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def form(self) -> t.Type[JournalEntryForm]:
 | 
				
			||||||
 | 
					        """Returns the form class.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The form class.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return CashReceiptJournalEntryForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_create_template(self, form: CashReceiptJournalEntryForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to create a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param form: The journal entry form.
 | 
				
			||||||
 | 
					        :return: the form to create a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/receipt/create.html",
 | 
				
			||||||
 | 
					            form=form,
 | 
				
			||||||
 | 
					            journal_entry_type=JournalEntryType.CASH_RECEIPT,
 | 
				
			||||||
 | 
					            currency_template=self.__currency_template,
 | 
				
			||||||
 | 
					            line_item_template=self._line_item_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_detail_template(self, journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the detail page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: the detail page.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template("accounting/journal-entry/receipt/detail.html",
 | 
				
			||||||
 | 
					                               obj=journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_edit_template(self, journal_entry: JournalEntry,
 | 
				
			||||||
 | 
					                             form: CashReceiptJournalEntryForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to edit a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :param form: The form.
 | 
				
			||||||
 | 
					        :return: the form to edit a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template("accounting/journal-entry/receipt/edit.html",
 | 
				
			||||||
 | 
					                               journal_entry=journal_entry, form=form,
 | 
				
			||||||
 | 
					                               currency_template=self.__currency_template,
 | 
				
			||||||
 | 
					                               line_item_template=self._line_item_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_my_type(self, journal_entry: JournalEntry) -> bool:
 | 
				
			||||||
 | 
					        """Checks and returns whether the journal entry belongs to the type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: True if the journal entry belongs to the type, or False
 | 
				
			||||||
 | 
					            otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return journal_entry.is_cash_receipt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __currency_template(self) -> str:
 | 
				
			||||||
 | 
					        """Renders and returns the template for the currency sub-form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The template for the currency sub-form.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/receipt/include/form-currency.html",
 | 
				
			||||||
 | 
					            currency_index="CURRENCY_INDEX",
 | 
				
			||||||
 | 
					            currency_code_data=default_currency_code(),
 | 
				
			||||||
 | 
					            credit_total="-")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CashDisbursementJournalEntry(JournalEntryOperator):
 | 
				
			||||||
 | 
					    """A cash disbursement journal entry."""
 | 
				
			||||||
 | 
					    CHECK_ORDER: int = 1
 | 
				
			||||||
 | 
					    """The order when checking the journal entry operator."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def form(self) -> t.Type[JournalEntryForm]:
 | 
				
			||||||
 | 
					        """Returns the form class.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The form class.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return CashDisbursementJournalEntryForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_create_template(self, form: CashDisbursementJournalEntryForm) \
 | 
				
			||||||
 | 
					            -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to create a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param form: The journal entry form.
 | 
				
			||||||
 | 
					        :return: the form to create a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/disbursement/create.html",
 | 
				
			||||||
 | 
					            form=form,
 | 
				
			||||||
 | 
					            journal_entry_type=JournalEntryType.CASH_DISBURSEMENT,
 | 
				
			||||||
 | 
					            currency_template=self.__currency_template,
 | 
				
			||||||
 | 
					            line_item_template=self._line_item_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_detail_template(self, journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the detail page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: the detail page.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/disbursement/detail.html",
 | 
				
			||||||
 | 
					            obj=journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_edit_template(self, journal_entry: JournalEntry,
 | 
				
			||||||
 | 
					                             form: CashDisbursementJournalEntryForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to edit a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :param form: The form.
 | 
				
			||||||
 | 
					        :return: the form to edit a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/disbursement/edit.html",
 | 
				
			||||||
 | 
					            journal_entry=journal_entry, form=form,
 | 
				
			||||||
 | 
					            currency_template=self.__currency_template,
 | 
				
			||||||
 | 
					            line_item_template=self._line_item_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_my_type(self, journal_entry: JournalEntry) -> bool:
 | 
				
			||||||
 | 
					        """Checks and returns whether the journal entry belongs to the type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: True if the journal entry belongs to the type, or False
 | 
				
			||||||
 | 
					            otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return journal_entry.is_cash_disbursement
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __currency_template(self) -> str:
 | 
				
			||||||
 | 
					        """Renders and returns the template for the currency sub-form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The template for the currency sub-form.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/disbursement/include/form-currency.html",
 | 
				
			||||||
 | 
					            currency_index="CURRENCY_INDEX",
 | 
				
			||||||
 | 
					            currency_code_data=default_currency_code(),
 | 
				
			||||||
 | 
					            debit_total="-")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TransferJournalEntry(JournalEntryOperator):
 | 
				
			||||||
 | 
					    """A transfer journal entry."""
 | 
				
			||||||
 | 
					    CHECK_ORDER: int = 3
 | 
				
			||||||
 | 
					    """The order when checking the journal entry operator."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def form(self) -> t.Type[JournalEntryForm]:
 | 
				
			||||||
 | 
					        """Returns the form class.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The form class.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return TransferJournalEntryForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_create_template(self, form: TransferJournalEntryForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to create a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param form: The journal entry form.
 | 
				
			||||||
 | 
					        :return: the form to create a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/transfer/create.html",
 | 
				
			||||||
 | 
					            form=form,
 | 
				
			||||||
 | 
					            journal_entry_type=JournalEntryType.TRANSFER,
 | 
				
			||||||
 | 
					            currency_template=self.__currency_template,
 | 
				
			||||||
 | 
					            line_item_template=self._line_item_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_detail_template(self, journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the detail page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: the detail page.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template("accounting/journal-entry/transfer/detail.html",
 | 
				
			||||||
 | 
					                               obj=journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_edit_template(self, journal_entry: JournalEntry,
 | 
				
			||||||
 | 
					                             form: TransferJournalEntryForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to edit a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :param form: The form.
 | 
				
			||||||
 | 
					        :return: the form to edit a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template("accounting/journal-entry/transfer/edit.html",
 | 
				
			||||||
 | 
					                               journal_entry=journal_entry, form=form,
 | 
				
			||||||
 | 
					                               currency_template=self.__currency_template,
 | 
				
			||||||
 | 
					                               line_item_template=self._line_item_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_my_type(self, journal_entry: JournalEntry) -> bool:
 | 
				
			||||||
 | 
					        """Checks and returns whether the journal entry belongs to the type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: True if the journal entry belongs to the type, or False
 | 
				
			||||||
 | 
					            otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __currency_template(self) -> str:
 | 
				
			||||||
 | 
					        """Renders and returns the template for the currency sub-form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The template for the currency sub-form.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/transfer/include/form-currency.html",
 | 
				
			||||||
 | 
					            currency_index="CURRENCY_INDEX",
 | 
				
			||||||
 | 
					            currency_code_data=default_currency_code(),
 | 
				
			||||||
 | 
					            debit_total="-", credit_total="-")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					JOURNAL_ENTRY_TYPE_TO_OP: dict[JournalEntryType, JournalEntryOperator] \
 | 
				
			||||||
 | 
					    = {JournalEntryType.CASH_RECEIPT: CashReceiptJournalEntry(),
 | 
				
			||||||
 | 
					       JournalEntryType.CASH_DISBURSEMENT: CashDisbursementJournalEntry(),
 | 
				
			||||||
 | 
					       JournalEntryType.TRANSFER: TransferJournalEntry()}
 | 
				
			||||||
 | 
					"""The map from the journal entry types to their operators."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_journal_entry_op(journal_entry: JournalEntry,
 | 
				
			||||||
 | 
					                         is_check_as: bool = False) -> JournalEntryOperator:
 | 
				
			||||||
 | 
					    """Returns the journal entry operator that may be specified in the "as"
 | 
				
			||||||
 | 
					    query parameter.  If it is not specified, check the journal entry type from
 | 
				
			||||||
 | 
					    the journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					    :param is_check_as: True to check the "as" parameter, or False otherwise.
 | 
				
			||||||
 | 
					    :return: None.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if is_check_as and "as" in request.args:
 | 
				
			||||||
 | 
					        type_dict: dict[str, JournalEntryType] \
 | 
				
			||||||
 | 
					            = {x.value: x for x in JournalEntryType}
 | 
				
			||||||
 | 
					        if request.args["as"] not in type_dict:
 | 
				
			||||||
 | 
					            abort(404)
 | 
				
			||||||
 | 
					        return JOURNAL_ENTRY_TYPE_TO_OP[type_dict[request.args["as"]]]
 | 
				
			||||||
 | 
					    for journal_entry_type in sorted(JOURNAL_ENTRY_TYPE_TO_OP.values(),
 | 
				
			||||||
 | 
					                                     key=lambda x: x.CHECK_ORDER):
 | 
				
			||||||
 | 
					        if journal_entry_type.is_my_type(journal_entry):
 | 
				
			||||||
 | 
					            return journal_entry_type
 | 
				
			||||||
@@ -23,7 +23,7 @@ import sqlalchemy as sa
 | 
				
			|||||||
from sqlalchemy.orm import selectinload
 | 
					from sqlalchemy.orm import selectinload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.models import Account, Voucher, JournalEntryLineItem
 | 
					from accounting.models import Account, JournalEntry, JournalEntryLineItem
 | 
				
			||||||
from accounting.utils.cast import be
 | 
					from accounting.utils.cast import be
 | 
				
			||||||
from .offset_alias import offset_alias
 | 
					from .offset_alias import offset_alias
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -71,12 +71,12 @@ def get_selectable_original_line_items(
 | 
				
			|||||||
           for x in db.session.execute(select_net_balances).all()}
 | 
					           for x in db.session.execute(select_net_balances).all()}
 | 
				
			||||||
    line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query\
 | 
					    line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query\
 | 
				
			||||||
        .filter(JournalEntryLineItem.id.in_({x for x in net_balances}))\
 | 
					        .filter(JournalEntryLineItem.id.in_({x for x in net_balances}))\
 | 
				
			||||||
        .join(Voucher)\
 | 
					        .join(JournalEntry)\
 | 
				
			||||||
        .order_by(Voucher.date, JournalEntryLineItem.is_debit,
 | 
					        .order_by(JournalEntry.date, JournalEntryLineItem.is_debit,
 | 
				
			||||||
                  JournalEntryLineItem.no)\
 | 
					                  JournalEntryLineItem.no)\
 | 
				
			||||||
        .options(selectinload(JournalEntryLineItem.currency),
 | 
					        .options(selectinload(JournalEntryLineItem.currency),
 | 
				
			||||||
                 selectinload(JournalEntryLineItem.account),
 | 
					                 selectinload(JournalEntryLineItem.account),
 | 
				
			||||||
                 selectinload(JournalEntryLineItem.voucher)).all()
 | 
					                 selectinload(JournalEntryLineItem.journal_entry)).all()
 | 
				
			||||||
    for line_item in line_items:
 | 
					    for line_item in line_items:
 | 
				
			||||||
        line_item.net_balance = line_item.amount \
 | 
					        line_item.net_balance = line_item.amount \
 | 
				
			||||||
            if net_balances[line_item.id] is None \
 | 
					            if net_balances[line_item.id] is None \
 | 
				
			||||||
							
								
								
									
										235
									
								
								src/accounting/journal_entry/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								src/accounting/journal_entry/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,235 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  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 journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					from urllib.parse import parse_qsl, urlencode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask import Blueprint, render_template, session, redirect, request, \
 | 
				
			||||||
 | 
					    flash, url_for
 | 
				
			||||||
 | 
					from werkzeug.datastructures import ImmutableMultiDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
 | 
					from accounting.models import JournalEntry
 | 
				
			||||||
 | 
					from accounting.utils.cast import s
 | 
				
			||||||
 | 
					from accounting.utils.flash_errors import flash_form_errors
 | 
				
			||||||
 | 
					from accounting.utils.next_uri import inherit_next, or_next
 | 
				
			||||||
 | 
					from accounting.utils.permission import has_permission, can_view, can_edit
 | 
				
			||||||
 | 
					from accounting.utils.journal_entry_types import JournalEntryType
 | 
				
			||||||
 | 
					from accounting.utils.user import get_current_user_pk
 | 
				
			||||||
 | 
					from .forms import sort_journal_entries_in, JournalEntryReorderForm
 | 
				
			||||||
 | 
					from .template_filters import with_type, to_transfer, format_amount_input, \
 | 
				
			||||||
 | 
					    text2html
 | 
				
			||||||
 | 
					from .utils.operators import JournalEntryOperator, JOURNAL_ENTRY_TYPE_TO_OP, \
 | 
				
			||||||
 | 
					    get_journal_entry_op
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bp: Blueprint = Blueprint("journal-entry", __name__)
 | 
				
			||||||
 | 
					"""The view blueprint for the journal entry management."""
 | 
				
			||||||
 | 
					bp.add_app_template_filter(with_type, "accounting_journal_entry_with_type")
 | 
				
			||||||
 | 
					bp.add_app_template_filter(to_transfer, "accounting_journal_entry_to_transfer")
 | 
				
			||||||
 | 
					bp.add_app_template_filter(format_amount_input,
 | 
				
			||||||
 | 
					                           "accounting_journal_entry_format_amount_input")
 | 
				
			||||||
 | 
					bp.add_app_template_filter(text2html, "accounting_journal_entry_text2html")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("/create/<journalEntryType:journal_entry_type>", endpoint="create")
 | 
				
			||||||
 | 
					@has_permission(can_edit)
 | 
				
			||||||
 | 
					def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
 | 
				
			||||||
 | 
					    """Shows the form to add a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry_type: The journal entry type.
 | 
				
			||||||
 | 
					    :return: The form to add a journal entry.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    journal_entry_op: JournalEntryOperator \
 | 
				
			||||||
 | 
					        = JOURNAL_ENTRY_TYPE_TO_OP[journal_entry_type]
 | 
				
			||||||
 | 
					    form: journal_entry_op.form
 | 
				
			||||||
 | 
					    if "form" in session:
 | 
				
			||||||
 | 
					        form = journal_entry_op.form(
 | 
				
			||||||
 | 
					            ImmutableMultiDict(parse_qsl(session["form"])))
 | 
				
			||||||
 | 
					        del session["form"]
 | 
				
			||||||
 | 
					        form.validate()
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        form = journal_entry_op.form()
 | 
				
			||||||
 | 
					        form.date.data = date.today()
 | 
				
			||||||
 | 
					    return journal_entry_op.render_create_template(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.post("/store/<journalEntryType:journal_entry_type>", endpoint="store")
 | 
				
			||||||
 | 
					@has_permission(can_edit)
 | 
				
			||||||
 | 
					def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
 | 
				
			||||||
 | 
					    """Adds a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry_type: The journal entry type.
 | 
				
			||||||
 | 
					    :return: The redirection to the journal entry detail on success, or the
 | 
				
			||||||
 | 
					        journal entry creation form on error.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    journal_entry_op: JournalEntryOperator \
 | 
				
			||||||
 | 
					        = JOURNAL_ENTRY_TYPE_TO_OP[journal_entry_type]
 | 
				
			||||||
 | 
					    form: journal_entry_op.form = journal_entry_op.form(request.form)
 | 
				
			||||||
 | 
					    if not form.validate():
 | 
				
			||||||
 | 
					        flash_form_errors(form)
 | 
				
			||||||
 | 
					        session["form"] = urlencode(list(request.form.items()))
 | 
				
			||||||
 | 
					        return redirect(inherit_next(with_type(
 | 
				
			||||||
 | 
					            url_for("accounting.journal-entry.create",
 | 
				
			||||||
 | 
					                    journal_entry_type=journal_entry_type))))
 | 
				
			||||||
 | 
					    journal_entry: JournalEntry = JournalEntry()
 | 
				
			||||||
 | 
					    form.populate_obj(journal_entry)
 | 
				
			||||||
 | 
					    db.session.add(journal_entry)
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    flash(s(lazy_gettext("The journal entry is added successfully")),
 | 
				
			||||||
 | 
					          "success")
 | 
				
			||||||
 | 
					    return redirect(inherit_next(__get_detail_uri(journal_entry)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("/<journalEntry:journal_entry>", endpoint="detail")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def show_journal_entry_detail(journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					    """Shows the journal entry detail.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					    :return: The detail.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    journal_entry_op: JournalEntryOperator \
 | 
				
			||||||
 | 
					        = get_journal_entry_op(journal_entry)
 | 
				
			||||||
 | 
					    return journal_entry_op.render_detail_template(journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("/<journalEntry:journal_entry>/edit", endpoint="edit")
 | 
				
			||||||
 | 
					@has_permission(can_edit)
 | 
				
			||||||
 | 
					def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					    """Shows the form to edit a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					    :return: The form to edit the journal entry.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    journal_entry_op: JournalEntryOperator \
 | 
				
			||||||
 | 
					        = get_journal_entry_op(journal_entry, is_check_as=True)
 | 
				
			||||||
 | 
					    form: journal_entry_op.form
 | 
				
			||||||
 | 
					    if "form" in session:
 | 
				
			||||||
 | 
					        form = journal_entry_op.form(
 | 
				
			||||||
 | 
					            ImmutableMultiDict(parse_qsl(session["form"])))
 | 
				
			||||||
 | 
					        del session["form"]
 | 
				
			||||||
 | 
					        form.obj = journal_entry
 | 
				
			||||||
 | 
					        form.validate()
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        form = journal_entry_op.form(obj=journal_entry)
 | 
				
			||||||
 | 
					    return journal_entry_op.render_edit_template(journal_entry, form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.post("/<journalEntry:journal_entry>/update", endpoint="update")
 | 
				
			||||||
 | 
					@has_permission(can_edit)
 | 
				
			||||||
 | 
					def update_journal_entry(journal_entry: JournalEntry) -> redirect:
 | 
				
			||||||
 | 
					    """Updates a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					    :return: The redirection to the journal entry detail on success, or the
 | 
				
			||||||
 | 
					        journal entry edit form on error.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    journal_entry_op: JournalEntryOperator \
 | 
				
			||||||
 | 
					        = get_journal_entry_op(journal_entry, is_check_as=True)
 | 
				
			||||||
 | 
					    form: journal_entry_op.form = journal_entry_op.form(request.form)
 | 
				
			||||||
 | 
					    form.obj = journal_entry
 | 
				
			||||||
 | 
					    if not form.validate():
 | 
				
			||||||
 | 
					        flash_form_errors(form)
 | 
				
			||||||
 | 
					        session["form"] = urlencode(list(request.form.items()))
 | 
				
			||||||
 | 
					        return redirect(inherit_next(with_type(
 | 
				
			||||||
 | 
					            url_for("accounting.journal-entry.edit",
 | 
				
			||||||
 | 
					                    journal_entry=journal_entry))))
 | 
				
			||||||
 | 
					    with db.session.no_autoflush:
 | 
				
			||||||
 | 
					        form.populate_obj(journal_entry)
 | 
				
			||||||
 | 
					    if not form.is_modified:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The journal entry was not modified.")),
 | 
				
			||||||
 | 
					              "success")
 | 
				
			||||||
 | 
					        return redirect(inherit_next(__get_detail_uri(journal_entry)))
 | 
				
			||||||
 | 
					    journal_entry.updated_by_id = get_current_user_pk()
 | 
				
			||||||
 | 
					    journal_entry.updated_at = sa.func.now()
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    flash(s(lazy_gettext("The journal entry is updated successfully.")),
 | 
				
			||||||
 | 
					          "success")
 | 
				
			||||||
 | 
					    return redirect(inherit_next(__get_detail_uri(journal_entry)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.post("/<journalEntry:journal_entry>/delete", endpoint="delete")
 | 
				
			||||||
 | 
					@has_permission(can_edit)
 | 
				
			||||||
 | 
					def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
 | 
				
			||||||
 | 
					    """Deletes a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					    :return: The redirection to the journal entry list on success, or the
 | 
				
			||||||
 | 
					        journal entry detail on error.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    journal_entry.delete()
 | 
				
			||||||
 | 
					    sort_journal_entries_in(journal_entry.date, journal_entry.id)
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    flash(s(lazy_gettext("The journal entry is deleted successfully.")),
 | 
				
			||||||
 | 
					          "success")
 | 
				
			||||||
 | 
					    return redirect(or_next(__get_default_page_uri()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("/dates/<date:journal_entry_date>", endpoint="order")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def show_journal_entry_order(journal_entry_date: date) -> str:
 | 
				
			||||||
 | 
					    """Shows the order of the journal entries in a same date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry_date: The date.
 | 
				
			||||||
 | 
					    :return: The order of the journal entries in the date.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    journal_entries: list[JournalEntry] = JournalEntry.query \
 | 
				
			||||||
 | 
					        .filter(JournalEntry.date == journal_entry_date) \
 | 
				
			||||||
 | 
					        .order_by(JournalEntry.no).all()
 | 
				
			||||||
 | 
					    return render_template("accounting/journal-entry/order.html",
 | 
				
			||||||
 | 
					                           date=journal_entry_date, list=journal_entries)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.post("/dates/<date:journal_entry_date>", endpoint="sort")
 | 
				
			||||||
 | 
					@has_permission(can_edit)
 | 
				
			||||||
 | 
					def sort_journal_entries(journal_entry_date: date) -> redirect:
 | 
				
			||||||
 | 
					    """Reorders the journal entries in a date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry_date: The date.
 | 
				
			||||||
 | 
					    :return: The redirection to the incoming account or the account list.  The
 | 
				
			||||||
 | 
					        reordering operation does not fail.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    form: JournalEntryReorderForm = JournalEntryReorderForm(journal_entry_date)
 | 
				
			||||||
 | 
					    form.save_order()
 | 
				
			||||||
 | 
					    if not form.is_modified:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The order was not modified.")), "success")
 | 
				
			||||||
 | 
					        return redirect(or_next(__get_default_page_uri()))
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    flash(s(lazy_gettext("The order is updated successfully.")), "success")
 | 
				
			||||||
 | 
					    return redirect(or_next(__get_default_page_uri()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_detail_uri(journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					    """Returns the detail URI of a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					    :return: The detail URI of the journal entry.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return url_for("accounting.journal-entry.detail",
 | 
				
			||||||
 | 
					                   journal_entry=journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_default_page_uri() -> str:
 | 
				
			||||||
 | 
					    """Returns the URI for the default page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The URI for the default page.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return url_for("accounting.report.default")
 | 
				
			||||||
@@ -449,12 +449,12 @@ class CurrencyL10n(db.Model):
 | 
				
			|||||||
    """The localized name."""
 | 
					    """The localized name."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VoucherCurrency:
 | 
					class JournalEntryCurrency:
 | 
				
			||||||
    """A currency in a voucher."""
 | 
					    """A currency in a journal entry."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, code: str, debit: list[JournalEntryLineItem],
 | 
					    def __init__(self, code: str, debit: list[JournalEntryLineItem],
 | 
				
			||||||
                 credit: list[JournalEntryLineItem]):
 | 
					                 credit: list[JournalEntryLineItem]):
 | 
				
			||||||
        """Constructs the currency in the voucher.
 | 
					        """Constructs the currency in the journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param code: The currency code.
 | 
					        :param code: The currency code.
 | 
				
			||||||
        :param debit: The debit line items.
 | 
					        :param debit: The debit line items.
 | 
				
			||||||
@@ -492,13 +492,13 @@ class VoucherCurrency:
 | 
				
			|||||||
        return sum([x.amount for x in self.credit])
 | 
					        return sum([x.amount for x in self.credit])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Voucher(db.Model):
 | 
					class JournalEntry(db.Model):
 | 
				
			||||||
    """A voucher."""
 | 
					    """A journal entry."""
 | 
				
			||||||
    __tablename__ = "accounting_vouchers"
 | 
					    __tablename__ = "accounting_journal_entries"
 | 
				
			||||||
    """The table name."""
 | 
					    """The table name."""
 | 
				
			||||||
    id = db.Column(db.Integer, nullable=False, primary_key=True,
 | 
					    id = db.Column(db.Integer, nullable=False, primary_key=True,
 | 
				
			||||||
                   autoincrement=False)
 | 
					                   autoincrement=False)
 | 
				
			||||||
    """The voucher ID."""
 | 
					    """The journal entry ID."""
 | 
				
			||||||
    date = db.Column(db.Date, nullable=False)
 | 
					    date = db.Column(db.Date, nullable=False)
 | 
				
			||||||
    """The date."""
 | 
					    """The date."""
 | 
				
			||||||
    no = db.Column(db.Integer, nullable=False, default=text("1"))
 | 
					    no = db.Column(db.Integer, nullable=False, default=text("1"))
 | 
				
			||||||
@@ -526,22 +526,23 @@ class Voucher(db.Model):
 | 
				
			|||||||
    updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
 | 
					    updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
 | 
				
			||||||
    """The updator."""
 | 
					    """The updator."""
 | 
				
			||||||
    line_items = db.relationship("JournalEntryLineItem",
 | 
					    line_items = db.relationship("JournalEntryLineItem",
 | 
				
			||||||
                                 back_populates="voucher")
 | 
					                                 back_populates="journal_entry")
 | 
				
			||||||
    """The line items."""
 | 
					    """The line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self) -> str:
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
        """Returns the string representation of this voucher.
 | 
					        """Returns the string representation of this journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: The string representation of this voucher.
 | 
					        :return: The string representation of this journal entry.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if self.is_cash_disbursement:
 | 
					        if self.is_cash_disbursement:
 | 
				
			||||||
            return gettext("Cash Disbursement Voucher#%(id)s", id=self.id)
 | 
					            return gettext("Cash Disbursement Journal Entry#%(id)s",
 | 
				
			||||||
 | 
					                           id=self.id)
 | 
				
			||||||
        if self.is_cash_receipt:
 | 
					        if self.is_cash_receipt:
 | 
				
			||||||
            return gettext("Cash Receipt Voucher#%(id)s", id=self.id)
 | 
					            return gettext("Cash Receipt Journal Entry#%(id)s", id=self.id)
 | 
				
			||||||
        return gettext("Transfer Voucher#%(id)s", id=self.id)
 | 
					        return gettext("Transfer Journal Entry#%(id)s", id=self.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def currencies(self) -> list[VoucherCurrency]:
 | 
					    def currencies(self) -> list[JournalEntryCurrency]:
 | 
				
			||||||
        """Returns the line items categorized by their currencies.
 | 
					        """Returns the line items categorized by their currencies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: The currency categories.
 | 
					        :return: The currency categories.
 | 
				
			||||||
@@ -555,18 +556,19 @@ class Voucher(db.Model):
 | 
				
			|||||||
                codes.append(line_item.currency_code)
 | 
					                codes.append(line_item.currency_code)
 | 
				
			||||||
                by_currency[line_item.currency_code] = []
 | 
					                by_currency[line_item.currency_code] = []
 | 
				
			||||||
            by_currency[line_item.currency_code].append(line_item)
 | 
					            by_currency[line_item.currency_code].append(line_item)
 | 
				
			||||||
        return [VoucherCurrency(code=x,
 | 
					        return [JournalEntryCurrency(code=x,
 | 
				
			||||||
                                debit=[y for y in by_currency[x]
 | 
					                                     debit=[y for y in by_currency[x]
 | 
				
			||||||
                                       if y.is_debit],
 | 
					                                            if y.is_debit],
 | 
				
			||||||
                                credit=[y for y in by_currency[x]
 | 
					                                     credit=[y for y in by_currency[x]
 | 
				
			||||||
                                        if not y.is_debit])
 | 
					                                             if not y.is_debit])
 | 
				
			||||||
                for x in codes]
 | 
					                for x in codes]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def is_cash_receipt(self) -> bool:
 | 
					    def is_cash_receipt(self) -> bool:
 | 
				
			||||||
        """Returns whether this is a cash receipt voucher.
 | 
					        """Returns whether this is a cash receipt journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: True if this is a cash receipt voucher, or False otherwise.
 | 
					        :return: True if this is a cash receipt journal entry, or False
 | 
				
			||||||
 | 
					            otherwise.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        for currency in self.currencies:
 | 
					        for currency in self.currencies:
 | 
				
			||||||
            if len(currency.debit) > 1:
 | 
					            if len(currency.debit) > 1:
 | 
				
			||||||
@@ -577,9 +579,9 @@ class Voucher(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def is_cash_disbursement(self) -> bool:
 | 
					    def is_cash_disbursement(self) -> bool:
 | 
				
			||||||
        """Returns whether this is a cash disbursement voucher.
 | 
					        """Returns whether this is a cash disbursement journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: True if this is a cash disbursement voucher, or False
 | 
					        :return: True if this is a cash disbursement journal entry, or False
 | 
				
			||||||
            otherwise.
 | 
					            otherwise.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        for currency in self.currencies:
 | 
					        for currency in self.currencies:
 | 
				
			||||||
@@ -591,9 +593,9 @@ class Voucher(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def can_delete(self) -> bool:
 | 
					    def can_delete(self) -> bool:
 | 
				
			||||||
        """Returns whether the voucher can be deleted.
 | 
					        """Returns whether the journal entry can be deleted.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: True if the voucher can be deleted, or False otherwise.
 | 
					        :return: True if the journal entry can be deleted, or False otherwise.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if not hasattr(self, "__can_delete"):
 | 
					        if not hasattr(self, "__can_delete"):
 | 
				
			||||||
            def has_offset() -> bool:
 | 
					            def has_offset() -> bool:
 | 
				
			||||||
@@ -605,12 +607,12 @@ class Voucher(db.Model):
 | 
				
			|||||||
        return getattr(self, "__can_delete")
 | 
					        return getattr(self, "__can_delete")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self) -> None:
 | 
					    def delete(self) -> None:
 | 
				
			||||||
        """Deletes the voucher.
 | 
					        """Deletes the journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        JournalEntryLineItem.query\
 | 
					        JournalEntryLineItem.query\
 | 
				
			||||||
            .filter(JournalEntryLineItem.voucher_id == self.id).delete()
 | 
					            .filter(JournalEntryLineItem.journal_entry_id == self.id).delete()
 | 
				
			||||||
        db.session.delete(self)
 | 
					        db.session.delete(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -621,17 +623,18 @@ class JournalEntryLineItem(db.Model):
 | 
				
			|||||||
    id = db.Column(db.Integer, nullable=False, primary_key=True,
 | 
					    id = db.Column(db.Integer, nullable=False, primary_key=True,
 | 
				
			||||||
                   autoincrement=False)
 | 
					                   autoincrement=False)
 | 
				
			||||||
    """The line item ID."""
 | 
					    """The line item ID."""
 | 
				
			||||||
    voucher_id = db.Column(db.Integer,
 | 
					    journal_entry_id = db.Column(db.Integer,
 | 
				
			||||||
                           db.ForeignKey(Voucher.id, onupdate="CASCADE",
 | 
					                                 db.ForeignKey(JournalEntry.id,
 | 
				
			||||||
                                         ondelete="CASCADE"),
 | 
					                                               onupdate="CASCADE",
 | 
				
			||||||
                           nullable=False)
 | 
					                                               ondelete="CASCADE"),
 | 
				
			||||||
    """The voucher ID."""
 | 
					                                 nullable=False)
 | 
				
			||||||
    voucher = db.relationship(Voucher, back_populates="line_items")
 | 
					    """The journal entry ID."""
 | 
				
			||||||
    """The voucher."""
 | 
					    journal_entry = db.relationship(JournalEntry, back_populates="line_items")
 | 
				
			||||||
 | 
					    """The journal entry."""
 | 
				
			||||||
    is_debit = db.Column(db.Boolean, nullable=False)
 | 
					    is_debit = db.Column(db.Boolean, nullable=False)
 | 
				
			||||||
    """True for a debit line item, or False for a credit line item."""
 | 
					    """True for a debit line item, or False for a credit line item."""
 | 
				
			||||||
    no = db.Column(db.Integer, nullable=False)
 | 
					    no = db.Column(db.Integer, nullable=False)
 | 
				
			||||||
    """The line item number under the voucher and debit or credit."""
 | 
					    """The line item number under the journal entry and debit or credit."""
 | 
				
			||||||
    original_line_item_id = db.Column(db.Integer,
 | 
					    original_line_item_id = db.Column(db.Integer,
 | 
				
			||||||
                                      db.ForeignKey(id, onupdate="CASCADE"),
 | 
					                                      db.ForeignKey(id, onupdate="CASCADE"),
 | 
				
			||||||
                                      nullable=True)
 | 
					                                      nullable=True)
 | 
				
			||||||
@@ -670,7 +673,7 @@ class JournalEntryLineItem(db.Model):
 | 
				
			|||||||
            from accounting.template_filters import format_date, format_amount
 | 
					            from accounting.template_filters import format_date, format_amount
 | 
				
			||||||
            setattr(self, "__str",
 | 
					            setattr(self, "__str",
 | 
				
			||||||
                    gettext("%(date)s %(description)s %(amount)s",
 | 
					                    gettext("%(date)s %(description)s %(amount)s",
 | 
				
			||||||
                            date=format_date(self.voucher.date),
 | 
					                            date=format_date(self.journal_entry.date),
 | 
				
			||||||
                            description="" if self.description is None
 | 
					                            description="" if self.description is None
 | 
				
			||||||
                            else self.description,
 | 
					                            else self.description,
 | 
				
			||||||
                            amount=format_amount(self.amount)))
 | 
					                            amount=format_amount(self.amount)))
 | 
				
			||||||
@@ -755,13 +758,16 @@ class JournalEntryLineItem(db.Model):
 | 
				
			|||||||
            frac: Decimal = (value - whole).normalize()
 | 
					            frac: Decimal = (value - whole).normalize()
 | 
				
			||||||
            return str(whole) + str(abs(frac))[1:]
 | 
					            return str(whole) + str(abs(frac))[1:]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        voucher_day: date = self.voucher.date
 | 
					        journal_entry_day: date = self.journal_entry.date
 | 
				
			||||||
        description: str = "" if self.description is None else self.description
 | 
					        description: str = "" if self.description is None else self.description
 | 
				
			||||||
        return ([description],
 | 
					        return ([description],
 | 
				
			||||||
                [str(voucher_day.year),
 | 
					                [str(journal_entry_day.year),
 | 
				
			||||||
                 "{}/{}".format(voucher_day.year, voucher_day.month),
 | 
					                 "{}/{}".format(journal_entry_day.year,
 | 
				
			||||||
                 "{}/{}".format(voucher_day.month, voucher_day.day),
 | 
					                                journal_entry_day.month),
 | 
				
			||||||
                 "{}/{}/{}".format(voucher_day.year, voucher_day.month,
 | 
					                 "{}/{}".format(journal_entry_day.month,
 | 
				
			||||||
                                   voucher_day.day),
 | 
					                                journal_entry_day.day),
 | 
				
			||||||
 | 
					                 "{}/{}/{}".format(journal_entry_day.year,
 | 
				
			||||||
 | 
					                                   journal_entry_day.month,
 | 
				
			||||||
 | 
					                                   journal_entry_day.day),
 | 
				
			||||||
                 format_amount(self.amount),
 | 
					                 format_amount(self.amount),
 | 
				
			||||||
                 format_amount(self.net_balance)])
 | 
					                 format_amount(self.net_balance)])
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,7 @@ This file is largely taken from the NanoParma ERP project, first written in
 | 
				
			|||||||
import typing as t
 | 
					import typing as t
 | 
				
			||||||
from datetime import date
 | 
					from datetime import date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.models import Voucher
 | 
					from accounting.models import JournalEntry
 | 
				
			||||||
from .period import Period
 | 
					from .period import Period
 | 
				
			||||||
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
 | 
					from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
 | 
				
			||||||
    LastYear, Today, Yesterday, AllTime, TemplatePeriod, YearPeriod
 | 
					    LastYear, Today, Yesterday, AllTime, TemplatePeriod, YearPeriod
 | 
				
			||||||
@@ -61,8 +61,8 @@ class PeriodChooser:
 | 
				
			|||||||
        self.url_template: str = get_url(TemplatePeriod())
 | 
					        self.url_template: str = get_url(TemplatePeriod())
 | 
				
			||||||
        """The URL template."""
 | 
					        """The URL template."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        first: Voucher | None \
 | 
					        first: JournalEntry | None \
 | 
				
			||||||
            = Voucher.query.order_by(Voucher.date).first()
 | 
					            = JournalEntry.query.order_by(JournalEntry.date).first()
 | 
				
			||||||
        start: date | None = None if first is None else first.date
 | 
					        start: date | None = None if first is None else first.date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Attributes
 | 
					        # Attributes
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,7 @@ from flask import render_template, Response
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.locale import gettext
 | 
					from accounting.locale import gettext
 | 
				
			||||||
from accounting.models import Currency, BaseAccount, Account, Voucher, \
 | 
					from accounting.models import Currency, BaseAccount, Account, JournalEntry, \
 | 
				
			||||||
    JournalEntryLineItem
 | 
					    JournalEntryLineItem
 | 
				
			||||||
from accounting.report.period import Period, PeriodChooser
 | 
					from accounting.report.period import Period, PeriodChooser
 | 
				
			||||||
from accounting.report.utils.base_page_params import BasePageParams
 | 
					from accounting.report.utils.base_page_params import BasePageParams
 | 
				
			||||||
@@ -127,14 +127,14 @@ class AccountCollector:
 | 
				
			|||||||
            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
				
			||||||
               sa.or_(*sub_conditions)]
 | 
					               sa.or_(*sub_conditions)]
 | 
				
			||||||
        if self.__period.end is not None:
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
            conditions.append(Voucher.date <= self.__period.end)
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
					        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
				
			||||||
            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
					            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
				
			||||||
            else_=-JournalEntryLineItem.amount)).label("balance")
 | 
					            else_=-JournalEntryLineItem.amount)).label("balance")
 | 
				
			||||||
        select_balance: sa.Select \
 | 
					        select_balance: sa.Select \
 | 
				
			||||||
            = sa.select(Account.id, Account.base_code, Account.no,
 | 
					            = sa.select(Account.id, Account.base_code, Account.no,
 | 
				
			||||||
                        balance_func)\
 | 
					                        balance_func)\
 | 
				
			||||||
            .join(Voucher).join(Account)\
 | 
					            .join(JournalEntry).join(Account)\
 | 
				
			||||||
            .filter(*conditions)\
 | 
					            .filter(*conditions)\
 | 
				
			||||||
            .group_by(Account.id, Account.base_code, Account.no)\
 | 
					            .group_by(Account.id, Account.base_code, Account.no)\
 | 
				
			||||||
            .order_by(Account.base_code, Account.no)
 | 
					            .order_by(Account.base_code, Account.no)
 | 
				
			||||||
@@ -179,7 +179,7 @@ class AccountCollector:
 | 
				
			|||||||
            return None
 | 
					            return None
 | 
				
			||||||
        conditions: list[sa.BinaryExpression] \
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
				
			||||||
               Voucher.date < self.__period.start]
 | 
					               JournalEntry.date < self.__period.start]
 | 
				
			||||||
        return self.__query_balance(conditions)
 | 
					        return self.__query_balance(conditions)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __add_current_period(self) -> None:
 | 
					    def __add_current_period(self) -> None:
 | 
				
			||||||
@@ -199,9 +199,9 @@ class AccountCollector:
 | 
				
			|||||||
        conditions: list[sa.BinaryExpression] \
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
            = [JournalEntryLineItem.currency_code == self.__currency.code]
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code]
 | 
				
			||||||
        if self.__period.start is not None:
 | 
					        if self.__period.start is not None:
 | 
				
			||||||
            conditions.append(Voucher.date >= self.__period.start)
 | 
					            conditions.append(JournalEntry.date >= self.__period.start)
 | 
				
			||||||
        if self.__period.end is not None:
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
            conditions.append(Voucher.date <= self.__period.end)
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
        return self.__query_balance(conditions)
 | 
					        return self.__query_balance(conditions)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
@@ -218,7 +218,7 @@ class AccountCollector:
 | 
				
			|||||||
            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
					            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
				
			||||||
            else_=-JournalEntryLineItem.amount))
 | 
					            else_=-JournalEntryLineItem.amount))
 | 
				
			||||||
        select_balance: sa.Select = sa.select(balance_func)\
 | 
					        select_balance: sa.Select = sa.select(balance_func)\
 | 
				
			||||||
            .join(Voucher).join(Account).filter(*conditions)
 | 
					            .join(JournalEntry).join(Account).filter(*conditions)
 | 
				
			||||||
        return db.session.scalar(select_balance)
 | 
					        return db.session.scalar(select_balance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __add_owner_s_equity(self, code: str, amount: Decimal | None,
 | 
					    def __add_owner_s_equity(self, code: str, amount: Decimal | None,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,7 +26,8 @@ from sqlalchemy.orm import selectinload
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.locale import gettext
 | 
					from accounting.locale import gettext
 | 
				
			||||||
from accounting.models import Currency, Account, Voucher, JournalEntryLineItem
 | 
					from accounting.models import Currency, Account, JournalEntry, \
 | 
				
			||||||
 | 
					    JournalEntryLineItem
 | 
				
			||||||
from accounting.report.period import Period, PeriodChooser
 | 
					from accounting.report.period import Period, PeriodChooser
 | 
				
			||||||
from accounting.report.utils.base_page_params import BasePageParams
 | 
					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
 | 
				
			||||||
@@ -70,14 +71,14 @@ class ReportLineItem:
 | 
				
			|||||||
        self.url: str | None = None
 | 
					        self.url: str | None = None
 | 
				
			||||||
        """The URL to the journal entry line item."""
 | 
					        """The URL to the journal entry line item."""
 | 
				
			||||||
        if line_item is not None:
 | 
					        if line_item is not None:
 | 
				
			||||||
            self.date = line_item.voucher.date
 | 
					            self.date = line_item.journal_entry.date
 | 
				
			||||||
            self.account = line_item.account
 | 
					            self.account = line_item.account
 | 
				
			||||||
            self.description = line_item.description
 | 
					            self.description = line_item.description
 | 
				
			||||||
            self.income = None if line_item.is_debit else line_item.amount
 | 
					            self.income = None if line_item.is_debit else line_item.amount
 | 
				
			||||||
            self.expense = line_item.amount if line_item.is_debit else None
 | 
					            self.expense = line_item.amount if line_item.is_debit else None
 | 
				
			||||||
            self.note = line_item.voucher.note
 | 
					            self.note = line_item.journal_entry.note
 | 
				
			||||||
            self.url = url_for("accounting.voucher.detail",
 | 
					            self.url = url_for("accounting.journal-entry.detail",
 | 
				
			||||||
                               voucher=line_item.voucher)
 | 
					                               journal_entry=line_item.journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LineItemCollector:
 | 
					class LineItemCollector:
 | 
				
			||||||
@@ -120,11 +121,11 @@ class LineItemCollector:
 | 
				
			|||||||
            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
					            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
				
			||||||
            else_=-JournalEntryLineItem.amount))
 | 
					            else_=-JournalEntryLineItem.amount))
 | 
				
			||||||
        select: sa.Select = sa.Select(balance_func)\
 | 
					        select: sa.Select = sa.Select(balance_func)\
 | 
				
			||||||
            .join(Voucher).join(Account)\
 | 
					            .join(JournalEntry).join(Account)\
 | 
				
			||||||
            .filter(be(JournalEntryLineItem.currency_code
 | 
					            .filter(be(JournalEntryLineItem.currency_code
 | 
				
			||||||
                       == self.__currency.code),
 | 
					                       == self.__currency.code),
 | 
				
			||||||
                    self.__account_condition,
 | 
					                    self.__account_condition,
 | 
				
			||||||
                    Voucher.date < self.__period.start)
 | 
					                    JournalEntry.date < self.__period.start)
 | 
				
			||||||
        balance: int | None = db.session.scalar(select)
 | 
					        balance: int | None = db.session.scalar(select)
 | 
				
			||||||
        if balance is None:
 | 
					        if balance is None:
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
@@ -149,25 +150,26 @@ class LineItemCollector:
 | 
				
			|||||||
            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
				
			||||||
               self.__account_condition]
 | 
					               self.__account_condition]
 | 
				
			||||||
        if self.__period.start is not None:
 | 
					        if self.__period.start is not None:
 | 
				
			||||||
            conditions.append(Voucher.date >= self.__period.start)
 | 
					            conditions.append(JournalEntry.date >= self.__period.start)
 | 
				
			||||||
        if self.__period.end is not None:
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
            conditions.append(Voucher.date <= self.__period.end)
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
        voucher_with_account: sa.Select = sa.Select(Voucher.id).\
 | 
					        journal_entry_with_account: sa.Select = sa.Select(JournalEntry.id).\
 | 
				
			||||||
            join(JournalEntryLineItem).join(Account).filter(*conditions)
 | 
					            join(JournalEntryLineItem).join(Account).filter(*conditions)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return [ReportLineItem(x)
 | 
					        return [ReportLineItem(x)
 | 
				
			||||||
                for x in JournalEntryLineItem.query.join(Voucher).join(Account)
 | 
					                for x in JournalEntryLineItem.query
 | 
				
			||||||
                .filter(JournalEntryLineItem.voucher_id
 | 
					                .join(JournalEntry).join(Account)
 | 
				
			||||||
                        .in_(voucher_with_account),
 | 
					                .filter(JournalEntryLineItem.journal_entry_id
 | 
				
			||||||
 | 
					                        .in_(journal_entry_with_account),
 | 
				
			||||||
                        JournalEntryLineItem.currency_code
 | 
					                        JournalEntryLineItem.currency_code
 | 
				
			||||||
                        == self.__currency.code,
 | 
					                        == self.__currency.code,
 | 
				
			||||||
                        sa.not_(self.__account_condition))
 | 
					                        sa.not_(self.__account_condition))
 | 
				
			||||||
                .order_by(Voucher.date,
 | 
					                .order_by(JournalEntry.date,
 | 
				
			||||||
                          Voucher.no,
 | 
					                          JournalEntry.no,
 | 
				
			||||||
                          JournalEntryLineItem.is_debit,
 | 
					                          JournalEntryLineItem.is_debit,
 | 
				
			||||||
                          JournalEntryLineItem.no)
 | 
					                          JournalEntryLineItem.no)
 | 
				
			||||||
                .options(selectinload(JournalEntryLineItem.account),
 | 
					                .options(selectinload(JournalEntryLineItem.account),
 | 
				
			||||||
                         selectinload(JournalEntryLineItem.voucher))]
 | 
					                         selectinload(JournalEntryLineItem.journal_entry))]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def __account_condition(self) -> sa.BinaryExpression:
 | 
					    def __account_condition(self) -> sa.BinaryExpression:
 | 
				
			||||||
@@ -216,7 +218,7 @@ class LineItemCollector:
 | 
				
			|||||||
class CSVRow(BaseCSVRow):
 | 
					class CSVRow(BaseCSVRow):
 | 
				
			||||||
    """A row in the CSV."""
 | 
					    """A row in the CSV."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, voucher_date: date | str | None,
 | 
					    def __init__(self, journal_entry_date: date | str | None,
 | 
				
			||||||
                 account: str | None,
 | 
					                 account: str | None,
 | 
				
			||||||
                 description: str | None,
 | 
					                 description: str | None,
 | 
				
			||||||
                 income: str | Decimal | None,
 | 
					                 income: str | Decimal | None,
 | 
				
			||||||
@@ -225,7 +227,7 @@ class CSVRow(BaseCSVRow):
 | 
				
			|||||||
                 note: str | None):
 | 
					                 note: str | None):
 | 
				
			||||||
        """Constructs a row in the CSV.
 | 
					        """Constructs a row in the CSV.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param voucher_date: The voucher date.
 | 
					        :param journal_entry_date: The journal entry date.
 | 
				
			||||||
        :param account: The account.
 | 
					        :param account: The account.
 | 
				
			||||||
        :param description: The description.
 | 
					        :param description: The description.
 | 
				
			||||||
        :param income: The income.
 | 
					        :param income: The income.
 | 
				
			||||||
@@ -233,7 +235,7 @@ class CSVRow(BaseCSVRow):
 | 
				
			|||||||
        :param balance: The balance.
 | 
					        :param balance: The balance.
 | 
				
			||||||
        :param note: The note.
 | 
					        :param note: The note.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.date: date | str | None = voucher_date
 | 
					        self.date: date | str | None = journal_entry_date
 | 
				
			||||||
        """The date."""
 | 
					        """The date."""
 | 
				
			||||||
        self.account: str | None = account
 | 
					        self.account: str | None = account
 | 
				
			||||||
        """The account."""
 | 
					        """The account."""
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,7 @@ from flask import render_template, Response
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.locale import gettext
 | 
					from accounting.locale import gettext
 | 
				
			||||||
from accounting.models import Currency, BaseAccount, Account, Voucher, \
 | 
					from accounting.models import Currency, BaseAccount, Account, JournalEntry, \
 | 
				
			||||||
    JournalEntryLineItem
 | 
					    JournalEntryLineItem
 | 
				
			||||||
from accounting.report.period import Period, PeriodChooser
 | 
					from accounting.report.period import Period, PeriodChooser
 | 
				
			||||||
from accounting.report.utils.base_page_params import BasePageParams
 | 
					from accounting.report.utils.base_page_params import BasePageParams
 | 
				
			||||||
@@ -259,14 +259,14 @@ class IncomeStatement(BaseReport):
 | 
				
			|||||||
            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
				
			||||||
               sa.or_(*sub_conditions)]
 | 
					               sa.or_(*sub_conditions)]
 | 
				
			||||||
        if self.__period.start is not None:
 | 
					        if self.__period.start is not None:
 | 
				
			||||||
            conditions.append(Voucher.date >= self.__period.start)
 | 
					            conditions.append(JournalEntry.date >= self.__period.start)
 | 
				
			||||||
        if self.__period.end is not None:
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
            conditions.append(Voucher.date <= self.__period.end)
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
					        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
				
			||||||
            (JournalEntryLineItem.is_debit, -JournalEntryLineItem.amount),
 | 
					            (JournalEntryLineItem.is_debit, -JournalEntryLineItem.amount),
 | 
				
			||||||
            else_=JournalEntryLineItem.amount)).label("balance")
 | 
					            else_=JournalEntryLineItem.amount)).label("balance")
 | 
				
			||||||
        select_balances: sa.Select = sa.select(Account.id, balance_func)\
 | 
					        select_balances: sa.Select = sa.select(Account.id, balance_func)\
 | 
				
			||||||
            .join(Voucher).join(Account)\
 | 
					            .join(JournalEntry).join(Account)\
 | 
				
			||||||
            .filter(*conditions)\
 | 
					            .filter(*conditions)\
 | 
				
			||||||
            .group_by(Account.id)\
 | 
					            .group_by(Account.id)\
 | 
				
			||||||
            .order_by(Account.base_code, Account.no)
 | 
					            .order_by(Account.base_code, Account.no)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,8 @@ from flask import render_template, Response
 | 
				
			|||||||
from sqlalchemy.orm import selectinload
 | 
					from sqlalchemy.orm import selectinload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.locale import gettext
 | 
					from accounting.locale import gettext
 | 
				
			||||||
from accounting.models import Currency, Account, Voucher, JournalEntryLineItem
 | 
					from accounting.models import Currency, Account, JournalEntry, \
 | 
				
			||||||
 | 
					    JournalEntryLineItem
 | 
				
			||||||
from accounting.report.period import Period, PeriodChooser
 | 
					from accounting.report.period import Period, PeriodChooser
 | 
				
			||||||
from accounting.report.utils.base_page_params import BasePageParams
 | 
					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
 | 
				
			||||||
@@ -47,8 +48,8 @@ class ReportLineItem:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        self.line_item: JournalEntryLineItem = line_item
 | 
					        self.line_item: JournalEntryLineItem = line_item
 | 
				
			||||||
        """The journal entry line item."""
 | 
					        """The journal entry line item."""
 | 
				
			||||||
        self.voucher: Voucher = line_item.voucher
 | 
					        self.journal_entry: JournalEntry = line_item.journal_entry
 | 
				
			||||||
        """The voucher."""
 | 
					        """The journal entry."""
 | 
				
			||||||
        self.currency: Currency = line_item.currency
 | 
					        self.currency: Currency = line_item.currency
 | 
				
			||||||
        """The account."""
 | 
					        """The account."""
 | 
				
			||||||
        self.account: Account = line_item.account
 | 
					        self.account: Account = line_item.account
 | 
				
			||||||
@@ -66,7 +67,7 @@ class ReportLineItem:
 | 
				
			|||||||
class CSVRow(BaseCSVRow):
 | 
					class CSVRow(BaseCSVRow):
 | 
				
			||||||
    """A row in the CSV."""
 | 
					    """A row in the CSV."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, voucher_date: str | date,
 | 
					    def __init__(self, journal_entry_date: str | date,
 | 
				
			||||||
                 currency: str,
 | 
					                 currency: str,
 | 
				
			||||||
                 account: str,
 | 
					                 account: str,
 | 
				
			||||||
                 description: str | None,
 | 
					                 description: str | None,
 | 
				
			||||||
@@ -75,13 +76,13 @@ class CSVRow(BaseCSVRow):
 | 
				
			|||||||
                 note: str | None):
 | 
					                 note: str | None):
 | 
				
			||||||
        """Constructs a row in the CSV.
 | 
					        """Constructs a row in the CSV.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param voucher_date: The voucher date.
 | 
					        :param journal_entry_date: The journal entry date.
 | 
				
			||||||
        :param description: The description.
 | 
					        :param description: The description.
 | 
				
			||||||
        :param debit: The debit amount.
 | 
					        :param debit: The debit amount.
 | 
				
			||||||
        :param credit: The credit amount.
 | 
					        :param credit: The credit amount.
 | 
				
			||||||
        :param note: The note.
 | 
					        :param note: The note.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.date: str | date = voucher_date
 | 
					        self.date: str | date = journal_entry_date
 | 
				
			||||||
        """The date."""
 | 
					        """The date."""
 | 
				
			||||||
        self.currency: str = currency
 | 
					        self.currency: str = currency
 | 
				
			||||||
        """The currency."""
 | 
					        """The currency."""
 | 
				
			||||||
@@ -155,9 +156,9 @@ def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:
 | 
				
			|||||||
                                 gettext("Account"), gettext("Description"),
 | 
					                                 gettext("Account"), gettext("Description"),
 | 
				
			||||||
                                 gettext("Debit"), gettext("Credit"),
 | 
					                                 gettext("Debit"), gettext("Credit"),
 | 
				
			||||||
                                 gettext("Note"))]
 | 
					                                 gettext("Note"))]
 | 
				
			||||||
    rows.extend([CSVRow(x.voucher.date, x.currency.code,
 | 
					    rows.extend([CSVRow(x.journal_entry.date, x.currency.code,
 | 
				
			||||||
                        str(x.account).title(), x.description,
 | 
					                        str(x.account).title(), x.description,
 | 
				
			||||||
                        x.debit, x.credit, x.voucher.note)
 | 
					                        x.debit, x.credit, x.journal_entry.note)
 | 
				
			||||||
                 for x in line_items])
 | 
					                 for x in line_items])
 | 
				
			||||||
    return rows
 | 
					    return rows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -183,18 +184,18 @@ class Journal(BaseReport):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        conditions: list[sa.BinaryExpression] = []
 | 
					        conditions: list[sa.BinaryExpression] = []
 | 
				
			||||||
        if self.__period.start is not None:
 | 
					        if self.__period.start is not None:
 | 
				
			||||||
            conditions.append(Voucher.date >= self.__period.start)
 | 
					            conditions.append(JournalEntry.date >= self.__period.start)
 | 
				
			||||||
        if self.__period.end is not None:
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
            conditions.append(Voucher.date <= self.__period.end)
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
        return JournalEntryLineItem.query.join(Voucher)\
 | 
					        return JournalEntryLineItem.query.join(JournalEntry)\
 | 
				
			||||||
            .filter(*conditions)\
 | 
					            .filter(*conditions)\
 | 
				
			||||||
            .order_by(Voucher.date,
 | 
					            .order_by(JournalEntry.date,
 | 
				
			||||||
                      Voucher.no,
 | 
					                      JournalEntry.no,
 | 
				
			||||||
                      JournalEntryLineItem.is_debit.desc(),
 | 
					                      JournalEntryLineItem.is_debit.desc(),
 | 
				
			||||||
                      JournalEntryLineItem.no)\
 | 
					                      JournalEntryLineItem.no)\
 | 
				
			||||||
            .options(selectinload(JournalEntryLineItem.account),
 | 
					            .options(selectinload(JournalEntryLineItem.account),
 | 
				
			||||||
                     selectinload(JournalEntryLineItem.currency),
 | 
					                     selectinload(JournalEntryLineItem.currency),
 | 
				
			||||||
                     selectinload(JournalEntryLineItem.voucher)).all()
 | 
					                     selectinload(JournalEntryLineItem.journal_entry)).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def csv(self) -> Response:
 | 
					    def csv(self) -> Response:
 | 
				
			||||||
        """Returns the report as CSV for download.
 | 
					        """Returns the report as CSV for download.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,7 +26,8 @@ from sqlalchemy.orm import selectinload
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.locale import gettext
 | 
					from accounting.locale import gettext
 | 
				
			||||||
from accounting.models import Currency, Account, Voucher, JournalEntryLineItem
 | 
					from accounting.models import Currency, Account, JournalEntry, \
 | 
				
			||||||
 | 
					    JournalEntryLineItem
 | 
				
			||||||
from accounting.report.period import Period, PeriodChooser
 | 
					from accounting.report.period import Period, PeriodChooser
 | 
				
			||||||
from accounting.report.utils.base_page_params import BasePageParams
 | 
					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
 | 
				
			||||||
@@ -67,13 +68,13 @@ class ReportLineItem:
 | 
				
			|||||||
        self.url: str | None = None
 | 
					        self.url: str | None = None
 | 
				
			||||||
        """The URL to the journal entry line item."""
 | 
					        """The URL to the journal entry line item."""
 | 
				
			||||||
        if line_item is not None:
 | 
					        if line_item is not None:
 | 
				
			||||||
            self.date = line_item.voucher.date
 | 
					            self.date = line_item.journal_entry.date
 | 
				
			||||||
            self.description = line_item.description
 | 
					            self.description = line_item.description
 | 
				
			||||||
            self.debit = line_item.amount if line_item.is_debit else None
 | 
					            self.debit = line_item.amount if line_item.is_debit else None
 | 
				
			||||||
            self.credit = None if line_item.is_debit else line_item.amount
 | 
					            self.credit = None if line_item.is_debit else line_item.amount
 | 
				
			||||||
            self.note = line_item.voucher.note
 | 
					            self.note = line_item.journal_entry.note
 | 
				
			||||||
            self.url = url_for("accounting.voucher.detail",
 | 
					            self.url = url_for("accounting.journal-entry.detail",
 | 
				
			||||||
                               voucher=line_item.voucher)
 | 
					                               journal_entry=line_item.journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LineItemCollector:
 | 
					class LineItemCollector:
 | 
				
			||||||
@@ -116,12 +117,12 @@ class LineItemCollector:
 | 
				
			|||||||
        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
					        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
				
			||||||
            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
					            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
				
			||||||
            else_=-JournalEntryLineItem.amount))
 | 
					            else_=-JournalEntryLineItem.amount))
 | 
				
			||||||
        select: sa.Select = sa.Select(balance_func).join(Voucher)\
 | 
					        select: sa.Select = sa.Select(balance_func).join(JournalEntry)\
 | 
				
			||||||
            .filter(be(JournalEntryLineItem.currency_code
 | 
					            .filter(be(JournalEntryLineItem.currency_code
 | 
				
			||||||
                       == self.__currency.code),
 | 
					                       == self.__currency.code),
 | 
				
			||||||
                    be(JournalEntryLineItem.account_id
 | 
					                    be(JournalEntryLineItem.account_id
 | 
				
			||||||
                       == self.__account.id),
 | 
					                       == self.__account.id),
 | 
				
			||||||
                    Voucher.date < self.__period.start)
 | 
					                    JournalEntry.date < self.__period.start)
 | 
				
			||||||
        balance: int | None = db.session.scalar(select)
 | 
					        balance: int | None = db.session.scalar(select)
 | 
				
			||||||
        if balance is None:
 | 
					        if balance is None:
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
@@ -145,17 +146,18 @@ class LineItemCollector:
 | 
				
			|||||||
            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
				
			||||||
               JournalEntryLineItem.account_id == self.__account.id]
 | 
					               JournalEntryLineItem.account_id == self.__account.id]
 | 
				
			||||||
        if self.__period.start is not None:
 | 
					        if self.__period.start is not None:
 | 
				
			||||||
            conditions.append(Voucher.date >= self.__period.start)
 | 
					            conditions.append(JournalEntry.date >= self.__period.start)
 | 
				
			||||||
        if self.__period.end is not None:
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
            conditions.append(Voucher.date <= self.__period.end)
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
        return [ReportLineItem(x) for x in JournalEntryLineItem.query
 | 
					        return [ReportLineItem(x) for x in JournalEntryLineItem.query
 | 
				
			||||||
                .join(Voucher)
 | 
					                .join(JournalEntry)
 | 
				
			||||||
                .filter(*conditions)
 | 
					                .filter(*conditions)
 | 
				
			||||||
                .order_by(Voucher.date,
 | 
					                .order_by(JournalEntry.date,
 | 
				
			||||||
                          Voucher.no,
 | 
					                          JournalEntry.no,
 | 
				
			||||||
                          JournalEntryLineItem.is_debit.desc(),
 | 
					                          JournalEntryLineItem.is_debit.desc(),
 | 
				
			||||||
                          JournalEntryLineItem.no)
 | 
					                          JournalEntryLineItem.no)
 | 
				
			||||||
                .options(selectinload(JournalEntryLineItem.voucher)).all()]
 | 
					                .options(selectinload(JournalEntryLineItem.journal_entry))
 | 
				
			||||||
 | 
					                .all()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __get_total(self) -> ReportLineItem | None:
 | 
					    def __get_total(self) -> ReportLineItem | None:
 | 
				
			||||||
        """Composes the total line item.
 | 
					        """Composes the total line item.
 | 
				
			||||||
@@ -197,7 +199,7 @@ class LineItemCollector:
 | 
				
			|||||||
class CSVRow(BaseCSVRow):
 | 
					class CSVRow(BaseCSVRow):
 | 
				
			||||||
    """A row in the CSV."""
 | 
					    """A row in the CSV."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, voucher_date: date | str | None,
 | 
					    def __init__(self, journal_entry_date: date | str | None,
 | 
				
			||||||
                 description: str | None,
 | 
					                 description: str | None,
 | 
				
			||||||
                 debit: str | Decimal | None,
 | 
					                 debit: str | Decimal | None,
 | 
				
			||||||
                 credit: str | Decimal | None,
 | 
					                 credit: str | Decimal | None,
 | 
				
			||||||
@@ -205,14 +207,14 @@ class CSVRow(BaseCSVRow):
 | 
				
			|||||||
                 note: str | None):
 | 
					                 note: str | None):
 | 
				
			||||||
        """Constructs a row in the CSV.
 | 
					        """Constructs a row in the CSV.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param voucher_date: The voucher date.
 | 
					        :param journal_entry_date: The journal entry date.
 | 
				
			||||||
        :param description: The description.
 | 
					        :param description: The description.
 | 
				
			||||||
        :param debit: The debit amount.
 | 
					        :param debit: The debit amount.
 | 
				
			||||||
        :param credit: The credit amount.
 | 
					        :param credit: The credit amount.
 | 
				
			||||||
        :param balance: The balance.
 | 
					        :param balance: The balance.
 | 
				
			||||||
        :param note: The note.
 | 
					        :param note: The note.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.date: date | str | None = voucher_date
 | 
					        self.date: date | str | None = journal_entry_date
 | 
				
			||||||
        """The date."""
 | 
					        """The date."""
 | 
				
			||||||
        self.description: str | None = description
 | 
					        self.description: str | None = description
 | 
				
			||||||
        """The description."""
 | 
					        """The description."""
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from accounting.locale import gettext
 | 
					from accounting.locale import gettext
 | 
				
			||||||
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
 | 
					from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
 | 
				
			||||||
    Voucher, JournalEntryLineItem
 | 
					    JournalEntry, JournalEntryLineItem
 | 
				
			||||||
from accounting.report.utils.base_page_params import BasePageParams
 | 
					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 csv_download
 | 
					from accounting.report.utils.csv_export import csv_download
 | 
				
			||||||
@@ -62,22 +62,23 @@ class LineItemCollector:
 | 
				
			|||||||
                       self.__get_account_condition(k)),
 | 
					                       self.__get_account_condition(k)),
 | 
				
			||||||
                   JournalEntryLineItem.currency_code.in_(
 | 
					                   JournalEntryLineItem.currency_code.in_(
 | 
				
			||||||
                       self.__get_currency_condition(k)),
 | 
					                       self.__get_currency_condition(k)),
 | 
				
			||||||
                   JournalEntryLineItem.voucher_id.in_(
 | 
					                   JournalEntryLineItem.journal_entry_id.in_(
 | 
				
			||||||
                       self.__get_voucher_condition(k))]
 | 
					                       self.__get_journal_entry_condition(k))]
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                sub_conditions.append(
 | 
					                sub_conditions.append(
 | 
				
			||||||
                    JournalEntryLineItem.amount == Decimal(k))
 | 
					                    JournalEntryLineItem.amount == Decimal(k))
 | 
				
			||||||
            except ArithmeticError:
 | 
					            except ArithmeticError:
 | 
				
			||||||
                pass
 | 
					                pass
 | 
				
			||||||
            conditions.append(sa.or_(*sub_conditions))
 | 
					            conditions.append(sa.or_(*sub_conditions))
 | 
				
			||||||
        return JournalEntryLineItem.query.join(Voucher).filter(*conditions)\
 | 
					        return JournalEntryLineItem.query.join(JournalEntry)\
 | 
				
			||||||
            .order_by(Voucher.date,
 | 
					            .filter(*conditions)\
 | 
				
			||||||
                      Voucher.no,
 | 
					            .order_by(JournalEntry.date,
 | 
				
			||||||
 | 
					                      JournalEntry.no,
 | 
				
			||||||
                      JournalEntryLineItem.is_debit,
 | 
					                      JournalEntryLineItem.is_debit,
 | 
				
			||||||
                      JournalEntryLineItem.no)\
 | 
					                      JournalEntryLineItem.no)\
 | 
				
			||||||
            .options(selectinload(JournalEntryLineItem.account),
 | 
					            .options(selectinload(JournalEntryLineItem.account),
 | 
				
			||||||
                     selectinload(JournalEntryLineItem.currency),
 | 
					                     selectinload(JournalEntryLineItem.currency),
 | 
				
			||||||
                     selectinload(JournalEntryLineItem.voucher)).all()
 | 
					                     selectinload(JournalEntryLineItem.journal_entry)).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def __get_account_condition(k: str) -> sa.Select:
 | 
					    def __get_account_condition(k: str) -> sa.Select:
 | 
				
			||||||
@@ -116,35 +117,40 @@ class LineItemCollector:
 | 
				
			|||||||
                   Currency.code.in_(select_l10n)))
 | 
					                   Currency.code.in_(select_l10n)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def __get_voucher_condition(k: str) -> sa.Select:
 | 
					    def __get_journal_entry_condition(k: str) -> sa.Select:
 | 
				
			||||||
        """Composes and returns the condition to filter the voucher.
 | 
					        """Composes and returns the condition to filter the journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param k: The keyword.
 | 
					        :param k: The keyword.
 | 
				
			||||||
        :return: The condition to filter the voucher.
 | 
					        :return: The condition to filter the journal entry.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        conditions: list[sa.BinaryExpression] = [Voucher.note.contains(k)]
 | 
					        conditions: list[sa.BinaryExpression] = [JournalEntry.note.contains(k)]
 | 
				
			||||||
        voucher_date: datetime
 | 
					        journal_entry_date: datetime
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            voucher_date = datetime.strptime(k, "%Y")
 | 
					            journal_entry_date = datetime.strptime(k, "%Y")
 | 
				
			||||||
            conditions.append(
 | 
					            conditions.append(
 | 
				
			||||||
                be(sa.extract("year", Voucher.date) == voucher_date.year))
 | 
					                be(sa.extract("year", JournalEntry.date)
 | 
				
			||||||
 | 
					                   == journal_entry_date.year))
 | 
				
			||||||
        except ValueError:
 | 
					        except ValueError:
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            voucher_date = datetime.strptime(k, "%Y/%m")
 | 
					            journal_entry_date = datetime.strptime(k, "%Y/%m")
 | 
				
			||||||
            conditions.append(sa.and_(
 | 
					            conditions.append(sa.and_(
 | 
				
			||||||
                sa.extract("year", Voucher.date) == voucher_date.year,
 | 
					                sa.extract("year", JournalEntry.date)
 | 
				
			||||||
                sa.extract("month", Voucher.date) == voucher_date.month))
 | 
					                == journal_entry_date.year,
 | 
				
			||||||
 | 
					                sa.extract("month", JournalEntry.date)
 | 
				
			||||||
 | 
					                == journal_entry_date.month))
 | 
				
			||||||
        except ValueError:
 | 
					        except ValueError:
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            voucher_date = datetime.strptime(f"2000/{k}", "%Y/%m/%d")
 | 
					            journal_entry_date = datetime.strptime(f"2000/{k}", "%Y/%m/%d")
 | 
				
			||||||
            conditions.append(sa.and_(
 | 
					            conditions.append(sa.and_(
 | 
				
			||||||
                sa.extract("month", Voucher.date) == voucher_date.month,
 | 
					                sa.extract("month", JournalEntry.date)
 | 
				
			||||||
                sa.extract("day", Voucher.date) == voucher_date.day))
 | 
					                == journal_entry_date.month,
 | 
				
			||||||
 | 
					                sa.extract("day", JournalEntry.date)
 | 
				
			||||||
 | 
					                == journal_entry_date.day))
 | 
				
			||||||
        except ValueError:
 | 
					        except ValueError:
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
        return sa.select(Voucher.id).filter(sa.or_(*conditions))
 | 
					        return sa.select(JournalEntry.id).filter(sa.or_(*conditions))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PageParams(BasePageParams):
 | 
					class PageParams(BasePageParams):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,8 @@ from flask import Response, render_template
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.locale import gettext
 | 
					from accounting.locale import gettext
 | 
				
			||||||
from accounting.models import Currency, Account, Voucher, JournalEntryLineItem
 | 
					from accounting.models import Currency, Account, JournalEntry, \
 | 
				
			||||||
 | 
					    JournalEntryLineItem
 | 
				
			||||||
from accounting.report.period import Period, PeriodChooser
 | 
					from accounting.report.period import Period, PeriodChooser
 | 
				
			||||||
from accounting.report.utils.base_page_params import BasePageParams
 | 
					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
 | 
				
			||||||
@@ -180,14 +181,14 @@ class TrialBalance(BaseReport):
 | 
				
			|||||||
        conditions: list[sa.BinaryExpression] \
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
            = [JournalEntryLineItem.currency_code == self.__currency.code]
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code]
 | 
				
			||||||
        if self.__period.start is not None:
 | 
					        if self.__period.start is not None:
 | 
				
			||||||
            conditions.append(Voucher.date >= self.__period.start)
 | 
					            conditions.append(JournalEntry.date >= self.__period.start)
 | 
				
			||||||
        if self.__period.end is not None:
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
            conditions.append(Voucher.date <= self.__period.end)
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
					        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
				
			||||||
            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
					            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
				
			||||||
            else_=-JournalEntryLineItem.amount)).label("balance")
 | 
					            else_=-JournalEntryLineItem.amount)).label("balance")
 | 
				
			||||||
        select_balances: sa.Select = sa.select(Account.id, balance_func)\
 | 
					        select_balances: sa.Select = sa.select(Account.id, balance_func)\
 | 
				
			||||||
            .join(Voucher).join(Account)\
 | 
					            .join(JournalEntry).join(Account)\
 | 
				
			||||||
            .filter(*conditions)\
 | 
					            .filter(*conditions)\
 | 
				
			||||||
            .group_by(Account.id)\
 | 
					            .group_by(Account.id)\
 | 
				
			||||||
            .order_by(Account.base_code, Account.no)
 | 
					            .order_by(Account.base_code, Account.no)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,7 +27,7 @@ from flask import request
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from accounting import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.models import Currency, JournalEntryLineItem
 | 
					from accounting.models import Currency, JournalEntryLineItem
 | 
				
			||||||
from accounting.utils.voucher_types import VoucherType
 | 
					from accounting.utils.journal_entry_types import JournalEntryType
 | 
				
			||||||
from .option_link import OptionLink
 | 
					from .option_link import OptionLink
 | 
				
			||||||
from .report_chooser import ReportChooser
 | 
					from .report_chooser import ReportChooser
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -52,12 +52,12 @@ class BasePageParams(ABC):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def voucher_types(self) -> t.Type[VoucherType]:
 | 
					    def journal_entry_types(self) -> t.Type[JournalEntryType]:
 | 
				
			||||||
        """Returns the voucher types.
 | 
					        """Returns the journal entry types.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: The voucher types.
 | 
					        :return: The journal entry types.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return VoucherType
 | 
					        return JournalEntryType
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def csv_uri(self) -> str:
 | 
					    def csv_uri(self) -> str:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -149,7 +149,7 @@
 | 
				
			|||||||
    overflow-y: scroll;
 | 
					    overflow-y: scroll;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** The voucher management */
 | 
					/** The journal entry management */
 | 
				
			||||||
.accounting-currency-control {
 | 
					.accounting-currency-control {
 | 
				
			||||||
    background-color: transparent;
 | 
					    background-color: transparent;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
/* The Mia! Accounting Flask Project
 | 
					/* The Mia! Accounting Flask Project
 | 
				
			||||||
 * voucher-form.js: The JavaScript for the voucher form
 | 
					 * journal-entry-form.js: The JavaScript for the journal entry form
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*  Copyright (c) 2023 imacat.
 | 
					/*  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -23,14 +23,14 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
document.addEventListener("DOMContentLoaded", () => {
 | 
					document.addEventListener("DOMContentLoaded", () => {
 | 
				
			||||||
    VoucherForm.initialize();
 | 
					    JournalEntryForm.initialize();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The voucher form
 | 
					 * The journal entry form
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class VoucherForm {
 | 
					class JournalEntryForm {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The form element
 | 
					     * The form element
 | 
				
			||||||
@@ -105,7 +105,7 @@ class VoucherForm {
 | 
				
			|||||||
    lineItemEditor;
 | 
					    lineItemEditor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Constructs the voucher form.
 | 
					     * Constructs the journal entry form.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
@@ -325,17 +325,17 @@ class VoucherForm {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The voucher form
 | 
					     * The journal entry form
 | 
				
			||||||
     * @type {VoucherForm}
 | 
					     * @type {JournalEntryForm}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    static #form;
 | 
					    static #form;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Initializes the voucher form.
 | 
					     * Initializes the journal entry form.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    static initialize() {
 | 
					    static initialize() {
 | 
				
			||||||
        this.#form = new VoucherForm()
 | 
					        this.#form = new JournalEntryForm()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -352,8 +352,8 @@ class CurrencySubForm {
 | 
				
			|||||||
    element;
 | 
					    element;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The voucher form
 | 
					     * The journal entry form
 | 
				
			||||||
     * @type {VoucherForm}
 | 
					     * @type {JournalEntryForm}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    form;
 | 
					    form;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -420,7 +420,7 @@ class CurrencySubForm {
 | 
				
			|||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Constructs a currency sub-form
 | 
					     * Constructs a currency sub-form
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param form {VoucherForm} the voucher form
 | 
					     * @param form {JournalEntryForm} the journal entry form
 | 
				
			||||||
     * @param element {HTMLDivElement} the currency sub-form element
 | 
					     * @param element {HTMLDivElement} the currency sub-form element
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(form, element) {
 | 
					    constructor(form, element) {
 | 
				
			||||||
@@ -29,8 +29,8 @@
 | 
				
			|||||||
class JournalEntryLineItemEditor {
 | 
					class JournalEntryLineItemEditor {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The voucher form
 | 
					     * The journal entry form
 | 
				
			||||||
     * @type {VoucherForm}
 | 
					     * @type {JournalEntryForm}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    form;
 | 
					    form;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -217,7 +217,7 @@ class JournalEntryLineItemEditor {
 | 
				
			|||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Constructs a new journal entry line item editor.
 | 
					     * Constructs a new journal entry line item editor.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param form {VoucherForm} the voucher form
 | 
					     * @param form {JournalEntryForm} the journal entry form
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    constructor(form) {
 | 
					    constructor(form) {
 | 
				
			||||||
        this.form = form;
 | 
					        this.form = form;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
/* The Mia! Accounting Flask Project
 | 
					/* The Mia! Accounting Flask Project
 | 
				
			||||||
 * voucher-order.js: The JavaScript for the voucher order
 | 
					 * journal-entry-order.js: The JavaScript for the journal entry order
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*  Copyright (c) 2023 imacat.
 | 
					/*  Copyright (c) 2023 imacat.
 | 
				
			||||||
@@ -105,7 +105,7 @@ class OriginalLineItemSelector {
 | 
				
			|||||||
     * Returns the net balance for an original line item.
 | 
					     * Returns the net balance for an original line item.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param currentLineItem {LineItemSubForm} the line item sub-form that is currently editing
 | 
					     * @param currentLineItem {LineItemSubForm} the line item sub-form that is currently editing
 | 
				
			||||||
     * @param form {VoucherForm} the voucher form
 | 
					     * @param form {JournalEntryForm} the journal entry form
 | 
				
			||||||
     * @param originalLineItemId {string} the ID of the original line item
 | 
					     * @param originalLineItemId {string} the ID of the original line item
 | 
				
			||||||
     * @return {Decimal} the net balance of the original line item
 | 
					     * @return {Decimal} the net balance of the original line item
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
create.html: The cash disbursement voucher creation form
 | 
					create.html: The cash disbursement journal entry creation form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,10 +19,10 @@ create.html: The cash disbursement voucher creation form
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/25
 | 
					First written: 2023/2/25
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/voucher/disbursement/include/form.html" %}
 | 
					{% extends "accounting/journal-entry/disbursement/include/form.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block header %}{% block title %}{{ A_("Add a New Cash Disbursement Voucher") }}{% endblock %}{% endblock %}
 | 
					{% block header %}{% block title %}{{ A_("Add a New Cash Disbursement Journal Entry") }}{% endblock %}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
 | 
					{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block action_url %}{{ url_for("accounting.voucher.store", voucher_type=voucher_type) }}{% endblock %}
 | 
					{% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %}
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
detail.html: The cash disbursement voucher detail
 | 
					detail.html: The cash disbursement journal entry detail
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,16 +19,16 @@ detail.html: The cash disbursement voucher detail
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/26
 | 
					First written: 2023/2/26
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/voucher/include/detail.html" %}
 | 
					{% extends "accounting/journal-entry/include/detail.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block to_transfer %}
 | 
					{% block to_transfer %}
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.voucher.edit", voucher=obj)|accounting_voucher_to_transfer|accounting_inherit_next }}">
 | 
					  <a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_journal_entry_to_transfer|accounting_inherit_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-bars-staggered"></i>
 | 
					    <i class="fa-solid fa-bars-staggered"></i>
 | 
				
			||||||
    {{ A_("To Transfer") }}
 | 
					    {{ A_("To Transfer") }}
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block voucher_currencies %}
 | 
					{% block journal_entry_currencies %}
 | 
				
			||||||
  {% for currency in obj.currencies %}
 | 
					  {% for currency in obj.currencies %}
 | 
				
			||||||
    <div class="mb-3">
 | 
					    <div class="mb-3">
 | 
				
			||||||
      <div class="mb-2 fw-bolder">{{ currency.name }}</div>
 | 
					      <div class="mb-2 fw-bolder">{{ currency.name }}</div>
 | 
				
			||||||
@@ -36,7 +36,7 @@ First written: 2023/2/26
 | 
				
			|||||||
      <ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
 | 
					      <ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
 | 
				
			||||||
        <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Content") }}</li>
 | 
					        <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Content") }}</li>
 | 
				
			||||||
        {% with line_items = currency.debit %}
 | 
					        {% with line_items = currency.debit %}
 | 
				
			||||||
          {% include "accounting/voucher/include/detail-line-items.html" %}
 | 
					          {% include "accounting/journal-entry/include/detail-line-items.html" %}
 | 
				
			||||||
        {% endwith %}
 | 
					        {% endwith %}
 | 
				
			||||||
        <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
 | 
					        <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
 | 
				
			||||||
          <div class="d-flex justify-content-between">
 | 
					          <div class="d-flex justify-content-between">
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
edit.html: The cash receipt voucher edit form
 | 
					edit.html: The cash disbursement journal entry edit form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,10 +19,10 @@ edit.html: The cash receipt voucher edit form
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/25
 | 
					First written: 2023/2/25
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/voucher/receipt/include/form.html" %}
 | 
					{% extends "accounting/journal-entry/disbursement/include/form.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block header %}{% block title %}{{ A_("Editing %(voucher)s", voucher=voucher) }}{% endblock %}{% endblock %}
 | 
					{% block header %}{% block title %}{{ A_("Editing %(journal_entry)s", journal_entry=journal_entry) }}{% endblock %}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block back_url %}{{ url_for("accounting.voucher.detail", voucher=voucher)|accounting_inherit_next }}{% endblock %}
 | 
					{% block back_url %}{{ url_for("accounting.journal-entry.detail", journal_entry=journal_entry)|accounting_inherit_next }}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block action_url %}{{ url_for("accounting.voucher.update", voucher=voucher)|accounting_voucher_with_type }}{% endblock %}
 | 
					{% block action_url %}{{ url_for("accounting.journal-entry.update", journal_entry=journal_entry)|accounting_journal_entry_with_type }}{% endblock %}
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
currency-sub-form.html: The currency sub-form in the cash disbursement voucher form
 | 
					form-currency.html: The currency sub-form in the cash disbursement journal entry form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -51,7 +51,7 @@ First written: 2023/2/25
 | 
				
			|||||||
                    line_item_index = loop.index,
 | 
					                    line_item_index = loop.index,
 | 
				
			||||||
                    only_one_line_item_form = debit_forms|length == 1,
 | 
					                    only_one_line_item_form = debit_forms|length == 1,
 | 
				
			||||||
                    form = line_item_form.form %}
 | 
					                    form = line_item_form.form %}
 | 
				
			||||||
              {% include "accounting/voucher/include/form-line-item.html" %}
 | 
					              {% include "accounting/journal-entry/include/form-line-item.html" %}
 | 
				
			||||||
            {% endwith %}
 | 
					            {% endwith %}
 | 
				
			||||||
          {% endfor %}
 | 
					          {% endfor %}
 | 
				
			||||||
        </ul>
 | 
					        </ul>
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
form.html: The cash disbursement voucher form
 | 
					form.html: The cash disbursement journal entry form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,7 +19,7 @@ form.html: The cash disbursement voucher form
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/25
 | 
					First written: 2023/2/25
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/voucher/include/form.html" %}
 | 
					{% extends "accounting/journal-entry/include/form.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block currency_sub_forms %}
 | 
					{% block currency_sub_forms %}
 | 
				
			||||||
  {% if form.currencies %}
 | 
					  {% if form.currencies %}
 | 
				
			||||||
@@ -33,7 +33,7 @@ First written: 2023/2/25
 | 
				
			|||||||
              debit_forms = currency_form.debit,
 | 
					              debit_forms = currency_form.debit,
 | 
				
			||||||
              debit_errors = currency_form.debit_errors,
 | 
					              debit_errors = currency_form.debit_errors,
 | 
				
			||||||
              debit_total = currency_form.form.debit_total|accounting_format_amount %}
 | 
					              debit_total = currency_form.form.debit_total|accounting_format_amount %}
 | 
				
			||||||
        {% include "accounting/voucher/disbursement/include/form-currency-item.html" %}
 | 
					        {% include "accounting/journal-entry/disbursement/include/form-currency.html" %}
 | 
				
			||||||
      {% endwith %}
 | 
					      {% endwith %}
 | 
				
			||||||
    {% endfor %}
 | 
					    {% endfor %}
 | 
				
			||||||
  {% else %}
 | 
					  {% else %}
 | 
				
			||||||
@@ -41,17 +41,17 @@ First written: 2023/2/25
 | 
				
			|||||||
            only_one_currency_form = True,
 | 
					            only_one_currency_form = True,
 | 
				
			||||||
            currency_code_data = accounting_default_currency_code(),
 | 
					            currency_code_data = accounting_default_currency_code(),
 | 
				
			||||||
            debit_total = "-" %}
 | 
					            debit_total = "-" %}
 | 
				
			||||||
      {% include "accounting/voucher/disbursement/include/form-currency-item.html" %}
 | 
					      {% include "accounting/journal-entry/disbursement/include/form-currency.html" %}
 | 
				
			||||||
    {% endwith %}
 | 
					    {% endwith %}
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block form_modals %}
 | 
					{% block form_modals %}
 | 
				
			||||||
  {% with description_editor = form.description_editor.debit %}
 | 
					  {% with description_editor = form.description_editor.debit %}
 | 
				
			||||||
    {% include "accounting/voucher/include/description-editor-modal.html" %}
 | 
					    {% include "accounting/journal-entry/include/description-editor-modal.html" %}
 | 
				
			||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
  {% with debit_credit = "debit",
 | 
					  {% with debit_credit = "debit",
 | 
				
			||||||
          account_options = form.debit_account_options %}
 | 
					          account_options = form.debit_account_options %}
 | 
				
			||||||
    {% include "accounting/voucher/include/account-selector-modal.html" %}
 | 
					    {% include "accounting/journal-entry/include/account-selector-modal.html" %}
 | 
				
			||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
detail-line-items-item: The line items in the voucher detail
 | 
					detail-line-items-item: The line items in the journal entry detail
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,7 +30,7 @@ First written: 2023/3/14
 | 
				
			|||||||
        {% endif %}
 | 
					        {% endif %}
 | 
				
			||||||
        {% if line_item.original_line_item %}
 | 
					        {% if line_item.original_line_item %}
 | 
				
			||||||
          <div class="fst-italic small accounting-original-line-item">
 | 
					          <div class="fst-italic small accounting-original-line-item">
 | 
				
			||||||
            <a href="{{ url_for("accounting.voucher.detail", voucher=line_item.original_line_item.voucher)|accounting_append_next }}">
 | 
					            <a href="{{ url_for("accounting.journal-entry.detail", journal_entry=line_item.original_line_item.journal_entry)|accounting_append_next }}">
 | 
				
			||||||
              {{ A_("Offset %(item)s", item=line_item.original_line_item) }}
 | 
					              {{ A_("Offset %(item)s", item=line_item.original_line_item) }}
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
@@ -43,8 +43,8 @@ First written: 2023/3/14
 | 
				
			|||||||
                <ul class="ms-2 ps-0">
 | 
					                <ul class="ms-2 ps-0">
 | 
				
			||||||
                  {% for offset in line_item.offsets %}
 | 
					                  {% for offset in line_item.offsets %}
 | 
				
			||||||
                    <li>
 | 
					                    <li>
 | 
				
			||||||
                      <a href="{{ url_for("accounting.voucher.detail", voucher=offset.voucher)|accounting_append_next }}">
 | 
					                      <a href="{{ url_for("accounting.journal-entry.detail", journal_entry=offset.journal_entry)|accounting_append_next }}">
 | 
				
			||||||
                        {{ offset.voucher.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}
 | 
					                        {{ offset.journal_entry.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}
 | 
				
			||||||
                      </a>
 | 
					                      </a>
 | 
				
			||||||
                    </li>
 | 
					                    </li>
 | 
				
			||||||
                  {% endfor %}
 | 
					                  {% endfor %}
 | 
				
			||||||
@@ -31,12 +31,12 @@ First written: 2023/2/26
 | 
				
			|||||||
    {{ A_("Back") }}
 | 
					    {{ A_("Back") }}
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
  {% if accounting_can_edit() %}
 | 
					  {% if accounting_can_edit() %}
 | 
				
			||||||
    <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.voucher.edit", voucher=obj)|accounting_inherit_next }}">
 | 
					    <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_inherit_next }}">
 | 
				
			||||||
      <i class="fa-solid fa-gear"></i>
 | 
					      <i class="fa-solid fa-gear"></i>
 | 
				
			||||||
      {{ A_("Settings") }}
 | 
					      {{ A_("Settings") }}
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.voucher.order", voucher_date=obj.date)|accounting_append_next }}">
 | 
					  <a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.order", journal_entry_date=obj.date)|accounting_append_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-bars-staggered"></i>
 | 
					    <i class="fa-solid fa-bars-staggered"></i>
 | 
				
			||||||
    {{ A_("Order") }}
 | 
					    {{ A_("Order") }}
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
@@ -58,14 +58,14 @@ 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.voucher.edit", voucher=obj)|accounting_inherit_next }}">
 | 
					    <a class="btn btn-primary" 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>
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% if accounting_can_edit() and obj.can_delete %}
 | 
					{% if accounting_can_edit() and obj.can_delete %}
 | 
				
			||||||
  <form action="{{ url_for("accounting.voucher.delete", voucher=obj) }}" method="post">
 | 
					  <form action="{{ url_for("accounting.journal-entry.delete", journal_entry=obj) }}" method="post">
 | 
				
			||||||
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
					    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
				
			||||||
    {% if request.args.next %}
 | 
					    {% if request.args.next %}
 | 
				
			||||||
      <input type="hidden" name="next" value="{{ request.args.next }}">
 | 
					      <input type="hidden" name="next" value="{{ request.args.next }}">
 | 
				
			||||||
@@ -74,11 +74,11 @@ First written: 2023/2/26
 | 
				
			|||||||
      <div class="modal-dialog">
 | 
					      <div class="modal-dialog">
 | 
				
			||||||
        <div class="modal-content">
 | 
					        <div class="modal-content">
 | 
				
			||||||
          <div class="modal-header">
 | 
					          <div class="modal-header">
 | 
				
			||||||
            <h1 class="modal-title fs-5" id="accounting-delete-modal-label">{{ A_("Delete Voucher Confirmation") }}</h1>
 | 
					            <h1 class="modal-title fs-5" id="accounting-delete-modal-label">{{ A_("Delete Journal Entry Confirmation") }}</h1>
 | 
				
			||||||
            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ A_("Close") }}"></button>
 | 
					            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ A_("Close") }}"></button>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div class="modal-body">
 | 
					          <div class="modal-body">
 | 
				
			||||||
            {{ A_("Do you really want to delete this voucher?") }}
 | 
					            {{ A_("Do you really want to delete this journal entry?") }}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div class="modal-footer">
 | 
					          <div class="modal-footer">
 | 
				
			||||||
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
 | 
					            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
 | 
				
			||||||
@@ -99,13 +99,13 @@ First written: 2023/2/26
 | 
				
			|||||||
    {{ obj.date|accounting_format_date }}
 | 
					    {{ obj.date|accounting_format_date }}
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {% block voucher_currencies %}{% endblock %}
 | 
					  {% block journal_entry_currencies %}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {% if obj.note %}
 | 
					  {% if obj.note %}
 | 
				
			||||||
    <div class="card mb-3">
 | 
					    <div class="card mb-3">
 | 
				
			||||||
      <div class="card-body">
 | 
					      <div class="card-body">
 | 
				
			||||||
        <i class="far fa-comment-dots"></i>
 | 
					        <i class="far fa-comment-dots"></i>
 | 
				
			||||||
        {{ obj.note|accounting_voucher_text2html|safe }}
 | 
					        {{ obj.note|accounting_journal_entry_text2html|safe }}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
form-line-item.html: The line item sub-form in the voucher form
 | 
					form-line-item.html: The line item sub-form in the journal entry form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -28,7 +28,7 @@ First written: 2023/2/25
 | 
				
			|||||||
  <input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original-line-item-id" class="accounting-original-line-item-id" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original_line_item_id" value="{{ form.original_line_item_id.data|accounting_default }}" data-date="{{ form.original_line_item_date|accounting_default }}" data-text="{{ form.original_line_item_text|accounting_default }}">
 | 
					  <input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original-line-item-id" class="accounting-original-line-item-id" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original_line_item_id" value="{{ form.original_line_item_id.data|accounting_default }}" data-date="{{ form.original_line_item_date|accounting_default }}" data-text="{{ form.original_line_item_text|accounting_default }}">
 | 
				
			||||||
  <input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-code" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account_code" value="{{ form.account_code.data|accounting_default }}" data-text="{{ form.account_text }}">
 | 
					  <input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-code" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account_code" value="{{ form.account_code.data|accounting_default }}" data-text="{{ form.account_text }}">
 | 
				
			||||||
  <input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" value="{{ form.description.data|accounting_default }}">
 | 
					  <input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" value="{{ form.description.data|accounting_default }}">
 | 
				
			||||||
  <input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" value="{{ form.amount.data|accounting_voucher_format_amount_input }}" data-min="{{ form.offset_total|accounting_default("0") }}">
 | 
					  <input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" value="{{ form.amount.data|accounting_journal_entry_format_amount_input }}" data-min="{{ form.offset_total|accounting_default("0") }}">
 | 
				
			||||||
  <div class="accounting-line-item-content">
 | 
					  <div class="accounting-line-item-content">
 | 
				
			||||||
    <div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-control" class="form-control clickable d-flex justify-content-between {% if form.all_errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
 | 
					    <div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-control" class="form-control clickable d-flex justify-content-between {% if form.all_errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
 | 
				
			||||||
      <div>
 | 
					      <div>
 | 
				
			||||||
@@ -43,7 +43,7 @@ First written: 2023/2/25
 | 
				
			|||||||
              <div>{{ A_("Offsets") }}</div>
 | 
					              <div>{{ A_("Offsets") }}</div>
 | 
				
			||||||
              <ul class="ms-2 ps-0">
 | 
					              <ul class="ms-2 ps-0">
 | 
				
			||||||
                {% for offset in form.offsets %}
 | 
					                {% for offset in form.offsets %}
 | 
				
			||||||
                  <li>{{ offset.voucher.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}</li>
 | 
					                  <li>{{ offset.journal_entry.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}</li>
 | 
				
			||||||
                {% endfor %}
 | 
					                {% endfor %}
 | 
				
			||||||
              </ul>
 | 
					              </ul>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
form.html: The base voucher form
 | 
					form.html: The base journal entry form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -23,7 +23,7 @@ First written: 2023/2/26
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block accounting_scripts %}
 | 
					{% 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/drag-and-drop-reorder.js") }}"></script>
 | 
				
			||||||
  <script src="{{ url_for("accounting.static", filename="js/voucher-form.js") }}"></script>
 | 
					  <script src="{{ url_for("accounting.static", filename="js/journal-entry-form.js") }}"></script>
 | 
				
			||||||
  <script src="{{ url_for("accounting.static", filename="js/journal-entry-line-item-editor.js") }}"></script>
 | 
					  <script src="{{ url_for("accounting.static", filename="js/journal-entry-line-item-editor.js") }}"></script>
 | 
				
			||||||
  <script src="{{ url_for("accounting.static", filename="js/account-selector.js") }}"></script>
 | 
					  <script src="{{ url_for("accounting.static", filename="js/account-selector.js") }}"></script>
 | 
				
			||||||
  <script src="{{ url_for("accounting.static", filename="js/original-line-item-selector.js") }}"></script>
 | 
					  <script src="{{ url_for("accounting.static", filename="js/original-line-item-selector.js") }}"></script>
 | 
				
			||||||
@@ -88,8 +88,8 @@ First written: 2023/2/26
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/voucher/include/journal-entry-line-item-editor-modal.html" %}
 | 
					{% include "accounting/journal-entry/include/journal-entry-line-item-editor-modal.html" %}
 | 
				
			||||||
{% block form_modals %}{% endblock %}
 | 
					{% block form_modals %}{% endblock %}
 | 
				
			||||||
{% include "accounting/voucher/include/original-line-item-selector-modal.html" %}
 | 
					{% include "accounting/journal-entry/include/original-line-item-selector-modal.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
@@ -37,8 +37,8 @@ First written: 2023/2/25
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        <ul id="accounting-original-line-item-selector-option-list" class="list-group accounting-selector-list">
 | 
					        <ul id="accounting-original-line-item-selector-option-list" class="list-group accounting-selector-list">
 | 
				
			||||||
          {% for line_item in form.original_line_item_options %}
 | 
					          {% for line_item in form.original_line_item_options %}
 | 
				
			||||||
            <li id="accounting-original-line-item-selector-option-{{ line_item.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-line-item-selector-option" data-id="{{ line_item.id }}" data-date="{{ line_item.voucher.date }}" data-debit-credit="{{ "debit" if line_item.is_debit else "credit" }}" data-currency-code="{{ line_item.currency.code }}" data-account-code="{{ line_item.account_code }}" data-account-text="{{ line_item.account }}" data-description="{{ line_item.description|accounting_default }}" data-net-balance="{{ line_item.net_balance|accounting_voucher_format_amount_input }}" data-text="{{ line_item }}" data-query-values="{{ line_item.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
 | 
					            <li id="accounting-original-line-item-selector-option-{{ line_item.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-line-item-selector-option" data-id="{{ line_item.id }}" data-date="{{ line_item.journal_entry.date }}" data-debit-credit="{{ "debit" if line_item.is_debit else "credit" }}" data-currency-code="{{ line_item.currency.code }}" data-account-code="{{ line_item.account_code }}" data-account-text="{{ line_item.account }}" data-description="{{ line_item.description|accounting_default }}" data-net-balance="{{ line_item.net_balance|accounting_journal_entry_format_amount_input }}" data-text="{{ line_item }}" data-query-values="{{ line_item.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
 | 
				
			||||||
              <div>{{ line_item.voucher.date|accounting_format_date }} {{ line_item.description|accounting_default }}</div>
 | 
					              <div>{{ line_item.journal_entry.date|accounting_format_date }} {{ line_item.description|accounting_default }}</div>
 | 
				
			||||||
              <div>
 | 
					              <div>
 | 
				
			||||||
                <span class="badge bg-primary rounded-pill">
 | 
					                <span class="badge bg-primary rounded-pill">
 | 
				
			||||||
                  <span id="accounting-original-line-item-selector-option-{{ line_item.id }}-net-balance">{{ line_item.net_balance|accounting_format_amount }}</span>
 | 
					                  <span id="accounting-original-line-item-selector-option-{{ line_item.id }}-net-balance">{{ line_item.net_balance|accounting_format_amount }}</span>
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
order.html: The order of the vouchers in a same day
 | 
					order.html: The order of the journal entries in a same day
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -23,10 +23,10 @@ First written: 2023/2/26
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block accounting_scripts %}
 | 
					{% 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/drag-and-drop-reorder.js") }}"></script>
 | 
				
			||||||
  <script src="{{ url_for("accounting.static", filename="js/voucher-order.js") }}"></script>
 | 
					  <script src="{{ url_for("accounting.static", filename="js/journal-entry-order.js") }}"></script>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block header %}{% block title %}{{ A_("Vouchers on %(date)s", date=date) }}{% endblock %}{% endblock %}
 | 
					{% block header %}{% block title %}{{ A_("Journal Entries on %(date)s", date=date) }}{% endblock %}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -38,7 +38,7 @@ First written: 2023/2/26
 | 
				
			|||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% if list|length > 1 and accounting_can_edit() %}
 | 
					{% if list|length > 1 and accounting_can_edit() %}
 | 
				
			||||||
  <form action="{{ url_for("accounting.voucher.sort", voucher_date=date) }}" method="post">
 | 
					  <form action="{{ url_for("accounting.journal-entry.sort", journal_entry_date=date) }}" method="post">
 | 
				
			||||||
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
					    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
				
			||||||
    {% if request.args.next %}
 | 
					    {% if request.args.next %}
 | 
				
			||||||
      <input type="hidden" name="next" value="{{ request.args.next }}">
 | 
					      <input type="hidden" name="next" value="{{ request.args.next }}">
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
create.html: The cash receipt voucher creation form
 | 
					create.html: The cash receipt journal entry creation form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,10 +19,10 @@ create.html: The cash receipt voucher creation form
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/25
 | 
					First written: 2023/2/25
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/voucher/receipt/include/form.html" %}
 | 
					{% extends "accounting/journal-entry/receipt/include/form.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block header %}{% block title %}{{ A_("Add a New Cash Receipt Voucher") }}{% endblock %}{% endblock %}
 | 
					{% block header %}{% block title %}{{ A_("Add a New Cash Receipt Journal Entry") }}{% endblock %}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
 | 
					{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block action_url %}{{ url_for("accounting.voucher.store", voucher_type=voucher_type) }}{% endblock %}
 | 
					{% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %}
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
detail.html: The cash receipt voucher detail
 | 
					detail.html: The cash receipt journal entry detail
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,16 +19,16 @@ detail.html: The cash receipt voucher detail
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/26
 | 
					First written: 2023/2/26
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/voucher/include/detail.html" %}
 | 
					{% extends "accounting/journal-entry/include/detail.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block to_transfer %}
 | 
					{% block to_transfer %}
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.voucher.edit", voucher=obj)|accounting_voucher_to_transfer|accounting_inherit_next }}">
 | 
					  <a class="btn btn-primary" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_journal_entry_to_transfer|accounting_inherit_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-bars-staggered"></i>
 | 
					    <i class="fa-solid fa-bars-staggered"></i>
 | 
				
			||||||
    {{ A_("To Transfer") }}
 | 
					    {{ A_("To Transfer") }}
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block voucher_currencies %}
 | 
					{% block journal_entry_currencies %}
 | 
				
			||||||
  {% for currency in obj.currencies %}
 | 
					  {% for currency in obj.currencies %}
 | 
				
			||||||
    <div class="mb-3">
 | 
					    <div class="mb-3">
 | 
				
			||||||
      <div class="mb-2 fw-bolder">{{ currency.name }}</div>
 | 
					      <div class="mb-2 fw-bolder">{{ currency.name }}</div>
 | 
				
			||||||
@@ -36,7 +36,7 @@ First written: 2023/2/26
 | 
				
			|||||||
      <ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
 | 
					      <ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
 | 
				
			||||||
        <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Content") }}</li>
 | 
					        <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Content") }}</li>
 | 
				
			||||||
        {% with line_items = currency.credit %}
 | 
					        {% with line_items = currency.credit %}
 | 
				
			||||||
          {% include "accounting/voucher/include/detail-line-items.html" %}
 | 
					          {% include "accounting/journal-entry/include/detail-line-items.html" %}
 | 
				
			||||||
        {% endwith %}
 | 
					        {% endwith %}
 | 
				
			||||||
        <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
 | 
					        <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
 | 
				
			||||||
          <div class="d-flex justify-content-between">
 | 
					          <div class="d-flex justify-content-between">
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
edit.html: The transfer voucher edit form
 | 
					edit.html: The cash receipt journal entry edit form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,10 +19,10 @@ edit.html: The transfer voucher edit form
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/25
 | 
					First written: 2023/2/25
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/voucher/transfer/include/form.html" %}
 | 
					{% extends "accounting/journal-entry/receipt/include/form.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block header %}{% block title %}{{ A_("Editing %(voucher)s", voucher=voucher) }}{% endblock %}{% endblock %}
 | 
					{% block header %}{% block title %}{{ A_("Editing %(journal_entry)s", journal_entry=journal_entry) }}{% endblock %}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block back_url %}{{ url_for("accounting.voucher.detail", voucher=voucher)|accounting_inherit_next }}{% endblock %}
 | 
					{% block back_url %}{{ url_for("accounting.journal-entry.detail", journal_entry=journal_entry)|accounting_inherit_next }}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block action_url %}{{ url_for("accounting.voucher.update", voucher=voucher)|accounting_voucher_with_type }}{% endblock %}
 | 
					{% block action_url %}{{ url_for("accounting.journal-entry.update", journal_entry=journal_entry)|accounting_journal_entry_with_type }}{% endblock %}
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
currency-sub-form.html: The currency sub-form in the cash receipt voucher form
 | 
					form-currency.html: The currency sub-form in the cash receipt journal entry form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -51,7 +51,7 @@ First written: 2023/2/25
 | 
				
			|||||||
                    line_item_index = loop.index,
 | 
					                    line_item_index = loop.index,
 | 
				
			||||||
                    only_one_line_item_form = credit_forms|length == 1,
 | 
					                    only_one_line_item_form = credit_forms|length == 1,
 | 
				
			||||||
                    form = line_item_form.form %}
 | 
					                    form = line_item_form.form %}
 | 
				
			||||||
              {% include "accounting/voucher/include/form-line-item.html" %}
 | 
					              {% include "accounting/journal-entry/include/form-line-item.html" %}
 | 
				
			||||||
            {% endwith %}
 | 
					            {% endwith %}
 | 
				
			||||||
          {% endfor %}
 | 
					          {% endfor %}
 | 
				
			||||||
        </ul>
 | 
					        </ul>
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
form.html: The cash receipt voucher form
 | 
					form.html: The cash receipt journal entry form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,7 +19,7 @@ form.html: The cash receipt voucher form
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/25
 | 
					First written: 2023/2/25
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/voucher/include/form.html" %}
 | 
					{% extends "accounting/journal-entry/include/form.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block currency_sub_forms %}
 | 
					{% block currency_sub_forms %}
 | 
				
			||||||
  {% if form.currencies %}
 | 
					  {% if form.currencies %}
 | 
				
			||||||
@@ -33,7 +33,7 @@ First written: 2023/2/25
 | 
				
			|||||||
              credit_forms = currency_form.credit,
 | 
					              credit_forms = currency_form.credit,
 | 
				
			||||||
              credit_errors = currency_form.credit_errors,
 | 
					              credit_errors = currency_form.credit_errors,
 | 
				
			||||||
              credit_total = currency_form.form.credit_total|accounting_format_amount %}
 | 
					              credit_total = currency_form.form.credit_total|accounting_format_amount %}
 | 
				
			||||||
        {% include "accounting/voucher/receipt/include/form-currency-item.html" %}
 | 
					        {% include "accounting/journal-entry/receipt/include/form-currency.html" %}
 | 
				
			||||||
      {% endwith %}
 | 
					      {% endwith %}
 | 
				
			||||||
    {% endfor %}
 | 
					    {% endfor %}
 | 
				
			||||||
  {% else %}
 | 
					  {% else %}
 | 
				
			||||||
@@ -41,17 +41,17 @@ First written: 2023/2/25
 | 
				
			|||||||
            only_one_currency_form = True,
 | 
					            only_one_currency_form = True,
 | 
				
			||||||
            currency_code_data = accounting_default_currency_code(),
 | 
					            currency_code_data = accounting_default_currency_code(),
 | 
				
			||||||
            credit_total = "-" %}
 | 
					            credit_total = "-" %}
 | 
				
			||||||
      {% include "accounting/voucher/receipt/include/form-currency-item.html" %}
 | 
					      {% include "accounting/journal-entry/receipt/include/form-currency.html" %}
 | 
				
			||||||
    {% endwith %}
 | 
					    {% endwith %}
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block form_modals %}
 | 
					{% block form_modals %}
 | 
				
			||||||
  {% with description_editor = form.description_editor.credit %}
 | 
					  {% with description_editor = form.description_editor.credit %}
 | 
				
			||||||
    {% include "accounting/voucher/include/description-editor-modal.html" %}
 | 
					    {% include "accounting/journal-entry/include/description-editor-modal.html" %}
 | 
				
			||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
  {% with debit_credit = "credit",
 | 
					  {% with debit_credit = "credit",
 | 
				
			||||||
          account_options = form.credit_account_options %}
 | 
					          account_options = form.credit_account_options %}
 | 
				
			||||||
    {% include "accounting/voucher/include/account-selector-modal.html" %}
 | 
					    {% include "accounting/journal-entry/include/account-selector-modal.html" %}
 | 
				
			||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
create.html: The transfer voucher creation form
 | 
					create.html: The transfer journal entry creation form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,10 +19,10 @@ create.html: The transfer voucher creation form
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/25
 | 
					First written: 2023/2/25
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/voucher/transfer/include/form.html" %}
 | 
					{% extends "accounting/journal-entry/transfer/include/form.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block header %}{% block title %}{{ A_("Add a New Transfer Voucher") }}{% endblock %}{% endblock %}
 | 
					{% block header %}{% block title %}{{ A_("Add a New Transfer Journal Entry") }}{% endblock %}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
 | 
					{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block action_url %}{{ url_for("accounting.voucher.store", voucher_type=voucher_type) }}{% endblock %}
 | 
					{% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %}
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
detail.html: The transfer voucher detail
 | 
					detail.html: The transfer journal entry detail
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,9 +19,9 @@ detail.html: The transfer voucher detail
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/26
 | 
					First written: 2023/2/26
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/voucher/include/detail.html" %}
 | 
					{% extends "accounting/journal-entry/include/detail.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block voucher_currencies %}
 | 
					{% block journal_entry_currencies %}
 | 
				
			||||||
  {% for currency in obj.currencies %}
 | 
					  {% for currency in obj.currencies %}
 | 
				
			||||||
    <div class="mb-3">
 | 
					    <div class="mb-3">
 | 
				
			||||||
      <div class="mb-2 fw-bolder">{{ currency.name }}</div>
 | 
					      <div class="mb-2 fw-bolder">{{ currency.name }}</div>
 | 
				
			||||||
@@ -32,7 +32,7 @@ First written: 2023/2/26
 | 
				
			|||||||
          <ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
 | 
					          <ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
 | 
				
			||||||
            <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Debit") }}</li>
 | 
					            <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Debit") }}</li>
 | 
				
			||||||
            {% with line_items = currency.debit %}
 | 
					            {% with line_items = currency.debit %}
 | 
				
			||||||
              {% include "accounting/voucher/include/detail-line-items.html" %}
 | 
					              {% include "accounting/journal-entry/include/detail-line-items.html" %}
 | 
				
			||||||
            {% endwith %}
 | 
					            {% endwith %}
 | 
				
			||||||
            <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
 | 
					            <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
 | 
				
			||||||
              <div class="d-flex justify-content-between">
 | 
					              <div class="d-flex justify-content-between">
 | 
				
			||||||
@@ -48,7 +48,7 @@ First written: 2023/2/26
 | 
				
			|||||||
          <ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
 | 
					          <ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
 | 
				
			||||||
            <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Credit") }}</li>
 | 
					            <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Credit") }}</li>
 | 
				
			||||||
            {% with line_items = currency.credit %}
 | 
					            {% with line_items = currency.credit %}
 | 
				
			||||||
              {% include "accounting/voucher/include/detail-line-items.html" %}
 | 
					              {% include "accounting/journal-entry/include/detail-line-items.html" %}
 | 
				
			||||||
            {% endwith %}
 | 
					            {% endwith %}
 | 
				
			||||||
            <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
 | 
					            <li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
 | 
				
			||||||
              <div class="d-flex justify-content-between">
 | 
					              <div class="d-flex justify-content-between">
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
edit.html: The cash disbursement voucher edit form
 | 
					edit.html: The transfer journal entry edit form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,10 +19,10 @@ edit.html: The cash disbursement voucher edit form
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/25
 | 
					First written: 2023/2/25
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/voucher/disbursement/include/form.html" %}
 | 
					{% extends "accounting/journal-entry/transfer/include/form.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block header %}{% block title %}{{ A_("Editing %(voucher)s", voucher=voucher) }}{% endblock %}{% endblock %}
 | 
					{% block header %}{% block title %}{{ A_("Editing %(journal_entry)s", journal_entry=journal_entry) }}{% endblock %}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block back_url %}{{ url_for("accounting.voucher.detail", voucher=voucher)|accounting_inherit_next }}{% endblock %}
 | 
					{% block back_url %}{{ url_for("accounting.journal-entry.detail", journal_entry=journal_entry)|accounting_inherit_next }}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block action_url %}{{ url_for("accounting.voucher.update", voucher=voucher)|accounting_voucher_with_type }}{% endblock %}
 | 
					{% block action_url %}{{ url_for("accounting.journal-entry.update", journal_entry=journal_entry)|accounting_journal_entry_with_type }}{% endblock %}
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
currency-sub-form.html: The currency sub-form in the transfer voucher form
 | 
					form-currency.html: The currency sub-form in the transfer journal entry form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -53,7 +53,7 @@ First written: 2023/2/25
 | 
				
			|||||||
                      line_item_index = loop.index,
 | 
					                      line_item_index = loop.index,
 | 
				
			||||||
                      only_one_line_item_form = debit_forms|length == 1,
 | 
					                      only_one_line_item_form = debit_forms|length == 1,
 | 
				
			||||||
                      form = line_item_form.form %}
 | 
					                      form = line_item_form.form %}
 | 
				
			||||||
                {% include "accounting/voucher/include/form-line-item.html" %}
 | 
					                {% include "accounting/journal-entry/include/form-line-item.html" %}
 | 
				
			||||||
              {% endwith %}
 | 
					              {% endwith %}
 | 
				
			||||||
            {% endfor %}
 | 
					            {% endfor %}
 | 
				
			||||||
          </ul>
 | 
					          </ul>
 | 
				
			||||||
@@ -84,7 +84,7 @@ First written: 2023/2/25
 | 
				
			|||||||
                      line_item_index = loop.index,
 | 
					                      line_item_index = loop.index,
 | 
				
			||||||
                      only_one_line_item_form = credit_forms|length == 1,
 | 
					                      only_one_line_item_form = credit_forms|length == 1,
 | 
				
			||||||
                      form = line_item_form.form %}
 | 
					                      form = line_item_form.form %}
 | 
				
			||||||
                {% include "accounting/voucher/include/form-line-item.html" %}
 | 
					                {% include "accounting/journal-entry/include/form-line-item.html" %}
 | 
				
			||||||
              {% endwith %}
 | 
					              {% endwith %}
 | 
				
			||||||
            {% endfor %}
 | 
					            {% endfor %}
 | 
				
			||||||
          </ul>
 | 
					          </ul>
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
form.html: The transfer voucher form
 | 
					form.html: The transfer journal entry form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,7 +19,7 @@ form.html: The transfer voucher form
 | 
				
			|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
First written: 2023/2/25
 | 
					First written: 2023/2/25
 | 
				
			||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/voucher/include/form.html" %}
 | 
					{% extends "accounting/journal-entry/include/form.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block currency_sub_forms %}
 | 
					{% block currency_sub_forms %}
 | 
				
			||||||
  {% if form.currencies %}
 | 
					  {% if form.currencies %}
 | 
				
			||||||
@@ -36,7 +36,7 @@ First written: 2023/2/25
 | 
				
			|||||||
              credit_forms = currency_form.credit,
 | 
					              credit_forms = currency_form.credit,
 | 
				
			||||||
              credit_errors = currency_form.credit_errors,
 | 
					              credit_errors = currency_form.credit_errors,
 | 
				
			||||||
              credit_total = currency_form.form.credit_total|accounting_format_amount %}
 | 
					              credit_total = currency_form.form.credit_total|accounting_format_amount %}
 | 
				
			||||||
        {% include "accounting/voucher/transfer/include/form-currency-item.html" %}
 | 
					        {% include "accounting/journal-entry/transfer/include/form-currency.html" %}
 | 
				
			||||||
      {% endwith %}
 | 
					      {% endwith %}
 | 
				
			||||||
    {% endfor %}
 | 
					    {% endfor %}
 | 
				
			||||||
  {% else %}
 | 
					  {% else %}
 | 
				
			||||||
@@ -45,24 +45,24 @@ First written: 2023/2/25
 | 
				
			|||||||
            currency_code_data = accounting_default_currency_code(),
 | 
					            currency_code_data = accounting_default_currency_code(),
 | 
				
			||||||
            debit_total = "-",
 | 
					            debit_total = "-",
 | 
				
			||||||
            credit_total = "-" %}
 | 
					            credit_total = "-" %}
 | 
				
			||||||
      {% include "accounting/voucher/transfer/include/form-currency-item.html" %}
 | 
					      {% include "accounting/journal-entry/transfer/include/form-currency.html" %}
 | 
				
			||||||
    {% endwith %}
 | 
					    {% endwith %}
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block form_modals %}
 | 
					{% block form_modals %}
 | 
				
			||||||
  {% with description_editor = form.description_editor.debit %}
 | 
					  {% with description_editor = form.description_editor.debit %}
 | 
				
			||||||
    {% include "accounting/voucher/include/description-editor-modal.html" %}
 | 
					    {% include "accounting/journal-entry/include/description-editor-modal.html" %}
 | 
				
			||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
  {% with description_editor = form.description_editor.credit %}
 | 
					  {% with description_editor = form.description_editor.credit %}
 | 
				
			||||||
    {% include "accounting/voucher/include/description-editor-modal.html" %}
 | 
					    {% include "accounting/journal-entry/include/description-editor-modal.html" %}
 | 
				
			||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
  {% with debit_credit = "debit",
 | 
					  {% with debit_credit = "debit",
 | 
				
			||||||
          account_options = form.debit_account_options %}
 | 
					          account_options = form.debit_account_options %}
 | 
				
			||||||
    {% include "accounting/voucher/include/account-selector-modal.html" %}
 | 
					    {% include "accounting/journal-entry/include/account-selector-modal.html" %}
 | 
				
			||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
  {% with debit_credit = "credit",
 | 
					  {% with debit_credit = "credit",
 | 
				
			||||||
          account_options = form.credit_account_options %}
 | 
					          account_options = form.credit_account_options %}
 | 
				
			||||||
    {% include "accounting/voucher/include/account-selector-modal.html" %}
 | 
					    {% include "accounting/journal-entry/include/account-selector-modal.html" %}
 | 
				
			||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
@@ -37,7 +37,7 @@ First written: 2023/3/7
 | 
				
			|||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
					{% include "accounting/report/include/add-journal-entry-material-fab.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/period-chooser.html" %}
 | 
					{% include "accounting/report/include/period-chooser.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{#
 | 
					{#
 | 
				
			||||||
The Mia! Accounting Flask Project
 | 
					The Mia! Accounting Flask Project
 | 
				
			||||||
add-voucher-material-fab.html: The material floating action buttons to add a new voucher
 | 
					add-journal-entry-material-fab.html: The material floating action buttons to add a new journal entry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Copyright (c) 2023 imacat.
 | 
					 Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -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.voucher.create", voucher_type=report.voucher_types.CASH_DISBURSEMENT)|accounting_append_next }}">
 | 
					      <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_("Cash expense") }}
 | 
					        {{ A_("Cash expense") }}
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
      <a class="btn rounded-pill" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.CASH_RECEIPT)|accounting_append_next }}">
 | 
					      <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_("Cash income") }}
 | 
					        {{ A_("Cash income") }}
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
      <a class="btn rounded-pill" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.TRANSFER)|accounting_append_next }}">
 | 
					      <a class="btn rounded-pill" 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>
 | 
				
			||||||
@@ -27,17 +27,17 @@ First written: 2023/3/8
 | 
				
			|||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
    <ul class="dropdown-menu">
 | 
					    <ul class="dropdown-menu">
 | 
				
			||||||
      <li>
 | 
					      <li>
 | 
				
			||||||
        <a class="dropdown-item" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.CASH_DISBURSEMENT)|accounting_append_next }}">
 | 
					        <a class="dropdown-item" href="{{ url_for("accounting.journal-entry.create", journal_entry_type=report.journal_entry_types.CASH_DISBURSEMENT)|accounting_append_next }}">
 | 
				
			||||||
          {{ A_("Cash Expense") }}
 | 
					          {{ A_("Cash Expense") }}
 | 
				
			||||||
        </a>
 | 
					        </a>
 | 
				
			||||||
      </li>
 | 
					      </li>
 | 
				
			||||||
      <li>
 | 
					      <li>
 | 
				
			||||||
        <a class="dropdown-item" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.CASH_RECEIPT)|accounting_append_next }}">
 | 
					        <a class="dropdown-item" href="{{ url_for("accounting.journal-entry.create", journal_entry_type=report.journal_entry_types.CASH_RECEIPT)|accounting_append_next }}">
 | 
				
			||||||
          {{ A_("Cash Income") }}
 | 
					          {{ A_("Cash Income") }}
 | 
				
			||||||
        </a>
 | 
					        </a>
 | 
				
			||||||
      </li>
 | 
					      </li>
 | 
				
			||||||
      <li>
 | 
					      <li>
 | 
				
			||||||
        <a class="dropdown-item" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.TRANSFER)|accounting_append_next }}">
 | 
					        <a class="dropdown-item" href="{{ url_for("accounting.journal-entry.create", journal_entry_type=report.journal_entry_types.TRANSFER)|accounting_append_next }}">
 | 
				
			||||||
          {{ A_("Transfer") }}
 | 
					          {{ A_("Transfer") }}
 | 
				
			||||||
        </a>
 | 
					        </a>
 | 
				
			||||||
      </li>
 | 
					      </li>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,7 +38,7 @@ First written: 2023/3/5
 | 
				
			|||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
					{% include "accounting/report/include/add-journal-entry-material-fab.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/period-chooser.html" %}
 | 
					{% include "accounting/report/include/period-chooser.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,7 +37,7 @@ First written: 2023/3/7
 | 
				
			|||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
					{% include "accounting/report/include/add-journal-entry-material-fab.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/period-chooser.html" %}
 | 
					{% include "accounting/report/include/period-chooser.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,7 +36,7 @@ First written: 2023/3/4
 | 
				
			|||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
					{% include "accounting/report/include/add-journal-entry-material-fab.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/period-chooser.html" %}
 | 
					{% include "accounting/report/include/period-chooser.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -60,8 +60,8 @@ First written: 2023/3/4
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="accounting-report-table-body">
 | 
					    <div class="accounting-report-table-body">
 | 
				
			||||||
      {% for line_item in report.line_items %}
 | 
					      {% for line_item in report.line_items %}
 | 
				
			||||||
        <a class="accounting-report-table-row" href="{{ url_for("accounting.voucher.detail", voucher=line_item.voucher)|accounting_append_next }}">
 | 
					        <a class="accounting-report-table-row" href="{{ url_for("accounting.journal-entry.detail", journal_entry=line_item.journal_entry)|accounting_append_next }}">
 | 
				
			||||||
          <div>{{ line_item.voucher.date|accounting_format_date }}</div>
 | 
					          <div>{{ line_item.journal_entry.date|accounting_format_date }}</div>
 | 
				
			||||||
          <div>{{ line_item.currency.name }}</div>
 | 
					          <div>{{ line_item.currency.name }}</div>
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
            <span class="d-none d-md-inline">{{ line_item.account.code }}</span>
 | 
					            <span class="d-none d-md-inline">{{ line_item.account.code }}</span>
 | 
				
			||||||
@@ -77,11 +77,11 @@ First written: 2023/3/4
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <div class="list-group d-md-none">
 | 
					  <div class="list-group d-md-none">
 | 
				
			||||||
  {% for line_item in report.line_items %}
 | 
					  {% for line_item in report.line_items %}
 | 
				
			||||||
    <a class="list-group-item list-group-item-action" href="{{ url_for("accounting.voucher.detail", voucher=line_item.voucher)|accounting_append_next }}">
 | 
					    <a class="list-group-item list-group-item-action" href="{{ url_for("accounting.journal-entry.detail", journal_entry=line_item.journal_entry)|accounting_append_next }}">
 | 
				
			||||||
      <div class="d-flex justify-content-between">
 | 
					      <div class="d-flex justify-content-between">
 | 
				
			||||||
        <div {% if not line_item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
 | 
					        <div {% if not line_item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
 | 
				
			||||||
          <div class="text-muted small">
 | 
					          <div class="text-muted small">
 | 
				
			||||||
            {{ line_item.voucher.date|accounting_format_date }}
 | 
					            {{ line_item.journal_entry.date|accounting_format_date }}
 | 
				
			||||||
            {{ line_item.account.title|title }}
 | 
					            {{ line_item.account.title|title }}
 | 
				
			||||||
            {% if line_item.currency.code != accounting_default_currency_code() %}
 | 
					            {% if line_item.currency.code != accounting_default_currency_code() %}
 | 
				
			||||||
              <span class="badge rounded-pill bg-info">{{ line_item.currency.code }}</span>
 | 
					              <span class="badge rounded-pill bg-info">{{ line_item.currency.code }}</span>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,7 +38,7 @@ First written: 2023/3/5
 | 
				
			|||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
					{% include "accounting/report/include/add-journal-entry-material-fab.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/period-chooser.html" %}
 | 
					{% include "accounting/report/include/period-chooser.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,7 +35,7 @@ First written: 2023/3/8
 | 
				
			|||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
					{% include "accounting/report/include/add-journal-entry-material-fab.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/search-modal.html" %}
 | 
					{% include "accounting/report/include/search-modal.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -57,8 +57,8 @@ First written: 2023/3/8
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="accounting-report-table-body">
 | 
					    <div class="accounting-report-table-body">
 | 
				
			||||||
      {% for line_item in report.line_items %}
 | 
					      {% for line_item in report.line_items %}
 | 
				
			||||||
        <a class="accounting-report-table-row" href="{{ url_for("accounting.voucher.detail", voucher=line_item.voucher)|accounting_append_next }}">
 | 
					        <a class="accounting-report-table-row" href="{{ url_for("accounting.journal-entry.detail", journal_entry=line_item.journal_entry)|accounting_append_next }}">
 | 
				
			||||||
          <div>{{ line_item.voucher.date|accounting_format_date }}</div>
 | 
					          <div>{{ line_item.journal_entry.date|accounting_format_date }}</div>
 | 
				
			||||||
          <div>{{ line_item.currency.name }}</div>
 | 
					          <div>{{ line_item.currency.name }}</div>
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
            <span class="d-none d-md-inline">{{ line_item.account.code }}</span>
 | 
					            <span class="d-none d-md-inline">{{ line_item.account.code }}</span>
 | 
				
			||||||
@@ -74,11 +74,11 @@ First written: 2023/3/8
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <div class="list-group d-md-none">
 | 
					  <div class="list-group d-md-none">
 | 
				
			||||||
  {% for line_item in report.line_items %}
 | 
					  {% for line_item in report.line_items %}
 | 
				
			||||||
    <a class="list-group-item list-group-item-action" href="{{ url_for("accounting.voucher.detail", voucher=line_item.voucher)|accounting_append_next }}">
 | 
					    <a class="list-group-item list-group-item-action" href="{{ url_for("accounting.journal-entry.detail", journal_entry=line_item.journal_entry)|accounting_append_next }}">
 | 
				
			||||||
      <div class="d-flex justify-content-between">
 | 
					      <div class="d-flex justify-content-between">
 | 
				
			||||||
        <div {% if not line_item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
 | 
					        <div {% if not line_item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
 | 
				
			||||||
          <div class="text-muted small">
 | 
					          <div class="text-muted small">
 | 
				
			||||||
            {{ line_item.voucher.date|accounting_format_date }}
 | 
					            {{ line_item.journal_entry.date|accounting_format_date }}
 | 
				
			||||||
            {{ line_item.account.title|title }}
 | 
					            {{ line_item.account.title|title }}
 | 
				
			||||||
            {% if line_item.currency.code != accounting_default_currency_code() %}
 | 
					            {% if line_item.currency.code != accounting_default_currency_code() %}
 | 
				
			||||||
              <span class="badge rounded-pill bg-info">{{ line_item.currency.code }}</span>
 | 
					              <span class="badge rounded-pill bg-info">{{ line_item.currency.code }}</span>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,7 +37,7 @@ First written: 2023/3/5
 | 
				
			|||||||
  {% endwith %}
 | 
					  {% endwith %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
 | 
					{% include "accounting/report/include/add-journal-entry-material-fab.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% include "accounting/report/include/period-chooser.html" %}
 | 
					{% include "accounting/report/include/period-chooser.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,17 +14,17 @@
 | 
				
			|||||||
#  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 voucher types.
 | 
					"""The journal entry types.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
from enum import Enum
 | 
					from enum import Enum
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VoucherType(Enum):
 | 
					class JournalEntryType(Enum):
 | 
				
			||||||
    """The voucher types."""
 | 
					    """The journal entry types."""
 | 
				
			||||||
    CASH_RECEIPT: str = "receipt"
 | 
					    CASH_RECEIPT: str = "receipt"
 | 
				
			||||||
    """The cash receipt voucher."""
 | 
					    """The cash receipt journal entry."""
 | 
				
			||||||
    CASH_DISBURSEMENT: str = "disbursement"
 | 
					    CASH_DISBURSEMENT: str = "disbursement"
 | 
				
			||||||
    """The cash disbursement voucher."""
 | 
					    """The cash disbursement journal entry."""
 | 
				
			||||||
    TRANSFER: str = "transfer"
 | 
					    TRANSFER: str = "transfer"
 | 
				
			||||||
    """The transfer voucher."""
 | 
					    """The transfer journal entry."""
 | 
				
			||||||
@@ -1,105 +0,0 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#  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 path converters for the voucher management.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"""
 | 
					 | 
				
			||||||
from datetime import date
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from flask import abort
 | 
					 | 
				
			||||||
from sqlalchemy.orm import selectinload
 | 
					 | 
				
			||||||
from werkzeug.routing import BaseConverter
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from accounting.models import Voucher, JournalEntryLineItem
 | 
					 | 
				
			||||||
from accounting.utils.voucher_types import VoucherType
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class VoucherConverter(BaseConverter):
 | 
					 | 
				
			||||||
    """The voucher converter to convert the voucher ID from and to the
 | 
					 | 
				
			||||||
    corresponding voucher in the routes."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def to_python(self, value: str) -> Voucher:
 | 
					 | 
				
			||||||
        """Converts a voucher ID to a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param value: The voucher ID.
 | 
					 | 
				
			||||||
        :return: The corresponding voucher.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        voucher: Voucher | None = Voucher.query.join(JournalEntryLineItem)\
 | 
					 | 
				
			||||||
            .filter(Voucher.id == value)\
 | 
					 | 
				
			||||||
            .options(selectinload(Voucher.line_items)
 | 
					 | 
				
			||||||
                     .selectinload(JournalEntryLineItem.offsets)
 | 
					 | 
				
			||||||
                     .selectinload(JournalEntryLineItem.voucher))\
 | 
					 | 
				
			||||||
            .first()
 | 
					 | 
				
			||||||
        if voucher is None:
 | 
					 | 
				
			||||||
            abort(404)
 | 
					 | 
				
			||||||
        return voucher
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def to_url(self, value: Voucher) -> str:
 | 
					 | 
				
			||||||
        """Converts a voucher to its ID.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param value: The voucher.
 | 
					 | 
				
			||||||
        :return: The ID.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return str(value.id)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class VoucherTypeConverter(BaseConverter):
 | 
					 | 
				
			||||||
    """The voucher converter to convert the voucher type ID from and to the
 | 
					 | 
				
			||||||
    corresponding voucher type in the routes."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def to_python(self, value: str) -> VoucherType:
 | 
					 | 
				
			||||||
        """Converts a voucher ID to a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param value: The voucher ID.
 | 
					 | 
				
			||||||
        :return: The corresponding voucher type.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        type_dict: dict[str, VoucherType] = {x.value: x for x in VoucherType}
 | 
					 | 
				
			||||||
        voucher_type: VoucherType | None = type_dict.get(value)
 | 
					 | 
				
			||||||
        if voucher_type is None:
 | 
					 | 
				
			||||||
            abort(404)
 | 
					 | 
				
			||||||
        return voucher_type
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def to_url(self, value: VoucherType) -> str:
 | 
					 | 
				
			||||||
        """Converts a voucher type to its ID.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param value: The voucher type.
 | 
					 | 
				
			||||||
        :return: The ID.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return str(value.value)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class DateConverter(BaseConverter):
 | 
					 | 
				
			||||||
    """The date converter to convert the ISO date from and to the
 | 
					 | 
				
			||||||
    corresponding date in the routes."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def to_python(self, value: str) -> date:
 | 
					 | 
				
			||||||
        """Converts an ISO date to a date.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param value: The ISO date.
 | 
					 | 
				
			||||||
        :return: The corresponding date.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            return date.fromisoformat(value)
 | 
					 | 
				
			||||||
        except ValueError:
 | 
					 | 
				
			||||||
            abort(404)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def to_url(self, value: date) -> str:
 | 
					 | 
				
			||||||
        """Converts a date to its ISO date.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param value: The date.
 | 
					 | 
				
			||||||
        :return: The ISO date.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return value.isoformat()
 | 
					 | 
				
			||||||
@@ -1,92 +0,0 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#  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 reorder forms for the voucher management.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"""
 | 
					 | 
				
			||||||
from datetime import date
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import sqlalchemy as sa
 | 
					 | 
				
			||||||
from flask import request
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from accounting import db
 | 
					 | 
				
			||||||
from accounting.models import Voucher
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def sort_vouchers_in(voucher_date: date, exclude: int | None = None) -> None:
 | 
					 | 
				
			||||||
    """Sorts the vouchers under a date after changing the date or deleting
 | 
					 | 
				
			||||||
    a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :param voucher_date: The date of the voucher.
 | 
					 | 
				
			||||||
    :param exclude: The voucher ID to exclude.
 | 
					 | 
				
			||||||
    :return: None.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    conditions: list[sa.BinaryExpression] = [Voucher.date == voucher_date]
 | 
					 | 
				
			||||||
    if exclude is not None:
 | 
					 | 
				
			||||||
        conditions.append(Voucher.id != exclude)
 | 
					 | 
				
			||||||
    vouchers: list[Voucher] = Voucher.query\
 | 
					 | 
				
			||||||
        .filter(*conditions)\
 | 
					 | 
				
			||||||
        .order_by(Voucher.no).all()
 | 
					 | 
				
			||||||
    for i in range(len(vouchers)):
 | 
					 | 
				
			||||||
        if vouchers[i].no != i + 1:
 | 
					 | 
				
			||||||
            vouchers[i].no = i + 1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class VoucherReorderForm:
 | 
					 | 
				
			||||||
    """The form to reorder the vouchers."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, voucher_date: date):
 | 
					 | 
				
			||||||
        """Constructs the form to reorder the vouchers in a day.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param voucher_date: The date.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        self.date: date = voucher_date
 | 
					 | 
				
			||||||
        self.is_modified: bool = False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def save_order(self) -> None:
 | 
					 | 
				
			||||||
        """Saves the order of the account.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return:
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        vouchers: list[Voucher] = Voucher.query\
 | 
					 | 
				
			||||||
            .filter(Voucher.date == self.date).all()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Collects the specified order.
 | 
					 | 
				
			||||||
        orders: dict[Voucher, int] = {}
 | 
					 | 
				
			||||||
        for voucher in vouchers:
 | 
					 | 
				
			||||||
            if f"{voucher.id}-no" in request.form:
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    orders[voucher] = int(request.form[f"{voucher.id}-no"])
 | 
					 | 
				
			||||||
                except ValueError:
 | 
					 | 
				
			||||||
                    pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Missing and invalid orders are appended to the end.
 | 
					 | 
				
			||||||
        missing: list[Voucher] \
 | 
					 | 
				
			||||||
            = [x for x in vouchers if x not in orders]
 | 
					 | 
				
			||||||
        if len(missing) > 0:
 | 
					 | 
				
			||||||
            next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
 | 
					 | 
				
			||||||
            for voucher in missing:
 | 
					 | 
				
			||||||
                orders[voucher] = next_no
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Sort by the specified order first, and their original order.
 | 
					 | 
				
			||||||
        vouchers.sort(key=lambda x: (orders[x], x.no))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Update the orders.
 | 
					 | 
				
			||||||
        with db.session.no_autoflush:
 | 
					 | 
				
			||||||
            for i in range(len(vouchers)):
 | 
					 | 
				
			||||||
                if vouchers[i].no != i + 1:
 | 
					 | 
				
			||||||
                    vouchers[i].no = i + 1
 | 
					 | 
				
			||||||
                    self.is_modified = True
 | 
					 | 
				
			||||||
@@ -1,328 +0,0 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#  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 operators for different voucher types.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"""
 | 
					 | 
				
			||||||
import typing as t
 | 
					 | 
				
			||||||
from abc import ABC, abstractmethod
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from flask import render_template, request, abort
 | 
					 | 
				
			||||||
from flask_wtf import FlaskForm
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from accounting.models import Voucher
 | 
					 | 
				
			||||||
from accounting.template_globals import default_currency_code
 | 
					 | 
				
			||||||
from accounting.utils.voucher_types import VoucherType
 | 
					 | 
				
			||||||
from accounting.voucher.forms import VoucherForm, CashReceiptVoucherForm, \
 | 
					 | 
				
			||||||
    CashDisbursementVoucherForm, TransferVoucherForm
 | 
					 | 
				
			||||||
from accounting.voucher.forms.line_item import LineItemForm
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class VoucherOperator(ABC):
 | 
					 | 
				
			||||||
    """The base voucher operator."""
 | 
					 | 
				
			||||||
    CHECK_ORDER: int = -1
 | 
					 | 
				
			||||||
    """The order when checking the voucher operator."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    @abstractmethod
 | 
					 | 
				
			||||||
    def form(self) -> t.Type[VoucherForm]:
 | 
					 | 
				
			||||||
        """Returns the form class.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: The form class.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @abstractmethod
 | 
					 | 
				
			||||||
    def render_create_template(self, form: FlaskForm) -> str:
 | 
					 | 
				
			||||||
        """Renders the template for the form to create a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param form: The voucher form.
 | 
					 | 
				
			||||||
        :return: the form to create a voucher.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @abstractmethod
 | 
					 | 
				
			||||||
    def render_detail_template(self, voucher: Voucher) -> str:
 | 
					 | 
				
			||||||
        """Renders the template for the detail page.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param voucher: The voucher.
 | 
					 | 
				
			||||||
        :return: the detail page.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @abstractmethod
 | 
					 | 
				
			||||||
    def render_edit_template(self, voucher: Voucher, form: FlaskForm) -> str:
 | 
					 | 
				
			||||||
        """Renders the template for the form to edit a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param voucher: The voucher.
 | 
					 | 
				
			||||||
        :param form: The form.
 | 
					 | 
				
			||||||
        :return: the form to edit a voucher.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @abstractmethod
 | 
					 | 
				
			||||||
    def is_my_type(self, voucher: Voucher) -> bool:
 | 
					 | 
				
			||||||
        """Checks and returns whether the voucher belongs to the type.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param voucher: The voucher.
 | 
					 | 
				
			||||||
        :return: True if the voucher belongs to the type, or False
 | 
					 | 
				
			||||||
            otherwise.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def _line_item_template(self) -> str:
 | 
					 | 
				
			||||||
        """Renders and returns the template for the line item sub-form.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: The template for the line item sub-form.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render_template(
 | 
					 | 
				
			||||||
            "accounting/voucher/include/form-line-item.html",
 | 
					 | 
				
			||||||
            currency_index="CURRENCY_INDEX",
 | 
					 | 
				
			||||||
            debit_credit="DEBIT_CREDIT",
 | 
					 | 
				
			||||||
            line_item_index="LINE_ITEM_INDEX",
 | 
					 | 
				
			||||||
            form=LineItemForm())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class CashReceiptVoucher(VoucherOperator):
 | 
					 | 
				
			||||||
    """A cash receipt voucher."""
 | 
					 | 
				
			||||||
    CHECK_ORDER: int = 2
 | 
					 | 
				
			||||||
    """The order when checking the voucher operator."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def form(self) -> t.Type[VoucherForm]:
 | 
					 | 
				
			||||||
        """Returns the form class.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: The form class.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return CashReceiptVoucherForm
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def render_create_template(self, form: CashReceiptVoucherForm) -> str:
 | 
					 | 
				
			||||||
        """Renders the template for the form to create a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param form: The voucher form.
 | 
					 | 
				
			||||||
        :return: the form to create a voucher.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render_template("accounting/voucher/receipt/create.html",
 | 
					 | 
				
			||||||
                               form=form,
 | 
					 | 
				
			||||||
                               voucher_type=VoucherType.CASH_RECEIPT,
 | 
					 | 
				
			||||||
                               currency_template=self.__currency_template,
 | 
					 | 
				
			||||||
                               line_item_template=self._line_item_template)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def render_detail_template(self, voucher: Voucher) -> str:
 | 
					 | 
				
			||||||
        """Renders the template for the detail page.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param voucher: The voucher.
 | 
					 | 
				
			||||||
        :return: the detail page.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render_template("accounting/voucher/receipt/detail.html",
 | 
					 | 
				
			||||||
                               obj=voucher)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def render_edit_template(self, voucher: Voucher,
 | 
					 | 
				
			||||||
                             form: CashReceiptVoucherForm) -> str:
 | 
					 | 
				
			||||||
        """Renders the template for the form to edit a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param voucher: The voucher.
 | 
					 | 
				
			||||||
        :param form: The form.
 | 
					 | 
				
			||||||
        :return: the form to edit a voucher.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render_template("accounting/voucher/receipt/edit.html",
 | 
					 | 
				
			||||||
                               voucher=voucher, form=form,
 | 
					 | 
				
			||||||
                               currency_template=self.__currency_template,
 | 
					 | 
				
			||||||
                               line_item_template=self._line_item_template)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def is_my_type(self, voucher: Voucher) -> bool:
 | 
					 | 
				
			||||||
        """Checks and returns whether the voucher belongs to the type.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param voucher: The voucher.
 | 
					 | 
				
			||||||
        :return: True if the voucher belongs to the type, or False
 | 
					 | 
				
			||||||
            otherwise.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return voucher.is_cash_receipt
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def __currency_template(self) -> str:
 | 
					 | 
				
			||||||
        """Renders and returns the template for the currency sub-form.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: The template for the currency sub-form.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render_template(
 | 
					 | 
				
			||||||
            "accounting/voucher/receipt/include/form-currency-item.html",
 | 
					 | 
				
			||||||
            currency_index="CURRENCY_INDEX",
 | 
					 | 
				
			||||||
            currency_code_data=default_currency_code(),
 | 
					 | 
				
			||||||
            credit_total="-")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class CashDisbursementVoucher(VoucherOperator):
 | 
					 | 
				
			||||||
    """A cash disbursement voucher."""
 | 
					 | 
				
			||||||
    CHECK_ORDER: int = 1
 | 
					 | 
				
			||||||
    """The order when checking the voucher operator."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def form(self) -> t.Type[VoucherForm]:
 | 
					 | 
				
			||||||
        """Returns the form class.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: The form class.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return CashDisbursementVoucherForm
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def render_create_template(self, form: CashDisbursementVoucherForm) -> str:
 | 
					 | 
				
			||||||
        """Renders the template for the form to create a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param form: The voucher form.
 | 
					 | 
				
			||||||
        :return: the form to create a voucher.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render_template("accounting/voucher/disbursement/create.html",
 | 
					 | 
				
			||||||
                               form=form,
 | 
					 | 
				
			||||||
                               voucher_type=VoucherType.CASH_DISBURSEMENT,
 | 
					 | 
				
			||||||
                               currency_template=self.__currency_template,
 | 
					 | 
				
			||||||
                               line_item_template=self._line_item_template)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def render_detail_template(self, voucher: Voucher) -> str:
 | 
					 | 
				
			||||||
        """Renders the template for the detail page.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param voucher: The voucher.
 | 
					 | 
				
			||||||
        :return: the detail page.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render_template("accounting/voucher/disbursement/detail.html",
 | 
					 | 
				
			||||||
                               obj=voucher)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def render_edit_template(self, voucher: Voucher,
 | 
					 | 
				
			||||||
                             form: CashDisbursementVoucherForm) -> str:
 | 
					 | 
				
			||||||
        """Renders the template for the form to edit a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param voucher: The voucher.
 | 
					 | 
				
			||||||
        :param form: The form.
 | 
					 | 
				
			||||||
        :return: the form to edit a voucher.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render_template("accounting/voucher/disbursement/edit.html",
 | 
					 | 
				
			||||||
                               voucher=voucher, form=form,
 | 
					 | 
				
			||||||
                               currency_template=self.__currency_template,
 | 
					 | 
				
			||||||
                               line_item_template=self._line_item_template)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def is_my_type(self, voucher: Voucher) -> bool:
 | 
					 | 
				
			||||||
        """Checks and returns whether the voucher belongs to the type.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param voucher: The voucher.
 | 
					 | 
				
			||||||
        :return: True if the voucher belongs to the type, or False
 | 
					 | 
				
			||||||
            otherwise.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return voucher.is_cash_disbursement
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def __currency_template(self) -> str:
 | 
					 | 
				
			||||||
        """Renders and returns the template for the currency sub-form.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: The template for the currency sub-form.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render_template(
 | 
					 | 
				
			||||||
            "accounting/voucher/disbursement/include/form-currency-item.html",
 | 
					 | 
				
			||||||
            currency_index="CURRENCY_INDEX",
 | 
					 | 
				
			||||||
            currency_code_data=default_currency_code(),
 | 
					 | 
				
			||||||
            debit_total="-")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class TransferVoucher(VoucherOperator):
 | 
					 | 
				
			||||||
    """A transfer voucher."""
 | 
					 | 
				
			||||||
    CHECK_ORDER: int = 3
 | 
					 | 
				
			||||||
    """The order when checking the voucher operator."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def form(self) -> t.Type[VoucherForm]:
 | 
					 | 
				
			||||||
        """Returns the form class.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: The form class.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return TransferVoucherForm
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def render_create_template(self, form: TransferVoucherForm) -> str:
 | 
					 | 
				
			||||||
        """Renders the template for the form to create a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param form: The voucher form.
 | 
					 | 
				
			||||||
        :return: the form to create a voucher.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render_template("accounting/voucher/transfer/create.html",
 | 
					 | 
				
			||||||
                               form=form,
 | 
					 | 
				
			||||||
                               voucher_type=VoucherType.TRANSFER,
 | 
					 | 
				
			||||||
                               currency_template=self.__currency_template,
 | 
					 | 
				
			||||||
                               line_item_template=self._line_item_template)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def render_detail_template(self, voucher: Voucher) -> str:
 | 
					 | 
				
			||||||
        """Renders the template for the detail page.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param voucher: The voucher.
 | 
					 | 
				
			||||||
        :return: the detail page.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render_template("accounting/voucher/transfer/detail.html",
 | 
					 | 
				
			||||||
                               obj=voucher)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def render_edit_template(self, voucher: Voucher,
 | 
					 | 
				
			||||||
                             form: TransferVoucherForm) -> str:
 | 
					 | 
				
			||||||
        """Renders the template for the form to edit a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param voucher: The voucher.
 | 
					 | 
				
			||||||
        :param form: The form.
 | 
					 | 
				
			||||||
        :return: the form to edit a voucher.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render_template("accounting/voucher/transfer/edit.html",
 | 
					 | 
				
			||||||
                               voucher=voucher, form=form,
 | 
					 | 
				
			||||||
                               currency_template=self.__currency_template,
 | 
					 | 
				
			||||||
                               line_item_template=self._line_item_template)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def is_my_type(self, voucher: Voucher) -> bool:
 | 
					 | 
				
			||||||
        """Checks and returns whether the voucher belongs to the type.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :param voucher: The voucher.
 | 
					 | 
				
			||||||
        :return: True if the voucher belongs to the type, or False
 | 
					 | 
				
			||||||
            otherwise.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return True
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def __currency_template(self) -> str:
 | 
					 | 
				
			||||||
        """Renders and returns the template for the currency sub-form.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: The template for the currency sub-form.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return render_template(
 | 
					 | 
				
			||||||
            "accounting/voucher/transfer/include/form-currency-item.html",
 | 
					 | 
				
			||||||
            currency_index="CURRENCY_INDEX",
 | 
					 | 
				
			||||||
            currency_code_data=default_currency_code(),
 | 
					 | 
				
			||||||
            debit_total="-", credit_total="-")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
VOUCHER_TYPE_TO_OP: dict[VoucherType, VoucherOperator] \
 | 
					 | 
				
			||||||
    = {VoucherType.CASH_RECEIPT: CashReceiptVoucher(),
 | 
					 | 
				
			||||||
       VoucherType.CASH_DISBURSEMENT: CashDisbursementVoucher(),
 | 
					 | 
				
			||||||
       VoucherType.TRANSFER: TransferVoucher()}
 | 
					 | 
				
			||||||
"""The map from the voucher types to their operators."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def get_voucher_op(voucher: Voucher, is_check_as: bool = False) \
 | 
					 | 
				
			||||||
        -> VoucherOperator:
 | 
					 | 
				
			||||||
    """Returns the voucher operator that may be specified in the "as" query
 | 
					 | 
				
			||||||
    parameter.  If it is not specified, check the voucher type from the
 | 
					 | 
				
			||||||
    voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :param voucher: The voucher.
 | 
					 | 
				
			||||||
    :param is_check_as: True to check the "as" parameter, or False otherwise.
 | 
					 | 
				
			||||||
    :return: None.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    if is_check_as and "as" in request.args:
 | 
					 | 
				
			||||||
        type_dict: dict[str, VoucherType] \
 | 
					 | 
				
			||||||
            = {x.value: x for x in VoucherType}
 | 
					 | 
				
			||||||
        if request.args["as"] not in type_dict:
 | 
					 | 
				
			||||||
            abort(404)
 | 
					 | 
				
			||||||
        return VOUCHER_TYPE_TO_OP[type_dict[request.args["as"]]]
 | 
					 | 
				
			||||||
    for voucher_type in sorted(VOUCHER_TYPE_TO_OP.values(),
 | 
					 | 
				
			||||||
                               key=lambda x: x.CHECK_ORDER):
 | 
					 | 
				
			||||||
        if voucher_type.is_my_type(voucher):
 | 
					 | 
				
			||||||
            return voucher_type
 | 
					 | 
				
			||||||
@@ -1,221 +0,0 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#  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 voucher management.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"""
 | 
					 | 
				
			||||||
from datetime import date
 | 
					 | 
				
			||||||
from urllib.parse import parse_qsl, urlencode
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import sqlalchemy as sa
 | 
					 | 
				
			||||||
from flask import Blueprint, render_template, session, redirect, request, \
 | 
					 | 
				
			||||||
    flash, url_for
 | 
					 | 
				
			||||||
from werkzeug.datastructures import ImmutableMultiDict
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from accounting import db
 | 
					 | 
				
			||||||
from accounting.locale import lazy_gettext
 | 
					 | 
				
			||||||
from accounting.models import Voucher
 | 
					 | 
				
			||||||
from accounting.utils.cast import s
 | 
					 | 
				
			||||||
from accounting.utils.flash_errors import flash_form_errors
 | 
					 | 
				
			||||||
from accounting.utils.next_uri import inherit_next, or_next
 | 
					 | 
				
			||||||
from accounting.utils.permission import has_permission, can_view, can_edit
 | 
					 | 
				
			||||||
from accounting.utils.voucher_types import VoucherType
 | 
					 | 
				
			||||||
from accounting.utils.user import get_current_user_pk
 | 
					 | 
				
			||||||
from .forms import sort_vouchers_in, VoucherReorderForm
 | 
					 | 
				
			||||||
from .template_filters import with_type, to_transfer, format_amount_input, \
 | 
					 | 
				
			||||||
    text2html
 | 
					 | 
				
			||||||
from .utils.operators import VoucherOperator, VOUCHER_TYPE_TO_OP, \
 | 
					 | 
				
			||||||
    get_voucher_op
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
bp: Blueprint = Blueprint("voucher", __name__)
 | 
					 | 
				
			||||||
"""The view blueprint for the voucher management."""
 | 
					 | 
				
			||||||
bp.add_app_template_filter(with_type, "accounting_voucher_with_type")
 | 
					 | 
				
			||||||
bp.add_app_template_filter(to_transfer, "accounting_voucher_to_transfer")
 | 
					 | 
				
			||||||
bp.add_app_template_filter(format_amount_input,
 | 
					 | 
				
			||||||
                           "accounting_voucher_format_amount_input")
 | 
					 | 
				
			||||||
bp.add_app_template_filter(text2html, "accounting_voucher_text2html")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.get("/create/<voucherType:voucher_type>", endpoint="create")
 | 
					 | 
				
			||||||
@has_permission(can_edit)
 | 
					 | 
				
			||||||
def show_add_voucher_form(voucher_type: VoucherType) -> str:
 | 
					 | 
				
			||||||
    """Shows the form to add a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :param voucher_type: The voucher type.
 | 
					 | 
				
			||||||
    :return: The form to add a voucher.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    voucher_op: VoucherOperator = VOUCHER_TYPE_TO_OP[voucher_type]
 | 
					 | 
				
			||||||
    form: voucher_op.form
 | 
					 | 
				
			||||||
    if "form" in session:
 | 
					 | 
				
			||||||
        form = voucher_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
 | 
					 | 
				
			||||||
        del session["form"]
 | 
					 | 
				
			||||||
        form.validate()
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        form = voucher_op.form()
 | 
					 | 
				
			||||||
        form.date.data = date.today()
 | 
					 | 
				
			||||||
    return voucher_op.render_create_template(form)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.post("/store/<voucherType:voucher_type>", endpoint="store")
 | 
					 | 
				
			||||||
@has_permission(can_edit)
 | 
					 | 
				
			||||||
def add_voucher(voucher_type: VoucherType) -> redirect:
 | 
					 | 
				
			||||||
    """Adds a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :param voucher_type: The voucher type.
 | 
					 | 
				
			||||||
    :return: The redirection to the voucher detail on success, or the
 | 
					 | 
				
			||||||
        voucher creation form on error.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    voucher_op: VoucherOperator = VOUCHER_TYPE_TO_OP[voucher_type]
 | 
					 | 
				
			||||||
    form: voucher_op.form = voucher_op.form(request.form)
 | 
					 | 
				
			||||||
    if not form.validate():
 | 
					 | 
				
			||||||
        flash_form_errors(form)
 | 
					 | 
				
			||||||
        session["form"] = urlencode(list(request.form.items()))
 | 
					 | 
				
			||||||
        return redirect(inherit_next(with_type(
 | 
					 | 
				
			||||||
            url_for("accounting.voucher.create", voucher_type=voucher_type))))
 | 
					 | 
				
			||||||
    voucher: Voucher = Voucher()
 | 
					 | 
				
			||||||
    form.populate_obj(voucher)
 | 
					 | 
				
			||||||
    db.session.add(voucher)
 | 
					 | 
				
			||||||
    db.session.commit()
 | 
					 | 
				
			||||||
    flash(s(lazy_gettext("The voucher is added successfully")), "success")
 | 
					 | 
				
			||||||
    return redirect(inherit_next(__get_detail_uri(voucher)))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.get("/<voucher:voucher>", endpoint="detail")
 | 
					 | 
				
			||||||
@has_permission(can_view)
 | 
					 | 
				
			||||||
def show_voucher_detail(voucher: Voucher) -> str:
 | 
					 | 
				
			||||||
    """Shows the voucher detail.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :param voucher: The voucher.
 | 
					 | 
				
			||||||
    :return: The detail.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    voucher_op: VoucherOperator = get_voucher_op(voucher)
 | 
					 | 
				
			||||||
    return voucher_op.render_detail_template(voucher)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.get("/<voucher:voucher>/edit", endpoint="edit")
 | 
					 | 
				
			||||||
@has_permission(can_edit)
 | 
					 | 
				
			||||||
def show_voucher_edit_form(voucher: Voucher) -> str:
 | 
					 | 
				
			||||||
    """Shows the form to edit a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :param voucher: The voucher.
 | 
					 | 
				
			||||||
    :return: The form to edit the voucher.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    voucher_op: VoucherOperator = get_voucher_op(voucher, is_check_as=True)
 | 
					 | 
				
			||||||
    form: voucher_op.form
 | 
					 | 
				
			||||||
    if "form" in session:
 | 
					 | 
				
			||||||
        form = voucher_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
 | 
					 | 
				
			||||||
        del session["form"]
 | 
					 | 
				
			||||||
        form.obj = voucher
 | 
					 | 
				
			||||||
        form.validate()
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        form = voucher_op.form(obj=voucher)
 | 
					 | 
				
			||||||
    return voucher_op.render_edit_template(voucher, form)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.post("/<voucher:voucher>/update", endpoint="update")
 | 
					 | 
				
			||||||
@has_permission(can_edit)
 | 
					 | 
				
			||||||
def update_voucher(voucher: Voucher) -> redirect:
 | 
					 | 
				
			||||||
    """Updates a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :param voucher: The voucher.
 | 
					 | 
				
			||||||
    :return: The redirection to the voucher detail on success, or the voucher
 | 
					 | 
				
			||||||
        edit form on error.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    voucher_op: VoucherOperator = get_voucher_op(voucher, is_check_as=True)
 | 
					 | 
				
			||||||
    form: voucher_op.form = voucher_op.form(request.form)
 | 
					 | 
				
			||||||
    form.obj = voucher
 | 
					 | 
				
			||||||
    if not form.validate():
 | 
					 | 
				
			||||||
        flash_form_errors(form)
 | 
					 | 
				
			||||||
        session["form"] = urlencode(list(request.form.items()))
 | 
					 | 
				
			||||||
        return redirect(inherit_next(with_type(
 | 
					 | 
				
			||||||
            url_for("accounting.voucher.edit", voucher=voucher))))
 | 
					 | 
				
			||||||
    with db.session.no_autoflush:
 | 
					 | 
				
			||||||
        form.populate_obj(voucher)
 | 
					 | 
				
			||||||
    if not form.is_modified:
 | 
					 | 
				
			||||||
        flash(s(lazy_gettext("The voucher was not modified.")), "success")
 | 
					 | 
				
			||||||
        return redirect(inherit_next(__get_detail_uri(voucher)))
 | 
					 | 
				
			||||||
    voucher.updated_by_id = get_current_user_pk()
 | 
					 | 
				
			||||||
    voucher.updated_at = sa.func.now()
 | 
					 | 
				
			||||||
    db.session.commit()
 | 
					 | 
				
			||||||
    flash(s(lazy_gettext("The voucher is updated successfully.")), "success")
 | 
					 | 
				
			||||||
    return redirect(inherit_next(__get_detail_uri(voucher)))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.post("/<voucher:voucher>/delete", endpoint="delete")
 | 
					 | 
				
			||||||
@has_permission(can_edit)
 | 
					 | 
				
			||||||
def delete_voucher(voucher: Voucher) -> redirect:
 | 
					 | 
				
			||||||
    """Deletes a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :param voucher: The voucher.
 | 
					 | 
				
			||||||
    :return: The redirection to the voucher list on success, or the voucher
 | 
					 | 
				
			||||||
        detail on error.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    voucher.delete()
 | 
					 | 
				
			||||||
    sort_vouchers_in(voucher.date, voucher.id)
 | 
					 | 
				
			||||||
    db.session.commit()
 | 
					 | 
				
			||||||
    flash(s(lazy_gettext("The voucher is deleted successfully.")), "success")
 | 
					 | 
				
			||||||
    return redirect(or_next(__get_default_page_uri()))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.get("/dates/<date:voucher_date>", endpoint="order")
 | 
					 | 
				
			||||||
@has_permission(can_view)
 | 
					 | 
				
			||||||
def show_voucher_order(voucher_date: date) -> str:
 | 
					 | 
				
			||||||
    """Shows the order of the vouchers in a same date.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :param voucher_date: The date.
 | 
					 | 
				
			||||||
    :return: The order of the vouchers in the date.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    vouchers: list[Voucher] = Voucher.query \
 | 
					 | 
				
			||||||
        .filter(Voucher.date == voucher_date) \
 | 
					 | 
				
			||||||
        .order_by(Voucher.no).all()
 | 
					 | 
				
			||||||
    return render_template("accounting/voucher/order.html",
 | 
					 | 
				
			||||||
                           date=voucher_date, list=vouchers)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.post("/dates/<date:voucher_date>", endpoint="sort")
 | 
					 | 
				
			||||||
@has_permission(can_edit)
 | 
					 | 
				
			||||||
def sort_vouchers(voucher_date: date) -> redirect:
 | 
					 | 
				
			||||||
    """Reorders the vouchers in a date.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :param voucher_date: The date.
 | 
					 | 
				
			||||||
    :return: The redirection to the incoming account or the account list.  The
 | 
					 | 
				
			||||||
        reordering operation does not fail.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    form: VoucherReorderForm = VoucherReorderForm(voucher_date)
 | 
					 | 
				
			||||||
    form.save_order()
 | 
					 | 
				
			||||||
    if not form.is_modified:
 | 
					 | 
				
			||||||
        flash(s(lazy_gettext("The order was not modified.")), "success")
 | 
					 | 
				
			||||||
        return redirect(or_next(__get_default_page_uri()))
 | 
					 | 
				
			||||||
    db.session.commit()
 | 
					 | 
				
			||||||
    flash(s(lazy_gettext("The order is updated successfully.")), "success")
 | 
					 | 
				
			||||||
    return redirect(or_next(__get_default_page_uri()))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def __get_detail_uri(voucher: Voucher) -> str:
 | 
					 | 
				
			||||||
    """Returns the detail URI of a voucher.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :param voucher: The voucher.
 | 
					 | 
				
			||||||
    :return: The detail URI of the voucher.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    return url_for("accounting.voucher.detail", voucher=voucher)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def __get_default_page_uri() -> str:
 | 
					 | 
				
			||||||
    """Returns the URI for the default page.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :return: The URI for the default page.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    return url_for("accounting.report.default")
 | 
					 | 
				
			||||||
@@ -25,7 +25,7 @@ from flask import Flask
 | 
				
			|||||||
from flask.testing import FlaskCliRunner
 | 
					from flask.testing import FlaskCliRunner
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from testlib import create_test_app, get_client
 | 
					from testlib import create_test_app, get_client
 | 
				
			||||||
from testlib_voucher import Accounts, NEXT_URI, add_voucher
 | 
					from testlib_journal_entry import Accounts, NEXT_URI, add_journal_entry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DescriptionEditorTestCase(unittest.TestCase):
 | 
					class DescriptionEditorTestCase(unittest.TestCase):
 | 
				
			||||||
@@ -41,7 +41,7 @@ class DescriptionEditorTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        runner: FlaskCliRunner = self.app.test_cli_runner()
 | 
					        runner: FlaskCliRunner = self.app.test_cli_runner()
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            from accounting.models import BaseAccount, Voucher, \
 | 
					            from accounting.models import BaseAccount, JournalEntry, \
 | 
				
			||||||
                JournalEntryLineItem
 | 
					                JournalEntryLineItem
 | 
				
			||||||
            result: Result
 | 
					            result: Result
 | 
				
			||||||
            result = runner.invoke(args="init-db")
 | 
					            result = runner.invoke(args="init-db")
 | 
				
			||||||
@@ -55,7 +55,7 @@ class DescriptionEditorTestCase(unittest.TestCase):
 | 
				
			|||||||
            result = runner.invoke(args=["accounting-init-accounts",
 | 
					            result = runner.invoke(args=["accounting-init-accounts",
 | 
				
			||||||
                                         "-u", "editor"])
 | 
					                                         "-u", "editor"])
 | 
				
			||||||
            self.assertEqual(result.exit_code, 0)
 | 
					            self.assertEqual(result.exit_code, 0)
 | 
				
			||||||
            Voucher.query.delete()
 | 
					            JournalEntry.query.delete()
 | 
				
			||||||
            JournalEntryLineItem.query.delete()
 | 
					            JournalEntryLineItem.query.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.client, self.csrf_token = get_client(self.app, "editor")
 | 
					        self.client, self.csrf_token = get_client(self.app, "editor")
 | 
				
			||||||
@@ -65,10 +65,10 @@ class DescriptionEditorTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.voucher.utils.description_editor import \
 | 
					        from accounting.journal_entry.utils.description_editor import \
 | 
				
			||||||
            DescriptionEditor
 | 
					            DescriptionEditor
 | 
				
			||||||
        for form in get_form_data(self.csrf_token):
 | 
					        for form in get_form_data(self.csrf_token):
 | 
				
			||||||
            add_voucher(self.client, form)
 | 
					            add_journal_entry(self.client, form)
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            editor: DescriptionEditor = DescriptionEditor()
 | 
					            editor: DescriptionEditor = DescriptionEditor()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -160,22 +160,22 @@ class DescriptionEditorTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_form_data(csrf_token: str) -> list[dict[str, str]]:
 | 
					def get_form_data(csrf_token: str) -> list[dict[str, str]]:
 | 
				
			||||||
    """Returns the form data for multiple voucher forms.
 | 
					    """Returns the form data for multiple journal entry forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param csrf_token: The CSRF token.
 | 
					    :param csrf_token: The CSRF token.
 | 
				
			||||||
    :return: A list of the form data.
 | 
					    :return: A list of the form data.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    voucher_date: str = date.today().isoformat()
 | 
					    journal_entry_date: str = date.today().isoformat()
 | 
				
			||||||
    return [{"csrf_token": csrf_token,
 | 
					    return [{"csrf_token": csrf_token,
 | 
				
			||||||
             "next": NEXT_URI,
 | 
					             "next": NEXT_URI,
 | 
				
			||||||
             "date": voucher_date,
 | 
					             "date": journal_entry_date,
 | 
				
			||||||
             "currency-0-code": "USD",
 | 
					             "currency-0-code": "USD",
 | 
				
			||||||
             "currency-0-credit-0-account_code": Accounts.SERVICE,
 | 
					             "currency-0-credit-0-account_code": Accounts.SERVICE,
 | 
				
			||||||
             "currency-0-credit-0-description": " Salary ",
 | 
					             "currency-0-credit-0-description": " Salary ",
 | 
				
			||||||
             "currency-0-credit-0-amount": "2500"},
 | 
					             "currency-0-credit-0-amount": "2500"},
 | 
				
			||||||
            {"csrf_token": csrf_token,
 | 
					            {"csrf_token": csrf_token,
 | 
				
			||||||
             "next": NEXT_URI,
 | 
					             "next": NEXT_URI,
 | 
				
			||||||
             "date": voucher_date,
 | 
					             "date": journal_entry_date,
 | 
				
			||||||
             "currency-0-code": "USD",
 | 
					             "currency-0-code": "USD",
 | 
				
			||||||
             "currency-0-debit-0-account_code": Accounts.MEAL,
 | 
					             "currency-0-debit-0-account_code": Accounts.MEAL,
 | 
				
			||||||
             "currency-0-debit-0-description": " Lunch—Fish ",
 | 
					             "currency-0-debit-0-description": " Lunch—Fish ",
 | 
				
			||||||
@@ -197,7 +197,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
 | 
				
			|||||||
             "currency-0-credit-2-amount": "4.25"},
 | 
					             "currency-0-credit-2-amount": "4.25"},
 | 
				
			||||||
            {"csrf_token": csrf_token,
 | 
					            {"csrf_token": csrf_token,
 | 
				
			||||||
             "next": NEXT_URI,
 | 
					             "next": NEXT_URI,
 | 
				
			||||||
             "date": voucher_date,
 | 
					             "date": journal_entry_date,
 | 
				
			||||||
             "currency-0-code": "USD",
 | 
					             "currency-0-code": "USD",
 | 
				
			||||||
             "currency-0-debit-0-account_code": Accounts.MEAL,
 | 
					             "currency-0-debit-0-account_code": Accounts.MEAL,
 | 
				
			||||||
             "currency-0-debit-0-description": " Lunch—Salad ",
 | 
					             "currency-0-debit-0-description": " Lunch—Salad ",
 | 
				
			||||||
@@ -213,7 +213,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
 | 
				
			|||||||
             "currency-0-credit-1-amount": "8.28"},
 | 
					             "currency-0-credit-1-amount": "8.28"},
 | 
				
			||||||
            {"csrf_token": csrf_token,
 | 
					            {"csrf_token": csrf_token,
 | 
				
			||||||
             "next": NEXT_URI,
 | 
					             "next": NEXT_URI,
 | 
				
			||||||
             "date": voucher_date,
 | 
					             "date": journal_entry_date,
 | 
				
			||||||
             "currency-0-code": "USD",
 | 
					             "currency-0-code": "USD",
 | 
				
			||||||
             "currency-0-debit-0-account_code": Accounts.MEAL,
 | 
					             "currency-0-debit-0-account_code": Accounts.MEAL,
 | 
				
			||||||
             "currency-0-debit-0-description": " Lunch—Pizza  ",
 | 
					             "currency-0-debit-0-description": " Lunch—Pizza  ",
 | 
				
			||||||
@@ -229,14 +229,14 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
 | 
				
			|||||||
             "currency-0-credit-1-amount": "7.47"},
 | 
					             "currency-0-credit-1-amount": "7.47"},
 | 
				
			||||||
            {"csrf_token": csrf_token,
 | 
					            {"csrf_token": csrf_token,
 | 
				
			||||||
             "next": NEXT_URI,
 | 
					             "next": NEXT_URI,
 | 
				
			||||||
             "date": voucher_date,
 | 
					             "date": journal_entry_date,
 | 
				
			||||||
             "currency-0-code": "USD",
 | 
					             "currency-0-code": "USD",
 | 
				
			||||||
             "currency-0-debit-0-account_code": Accounts.TRAVEL,
 | 
					             "currency-0-debit-0-account_code": Accounts.TRAVEL,
 | 
				
			||||||
             "currency-0-debit-0-description": " Airplane—Lake City↔Hill Town",
 | 
					             "currency-0-debit-0-description": " Airplane—Lake City↔Hill Town",
 | 
				
			||||||
             "currency-0-debit-0-amount": "800"},
 | 
					             "currency-0-debit-0-amount": "800"},
 | 
				
			||||||
            {"csrf_token": csrf_token,
 | 
					            {"csrf_token": csrf_token,
 | 
				
			||||||
             "next": NEXT_URI,
 | 
					             "next": NEXT_URI,
 | 
				
			||||||
             "date": voucher_date,
 | 
					             "date": journal_entry_date,
 | 
				
			||||||
             "currency-0-code": "USD",
 | 
					             "currency-0-code": "USD",
 | 
				
			||||||
             "currency-0-debit-0-account_code": Accounts.TRAVEL,
 | 
					             "currency-0-debit-0-account_code": Accounts.TRAVEL,
 | 
				
			||||||
             "currency-0-debit-0-description": " Bus—323—Downtown→Museum ",
 | 
					             "currency-0-debit-0-description": " Bus—323—Downtown→Museum ",
 | 
				
			||||||
@@ -264,7 +264,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
 | 
				
			|||||||
             "currency-0-credit-3-amount": "4.4"},
 | 
					             "currency-0-credit-3-amount": "4.4"},
 | 
				
			||||||
            {"csrf_token": csrf_token,
 | 
					            {"csrf_token": csrf_token,
 | 
				
			||||||
             "next": NEXT_URI,
 | 
					             "next": NEXT_URI,
 | 
				
			||||||
             "date": voucher_date,
 | 
					             "date": journal_entry_date,
 | 
				
			||||||
             "currency-0-code": "USD",
 | 
					             "currency-0-code": "USD",
 | 
				
			||||||
             "currency-0-debit-0-account_code": Accounts.TRAVEL,
 | 
					             "currency-0-debit-0-account_code": Accounts.TRAVEL,
 | 
				
			||||||
             "currency-0-debit-0-description": " Taxi—Museum→Office ",
 | 
					             "currency-0-debit-0-description": " Taxi—Museum→Office ",
 | 
				
			||||||
@@ -310,7 +310,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
 | 
				
			|||||||
             "currency-0-credit-6-amount": "5.5"},
 | 
					             "currency-0-credit-6-amount": "5.5"},
 | 
				
			||||||
            {"csrf_token": csrf_token,
 | 
					            {"csrf_token": csrf_token,
 | 
				
			||||||
             "next": NEXT_URI,
 | 
					             "next": NEXT_URI,
 | 
				
			||||||
             "date": voucher_date,
 | 
					             "date": journal_entry_date,
 | 
				
			||||||
             "currency-0-code": "USD",
 | 
					             "currency-0-code": "USD",
 | 
				
			||||||
             "currency-0-debit-0-account_code": Accounts.PETTY_CASH,
 | 
					             "currency-0-debit-0-account_code": Accounts.PETTY_CASH,
 | 
				
			||||||
             "currency-0-debit-0-description": " Dinner—Steak  ",
 | 
					             "currency-0-debit-0-description": " Dinner—Steak  ",
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -27,12 +27,12 @@ from flask.testing import FlaskCliRunner
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from test_site import db
 | 
					from test_site import db
 | 
				
			||||||
from testlib import create_test_app, get_client
 | 
					from testlib import create_test_app, get_client
 | 
				
			||||||
from testlib_offset import TestData, JournalEntryLineItemData, VoucherData, \
 | 
					from testlib_journal_entry import Accounts, match_journal_entry_detail
 | 
				
			||||||
    CurrencyData
 | 
					from testlib_offset import TestData, JournalEntryLineItemData, \
 | 
				
			||||||
from testlib_voucher import Accounts, match_voucher_detail
 | 
					    JournalEntryData, CurrencyData
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PREFIX: str = "/accounting/vouchers"
 | 
					PREFIX: str = "/accounting/journal-entries"
 | 
				
			||||||
"""The URL prefix for the voucher management."""
 | 
					"""The URL prefix for the journal entry management."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OffsetTestCase(unittest.TestCase):
 | 
					class OffsetTestCase(unittest.TestCase):
 | 
				
			||||||
@@ -48,7 +48,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        runner: FlaskCliRunner = self.app.test_cli_runner()
 | 
					        runner: FlaskCliRunner = self.app.test_cli_runner()
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            from accounting.models import BaseAccount, Voucher, \
 | 
					            from accounting.models import BaseAccount, JournalEntry, \
 | 
				
			||||||
                JournalEntryLineItem
 | 
					                JournalEntryLineItem
 | 
				
			||||||
            result: Result
 | 
					            result: Result
 | 
				
			||||||
            result = runner.invoke(args="init-db")
 | 
					            result = runner.invoke(args="init-db")
 | 
				
			||||||
@@ -62,7 +62,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
            result = runner.invoke(args=["accounting-init-accounts",
 | 
					            result = runner.invoke(args=["accounting-init-accounts",
 | 
				
			||||||
                                         "-u", "editor"])
 | 
					                                         "-u", "editor"])
 | 
				
			||||||
            self.assertEqual(result.exit_code, 0)
 | 
					            self.assertEqual(result.exit_code, 0)
 | 
				
			||||||
            Voucher.query.delete()
 | 
					            JournalEntry.query.delete()
 | 
				
			||||||
            JournalEntryLineItem.query.delete()
 | 
					            JournalEntryLineItem.query.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.client, self.csrf_token = get_client(self.app, "editor")
 | 
					        self.client, self.csrf_token = get_client(self.app, "editor")
 | 
				
			||||||
@@ -73,36 +73,39 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Account, Voucher
 | 
					        from accounting.models import Account, JournalEntry
 | 
				
			||||||
        create_uri: str = f"{PREFIX}/create/receipt?next=%2F_next"
 | 
					        create_uri: str = f"{PREFIX}/create/receipt?next=%2F_next"
 | 
				
			||||||
        store_uri: str = f"{PREFIX}/store/receipt"
 | 
					        store_uri: str = f"{PREFIX}/store/receipt"
 | 
				
			||||||
        form: dict[str, str]
 | 
					        form: dict[str, str]
 | 
				
			||||||
        old_amount: Decimal
 | 
					        old_amount: Decimal
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        voucher_data: VoucherData = VoucherData(
 | 
					        journal_entry_data: JournalEntryData = JournalEntryData(
 | 
				
			||||||
            self.data.e_r_or3d.voucher.days, [CurrencyData(
 | 
					            self.data.e_r_or3d.journal_entry.days, [CurrencyData(
 | 
				
			||||||
                "USD",
 | 
					                "USD",
 | 
				
			||||||
                [],
 | 
					                [],
 | 
				
			||||||
                [JournalEntryLineItemData(Accounts.RECEIVABLE,
 | 
					                [JournalEntryLineItemData(
 | 
				
			||||||
                                          self.data.e_r_or1d.description, "300",
 | 
					                    Accounts.RECEIVABLE,
 | 
				
			||||||
                                          original_line_item=self.data.e_r_or1d),
 | 
					                    self.data.e_r_or1d.description, "300",
 | 
				
			||||||
                 JournalEntryLineItemData(Accounts.RECEIVABLE,
 | 
					                    original_line_item=self.data.e_r_or1d),
 | 
				
			||||||
                                          self.data.e_r_or1d.description, "100",
 | 
					                 JournalEntryLineItemData(
 | 
				
			||||||
                                          original_line_item=self.data.e_r_or1d),
 | 
					                     Accounts.RECEIVABLE,
 | 
				
			||||||
                 JournalEntryLineItemData(Accounts.RECEIVABLE,
 | 
					                     self.data.e_r_or1d.description, "100",
 | 
				
			||||||
                                          self.data.e_r_or3d.description, "100",
 | 
					                     original_line_item=self.data.e_r_or1d),
 | 
				
			||||||
                                          original_line_item=self.data.e_r_or3d)])])
 | 
					                 JournalEntryLineItemData(
 | 
				
			||||||
 | 
					                     Accounts.RECEIVABLE,
 | 
				
			||||||
 | 
					                     self.data.e_r_or3d.description, "100",
 | 
				
			||||||
 | 
					                     original_line_item=self.data.e_r_or3d)])])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Non-existing original line item ID
 | 
					        # Non-existing original line item ID
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-credit-1-original_line_item_id"] = "9999"
 | 
					        form["currency-1-credit-1-original_line_item_id"] = "9999"
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # The same debit or credit
 | 
					        # The same debit or credit
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-credit-1-original_line_item_id"] \
 | 
					        form["currency-1-credit-1-original_line_item_id"] \
 | 
				
			||||||
            = self.data.e_p_or1c.id
 | 
					            = self.data.e_p_or1c.id
 | 
				
			||||||
        form["currency-1-credit-1-account_code"] = self.data.e_p_or1c.account
 | 
					        form["currency-1-credit-1-account_code"] = self.data.e_p_or1c.account
 | 
				
			||||||
@@ -117,7 +120,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
            account.is_need_offset = False
 | 
					            account.is_need_offset = False
 | 
				
			||||||
            db.session.commit()
 | 
					            db.session.commit()
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.client.post(
 | 
				
			||||||
            store_uri, data=voucher_data.new_form(self.csrf_token))
 | 
					            store_uri, data=journal_entry_data.new_form(self.csrf_token))
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
@@ -126,7 +129,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
            db.session.commit()
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # The original line item is also an offset
 | 
					        # The original line item is also an offset
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-credit-1-original_line_item_id"] \
 | 
					        form["currency-1-credit-1-original_line_item_id"] \
 | 
				
			||||||
            = self.data.e_p_of1d.id
 | 
					            = self.data.e_p_of1d.id
 | 
				
			||||||
        form["currency-1-credit-1-account_code"] = self.data.e_p_of1d.account
 | 
					        form["currency-1-credit-1-account_code"] = self.data.e_p_of1d.account
 | 
				
			||||||
@@ -135,54 +138,55 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not the same currency
 | 
					        # Not the same currency
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-code"] = "EUR"
 | 
					        form["currency-1-code"] = "EUR"
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not the same account
 | 
					        # Not the same account
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
 | 
					        form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not exceeding net balance - partially offset
 | 
					        # Not exceeding net balance - partially offset
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-credit-1-amount"] \
 | 
					        form["currency-1-credit-1-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].credit[0].amount
 | 
					            = str(journal_entry_data.currencies[0].credit[0].amount
 | 
				
			||||||
                  + Decimal("0.01"))
 | 
					                  + Decimal("0.01"))
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not exceeding net balance - unmatched
 | 
					        # Not exceeding net balance - unmatched
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-credit-3-amount"] \
 | 
					        form["currency-1-credit-3-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].credit[2].amount
 | 
					            = str(journal_entry_data.currencies[0].credit[2].amount
 | 
				
			||||||
                  + Decimal("0.01"))
 | 
					                  + Decimal("0.01"))
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not before the original line items
 | 
					        # Not before the original line items
 | 
				
			||||||
        old_days = voucher_data.days
 | 
					        old_days = journal_entry_data.days
 | 
				
			||||||
        voucher_data.days = old_days + 1
 | 
					        journal_entry_data.days = old_days + 1
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
        voucher_data.days = old_days
 | 
					        journal_entry_data.days = old_days
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Success
 | 
					        # Success
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        voucher_id: int = match_voucher_detail(response.headers["Location"])
 | 
					        journal_entry_id: int \
 | 
				
			||||||
 | 
					            = match_journal_entry_detail(response.headers["Location"])
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            voucher = db.session.get(Voucher, voucher_id)
 | 
					            journal_entry = db.session.get(JournalEntry, journal_entry_id)
 | 
				
			||||||
            for offset in voucher.currencies[0].credit:
 | 
					            for offset in journal_entry.currencies[0].credit:
 | 
				
			||||||
                self.assertIsNotNone(offset.original_line_item_id)
 | 
					                self.assertIsNotNone(offset.original_line_item_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_edit_receivable_offset(self) -> None:
 | 
					    def test_edit_receivable_offset(self) -> None:
 | 
				
			||||||
@@ -191,27 +195,27 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Account
 | 
					        from accounting.models import Account
 | 
				
			||||||
        voucher_data: VoucherData = self.data.v_r_of2
 | 
					        journal_entry_data: JournalEntryData = self.data.v_r_of2
 | 
				
			||||||
        edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
 | 
					        edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next"
 | 
				
			||||||
        update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
 | 
					        update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
 | 
				
			||||||
        form: dict[str, str]
 | 
					        form: dict[str, str]
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        voucher_data.days = self.data.v_r_or2.days
 | 
					        journal_entry_data.days = self.data.v_r_or2.days
 | 
				
			||||||
        voucher_data.currencies[0].debit[0].amount = Decimal("600")
 | 
					        journal_entry_data.currencies[0].debit[0].amount = Decimal("600")
 | 
				
			||||||
        voucher_data.currencies[0].credit[0].amount = Decimal("600")
 | 
					        journal_entry_data.currencies[0].credit[0].amount = Decimal("600")
 | 
				
			||||||
        voucher_data.currencies[0].debit[2].amount = Decimal("600")
 | 
					        journal_entry_data.currencies[0].debit[2].amount = Decimal("600")
 | 
				
			||||||
        voucher_data.currencies[0].credit[2].amount = Decimal("600")
 | 
					        journal_entry_data.currencies[0].credit[2].amount = Decimal("600")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Non-existing original line item ID
 | 
					        # Non-existing original line item ID
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-credit-1-original_line_item_id"] = "9999"
 | 
					        form["currency-1-credit-1-original_line_item_id"] = "9999"
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # The same debit or credit
 | 
					        # The same debit or credit
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-credit-1-original_line_item_id"] \
 | 
					        form["currency-1-credit-1-original_line_item_id"] \
 | 
				
			||||||
            = self.data.e_p_or1c.id
 | 
					            = self.data.e_p_or1c.id
 | 
				
			||||||
        form["currency-1-credit-1-account_code"] = self.data.e_p_or1c.account
 | 
					        form["currency-1-credit-1-account_code"] = self.data.e_p_or1c.account
 | 
				
			||||||
@@ -227,7 +231,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
            account.is_need_offset = False
 | 
					            account.is_need_offset = False
 | 
				
			||||||
            db.session.commit()
 | 
					            db.session.commit()
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.client.post(
 | 
				
			||||||
            update_uri, data=voucher_data.update_form(self.csrf_token))
 | 
					            update_uri, data=journal_entry_data.update_form(self.csrf_token))
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
@@ -236,7 +240,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
            db.session.commit()
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # The original line item is also an offset
 | 
					        # The original line item is also an offset
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-credit-1-original_line_item_id"] \
 | 
					        form["currency-1-credit-1-original_line_item_id"] \
 | 
				
			||||||
            = self.data.e_p_of1d.id
 | 
					            = self.data.e_p_of1d.id
 | 
				
			||||||
        form["currency-1-credit-1-account_code"] = self.data.e_p_of1d.account
 | 
					        form["currency-1-credit-1-account_code"] = self.data.e_p_of1d.account
 | 
				
			||||||
@@ -245,180 +249,187 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not the same currency
 | 
					        # Not the same currency
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-code"] = "EUR"
 | 
					        form["currency-1-code"] = "EUR"
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not the same account
 | 
					        # Not the same account
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
 | 
					        form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not exceeding net balance - partially offset
 | 
					        # Not exceeding net balance - partially offset
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-amount"] \
 | 
					        form["currency-1-debit-1-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].debit[0].amount + Decimal("0.01"))
 | 
					            = str(journal_entry_data.currencies[0].debit[0].amount
 | 
				
			||||||
 | 
					                  + Decimal("0.01"))
 | 
				
			||||||
        form["currency-1-credit-1-amount"] \
 | 
					        form["currency-1-credit-1-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].credit[0].amount
 | 
					            = str(journal_entry_data.currencies[0].credit[0].amount
 | 
				
			||||||
                  + Decimal("0.01"))
 | 
					                  + Decimal("0.01"))
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not exceeding net balance - unmatched
 | 
					        # Not exceeding net balance - unmatched
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-3-amount"] \
 | 
					        form["currency-1-debit-3-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].debit[2].amount + Decimal("0.01"))
 | 
					            = str(journal_entry_data.currencies[0].debit[2].amount
 | 
				
			||||||
 | 
					                  + Decimal("0.01"))
 | 
				
			||||||
        form["currency-1-credit-3-amount"] \
 | 
					        form["currency-1-credit-3-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].credit[2].amount
 | 
					            = str(journal_entry_data.currencies[0].credit[2].amount
 | 
				
			||||||
                  + Decimal("0.01"))
 | 
					                  + Decimal("0.01"))
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not before the original line items
 | 
					        # Not before the original line items
 | 
				
			||||||
        old_days: int = voucher_data.days
 | 
					        old_days: int = journal_entry_data.days
 | 
				
			||||||
        voucher_data.days = old_days + 1
 | 
					        journal_entry_data.days = old_days + 1
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
        voucher_data.days = old_days
 | 
					        journal_entry_data.days = old_days
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Success
 | 
					        # Success
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"],
 | 
					        self.assertEqual(response.headers["Location"],
 | 
				
			||||||
                         f"{PREFIX}/{voucher_data.id}?next=%2F_next")
 | 
					                         f"{PREFIX}/{journal_entry_data.id}?next=%2F_next")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_edit_receivable_original_line_item(self) -> None:
 | 
					    def test_edit_receivable_original_line_item(self) -> None:
 | 
				
			||||||
        """Tests to edit the receivable original line item.
 | 
					        """Tests to edit the receivable original line item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Voucher
 | 
					        from accounting.models import JournalEntry
 | 
				
			||||||
        voucher_data: VoucherData = self.data.v_r_or1
 | 
					        journal_entry_data: JournalEntryData = self.data.v_r_or1
 | 
				
			||||||
        edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
 | 
					        edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next"
 | 
				
			||||||
        update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
 | 
					        update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
 | 
				
			||||||
        form: dict[str, str]
 | 
					        form: dict[str, str]
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        voucher_data.days = self.data.v_r_of1.days
 | 
					        journal_entry_data.days = self.data.v_r_of1.days
 | 
				
			||||||
        voucher_data.currencies[0].debit[0].amount = Decimal("800")
 | 
					        journal_entry_data.currencies[0].debit[0].amount = Decimal("800")
 | 
				
			||||||
        voucher_data.currencies[0].credit[0].amount = Decimal("800")
 | 
					        journal_entry_data.currencies[0].credit[0].amount = Decimal("800")
 | 
				
			||||||
        voucher_data.currencies[0].debit[1].amount = Decimal("3.4")
 | 
					        journal_entry_data.currencies[0].debit[1].amount = Decimal("3.4")
 | 
				
			||||||
        voucher_data.currencies[0].credit[1].amount = Decimal("3.4")
 | 
					        journal_entry_data.currencies[0].credit[1].amount = Decimal("3.4")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not the same currency
 | 
					        # Not the same currency
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-code"] = "EUR"
 | 
					        form["currency-1-code"] = "EUR"
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not the same account
 | 
					        # Not the same account
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-account_code"] = Accounts.NOTES_RECEIVABLE
 | 
					        form["currency-1-debit-1-account_code"] = Accounts.NOTES_RECEIVABLE
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not less than offset total - partially offset
 | 
					        # Not less than offset total - partially offset
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-amount"] \
 | 
					        form["currency-1-debit-1-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].debit[0].amount - Decimal("0.01"))
 | 
					            = str(journal_entry_data.currencies[0].debit[0].amount
 | 
				
			||||||
 | 
					                  - Decimal("0.01"))
 | 
				
			||||||
        form["currency-1-credit-1-amount"] \
 | 
					        form["currency-1-credit-1-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].credit[0].amount
 | 
					            = str(journal_entry_data.currencies[0].credit[0].amount
 | 
				
			||||||
                  - Decimal("0.01"))
 | 
					                  - Decimal("0.01"))
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not less than offset total - fully offset
 | 
					        # Not less than offset total - fully offset
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-2-amount"] \
 | 
					        form["currency-1-debit-2-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].debit[1].amount - Decimal("0.01"))
 | 
					            = str(journal_entry_data.currencies[0].debit[1].amount
 | 
				
			||||||
 | 
					                  - Decimal("0.01"))
 | 
				
			||||||
        form["currency-1-credit-2-amount"] \
 | 
					        form["currency-1-credit-2-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].credit[1].amount
 | 
					            = str(journal_entry_data.currencies[0].credit[1].amount
 | 
				
			||||||
                  - Decimal("0.01"))
 | 
					                  - Decimal("0.01"))
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not after the offset items
 | 
					        # Not after the offset items
 | 
				
			||||||
        old_days: int = voucher_data.days
 | 
					        old_days: int = journal_entry_data.days
 | 
				
			||||||
        voucher_data.days = old_days - 1
 | 
					        journal_entry_data.days = old_days - 1
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
        voucher_data.days = old_days
 | 
					        journal_entry_data.days = old_days
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not deleting matched original line items
 | 
					        # Not deleting matched original line items
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        del form["currency-1-debit-1-eid"]
 | 
					        del form["currency-1-debit-1-eid"]
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Success
 | 
					        # Success
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"],
 | 
					        self.assertEqual(response.headers["Location"],
 | 
				
			||||||
                         f"{PREFIX}/{voucher_data.id}?next=%2F_next")
 | 
					                         f"{PREFIX}/{journal_entry_data.id}?next=%2F_next")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # The original line item is always before the offset item, even when
 | 
					        # The original line item is always before the offset item, even when
 | 
				
			||||||
        # they happen in the same day.
 | 
					        # they happen in the same day.
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            voucher_or: Voucher | None = db.session.get(
 | 
					            journal_entry_or: JournalEntry | None = db.session.get(
 | 
				
			||||||
                Voucher, voucher_data.id)
 | 
					                JournalEntry, journal_entry_data.id)
 | 
				
			||||||
            self.assertIsNotNone(voucher_or)
 | 
					            self.assertIsNotNone(journal_entry_or)
 | 
				
			||||||
            voucher_of: Voucher | None = db.session.get(
 | 
					            journal_entry_of: JournalEntry | None = db.session.get(
 | 
				
			||||||
                Voucher, self.data.v_r_of1.id)
 | 
					                JournalEntry, self.data.v_r_of1.id)
 | 
				
			||||||
            self.assertIsNotNone(voucher_of)
 | 
					            self.assertIsNotNone(journal_entry_of)
 | 
				
			||||||
            self.assertEqual(voucher_or.date, voucher_of.date)
 | 
					            self.assertEqual(journal_entry_or.date, journal_entry_of.date)
 | 
				
			||||||
            self.assertLess(voucher_or.no, voucher_of.no)
 | 
					            self.assertLess(journal_entry_or.no, journal_entry_of.no)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_add_payable_offset(self) -> None:
 | 
					    def test_add_payable_offset(self) -> None:
 | 
				
			||||||
        """Tests to add the payable offset.
 | 
					        """Tests to add the payable offset.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Account, Voucher
 | 
					        from accounting.models import Account, JournalEntry
 | 
				
			||||||
        create_uri: str = f"{PREFIX}/create/disbursement?next=%2F_next"
 | 
					        create_uri: str = f"{PREFIX}/create/disbursement?next=%2F_next"
 | 
				
			||||||
        store_uri: str = f"{PREFIX}/store/disbursement"
 | 
					        store_uri: str = f"{PREFIX}/store/disbursement"
 | 
				
			||||||
        form: dict[str, str]
 | 
					        form: dict[str, str]
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        voucher_data: VoucherData = VoucherData(
 | 
					        journal_entry_data: JournalEntryData = JournalEntryData(
 | 
				
			||||||
            self.data.e_p_or3c.voucher.days, [CurrencyData(
 | 
					            self.data.e_p_or3c.journal_entry.days, [CurrencyData(
 | 
				
			||||||
                "USD",
 | 
					                "USD",
 | 
				
			||||||
                [JournalEntryLineItemData(Accounts.PAYABLE,
 | 
					                [JournalEntryLineItemData(
 | 
				
			||||||
                                          self.data.e_p_or1c.description, "500",
 | 
					                    Accounts.PAYABLE,
 | 
				
			||||||
                                          original_line_item=self.data.e_p_or1c),
 | 
					                    self.data.e_p_or1c.description, "500",
 | 
				
			||||||
                 JournalEntryLineItemData(Accounts.PAYABLE,
 | 
					                    original_line_item=self.data.e_p_or1c),
 | 
				
			||||||
                                          self.data.e_p_or1c.description, "300",
 | 
					                 JournalEntryLineItemData(
 | 
				
			||||||
                                          original_line_item=self.data.e_p_or1c),
 | 
					                     Accounts.PAYABLE,
 | 
				
			||||||
                 JournalEntryLineItemData(Accounts.PAYABLE,
 | 
					                     self.data.e_p_or1c.description, "300",
 | 
				
			||||||
                                          self.data.e_p_or3c.description, "120",
 | 
					                     original_line_item=self.data.e_p_or1c),
 | 
				
			||||||
                                          original_line_item=self.data.e_p_or3c)],
 | 
					                 JournalEntryLineItemData(
 | 
				
			||||||
 | 
					                     Accounts.PAYABLE,
 | 
				
			||||||
 | 
					                     self.data.e_p_or3c.description, "120",
 | 
				
			||||||
 | 
					                     original_line_item=self.data.e_p_or3c)],
 | 
				
			||||||
                [])])
 | 
					                [])])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Non-existing original line item ID
 | 
					        # Non-existing original line item ID
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-original_line_item_id"] = "9999"
 | 
					        form["currency-1-debit-1-original_line_item_id"] = "9999"
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # The same debit or credit
 | 
					        # The same debit or credit
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-original_line_item_id"] \
 | 
					        form["currency-1-debit-1-original_line_item_id"] \
 | 
				
			||||||
            = self.data.e_r_or1d.id
 | 
					            = self.data.e_r_or1d.id
 | 
				
			||||||
        form["currency-1-debit-1-account_code"] = self.data.e_r_or1d.account
 | 
					        form["currency-1-debit-1-account_code"] = self.data.e_r_or1d.account
 | 
				
			||||||
@@ -433,7 +444,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
            account.is_need_offset = False
 | 
					            account.is_need_offset = False
 | 
				
			||||||
            db.session.commit()
 | 
					            db.session.commit()
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.client.post(
 | 
				
			||||||
            store_uri, data=voucher_data.new_form(self.csrf_token))
 | 
					            store_uri, data=journal_entry_data.new_form(self.csrf_token))
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
@@ -442,7 +453,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
            db.session.commit()
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # The original line item is also an offset
 | 
					        # The original line item is also an offset
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-original_line_item_id"] \
 | 
					        form["currency-1-debit-1-original_line_item_id"] \
 | 
				
			||||||
            = self.data.e_r_of1c.id
 | 
					            = self.data.e_r_of1c.id
 | 
				
			||||||
        form["currency-1-debit-1-account_code"] = self.data.e_r_of1c.account
 | 
					        form["currency-1-debit-1-account_code"] = self.data.e_r_of1c.account
 | 
				
			||||||
@@ -451,52 +462,55 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not the same currency
 | 
					        # Not the same currency
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-code"] = "EUR"
 | 
					        form["currency-1-code"] = "EUR"
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not the same account
 | 
					        # Not the same account
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
 | 
					        form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not exceeding net balance - partially offset
 | 
					        # Not exceeding net balance - partially offset
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-amount"] \
 | 
					        form["currency-1-debit-1-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].debit[0].amount + Decimal("0.01"))
 | 
					            = str(journal_entry_data.currencies[0].debit[0].amount
 | 
				
			||||||
 | 
					                  + Decimal("0.01"))
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not exceeding net balance - unmatched
 | 
					        # Not exceeding net balance - unmatched
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-3-amount"] \
 | 
					        form["currency-1-debit-3-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].debit[2].amount + Decimal("0.01"))
 | 
					            = str(journal_entry_data.currencies[0].debit[2].amount
 | 
				
			||||||
 | 
					                  + Decimal("0.01"))
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not before the original line items
 | 
					        # Not before the original line items
 | 
				
			||||||
        old_days: int = voucher_data.days
 | 
					        old_days: int = journal_entry_data.days
 | 
				
			||||||
        voucher_data.days = old_days + 1
 | 
					        journal_entry_data.days = old_days + 1
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], create_uri)
 | 
					        self.assertEqual(response.headers["Location"], create_uri)
 | 
				
			||||||
        voucher_data.days = old_days
 | 
					        journal_entry_data.days = old_days
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Success
 | 
					        # Success
 | 
				
			||||||
        form = voucher_data.new_form(self.csrf_token)
 | 
					        form = journal_entry_data.new_form(self.csrf_token)
 | 
				
			||||||
        response = self.client.post(store_uri, data=form)
 | 
					        response = self.client.post(store_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        voucher_id: int = match_voucher_detail(response.headers["Location"])
 | 
					        journal_entry_id: int \
 | 
				
			||||||
 | 
					            = match_journal_entry_detail(response.headers["Location"])
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            voucher = db.session.get(Voucher, voucher_id)
 | 
					            journal_entry = db.session.get(JournalEntry, journal_entry_id)
 | 
				
			||||||
            for offset in voucher.currencies[0].debit:
 | 
					            for offset in journal_entry.currencies[0].debit:
 | 
				
			||||||
                self.assertIsNotNone(offset.original_line_item_id)
 | 
					                self.assertIsNotNone(offset.original_line_item_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_edit_payable_offset(self) -> None:
 | 
					    def test_edit_payable_offset(self) -> None:
 | 
				
			||||||
@@ -504,28 +518,28 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Account, Voucher
 | 
					        from accounting.models import Account, JournalEntry
 | 
				
			||||||
        voucher_data: VoucherData = self.data.v_p_of2
 | 
					        journal_entry_data: JournalEntryData = self.data.v_p_of2
 | 
				
			||||||
        edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
 | 
					        edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next"
 | 
				
			||||||
        update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
 | 
					        update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
 | 
				
			||||||
        form: dict[str, str]
 | 
					        form: dict[str, str]
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        voucher_data.days = self.data.v_p_or2.days
 | 
					        journal_entry_data.days = self.data.v_p_or2.days
 | 
				
			||||||
        voucher_data.currencies[0].debit[0].amount = Decimal("1100")
 | 
					        journal_entry_data.currencies[0].debit[0].amount = Decimal("1100")
 | 
				
			||||||
        voucher_data.currencies[0].credit[0].amount = Decimal("1100")
 | 
					        journal_entry_data.currencies[0].credit[0].amount = Decimal("1100")
 | 
				
			||||||
        voucher_data.currencies[0].debit[2].amount = Decimal("900")
 | 
					        journal_entry_data.currencies[0].debit[2].amount = Decimal("900")
 | 
				
			||||||
        voucher_data.currencies[0].credit[2].amount = Decimal("900")
 | 
					        journal_entry_data.currencies[0].credit[2].amount = Decimal("900")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Non-existing original line item ID
 | 
					        # Non-existing original line item ID
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-original_line_item_id"] = "9999"
 | 
					        form["currency-1-debit-1-original_line_item_id"] = "9999"
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # The same debit or credit
 | 
					        # The same debit or credit
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-original_line_item_id"] \
 | 
					        form["currency-1-debit-1-original_line_item_id"] \
 | 
				
			||||||
            = self.data.e_r_or1d.id
 | 
					            = self.data.e_r_or1d.id
 | 
				
			||||||
        form["currency-1-debit-1-account_code"] = self.data.e_r_or1d.account
 | 
					        form["currency-1-debit-1-account_code"] = self.data.e_r_or1d.account
 | 
				
			||||||
@@ -541,7 +555,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
            account.is_need_offset = False
 | 
					            account.is_need_offset = False
 | 
				
			||||||
            db.session.commit()
 | 
					            db.session.commit()
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.client.post(
 | 
				
			||||||
            update_uri, data=voucher_data.update_form(self.csrf_token))
 | 
					            update_uri, data=journal_entry_data.update_form(self.csrf_token))
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
@@ -550,7 +564,7 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
            db.session.commit()
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # The original line item is also an offset
 | 
					        # The original line item is also an offset
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-original_line_item_id"] \
 | 
					        form["currency-1-debit-1-original_line_item_id"] \
 | 
				
			||||||
            = self.data.e_r_of1c.id
 | 
					            = self.data.e_r_of1c.id
 | 
				
			||||||
        form["currency-1-debit-1-account_code"] = self.data.e_r_of1c.account
 | 
					        form["currency-1-debit-1-account_code"] = self.data.e_r_of1c.account
 | 
				
			||||||
@@ -559,58 +573,61 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not the same currency
 | 
					        # Not the same currency
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-code"] = "EUR"
 | 
					        form["currency-1-code"] = "EUR"
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not the same account
 | 
					        # Not the same account
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
 | 
					        form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not exceeding net balance - partially offset
 | 
					        # Not exceeding net balance - partially offset
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-amount"] \
 | 
					        form["currency-1-debit-1-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].debit[0].amount + Decimal("0.01"))
 | 
					            = str(journal_entry_data.currencies[0].debit[0].amount
 | 
				
			||||||
 | 
					                  + Decimal("0.01"))
 | 
				
			||||||
        form["currency-1-credit-1-amount"] \
 | 
					        form["currency-1-credit-1-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].credit[0].amount
 | 
					            = str(journal_entry_data.currencies[0].credit[0].amount
 | 
				
			||||||
                  + Decimal("0.01"))
 | 
					                  + Decimal("0.01"))
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not exceeding net balance - unmatched
 | 
					        # Not exceeding net balance - unmatched
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-3-amount"] \
 | 
					        form["currency-1-debit-3-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].debit[2].amount + Decimal("0.01"))
 | 
					            = str(journal_entry_data.currencies[0].debit[2].amount
 | 
				
			||||||
 | 
					                  + Decimal("0.01"))
 | 
				
			||||||
        form["currency-1-credit-3-amount"] \
 | 
					        form["currency-1-credit-3-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].credit[2].amount
 | 
					            = str(journal_entry_data.currencies[0].credit[2].amount
 | 
				
			||||||
                  + Decimal("0.01"))
 | 
					                  + Decimal("0.01"))
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not before the original line items
 | 
					        # Not before the original line items
 | 
				
			||||||
        old_days: int = voucher_data.days
 | 
					        old_days: int = journal_entry_data.days
 | 
				
			||||||
        voucher_data.days = old_days + 1
 | 
					        journal_entry_data.days = old_days + 1
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
        voucher_data.days = old_days
 | 
					        journal_entry_data.days = old_days
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Success
 | 
					        # Success
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        voucher_id: int = match_voucher_detail(response.headers["Location"])
 | 
					        journal_entry_id: int \
 | 
				
			||||||
 | 
					            = match_journal_entry_detail(response.headers["Location"])
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            voucher = db.session.get(Voucher, voucher_id)
 | 
					            journal_entry = db.session.get(JournalEntry, journal_entry_id)
 | 
				
			||||||
            for offset in voucher.currencies[0].debit:
 | 
					            for offset in journal_entry.currencies[0].debit:
 | 
				
			||||||
                self.assertIsNotNone(offset.original_line_item_id)
 | 
					                self.assertIsNotNone(offset.original_line_item_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_edit_payable_original_line_item(self) -> None:
 | 
					    def test_edit_payable_original_line_item(self) -> None:
 | 
				
			||||||
@@ -618,86 +635,88 @@ class OffsetTestCase(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Voucher
 | 
					        from accounting.models import JournalEntry
 | 
				
			||||||
        voucher_data: VoucherData = self.data.v_p_or1
 | 
					        journal_entry_data: JournalEntryData = self.data.v_p_or1
 | 
				
			||||||
        edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
 | 
					        edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next"
 | 
				
			||||||
        update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
 | 
					        update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
 | 
				
			||||||
        form: dict[str, str]
 | 
					        form: dict[str, str]
 | 
				
			||||||
        response: httpx.Response
 | 
					        response: httpx.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        voucher_data.days = self.data.v_p_of1.days
 | 
					        journal_entry_data.days = self.data.v_p_of1.days
 | 
				
			||||||
        voucher_data.currencies[0].debit[0].amount = Decimal("1200")
 | 
					        journal_entry_data.currencies[0].debit[0].amount = Decimal("1200")
 | 
				
			||||||
        voucher_data.currencies[0].credit[0].amount = Decimal("1200")
 | 
					        journal_entry_data.currencies[0].credit[0].amount = Decimal("1200")
 | 
				
			||||||
        voucher_data.currencies[0].debit[1].amount = Decimal("0.9")
 | 
					        journal_entry_data.currencies[0].debit[1].amount = Decimal("0.9")
 | 
				
			||||||
        voucher_data.currencies[0].credit[1].amount = Decimal("0.9")
 | 
					        journal_entry_data.currencies[0].credit[1].amount = Decimal("0.9")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not the same currency
 | 
					        # Not the same currency
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-code"] = "EUR"
 | 
					        form["currency-1-code"] = "EUR"
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not the same account
 | 
					        # Not the same account
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-credit-1-account_code"] = Accounts.NOTES_PAYABLE
 | 
					        form["currency-1-credit-1-account_code"] = Accounts.NOTES_PAYABLE
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not less than offset total - partially offset
 | 
					        # Not less than offset total - partially offset
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-1-amount"] \
 | 
					        form["currency-1-debit-1-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].debit[0].amount - Decimal("0.01"))
 | 
					            = str(journal_entry_data.currencies[0].debit[0].amount
 | 
				
			||||||
 | 
					                  - Decimal("0.01"))
 | 
				
			||||||
        form["currency-1-credit-1-amount"] \
 | 
					        form["currency-1-credit-1-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].credit[0].amount
 | 
					            = str(journal_entry_data.currencies[0].credit[0].amount
 | 
				
			||||||
                  - Decimal("0.01"))
 | 
					                  - Decimal("0.01"))
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not less than offset total - fully offset
 | 
					        # Not less than offset total - fully offset
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        form["currency-1-debit-2-amount"] \
 | 
					        form["currency-1-debit-2-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].debit[1].amount - Decimal("0.01"))
 | 
					            = str(journal_entry_data.currencies[0].debit[1].amount
 | 
				
			||||||
 | 
					                  - Decimal("0.01"))
 | 
				
			||||||
        form["currency-1-credit-2-amount"] \
 | 
					        form["currency-1-credit-2-amount"] \
 | 
				
			||||||
            = str(voucher_data.currencies[0].credit[1].amount
 | 
					            = str(journal_entry_data.currencies[0].credit[1].amount
 | 
				
			||||||
                  - Decimal("0.01"))
 | 
					                  - Decimal("0.01"))
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not after the offset items
 | 
					        # Not after the offset items
 | 
				
			||||||
        old_days: int = voucher_data.days
 | 
					        old_days: int = journal_entry_data.days
 | 
				
			||||||
        voucher_data.days = old_days - 1
 | 
					        journal_entry_data.days = old_days - 1
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
        voucher_data.days = old_days
 | 
					        journal_entry_data.days = old_days
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Not deleting matched original line items
 | 
					        # Not deleting matched original line items
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        del form["currency-1-credit-1-eid"]
 | 
					        del form["currency-1-credit-1-eid"]
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"], edit_uri)
 | 
					        self.assertEqual(response.headers["Location"], edit_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Success
 | 
					        # Success
 | 
				
			||||||
        form = voucher_data.update_form(self.csrf_token)
 | 
					        form = journal_entry_data.update_form(self.csrf_token)
 | 
				
			||||||
        response = self.client.post(update_uri, data=form)
 | 
					        response = self.client.post(update_uri, data=form)
 | 
				
			||||||
        self.assertEqual(response.status_code, 302)
 | 
					        self.assertEqual(response.status_code, 302)
 | 
				
			||||||
        self.assertEqual(response.headers["Location"],
 | 
					        self.assertEqual(response.headers["Location"],
 | 
				
			||||||
                         f"{PREFIX}/{voucher_data.id}?next=%2F_next")
 | 
					                         f"{PREFIX}/{journal_entry_data.id}?next=%2F_next")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # The original line item is always before the offset item, even when
 | 
					        # The original line item is always before the offset item, even when
 | 
				
			||||||
        # they happen in the same day
 | 
					        # they happen in the same day
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            voucher_or: Voucher | None = db.session.get(
 | 
					            journal_entry_or: JournalEntry | None = db.session.get(
 | 
				
			||||||
                Voucher, voucher_data.id)
 | 
					                JournalEntry, journal_entry_data.id)
 | 
				
			||||||
            self.assertIsNotNone(voucher_or)
 | 
					            self.assertIsNotNone(journal_entry_or)
 | 
				
			||||||
            voucher_of: Voucher | None = db.session.get(
 | 
					            journal_entry_of: JournalEntry | None = db.session.get(
 | 
				
			||||||
                Voucher, self.data.v_p_of1.id)
 | 
					                JournalEntry, self.data.v_p_of1.id)
 | 
				
			||||||
            self.assertIsNotNone(voucher_of)
 | 
					            self.assertIsNotNone(journal_entry_of)
 | 
				
			||||||
            self.assertEqual(voucher_or.date, voucher_of.date)
 | 
					            self.assertEqual(journal_entry_or.date, journal_entry_of.date)
 | 
				
			||||||
            self.assertLess(voucher_or.no, voucher_of.no)
 | 
					            self.assertLess(journal_entry_or.no, journal_entry_of.no)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
#  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 common test libraries for the voucher test cases.
 | 
					"""The common test libraries for the journal entry test cases.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
@@ -57,10 +57,10 @@ class Accounts:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_add_form(csrf_token: str) -> dict[str, str]:
 | 
					def get_add_form(csrf_token: str) -> dict[str, str]:
 | 
				
			||||||
    """Returns the form data to add a new voucher.
 | 
					    """Returns the form data to add a new journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param csrf_token: The CSRF token.
 | 
					    :param csrf_token: The CSRF token.
 | 
				
			||||||
    :return: The form data to add a new voucher.
 | 
					    :return: The form data to add a new journal entry.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    return {"csrf_token": csrf_token,
 | 
					    return {"csrf_token": csrf_token,
 | 
				
			||||||
            "next": NEXT_URI,
 | 
					            "next": NEXT_URI,
 | 
				
			||||||
@@ -124,28 +124,30 @@ def get_add_form(csrf_token: str) -> dict[str, str]:
 | 
				
			|||||||
            "note": f"\n \n\n  \n{NON_EMPTY_NOTE}  \n  \n\n  "}
 | 
					            "note": f"\n \n\n  \n{NON_EMPTY_NOTE}  \n  \n\n  "}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_unchanged_update_form(voucher_id: int, app: Flask, csrf_token: str) \
 | 
					def get_unchanged_update_form(journal_entry_id: int, app: Flask,
 | 
				
			||||||
        -> dict[str, str]:
 | 
					                              csrf_token: str) -> dict[str, str]:
 | 
				
			||||||
    """Returns the form data to update a voucher, where the data are not
 | 
					    """Returns the form data to update a journal entry, where the data are not
 | 
				
			||||||
    changed.
 | 
					    changed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param voucher_id: The voucher ID.
 | 
					    :param journal_entry_id: The journal entry ID.
 | 
				
			||||||
    :param app: The Flask application.
 | 
					    :param app: The Flask application.
 | 
				
			||||||
    :param csrf_token: The CSRF token.
 | 
					    :param csrf_token: The CSRF token.
 | 
				
			||||||
    :return: The form data to update the voucher, where the data are not
 | 
					    :return: The form data to update the journal entry, where the data are not
 | 
				
			||||||
        changed.
 | 
					        changed.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    from accounting.models import Voucher, VoucherCurrency
 | 
					    from accounting.models import JournalEntry, JournalEntryCurrency
 | 
				
			||||||
    with app.app_context():
 | 
					    with app.app_context():
 | 
				
			||||||
        voucher: Voucher | None = db.session.get(Voucher, voucher_id)
 | 
					        journal_entry: JournalEntry | None \
 | 
				
			||||||
        assert voucher is not None
 | 
					            = db.session.get(JournalEntry, journal_entry_id)
 | 
				
			||||||
        currencies: list[VoucherCurrency] = voucher.currencies
 | 
					        assert journal_entry is not None
 | 
				
			||||||
 | 
					        currencies: list[JournalEntryCurrency] = journal_entry.currencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    form: dict[str, str] = {"csrf_token": csrf_token,
 | 
					    form: dict[str, str] \
 | 
				
			||||||
                            "next": NEXT_URI,
 | 
					        = {"csrf_token": csrf_token,
 | 
				
			||||||
                            "date": voucher.date,
 | 
					           "next": NEXT_URI,
 | 
				
			||||||
                            "note": "  \n \n\n  " if voucher.note is None
 | 
					           "date": journal_entry.date,
 | 
				
			||||||
                            else f"\n    \n\n \n  \n{voucher.note}  \n\n   "}
 | 
					           "note": "  \n \n\n  " if journal_entry.note is None
 | 
				
			||||||
 | 
					           else f"\n    \n\n \n  \n{journal_entry.note}  \n\n   "}
 | 
				
			||||||
    currency_indices_used: set[int] = set()
 | 
					    currency_indices_used: set[int] = set()
 | 
				
			||||||
    currency_no: int = 0
 | 
					    currency_no: int = 0
 | 
				
			||||||
    for currency in currencies:
 | 
					    for currency in currencies:
 | 
				
			||||||
@@ -182,7 +184,8 @@ def get_unchanged_update_form(voucher_id: int, app: Flask, csrf_token: str) \
 | 
				
			|||||||
            form[f"{prefix}-no"] = str(line_item_no)
 | 
					            form[f"{prefix}-no"] = str(line_item_no)
 | 
				
			||||||
            form[f"{prefix}-account_code"] = line_item.account.code
 | 
					            form[f"{prefix}-account_code"] = line_item.account.code
 | 
				
			||||||
            form[f"{prefix}-description"] \
 | 
					            form[f"{prefix}-description"] \
 | 
				
			||||||
                = "  " if line_item.description is None else f" {line_item.description} "
 | 
					                = "  " if line_item.description is None \
 | 
				
			||||||
 | 
					                else f" {line_item.description} "
 | 
				
			||||||
            form[f"{prefix}-amount"] = str(line_item.amount)
 | 
					            form[f"{prefix}-amount"] = str(line_item.amount)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return form
 | 
					    return form
 | 
				
			||||||
@@ -201,21 +204,21 @@ def __get_new_index(indices_used: set[int]) -> int:
 | 
				
			|||||||
            return index
 | 
					            return index
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_update_form(voucher_id: int, app: Flask,
 | 
					def get_update_form(journal_entry_id: int, app: Flask,
 | 
				
			||||||
                    csrf_token: str, is_debit: bool | None) -> dict[str, str]:
 | 
					                    csrf_token: str, is_debit: bool | None) -> dict[str, str]:
 | 
				
			||||||
    """Returns the form data to update a voucher, where the data are
 | 
					    """Returns the form data to update a journal entry, where the data are
 | 
				
			||||||
    changed.
 | 
					    changed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param voucher_id: The voucher ID.
 | 
					    :param journal_entry_id: The journal entry ID.
 | 
				
			||||||
    :param app: The Flask application.
 | 
					    :param app: The Flask application.
 | 
				
			||||||
    :param csrf_token: The CSRF token.
 | 
					    :param csrf_token: The CSRF token.
 | 
				
			||||||
    :param is_debit: True for a cash disbursement voucher, False for a cash
 | 
					    :param is_debit: True for a cash disbursement journal entry, False for a
 | 
				
			||||||
        receipt voucher, or None for a transfer voucher
 | 
					        cash receipt journal entry, or None for a transfer journal entry.
 | 
				
			||||||
    :return: The form data to update the voucher, where the data are
 | 
					    :return: The form data to update the journal entry, where the data are
 | 
				
			||||||
        changed.
 | 
					        changed.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    form: dict[str, str] = get_unchanged_update_form(
 | 
					    form: dict[str, str] = get_unchanged_update_form(
 | 
				
			||||||
        voucher_id, app, csrf_token)
 | 
					        journal_entry_id, app, csrf_token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Mess up the line items in a currency
 | 
					    # Mess up the line items in a currency
 | 
				
			||||||
    currency_prefix: str = __get_currency_prefix(form, "USD")
 | 
					    currency_prefix: str = __get_currency_prefix(form, "USD")
 | 
				
			||||||
@@ -263,8 +266,10 @@ def __mess_up_debit(form: dict[str, str], currency_prefix: str) \
 | 
				
			|||||||
    form[f"{debit_prefix}{new_index}-amount"] = str(amount)
 | 
					    form[f"{debit_prefix}{new_index}-amount"] = str(amount)
 | 
				
			||||||
    form[f"{debit_prefix}{new_index}-account_code"] = Accounts.TRAVEL
 | 
					    form[f"{debit_prefix}{new_index}-account_code"] = Accounts.TRAVEL
 | 
				
			||||||
    # Swap the cash and the bank order
 | 
					    # Swap the cash and the bank order
 | 
				
			||||||
    key_cash: str = __get_line_item_no_key(form, currency_prefix, Accounts.CASH)
 | 
					    key_cash: str = __get_line_item_no_key(
 | 
				
			||||||
    key_bank: str = __get_line_item_no_key(form, currency_prefix, Accounts.BANK)
 | 
					        form, currency_prefix, Accounts.CASH)
 | 
				
			||||||
 | 
					    key_bank: str = __get_line_item_no_key(
 | 
				
			||||||
 | 
					        form, currency_prefix, Accounts.BANK)
 | 
				
			||||||
    form[key_cash], form[key_bank] = form[key_bank], form[key_cash]
 | 
					    form[key_cash], form[key_bank] = form[key_bank], form[key_cash]
 | 
				
			||||||
    return form
 | 
					    return form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -302,8 +307,10 @@ def __mess_up_credit(form: dict[str, str], currency_prefix: str) \
 | 
				
			|||||||
    form[f"{credit_prefix}{new_index}-amount"] = str(amount)
 | 
					    form[f"{credit_prefix}{new_index}-amount"] = str(amount)
 | 
				
			||||||
    form[f"{credit_prefix}{new_index}-account_code"] = Accounts.AGENCY
 | 
					    form[f"{credit_prefix}{new_index}-account_code"] = Accounts.AGENCY
 | 
				
			||||||
    # Swap the service and the interest order
 | 
					    # Swap the service and the interest order
 | 
				
			||||||
    key_srv: str = __get_line_item_no_key(form, currency_prefix, Accounts.SERVICE)
 | 
					    key_srv: str = __get_line_item_no_key(
 | 
				
			||||||
    key_int: str = __get_line_item_no_key(form, currency_prefix, Accounts.INTEREST)
 | 
					        form, currency_prefix, Accounts.SERVICE)
 | 
				
			||||||
 | 
					    key_int: str = __get_line_item_no_key(
 | 
				
			||||||
 | 
					        form, currency_prefix, Accounts.INTEREST)
 | 
				
			||||||
    form[key_srv], form[key_int] = form[key_int], form[key_srv]
 | 
					    form[key_srv], form[key_int] = form[key_int], form[key_srv]
 | 
				
			||||||
    return form
 | 
					    return form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -390,35 +397,35 @@ def __get_currency_prefix(form: dict[str, str], code: str) -> str:
 | 
				
			|||||||
    return m.group(1)
 | 
					    return m.group(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def add_voucher(client: httpx.Client, form: dict[str, str]) -> int:
 | 
					def add_journal_entry(client: httpx.Client, form: dict[str, str]) -> int:
 | 
				
			||||||
    """Adds a transfer voucher.
 | 
					    """Adds a transfer journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param client: The client.
 | 
					    :param client: The client.
 | 
				
			||||||
    :param form: The form data.
 | 
					    :param form: The form data.
 | 
				
			||||||
    :return: The newly-added voucher ID.
 | 
					    :return: The newly-added journal entry ID.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    prefix: str = "/accounting/vouchers"
 | 
					    prefix: str = "/accounting/journal-entries"
 | 
				
			||||||
    voucher_type: str = "transfer"
 | 
					    journal_entry_type: str = "transfer"
 | 
				
			||||||
    if len({x for x in form if "-debit-" in x}) == 0:
 | 
					    if len({x for x in form if "-debit-" in x}) == 0:
 | 
				
			||||||
        voucher_type = "receipt"
 | 
					        journal_entry_type = "receipt"
 | 
				
			||||||
    elif len({x for x in form if "-credit-" in x}) == 0:
 | 
					    elif len({x for x in form if "-credit-" in x}) == 0:
 | 
				
			||||||
        voucher_type = "disbursement"
 | 
					        journal_entry_type = "disbursement"
 | 
				
			||||||
    store_uri = f"{prefix}/store/{voucher_type}"
 | 
					    store_uri = f"{prefix}/store/{journal_entry_type}"
 | 
				
			||||||
    response: httpx.Response = client.post(store_uri, data=form)
 | 
					    response: httpx.Response = client.post(store_uri, data=form)
 | 
				
			||||||
    assert response.status_code == 302
 | 
					    assert response.status_code == 302
 | 
				
			||||||
    return match_voucher_detail(response.headers["Location"])
 | 
					    return match_journal_entry_detail(response.headers["Location"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def match_voucher_detail(location: str) -> int:
 | 
					def match_journal_entry_detail(location: str) -> int:
 | 
				
			||||||
    """Validates if the redirect location is the voucher detail, and
 | 
					    """Validates if the redirect location is the journal entry detail, and
 | 
				
			||||||
    returns the voucher ID on success.
 | 
					    returns the journal entry ID on success.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param location: The redirect location.
 | 
					    :param location: The redirect location.
 | 
				
			||||||
    :return: The voucher ID.
 | 
					    :return: The journal entry ID.
 | 
				
			||||||
    :raise AssertionError: When the location is not the voucher detail.
 | 
					    :raise AssertionError: When the location is not the journal entry detail.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    m: re.Match = re.match(r"^/accounting/vouchers/(\d+)\?next=%2F_next",
 | 
					    m: re.Match = re.match(
 | 
				
			||||||
                           location)
 | 
					        r"^/accounting/journal-entries/(\d+)\?next=%2F_next", location)
 | 
				
			||||||
    assert m is not None
 | 
					    assert m is not None
 | 
				
			||||||
    return int(m.group(1))
 | 
					    return int(m.group(1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -26,7 +26,8 @@ import httpx
 | 
				
			|||||||
from flask import Flask
 | 
					from flask import Flask
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from test_site import db
 | 
					from test_site import db
 | 
				
			||||||
from testlib_voucher import Accounts, match_voucher_detail, NEXT_URI
 | 
					from testlib_journal_entry import Accounts, match_journal_entry_detail, \
 | 
				
			||||||
 | 
					    NEXT_URI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class JournalEntryLineItemData:
 | 
					class JournalEntryLineItemData:
 | 
				
			||||||
@@ -41,7 +42,7 @@ class JournalEntryLineItemData:
 | 
				
			|||||||
        :param amount: The amount.
 | 
					        :param amount: The amount.
 | 
				
			||||||
        :param original_line_item: The original journal entry line item.
 | 
					        :param original_line_item: The original journal entry line item.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.voucher: VoucherData | None = None
 | 
					        self.journal_entry: JournalEntryData | None = None
 | 
				
			||||||
        self.id: int = -1
 | 
					        self.id: int = -1
 | 
				
			||||||
        self.no: int = -1
 | 
					        self.no: int = -1
 | 
				
			||||||
        self.original_line_item: JournalEntryLineItemData | None \
 | 
					        self.original_line_item: JournalEntryLineItemData | None \
 | 
				
			||||||
@@ -75,11 +76,11 @@ class JournalEntryLineItemData:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CurrencyData:
 | 
					class CurrencyData:
 | 
				
			||||||
    """The voucher currency data."""
 | 
					    """The journal entry currency data."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, currency: str, debit: list[JournalEntryLineItemData],
 | 
					    def __init__(self, currency: str, debit: list[JournalEntryLineItemData],
 | 
				
			||||||
                 credit: list[JournalEntryLineItemData]):
 | 
					                 credit: list[JournalEntryLineItemData]):
 | 
				
			||||||
        """Constructs the voucher currency data.
 | 
					        """Constructs the journal entry currency data.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param currency: The currency code.
 | 
					        :param currency: The currency code.
 | 
				
			||||||
        :param debit: The debit line items.
 | 
					        :param debit: The debit line items.
 | 
				
			||||||
@@ -106,14 +107,14 @@ class CurrencyData:
 | 
				
			|||||||
        return form
 | 
					        return form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VoucherData:
 | 
					class JournalEntryData:
 | 
				
			||||||
    """The voucher data."""
 | 
					    """The journal entry data."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, days: int, currencies: list[CurrencyData]):
 | 
					    def __init__(self, days: int, currencies: list[CurrencyData]):
 | 
				
			||||||
        """Constructs a voucher.
 | 
					        """Constructs a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param days: The number of days before today.
 | 
					        :param days: The number of days before today.
 | 
				
			||||||
        :param currencies: The voucher currency data.
 | 
					        :param currencies: The journal entry currency data.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.id: int = -1
 | 
					        self.id: int = -1
 | 
				
			||||||
        self.days: int = days
 | 
					        self.days: int = days
 | 
				
			||||||
@@ -121,38 +122,38 @@ class VoucherData:
 | 
				
			|||||||
        self.note: str | None = None
 | 
					        self.note: str | None = None
 | 
				
			||||||
        for currency in self.currencies:
 | 
					        for currency in self.currencies:
 | 
				
			||||||
            for line_item in currency.debit:
 | 
					            for line_item in currency.debit:
 | 
				
			||||||
                line_item.voucher = self
 | 
					                line_item.journal_entry = self
 | 
				
			||||||
            for line_item in currency.credit:
 | 
					            for line_item in currency.credit:
 | 
				
			||||||
                line_item.voucher = self
 | 
					                line_item.journal_entry = self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def new_form(self, csrf_token: str) -> dict[str, str]:
 | 
					    def new_form(self, csrf_token: str) -> dict[str, str]:
 | 
				
			||||||
        """Returns the voucher as a creation form.
 | 
					        """Returns the journal entry as a creation form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param csrf_token: The CSRF token.
 | 
					        :param csrf_token: The CSRF token.
 | 
				
			||||||
        :return: The voucher as a creation form.
 | 
					        :return: The journal entry as a creation form.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self.__form(csrf_token, is_update=False)
 | 
					        return self.__form(csrf_token, is_update=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update_form(self, csrf_token: str) -> dict[str, str]:
 | 
					    def update_form(self, csrf_token: str) -> dict[str, str]:
 | 
				
			||||||
        """Returns the voucher as a update form.
 | 
					        """Returns the journal entry as an update form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param csrf_token: The CSRF token.
 | 
					        :param csrf_token: The CSRF token.
 | 
				
			||||||
        :return: The voucher as a update form.
 | 
					        :return: The journal entry as an update form.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self.__form(csrf_token, is_update=True)
 | 
					        return self.__form(csrf_token, is_update=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __form(self, csrf_token: str, is_update: bool = False) \
 | 
					    def __form(self, csrf_token: str, is_update: bool = False) \
 | 
				
			||||||
            -> dict[str, str]:
 | 
					            -> dict[str, str]:
 | 
				
			||||||
        """Returns the voucher as a form.
 | 
					        """Returns the journal entry as a form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param csrf_token: The CSRF token.
 | 
					        :param csrf_token: The CSRF token.
 | 
				
			||||||
        :param is_update: True for an update operation, or False otherwise
 | 
					        :param is_update: True for an update operation, or False otherwise
 | 
				
			||||||
        :return: The voucher as a form.
 | 
					        :return: The journal entry as a form.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        voucher_date: date = date.today() - timedelta(days=self.days)
 | 
					        journal_entry_date: date = date.today() - timedelta(days=self.days)
 | 
				
			||||||
        form: dict[str, str] = {"csrf_token": csrf_token,
 | 
					        form: dict[str, str] = {"csrf_token": csrf_token,
 | 
				
			||||||
                                "next": NEXT_URI,
 | 
					                                "next": NEXT_URI,
 | 
				
			||||||
                                "date": voucher_date.isoformat()}
 | 
					                                "date": journal_entry_date.isoformat()}
 | 
				
			||||||
        for i in range(len(self.currencies)):
 | 
					        for i in range(len(self.currencies)):
 | 
				
			||||||
            form.update(self.currencies[i].form(i + 1, is_update))
 | 
					            form.update(self.currencies[i].form(i + 1, is_update))
 | 
				
			||||||
        if self.note is not None:
 | 
					        if self.note is not None:
 | 
				
			||||||
@@ -207,24 +208,24 @@ class TestData:
 | 
				
			|||||||
        self.e_p_or4d, self.e_p_or4c = couple(
 | 
					        self.e_p_or4d, self.e_p_or4c = couple(
 | 
				
			||||||
            "Envelop", "0.9", Accounts.OFFICE, Accounts.PAYABLE)
 | 
					            "Envelop", "0.9", Accounts.OFFICE, Accounts.PAYABLE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Original vouchers
 | 
					        # Original journal entries
 | 
				
			||||||
        self.v_r_or1: VoucherData = VoucherData(
 | 
					        self.v_r_or1: JournalEntryData = JournalEntryData(
 | 
				
			||||||
            50, [CurrencyData("USD", [self.e_r_or1d, self.e_r_or4d],
 | 
					            50, [CurrencyData("USD", [self.e_r_or1d, self.e_r_or4d],
 | 
				
			||||||
                              [self.e_r_or1c, self.e_r_or4c])])
 | 
					                              [self.e_r_or1c, self.e_r_or4c])])
 | 
				
			||||||
        self.v_r_or2: VoucherData = VoucherData(
 | 
					        self.v_r_or2: JournalEntryData = JournalEntryData(
 | 
				
			||||||
            30, [CurrencyData("USD", [self.e_r_or2d, self.e_r_or3d],
 | 
					            30, [CurrencyData("USD", [self.e_r_or2d, self.e_r_or3d],
 | 
				
			||||||
                              [self.e_r_or2c, self.e_r_or3c])])
 | 
					                              [self.e_r_or2c, self.e_r_or3c])])
 | 
				
			||||||
        self.v_p_or1: VoucherData = VoucherData(
 | 
					        self.v_p_or1: JournalEntryData = JournalEntryData(
 | 
				
			||||||
            40, [CurrencyData("USD", [self.e_p_or1d, self.e_p_or4d],
 | 
					            40, [CurrencyData("USD", [self.e_p_or1d, self.e_p_or4d],
 | 
				
			||||||
                              [self.e_p_or1c, self.e_p_or4c])])
 | 
					                              [self.e_p_or1c, self.e_p_or4c])])
 | 
				
			||||||
        self.v_p_or2: VoucherData = VoucherData(
 | 
					        self.v_p_or2: JournalEntryData = JournalEntryData(
 | 
				
			||||||
            20, [CurrencyData("USD", [self.e_p_or2d, self.e_p_or3d],
 | 
					            20, [CurrencyData("USD", [self.e_p_or2d, self.e_p_or3d],
 | 
				
			||||||
                              [self.e_p_or2c, self.e_p_or3c])])
 | 
					                              [self.e_p_or2c, self.e_p_or3c])])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.__add_voucher(self.v_r_or1)
 | 
					        self.__add_journal_entry(self.v_r_or1)
 | 
				
			||||||
        self.__add_voucher(self.v_r_or2)
 | 
					        self.__add_journal_entry(self.v_r_or2)
 | 
				
			||||||
        self.__add_voucher(self.v_p_or1)
 | 
					        self.__add_journal_entry(self.v_p_or1)
 | 
				
			||||||
        self.__add_voucher(self.v_p_or2)
 | 
					        self.__add_journal_entry(self.v_p_or2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Receivable offset items
 | 
					        # Receivable offset items
 | 
				
			||||||
        self.e_r_of1d, self.e_r_of1c = couple(
 | 
					        self.e_r_of1d, self.e_r_of1c = couple(
 | 
				
			||||||
@@ -260,52 +261,55 @@ class TestData:
 | 
				
			|||||||
            "Envelop", "0.9", Accounts.PAYABLE, Accounts.CASH)
 | 
					            "Envelop", "0.9", Accounts.PAYABLE, Accounts.CASH)
 | 
				
			||||||
        self.e_p_of5d.original_line_item = self.e_p_or4c
 | 
					        self.e_p_of5d.original_line_item = self.e_p_or4c
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Offset vouchers
 | 
					        # Offset journal entries
 | 
				
			||||||
        self.v_r_of1: VoucherData = VoucherData(
 | 
					        self.v_r_of1: JournalEntryData = JournalEntryData(
 | 
				
			||||||
            25, [CurrencyData("USD", [self.e_r_of1d], [self.e_r_of1c])])
 | 
					            25, [CurrencyData("USD", [self.e_r_of1d], [self.e_r_of1c])])
 | 
				
			||||||
        self.v_r_of2: VoucherData = VoucherData(
 | 
					        self.v_r_of2: JournalEntryData = JournalEntryData(
 | 
				
			||||||
            20, [CurrencyData("USD",
 | 
					            20, [CurrencyData("USD",
 | 
				
			||||||
                              [self.e_r_of2d, self.e_r_of3d, self.e_r_of4d],
 | 
					                              [self.e_r_of2d, self.e_r_of3d, self.e_r_of4d],
 | 
				
			||||||
                              [self.e_r_of2c, self.e_r_of3c, self.e_r_of4c])])
 | 
					                              [self.e_r_of2c, self.e_r_of3c, self.e_r_of4c])])
 | 
				
			||||||
        self.v_r_of3: VoucherData = VoucherData(
 | 
					        self.v_r_of3: JournalEntryData = JournalEntryData(
 | 
				
			||||||
            15, [CurrencyData("USD", [self.e_r_of5d], [self.e_r_of5c])])
 | 
					            15, [CurrencyData("USD", [self.e_r_of5d], [self.e_r_of5c])])
 | 
				
			||||||
        self.v_p_of1: VoucherData = VoucherData(
 | 
					        self.v_p_of1: JournalEntryData = JournalEntryData(
 | 
				
			||||||
            15, [CurrencyData("USD", [self.e_p_of1d], [self.e_p_of1c])])
 | 
					            15, [CurrencyData("USD", [self.e_p_of1d], [self.e_p_of1c])])
 | 
				
			||||||
        self.v_p_of2: VoucherData = VoucherData(
 | 
					        self.v_p_of2: JournalEntryData = JournalEntryData(
 | 
				
			||||||
            10, [CurrencyData("USD",
 | 
					            10, [CurrencyData("USD",
 | 
				
			||||||
                              [self.e_p_of2d, self.e_p_of3d, self.e_p_of4d],
 | 
					                              [self.e_p_of2d, self.e_p_of3d, self.e_p_of4d],
 | 
				
			||||||
                              [self.e_p_of2c, self.e_p_of3c, self.e_p_of4c])])
 | 
					                              [self.e_p_of2c, self.e_p_of3c, self.e_p_of4c])])
 | 
				
			||||||
        self.v_p_of3: VoucherData = VoucherData(
 | 
					        self.v_p_of3: JournalEntryData = JournalEntryData(
 | 
				
			||||||
            5, [CurrencyData("USD", [self.e_p_of5d], [self.e_p_of5c])])
 | 
					            5, [CurrencyData("USD", [self.e_p_of5d], [self.e_p_of5c])])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.__add_voucher(self.v_r_of1)
 | 
					        self.__add_journal_entry(self.v_r_of1)
 | 
				
			||||||
        self.__add_voucher(self.v_r_of2)
 | 
					        self.__add_journal_entry(self.v_r_of2)
 | 
				
			||||||
        self.__add_voucher(self.v_r_of3)
 | 
					        self.__add_journal_entry(self.v_r_of3)
 | 
				
			||||||
        self.__add_voucher(self.v_p_of1)
 | 
					        self.__add_journal_entry(self.v_p_of1)
 | 
				
			||||||
        self.__add_voucher(self.v_p_of2)
 | 
					        self.__add_journal_entry(self.v_p_of2)
 | 
				
			||||||
        self.__add_voucher(self.v_p_of3)
 | 
					        self.__add_journal_entry(self.v_p_of3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __add_voucher(self, voucher_data: VoucherData) -> None:
 | 
					    def __add_journal_entry(self, journal_entry_data: JournalEntryData) \
 | 
				
			||||||
        """Adds a voucher.
 | 
					            -> None:
 | 
				
			||||||
 | 
					        """Adds a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param voucher_data: The voucher data.
 | 
					        :param journal_entry_data: The journal entry data.
 | 
				
			||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from accounting.models import Voucher
 | 
					        from accounting.models import JournalEntry
 | 
				
			||||||
        store_uri: str = "/accounting/vouchers/store/transfer"
 | 
					        store_uri: str = "/accounting/journal-entries/store/transfer"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response: httpx.Response = self.client.post(
 | 
					        response: httpx.Response = self.client.post(
 | 
				
			||||||
            store_uri, data=voucher_data.new_form(self.csrf_token))
 | 
					            store_uri, data=journal_entry_data.new_form(self.csrf_token))
 | 
				
			||||||
        assert response.status_code == 302
 | 
					        assert response.status_code == 302
 | 
				
			||||||
        voucher_id: int = match_voucher_detail(response.headers["Location"])
 | 
					        journal_entry_id: int \
 | 
				
			||||||
        voucher_data.id = voucher_id
 | 
					            = match_journal_entry_detail(response.headers["Location"])
 | 
				
			||||||
 | 
					        journal_entry_data.id = journal_entry_id
 | 
				
			||||||
        with self.app.app_context():
 | 
					        with self.app.app_context():
 | 
				
			||||||
            voucher: Voucher | None = db.session.get(Voucher, voucher_id)
 | 
					            journal_entry: JournalEntry | None \
 | 
				
			||||||
            assert voucher is not None
 | 
					                = db.session.get(JournalEntry, journal_entry_id)
 | 
				
			||||||
            for i in range(len(voucher.currencies)):
 | 
					            assert journal_entry is not None
 | 
				
			||||||
                for j in range(len(voucher.currencies[i].debit)):
 | 
					            for i in range(len(journal_entry.currencies)):
 | 
				
			||||||
                    voucher_data.currencies[i].debit[j].id \
 | 
					                for j in range(len(journal_entry.currencies[i].debit)):
 | 
				
			||||||
                        = voucher.currencies[i].debit[j].id
 | 
					                    journal_entry_data.currencies[i].debit[j].id \
 | 
				
			||||||
                for j in range(len(voucher.currencies[i].credit)):
 | 
					                        = journal_entry.currencies[i].debit[j].id
 | 
				
			||||||
                    voucher_data.currencies[i].credit[j].id \
 | 
					                for j in range(len(journal_entry.currencies[i].credit)):
 | 
				
			||||||
                        = voucher.currencies[i].credit[j].id
 | 
					                    journal_entry_data.currencies[i].credit[j].id \
 | 
				
			||||||
 | 
					                        = journal_entry.currencies[i].credit[j].id
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user