From f389c562595f74bea86e49c29949f4a7b0e78900 Mon Sep 17 00:00:00 2001 From: dark0dave <52840419+dark0dave@users.noreply.github.com> Date: Wed, 29 Apr 2020 21:57:08 +0100 Subject: [PATCH] Feature/currencies (#541) Now each project can have a currency, default to None. Each bill can use a different currency, and a conversion to project default currency is done on settle. Fix #512 --- .gitignore | 6 +- ihatemoney/currency_convertor.py | 46 ++++++++++++ ihatemoney/forms.py | 62 +++++++++++++++- ihatemoney/history.py | 8 ++ .../versions/927ed575acbd_add_currencies.py | 73 +++++++++++++++++++ ihatemoney/models.py | 21 +++++- ihatemoney/run.py | 4 + ihatemoney/templates/forms.html | 5 ++ ihatemoney/templates/history.html | 4 + ihatemoney/templates/list_bills.html | 23 +++++- ihatemoney/tests/tests.py | 51 ++++++++++++- ihatemoney/web.py | 19 ++++- setup.cfg | 2 + 13 files changed, 313 insertions(+), 11 deletions(-) create mode 100644 ihatemoney/currency_convertor.py create mode 100644 ihatemoney/migrations/versions/927ed575acbd_add_currencies.py diff --git a/.gitignore b/.gitignore index d8d18940..9e3c42ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -ihatemoney.cfg *.pyc *.egg-info dist @@ -13,3 +12,8 @@ build .pytest_cache ihatemoney/budget.db .idea/ +.envrc +.DS_Store +.idea +.python-version + diff --git a/ihatemoney/currency_convertor.py b/ihatemoney/currency_convertor.py new file mode 100644 index 00000000..75fa8342 --- /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 989b3022..7a6a57e4 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 9dda3de6..faa12c09 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 00000000..b70d9025 --- /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 742bc8ca..9e474c60 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" + {{ input(form.default_currency) }}
{{ _("delete") }} @@ -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) }}
diff --git a/ihatemoney/templates/history.html b/ihatemoney/templates/history.html index 1ac3284f..a9a9a4db 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 0f2a68a5..95088eb3 100644 --- a/ihatemoney/templates/list_bills.html +++ b/ihatemoney/templates/list_bills.html @@ -111,7 +111,19 @@
- + + + {% for bill in bills.items %} @@ -130,7 +142,14 @@ {%- else -%} {{ bill.owers|join(', ', 'name') }} {%- endif %} - + +
{{ _("When?") }}{{ _("Who paid?") }}{{ _("For what?") }}{{ _("For whom?") }}{{ _("How much?") }}{{ _("Actions") }}
{{ _("When?") }} + {{ _("Who paid?") }} + {{ _("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") }}
{{ "%0.2f"|format(bill.amount) }} ({{ "%0.2f"|format(bill.pay_each()) }} {{ _("each") }}) + {% 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 %} + {{ "%0.2f"|format(bill.converted_amount) }} {{ _('edit') }} {{ _('delete') }} diff --git a/ihatemoney/tests/tests.py b/ihatemoney/tests/tests.py index 62cb0485..fb314bfc 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 18ce0c7a..bbc98c4d 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("//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() diff --git a/setup.cfg b/setup.cfg index 50a24a41..d632d515 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,7 @@ include_package_data = True zip_safe = False install_requires = blinker==1.4 + cachetools==4.1.0 debts==0.5 email_validator==1.0.5 Flask-Babel==1.0.0 @@ -37,6 +38,7 @@ install_requires = Flask==1.1.2 itsdangerous==1.1.0 Jinja2==2.11.2 + requests==2.22.0 SQLAlchemy-Continuum==1.3.9 [options.extras_require]