mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-04-30 18:22:38 +02:00
Improve currencies (#604)
- Rename "No Currency" to ISO4217 "XXX" - Use Babel to render currency symbols and names in currency lists - Improve i18n in bill lists Fix #601 Fix #600
This commit is contained in:
parent
76911983af
commit
981edd413a
8 changed files with 93 additions and 73 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }}
|
||||
|
|
|
@ -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 @@
|
|||
</th><th>{{ _("For what?") }}
|
||||
</th><th>{{ _("For whom?") }}
|
||||
</th><th>{{ _("How much?") }}
|
||||
{% if g.project.default_currency != "No Currency" %}
|
||||
</th><th>{{ _("Amount in %(currency)s", currency=g.project.default_currency) }}
|
||||
{%- else -%}
|
||||
</th><th>{{ _("Amount") }}
|
||||
{% endif %}
|
||||
</th><th>{{ _("Actions") }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -149,13 +148,11 @@
|
|||
{{ bill.owers|join(', ', 'name') }}
|
||||
{%- endif %}</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
<span data-toggle="tooltip" data-placement="top"
|
||||
title="{{ bill_amount(bill, g.project.default_currency, bill.converted_amount) if bill.original_currency != g.project.default_currency else '' }}">
|
||||
{{ bill_amount(bill) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ "%0.2f"|format(bill.converted_amount) }}</td>
|
||||
<td class="bill-actions">
|
||||
<a class="edit" href="{{ url_for(".edit_bill", bill_id=bill.id) }}" title="{{ _("edit") }}">{{ _('edit') }}</a>
|
||||
<a class="delete" href="{{ url_for(".delete_bill", bill_id=bill.id) }}" title="{{ _("delete") }}">{{ _('delete') }}</a>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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("/<project_id>/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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue