diff --git a/src/accounting/__init__.py b/src/accounting/__init__.py index 32b7693..e39207f 100644 --- a/src/accounting/__init__.py +++ b/src/accounting/__init__.py @@ -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) diff --git a/src/accounting/voucher/__init__.py b/src/accounting/journal_entry/__init__.py similarity index 72% rename from src/accounting/voucher/__init__.py rename to src/accounting/journal_entry/__init__.py index 03209b1..cdfc7d7 100644 --- a/src/accounting/voucher/__init__.py +++ b/src/accounting/journal_entry/__init__.py @@ -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") diff --git a/src/accounting/journal_entry/converters.py b/src/accounting/journal_entry/converters.py new file mode 100644 index 0000000..33c6620 --- /dev/null +++ b/src/accounting/journal_entry/converters.py @@ -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() diff --git a/src/accounting/voucher/forms/__init__.py b/src/accounting/journal_entry/forms/__init__.py similarity index 72% rename from src/accounting/voucher/forms/__init__.py rename to src/accounting/journal_entry/forms/__init__.py index 0fcab97..40a7f47 100644 --- a/src/accounting/voucher/forms/__init__.py +++ b/src/accounting/journal_entry/forms/__init__.py @@ -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 diff --git a/src/accounting/voucher/forms/currency.py b/src/accounting/journal_entry/forms/currency.py similarity index 94% rename from src/accounting/voucher/forms/currency.py rename to src/accounting/journal_entry/forms/currency.py index 535fc4c..334ad27 100644 --- a/src/accounting/voucher/forms/currency.py +++ b/src/accounting/journal_entry/forms/currency.py @@ -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, diff --git a/src/accounting/voucher/forms/voucher.py b/src/accounting/journal_entry/forms/journal_entry.py similarity index 86% rename from src/accounting/voucher/forms/voucher.py rename to src/accounting/journal_entry/forms/journal_entry.py index bab8fbe..2abafc2 100644 --- a/src/accounting/voucher/forms/voucher.py +++ b/src/accounting/journal_entry/forms/journal_entry.py @@ -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] \ diff --git a/src/accounting/voucher/forms/line_item.py b/src/accounting/journal_entry/forms/line_item.py similarity index 96% rename from src/accounting/voucher/forms/line_item.py rename to src/accounting/journal_entry/forms/line_item.py index dac1a38..c27ae4a 100644 --- a/src/accounting/voucher/forms/line_item.py +++ b/src/accounting/journal_entry/forms/line_item.py @@ -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") diff --git a/src/accounting/journal_entry/forms/reorder.py b/src/accounting/journal_entry/forms/reorder.py new file mode 100644 index 0000000..51a2538 --- /dev/null +++ b/src/accounting/journal_entry/forms/reorder.py @@ -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 diff --git a/src/accounting/voucher/template_filters.py b/src/accounting/journal_entry/template_filters.py similarity index 87% rename from src/accounting/voucher/template_filters.py rename to src/accounting/journal_entry/template_filters.py index 31df53a..c24db75 100644 --- a/src/accounting/voucher/template_filters.py +++ b/src/accounting/journal_entry/template_filters.py @@ -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) diff --git a/src/accounting/voucher/utils/__init__.py b/src/accounting/journal_entry/utils/__init__.py similarity index 93% rename from src/accounting/voucher/utils/__init__.py rename to src/accounting/journal_entry/utils/__init__.py index 39d624e..024ca08 100644 --- a/src/accounting/voucher/utils/__init__.py +++ b/src/accounting/journal_entry/utils/__init__.py @@ -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. """ diff --git a/src/accounting/voucher/utils/account_option.py b/src/accounting/journal_entry/utils/account_option.py similarity index 96% rename from src/accounting/voucher/utils/account_option.py rename to src/accounting/journal_entry/utils/account_option.py index 18b655f..18068cb 100644 --- a/src/accounting/voucher/utils/account_option.py +++ b/src/accounting/journal_entry/utils/account_option.py @@ -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 diff --git a/src/accounting/voucher/utils/description_editor.py b/src/accounting/journal_entry/utils/description_editor.py similarity index 100% rename from src/accounting/voucher/utils/description_editor.py rename to src/accounting/journal_entry/utils/description_editor.py diff --git a/src/accounting/voucher/utils/offset_alias.py b/src/accounting/journal_entry/utils/offset_alias.py similarity index 100% rename from src/accounting/voucher/utils/offset_alias.py rename to src/accounting/journal_entry/utils/offset_alias.py diff --git a/src/accounting/journal_entry/utils/operators.py b/src/accounting/journal_entry/utils/operators.py new file mode 100644 index 0000000..b43bcbd --- /dev/null +++ b/src/accounting/journal_entry/utils/operators.py @@ -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 diff --git a/src/accounting/voucher/utils/original_line_items.py b/src/accounting/journal_entry/utils/original_line_items.py similarity index 93% rename from src/accounting/voucher/utils/original_line_items.py rename to src/accounting/journal_entry/utils/original_line_items.py index b8bd94a..c5def19 100644 --- a/src/accounting/voucher/utils/original_line_items.py +++ b/src/accounting/journal_entry/utils/original_line_items.py @@ -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 \ diff --git a/src/accounting/journal_entry/views.py b/src/accounting/journal_entry/views.py new file mode 100644 index 0000000..25f0e71 --- /dev/null +++ b/src/accounting/journal_entry/views.py @@ -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/", 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/", 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("/", 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("//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("//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("//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/", 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/", 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") diff --git a/src/accounting/models.py b/src/accounting/models.py index f9e4ad3..a563fb2 100644 --- a/src/accounting/models.py +++ b/src/accounting/models.py @@ -449,12 +449,12 @@ class CurrencyL10n(db.Model): """The localized name.""" -class VoucherCurrency: - """A currency in a voucher.""" +class JournalEntryCurrency: + """A currency in a journal entry.""" def __init__(self, code: str, debit: list[JournalEntryLineItem], credit: list[JournalEntryLineItem]): - """Constructs the currency in the voucher. + """Constructs the currency in the journal entry. :param code: The currency code. :param debit: The debit line items. @@ -492,13 +492,13 @@ class VoucherCurrency: return sum([x.amount for x in self.credit]) -class Voucher(db.Model): - """A voucher.""" - __tablename__ = "accounting_vouchers" +class JournalEntry(db.Model): + """A journal entry.""" + __tablename__ = "accounting_journal_entries" """The table name.""" id = db.Column(db.Integer, nullable=False, primary_key=True, autoincrement=False) - """The voucher ID.""" + """The journal entry ID.""" date = db.Column(db.Date, nullable=False) """The date.""" no = db.Column(db.Integer, nullable=False, default=text("1")) @@ -526,22 +526,23 @@ class Voucher(db.Model): updated_by = db.relationship(user_cls, foreign_keys=updated_by_id) """The updator.""" line_items = db.relationship("JournalEntryLineItem", - back_populates="voucher") + back_populates="journal_entry") """The line items.""" def __str__(self) -> str: - """Returns the string representation of this voucher. + """Returns the string representation of this journal entry. - :return: The string representation of this voucher. + :return: The string representation of this journal entry. """ if self.is_cash_disbursement: - return gettext("Cash Disbursement Voucher#%(id)s", id=self.id) + return gettext("Cash Disbursement Journal Entry#%(id)s", + id=self.id) if self.is_cash_receipt: - return gettext("Cash Receipt Voucher#%(id)s", id=self.id) - return gettext("Transfer Voucher#%(id)s", id=self.id) + return gettext("Cash Receipt Journal Entry#%(id)s", id=self.id) + return gettext("Transfer Journal Entry#%(id)s", id=self.id) @property - def currencies(self) -> list[VoucherCurrency]: + def currencies(self) -> list[JournalEntryCurrency]: """Returns the line items categorized by their currencies. :return: The currency categories. @@ -555,18 +556,19 @@ class Voucher(db.Model): codes.append(line_item.currency_code) by_currency[line_item.currency_code] = [] by_currency[line_item.currency_code].append(line_item) - return [VoucherCurrency(code=x, - debit=[y for y in by_currency[x] - if y.is_debit], - credit=[y for y in by_currency[x] - if not y.is_debit]) + return [JournalEntryCurrency(code=x, + debit=[y for y in by_currency[x] + if y.is_debit], + credit=[y for y in by_currency[x] + if not y.is_debit]) for x in codes] @property def is_cash_receipt(self) -> bool: - """Returns whether this is a cash receipt voucher. + """Returns whether this is a cash receipt journal entry. - :return: True if this is a cash receipt voucher, or False otherwise. + :return: True if this is a cash receipt journal entry, or False + otherwise. """ for currency in self.currencies: if len(currency.debit) > 1: @@ -577,9 +579,9 @@ class Voucher(db.Model): @property def is_cash_disbursement(self) -> bool: - """Returns whether this is a cash disbursement voucher. + """Returns whether this is a cash disbursement journal entry. - :return: True if this is a cash disbursement voucher, or False + :return: True if this is a cash disbursement journal entry, or False otherwise. """ for currency in self.currencies: @@ -591,9 +593,9 @@ class Voucher(db.Model): @property def can_delete(self) -> bool: - """Returns whether the voucher can be deleted. + """Returns whether the journal entry can be deleted. - :return: True if the voucher can be deleted, or False otherwise. + :return: True if the journal entry can be deleted, or False otherwise. """ if not hasattr(self, "__can_delete"): def has_offset() -> bool: @@ -605,12 +607,12 @@ class Voucher(db.Model): return getattr(self, "__can_delete") def delete(self) -> None: - """Deletes the voucher. + """Deletes the journal entry. :return: None. """ JournalEntryLineItem.query\ - .filter(JournalEntryLineItem.voucher_id == self.id).delete() + .filter(JournalEntryLineItem.journal_entry_id == self.id).delete() db.session.delete(self) @@ -621,17 +623,18 @@ class JournalEntryLineItem(db.Model): id = db.Column(db.Integer, nullable=False, primary_key=True, autoincrement=False) """The line item ID.""" - voucher_id = db.Column(db.Integer, - db.ForeignKey(Voucher.id, onupdate="CASCADE", - ondelete="CASCADE"), - nullable=False) - """The voucher ID.""" - voucher = db.relationship(Voucher, back_populates="line_items") - """The voucher.""" + journal_entry_id = db.Column(db.Integer, + db.ForeignKey(JournalEntry.id, + onupdate="CASCADE", + ondelete="CASCADE"), + nullable=False) + """The journal entry ID.""" + journal_entry = db.relationship(JournalEntry, back_populates="line_items") + """The journal entry.""" is_debit = db.Column(db.Boolean, nullable=False) """True for a debit line item, or False for a credit line item.""" no = db.Column(db.Integer, nullable=False) - """The line item number under the voucher and debit or credit.""" + """The line item number under the journal entry and debit or credit.""" original_line_item_id = db.Column(db.Integer, db.ForeignKey(id, onupdate="CASCADE"), nullable=True) @@ -670,7 +673,7 @@ class JournalEntryLineItem(db.Model): from accounting.template_filters import format_date, format_amount setattr(self, "__str", gettext("%(date)s %(description)s %(amount)s", - date=format_date(self.voucher.date), + date=format_date(self.journal_entry.date), description="" if self.description is None else self.description, amount=format_amount(self.amount))) @@ -755,13 +758,16 @@ class JournalEntryLineItem(db.Model): frac: Decimal = (value - whole).normalize() return str(whole) + str(abs(frac))[1:] - voucher_day: date = self.voucher.date + journal_entry_day: date = self.journal_entry.date description: str = "" if self.description is None else self.description return ([description], - [str(voucher_day.year), - "{}/{}".format(voucher_day.year, voucher_day.month), - "{}/{}".format(voucher_day.month, voucher_day.day), - "{}/{}/{}".format(voucher_day.year, voucher_day.month, - voucher_day.day), + [str(journal_entry_day.year), + "{}/{}".format(journal_entry_day.year, + journal_entry_day.month), + "{}/{}".format(journal_entry_day.month, + journal_entry_day.day), + "{}/{}/{}".format(journal_entry_day.year, + journal_entry_day.month, + journal_entry_day.day), format_amount(self.amount), format_amount(self.net_balance)]) diff --git a/src/accounting/report/period/chooser.py b/src/accounting/report/period/chooser.py index 0dd684a..9bf8c24 100644 --- a/src/accounting/report/period/chooser.py +++ b/src/accounting/report/period/chooser.py @@ -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 diff --git a/src/accounting/report/reports/balance_sheet.py b/src/accounting/report/reports/balance_sheet.py index 7588c7d..d51b094 100644 --- a/src/accounting/report/reports/balance_sheet.py +++ b/src/accounting/report/reports/balance_sheet.py @@ -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, diff --git a/src/accounting/report/reports/income_expenses.py b/src/accounting/report/reports/income_expenses.py index 34e0b18..b2ead32 100644 --- a/src/accounting/report/reports/income_expenses.py +++ b/src/accounting/report/reports/income_expenses.py @@ -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.""" diff --git a/src/accounting/report/reports/income_statement.py b/src/accounting/report/reports/income_statement.py index 01edbcb..18b93dc 100644 --- a/src/accounting/report/reports/income_statement.py +++ b/src/accounting/report/reports/income_statement.py @@ -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) diff --git a/src/accounting/report/reports/journal.py b/src/accounting/report/reports/journal.py index fb6b874..a062c96 100644 --- a/src/accounting/report/reports/journal.py +++ b/src/accounting/report/reports/journal.py @@ -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. diff --git a/src/accounting/report/reports/ledger.py b/src/accounting/report/reports/ledger.py index cb3629a..65534de 100644 --- a/src/accounting/report/reports/ledger.py +++ b/src/accounting/report/reports/ledger.py @@ -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.""" diff --git a/src/accounting/report/reports/search.py b/src/accounting/report/reports/search.py index c0cf38f..7427e5c 100644 --- a/src/accounting/report/reports/search.py +++ b/src/accounting/report/reports/search.py @@ -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): diff --git a/src/accounting/report/reports/trial_balance.py b/src/accounting/report/reports/trial_balance.py index f9c32b8..1c99c0f 100644 --- a/src/accounting/report/reports/trial_balance.py +++ b/src/accounting/report/reports/trial_balance.py @@ -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) diff --git a/src/accounting/report/utils/base_page_params.py b/src/accounting/report/utils/base_page_params.py index bbb7968..e322134 100644 --- a/src/accounting/report/utils/base_page_params.py +++ b/src/accounting/report/utils/base_page_params.py @@ -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: diff --git a/src/accounting/static/css/style.css b/src/accounting/static/css/style.css index 78629ee..ccd571d 100644 --- a/src/accounting/static/css/style.css +++ b/src/accounting/static/css/style.css @@ -149,7 +149,7 @@ overflow-y: scroll; } -/** The voucher management */ +/** The journal entry management */ .accounting-currency-control { background-color: transparent; } diff --git a/src/accounting/static/js/voucher-form.js b/src/accounting/static/js/journal-entry-form.js similarity index 98% rename from src/accounting/static/js/voucher-form.js rename to src/accounting/static/js/journal-entry-form.js index 53cfaa8..4dab709 100644 --- a/src/accounting/static/js/voucher-form.js +++ b/src/accounting/static/js/journal-entry-form.js @@ -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) { diff --git a/src/accounting/static/js/journal-entry-line-item-editor.js b/src/accounting/static/js/journal-entry-line-item-editor.js index 3e13aae..365f8d1 100644 --- a/src/accounting/static/js/journal-entry-line-item-editor.js +++ b/src/accounting/static/js/journal-entry-line-item-editor.js @@ -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; diff --git a/src/accounting/static/js/voucher-order.js b/src/accounting/static/js/journal-entry-order.js similarity index 94% rename from src/accounting/static/js/voucher-order.js rename to src/accounting/static/js/journal-entry-order.js index 6c7d44c..b7a87ba 100644 --- a/src/accounting/static/js/voucher-order.js +++ b/src/accounting/static/js/journal-entry-order.js @@ -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. diff --git a/src/accounting/static/js/original-line-item-selector.js b/src/accounting/static/js/original-line-item-selector.js index 895dcc8..995f97a 100644 --- a/src/accounting/static/js/original-line-item-selector.js +++ b/src/accounting/static/js/original-line-item-selector.js @@ -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 */ diff --git a/src/accounting/templates/accounting/voucher/disbursement/create.html b/src/accounting/templates/accounting/journal-entry/disbursement/create.html similarity index 73% rename from src/accounting/templates/accounting/voucher/disbursement/create.html rename to src/accounting/templates/accounting/journal-entry/disbursement/create.html index b513844..a9b272a 100644 --- a/src/accounting/templates/accounting/voucher/disbursement/create.html +++ b/src/accounting/templates/accounting/journal-entry/disbursement/create.html @@ -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 %} diff --git a/src/accounting/templates/accounting/voucher/disbursement/detail.html b/src/accounting/templates/accounting/journal-entry/disbursement/detail.html similarity index 79% rename from src/accounting/templates/accounting/voucher/disbursement/detail.html rename to src/accounting/templates/accounting/journal-entry/disbursement/detail.html index d6a3215..cebdbca 100644 --- a/src/accounting/templates/accounting/voucher/disbursement/detail.html +++ b/src/accounting/templates/accounting/journal-entry/disbursement/detail.html @@ -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_("To Transfer") }} {% endblock %} -{% block voucher_currencies %} +{% block journal_entry_currencies %} {% for currency in obj.currencies %}
{{ currency.name }}
@@ -36,7 +36,7 @@ First written: 2023/2/26
  • {{ A_("Content") }}
  • {% with line_items = currency.debit %} - {% include "accounting/voucher/include/detail-line-items.html" %} + {% include "accounting/journal-entry/include/detail-line-items.html" %} {% endwith %}
  • diff --git a/src/accounting/templates/accounting/voucher/receipt/edit.html b/src/accounting/templates/accounting/journal-entry/disbursement/edit.html similarity index 55% rename from src/accounting/templates/accounting/voucher/receipt/edit.html rename to src/accounting/templates/accounting/journal-entry/disbursement/edit.html index d931dfb..abbdefd 100644 --- a/src/accounting/templates/accounting/voucher/receipt/edit.html +++ b/src/accounting/templates/accounting/journal-entry/disbursement/edit.html @@ -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 %} diff --git a/src/accounting/templates/accounting/voucher/disbursement/include/form-currency-item.html b/src/accounting/templates/accounting/journal-entry/disbursement/include/form-currency.html similarity index 96% rename from src/accounting/templates/accounting/voucher/disbursement/include/form-currency-item.html rename to src/accounting/templates/accounting/journal-entry/disbursement/include/form-currency.html index d7ec6e4..d2339ae 100644 --- a/src/accounting/templates/accounting/voucher/disbursement/include/form-currency-item.html +++ b/src/accounting/templates/accounting/journal-entry/disbursement/include/form-currency.html @@ -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 %}
diff --git a/src/accounting/templates/accounting/voucher/disbursement/include/form.html b/src/accounting/templates/accounting/journal-entry/disbursement/include/form.html similarity index 80% rename from src/accounting/templates/accounting/voucher/disbursement/include/form.html rename to src/accounting/templates/accounting/journal-entry/disbursement/include/form.html index eabea46..2cd2ebc 100644 --- a/src/accounting/templates/accounting/voucher/disbursement/include/form.html +++ b/src/accounting/templates/accounting/journal-entry/disbursement/include/form.html @@ -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 %} diff --git a/src/accounting/templates/accounting/voucher/include/account-selector-modal.html b/src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html similarity index 100% rename from src/accounting/templates/accounting/voucher/include/account-selector-modal.html rename to src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html diff --git a/src/accounting/templates/accounting/voucher/include/description-editor-modal.html b/src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html similarity index 100% rename from src/accounting/templates/accounting/voucher/include/description-editor-modal.html rename to src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html diff --git a/src/accounting/templates/accounting/voucher/include/detail-line-items.html b/src/accounting/templates/accounting/journal-entry/include/detail-line-items.html similarity index 84% rename from src/accounting/templates/accounting/voucher/include/detail-line-items.html rename to src/accounting/templates/accounting/journal-entry/include/detail-line-items.html index 1d6f04c..3b74da7 100644 --- a/src/accounting/templates/accounting/voucher/include/detail-line-items.html +++ b/src/accounting/templates/accounting/journal-entry/include/detail-line-items.html @@ -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 %} @@ -43,8 +43,8 @@ First written: 2023/3/14