From 04b18a8d3d00f860aca6a9f8e7e289094784cf53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20M=C3=A9taireau?= Date: Thu, 26 Dec 2024 17:21:33 +0100 Subject: [PATCH] Remove multi currencies support For more information about the rationale, this was [discussed](https://github.com/spiral-project/ihatemoney/issues/1232#issuecomment-2081517453) in our bugtracker. --- CHANGELOG.md | 3 +- docs/api.md | 37 --- ihatemoney/api/common.py | 8 - ihatemoney/api/v1/resources.py | 2 - ihatemoney/currency_convertor.py | 228 ------------- ihatemoney/forms.py | 104 +----- ihatemoney/history.py | 1 - .../versions/927ed575acbd_add_currencies.py | 5 +- ihatemoney/models.py | 83 +---- ihatemoney/run.py | 26 +- ihatemoney/templates/forms.html | 4 - ihatemoney/templates/history.html | 6 +- ihatemoney/templates/list_bills.html | 11 +- ihatemoney/templates/project_feed.xml | 4 +- ihatemoney/templates/settle_bills.html | 2 +- .../templates/sidebar_table_layout.html | 4 +- ihatemoney/templates/statistics.html | 6 +- ihatemoney/tests/api_test.py | 188 +---------- ihatemoney/tests/budget_test.py | 269 +--------------- .../tests/common/ihatemoney_testcase.py | 7 +- ihatemoney/tests/conftest.py | 21 -- ihatemoney/tests/history_test.py | 19 +- ihatemoney/tests/import_test.py | 300 +----------------- ihatemoney/tests/main_test.py | 39 --- ihatemoney/utils.py | 17 - ihatemoney/web.py | 30 +- 26 files changed, 85 insertions(+), 1339 deletions(-) delete mode 100644 ihatemoney/currency_convertor.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6adc4b2aa..b6da7d60a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ This document describes changes between each past release. -## 6.2.0 (unreleased) +## 7.0.0 (unreleased) +- Remove the support for multiple currencies, [as discussed](https://github.com/spiral-project/ihatemoney/issues/1232#issuecomment-2081517453) in our bugtracker. - Add support for python 3.12 (#757) - Migrate from setup.cfg to pyproject.toml (#1243) - Update to wtforms 3.1 (#1248) diff --git a/docs/api.md b/docs/api.md index a24dd9295..403302b50 100644 --- a/docs/api.md +++ b/docs/api.md @@ -71,13 +71,6 @@ A project needs the following arguments: - `password`: the project password / private code (string) - `contact_email`: the contact email, used to recover the private code (string) -Optional arguments: - -- `default_currency`: the default currency to use for a multi-currency - project, in ISO 4217 format. Bills are converted to this currency - for operations like balance or statistics. Default value: `XXX` (no - currency). - Here is the command: $ curl -X POST https://ihatemoney.org/api/projects \ @@ -97,7 +90,6 @@ Getting information about the project: "id": "demo", "name": "demonstration", "contact_email": "demo@notmyidea.org", - "default_currency": "XXX", "members": [{"id": 11515, "name": "f", "weight": 1.0, "activated": true, "balance": 0}, {"id": 11531, "name": "g", "weight": 1.0, "activated": true, "balance": 0}, {"id": 11532, "name": "peter", "weight": 1.0, "activated": true, "balance": 5.0}, @@ -181,14 +173,8 @@ Or get a specific bill by ID: "creation_date": "2021-01-13", "what": "Raclette du nouvel an", "external_link": "", - "original_currency": "XXX", - "converted_amount": 100 } -`amount` is expressed in the `original_currency` of the bill, while -`converted_amount` is expressed in the project `default_currency`. Here, -they are the same. - Add a bill with a `POST` query on `/api/projects//bills`. You need the following required parameters: @@ -203,9 +189,6 @@ And optional parameters: - `date`: the date of the bill (`yyyy-mm-dd` format). Defaults to current date if not provided. -- `original_currency`: the currency in which `amount` has been paid - (ISO 4217 code). Only makes sense for a project with currencies. - Defaults to the project `default_currency`. - `external_link`: an optional URL associated with the bill. Returns the id of the created bill : @@ -250,23 +233,3 @@ You can get some project stats with a `GET` on "balance": -10.5 } ] - -### Currencies - -You can get a list of supported currencies with a `GET` on -`/api/currencies`: - - $ curl --basic https://ihatemoney.org/api/currencies - [ - "XXX", - "AED", - "AFN", - . - . - . - "ZAR", - "ZMW", - "ZWL" - ] - - diff --git a/ihatemoney/api/common.py b/ihatemoney/api/common.py index caf8148bc..1d7c651a9 100644 --- a/ihatemoney/api/common.py +++ b/ihatemoney/api/common.py @@ -5,7 +5,6 @@ from werkzeug.security import check_password_hash from wtforms.fields import BooleanField -from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.emails import send_creation_email from ihatemoney.forms import EditProjectForm, MemberForm, ProjectForm, get_billform_for from ihatemoney.models import Bill, Person, Project, db @@ -50,13 +49,6 @@ def wrapper(*args, **kwargs): return wrapper -class CurrenciesHandler(Resource): - currency_helper = CurrencyConverter() - - def get(self): - return self.currency_helper.get_currencies() - - class ProjectsHandler(Resource): def post(self): form = ProjectForm(meta={"csrf": False}) diff --git a/ihatemoney/api/v1/resources.py b/ihatemoney/api/v1/resources.py index 6c46e4f9d..dc1708ce8 100644 --- a/ihatemoney/api/v1/resources.py +++ b/ihatemoney/api/v1/resources.py @@ -5,7 +5,6 @@ from ihatemoney.api.common import ( BillHandler, BillsHandler, - CurrenciesHandler, MemberHandler, MembersHandler, ProjectHandler, @@ -18,7 +17,6 @@ CORS(api) restful_api = Api(api) -restful_api.add_resource(CurrenciesHandler, "/currencies") restful_api.add_resource(ProjectsHandler, "/projects") restful_api.add_resource(ProjectHandler, "/projects/") restful_api.add_resource(TokenHandler, "/projects//token") diff --git a/ihatemoney/currency_convertor.py b/ihatemoney/currency_convertor.py deleted file mode 100644 index ca0ff3669..000000000 --- a/ihatemoney/currency_convertor.py +++ /dev/null @@ -1,228 +0,0 @@ -import traceback -import warnings - -from cachetools import TTLCache, cached -import requests - - -class Singleton(type): - _instances = {} - - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) - return cls._instances[cls] - - -class CurrencyConverter(object, metaclass=Singleton): - # Get exchange rates - no_currency = "XXX" - api_url = "https://api.exchangerate.host/latest?base=USD" - - def __init__(self): - pass - - @cached(cache=TTLCache(maxsize=1, ttl=86400)) - def get_rates(self): - try: - rates = requests.get(self.api_url).json()["rates"] - except Exception: - warnings.warn( - f"Call to {self.api_url} failed: {traceback.format_exc(limit=0).strip()}" - ) - # In case of any exception, let's have an empty value - rates = {} - rates[self.no_currency] = 1.0 - return rates - - def get_currencies(self, with_no_currency=True): - currencies = [ - "AED", - "AFN", - "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", - ] - if with_no_currency: - currencies.append(self.no_currency) - return currencies - - def exchange_currency(self, amount, source_currency, dest_currency): - if ( - source_currency == dest_currency - or source_currency == self.no_currency - or dest_currency == self.no_currency - ): - return amount - - rates = self.get_rates() - source_rate = rates[source_currency] - dest_rate = rates[dest_currency] - new_amount = (float(amount) / source_rate) * dest_rate - # round to two digits because we are dealing with money - return round(new_amount, 2) diff --git a/ihatemoney/forms.py b/ihatemoney/forms.py index 0fee97793..4a38ff20f 100644 --- a/ihatemoney/forms.py +++ b/ihatemoney/forms.py @@ -14,7 +14,6 @@ BooleanField, DateField, DecimalField, - Label, PasswordField, SelectField, SelectMultipleField, @@ -38,13 +37,11 @@ ValidationError, ) -from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project from ihatemoney.utils import ( em_surround, eval_arithmetic_expression, generate_password_hash, - render_localized_currency, slugify, ) @@ -64,20 +61,6 @@ def get_billform_for(project, set_default=True, **kwargs): """ form = BillForm(**kwargs) - if form.original_currency.data is None: - 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 - - form.original_currency.choices = [ - (currency_name, render_localized_currency(currency_name, detailed=False)) - for currency_name in form.currency_helper.get_currencies( - with_no_currency=show_no_currency - ) - ] active_members = [(m.id, m.name) for m in project.active_members] @@ -137,30 +120,6 @@ class EditProjectForm(FlaskForm): contact_email = StringField(_("Email"), validators=[DataRequired(), Email()]) project_history = BooleanField(_("Enable project history")) ip_recording = BooleanField(_("Use IP tracking for project history")) - currency_helper = CurrencyConverter() - default_currency = SelectField( - _("Default Currency"), - validators=[DataRequired()], - default=CurrencyConverter.no_currency, - description=_( - "Setting a default currency enables currency conversion between bills" - ), - ) - - def __init__(self, *args, **kwargs): - if not hasattr(self, "id"): - # We must access the project to validate the default currency, using its id. - # In ProjectForm, 'id' is provided, but not in this base class, so it *must* - # be provided by callers. - # Since id can be defined as a WTForms.StringField, we mimics it, - # using an object that can have a 'data' attribute. - # It defaults to empty string to ensure that query run smoothly. - self.id = SimpleNamespace(data=kwargs.pop("id", "")) - super().__init__(*args, **kwargs) - self.default_currency.choices = [ - (currency_name, render_localized_currency(currency_name, detailed=True)) - for currency_name in self.currency_helper.get_currencies() - ] def validate_current_password(self, field): project = Project.query.get(self.id.data) @@ -180,28 +139,6 @@ def logging_preference(self): else: return LoggingMode.ENABLED - def validate_default_currency(self, field): - project = Project.query.get(self.id.data) - if ( - project is not None - and field.data == CurrencyConverter.no_currency - and project.has_multiple_currencies() - ): - msg = _( - "This project cannot be set to 'no currency'" - " because it contains bills in multiple currencies." - ) - raise ValidationError(msg) - if ( - project is not None - and field.data != project.default_currency - and project.has_bills() - ): - msg = _( - "Cannot change project currency because currency conversion is broken" - ) - raise ValidationError(msg) - def update(self, project): """Update the project with the information from the form""" project.name = self.name.data @@ -217,10 +154,20 @@ def update(self, project): project.contact_email = self.contact_email.data project.logging_preference = self.logging_preference - project.switch_currency(self.default_currency.data) return project + def __init__(self, *args, **kwargs): + if not hasattr(self, "id"): + # We must access the project to validate the default currency, using its id. + # In ProjectForm, 'id' is provided, but not in this base class, so it *must* + # be provided by callers. + # Since id can be defined as a WTForms.StringField, we mimic it, + # using an object that can have a 'data' attribute. + # It defaults to empty string to ensure that query run smoothly. + self.id = SimpleNamespace(data=kwargs.pop("id", "")) + super().__init__(*args, **kwargs) + class ImportProjectForm(FlaskForm): file = FileField( @@ -258,7 +205,6 @@ def save(self): password=generate_password_hash(self.password.data), contact_email=self.contact_email.data, logging_preference=self.logging_preference, - default_currency=self.default_currency.data, ) return project @@ -352,8 +298,6 @@ class BillForm(FlaskForm): what = StringField(_("What?"), validators=[DataRequired()]) payer = SelectField(_("Who paid?"), validators=[DataRequired()], coerce=int) amount = CalculatorStringField(_("How much?"), validators=[DataRequired()]) - currency_helper = CurrencyConverter() - original_currency = SelectField(_("Currency"), validators=[DataRequired()]) external_link = URLField( _("External link"), default="", @@ -377,10 +321,8 @@ def export(self, project): amount=float(self.amount.data), date=self.date.data, external_link=self.external_link.data, - original_currency=str(self.original_currency.data), owers=Person.query.get_by_ids(self.payed_for.data, project), payer_id=self.payer.data, - project_default_currency=project.default_currency, what=self.what.data, bill_type=self.bill_type.data, ) @@ -393,10 +335,6 @@ def save(self, bill, project): bill.external_link = self.external_link.data bill.date = self.date.data bill.owers = Person.query.get_by_ids(self.payed_for.data, project) - bill.original_currency = self.original_currency.data - bill.converted_amount = self.currency_helper.exchange_currency( - bill.amount, bill.original_currency, project.default_currency - ) return bill def fill(self, bill, project): @@ -405,18 +343,9 @@ def fill(self, bill, project): self.what.data = bill.what self.bill_type.data = bill.bill_type self.external_link.data = bill.external_link - self.original_currency.data = bill.original_currency self.date.data = bill.date self.payed_for.data = [int(ower.id) for ower in bill.owers] - self.original_currency.label = Label("original_currency", _("Currency")) - self.original_currency.description = _( - "Project default: %(currency)s", - currency=render_localized_currency( - project.default_currency, detailed=False - ), - ) - def set_default(self): self.payed_for.data = self.payed_for.default @@ -425,17 +354,6 @@ def validate_amount(self, field): # See https://github.com/python-babel/babel/issues/821 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): name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter]) diff --git a/ihatemoney/history.py b/ihatemoney/history.py index 273cffb87..672d36d28 100644 --- a/ihatemoney/history.py +++ b/ihatemoney/history.py @@ -102,7 +102,6 @@ def get_history(project, human_readable_names=True): "amount": detailed_version.amount, "owers": [describe_version(o) for o in detailed_version.owers], "external_link": detailed_version.external_link, - "original_currency": detailed_version.original_currency, } common_properties["bill_details"] = details diff --git a/ihatemoney/migrations/versions/927ed575acbd_add_currencies.py b/ihatemoney/migrations/versions/927ed575acbd_add_currencies.py index 88b8a5b00..751c2f451 100644 --- a/ihatemoney/migrations/versions/927ed575acbd_add_currencies.py +++ b/ihatemoney/migrations/versions/927ed575acbd_add_currencies.py @@ -12,7 +12,6 @@ from alembic import op import sqlalchemy as sa -from ihatemoney.currency_convertor import CurrencyConverter def upgrade(): @@ -23,7 +22,7 @@ def upgrade(): sa.Column( "original_currency", sa.String(length=3), - server_default=CurrencyConverter.no_currency, + server_default="", nullable=True, ), ) @@ -42,7 +41,7 @@ def upgrade(): sa.Column( "default_currency", sa.String(length=3), - server_default=CurrencyConverter.no_currency, + server_default="", nullable=True, ), ) diff --git a/ihatemoney/models.py b/ihatemoney/models.py index c591b85b6..77c5fd145 100644 --- a/ihatemoney/models.py +++ b/ihatemoney/models.py @@ -20,7 +20,6 @@ from sqlalchemy_continuum import make_versioned, version_class from sqlalchemy_continuum.plugins import FlaskPlugin -from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.monkeypath_continuum import PatchedTransactionFactory from ihatemoney.utils import generate_password_hash, get_members, same_bill from ihatemoney.versioning import ( @@ -86,7 +85,6 @@ def get_by_name(self, name): members = db.relationship("Person", backref="project") query_class = ProjectQuery - default_currency = db.Column(db.String(3)) @property def _to_serialize(self): @@ -96,7 +94,6 @@ def _to_serialize(self): "contact_email": self.contact_email, "logging_preference": self.logging_preference.value, "members": [], - "default_currency": self.default_currency, } balance = self.balance @@ -130,16 +127,14 @@ def full_balance(self): total_weight = sum(ower.weight for ower in bill.owers) if bill.bill_type == BillType.EXPENSE: - should_receive[bill.payer.id] += bill.converted_amount + should_receive[bill.payer.id] += bill.amount for ower in bill.owers: - should_pay[ower.id] += ( - ower.weight * bill.converted_amount / total_weight - ) + should_pay[ower.id] += ower.weight * bill.amount / total_weight if bill.bill_type == BillType.REIMBURSEMENT: - should_receive[bill.payer.id] += bill.converted_amount + should_receive[bill.payer.id] += bill.amount for ower in bill.owers: - should_receive[ower.id] -= bill.converted_amount + should_receive[ower.id] -= bill.amount for person in self.members: balance = should_receive[person.id] - should_pay[person.id] @@ -183,7 +178,7 @@ def monthly_stats(self): monthly = defaultdict(lambda: defaultdict(float)) for bill in self.get_bills_unordered().all(): if bill.bill_type == BillType.EXPENSE: - monthly[bill.date.year][bill.date.month] += bill.converted_amount + monthly[bill.date.year][bill.date.month] += bill.amount return monthly @property @@ -204,7 +199,6 @@ def prettify(transactions, pretty_output): "ower": transaction["ower"].name, "receiver": transaction["receiver"].name, "amount": round(transaction["amount"], 2), - "currency": transaction["currency"], } ) return pretty_transactions @@ -217,7 +211,6 @@ def prettify(transactions, pretty_output): "ower": members[ower_id], "receiver": members[receiver_id], "amount": amount, - "currency": self.default_currency, } for ower_id, amount, receiver_id in settle_plan ] @@ -228,16 +221,6 @@ def has_bills(self): """return if the project do have bills or not""" return self.get_bills_unordered().count() > 0 - def has_multiple_currencies(self): - """Returns True if multiple currencies are used""" - # It would be more efficient to do the counting in the database, - # but this is called very rarely so we can tolerate if it's a bit - # slow. And doing this in Python is much more readable, see #784. - nb_currencies = len( - set(bill.original_currency for bill in self.get_bills_unordered()) - ) - return nb_currencies > 1 - def get_bills_unordered(self): """Base query for bill list""" # The subqueryload option allows to pre-load data from the @@ -344,7 +327,6 @@ def get_pretty_bills(self, export_format="json"): "what": bill.what, "bill_type": bill.bill_type.value, "amount": round(bill.amount, 2), - "currency": bill.original_currency, "date": str(bill.date), "payer_name": Person.query.get(bill.payer_id).name, "payer_weight": Person.query.get(bill.payer_id).weight, @@ -353,41 +335,6 @@ def get_pretty_bills(self, export_format="json"): ) return pretty_bills - def switch_currency(self, new_currency): - if new_currency == self.default_currency: - return - # Update converted currency - if new_currency == CurrencyConverter.no_currency: - if self.has_multiple_currencies(): - raise ValueError(f"Can't unset currency of project {self.id}") - - for bill in self.get_bills_unordered(): - # We are removing the currency, and we already checked that all bills - # had the same currency: it means that we can simply strip the currency - # without converting the amounts. We basically ignore the current default_currency - - # Reset converted amount in case it was different from the original amount - bill.converted_amount = bill.amount - # Strip currency - bill.original_currency = CurrencyConverter.no_currency - db.session.add(bill) - else: - for bill in self.get_bills_unordered(): - if bill.original_currency == CurrencyConverter.no_currency: - # Bills that were created without currency will be set to the new currency - bill.original_currency = new_currency - bill.converted_amount = bill.amount - else: - # Convert amount for others, without touching original_currency - bill.converted_amount = CurrencyConverter().exchange_currency( - bill.amount, bill.original_currency, new_currency - ) - db.session.add(bill) - - self.default_currency = new_currency - db.session.add(self) - db.session.commit() - def import_bills(self, bills: list): """Import bills from a list of dictionaries""" # Add members not already in the project @@ -416,10 +363,8 @@ def import_bills(self, bills: list): date=parse(b["date"]), bill_type=b["bill_type"], external_link="", - original_currency=b["currency"], owers=Person.query.get_by_names(b["owers"], self), payer_id=id_dict[b["payer_name"]], - project_default_currency=self.default_currency, what=b["what"], ) except Exception as e: @@ -527,7 +472,6 @@ def create_demo_project(): name="demonstration", password=generate_password_hash("demo"), contact_email="demo@notmyidea.org", - default_currency="XXX", ) db.session.add(project) db.session.commit() @@ -554,10 +498,8 @@ def create_demo_project(): Bill( amount=amount, bill_type=bill_type, - original_currency=project.default_currency, owers=[members[name] for name in owers], payer_id=members[payer].id, - project_default_currency=project.default_currency, what=what, ) ) @@ -689,22 +631,15 @@ def delete(self, project, id): bill_type = db.Column(db.Enum(BillType)) external_link = db.Column(db.UnicodeText) - original_currency = db.Column(db.String(3)) - converted_amount = db.Column(db.Float) - archive = db.Column(db.Integer, db.ForeignKey("archive.id")) - currency_helper = CurrencyConverter() - def __init__( self, amount: float, date: datetime.datetime = None, external_link: str = "", - original_currency: str = "", owers: list = [], payer_id: int = None, - project_default_currency: str = "", what: str = "", bill_type: str = "Expense", ): @@ -712,14 +647,10 @@ def __init__( self.amount = amount self.date = date self.external_link = external_link - self.original_currency = original_currency self.owers = owers self.payer_id = payer_id self.what = what self.bill_type = BillType(bill_type) - self.converted_amount = self.currency_helper.exchange_currency( - self.amount, self.original_currency, project_default_currency - ) @property def _to_serialize(self): @@ -733,8 +664,6 @@ def _to_serialize(self): "what": self.what, "bill_type": self.bill_type.value, "external_link": self.external_link, - "original_currency": self.original_currency, - "converted_amount": self.converted_amount, } def pay_each_default(self, amount): @@ -759,7 +688,7 @@ def pay_each(self): """Warning: this is slow, if you need to compute this for many bills, do it differently (see balance_full function) """ - return self.pay_each_default(self.converted_amount) + return self.pay_each_default(self.amount) def __repr__(self): return ( diff --git a/ihatemoney/run.py b/ihatemoney/run.py index 23084fe48..f8648c0e5 100644 --- a/ihatemoney/run.py +++ b/ihatemoney/run.py @@ -5,18 +5,16 @@ from babel.dates import LOCALTZ from flask import Flask, g, render_template, request, session -from flask_babel import Babel, format_currency +from flask_babel import Babel from flask_mail import Mail from flask_migrate import Migrate, stamp, upgrade from flask_talisman import Talisman -from jinja2 import pass_context from markupsafe import Markup import pytz from werkzeug.middleware.proxy_fix import ProxyFix from ihatemoney import default_settings from ihatemoney.api.v1 import api as apiv1 -from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.models import db from ihatemoney.utils import ( IhmJSONEncoder, @@ -176,9 +174,6 @@ def create_app( # Configure the a, root="main"pplication setup_database(app) - # Setup Currency Cache - CurrencyConverter() - mail = Mail() mail.init_app(app) app.mail = mail @@ -220,25 +215,6 @@ def get_locale(): else: Babel(app, default_timezone=default_timezone, locale_selector=get_locale) - # Undocumented currencyformat filter from flask_babel is forwarding to Babel format_currency - # We overwrite it to remove the currency sign ¤ when there is no currency - @pass_context - def currency(context, number, currency=None, *args, **kwargs): - if currency is None: - currency = context.get("g").project.default_currency - """ - Same as flask_babel.Babel.currencyformat, but without the "no currency ¤" sign - when there is no currency. - """ - return format_currency( - number, - currency if currency != CurrencyConverter.no_currency else "", - *args, - **kwargs, - ).strip() - - app.jinja_env.filters["currency"] = currency - return app diff --git a/ihatemoney/templates/forms.html b/ihatemoney/templates/forms.html index 9b299d65c..b262287b1 100644 --- a/ihatemoney/templates/forms.html +++ b/ihatemoney/templates/forms.html @@ -99,7 +99,6 @@ - {{ input(form.default_currency) }} {{ input(form.current_password) }}
@@ -198,9 +197,6 @@
{{ _("More options") }} - {% if g.project.default_currency != "XXX" %} - {{ input(form.original_currency, inline=True, class="form-control custom-select") }} - {% endif %} {{ input(form.external_link, inline=True) }}
diff --git a/ihatemoney/templates/history.html b/ihatemoney/templates/history.html index f75b2de14..7f2ad21c4 100644 --- a/ihatemoney/templates/history.html +++ b/ihatemoney/templates/history.html @@ -97,7 +97,7 @@ {% if before %} {{ _("Details of the bill (before the change)") }} {% else %} {{ _("Details of the bill") }} {% endif %} {{ _("Date:") }} {{ details.date|em_surround }}. {{ _("Payer:") }} {{ details.payer|em_surround }}. - {{ _("Amount:") }} {{ details.amount|currency(details.original_currency)|em_surround }}. + {{ _("Amount:") }} {{ details.amount|em_surround }}. {{ _("Owers:") }} {{ owers_list_str }}. {% if details.external_link %} {{ _("External link:") }} @@ -229,10 +229,6 @@ {{ bill_property_change(event, _("Amount")) }} {% elif event.prop_changed == "date" %} {{ bill_property_change(event, _("Date")) }} - {% elif event.prop_changed == "original_currency" %} - {{ bill_property_change(event, _("Currency")) }} - {% elif event.prop_changed == "converted_amount" %} - {{ bill_property_change(event, _("Amount in %(currency)s", currency=g.project.default_currency)) }} {% else %} {% trans %}Bill {{ name }} modified{% endtrans %} {% endif %} diff --git a/ihatemoney/templates/list_bills.html b/ihatemoney/templates/list_bills.html index 79e252625..b8c705779 100644 --- a/ihatemoney/templates/list_bills.html +++ b/ihatemoney/templates/list_bills.html @@ -1,9 +1,9 @@ {% extends "sidebar_table_layout.html" %} -{%- macro weighted_bill_amount(bill, weights, currency=bill.original_currency, amount=bill.amount) %} - {{ amount|currency(currency) }} +{%- macro weighted_bill_amount(bill, weights, amount=bill.amount) %} + {{ amount }} {%- if weights != 1.0 %} - ({{ _("%(amount)s each", amount=(amount / weights)|currency(currency)) }}) + ({{ _("%(amount)s each", amount=(amount / weights)) }}) {%- endif -%} {% endmacro -%} @@ -142,10 +142,7 @@ {{ bill.owers|join(', ', 'name') }} {%- endif %} - - {{ weighted_bill_amount(bill, weights) }} - + {{ weighted_bill_amount(bill, weights) }} {{ _('edit') }} diff --git a/ihatemoney/templates/project_feed.xml b/ihatemoney/templates/project_feed.xml index f805a9cb8..588e46715 100644 --- a/ihatemoney/templates/project_feed.xml +++ b/ihatemoney/templates/project_feed.xml @@ -10,11 +10,11 @@ {{ url_for(".list_bills", _external=True) }} {% for (weights, bill) in bills.items -%} - {{ bill.what }} - {{ bill.amount|currency(bill.original_currency) }} + {{ bill.what }} - {{ bill.amount | round(2) }} {{ bill.id }} {{ bill.payer }} {% if bill.external_link %}{{ bill.external_link }}{% endif -%} - {{ bill.date|dateformat("long") }} - {{ bill.owers|join(', ', 'name') }} : {{ (bill.amount/weights)|currency(bill.original_currency) }} + {{ bill.date|dateformat("long") }} - {{ bill.owers|join(', ', 'name') }} : {{ bill.amount/weights | round(2)}} {{ bill.creation_date.strftime("%a, %d %b %Y %T") }} +0000 {% endfor -%} diff --git a/ihatemoney/templates/settle_bills.html b/ihatemoney/templates/settle_bills.html index d23f9c062..23a38165e 100644 --- a/ihatemoney/templates/settle_bills.html +++ b/ihatemoney/templates/settle_bills.html @@ -15,7 +15,7 @@ {{ bill.ower }} {{ bill.receiver }} - {{ bill.amount|currency }} + {{ bill.amount }} diff --git a/ihatemoney/templates/sidebar_table_layout.html b/ihatemoney/templates/sidebar_table_layout.html index 7450bca15..87643e891 100644 --- a/ihatemoney/templates/sidebar_table_layout.html +++ b/ihatemoney/templates/sidebar_table_layout.html @@ -39,7 +39,7 @@ {%- endif %} {%- endif %} - {% if balance[member.id] | round(2) > 0 %}+{% endif %}{{ balance[member.id]|currency }} + {% if balance[member.id] | round(2) > 0 %}+{% endif %}{{ balance[member.id] }} {%- endfor %} @@ -61,4 +61,4 @@ {% endblock %} {# It must be set outside of the block definition #} -{% set messages_shown = True %} \ No newline at end of file +{% set messages_shown = True %} diff --git a/ihatemoney/templates/statistics.html b/ihatemoney/templates/statistics.html index 86f9cd42e..823b11e3c 100644 --- a/ihatemoney/templates/statistics.html +++ b/ihatemoney/templates/statistics.html @@ -15,8 +15,8 @@ {% for stat in members_stats|sort(attribute='member.name') %} {{ stat.member.name }} - {{ stat.paid|currency }} - {{ stat.spent|currency }} + {{ stat.paid | round(2) }} + {{ stat.spent | round(2) }} {% endfor %} @@ -28,7 +28,7 @@

{{ _("Expenses by Month") }}

{% for month in months %} {{ month|dateformat("MMMM yyyy") }} - {{ monthly_stats[month.year][month.month]|currency }} + {{ monthly_stats[month.year][month.month] | round(2) }} {% endfor %} diff --git a/ihatemoney/tests/api_test.py b/ihatemoney/tests/api_test.py index c676e3e32..fd1b473c5 100644 --- a/ihatemoney/tests/api_test.py +++ b/ihatemoney/tests/api_test.py @@ -2,8 +2,6 @@ import datetime import json -import pytest - from ihatemoney.tests.common.help_functions import em_surround from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase @@ -11,9 +9,7 @@ class TestAPI(IhatemoneyTestCase): """Tests the API""" - def api_create( - self, name, id=None, password=None, contact=None, default_currency=None - ): + def api_create(self, name, id=None, password=None, contact=None): id = id or name password = password or name contact = contact or f"{name}@notmyidea.org" @@ -24,8 +20,6 @@ def api_create( "password": password, "contact_email": contact, } - if default_currency: - data["default_currency"] = default_currency return self.client.post( "/api/projects", @@ -90,7 +84,6 @@ def test_project(self): "id": "raclette", "password": "raclette", "contact_email": "not-an-email", - "default_currency": "XXX", }, ) @@ -124,7 +117,6 @@ def test_project(self): "members": [], "name": "raclette", "contact_email": "raclette@notmyidea.org", - "default_currency": "XXX", "id": "raclette", "logging_preference": 1, } @@ -136,7 +128,6 @@ def test_project(self): "/api/projects/raclette", data={ "contact_email": "yeah@notmyidea.org", - "default_currency": "XXX", "password": "raclette", "name": "The raclette party", "project_history": "y", @@ -150,7 +141,6 @@ def test_project(self): "/api/projects/raclette", data={ "contact_email": "yeah@notmyidea.org", - "default_currency": "XXX", "current_password": "fromage aux patates", "password": "raclette", "name": "The raclette party", @@ -165,7 +155,6 @@ def test_project(self): "/api/projects/raclette", data={ "contact_email": "yeah@notmyidea.org", - "default_currency": "XXX", "current_password": "raclette", "password": "raclette", "name": "The raclette party", @@ -183,7 +172,6 @@ def test_project(self): expected = { "name": "The raclette party", "contact_email": "yeah@notmyidea.org", - "default_currency": "XXX", "members": [], "id": "raclette", "logging_preference": 1, @@ -196,7 +184,6 @@ def test_project(self): "/api/projects/raclette", data={ "contact_email": "yeah@notmyidea.org", - "default_currency": "XXX", "current_password": "raclette", "password": "tartiflette", "name": "The raclette party", @@ -250,7 +237,6 @@ def test_token_creation(self): "/api/projects/raclette", data={ "contact_email": "yeah@notmyidea.org", - "default_currency": "XXX", "password": "tartiflette", "name": "The raclette party", }, @@ -435,8 +421,6 @@ def test_bills(self): "amount": 25.0, "date": "2011-08-10", "id": 1, - "converted_amount": 25.0, - "original_currency": "XXX", "external_link": "https://raclette.fr", } @@ -507,8 +491,6 @@ def test_bills(self): "amount": 25.0, "date": "2011-09-10", "external_link": "https://raclette.fr", - "converted_amount": 25.0, - "original_currency": "XXX", "id": 1, } @@ -588,8 +570,6 @@ def test_bills_with_calculation(self): "date": "2011-08-10", "id": id, "external_link": "", - "original_currency": "XXX", - "converted_amount": expected_amount, } got = json.loads(req.data.decode("utf-8")) @@ -624,169 +604,6 @@ def test_bills_with_calculation(self): ) self.assertStatus(400, req) - @pytest.mark.skip(reason="Currency conversion is broken") - def test_currencies(self): - # check /currencies for list of supported currencies - resp = self.client.get("/api/currencies") - assert 200 == resp.status_code - assert "XXX" in json.loads(resp.data.decode("utf-8")) - - # create project with a default currency - resp = self.api_create("raclette", default_currency="EUR") - assert 201 == resp.status_code - - # get information about it - resp = self.client.get( - "/api/projects/raclette", headers=self.get_auth("raclette") - ) - - assert 200 == resp.status_code - expected = { - "members": [], - "name": "raclette", - "contact_email": "raclette@notmyidea.org", - "default_currency": "EUR", - "id": "raclette", - "logging_preference": 1, - } - decoded_resp = json.loads(resp.data.decode("utf-8")) - assert decoded_resp == expected - - # Add participants - self.api_add_member("raclette", "zorglub") - self.api_add_member("raclette", "jeanne") - self.api_add_member("raclette", "quentin") - - # Add a bill without explicit currency - req = self.client.post( - "/api/projects/raclette/bills", - data={ - "date": "2011-08-10", - "what": "fromage", - "payer": "1", - "payed_for": ["1", "2"], - "bill_type": "Expense", - "amount": "25", - "external_link": "https://raclette.fr", - }, - headers=self.get_auth("raclette"), - ) - - # should return the id - self.assertStatus(201, req) - assert req.data.decode("utf-8") == "1\n" - - # get this bill details - req = self.client.get( - "/api/projects/raclette/bills/1", headers=self.get_auth("raclette") - ) - - # compare with the added info - self.assertStatus(200, req) - expected = { - "what": "fromage", - "payer_id": 1, - "owers": [ - {"activated": True, "id": 1, "name": "zorglub", "weight": 1}, - {"activated": True, "id": 2, "name": "jeanne", "weight": 1}, - ], - "bill_type": "Expense", - "amount": 25.0, - "date": "2011-08-10", - "id": 1, - "converted_amount": 25.0, - "original_currency": "EUR", - "external_link": "https://raclette.fr", - } - - got = json.loads(req.data.decode("utf-8")) - assert ( - datetime.date.today() - == datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date() - ) - del got["creation_date"] - assert expected == got - - # Change bill amount and currency - req = self.client.put( - "/api/projects/raclette/bills/1", - data={ - "date": "2011-08-10", - "what": "fromage", - "payer": "1", - "payed_for": ["1", "2"], - "bill_type": "Expense", - "amount": "30", - "external_link": "https://raclette.fr", - "original_currency": "CAD", - }, - headers=self.get_auth("raclette"), - ) - self.assertStatus(200, req) - - # Check result - req = self.client.get( - "/api/projects/raclette/bills/1", headers=self.get_auth("raclette") - ) - self.assertStatus(200, req) - expected_amount = self.converter.exchange_currency(30.0, "CAD", "EUR") - expected = { - "what": "fromage", - "payer_id": 1, - "owers": [ - {"activated": True, "id": 1, "name": "zorglub", "weight": 1.0}, - {"activated": True, "id": 2, "name": "jeanne", "weight": 1.0}, - ], - "bill_type": "Expense", - "amount": 30.0, - "date": "2011-08-10", - "id": 1, - "converted_amount": expected_amount, - "original_currency": "CAD", - "external_link": "https://raclette.fr", - } - - got = json.loads(req.data.decode("utf-8")) - del got["creation_date"] - assert expected == got - - # Add a bill with yet another currency - req = self.client.post( - "/api/projects/raclette/bills", - data={ - "date": "2011-09-10", - "what": "Pierogi", - "payer": "1", - "payed_for": ["2", "3"], - "bill_type": "Expense", - "amount": "80", - "original_currency": "PLN", - }, - headers=self.get_auth("raclette"), - ) - - # should return the id - self.assertStatus(201, req) - assert req.data.decode("utf-8") == "2\n" - - # Try to remove default project currency, it should fail - req = self.client.put( - "/api/projects/raclette", - data={ - "contact_email": "yeah@notmyidea.org", - "default_currency": "XXX", - "current_password": "raclette", - "password": "raclette", - "name": "The raclette party", - }, - headers=self.get_auth("raclette"), - ) - self.assertStatus(400, req) - assert "This project cannot be set" in req.data.decode("utf-8") - assert "because it contains bills in multiple currencies" in req.data.decode( - "utf-8" - ) - def test_statistics(self): # create a project self.api_create("raclette") @@ -896,8 +713,6 @@ def test_weighted_bills(self): "date": "2011-08-10", "id": 1, "external_link": "", - "converted_amount": 25.0, - "original_currency": "XXX", } got = json.loads(req.data.decode("utf-8")) assert ( @@ -940,7 +755,6 @@ def test_weighted_bills(self): "id": "raclette", "name": "raclette", "logging_preference": 1, - "default_currency": "XXX", } self.assertStatus(200, req) diff --git a/ihatemoney/tests/budget_test.py b/ihatemoney/tests/budget_test.py index a3fc813f8..a34c87dca 100644 --- a/ihatemoney/tests/budget_test.py +++ b/ihatemoney/tests/budget_test.py @@ -4,11 +4,9 @@ from urllib.parse import unquote, urlparse, urlunparse from flask import session, url_for -import pytest from werkzeug.security import check_password_hash from ihatemoney import models -from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.tests.common.help_functions import extract_link from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase from ihatemoney.utils import generate_password_hash @@ -182,7 +180,6 @@ def test_invite_code_invalidation(self): "contact_email": "zorglub@notmyidea.org", "current_password": "raclette", "password": "didoudida", - "default_currency": "XXX", }, follow_redirects=True, ) @@ -259,7 +256,6 @@ def test_project_creation(self): "id": "raclette", "password": "party", "contact_email": "raclette@notmyidea.org", - "default_currency": "USD", }, follow_redirects=True, ) @@ -287,7 +283,6 @@ def test_project_creation(self): "id": "raclette", # already used ! "password": "party", "contact_email": "raclette@notmyidea.org", - "default_currency": "USD", }, ) @@ -305,7 +300,6 @@ def test_project_creation_without_public_permissions(self): "id": "raclette", "password": "party", "contact_email": "raclette@notmyidea.org", - "default_currency": "USD", }, ) @@ -326,7 +320,6 @@ def test_project_creation_with_public_permissions(self): "id": "raclette", "password": "party", "contact_email": "raclette@notmyidea.org", - "default_currency": "USD", }, ) @@ -345,7 +338,6 @@ def test_project_deletion(self): "id": "raclette", "password": "party", "contact_email": "raclette@notmyidea.org", - "default_currency": "USD", }, ) @@ -963,6 +955,7 @@ def test_rounding(self): result[self.get_project("raclette").members[0].id] = 8.12 result[self.get_project("raclette").members[1].id] = 0.0 result[self.get_project("raclette").members[2].id] = -8.12 + # Since we're using floating point to store currency, we can have some # rounding issues that prevent test from working. # However, we should obtain the same values as the theoretical ones if we @@ -979,7 +972,6 @@ def test_edit_project(self): "contact_email": "zorglub@notmyidea.org", "password": "didoudida", "logging_preference": LoggingMode.ENABLED.value, - "default_currency": "USD", } # It should fail if we don't provide the current password @@ -988,7 +980,6 @@ def test_edit_project(self): project = self.get_project("raclette") assert project.name != new_data["name"] assert project.contact_email != new_data["contact_email"] - assert project.default_currency != new_data["default_currency"] assert not check_password_hash(project.password, new_data["password"]) # It should fail if we provide the wrong current password @@ -998,7 +989,6 @@ def test_edit_project(self): project = self.get_project("raclette") assert project.name != new_data["name"] assert project.contact_email != new_data["contact_email"] - assert project.default_currency != new_data["default_currency"] assert not check_password_hash(project.password, new_data["password"]) # It should work if we give the current private code @@ -1008,7 +998,6 @@ def test_edit_project(self): project = self.get_project("raclette") assert project.name == new_data["name"] assert project.contact_email == new_data["contact_email"] - assert project.default_currency == new_data["default_currency"] assert check_password_hash(project.password, new_data["password"]) # Editing a project with a wrong email address should fail @@ -1055,7 +1044,7 @@ def test_statistics_page(self): def test_statistics(self): # Output is checked with the USD sign - self.post_project("raclette", default_currency="USD") + self.post_project("raclette") # add participants self.client.post("/raclette/members/add", data={"name": "zorglub", "weight": 2}) @@ -1117,18 +1106,18 @@ def test_statistics(self): response = self.client.get("/raclette/statistics") regex = r"{}\s*{}\s*{}" assert re.search( - regex.format("zorglub", r"\$20\.00", r"\$31\.67"), + regex.format("zorglub", r"20\.0", r"31\.67"), response.data.decode("utf-8"), ) assert re.search( - regex.format("jeanne", r"\$20\.00", r"\$5\.83"), + regex.format("jeanne", r"20\.0", r"5\.83"), response.data.decode("utf-8"), ) assert re.search( - regex.format("tata", r"\$0\.00", r"\$2\.50"), response.data.decode("utf-8") + regex.format("tata", r"0", r"2\.5"), response.data.decode("utf-8") ) assert re.search( - regex.format("pépé", r"\$0\.00", r"\$0\.00"), response.data.decode("utf-8") + regex.format("pépé", r"0", r"0"), response.data.decode("utf-8") ) # Check that the order of participants in the sidebar table is the @@ -1556,225 +1545,6 @@ def test_access_other_projects(self): member = models.Person.query.filter(models.Person.id == 1).one_or_none() assert member is None - @pytest.mark.skip(reason="Currency conversion is broken") - def test_currency_switch(self): - # A project should be editable - self.post_project("raclette") - - # add participants - self.client.post("/raclette/members/add", data={"name": "zorglub"}) - self.client.post("/raclette/members/add", data={"name": "jeanne"}) - self.client.post("/raclette/members/add", data={"name": "tata"}) - - # create bills - self.client.post( - "/raclette/add", - data={ - "date": "2016-12-31", - "what": "fromage à raclette", - "payer": 1, - "payed_for": [1, 2, 3], - "bill_type": "Expense", - "amount": "10.0", - }, - ) - - self.client.post( - "/raclette/add", - data={ - "date": "2016-12-31", - "what": "red wine", - "payer": 2, - "payed_for": [1, 3], - "bill_type": "Expense", - "amount": "20", - }, - ) - - self.client.post( - "/raclette/add", - data={ - "date": "2017-01-01", - "what": "refund", - "payer": 3, - "payed_for": [2], - "bill_type": "Expense", - "amount": "13.33", - }, - ) - - project = self.get_project("raclette") - - # First all converted_amount should be the same as amount, with no currency - for bill in project.get_bills(): - assert bill.original_currency == CurrencyConverter.no_currency - assert bill.amount == bill.converted_amount - - # Then, switch to EUR, all bills must have been changed to this currency - project.switch_currency("EUR") - for bill in project.get_bills(): - assert bill.original_currency == "EUR" - assert bill.amount == bill.converted_amount - - # Add a bill in EUR, the current default currency - self.client.post( - "/raclette/add", - data={ - "date": "2017-01-01", - "what": "refund from EUR", - "payer": 3, - "payed_for": [2], - "bill_type": "Expense", - "amount": "20", - "original_currency": "EUR", - }, - ) - last_bill = project.get_bills().first() - assert last_bill.converted_amount == last_bill.amount - - # Erase all currencies - project.switch_currency(CurrencyConverter.no_currency) - for bill in project.get_bills(): - assert bill.original_currency == CurrencyConverter.no_currency - assert bill.amount == bill.converted_amount - - # Let's go back to EUR to test conversion - project.switch_currency("EUR") - # This is a bill in CAD - self.client.post( - "/raclette/add", - data={ - "date": "2017-01-01", - "what": "Poutine", - "payer": 3, - "payed_for": [2], - "bill_type": "Expense", - "amount": "18", - "original_currency": "CAD", - }, - ) - last_bill = project.get_bills().first() - expected_amount = self.converter.exchange_currency( - last_bill.amount, "CAD", "EUR" - ) - assert last_bill.converted_amount == expected_amount - - # Switch to USD. Now, NO bill should be in USD, since they already had a currency - project.switch_currency("USD") - for bill in project.get_bills(): - assert bill.original_currency != "USD" - expected_amount = self.converter.exchange_currency( - bill.amount, bill.original_currency, "USD" - ) - assert bill.converted_amount == expected_amount - - # Switching back to no currency must fail - with pytest.raises(ValueError): - project.switch_currency(CurrencyConverter.no_currency) - - # It also must fails with a nice error using the form - resp = self.client.post( - "/raclette/edit", - data={ - "name": "demonstration", - "password": "demo", - "contact_email": "demo@notmyidea.org", - "project_history": "y", - "default_currency": CurrencyConverter.no_currency, - }, - ) - # A user displayed error should be generated, and its currency should be the same. - self.assertStatus(200, resp) - assert '

' in resp.data.decode("utf-8") - assert self.get_project("raclette").default_currency == "USD" - - @pytest.mark.skip(reason="Currency conversion is broken") - def test_currency_switch_to_bill_currency(self): - # Default currency is 'XXX', but we should start from a project with a currency - self.post_project("raclette", default_currency="USD") - - # add participants - self.client.post("/raclette/members/add", data={"name": "zorglub"}) - self.client.post("/raclette/members/add", data={"name": "jeanne"}) - - # Bill with a different currency than project's default - self.client.post( - "/raclette/add", - data={ - "date": "2016-12-31", - "what": "fromage à raclette", - "payer": 1, - "payed_for": [1, 2], - "bill_type": "Expense", - "amount": "10.0", - "original_currency": "EUR", - }, - ) - - project = self.get_project("raclette") - - bill = project.get_bills().first() - assert ( - self.converter.exchange_currency(bill.amount, "EUR", "USD") - == bill.converted_amount - ) - - # And switch project to the currency from the bill we created - project.switch_currency("EUR") - bill = project.get_bills().first() - assert bill.converted_amount == bill.amount - - @pytest.mark.skip(reason="Currency conversion is broken") - def test_currency_switch_to_no_currency(self): - # Default currency is 'XXX', but we should start from a project with a currency - self.post_project("raclette", default_currency="USD") - - # add participants - self.client.post("/raclette/members/add", data={"name": "zorglub"}) - self.client.post("/raclette/members/add", data={"name": "jeanne"}) - - # Bills with a different currency than project's default - self.client.post( - "/raclette/add", - data={ - "date": "2016-12-31", - "what": "fromage à raclette", - "payer": 1, - "payed_for": [1, 2], - "bill_type": "Expense", - "amount": "10.0", - "original_currency": "EUR", - }, - ) - - self.client.post( - "/raclette/add", - data={ - "date": "2017-01-01", - "what": "aspirine", - "payer": 2, - "payed_for": [1, 2], - "bill_type": "Expense", - "amount": "5.0", - "original_currency": "EUR", - }, - ) - - project = self.get_project("raclette") - - for bill in project.get_bills_unordered(): - assert ( - self.converter.exchange_currency(bill.amount, "EUR", "USD") - == bill.converted_amount - ) - - # And switch project to no currency: amount should be equal to what was submitted - project.switch_currency(CurrencyConverter.no_currency) - no_currency_bills = [ - (bill.amount, bill.converted_amount) for bill in project.get_bills() - ] - assert no_currency_bills == [(5.0, 5.0), (10.0, 10.0)] - def test_amount_is_null(self): self.post_project("raclette") @@ -1791,7 +1561,6 @@ def test_amount_is_null(self): "payed_for": [1], "bill_type": "Expense", "amount": "0", - "original_currency": "XXX", }, ) @@ -1836,7 +1605,6 @@ def test_amount_too_high(self): "payed_for": [1], "bill_type": "Expense", "amount": "9347242149381274732472348728748723473278472843.12", - "original_currency": "EUR", }, ) assert '

' in resp.data.decode("utf-8") @@ -1870,7 +1638,7 @@ def test_rss_feed(self): """ Tests that the RSS feed output content is expected. """ - self.post_project("raclette", default_currency="EUR") + self.post_project("raclette") 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": "steven"}) @@ -1883,7 +1651,6 @@ def test_rss_feed(self): "payer": 1, "payed_for": [1, 2, 3], "amount": "12", - "original_currency": "EUR", "bill_type": "Expense", }, ) @@ -1895,7 +1662,6 @@ def test_rss_feed(self): "payer": 2, "payed_for": [1, 2], "amount": "15", - "original_currency": "EUR", "bill_type": "Expense", }, ) @@ -1907,7 +1673,6 @@ def test_rss_feed(self): "payer": 2, "payed_for": [1, 2], "amount": "10", - "original_currency": "EUR", "bill_type": "Expense", }, ) @@ -1925,23 +1690,23 @@ def test_rss_feed(self): http://localhost/raclette/ - fromage à raclette - €12.00 + fromage à raclette - 12.0 1 george - December 31, 2016 - george, peter, steven : €4.00 + December 31, 2016 - george, peter, steven : 4.0 """ in content ) - assert """charcuterie - €15.00""" in content - assert """vin blanc - €10.00""" in content + assert "charcuterie - 15.0" in content + assert "vin blanc - 10.0" in content def test_rss_feed_history_disabled(self): """ Tests that RSS feeds is correctly rendered even if the project history is disabled. """ - self.post_project("raclette", default_currency="EUR", project_history=False) + self.post_project("raclette", project_history=False) 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": "steven"}) @@ -1954,7 +1719,6 @@ def test_rss_feed_history_disabled(self): "payer": 1, "payed_for": [1, 2, 3], "amount": "12", - "original_currency": "EUR", "bill_type": "Expense", }, ) @@ -1966,7 +1730,6 @@ def test_rss_feed_history_disabled(self): "payer": 2, "payed_for": [1, 2], "amount": "15", - "original_currency": "EUR", "bill_type": "Expense", }, ) @@ -1978,7 +1741,6 @@ def test_rss_feed_history_disabled(self): "payer": 2, "payed_for": [1, 2], "amount": "10", - "original_currency": "EUR", "bill_type": "Expense", }, ) @@ -1988,8 +1750,8 @@ def test_rss_feed_history_disabled(self): resp = self.client.get(f"/raclette/feed/{token}.xml") content = resp.data.decode() - assert """charcuterie - €15.00""" in content - assert """vin blanc - €10.00""" in content + assert """charcuterie - 15.0""" in content + assert """vin blanc - 10.0""" in content def test_rss_if_modified_since_header(self): # Project creation @@ -2036,7 +1798,6 @@ def test_rss_if_modified_since_header(self): "payer": 1, "payed_for": [1], "amount": "12", - "original_currency": "XXX", "bill_type": "Expense", }, follow_redirects=True, @@ -2094,7 +1855,6 @@ def test_rss_etag_headers(self): "payed_for": [1], "amount": "12", "bill_type": "Expense", - "original_currency": "XXX", }, follow_redirects=True, ) @@ -2175,7 +1935,6 @@ def test_rss_feed_invalidated_token(self): "contact_email": "zorglub@notmyidea.org", "current_password": "raclette", "password": "didoudida", - "default_currency": "XXX", }, follow_redirects=True, ) diff --git a/ihatemoney/tests/common/ihatemoney_testcase.py b/ihatemoney/tests/common/ihatemoney_testcase.py index 6369a8aac..df4e007ec 100644 --- a/ihatemoney/tests/common/ihatemoney_testcase.py +++ b/ihatemoney/tests/common/ihatemoney_testcase.py @@ -6,7 +6,7 @@ from ihatemoney.utils import generate_password_hash -@pytest.mark.usefixtures("client", "converter") +@pytest.mark.usefixtures("client") class BaseTestCase: SECRET_KEY = "TEST SESSION" SQLALCHEMY_DATABASE_URI = os.environ.get( @@ -29,7 +29,6 @@ def post_project( self, id, follow_redirects=True, - default_currency="XXX", name=None, password=None, project_history=True, @@ -45,7 +44,6 @@ def post_project( "id": id, "password": password, "contact_email": f"{id}@notmyidea.org", - "default_currency": default_currency, "project_history": project_history, }, follow_redirects=follow_redirects, @@ -59,7 +57,7 @@ def import_project(self, id, data, success=True): ) assert ("/{id}/edit" in str(resp.response)) == (not success) - def create_project(self, id, default_currency="XXX", name=None, password=None): + def create_project(self, id, name=None, password=None): name = name or str(id) password = password or id project = models.Project( @@ -67,7 +65,6 @@ def create_project(self, id, default_currency="XXX", name=None, password=None): name=name, password=generate_password_hash(password), contact_email=f"{id}@notmyidea.org", - default_currency=default_currency, ) models.db.session.add(project) models.db.session.commit() diff --git a/ihatemoney/tests/conftest.py b/ihatemoney/tests/conftest.py index ebeba15fe..8e32a3ec3 100644 --- a/ihatemoney/tests/conftest.py +++ b/ihatemoney/tests/conftest.py @@ -1,11 +1,8 @@ -from unittest.mock import MagicMock - from flask import Flask from jinja2 import FileSystemBytecodeCache import pytest from ihatemoney.babel_utils import compile_catalogs -from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.run import create_app, db @@ -44,21 +41,3 @@ def client(app: Flask, request: pytest.FixtureRequest): request.cls.client = client yield client - - -@pytest.fixture -def converter(request: pytest.FixtureRequest): - # Add dummy data to CurrencyConverter for all tests (since it's a singleton) - mock_data = { - "USD": 1, - "EUR": 0.8, - "CAD": 1.2, - "PLN": 4, - CurrencyConverter.no_currency: 1, - } - converter = CurrencyConverter() - converter.get_rates = MagicMock(return_value=mock_data) - # Also add it to an attribute to make tests clearer - request.cls.converter = converter - - yield converter diff --git a/ihatemoney/tests/history_test.py b/ihatemoney/tests/history_test.py index 828a23ca4..595f96a3f 100644 --- a/ihatemoney/tests/history_test.py +++ b/ihatemoney/tests/history_test.py @@ -17,7 +17,6 @@ def demo(client): "id": "demo", "password": "demo", "contact_email": "demo@notmyidea.org", - "default_currency": "XXX", "project_history": True, }, ) @@ -43,7 +42,6 @@ def change_privacy_to(self, current_password, logging_preference): "contact_email": "demo@notmyidea.org", "current_password": current_password, "password": "demo", - "default_currency": "XXX", } if logging_preference != LoggingMode.DISABLED: @@ -93,7 +91,6 @@ def test_project_edit(self): "current_password": "demo", "password": "123456", "project_history": "y", - "default_currency": "USD", # Currency changed from default } resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True) @@ -114,7 +111,7 @@ def test_project_edit(self): assert resp.data.decode("utf-8").index("Project renamed ") < resp.data.decode( "utf-8" ).index("Project private code changed") - assert resp.data.decode("utf-8").count(" -- ") == 5 + assert resp.data.decode("utf-8").count(" -- ") == 4 assert "127.0.0.1" not in resp.data.decode("utf-8") def test_project_privacy_edit(self): @@ -184,7 +181,6 @@ def do_misc_database_operations(self, logging_mode): "contact_email": "demo2@notmyidea.org", "current_password": "demo", "password": "123456", - "default_currency": "USD", } # Keep privacy settings where they were @@ -307,7 +303,7 @@ def test_clear_ip_records(self): ) assert "Nothing to list" not in resp.data.decode("utf-8") assert "Some entries below contain IP addresses," in resp.data.decode("utf-8") - assert resp.data.decode("utf-8").count("127.0.0.1") == 12 + assert resp.data.decode("utf-8").count("127.0.0.1") == 10 assert resp.data.decode("utf-8").count(" -- ") == 1 # Generate more operations to confirm additional IP info isn't recorded @@ -315,8 +311,8 @@ def test_clear_ip_records(self): resp = self.client.get("/demo/history") assert resp.status_code == 200 - assert resp.data.decode("utf-8").count("127.0.0.1") == 12 - assert resp.data.decode("utf-8").count(" -- ") == 7 + assert resp.data.decode("utf-8").count("127.0.0.1") == 10 + assert resp.data.decode("utf-8").count(" -- ") == 6 # Ensure we can't clear IP data with a GET or with a password-less POST resp = self.client.get("/demo/strip_ip_addresses") @@ -326,8 +322,8 @@ def test_clear_ip_records(self): resp = self.client.get("/demo/history") assert resp.status_code == 200 - assert resp.data.decode("utf-8").count("127.0.0.1") == 12 - assert resp.data.decode("utf-8").count(" -- ") == 7 + assert resp.data.decode("utf-8").count("127.0.0.1") == 10 + assert resp.data.decode("utf-8").count(" -- ") == 6 # Clear IP Data resp = self.client.post( @@ -350,7 +346,7 @@ def test_clear_ip_records(self): "utf-8" ) assert resp.data.decode("utf-8").count("127.0.0.1") == 0 - assert resp.data.decode("utf-8").count(" -- ") == 19 + assert resp.data.decode("utf-8").count(" -- ") == 16 def test_logs_for_common_actions(self): # adds a member to this project @@ -638,7 +634,6 @@ def test_delete_history_with_project(self): "payed_for": [1], "bill_type": "Expense", "amount": "10", - "original_currency": "EUR", }, ) diff --git a/ihatemoney/tests/import_test.py b/ihatemoney/tests/import_test.py index 4e93bb56e..afb86595f 100644 --- a/ihatemoney/tests/import_test.py +++ b/ihatemoney/tests/import_test.py @@ -81,50 +81,9 @@ def populate_data_with_currencies(self, currencies): for d in range(len(self.data)): self.data[d]["currency"] = currencies[d] - def test_import_currencies_in_empty_project_with_currency(self): - # Import JSON with currencies in an empty project with a default currency - - self.post_project("raclette", default_currency="EUR") - self.login("raclette") - - project = self.get_project("raclette") - - self.populate_data_with_currencies(["EUR", "CAD", "EUR"]) - self.import_project("raclette", self.generate_form_data(self.data)) - - bills = project.get_pretty_bills() - - # Check if all bills have been added - assert len(bills) == len(self.data) - - # Check if name of bills are ok - b = [e["what"] for e in bills] - b.sort() - ref = [e["what"] for e in self.data] - ref.sort() - - assert b == ref - - # Check if other informations in bill are ok - for d in self.data: - for b in bills: - if b["what"] == d["what"]: - assert b["payer_name"] == d["payer_name"] - assert b["amount"] == d["amount"] - assert b["currency"] == d["currency"] - assert b["payer_weight"] == d["payer_weight"] - assert b["date"] == d["date"] - assert b["bill_type"] == d["bill_type"] - list_project = [ower for ower in b["owers"]] - list_project.sort() - list_json = [ower for ower in d["owers"]] - list_json.sort() - assert list_project == list_json - - def test_import_single_currency_in_empty_project_without_currency(self): - # Import JSON with a single currency in an empty project with no - # default currency. It should work by stripping the currency from - # bills. + def test_import_single_currency_in_empty_project(self): + # Import JSON with a single currency in an empty project + # It should work by stripping the currency from bills. self.post_project("raclette") self.login("raclette") @@ -153,8 +112,6 @@ def test_import_single_currency_in_empty_project_without_currency(self): if b["what"] == d["what"]: assert b["payer_name"] == d["payer_name"] assert b["amount"] == d["amount"] - # Currency should have been stripped - assert b["currency"] == "XXX" assert b["payer_weight"] == d["payer_weight"] assert b["date"] == d["date"] assert b["bill_type"] == d["bill_type"] @@ -182,51 +139,7 @@ def test_import_multiple_currencies_in_empty_project_without_currency(self): # Check that there are no bills assert len(bills) == 0 - def test_import_no_currency_in_empty_project_with_currency(self): - # Import JSON without currencies (from ihatemoney < 5) in an empty - # project with a default currency. - - self.post_project("raclette", default_currency="EUR") - self.login("raclette") - - project = self.get_project("raclette") - - self.import_project("raclette", self.generate_form_data(self.data)) - - bills = project.get_pretty_bills() - - # Check if all bills have been added - assert len(bills) == len(self.data) - - # Check if name of bills are ok - b = [e["what"] for e in bills] - b.sort() - ref = [e["what"] for e in self.data] - ref.sort() - - assert b == ref - - # Check if other informations in bill are ok - for d in self.data: - for b in bills: - if b["what"] == d["what"]: - assert b["payer_name"] == d["payer_name"] - assert b["amount"] == d["amount"] - # All bills are converted to default project currency - assert b["currency"] == "EUR" - assert b["payer_weight"] == d["payer_weight"] - assert b["date"] == d["date"] - assert b["bill_type"] == d["bill_type"] - list_project = [ower for ower in b["owers"]] - list_project.sort() - list_json = [ower for ower in d["owers"]] - list_json.sort() - assert list_project == list_json - - def test_import_no_currency_in_empty_project_without_currency(self): - # Import JSON without currencies (from ihatemoney < 5) in an empty - # project with no default currency. - + def test_import_no_currency_in_empty_project(self): self.post_project("raclette") self.login("raclette") @@ -253,7 +166,6 @@ def test_import_no_currency_in_empty_project_without_currency(self): if b["what"] == d["what"]: assert b["payer_name"] == d["payer_name"] assert b["amount"] == d["amount"] - assert b["currency"] == "XXX" assert b["payer_weight"] == d["payer_weight"] assert b["date"] == d["date"] assert b["bill_type"] == d["bill_type"] @@ -288,8 +200,6 @@ def test_import_partial_project(self): }, ) - self.populate_data_with_currencies(["XXX", "XXX", "XXX"]) - self.import_project("raclette", self.generate_form_data(self.data)) bills = project.get_pretty_bills() @@ -311,7 +221,6 @@ def test_import_partial_project(self): if b["what"] == d["what"]: assert b["payer_name"] == d["payer_name"] assert b["amount"] == d["amount"] - assert b["currency"] == d["currency"] assert b["payer_weight"] == d["payer_weight"] assert b["date"] == d["date"] assert b["bill_type"] == d["bill_type"] @@ -406,7 +315,6 @@ def test_export(self): "bill_type": "Reimbursement", "what": "refund", "amount": 13.33, - "currency": "XXX", "payer_name": "tata", "payer_weight": 1.0, "owers": ["jeanne"], @@ -416,7 +324,6 @@ def test_export(self): "bill_type": "Expense", "what": "red wine", "amount": 200.0, - "currency": "XXX", "payer_name": "jeanne", "payer_weight": 1.0, "owers": ["zorglub", "tata"], @@ -426,7 +333,6 @@ def test_export(self): "bill_type": "Expense", "what": "\xe0 raclette", "amount": 10.0, - "currency": "XXX", "payer_name": "zorglub", "payer_weight": 2.0, "owers": ["zorglub", "jeanne", "tata", "p\xe9p\xe9"], @@ -437,10 +343,10 @@ def test_export(self): # generate csv export of bills resp = self.client.get("/raclette/export/bills.csv") expected = [ - "date,what,bill_type,amount,currency,payer_name,payer_weight,owers", - "2017-01-01,refund,Reimbursement,XXX,13.33,tata,1.0,jeanne", - '2016-12-31,red wine,Expense,XXX,200.0,jeanne,1.0,"zorglub, tata"', - '2016-12-31,à raclette,Expense,10.0,XXX,zorglub,2.0,"zorglub, jeanne, tata, pépé"', + "date,what,bill_type,amount,payer_name,payer_weight,owers", + "2017-01-01,refund,Reimbursement,13.33,tata,1.0,jeanne", + '2016-12-31,red wine,Expense,200.0,jeanne,1.0,"zorglub, tata"', + '2016-12-31,à raclette,Expense,10.0,zorglub,2.0,"zorglub, jeanne, tata, pépé"', ] received_lines = resp.data.decode("utf-8").split("\n") @@ -452,14 +358,12 @@ def test_export(self): expected = [ { "amount": 2.00, - "currency": "XXX", "receiver": "jeanne", "ower": "p\xe9p\xe9", }, - {"amount": 55.34, "currency": "XXX", "receiver": "jeanne", "ower": "tata"}, + {"amount": 55.34, "receiver": "jeanne", "ower": "tata"}, { "amount": 127.33, - "currency": "XXX", "receiver": "jeanne", "ower": "zorglub", }, @@ -471,10 +375,10 @@ def test_export(self): resp = self.client.get("/raclette/export/transactions.csv") expected = [ - "amount,currency,receiver,ower", - "2.0,XXX,jeanne,pépé", - "55.34,XXX,jeanne,tata", - "127.33,XXX,jeanne,zorglub", + "amount,receiver,ower", + "2.0,jeanne,pépé", + "55.34,jeanne,tata", + "127.33,jeanne,zorglub", ] received_lines = resp.data.decode("utf-8").split("\n") @@ -485,179 +389,8 @@ def test_export(self): resp = self.client.get("/raclette/export/transactions.wrong") assert resp.status_code == 404 - @pytest.mark.skip(reason="Currency conversion is broken") - def test_export_with_currencies(self): - self.post_project("raclette", default_currency="EUR") - - # add participants - self.client.post("/raclette/members/add", data={"name": "zorglub", "weight": 2}) - self.client.post("/raclette/members/add", data={"name": "jeanne"}) - self.client.post("/raclette/members/add", data={"name": "tata"}) - self.client.post("/raclette/members/add", data={"name": "pépé"}) - - # create bills - self.client.post( - "/raclette/add", - data={ - "date": "2016-12-31", - "what": "à raclette", - "bill_type": "Expense", - "payer": 1, - "payed_for": [1, 2, 3, 4], - "amount": "10.0", - "original_currency": "EUR", - }, - ) - - self.client.post( - "/raclette/add", - data={ - "date": "2016-12-31", - "what": "poutine from Québec", - "bill_type": "Expense", - "payer": 2, - "payed_for": [1, 3], - "amount": "100", - "original_currency": "CAD", - }, - ) - - self.client.post( - "/raclette/add", - data={ - "date": "2017-01-01", - "what": "refund", - "bill_type": "Reimbursement", - "payer": 3, - "payed_for": [2], - "amount": "13.33", - "original_currency": "EUR", - }, - ) - - # generate json export of bills - resp = self.client.get("/raclette/export/bills.json") - expected = [ - { - "date": "2017-01-01", - "what": "refund", - "bill_type": "Reimbursement", - "amount": 13.33, - "currency": "EUR", - "payer_name": "tata", - "payer_weight": 1.0, - "owers": ["jeanne"], - }, - { - "date": "2016-12-31", - "what": "poutine from Qu\xe9bec", - "bill_type": "Expense", - "amount": 100.0, - "currency": "CAD", - "payer_name": "jeanne", - "payer_weight": 1.0, - "owers": ["zorglub", "tata"], - }, - { - "date": "2016-12-31", - "what": "fromage \xe0 raclette", - "bill_type": "Expense", - "amount": 10.0, - "currency": "EUR", - "payer_name": "zorglub", - "payer_weight": 2.0, - "owers": ["zorglub", "jeanne", "tata", "p\xe9p\xe9"], - }, - ] - assert json.loads(resp.data.decode("utf-8")) == expected - - # generate csv export of bills - resp = self.client.get("/raclette/export/bills.csv") - expected = [ - "date,what,bill_type,amount,currency,payer_name,payer_weight,owers", - "2017-01-01,refund,Reimbursement,13.33,EUR,tata,1.0,jeanne", - '2016-12-31,poutine from Québec,Expense,100.0,CAD,jeanne,1.0,"zorglub, tata"', - '2016-12-31,à raclette,Expense,10.0,EUR,zorglub,2.0,"zorglub, jeanne, tata, pépé"', - ] - received_lines = resp.data.decode("utf-8").split("\n") - - for i, line in enumerate(expected): - assert set(line.split(",")) == set(received_lines[i].strip("\r").split(",")) - - # generate json export of transactions (in EUR!) - resp = self.client.get("/raclette/export/transactions.json") - expected = [ - { - "amount": 2.00, - "currency": "EUR", - "receiver": "jeanne", - "ower": "p\xe9p\xe9", - }, - {"amount": 10.89, "currency": "EUR", "receiver": "jeanne", "ower": "tata"}, - { - "amount": 38.45, - "currency": "EUR", - "receiver": "jeanne", - "ower": "zorglub", - }, - ] - - assert json.loads(resp.data.decode("utf-8")) == expected - - # generate csv export of transactions - resp = self.client.get("/raclette/export/transactions.csv") - - expected = [ - "amount,currency,receiver,ower", - "2.0,EUR,jeanne,pépé", - "10.89,EUR,jeanne,tata", - "38.45,EUR,jeanne,zorglub", - ] - received_lines = resp.data.decode("utf-8").split("\n") - - for i, line in enumerate(expected): - assert set(line.split(",")) == set(received_lines[i].strip("\r").split(",")) - - # Change project currency to CAD - project = self.get_project("raclette") - project.switch_currency("CAD") - - # generate json export of transactions (now in CAD!) - resp = self.client.get("/raclette/export/transactions.json") - expected = [ - { - "amount": 3.00, - "currency": "CAD", - "receiver": "jeanne", - "ower": "p\xe9p\xe9", - }, - {"amount": 16.34, "currency": "CAD", "receiver": "jeanne", "ower": "tata"}, - { - "amount": 57.67, - "currency": "CAD", - "receiver": "jeanne", - "ower": "zorglub", - }, - ] - - assert json.loads(resp.data.decode("utf-8")) == expected - - # generate csv export of transactions - resp = self.client.get("/raclette/export/transactions.csv") - - expected = [ - "amount,currency,receiver,ower", - "3.0,CAD,jeanne,pépé", - "16.34,CAD,jeanne,tata", - "57.67,CAD,jeanne,zorglub", - ] - received_lines = resp.data.decode("utf-8").split("\n") - - for i, line in enumerate(expected): - assert set(line.split(",")) == set(received_lines[i].strip("\r").split(",")) - def test_export_escape_formulae(self): - self.post_project("raclette", default_currency="EUR") + self.post_project("raclette") # add participants self.client.post("/raclette/members/add", data={"name": "zorglub"}) @@ -672,15 +405,14 @@ def test_export_escape_formulae(self): "payer": 1, "payed_for": [1], "amount": "10.0", - "original_currency": "EUR", }, ) # generate csv export of bills resp = self.client.get("/raclette/export/bills.csv") expected = [ - "date,what,bill_type,amount,currency,payer_name,payer_weight,owers", - "2016-12-31,'=COS(36),Expense,10.0,EUR,zorglub,1.0,zorglub", + "date,what,bill_type,amount,payer_name,payer_weight,owers", + "2016-12-31,'=COS(36),Expense,10.0,zorglub,1.0,zorglub", ] received_lines = resp.data.decode("utf-8").split("\n") diff --git a/ihatemoney/tests/main_test.py b/ihatemoney/tests/main_test.py index 4d131e0a7..5fea0712e 100644 --- a/ihatemoney/tests/main_test.py +++ b/ihatemoney/tests/main_test.py @@ -7,7 +7,6 @@ from werkzeug.security import check_password_hash from ihatemoney import models -from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.manage import ( delete_project, generate_config, @@ -376,7 +375,6 @@ def test_project_creation_with_captcha_case_insensitive(self): "id": "raclette", "password": "party", "contact_email": "raclette@notmyidea.org", - "default_currency": "USD", "captcha": "éùüß", }, ) @@ -391,7 +389,6 @@ def test_project_creation_with_captcha(self): "id": "raclette", "password": "party", "contact_email": "raclette@notmyidea.org", - "default_currency": "USD", }, ) assert len(models.Project.query.all()) == 0 @@ -403,7 +400,6 @@ def test_project_creation_with_captcha(self): "id": "raclette", "password": "party", "contact_email": "raclette@notmyidea.org", - "default_currency": "USD", "captcha": "nope", }, ) @@ -416,7 +412,6 @@ def test_project_creation_with_captcha(self): "id": "raclette", "password": "party", "contact_email": "raclette@notmyidea.org", - "default_currency": "USD", "captcha": "euro", }, ) @@ -435,37 +430,3 @@ def test_api_project_creation_does_not_need_captcha(self): ) assert resp.status_code == 201 assert len(models.Project.query.all()) == 1 - - -class TestCurrencyConverter: - converter = CurrencyConverter() - mock_data = { - "USD": 1, - "EUR": 0.8, - "CAD": 1.2, - "PLN": 4, - CurrencyConverter.no_currency: 1, - } - converter.get_rates = MagicMock(return_value=mock_data) - - def test_only_one_instance(self): - one = id(CurrencyConverter()) - two = id(CurrencyConverter()) - assert one == two - - def test_get_currencies(self): - currencies = self.converter.get_currencies() - for currency in ["USD", "EUR", "CAD", "PLN", CurrencyConverter.no_currency]: - assert currency in currencies - - def test_exchange_currency(self): - result = self.converter.exchange_currency(100, "USD", "EUR") - assert result == 80.0 - - def test_failing_remote(self): - rates = {} - with patch("requests.Response.json", new=lambda _: {}): - # we need a non-patched converter, but it seems that MagickMock - # is mocking EVERY instance of the class method. Too bad. - rates = CurrencyConverter.get_rates(self.converter) - assert rates == {CurrencyConverter.no_currency: 1} diff --git a/ihatemoney/utils.py b/ihatemoney/utils.py index 7af4967bc..32c9b9583 100644 --- a/ihatemoney/utils.py +++ b/ihatemoney/utils.py @@ -11,7 +11,6 @@ import socket from babel import Locale -from babel.numbers import get_currency_name, get_currency_symbol from flask import current_app, flash, redirect, render_template from flask_babel import get_locale, lazy_gettext as _ from flask_limiter import Limiter @@ -309,7 +308,6 @@ def same_bill(bill1, bill2): "payer_name", "payer_weight", "amount", - "currency", "date", "owers", ] @@ -410,21 +408,6 @@ def localize_list(items, surround_with_em=True): return output_str.format(start_object=wrapped_items.pop()) -def render_localized_currency(code, detailed=True): - # We cannot use CurrencyConvertor.no_currency here because of circular dependencies - if code == "XXX": - return _("No Currency") - locale = get_locale() or "en_US" - symbol = get_currency_symbol(code, locale=locale) - details = "" - if detailed: - details = f" − {get_currency_name(code, locale=locale)}" - if symbol == code: - return f"{code}{details}" - else: - return f"{code} − {symbol}{details}" - - def render_localized_template(template_name_prefix, **context): """Like render_template(), but selects the right template according to the current user language. Fallback to English if a template for the diff --git a/ihatemoney/web.py b/ihatemoney/web.py index 43b04c213..75b5ba8e0 100644 --- a/ihatemoney/web.py +++ b/ihatemoney/web.py @@ -40,7 +40,6 @@ from werkzeug.exceptions import NotFound from werkzeug.security import check_password_hash -from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.emails import send_creation_email from ihatemoney.forms import ( AdminAuthenticationForm, @@ -448,7 +447,6 @@ def edit_project(): edit_form.ip_recording.data = True edit_form.contact_email.data = g.project.contact_email - edit_form.default_currency.data = g.project.default_currency return render_template( "edit_project.html", @@ -479,7 +477,6 @@ def import_project(): attr = [ "amount", "bill_type", - "currency", "date", "owers", "payer_name", @@ -488,28 +485,22 @@ def import_project(): ] currencies = set() for b in bills: - if b.get("currency", "") in ["", "XXX"]: - b["currency"] = g.project.default_currency + if "currency" in b.keys(): + currencies.add(b["currency"]) + del b["currency"] for a in attr: if a not in b: raise ValueError( _("Missing attribute: %(attribute)s", attribute=a) ) - currencies.add(b["currency"]) - - # Additional checks if project has no default currency - if g.project.default_currency == CurrencyConverter.no_currency: - # If bills have currencies, they must be consistent - if len(currencies - {CurrencyConverter.no_currency}) >= 2: - raise ValueError( - _( - "Cannot add bills in multiple currencies to a project without default " - "currency" - ) + + if len(currencies) > 1: + raise ValueError( + _( + "Cannot add bills in multiple currencies to a project without default " + "currency" ) - # Strip currency from bills (since it's the same for every bill) - for b in bills: - b["currency"] = CurrencyConverter.no_currency + ) g.project.import_bills(bills) @@ -863,7 +854,6 @@ def settle(amount, ower_id, payer_id): date=datetime.datetime.today(), owers=[Person.query.get(payer_id)], payer_id=ower_id, - project_default_currency=g.project.default_currency, bill_type=BillType.REIMBURSEMENT, what=_("Settlement"), )