Renamed "voucher" to "journal entry".

This commit is contained in:
依瑪貓 2023-03-20 22:08:58 +08:00
parent 8f909965a9
commit b1af1d7425
74 changed files with 1956 additions and 1816 deletions

View File

@ -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)

View File

@ -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")

View 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()

View File

@ -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

View File

@ -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,

View File

@ -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] \

View File

@ -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")

View 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

View File

@ -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)

View File

@ -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.
""" """

View File

@ -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

View 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

View File

@ -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 \

View 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")

View File

@ -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)])

View File

@ -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

View File

@ -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,

View File

@ -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."""

View File

@ -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)

View File

@ -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.

View File

@ -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."""

View File

@ -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):

View File

@ -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)

View File

@ -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:

View File

@ -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;
} }

View File

@ -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) {

View File

@ -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;

View File

@ -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.

View File

@ -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
*/ */

View File

@ -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 %}

View File

@ -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">

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -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 }}">

View File

@ -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 %}

View File

@ -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">

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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">

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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" %}

View File

@ -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>

View File

@ -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>

View File

@ -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" %}

View File

@ -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" %}

View File

@ -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>

View File

@ -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" %}

View File

@ -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>

View File

@ -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" %}

View File

@ -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."""

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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