diff --git a/ihatemoney/currency_convertor.py b/ihatemoney/currency_convertor.py index 75fa8342..10026eea 100644 --- a/ihatemoney/currency_convertor.py +++ b/ihatemoney/currency_convertor.py @@ -13,7 +13,7 @@ class Singleton(type): class CurrencyConverter(object, metaclass=Singleton): # Get exchange rates - default = "No Currency" + no_currency = "XXX" api_url = "https://api.exchangeratesapi.io/latest?base=USD" def __init__(self): @@ -22,19 +22,23 @@ class CurrencyConverter(object, metaclass=Singleton): @cached(cache=TTLCache(maxsize=1, ttl=86400)) def get_rates(self): rates = requests.get(self.api_url).json()["rates"] - rates[self.default] = 1.0 + rates[self.no_currency] = 1.0 return rates - def get_currencies(self): - rates = [rate for rate in self.get_rates()] - rates.sort(key=lambda rate: "" if rate == self.default else rate) + def get_currencies(self, with_no_currency=True): + rates = [ + rate + for rate in self.get_rates() + if with_no_currency or rate != self.no_currency + ] + rates.sort(key=lambda rate: "" if rate == self.no_currency else rate) return rates def exchange_currency(self, amount, source_currency, dest_currency): if ( source_currency == dest_currency - or source_currency == self.default - or dest_currency == self.default + or source_currency == self.no_currency + or dest_currency == self.no_currency ): return amount diff --git a/ihatemoney/forms.py b/ihatemoney/forms.py index 7a6a57e4..8fd13f6d 100644 --- a/ihatemoney/forms.py +++ b/ihatemoney/forms.py @@ -1,4 +1,3 @@ -import copy from datetime import datetime from re import match @@ -23,7 +22,11 @@ from wtforms.validators import ( from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.models import LoggingMode, Person, Project -from ihatemoney.utils import eval_arithmetic_expression, slugify +from ihatemoney.utils import ( + eval_arithmetic_expression, + render_localized_currency, + slugify, +) def strip_filter(string): @@ -33,18 +36,6 @@ def strip_filter(string): return string -def get_editprojectform_for(project, **kwargs): - """Return an instance of EditProjectForm configured for a particular project. - """ - form = EditProjectForm(**kwargs) - choices = copy.copy(form.default_currency.choices) - choices.sort( - key=lambda rates: "" if rates[0] == project.default_currency else rates[0] - ) - form.default_currency.choices = choices - return form - - def get_billform_for(project, set_default=True, **kwargs): """Return an instance of BillForm configured for a particular project. @@ -56,20 +47,15 @@ def get_billform_for(project, set_default=True, **kwargs): if form.original_currency.data == "None": form.original_currency.data = project.default_currency - if form.original_currency.data != CurrencyConverter.default: - choices = copy.copy(form.original_currency.choices) - choices.remove((CurrencyConverter.default, CurrencyConverter.default)) - choices.sort( - key=lambda rates: "" if rates[0] == project.default_currency else rates[0] - ) - form.original_currency.choices = choices - else: - form.original_currency.render_kw = {"default": True} - form.original_currency.data = CurrencyConverter.default + 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 + ) + ] - form.original_currency.label = Label( - "original_currency", "Currency (Default: %s)" % (project.default_currency) - ) active_members = [(m.id, m.name) for m in project.active_members] form.payed_for.choices = form.payer.choices = active_members @@ -121,14 +107,14 @@ class EditProjectForm(FlaskForm): project_history = BooleanField(_("Enable project history")) ip_recording = BooleanField(_("Use IP tracking for project history")) currency_helper = CurrencyConverter() - default_currency = SelectField( - _("Default Currency"), - choices=[ - (currency_name, currency_name) - for currency_name in currency_helper.get_currencies() - ], - validators=[DataRequired()], - ) + default_currency = SelectField(_("Default Currency"), validators=[DataRequired()],) + + def __init__(self, *args, **kwargs): + 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() + ] @property def logging_preference(self): @@ -242,14 +228,7 @@ class BillForm(FlaskForm): payer = SelectField(_("Payer"), validators=[DataRequired()], coerce=int) amount = CalculatorStringField(_("Amount paid"), validators=[DataRequired()]) currency_helper = CurrencyConverter() - original_currency = SelectField( - _("Currency"), - choices=[ - (currency_name, currency_name) - for currency_name in currency_helper.get_currencies() - ], - validators=[DataRequired()], - ) + original_currency = SelectField(_("Currency"), validators=[DataRequired()],) external_link = URLField( _("External link"), validators=[Optional()], @@ -281,14 +260,14 @@ class BillForm(FlaskForm): bill.external_link = "" bill.date = self.date bill.owers = [Person.query.get(ower, project) for ower in self.payed_for] - bill.original_currency = CurrencyConverter.default + bill.original_currency = CurrencyConverter.no_currency bill.converted_amount = self.currency_helper.exchange_currency( bill.amount, bill.original_currency, project.default_currency ) return bill - def fill(self, bill): + def fill(self, bill, project): self.payer.data = bill.payer_id self.amount.data = bill.amount self.what.data = bill.what @@ -297,6 +276,14 @@ class BillForm(FlaskForm): 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 diff --git a/ihatemoney/migrations/versions/927ed575acbd_add_currencies.py b/ihatemoney/migrations/versions/927ed575acbd_add_currencies.py index b70d9025..88b8a5b0 100644 --- a/ihatemoney/migrations/versions/927ed575acbd_add_currencies.py +++ b/ihatemoney/migrations/versions/927ed575acbd_add_currencies.py @@ -23,7 +23,7 @@ def upgrade(): sa.Column( "original_currency", sa.String(length=3), - server_default=CurrencyConverter.default, + server_default=CurrencyConverter.no_currency, nullable=True, ), ) @@ -42,7 +42,7 @@ def upgrade(): sa.Column( "default_currency", sa.String(length=3), - server_default=CurrencyConverter.default, + server_default=CurrencyConverter.no_currency, nullable=True, ), ) diff --git a/ihatemoney/run.py b/ihatemoney/run.py index b6c8cbb2..e084e5bc 100644 --- a/ihatemoney/run.py +++ b/ihatemoney/run.py @@ -3,7 +3,7 @@ import os.path import warnings from flask import Flask, g, render_template, request, session -from flask_babel import Babel +from flask_babel import Babel, format_currency from flask_mail import Mail from flask_migrate import Migrate, stamp, upgrade from werkzeug.middleware.proxy_fix import ProxyFix @@ -153,6 +153,22 @@ def create_app( # Translations babel = Babel(app) + # 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 + def currencyformat_nc(number, currency, *args, **kwargs): + """ + 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["currencyformat_nc"] = currencyformat_nc + @babel.localeselector def get_locale(): # get the lang from the session if defined, fallback on the browser "accept diff --git a/ihatemoney/templates/forms.html b/ihatemoney/templates/forms.html index 0900d2f0..82b960e3 100644 --- a/ihatemoney/templates/forms.html +++ b/ihatemoney/templates/forms.html @@ -124,7 +124,7 @@ {{ input(form.what, inline=True) }} {{ input(form.payer, inline=True, class="form-control custom-select") }} {{ input(form.amount, inline=True) }} - {% if not form.original_currency.render_kw %} + {% if g.project.default_currency != "XXX" %} {{ input(form.original_currency, inline=True) }} {% endif %} {{ input(form.external_link, inline=True) }} diff --git a/ihatemoney/templates/list_bills.html b/ihatemoney/templates/list_bills.html index be55c199..7ae3bd67 100644 --- a/ihatemoney/templates/list_bills.html +++ b/ihatemoney/templates/list_bills.html @@ -1,5 +1,9 @@ {% extends "sidebar_table_layout.html" %} +{%- macro bill_amount(bill, currency=bill.original_currency, amount=bill.amount) %} + {{ amount|currencyformat_nc(currency) }} ({{ _("%(amount)s each", amount=bill.pay_each_default(amount)|currencyformat_nc(currency)) }}) +{% endmacro -%} + {% block title %} - {{ g.project.name }}{% endblock %} {% block js %} {% if add_bill %} $('#new-bill > a').click(); {% endif %} @@ -123,11 +127,6 @@ {{ _("For what?") }} {{ _("For whom?") }} {{ _("How much?") }} - {% if g.project.default_currency != "No Currency" %} - {{ _("Amount in %(currency)s", currency=g.project.default_currency) }} - {%- else -%} - {{ _("Amount") }} - {% endif %} {{ _("Actions") }} @@ -149,13 +148,11 @@ {{ bill.owers|join(', ', 'name') }} {%- endif %} - {% if bill.original_currency != "No Currency" %} - {{ "%0.2f"|format(bill.amount) }} {{bill.original_currency}} ({{ "%0.2f"|format(bill.pay_each_default(bill.amount)) }} {{bill.original_currency}} {{ _(" each") }}) - {%- else -%} - {{ "%0.2f"|format(bill.amount) }} ({{ "%0.2f"|format(bill.pay_each_default(bill.amount)) }} {{ _(" each") }}) - {% endif %} + + {{ bill_amount(bill) }} + - {{ "%0.2f"|format(bill.converted_amount) }} {{ _('edit') }} {{ _('delete') }} diff --git a/ihatemoney/utils.py b/ihatemoney/utils.py index 7fdad61a..175b7621 100644 --- a/ihatemoney/utils.py +++ b/ihatemoney/utils.py @@ -9,8 +9,9 @@ import os import re from babel import Locale +from babel.numbers import get_currency_name, get_currency_symbol from flask import current_app, redirect, render_template -from flask_babel import get_locale +from flask_babel import get_locale, lazy_gettext as _ import jinja2 from werkzeug.routing import HTTPException, RoutingException @@ -281,6 +282,20 @@ class FormEnum(Enum): return str(self.value) +def render_localized_currency(code, detailed=True): + 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 bbc98c4d..ae124ac5 100644 --- a/ihatemoney/web.py +++ b/ihatemoney/web.py @@ -41,6 +41,7 @@ from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.forms import ( AdminAuthenticationForm, AuthenticationForm, + EditProjectForm, InviteForm, MemberForm, PasswordReminder, @@ -48,7 +49,6 @@ from ihatemoney.forms import ( ResetPasswordForm, UploadForm, get_billform_for, - get_editprojectform_for, ) from ihatemoney.history import get_history, get_history_queries from ihatemoney.models import Bill, LoggingMode, Person, Project, db @@ -377,7 +377,7 @@ def reset_password(): @main.route("//edit", methods=["GET", "POST"]) def edit_project(): - edit_form = get_editprojectform_for(g.project) + edit_form = EditProjectForm() import_form = UploadForm() # Import form if import_form.validate_on_submit(): @@ -393,10 +393,10 @@ def edit_project(): if edit_form.validate_on_submit(): project = edit_form.update(g.project) # Update converted currency - if project.default_currency != CurrencyConverter.default: + if project.default_currency != CurrencyConverter.no_currency: for bill in project.get_bills(): - if bill.original_currency == CurrencyConverter.default: + if bill.original_currency == CurrencyConverter.no_currency: bill.original_currency = project.default_currency bill.converted_amount = CurrencyConverter().exchange_currency( @@ -417,6 +417,7 @@ 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", @@ -732,7 +733,7 @@ def edit_bill(bill_id): return redirect(url_for(".list_bills")) if not form.errors: - form.fill(bill) + form.fill(bill, g.project) return render_template("add_bill.html", form=form, edit=True)