13 Commits

Author SHA1 Message Date
a9c4fa9de0 Advanced to version 1.5.6. 2023-05-23 09:32:48 +08:00
3a676e0b5a Fixed the back URL of the creation forms, applying the accounting_or_next filter for the decoded next URI instead of getting the next URI directly. 2023-05-23 09:32:48 +08:00
9cc7b64bb3 Moved the "__as_next" utility from the test site to the "accounting.utils.next_uri" module, and applied it to the template of the unmatched offset list. 2023-05-23 09:32:48 +08:00
352867797d Advanced to version 1.5.5. 2023-05-23 09:30:33 +08:00
09a344d749 Removed excess spaces from the test_change_date test of the JournalEntryReorderTestCase test case. 2023-05-23 09:30:33 +08:00
818c357613 Revised the next URI utilities to apply URLSafeSerializer for encoding and decoding the next URI, in order to prevent tampering with the next URI. 2023-05-23 09:30:19 +08:00
822c8fc49b Renamed the "__get_next_uri" function to "__get_next" in the "accounting.utils.next_uri" module. 2023-05-23 07:10:30 +08:00
3b8a2e3bb1 Replaced the "accounting-dummy-form" name with the dummy CSRF token to work with OWASP ZAP CSRF token scans. 2023-05-22 18:32:24 +08:00
9e4927ee0b Replaced the get_errors_view with the get_messages_view in the create_test_app function in testlib.py. 2023-05-22 00:03:13 +08:00
3b030c577c Added the integrity value of the CDN stylesheet links in the base template of the test site. 2023-05-19 18:17:29 +08:00
60b33f2a3b Revised the link to the stylesheet of tempus dominus in the base template of the test site. 2023-05-19 18:17:20 +08:00
08fdf59844 Revised the indent of the flashed success messages in the base template of the test site. 2023-05-19 18:17:11 +08:00
b397515457 Removed the size restriction in the next URI utilities. Buffer overflow may happen with any parameter, not only the "next" parameter. It should be solved in uWSGI, but not the application. 2023-05-18 23:30:36 +08:00
25 changed files with 476 additions and 291 deletions

View File

@ -2,6 +2,30 @@ Change Log
========== ==========
Version 1.5.6
-------------
Released 2023/5/23
Bug fixes.
* Fixed the return URI of the creation forms to decode the next URI.
* Fixed the unmatched offset list to use the encoded next URI.
Version 1.5.5
-------------
Released 2023/5/23
Security fixes.
* Revised the next URI utilities to encode and decode the next URI
preventing tampering with the next URI.
* Added the integrity value of the CDN stylesheet links.
* Various fixes.
Version 1.5.4 Version 1.5.4
------------- -------------

View File

@ -24,7 +24,7 @@ from flask_sqlalchemy import SQLAlchemy
from accounting.utils.user import UserUtilityInterface from accounting.utils.user import UserUtilityInterface
VERSION: str = "1.5.4" VERSION: str = "1.5.6"
"""The package version.""" """The package version."""
db: SQLAlchemy = SQLAlchemy() db: SQLAlchemy = SQLAlchemy()
"""The database instance.""" """The database instance."""

View File

@ -23,6 +23,6 @@ First written: 2023/2/1
{% block header %}{% block title %}{{ A_("Add a New Account") }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ A_("Add a New Account") }}{% endblock %}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting.account.list") }}{% endblock %} {% block back_url %}{{ url_for("accounting.account.list")|accounting_or_next }}{% endblock %}
{% block action_url %}{{ url_for("accounting.account.store") }}{% endblock %} {% block action_url %}{{ url_for("accounting.account.store") }}{% endblock %}

View File

@ -23,6 +23,6 @@ First written: 2023/2/6
{% block header %}{% block title %}{{ A_("Add a New Currency") }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ A_("Add a New Currency") }}{% endblock %}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting.currency.list") }}{% endblock %} {% block back_url %}{{ url_for("accounting.currency.list")|accounting_or_next }}{% endblock %}
{% block action_url %}{{ url_for("accounting.currency.store") }}{% endblock %} {% block action_url %}{{ url_for("accounting.currency.store") }}{% endblock %}

View File

@ -23,6 +23,6 @@ First written: 2023/2/25
{% block header %}{% block title %}{{ A_("Add a New Cash Disbursement Journal Entry") }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ A_("Add a New Cash Disbursement Journal Entry") }}{% endblock %}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting-report.default") }}{% endblock %} {% block back_url %}{{ url_for("accounting-report.default")|accounting_or_next }}{% endblock %}
{% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %} {% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %}

View File

@ -19,7 +19,8 @@ description-editor-modal.html: The modal of the description editor
Author: imacat@mail.imacat.idv.tw (imacat) Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/28 First written: 2023/2/28
#} #}
<form id="accounting-description-editor-{{ description_editor.debit_credit }}" class="accounting-description-editor" name="accounting-dummy-form" data-debit-credit="{{ description_editor.debit_credit }}"> <form id="accounting-description-editor-{{ description_editor.debit_credit }}" class="accounting-description-editor" data-debit-credit="{{ description_editor.debit_credit }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-modal-label" aria-hidden="true"> <div id="accounting-description-editor-{{ description_editor.debit_credit }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-modal-label" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">

View File

@ -19,7 +19,8 @@ journal-entry-line-item-editor-modal: The modal of the journal entry line item e
Author: imacat@mail.imacat.idv.tw (imacat) Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/25 First written: 2023/2/25
#} #}
<form id="accounting-line-item-editor" name="accounting-dummy-form"> <form id="accounting-line-item-editor">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div id="accounting-line-item-editor-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-line-item-editor-modal-label" aria-hidden="true"> <div id="accounting-line-item-editor-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-line-item-editor-modal-label" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">

View File

@ -23,6 +23,6 @@ First written: 2023/2/25
{% block header %}{% block title %}{{ A_("Add a New Cash Receipt Journal Entry") }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ A_("Add a New Cash Receipt Journal Entry") }}{% endblock %}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting-report.default") }}{% endblock %} {% block back_url %}{{ url_for("accounting-report.default")|accounting_or_next }}{% endblock %}
{% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %} {% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %}

View File

@ -23,6 +23,6 @@ First written: 2023/2/25
{% block header %}{% block title %}{{ A_("Add a New Transfer Journal Entry") }}{% endblock %}{% endblock %} {% block header %}{% block title %}{{ A_("Add a New Transfer Journal Entry") }}{% endblock %}{% endblock %}
{% block back_url %}{{ request.args.get("next") or url_for("accounting-report.default") }}{% endblock %} {% block back_url %}{{ url_for("accounting-report.default")|accounting_or_next }}{% endblock %}
{% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %} {% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %}

View File

@ -19,7 +19,8 @@ recurring-item-editor-modal.html: The modal of the recurring item editor
Author: imacat@mail.imacat.idv.tw (imacat) Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/3/22 First written: 2023/3/22
#} #}
<form id="accounting-recurring-item-editor-{{ expense_income }}" name="accounting-dummy-form"> <form id="accounting-recurring-item-editor-{{ expense_income }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div id="accounting-recurring-item-editor-{{ expense_income }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-recurring-item-editor-{{ expense_income }}-modal-label" aria-hidden="true"> <div id="accounting-recurring-item-editor-{{ expense_income }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-recurring-item-editor-{{ expense_income }}-modal-label" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">

View File

@ -49,7 +49,7 @@ First written: 2023/4/17
<form action="{{ url_for("accounting-report.match-offsets", currency=report.currency, account=report.account) }}" method="post"> <form action="{{ url_for("accounting-report.match-offsets", currency=report.currency, account=report.account) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="next" value="{{ request.full_path if request.query_string else request.path }}"> <input type="hidden" name="next" value="{{ accounting_as_next() }}">
<div class="modal fade" id="accounting-match-modal" tabindex="-1" aria-labelledby="accounting-match-modal-label" aria-hidden="true"> <div class="modal fade" id="accounting-match-modal" tabindex="-1" aria-labelledby="accounting-match-modal-label" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">

View File

@ -22,7 +22,17 @@ This module should not import any other module from the application.
from urllib.parse import urlparse, parse_qsl, ParseResult, urlencode, \ from urllib.parse import urlparse, parse_qsl, ParseResult, urlencode, \
urlunparse urlunparse
from flask import request, Blueprint from flask import request, Blueprint, current_app
from itsdangerous import URLSafeSerializer, BadData
def __as_next() -> str:
"""Encodes the current request URI as value for the next URI.
:return: The current request URI as value for the next URI.
"""
return encode_next(
request.full_path if request.query_string else request.path)
def append_next(uri: str) -> str: def append_next(uri: str) -> str:
@ -41,7 +51,7 @@ def inherit_next(uri: str) -> str:
:param uri: The URI. :param uri: The URI.
:return: The URI with the current next URI added at the query argument. :return: The URI with the current next URI added at the query argument.
""" """
next_uri: str | None = __get_next_uri() next_uri: str | None = __get_next()
return uri if next_uri is None else __set_next(uri, next_uri) return uri if next_uri is None else __set_next(uri, next_uri)
@ -51,22 +61,24 @@ def or_next(uri: str) -> str:
:param uri: The URI. :param uri: The URI.
:return: The next URI or the supplied URI. :return: The next URI or the supplied URI.
""" """
next_uri: str | None = __get_next_uri() next_uri: str | None = __get_next()
return uri if next_uri is None else next_uri return uri if next_uri is None else next_uri
def __get_next_uri() -> str | None: def __get_next() -> str | None:
"""Returns the valid next URI. """Returns the valid next URI.
:return: The valid next URI. :return: The valid next URI.
""" """
next_uri: str | None = request.form.get("next") \ next_uri: str | None = request.form.get("next") \
if request.method == "POST" else request.args.get("next") if request.method == "POST" else request.args.get("next")
if next_uri is None or not next_uri.startswith("/"): if next_uri is None:
return None
try:
return URLSafeSerializer(current_app.config["SECRET_KEY"])\
.loads(next_uri, "next")
except BadData:
return None return None
if len(next_uri) > 512:
return next_uri[:512]
return next_uri
def __set_next(uri: str, next_uri: str) -> str: def __set_next(uri: str, next_uri: str) -> str:
@ -79,18 +91,29 @@ def __set_next(uri: str, next_uri: str) -> str:
uri_p: ParseResult = urlparse(uri) uri_p: ParseResult = urlparse(uri)
params: list[tuple[str, str]] = parse_qsl(uri_p.query) params: list[tuple[str, str]] = parse_qsl(uri_p.query)
params = [x for x in params if x[0] != "next"] params = [x for x in params if x[0] != "next"]
params.append(("next", next_uri)) params.append(("next", encode_next(next_uri)))
parts: list[str] = list(uri_p) parts: list[str] = list(uri_p)
parts[4] = urlencode(params) parts[4] = urlencode(params)
return urlunparse(parts) return urlunparse(parts)
def encode_next(uri: str) -> str:
"""Encodes the next URI.
:param uri: The next URI.
:return: The encoded next URI.
"""
return URLSafeSerializer(current_app.config["SECRET_KEY"])\
.dumps(uri, "next")
def init_app(bp: Blueprint) -> None: def init_app(bp: Blueprint) -> None:
"""Initializes the application. """Initializes the application.
:param bp: The blueprint of the accounting application. :param bp: The blueprint of the accounting application.
:return: None. :return: None.
""" """
bp.add_app_template_global(__as_next, "accounting_as_next")
bp.add_app_template_filter(append_next, "accounting_append_next") bp.add_app_template_filter(append_next, "accounting_append_next")
bp.add_app_template_filter(inherit_next, "accounting_inherit_next") bp.add_app_template_filter(inherit_next, "accounting_inherit_next")
bp.add_app_template_filter(or_next, "accounting_or_next") bp.add_app_template_filter(or_next, "accounting_or_next")

View File

@ -23,6 +23,7 @@ import unittest
import httpx import httpx
from flask import Flask from flask import Flask
from accounting.utils.next_uri import encode_next
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 add_journal_entry
@ -78,6 +79,7 @@ class AccountTestCase(unittest.TestCase):
AccountL10n.query.delete() AccountL10n.query.delete()
Account.query.delete() Account.query.delete()
db.session.commit() db.session.commit()
self.encoded_next_uri: str = encode_next(NEXT_URI)
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")
response: httpx.Response response: httpx.Response
@ -143,7 +145,7 @@ class AccountTestCase(unittest.TestCase):
response = client.post(f"{PREFIX}/bases/{CASH.base_code}", response = client.post(f"{PREFIX}/bases/{CASH.base_code}",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"next": NEXT_URI, "next": self.encoded_next_uri,
f"{cash_id}-no": "5"}) f"{cash_id}-no": "5"})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
@ -192,7 +194,7 @@ class AccountTestCase(unittest.TestCase):
response = client.post(f"{PREFIX}/bases/{CASH.base_code}", response = client.post(f"{PREFIX}/bases/{CASH.base_code}",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"next": NEXT_URI, "next": self.encoded_next_uri,
f"{cash_id}-no": "5"}) f"{cash_id}-no": "5"})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
@ -244,7 +246,7 @@ class AccountTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/bases/{CASH.base_code}", response = self.client.post(f"{PREFIX}/bases/{CASH.base_code}",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI, "next": self.encoded_next_uri,
f"{cash_id}-no": "5"}) f"{cash_id}-no": "5"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
@ -526,7 +528,7 @@ class AccountTestCase(unittest.TestCase):
self.assertEqual(account.title_l10n, CASH.title) self.assertEqual(account.title_l10n, CASH.title)
self.assertEqual(account.l10n, []) self.assertEqual(account.l10n, [])
set_locale(self.client, self.csrf_token, "zh_Hant") set_locale(self.app, self.client, self.csrf_token, "zh_Hant")
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
@ -541,7 +543,7 @@ class AccountTestCase(unittest.TestCase):
self.assertEqual({(x.locale, x.title) for x in account.l10n}, self.assertEqual({(x.locale, x.title) for x in account.l10n},
{("zh_Hant", f"{CASH.title}-zh_Hant")}) {("zh_Hant", f"{CASH.title}-zh_Hant")})
set_locale(self.client, self.csrf_token, "en") set_locale(self.app, self.client, self.csrf_token, "en")
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
@ -556,7 +558,7 @@ class AccountTestCase(unittest.TestCase):
self.assertEqual({(x.locale, x.title) for x in account.l10n}, self.assertEqual({(x.locale, x.title) for x in account.l10n},
{("zh_Hant", f"{CASH.title}-zh_Hant")}) {("zh_Hant", f"{CASH.title}-zh_Hant")})
set_locale(self.client, self.csrf_token, "zh_Hant") set_locale(self.app, self.client, self.csrf_token, "zh_Hant")
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
@ -591,7 +593,7 @@ class AccountTestCase(unittest.TestCase):
add_journal_entry(self.client, add_journal_entry(self.client,
form={"csrf_token": self.csrf_token, form={"csrf_token": self.csrf_token,
"next": NEXT_URI, "next": self.encoded_next_uri,
"date": dt.date.today().isoformat(), "date": dt.date.today().isoformat(),
"currency-1-code": "USD", "currency-1-code": "USD",
"currency-1-credit-1-account_code": BANK.code, "currency-1-credit-1-account_code": BANK.code,
@ -709,7 +711,7 @@ class AccountTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/bases/1111", response = self.client.post(f"{PREFIX}/bases/1111",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI, "next": self.encoded_next_uri,
f"{id_1}-no": "4", f"{id_1}-no": "4",
f"{id_2}-no": "1", f"{id_2}-no": "1",
f"{id_3}-no": "5", f"{id_3}-no": "5",
@ -736,7 +738,7 @@ class AccountTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/bases/1111", response = self.client.post(f"{PREFIX}/bases/1111",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI, "next": self.encoded_next_uri,
f"{id_2}-no": "3a", f"{id_2}-no": "3a",
f"{id_3}-no": "5", f"{id_3}-no": "5",
f"{id_4}-no": "2"}) f"{id_4}-no": "2"})

View File

@ -23,6 +23,7 @@ import unittest
import httpx import httpx
from flask import Flask from flask import Flask
from accounting.utils.next_uri import encode_next
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 add_journal_entry
@ -468,7 +469,7 @@ class CurrencyTestCase(unittest.TestCase):
self.assertEqual(currency.name_l10n, USD.name) self.assertEqual(currency.name_l10n, USD.name)
self.assertEqual(currency.l10n, []) self.assertEqual(currency.l10n, [])
set_locale(self.client, self.csrf_token, "zh_Hant") set_locale(self.app, self.client, self.csrf_token, "zh_Hant")
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
@ -483,7 +484,7 @@ class CurrencyTestCase(unittest.TestCase):
self.assertEqual({(x.locale, x.name) for x in currency.l10n}, self.assertEqual({(x.locale, x.name) for x in currency.l10n},
{("zh_Hant", f"{USD.name}-zh_Hant")}) {("zh_Hant", f"{USD.name}-zh_Hant")})
set_locale(self.client, self.csrf_token, "en") set_locale(self.app, self.client, self.csrf_token, "en")
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
@ -498,7 +499,7 @@ class CurrencyTestCase(unittest.TestCase):
self.assertEqual({(x.locale, x.name) for x in currency.l10n}, self.assertEqual({(x.locale, x.name) for x in currency.l10n},
{("zh_Hant", f"{USD.name}-zh_Hant")}) {("zh_Hant", f"{USD.name}-zh_Hant")})
set_locale(self.client, self.csrf_token, "zh_Hant") set_locale(self.app, self.client, self.csrf_token, "zh_Hant")
response = self.client.post(update_uri, response = self.client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
@ -521,6 +522,8 @@ class CurrencyTestCase(unittest.TestCase):
from accounting.models import Currency from accounting.models import Currency
detail_uri: str = f"{PREFIX}/{JPY.code}" detail_uri: str = f"{PREFIX}/{JPY.code}"
delete_uri: str = f"{PREFIX}/{JPY.code}/delete" delete_uri: str = f"{PREFIX}/{JPY.code}/delete"
with self.app.app_context():
encoded_next_uri: str = encode_next(NEXT_URI)
list_uri: str = PREFIX list_uri: str = PREFIX
response: httpx.Response response: httpx.Response
@ -533,7 +536,7 @@ class CurrencyTestCase(unittest.TestCase):
add_journal_entry(self.client, add_journal_entry(self.client,
form={"csrf_token": self.csrf_token, form={"csrf_token": self.csrf_token,
"next": NEXT_URI, "next": encoded_next_uri,
"date": dt.date.today().isoformat(), "date": dt.date.today().isoformat(),
"currency-1-code": EUR.code, "currency-1-code": EUR.code,
"currency-1-credit-1-account_code": "1111-001", "currency-1-credit-1-account_code": "1111-001",

View File

@ -22,6 +22,7 @@ import unittest
from flask import Flask from flask import Flask
from accounting.utils.next_uri import encode_next
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 add_journal_entry
@ -41,6 +42,7 @@ class DescriptionEditorTestCase(unittest.TestCase):
from accounting.models import JournalEntry, JournalEntryLineItem from accounting.models import JournalEntry, JournalEntryLineItem
JournalEntry.query.delete() JournalEntry.query.delete()
JournalEntryLineItem.query.delete() JournalEntryLineItem.query.delete()
self.encoded_next_uri: str = encode_next(NEXT_URI)
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")
@ -51,7 +53,7 @@ class DescriptionEditorTestCase(unittest.TestCase):
""" """
from accounting.journal_entry.utils.description_editor import \ from accounting.journal_entry.utils.description_editor import \
DescriptionEditor DescriptionEditor
for form in get_form_data(self.csrf_token): for form in get_form_data(self.csrf_token, self.encoded_next_uri):
add_journal_entry(self.client, form) add_journal_entry(self.client, form)
with self.app.app_context(): with self.app.app_context():
editor: DescriptionEditor = DescriptionEditor() editor: DescriptionEditor = DescriptionEditor()
@ -143,22 +145,24 @@ class DescriptionEditorTestCase(unittest.TestCase):
Accounts.PREPAID) Accounts.PREPAID)
def get_form_data(csrf_token: str) -> list[dict[str, str]]: def get_form_data(csrf_token: str, encoded_next_uri: str) \
-> list[dict[str, str]]:
"""Returns the form data for multiple journal entry forms. """Returns the form data for multiple journal entry forms.
:param csrf_token: The CSRF token. :param csrf_token: The CSRF token.
:param encoded_next_uri: The encoded next URI.
:return: A list of the form data. :return: A list of the form data.
""" """
journal_entry_date: str = dt.date.today().isoformat() journal_entry_date: str = dt.date.today().isoformat()
return [{"csrf_token": csrf_token, return [{"csrf_token": csrf_token,
"next": NEXT_URI, "next": encoded_next_uri,
"date": journal_entry_date, "date": journal_entry_date,
"currency-0-code": "USD", "currency-0-code": "USD",
"currency-0-credit-0-account_code": Accounts.SERVICE, "currency-0-credit-0-account_code": Accounts.SERVICE,
"currency-0-credit-0-description": " Salary ", "currency-0-credit-0-description": " Salary ",
"currency-0-credit-0-amount": "2500"}, "currency-0-credit-0-amount": "2500"},
{"csrf_token": csrf_token, {"csrf_token": csrf_token,
"next": NEXT_URI, "next": encoded_next_uri,
"date": journal_entry_date, "date": journal_entry_date,
"currency-0-code": "USD", "currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.MEAL, "currency-0-debit-0-account_code": Accounts.MEAL,
@ -180,7 +184,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
"currency-0-credit-2-description": " Dinner—Hamburger ", "currency-0-credit-2-description": " Dinner—Hamburger ",
"currency-0-credit-2-amount": "4.25"}, "currency-0-credit-2-amount": "4.25"},
{"csrf_token": csrf_token, {"csrf_token": csrf_token,
"next": NEXT_URI, "next": encoded_next_uri,
"date": journal_entry_date, "date": journal_entry_date,
"currency-0-code": "USD", "currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.MEAL, "currency-0-debit-0-account_code": Accounts.MEAL,
@ -196,7 +200,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
"currency-0-credit-1-description": " Dinner—Steak ", "currency-0-credit-1-description": " Dinner—Steak ",
"currency-0-credit-1-amount": "8.28"}, "currency-0-credit-1-amount": "8.28"},
{"csrf_token": csrf_token, {"csrf_token": csrf_token,
"next": NEXT_URI, "next": encoded_next_uri,
"date": journal_entry_date, "date": journal_entry_date,
"currency-0-code": "USD", "currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.MEAL, "currency-0-debit-0-account_code": Accounts.MEAL,
@ -212,14 +216,14 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
"currency-0-credit-1-description": " Lunch—Noodles ", "currency-0-credit-1-description": " Lunch—Noodles ",
"currency-0-credit-1-amount": "7.47"}, "currency-0-credit-1-amount": "7.47"},
{"csrf_token": csrf_token, {"csrf_token": csrf_token,
"next": NEXT_URI, "next": encoded_next_uri,
"date": journal_entry_date, "date": journal_entry_date,
"currency-0-code": "USD", "currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.TRAVEL, "currency-0-debit-0-account_code": Accounts.TRAVEL,
"currency-0-debit-0-description": " Airplane—Lake City↔Hill Town", "currency-0-debit-0-description": " Airplane—Lake City↔Hill Town",
"currency-0-debit-0-amount": "800"}, "currency-0-debit-0-amount": "800"},
{"csrf_token": csrf_token, {"csrf_token": csrf_token,
"next": NEXT_URI, "next": encoded_next_uri,
"date": journal_entry_date, "date": journal_entry_date,
"currency-0-code": "USD", "currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.TRAVEL, "currency-0-debit-0-account_code": Accounts.TRAVEL,
@ -247,7 +251,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
"currency-0-credit-3-description": " Train—Red—Mall→Museum ", "currency-0-credit-3-description": " Train—Red—Mall→Museum ",
"currency-0-credit-3-amount": "4.4"}, "currency-0-credit-3-amount": "4.4"},
{"csrf_token": csrf_token, {"csrf_token": csrf_token,
"next": NEXT_URI, "next": encoded_next_uri,
"date": journal_entry_date, "date": journal_entry_date,
"currency-0-code": "USD", "currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.TRAVEL, "currency-0-debit-0-account_code": Accounts.TRAVEL,
@ -293,7 +297,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
"currency-0-credit-6-description": " Bike—Theatre→Home ", "currency-0-credit-6-description": " Bike—Theatre→Home ",
"currency-0-credit-6-amount": "5.5"}, "currency-0-credit-6-amount": "5.5"},
{"csrf_token": csrf_token, {"csrf_token": csrf_token,
"next": NEXT_URI, "next": encoded_next_uri,
"date": journal_entry_date, "date": journal_entry_date,
"currency-0-code": "USD", "currency-0-code": "USD",
"currency-0-debit-0-account_code": Accounts.PETTY_CASH, "currency-0-debit-0-account_code": Accounts.PETTY_CASH,

View File

@ -24,6 +24,7 @@ from decimal import Decimal
import httpx import httpx
from flask import Flask from flask import Flask
from accounting.utils.next_uri import encode_next
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 add_journal_entry, match_journal_entry_detail
@ -53,6 +54,7 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry, JournalEntryLineItem from accounting.models import JournalEntry, JournalEntryLineItem
JournalEntry.query.delete() JournalEntry.query.delete()
JournalEntryLineItem.query.delete() JournalEntryLineItem.query.delete()
self.encoded_next_uri: str = encode_next(NEXT_URI)
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")
@ -153,7 +155,8 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
data=update_form) data=update_form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{journal_entry_id}?next=%2F_next") f"{PREFIX}/{journal_entry_id}?"
f"next={self.encoded_next_uri}")
response = self.client.post(f"{PREFIX}/{journal_entry_id}/delete", response = self.client.post(f"{PREFIX}/{journal_entry_id}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.csrf_token})
@ -166,7 +169,8 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import JournalEntry, JournalEntryCurrency from accounting.models import JournalEntry, JournalEntryCurrency
create_uri: str = f"{PREFIX}/create/receipt?next=%2F_next" create_uri: str = (f"{PREFIX}/create/receipt?"
f"next={self.encoded_next_uri}")
store_uri: str = f"{PREFIX}/store/receipt" store_uri: str = f"{PREFIX}/store/receipt"
response: httpx.Response response: httpx.Response
form: dict[str, str] form: dict[str, str]
@ -322,8 +326,10 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry, JournalEntryCurrency from accounting.models import JournalEntry, JournalEntryCurrency
journal_entry_id: int \ journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
edit_uri: str = f"{PREFIX}/{journal_entry_id}/edit?next=%2F_next" f"next={self.encoded_next_uri}")
edit_uri: str = (f"{PREFIX}/{journal_entry_id}/edit?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_id}/update" update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
form_0: dict[str, str] = self.__get_update_form(journal_entry_id) form_0: dict[str, str] = self.__get_update_form(journal_entry_id)
@ -485,7 +491,8 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry from accounting.models import JournalEntry
journal_entry_id: int \ journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_id}/update" update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
journal_entry: JournalEntry journal_entry: JournalEntry
response: httpx.Response response: httpx.Response
@ -524,7 +531,8 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
editor_username, admin_username = "editor", "admin" editor_username, admin_username = "editor", "admin"
client, csrf_token = get_client(self.app, admin_username) client, csrf_token = get_client(self.app, admin_username)
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_id}/update" update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
journal_entry: JournalEntry journal_entry: JournalEntry
response: httpx.Response response: httpx.Response
@ -557,7 +565,8 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry, JournalEntryLineItem from accounting.models import JournalEntry, JournalEntryLineItem
journal_entry_id_1: int \ journal_entry_id_1: int \
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{journal_entry_id_1}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id_1}?"
f"next={self.encoded_next_uri}")
delete_uri: str = f"{PREFIX}/{journal_entry_id_1}/delete" delete_uri: str = f"{PREFIX}/{journal_entry_id_1}/delete"
response: httpx.Response response: httpx.Response
@ -575,7 +584,7 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
add_journal_entry( add_journal_entry(
self.client, self.client,
form={"csrf_token": self.csrf_token, form={"csrf_token": self.csrf_token,
"next": NEXT_URI, "next": self.encoded_next_uri,
"date": dt.date.today().isoformat(), "date": dt.date.today().isoformat(),
"currency-1-code": line_item.currency_code, "currency-1-code": line_item.currency_code,
"currency-1-debit-1-original_line_item_id": line_item.id, "currency-1-debit-1-original_line_item_id": line_item.id,
@ -585,17 +594,18 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
# Cannot delete the journal entry that is in use # Cannot delete the journal entry that is in use
response = self.client.post(f"{PREFIX}/{journal_entry_id_2}/delete", response = self.client.post(f"{PREFIX}/{journal_entry_id_2}/delete",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{journal_entry_id_2}?next=%2F_next") f"{PREFIX}/{journal_entry_id_2}?"
f"next={self.encoded_next_uri}")
# Success # Success
response = self.client.get(detail_uri) response = self.client.get(detail_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(delete_uri, response = self.client.post(delete_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
@ -603,7 +613,7 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
response = self.client.post(delete_uri, response = self.client.post(delete_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def __get_add_form(self) -> dict[str, str]: def __get_add_form(self) -> dict[str, str]:
@ -611,7 +621,8 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
:return: The form data to add a new journal entry. :return: The form data to add a new journal entry.
""" """
form: dict[str, str] = get_add_form(self.csrf_token) form: dict[str, str] = get_add_form(self.csrf_token,
self.encoded_next_uri)
form = {x: form[x] for x in form if "-debit-" not in x} form = {x: form[x] for x in form if "-debit-" not in x}
return form return form
@ -625,7 +636,7 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
not changed. not changed.
""" """
form: dict[str, str] = get_unchanged_update_form( form: dict[str, str] = get_unchanged_update_form(
journal_entry_id, self.app, self.csrf_token) journal_entry_id, self.app, self.csrf_token, self.encoded_next_uri)
form = {x: form[x] for x in form if "-debit-" not in x} form = {x: form[x] for x in form if "-debit-" not in x}
return form return form
@ -638,7 +649,8 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
changed. changed.
""" """
form: dict[str, str] = get_update_form( form: dict[str, str] = get_update_form(
journal_entry_id, self.app, self.csrf_token, False) journal_entry_id, self.app, self.csrf_token, self.encoded_next_uri,
False)
form = {x: form[x] for x in form if "-debit-" not in x} form = {x: form[x] for x in form if "-debit-" not in x}
return form return form
@ -658,6 +670,7 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry, JournalEntryLineItem from accounting.models import JournalEntry, JournalEntryLineItem
JournalEntry.query.delete() JournalEntry.query.delete()
JournalEntryLineItem.query.delete() JournalEntryLineItem.query.delete()
self.encoded_next_uri: str = encode_next(NEXT_URI)
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")
@ -758,7 +771,8 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
data=update_form) data=update_form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{journal_entry_id}?next=%2F_next") f"{PREFIX}/{journal_entry_id}?"
f"next={self.encoded_next_uri}")
response = self.client.post(f"{PREFIX}/{journal_entry_id}/delete", response = self.client.post(f"{PREFIX}/{journal_entry_id}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.csrf_token})
@ -771,7 +785,8 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import JournalEntry, JournalEntryCurrency from accounting.models import JournalEntry, JournalEntryCurrency
create_uri: str = f"{PREFIX}/create/disbursement?next=%2F_next" create_uri: str = (f"{PREFIX}/create/disbursement?"
f"next={self.encoded_next_uri}")
store_uri: str = f"{PREFIX}/store/disbursement" store_uri: str = f"{PREFIX}/store/disbursement"
response: httpx.Response response: httpx.Response
form: dict[str, str] form: dict[str, str]
@ -930,8 +945,10 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry, JournalEntryCurrency from accounting.models import JournalEntry, JournalEntryCurrency
journal_entry_id: int \ journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
edit_uri: str = f"{PREFIX}/{journal_entry_id}/edit?next=%2F_next" f"next={self.encoded_next_uri}")
edit_uri: str = (f"{PREFIX}/{journal_entry_id}/edit?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_id}/update" update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
form_0: dict[str, str] = self.__get_update_form(journal_entry_id) form_0: dict[str, str] = self.__get_update_form(journal_entry_id)
@ -1097,7 +1114,8 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry from accounting.models import JournalEntry
journal_entry_id: int \ journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_id}/update" update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
journal_entry: JournalEntry journal_entry: JournalEntry
response: httpx.Response response: httpx.Response
@ -1136,7 +1154,8 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
editor_username, admin_username = "editor", "admin" editor_username, admin_username = "editor", "admin"
client, csrf_token = get_client(self.app, admin_username) client, csrf_token = get_client(self.app, admin_username)
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_id}/update" update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
journal_entry: JournalEntry journal_entry: JournalEntry
response: httpx.Response response: httpx.Response
@ -1168,7 +1187,8 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
""" """
journal_entry_id: int \ journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
f"next={self.encoded_next_uri}")
delete_uri: str = f"{PREFIX}/{journal_entry_id}/delete" delete_uri: str = f"{PREFIX}/{journal_entry_id}/delete"
response: httpx.Response response: httpx.Response
@ -1176,7 +1196,7 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(delete_uri, response = self.client.post(delete_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
@ -1184,7 +1204,7 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
response = self.client.post(delete_uri, response = self.client.post(delete_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def __get_add_form(self) -> dict[str, str]: def __get_add_form(self) -> dict[str, str]:
@ -1192,7 +1212,8 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
:return: The form data to add a new journal entry. :return: The form data to add a new journal entry.
""" """
form: dict[str, str] = get_add_form(self.csrf_token) form: dict[str, str] = get_add_form(self.csrf_token,
self.encoded_next_uri)
form = {x: form[x] for x in form if "-credit-" not in x} form = {x: form[x] for x in form if "-credit-" not in x}
return form return form
@ -1206,7 +1227,7 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
not changed. not changed.
""" """
form: dict[str, str] = get_unchanged_update_form( form: dict[str, str] = get_unchanged_update_form(
journal_entry_id, self.app, self.csrf_token) journal_entry_id, self.app, self.csrf_token, self.encoded_next_uri)
form = {x: form[x] for x in form if "-credit-" not in x} form = {x: form[x] for x in form if "-credit-" not in x}
return form return form
@ -1219,7 +1240,8 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
changed. changed.
""" """
form: dict[str, str] = get_update_form( form: dict[str, str] = get_update_form(
journal_entry_id, self.app, self.csrf_token, True) journal_entry_id, self.app, self.csrf_token, self.encoded_next_uri,
True)
form = {x: form[x] for x in form if "-credit-" not in x} form = {x: form[x] for x in form if "-credit-" not in x}
return form return form
@ -1240,6 +1262,7 @@ class TransferJournalEntryTestCase(unittest.TestCase):
JournalEntryLineItem JournalEntryLineItem
JournalEntry.query.delete() JournalEntry.query.delete()
JournalEntryLineItem.query.delete() JournalEntryLineItem.query.delete()
self.encoded_next_uri: str = encode_next(NEXT_URI)
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")
@ -1340,7 +1363,8 @@ class TransferJournalEntryTestCase(unittest.TestCase):
data=update_form) data=update_form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{journal_entry_id}?next=%2F_next") f"{PREFIX}/{journal_entry_id}?"
f"next={self.encoded_next_uri}")
response = self.client.post(f"{PREFIX}/{journal_entry_id}/delete", response = self.client.post(f"{PREFIX}/{journal_entry_id}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.csrf_token})
@ -1353,7 +1377,8 @@ class TransferJournalEntryTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import JournalEntry, JournalEntryCurrency from accounting.models import JournalEntry, JournalEntryCurrency
create_uri: str = f"{PREFIX}/create/transfer?next=%2F_next" create_uri: str = (f"{PREFIX}/create/transfer?"
f"next={self.encoded_next_uri}")
store_uri: str = f"{PREFIX}/store/transfer" store_uri: str = f"{PREFIX}/store/transfer"
response: httpx.Response response: httpx.Response
form: dict[str, str] form: dict[str, str]
@ -1548,8 +1573,10 @@ class TransferJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry, JournalEntryCurrency from accounting.models import JournalEntry, JournalEntryCurrency
journal_entry_id: int \ journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
edit_uri: str = f"{PREFIX}/{journal_entry_id}/edit?next=%2F_next" f"next={self.encoded_next_uri}")
edit_uri: str = (f"{PREFIX}/{journal_entry_id}/edit?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_id}/update" update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
form_0: dict[str, str] = self.__get_update_form(journal_entry_id) form_0: dict[str, str] = self.__get_update_form(journal_entry_id)
@ -1758,7 +1785,8 @@ class TransferJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry from accounting.models import JournalEntry
journal_entry_id: int \ journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_id}/update" update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
journal_entry: JournalEntry journal_entry: JournalEntry
response: httpx.Response response: httpx.Response
@ -1797,7 +1825,8 @@ class TransferJournalEntryTestCase(unittest.TestCase):
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
editor_username, admin_username = "editor", "admin" editor_username, admin_username = "editor", "admin"
client, csrf_token = get_client(self.app, admin_username) client, csrf_token = get_client(self.app, admin_username)
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_id}/update" update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
journal_entry: JournalEntry journal_entry: JournalEntry
response: httpx.Response response: httpx.Response
@ -1831,7 +1860,8 @@ class TransferJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry, JournalEntryCurrency from accounting.models import JournalEntry, JournalEntryCurrency
journal_entry_id: int \ journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_id}/update?as=receipt" update_uri: str = f"{PREFIX}/{journal_entry_id}/update?as=receipt"
form_0: dict[str, str] = self.__get_update_form(journal_entry_id) form_0: dict[str, str] = self.__get_update_form(journal_entry_id)
form_0 = {x: form_0[x] for x in form_0 if "-debit-" not in x} form_0 = {x: form_0[x] for x in form_0 if "-debit-" not in x}
@ -1932,7 +1962,8 @@ class TransferJournalEntryTestCase(unittest.TestCase):
from accounting.models import JournalEntry, JournalEntryCurrency from accounting.models import JournalEntry, JournalEntryCurrency
journal_entry_id: int \ journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_id}/update?as=disbursement" update_uri: str = f"{PREFIX}/{journal_entry_id}/update?as=disbursement"
form_0: dict[str, str] = self.__get_update_form(journal_entry_id) form_0: dict[str, str] = self.__get_update_form(journal_entry_id)
form_0 = {x: form_0[x] for x in form_0 if "-credit-" not in x} form_0 = {x: form_0[x] for x in form_0 if "-credit-" not in x}
@ -2035,7 +2066,8 @@ class TransferJournalEntryTestCase(unittest.TestCase):
""" """
journal_entry_id: int \ journal_entry_id: int \
= add_journal_entry(self.client, self.__get_add_form()) = add_journal_entry(self.client, self.__get_add_form())
detail_uri: str = f"{PREFIX}/{journal_entry_id}?next=%2F_next" detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
f"next={self.encoded_next_uri}")
delete_uri: str = f"{PREFIX}/{journal_entry_id}/delete" delete_uri: str = f"{PREFIX}/{journal_entry_id}/delete"
response: httpx.Response response: httpx.Response
@ -2043,7 +2075,7 @@ class TransferJournalEntryTestCase(unittest.TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(delete_uri, response = self.client.post(delete_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
@ -2051,7 +2083,7 @@ class TransferJournalEntryTestCase(unittest.TestCase):
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
response = self.client.post(delete_uri, response = self.client.post(delete_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def __get_add_form(self) -> dict[str, str]: def __get_add_form(self) -> dict[str, str]:
@ -2059,7 +2091,7 @@ class TransferJournalEntryTestCase(unittest.TestCase):
:return: The form data to add a new journal entry. :return: The form data to add a new journal entry.
""" """
return get_add_form(self.csrf_token) return get_add_form(self.csrf_token, self.encoded_next_uri)
def __get_unchanged_update_form(self, journal_entry_id: int) \ def __get_unchanged_update_form(self, journal_entry_id: int) \
-> dict[str, str]: -> dict[str, str]:
@ -2071,7 +2103,7 @@ class TransferJournalEntryTestCase(unittest.TestCase):
not changed. not changed.
""" """
return get_unchanged_update_form( return get_unchanged_update_form(
journal_entry_id, self.app, self.csrf_token) journal_entry_id, self.app, self.csrf_token, self.encoded_next_uri)
def __get_update_form(self, journal_entry_id: int) -> dict[str, str]: def __get_update_form(self, journal_entry_id: int) -> dict[str, str]:
"""Returns the form data to update a journal entry, where the data are """Returns the form data to update a journal entry, where the data are
@ -2081,8 +2113,9 @@ class TransferJournalEntryTestCase(unittest.TestCase):
:return: The form data to update the journal entry, where the data are :return: The form data to update the journal entry, where the data are
changed. changed.
""" """
return get_update_form(journal_entry_id, return get_update_form(
self.app, self.csrf_token, None) journal_entry_id, self.app, self.csrf_token, self.encoded_next_uri,
None)
class JournalEntryReorderTestCase(unittest.TestCase): class JournalEntryReorderTestCase(unittest.TestCase):
@ -2100,6 +2133,7 @@ class JournalEntryReorderTestCase(unittest.TestCase):
from accounting.models import JournalEntry, JournalEntryLineItem from accounting.models import JournalEntry, JournalEntryLineItem
JournalEntry.query.delete() JournalEntry.query.delete()
JournalEntryLineItem.query.delete() JournalEntryLineItem.query.delete()
self.encoded_next_uri: str = encode_next(NEXT_URI)
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")
@ -2147,13 +2181,13 @@ class JournalEntryReorderTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/{id_2}/update", data=form) response = self.client.post(f"{PREFIX}/{id_2}/update", data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{id_2}?next=%2F_next") f"{PREFIX}/{id_2}?next={self.encoded_next_uri}")
with self.app.app_context(): with self.app.app_context():
self.assertEqual(db.session.get(JournalEntry, id_1).no, 1) self.assertEqual(db.session.get(JournalEntry, id_1).no, 1)
self.assertEqual(db.session.get(JournalEntry, id_2).no, 3) self.assertEqual(db.session.get(JournalEntry, id_2).no, 3)
self.assertEqual(db.session.get(JournalEntry, id_3).no, 2) self.assertEqual(db.session.get(JournalEntry, id_3).no, 2)
self.assertEqual( db.session.get(JournalEntry, id_4).no, 1) self.assertEqual(db.session.get(JournalEntry, id_4).no, 1)
self.assertEqual(db.session.get(JournalEntry, id_5).no, 2) self.assertEqual(db.session.get(JournalEntry, id_5).no, 2)
def test_reorder(self) -> None: def test_reorder(self) -> None:
@ -2181,7 +2215,7 @@ class JournalEntryReorderTestCase(unittest.TestCase):
response = self.client.post( response = self.client.post(
f"{PREFIX}/dates/{date.isoformat()}", f"{PREFIX}/dates/{date.isoformat()}",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI, "next": self.encoded_next_uri,
f"{id_1}-no": "4", f"{id_1}-no": "4",
f"{id_2}-no": "1", f"{id_2}-no": "1",
f"{id_3}-no": "5", f"{id_3}-no": "5",
@ -2209,7 +2243,7 @@ class JournalEntryReorderTestCase(unittest.TestCase):
response = self.client.post( response = self.client.post(
f"{PREFIX}/dates/{date.isoformat()}", f"{PREFIX}/dates/{date.isoformat()}",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI, "next": self.encoded_next_uri,
f"{id_2}-no": "3a", f"{id_2}-no": "3a",
f"{id_3}-no": "5", f"{id_3}-no": "5",
f"{id_4}-no": "2"}) f"{id_4}-no": "2"})
@ -2228,7 +2262,8 @@ class JournalEntryReorderTestCase(unittest.TestCase):
:return: The form data to add a new cash receipt journal entry. :return: The form data to add a new cash receipt journal entry.
""" """
form: dict[str, str] = get_add_form(self.csrf_token) form: dict[str, str] = get_add_form(self.csrf_token,
self.encoded_next_uri)
form = {x: form[x] for x in form if "-debit-" not in x} form = {x: form[x] for x in form if "-debit-" not in x}
return form return form
@ -2237,7 +2272,8 @@ class JournalEntryReorderTestCase(unittest.TestCase):
:return: The form data to add a new cash disbursement journal entry. :return: The form data to add a new cash disbursement journal entry.
""" """
form: dict[str, str] = get_add_form(self.csrf_token) form: dict[str, str] = get_add_form(self.csrf_token,
self.encoded_next_uri)
form = {x: form[x] for x in form if "-credit-" not in x} form = {x: form[x] for x in form if "-credit-" not in x}
return form return form
@ -2251,7 +2287,7 @@ class JournalEntryReorderTestCase(unittest.TestCase):
where the data are not changed. where the data are not changed.
""" """
form: dict[str, str] = get_unchanged_update_form( form: dict[str, str] = get_unchanged_update_form(
journal_entry_id, self.app, self.csrf_token) journal_entry_id, self.app, self.csrf_token, self.encoded_next_uri)
form = {x: form[x] for x in form if "-credit-" not in x} form = {x: form[x] for x in form if "-credit-" not in x}
return form return form
@ -2260,4 +2296,4 @@ class JournalEntryReorderTestCase(unittest.TestCase):
:return: The form data to add a new journal entry. :return: The form data to add a new journal entry.
""" """
return get_add_form(self.csrf_token) return get_add_form(self.csrf_token, self.encoded_next_uri)

View File

@ -25,6 +25,7 @@ from decimal import Decimal
import httpx import httpx
from flask import Flask from flask import Flask
from accounting.utils.next_uri import encode_next
from test_site import db from test_site import db
from test_site.lib import JournalEntryLineItemData, JournalEntryCurrencyData, \ from test_site.lib import JournalEntryLineItemData, JournalEntryCurrencyData, \
JournalEntryData, BaseTestData JournalEntryData, BaseTestData
@ -50,6 +51,7 @@ class OffsetTestCase(unittest.TestCase):
from accounting.models import JournalEntry, JournalEntryLineItem from accounting.models import JournalEntry, JournalEntryLineItem
JournalEntry.query.delete() JournalEntry.query.delete()
JournalEntryLineItem.query.delete() JournalEntryLineItem.query.delete()
self.encoded_next_uri: str = encode_next(NEXT_URI)
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.app, "editor") self.data: OffsetTestData = OffsetTestData(self.app, "editor")
@ -61,7 +63,8 @@ class OffsetTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import Account, JournalEntry from accounting.models import Account, JournalEntry
create_uri: str = f"{PREFIX}/create/receipt?next=%2F_next" create_uri: str = (f"{PREFIX}/create/receipt?"
f"next={self.encoded_next_uri}")
store_uri: str = f"{PREFIX}/store/receipt" store_uri: str = f"{PREFIX}/store/receipt"
form: dict[str, str] form: dict[str, str]
old_amount: Decimal old_amount: Decimal
@ -85,14 +88,16 @@ class OffsetTestCase(unittest.TestCase):
original_line_item=self.data.l_r_or3d)])]) original_line_item=self.data.l_r_or3d)])])
# Non-existing original line item ID # Non-existing original line item ID
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-credit-1-original_line_item_id"] = "9999" form["currency-1-credit-1-original_line_item_id"] = "9999"
response = self.client.post(store_uri, data=form) response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# The same debit or credit # The same debit or credit
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
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) = str(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
@ -108,7 +113,8 @@ class OffsetTestCase(unittest.TestCase):
db.session.commit() db.session.commit()
response = self.client.post( response = self.client.post(
store_uri, store_uri,
data=journal_entry_data.new_form(self.csrf_token, NEXT_URI)) data=journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
with self.app.app_context(): with self.app.app_context():
@ -117,7 +123,8 @@ class OffsetTestCase(unittest.TestCase):
db.session.commit() db.session.commit()
# The original line item is also an offset # The original line item is also an offset
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
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) = str(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
@ -126,21 +133,24 @@ class OffsetTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Not the same currency # Not the same currency
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-code"] = "EUR" form["currency-1-code"] = "EUR"
response = self.client.post(store_uri, data=form) response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Not the same account # Not the same account
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
response = self.client.post(store_uri, data=form) response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Not exceeding net balance - partially offset # Not exceeding net balance - partially offset
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-credit-1-amount"] \ form["currency-1-credit-1-amount"] \
= str(journal_entry_data.currencies[0].credit[0].amount = str(journal_entry_data.currencies[0].credit[0].amount
+ Decimal("0.01")) + Decimal("0.01"))
@ -149,7 +159,8 @@ class OffsetTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Not exceeding net balance - unmatched # Not exceeding net balance - unmatched
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-credit-3-amount"] \ form["currency-1-credit-3-amount"] \
= str(journal_entry_data.currencies[0].credit[2].amount = str(journal_entry_data.currencies[0].credit[2].amount
+ Decimal("0.01")) + Decimal("0.01"))
@ -160,14 +171,16 @@ class OffsetTestCase(unittest.TestCase):
# Not before the original line items # Not before the original line items
old_days = journal_entry_data.days old_days = journal_entry_data.days
journal_entry_data.days = old_days + 1 journal_entry_data.days = old_days + 1
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
response = self.client.post(store_uri, data=form) response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
journal_entry_data.days = old_days journal_entry_data.days = old_days
# Success # Success
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
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)
journal_entry_id: int \ journal_entry_id: int \
@ -184,7 +197,8 @@ class OffsetTestCase(unittest.TestCase):
""" """
from accounting.models import Account from accounting.models import Account
journal_entry_data: JournalEntryData = self.data.j_r_of2 journal_entry_data: JournalEntryData = self.data.j_r_of2
edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next" edit_uri: str = (f"{PREFIX}/{journal_entry_data.id}/edit?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update" update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
form: dict[str, str] form: dict[str, str]
response: httpx.Response response: httpx.Response
@ -196,14 +210,16 @@ class OffsetTestCase(unittest.TestCase):
journal_entry_data.currencies[0].credit[2].amount = Decimal("600") journal_entry_data.currencies[0].credit[2].amount = Decimal("600")
# Non-existing original line item ID # Non-existing original line item ID
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-credit-1-original_line_item_id"] = "9999" form["currency-1-credit-1-original_line_item_id"] = "9999"
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# The same debit or credit # The same debit or credit
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
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) = str(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
@ -220,7 +236,8 @@ class OffsetTestCase(unittest.TestCase):
db.session.commit() db.session.commit()
response = self.client.post( response = self.client.post(
update_uri, update_uri,
data=journal_entry_data.update_form(self.csrf_token, NEXT_URI)) data=journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
with self.app.app_context(): with self.app.app_context():
@ -229,7 +246,8 @@ class OffsetTestCase(unittest.TestCase):
db.session.commit() db.session.commit()
# The original line item is also an offset # The original line item is also an offset
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
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) = str(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
@ -238,21 +256,24 @@ class OffsetTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not the same currency # Not the same currency
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-code"] = "EUR" form["currency-1-code"] = "EUR"
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not the same account # Not the same account
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not exceeding net balance - partially offset # Not exceeding net balance - partially offset
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-1-amount"] \ form["currency-1-debit-1-amount"] \
= str(journal_entry_data.currencies[0].debit[0].amount = str(journal_entry_data.currencies[0].debit[0].amount
+ Decimal("0.01")) + Decimal("0.01"))
@ -264,7 +285,8 @@ class OffsetTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not exceeding net balance - unmatched # Not exceeding net balance - unmatched
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-3-amount"] \ form["currency-1-debit-3-amount"] \
= str(journal_entry_data.currencies[0].debit[2].amount = str(journal_entry_data.currencies[0].debit[2].amount
+ Decimal("0.01")) + Decimal("0.01"))
@ -278,18 +300,21 @@ class OffsetTestCase(unittest.TestCase):
# Not before the original line items # Not before the original line items
old_days: int = journal_entry_data.days old_days: int = journal_entry_data.days
journal_entry_data.days = old_days + 1 journal_entry_data.days = old_days + 1
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
journal_entry_data.days = old_days journal_entry_data.days = old_days
# Success # Success
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{journal_entry_data.id}?next=%2F_next") f"{PREFIX}/{journal_entry_data.id}?"
f"next={self.encoded_next_uri}")
def test_edit_receivable_original_line_item(self) -> None: def test_edit_receivable_original_line_item(self) -> None:
"""Tests to edit the receivable original line item. """Tests to edit the receivable original line item.
@ -298,7 +323,8 @@ class OffsetTestCase(unittest.TestCase):
""" """
from accounting.models import JournalEntry from accounting.models import JournalEntry
journal_entry_data: JournalEntryData = self.data.j_r_or1 journal_entry_data: JournalEntryData = self.data.j_r_or1
edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next" edit_uri: str = (f"{PREFIX}/{journal_entry_data.id}/edit?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update" update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
form: dict[str, str] form: dict[str, str]
response: httpx.Response response: httpx.Response
@ -310,21 +336,24 @@ class OffsetTestCase(unittest.TestCase):
journal_entry_data.currencies[0].credit[1].amount = Decimal("3.4") journal_entry_data.currencies[0].credit[1].amount = Decimal("3.4")
# Not the same currency # Not the same currency
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-code"] = "EUR" form["currency-1-code"] = "EUR"
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not the same account # Not the same account
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-1-account_code"] = Accounts.NOTES_RECEIVABLE form["currency-1-debit-1-account_code"] = Accounts.NOTES_RECEIVABLE
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not less than offset total - partially offset # Not less than offset total - partially offset
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-1-amount"] \ form["currency-1-debit-1-amount"] \
= str(journal_entry_data.currencies[0].debit[0].amount = str(journal_entry_data.currencies[0].debit[0].amount
- Decimal("0.01")) - Decimal("0.01"))
@ -336,7 +365,8 @@ class OffsetTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not less than offset total - fully offset # Not less than offset total - fully offset
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-2-amount"] \ form["currency-1-debit-2-amount"] \
= str(journal_entry_data.currencies[0].debit[1].amount = str(journal_entry_data.currencies[0].debit[1].amount
- Decimal("0.01")) - Decimal("0.01"))
@ -350,25 +380,29 @@ class OffsetTestCase(unittest.TestCase):
# Not after the offset items # Not after the offset items
old_days: int = journal_entry_data.days old_days: int = journal_entry_data.days
journal_entry_data.days = old_days - 1 journal_entry_data.days = old_days - 1
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
journal_entry_data.days = old_days journal_entry_data.days = old_days
# Not deleting matched original line items # Not deleting matched original line items
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
del form["currency-1-debit-1-id"] del form["currency-1-debit-1-id"]
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Success # Success
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{journal_entry_data.id}?next=%2F_next") f"{PREFIX}/{journal_entry_data.id}?"
f"next={self.encoded_next_uri}")
# The original line item is always before the offset item, even when # The original line item is always before the offset item, even when
# they happen in the same day. # they happen in the same day.
@ -388,7 +422,8 @@ class OffsetTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import Account, JournalEntry from accounting.models import Account, JournalEntry
create_uri: str = f"{PREFIX}/create/disbursement?next=%2F_next" create_uri: str = (f"{PREFIX}/create/disbursement?"
f"next={self.encoded_next_uri}")
store_uri: str = f"{PREFIX}/store/disbursement" store_uri: str = f"{PREFIX}/store/disbursement"
form: dict[str, str] form: dict[str, str]
response: httpx.Response response: httpx.Response
@ -411,14 +446,16 @@ class OffsetTestCase(unittest.TestCase):
[])]) [])])
# Non-existing original line item ID # Non-existing original line item ID
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-1-original_line_item_id"] = "9999" form["currency-1-debit-1-original_line_item_id"] = "9999"
response = self.client.post(store_uri, data=form) response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# The same debit or credit # The same debit or credit
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
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) = str(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
@ -434,7 +471,8 @@ class OffsetTestCase(unittest.TestCase):
db.session.commit() db.session.commit()
response = self.client.post( response = self.client.post(
store_uri, store_uri,
data=journal_entry_data.new_form(self.csrf_token, NEXT_URI)) data=journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
with self.app.app_context(): with self.app.app_context():
@ -443,7 +481,8 @@ class OffsetTestCase(unittest.TestCase):
db.session.commit() db.session.commit()
# The original line item is also an offset # The original line item is also an offset
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
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) = str(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
@ -452,21 +491,24 @@ class OffsetTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Not the same currency # Not the same currency
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-code"] = "EUR" form["currency-1-code"] = "EUR"
response = self.client.post(store_uri, data=form) response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Not the same account # Not the same account
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
response = self.client.post(store_uri, data=form) response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Not exceeding net balance - partially offset # Not exceeding net balance - partially offset
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-1-amount"] \ form["currency-1-debit-1-amount"] \
= str(journal_entry_data.currencies[0].debit[0].amount = str(journal_entry_data.currencies[0].debit[0].amount
+ Decimal("0.01")) + Decimal("0.01"))
@ -475,7 +517,8 @@ class OffsetTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Not exceeding net balance - unmatched # Not exceeding net balance - unmatched
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-3-amount"] \ form["currency-1-debit-3-amount"] \
= str(journal_entry_data.currencies[0].debit[2].amount = str(journal_entry_data.currencies[0].debit[2].amount
+ Decimal("0.01")) + Decimal("0.01"))
@ -486,14 +529,16 @@ class OffsetTestCase(unittest.TestCase):
# Not before the original line items # Not before the original line items
old_days: int = journal_entry_data.days old_days: int = journal_entry_data.days
journal_entry_data.days = old_days + 1 journal_entry_data.days = old_days + 1
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
response = self.client.post(store_uri, data=form) response = self.client.post(store_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
journal_entry_data.days = old_days journal_entry_data.days = old_days
# Success # Success
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI) form = journal_entry_data.new_form(self.csrf_token,
self.encoded_next_uri)
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)
journal_entry_id: int \ journal_entry_id: int \
@ -510,7 +555,8 @@ class OffsetTestCase(unittest.TestCase):
""" """
from accounting.models import Account, JournalEntry from accounting.models import Account, JournalEntry
journal_entry_data: JournalEntryData = self.data.j_p_of2 journal_entry_data: JournalEntryData = self.data.j_p_of2
edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next" edit_uri: str = (f"{PREFIX}/{journal_entry_data.id}/edit?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update" update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
form: dict[str, str] form: dict[str, str]
response: httpx.Response response: httpx.Response
@ -522,14 +568,16 @@ class OffsetTestCase(unittest.TestCase):
journal_entry_data.currencies[0].credit[2].amount = Decimal("900") journal_entry_data.currencies[0].credit[2].amount = Decimal("900")
# Non-existing original line item ID # Non-existing original line item ID
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-1-original_line_item_id"] = "9999" form["currency-1-debit-1-original_line_item_id"] = "9999"
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# The same debit or credit # The same debit or credit
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
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) = str(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
@ -546,7 +594,8 @@ class OffsetTestCase(unittest.TestCase):
db.session.commit() db.session.commit()
response = self.client.post( response = self.client.post(
update_uri, update_uri,
data=journal_entry_data.update_form(self.csrf_token, NEXT_URI)) data=journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
with self.app.app_context(): with self.app.app_context():
@ -555,7 +604,8 @@ class OffsetTestCase(unittest.TestCase):
db.session.commit() db.session.commit()
# The original line item is also an offset # The original line item is also an offset
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
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) = str(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
@ -564,21 +614,24 @@ class OffsetTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not the same currency # Not the same currency
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-code"] = "EUR" form["currency-1-code"] = "EUR"
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not the same account # Not the same account
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not exceeding net balance - partially offset # Not exceeding net balance - partially offset
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-1-amount"] \ form["currency-1-debit-1-amount"] \
= str(journal_entry_data.currencies[0].debit[0].amount = str(journal_entry_data.currencies[0].debit[0].amount
+ Decimal("0.01")) + Decimal("0.01"))
@ -590,7 +643,8 @@ class OffsetTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not exceeding net balance - unmatched # Not exceeding net balance - unmatched
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-3-amount"] \ form["currency-1-debit-3-amount"] \
= str(journal_entry_data.currencies[0].debit[2].amount = str(journal_entry_data.currencies[0].debit[2].amount
+ Decimal("0.01")) + Decimal("0.01"))
@ -604,14 +658,16 @@ class OffsetTestCase(unittest.TestCase):
# Not before the original line items # Not before the original line items
old_days: int = journal_entry_data.days old_days: int = journal_entry_data.days
journal_entry_data.days = old_days + 1 journal_entry_data.days = old_days + 1
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
journal_entry_data.days = old_days journal_entry_data.days = old_days
# Success # Success
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
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)
journal_entry_id: int \ journal_entry_id: int \
@ -628,7 +684,8 @@ class OffsetTestCase(unittest.TestCase):
""" """
from accounting.models import JournalEntry from accounting.models import JournalEntry
journal_entry_data: JournalEntryData = self.data.j_p_or1 journal_entry_data: JournalEntryData = self.data.j_p_or1
edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next" edit_uri: str = (f"{PREFIX}/{journal_entry_data.id}/edit?"
f"next={self.encoded_next_uri}")
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update" update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
form: dict[str, str] form: dict[str, str]
response: httpx.Response response: httpx.Response
@ -640,21 +697,24 @@ class OffsetTestCase(unittest.TestCase):
journal_entry_data.currencies[0].credit[1].amount = Decimal("0.9") journal_entry_data.currencies[0].credit[1].amount = Decimal("0.9")
# Not the same currency # Not the same currency
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-code"] = "EUR" form["currency-1-code"] = "EUR"
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not the same account # Not the same account
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-credit-1-account_code"] = Accounts.NOTES_PAYABLE form["currency-1-credit-1-account_code"] = Accounts.NOTES_PAYABLE
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not less than offset total - partially offset # Not less than offset total - partially offset
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-1-amount"] \ form["currency-1-debit-1-amount"] \
= str(journal_entry_data.currencies[0].debit[0].amount = str(journal_entry_data.currencies[0].debit[0].amount
- Decimal("0.01")) - Decimal("0.01"))
@ -666,7 +726,8 @@ class OffsetTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not less than offset total - fully offset # Not less than offset total - fully offset
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
form["currency-1-debit-2-amount"] \ form["currency-1-debit-2-amount"] \
= str(journal_entry_data.currencies[0].debit[1].amount = str(journal_entry_data.currencies[0].debit[1].amount
- Decimal("0.01")) - Decimal("0.01"))
@ -680,25 +741,29 @@ class OffsetTestCase(unittest.TestCase):
# Not after the offset items # Not after the offset items
old_days: int = journal_entry_data.days old_days: int = journal_entry_data.days
journal_entry_data.days = old_days - 1 journal_entry_data.days = old_days - 1
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
journal_entry_data.days = old_days journal_entry_data.days = old_days
# Not deleting matched original line items # Not deleting matched original line items
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
del form["currency-1-credit-1-id"] del form["currency-1-credit-1-id"]
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Success # Success
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI) form = journal_entry_data.update_form(self.csrf_token,
self.encoded_next_uri)
response = self.client.post(update_uri, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{journal_entry_data.id}?next=%2F_next") f"{PREFIX}/{journal_entry_data.id}?"
f"next={self.encoded_next_uri}")
# The original line item is always before the offset item, even when # The original line item is always before the offset item, even when
# they happen in the same day # they happen in the same day

View File

@ -23,17 +23,12 @@ import unittest
import httpx import httpx
from flask import Flask from flask import Flask
from accounting.utils.next_uri import encode_next
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
PREFIX: str = "/accounting/options" PREFIX: str = "/accounting/options"
"""The URL prefix for the option management.""" """The URL prefix for the option management."""
DETAIL_URI: str = f"{PREFIX}?next=%2F_next"
"""THE URI for the option detail."""
EDIT_URI: str = f"{PREFIX}/edit?next=%2F_next"
"""THE URI for the form to edit the options."""
UPDATE_URI: str = f"{PREFIX}/update"
"""THE URI to update the options."""
class OptionTestCase(unittest.TestCase): class OptionTestCase(unittest.TestCase):
@ -50,6 +45,7 @@ class OptionTestCase(unittest.TestCase):
with self.app.app_context(): with self.app.app_context():
from accounting.models import Option from accounting.models import Option
Option.query.delete() Option.query.delete()
self.encoded_next_uri: str = encode_next(NEXT_URI)
self.client, self.csrf_token = get_client(self.app, "admin") self.client, self.csrf_token = get_client(self.app, "admin")
@ -59,15 +55,18 @@ class OptionTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "nobody") client, csrf_token = get_client(self.app, "nobody")
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}"
edit_uri: str = f"{PREFIX}/edit?next={self.encoded_next_uri}"
update_uri: str = f"{PREFIX}/update"
response: httpx.Response response: httpx.Response
response = client.get(DETAIL_URI) response = client.get(detail_uri)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.get(EDIT_URI) response = client.get(edit_uri)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token)) response = client.post(update_uri, data=self.__get_form(csrf_token))
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_viewer(self) -> None: def test_viewer(self) -> None:
@ -76,15 +75,18 @@ class OptionTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "viewer") client, csrf_token = get_client(self.app, "viewer")
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}"
edit_uri: str = f"{PREFIX}/edit?next={self.encoded_next_uri}"
update_uri: str = f"{PREFIX}/update"
response: httpx.Response response: httpx.Response
response = client.get(DETAIL_URI) response = client.get(detail_uri)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.get(EDIT_URI) response = client.get(edit_uri)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token)) response = client.post(update_uri, data=self.__get_form(csrf_token))
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_editor(self) -> None: def test_editor(self) -> None:
@ -93,15 +95,18 @@ class OptionTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "editor") client, csrf_token = get_client(self.app, "editor")
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}"
edit_uri: str = f"{PREFIX}/edit?next={self.encoded_next_uri}"
update_uri: str = f"{PREFIX}/update"
response: httpx.Response response: httpx.Response
response = client.get(DETAIL_URI) response = client.get(detail_uri)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.get(EDIT_URI) response = client.get(edit_uri)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token)) response = client.post(update_uri, data=self.__get_form(csrf_token))
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_admin(self) -> None: def test_admin(self) -> None:
@ -109,17 +114,20 @@ class OptionTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}"
edit_uri: str = f"{PREFIX}/edit?next={self.encoded_next_uri}"
update_uri: str = f"{PREFIX}/update"
response: httpx.Response response: httpx.Response
response = self.client.get(DETAIL_URI) response = self.client.get(detail_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(EDIT_URI) response = self.client.get(edit_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(UPDATE_URI, data=self.__get_form()) response = self.client.post(update_uri, data=self.__get_form())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI) self.assertEqual(response.headers["Location"], detail_uri)
def test_set(self) -> None: def test_set(self) -> None:
"""Test to set the options. """Test to set the options.
@ -127,59 +135,62 @@ class OptionTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.utils.options import options from accounting.utils.options import options
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}"
edit_uri: str = f"{PREFIX}/edit?next={self.encoded_next_uri}"
update_uri: str = f"{PREFIX}/update"
form: dict[str, str] form: dict[str, str]
response: httpx.Response response: httpx.Response
# Empty currency code # Empty currency code
form = self.__get_form() form = self.__get_form()
form["default_currency_code"] = " " form["default_currency_code"] = " "
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI) self.assertEqual(response.headers["Location"], edit_uri)
# Non-existing currency code # Non-existing currency code
form = self.__get_form() form = self.__get_form()
form["default_currency_code"] = "ZZZ" form["default_currency_code"] = "ZZZ"
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI) self.assertEqual(response.headers["Location"], edit_uri)
# Empty current account # Empty current account
form = self.__get_form() form = self.__get_form()
form["default_ie_account_code"] = " " form["default_ie_account_code"] = " "
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI) self.assertEqual(response.headers["Location"], edit_uri)
# Non-existing current account # Non-existing current account
form = self.__get_form() form = self.__get_form()
form["default_ie_account_code"] = "9999-999" form["default_ie_account_code"] = "9999-999"
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI) self.assertEqual(response.headers["Location"], edit_uri)
# Not a current account # Not a current account
form = self.__get_form() form = self.__get_form()
form["default_ie_account_code"] = Accounts.MEAL form["default_ie_account_code"] = Accounts.MEAL
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI) self.assertEqual(response.headers["Location"], edit_uri)
# Recurring item name empty # Recurring item name empty
form = self.__get_form() form = self.__get_form()
key = [x for x in form if x.endswith("-name")][0] key = [x for x in form if x.endswith("-name")][0]
form[key] = " " form[key] = " "
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI) self.assertEqual(response.headers["Location"], edit_uri)
# Recurring item account empty # Recurring item account empty
form = self.__get_form() form = self.__get_form()
key = [x for x in form if x.endswith("-account_code")][0] key = [x for x in form if x.endswith("-account_code")][0]
form[key] = " " form[key] = " "
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI) self.assertEqual(response.headers["Location"], edit_uri)
# Recurring item non-expense account # Recurring item non-expense account
form = self.__get_form() form = self.__get_form()
@ -187,9 +198,9 @@ class OptionTestCase(unittest.TestCase):
if x.startswith("recurring-expense-") if x.startswith("recurring-expense-")
and x.endswith("-account_code")][0] and x.endswith("-account_code")][0]
form[key] = Accounts.SERVICE form[key] = Accounts.SERVICE
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI) self.assertEqual(response.headers["Location"], edit_uri)
# Recurring item non-income account # Recurring item non-income account
form = self.__get_form() form = self.__get_form()
@ -197,9 +208,9 @@ class OptionTestCase(unittest.TestCase):
if x.startswith("recurring-income-") if x.startswith("recurring-income-")
and x.endswith("-account_code")][0] and x.endswith("-account_code")][0]
form[key] = Accounts.UTILITIES form[key] = Accounts.UTILITIES
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI) self.assertEqual(response.headers["Location"], edit_uri)
# Recurring item payable expense # Recurring item payable expense
form = self.__get_form() form = self.__get_form()
@ -207,9 +218,9 @@ class OptionTestCase(unittest.TestCase):
if x.startswith("recurring-expense-") if x.startswith("recurring-expense-")
and x.endswith("-account_code")][0] and x.endswith("-account_code")][0]
form[key] = Accounts.PAYABLE form[key] = Accounts.PAYABLE
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI) self.assertEqual(response.headers["Location"], edit_uri)
# Recurring item receivable income # Recurring item receivable income
form = self.__get_form() form = self.__get_form()
@ -217,17 +228,17 @@ class OptionTestCase(unittest.TestCase):
if x.startswith("recurring-income-") if x.startswith("recurring-income-")
and x.endswith("-account_code")][0] and x.endswith("-account_code")][0]
form[key] = Accounts.RECEIVABLE form[key] = Accounts.RECEIVABLE
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI) self.assertEqual(response.headers["Location"], edit_uri)
# Recurring item description template empty # Recurring item description template empty
form = self.__get_form() form = self.__get_form()
key = [x for x in form if x.endswith("-description_template")][0] key = [x for x in form if x.endswith("-description_template")][0]
form[key] = " " form[key] = " "
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI) self.assertEqual(response.headers["Location"], edit_uri)
# Success, with malformed order # Success, with malformed order
with self.app.app_context(): with self.app.app_context():
@ -236,9 +247,9 @@ class OptionTestCase(unittest.TestCase):
self.assertEqual(len(options.recurring.expenses), 0) self.assertEqual(len(options.recurring.expenses), 0)
self.assertEqual(len(options.recurring.incomes), 0) self.assertEqual(len(options.recurring.incomes), 0)
response = self.client.post(UPDATE_URI, data=self.__get_form()) response = self.client.post(update_uri, data=self.__get_form())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
self.assertEqual(options.default_currency_code, "EUR") self.assertEqual(options.default_currency_code, "EUR")
@ -261,9 +272,9 @@ class OptionTestCase(unittest.TestCase):
# Success, with no recurring data # Success, with no recurring data
form = self.__get_form() form = self.__get_form()
form = {x: form[x] for x in form if not x.startswith("recurring-")} form = {x: form[x] for x in form if not x.startswith("recurring-")}
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
self.assertEqual(len(options.recurring.expenses), 0) self.assertEqual(len(options.recurring.expenses), 0)
@ -275,13 +286,15 @@ class OptionTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import Option from accounting.models import Option
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}"
update_uri: str = f"{PREFIX}/update"
form: dict[str, str] form: dict[str, str]
option: Option | None option: Option | None
resource: httpx.Response resource: httpx.Response
response = self.client.post(UPDATE_URI, data=self.__get_form()) response = self.client.post(update_uri, data=self.__get_form())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
option = db.session.get(Option, "recurring") option = db.session.get(Option, "recurring")
@ -295,9 +308,9 @@ class OptionTestCase(unittest.TestCase):
# The recurring setting was not modified # The recurring setting was not modified
form = self.__get_form() form = self.__get_form()
form["default_currency_code"] = "JPY" form["default_currency_code"] = "JPY"
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
option = db.session.get(Option, "recurring") option = db.session.get(Option, "recurring")
@ -311,9 +324,9 @@ class OptionTestCase(unittest.TestCase):
if x.startswith("recurring-expense-") if x.startswith("recurring-expense-")
and x.endswith("-account_code")][0] and x.endswith("-account_code")][0]
form[key] = Accounts.MEAL form[key] = Accounts.MEAL
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
option = db.session.get(Option, "recurring") option = db.session.get(Option, "recurring")
@ -328,12 +341,14 @@ class OptionTestCase(unittest.TestCase):
from accounting.models import Option from accounting.models import Option
from accounting.utils.user import get_user_pk from accounting.utils.user import get_user_pk
admin_username, editor_username = "admin", "editor" admin_username, editor_username = "admin", "editor"
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}"
update_uri: str = f"{PREFIX}/update"
option: Option | None option: Option | None
response: httpx.Response response: httpx.Response
response = self.client.post(UPDATE_URI, data=self.__get_form()) response = self.client.post(update_uri, data=self.__get_form())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
editor_pk: int = get_user_pk(editor_username) editor_pk: int = get_user_pk(editor_username)
@ -348,9 +363,9 @@ class OptionTestCase(unittest.TestCase):
if x.startswith("recurring-expense-") if x.startswith("recurring-expense-")
and x.endswith("-account_code")][0] and x.endswith("-account_code")][0]
form[key] = Accounts.MEAL form[key] = Accounts.MEAL
response = self.client.post(UPDATE_URI, data=form) response = self.client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.app.app_context():
option = db.session.get(Option, "recurring") option = db.session.get(Option, "recurring")
@ -367,7 +382,7 @@ class OptionTestCase(unittest.TestCase):
if csrf_token is None: if csrf_token is None:
csrf_token = self.csrf_token csrf_token = self.csrf_token
return {"csrf_token": csrf_token, return {"csrf_token": csrf_token,
"next": NEXT_URI, "next": self.encoded_next_uri,
"default_currency_code": "EUR", "default_currency_code": "EUR",
"default_ie_account_code": "0000-000", "default_ie_account_code": "0000-000",
"recurring-expense-1-name": "Water bill", "recurring-expense-1-name": "Water bill",

View File

@ -23,13 +23,15 @@ from typing import Type
from click.testing import Result from click.testing import Result
from flask import Flask, Blueprint, render_template, redirect, Response, \ from flask import Flask, Blueprint, render_template, redirect, Response, \
url_for url_for, request
from flask.testing import FlaskCliRunner from flask.testing import FlaskCliRunner
from flask_babel_js import BabelJS from flask_babel_js import BabelJS
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_wtf import CSRFProtect from flask_wtf import CSRFProtect
from sqlalchemy import Column from sqlalchemy import Column
from accounting.utils.next_uri import encode_next
bp: Blueprint = Blueprint("home", __name__) bp: Blueprint = Blueprint("home", __name__)
"""The global blueprint.""" """The global blueprint."""
babel_js: BabelJS = BabelJS() babel_js: BabelJS = BabelJS()

View File

@ -140,36 +140,38 @@ class JournalEntryData:
for line_item in currency.credit: for line_item in currency.credit:
line_item.journal_entry = self line_item.journal_entry = self
def new_form(self, csrf_token: str, next_uri: str) -> dict[str, str]: def new_form(self, csrf_token: str, encoded_next_uri: str) \
-> dict[str, str]:
"""Returns the journal entry as a creation form. """Returns the journal entry as a creation form.
:param csrf_token: The CSRF token. :param csrf_token: The CSRF token.
:param next_uri: The next URI. :param encoded_next_uri: The encoded next URI.
:return: The journal entry as a creation form. :return: The journal entry as a creation form.
""" """
return self.__form(csrf_token, next_uri, is_update=False) return self.__form(csrf_token, encoded_next_uri, is_update=False)
def update_form(self, csrf_token: str, next_uri: str) -> dict[str, str]: def update_form(self, csrf_token: str, encoded_next_uri: str) \
-> dict[str, str]:
"""Returns the journal entry as an update form. """Returns the journal entry as an update form.
:param csrf_token: The CSRF token. :param csrf_token: The CSRF token.
:param next_uri: The next URI. :param encoded_next_uri: The encoded next URI.
:return: The journal entry as an update form. :return: The journal entry as an update form.
""" """
return self.__form(csrf_token, next_uri, is_update=True) return self.__form(csrf_token, encoded_next_uri, is_update=True)
def __form(self, csrf_token: str, next_uri: str, is_update: bool = False) \ def __form(self, csrf_token: str, encoded_next_uri: str,
-> dict[str, str]: is_update: bool = False) -> dict[str, str]:
"""Returns the journal entry as a form. """Returns the journal entry as a form.
:param csrf_token: The CSRF token. :param csrf_token: The CSRF token.
:param next_uri: The next URI. :param encoded_next_uri: The encoded next URI.
:param is_update: True for an update operation, or False otherwise :param is_update: True for an update operation, or False otherwise
:return: The journal entry as a form. :return: The journal entry as a form.
""" """
date: dt.date = dt.date.today() - dt.timedelta(days=self.days) date: dt.date = dt.date.today() - dt.timedelta(days=self.days)
form: dict[str, str] = {"csrf_token": csrf_token, form: dict[str, str] = {"csrf_token": csrf_token,
"next": next_uri, "next": encoded_next_uri,
"date": date.isoformat()} "date": date.isoformat()}
for i in range(len(self.currencies)): for i in range(len(self.currencies)):
form.update(self.currencies[i].form(i + 1, is_update)) form.update(self.currencies[i].form(i + 1, is_update))

View File

@ -25,9 +25,9 @@ First written: 2023/1/27
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="{{ "imacat" }}" /> <meta name="author" content="{{ "imacat" }}" />
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" crossorigin="anonymous"> <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css" crossorigin="anonymous"> <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css" integrity="sha384-iw3OoTErCYJJB9mCa8LNS2hbsQ7M3C0EpIsO/H5+EGAkPGc6rk+V8i04oW/K5xq0" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.7.7/dist/css/tempus-dominus.min.css" crossorigin="anonymous"> <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.7.7/dist/css/tempus-dominus.min.css" integrity="sha384-l66rSL7gUubrdJxFRbXUo/tO7eNPAcCiZXFs/Xl147146xNqQ1qt4oPW6jlVezsS" crossorigin="anonymous">
{% block styles %}{% endblock %} {% block styles %}{% endblock %}
<script src="{{ url_for("babel_catalog") }}"></script> <script src="{{ url_for("babel_catalog") }}"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
@ -96,7 +96,7 @@ First written: 2023/1/27
</span> </span>
<form action="{{ url_for("locale.set-locale") }}" method="post"> <form action="{{ url_for("locale.set-locale") }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="next" value="{{ request.full_path if request.query_string else request.path }}"> <input type="hidden" name="next" value="{{ accounting_as_next() }}">
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
{% for locale_code, locale_name in get_all_linguas().items() %} {% for locale_code, locale_name in get_all_linguas().items() %}
<li> <li>
@ -121,10 +121,10 @@ First written: 2023/1/27
{% if messages %} {% if messages %}
{% for category, message in messages %} {% for category, message in messages %}
{% if category == "success" %} {% if category == "success" %}
<div class="alert alert-success alert-dismissible fade show" role="alert"> <div class="alert alert-success alert-dismissible fade show" role="alert">
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% elif category == "error" %} {% elif category == "error" %}
<div class="alert alert-danger alert-dismissible fade show" role="alert"> <div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>{{ _("Error:") }}</strong> {{ message }} <strong>{{ _("Error:") }}</strong> {{ message }}

View File

@ -22,6 +22,7 @@ import unittest
import httpx import httpx
from flask import Flask from flask import Flask
from accounting.utils.next_uri import encode_next
from test_site import db from test_site import db
from test_site.lib import JournalEntryCurrencyData, JournalEntryData, \ from test_site.lib import JournalEntryCurrencyData, JournalEntryData, \
BaseTestData BaseTestData
@ -46,6 +47,7 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
from accounting.models import JournalEntry, JournalEntryLineItem from accounting.models import JournalEntry, JournalEntryLineItem
JournalEntry.query.delete() JournalEntry.query.delete()
JournalEntryLineItem.query.delete() JournalEntryLineItem.query.delete()
self.encoded_next_uri: str = encode_next(NEXT_URI)
self.client, self.csrf_token = get_client(self.app, "editor") self.client, self.csrf_token = get_client(self.app, "editor")
@ -60,7 +62,7 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
response = client.post(f"{PREFIX}/{Accounts.PAYABLE}", response = client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_viewer(self) -> None: def test_viewer(self) -> None:
@ -74,7 +76,7 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
response = client.post(f"{PREFIX}/{Accounts.PAYABLE}", response = client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_editor(self) -> None: def test_editor(self) -> None:
@ -87,7 +89,7 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/{Accounts.PAYABLE}", response = self.client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
@ -100,7 +102,7 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
response = self.client.post(f"{PREFIX}/{Accounts.PAYABLE}", response = self.client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
@ -150,7 +152,7 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
match_uri = f"{PREFIX}/{Accounts.RECEIVABLE}" match_uri = f"{PREFIX}/{Accounts.RECEIVABLE}"
response = self.client.post(match_uri, response = self.client.post(match_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
@ -200,7 +202,7 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
match_uri = f"{PREFIX}/{Accounts.PAYABLE}" match_uri = f"{PREFIX}/{Accounts.PAYABLE}"
response = self.client.post(match_uri, response = self.client.post(match_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
@ -278,7 +280,7 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
match_uri = f"{PREFIX}/{Accounts.RECEIVABLE}" match_uri = f"{PREFIX}/{Accounts.RECEIVABLE}"
response = self.client.post(match_uri, response = self.client.post(match_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
@ -344,7 +346,7 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
match_uri = f"{PREFIX}/{Accounts.PAYABLE}" match_uri = f"{PREFIX}/{Accounts.PAYABLE}"
response = self.client.post(match_uri, response = self.client.post(match_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.csrf_token,
"next": NEXT_URI}) "next": self.encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)

View File

@ -22,11 +22,12 @@ from urllib.parse import quote_plus
import httpx import httpx
from flask import Flask, request from flask import Flask, request
from itsdangerous import URLSafeSerializer
from accounting.utils.next_uri import append_next, inherit_next, or_next from accounting.utils.next_uri import append_next, inherit_next, or_next
from accounting.utils.pagination import Pagination, DEFAULT_PAGE_SIZE from accounting.utils.pagination import Pagination, DEFAULT_PAGE_SIZE
from accounting.utils.query import parse_query_keywords from accounting.utils.query import parse_query_keywords
from testlib import TEST_SERVER, create_test_app, get_csrf_token from testlib import TEST_SERVER, create_test_app, get_csrf_token, NEXT_URI
class NextUriTestCase(unittest.TestCase): class NextUriTestCase(unittest.TestCase):
@ -40,6 +41,8 @@ class NextUriTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
self.app: Flask = create_test_app() self.app: Flask = create_test_app()
self.serializer: URLSafeSerializer \
= URLSafeSerializer(self.app.config["SECRET_KEY"])
def test_next_uri(self) -> None: def test_next_uri(self) -> None:
"""Tests the next URI utilities with the next URI. """Tests the next URI utilities with the next URI.
@ -51,12 +54,12 @@ class NextUriTestCase(unittest.TestCase):
current_uri: str = request.full_path if request.query_string \ current_uri: str = request.full_path if request.query_string \
else request.path else request.path
self.assertEqual(append_next(self.TARGET), self.assertEqual(append_next(self.TARGET),
f"{self.TARGET}?next={quote_plus(current_uri)}") f"{self.TARGET}?next={self.__encode(current_uri)}")
next_uri: str = request.form["next"] if request.method == "POST" \ next_uri: str = request.form["next"] if request.method == "POST" \
else request.args["next"] else request.args["next"]
self.assertEqual(inherit_next(self.TARGET), self.assertEqual(inherit_next(self.TARGET),
f"{self.TARGET}?next={quote_plus(next_uri)}") f"{self.TARGET}?next={next_uri}")
self.assertEqual(or_next(self.TARGET), next_uri) self.assertEqual(or_next(self.TARGET), self.__decode(next_uri))
return "" return ""
self.app.add_url_rule("/test-next", view_func=test_next_uri_view, self.app.add_url_rule("/test-next", view_func=test_next_uri_view,
@ -66,10 +69,11 @@ class NextUriTestCase(unittest.TestCase):
csrf_token: str = get_csrf_token(client) csrf_token: str = get_csrf_token(client)
response: httpx.Response response: httpx.Response
response = client.get("/test-next?next=/next&q=abc&page-no=4") encoded_uri: str = self.__encode(NEXT_URI)
response = client.get(f"/test-next?next={encoded_uri}&q=abc&page-no=4")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = client.post("/test-next", data={"csrf_token": csrf_token, response = client.post("/test-next", data={"csrf_token": csrf_token,
"next": "/next", "next": encoded_uri,
"name": "viewer"}) "name": "viewer"})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -80,10 +84,6 @@ class NextUriTestCase(unittest.TestCase):
""" """
def test_no_next_uri_view() -> str: def test_no_next_uri_view() -> str:
"""The test view without the next URI.""" """The test view without the next URI."""
current_uri: str = request.full_path if request.query_string \
else request.path
self.assertEqual(append_next(self.TARGET),
f"{self.TARGET}?next={quote_plus(current_uri)}")
self.assertEqual(inherit_next(self.TARGET), self.TARGET) self.assertEqual(inherit_next(self.TARGET), self.TARGET)
self.assertEqual(or_next(self.TARGET), self.TARGET) self.assertEqual(or_next(self.TARGET), self.TARGET)
return "" return ""
@ -108,10 +108,8 @@ class NextUriTestCase(unittest.TestCase):
""" """
def test_invalid_next_uri_view() -> str: def test_invalid_next_uri_view() -> str:
"""The test view without the next URI.""" """The test view without the next URI."""
self.assertEqual(inherit_next(self.TARGET), self.assertEqual(inherit_next(self.TARGET), self.TARGET)
request.args.get("inherit-expected")) self.assertEqual(or_next(self.TARGET), self.TARGET)
self.assertEqual(or_next(self.TARGET),
request.args.get("or-expected"))
return "" return ""
self.app.add_url_rule("/test-invalid-next", self.app.add_url_rule("/test-invalid-next",
@ -127,33 +125,28 @@ class NextUriTestCase(unittest.TestCase):
# A foreign URI # A foreign URI
next_uri = "https://example.com" next_uri = "https://example.com"
expected1 = self.TARGET response = client.get(f"/test-invalid-next?next={quote_plus(next_uri)}")
expected2 = self.TARGET
response = client.get(f"/test-invalid-next?next={quote_plus(next_uri)}"
f"&inherit-expected={quote_plus(expected1)}"
f"&or-expected={quote_plus(expected2)}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = client.post("/test-invalid-next" response = client.post("/test-invalid-next",
f"?inherit-expected={quote_plus(expected1)}"
f"&or-expected={quote_plus(expected2)}",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"next": next_uri}) "next": next_uri})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# An extremely-long URI to trigger the error def __encode(self, uri: str) -> str:
next_uri = "/" + "x" * 1024 """Encodes the next URI.
expected2 = next_uri[:512]
expected1 = f"{self.TARGET}?next={quote_plus(expected2)}" :param uri: The next URI.
response = client.get(f"/test-invalid-next?next={quote_plus(next_uri)}" :return: The encoded next URI.
f"&inherit-expected={quote_plus(expected1)}" """
f"&or-expected={quote_plus(expected2)}") return self.serializer.dumps(uri, "next")
self.assertEqual(response.status_code, 200)
response = client.post("/test-invalid-next" def __decode(self, uri: str) -> str:
f"?inherit-expected={quote_plus(expected1)}" """Decodes the next URI.
f"&or-expected={quote_plus(expected2)}",
data={"csrf_token": csrf_token, :param uri: The encoded next URI.
"next": next_uri}) :return: The next URI.
self.assertEqual(response.status_code, 200) """
return self.serializer.loads(uri, "next")
class QueryKeywordParserTestCase(unittest.TestCase): class QueryKeywordParserTestCase(unittest.TestCase):

View File

@ -25,6 +25,7 @@ from typing import Literal
import httpx import httpx
from flask import Flask, render_template_string from flask import Flask, render_template_string
from accounting.utils.next_uri import encode_next
from test_site import create_app from test_site import create_app
TEST_SERVER: str = "https://testserver" TEST_SERVER: str = "https://testserver"
@ -71,9 +72,9 @@ def create_test_app() -> Flask:
"""The test view to return the CSRF token.""" """The test view to return the CSRF token."""
return render_template_string("{{csrf_token()}}") return render_template_string("{{csrf_token()}}")
@app.get("/.errors") @app.get("/.messages")
def get_errors_view() -> str: def get_messages_view() -> str:
"""The test view to return the CSRF token.""" """The test view to return the flashed messages."""
return render_template_string("{{get_flashed_messages()|tojson}}") return render_template_string("{{get_flashed_messages()|tojson}}")
return app return app
@ -98,30 +99,35 @@ def get_client(app: Flask, username: str) -> tuple[httpx.Client, str]:
client: httpx.Client = httpx.Client(app=app, base_url=TEST_SERVER) client: httpx.Client = httpx.Client(app=app, base_url=TEST_SERVER)
client.headers["Referer"] = TEST_SERVER client.headers["Referer"] = TEST_SERVER
csrf_token: str = get_csrf_token(client) csrf_token: str = get_csrf_token(client)
with app.app_context():
encoded_next_uri: str = encode_next(NEXT_URI)
response: httpx.Response = client.post("/login", response: httpx.Response = client.post("/login",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"next": "/", "next": encoded_next_uri,
"username": username}) "username": username})
assert response.status_code == 302 assert response.status_code == 302
assert response.headers["Location"] == "/" assert response.headers["Location"] == NEXT_URI
return client, csrf_token return client, csrf_token
def set_locale(client: httpx.Client, csrf_token: str, def set_locale(app: Flask, client: httpx.Client, csrf_token: str,
locale: Literal["en", "zh_Hant", "zh_Hans"]) -> None: locale: Literal["en", "zh_Hant", "zh_Hans"]) -> None:
"""Sets the current locale. """Sets the current locale.
:param app: The Flask application.
:param client: The test client. :param client: The test client.
:param csrf_token: The CSRF token. :param csrf_token: The CSRF token.
:param locale: The locale. :param locale: The locale.
:return: None. :return: None.
""" """
with app.app_context():
encoded_next_uri: str = encode_next(NEXT_URI)
response: httpx.Response = client.post("/locale", response: httpx.Response = client.post("/locale",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"locale": locale, "locale": locale,
"next": "/next"}) "next": encoded_next_uri})
assert response.status_code == 302 assert response.status_code == 302
assert response.headers["Location"] == "/next" assert response.headers["Location"] == NEXT_URI
def add_journal_entry(client: httpx.Client, form: dict[str, str]) -> int: def add_journal_entry(client: httpx.Client, form: dict[str, str]) -> int:
@ -152,6 +158,6 @@ def match_journal_entry_detail(location: str) -> int:
:raise AssertionError: When the location is not the journal entry detail. :raise AssertionError: When the location is not the journal entry detail.
""" """
m: re.Match = re.match( m: re.Match = re.match(
r"^/accounting/journal-entries/(\d+)\?next=%2F_next", location) r"^/accounting/journal-entries/(\d+)\?next=", location)
assert m is not None assert m is not None
return int(m.group(1)) return int(m.group(1))

View File

@ -33,14 +33,15 @@ EMPTY_NOTE: str = " \n\n "
"""The empty note content.""" """The empty note content."""
def get_add_form(csrf_token: str) -> dict[str, str]: def get_add_form(csrf_token: str, encoded_next_uri: str) -> dict[str, str]:
"""Returns the form data to add a new journal entry. """Returns the form data to add a new journal entry.
:param csrf_token: The CSRF token. :param csrf_token: The CSRF token.
:param encoded_next_uri: The encoded next URI.
:return: The form data to add a new journal entry. :return: The form data to add a new journal entry.
""" """
return {"csrf_token": csrf_token, return {"csrf_token": csrf_token,
"next": NEXT_URI, "next": encoded_next_uri,
"date": dt.date.today().isoformat(), "date": dt.date.today().isoformat(),
"currency-0-code": "USD", "currency-0-code": "USD",
"currency-0-debit-0-no": "16", "currency-0-debit-0-no": "16",
@ -102,13 +103,15 @@ def get_add_form(csrf_token: str) -> dict[str, str]:
def get_unchanged_update_form(journal_entry_id: int, app: Flask, def get_unchanged_update_form(journal_entry_id: int, app: Flask,
csrf_token: str) -> dict[str, str]: csrf_token: str, encoded_next_uri: str) \
-> dict[str, str]:
"""Returns the form data to update a journal entry, where the data are not """Returns the form data to update a journal entry, where the data are not
changed. changed.
:param journal_entry_id: The journal entry ID. :param journal_entry_id: The journal entry ID.
:param app: The Flask application. :param app: The Flask application.
:param csrf_token: The CSRF token. :param csrf_token: The CSRF token.
:param encoded_next_uri: The encoded next URI.
:return: The form data to update the journal entry, where the data are not :return: The form data to update the journal entry, where the data are not
changed. changed.
""" """
@ -121,7 +124,7 @@ def get_unchanged_update_form(journal_entry_id: int, app: Flask,
form: dict[str, str] \ form: dict[str, str] \
= {"csrf_token": csrf_token, = {"csrf_token": csrf_token,
"next": NEXT_URI, "next": encoded_next_uri,
"date": journal_entry.date, "date": journal_entry.date,
"note": " \n \n\n " if journal_entry.note is None "note": " \n \n\n " if journal_entry.note is None
else f"\n \n\n \n \n{journal_entry.note} \n\n "} else f"\n \n\n \n \n{journal_entry.note} \n\n "}
@ -182,20 +185,22 @@ def __get_new_index(indices_used: set[int]) -> int:
def get_update_form(journal_entry_id: int, app: Flask, def get_update_form(journal_entry_id: int, app: Flask,
csrf_token: str, is_debit: bool | None) -> dict[str, str]: csrf_token: str, encoded_next_uri: str,
is_debit: bool | None) -> dict[str, str]:
"""Returns the form data to update a journal entry, where the data are """Returns the form data to update a journal entry, where the data are
changed. changed.
:param journal_entry_id: The journal entry ID. :param journal_entry_id: The journal entry ID.
:param app: The Flask application. :param app: The Flask application.
:param csrf_token: The CSRF token. :param csrf_token: The CSRF token.
:param encoded_next_uri: The encoded next URI.
:param is_debit: True for a cash disbursement journal entry, False for a :param is_debit: True for a cash disbursement journal entry, False for a
cash receipt journal entry, or None for a transfer journal entry. cash receipt journal entry, or None for a transfer journal entry.
:return: The form data to update the journal entry, where the data are :return: The form data to update the journal entry, where the data are
changed. changed.
""" """
form: dict[str, str] = get_unchanged_update_form( form: dict[str, str] = get_unchanged_update_form(
journal_entry_id, app, csrf_token) journal_entry_id, app, csrf_token, encoded_next_uri)
# Mess up the line items in a currency # Mess up the line items in a currency
currency_prefix: str = __get_currency_prefix(form, "USD") currency_prefix: str = __get_currency_prefix(form, "USD")