Renamed "voucher" to "journal entry".
This commit is contained in:
parent
8f909965a9
commit
b1af1d7425
@ -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)
|
||||
|
@ -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")
|
107
src/accounting/journal_entry/converters.py
Normal file
107
src/accounting/journal_entry/converters.py
Normal file
@ -0,0 +1,107 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""The path converters for the journal entry management.
|
||||
|
||||
"""
|
||||
from datetime import date
|
||||
|
||||
from flask import abort
|
||||
from sqlalchemy.orm import selectinload
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
from accounting.models import JournalEntry, JournalEntryLineItem
|
||||
from accounting.utils.journal_entry_types import JournalEntryType
|
||||
|
||||
|
||||
class JournalEntryConverter(BaseConverter):
|
||||
"""The journal entry converter to convert the journal entry ID from and to
|
||||
the corresponding journal entry in the routes."""
|
||||
|
||||
def to_python(self, value: str) -> JournalEntry:
|
||||
"""Converts a journal entry ID to a journal entry.
|
||||
|
||||
:param value: The journal entry ID.
|
||||
:return: The corresponding journal entry.
|
||||
"""
|
||||
journal_entry: JournalEntry | None = JournalEntry.query\
|
||||
.join(JournalEntryLineItem)\
|
||||
.filter(JournalEntry.id == value)\
|
||||
.options(selectinload(JournalEntry.line_items)
|
||||
.selectinload(JournalEntryLineItem.offsets)
|
||||
.selectinload(JournalEntryLineItem.journal_entry))\
|
||||
.first()
|
||||
if journal_entry is None:
|
||||
abort(404)
|
||||
return journal_entry
|
||||
|
||||
def to_url(self, value: JournalEntry) -> str:
|
||||
"""Converts a journal entry to its ID.
|
||||
|
||||
:param value: The journal entry.
|
||||
:return: The ID.
|
||||
"""
|
||||
return str(value.id)
|
||||
|
||||
|
||||
class JournalEntryTypeConverter(BaseConverter):
|
||||
"""The journal entry converter to convert the journal entry type ID from
|
||||
and to the corresponding journal entry type in the routes."""
|
||||
|
||||
def to_python(self, value: str) -> JournalEntryType:
|
||||
"""Converts a journal entry ID to a journal entry.
|
||||
|
||||
:param value: The journal entry ID.
|
||||
:return: The corresponding journal entry type.
|
||||
"""
|
||||
type_dict: dict[str, JournalEntryType] \
|
||||
= {x.value: x for x in JournalEntryType}
|
||||
journal_entry_type: JournalEntryType | None = type_dict.get(value)
|
||||
if journal_entry_type is None:
|
||||
abort(404)
|
||||
return journal_entry_type
|
||||
|
||||
def to_url(self, value: JournalEntryType) -> str:
|
||||
"""Converts a journal entry type to its ID.
|
||||
|
||||
:param value: The journal entry type.
|
||||
:return: The ID.
|
||||
"""
|
||||
return str(value.value)
|
||||
|
||||
|
||||
class DateConverter(BaseConverter):
|
||||
"""The date converter to convert the ISO date from and to the
|
||||
corresponding date in the routes."""
|
||||
|
||||
def to_python(self, value: str) -> date:
|
||||
"""Converts an ISO date to a date.
|
||||
|
||||
:param value: The ISO date.
|
||||
:return: The corresponding date.
|
||||
"""
|
||||
try:
|
||||
return date.fromisoformat(value)
|
||||
except ValueError:
|
||||
abort(404)
|
||||
|
||||
def to_url(self, value: date) -> str:
|
||||
"""Converts a date to its ISO date.
|
||||
|
||||
:param value: The date.
|
||||
:return: The ISO date.
|
||||
"""
|
||||
return value.isoformat()
|
@ -14,9 +14,9 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# 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
|
@ -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,
|
@ -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] \
|
@ -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")
|
||||
|
95
src/accounting/journal_entry/forms/reorder.py
Normal file
95
src/accounting/journal_entry/forms/reorder.py
Normal file
@ -0,0 +1,95 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""The reorder forms for the journal entry management.
|
||||
|
||||
"""
|
||||
from datetime import date
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import request
|
||||
|
||||
from accounting import db
|
||||
from accounting.models import JournalEntry
|
||||
|
||||
|
||||
def sort_journal_entries_in(journal_entry_date: date,
|
||||
exclude: int | None = None) -> None:
|
||||
"""Sorts the journal entries under a date after changing the date or
|
||||
deleting a journal entry.
|
||||
|
||||
:param journal_entry_date: The date of the journal entry.
|
||||
:param exclude: The journal entry ID to exclude.
|
||||
:return: None.
|
||||
"""
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [JournalEntry.date == journal_entry_date]
|
||||
if exclude is not None:
|
||||
conditions.append(JournalEntry.id != exclude)
|
||||
journal_entries: list[JournalEntry] = JournalEntry.query\
|
||||
.filter(*conditions)\
|
||||
.order_by(JournalEntry.no).all()
|
||||
for i in range(len(journal_entries)):
|
||||
if journal_entries[i].no != i + 1:
|
||||
journal_entries[i].no = i + 1
|
||||
|
||||
|
||||
class JournalEntryReorderForm:
|
||||
"""The form to reorder the journal entries."""
|
||||
|
||||
def __init__(self, journal_entry_date: date):
|
||||
"""Constructs the form to reorder the journal entries in a day.
|
||||
|
||||
:param journal_entry_date: The date.
|
||||
"""
|
||||
self.date: date = journal_entry_date
|
||||
self.is_modified: bool = False
|
||||
|
||||
def save_order(self) -> None:
|
||||
"""Saves the order of the account.
|
||||
|
||||
:return:
|
||||
"""
|
||||
journal_entries: list[JournalEntry] = JournalEntry.query\
|
||||
.filter(JournalEntry.date == self.date).all()
|
||||
|
||||
# Collects the specified order.
|
||||
orders: dict[JournalEntry, int] = {}
|
||||
for journal_entry in journal_entries:
|
||||
if f"{journal_entry.id}-no" in request.form:
|
||||
try:
|
||||
orders[journal_entry] \
|
||||
= int(request.form[f"{journal_entry.id}-no"])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Missing and invalid orders are appended to the end.
|
||||
missing: list[JournalEntry] \
|
||||
= [x for x in journal_entries if x not in orders]
|
||||
if len(missing) > 0:
|
||||
next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
|
||||
for journal_entry in missing:
|
||||
orders[journal_entry] = next_no
|
||||
|
||||
# Sort by the specified order first, and their original order.
|
||||
journal_entries.sort(key=lambda x: (orders[x], x.no))
|
||||
|
||||
# Update the orders.
|
||||
with db.session.no_autoflush:
|
||||
for i in range(len(journal_entries)):
|
||||
if journal_entries[i].no != i + 1:
|
||||
journal_entries[i].no = i + 1
|
||||
self.is_modified = True
|
@ -14,7 +14,7 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# 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)
|
@ -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.
|
||||
|
||||
"""
|
@ -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
|
336
src/accounting/journal_entry/utils/operators.py
Normal file
336
src/accounting/journal_entry/utils/operators.py
Normal file
@ -0,0 +1,336 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""The operators for different journal entry types.
|
||||
|
||||
"""
|
||||
import typing as t
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from flask import render_template, request, abort
|
||||
from flask_wtf import FlaskForm
|
||||
|
||||
from accounting.models import JournalEntry
|
||||
from accounting.template_globals import default_currency_code
|
||||
from accounting.utils.journal_entry_types import JournalEntryType
|
||||
from accounting.journal_entry.forms import JournalEntryForm, \
|
||||
CashReceiptJournalEntryForm, CashDisbursementJournalEntryForm, \
|
||||
TransferJournalEntryForm
|
||||
from accounting.journal_entry.forms.line_item import LineItemForm
|
||||
|
||||
|
||||
class JournalEntryOperator(ABC):
|
||||
"""The base journal entry operator."""
|
||||
CHECK_ORDER: int = -1
|
||||
"""The order when checking the journal entry operator."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def form(self) -> t.Type[JournalEntryForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def render_create_template(self, form: FlaskForm) -> str:
|
||||
"""Renders the template for the form to create a journal entry.
|
||||
|
||||
:param form: The journal entry form.
|
||||
:return: the form to create a journal entry.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def render_detail_template(self, journal_entry: JournalEntry) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:return: the detail page.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def render_edit_template(self, journal_entry: JournalEntry,
|
||||
form: FlaskForm) -> str:
|
||||
"""Renders the template for the form to edit a journal entry.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:param form: The form.
|
||||
:return: the form to edit a journal entry.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def is_my_type(self, journal_entry: JournalEntry) -> bool:
|
||||
"""Checks and returns whether the journal entry belongs to the type.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:return: True if the journal entry belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
|
||||
@property
|
||||
def _line_item_template(self) -> str:
|
||||
"""Renders and returns the template for the line item sub-form.
|
||||
|
||||
:return: The template for the line item sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/journal-entry/include/form-line-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
debit_credit="DEBIT_CREDIT",
|
||||
line_item_index="LINE_ITEM_INDEX",
|
||||
form=LineItemForm())
|
||||
|
||||
|
||||
class CashReceiptJournalEntry(JournalEntryOperator):
|
||||
"""A cash receipt journal entry."""
|
||||
CHECK_ORDER: int = 2
|
||||
"""The order when checking the journal entry operator."""
|
||||
|
||||
@property
|
||||
def form(self) -> t.Type[JournalEntryForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
return CashReceiptJournalEntryForm
|
||||
|
||||
def render_create_template(self, form: CashReceiptJournalEntryForm) -> str:
|
||||
"""Renders the template for the form to create a journal entry.
|
||||
|
||||
:param form: The journal entry form.
|
||||
:return: the form to create a journal entry.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/journal-entry/receipt/create.html",
|
||||
form=form,
|
||||
journal_entry_type=JournalEntryType.CASH_RECEIPT,
|
||||
currency_template=self.__currency_template,
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def render_detail_template(self, journal_entry: JournalEntry) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:return: the detail page.
|
||||
"""
|
||||
return render_template("accounting/journal-entry/receipt/detail.html",
|
||||
obj=journal_entry)
|
||||
|
||||
def render_edit_template(self, journal_entry: JournalEntry,
|
||||
form: CashReceiptJournalEntryForm) -> str:
|
||||
"""Renders the template for the form to edit a journal entry.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:param form: The form.
|
||||
:return: the form to edit a journal entry.
|
||||
"""
|
||||
return render_template("accounting/journal-entry/receipt/edit.html",
|
||||
journal_entry=journal_entry, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def is_my_type(self, journal_entry: JournalEntry) -> bool:
|
||||
"""Checks and returns whether the journal entry belongs to the type.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:return: True if the journal entry belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
return journal_entry.is_cash_receipt
|
||||
|
||||
@property
|
||||
def __currency_template(self) -> str:
|
||||
"""Renders and returns the template for the currency sub-form.
|
||||
|
||||
:return: The template for the currency sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/journal-entry/receipt/include/form-currency.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
currency_code_data=default_currency_code(),
|
||||
credit_total="-")
|
||||
|
||||
|
||||
class CashDisbursementJournalEntry(JournalEntryOperator):
|
||||
"""A cash disbursement journal entry."""
|
||||
CHECK_ORDER: int = 1
|
||||
"""The order when checking the journal entry operator."""
|
||||
|
||||
@property
|
||||
def form(self) -> t.Type[JournalEntryForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
return CashDisbursementJournalEntryForm
|
||||
|
||||
def render_create_template(self, form: CashDisbursementJournalEntryForm) \
|
||||
-> str:
|
||||
"""Renders the template for the form to create a journal entry.
|
||||
|
||||
:param form: The journal entry form.
|
||||
:return: the form to create a journal entry.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/journal-entry/disbursement/create.html",
|
||||
form=form,
|
||||
journal_entry_type=JournalEntryType.CASH_DISBURSEMENT,
|
||||
currency_template=self.__currency_template,
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def render_detail_template(self, journal_entry: JournalEntry) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:return: the detail page.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/journal-entry/disbursement/detail.html",
|
||||
obj=journal_entry)
|
||||
|
||||
def render_edit_template(self, journal_entry: JournalEntry,
|
||||
form: CashDisbursementJournalEntryForm) -> str:
|
||||
"""Renders the template for the form to edit a journal entry.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:param form: The form.
|
||||
:return: the form to edit a journal entry.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/journal-entry/disbursement/edit.html",
|
||||
journal_entry=journal_entry, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def is_my_type(self, journal_entry: JournalEntry) -> bool:
|
||||
"""Checks and returns whether the journal entry belongs to the type.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:return: True if the journal entry belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
return journal_entry.is_cash_disbursement
|
||||
|
||||
@property
|
||||
def __currency_template(self) -> str:
|
||||
"""Renders and returns the template for the currency sub-form.
|
||||
|
||||
:return: The template for the currency sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/journal-entry/disbursement/include/form-currency.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
currency_code_data=default_currency_code(),
|
||||
debit_total="-")
|
||||
|
||||
|
||||
class TransferJournalEntry(JournalEntryOperator):
|
||||
"""A transfer journal entry."""
|
||||
CHECK_ORDER: int = 3
|
||||
"""The order when checking the journal entry operator."""
|
||||
|
||||
@property
|
||||
def form(self) -> t.Type[JournalEntryForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
return TransferJournalEntryForm
|
||||
|
||||
def render_create_template(self, form: TransferJournalEntryForm) -> str:
|
||||
"""Renders the template for the form to create a journal entry.
|
||||
|
||||
:param form: The journal entry form.
|
||||
:return: the form to create a journal entry.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/journal-entry/transfer/create.html",
|
||||
form=form,
|
||||
journal_entry_type=JournalEntryType.TRANSFER,
|
||||
currency_template=self.__currency_template,
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def render_detail_template(self, journal_entry: JournalEntry) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:return: the detail page.
|
||||
"""
|
||||
return render_template("accounting/journal-entry/transfer/detail.html",
|
||||
obj=journal_entry)
|
||||
|
||||
def render_edit_template(self, journal_entry: JournalEntry,
|
||||
form: TransferJournalEntryForm) -> str:
|
||||
"""Renders the template for the form to edit a journal entry.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:param form: The form.
|
||||
:return: the form to edit a journal entry.
|
||||
"""
|
||||
return render_template("accounting/journal-entry/transfer/edit.html",
|
||||
journal_entry=journal_entry, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def is_my_type(self, journal_entry: JournalEntry) -> bool:
|
||||
"""Checks and returns whether the journal entry belongs to the type.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:return: True if the journal entry belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def __currency_template(self) -> str:
|
||||
"""Renders and returns the template for the currency sub-form.
|
||||
|
||||
:return: The template for the currency sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/journal-entry/transfer/include/form-currency.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
currency_code_data=default_currency_code(),
|
||||
debit_total="-", credit_total="-")
|
||||
|
||||
|
||||
JOURNAL_ENTRY_TYPE_TO_OP: dict[JournalEntryType, JournalEntryOperator] \
|
||||
= {JournalEntryType.CASH_RECEIPT: CashReceiptJournalEntry(),
|
||||
JournalEntryType.CASH_DISBURSEMENT: CashDisbursementJournalEntry(),
|
||||
JournalEntryType.TRANSFER: TransferJournalEntry()}
|
||||
"""The map from the journal entry types to their operators."""
|
||||
|
||||
|
||||
def get_journal_entry_op(journal_entry: JournalEntry,
|
||||
is_check_as: bool = False) -> JournalEntryOperator:
|
||||
"""Returns the journal entry operator that may be specified in the "as"
|
||||
query parameter. If it is not specified, check the journal entry type from
|
||||
the journal entry.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:param is_check_as: True to check the "as" parameter, or False otherwise.
|
||||
:return: None.
|
||||
"""
|
||||
if is_check_as and "as" in request.args:
|
||||
type_dict: dict[str, JournalEntryType] \
|
||||
= {x.value: x for x in JournalEntryType}
|
||||
if request.args["as"] not in type_dict:
|
||||
abort(404)
|
||||
return JOURNAL_ENTRY_TYPE_TO_OP[type_dict[request.args["as"]]]
|
||||
for journal_entry_type in sorted(JOURNAL_ENTRY_TYPE_TO_OP.values(),
|
||||
key=lambda x: x.CHECK_ORDER):
|
||||
if journal_entry_type.is_my_type(journal_entry):
|
||||
return journal_entry_type
|
@ -23,7 +23,7 @@ import sqlalchemy as sa
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from 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 \
|
235
src/accounting/journal_entry/views.py
Normal file
235
src/accounting/journal_entry/views.py
Normal file
@ -0,0 +1,235 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""The views for the journal entry management.
|
||||
|
||||
"""
|
||||
from datetime import date
|
||||
from urllib.parse import parse_qsl, urlencode
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import Blueprint, render_template, session, redirect, request, \
|
||||
flash, url_for
|
||||
from werkzeug.datastructures import ImmutableMultiDict
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import lazy_gettext
|
||||
from accounting.models import JournalEntry
|
||||
from accounting.utils.cast import s
|
||||
from accounting.utils.flash_errors import flash_form_errors
|
||||
from accounting.utils.next_uri import inherit_next, or_next
|
||||
from accounting.utils.permission import has_permission, can_view, can_edit
|
||||
from accounting.utils.journal_entry_types import JournalEntryType
|
||||
from accounting.utils.user import get_current_user_pk
|
||||
from .forms import sort_journal_entries_in, JournalEntryReorderForm
|
||||
from .template_filters import with_type, to_transfer, format_amount_input, \
|
||||
text2html
|
||||
from .utils.operators import JournalEntryOperator, JOURNAL_ENTRY_TYPE_TO_OP, \
|
||||
get_journal_entry_op
|
||||
|
||||
bp: Blueprint = Blueprint("journal-entry", __name__)
|
||||
"""The view blueprint for the journal entry management."""
|
||||
bp.add_app_template_filter(with_type, "accounting_journal_entry_with_type")
|
||||
bp.add_app_template_filter(to_transfer, "accounting_journal_entry_to_transfer")
|
||||
bp.add_app_template_filter(format_amount_input,
|
||||
"accounting_journal_entry_format_amount_input")
|
||||
bp.add_app_template_filter(text2html, "accounting_journal_entry_text2html")
|
||||
|
||||
|
||||
@bp.get("/create/<journalEntryType:journal_entry_type>", endpoint="create")
|
||||
@has_permission(can_edit)
|
||||
def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
|
||||
"""Shows the form to add a journal entry.
|
||||
|
||||
:param journal_entry_type: The journal entry type.
|
||||
:return: The form to add a journal entry.
|
||||
"""
|
||||
journal_entry_op: JournalEntryOperator \
|
||||
= JOURNAL_ENTRY_TYPE_TO_OP[journal_entry_type]
|
||||
form: journal_entry_op.form
|
||||
if "form" in session:
|
||||
form = journal_entry_op.form(
|
||||
ImmutableMultiDict(parse_qsl(session["form"])))
|
||||
del session["form"]
|
||||
form.validate()
|
||||
else:
|
||||
form = journal_entry_op.form()
|
||||
form.date.data = date.today()
|
||||
return journal_entry_op.render_create_template(form)
|
||||
|
||||
|
||||
@bp.post("/store/<journalEntryType:journal_entry_type>", endpoint="store")
|
||||
@has_permission(can_edit)
|
||||
def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
|
||||
"""Adds a journal entry.
|
||||
|
||||
:param journal_entry_type: The journal entry type.
|
||||
:return: The redirection to the journal entry detail on success, or the
|
||||
journal entry creation form on error.
|
||||
"""
|
||||
journal_entry_op: JournalEntryOperator \
|
||||
= JOURNAL_ENTRY_TYPE_TO_OP[journal_entry_type]
|
||||
form: journal_entry_op.form = journal_entry_op.form(request.form)
|
||||
if not form.validate():
|
||||
flash_form_errors(form)
|
||||
session["form"] = urlencode(list(request.form.items()))
|
||||
return redirect(inherit_next(with_type(
|
||||
url_for("accounting.journal-entry.create",
|
||||
journal_entry_type=journal_entry_type))))
|
||||
journal_entry: JournalEntry = JournalEntry()
|
||||
form.populate_obj(journal_entry)
|
||||
db.session.add(journal_entry)
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The journal entry is added successfully")),
|
||||
"success")
|
||||
return redirect(inherit_next(__get_detail_uri(journal_entry)))
|
||||
|
||||
|
||||
@bp.get("/<journalEntry:journal_entry>", endpoint="detail")
|
||||
@has_permission(can_view)
|
||||
def show_journal_entry_detail(journal_entry: JournalEntry) -> str:
|
||||
"""Shows the journal entry detail.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:return: The detail.
|
||||
"""
|
||||
journal_entry_op: JournalEntryOperator \
|
||||
= get_journal_entry_op(journal_entry)
|
||||
return journal_entry_op.render_detail_template(journal_entry)
|
||||
|
||||
|
||||
@bp.get("/<journalEntry:journal_entry>/edit", endpoint="edit")
|
||||
@has_permission(can_edit)
|
||||
def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
|
||||
"""Shows the form to edit a journal entry.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:return: The form to edit the journal entry.
|
||||
"""
|
||||
journal_entry_op: JournalEntryOperator \
|
||||
= get_journal_entry_op(journal_entry, is_check_as=True)
|
||||
form: journal_entry_op.form
|
||||
if "form" in session:
|
||||
form = journal_entry_op.form(
|
||||
ImmutableMultiDict(parse_qsl(session["form"])))
|
||||
del session["form"]
|
||||
form.obj = journal_entry
|
||||
form.validate()
|
||||
else:
|
||||
form = journal_entry_op.form(obj=journal_entry)
|
||||
return journal_entry_op.render_edit_template(journal_entry, form)
|
||||
|
||||
|
||||
@bp.post("/<journalEntry:journal_entry>/update", endpoint="update")
|
||||
@has_permission(can_edit)
|
||||
def update_journal_entry(journal_entry: JournalEntry) -> redirect:
|
||||
"""Updates a journal entry.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:return: The redirection to the journal entry detail on success, or the
|
||||
journal entry edit form on error.
|
||||
"""
|
||||
journal_entry_op: JournalEntryOperator \
|
||||
= get_journal_entry_op(journal_entry, is_check_as=True)
|
||||
form: journal_entry_op.form = journal_entry_op.form(request.form)
|
||||
form.obj = journal_entry
|
||||
if not form.validate():
|
||||
flash_form_errors(form)
|
||||
session["form"] = urlencode(list(request.form.items()))
|
||||
return redirect(inherit_next(with_type(
|
||||
url_for("accounting.journal-entry.edit",
|
||||
journal_entry=journal_entry))))
|
||||
with db.session.no_autoflush:
|
||||
form.populate_obj(journal_entry)
|
||||
if not form.is_modified:
|
||||
flash(s(lazy_gettext("The journal entry was not modified.")),
|
||||
"success")
|
||||
return redirect(inherit_next(__get_detail_uri(journal_entry)))
|
||||
journal_entry.updated_by_id = get_current_user_pk()
|
||||
journal_entry.updated_at = sa.func.now()
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The journal entry is updated successfully.")),
|
||||
"success")
|
||||
return redirect(inherit_next(__get_detail_uri(journal_entry)))
|
||||
|
||||
|
||||
@bp.post("/<journalEntry:journal_entry>/delete", endpoint="delete")
|
||||
@has_permission(can_edit)
|
||||
def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
|
||||
"""Deletes a journal entry.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:return: The redirection to the journal entry list on success, or the
|
||||
journal entry detail on error.
|
||||
"""
|
||||
journal_entry.delete()
|
||||
sort_journal_entries_in(journal_entry.date, journal_entry.id)
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The journal entry is deleted successfully.")),
|
||||
"success")
|
||||
return redirect(or_next(__get_default_page_uri()))
|
||||
|
||||
|
||||
@bp.get("/dates/<date:journal_entry_date>", endpoint="order")
|
||||
@has_permission(can_view)
|
||||
def show_journal_entry_order(journal_entry_date: date) -> str:
|
||||
"""Shows the order of the journal entries in a same date.
|
||||
|
||||
:param journal_entry_date: The date.
|
||||
:return: The order of the journal entries in the date.
|
||||
"""
|
||||
journal_entries: list[JournalEntry] = JournalEntry.query \
|
||||
.filter(JournalEntry.date == journal_entry_date) \
|
||||
.order_by(JournalEntry.no).all()
|
||||
return render_template("accounting/journal-entry/order.html",
|
||||
date=journal_entry_date, list=journal_entries)
|
||||
|
||||
|
||||
@bp.post("/dates/<date:journal_entry_date>", endpoint="sort")
|
||||
@has_permission(can_edit)
|
||||
def sort_journal_entries(journal_entry_date: date) -> redirect:
|
||||
"""Reorders the journal entries in a date.
|
||||
|
||||
:param journal_entry_date: The date.
|
||||
:return: The redirection to the incoming account or the account list. The
|
||||
reordering operation does not fail.
|
||||
"""
|
||||
form: JournalEntryReorderForm = JournalEntryReorderForm(journal_entry_date)
|
||||
form.save_order()
|
||||
if not form.is_modified:
|
||||
flash(s(lazy_gettext("The order was not modified.")), "success")
|
||||
return redirect(or_next(__get_default_page_uri()))
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The order is updated successfully.")), "success")
|
||||
return redirect(or_next(__get_default_page_uri()))
|
||||
|
||||
|
||||
def __get_detail_uri(journal_entry: JournalEntry) -> str:
|
||||
"""Returns the detail URI of a journal entry.
|
||||
|
||||
:param journal_entry: The journal entry.
|
||||
:return: The detail URI of the journal entry.
|
||||
"""
|
||||
return url_for("accounting.journal-entry.detail",
|
||||
journal_entry=journal_entry)
|
||||
|
||||
|
||||
def __get_default_page_uri() -> str:
|
||||
"""Returns the URI for the default page.
|
||||
|
||||
:return: The URI for the default page.
|
||||
"""
|
||||
return url_for("accounting.report.default")
|
@ -449,12 +449,12 @@ class CurrencyL10n(db.Model):
|
||||
"""The localized name."""
|
||||
|
||||
|
||||
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,7 +556,7 @@ 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,
|
||||
return [JournalEntryCurrency(code=x,
|
||||
debit=[y for y in by_currency[x]
|
||||
if y.is_debit],
|
||||
credit=[y for y in by_currency[x]
|
||||
@ -564,9 +565,10 @@ class Voucher(db.Model):
|
||||
|
||||
@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",
|
||||
journal_entry_id = db.Column(db.Integer,
|
||||
db.ForeignKey(JournalEntry.id,
|
||||
onupdate="CASCADE",
|
||||
ondelete="CASCADE"),
|
||||
nullable=False)
|
||||
"""The voucher ID."""
|
||||
voucher = db.relationship(Voucher, back_populates="line_items")
|
||||
"""The voucher."""
|
||||
"""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)])
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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."""
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -149,7 +149,7 @@
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/** The voucher management */
|
||||
/** The journal entry management */
|
||||
.accounting-currency-control {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
@ -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) {
|
@ -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;
|
||||
|
@ -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.
|
@ -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
|
||||
*/
|
||||
|
@ -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 %}
|
@ -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">
|
@ -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 %}
|
@ -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>
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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>
|
@ -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 %}
|
@ -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>
|
@ -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 }}">
|
@ -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 %}
|
@ -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">
|
@ -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 %}
|
@ -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>
|
@ -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 %}
|
@ -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 %}
|
@ -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">
|
@ -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 %}
|
@ -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>
|
@ -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 %}
|
@ -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" %}
|
||||
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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" %}
|
||||
|
||||
|
@ -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" %}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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" %}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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" %}
|
||||
|
||||
|
@ -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."""
|
@ -1,105 +0,0 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""The path converters for the voucher management.
|
||||
|
||||
"""
|
||||
from datetime import date
|
||||
|
||||
from flask import abort
|
||||
from sqlalchemy.orm import selectinload
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
from accounting.models import Voucher, JournalEntryLineItem
|
||||
from accounting.utils.voucher_types import VoucherType
|
||||
|
||||
|
||||
class VoucherConverter(BaseConverter):
|
||||
"""The voucher converter to convert the voucher ID from and to the
|
||||
corresponding voucher in the routes."""
|
||||
|
||||
def to_python(self, value: str) -> Voucher:
|
||||
"""Converts a voucher ID to a voucher.
|
||||
|
||||
:param value: The voucher ID.
|
||||
:return: The corresponding voucher.
|
||||
"""
|
||||
voucher: Voucher | None = Voucher.query.join(JournalEntryLineItem)\
|
||||
.filter(Voucher.id == value)\
|
||||
.options(selectinload(Voucher.line_items)
|
||||
.selectinload(JournalEntryLineItem.offsets)
|
||||
.selectinload(JournalEntryLineItem.voucher))\
|
||||
.first()
|
||||
if voucher is None:
|
||||
abort(404)
|
||||
return voucher
|
||||
|
||||
def to_url(self, value: Voucher) -> str:
|
||||
"""Converts a voucher to its ID.
|
||||
|
||||
:param value: The voucher.
|
||||
:return: The ID.
|
||||
"""
|
||||
return str(value.id)
|
||||
|
||||
|
||||
class VoucherTypeConverter(BaseConverter):
|
||||
"""The voucher converter to convert the voucher type ID from and to the
|
||||
corresponding voucher type in the routes."""
|
||||
|
||||
def to_python(self, value: str) -> VoucherType:
|
||||
"""Converts a voucher ID to a voucher.
|
||||
|
||||
:param value: The voucher ID.
|
||||
:return: The corresponding voucher type.
|
||||
"""
|
||||
type_dict: dict[str, VoucherType] = {x.value: x for x in VoucherType}
|
||||
voucher_type: VoucherType | None = type_dict.get(value)
|
||||
if voucher_type is None:
|
||||
abort(404)
|
||||
return voucher_type
|
||||
|
||||
def to_url(self, value: VoucherType) -> str:
|
||||
"""Converts a voucher type to its ID.
|
||||
|
||||
:param value: The voucher type.
|
||||
:return: The ID.
|
||||
"""
|
||||
return str(value.value)
|
||||
|
||||
|
||||
class DateConverter(BaseConverter):
|
||||
"""The date converter to convert the ISO date from and to the
|
||||
corresponding date in the routes."""
|
||||
|
||||
def to_python(self, value: str) -> date:
|
||||
"""Converts an ISO date to a date.
|
||||
|
||||
:param value: The ISO date.
|
||||
:return: The corresponding date.
|
||||
"""
|
||||
try:
|
||||
return date.fromisoformat(value)
|
||||
except ValueError:
|
||||
abort(404)
|
||||
|
||||
def to_url(self, value: date) -> str:
|
||||
"""Converts a date to its ISO date.
|
||||
|
||||
:param value: The date.
|
||||
:return: The ISO date.
|
||||
"""
|
||||
return value.isoformat()
|
@ -1,92 +0,0 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""The reorder forms for the voucher management.
|
||||
|
||||
"""
|
||||
from datetime import date
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import request
|
||||
|
||||
from accounting import db
|
||||
from accounting.models import Voucher
|
||||
|
||||
|
||||
def sort_vouchers_in(voucher_date: date, exclude: int | None = None) -> None:
|
||||
"""Sorts the vouchers under a date after changing the date or deleting
|
||||
a voucher.
|
||||
|
||||
:param voucher_date: The date of the voucher.
|
||||
:param exclude: The voucher ID to exclude.
|
||||
:return: None.
|
||||
"""
|
||||
conditions: list[sa.BinaryExpression] = [Voucher.date == voucher_date]
|
||||
if exclude is not None:
|
||||
conditions.append(Voucher.id != exclude)
|
||||
vouchers: list[Voucher] = Voucher.query\
|
||||
.filter(*conditions)\
|
||||
.order_by(Voucher.no).all()
|
||||
for i in range(len(vouchers)):
|
||||
if vouchers[i].no != i + 1:
|
||||
vouchers[i].no = i + 1
|
||||
|
||||
|
||||
class VoucherReorderForm:
|
||||
"""The form to reorder the vouchers."""
|
||||
|
||||
def __init__(self, voucher_date: date):
|
||||
"""Constructs the form to reorder the vouchers in a day.
|
||||
|
||||
:param voucher_date: The date.
|
||||
"""
|
||||
self.date: date = voucher_date
|
||||
self.is_modified: bool = False
|
||||
|
||||
def save_order(self) -> None:
|
||||
"""Saves the order of the account.
|
||||
|
||||
:return:
|
||||
"""
|
||||
vouchers: list[Voucher] = Voucher.query\
|
||||
.filter(Voucher.date == self.date).all()
|
||||
|
||||
# Collects the specified order.
|
||||
orders: dict[Voucher, int] = {}
|
||||
for voucher in vouchers:
|
||||
if f"{voucher.id}-no" in request.form:
|
||||
try:
|
||||
orders[voucher] = int(request.form[f"{voucher.id}-no"])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Missing and invalid orders are appended to the end.
|
||||
missing: list[Voucher] \
|
||||
= [x for x in vouchers if x not in orders]
|
||||
if len(missing) > 0:
|
||||
next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
|
||||
for voucher in missing:
|
||||
orders[voucher] = next_no
|
||||
|
||||
# Sort by the specified order first, and their original order.
|
||||
vouchers.sort(key=lambda x: (orders[x], x.no))
|
||||
|
||||
# Update the orders.
|
||||
with db.session.no_autoflush:
|
||||
for i in range(len(vouchers)):
|
||||
if vouchers[i].no != i + 1:
|
||||
vouchers[i].no = i + 1
|
||||
self.is_modified = True
|
@ -1,328 +0,0 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""The operators for different voucher types.
|
||||
|
||||
"""
|
||||
import typing as t
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from flask import render_template, request, abort
|
||||
from flask_wtf import FlaskForm
|
||||
|
||||
from accounting.models import Voucher
|
||||
from accounting.template_globals import default_currency_code
|
||||
from accounting.utils.voucher_types import VoucherType
|
||||
from accounting.voucher.forms import VoucherForm, CashReceiptVoucherForm, \
|
||||
CashDisbursementVoucherForm, TransferVoucherForm
|
||||
from accounting.voucher.forms.line_item import LineItemForm
|
||||
|
||||
|
||||
class VoucherOperator(ABC):
|
||||
"""The base voucher operator."""
|
||||
CHECK_ORDER: int = -1
|
||||
"""The order when checking the voucher operator."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def form(self) -> t.Type[VoucherForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def render_create_template(self, form: FlaskForm) -> str:
|
||||
"""Renders the template for the form to create a voucher.
|
||||
|
||||
:param form: The voucher form.
|
||||
:return: the form to create a voucher.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def render_detail_template(self, voucher: Voucher) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: the detail page.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def render_edit_template(self, voucher: Voucher, form: FlaskForm) -> str:
|
||||
"""Renders the template for the form to edit a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:param form: The form.
|
||||
:return: the form to edit a voucher.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def is_my_type(self, voucher: Voucher) -> bool:
|
||||
"""Checks and returns whether the voucher belongs to the type.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: True if the voucher belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
|
||||
@property
|
||||
def _line_item_template(self) -> str:
|
||||
"""Renders and returns the template for the line item sub-form.
|
||||
|
||||
:return: The template for the line item sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/voucher/include/form-line-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
debit_credit="DEBIT_CREDIT",
|
||||
line_item_index="LINE_ITEM_INDEX",
|
||||
form=LineItemForm())
|
||||
|
||||
|
||||
class CashReceiptVoucher(VoucherOperator):
|
||||
"""A cash receipt voucher."""
|
||||
CHECK_ORDER: int = 2
|
||||
"""The order when checking the voucher operator."""
|
||||
|
||||
@property
|
||||
def form(self) -> t.Type[VoucherForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
return CashReceiptVoucherForm
|
||||
|
||||
def render_create_template(self, form: CashReceiptVoucherForm) -> str:
|
||||
"""Renders the template for the form to create a voucher.
|
||||
|
||||
:param form: The voucher form.
|
||||
:return: the form to create a voucher.
|
||||
"""
|
||||
return render_template("accounting/voucher/receipt/create.html",
|
||||
form=form,
|
||||
voucher_type=VoucherType.CASH_RECEIPT,
|
||||
currency_template=self.__currency_template,
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def render_detail_template(self, voucher: Voucher) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: the detail page.
|
||||
"""
|
||||
return render_template("accounting/voucher/receipt/detail.html",
|
||||
obj=voucher)
|
||||
|
||||
def render_edit_template(self, voucher: Voucher,
|
||||
form: CashReceiptVoucherForm) -> str:
|
||||
"""Renders the template for the form to edit a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:param form: The form.
|
||||
:return: the form to edit a voucher.
|
||||
"""
|
||||
return render_template("accounting/voucher/receipt/edit.html",
|
||||
voucher=voucher, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def is_my_type(self, voucher: Voucher) -> bool:
|
||||
"""Checks and returns whether the voucher belongs to the type.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: True if the voucher belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
return voucher.is_cash_receipt
|
||||
|
||||
@property
|
||||
def __currency_template(self) -> str:
|
||||
"""Renders and returns the template for the currency sub-form.
|
||||
|
||||
:return: The template for the currency sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/voucher/receipt/include/form-currency-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
currency_code_data=default_currency_code(),
|
||||
credit_total="-")
|
||||
|
||||
|
||||
class CashDisbursementVoucher(VoucherOperator):
|
||||
"""A cash disbursement voucher."""
|
||||
CHECK_ORDER: int = 1
|
||||
"""The order when checking the voucher operator."""
|
||||
|
||||
@property
|
||||
def form(self) -> t.Type[VoucherForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
return CashDisbursementVoucherForm
|
||||
|
||||
def render_create_template(self, form: CashDisbursementVoucherForm) -> str:
|
||||
"""Renders the template for the form to create a voucher.
|
||||
|
||||
:param form: The voucher form.
|
||||
:return: the form to create a voucher.
|
||||
"""
|
||||
return render_template("accounting/voucher/disbursement/create.html",
|
||||
form=form,
|
||||
voucher_type=VoucherType.CASH_DISBURSEMENT,
|
||||
currency_template=self.__currency_template,
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def render_detail_template(self, voucher: Voucher) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: the detail page.
|
||||
"""
|
||||
return render_template("accounting/voucher/disbursement/detail.html",
|
||||
obj=voucher)
|
||||
|
||||
def render_edit_template(self, voucher: Voucher,
|
||||
form: CashDisbursementVoucherForm) -> str:
|
||||
"""Renders the template for the form to edit a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:param form: The form.
|
||||
:return: the form to edit a voucher.
|
||||
"""
|
||||
return render_template("accounting/voucher/disbursement/edit.html",
|
||||
voucher=voucher, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def is_my_type(self, voucher: Voucher) -> bool:
|
||||
"""Checks and returns whether the voucher belongs to the type.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: True if the voucher belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
return voucher.is_cash_disbursement
|
||||
|
||||
@property
|
||||
def __currency_template(self) -> str:
|
||||
"""Renders and returns the template for the currency sub-form.
|
||||
|
||||
:return: The template for the currency sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/voucher/disbursement/include/form-currency-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
currency_code_data=default_currency_code(),
|
||||
debit_total="-")
|
||||
|
||||
|
||||
class TransferVoucher(VoucherOperator):
|
||||
"""A transfer voucher."""
|
||||
CHECK_ORDER: int = 3
|
||||
"""The order when checking the voucher operator."""
|
||||
|
||||
@property
|
||||
def form(self) -> t.Type[VoucherForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
return TransferVoucherForm
|
||||
|
||||
def render_create_template(self, form: TransferVoucherForm) -> str:
|
||||
"""Renders the template for the form to create a voucher.
|
||||
|
||||
:param form: The voucher form.
|
||||
:return: the form to create a voucher.
|
||||
"""
|
||||
return render_template("accounting/voucher/transfer/create.html",
|
||||
form=form,
|
||||
voucher_type=VoucherType.TRANSFER,
|
||||
currency_template=self.__currency_template,
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def render_detail_template(self, voucher: Voucher) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: the detail page.
|
||||
"""
|
||||
return render_template("accounting/voucher/transfer/detail.html",
|
||||
obj=voucher)
|
||||
|
||||
def render_edit_template(self, voucher: Voucher,
|
||||
form: TransferVoucherForm) -> str:
|
||||
"""Renders the template for the form to edit a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:param form: The form.
|
||||
:return: the form to edit a voucher.
|
||||
"""
|
||||
return render_template("accounting/voucher/transfer/edit.html",
|
||||
voucher=voucher, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
line_item_template=self._line_item_template)
|
||||
|
||||
def is_my_type(self, voucher: Voucher) -> bool:
|
||||
"""Checks and returns whether the voucher belongs to the type.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: True if the voucher belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def __currency_template(self) -> str:
|
||||
"""Renders and returns the template for the currency sub-form.
|
||||
|
||||
:return: The template for the currency sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/voucher/transfer/include/form-currency-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
currency_code_data=default_currency_code(),
|
||||
debit_total="-", credit_total="-")
|
||||
|
||||
|
||||
VOUCHER_TYPE_TO_OP: dict[VoucherType, VoucherOperator] \
|
||||
= {VoucherType.CASH_RECEIPT: CashReceiptVoucher(),
|
||||
VoucherType.CASH_DISBURSEMENT: CashDisbursementVoucher(),
|
||||
VoucherType.TRANSFER: TransferVoucher()}
|
||||
"""The map from the voucher types to their operators."""
|
||||
|
||||
|
||||
def get_voucher_op(voucher: Voucher, is_check_as: bool = False) \
|
||||
-> VoucherOperator:
|
||||
"""Returns the voucher operator that may be specified in the "as" query
|
||||
parameter. If it is not specified, check the voucher type from the
|
||||
voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:param is_check_as: True to check the "as" parameter, or False otherwise.
|
||||
:return: None.
|
||||
"""
|
||||
if is_check_as and "as" in request.args:
|
||||
type_dict: dict[str, VoucherType] \
|
||||
= {x.value: x for x in VoucherType}
|
||||
if request.args["as"] not in type_dict:
|
||||
abort(404)
|
||||
return VOUCHER_TYPE_TO_OP[type_dict[request.args["as"]]]
|
||||
for voucher_type in sorted(VOUCHER_TYPE_TO_OP.values(),
|
||||
key=lambda x: x.CHECK_ORDER):
|
||||
if voucher_type.is_my_type(voucher):
|
||||
return voucher_type
|
@ -1,221 +0,0 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
|
||||
|
||||
# Copyright (c) 2023 imacat.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""The views for the voucher management.
|
||||
|
||||
"""
|
||||
from datetime import date
|
||||
from urllib.parse import parse_qsl, urlencode
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import Blueprint, render_template, session, redirect, request, \
|
||||
flash, url_for
|
||||
from werkzeug.datastructures import ImmutableMultiDict
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import lazy_gettext
|
||||
from accounting.models import Voucher
|
||||
from accounting.utils.cast import s
|
||||
from accounting.utils.flash_errors import flash_form_errors
|
||||
from accounting.utils.next_uri import inherit_next, or_next
|
||||
from accounting.utils.permission import has_permission, can_view, can_edit
|
||||
from accounting.utils.voucher_types import VoucherType
|
||||
from accounting.utils.user import get_current_user_pk
|
||||
from .forms import sort_vouchers_in, VoucherReorderForm
|
||||
from .template_filters import with_type, to_transfer, format_amount_input, \
|
||||
text2html
|
||||
from .utils.operators import VoucherOperator, VOUCHER_TYPE_TO_OP, \
|
||||
get_voucher_op
|
||||
|
||||
bp: Blueprint = Blueprint("voucher", __name__)
|
||||
"""The view blueprint for the voucher management."""
|
||||
bp.add_app_template_filter(with_type, "accounting_voucher_with_type")
|
||||
bp.add_app_template_filter(to_transfer, "accounting_voucher_to_transfer")
|
||||
bp.add_app_template_filter(format_amount_input,
|
||||
"accounting_voucher_format_amount_input")
|
||||
bp.add_app_template_filter(text2html, "accounting_voucher_text2html")
|
||||
|
||||
|
||||
@bp.get("/create/<voucherType:voucher_type>", endpoint="create")
|
||||
@has_permission(can_edit)
|
||||
def show_add_voucher_form(voucher_type: VoucherType) -> str:
|
||||
"""Shows the form to add a voucher.
|
||||
|
||||
:param voucher_type: The voucher type.
|
||||
:return: The form to add a voucher.
|
||||
"""
|
||||
voucher_op: VoucherOperator = VOUCHER_TYPE_TO_OP[voucher_type]
|
||||
form: voucher_op.form
|
||||
if "form" in session:
|
||||
form = voucher_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
|
||||
del session["form"]
|
||||
form.validate()
|
||||
else:
|
||||
form = voucher_op.form()
|
||||
form.date.data = date.today()
|
||||
return voucher_op.render_create_template(form)
|
||||
|
||||
|
||||
@bp.post("/store/<voucherType:voucher_type>", endpoint="store")
|
||||
@has_permission(can_edit)
|
||||
def add_voucher(voucher_type: VoucherType) -> redirect:
|
||||
"""Adds a voucher.
|
||||
|
||||
:param voucher_type: The voucher type.
|
||||
:return: The redirection to the voucher detail on success, or the
|
||||
voucher creation form on error.
|
||||
"""
|
||||
voucher_op: VoucherOperator = VOUCHER_TYPE_TO_OP[voucher_type]
|
||||
form: voucher_op.form = voucher_op.form(request.form)
|
||||
if not form.validate():
|
||||
flash_form_errors(form)
|
||||
session["form"] = urlencode(list(request.form.items()))
|
||||
return redirect(inherit_next(with_type(
|
||||
url_for("accounting.voucher.create", voucher_type=voucher_type))))
|
||||
voucher: Voucher = Voucher()
|
||||
form.populate_obj(voucher)
|
||||
db.session.add(voucher)
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The voucher is added successfully")), "success")
|
||||
return redirect(inherit_next(__get_detail_uri(voucher)))
|
||||
|
||||
|
||||
@bp.get("/<voucher:voucher>", endpoint="detail")
|
||||
@has_permission(can_view)
|
||||
def show_voucher_detail(voucher: Voucher) -> str:
|
||||
"""Shows the voucher detail.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: The detail.
|
||||
"""
|
||||
voucher_op: VoucherOperator = get_voucher_op(voucher)
|
||||
return voucher_op.render_detail_template(voucher)
|
||||
|
||||
|
||||
@bp.get("/<voucher:voucher>/edit", endpoint="edit")
|
||||
@has_permission(can_edit)
|
||||
def show_voucher_edit_form(voucher: Voucher) -> str:
|
||||
"""Shows the form to edit a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: The form to edit the voucher.
|
||||
"""
|
||||
voucher_op: VoucherOperator = get_voucher_op(voucher, is_check_as=True)
|
||||
form: voucher_op.form
|
||||
if "form" in session:
|
||||
form = voucher_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
|
||||
del session["form"]
|
||||
form.obj = voucher
|
||||
form.validate()
|
||||
else:
|
||||
form = voucher_op.form(obj=voucher)
|
||||
return voucher_op.render_edit_template(voucher, form)
|
||||
|
||||
|
||||
@bp.post("/<voucher:voucher>/update", endpoint="update")
|
||||
@has_permission(can_edit)
|
||||
def update_voucher(voucher: Voucher) -> redirect:
|
||||
"""Updates a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: The redirection to the voucher detail on success, or the voucher
|
||||
edit form on error.
|
||||
"""
|
||||
voucher_op: VoucherOperator = get_voucher_op(voucher, is_check_as=True)
|
||||
form: voucher_op.form = voucher_op.form(request.form)
|
||||
form.obj = voucher
|
||||
if not form.validate():
|
||||
flash_form_errors(form)
|
||||
session["form"] = urlencode(list(request.form.items()))
|
||||
return redirect(inherit_next(with_type(
|
||||
url_for("accounting.voucher.edit", voucher=voucher))))
|
||||
with db.session.no_autoflush:
|
||||
form.populate_obj(voucher)
|
||||
if not form.is_modified:
|
||||
flash(s(lazy_gettext("The voucher was not modified.")), "success")
|
||||
return redirect(inherit_next(__get_detail_uri(voucher)))
|
||||
voucher.updated_by_id = get_current_user_pk()
|
||||
voucher.updated_at = sa.func.now()
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The voucher is updated successfully.")), "success")
|
||||
return redirect(inherit_next(__get_detail_uri(voucher)))
|
||||
|
||||
|
||||
@bp.post("/<voucher:voucher>/delete", endpoint="delete")
|
||||
@has_permission(can_edit)
|
||||
def delete_voucher(voucher: Voucher) -> redirect:
|
||||
"""Deletes a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: The redirection to the voucher list on success, or the voucher
|
||||
detail on error.
|
||||
"""
|
||||
voucher.delete()
|
||||
sort_vouchers_in(voucher.date, voucher.id)
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The voucher is deleted successfully.")), "success")
|
||||
return redirect(or_next(__get_default_page_uri()))
|
||||
|
||||
|
||||
@bp.get("/dates/<date:voucher_date>", endpoint="order")
|
||||
@has_permission(can_view)
|
||||
def show_voucher_order(voucher_date: date) -> str:
|
||||
"""Shows the order of the vouchers in a same date.
|
||||
|
||||
:param voucher_date: The date.
|
||||
:return: The order of the vouchers in the date.
|
||||
"""
|
||||
vouchers: list[Voucher] = Voucher.query \
|
||||
.filter(Voucher.date == voucher_date) \
|
||||
.order_by(Voucher.no).all()
|
||||
return render_template("accounting/voucher/order.html",
|
||||
date=voucher_date, list=vouchers)
|
||||
|
||||
|
||||
@bp.post("/dates/<date:voucher_date>", endpoint="sort")
|
||||
@has_permission(can_edit)
|
||||
def sort_vouchers(voucher_date: date) -> redirect:
|
||||
"""Reorders the vouchers in a date.
|
||||
|
||||
:param voucher_date: The date.
|
||||
:return: The redirection to the incoming account or the account list. The
|
||||
reordering operation does not fail.
|
||||
"""
|
||||
form: VoucherReorderForm = VoucherReorderForm(voucher_date)
|
||||
form.save_order()
|
||||
if not form.is_modified:
|
||||
flash(s(lazy_gettext("The order was not modified.")), "success")
|
||||
return redirect(or_next(__get_default_page_uri()))
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The order is updated successfully.")), "success")
|
||||
return redirect(or_next(__get_default_page_uri()))
|
||||
|
||||
|
||||
def __get_detail_uri(voucher: Voucher) -> str:
|
||||
"""Returns the detail URI of a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: The detail URI of the voucher.
|
||||
"""
|
||||
return url_for("accounting.voucher.detail", voucher=voucher)
|
||||
|
||||
|
||||
def __get_default_page_uri() -> str:
|
||||
"""Returns the URI for the default page.
|
||||
|
||||
:return: The URI for the default page.
|
||||
"""
|
||||
return url_for("accounting.report.default")
|
@ -25,7 +25,7 @@ from flask import Flask
|
||||
from flask.testing import FlaskCliRunner
|
||||
|
||||
from 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
@ -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,
|
||||
[JournalEntryLineItemData(
|
||||
Accounts.RECEIVABLE,
|
||||
self.data.e_r_or1d.description, "300",
|
||||
original_line_item=self.data.e_r_or1d),
|
||||
JournalEntryLineItemData(Accounts.RECEIVABLE,
|
||||
JournalEntryLineItemData(
|
||||
Accounts.RECEIVABLE,
|
||||
self.data.e_r_or1d.description, "100",
|
||||
original_line_item=self.data.e_r_or1d),
|
||||
JournalEntryLineItemData(Accounts.RECEIVABLE,
|
||||
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,
|
||||
[JournalEntryLineItemData(
|
||||
Accounts.PAYABLE,
|
||||
self.data.e_p_or1c.description, "500",
|
||||
original_line_item=self.data.e_p_or1c),
|
||||
JournalEntryLineItemData(Accounts.PAYABLE,
|
||||
JournalEntryLineItemData(
|
||||
Accounts.PAYABLE,
|
||||
self.data.e_p_or1c.description, "300",
|
||||
original_line_item=self.data.e_p_or1c),
|
||||
JournalEntryLineItemData(Accounts.PAYABLE,
|
||||
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)
|
||||
|
@ -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,
|
||||
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 "}
|
||||
"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))
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user