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
currency.init_app(app, bp)
from . import voucher
voucher.init_app(app, bp)
from . import journal_entry
journal_entry.init_app(app, bp)
from . import report
report.init_app(app, bp)

View File

@ -14,7 +14,7 @@
# 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 voucher management.
"""The journal entry management.
"""
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.
:return: None.
"""
from .converters import VoucherConverter, VoucherTypeConverter, \
from .converters import JournalEntryConverter, JournalEntryTypeConverter, \
DateConverter
app.url_map.converters["voucher"] = VoucherConverter
app.url_map.converters["voucherType"] = VoucherTypeConverter
app.url_map.converters["journalEntry"] = JournalEntryConverter
app.url_map.converters["journalEntryType"] = JournalEntryTypeConverter
app.url_map.converters["date"] = DateConverter
from .views import bp as voucher_bp
bp.register_blueprint(voucher_bp, url_prefix="/vouchers")
from .views import bp as journal_entry_bp
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.
# See the License for the specific language governing permissions and
# 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 .voucher import VoucherForm, CashReceiptVoucherForm, \
CashDisbursementVoucherForm, TransferVoucherForm
from .reorder import sort_journal_entries_in, JournalEntryReorderForm
from .journal_entry import JournalEntryForm, CashReceiptJournalEntryForm, \
CashDisbursementJournalEntryForm, TransferJournalEntryForm

View File

@ -14,7 +14,7 @@
# 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 currency sub-forms for the voucher management.
"""The currency sub-forms for the journal entry management.
"""
from decimal import Decimal
@ -29,7 +29,7 @@ from wtforms.validators import DataRequired
from accounting import db
from accounting.locale import lazy_gettext
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.strip_text import strip_text
from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
@ -123,9 +123,9 @@ class IsBalanced:
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()
"""The order in the voucher."""
"""The order in the journal entry."""
code = StringField()
"""The currency code."""
whole_form = BooleanField()
@ -169,9 +169,10 @@ class CurrencyForm(FlaskForm):
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()
"""The order in the voucher."""
"""The order in the journal entry."""
code = StringField(
filters=[strip_text],
validators=[CURRENCY_REQUIRED,
@ -206,9 +207,10 @@ class CashReceiptCurrencyForm(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()
"""The order in the voucher."""
"""The order in the journal entry."""
code = StringField(
filters=[strip_text],
validators=[CURRENCY_REQUIRED,
@ -243,9 +245,9 @@ class CashDisbursementCurrencyForm(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()
"""The order in the voucher."""
"""The order in the journal entry."""
code = StringField(
filters=[strip_text],
validators=[CURRENCY_REQUIRED,

View File

@ -14,7 +14,7 @@
# 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 voucher forms for the voucher management.
"""The journal entry forms for the journal entry management.
"""
import datetime as dt
@ -30,19 +30,19 @@ from wtforms.validators import DataRequired, ValidationError
from accounting import db
from accounting.locale import lazy_gettext
from accounting.models import Voucher, Account, JournalEntryLineItem, \
VoucherCurrency
from accounting.voucher.utils.account_option import AccountOption
from accounting.voucher.utils.original_line_items import \
from accounting.models import JournalEntry, Account, JournalEntryLineItem, \
JournalEntryCurrency
from accounting.journal_entry.utils.account_option import AccountOption
from accounting.journal_entry.utils.original_line_items import \
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.strip_text import strip_multiline_text
from accounting.utils.user import get_current_user_pk
from .currency import CurrencyForm, CashReceiptCurrencyForm, \
CashDisbursementCurrencyForm, TransferCurrencyForm
from .line_item import LineItemForm, DebitLineItemForm, CreditLineItemForm
from .reorder import sort_vouchers_in
from .reorder import sort_journal_entries_in
DATE_REQUIRED: DataRequired = DataRequired(
lazy_gettext("Please fill in the date."))
@ -54,7 +54,7 @@ class NotBeforeOriginalLineItems:
original line items."""
def __call__(self, form: FlaskForm, field: DateField) -> None:
assert isinstance(form, VoucherForm)
assert isinstance(form, JournalEntryForm)
if field.data is None:
return
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."""
def __call__(self, form: FlaskForm, field: DateField) -> None:
assert isinstance(form, VoucherForm)
assert isinstance(form, JournalEntryForm)
if field.data is None:
return
max_date: dt.date | None = form.max_date
@ -92,7 +92,7 @@ class CannotDeleteOriginalLineItemsWithOffset:
"""The validator to check the original line items with offset."""
def __call__(self, form: FlaskForm, field: FieldList) -> None:
assert isinstance(form, VoucherForm)
assert isinstance(form, JournalEntryForm)
if form.obj is None:
return
existing_matched_original_line_item_id: set[int] \
@ -105,8 +105,8 @@ class CannotDeleteOriginalLineItemsWithOffset:
"Line items with offset cannot be deleted."))
class VoucherForm(FlaskForm):
"""The base form to create or edit a voucher."""
class JournalEntryForm(FlaskForm):
"""The base form to create or edit a journal entry."""
date = DateField()
"""The date."""
currencies = FieldList(FormField(CurrencyForm))
@ -115,20 +115,20 @@ class VoucherForm(FlaskForm):
"""The note."""
def __init__(self, *args, **kwargs):
"""Constructs a base voucher form.
"""Constructs a base journal entry form.
:param args: The arguments.
:param kwargs: The keyword arguments.
"""
super().__init__(*args, **kwargs)
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
"""The line item collector. The default is the base abstract
collector only to provide the correct type. The subclass forms should
provide their own collectors."""
self.obj: Voucher | None = kwargs.get("obj")
"""The voucher, when editing an existing one."""
self.obj: JournalEntry | None = kwargs.get("obj")
"""The journal entry, when editing an existing one."""
self._is_need_payable: bool = False
"""Whether we need the payable original line items."""
self._is_need_receivable: bool = False
@ -140,17 +140,17 @@ class VoucherForm(FlaskForm):
"""The original line items whose net balances were exceeded by the
amounts in the line item sub-forms."""
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:
"""Populates the form data into a voucher object.
def populate_obj(self, obj: JournalEntry) -> None:
"""Populates the form data into a journal entry object.
:param obj: The voucher object.
:param obj: The journal entry object.
:return: None.
"""
is_new: bool = obj.id is None
if is_new:
obj.id = new_id(Voucher)
obj.id = new_id(JournalEntry)
self.date: DateField
self.__set_date(obj, self.date.data)
obj.note = self.note.data
@ -185,31 +185,31 @@ class VoucherForm(FlaskForm):
line_items.extend(currency.line_items)
return line_items
def __set_date(self, obj: Voucher, new_date: dt.date) -> None:
"""Sets the voucher date and number.
def __set_date(self, obj: JournalEntry, new_date: dt.date) -> None:
"""Sets the journal entry date and number.
:param obj: The voucher object.
:param obj: The journal entry object.
:param new_date: The new date.
:return: None.
"""
if obj.date is None or obj.date != new_date:
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:
db_min_no: int | None = db.session.scalar(
sa.select(sa.func.min(Voucher.no))
.filter(Voucher.date == new_date))
sa.select(sa.func.min(JournalEntry.no))
.filter(JournalEntry.date == new_date))
if db_min_no is None:
obj.date = new_date
obj.no = 1
else:
obj.date = new_date
obj.no = db_min_no - 1
sort_vouchers_in(new_date)
sort_journal_entries_in(new_date)
else:
sort_vouchers_in(new_date, obj.id)
count: int = Voucher.query\
.filter(Voucher.date == new_date).count()
sort_journal_entries_in(new_date, obj.id)
count: int = JournalEntry.query\
.filter(JournalEntry.date == new_date).count()
obj.date = new_date
obj.no = count + 1
@ -289,7 +289,7 @@ class VoucherForm(FlaskForm):
if x.original_line_item_id.data is not None}
if len(original_line_item_id) == 0:
return None
select: sa.Select = sa.select(sa.func.max(Voucher.date))\
select: sa.Select = sa.select(sa.func.max(JournalEntry.date))\
.join(JournalEntryLineItem)\
.filter(JournalEntryLineItem.id.in_(original_line_item_id))
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
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)\
.filter(JournalEntryLineItem.original_line_item_id
.in_(line_item_id))
return db.session.scalar(select)
T = t.TypeVar("T", bound=VoucherForm)
"""A voucher form variant."""
T = t.TypeVar("T", bound=JournalEntryForm)
"""A journal entry form variant."""
class LineItemCollector(t.Generic[T], ABC):
"""The line item collector."""
def __init__(self, form: T, obj: Voucher):
def __init__(self, form: T, obj: JournalEntry):
"""Constructs the line item collector.
:param form: The voucher form.
:param obj: The voucher.
:param form: The journal entry form.
:param obj: The journal entry.
"""
self.form: T = form
"""The voucher form."""
self.__obj: Voucher = obj
"""The voucher object."""
"""The journal entry form."""
self.__obj: JournalEntry = obj
"""The journal entry object."""
self.__line_items: list[JournalEntryLineItem] = list(obj.line_items)
"""The existing line items."""
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] \
= {x.id: x.no for x in self.__line_items}
"""A dictionary from the line item number to their line items."""
self.__currencies: list[VoucherCurrency] = obj.currencies
"""The currencies in the voucher."""
self.__currencies: list[JournalEntryCurrency] = obj.currencies
"""The currencies in the journal entry."""
self._debit_no: int = 1
"""The number index for the debit line items."""
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,
currency_code: str, no: int) -> None:
"""Composes the cash line item at the other debit or credit of the cash
voucher.
"""Composes the cash line item at the other debit or credit of the
cash journal entry.
:param forms: The line item forms in the same currency.
:param is_debit: True for a cash receipt voucher, or False for a
cash disbursement voucher.
:param is_debit: True for a cash receipt journal entry, or False for a
cash disbursement journal entry.
:param currency_code: The code of the currency.
:param no: The number of the line item.
:return: None.
@ -449,8 +449,8 @@ class LineItemCollector(t.Generic[T], ABC):
ord_by_form.get(x)))
class CashReceiptVoucherForm(VoucherForm):
"""The form to create or edit a cash receipt voucher."""
class CashReceiptJournalEntryForm(JournalEntryForm):
"""The form to create or edit a cash receipt journal entry."""
date = DateField(
validators=[DATE_REQUIRED,
NotBeforeOriginalLineItems(),
@ -469,8 +469,8 @@ class CashReceiptVoucherForm(VoucherForm):
super().__init__(*args, **kwargs)
self._is_need_receivable = True
class Collector(LineItemCollector[CashReceiptVoucherForm]):
"""The line item collector for the cash receipt vouchers."""
class Collector(LineItemCollector[CashReceiptJournalEntryForm]):
"""The line item collector for the cash receipt journal entries."""
def collect(self) -> None:
currencies: list[CashReceiptCurrencyForm] \
@ -495,8 +495,8 @@ class CashReceiptVoucherForm(VoucherForm):
self.collector = Collector
class CashDisbursementVoucherForm(VoucherForm):
"""The form to create or edit a cash disbursement voucher."""
class CashDisbursementJournalEntryForm(JournalEntryForm):
"""The form to create or edit a cash disbursement journal entry."""
date = DateField(
validators=[DATE_REQUIRED,
NotBeforeOriginalLineItems(),
@ -516,8 +516,9 @@ class CashDisbursementVoucherForm(VoucherForm):
super().__init__(*args, **kwargs)
self._is_need_payable = True
class Collector(LineItemCollector[CashDisbursementVoucherForm]):
"""The line item collector for the cash disbursement vouchers."""
class Collector(LineItemCollector[CashDisbursementJournalEntryForm]):
"""The line item collector for the cash disbursement journal
entries."""
def collect(self) -> None:
currencies: list[CashDisbursementCurrencyForm] \
@ -542,8 +543,8 @@ class CashDisbursementVoucherForm(VoucherForm):
self.collector = Collector
class TransferVoucherForm(VoucherForm):
"""The form to create or edit a transfer voucher."""
class TransferJournalEntryForm(JournalEntryForm):
"""The form to create or edit a transfer journal entry."""
date = DateField(
validators=[DATE_REQUIRED,
NotBeforeOriginalLineItems(),
@ -563,8 +564,8 @@ class TransferVoucherForm(VoucherForm):
self._is_need_payable = True
self._is_need_receivable = True
class Collector(LineItemCollector[TransferVoucherForm]):
"""The line item collector for the transfer vouchers."""
class Collector(LineItemCollector[TransferJournalEntryForm]):
"""The line item collector for the transfer journal entries."""
def collect(self) -> None:
currencies: list[TransferCurrencyForm] \

View File

@ -14,7 +14,7 @@
# 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 line item sub-forms for the voucher management.
"""The line item sub-forms for the journal entry management.
"""
import re
@ -238,9 +238,9 @@ class NotExceedingOriginalLineItemNetBalance:
return
is_debit: bool = isinstance(form, DebitLineItemForm)
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 \
= {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(
(be(JournalEntryLineItem.is_debit == is_debit),
JournalEntryLineItem.amount),
@ -253,7 +253,7 @@ class NotExceedingOriginalLineItemNetBalance:
if offset_total_but_form is None:
offset_total_but_form = Decimal("0")
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
and x.amount != field and x.amount.data is not None])
net_balance: Decimal = original_line_item.amount \
@ -307,9 +307,9 @@ class LineItemForm(FlaskForm):
:param kwargs: The keyword arguments.
"""
super().__init__(*args, **kwargs)
from .voucher import VoucherForm
self.voucher_form: VoucherForm | None = None
"""The source voucher form."""
from .journal_entry import JournalEntryForm
self.journal_entry_form: JournalEntryForm | None = None
"""The source journal entry form."""
@property
def account_text(self) -> str:
@ -346,7 +346,7 @@ class LineItemForm(FlaskForm):
:return: The text representation of the original line item.
"""
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
def original_line_item_text(self) -> str | None:
@ -389,10 +389,11 @@ class LineItemForm(FlaskForm):
return JournalEntryLineItem.query\
.filter(JournalEntryLineItem.original_line_item_id
== self.eid.data)\
.options(selectinload(JournalEntryLineItem.voucher),
.options(selectinload(JournalEntryLineItem.journal_entry),
selectinload(JournalEntryLineItem.account),
selectinload(JournalEntryLineItem.offsets)
.selectinload(JournalEntryLineItem.voucher)).all()
.selectinload(
JournalEntryLineItem.journal_entry)).all()
setattr(self, "__offsets", get_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.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The template filters for the voucher management.
"""The template filters for the journal entry management.
"""
from decimal import Decimal
@ -26,10 +26,10 @@ from flask import request
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.
: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:
return uri
@ -43,10 +43,10 @@ def with_type(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.
: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)
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.
# See the License for the specific language governing permissions and
# 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.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The account option for the voucher management.
"""The account option for the journal entry management.
"""
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 accounting import db
from accounting.models import Account, Voucher, JournalEntryLineItem
from accounting.models import Account, JournalEntry, JournalEntryLineItem
from accounting.utils.cast import be
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()}
line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query\
.filter(JournalEntryLineItem.id.in_({x for x in net_balances}))\
.join(Voucher)\
.order_by(Voucher.date, JournalEntryLineItem.is_debit,
.join(JournalEntry)\
.order_by(JournalEntry.date, JournalEntryLineItem.is_debit,
JournalEntryLineItem.no)\
.options(selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.account),
selectinload(JournalEntryLineItem.voucher)).all()
selectinload(JournalEntryLineItem.journal_entry)).all()
for line_item in line_items:
line_item.net_balance = line_item.amount \
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."""
class VoucherCurrency:
"""A currency in a voucher."""
class JournalEntryCurrency:
"""A currency in a journal entry."""
def __init__(self, code: str, debit: list[JournalEntryLineItem],
credit: list[JournalEntryLineItem]):
"""Constructs the currency in the voucher.
"""Constructs the currency in the journal entry.
:param code: The currency code.
:param debit: The debit line items.
@ -492,13 +492,13 @@ class VoucherCurrency:
return sum([x.amount for x in self.credit])
class Voucher(db.Model):
"""A voucher."""
__tablename__ = "accounting_vouchers"
class JournalEntry(db.Model):
"""A journal entry."""
__tablename__ = "accounting_journal_entries"
"""The table name."""
id = db.Column(db.Integer, nullable=False, primary_key=True,
autoincrement=False)
"""The voucher ID."""
"""The journal entry ID."""
date = db.Column(db.Date, nullable=False)
"""The date."""
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)
"""The updator."""
line_items = db.relationship("JournalEntryLineItem",
back_populates="voucher")
back_populates="journal_entry")
"""The line items."""
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:
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:
return gettext("Cash Receipt Voucher#%(id)s", id=self.id)
return gettext("Transfer Voucher#%(id)s", id=self.id)
return gettext("Cash Receipt Journal Entry#%(id)s", id=self.id)
return gettext("Transfer Journal Entry#%(id)s", id=self.id)
@property
def currencies(self) -> list[VoucherCurrency]:
def currencies(self) -> list[JournalEntryCurrency]:
"""Returns the line items categorized by their currencies.
:return: The currency categories.
@ -555,18 +556,19 @@ class Voucher(db.Model):
codes.append(line_item.currency_code)
by_currency[line_item.currency_code] = []
by_currency[line_item.currency_code].append(line_item)
return [VoucherCurrency(code=x,
debit=[y for y in by_currency[x]
if y.is_debit],
credit=[y for y in by_currency[x]
if not y.is_debit])
return [JournalEntryCurrency(code=x,
debit=[y for y in by_currency[x]
if y.is_debit],
credit=[y for y in by_currency[x]
if not y.is_debit])
for x in codes]
@property
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:
if len(currency.debit) > 1:
@ -577,9 +579,9 @@ class Voucher(db.Model):
@property
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.
"""
for currency in self.currencies:
@ -591,9 +593,9 @@ class Voucher(db.Model):
@property
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"):
def has_offset() -> bool:
@ -605,12 +607,12 @@ class Voucher(db.Model):
return getattr(self, "__can_delete")
def delete(self) -> None:
"""Deletes the voucher.
"""Deletes the journal entry.
:return: None.
"""
JournalEntryLineItem.query\
.filter(JournalEntryLineItem.voucher_id == self.id).delete()
.filter(JournalEntryLineItem.journal_entry_id == self.id).delete()
db.session.delete(self)
@ -621,17 +623,18 @@ class JournalEntryLineItem(db.Model):
id = db.Column(db.Integer, nullable=False, primary_key=True,
autoincrement=False)
"""The line item ID."""
voucher_id = db.Column(db.Integer,
db.ForeignKey(Voucher.id, onupdate="CASCADE",
ondelete="CASCADE"),
nullable=False)
"""The voucher ID."""
voucher = db.relationship(Voucher, back_populates="line_items")
"""The voucher."""
journal_entry_id = db.Column(db.Integer,
db.ForeignKey(JournalEntry.id,
onupdate="CASCADE",
ondelete="CASCADE"),
nullable=False)
"""The journal entry ID."""
journal_entry = db.relationship(JournalEntry, back_populates="line_items")
"""The journal entry."""
is_debit = db.Column(db.Boolean, nullable=False)
"""True for a debit line item, or False for a credit line item."""
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,
db.ForeignKey(id, onupdate="CASCADE"),
nullable=True)
@ -670,7 +673,7 @@ class JournalEntryLineItem(db.Model):
from accounting.template_filters import format_date, format_amount
setattr(self, "__str",
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
else self.description,
amount=format_amount(self.amount)))
@ -755,13 +758,16 @@ class JournalEntryLineItem(db.Model):
frac: Decimal = (value - whole).normalize()
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
return ([description],
[str(voucher_day.year),
"{}/{}".format(voucher_day.year, voucher_day.month),
"{}/{}".format(voucher_day.month, voucher_day.day),
"{}/{}/{}".format(voucher_day.year, voucher_day.month,
voucher_day.day),
[str(journal_entry_day.year),
"{}/{}".format(journal_entry_day.year,
journal_entry_day.month),
"{}/{}".format(journal_entry_day.month,
journal_entry_day.day),
"{}/{}/{}".format(journal_entry_day.year,
journal_entry_day.month,
journal_entry_day.day),
format_amount(self.amount),
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
from datetime import date
from accounting.models import Voucher
from accounting.models import JournalEntry
from .period import Period
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
LastYear, Today, Yesterday, AllTime, TemplatePeriod, YearPeriod
@ -61,8 +61,8 @@ class PeriodChooser:
self.url_template: str = get_url(TemplatePeriod())
"""The URL template."""
first: Voucher | None \
= Voucher.query.order_by(Voucher.date).first()
first: JournalEntry | None \
= JournalEntry.query.order_by(JournalEntry.date).first()
start: date | None = None if first is None else first.date
# Attributes

View File

@ -24,7 +24,7 @@ from flask import render_template, Response
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, BaseAccount, Account, Voucher, \
from accounting.models import Currency, BaseAccount, Account, JournalEntry, \
JournalEntryLineItem
from accounting.report.period import Period, PeriodChooser
from accounting.report.utils.base_page_params import BasePageParams
@ -127,14 +127,14 @@ class AccountCollector:
= [JournalEntryLineItem.currency_code == self.__currency.code,
sa.or_(*sub_conditions)]
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(
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount)).label("balance")
select_balance: sa.Select \
= sa.select(Account.id, Account.base_code, Account.no,
balance_func)\
.join(Voucher).join(Account)\
.join(JournalEntry).join(Account)\
.filter(*conditions)\
.group_by(Account.id, Account.base_code, Account.no)\
.order_by(Account.base_code, Account.no)
@ -179,7 +179,7 @@ class AccountCollector:
return None
conditions: list[sa.BinaryExpression] \
= [JournalEntryLineItem.currency_code == self.__currency.code,
Voucher.date < self.__period.start]
JournalEntry.date < self.__period.start]
return self.__query_balance(conditions)
def __add_current_period(self) -> None:
@ -199,9 +199,9 @@ class AccountCollector:
conditions: list[sa.BinaryExpression] \
= [JournalEntryLineItem.currency_code == self.__currency.code]
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:
conditions.append(Voucher.date <= self.__period.end)
conditions.append(JournalEntry.date <= self.__period.end)
return self.__query_balance(conditions)
@staticmethod
@ -218,7 +218,7 @@ class AccountCollector:
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount))
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)
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.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.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport
@ -70,14 +71,14 @@ class ReportLineItem:
self.url: str | None = None
"""The URL to the journal entry line item."""
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.description = line_item.description
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.note = line_item.voucher.note
self.url = url_for("accounting.voucher.detail",
voucher=line_item.voucher)
self.note = line_item.journal_entry.note
self.url = url_for("accounting.journal-entry.detail",
journal_entry=line_item.journal_entry)
class LineItemCollector:
@ -120,11 +121,11 @@ class LineItemCollector:
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount))
select: sa.Select = sa.Select(balance_func)\
.join(Voucher).join(Account)\
.join(JournalEntry).join(Account)\
.filter(be(JournalEntryLineItem.currency_code
== self.__currency.code),
self.__account_condition,
Voucher.date < self.__period.start)
JournalEntry.date < self.__period.start)
balance: int | None = db.session.scalar(select)
if balance is None:
return None
@ -149,25 +150,26 @@ class LineItemCollector:
= [JournalEntryLineItem.currency_code == self.__currency.code,
self.__account_condition]
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:
conditions.append(Voucher.date <= self.__period.end)
voucher_with_account: sa.Select = sa.Select(Voucher.id).\
conditions.append(JournalEntry.date <= self.__period.end)
journal_entry_with_account: sa.Select = sa.Select(JournalEntry.id).\
join(JournalEntryLineItem).join(Account).filter(*conditions)
return [ReportLineItem(x)
for x in JournalEntryLineItem.query.join(Voucher).join(Account)
.filter(JournalEntryLineItem.voucher_id
.in_(voucher_with_account),
for x in JournalEntryLineItem.query
.join(JournalEntry).join(Account)
.filter(JournalEntryLineItem.journal_entry_id
.in_(journal_entry_with_account),
JournalEntryLineItem.currency_code
== self.__currency.code,
sa.not_(self.__account_condition))
.order_by(Voucher.date,
Voucher.no,
.order_by(JournalEntry.date,
JournalEntry.no,
JournalEntryLineItem.is_debit,
JournalEntryLineItem.no)
.options(selectinload(JournalEntryLineItem.account),
selectinload(JournalEntryLineItem.voucher))]
selectinload(JournalEntryLineItem.journal_entry))]
@property
def __account_condition(self) -> sa.BinaryExpression:
@ -216,7 +218,7 @@ class LineItemCollector:
class CSVRow(BaseCSVRow):
"""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,
description: str | None,
income: str | Decimal | None,
@ -225,7 +227,7 @@ class CSVRow(BaseCSVRow):
note: str | None):
"""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 description: The description.
:param income: The income.
@ -233,7 +235,7 @@ class CSVRow(BaseCSVRow):
:param balance: The balance.
:param note: The note.
"""
self.date: date | str | None = voucher_date
self.date: date | str | None = journal_entry_date
"""The date."""
self.account: str | None = account
"""The account."""

View File

@ -24,7 +24,7 @@ from flask import render_template, Response
from accounting import db
from accounting.locale import gettext
from accounting.models import Currency, BaseAccount, Account, Voucher, \
from accounting.models import Currency, BaseAccount, Account, JournalEntry, \
JournalEntryLineItem
from accounting.report.period import Period, PeriodChooser
from accounting.report.utils.base_page_params import BasePageParams
@ -259,14 +259,14 @@ class IncomeStatement(BaseReport):
= [JournalEntryLineItem.currency_code == self.__currency.code,
sa.or_(*sub_conditions)]
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:
conditions.append(Voucher.date <= self.__period.end)
conditions.append(JournalEntry.date <= self.__period.end)
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntryLineItem.is_debit, -JournalEntryLineItem.amount),
else_=JournalEntryLineItem.amount)).label("balance")
select_balances: sa.Select = sa.select(Account.id, balance_func)\
.join(Voucher).join(Account)\
.join(JournalEntry).join(Account)\
.filter(*conditions)\
.group_by(Account.id)\
.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 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.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport
@ -47,8 +48,8 @@ class ReportLineItem:
"""
self.line_item: JournalEntryLineItem = line_item
"""The journal entry line item."""
self.voucher: Voucher = line_item.voucher
"""The voucher."""
self.journal_entry: JournalEntry = line_item.journal_entry
"""The journal entry."""
self.currency: Currency = line_item.currency
"""The account."""
self.account: Account = line_item.account
@ -66,7 +67,7 @@ class ReportLineItem:
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, voucher_date: str | date,
def __init__(self, journal_entry_date: str | date,
currency: str,
account: str,
description: str | None,
@ -75,13 +76,13 @@ class CSVRow(BaseCSVRow):
note: str | None):
"""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 debit: The debit amount.
:param credit: The credit amount.
:param note: The note.
"""
self.date: str | date = voucher_date
self.date: str | date = journal_entry_date
"""The date."""
self.currency: str = currency
"""The currency."""
@ -155,9 +156,9 @@ def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:
gettext("Account"), gettext("Description"),
gettext("Debit"), gettext("Credit"),
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,
x.debit, x.credit, x.voucher.note)
x.debit, x.credit, x.journal_entry.note)
for x in line_items])
return rows
@ -183,18 +184,18 @@ class Journal(BaseReport):
"""
conditions: list[sa.BinaryExpression] = []
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:
conditions.append(Voucher.date <= self.__period.end)
return JournalEntryLineItem.query.join(Voucher)\
conditions.append(JournalEntry.date <= self.__period.end)
return JournalEntryLineItem.query.join(JournalEntry)\
.filter(*conditions)\
.order_by(Voucher.date,
Voucher.no,
.order_by(JournalEntry.date,
JournalEntry.no,
JournalEntryLineItem.is_debit.desc(),
JournalEntryLineItem.no)\
.options(selectinload(JournalEntryLineItem.account),
selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.voucher)).all()
selectinload(JournalEntryLineItem.journal_entry)).all()
def csv(self) -> Response:
"""Returns the report as CSV for download.

View File

@ -26,7 +26,8 @@ from sqlalchemy.orm import selectinload
from accounting import db
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.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport
@ -67,13 +68,13 @@ class ReportLineItem:
self.url: str | None = None
"""The URL to the journal entry line item."""
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.debit = line_item.amount if line_item.is_debit else None
self.credit = None if line_item.is_debit else line_item.amount
self.note = line_item.voucher.note
self.url = url_for("accounting.voucher.detail",
voucher=line_item.voucher)
self.note = line_item.journal_entry.note
self.url = url_for("accounting.journal-entry.detail",
journal_entry=line_item.journal_entry)
class LineItemCollector:
@ -116,12 +117,12 @@ class LineItemCollector:
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntryLineItem.is_debit, 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
== self.__currency.code),
be(JournalEntryLineItem.account_id
== self.__account.id),
Voucher.date < self.__period.start)
JournalEntry.date < self.__period.start)
balance: int | None = db.session.scalar(select)
if balance is None:
return None
@ -145,17 +146,18 @@ class LineItemCollector:
= [JournalEntryLineItem.currency_code == self.__currency.code,
JournalEntryLineItem.account_id == self.__account.id]
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:
conditions.append(Voucher.date <= self.__period.end)
conditions.append(JournalEntry.date <= self.__period.end)
return [ReportLineItem(x) for x in JournalEntryLineItem.query
.join(Voucher)
.join(JournalEntry)
.filter(*conditions)
.order_by(Voucher.date,
Voucher.no,
.order_by(JournalEntry.date,
JournalEntry.no,
JournalEntryLineItem.is_debit.desc(),
JournalEntryLineItem.no)
.options(selectinload(JournalEntryLineItem.voucher)).all()]
.options(selectinload(JournalEntryLineItem.journal_entry))
.all()]
def __get_total(self) -> ReportLineItem | None:
"""Composes the total line item.
@ -197,7 +199,7 @@ class LineItemCollector:
class CSVRow(BaseCSVRow):
"""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,
debit: str | Decimal | None,
credit: str | Decimal | None,
@ -205,14 +207,14 @@ class CSVRow(BaseCSVRow):
note: str | None):
"""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 debit: The debit amount.
:param credit: The credit amount.
:param balance: The balance.
:param note: The note.
"""
self.date: date | str | None = voucher_date
self.date: date | str | None = journal_entry_date
"""The date."""
self.description: str | None = description
"""The description."""

View File

@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
from accounting.locale import gettext
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_report import BaseReport
from accounting.report.utils.csv_export import csv_download
@ -62,22 +62,23 @@ class LineItemCollector:
self.__get_account_condition(k)),
JournalEntryLineItem.currency_code.in_(
self.__get_currency_condition(k)),
JournalEntryLineItem.voucher_id.in_(
self.__get_voucher_condition(k))]
JournalEntryLineItem.journal_entry_id.in_(
self.__get_journal_entry_condition(k))]
try:
sub_conditions.append(
JournalEntryLineItem.amount == Decimal(k))
except ArithmeticError:
pass
conditions.append(sa.or_(*sub_conditions))
return JournalEntryLineItem.query.join(Voucher).filter(*conditions)\
.order_by(Voucher.date,
Voucher.no,
return JournalEntryLineItem.query.join(JournalEntry)\
.filter(*conditions)\
.order_by(JournalEntry.date,
JournalEntry.no,
JournalEntryLineItem.is_debit,
JournalEntryLineItem.no)\
.options(selectinload(JournalEntryLineItem.account),
selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.voucher)).all()
selectinload(JournalEntryLineItem.journal_entry)).all()
@staticmethod
def __get_account_condition(k: str) -> sa.Select:
@ -116,35 +117,40 @@ class LineItemCollector:
Currency.code.in_(select_l10n)))
@staticmethod
def __get_voucher_condition(k: str) -> sa.Select:
"""Composes and returns the condition to filter the voucher.
def __get_journal_entry_condition(k: str) -> sa.Select:
"""Composes and returns the condition to filter the journal entry.
: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)]
voucher_date: datetime
conditions: list[sa.BinaryExpression] = [JournalEntry.note.contains(k)]
journal_entry_date: datetime
try:
voucher_date = datetime.strptime(k, "%Y")
journal_entry_date = datetime.strptime(k, "%Y")
conditions.append(
be(sa.extract("year", Voucher.date) == voucher_date.year))
be(sa.extract("year", JournalEntry.date)
== journal_entry_date.year))
except ValueError:
pass
try:
voucher_date = datetime.strptime(k, "%Y/%m")
journal_entry_date = datetime.strptime(k, "%Y/%m")
conditions.append(sa.and_(
sa.extract("year", Voucher.date) == voucher_date.year,
sa.extract("month", Voucher.date) == voucher_date.month))
sa.extract("year", JournalEntry.date)
== journal_entry_date.year,
sa.extract("month", JournalEntry.date)
== journal_entry_date.month))
except ValueError:
pass
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_(
sa.extract("month", Voucher.date) == voucher_date.month,
sa.extract("day", Voucher.date) == voucher_date.day))
sa.extract("month", JournalEntry.date)
== journal_entry_date.month,
sa.extract("day", JournalEntry.date)
== journal_entry_date.day))
except ValueError:
pass
return sa.select(Voucher.id).filter(sa.or_(*conditions))
return sa.select(JournalEntry.id).filter(sa.or_(*conditions))
class PageParams(BasePageParams):

View File

@ -24,7 +24,8 @@ from flask import Response, render_template
from accounting import db
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.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport
@ -180,14 +181,14 @@ class TrialBalance(BaseReport):
conditions: list[sa.BinaryExpression] \
= [JournalEntryLineItem.currency_code == self.__currency.code]
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:
conditions.append(Voucher.date <= self.__period.end)
conditions.append(JournalEntry.date <= self.__period.end)
balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount)).label("balance")
select_balances: sa.Select = sa.select(Account.id, balance_func)\
.join(Voucher).join(Account)\
.join(JournalEntry).join(Account)\
.filter(*conditions)\
.group_by(Account.id)\
.order_by(Account.base_code, Account.no)

View File

@ -27,7 +27,7 @@ from flask import request
from accounting import db
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 .report_chooser import ReportChooser
@ -52,12 +52,12 @@ class BasePageParams(ABC):
"""
@property
def voucher_types(self) -> t.Type[VoucherType]:
"""Returns the voucher types.
def journal_entry_types(self) -> t.Type[JournalEntryType]:
"""Returns the journal entry types.
:return: The voucher types.
:return: The journal entry types.
"""
return VoucherType
return JournalEntryType
@property
def csv_uri(self) -> str:

View File

@ -149,7 +149,7 @@
overflow-y: scroll;
}
/** The voucher management */
/** The journal entry management */
.accounting-currency-control {
background-color: transparent;
}

View File

@ -1,5 +1,5 @@
/* 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.
@ -23,14 +23,14 @@
"use strict";
document.addEventListener("DOMContentLoaded", () => {
VoucherForm.initialize();
JournalEntryForm.initialize();
});
/**
* The voucher form
* The journal entry form
*
*/
class VoucherForm {
class JournalEntryForm {
/**
* The form element
@ -105,7 +105,7 @@ class VoucherForm {
lineItemEditor;
/**
* Constructs the voucher form.
* Constructs the journal entry form.
*
*/
constructor() {
@ -325,17 +325,17 @@ class VoucherForm {
}
/**
* The voucher form
* @type {VoucherForm}
* The journal entry form
* @type {JournalEntryForm}
*/
static #form;
/**
* Initializes the voucher form.
* Initializes the journal entry form.
*
*/
static initialize() {
this.#form = new VoucherForm()
this.#form = new JournalEntryForm()
}
}
@ -352,8 +352,8 @@ class CurrencySubForm {
element;
/**
* The voucher form
* @type {VoucherForm}
* The journal entry form
* @type {JournalEntryForm}
*/
form;
@ -420,7 +420,7 @@ class CurrencySubForm {
/**
* 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
*/
constructor(form, element) {

View File

@ -29,8 +29,8 @@
class JournalEntryLineItemEditor {
/**
* The voucher form
* @type {VoucherForm}
* The journal entry form
* @type {JournalEntryForm}
*/
form;
@ -217,7 +217,7 @@ class JournalEntryLineItemEditor {
/**
* Constructs a new journal entry line item editor.
*
* @param form {VoucherForm} the voucher form
* @param form {JournalEntryForm} the journal entry form
*/
constructor(form) {
this.form = form;

View File

@ -1,5 +1,5 @@
/* 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.

View File

@ -105,7 +105,7 @@ class OriginalLineItemSelector {
* Returns the net balance for an original line item.
*
* @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
* @return {Decimal} the net balance of the original line item
*/

View File

@ -1,6 +1,6 @@
{#
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.
@ -19,10 +19,10 @@ create.html: The cash disbursement voucher creation form
Author: imacat@mail.imacat.idv.tw (imacat)
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 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
detail.html: The cash disbursement voucher detail
detail.html: The cash disbursement journal entry detail
Copyright (c) 2023 imacat.
@ -19,16 +19,16 @@ detail.html: The cash disbursement voucher detail
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/26
#}
{% extends "accounting/voucher/include/detail.html" %}
{% extends "accounting/journal-entry/include/detail.html" %}
{% 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>
{{ A_("To Transfer") }}
</a>
{% endblock %}
{% block voucher_currencies %}
{% block journal_entry_currencies %}
{% for currency in obj.currencies %}
<div class="mb-3">
<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">
<li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Content") }}</li>
{% with line_items = currency.debit %}
{% include "accounting/voucher/include/detail-line-items.html" %}
{% include "accounting/journal-entry/include/detail-line-items.html" %}
{% endwith %}
<li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
<div class="d-flex justify-content-between">

View File

@ -1,6 +1,6 @@
{#
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.
@ -19,10 +19,10 @@ edit.html: The cash receipt voucher edit form
Author: imacat@mail.imacat.idv.tw (imacat)
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
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.
@ -51,7 +51,7 @@ First written: 2023/2/25
line_item_index = loop.index,
only_one_line_item_form = debit_forms|length == 1,
form = line_item_form.form %}
{% include "accounting/voucher/include/form-line-item.html" %}
{% include "accounting/journal-entry/include/form-line-item.html" %}
{% endwith %}
{% endfor %}
</ul>

View File

@ -1,6 +1,6 @@
{#
The Mia! Accounting Flask Project
form.html: The cash disbursement voucher form
form.html: The cash disbursement journal entry form
Copyright (c) 2023 imacat.
@ -19,7 +19,7 @@ form.html: The cash disbursement voucher form
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/25
#}
{% extends "accounting/voucher/include/form.html" %}
{% extends "accounting/journal-entry/include/form.html" %}
{% block currency_sub_forms %}
{% if form.currencies %}
@ -33,7 +33,7 @@ First written: 2023/2/25
debit_forms = currency_form.debit,
debit_errors = currency_form.debit_errors,
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 %}
{% endfor %}
{% else %}
@ -41,17 +41,17 @@ First written: 2023/2/25
only_one_currency_form = True,
currency_code_data = accounting_default_currency_code(),
debit_total = "-" %}
{% include "accounting/voucher/disbursement/include/form-currency-item.html" %}
{% include "accounting/journal-entry/disbursement/include/form-currency.html" %}
{% endwith %}
{% endif %}
{% endblock %}
{% block form_modals %}
{% 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 %}
{% with debit_credit = "debit",
account_options = form.debit_account_options %}
{% include "accounting/voucher/include/account-selector-modal.html" %}
{% include "accounting/journal-entry/include/account-selector-modal.html" %}
{% endwith %}
{% endblock %}

View File

@ -1,6 +1,6 @@
{#
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.
@ -30,7 +30,7 @@ First written: 2023/3/14
{% endif %}
{% if line_item.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>
</div>
@ -43,8 +43,8 @@ First written: 2023/3/14
<ul class="ms-2 ps-0">
{% for offset in line_item.offsets %}
<li>
<a href="{{ url_for("accounting.voucher.detail", voucher=offset.voucher)|accounting_append_next }}">
{{ offset.voucher.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}
<a href="{{ url_for("accounting.journal-entry.detail", journal_entry=offset.journal_entry)|accounting_append_next }}">
{{ offset.journal_entry.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}
</a>
</li>
{% endfor %}

View File

@ -31,12 +31,12 @@ First written: 2023/2/26
{{ A_("Back") }}
</a>
{% 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>
{{ A_("Settings") }}
</a>
{% 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>
{{ A_("Order") }}
</a>
@ -58,14 +58,14 @@ First written: 2023/2/26
{% if accounting_can_edit() %}
<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>
</a>
</div>
{% endif %}
{% 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() }}">
{% if 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-content">
<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>
</div>
<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 class="modal-footer">
<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 }}
</div>
{% block voucher_currencies %}{% endblock %}
{% block journal_entry_currencies %}{% endblock %}
{% if obj.note %}
<div class="card mb-3">
<div class="card-body">
<i class="far fa-comment-dots"></i>
{{ obj.note|accounting_voucher_text2html|safe }}
{{ obj.note|accounting_journal_entry_text2html|safe }}
</div>
</div>
{% endif %}

View File

@ -1,6 +1,6 @@
{#
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.
@ -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 }}-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 }}-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 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>
@ -43,7 +43,7 @@ First written: 2023/2/25
<div>{{ A_("Offsets") }}</div>
<ul class="ms-2 ps-0">
{% 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 %}
</ul>
</div>

View File

@ -1,6 +1,6 @@
{#
The Mia! Accounting Flask Project
form.html: The base voucher form
form.html: The base journal entry form
Copyright (c) 2023 imacat.
@ -23,7 +23,7 @@ First written: 2023/2/26
{% 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/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/account-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>
</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 %}
{% include "accounting/voucher/include/original-line-item-selector-modal.html" %}
{% include "accounting/journal-entry/include/original-line-item-selector-modal.html" %}
{% 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">
{% 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">
<div>{{ line_item.voucher.date|accounting_format_date }} {{ line_item.description|accounting_default }}</div>
<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.journal_entry.date|accounting_format_date }} {{ line_item.description|accounting_default }}</div>
<div>
<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>

View File

@ -1,6 +1,6 @@
{#
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.
@ -23,10 +23,10 @@ First written: 2023/2/26
{% 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/voucher-order.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/journal-entry-order.js") }}"></script>
{% 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 %}
@ -38,7 +38,7 @@ First written: 2023/2/26
</div>
{% 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() }}">
{% if request.args.next %}
<input type="hidden" name="next" value="{{ request.args.next }}">

View File

@ -1,6 +1,6 @@
{#
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.
@ -19,10 +19,10 @@ create.html: The cash receipt voucher creation form
Author: imacat@mail.imacat.idv.tw (imacat)
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 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
detail.html: The cash receipt voucher detail
detail.html: The cash receipt journal entry detail
Copyright (c) 2023 imacat.
@ -19,16 +19,16 @@ detail.html: The cash receipt voucher detail
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/26
#}
{% extends "accounting/voucher/include/detail.html" %}
{% extends "accounting/journal-entry/include/detail.html" %}
{% 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>
{{ A_("To Transfer") }}
</a>
{% endblock %}
{% block voucher_currencies %}
{% block journal_entry_currencies %}
{% for currency in obj.currencies %}
<div class="mb-3">
<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">
<li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Content") }}</li>
{% with line_items = currency.credit %}
{% include "accounting/voucher/include/detail-line-items.html" %}
{% include "accounting/journal-entry/include/detail-line-items.html" %}
{% endwith %}
<li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
<div class="d-flex justify-content-between">

View File

@ -1,6 +1,6 @@
{#
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.
@ -19,10 +19,10 @@ edit.html: The transfer voucher edit form
Author: imacat@mail.imacat.idv.tw (imacat)
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
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.
@ -51,7 +51,7 @@ First written: 2023/2/25
line_item_index = loop.index,
only_one_line_item_form = credit_forms|length == 1,
form = line_item_form.form %}
{% include "accounting/voucher/include/form-line-item.html" %}
{% include "accounting/journal-entry/include/form-line-item.html" %}
{% endwith %}
{% endfor %}
</ul>

View File

@ -1,6 +1,6 @@
{#
The Mia! Accounting Flask Project
form.html: The cash receipt voucher form
form.html: The cash receipt journal entry form
Copyright (c) 2023 imacat.
@ -19,7 +19,7 @@ form.html: The cash receipt voucher form
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/25
#}
{% extends "accounting/voucher/include/form.html" %}
{% extends "accounting/journal-entry/include/form.html" %}
{% block currency_sub_forms %}
{% if form.currencies %}
@ -33,7 +33,7 @@ First written: 2023/2/25
credit_forms = currency_form.credit,
credit_errors = currency_form.credit_errors,
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 %}
{% endfor %}
{% else %}
@ -41,17 +41,17 @@ First written: 2023/2/25
only_one_currency_form = True,
currency_code_data = accounting_default_currency_code(),
credit_total = "-" %}
{% include "accounting/voucher/receipt/include/form-currency-item.html" %}
{% include "accounting/journal-entry/receipt/include/form-currency.html" %}
{% endwith %}
{% endif %}
{% endblock %}
{% block form_modals %}
{% 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 %}
{% with debit_credit = "credit",
account_options = form.credit_account_options %}
{% include "accounting/voucher/include/account-selector-modal.html" %}
{% include "accounting/journal-entry/include/account-selector-modal.html" %}
{% endwith %}
{% endblock %}

View File

@ -1,6 +1,6 @@
{#
The Mia! Accounting Flask Project
create.html: The transfer voucher creation form
create.html: The transfer journal entry creation form
Copyright (c) 2023 imacat.
@ -19,10 +19,10 @@ create.html: The transfer voucher creation form
Author: imacat@mail.imacat.idv.tw (imacat)
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 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
detail.html: The transfer voucher detail
detail.html: The transfer journal entry detail
Copyright (c) 2023 imacat.
@ -19,9 +19,9 @@ detail.html: The transfer voucher detail
Author: imacat@mail.imacat.idv.tw (imacat)
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 %}
<div class="mb-3">
<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">
<li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Debit") }}</li>
{% with line_items = currency.debit %}
{% include "accounting/voucher/include/detail-line-items.html" %}
{% include "accounting/journal-entry/include/detail-line-items.html" %}
{% endwith %}
<li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
<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">
<li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-header">{{ A_("Credit") }}</li>
{% with line_items = currency.credit %}
{% include "accounting/voucher/include/detail-line-items.html" %}
{% include "accounting/journal-entry/include/detail-line-items.html" %}
{% endwith %}
<li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
<div class="d-flex justify-content-between">

View File

@ -1,6 +1,6 @@
{#
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.
@ -19,10 +19,10 @@ edit.html: The cash disbursement voucher edit form
Author: imacat@mail.imacat.idv.tw (imacat)
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
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.
@ -53,7 +53,7 @@ First written: 2023/2/25
line_item_index = loop.index,
only_one_line_item_form = debit_forms|length == 1,
form = line_item_form.form %}
{% include "accounting/voucher/include/form-line-item.html" %}
{% include "accounting/journal-entry/include/form-line-item.html" %}
{% endwith %}
{% endfor %}
</ul>
@ -84,7 +84,7 @@ First written: 2023/2/25
line_item_index = loop.index,
only_one_line_item_form = credit_forms|length == 1,
form = line_item_form.form %}
{% include "accounting/voucher/include/form-line-item.html" %}
{% include "accounting/journal-entry/include/form-line-item.html" %}
{% endwith %}
{% endfor %}
</ul>

View File

@ -1,6 +1,6 @@
{#
The Mia! Accounting Flask Project
form.html: The transfer voucher form
form.html: The transfer journal entry form
Copyright (c) 2023 imacat.
@ -19,7 +19,7 @@ form.html: The transfer voucher form
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/25
#}
{% extends "accounting/voucher/include/form.html" %}
{% extends "accounting/journal-entry/include/form.html" %}
{% block currency_sub_forms %}
{% if form.currencies %}
@ -36,7 +36,7 @@ First written: 2023/2/25
credit_forms = currency_form.credit,
credit_errors = currency_form.credit_errors,
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 %}
{% endfor %}
{% else %}
@ -45,24 +45,24 @@ First written: 2023/2/25
currency_code_data = accounting_default_currency_code(),
debit_total = "-",
credit_total = "-" %}
{% include "accounting/voucher/transfer/include/form-currency-item.html" %}
{% include "accounting/journal-entry/transfer/include/form-currency.html" %}
{% endwith %}
{% endif %}
{% endblock %}
{% block form_modals %}
{% 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 %}
{% 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 %}
{% with debit_credit = "debit",
account_options = form.debit_account_options %}
{% include "accounting/voucher/include/account-selector-modal.html" %}
{% include "accounting/journal-entry/include/account-selector-modal.html" %}
{% endwith %}
{% with debit_credit = "credit",
account_options = form.credit_account_options %}
{% include "accounting/voucher/include/account-selector-modal.html" %}
{% include "accounting/journal-entry/include/account-selector-modal.html" %}
{% endwith %}
{% endblock %}

View File

@ -37,7 +37,7 @@ First written: 2023/3/7
{% endwith %}
</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" %}

View File

@ -1,6 +1,6 @@
{#
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.
@ -22,13 +22,13 @@ First written: 2023/2/25
{% 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-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>
<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>
<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>
</div>

View File

@ -27,17 +27,17 @@ First written: 2023/3/8
</button>
<ul class="dropdown-menu">
<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>
</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>
</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>
</li>

View File

@ -38,7 +38,7 @@ First written: 2023/3/5
{% endwith %}
</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" %}

View File

@ -37,7 +37,7 @@ First written: 2023/3/7
{% endwith %}
</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" %}

View File

@ -36,7 +36,7 @@ First written: 2023/3/4
{% endwith %}
</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" %}
@ -60,8 +60,8 @@ First written: 2023/3/4
</div>
<div class="accounting-report-table-body">
{% 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 }}">
<div>{{ line_item.voucher.date|accounting_format_date }}</div>
<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.journal_entry.date|accounting_format_date }}</div>
<div>{{ line_item.currency.name }}</div>
<div>
<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">
{% 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 {% if not line_item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
<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 }}
{% if line_item.currency.code != accounting_default_currency_code() %}
<span class="badge rounded-pill bg-info">{{ line_item.currency.code }}</span>

View File

@ -38,7 +38,7 @@ First written: 2023/3/5
{% endwith %}
</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" %}

View File

@ -35,7 +35,7 @@ First written: 2023/3/8
{% endwith %}
</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" %}
@ -57,8 +57,8 @@ First written: 2023/3/8
</div>
<div class="accounting-report-table-body">
{% 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 }}">
<div>{{ line_item.voucher.date|accounting_format_date }}</div>
<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.journal_entry.date|accounting_format_date }}</div>
<div>{{ line_item.currency.name }}</div>
<div>
<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">
{% 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 {% if not line_item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
<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 }}
{% if line_item.currency.code != accounting_default_currency_code() %}
<span class="badge rounded-pill bg-info">{{ line_item.currency.code }}</span>

View File

@ -37,7 +37,7 @@ First written: 2023/3/5
{% endwith %}
</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" %}

View File

@ -14,17 +14,17 @@
# 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 voucher types.
"""The journal entry types.
"""
from enum import Enum
class VoucherType(Enum):
"""The voucher types."""
class JournalEntryType(Enum):
"""The journal entry types."""
CASH_RECEIPT: str = "receipt"
"""The cash receipt voucher."""
"""The cash receipt journal entry."""
CASH_DISBURSEMENT: str = "disbursement"
"""The cash disbursement voucher."""
"""The cash disbursement journal entry."""
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 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):
@ -41,7 +41,7 @@ class DescriptionEditorTestCase(unittest.TestCase):
runner: FlaskCliRunner = self.app.test_cli_runner()
with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \
from accounting.models import BaseAccount, JournalEntry, \
JournalEntryLineItem
result: Result
result = runner.invoke(args="init-db")
@ -55,7 +55,7 @@ class DescriptionEditorTestCase(unittest.TestCase):
result = runner.invoke(args=["accounting-init-accounts",
"-u", "editor"])
self.assertEqual(result.exit_code, 0)
Voucher.query.delete()
JournalEntry.query.delete()
JournalEntryLineItem.query.delete()
self.client, self.csrf_token = get_client(self.app, "editor")
@ -65,10 +65,10 @@ class DescriptionEditorTestCase(unittest.TestCase):
:return: None.
"""
from accounting.voucher.utils.description_editor import \
from accounting.journal_entry.utils.description_editor import \
DescriptionEditor
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():
editor: DescriptionEditor = DescriptionEditor()
@ -160,22 +160,22 @@ class DescriptionEditorTestCase(unittest.TestCase):
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.
: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,
"next": NEXT_URI,
"date": voucher_date,
"date": journal_entry_date,
"currency-0-code": "USD",
"currency-0-credit-0-account_code": Accounts.SERVICE,
"currency-0-credit-0-description": " Salary ",
"currency-0-credit-0-amount": "2500"},
{"csrf_token": csrf_token,
"next": NEXT_URI,
"date": voucher_date,
"date": journal_entry_date,
"currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.MEAL,
"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"},
{"csrf_token": csrf_token,
"next": NEXT_URI,
"date": voucher_date,
"date": journal_entry_date,
"currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.MEAL,
"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"},
{"csrf_token": csrf_token,
"next": NEXT_URI,
"date": voucher_date,
"date": journal_entry_date,
"currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.MEAL,
"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"},
{"csrf_token": csrf_token,
"next": NEXT_URI,
"date": voucher_date,
"date": journal_entry_date,
"currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.TRAVEL,
"currency-0-debit-0-description": " Airplane—Lake City↔Hill Town",
"currency-0-debit-0-amount": "800"},
{"csrf_token": csrf_token,
"next": NEXT_URI,
"date": voucher_date,
"date": journal_entry_date,
"currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.TRAVEL,
"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"},
{"csrf_token": csrf_token,
"next": NEXT_URI,
"date": voucher_date,
"date": journal_entry_date,
"currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.TRAVEL,
"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"},
{"csrf_token": csrf_token,
"next": NEXT_URI,
"date": voucher_date,
"date": journal_entry_date,
"currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.PETTY_CASH,
"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 testlib import create_test_app, get_client
from testlib_offset import TestData, JournalEntryLineItemData, VoucherData, \
CurrencyData
from testlib_voucher import Accounts, match_voucher_detail
from testlib_journal_entry import Accounts, match_journal_entry_detail
from testlib_offset import TestData, JournalEntryLineItemData, \
JournalEntryData, CurrencyData
PREFIX: str = "/accounting/vouchers"
"""The URL prefix for the voucher management."""
PREFIX: str = "/accounting/journal-entries"
"""The URL prefix for the journal entry management."""
class OffsetTestCase(unittest.TestCase):
@ -48,7 +48,7 @@ class OffsetTestCase(unittest.TestCase):
runner: FlaskCliRunner = self.app.test_cli_runner()
with self.app.app_context():
from accounting.models import BaseAccount, Voucher, \
from accounting.models import BaseAccount, JournalEntry, \
JournalEntryLineItem
result: Result
result = runner.invoke(args="init-db")
@ -62,7 +62,7 @@ class OffsetTestCase(unittest.TestCase):
result = runner.invoke(args=["accounting-init-accounts",
"-u", "editor"])
self.assertEqual(result.exit_code, 0)
Voucher.query.delete()
JournalEntry.query.delete()
JournalEntryLineItem.query.delete()
self.client, self.csrf_token = get_client(self.app, "editor")
@ -73,36 +73,39 @@ class OffsetTestCase(unittest.TestCase):
:return: None.
"""
from accounting.models import Account, Voucher
from accounting.models import Account, JournalEntry
create_uri: str = f"{PREFIX}/create/receipt?next=%2F_next"
store_uri: str = f"{PREFIX}/store/receipt"
form: dict[str, str]
old_amount: Decimal
response: httpx.Response
voucher_data: VoucherData = VoucherData(
self.data.e_r_or3d.voucher.days, [CurrencyData(
journal_entry_data: JournalEntryData = JournalEntryData(
self.data.e_r_or3d.journal_entry.days, [CurrencyData(
"USD",
[],
[JournalEntryLineItemData(Accounts.RECEIVABLE,
self.data.e_r_or1d.description, "300",
original_line_item=self.data.e_r_or1d),
JournalEntryLineItemData(Accounts.RECEIVABLE,
self.data.e_r_or1d.description, "100",
original_line_item=self.data.e_r_or1d),
JournalEntryLineItemData(Accounts.RECEIVABLE,
self.data.e_r_or3d.description, "100",
original_line_item=self.data.e_r_or3d)])])
[JournalEntryLineItemData(
Accounts.RECEIVABLE,
self.data.e_r_or1d.description, "300",
original_line_item=self.data.e_r_or1d),
JournalEntryLineItemData(
Accounts.RECEIVABLE,
self.data.e_r_or1d.description, "100",
original_line_item=self.data.e_r_or1d),
JournalEntryLineItemData(
Accounts.RECEIVABLE,
self.data.e_r_or3d.description, "100",
original_line_item=self.data.e_r_or3d)])])
# 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"
response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri)
# 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"] \
= self.data.e_p_or1c.id
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
db.session.commit()
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.headers["Location"], create_uri)
with self.app.app_context():
@ -126,7 +129,7 @@ class OffsetTestCase(unittest.TestCase):
db.session.commit()
# 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"] \
= self.data.e_p_of1d.id
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)
# 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"
response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri)
# 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
response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri)
# 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"] \
= str(voucher_data.currencies[0].credit[0].amount
= str(journal_entry_data.currencies[0].credit[0].amount
+ Decimal("0.01"))
response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri)
# 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"] \
= str(voucher_data.currencies[0].credit[2].amount
= str(journal_entry_data.currencies[0].credit[2].amount
+ Decimal("0.01"))
response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri)
# Not before the original line items
old_days = voucher_data.days
voucher_data.days = old_days + 1
form = voucher_data.new_form(self.csrf_token)
old_days = journal_entry_data.days
journal_entry_data.days = old_days + 1
form = journal_entry_data.new_form(self.csrf_token)
response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri)
voucher_data.days = old_days
journal_entry_data.days = old_days
# 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)
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():
voucher = db.session.get(Voucher, voucher_id)
for offset in voucher.currencies[0].credit:
journal_entry = db.session.get(JournalEntry, journal_entry_id)
for offset in journal_entry.currencies[0].credit:
self.assertIsNotNone(offset.original_line_item_id)
def test_edit_receivable_offset(self) -> None:
@ -191,27 +195,27 @@ class OffsetTestCase(unittest.TestCase):
:return: None.
"""
from accounting.models import Account
voucher_data: VoucherData = self.data.v_r_of2
edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
journal_entry_data: JournalEntryData = self.data.v_r_of2
edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next"
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
form: dict[str, str]
response: httpx.Response
voucher_data.days = self.data.v_r_or2.days
voucher_data.currencies[0].debit[0].amount = Decimal("600")
voucher_data.currencies[0].credit[0].amount = Decimal("600")
voucher_data.currencies[0].debit[2].amount = Decimal("600")
voucher_data.currencies[0].credit[2].amount = Decimal("600")
journal_entry_data.days = self.data.v_r_or2.days
journal_entry_data.currencies[0].debit[0].amount = Decimal("600")
journal_entry_data.currencies[0].credit[0].amount = Decimal("600")
journal_entry_data.currencies[0].debit[2].amount = Decimal("600")
journal_entry_data.currencies[0].credit[2].amount = Decimal("600")
# 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"
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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"] \
= self.data.e_p_or1c.id
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
db.session.commit()
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.headers["Location"], edit_uri)
with self.app.app_context():
@ -236,7 +240,7 @@ class OffsetTestCase(unittest.TestCase):
db.session.commit()
# 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"] \
= self.data.e_p_of1d.id
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)
# 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"
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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"] \
= 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"] \
= str(voucher_data.currencies[0].credit[0].amount
= str(journal_entry_data.currencies[0].credit[0].amount
+ Decimal("0.01"))
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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"] \
= 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"] \
= str(voucher_data.currencies[0].credit[2].amount
= str(journal_entry_data.currencies[0].credit[2].amount
+ Decimal("0.01"))
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# Not before the original line items
old_days: int = voucher_data.days
voucher_data.days = old_days + 1
form = voucher_data.update_form(self.csrf_token)
old_days: int = journal_entry_data.days
journal_entry_data.days = old_days + 1
form = journal_entry_data.update_form(self.csrf_token)
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
voucher_data.days = old_days
journal_entry_data.days = old_days
# 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)
self.assertEqual(response.status_code, 302)
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:
"""Tests to edit the receivable original line item.
:return: None.
"""
from accounting.models import Voucher
voucher_data: VoucherData = self.data.v_r_or1
edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
from accounting.models import JournalEntry
journal_entry_data: JournalEntryData = self.data.v_r_or1
edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next"
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
form: dict[str, str]
response: httpx.Response
voucher_data.days = self.data.v_r_of1.days
voucher_data.currencies[0].debit[0].amount = Decimal("800")
voucher_data.currencies[0].credit[0].amount = Decimal("800")
voucher_data.currencies[0].debit[1].amount = Decimal("3.4")
voucher_data.currencies[0].credit[1].amount = Decimal("3.4")
journal_entry_data.days = self.data.v_r_of1.days
journal_entry_data.currencies[0].debit[0].amount = Decimal("800")
journal_entry_data.currencies[0].credit[0].amount = Decimal("800")
journal_entry_data.currencies[0].debit[1].amount = Decimal("3.4")
journal_entry_data.currencies[0].credit[1].amount = Decimal("3.4")
# 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"
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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"] \
= 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"] \
= str(voucher_data.currencies[0].credit[0].amount
= str(journal_entry_data.currencies[0].credit[0].amount
- Decimal("0.01"))
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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"] \
= 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"] \
= str(voucher_data.currencies[0].credit[1].amount
= str(journal_entry_data.currencies[0].credit[1].amount
- Decimal("0.01"))
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# Not after the offset items
old_days: int = voucher_data.days
voucher_data.days = old_days - 1
form = voucher_data.update_form(self.csrf_token)
old_days: int = journal_entry_data.days
journal_entry_data.days = old_days - 1
form = journal_entry_data.update_form(self.csrf_token)
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
voucher_data.days = old_days
journal_entry_data.days = old_days
# 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"]
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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)
self.assertEqual(response.status_code, 302)
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
# they happen in the same day.
with self.app.app_context():
voucher_or: Voucher | None = db.session.get(
Voucher, voucher_data.id)
self.assertIsNotNone(voucher_or)
voucher_of: Voucher | None = db.session.get(
Voucher, self.data.v_r_of1.id)
self.assertIsNotNone(voucher_of)
self.assertEqual(voucher_or.date, voucher_of.date)
self.assertLess(voucher_or.no, voucher_of.no)
journal_entry_or: JournalEntry | None = db.session.get(
JournalEntry, journal_entry_data.id)
self.assertIsNotNone(journal_entry_or)
journal_entry_of: JournalEntry | None = db.session.get(
JournalEntry, self.data.v_r_of1.id)
self.assertIsNotNone(journal_entry_of)
self.assertEqual(journal_entry_or.date, journal_entry_of.date)
self.assertLess(journal_entry_or.no, journal_entry_of.no)
def test_add_payable_offset(self) -> None:
"""Tests to add the payable offset.
:return: None.
"""
from accounting.models import Account, Voucher
from accounting.models import Account, JournalEntry
create_uri: str = f"{PREFIX}/create/disbursement?next=%2F_next"
store_uri: str = f"{PREFIX}/store/disbursement"
form: dict[str, str]
response: httpx.Response
voucher_data: VoucherData = VoucherData(
self.data.e_p_or3c.voucher.days, [CurrencyData(
journal_entry_data: JournalEntryData = JournalEntryData(
self.data.e_p_or3c.journal_entry.days, [CurrencyData(
"USD",
[JournalEntryLineItemData(Accounts.PAYABLE,
self.data.e_p_or1c.description, "500",
original_line_item=self.data.e_p_or1c),
JournalEntryLineItemData(Accounts.PAYABLE,
self.data.e_p_or1c.description, "300",
original_line_item=self.data.e_p_or1c),
JournalEntryLineItemData(Accounts.PAYABLE,
self.data.e_p_or3c.description, "120",
original_line_item=self.data.e_p_or3c)],
[JournalEntryLineItemData(
Accounts.PAYABLE,
self.data.e_p_or1c.description, "500",
original_line_item=self.data.e_p_or1c),
JournalEntryLineItemData(
Accounts.PAYABLE,
self.data.e_p_or1c.description, "300",
original_line_item=self.data.e_p_or1c),
JournalEntryLineItemData(
Accounts.PAYABLE,
self.data.e_p_or3c.description, "120",
original_line_item=self.data.e_p_or3c)],
[])])
# 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"
response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri)
# 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"] \
= self.data.e_r_or1d.id
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
db.session.commit()
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.headers["Location"], create_uri)
with self.app.app_context():
@ -442,7 +453,7 @@ class OffsetTestCase(unittest.TestCase):
db.session.commit()
# 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"] \
= self.data.e_r_of1c.id
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)
# 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"
response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri)
# 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
response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri)
# 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"] \
= 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)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri)
# 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"] \
= 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)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri)
# Not before the original line items
old_days: int = voucher_data.days
voucher_data.days = old_days + 1
form = voucher_data.new_form(self.csrf_token)
old_days: int = journal_entry_data.days
journal_entry_data.days = old_days + 1
form = journal_entry_data.new_form(self.csrf_token)
response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri)
voucher_data.days = old_days
journal_entry_data.days = old_days
# 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)
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():
voucher = db.session.get(Voucher, voucher_id)
for offset in voucher.currencies[0].debit:
journal_entry = db.session.get(JournalEntry, journal_entry_id)
for offset in journal_entry.currencies[0].debit:
self.assertIsNotNone(offset.original_line_item_id)
def test_edit_payable_offset(self) -> None:
@ -504,28 +518,28 @@ class OffsetTestCase(unittest.TestCase):
:return: None.
"""
from accounting.models import Account, Voucher
voucher_data: VoucherData = self.data.v_p_of2
edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
from accounting.models import Account, JournalEntry
journal_entry_data: JournalEntryData = self.data.v_p_of2
edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next"
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
form: dict[str, str]
response: httpx.Response
voucher_data.days = self.data.v_p_or2.days
voucher_data.currencies[0].debit[0].amount = Decimal("1100")
voucher_data.currencies[0].credit[0].amount = Decimal("1100")
voucher_data.currencies[0].debit[2].amount = Decimal("900")
voucher_data.currencies[0].credit[2].amount = Decimal("900")
journal_entry_data.days = self.data.v_p_or2.days
journal_entry_data.currencies[0].debit[0].amount = Decimal("1100")
journal_entry_data.currencies[0].credit[0].amount = Decimal("1100")
journal_entry_data.currencies[0].debit[2].amount = Decimal("900")
journal_entry_data.currencies[0].credit[2].amount = Decimal("900")
# 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"
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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"] \
= self.data.e_r_or1d.id
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
db.session.commit()
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.headers["Location"], edit_uri)
with self.app.app_context():
@ -550,7 +564,7 @@ class OffsetTestCase(unittest.TestCase):
db.session.commit()
# 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"] \
= self.data.e_r_of1c.id
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)
# 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"
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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"] \
= 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"] \
= str(voucher_data.currencies[0].credit[0].amount
= str(journal_entry_data.currencies[0].credit[0].amount
+ Decimal("0.01"))
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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"] \
= 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"] \
= str(voucher_data.currencies[0].credit[2].amount
= str(journal_entry_data.currencies[0].credit[2].amount
+ Decimal("0.01"))
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# Not before the original line items
old_days: int = voucher_data.days
voucher_data.days = old_days + 1
form = voucher_data.update_form(self.csrf_token)
old_days: int = journal_entry_data.days
journal_entry_data.days = old_days + 1
form = journal_entry_data.update_form(self.csrf_token)
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
voucher_data.days = old_days
journal_entry_data.days = old_days
# 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)
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():
voucher = db.session.get(Voucher, voucher_id)
for offset in voucher.currencies[0].debit:
journal_entry = db.session.get(JournalEntry, journal_entry_id)
for offset in journal_entry.currencies[0].debit:
self.assertIsNotNone(offset.original_line_item_id)
def test_edit_payable_original_line_item(self) -> None:
@ -618,86 +635,88 @@ class OffsetTestCase(unittest.TestCase):
:return: None.
"""
from accounting.models import Voucher
voucher_data: VoucherData = self.data.v_p_or1
edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
from accounting.models import JournalEntry
journal_entry_data: JournalEntryData = self.data.v_p_or1
edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next"
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
form: dict[str, str]
response: httpx.Response
voucher_data.days = self.data.v_p_of1.days
voucher_data.currencies[0].debit[0].amount = Decimal("1200")
voucher_data.currencies[0].credit[0].amount = Decimal("1200")
voucher_data.currencies[0].debit[1].amount = Decimal("0.9")
voucher_data.currencies[0].credit[1].amount = Decimal("0.9")
journal_entry_data.days = self.data.v_p_of1.days
journal_entry_data.currencies[0].debit[0].amount = Decimal("1200")
journal_entry_data.currencies[0].credit[0].amount = Decimal("1200")
journal_entry_data.currencies[0].debit[1].amount = Decimal("0.9")
journal_entry_data.currencies[0].credit[1].amount = Decimal("0.9")
# 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"
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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"] \
= 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"] \
= str(voucher_data.currencies[0].credit[0].amount
= str(journal_entry_data.currencies[0].credit[0].amount
- Decimal("0.01"))
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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"] \
= 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"] \
= str(voucher_data.currencies[0].credit[1].amount
= str(journal_entry_data.currencies[0].credit[1].amount
- Decimal("0.01"))
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# Not after the offset items
old_days: int = voucher_data.days
voucher_data.days = old_days - 1
form = voucher_data.update_form(self.csrf_token)
old_days: int = journal_entry_data.days
journal_entry_data.days = old_days - 1
form = journal_entry_data.update_form(self.csrf_token)
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
voucher_data.days = old_days
journal_entry_data.days = old_days
# 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"]
response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri)
# 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)
self.assertEqual(response.status_code, 302)
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
# they happen in the same day
with self.app.app_context():
voucher_or: Voucher | None = db.session.get(
Voucher, voucher_data.id)
self.assertIsNotNone(voucher_or)
voucher_of: Voucher | None = db.session.get(
Voucher, self.data.v_p_of1.id)
self.assertIsNotNone(voucher_of)
self.assertEqual(voucher_or.date, voucher_of.date)
self.assertLess(voucher_or.no, voucher_of.no)
journal_entry_or: JournalEntry | None = db.session.get(
JournalEntry, journal_entry_data.id)
self.assertIsNotNone(journal_entry_or)
journal_entry_of: JournalEntry | None = db.session.get(
JournalEntry, self.data.v_p_of1.id)
self.assertIsNotNone(journal_entry_of)
self.assertEqual(journal_entry_or.date, journal_entry_of.date)
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.
# See the License for the specific language governing permissions and
# 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
@ -57,10 +57,10 @@ class Accounts:
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.
:return: The form data to add a new voucher.
:return: The form data to add a new journal entry.
"""
return {"csrf_token": csrf_token,
"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 "}
def get_unchanged_update_form(voucher_id: int, app: Flask, csrf_token: str) \
-> dict[str, str]:
"""Returns the form data to update a voucher, where the data are not
def get_unchanged_update_form(journal_entry_id: int, app: Flask,
csrf_token: str) -> dict[str, str]:
"""Returns the form data to update a journal entry, where the data are not
changed.
:param voucher_id: The voucher ID.
:param journal_entry_id: The journal entry ID.
:param app: The Flask application.
: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.
"""
from accounting.models import Voucher, VoucherCurrency
from accounting.models import JournalEntry, JournalEntryCurrency
with app.app_context():
voucher: Voucher | None = db.session.get(Voucher, voucher_id)
assert voucher is not None
currencies: list[VoucherCurrency] = voucher.currencies
journal_entry: JournalEntry | None \
= db.session.get(JournalEntry, journal_entry_id)
assert journal_entry is not None
currencies: list[JournalEntryCurrency] = journal_entry.currencies
form: dict[str, str] = {"csrf_token": csrf_token,
"next": NEXT_URI,
"date": voucher.date,
"note": " \n \n\n " if voucher.note is None
else f"\n \n\n \n \n{voucher.note} \n\n "}
form: dict[str, str] \
= {"csrf_token": csrf_token,
"next": NEXT_URI,
"date": journal_entry.date,
"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_no: int = 0
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}-account_code"] = line_item.account.code
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)
return form
@ -201,21 +204,21 @@ def __get_new_index(indices_used: set[int]) -> int:
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]:
"""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.
:param voucher_id: The voucher ID.
:param journal_entry_id: The journal entry ID.
:param app: The Flask application.
:param csrf_token: The CSRF token.
:param is_debit: True for a cash disbursement voucher, False for a cash
receipt voucher, or None for a transfer voucher
:return: The form data to update the voucher, where the data are
:param is_debit: True for a cash disbursement journal entry, False for a
cash receipt journal entry, or None for a transfer journal entry.
:return: The form data to update the journal entry, where the data are
changed.
"""
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
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}-account_code"] = Accounts.TRAVEL
# Swap the cash and the bank order
key_cash: str = __get_line_item_no_key(form, currency_prefix, Accounts.CASH)
key_bank: str = __get_line_item_no_key(form, currency_prefix, Accounts.BANK)
key_cash: str = __get_line_item_no_key(
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]
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}-account_code"] = Accounts.AGENCY
# Swap the service and the interest order
key_srv: str = __get_line_item_no_key(form, currency_prefix, Accounts.SERVICE)
key_int: str = __get_line_item_no_key(form, currency_prefix, Accounts.INTEREST)
key_srv: str = __get_line_item_no_key(
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]
return form
@ -390,35 +397,35 @@ def __get_currency_prefix(form: dict[str, str], code: str) -> str:
return m.group(1)
def add_voucher(client: httpx.Client, form: dict[str, str]) -> int:
"""Adds a transfer voucher.
def add_journal_entry(client: httpx.Client, form: dict[str, str]) -> int:
"""Adds a transfer journal entry.
:param client: The client.
:param form: The form data.
:return: The newly-added voucher ID.
:return: The newly-added journal entry ID.
"""
prefix: str = "/accounting/vouchers"
voucher_type: str = "transfer"
prefix: str = "/accounting/journal-entries"
journal_entry_type: str = "transfer"
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:
voucher_type = "disbursement"
store_uri = f"{prefix}/store/{voucher_type}"
journal_entry_type = "disbursement"
store_uri = f"{prefix}/store/{journal_entry_type}"
response: httpx.Response = client.post(store_uri, data=form)
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:
"""Validates if the redirect location is the voucher detail, and
returns the voucher ID on success.
def match_journal_entry_detail(location: str) -> int:
"""Validates if the redirect location is the journal entry detail, and
returns the journal entry ID on success.
:param location: The redirect location.
:return: The voucher ID.
:raise AssertionError: When the location is not the voucher detail.
:return: The journal entry ID.
:raise AssertionError: When the location is not the journal entry detail.
"""
m: re.Match = re.match(r"^/accounting/vouchers/(\d+)\?next=%2F_next",
location)
m: re.Match = re.match(
r"^/accounting/journal-entries/(\d+)\?next=%2F_next", location)
assert m is not None
return int(m.group(1))

View File

@ -26,7 +26,8 @@ import httpx
from flask import Flask
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:
@ -41,7 +42,7 @@ class JournalEntryLineItemData:
:param amount: The amount.
: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.no: int = -1
self.original_line_item: JournalEntryLineItemData | None \
@ -75,11 +76,11 @@ class JournalEntryLineItemData:
class CurrencyData:
"""The voucher currency data."""
"""The journal entry currency data."""
def __init__(self, currency: str, debit: list[JournalEntryLineItemData],
credit: list[JournalEntryLineItemData]):
"""Constructs the voucher currency data.
"""Constructs the journal entry currency data.
:param currency: The currency code.
:param debit: The debit line items.
@ -106,14 +107,14 @@ class CurrencyData:
return form
class VoucherData:
"""The voucher data."""
class JournalEntryData:
"""The journal entry data."""
def __init__(self, days: int, currencies: list[CurrencyData]):
"""Constructs a voucher.
"""Constructs a journal entry.
: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.days: int = days
@ -121,38 +122,38 @@ class VoucherData:
self.note: str | None = None
for currency in self.currencies:
for line_item in currency.debit:
line_item.voucher = self
line_item.journal_entry = self
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]:
"""Returns the voucher as a creation form.
"""Returns the journal entry as a creation form.
: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)
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.
:return: The voucher as a update form.
:return: The journal entry as an update form.
"""
return self.__form(csrf_token, is_update=True)
def __form(self, csrf_token: str, is_update: bool = False) \
-> dict[str, str]:
"""Returns the voucher as a form.
"""Returns the journal entry as a form.
:param csrf_token: The CSRF token.
: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,
"next": NEXT_URI,
"date": voucher_date.isoformat()}
"date": journal_entry_date.isoformat()}
for i in range(len(self.currencies)):
form.update(self.currencies[i].form(i + 1, is_update))
if self.note is not None:
@ -207,24 +208,24 @@ class TestData:
self.e_p_or4d, self.e_p_or4c = couple(
"Envelop", "0.9", Accounts.OFFICE, Accounts.PAYABLE)
# Original vouchers
self.v_r_or1: VoucherData = VoucherData(
# Original journal entries
self.v_r_or1: JournalEntryData = JournalEntryData(
50, [CurrencyData("USD", [self.e_r_or1d, self.e_r_or4d],
[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],
[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],
[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],
[self.e_p_or2c, self.e_p_or3c])])
self.__add_voucher(self.v_r_or1)
self.__add_voucher(self.v_r_or2)
self.__add_voucher(self.v_p_or1)
self.__add_voucher(self.v_p_or2)
self.__add_journal_entry(self.v_r_or1)
self.__add_journal_entry(self.v_r_or2)
self.__add_journal_entry(self.v_p_or1)
self.__add_journal_entry(self.v_p_or2)
# Receivable offset items
self.e_r_of1d, self.e_r_of1c = couple(
@ -260,52 +261,55 @@ class TestData:
"Envelop", "0.9", Accounts.PAYABLE, Accounts.CASH)
self.e_p_of5d.original_line_item = self.e_p_or4c
# Offset vouchers
self.v_r_of1: VoucherData = VoucherData(
# Offset journal entries
self.v_r_of1: JournalEntryData = JournalEntryData(
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",
[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.v_r_of3: VoucherData = VoucherData(
self.v_r_of3: JournalEntryData = JournalEntryData(
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])])
self.v_p_of2: VoucherData = VoucherData(
self.v_p_of2: JournalEntryData = JournalEntryData(
10, [CurrencyData("USD",
[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.v_p_of3: VoucherData = VoucherData(
self.v_p_of3: JournalEntryData = JournalEntryData(
5, [CurrencyData("USD", [self.e_p_of5d], [self.e_p_of5c])])
self.__add_voucher(self.v_r_of1)
self.__add_voucher(self.v_r_of2)
self.__add_voucher(self.v_r_of3)
self.__add_voucher(self.v_p_of1)
self.__add_voucher(self.v_p_of2)
self.__add_voucher(self.v_p_of3)
self.__add_journal_entry(self.v_r_of1)
self.__add_journal_entry(self.v_r_of2)
self.__add_journal_entry(self.v_r_of3)
self.__add_journal_entry(self.v_p_of1)
self.__add_journal_entry(self.v_p_of2)
self.__add_journal_entry(self.v_p_of3)
def __add_voucher(self, voucher_data: VoucherData) -> None:
"""Adds a voucher.
def __add_journal_entry(self, journal_entry_data: JournalEntryData) \
-> None:
"""Adds a journal entry.
:param voucher_data: The voucher data.
:param journal_entry_data: The journal entry data.
:return: None.
"""
from accounting.models import Voucher
store_uri: str = "/accounting/vouchers/store/transfer"
from accounting.models import JournalEntry
store_uri: str = "/accounting/journal-entries/store/transfer"
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
voucher_id: int = match_voucher_detail(response.headers["Location"])
voucher_data.id = voucher_id
journal_entry_id: int \
= match_journal_entry_detail(response.headers["Location"])
journal_entry_data.id = journal_entry_id
with self.app.app_context():
voucher: Voucher | None = db.session.get(Voucher, voucher_id)
assert voucher is not None
for i in range(len(voucher.currencies)):
for j in range(len(voucher.currencies[i].debit)):
voucher_data.currencies[i].debit[j].id \
= voucher.currencies[i].debit[j].id
for j in range(len(voucher.currencies[i].credit)):
voucher_data.currencies[i].credit[j].id \
= voucher.currencies[i].credit[j].id
journal_entry: JournalEntry | None \
= db.session.get(JournalEntry, journal_entry_id)
assert journal_entry is not None
for i in range(len(journal_entry.currencies)):
for j in range(len(journal_entry.currencies[i].debit)):
journal_entry_data.currencies[i].debit[j].id \
= journal_entry.currencies[i].debit[j].id
for j in range(len(journal_entry.currencies[i].credit)):
journal_entry_data.currencies[i].credit[j].id \
= journal_entry.currencies[i].credit[j].id