mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-05-06 05:01:48 +02:00
This work is from the previous pr, I have merged it all together so its
possible to continue their fine work. Add original currency and amount fields to Bill model Add default currency to Project model Add minor fix to String length Reorganize the new migration file Crete a migration file for Project.default_currency Change models.py to have Project.default_currency Added new fields to to_serialize() Update bill view to display original currencies and original amounts Minor change to new labels Add new fields to tests Include default_currency field in tests.py Datafield entry in forms adding a stray file added requests to the requirements file Added another exception for catching errors related to mail Testing to find the issue revert change Adding a removed change to list_bills.html template added currency conversion to the CurrencyConversion class fixed typo in function name Edit BillForm to have currency dropdown and convert functionality Show currency dropdown in BillForm Fix minor issues with BillForm task Edit tests.py to include missing original_currency and amount fields Fix failing tests Fix other failing tests Final fix to tests Ran black on the project directory Make small ammendments to tests Temporarily set maxDiff as None Fix test failure in test_export() formatted util file so it could pass formatting tests changed the structure of the table in the bill view to be more reader friendly, by adding the different curriencies in their respective columns Cleaning up pr with comments from previous prs Signed-off-by: dark0dave <dark0dave@mykolab.com> Now caching respone for one day to ensure it is not call often Signed-off-by: dark0dave <dark0dave@mykolab.com> Updated migration and currency converter to help existing bills Signed-off-by: dark0dave <dark0dave@mykolab.com>
This commit is contained in:
parent
68f6df6abc
commit
c6ace4f710
11 changed files with 299 additions and 10 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -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
|
||||
|
||||
|
|
46
ihatemoney/currency_convertor.py
Normal file
46
ihatemoney/currency_convertor.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
import requests
|
||||
from cachetools import cached, TTLCache
|
||||
|
||||
|
||||
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)
|
|
@ -5,12 +5,15 @@ import email_validator
|
|||
from flask import request
|
||||
from flask_babel import lazy_gettext as _
|
||||
from flask_wtf.file import FileAllowed, FileField, FileRequired
|
||||
import copy
|
||||
|
||||
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.html5 import DateField, DecimalField, URLField
|
||||
from wtforms.fields.simple import BooleanField, PasswordField, StringField, SubmitField
|
||||
from wtforms.fields.core import Label
|
||||
from wtforms.validators import (
|
||||
DataRequired,
|
||||
Email,
|
||||
|
@ -22,6 +25,7 @@ from wtforms.validators import (
|
|||
|
||||
from ihatemoney.models import LoggingMode, Person, Project
|
||||
from ihatemoney.utils import eval_arithmetic_expression, slugify
|
||||
from ihatemoney.currency_convertor import CurrencyConverter
|
||||
|
||||
|
||||
def strip_filter(string):
|
||||
|
@ -31,6 +35,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 +55,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 +122,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 +154,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 +168,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 +243,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 +270,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 +283,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 +295,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]
|
||||
|
||||
|
|
|
@ -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 ###
|
|
@ -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 "
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 ") }}{{ 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>
|
||||
|
|
|
@ -6,7 +6,7 @@ import json
|
|||
import os
|
||||
from time import sleep
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from flask import session
|
||||
from flask_testing import TestCase
|
||||
|
@ -15,6 +15,7 @@ from werkzeug.security import check_password_hash, generate_password_hash
|
|||
|
||||
from ihatemoney import history, models, utils
|
||||
from ihatemoney.manage import DeleteProject, GenerateConfig, GeneratePasswordHash
|
||||
from ihatemoney.currency_convertor import CurrencyConverter
|
||||
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)
|
||||
|
@ -2850,5 +2878,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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue