diff --git a/src/accounting/currency/views.py b/src/accounting/currency/views.py index 7abe0d6..56e3a3c 100644 --- a/src/accounting/currency/views.py +++ b/src/accounting/currency/views.py @@ -160,6 +160,9 @@ def delete_currency(currency: Currency) -> redirect: :return: The redirection to the currency list on success, or the currency detail on error. """ + if not currency.can_delete: + flash(s(lazy_gettext("The currency cannot be deleted.")), "error") + return redirect(inherit_next(__get_detail_uri(currency))) currency.delete() db.session.commit() flash(s(lazy_gettext("The currency is deleted successfully.")), "success") diff --git a/src/accounting/models.py b/src/accounting/models.py index 7c8ddab..c447623 100644 --- a/src/accounting/models.py +++ b/src/accounting/models.py @@ -432,6 +432,17 @@ class Currency(db.Model): return True return False + @property + def can_delete(self) -> bool: + """Returns whether the currency can be deleted. + + :return: True if the currency can be deleted, or False otherwise. + """ + from accounting.template_globals import default_currency_code + if self.code == default_currency_code(): + return False + return len(self.line_items) == 0 + def delete(self) -> None: """Deletes the currency. diff --git a/src/accounting/templates/accounting/currency/detail.html b/src/accounting/templates/accounting/currency/detail.html index 5597386..0b3ca6d 100644 --- a/src/accounting/templates/accounting/currency/detail.html +++ b/src/accounting/templates/accounting/currency/detail.html @@ -37,10 +37,17 @@ First written: 2023/2/6 {% endif %} {% if accounting_can_edit() %} - + {% if obj.can_delete %} + + {% else %} + + {% endif %} {% endif %} @@ -52,7 +59,7 @@ First written: 2023/2/6 {% endif %} -{% if accounting_can_edit() %} +{% if accounting_can_edit() and obj.can_delete %}
{% if request.args.next %} diff --git a/tests/test_currency.py b/tests/test_currency.py index e1dacfc..c6a040e 100644 --- a/tests/test_currency.py +++ b/tests/test_currency.py @@ -20,7 +20,7 @@ import csv import typing as t import unittest -from datetime import timedelta +from datetime import timedelta, date import httpx from click.testing import Result @@ -29,6 +29,7 @@ from flask.testing import FlaskCliRunner from test_site import db from testlib import create_test_app, get_client, set_locale +from testlib_journal_entry import add_journal_entry, NEXT_URI class CurrencyData: @@ -46,14 +47,14 @@ class CurrencyData: """The name.""" -zza: CurrencyData = CurrencyData("ZZA", "Testing Dollar #A") -"""The first test currency.""" -zzb: CurrencyData = CurrencyData("ZZB", "Testing Dollar #B") -"""The second test currency.""" -zzc: CurrencyData = CurrencyData("ZZC", "Testing Dollar #C") -"""The third test currency.""" -zzd: CurrencyData = CurrencyData("ZZD", "Testing Dollar #D") -"""The fourth test currency.""" +USD: CurrencyData = CurrencyData("USD", "US Dollar") +"""The US dollars.""" +EUR: CurrencyData = CurrencyData("EUR", "Euro") +"""The European dollars.""" +TWD: CurrencyData = CurrencyData("TWD", "Taiwan dollars") +"""The Taiwan dollars.""" +JPY: CurrencyData = CurrencyData("JPY", "Japanese yen") +"""The Japanese yen.""" PREFIX: str = "/accounting/currencies" """The URL prefix for the currency management.""" @@ -140,17 +141,17 @@ class CurrencyTestCase(unittest.TestCase): response = self.client.post(f"{PREFIX}/store", data={"csrf_token": self.csrf_token, - "code": zza.code, - "name": zza.name}) + "code": USD.code, + "name": USD.name}) self.assertEqual(response.status_code, 302) - self.assertEqual(response.headers["Location"], f"{PREFIX}/{zza.code}") + self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}") response = self.client.post(f"{PREFIX}/store", data={"csrf_token": self.csrf_token, - "code": zzb.code, - "name": zzb.name}) + "code": EUR.code, + "name": EUR.name}) self.assertEqual(response.status_code, 302) - self.assertEqual(response.headers["Location"], f"{PREFIX}/{zzb.code}") + self.assertEqual(response.headers["Location"], f"{PREFIX}/{EUR.code}") def test_nobody(self) -> None: """Test the permission as nobody. @@ -163,7 +164,7 @@ class CurrencyTestCase(unittest.TestCase): response = client.get(PREFIX) self.assertEqual(response.status_code, 403) - response = client.get(f"{PREFIX}/{zza.code}") + response = client.get(f"{PREFIX}/{USD.code}") self.assertEqual(response.status_code, 403) response = client.get(f"{PREFIX}/create") @@ -171,20 +172,20 @@ class CurrencyTestCase(unittest.TestCase): response = client.post(f"{PREFIX}/store", data={"csrf_token": csrf_token, - "code": zzc.code, - "name": zzc.name}) + "code": TWD.code, + "name": TWD.name}) self.assertEqual(response.status_code, 403) - response = client.get(f"{PREFIX}/{zza.code}/edit") + response = client.get(f"{PREFIX}/{USD.code}/edit") self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{zza.code}/update", + response = client.post(f"{PREFIX}/{USD.code}/update", data={"csrf_token": csrf_token, - "code": zzd.code, - "name": zzd.name}) + "code": JPY.code, + "name": JPY.name}) self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{zzb.code}/delete", + response = client.post(f"{PREFIX}/{EUR.code}/delete", data={"csrf_token": csrf_token}) self.assertEqual(response.status_code, 403) @@ -199,7 +200,7 @@ class CurrencyTestCase(unittest.TestCase): response = client.get(PREFIX) self.assertEqual(response.status_code, 200) - response = client.get(f"{PREFIX}/{zza.code}") + response = client.get(f"{PREFIX}/{USD.code}") self.assertEqual(response.status_code, 200) response = client.get(f"{PREFIX}/create") @@ -207,20 +208,20 @@ class CurrencyTestCase(unittest.TestCase): response = client.post(f"{PREFIX}/store", data={"csrf_token": csrf_token, - "code": zzc.code, - "name": zzc.name}) + "code": TWD.code, + "name": TWD.name}) self.assertEqual(response.status_code, 403) - response = client.get(f"{PREFIX}/{zza.code}/edit") + response = client.get(f"{PREFIX}/{USD.code}/edit") self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{zza.code}/update", + response = client.post(f"{PREFIX}/{USD.code}/update", data={"csrf_token": csrf_token, - "code": zzd.code, - "name": zzd.name}) + "code": JPY.code, + "name": JPY.name}) self.assertEqual(response.status_code, 403) - response = client.post(f"{PREFIX}/{zzb.code}/delete", + response = client.post(f"{PREFIX}/{EUR.code}/delete", data={"csrf_token": csrf_token}) self.assertEqual(response.status_code, 403) @@ -234,7 +235,7 @@ class CurrencyTestCase(unittest.TestCase): response = self.client.get(PREFIX) self.assertEqual(response.status_code, 200) - response = self.client.get(f"{PREFIX}/{zza.code}") + response = self.client.get(f"{PREFIX}/{USD.code}") self.assertEqual(response.status_code, 200) response = self.client.get(f"{PREFIX}/create") @@ -242,22 +243,22 @@ class CurrencyTestCase(unittest.TestCase): response = self.client.post(f"{PREFIX}/store", data={"csrf_token": self.csrf_token, - "code": zzc.code, - "name": zzc.name}) + "code": TWD.code, + "name": TWD.name}) self.assertEqual(response.status_code, 302) - self.assertEqual(response.headers["Location"], f"{PREFIX}/{zzc.code}") + self.assertEqual(response.headers["Location"], f"{PREFIX}/{TWD.code}") - response = self.client.get(f"{PREFIX}/{zza.code}/edit") + response = self.client.get(f"{PREFIX}/{USD.code}/edit") self.assertEqual(response.status_code, 200) - response = self.client.post(f"{PREFIX}/{zza.code}/update", + response = self.client.post(f"{PREFIX}/{USD.code}/update", data={"csrf_token": self.csrf_token, - "code": zzd.code, - "name": zzd.name}) + "code": JPY.code, + "name": JPY.name}) self.assertEqual(response.status_code, 302) - self.assertEqual(response.headers["Location"], f"{PREFIX}/{zzd.code}") + self.assertEqual(response.headers["Location"], f"{PREFIX}/{JPY.code}") - response = self.client.post(f"{PREFIX}/{zzb.code}/delete", + response = self.client.post(f"{PREFIX}/{EUR.code}/delete", data={"csrf_token": self.csrf_token}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], PREFIX) @@ -270,31 +271,31 @@ class CurrencyTestCase(unittest.TestCase): from accounting.models import Currency create_uri: str = f"{PREFIX}/create" store_uri: str = f"{PREFIX}/store" - detail_uri: str = f"{PREFIX}/{zzc.code}" + detail_uri: str = f"{PREFIX}/{TWD.code}" response: httpx.Response with self.app.app_context(): self.assertEqual({x.code for x in Currency.query.all()}, - {zza.code, zzb.code}) + {USD.code, EUR.code}) # Missing CSRF token response = self.client.post(store_uri, - data={"code": zzc.code, - "name": zzc.name}) + data={"code": TWD.code, + "name": TWD.name}) self.assertEqual(response.status_code, 400) # CSRF token mismatch response = self.client.post(store_uri, data={"csrf_token": f"{self.csrf_token}-2", - "code": zzc.code, - "name": zzc.name}) + "code": TWD.code, + "name": TWD.name}) self.assertEqual(response.status_code, 400) # Empty code response = self.client.post(store_uri, data={"csrf_token": self.csrf_token, "code": " ", - "name": zzc.name}) + "name": TWD.name}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], create_uri) @@ -302,7 +303,7 @@ class CurrencyTestCase(unittest.TestCase): response = self.client.post(store_uri, data={"csrf_token": self.csrf_token, "code": " create ", - "name": zzc.name}) + "name": TWD.name}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], create_uri) @@ -310,14 +311,14 @@ class CurrencyTestCase(unittest.TestCase): response = self.client.post(store_uri, data={"csrf_token": self.csrf_token, "code": " zzc ", - "name": zzc.name}) + "name": TWD.name}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], create_uri) # Empty name response = self.client.post(store_uri, data={"csrf_token": self.csrf_token, - "code": zzc.code, + "code": TWD.code, "name": " "}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], create_uri) @@ -325,26 +326,26 @@ class CurrencyTestCase(unittest.TestCase): # Success, with spaces to be stripped response = self.client.post(store_uri, data={"csrf_token": self.csrf_token, - "code": f" {zzc.code} ", - "name": f" {zzc.name} "}) + "code": f" {TWD.code} ", + "name": f" {TWD.name} "}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) # Duplicated code response = self.client.post(store_uri, data={"csrf_token": self.csrf_token, - "code": zzc.code, - "name": zzc.name}) + "code": TWD.code, + "name": TWD.name}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], create_uri) with self.app.app_context(): self.assertEqual({x.code for x in Currency.query.all()}, - {zza.code, zzb.code, zzc.code}) + {USD.code, EUR.code, TWD.code}) - currency: Currency = db.session.get(Currency, zzc.code) - self.assertEqual(currency.code, zzc.code) - self.assertEqual(currency.name_l10n, zzc.name) + currency: Currency = db.session.get(Currency, TWD.code) + self.assertEqual(currency.code, TWD.code) + self.assertEqual(currency.name_l10n, TWD.name) def test_basic_update(self) -> None: """Tests the basic rules to update a user. @@ -352,30 +353,30 @@ class CurrencyTestCase(unittest.TestCase): :return: None. """ from accounting.models import Currency - detail_uri: str = f"{PREFIX}/{zza.code}" - edit_uri: str = f"{PREFIX}/{zza.code}/edit" - update_uri: str = f"{PREFIX}/{zza.code}/update" - detail_c_uri: str = f"{PREFIX}/{zzc.code}" + detail_uri: str = f"{PREFIX}/{USD.code}" + edit_uri: str = f"{PREFIX}/{USD.code}/edit" + update_uri: str = f"{PREFIX}/{USD.code}/update" + detail_c_uri: str = f"{PREFIX}/{TWD.code}" response: httpx.Response # Success, with spaces to be stripped response = self.client.post(update_uri, data={"csrf_token": self.csrf_token, - "code": f" {zza.code} ", - "name": f" {zza.name}-1 "}) + "code": f" {USD.code} ", + "name": f" {USD.name}-1 "}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - currency: Currency = db.session.get(Currency, zza.code) - self.assertEqual(currency.code, zza.code) - self.assertEqual(currency.name_l10n, f"{zza.name}-1") + currency: Currency = db.session.get(Currency, USD.code) + self.assertEqual(currency.code, USD.code) + self.assertEqual(currency.name_l10n, f"{USD.name}-1") # Empty code response = self.client.post(update_uri, data={"csrf_token": self.csrf_token, "code": " ", - "name": zzc.name}) + "name": TWD.name}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], edit_uri) @@ -383,7 +384,7 @@ class CurrencyTestCase(unittest.TestCase): response = self.client.post(update_uri, data={"csrf_token": self.csrf_token, "code": " create ", - "name": zzc.name}) + "name": TWD.name}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], edit_uri) @@ -391,14 +392,14 @@ class CurrencyTestCase(unittest.TestCase): response = self.client.post(update_uri, data={"csrf_token": self.csrf_token, "code": "abc/def", - "name": zzc.name}) + "name": TWD.name}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], edit_uri) # Empty name response = self.client.post(update_uri, data={"csrf_token": self.csrf_token, - "code": zzc.code, + "code": TWD.code, "name": " "}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], edit_uri) @@ -406,16 +407,16 @@ class CurrencyTestCase(unittest.TestCase): # Duplicated code response = self.client.post(update_uri, data={"csrf_token": self.csrf_token, - "code": zzb.code, - "name": zzc.name}) + "code": EUR.code, + "name": TWD.name}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], edit_uri) # Change code response = self.client.post(update_uri, data={"csrf_token": self.csrf_token, - "code": zzc.code, - "name": zzc.name}) + "code": TWD.code, + "name": TWD.name}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_c_uri) @@ -431,20 +432,20 @@ class CurrencyTestCase(unittest.TestCase): :return: None. """ from accounting.models import Currency - detail_uri: str = f"{PREFIX}/{zza.code}" - update_uri: str = f"{PREFIX}/{zza.code}/update" + detail_uri: str = f"{PREFIX}/{USD.code}" + update_uri: str = f"{PREFIX}/{USD.code}/update" currency: Currency | None response: httpx.Response response = self.client.post(update_uri, data={"csrf_token": self.csrf_token, - "code": f" {zza.code} ", - "name": f" {zza.name} "}) + "code": f" {USD.code} ", + "name": f" {USD.name} "}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - currency = db.session.get(Currency, zza.code) + currency = db.session.get(Currency, USD.code) self.assertIsNotNone(currency) currency.created_at \ = currency.created_at - timedelta(seconds=5) @@ -453,13 +454,13 @@ class CurrencyTestCase(unittest.TestCase): response = self.client.post(update_uri, data={"csrf_token": self.csrf_token, - "code": zza.code, - "name": zzc.name}) + "code": USD.code, + "name": TWD.name}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - currency = db.session.get(Currency, zza.code) + currency = db.session.get(Currency, USD.code) self.assertIsNotNone(currency) self.assertLess(currency.created_at, currency.updated_at) @@ -472,25 +473,25 @@ class CurrencyTestCase(unittest.TestCase): from accounting.models import Currency editor_username, editor2_username = "editor", "editor2" client, csrf_token = get_client(self.app, editor2_username) - detail_uri: str = f"{PREFIX}/{zza.code}" - update_uri: str = f"{PREFIX}/{zza.code}/update" + detail_uri: str = f"{PREFIX}/{USD.code}" + update_uri: str = f"{PREFIX}/{USD.code}/update" currency: Currency response: httpx.Response with self.app.app_context(): - currency = db.session.get(Currency, zza.code) + currency = db.session.get(Currency, USD.code) self.assertEqual(currency.created_by.username, editor_username) self.assertEqual(currency.updated_by.username, editor_username) response = client.post(update_uri, data={"csrf_token": csrf_token, - "code": zza.code, - "name": f"{zza.name}-2"}) + "code": USD.code, + "name": f"{USD.name}-2"}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - currency = db.session.get(Currency, zza.code) + currency = db.session.get(Currency, USD.code) self.assertEqual(currency.created_by.username, editor_username) self.assertEqual(currency.updated_by.username, editor2_username) @@ -502,14 +503,14 @@ class CurrencyTestCase(unittest.TestCase): response: httpx.Response response = self.client.get( - f"/accounting/api/currencies/exists-code?q={zza.code}") + f"/accounting/api/currencies/exists-code?q={USD.code}") self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(set(data.keys()), {"exists"}) self.assertTrue(data["exists"]) response = self.client.get( - f"/accounting/api/currencies/exists-code?q={zza.code}-1") + f"/accounting/api/currencies/exists-code?q={USD.code}-1") self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(set(data.keys()), {"exists"}) @@ -521,60 +522,60 @@ class CurrencyTestCase(unittest.TestCase): :return: None """ from accounting.models import Currency - detail_uri: str = f"{PREFIX}/{zza.code}" - update_uri: str = f"{PREFIX}/{zza.code}/update" + detail_uri: str = f"{PREFIX}/{USD.code}" + update_uri: str = f"{PREFIX}/{USD.code}/update" currency: Currency response: httpx.Response with self.app.app_context(): - currency = db.session.get(Currency, zza.code) - self.assertEqual(currency.name_l10n, zza.name) + currency = db.session.get(Currency, USD.code) + self.assertEqual(currency.name_l10n, USD.name) self.assertEqual(currency.l10n, []) set_locale(self.client, self.csrf_token, "zh_Hant") response = self.client.post(update_uri, data={"csrf_token": self.csrf_token, - "code": zza.code, - "name": f"{zza.name}-zh_Hant"}) + "code": USD.code, + "name": f"{USD.name}-zh_Hant"}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - currency = db.session.get(Currency, zza.code) - self.assertEqual(currency.name_l10n, zza.name) + currency = db.session.get(Currency, USD.code) + self.assertEqual(currency.name_l10n, USD.name) self.assertEqual({(x.locale, x.name) for x in currency.l10n}, - {("zh_Hant", f"{zza.name}-zh_Hant")}) + {("zh_Hant", f"{USD.name}-zh_Hant")}) set_locale(self.client, self.csrf_token, "en") response = self.client.post(update_uri, data={"csrf_token": self.csrf_token, - "code": zza.code, - "name": f"{zza.name}-2"}) + "code": USD.code, + "name": f"{USD.name}-2"}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - currency = db.session.get(Currency, zza.code) - self.assertEqual(currency.name_l10n, f"{zza.name}-2") + currency = db.session.get(Currency, USD.code) + self.assertEqual(currency.name_l10n, f"{USD.name}-2") self.assertEqual({(x.locale, x.name) for x in currency.l10n}, - {("zh_Hant", f"{zza.name}-zh_Hant")}) + {("zh_Hant", f"{USD.name}-zh_Hant")}) set_locale(self.client, self.csrf_token, "zh_Hant") response = self.client.post(update_uri, data={"csrf_token": self.csrf_token, - "code": zza.code, - "name": f"{zza.name}-zh_Hant-2"}) + "code": USD.code, + "name": f"{USD.name}-zh_Hant-2"}) self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], detail_uri) with self.app.app_context(): - currency = db.session.get(Currency, zza.code) - self.assertEqual(currency.name_l10n, f"{zza.name}-2") + currency = db.session.get(Currency, USD.code) + self.assertEqual(currency.name_l10n, f"{USD.name}-2") self.assertEqual({(x.locale, x.name) for x in currency.l10n}, - {("zh_Hant", f"{zza.name}-zh_Hant-2")}) + {("zh_Hant", f"{USD.name}-zh_Hant-2")}) def test_delete(self) -> None: """Tests to delete a currency. @@ -582,15 +583,58 @@ class CurrencyTestCase(unittest.TestCase): :return: None. """ from accounting.models import Currency - detail_uri: str = f"{PREFIX}/{zza.code}" - delete_uri: str = f"{PREFIX}/{zza.code}/delete" + detail_uri: str = f"{PREFIX}/{JPY.code}" + delete_uri: str = f"{PREFIX}/{JPY.code}/delete" list_uri: str = PREFIX response: httpx.Response + runner: FlaskCliRunner = self.app.test_cli_runner() + with self.app.app_context(): + from accounting.models import BaseAccount + if BaseAccount.query.first() is None: + result = runner.invoke(args="accounting-init-base") + self.assertEqual(result.exit_code, 0) + + response = self.client.post("/accounting/accounts/store", + data={"csrf_token": self.csrf_token, + "base_code": "1111", + "title": "Cash"}) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.headers["Location"], + "/accounting/accounts/1111-001") + + response = self.client.post(f"{PREFIX}/store", + data={"csrf_token": self.csrf_token, + "code": JPY.code, + "name": JPY.name}) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.headers["Location"], detail_uri) + + add_journal_entry(self.client, + form={"csrf_token": self.csrf_token, + "next": NEXT_URI, + "date": date.today().isoformat(), + "currency-1-code": EUR.code, + "currency-1-credit-1-account_code": "1111-001", + "currency-1-credit-1-amount": "20"}) + with self.app.app_context(): self.assertEqual({x.code for x in Currency.query.all()}, - {zza.code, zzb.code}) + {USD.code, EUR.code, JPY.code}) + # Cannot delete the default currency + response = self.client.post(f"{PREFIX}/{USD.code}/delete", + data={"csrf_token": self.csrf_token}) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}") + + # Cannot delete the account that is in use + response = self.client.post(f"{PREFIX}/{EUR.code}/delete", + data={"csrf_token": self.csrf_token}) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.headers["Location"], f"{PREFIX}/{EUR.code}") + + # Success response = self.client.get(detail_uri) self.assertEqual(response.status_code, 200) response = self.client.post(delete_uri, @@ -600,7 +644,7 @@ class CurrencyTestCase(unittest.TestCase): with self.app.app_context(): self.assertEqual({x.code for x in Currency.query.all()}, - {zzb.code}) + {USD.code, EUR.code}) response = self.client.get(detail_uri) self.assertEqual(response.status_code, 404)