Compare commits

..

No commits in common. "1224d6f83e12c997272326992b73d753c5c3bbed" and "6bac76be64cbbe15b8a5929adf8fd81081249c2f" have entirely different histories.

18 changed files with 775 additions and 1105 deletions

View File

@ -53,7 +53,7 @@ def list_accounts() -> str:
list=pagination.list, pagination=pagination) list=pagination.list, pagination=pagination)
@bp.get("create", endpoint="create") @bp.get("/create", endpoint="create")
@has_permission(can_edit) @has_permission(can_edit)
def show_add_account_form() -> str: def show_add_account_form() -> str:
"""Shows the form to add an account. """Shows the form to add an account.
@ -70,7 +70,7 @@ def show_add_account_form() -> str:
form=form) form=form)
@bp.post("store", endpoint="store") @bp.post("/store", endpoint="store")
@has_permission(can_edit) @has_permission(can_edit)
def add_account() -> redirect: def add_account() -> redirect:
"""Adds an account. """Adds an account.
@ -91,7 +91,7 @@ def add_account() -> redirect:
return redirect(inherit_next(__get_detail_uri(account))) return redirect(inherit_next(__get_detail_uri(account)))
@bp.get("<account:account>", endpoint="detail") @bp.get("/<account:account>", endpoint="detail")
@has_permission(can_view) @has_permission(can_view)
def show_account_detail(account: Account) -> str: def show_account_detail(account: Account) -> str:
"""Shows the account detail. """Shows the account detail.
@ -102,7 +102,7 @@ def show_account_detail(account: Account) -> str:
return render_template("accounting/account/detail.html", obj=account) return render_template("accounting/account/detail.html", obj=account)
@bp.get("<account:account>/edit", endpoint="edit") @bp.get("/<account:account>/edit", endpoint="edit")
@has_permission(can_edit) @has_permission(can_edit)
def show_account_edit_form(account: Account) -> str: def show_account_edit_form(account: Account) -> str:
"""Shows the form to edit an account. """Shows the form to edit an account.
@ -121,7 +121,7 @@ def show_account_edit_form(account: Account) -> str:
account=account, form=form) account=account, form=form)
@bp.post("<account:account>/update", endpoint="update") @bp.post("/<account:account>/update", endpoint="update")
@has_permission(can_edit) @has_permission(can_edit)
def update_account(account: Account) -> redirect: def update_account(account: Account) -> redirect:
"""Updates an account. """Updates an account.
@ -148,7 +148,7 @@ def update_account(account: Account) -> redirect:
return redirect(inherit_next(__get_detail_uri(account))) return redirect(inherit_next(__get_detail_uri(account)))
@bp.post("<account:account>/delete", endpoint="delete") @bp.post("/<account:account>/delete", endpoint="delete")
@has_permission(can_edit) @has_permission(can_edit)
def delete_account(account: Account) -> redirect: def delete_account(account: Account) -> redirect:
"""Deletes an account. """Deletes an account.
@ -167,7 +167,7 @@ def delete_account(account: Account) -> redirect:
return redirect(or_next(__get_list_uri())) return redirect(or_next(__get_list_uri()))
@bp.get("bases/<baseAccount:base>", endpoint="order") @bp.get("/bases/<baseAccount:base>", endpoint="order")
@has_permission(can_view) @has_permission(can_view)
def show_account_order(base: BaseAccount) -> str: def show_account_order(base: BaseAccount) -> str:
"""Shows the order of the accounts under a same base account. """Shows the order of the accounts under a same base account.
@ -178,7 +178,7 @@ def show_account_order(base: BaseAccount) -> str:
return render_template("accounting/account/order.html", base=base) return render_template("accounting/account/order.html", base=base)
@bp.post("bases/<baseAccount:base>", endpoint="sort") @bp.post("/bases/<baseAccount:base>", endpoint="sort")
@has_permission(can_edit) @has_permission(can_edit)
def sort_accounts(base: BaseAccount) -> redirect: def sort_accounts(base: BaseAccount) -> redirect:
"""Reorders the accounts under a base account. """Reorders the accounts under a base account.

View File

@ -41,7 +41,7 @@ def list_accounts() -> str:
list=pagination.list, pagination=pagination) list=pagination.list, pagination=pagination)
@bp.get("<baseAccount:account>", endpoint="detail") @bp.get("/<baseAccount:account>", endpoint="detail")
@has_permission(can_view) @has_permission(can_view)
def show_account_detail(account: BaseAccount) -> str: def show_account_detail(account: BaseAccount) -> str:
"""Shows the account detail. """Shows the account detail.

View File

@ -55,7 +55,7 @@ def list_currencies() -> str:
list=pagination.list, pagination=pagination) list=pagination.list, pagination=pagination)
@bp.get("create", endpoint="create") @bp.get("/create", endpoint="create")
@has_permission(can_edit) @has_permission(can_edit)
def show_add_currency_form() -> str: def show_add_currency_form() -> str:
"""Shows the form to add a currency. """Shows the form to add a currency.
@ -72,7 +72,7 @@ def show_add_currency_form() -> str:
form=form) form=form)
@bp.post("store", endpoint="store") @bp.post("/store", endpoint="store")
@has_permission(can_edit) @has_permission(can_edit)
def add_currency() -> redirect: def add_currency() -> redirect:
"""Adds a currency. """Adds a currency.
@ -93,7 +93,7 @@ def add_currency() -> redirect:
return redirect(inherit_next(__get_detail_uri(currency))) return redirect(inherit_next(__get_detail_uri(currency)))
@bp.get("<currency:currency>", endpoint="detail") @bp.get("/<currency:currency>", endpoint="detail")
@has_permission(can_view) @has_permission(can_view)
def show_currency_detail(currency: Currency) -> str: def show_currency_detail(currency: Currency) -> str:
"""Shows the currency detail. """Shows the currency detail.
@ -104,7 +104,7 @@ def show_currency_detail(currency: Currency) -> str:
return render_template("accounting/currency/detail.html", obj=currency) return render_template("accounting/currency/detail.html", obj=currency)
@bp.get("<currency:currency>/edit", endpoint="edit") @bp.get("/<currency:currency>/edit", endpoint="edit")
@has_permission(can_edit) @has_permission(can_edit)
def show_currency_edit_form(currency: Currency) -> str: def show_currency_edit_form(currency: Currency) -> str:
"""Shows the form to edit a currency. """Shows the form to edit a currency.
@ -123,7 +123,7 @@ def show_currency_edit_form(currency: Currency) -> str:
currency=currency, form=form) currency=currency, form=form)
@bp.post("<currency:currency>/update", endpoint="update") @bp.post("/<currency:currency>/update", endpoint="update")
@has_permission(can_edit) @has_permission(can_edit)
def update_currency(currency: Currency) -> redirect: def update_currency(currency: Currency) -> redirect:
"""Updates a currency. """Updates a currency.
@ -151,7 +151,7 @@ def update_currency(currency: Currency) -> redirect:
return redirect(inherit_next(__get_detail_uri(currency))) return redirect(inherit_next(__get_detail_uri(currency)))
@bp.post("<currency:currency>/delete", endpoint="delete") @bp.post("/<currency:currency>/delete", endpoint="delete")
@has_permission(can_edit) @has_permission(can_edit)
def delete_currency(currency: Currency) -> redirect: def delete_currency(currency: Currency) -> redirect:
"""Deletes a currency. """Deletes a currency.
@ -169,7 +169,7 @@ def delete_currency(currency: Currency) -> redirect:
return redirect(or_next(url_for("accounting.currency.list"))) return redirect(or_next(url_for("accounting.currency.list")))
@api_bp.get("exists-code", endpoint="exists") @api_bp.get("/exists-code", endpoint="exists")
@has_permission(can_edit) @has_permission(can_edit)
def exists_code() -> dict[str, bool]: def exists_code() -> dict[str, bool]:
"""Validates whether a currency code exists. """Validates whether a currency code exists.

View File

@ -49,7 +49,7 @@ bp.add_app_template_filter(format_amount_input,
bp.add_app_template_filter(text2html, "accounting_journal_entry_text2html") bp.add_app_template_filter(text2html, "accounting_journal_entry_text2html")
@bp.get("create/<journalEntryType:journal_entry_type>", endpoint="create") @bp.get("/create/<journalEntryType:journal_entry_type>", endpoint="create")
@has_permission(can_edit) @has_permission(can_edit)
def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str: def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
"""Shows the form to add a journal entry. """Shows the form to add a journal entry.
@ -71,7 +71,7 @@ def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
return journal_entry_op.render_create_template(form) return journal_entry_op.render_create_template(form)
@bp.post("store/<journalEntryType:journal_entry_type>", endpoint="store") @bp.post("/store/<journalEntryType:journal_entry_type>", endpoint="store")
@has_permission(can_edit) @has_permission(can_edit)
def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect: def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
"""Adds a journal entry. """Adds a journal entry.
@ -98,7 +98,7 @@ def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
return redirect(inherit_next(__get_detail_uri(journal_entry))) return redirect(inherit_next(__get_detail_uri(journal_entry)))
@bp.get("<journalEntry:journal_entry>", endpoint="detail") @bp.get("/<journalEntry:journal_entry>", endpoint="detail")
@has_permission(can_view) @has_permission(can_view)
def show_journal_entry_detail(journal_entry: JournalEntry) -> str: def show_journal_entry_detail(journal_entry: JournalEntry) -> str:
"""Shows the journal entry detail. """Shows the journal entry detail.
@ -111,7 +111,7 @@ def show_journal_entry_detail(journal_entry: JournalEntry) -> str:
return journal_entry_op.render_detail_template(journal_entry) return journal_entry_op.render_detail_template(journal_entry)
@bp.get("<journalEntry:journal_entry>/edit", endpoint="edit") @bp.get("/<journalEntry:journal_entry>/edit", endpoint="edit")
@has_permission(can_edit) @has_permission(can_edit)
def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str: def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
"""Shows the form to edit a journal entry. """Shows the form to edit a journal entry.
@ -133,7 +133,7 @@ def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
return journal_entry_op.render_edit_template(journal_entry, form) return journal_entry_op.render_edit_template(journal_entry, form)
@bp.post("<journalEntry:journal_entry>/update", endpoint="update") @bp.post("/<journalEntry:journal_entry>/update", endpoint="update")
@has_permission(can_edit) @has_permission(can_edit)
def update_journal_entry(journal_entry: JournalEntry) -> redirect: def update_journal_entry(journal_entry: JournalEntry) -> redirect:
"""Updates a journal entry. """Updates a journal entry.
@ -166,7 +166,7 @@ def update_journal_entry(journal_entry: JournalEntry) -> redirect:
return redirect(inherit_next(__get_detail_uri(journal_entry))) return redirect(inherit_next(__get_detail_uri(journal_entry)))
@bp.post("<journalEntry:journal_entry>/delete", endpoint="delete") @bp.post("/<journalEntry:journal_entry>/delete", endpoint="delete")
@has_permission(can_edit) @has_permission(can_edit)
def delete_journal_entry(journal_entry: JournalEntry) -> redirect: def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
"""Deletes a journal entry. """Deletes a journal entry.
@ -186,7 +186,7 @@ def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
return redirect(or_next(__get_default_page_uri())) return redirect(or_next(__get_default_page_uri()))
@bp.get("dates/<date:journal_entry_date>", endpoint="order") @bp.get("/dates/<date:journal_entry_date>", endpoint="order")
@has_permission(can_view) @has_permission(can_view)
def show_journal_entry_order(journal_entry_date: date) -> str: def show_journal_entry_order(journal_entry_date: date) -> str:
"""Shows the order of the journal entries in a same date. """Shows the order of the journal entries in a same date.
@ -201,7 +201,7 @@ def show_journal_entry_order(journal_entry_date: date) -> str:
date=journal_entry_date, list=journal_entries) date=journal_entry_date, list=journal_entries)
@bp.post("dates/<date:journal_entry_date>", endpoint="sort") @bp.post("/dates/<date:journal_entry_date>", endpoint="sort")
@has_permission(can_edit) @has_permission(can_edit)
def sort_journal_entries(journal_entry_date: date) -> redirect: def sort_journal_entries(journal_entry_date: date) -> redirect:
"""Reorders the journal entries in a date. """Reorders the journal entries in a date.

View File

@ -22,7 +22,6 @@ from abc import ABC, abstractmethod
from datetime import timedelta, date from datetime import timedelta, date
from decimal import Decimal from decimal import Decimal
from io import StringIO from io import StringIO
from urllib.parse import quote
from flask import Response from flask import Response
@ -54,7 +53,7 @@ def csv_download(filename: str, rows: list[BaseCSVRow]) -> Response:
fp.seek(0) fp.seek(0)
response: Response = Response(fp.read(), mimetype="text/csv") response: Response = Response(fp.read(), mimetype="text/csv")
response.headers["Content-Disposition"] \ response.headers["Content-Disposition"] \
= f"attachment; filename={quote(filename)}" = f"attachment; filename={filename}"
return response return response

View File

@ -47,10 +47,9 @@ def ledger_url(currency: Currency, account: Account, period: Period) \
:param period: The period. :param period: The period.
:return: The URL of the ledger. :return: The URL of the ledger.
""" """
if currency.code == default_currency_code() \ if period.is_default:
and account.code == Account.CASH_CODE \ return url_for("accounting-report.ledger-default",
and period.is_default: currency=currency, account=account)
return url_for("accounting-report.ledger-default")
return url_for("accounting-report.ledger", return url_for("accounting-report.ledger",
currency=currency, account=account, currency=currency, account=account,
period=period) period=period)
@ -69,6 +68,9 @@ def income_expenses_url(currency: Currency, account: CurrentAccount,
and account.code == options.default_ie_account_code \ and account.code == options.default_ie_account_code \
and period.is_default: and period.is_default:
return url_for("accounting-report.default") return url_for("accounting-report.default")
if period.is_default:
return url_for("accounting-report.income-expenses-default",
currency=currency, account=account)
return url_for("accounting-report.income-expenses", return url_for("accounting-report.income-expenses",
currency=currency, account=account, currency=currency, account=account,
period=period) period=period)
@ -81,8 +83,9 @@ def trial_balance_url(currency: Currency, period: Period) -> str:
:param period: The period. :param period: The period.
:return: The URL of the trial balance. :return: The URL of the trial balance.
""" """
if currency.code == default_currency_code() and period.is_default: if period.is_default:
return url_for("accounting-report.trial-balance-default") return url_for("accounting-report.trial-balance-default",
currency=currency)
return url_for("accounting-report.trial-balance", return url_for("accounting-report.trial-balance",
currency=currency, period=period) currency=currency, period=period)
@ -94,8 +97,9 @@ def income_statement_url(currency: Currency, period: Period) -> str:
:param period: The period. :param period: The period.
:return: The URL of the income statement. :return: The URL of the income statement.
""" """
if currency.code == default_currency_code() and period.is_default: if period.is_default:
return url_for("accounting-report.income-statement-default") return url_for("accounting-report.income-statement-default",
currency=currency)
return url_for("accounting-report.income-statement", return url_for("accounting-report.income-statement",
currency=currency, period=period) currency=currency, period=period)
@ -107,8 +111,9 @@ def balance_sheet_url(currency: Currency, period: Period) -> str:
:param period: The period. :param period: The period.
:return: The URL of the balance sheet. :return: The URL of the balance sheet.
""" """
if currency.code == default_currency_code() and period.is_default: if period.is_default:
return url_for("accounting-report.balance-sheet-default") return url_for("accounting-report.balance-sheet-default",
currency=currency)
return url_for("accounting-report.balance-sheet", return url_for("accounting-report.balance-sheet",
currency=currency, period=period) currency=currency, period=period)

View File

@ -44,7 +44,10 @@ def get_default_report() -> str | Response:
:return: The income and expenses log in the default period. :return: The income and expenses log in the default period.
""" """
return get_default_income_expenses() return __get_income_expenses(
db.session.get(Currency, default_currency_code()),
options.default_ie_account,
get_period())
@bp.get("journal", endpoint="journal-default") @bp.get("journal", endpoint="journal-default")
@ -80,15 +83,17 @@ def __get_journal(period: Period) -> str | Response:
return report.html() return report.html()
@bp.get("ledger", endpoint="ledger-default") @bp.get("ledger/<currency:currency>/<account:account>",
endpoint="ledger-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_ledger() -> str | Response: def get_default_ledger(currency: Currency, account: Account) -> str | Response:
"""Returns the ledger in the default currency, cash, and default period. """Returns the ledger in the default period.
:return: The ledger in the default currency, cash, and default period. :param currency: The currency.
:param account: The account.
:return: The ledger in the default period.
""" """
return __get_ledger(db.session.get(Currency, default_currency_code()), return __get_ledger(currency, account, get_period())
Account.cash(), get_period())
@bp.get("ledger/<currency:currency>/<account:account>/<period:period>", @bp.get("ledger/<currency:currency>/<account:account>/<period:period>",
@ -121,17 +126,18 @@ def __get_ledger(currency: Currency, account: Account, period: Period) \
return report.html() return report.html()
@bp.get("income-expenses", endpoint="income-expenses-default") @bp.get("income-expenses/<currency:currency>/<currentAccount:account>",
endpoint="income-expenses-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_income_expenses() -> str | Response: def get_default_income_expenses(currency: Currency, account: CurrentAccount) \
-> str | Response:
"""Returns the income and expenses log in the default period. """Returns the income and expenses log in the default period.
:param currency: The currency.
:param account: The account.
:return: The income and expenses log in the default period. :return: The income and expenses log in the default period.
""" """
return __get_income_expenses( return __get_income_expenses(currency, account, get_period())
db.session.get(Currency, default_currency_code()),
options.default_ie_account,
get_period())
@bp.get("income-expenses/<currency:currency>/<currentAccount:account>/" @bp.get("income-expenses/<currency:currency>/<currentAccount:account>/"
@ -164,15 +170,16 @@ def __get_income_expenses(currency: Currency, account: CurrentAccount,
return report.html() return report.html()
@bp.get("trial-balance", endpoint="trial-balance-default") @bp.get("trial-balance/<currency:currency>",
endpoint="trial-balance-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_trial_balance() -> str | Response: def get_default_trial_balance(currency: Currency) -> str | Response:
"""Returns the trial balance in the default period. """Returns the trial balance in the default period.
:param currency: The currency.
:return: The trial balance in the default period. :return: The trial balance in the default period.
""" """
return __get_trial_balance( return __get_trial_balance(currency, get_period())
db.session.get(Currency, default_currency_code()), get_period())
@bp.get("trial-balance/<currency:currency>/<period:period>", @bp.get("trial-balance/<currency:currency>/<period:period>",
@ -201,15 +208,16 @@ def __get_trial_balance(currency: Currency, period: Period) -> str | Response:
return report.html() return report.html()
@bp.get("income-statement", endpoint="income-statement-default") @bp.get("income-statement/<currency:currency>",
endpoint="income-statement-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_income_statement() -> str | Response: def get_default_income_statement(currency: Currency) -> str | Response:
"""Returns the income statement in the default period. """Returns the income statement in the default period.
:param currency: The currency.
:return: The income statement in the default period. :return: The income statement in the default period.
""" """
return __get_income_statement( return __get_income_statement(currency, get_period())
db.session.get(Currency, default_currency_code()), get_period())
@bp.get("income-statement/<currency:currency>/<period:period>", @bp.get("income-statement/<currency:currency>/<period:period>",
@ -239,15 +247,16 @@ def __get_income_statement(currency: Currency, period: Period) \
return report.html() return report.html()
@bp.get("balance-sheet", endpoint="balance-sheet-default") @bp.get("balance-sheet/<currency:currency>",
endpoint="balance-sheet-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_balance_sheet() -> str | Response: def get_default_balance_sheet(currency: Currency) -> str | Response:
"""Returns the balance sheet in the default period. """Returns the balance sheet in the default period.
:param currency: The currency.
:return: The balance sheet in the default period. :return: The balance sheet in the default period.
""" """
return __get_balance_sheet( return __get_balance_sheet(currency, get_period())
db.session.get(Currency, default_currency_code()), get_period())
@bp.get("balance-sheet/<currency:currency>/<period:period>", @bp.get("balance-sheet/<currency:currency>/<period:period>",

View File

@ -27,8 +27,8 @@ from flask import Flask
from flask.testing import FlaskCliRunner from flask.testing import FlaskCliRunner
from test_site import db from test_site import db
from testlib import NEXT_URI, create_test_app, get_client, set_locale, \ from testlib import NEXT_URI, create_test_app, get_client, set_locale
add_journal_entry from testlib_journal_entry import add_journal_entry
class AccountData: class AccountData:

View File

@ -28,8 +28,8 @@ from flask import Flask
from flask.testing import FlaskCliRunner from flask.testing import FlaskCliRunner
from test_site import db from test_site import db
from testlib import NEXT_URI, create_test_app, get_client, set_locale, \ from testlib import NEXT_URI, create_test_app, get_client, set_locale
add_journal_entry from testlib_journal_entry import add_journal_entry
class CurrencyData: class CurrencyData:

View File

@ -24,8 +24,8 @@ from click.testing import Result
from flask import Flask from flask import Flask
from flask.testing import FlaskCliRunner from flask.testing import FlaskCliRunner
from testlib import NEXT_URI, Accounts, create_test_app, get_client, \ from testlib import NEXT_URI, Accounts, create_test_app, get_client
add_journal_entry from testlib_journal_entry import add_journal_entry
class DescriptionEditorTestCase(unittest.TestCase): class DescriptionEditorTestCase(unittest.TestCase):

View File

@ -27,12 +27,11 @@ from flask import Flask
from flask.testing import FlaskCliRunner from flask.testing import FlaskCliRunner
from test_site import db from test_site import db
from testlib import NEXT_URI, Accounts, create_test_app, get_client, \ from testlib import NEXT_URI, Accounts, create_test_app, get_client
add_journal_entry, match_journal_entry_detail
from testlib_journal_entry import NON_EMPTY_NOTE, EMPTY_NOTE, \ from testlib_journal_entry import NON_EMPTY_NOTE, EMPTY_NOTE, \
get_add_form, get_unchanged_update_form, get_update_form, \ get_add_form, get_unchanged_update_form, get_update_form, \
set_negative_amount, remove_debit_in_a_currency, \ match_journal_entry_detail, set_negative_amount, \
remove_credit_in_a_currency remove_debit_in_a_currency, remove_credit_in_a_currency, add_journal_entry
PREFIX: str = "/accounting/journal-entries" PREFIX: str = "/accounting/journal-entries"
"""The URL prefix for the journal entry management.""" """The URL prefix for the journal entry management."""

View File

@ -17,8 +17,6 @@
"""The test for the offset. """The test for the offset.
""" """
from __future__ import annotations
import unittest import unittest
from decimal import Decimal from decimal import Decimal
@ -28,9 +26,10 @@ from flask import Flask
from flask.testing import FlaskCliRunner from flask.testing import FlaskCliRunner
from test_site import db from test_site import db
from testlib import Accounts, create_test_app, get_client, \ from testlib import Accounts, create_test_app, get_client
match_journal_entry_detail, JournalEntryLineItemData, \ from testlib_journal_entry import match_journal_entry_detail
JournalEntryCurrencyData, JournalEntryData, BaseTestData from testlib_offset import TestData, JournalEntryLineItemData, \
JournalEntryData, CurrencyData
PREFIX: str = "/accounting/journal-entries" PREFIX: str = "/accounting/journal-entries"
"""The URL prefix for the journal entry management.""" """The URL prefix for the journal entry management."""
@ -67,8 +66,7 @@ class OffsetTestCase(unittest.TestCase):
JournalEntryLineItem.query.delete() JournalEntryLineItem.query.delete()
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")
self.data: OffsetTestData = OffsetTestData( self.data: TestData = TestData(self.app, self.client, self.csrf_token)
self.app, self.client, self.csrf_token)
def test_add_receivable_offset(self) -> None: def test_add_receivable_offset(self) -> None:
"""Tests to add the receivable offset. """Tests to add the receivable offset.
@ -83,7 +81,7 @@ class OffsetTestCase(unittest.TestCase):
response: httpx.Response response: httpx.Response
journal_entry_data: JournalEntryData = JournalEntryData( journal_entry_data: JournalEntryData = JournalEntryData(
self.data.l_r_or3d.journal_entry.days, [JournalEntryCurrencyData( self.data.l_r_or3d.journal_entry.days, [CurrencyData(
"USD", "USD",
[], [],
[JournalEntryLineItemData( [JournalEntryLineItemData(
@ -109,7 +107,7 @@ class OffsetTestCase(unittest.TestCase):
# The same debit or credit # The same debit or credit
form = journal_entry_data.new_form(self.csrf_token) form = journal_entry_data.new_form(self.csrf_token)
form["currency-1-credit-1-original_line_item_id"] \ form["currency-1-credit-1-original_line_item_id"] \
= str(self.data.l_p_or1c.id) = self.data.l_p_or1c.id
form["currency-1-credit-1-account_code"] = self.data.l_p_or1c.account form["currency-1-credit-1-account_code"] = self.data.l_p_or1c.account
form["currency-1-credit-1-amount"] = "100" form["currency-1-credit-1-amount"] = "100"
response = self.client.post(store_uri, data=form) response = self.client.post(store_uri, data=form)
@ -133,7 +131,7 @@ class OffsetTestCase(unittest.TestCase):
# The original line item is also an offset # The original line item is also an offset
form = journal_entry_data.new_form(self.csrf_token) form = journal_entry_data.new_form(self.csrf_token)
form["currency-1-credit-1-original_line_item_id"] \ form["currency-1-credit-1-original_line_item_id"] \
= str(self.data.l_p_of1d.id) = self.data.l_p_of1d.id
form["currency-1-credit-1-account_code"] = self.data.l_p_of1d.account form["currency-1-credit-1-account_code"] = self.data.l_p_of1d.account
response = self.client.post(store_uri, data=form) response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
@ -219,7 +217,7 @@ class OffsetTestCase(unittest.TestCase):
# The same debit or credit # The same debit or credit
form = journal_entry_data.update_form(self.csrf_token) form = journal_entry_data.update_form(self.csrf_token)
form["currency-1-credit-1-original_line_item_id"] \ form["currency-1-credit-1-original_line_item_id"] \
= str(self.data.l_p_or1c.id) = self.data.l_p_or1c.id
form["currency-1-credit-1-account_code"] = self.data.l_p_or1c.account form["currency-1-credit-1-account_code"] = self.data.l_p_or1c.account
form["currency-1-debit-1-amount"] = "100" form["currency-1-debit-1-amount"] = "100"
form["currency-1-credit-1-amount"] = "100" form["currency-1-credit-1-amount"] = "100"
@ -244,7 +242,7 @@ class OffsetTestCase(unittest.TestCase):
# The original line item is also an offset # The original line item is also an offset
form = journal_entry_data.update_form(self.csrf_token) form = journal_entry_data.update_form(self.csrf_token)
form["currency-1-credit-1-original_line_item_id"] \ form["currency-1-credit-1-original_line_item_id"] \
= str(self.data.l_p_of1d.id) = self.data.l_p_of1d.id
form["currency-1-credit-1-account_code"] = self.data.l_p_of1d.account form["currency-1-credit-1-account_code"] = self.data.l_p_of1d.account
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
@ -407,7 +405,7 @@ class OffsetTestCase(unittest.TestCase):
response: httpx.Response response: httpx.Response
journal_entry_data: JournalEntryData = JournalEntryData( journal_entry_data: JournalEntryData = JournalEntryData(
self.data.l_p_or3c.journal_entry.days, [JournalEntryCurrencyData( self.data.l_p_or3c.journal_entry.days, [CurrencyData(
"USD", "USD",
[JournalEntryLineItemData( [JournalEntryLineItemData(
Accounts.PAYABLE, Accounts.PAYABLE,
@ -433,7 +431,7 @@ class OffsetTestCase(unittest.TestCase):
# The same debit or credit # The same debit or credit
form = journal_entry_data.new_form(self.csrf_token) form = journal_entry_data.new_form(self.csrf_token)
form["currency-1-debit-1-original_line_item_id"] \ form["currency-1-debit-1-original_line_item_id"] \
= str(self.data.l_r_or1d.id) = self.data.l_r_or1d.id
form["currency-1-debit-1-account_code"] = self.data.l_r_or1d.account form["currency-1-debit-1-account_code"] = self.data.l_r_or1d.account
form["currency-1-debit-1-amount"] = "100" form["currency-1-debit-1-amount"] = "100"
response = self.client.post(store_uri, data=form) response = self.client.post(store_uri, data=form)
@ -457,7 +455,7 @@ class OffsetTestCase(unittest.TestCase):
# The original line item is also an offset # The original line item is also an offset
form = journal_entry_data.new_form(self.csrf_token) form = journal_entry_data.new_form(self.csrf_token)
form["currency-1-debit-1-original_line_item_id"] \ form["currency-1-debit-1-original_line_item_id"] \
= str(self.data.l_r_of1c.id) = self.data.l_r_of1c.id
form["currency-1-debit-1-account_code"] = self.data.l_r_of1c.account form["currency-1-debit-1-account_code"] = self.data.l_r_of1c.account
response = self.client.post(store_uri, data=form) response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
@ -543,7 +541,7 @@ class OffsetTestCase(unittest.TestCase):
# The same debit or credit # The same debit or credit
form = journal_entry_data.update_form(self.csrf_token) form = journal_entry_data.update_form(self.csrf_token)
form["currency-1-debit-1-original_line_item_id"] \ form["currency-1-debit-1-original_line_item_id"] \
= str(self.data.l_r_or1d.id) = self.data.l_r_or1d.id
form["currency-1-debit-1-account_code"] = self.data.l_r_or1d.account form["currency-1-debit-1-account_code"] = self.data.l_r_or1d.account
form["currency-1-debit-1-amount"] = "100" form["currency-1-debit-1-amount"] = "100"
form["currency-1-credit-1-amount"] = "100" form["currency-1-credit-1-amount"] = "100"
@ -568,7 +566,7 @@ class OffsetTestCase(unittest.TestCase):
# The original line item is also an offset # The original line item is also an offset
form = journal_entry_data.update_form(self.csrf_token) form = journal_entry_data.update_form(self.csrf_token)
form["currency-1-debit-1-original_line_item_id"] \ form["currency-1-debit-1-original_line_item_id"] \
= str(self.data.l_r_of1c.id) = self.data.l_r_of1c.id
form["currency-1-debit-1-account_code"] = self.data.l_r_of1c.account form["currency-1-debit-1-account_code"] = self.data.l_r_of1c.account
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
@ -722,114 +720,3 @@ class OffsetTestCase(unittest.TestCase):
self.assertIsNotNone(journal_entry_of) self.assertIsNotNone(journal_entry_of)
self.assertEqual(journal_entry_or.date, journal_entry_of.date) self.assertEqual(journal_entry_or.date, journal_entry_of.date)
self.assertLess(journal_entry_or.no, journal_entry_of.no) self.assertLess(journal_entry_or.no, journal_entry_of.no)
class OffsetTestData(BaseTestData):
"""The offset test data."""
def _init_data(self) -> None:
# Receivable original line items
self.l_r_or1d, self.l_r_or1c = self._couple(
"Accountant", "1200", Accounts.RECEIVABLE, Accounts.SERVICE)
self.l_r_or2d, self.l_r_or2c = self._couple(
"Toy", "600", Accounts.RECEIVABLE, Accounts.SALES)
self.l_r_or3d, self.l_r_or3c = self._couple(
"Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES)
self.l_r_or4d, self.l_r_or4c = self._couple(
"Interest", "3.4", Accounts.RECEIVABLE, Accounts.INTEREST)
# Payable original line items
self.l_p_or1d, self.l_p_or1c = self._couple(
"Airplane", "2000", Accounts.TRAVEL, Accounts.PAYABLE)
self.l_p_or2d, self.l_p_or2c = self._couple(
"Phone", "900", Accounts.OFFICE, Accounts.PAYABLE)
self.l_p_or3d, self.l_p_or3c = self._couple(
"Steak", "120", Accounts.MEAL, Accounts.PAYABLE)
self.l_p_or4d, self.l_p_or4c = self._couple(
"Envelop", "0.9", Accounts.OFFICE, Accounts.PAYABLE)
# Original journal entries
self.j_r_or1: JournalEntryData = JournalEntryData(
50, [JournalEntryCurrencyData(
"USD", [self.l_r_or1d, self.l_r_or4d],
[self.l_r_or1c, self.l_r_or4c])])
self.j_r_or2: JournalEntryData = JournalEntryData(
30, [JournalEntryCurrencyData(
"USD", [self.l_r_or2d, self.l_r_or3d],
[self.l_r_or2c, self.l_r_or3c])])
self.j_p_or1: JournalEntryData = JournalEntryData(
40, [JournalEntryCurrencyData(
"USD", [self.l_p_or1d, self.l_p_or4d],
[self.l_p_or1c, self.l_p_or4c])])
self.j_p_or2: JournalEntryData = JournalEntryData(
20, [JournalEntryCurrencyData(
"USD", [self.l_p_or2d, self.l_p_or3d],
[self.l_p_or2c, self.l_p_or3c])])
self._add_journal_entry(self.j_r_or1)
self._add_journal_entry(self.j_r_or2)
self._add_journal_entry(self.j_p_or1)
self._add_journal_entry(self.j_p_or2)
# Receivable offset items
self.l_r_of1d, self.l_r_of1c = self._couple(
"Accountant", "500", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of1c.original_line_item = self.l_r_or1d
self.l_r_of2d, self.l_r_of2c = self._couple(
"Accountant", "200", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of2c.original_line_item = self.l_r_or1d
self.l_r_of3d, self.l_r_of3c = self._couple(
"Accountant", "100", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of3c.original_line_item = self.l_r_or1d
self.l_r_of4d, self.l_r_of4c = self._couple(
"Toy", "240", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of4c.original_line_item = self.l_r_or2d
self.l_r_of5d, self.l_r_of5c = self._couple(
"Interest", "3.4", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of5c.original_line_item = self.l_r_or4d
# Payable offset items
self.l_p_of1d, self.l_p_of1c = self._couple(
"Airplane", "800", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of1d.original_line_item = self.l_p_or1c
self.l_p_of2d, self.l_p_of2c = self._couple(
"Airplane", "300", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of2d.original_line_item = self.l_p_or1c
self.l_p_of3d, self.l_p_of3c = self._couple(
"Airplane", "100", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of3d.original_line_item = self.l_p_or1c
self.l_p_of4d, self.l_p_of4c = self._couple(
"Phone", "400", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of4d.original_line_item = self.l_p_or2c
self.l_p_of5d, self.l_p_of5c = self._couple(
"Envelop", "0.9", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of5d.original_line_item = self.l_p_or4c
# Offset journal entries
self.j_r_of1: JournalEntryData = JournalEntryData(
25, [JournalEntryCurrencyData(
"USD", [self.l_r_of1d], [self.l_r_of1c])])
self.j_r_of2: JournalEntryData = JournalEntryData(
20, [JournalEntryCurrencyData(
"USD", [self.l_r_of2d, self.l_r_of3d, self.l_r_of4d],
[self.l_r_of2c, self.l_r_of3c, self.l_r_of4c])])
self.j_r_of3: JournalEntryData = JournalEntryData(
15, [JournalEntryCurrencyData(
"USD", [self.l_r_of5d], [self.l_r_of5c])])
self.j_p_of1: JournalEntryData = JournalEntryData(
15, [JournalEntryCurrencyData(
"USD", [self.l_p_of1d], [self.l_p_of1c])])
self.j_p_of2: JournalEntryData = JournalEntryData(
10, [JournalEntryCurrencyData(
"USD", [self.l_p_of2d, self.l_p_of3d, self.l_p_of4d],
[self.l_p_of2c, self.l_p_of3c, self.l_p_of4c])])
self.j_p_of3: JournalEntryData = JournalEntryData(
5, [JournalEntryCurrencyData(
"USD", [self.l_p_of5d], [self.l_p_of5c])])
self._add_journal_entry(self.j_r_of1)
self._add_journal_entry(self.j_r_of2)
self._add_journal_entry(self.j_r_of3)
self._add_journal_entry(self.j_p_of1)
self._add_journal_entry(self.j_p_of2)
self._add_journal_entry(self.j_p_of3)

View File

@ -1,5 +1,5 @@
# The Mia! Accounting Project. # The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/8 # Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/11
# Copyright (c) 2023 imacat. # Copyright (c) 2023 imacat.
# #
@ -14,7 +14,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""The test for the unmatched offsets. """The test for the offset matcher.
""" """
import unittest import unittest
@ -25,15 +25,14 @@ from flask import Flask
from flask.testing import FlaskCliRunner from flask.testing import FlaskCliRunner
from test_site import db from test_site import db
from testlib import create_test_app, get_client, Accounts, \ from testlib import create_test_app, get_client, Accounts
JournalEntryCurrencyData, JournalEntryData, BaseTestData from testlib_journal_entry import match_journal_entry_detail
from testlib_offset import JournalEntryData, CurrencyData, \
PREFIX: str = "/accounting/unmatched-offsets" JournalEntryLineItemData
"""The URL prefix for the unmatched offset management."""
class UnmatchedOffsetTestCase(unittest.TestCase): class OffsetMatcherTestCase(unittest.TestCase):
"""The unmatched offset test case.""" """The offset matcher test case."""
def setUp(self) -> None: def setUp(self) -> None:
"""Sets up the test. """Sets up the test.
@ -64,83 +63,6 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")
def test_nobody(self) -> None:
"""Test the permission as nobody.
:return: None.
"""
client, csrf_token = get_client(self.app, "nobody")
DifferentTestData(self.app, self.client, self.csrf_token)
response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 403)
response = client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": csrf_token})
self.assertEqual(response.status_code, 403)
def test_viewer(self) -> None:
"""Test the permission as viewer.
:return: None.
"""
client, csrf_token = get_client(self.app, "viewer")
DifferentTestData(self.app, self.client, self.csrf_token)
response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 403)
response = client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": csrf_token})
self.assertEqual(response.status_code, 403)
def test_editor(self) -> None:
"""Test the permission as editor.
:return: None.
"""
DifferentTestData(self.app, self.client, self.csrf_token)
response: httpx.Response
response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200)
response = self.client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"],
f"{PREFIX}/{Accounts.PAYABLE}")
def test_empty_db(self) -> None:
"""Test the empty database.
:return: None.
"""
response: httpx.Response
response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200)
response = self.client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"],
f"{PREFIX}/{Accounts.PAYABLE}")
def test_different(self) -> None: def test_different(self) -> None:
"""Tests to match against different descriptions and amounts. """Tests to match against different descriptions and amounts.
@ -154,7 +76,6 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
line_item: JournalEntryLineItem | None line_item: JournalEntryLineItem | None
matcher: OffsetMatcher matcher: OffsetMatcher
list_uri: str list_uri: str
match_uri: str
response: httpx.Response response: httpx.Response
# The receivables # The receivables
@ -179,9 +100,8 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertIsNotNone(line_item) self.assertIsNotNone(line_item)
self.assertIsNone(line_item.original_line_item_id) self.assertIsNone(line_item.original_line_item_id)
list_uri = f"{PREFIX}/{Accounts.RECEIVABLE}" list_uri = f"/accounting/unmatched-offsets/{Accounts.RECEIVABLE}"
match_uri = f"{PREFIX}/{Accounts.RECEIVABLE}" response = self.client.post(list_uri,
response = self.client.post(match_uri,
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], list_uri) self.assertEqual(response.headers["Location"], list_uri)
@ -229,9 +149,8 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertIsNotNone(line_item) self.assertIsNotNone(line_item)
self.assertIsNone(line_item.original_line_item_id) self.assertIsNone(line_item.original_line_item_id)
list_uri = f"{PREFIX}/{Accounts.PAYABLE}" list_uri = f"/accounting/unmatched-offsets/{Accounts.PAYABLE}"
match_uri = f"{PREFIX}/{Accounts.PAYABLE}" response = self.client.post(list_uri,
response = self.client.post(match_uri,
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], list_uri) self.assertEqual(response.headers["Location"], list_uri)
@ -270,7 +189,6 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
line_item: JournalEntryLineItem | None line_item: JournalEntryLineItem | None
matcher: OffsetMatcher matcher: OffsetMatcher
list_uri: str list_uri: str
match_uri: str
response: httpx.Response response: httpx.Response
# The receivables # The receivables
@ -302,9 +220,8 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertIsNotNone(line_item.original_line_item_id) self.assertIsNotNone(line_item.original_line_item_id)
self.assertEqual(line_item.original_line_item_id, data.l_r_or2d.id) self.assertEqual(line_item.original_line_item_id, data.l_r_or2d.id)
list_uri = f"{PREFIX}/{Accounts.RECEIVABLE}" list_uri = f"/accounting/unmatched-offsets/{Accounts.RECEIVABLE}"
match_uri = f"{PREFIX}/{Accounts.RECEIVABLE}" response = self.client.post(list_uri,
response = self.client.post(match_uri,
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], list_uri) self.assertEqual(response.headers["Location"], list_uri)
@ -368,9 +285,8 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertIsNotNone(line_item.original_line_item_id) self.assertIsNotNone(line_item.original_line_item_id)
self.assertEqual(line_item.original_line_item_id, data.l_p_or2c.id) self.assertEqual(line_item.original_line_item_id, data.l_p_or2c.id)
list_uri = f"{PREFIX}/{Accounts.PAYABLE}" list_uri = f"/accounting/unmatched-offsets/{Accounts.PAYABLE}"
match_uri = f"{PREFIX}/{Accounts.PAYABLE}" response = self.client.post(list_uri,
response = self.client.post(match_uri,
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], list_uri) self.assertEqual(response.headers["Location"], list_uri)
@ -406,179 +322,371 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertEqual(line_item.original_line_item_id, data.l_p_or4c.id) self.assertEqual(line_item.original_line_item_id, data.l_p_or4c.id)
class DifferentTestData(BaseTestData): class DifferentTestData:
"""The test data for different descriptions and amounts.""" """The test data for different descriptions and amounts."""
def _init_data(self) -> None: def __init__(self, app: Flask, client: httpx.Client, csrf_token: str):
"""Constructs the test data.
:param app: The Flask application.
:param client: The client.
:param csrf_token: The CSRF token.
"""
self.app: Flask = app
self.client: httpx.Client = client
self.csrf_token: str = csrf_token
def couple(description: str, amount: str, debit: str, credit: str) \
-> tuple[JournalEntryLineItemData, JournalEntryLineItemData]:
"""Returns a couple of debit-credit line items.
:param description: The description.
:param amount: The amount.
:param debit: The debit account code.
:param credit: The credit account code.
:return: The debit line item and credit line item.
"""
return JournalEntryLineItemData(debit, description, amount),\
JournalEntryLineItemData(credit, description, amount)
# Receivable original line items # Receivable original line items
self.l_r_or1d, self.l_r_or1c = self._couple( self.l_r_or1d, self.l_r_or1c = couple(
"Accountant", "1200", Accounts.RECEIVABLE, Accounts.SERVICE) "Accountant", "1200", Accounts.RECEIVABLE, Accounts.SERVICE)
self.l_r_or2d, self.l_r_or2c = self._couple( self.l_r_or2d, self.l_r_or2c = couple(
"Toy", "600", Accounts.RECEIVABLE, Accounts.SALES) "Toy", "600", Accounts.RECEIVABLE, Accounts.SALES)
self.l_r_or3d, self.l_r_or3c = self._couple( self.l_r_or3d, self.l_r_or3c = couple(
"Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES) "Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES)
self.l_r_or4d, self.l_r_or4c = self._couple( self.l_r_or4d, self.l_r_or4c = couple(
"Interest", "3.4", Accounts.RECEIVABLE, Accounts.INTEREST) "Interest", "3.4", Accounts.RECEIVABLE, Accounts.INTEREST)
# Payable original line items # Payable original line items
self.l_p_or1d, self.l_p_or1c = self._couple( self.l_p_or1d, self.l_p_or1c = couple(
"Airplane", "2000", Accounts.TRAVEL, Accounts.PAYABLE) "Airplane", "2000", Accounts.TRAVEL, Accounts.PAYABLE)
self.l_p_or2d, self.l_p_or2c = self._couple( self.l_p_or2d, self.l_p_or2c = couple(
"Phone", "900", Accounts.OFFICE, Accounts.PAYABLE) "Phone", "900", Accounts.OFFICE, Accounts.PAYABLE)
self.l_p_or3d, self.l_p_or3c = self._couple( self.l_p_or3d, self.l_p_or3c = couple(
"Steak", "120", Accounts.MEAL, Accounts.PAYABLE) "Steak", "120", Accounts.MEAL, Accounts.PAYABLE)
self.l_p_or4d, self.l_p_or4c = self._couple( self.l_p_or4d, self.l_p_or4c = couple(
"Envelop", "0.9", Accounts.OFFICE, Accounts.PAYABLE) "Envelop", "0.9", Accounts.OFFICE, Accounts.PAYABLE)
# Original journal entries # Original journal entries
self.j_r_or1: JournalEntryData = JournalEntryData( self.j_r_or1: JournalEntryData = JournalEntryData(
50, [JournalEntryCurrencyData( 50, [CurrencyData("USD", [self.l_r_or1d, self.l_r_or4d],
"USD", [self.l_r_or1d, self.l_r_or4d],
[self.l_r_or1c, self.l_r_or4c])]) [self.l_r_or1c, self.l_r_or4c])])
self.j_r_or2: JournalEntryData = JournalEntryData( self.j_r_or2: JournalEntryData = JournalEntryData(
30, [JournalEntryCurrencyData( 30, [CurrencyData("USD", [self.l_r_or2d, self.l_r_or3d],
"USD", [self.l_r_or2d, self.l_r_or3d],
[self.l_r_or2c, self.l_r_or3c])]) [self.l_r_or2c, self.l_r_or3c])])
self.j_p_or1: JournalEntryData = JournalEntryData( self.j_p_or1: JournalEntryData = JournalEntryData(
40, [JournalEntryCurrencyData( 40, [CurrencyData("USD", [self.l_p_or1d, self.l_p_or4d],
"USD", [self.l_p_or1d, self.l_p_or4d],
[self.l_p_or1c, self.l_p_or4c])]) [self.l_p_or1c, self.l_p_or4c])])
self.j_p_or2: JournalEntryData = JournalEntryData( self.j_p_or2: JournalEntryData = JournalEntryData(
20, [JournalEntryCurrencyData( 20, [CurrencyData("USD", [self.l_p_or2d, self.l_p_or3d],
"USD", [self.l_p_or2d, self.l_p_or3d],
[self.l_p_or2c, self.l_p_or3c])]) [self.l_p_or2c, self.l_p_or3c])])
self._add_journal_entry(self.j_r_or1) self.__add_journal_entry(self.j_r_or1)
self._add_journal_entry(self.j_r_or2) self.__add_journal_entry(self.j_r_or2)
self._add_journal_entry(self.j_p_or1) self.__add_journal_entry(self.j_p_or1)
self._add_journal_entry(self.j_p_or2) self.__add_journal_entry(self.j_p_or2)
# Receivable offset items # Receivable offset items
self.l_r_of1d, self.l_r_of1c = self._couple( self.l_r_of1d, self.l_r_of1c = couple(
"Accountant", "500", Accounts.CASH, Accounts.RECEIVABLE) "Accountant", "500", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of2d, self.l_r_of2c = self._couple( self.l_r_of2d, self.l_r_of2c = couple(
"Accountant", "200", Accounts.CASH, Accounts.RECEIVABLE) "Accountant", "200", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of3d, self.l_r_of3c = self._couple( self.l_r_of3d, self.l_r_of3c = couple(
"Accountant", "100", Accounts.CASH, Accounts.RECEIVABLE) "Accountant", "100", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of4d, self.l_r_of4c = self._couple( self.l_r_of4d, self.l_r_of4c = couple(
"Toy", "240", Accounts.CASH, Accounts.RECEIVABLE) "Toy", "240", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of5d, self.l_r_of5c = self._couple( self.l_r_of5d, self.l_r_of5c = couple(
"Interest", "3.4", Accounts.CASH, Accounts.RECEIVABLE) "Interest", "3.4", Accounts.CASH, Accounts.RECEIVABLE)
# Payable offset items # Payable offset items
self.l_p_of1d, self.l_p_of1c = self._couple( self.l_p_of1d, self.l_p_of1c = couple(
"Airplane", "800", Accounts.PAYABLE, Accounts.CASH) "Airplane", "800", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of2d, self.l_p_of2c = self._couple( self.l_p_of2d, self.l_p_of2c = couple(
"Airplane", "300", Accounts.PAYABLE, Accounts.CASH) "Airplane", "300", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of3d, self.l_p_of3c = self._couple( self.l_p_of3d, self.l_p_of3c = couple(
"Airplane", "100", Accounts.PAYABLE, Accounts.CASH) "Airplane", "100", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of4d, self.l_p_of4c = self._couple( self.l_p_of4d, self.l_p_of4c = couple(
"Phone", "400", Accounts.PAYABLE, Accounts.CASH) "Phone", "400", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of5d, self.l_p_of5c = self._couple( self.l_p_of5d, self.l_p_of5c = couple(
"Envelop", "0.9", Accounts.PAYABLE, Accounts.CASH) "Envelop", "0.9", Accounts.PAYABLE, Accounts.CASH)
# Offset journal entries # Offset journal entries
self.j_r_of1: JournalEntryData = JournalEntryData( self.j_r_of1: JournalEntryData = JournalEntryData(
25, [JournalEntryCurrencyData( 25, [CurrencyData("USD", [self.l_r_of1d], [self.l_r_of1c])])
"USD", [self.l_r_of1d], [self.l_r_of1c])])
self.j_r_of2: JournalEntryData = JournalEntryData( self.j_r_of2: JournalEntryData = JournalEntryData(
20, [JournalEntryCurrencyData( 20, [CurrencyData("USD",
"USD", [self.l_r_of2d, self.l_r_of3d, self.l_r_of4d], [self.l_r_of2d, self.l_r_of3d, self.l_r_of4d],
[self.l_r_of2c, self.l_r_of3c, self.l_r_of4c])]) [self.l_r_of2c, self.l_r_of3c, self.l_r_of4c])])
self.j_r_of3: JournalEntryData = JournalEntryData( self.j_r_of3: JournalEntryData = JournalEntryData(
15, [JournalEntryCurrencyData( 15, [CurrencyData("USD", [self.l_r_of5d], [self.l_r_of5c])])
"USD", [self.l_r_of5d], [self.l_r_of5c])])
self.j_p_of1: JournalEntryData = JournalEntryData( self.j_p_of1: JournalEntryData = JournalEntryData(
15, [JournalEntryCurrencyData( 15, [CurrencyData("USD", [self.l_p_of1d], [self.l_p_of1c])])
"USD", [self.l_p_of1d], [self.l_p_of1c])])
self.j_p_of2: JournalEntryData = JournalEntryData( self.j_p_of2: JournalEntryData = JournalEntryData(
10, [JournalEntryCurrencyData( 10, [CurrencyData("USD",
"USD", [self.l_p_of2d, self.l_p_of3d, self.l_p_of4d], [self.l_p_of2d, self.l_p_of3d, self.l_p_of4d],
[self.l_p_of2c, self.l_p_of3c, self.l_p_of4c])]) [self.l_p_of2c, self.l_p_of3c, self.l_p_of4c])])
self.j_p_of3: JournalEntryData = JournalEntryData( self.j_p_of3: JournalEntryData = JournalEntryData(
5, [JournalEntryCurrencyData( 5, [CurrencyData("USD", [self.l_p_of5d], [self.l_p_of5c])])
"USD", [self.l_p_of5d], [self.l_p_of5c])])
self._set_need_offset({Accounts.RECEIVABLE, Accounts.PAYABLE}, False) self.__set_is_need_offset(False)
self._add_journal_entry(self.j_r_of1) self.__add_journal_entry(self.j_r_of1)
self._add_journal_entry(self.j_r_of2) self.__add_journal_entry(self.j_r_of2)
self._add_journal_entry(self.j_r_of3) self.__add_journal_entry(self.j_r_of3)
self._add_journal_entry(self.j_p_of1) self.__add_journal_entry(self.j_p_of1)
self._add_journal_entry(self.j_p_of2) self.__add_journal_entry(self.j_p_of2)
self._add_journal_entry(self.j_p_of3) self.__add_journal_entry(self.j_p_of3)
self._set_need_offset({Accounts.RECEIVABLE, Accounts.PAYABLE}, True) self.__set_is_need_offset(True)
def __set_is_need_offset(self, is_need_offset: bool) -> None:
"""Sets whether the payables and receivables need offset.
:param is_need_offset: True if payables and receivables need offset, or
False otherwise.
:return:
"""
from accounting.models import Account
with self.app.app_context():
for code in {Accounts.RECEIVABLE, Accounts.PAYABLE}:
account: Account | None = Account.find_by_code(code)
assert account is not None
account.is_need_offset = is_need_offset
db.session.commit()
def __add_journal_entry(self, journal_entry_data: JournalEntryData) \
-> None:
"""Adds a journal entry.
:param journal_entry_data: The journal entry data.
:return: None.
"""
from accounting.models import JournalEntry
store_uri: str = "/accounting/journal-entries/store/transfer"
response: httpx.Response = self.client.post(
store_uri, data=journal_entry_data.new_form(self.csrf_token))
assert response.status_code == 302
journal_entry_id: int \
= match_journal_entry_detail(response.headers["Location"])
journal_entry_data.id = journal_entry_id
with self.app.app_context():
journal_entry: JournalEntry | None \
= db.session.get(JournalEntry, journal_entry_id)
assert journal_entry is not None
for i in range(len(journal_entry.currencies)):
for j in range(len(journal_entry.currencies[i].debit)):
journal_entry_data.currencies[i].debit[j].id \
= journal_entry.currencies[i].debit[j].id
for j in range(len(journal_entry.currencies[i].credit)):
journal_entry_data.currencies[i].credit[j].id \
= journal_entry.currencies[i].credit[j].id
class SameTestData(BaseTestData): class SameTestData:
"""The test data with same descriptions and amounts.""" """The test data with same descriptions and amounts."""
def _init_data(self) -> None: def __init__(self, app: Flask, client: httpx.Client, csrf_token: str):
"""Constructs the test data.
:param app: The Flask application.
:param client: The client.
:param csrf_token: The CSRF token.
"""
self.app: Flask = app
self.client: httpx.Client = client
self.csrf_token: str = csrf_token
def couple(description: str, amount: str, debit: str, credit: str) \
-> tuple[JournalEntryLineItemData, JournalEntryLineItemData]:
"""Returns a couple of debit-credit line items.
:param description: The description.
:param amount: The amount.
:param debit: The debit account code.
:param credit: The credit account code.
:return: The debit line item and credit line item.
"""
return JournalEntryLineItemData(debit, description, amount),\
JournalEntryLineItemData(credit, description, amount)
# Receivable original line items # Receivable original line items
self.l_r_or1d, self.l_r_or1c = self._add_simple_journal_entry( self.l_r_or1d, self.l_r_or1c = couple(
60, "USD", "Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES) "Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES)
self.l_r_or2d, self.l_r_or2c = self._add_simple_journal_entry( self.l_r_or2d, self.l_r_or2c = couple(
50, "USD", "Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES) "Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES)
self.l_r_or3d, self.l_r_or3c = self._add_simple_journal_entry( self.l_r_or3d, self.l_r_or3c = couple(
40, "USD", "Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES) "Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES)
self.l_r_or4d, self.l_r_or4c = self._add_simple_journal_entry( self.l_r_or4d, self.l_r_or4c = couple(
30, "USD", "Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES) "Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES)
self.l_r_or5d, self.l_r_or5c = self._add_simple_journal_entry( self.l_r_or5d, self.l_r_or5c = couple(
20, "USD", "Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES) "Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES)
self.l_r_or6d, self.l_r_or6c = self._add_simple_journal_entry( self.l_r_or6d, self.l_r_or6c = couple(
10, "USD", "Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES) "Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES)
# Payable original line items # Payable original line items
self.l_p_or1d, self.l_p_or1c = self._add_simple_journal_entry( self.l_p_or1d, self.l_p_or1c = couple(
60, "USD", "Steak", "120", Accounts.MEAL, Accounts.PAYABLE) "Steak", "120", Accounts.MEAL, Accounts.PAYABLE)
self.l_p_or2d, self.l_p_or2c = self._add_simple_journal_entry( self.l_p_or2d, self.l_p_or2c = couple(
50, "USD", "Steak", "120", Accounts.MEAL, Accounts.PAYABLE) "Steak", "120", Accounts.MEAL, Accounts.PAYABLE)
self.l_p_or3d, self.l_p_or3c = self._add_simple_journal_entry( self.l_p_or3d, self.l_p_or3c = couple(
40, "USD", "Steak", "120", Accounts.MEAL, Accounts.PAYABLE) "Steak", "120", Accounts.MEAL, Accounts.PAYABLE)
self.l_p_or4d, self.l_p_or4c = self._add_simple_journal_entry( self.l_p_or4d, self.l_p_or4c = couple(
30, "USD", "Steak", "120", Accounts.MEAL, Accounts.PAYABLE) "Steak", "120", Accounts.MEAL, Accounts.PAYABLE)
self.l_p_or5d, self.l_p_or5c = self._add_simple_journal_entry( self.l_p_or5d, self.l_p_or5c = couple(
20, "USD", "Steak", "120", Accounts.MEAL, Accounts.PAYABLE) "Steak", "120", Accounts.MEAL, Accounts.PAYABLE)
self.l_p_or6d, self.l_p_or6c = self._add_simple_journal_entry( self.l_p_or6d, self.l_p_or6c = couple(
10, "USD", "Steak", "120", Accounts.MEAL, Accounts.PAYABLE) "Steak", "120", Accounts.MEAL, Accounts.PAYABLE)
self._set_need_offset({Accounts.RECEIVABLE, Accounts.PAYABLE}, False) # Original journal entries
self.j_r_or1: JournalEntryData = JournalEntryData(
60, [CurrencyData("USD", [self.l_r_or1d], [self.l_r_or1c])])
self.j_r_or2: JournalEntryData = JournalEntryData(
50, [CurrencyData("USD", [self.l_r_or2d], [self.l_r_or2c])])
self.j_r_or3: JournalEntryData = JournalEntryData(
40, [CurrencyData("USD", [self.l_r_or3d], [self.l_r_or3c])])
self.j_r_or4: JournalEntryData = JournalEntryData(
30, [CurrencyData("USD", [self.l_r_or4d], [self.l_r_or4c])])
self.j_r_or5: JournalEntryData = JournalEntryData(
20, [CurrencyData("USD", [self.l_r_or5d], [self.l_r_or5c])])
self.j_r_or6: JournalEntryData = JournalEntryData(
10, [CurrencyData("USD", [self.l_r_or6d], [self.l_r_or6c])])
self.j_p_or1: JournalEntryData = JournalEntryData(
60, [CurrencyData("USD", [self.l_p_or1d], [self.l_p_or1c])])
self.j_p_or2: JournalEntryData = JournalEntryData(
50, [CurrencyData("USD", [self.l_p_or2d], [self.l_p_or2c])])
self.j_p_or3: JournalEntryData = JournalEntryData(
40, [CurrencyData("USD", [self.l_p_or3d], [self.l_p_or3c])])
self.j_p_or4: JournalEntryData = JournalEntryData(
30, [CurrencyData("USD", [self.l_p_or4d], [self.l_p_or4c])])
self.j_p_or5: JournalEntryData = JournalEntryData(
20, [CurrencyData("USD", [self.l_p_or5d], [self.l_p_or5c])])
self.j_p_or6: JournalEntryData = JournalEntryData(
10, [CurrencyData("USD", [self.l_p_or6d], [self.l_p_or6c])])
self.__add_journal_entry(self.j_r_or1)
self.__add_journal_entry(self.j_r_or2)
self.__add_journal_entry(self.j_r_or3)
self.__add_journal_entry(self.j_r_or4)
self.__add_journal_entry(self.j_r_or5)
self.__add_journal_entry(self.j_r_or6)
self.__add_journal_entry(self.j_p_or1)
self.__add_journal_entry(self.j_p_or2)
self.__add_journal_entry(self.j_p_or3)
self.__add_journal_entry(self.j_p_or4)
self.__add_journal_entry(self.j_p_or5)
self.__add_journal_entry(self.j_p_or6)
# Receivable offset items # Receivable offset items
self.l_r_of1d, self.l_r_of1c = self._add_simple_journal_entry( self.l_r_of1d, self.l_r_of1c = couple(
65, "USD", "Noodles", "100", Accounts.CASH, Accounts.RECEIVABLE) "Noodles", "100", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of2d, self.l_r_of2c = self._add_simple_journal_entry( self.l_r_of2d, self.l_r_of2c = couple(
35, "USD", "Noodles", "100", Accounts.CASH, Accounts.RECEIVABLE) "Noodles", "100", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of3d, self.l_r_of3c = self._couple( self.l_r_of3d, self.l_r_of3c = couple(
"Noodles", "100", Accounts.CASH, Accounts.RECEIVABLE) "Noodles", "100", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of3c.original_line_item = self.l_r_or2d self.l_r_of3c.original_line_item = self.l_r_or2d
j_r_of3: JournalEntryData = JournalEntryData( self.l_r_of4d, self.l_r_of4c = couple(
35, [JournalEntryCurrencyData( "Noodles", "100", Accounts.CASH, Accounts.RECEIVABLE)
"USD", [self.l_r_of3d], [self.l_r_of3c])]) self.l_r_of5d, self.l_r_of5c = couple(
self.l_r_of4d, self.l_r_of4c = self._add_simple_journal_entry( "Noodles", "100", Accounts.CASH, Accounts.RECEIVABLE)
35, "USD", "Noodles", "100", Accounts.CASH, Accounts.RECEIVABLE) self.l_r_of6d, self.l_r_of6c = couple(
self.l_r_of5d, self.l_r_of5c = self._add_simple_journal_entry( "Noodles", "100", Accounts.CASH, Accounts.RECEIVABLE)
35, "USD", "Noodles", "100", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of6d, self.l_r_of6c = self._add_simple_journal_entry(
15, "USD", "Noodles", "100", Accounts.CASH, Accounts.RECEIVABLE)
# Payable offset items # Payable offset items
self.l_p_of1d, self.l_p_of1c = self._add_simple_journal_entry( self.l_p_of1d, self.l_p_of1c = couple(
65, "USD", "Steak", "120", Accounts.PAYABLE, Accounts.CASH) "Steak", "120", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of2d, self.l_p_of2c = self._add_simple_journal_entry( self.l_p_of2d, self.l_p_of2c = couple(
35, "USD", "Steak", "120", Accounts.PAYABLE, Accounts.CASH) "Steak", "120", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of3d, self.l_p_of3c = self._couple( self.l_p_of3d, self.l_p_of3c = couple(
"Steak", "120", Accounts.PAYABLE, Accounts.CASH) "Steak", "120", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of3d.original_line_item = self.l_p_or2c self.l_p_of3d.original_line_item = self.l_p_or2c
j_p_of3: JournalEntryData = JournalEntryData( self.l_p_of4d, self.l_p_of4c = couple(
35, [JournalEntryCurrencyData( "Steak", "120", Accounts.PAYABLE, Accounts.CASH)
"USD", [self.l_p_of3d], [self.l_p_of3c])]) self.l_p_of5d, self.l_p_of5c = couple(
self.l_p_of4d, self.l_p_of4c = self._add_simple_journal_entry( "Steak", "120", Accounts.PAYABLE, Accounts.CASH)
35, "USD", "Steak", "120", Accounts.PAYABLE, Accounts.CASH) self.l_p_of6d, self.l_p_of6c = couple(
self.l_p_of5d, self.l_p_of5c = self._add_simple_journal_entry( "Steak", "120", Accounts.PAYABLE, Accounts.CASH)
35, "USD", "Steak", "120", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of6d, self.l_p_of6c = self._add_simple_journal_entry(
15, "USD", "Steak", "120", Accounts.PAYABLE, Accounts.CASH)
self._set_need_offset({Accounts.RECEIVABLE, Accounts.PAYABLE}, True) # Offset journal entries
self._add_journal_entry(j_r_of3) self.j_r_of1: JournalEntryData = JournalEntryData(
self._add_journal_entry(j_p_of3) 65, [CurrencyData("USD", [self.l_r_of1d], [self.l_r_of1c])])
self.j_r_of2: JournalEntryData = JournalEntryData(
35, [CurrencyData("USD", [self.l_r_of2d], [self.l_r_of2c])])
self.j_r_of3: JournalEntryData = JournalEntryData(
35, [CurrencyData("USD", [self.l_r_of3d], [self.l_r_of3c])])
self.j_r_of4: JournalEntryData = JournalEntryData(
35, [CurrencyData("USD", [self.l_r_of4d], [self.l_r_of4c])])
self.j_r_of5: JournalEntryData = JournalEntryData(
35, [CurrencyData("USD", [self.l_r_of5d], [self.l_r_of5c])])
self.j_r_of6: JournalEntryData = JournalEntryData(
15, [CurrencyData("USD", [self.l_r_of6d], [self.l_r_of6c])])
self.j_p_of1: JournalEntryData = JournalEntryData(
65, [CurrencyData("USD", [self.l_p_of1d], [self.l_p_of1c])])
self.j_p_of2: JournalEntryData = JournalEntryData(
35, [CurrencyData("USD", [self.l_p_of2d], [self.l_p_of2c])])
self.j_p_of3: JournalEntryData = JournalEntryData(
35, [CurrencyData("USD", [self.l_p_of3d], [self.l_p_of3c])])
self.j_p_of4: JournalEntryData = JournalEntryData(
35, [CurrencyData("USD", [self.l_p_of4d], [self.l_p_of4c])])
self.j_p_of5: JournalEntryData = JournalEntryData(
35, [CurrencyData("USD", [self.l_p_of5d], [self.l_p_of5c])])
self.j_p_of6: JournalEntryData = JournalEntryData(
15, [CurrencyData("USD", [self.l_p_of6d], [self.l_p_of6c])])
self.__set_is_need_offset(False)
self.__add_journal_entry(self.j_r_of1)
self.__add_journal_entry(self.j_r_of2)
self.__add_journal_entry(self.j_r_of4)
self.__add_journal_entry(self.j_r_of5)
self.__add_journal_entry(self.j_r_of6)
self.__add_journal_entry(self.j_p_of1)
self.__add_journal_entry(self.j_p_of2)
self.__add_journal_entry(self.j_p_of4)
self.__add_journal_entry(self.j_p_of5)
self.__add_journal_entry(self.j_p_of6)
self.__set_is_need_offset(True)
self.__add_journal_entry(self.j_r_of3)
self.__add_journal_entry(self.j_p_of3)
def __set_is_need_offset(self, is_need_offset: bool) -> None:
"""Sets whether the payables and receivables need offset.
:param is_need_offset: True if payables and receivables need offset, or
False otherwise.
:return:
"""
from accounting.models import Account
with self.app.app_context():
for code in {Accounts.RECEIVABLE, Accounts.PAYABLE}:
account: Account | None = Account.find_by_code(code)
assert account is not None
account.is_need_offset = is_need_offset
db.session.commit()
def __add_journal_entry(self, journal_entry_data: JournalEntryData) \
-> None:
"""Adds a journal entry.
:param journal_entry_data: The journal entry data.
:return: None.
"""
from accounting.models import JournalEntry
store_uri: str = "/accounting/journal-entries/store/transfer"
response: httpx.Response = self.client.post(
store_uri, data=journal_entry_data.new_form(self.csrf_token))
assert response.status_code == 302
journal_entry_id: int \
= match_journal_entry_detail(response.headers["Location"])
journal_entry_data.id = journal_entry_id
with self.app.app_context():
journal_entry: JournalEntry | None \
= db.session.get(JournalEntry, journal_entry_id)
assert journal_entry is not None
for i in range(len(journal_entry.currencies)):
for j in range(len(journal_entry.currencies[i].debit)):
journal_entry_data.currencies[i].debit[j].id \
= journal_entry.currencies[i].debit[j].id
for j in range(len(journal_entry.currencies[i].credit)):
journal_entry_data.currencies[i].credit[j].id \
= journal_entry.currencies[i].credit[j].id

View File

@ -27,6 +27,7 @@ from flask.testing import FlaskCliRunner
from test_site import db from test_site import db
from testlib import NEXT_URI, Accounts, create_test_app, get_client from testlib import NEXT_URI, Accounts, create_test_app, get_client
from testlib_offset import TestData
PREFIX: str = "/accounting/options" PREFIX: str = "/accounting/options"
"""The URL prefix for the option management.""" """The URL prefix for the option management."""
@ -67,6 +68,7 @@ class OptionTestCase(unittest.TestCase):
Option.query.delete() Option.query.delete()
self.client, self.csrf_token = get_client(self.app, "admin") self.client, self.csrf_token = get_client(self.app, "admin")
self.data: TestData = TestData(self.app, self.client, self.csrf_token)
def test_nobody(self) -> None: def test_nobody(self) -> None:
"""Test the permission as nobody. """Test the permission as nobody.

View File

@ -1,415 +0,0 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/9
# 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 test for the reports.
"""
import unittest
from datetime import date
import httpx
from click.testing import Result
from flask import Flask
from flask.testing import FlaskCliRunner
from testlib import create_test_app, get_client, Accounts, BaseTestData
PREFIX: str = "/accounting"
"""The URL prefix for the reports."""
CSV_MIME: str = "text/csv; charset=utf-8"
"""The MIME type of the downloaded CSV files."""
class ReportTestCase(unittest.TestCase):
"""The report test case."""
def setUp(self) -> None:
"""Sets up the test.
This is run once per test.
:return: None.
"""
self.app: Flask = create_test_app()
runner: FlaskCliRunner = self.app.test_cli_runner()
with self.app.app_context():
from accounting.models import BaseAccount, JournalEntry, \
JournalEntryLineItem
result: Result
result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0)
if BaseAccount.query.first() is None:
result = runner.invoke(args="accounting-init-base")
self.assertEqual(result.exit_code, 0)
result = runner.invoke(args=["accounting-init-currencies",
"-u", "editor"])
self.assertEqual(result.exit_code, 0)
result = runner.invoke(args=["accounting-init-accounts",
"-u", "editor"])
self.assertEqual(result.exit_code, 0)
JournalEntry.query.delete()
JournalEntryLineItem.query.delete()
self.client, self.csrf_token = get_client(self.app, "editor")
def test_nobody(self) -> None:
"""Test the permission as nobody.
:return: None.
"""
client, csrf_token = get_client(self.app, "nobody")
ReportTestData(self.app, self.client, self.csrf_token)
response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/journal")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/journal?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/ledger")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/ledger?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/income-expenses")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/income-expenses?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/trial-balance")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/trial-balance?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/income-statement")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/income-statement?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/balance-sheet")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/balance-sheet?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/unapplied")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/unapplied?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/unapplied/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/unapplied/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/search?q=Salary")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/search?q=Salary&as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/search?q=薪水")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/search?q=薪水&as=csv")
self.assertEqual(response.status_code, 403)
def test_viewer(self) -> None:
"""Test the permission as viewer.
:return: None.
"""
client, csrf_token = get_client(self.app, "viewer")
ReportTestData(self.app, self.client, self.csrf_token)
response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = client.get(f"{PREFIX}/journal")
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/journal?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = client.get(f"{PREFIX}/ledger")
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/ledger?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = client.get(f"{PREFIX}/income-expenses")
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/income-expenses?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = client.get(f"{PREFIX}/trial-balance")
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/trial-balance?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = client.get(f"{PREFIX}/income-statement")
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/income-statement?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = client.get(f"{PREFIX}/balance-sheet")
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/balance-sheet?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = client.get(f"{PREFIX}/unapplied")
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/unapplied?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = client.get(f"{PREFIX}/unapplied/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/unapplied/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = client.get(f"{PREFIX}/search?q=Salary")
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/search?q=Salary&as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = client.get(f"{PREFIX}/search?q=薪水")
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/search?q=薪水&as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
def test_editor(self) -> None:
"""Test the permission as editor.
:return: None.
"""
ReportTestData(self.app, self.client, self.csrf_token)
response: httpx.Response
response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/journal")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/journal?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/ledger")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/ledger?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/income-expenses")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/income-expenses?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/trial-balance")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/trial-balance?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/income-statement")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/income-statement?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/balance-sheet")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/balance-sheet?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/unapplied")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/unapplied?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/unapplied/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200)
response = self.client.get(
f"{PREFIX}/unapplied/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/search?q=Salary")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/search?q=Salary&as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/search?q=薪水")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/search?q=薪水&as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
def test_empty_db(self) -> None:
"""Tests the empty database.
:return: None.
"""
response: httpx.Response
response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/journal")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/journal?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/ledger")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/ledger?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/income-expenses")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/income-expenses?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/trial-balance")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/trial-balance?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/income-statement")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/income-statement?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/balance-sheet")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/balance-sheet?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/unapplied")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/unapplied?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/unapplied/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200)
response = self.client.get(
f"{PREFIX}/unapplied/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/search?q=Salary")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/search?q=Salary&as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
class ReportTestData(BaseTestData):
"""The report test data."""
def _init_data(self) -> None:
today: date = date.today()
year: int = today.year - 5
month: int = today.month
while True:
j_date: date = date(year, month, 5)
if j_date > today:
break
self._add_simple_journal_entry(
(j_date - today).days, "USD",
"Salary薪水", "1200", Accounts.BANK, Accounts.SERVICE)
month = month + 1
if month > 12:
year = year + 1
month = 1
self._add_simple_journal_entry(
1, "USD", "Withdraw領錢", "1000", Accounts.CASH, Accounts.BANK)
self._add_simple_journal_entry(
0, "USD", "Dinner晚餐", "40", Accounts.MEAL, Accounts.CASH)

View File

@ -17,19 +17,12 @@
"""The common test libraries. """The common test libraries.
""" """
from __future__ import annotations
import re
import typing as t import typing as t
from abc import ABC, abstractmethod
from datetime import date, timedelta
from _decimal import Decimal
import httpx import httpx
from flask import Flask, render_template_string from flask import Flask, render_template_string
from test_site import create_app, db from test_site import create_app
TEST_SERVER: str = "https://testserver" TEST_SERVER: str = "https://testserver"
"""The test server URI.""" """The test server URI."""
@ -124,269 +117,3 @@ def set_locale(client: httpx.Client, csrf_token: str,
"next": "/next"}) "next": "/next"})
assert response.status_code == 302 assert response.status_code == 302
assert response.headers["Location"] == "/next" assert response.headers["Location"] == "/next"
def add_journal_entry(client: httpx.Client, form: dict[str, str]) -> int:
"""Adds a transfer journal entry.
:param client: The client.
:param form: The form data.
:return: The newly-added journal entry ID.
"""
prefix: str = "/accounting/journal-entries"
journal_entry_type: str = "transfer"
if len({x for x in form if "-debit-" in x}) == 0:
journal_entry_type = "receipt"
elif len({x for x in form if "-credit-" in x}) == 0:
journal_entry_type = "disbursement"
store_uri = f"{prefix}/store/{journal_entry_type}"
response: httpx.Response = client.post(store_uri, data=form)
assert response.status_code == 302
return match_journal_entry_detail(response.headers["Location"])
def match_journal_entry_detail(location: str) -> int:
"""Validates if the redirect location is the journal entry detail, and
returns the journal entry ID on success.
:param location: The redirect location.
:return: The journal entry ID.
:raise AssertionError: When the location is not the journal entry detail.
"""
m: re.Match = re.match(
r"^/accounting/journal-entries/(\d+)\?next=%2F_next", location)
assert m is not None
return int(m.group(1))
class JournalEntryLineItemData:
"""The journal entry line item data."""
def __init__(self, account: str, description: str, amount: str,
original_line_item: JournalEntryLineItemData | None = None):
"""Constructs the journal entry line item data.
:param account: The account code.
:param description: The description.
:param amount: The amount.
:param original_line_item: The original journal entry line item.
"""
self.journal_entry: JournalEntryData | None = None
self.id: int = -1
self.no: int = -1
self.original_line_item: JournalEntryLineItemData | None \
= original_line_item
self.account: str = account
self.description: str = description
self.amount: Decimal = Decimal(amount)
def form(self, prefix: str, debit_credit: str, index: int,
is_update: bool) -> dict[str, str]:
"""Returns the line item as form data.
:param prefix: The prefix of the form fields.
:param debit_credit: Either "debit" or "credit".
:param index: The line item index.
:param is_update: True for an update operation, or False otherwise
:return: The form data.
"""
prefix = f"{prefix}-{debit_credit}-{index}"
form: dict[str, str] = {f"{prefix}-account_code": self.account,
f"{prefix}-description": self.description,
f"{prefix}-amount": str(self.amount)}
if is_update and self.id != -1:
form[f"{prefix}-id"] = str(self.id)
form[f"{prefix}-no"] = str(index) if self.no == -1 else str(self.no)
if self.original_line_item is not None:
assert self.original_line_item.id != -1
form[f"{prefix}-original_line_item_id"] \
= str(self.original_line_item.id)
return form
class JournalEntryCurrencyData:
"""The journal entry currency data."""
def __init__(self, currency: str, debit: list[JournalEntryLineItemData],
credit: list[JournalEntryLineItemData]):
"""Constructs the journal entry currency data.
:param currency: The currency code.
:param debit: The debit line items.
:param credit: The credit line items.
"""
self.code: str = currency
self.debit: list[JournalEntryLineItemData] = debit
self.credit: list[JournalEntryLineItemData] = credit
def form(self, index: int, is_update: bool) -> dict[str, str]:
"""Returns the currency as form data.
:param index: The currency index.
:param is_update: True for an update operation, or False otherwise
:return: The form data.
"""
prefix: str = f"currency-{index}"
form: dict[str, str] = {f"{prefix}-code": self.code}
for i in range(len(self.debit)):
form.update(self.debit[i].form(prefix, "debit", i + 1, is_update))
for i in range(len(self.credit)):
form.update(self.credit[i].form(prefix, "credit", i + 1,
is_update))
return form
class JournalEntryData:
"""The journal entry data."""
def __init__(self, days: int, currencies: list[JournalEntryCurrencyData]):
"""Constructs a journal entry.
:param days: The number of days before today.
:param currencies: The journal entry currency data.
"""
self.id: int = -1
self.days: int = days
self.currencies: list[JournalEntryCurrencyData] = currencies
self.note: str | None = None
for currency in self.currencies:
for line_item in currency.debit:
line_item.journal_entry = self
for line_item in currency.credit:
line_item.journal_entry = self
def new_form(self, csrf_token: str) -> dict[str, str]:
"""Returns the journal entry as a creation form.
:param csrf_token: The CSRF token.
:return: The journal entry as a creation form.
"""
return self.__form(csrf_token, is_update=False)
def update_form(self, csrf_token: str) -> dict[str, str]:
"""Returns the journal entry as an update form.
:param csrf_token: The CSRF token.
:return: The journal entry as an update form.
"""
return self.__form(csrf_token, is_update=True)
def __form(self, csrf_token: str, is_update: bool = False) \
-> dict[str, str]:
"""Returns the journal entry as a form.
:param csrf_token: The CSRF token.
:param is_update: True for an update operation, or False otherwise
:return: The journal entry as a form.
"""
journal_entry_date: date = date.today() - timedelta(days=self.days)
form: dict[str, str] = {"csrf_token": csrf_token,
"next": NEXT_URI,
"date": journal_entry_date.isoformat()}
for i in range(len(self.currencies)):
form.update(self.currencies[i].form(i + 1, is_update))
if self.note is not None:
form["note"] = self.note
return form
class BaseTestData(ABC):
"""The base test data."""
def __init__(self, app: Flask, client: httpx.Client, csrf_token: str):
"""Constructs the test data.
:param app: The Flask application.
:param client: The client.
:param csrf_token: The CSRF token.
"""
self.app: Flask = app
self.client: httpx.Client = client
self.csrf_token: str = csrf_token
self._init_data()
@abstractmethod
def _init_data(self) -> None:
"""Initializes the test data.
:return: None
"""
@staticmethod
def _couple(description: str, amount: str, debit: str, credit: str) \
-> tuple[JournalEntryLineItemData, JournalEntryLineItemData]:
"""Returns a couple of debit-credit line items.
:param description: The description.
:param amount: The amount.
:param debit: The debit account code.
:param credit: The credit account code.
:return: The debit line item and credit line item.
"""
return JournalEntryLineItemData(debit, description, amount),\
JournalEntryLineItemData(credit, description, amount)
def _add_journal_entry(self, journal_entry_data: JournalEntryData) -> None:
"""Adds a journal entry.
:param journal_entry_data: The journal entry data.
:return: None.
"""
from accounting.models import JournalEntry
store_uri: str = "/accounting/journal-entries/store/transfer"
response: httpx.Response = self.client.post(
store_uri, data=journal_entry_data.new_form(self.csrf_token))
assert response.status_code == 302
journal_entry_id: int \
= match_journal_entry_detail(response.headers["Location"])
journal_entry_data.id = journal_entry_id
with self.app.app_context():
journal_entry: JournalEntry | None \
= db.session.get(JournalEntry, journal_entry_id)
assert journal_entry is not None
for i in range(len(journal_entry.currencies)):
for j in range(len(journal_entry.currencies[i].debit)):
journal_entry_data.currencies[i].debit[j].id \
= journal_entry.currencies[i].debit[j].id
for j in range(len(journal_entry.currencies[i].credit)):
journal_entry_data.currencies[i].credit[j].id \
= journal_entry.currencies[i].credit[j].id
def _add_simple_journal_entry(
self, days: int, currency: str, description: str, amount: str,
debit: str, credit: str) \
-> tuple[JournalEntryLineItemData, JournalEntryLineItemData]:
"""Adds a simple journal entry.
:param days: The number of days before today.
:param currency: The currency code.
:param description: The description.
:param amount: The amount.
:param debit: The debit account code.
:param credit: The credit account code.
:return: The debit line item and credit line item.
"""
debit_item, credit_item = self._couple(
description, amount, debit, credit)
self._add_journal_entry(JournalEntryData(
days, [JournalEntryCurrencyData(
currency, [debit_item], [credit_item])]))
return debit_item, credit_item
def _set_need_offset(self, account_codes: set[str],
is_need_offset: bool) -> None:
"""Sets whether the line items in some accounts need offset.
:param account_codes: The account codes.
:param is_need_offset: True if the line items in the accounts need
offset, or False otherwise.
:return:
"""
from accounting.models import Account
with self.app.app_context():
for code in account_codes:
account: Account | None = Account.find_by_code(code)
assert account is not None
account.is_need_offset = is_need_offset
db.session.commit()

View File

@ -18,10 +18,11 @@
""" """
import re import re
from datetime import date
from decimal import Decimal from decimal import Decimal
from datetime import date
from secrets import randbelow from secrets import randbelow
import httpx
from flask import Flask from flask import Flask
from test_site import db from test_site import db
@ -374,6 +375,39 @@ def __get_currency_prefix(form: dict[str, str], code: str) -> str:
return m.group(1) return m.group(1)
def add_journal_entry(client: httpx.Client, form: dict[str, str]) -> int:
"""Adds a transfer journal entry.
:param client: The client.
:param form: The form data.
:return: The newly-added journal entry ID.
"""
prefix: str = "/accounting/journal-entries"
journal_entry_type: str = "transfer"
if len({x for x in form if "-debit-" in x}) == 0:
journal_entry_type = "receipt"
elif len({x for x in form if "-credit-" in x}) == 0:
journal_entry_type = "disbursement"
store_uri = f"{prefix}/store/{journal_entry_type}"
response: httpx.Response = client.post(store_uri, data=form)
assert response.status_code == 302
return match_journal_entry_detail(response.headers["Location"])
def match_journal_entry_detail(location: str) -> int:
"""Validates if the redirect location is the journal entry detail, and
returns the journal entry ID on success.
:param location: The redirect location.
:return: The journal entry ID.
:raise AssertionError: When the location is not the journal entry detail.
"""
m: re.Match = re.match(
r"^/accounting/journal-entries/(\d+)\?next=%2F_next", location)
assert m is not None
return int(m.group(1))
def set_negative_amount(form: dict[str, str]) -> None: def set_negative_amount(form: dict[str, str]) -> None:
"""Sets a negative amount in the form data, keeping the balance. """Sets a negative amount in the form data, keeping the balance.

315
tests/testlib_offset.py Normal file
View File

@ -0,0 +1,315 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/27
# 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 common test libraries for the offset test cases.
"""
from __future__ import annotations
from datetime import date, timedelta
from decimal import Decimal
import httpx
from flask import Flask
from test_site import db
from testlib import NEXT_URI, Accounts
from testlib_journal_entry import match_journal_entry_detail
class JournalEntryLineItemData:
"""The journal entry line item data."""
def __init__(self, account: str, description: str, amount: str,
original_line_item: JournalEntryLineItemData | None = None):
"""Constructs the journal entry line item data.
:param account: The account code.
:param description: The description.
:param amount: The amount.
:param original_line_item: The original journal entry line item.
"""
self.journal_entry: JournalEntryData | None = None
self.id: int = -1
self.no: int = -1
self.original_line_item: JournalEntryLineItemData | None \
= original_line_item
self.account: str = account
self.description: str = description
self.amount: Decimal = Decimal(amount)
def form(self, prefix: str, debit_credit: str, index: int,
is_update: bool) -> dict[str, str]:
"""Returns the line item as form data.
:param prefix: The prefix of the form fields.
:param debit_credit: Either "debit" or "credit".
:param index: The line item index.
:param is_update: True for an update operation, or False otherwise
:return: The form data.
"""
prefix = f"{prefix}-{debit_credit}-{index}"
form: dict[str, str] = {f"{prefix}-account_code": self.account,
f"{prefix}-description": self.description,
f"{prefix}-amount": str(self.amount)}
if is_update and self.id != -1:
form[f"{prefix}-id"] = str(self.id)
form[f"{prefix}-no"] = str(index) if self.no == -1 else str(self.no)
if self.original_line_item is not None:
assert self.original_line_item.id != -1
form[f"{prefix}-original_line_item_id"] \
= str(self.original_line_item.id)
return form
class CurrencyData:
"""The journal entry currency data."""
def __init__(self, currency: str, debit: list[JournalEntryLineItemData],
credit: list[JournalEntryLineItemData]):
"""Constructs the journal entry currency data.
:param currency: The currency code.
:param debit: The debit line items.
:param credit: The credit line items.
"""
self.code: str = currency
self.debit: list[JournalEntryLineItemData] = debit
self.credit: list[JournalEntryLineItemData] = credit
def form(self, index: int, is_update: bool) -> dict[str, str]:
"""Returns the currency as form data.
:param index: The currency index.
:param is_update: True for an update operation, or False otherwise
:return: The form data.
"""
prefix: str = f"currency-{index}"
form: dict[str, str] = {f"{prefix}-code": self.code}
for i in range(len(self.debit)):
form.update(self.debit[i].form(prefix, "debit", i + 1, is_update))
for i in range(len(self.credit)):
form.update(self.credit[i].form(prefix, "credit", i + 1,
is_update))
return form
class JournalEntryData:
"""The journal entry data."""
def __init__(self, days: int, currencies: list[CurrencyData]):
"""Constructs a journal entry.
:param days: The number of days before today.
:param currencies: The journal entry currency data.
"""
self.id: int = -1
self.days: int = days
self.currencies: list[CurrencyData] = currencies
self.note: str | None = None
for currency in self.currencies:
for line_item in currency.debit:
line_item.journal_entry = self
for line_item in currency.credit:
line_item.journal_entry = self
def new_form(self, csrf_token: str) -> dict[str, str]:
"""Returns the journal entry as a creation form.
:param csrf_token: The CSRF token.
:return: The journal entry as a creation form.
"""
return self.__form(csrf_token, is_update=False)
def update_form(self, csrf_token: str) -> dict[str, str]:
"""Returns the journal entry as an update form.
:param csrf_token: The CSRF token.
:return: The journal entry as an update form.
"""
return self.__form(csrf_token, is_update=True)
def __form(self, csrf_token: str, is_update: bool = False) \
-> dict[str, str]:
"""Returns the journal entry as a form.
:param csrf_token: The CSRF token.
:param is_update: True for an update operation, or False otherwise
:return: The journal entry as a form.
"""
journal_entry_date: date = date.today() - timedelta(days=self.days)
form: dict[str, str] = {"csrf_token": csrf_token,
"next": NEXT_URI,
"date": journal_entry_date.isoformat()}
for i in range(len(self.currencies)):
form.update(self.currencies[i].form(i + 1, is_update))
if self.note is not None:
form["note"] = self.note
return form
class TestData:
"""The test data."""
def __init__(self, app: Flask, client: httpx.Client, csrf_token: str):
"""Constructs the test data.
:param app: The Flask application.
:param client: The client.
:param csrf_token: The CSRF token.
"""
self.app: Flask = app
self.client: httpx.Client = client
self.csrf_token: str = csrf_token
def couple(description: str, amount: str, debit: str, credit: str) \
-> tuple[JournalEntryLineItemData, JournalEntryLineItemData]:
"""Returns a couple of debit-credit line items.
:param description: The description.
:param amount: The amount.
:param debit: The debit account code.
:param credit: The credit account code.
:return: The debit line item and credit line item.
"""
return JournalEntryLineItemData(debit, description, amount),\
JournalEntryLineItemData(credit, description, amount)
# Receivable original line items
self.l_r_or1d, self.l_r_or1c = couple(
"Accountant", "1200", Accounts.RECEIVABLE, Accounts.SERVICE)
self.l_r_or2d, self.l_r_or2c = couple(
"Toy", "600", Accounts.RECEIVABLE, Accounts.SALES)
self.l_r_or3d, self.l_r_or3c = couple(
"Noodles", "100", Accounts.RECEIVABLE, Accounts.SALES)
self.l_r_or4d, self.l_r_or4c = couple(
"Interest", "3.4", Accounts.RECEIVABLE, Accounts.INTEREST)
# Payable original line items
self.l_p_or1d, self.l_p_or1c = couple(
"Airplane", "2000", Accounts.TRAVEL, Accounts.PAYABLE)
self.l_p_or2d, self.l_p_or2c = couple(
"Phone", "900", Accounts.OFFICE, Accounts.PAYABLE)
self.l_p_or3d, self.l_p_or3c = couple(
"Steak", "120", Accounts.MEAL, Accounts.PAYABLE)
self.l_p_or4d, self.l_p_or4c = couple(
"Envelop", "0.9", Accounts.OFFICE, Accounts.PAYABLE)
# Original journal entries
self.j_r_or1: JournalEntryData = JournalEntryData(
50, [CurrencyData("USD", [self.l_r_or1d, self.l_r_or4d],
[self.l_r_or1c, self.l_r_or4c])])
self.j_r_or2: JournalEntryData = JournalEntryData(
30, [CurrencyData("USD", [self.l_r_or2d, self.l_r_or3d],
[self.l_r_or2c, self.l_r_or3c])])
self.j_p_or1: JournalEntryData = JournalEntryData(
40, [CurrencyData("USD", [self.l_p_or1d, self.l_p_or4d],
[self.l_p_or1c, self.l_p_or4c])])
self.j_p_or2: JournalEntryData = JournalEntryData(
20, [CurrencyData("USD", [self.l_p_or2d, self.l_p_or3d],
[self.l_p_or2c, self.l_p_or3c])])
self.__add_journal_entry(self.j_r_or1)
self.__add_journal_entry(self.j_r_or2)
self.__add_journal_entry(self.j_p_or1)
self.__add_journal_entry(self.j_p_or2)
# Receivable offset items
self.l_r_of1d, self.l_r_of1c = couple(
"Accountant", "500", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of1c.original_line_item = self.l_r_or1d
self.l_r_of2d, self.l_r_of2c = couple(
"Accountant", "200", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of2c.original_line_item = self.l_r_or1d
self.l_r_of3d, self.l_r_of3c = couple(
"Accountant", "100", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of3c.original_line_item = self.l_r_or1d
self.l_r_of4d, self.l_r_of4c = couple(
"Toy", "240", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of4c.original_line_item = self.l_r_or2d
self.l_r_of5d, self.l_r_of5c = couple(
"Interest", "3.4", Accounts.CASH, Accounts.RECEIVABLE)
self.l_r_of5c.original_line_item = self.l_r_or4d
# Payable offset items
self.l_p_of1d, self.l_p_of1c = couple(
"Airplane", "800", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of1d.original_line_item = self.l_p_or1c
self.l_p_of2d, self.l_p_of2c = couple(
"Airplane", "300", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of2d.original_line_item = self.l_p_or1c
self.l_p_of3d, self.l_p_of3c = couple(
"Airplane", "100", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of3d.original_line_item = self.l_p_or1c
self.l_p_of4d, self.l_p_of4c = couple(
"Phone", "400", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of4d.original_line_item = self.l_p_or2c
self.l_p_of5d, self.l_p_of5c = couple(
"Envelop", "0.9", Accounts.PAYABLE, Accounts.CASH)
self.l_p_of5d.original_line_item = self.l_p_or4c
# Offset journal entries
self.j_r_of1: JournalEntryData = JournalEntryData(
25, [CurrencyData("USD", [self.l_r_of1d], [self.l_r_of1c])])
self.j_r_of2: JournalEntryData = JournalEntryData(
20, [CurrencyData("USD",
[self.l_r_of2d, self.l_r_of3d, self.l_r_of4d],
[self.l_r_of2c, self.l_r_of3c, self.l_r_of4c])])
self.j_r_of3: JournalEntryData = JournalEntryData(
15, [CurrencyData("USD", [self.l_r_of5d], [self.l_r_of5c])])
self.j_p_of1: JournalEntryData = JournalEntryData(
15, [CurrencyData("USD", [self.l_p_of1d], [self.l_p_of1c])])
self.j_p_of2: JournalEntryData = JournalEntryData(
10, [CurrencyData("USD",
[self.l_p_of2d, self.l_p_of3d, self.l_p_of4d],
[self.l_p_of2c, self.l_p_of3c, self.l_p_of4c])])
self.j_p_of3: JournalEntryData = JournalEntryData(
5, [CurrencyData("USD", [self.l_p_of5d], [self.l_p_of5c])])
self.__add_journal_entry(self.j_r_of1)
self.__add_journal_entry(self.j_r_of2)
self.__add_journal_entry(self.j_r_of3)
self.__add_journal_entry(self.j_p_of1)
self.__add_journal_entry(self.j_p_of2)
self.__add_journal_entry(self.j_p_of3)
def __add_journal_entry(self, journal_entry_data: JournalEntryData) \
-> None:
"""Adds a journal entry.
:param journal_entry_data: The journal entry data.
:return: None.
"""
from accounting.models import JournalEntry
store_uri: str = "/accounting/journal-entries/store/transfer"
response: httpx.Response = self.client.post(
store_uri, data=journal_entry_data.new_form(self.csrf_token))
assert response.status_code == 302
journal_entry_id: int \
= match_journal_entry_detail(response.headers["Location"])
journal_entry_data.id = journal_entry_id
with self.app.app_context():
journal_entry: JournalEntry | None \
= db.session.get(JournalEntry, journal_entry_id)
assert journal_entry is not None
for i in range(len(journal_entry.currencies)):
for j in range(len(journal_entry.currencies[i].debit)):
journal_entry_data.currencies[i].debit[j].id \
= journal_entry.currencies[i].debit[j].id
for j in range(len(journal_entry.currencies[i].credit)):
journal_entry_data.currencies[i].credit[j].id \
= journal_entry.currencies[i].credit[j].id