Currency hotfixes (#1240)

* hotfix: hardcode list of currencies to workaround failing API calls

See https://github.com/spiral-project/ihatemoney/issues/1232 for a discussion on currencies

* Temporarily disable some currency operations to prevent crashes

Here is what is disabled:

- setting or changing the default currency on an existing project

- adding or editing a bill with a currency that differs from the default
  currency of the project

---------

Co-authored-by: Baptiste Jonglez <git@bitsofnetworks.org>
This commit is contained in:
zorun 2023-10-04 00:05:10 +02:00 committed by GitHub
parent c5c8dba631
commit 1a2fa0476b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 216 additions and 19 deletions

View file

@ -36,13 +36,181 @@ class CurrencyConverter(object, metaclass=Singleton):
return rates return rates
def get_currencies(self, with_no_currency=True): def get_currencies(self, with_no_currency=True):
rates = [ currencies = [
rate "AED",
for rate in self.get_rates() "AFN",
if with_no_currency or rate != self.no_currency "ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTC",
"BTN",
"BWP",
"BYN",
"BZD",
"CAD",
"CDF",
"CHF",
"CLF",
"CLP",
"CNH",
"CNY",
"COP",
"CRC",
"CUC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GGP",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"IMP",
"INR",
"IQD",
"IRR",
"ISK",
"JEP",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRU",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"STN",
"SVC",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VES",
"VND",
"VUV",
"WST",
"XAF",
"XAG",
"XAU",
"XCD",
"XDR",
"XOF",
"XPD",
"XPF",
"XPT",
"YER",
"ZAR",
"ZMW",
"ZWL",
] ]
rates.sort(key=lambda rate: "" if rate == self.no_currency else rate) if with_no_currency:
return rates currencies.append(self.no_currency)
return currencies
def exchange_currency(self, amount, source_currency, dest_currency): def exchange_currency(self, amount, source_currency, dest_currency):
if ( if (

View file

@ -67,6 +67,9 @@ def get_billform_for(project, set_default=True, **kwargs):
if form.original_currency.data is None: if form.original_currency.data is None:
form.original_currency.data = project.default_currency form.original_currency.data = project.default_currency
# Used in validate_original_currency
form.project_currency = project.default_currency
show_no_currency = form.original_currency.data == CurrencyConverter.no_currency show_no_currency = form.original_currency.data == CurrencyConverter.no_currency
form.original_currency.choices = [ form.original_currency.choices = [
@ -185,12 +188,20 @@ class EditProjectForm(FlaskForm):
and field.data == CurrencyConverter.no_currency and field.data == CurrencyConverter.no_currency
and project.has_multiple_currencies() and project.has_multiple_currencies()
): ):
raise ValidationError( msg = _(
_(
"This project cannot be set to 'no currency'" "This project cannot be set to 'no currency'"
" because it contains bills in multiple currencies." " because it contains bills in multiple currencies."
) )
raise ValidationError(msg)
if (
project is not None
and field.data != CurrencyConverter.no_currency
and project.has_bills()
):
msg = _(
"Cannot change project currency because currency conversion is broken"
) )
raise ValidationError(msg)
def update(self, project): def update(self, project):
"""Update the project with the information from the form""" """Update the project with the information from the form"""
@ -406,6 +417,17 @@ class BillForm(FlaskForm):
# See https://github.com/python-babel/babel/issues/821 # See https://github.com/python-babel/babel/issues/821
raise ValidationError(f"Result is too high: {field.data}") raise ValidationError(f"Result is too high: {field.data}")
def validate_original_currency(self, field):
# Workaround for currency API breakage
# See #1232
if field.data not in [CurrencyConverter.no_currency, self.project_currency]:
msg = _(
"Failed to convert from %(bill_currency)s currency to %(project_currency)s",
bill_currency=field.data,
project_currency=self.project_currency,
)
raise ValidationError(msg)
class MemberForm(FlaskForm): class MemberForm(FlaskForm):
name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter]) name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter])

View file

@ -2,6 +2,8 @@ import base64
import datetime import datetime
import json import json
import pytest
from ihatemoney.tests.common.help_functions import em_surround from ihatemoney.tests.common.help_functions import em_surround
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
@ -615,6 +617,7 @@ class TestAPI(IhatemoneyTestCase):
) )
self.assertStatus(400, req) self.assertStatus(400, req)
@pytest.mark.skip(reason="Currency conversion is broken")
def test_currencies(self): def test_currencies(self):
# check /currencies for list of supported currencies # check /currencies for list of supported currencies
resp = self.client.get("/api/currencies") resp = self.client.get("/api/currencies")

View file

@ -1403,6 +1403,7 @@ class TestBudget(IhatemoneyTestCase):
member = models.Person.query.filter(models.Person.id == 1).one_or_none() member = models.Person.query.filter(models.Person.id == 1).one_or_none()
assert member is None assert member is None
@pytest.mark.skip(reason="Currency conversion is broken")
def test_currency_switch(self): def test_currency_switch(self):
# A project should be editable # A project should be editable
self.post_project("raclette") self.post_project("raclette")
@ -1529,6 +1530,7 @@ class TestBudget(IhatemoneyTestCase):
assert '<p class="alert alert-danger">' in resp.data.decode("utf-8") assert '<p class="alert alert-danger">' in resp.data.decode("utf-8")
assert self.get_project("raclette").default_currency == "USD" assert self.get_project("raclette").default_currency == "USD"
@pytest.mark.skip(reason="Currency conversion is broken")
def test_currency_switch_to_bill_currency(self): def test_currency_switch_to_bill_currency(self):
# Default currency is 'XXX', but we should start from a project with a currency # Default currency is 'XXX', but we should start from a project with a currency
self.post_project("raclette", default_currency="USD") self.post_project("raclette", default_currency="USD")
@ -1563,6 +1565,7 @@ class TestBudget(IhatemoneyTestCase):
bill = project.get_bills().first() bill = project.get_bills().first()
assert bill.converted_amount == bill.amount assert bill.converted_amount == bill.amount
@pytest.mark.skip(reason="Currency conversion is broken")
def test_currency_switch_to_no_currency(self): def test_currency_switch_to_no_currency(self):
# Default currency is 'XXX', but we should start from a project with a currency # Default currency is 'XXX', but we should start from a project with a currency
self.post_project("raclette", default_currency="USD") self.post_project("raclette", default_currency="USD")
@ -1626,7 +1629,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": 1, "payer": 1,
"payed_for": [1], "payed_for": [1],
"amount": "0", "amount": "0",
"original_currency": "EUR", "original_currency": "XXX",
}, },
) )
@ -1703,7 +1706,7 @@ class TestBudget(IhatemoneyTestCase):
Tests that the RSS feed output content is expected. Tests that the RSS feed output content is expected.
""" """
with fake_time("2023-07-25 12:00:00"): with fake_time("2023-07-25 12:00:00"):
self.post_project("raclette") self.post_project("raclette", default_currency="EUR")
self.client.post("/raclette/members/add", data={"name": "george"}) self.client.post("/raclette/members/add", data={"name": "george"})
self.client.post("/raclette/members/add", data={"name": "peter"}) self.client.post("/raclette/members/add", data={"name": "peter"})
self.client.post("/raclette/members/add", data={"name": "steven"}) self.client.post("/raclette/members/add", data={"name": "steven"})
@ -1787,7 +1790,7 @@ class TestBudget(IhatemoneyTestCase):
history is disabled. history is disabled.
""" """
with fake_time("2023-07-25 12:00:00"): with fake_time("2023-07-25 12:00:00"):
self.post_project("raclette", project_history=False) self.post_project("raclette", default_currency="EUR", project_history=False)
self.client.post("/raclette/members/add", data={"name": "george"}) self.client.post("/raclette/members/add", data={"name": "george"})
self.client.post("/raclette/members/add", data={"name": "peter"}) self.client.post("/raclette/members/add", data={"name": "peter"})
self.client.post("/raclette/members/add", data={"name": "steven"}) self.client.post("/raclette/members/add", data={"name": "steven"})
@ -1900,7 +1903,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": 1, "payer": 1,
"payed_for": [1], "payed_for": [1],
"amount": "12", "amount": "12",
"original_currency": "EUR", "original_currency": "XXX",
}, },
follow_redirects=True, follow_redirects=True,
) )
@ -1961,7 +1964,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": 1, "payer": 1,
"payed_for": [1], "payed_for": [1],
"amount": "12", "amount": "12",
"original_currency": "EUR", "original_currency": "XXX",
}, },
follow_redirects=True, follow_redirects=True,
) )

View file

@ -466,6 +466,7 @@ class TestExport(IhatemoneyTestCase):
resp = self.client.get("/raclette/export/transactions.wrong") resp = self.client.get("/raclette/export/transactions.wrong")
assert resp.status_code == 404 assert resp.status_code == 404
@pytest.mark.skip(reason="Currency conversion is broken")
def test_export_with_currencies(self): def test_export_with_currencies(self):
self.post_project("raclette", default_currency="EUR") self.post_project("raclette", default_currency="EUR")

View file

@ -385,9 +385,9 @@ class TestCurrencyConverter:
assert one == two assert one == two
def test_get_currencies(self): def test_get_currencies(self):
assert set(self.converter.get_currencies()) == set( currencies = self.converter.get_currencies()
["USD", "EUR", "CAD", "PLN", CurrencyConverter.no_currency] for currency in ["USD", "EUR", "CAD", "PLN", CurrencyConverter.no_currency]:
) assert currency in currencies
def test_exchange_currency(self): def test_exchange_currency(self):
result = self.converter.exchange_currency(100, "USD", "EUR") result = self.converter.exchange_currency(100, "USD", "EUR")