aboutsummaryrefslogtreecommitdiff
path: root/ihatemoney
diff options
context:
space:
mode:
Diffstat (limited to 'ihatemoney')
-rw-r--r--ihatemoney/currency_convertor.py46
-rw-r--r--ihatemoney/forms.py62
-rw-r--r--ihatemoney/history.py8
-rw-r--r--ihatemoney/migrations/versions/927ed575acbd_add_currencies.py73
-rw-r--r--ihatemoney/models.py21
-rw-r--r--ihatemoney/run.py4
-rw-r--r--ihatemoney/templates/forms.html5
-rw-r--r--ihatemoney/templates/history.html4
-rw-r--r--ihatemoney/templates/list_bills.html23
-rw-r--r--ihatemoney/tests/tests.py51
-rw-r--r--ihatemoney/web.py19
11 files changed, 306 insertions, 10 deletions
diff --git a/ihatemoney/currency_convertor.py b/ihatemoney/currency_convertor.py
new file mode 100644
index 0000000..75fa834
--- /dev/null
+++ b/ihatemoney/currency_convertor.py
@@ -0,0 +1,46 @@
+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
+ default = "No Currency"
+ api_url = "https://api.exchangeratesapi.io/latest?base=USD"
+
+ def __init__(self):
+ pass
+
+ @cached(cache=TTLCache(maxsize=1, ttl=86400))
+ def get_rates(self):
+ rates = requests.get(self.api_url).json()["rates"]
+ rates[self.default] = 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)
+ 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
+ ):
+ 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 989b302..7a6a57e 100644
--- a/ihatemoney/forms.py
+++ b/ihatemoney/forms.py
@@ -1,3 +1,4 @@
+import copy
from datetime import datetime
from re import match
@@ -8,7 +9,7 @@ from flask_wtf.file import FileAllowed, FileField, FileRequired
from flask_wtf.form import FlaskForm
from jinja2 import Markup
from werkzeug.security import check_password_hash, generate_password_hash
-from wtforms.fields.core import SelectField, SelectMultipleField
+from wtforms.fields.core import Label, SelectField, SelectMultipleField
from wtforms.fields.html5 import DateField, DecimalField, URLField
from wtforms.fields.simple import BooleanField, PasswordField, StringField, SubmitField
from wtforms.validators import (
@@ -20,6 +21,7 @@ from wtforms.validators import (
ValidationError,
)
+from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.models import LoggingMode, Person, Project
from ihatemoney.utils import eval_arithmetic_expression, slugify
@@ -31,6 +33,18 @@ 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.
@@ -39,6 +53,23 @@ def get_billform_for(project, set_default=True, **kwargs):
"""
form = BillForm(**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
+
+ 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
@@ -89,6 +120,15 @@ 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"),
+ choices=[
+ (currency_name, currency_name)
+ for currency_name in currency_helper.get_currencies()
+ ],
+ validators=[DataRequired()],
+ )
@property
def logging_preference(self):
@@ -112,6 +152,7 @@ class EditProjectForm(FlaskForm):
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
@@ -125,6 +166,7 @@ class EditProjectForm(FlaskForm):
project.contact_email = self.contact_email.data
project.logging_preference = self.logging_preference
+ project.default_currency = self.default_currency.data
return project
@@ -199,6 +241,15 @@ class BillForm(FlaskForm):
what = StringField(_("What?"), validators=[DataRequired()])
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()],
+ )
external_link = URLField(
_("External link"),
validators=[Optional()],
@@ -217,6 +268,10 @@ class BillForm(FlaskForm):
bill.external_link = self.external_link.data
bill.date = self.date.data
bill.owers = [Person.query.get(ower, project) for ower in self.payed_for.data]
+ 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 fake_form(self, bill, project):
@@ -226,6 +281,10 @@ 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.converted_amount = self.currency_helper.exchange_currency(
+ bill.amount, bill.original_currency, project.default_currency
+ )
return bill
@@ -234,6 +293,7 @@ class BillForm(FlaskForm):
self.amount.data = bill.amount
self.what.data = bill.what
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]
diff --git a/ihatemoney/history.py b/ihatemoney/history.py
index 9dda3de..faa12c0 100644
--- a/ihatemoney/history.py
+++ b/ihatemoney/history.py
@@ -105,6 +105,14 @@ def get_history(project, human_readable_names=True):
if removed:
changeset["owers_removed"] = (None, removed)
+ # Remove converted_amount if amount changed in the same way.
+ if (
+ "amount" in changeset
+ and "converted_amount" in changeset
+ and changeset["amount"] == changeset["converted_amount"]
+ ):
+ del changeset["converted_amount"]
+
for (prop, (val_before, val_after),) in changeset.items():
if human_readable_names:
if prop == "payer_id":
diff --git a/ihatemoney/migrations/versions/927ed575acbd_add_currencies.py b/ihatemoney/migrations/versions/927ed575acbd_add_currencies.py
new file mode 100644
index 0000000..b70d902
--- /dev/null
+++ b/ihatemoney/migrations/versions/927ed575acbd_add_currencies.py
@@ -0,0 +1,73 @@
+"""Add currencies
+
+Revision ID: 927ed575acbd
+Revises: cb038f79982e
+Create Date: 2020-04-25 14:49:41.136602
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = "927ed575acbd"
+down_revision = "cb038f79982e"
+
+from alembic import op
+import sqlalchemy as sa
+from ihatemoney.currency_convertor import CurrencyConverter
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column("bill", sa.Column("converted_amount", sa.Float(), nullable=True))
+ op.add_column(
+ "bill",
+ sa.Column(
+ "original_currency",
+ sa.String(length=3),
+ server_default=CurrencyConverter.default,
+ nullable=True,
+ ),
+ )
+ op.add_column(
+ "bill_version",
+ sa.Column("converted_amount", sa.Float(), autoincrement=False, nullable=True),
+ )
+ op.add_column(
+ "bill_version",
+ sa.Column(
+ "original_currency", sa.String(length=3), autoincrement=False, nullable=True
+ ),
+ )
+ op.add_column(
+ "project",
+ sa.Column(
+ "default_currency",
+ sa.String(length=3),
+ server_default=CurrencyConverter.default,
+ nullable=True,
+ ),
+ )
+ op.add_column(
+ "project_version",
+ sa.Column(
+ "default_currency", sa.String(length=3), autoincrement=False, nullable=True
+ ),
+ )
+ # ### end Alembic commands ###
+ op.execute(
+ """
+ UPDATE bill
+ SET converted_amount = amount
+ WHERE converted_amount IS NULL
+ """
+ )
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column("project_version", "default_currency")
+ op.drop_column("project", "default_currency")
+ op.drop_column("bill_version", "original_currency")
+ op.drop_column("bill_version", "converted_amount")
+ op.drop_column("bill", "original_currency")
+ op.drop_column("bill", "converted_amount")
+ # ### end Alembic commands ###
diff --git a/ihatemoney/models.py b/ihatemoney/models.py
index 742bc8c..9e474c6 100644
--- a/ihatemoney/models.py
+++ b/ihatemoney/models.py
@@ -71,6 +71,7 @@ class Project(db.Model):
members = db.relationship("Person", backref="project")
query_class = ProjectQuery
+ default_currency = db.Column(db.String(3))
@property
def _to_serialize(self):
@@ -80,6 +81,7 @@ class Project(db.Model):
"contact_email": self.contact_email,
"logging_preference": self.logging_preference.value,
"members": [],
+ "default_currency": self.default_currency,
}
balance = self.balance
@@ -128,7 +130,10 @@ class Project(db.Model):
{
"member": member,
"paid": sum(
- [bill.amount for bill in self.get_member_bills(member.id).all()]
+ [
+ bill.converted_amount
+ for bill in self.get_member_bills(member.id).all()
+ ]
),
"spent": sum(
[
@@ -151,7 +156,7 @@ class Project(db.Model):
"""
monthly = defaultdict(lambda: defaultdict(float))
for bill in self.get_bills().all():
- monthly[bill.date.year][bill.date.month] += bill.amount
+ monthly[bill.date.year][bill.date.month] += bill.converted_amount
return monthly
@property
@@ -432,6 +437,9 @@ class Bill(db.Model):
what = db.Column(db.UnicodeText)
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"))
@property
@@ -445,9 +453,11 @@ class Bill(db.Model):
"creation_date": self.creation_date,
"what": self.what,
"external_link": self.external_link,
+ "original_currency": self.original_currency,
+ "converted_amount": self.converted_amount,
}
- def pay_each(self):
+ def pay_each_default(self, amount):
"""Compute what each share has to pay"""
if self.owers:
weights = (
@@ -455,13 +465,16 @@ class Bill(db.Model):
.join(billowers, Bill)
.filter(Bill.id == self.id)
).scalar()
- return self.amount / weights
+ return amount / weights
else:
return 0
def __str__(self):
return self.what
+ def pay_each(self):
+ return self.pay_each_default(self.converted_amount)
+
def __repr__(self):
return (
f"<Bill of {self.amount} from {self.payer} for "
diff --git a/ihatemoney/run.py b/ihatemoney/run.py
index c4b5323..b6c8cbb 100644
--- a/ihatemoney/run.py
+++ b/ihatemoney/run.py
@@ -10,6 +10,7 @@ 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,
@@ -137,6 +138,9 @@ 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
diff --git a/ihatemoney/templates/forms.html b/ihatemoney/templates/forms.html
index 33a283f..0900d2f 100644
--- a/ihatemoney/templates/forms.html
+++ b/ihatemoney/templates/forms.html
@@ -75,6 +75,7 @@
{{ input(form.name) }}
{{ input(form.password) }}
{{ input(form.contact_email) }}
+ {{ input(form.default_currency) }}
{% if not home %}
{{ submit(form.submit, home=True) }}
{% endif %}
@@ -96,6 +97,7 @@
</div>
</div>
+ {{ input(form.default_currency) }}
<div class="actions">
<button class="btn btn-primary">{{ _("Edit the project") }}</button>
<a id="delete-project" style="color:red; margin-left:10px; cursor:pointer; ">{{ _("delete") }}</a>
@@ -122,6 +124,9 @@
{{ 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 %}
+ {{ input(form.original_currency, inline=True) }}
+ {% endif %}
{{ input(form.external_link, inline=True) }}
<div class="form-group row">
diff --git a/ihatemoney/templates/history.html b/ihatemoney/templates/history.html
index 1ac3284..a9a9a4d 100644
--- a/ihatemoney/templates/history.html
+++ b/ihatemoney/templates/history.html
@@ -225,6 +225,10 @@
{{ simple_property_change(event, _("Amount")) }}
{% elif event.prop_changed == "date" %}
{{ simple_property_change(event, _("Date")) }}
+ {% elif event.prop_changed == "original_currency" %}
+ {{ simple_property_change(event, _("Currency")) }}
+ {% elif event.prop_changed == "converted_amount" %}
+ {{ simple_property_change(event, _("Amount in %(currency)s", currency=g.project.default_currency)) }}
{% else %}
{{ describe_object(event) }} {{ _("modified") }}
{% endif %}
diff --git a/ihatemoney/templates/list_bills.html b/ihatemoney/templates/list_bills.html
index 0f2a68a..95088eb 100644
--- a/ihatemoney/templates/list_bills.html
+++ b/ihatemoney/templates/list_bills.html
@@ -111,7 +111,19 @@
<div class="clearfix"></div>
<table id="bill_table" class="col table table-striped table-hover table-responsive-sm">
- <thead><tr><th>{{ _("When?") }}</th><th>{{ _("Who paid?") }}</<th><th>{{ _("For what?") }}</th><th>{{ _("For whom?") }}</th><th>{{ _("How much?") }}</th><th>{{ _("Actions") }}</th></tr></thead>
+ <thead>
+ <tr><th>{{ _("When?") }}
+ </th><th>{{ _("Who paid?") }}
+ </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>
{% for bill in bills.items %}
<tr owers="{{bill.owers|join(',','id')}}" payer="{{bill.payer.id}}">
@@ -130,7 +142,14 @@
{%- else -%}
{{ bill.owers|join(', ', 'name') }}
{%- endif %}</td>
- <td>{{ "%0.2f"|format(bill.amount) }} ({{ "%0.2f"|format(bill.pay_each()) }} {{ _("each") }})</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 %}
+ </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>
diff --git a/ihatemoney/tests/tests.py b/ihatemoney/tests/tests.py
index 62cb048..fb314bf 100644
--- a/ihatemoney/tests/tests.py
+++ b/ihatemoney/tests/tests.py
@@ -6,7 +6,7 @@ import json
import os
from time import sleep
import unittest
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
from flask import session
from flask_testing import TestCase
@@ -14,6 +14,7 @@ from sqlalchemy import orm
from werkzeug.security import check_password_hash, generate_password_hash
from ihatemoney import history, models, utils
+from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.manage import DeleteProject, GenerateConfig, GeneratePasswordHash
from ihatemoney.run import create_app, db, load_configuration
from ihatemoney.versioning import LoggingMode
@@ -59,6 +60,7 @@ class BaseTestCase(TestCase):
"id": name,
"password": name,
"contact_email": f"{name}@notmyidea.org",
+ "default_currency": "USD",
},
)
@@ -68,6 +70,7 @@ class BaseTestCase(TestCase):
name=str(name),
password=generate_password_hash(name),
contact_email=f"{name}@notmyidea.org",
+ default_currency="USD",
)
models.db.session.add(project)
models.db.session.commit()
@@ -254,6 +257,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"id": "raclette",
"password": "party",
"contact_email": "raclette@notmyidea.org",
+ "default_currency": "USD",
},
)
@@ -273,6 +277,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"id": "raclette", # already used !
"password": "party",
"contact_email": "raclette@notmyidea.org",
+ "default_currency": "USD",
},
)
@@ -290,6 +295,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"id": "raclette",
"password": "party",
"contact_email": "raclette@notmyidea.org",
+ "default_currency": "USD",
},
)
@@ -310,6 +316,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"id": "raclette",
"password": "party",
"contact_email": "raclette@notmyidea.org",
+ "default_currency": "USD",
},
)
@@ -329,6 +336,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"id": "raclette",
"password": "party",
"contact_email": "raclette@notmyidea.org",
+ "default_currency": "USD",
},
)
@@ -841,6 +849,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"contact_email": "alexis@notmyidea.org",
"password": "didoudida",
"logging_preference": LoggingMode.ENABLED.value,
+ "default_currency": "USD",
}
resp = self.client.post("/raclette/edit", data=new_data, follow_redirects=True)
@@ -849,6 +858,7 @@ class BudgetTestCase(IhatemoneyTestCase):
self.assertEqual(project.name, new_data["name"])
self.assertEqual(project.contact_email, new_data["contact_email"])
+ self.assertEqual(project.default_currency, new_data["default_currency"])
self.assertTrue(check_password_hash(project.password, new_data["password"]))
# Editing a project with a wrong email address should fail
@@ -1099,6 +1109,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"payer": 1,
"payed_for": [1, 2, 3, 4],
"amount": "10.0",
+ "original_currency": "USD",
},
)
@@ -1110,6 +1121,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"payer": 2,
"payed_for": [1, 3],
"amount": "200",
+ "original_currency": "USD",
},
)
@@ -1121,6 +1133,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"payer": 3,
"payed_for": [2],
"amount": "13.33",
+ "original_currency": "USD",
},
)
@@ -1425,6 +1438,7 @@ class APITestCase(IhatemoneyTestCase):
"id": id,
"password": password,
"contact_email": contact,
+ "default_currency": "USD",
},
)
@@ -1486,6 +1500,7 @@ class APITestCase(IhatemoneyTestCase):
"id": "raclette",
"password": "raclette",
"contact_email": "not-an-email",
+ "default_currency": "USD",
},
)
@@ -1514,6 +1529,7 @@ class APITestCase(IhatemoneyTestCase):
"members": [],
"name": "raclette",
"contact_email": "raclette@notmyidea.org",
+ "default_currency": "USD",
"id": "raclette",
"logging_preference": 1,
}
@@ -1525,6 +1541,7 @@ class APITestCase(IhatemoneyTestCase):
"/api/projects/raclette",
data={
"contact_email": "yeah@notmyidea.org",
+ "default_currency": "USD",
"password": "raclette",
"name": "The raclette party",
"project_history": "y",
@@ -1542,6 +1559,7 @@ class APITestCase(IhatemoneyTestCase):
expected = {
"name": "The raclette party",
"contact_email": "yeah@notmyidea.org",
+ "default_currency": "USD",
"members": [],
"id": "raclette",
"logging_preference": 1,
@@ -1554,6 +1572,7 @@ class APITestCase(IhatemoneyTestCase):
"/api/projects/raclette",
data={
"contact_email": "yeah@notmyidea.org",
+ "default_currency": "USD",
"password": "tartiflette",
"name": "The raclette party",
},
@@ -1776,6 +1795,8 @@ class APITestCase(IhatemoneyTestCase):
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
+ "converted_amount": 25.0,
+ "original_currency": "USD",
"external_link": "https://raclette.fr",
}
@@ -1845,6 +1866,8 @@ class APITestCase(IhatemoneyTestCase):
"amount": 25.0,
"date": "2011-09-10",
"external_link": "https://raclette.fr",
+ "converted_amount": 25.0,
+ "original_currency": "USD",
"id": 1,
}
@@ -1922,6 +1945,8 @@ class APITestCase(IhatemoneyTestCase):
"date": "2011-08-10",
"id": id,
"external_link": "",
+ "original_currency": "USD",
+ "converted_amount": expected_amount,
}
got = json.loads(req.data.decode("utf-8"))
@@ -2064,6 +2089,8 @@ class APITestCase(IhatemoneyTestCase):
"date": "2011-08-10",
"id": 1,
"external_link": "",
+ "converted_amount": 25.0,
+ "original_currency": "USD",
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
@@ -2106,6 +2133,7 @@ class APITestCase(IhatemoneyTestCase):
"id": "raclette",
"name": "raclette",
"logging_preference": 1,
+ "default_currency": "USD",
}
self.assertStatus(200, req)
@@ -2273,6 +2301,7 @@ class HistoryTestCase(IhatemoneyTestCase):
"name": "demo",
"contact_email": "demo@notmyidea.org",
"password": "demo",
+ "default_currency": "USD",
}
if logging_preference != LoggingMode.DISABLED:
@@ -2327,6 +2356,7 @@ class HistoryTestCase(IhatemoneyTestCase):
"contact_email": "demo2@notmyidea.org",
"password": "123456",
"project_history": "y",
+ "default_currency": "USD",
}
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True)
@@ -2422,6 +2452,7 @@ class HistoryTestCase(IhatemoneyTestCase):
"name": "demo2",
"contact_email": "demo2@notmyidea.org",
"password": "123456",
+ "default_currency": "USD",
}
# Keep privacy settings where they were
@@ -2850,5 +2881,23 @@ class HistoryTestCase(IhatemoneyTestCase):
self.assertEqual(len(history_list), 6)
+class TestCurrencyConverter(unittest.TestCase):
+ converter = CurrencyConverter()
+ mock_data = {"USD": 1, "EUR": 0.8115}
+ converter.get_rates = MagicMock(return_value=mock_data)
+
+ def test_only_one_instance(self):
+ one = id(CurrencyConverter())
+ two = id(CurrencyConverter())
+ self.assertEqual(one, two)
+
+ def test_get_currencies(self):
+ self.assertCountEqual(self.converter.get_currencies(), ["USD", "EUR"])
+
+ def test_exchange_currency(self):
+ result = self.converter.exchange_currency(100, "USD", "EUR")
+ self.assertEqual(result, 81.15)
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/ihatemoney/web.py b/ihatemoney/web.py
index 18ce0c7..bbc98c4 100644
--- a/ihatemoney/web.py
+++ b/ihatemoney/web.py
@@ -37,10 +37,10 @@ from sqlalchemy_continuum import Operation
from werkzeug.exceptions import NotFound
from werkzeug.security import check_password_hash, generate_password_hash
+from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.forms import (
AdminAuthenticationForm,
AuthenticationForm,
- EditProjectForm,
InviteForm,
MemberForm,
PasswordReminder,
@@ -48,6 +48,7 @@ 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
@@ -376,7 +377,7 @@ def reset_password():
@main.route("/<project_id>/edit", methods=["GET", "POST"])
def edit_project():
- edit_form = EditProjectForm()
+ edit_form = get_editprojectform_for(g.project)
import_form = UploadForm()
# Import form
if import_form.validate_on_submit():
@@ -391,6 +392,18 @@ def edit_project():
# Edit form
if edit_form.validate_on_submit():
project = edit_form.update(g.project)
+ # Update converted currency
+ if project.default_currency != CurrencyConverter.default:
+ for bill in project.get_bills():
+
+ if bill.original_currency == CurrencyConverter.default:
+ bill.original_currency = project.default_currency
+
+ bill.converted_amount = CurrencyConverter().exchange_currency(
+ bill.amount, bill.original_currency, project.default_currency
+ )
+ db.session.add(bill)
+
db.session.add(project)
db.session.commit()
@@ -478,6 +491,7 @@ def import_project(file, project):
form.date = parse(b["date"])
form.payer = id_dict[b["payer_name"]]
form.payed_for = owers_id
+ form.original_currency = b.get("original_currency")
db.session.add(form.fake_form(bill, project))
@@ -543,6 +557,7 @@ def demo():
name="demonstration",
password=generate_password_hash("demo"),
contact_email="demo@notmyidea.org",
+ default_currency="EUR",
)
db.session.add(project)
db.session.commit()