mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-04-28 17:32:38 +02:00
Remove multi currencies support
For more information about the rationale, this was [discussed](https://github.com/spiral-project/ihatemoney/issues/1232#issuecomment-2081517453) in our bugtracker.
This commit is contained in:
parent
4f9cad88bd
commit
04b18a8d3d
26 changed files with 84 additions and 1338 deletions
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
This document describes changes between each past release.
|
This document describes changes between each past release.
|
||||||
|
|
||||||
## 6.2.0 (unreleased)
|
## 7.0.0 (unreleased)
|
||||||
|
|
||||||
|
- Remove the support for multiple currencies, [as discussed](https://github.com/spiral-project/ihatemoney/issues/1232#issuecomment-2081517453) in our bugtracker.
|
||||||
- Add support for python 3.12 (#757)
|
- Add support for python 3.12 (#757)
|
||||||
- Migrate from setup.cfg to pyproject.toml (#1243)
|
- Migrate from setup.cfg to pyproject.toml (#1243)
|
||||||
- Update to wtforms 3.1 (#1248)
|
- Update to wtforms 3.1 (#1248)
|
||||||
|
|
37
docs/api.md
37
docs/api.md
|
@ -71,13 +71,6 @@ A project needs the following arguments:
|
||||||
- `password`: the project password / private code (string)
|
- `password`: the project password / private code (string)
|
||||||
- `contact_email`: the contact email, used to recover the private code (string)
|
- `contact_email`: the contact email, used to recover the private code (string)
|
||||||
|
|
||||||
Optional arguments:
|
|
||||||
|
|
||||||
- `default_currency`: the default currency to use for a multi-currency
|
|
||||||
project, in ISO 4217 format. Bills are converted to this currency
|
|
||||||
for operations like balance or statistics. Default value: `XXX` (no
|
|
||||||
currency).
|
|
||||||
|
|
||||||
Here is the command:
|
Here is the command:
|
||||||
|
|
||||||
$ curl -X POST https://ihatemoney.org/api/projects \
|
$ curl -X POST https://ihatemoney.org/api/projects \
|
||||||
|
@ -97,7 +90,6 @@ Getting information about the project:
|
||||||
"id": "demo",
|
"id": "demo",
|
||||||
"name": "demonstration",
|
"name": "demonstration",
|
||||||
"contact_email": "demo@notmyidea.org",
|
"contact_email": "demo@notmyidea.org",
|
||||||
"default_currency": "XXX",
|
|
||||||
"members": [{"id": 11515, "name": "f", "weight": 1.0, "activated": true, "balance": 0},
|
"members": [{"id": 11515, "name": "f", "weight": 1.0, "activated": true, "balance": 0},
|
||||||
{"id": 11531, "name": "g", "weight": 1.0, "activated": true, "balance": 0},
|
{"id": 11531, "name": "g", "weight": 1.0, "activated": true, "balance": 0},
|
||||||
{"id": 11532, "name": "peter", "weight": 1.0, "activated": true, "balance": 5.0},
|
{"id": 11532, "name": "peter", "weight": 1.0, "activated": true, "balance": 5.0},
|
||||||
|
@ -181,14 +173,8 @@ Or get a specific bill by ID:
|
||||||
"creation_date": "2021-01-13",
|
"creation_date": "2021-01-13",
|
||||||
"what": "Raclette du nouvel an",
|
"what": "Raclette du nouvel an",
|
||||||
"external_link": "",
|
"external_link": "",
|
||||||
"original_currency": "XXX",
|
|
||||||
"converted_amount": 100
|
|
||||||
}
|
}
|
||||||
|
|
||||||
`amount` is expressed in the `original_currency` of the bill, while
|
|
||||||
`converted_amount` is expressed in the project `default_currency`. Here,
|
|
||||||
they are the same.
|
|
||||||
|
|
||||||
Add a bill with a `POST` query on `/api/projects/<id>/bills`. You need
|
Add a bill with a `POST` query on `/api/projects/<id>/bills`. You need
|
||||||
the following required parameters:
|
the following required parameters:
|
||||||
|
|
||||||
|
@ -203,9 +189,6 @@ And optional parameters:
|
||||||
|
|
||||||
- `date`: the date of the bill (`yyyy-mm-dd` format). Defaults to
|
- `date`: the date of the bill (`yyyy-mm-dd` format). Defaults to
|
||||||
current date if not provided.
|
current date if not provided.
|
||||||
- `original_currency`: the currency in which `amount` has been paid
|
|
||||||
(ISO 4217 code). Only makes sense for a project with currencies.
|
|
||||||
Defaults to the project `default_currency`.
|
|
||||||
- `external_link`: an optional URL associated with the bill.
|
- `external_link`: an optional URL associated with the bill.
|
||||||
|
|
||||||
Returns the id of the created bill :
|
Returns the id of the created bill :
|
||||||
|
@ -250,23 +233,3 @@ You can get some project stats with a `GET` on
|
||||||
"balance": -10.5
|
"balance": -10.5
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
### Currencies
|
|
||||||
|
|
||||||
You can get a list of supported currencies with a `GET` on
|
|
||||||
`/api/currencies`:
|
|
||||||
|
|
||||||
$ curl --basic https://ihatemoney.org/api/currencies
|
|
||||||
[
|
|
||||||
"XXX",
|
|
||||||
"AED",
|
|
||||||
"AFN",
|
|
||||||
.
|
|
||||||
.
|
|
||||||
.
|
|
||||||
"ZAR",
|
|
||||||
"ZMW",
|
|
||||||
"ZWL"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ from flask_restful import Resource, abort
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
from wtforms.fields import BooleanField
|
from wtforms.fields import BooleanField
|
||||||
|
|
||||||
from ihatemoney.currency_convertor import CurrencyConverter
|
|
||||||
from ihatemoney.emails import send_creation_email
|
from ihatemoney.emails import send_creation_email
|
||||||
from ihatemoney.forms import EditProjectForm, MemberForm, ProjectForm, get_billform_for
|
from ihatemoney.forms import EditProjectForm, MemberForm, ProjectForm, get_billform_for
|
||||||
from ihatemoney.models import Bill, Person, Project, db
|
from ihatemoney.models import Bill, Person, Project, db
|
||||||
|
@ -50,13 +49,6 @@ def need_auth(f):
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
class CurrenciesHandler(Resource):
|
|
||||||
currency_helper = CurrencyConverter()
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
return self.currency_helper.get_currencies()
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectsHandler(Resource):
|
class ProjectsHandler(Resource):
|
||||||
def post(self):
|
def post(self):
|
||||||
form = ProjectForm(meta={"csrf": False})
|
form = ProjectForm(meta={"csrf": False})
|
||||||
|
|
|
@ -5,7 +5,6 @@ from flask_restful import Api
|
||||||
from ihatemoney.api.common import (
|
from ihatemoney.api.common import (
|
||||||
BillHandler,
|
BillHandler,
|
||||||
BillsHandler,
|
BillsHandler,
|
||||||
CurrenciesHandler,
|
|
||||||
MemberHandler,
|
MemberHandler,
|
||||||
MembersHandler,
|
MembersHandler,
|
||||||
ProjectHandler,
|
ProjectHandler,
|
||||||
|
@ -18,7 +17,6 @@ api = Blueprint("api", __name__, url_prefix="/api")
|
||||||
CORS(api)
|
CORS(api)
|
||||||
restful_api = Api(api)
|
restful_api = Api(api)
|
||||||
|
|
||||||
restful_api.add_resource(CurrenciesHandler, "/currencies")
|
|
||||||
restful_api.add_resource(ProjectsHandler, "/projects")
|
restful_api.add_resource(ProjectsHandler, "/projects")
|
||||||
restful_api.add_resource(ProjectHandler, "/projects/<string:project_id>")
|
restful_api.add_resource(ProjectHandler, "/projects/<string:project_id>")
|
||||||
restful_api.add_resource(TokenHandler, "/projects/<string:project_id>/token")
|
restful_api.add_resource(TokenHandler, "/projects/<string:project_id>/token")
|
||||||
|
|
|
@ -1,228 +0,0 @@
|
||||||
import traceback
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
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
|
|
||||||
no_currency = "XXX"
|
|
||||||
api_url = "https://api.exchangerate.host/latest?base=USD"
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@cached(cache=TTLCache(maxsize=1, ttl=86400))
|
|
||||||
def get_rates(self):
|
|
||||||
try:
|
|
||||||
rates = requests.get(self.api_url).json()["rates"]
|
|
||||||
except Exception:
|
|
||||||
warnings.warn(
|
|
||||||
f"Call to {self.api_url} failed: {traceback.format_exc(limit=0).strip()}"
|
|
||||||
)
|
|
||||||
# In case of any exception, let's have an empty value
|
|
||||||
rates = {}
|
|
||||||
rates[self.no_currency] = 1.0
|
|
||||||
return rates
|
|
||||||
|
|
||||||
def get_currencies(self, with_no_currency=True):
|
|
||||||
currencies = [
|
|
||||||
"AED",
|
|
||||||
"AFN",
|
|
||||||
"ALL",
|
|
||||||
"AMD",
|
|
||||||
"ANG",
|
|
||||||
"AOA",
|
|
||||||
"ARS",
|
|
||||||
"AUD",
|
|
||||||
"AWG",
|
|
||||||
"AZN",
|
|
||||||
"BAM",
|
|
||||||
"BBD",
|
|
||||||
"BDT",
|
|
||||||
"BGN",
|
|
||||||
"BHD",
|
|
||||||
"BIF",
|
|
||||||
"BMD",
|
|
||||||
"BND",
|
|
||||||
"BOB",
|
|
||||||
"BRL",
|
|
||||||
"BSD",
|
|
||||||
"BTC",
|
|
||||||
"BTN",
|
|
||||||
"BWP",
|
|
||||||
"BYN",
|
|
||||||
"BZD",
|
|
||||||
"CAD",
|
|
||||||
"CDF",
|
|
||||||
"CHF",
|
|
||||||
"CLF",
|
|
||||||
"CLP",
|
|
||||||
"CNH",
|
|
||||||
"CNY",
|
|
||||||
"COP",
|
|
||||||
"CRC",
|
|
||||||
"CUC",
|
|
||||||
"CUP",
|
|
||||||
"CVE",
|
|
||||||
"CZK",
|
|
||||||
"DJF",
|
|
||||||
"DKK",
|
|
||||||
"DOP",
|
|
||||||
"DZD",
|
|
||||||
"EGP",
|
|
||||||
"ERN",
|
|
||||||
"ETB",
|
|
||||||
"EUR",
|
|
||||||
"FJD",
|
|
||||||
"FKP",
|
|
||||||
"GBP",
|
|
||||||
"GEL",
|
|
||||||
"GGP",
|
|
||||||
"GHS",
|
|
||||||
"GIP",
|
|
||||||
"GMD",
|
|
||||||
"GNF",
|
|
||||||
"GTQ",
|
|
||||||
"GYD",
|
|
||||||
"HKD",
|
|
||||||
"HNL",
|
|
||||||
"HRK",
|
|
||||||
"HTG",
|
|
||||||
"HUF",
|
|
||||||
"IDR",
|
|
||||||
"ILS",
|
|
||||||
"IMP",
|
|
||||||
"INR",
|
|
||||||
"IQD",
|
|
||||||
"IRR",
|
|
||||||
"ISK",
|
|
||||||
"JEP",
|
|
||||||
"JMD",
|
|
||||||
"JOD",
|
|
||||||
"JPY",
|
|
||||||
"KES",
|
|
||||||
"KGS",
|
|
||||||
"KHR",
|
|
||||||
"KMF",
|
|
||||||
"KPW",
|
|
||||||
"KRW",
|
|
||||||
"KWD",
|
|
||||||
"KYD",
|
|
||||||
"KZT",
|
|
||||||
"LAK",
|
|
||||||
"LBP",
|
|
||||||
"LKR",
|
|
||||||
"LRD",
|
|
||||||
"LSL",
|
|
||||||
"LYD",
|
|
||||||
"MAD",
|
|
||||||
"MDL",
|
|
||||||
"MGA",
|
|
||||||
"MKD",
|
|
||||||
"MMK",
|
|
||||||
"MNT",
|
|
||||||
"MOP",
|
|
||||||
"MRU",
|
|
||||||
"MUR",
|
|
||||||
"MVR",
|
|
||||||
"MWK",
|
|
||||||
"MXN",
|
|
||||||
"MYR",
|
|
||||||
"MZN",
|
|
||||||
"NAD",
|
|
||||||
"NGN",
|
|
||||||
"NIO",
|
|
||||||
"NOK",
|
|
||||||
"NPR",
|
|
||||||
"NZD",
|
|
||||||
"OMR",
|
|
||||||
"PAB",
|
|
||||||
"PEN",
|
|
||||||
"PGK",
|
|
||||||
"PHP",
|
|
||||||
"PKR",
|
|
||||||
"PLN",
|
|
||||||
"PYG",
|
|
||||||
"QAR",
|
|
||||||
"RON",
|
|
||||||
"RSD",
|
|
||||||
"RUB",
|
|
||||||
"RWF",
|
|
||||||
"SAR",
|
|
||||||
"SBD",
|
|
||||||
"SCR",
|
|
||||||
"SDG",
|
|
||||||
"SEK",
|
|
||||||
"SGD",
|
|
||||||
"SHP",
|
|
||||||
"SLL",
|
|
||||||
"SOS",
|
|
||||||
"SRD",
|
|
||||||
"SSP",
|
|
||||||
"STD",
|
|
||||||
"STN",
|
|
||||||
"SVC",
|
|
||||||
"SYP",
|
|
||||||
"SZL",
|
|
||||||
"THB",
|
|
||||||
"TJS",
|
|
||||||
"TMT",
|
|
||||||
"TND",
|
|
||||||
"TOP",
|
|
||||||
"TRY",
|
|
||||||
"TTD",
|
|
||||||
"TWD",
|
|
||||||
"TZS",
|
|
||||||
"UAH",
|
|
||||||
"UGX",
|
|
||||||
"USD",
|
|
||||||
"UYU",
|
|
||||||
"UZS",
|
|
||||||
"VEF",
|
|
||||||
"VES",
|
|
||||||
"VND",
|
|
||||||
"VUV",
|
|
||||||
"WST",
|
|
||||||
"XAF",
|
|
||||||
"XAG",
|
|
||||||
"XAU",
|
|
||||||
"XCD",
|
|
||||||
"XDR",
|
|
||||||
"XOF",
|
|
||||||
"XPD",
|
|
||||||
"XPF",
|
|
||||||
"XPT",
|
|
||||||
"YER",
|
|
||||||
"ZAR",
|
|
||||||
"ZMW",
|
|
||||||
"ZWL",
|
|
||||||
]
|
|
||||||
if with_no_currency:
|
|
||||||
currencies.append(self.no_currency)
|
|
||||||
return currencies
|
|
||||||
|
|
||||||
def exchange_currency(self, amount, source_currency, dest_currency):
|
|
||||||
if (
|
|
||||||
source_currency == dest_currency
|
|
||||||
or source_currency == self.no_currency
|
|
||||||
or dest_currency == self.no_currency
|
|
||||||
):
|
|
||||||
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)
|
|
|
@ -14,7 +14,6 @@ from wtforms.fields import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
DateField,
|
DateField,
|
||||||
DecimalField,
|
DecimalField,
|
||||||
Label,
|
|
||||||
PasswordField,
|
PasswordField,
|
||||||
SelectField,
|
SelectField,
|
||||||
SelectMultipleField,
|
SelectMultipleField,
|
||||||
|
@ -38,13 +37,11 @@ from wtforms.validators import (
|
||||||
ValidationError,
|
ValidationError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ihatemoney.currency_convertor import CurrencyConverter
|
|
||||||
from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project
|
from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project
|
||||||
from ihatemoney.utils import (
|
from ihatemoney.utils import (
|
||||||
em_surround,
|
em_surround,
|
||||||
eval_arithmetic_expression,
|
eval_arithmetic_expression,
|
||||||
generate_password_hash,
|
generate_password_hash,
|
||||||
render_localized_currency,
|
|
||||||
slugify,
|
slugify,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -64,20 +61,6 @@ def get_billform_for(project, set_default=True, **kwargs):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
form = BillForm(**kwargs)
|
form = BillForm(**kwargs)
|
||||||
if form.original_currency.data is None:
|
|
||||||
form.original_currency.data = project.default_currency
|
|
||||||
|
|
||||||
# Used in validate_original_currency
|
|
||||||
form.project_currency = project.default_currency
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
active_members = [(m.id, m.name) for m in project.active_members]
|
active_members = [(m.id, m.name) for m in project.active_members]
|
||||||
|
|
||||||
|
@ -137,30 +120,6 @@ class EditProjectForm(FlaskForm):
|
||||||
contact_email = StringField(_("Email"), validators=[DataRequired(), Email()])
|
contact_email = StringField(_("Email"), validators=[DataRequired(), Email()])
|
||||||
project_history = BooleanField(_("Enable project history"))
|
project_history = BooleanField(_("Enable project history"))
|
||||||
ip_recording = BooleanField(_("Use IP tracking for project history"))
|
ip_recording = BooleanField(_("Use IP tracking for project history"))
|
||||||
currency_helper = CurrencyConverter()
|
|
||||||
default_currency = SelectField(
|
|
||||||
_("Default Currency"),
|
|
||||||
validators=[DataRequired()],
|
|
||||||
default=CurrencyConverter.no_currency,
|
|
||||||
description=_(
|
|
||||||
"Setting a default currency enables currency conversion between bills"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if not hasattr(self, "id"):
|
|
||||||
# We must access the project to validate the default currency, using its id.
|
|
||||||
# In ProjectForm, 'id' is provided, but not in this base class, so it *must*
|
|
||||||
# be provided by callers.
|
|
||||||
# Since id can be defined as a WTForms.StringField, we mimics it,
|
|
||||||
# using an object that can have a 'data' attribute.
|
|
||||||
# It defaults to empty string to ensure that query run smoothly.
|
|
||||||
self.id = SimpleNamespace(data=kwargs.pop("id", ""))
|
|
||||||
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()
|
|
||||||
]
|
|
||||||
|
|
||||||
def validate_current_password(self, field):
|
def validate_current_password(self, field):
|
||||||
project = Project.query.get(self.id.data)
|
project = Project.query.get(self.id.data)
|
||||||
|
@ -180,28 +139,6 @@ class EditProjectForm(FlaskForm):
|
||||||
else:
|
else:
|
||||||
return LoggingMode.ENABLED
|
return LoggingMode.ENABLED
|
||||||
|
|
||||||
def validate_default_currency(self, field):
|
|
||||||
project = Project.query.get(self.id.data)
|
|
||||||
if (
|
|
||||||
project is not None
|
|
||||||
and field.data == CurrencyConverter.no_currency
|
|
||||||
and project.has_multiple_currencies()
|
|
||||||
):
|
|
||||||
msg = _(
|
|
||||||
"This project cannot be set to 'no currency'"
|
|
||||||
" because it contains bills in multiple currencies."
|
|
||||||
)
|
|
||||||
raise ValidationError(msg)
|
|
||||||
if (
|
|
||||||
project is not None
|
|
||||||
and field.data != project.default_currency
|
|
||||||
and project.has_bills()
|
|
||||||
):
|
|
||||||
msg = _(
|
|
||||||
"Cannot change project currency because currency conversion is broken"
|
|
||||||
)
|
|
||||||
raise ValidationError(msg)
|
|
||||||
|
|
||||||
def update(self, project):
|
def update(self, project):
|
||||||
"""Update the project with the information from the form"""
|
"""Update the project with the information from the form"""
|
||||||
project.name = self.name.data
|
project.name = self.name.data
|
||||||
|
@ -217,10 +154,20 @@ class EditProjectForm(FlaskForm):
|
||||||
|
|
||||||
project.contact_email = self.contact_email.data
|
project.contact_email = self.contact_email.data
|
||||||
project.logging_preference = self.logging_preference
|
project.logging_preference = self.logging_preference
|
||||||
project.switch_currency(self.default_currency.data)
|
|
||||||
|
|
||||||
return project
|
return project
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if not hasattr(self, "id"):
|
||||||
|
# We must access the project to validate the default currency, using its id.
|
||||||
|
# In ProjectForm, 'id' is provided, but not in this base class, so it *must*
|
||||||
|
# be provided by callers.
|
||||||
|
# Since id can be defined as a WTForms.StringField, we mimic it,
|
||||||
|
# using an object that can have a 'data' attribute.
|
||||||
|
# It defaults to empty string to ensure that query run smoothly.
|
||||||
|
self.id = SimpleNamespace(data=kwargs.pop("id", ""))
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ImportProjectForm(FlaskForm):
|
class ImportProjectForm(FlaskForm):
|
||||||
file = FileField(
|
file = FileField(
|
||||||
|
@ -258,7 +205,6 @@ class ProjectForm(EditProjectForm):
|
||||||
password=generate_password_hash(self.password.data),
|
password=generate_password_hash(self.password.data),
|
||||||
contact_email=self.contact_email.data,
|
contact_email=self.contact_email.data,
|
||||||
logging_preference=self.logging_preference,
|
logging_preference=self.logging_preference,
|
||||||
default_currency=self.default_currency.data,
|
|
||||||
)
|
)
|
||||||
return project
|
return project
|
||||||
|
|
||||||
|
@ -352,8 +298,6 @@ class BillForm(FlaskForm):
|
||||||
what = StringField(_("What?"), validators=[DataRequired()])
|
what = StringField(_("What?"), validators=[DataRequired()])
|
||||||
payer = SelectField(_("Who paid?"), validators=[DataRequired()], coerce=int)
|
payer = SelectField(_("Who paid?"), validators=[DataRequired()], coerce=int)
|
||||||
amount = CalculatorStringField(_("How much?"), validators=[DataRequired()])
|
amount = CalculatorStringField(_("How much?"), validators=[DataRequired()])
|
||||||
currency_helper = CurrencyConverter()
|
|
||||||
original_currency = SelectField(_("Currency"), validators=[DataRequired()])
|
|
||||||
external_link = URLField(
|
external_link = URLField(
|
||||||
_("External link"),
|
_("External link"),
|
||||||
default="",
|
default="",
|
||||||
|
@ -377,10 +321,8 @@ class BillForm(FlaskForm):
|
||||||
amount=float(self.amount.data),
|
amount=float(self.amount.data),
|
||||||
date=self.date.data,
|
date=self.date.data,
|
||||||
external_link=self.external_link.data,
|
external_link=self.external_link.data,
|
||||||
original_currency=str(self.original_currency.data),
|
|
||||||
owers=Person.query.get_by_ids(self.payed_for.data, project),
|
owers=Person.query.get_by_ids(self.payed_for.data, project),
|
||||||
payer_id=self.payer.data,
|
payer_id=self.payer.data,
|
||||||
project_default_currency=project.default_currency,
|
|
||||||
what=self.what.data,
|
what=self.what.data,
|
||||||
bill_type=self.bill_type.data,
|
bill_type=self.bill_type.data,
|
||||||
)
|
)
|
||||||
|
@ -393,10 +335,6 @@ class BillForm(FlaskForm):
|
||||||
bill.external_link = self.external_link.data
|
bill.external_link = self.external_link.data
|
||||||
bill.date = self.date.data
|
bill.date = self.date.data
|
||||||
bill.owers = Person.query.get_by_ids(self.payed_for.data, project)
|
bill.owers = Person.query.get_by_ids(self.payed_for.data, project)
|
||||||
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
|
return bill
|
||||||
|
|
||||||
def fill(self, bill, project):
|
def fill(self, bill, project):
|
||||||
|
@ -405,18 +343,9 @@ class BillForm(FlaskForm):
|
||||||
self.what.data = bill.what
|
self.what.data = bill.what
|
||||||
self.bill_type.data = bill.bill_type
|
self.bill_type.data = bill.bill_type
|
||||||
self.external_link.data = bill.external_link
|
self.external_link.data = bill.external_link
|
||||||
self.original_currency.data = bill.original_currency
|
|
||||||
self.date.data = bill.date
|
self.date.data = bill.date
|
||||||
self.payed_for.data = [int(ower.id) for ower in bill.owers]
|
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):
|
def set_default(self):
|
||||||
self.payed_for.data = self.payed_for.default
|
self.payed_for.data = self.payed_for.default
|
||||||
|
|
||||||
|
@ -425,17 +354,6 @@ class BillForm(FlaskForm):
|
||||||
# See https://github.com/python-babel/babel/issues/821
|
# See https://github.com/python-babel/babel/issues/821
|
||||||
raise ValidationError(f"Result is too high: {field.data}")
|
raise ValidationError(f"Result is too high: {field.data}")
|
||||||
|
|
||||||
def validate_original_currency(self, field):
|
|
||||||
# Workaround for currency API breakage
|
|
||||||
# See #1232
|
|
||||||
if field.data not in [CurrencyConverter.no_currency, self.project_currency]:
|
|
||||||
msg = _(
|
|
||||||
"Failed to convert from %(bill_currency)s currency to %(project_currency)s",
|
|
||||||
bill_currency=field.data,
|
|
||||||
project_currency=self.project_currency,
|
|
||||||
)
|
|
||||||
raise ValidationError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
class MemberForm(FlaskForm):
|
class MemberForm(FlaskForm):
|
||||||
name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter])
|
name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter])
|
||||||
|
|
|
@ -102,7 +102,6 @@ def get_history(project, human_readable_names=True):
|
||||||
"amount": detailed_version.amount,
|
"amount": detailed_version.amount,
|
||||||
"owers": [describe_version(o) for o in detailed_version.owers],
|
"owers": [describe_version(o) for o in detailed_version.owers],
|
||||||
"external_link": detailed_version.external_link,
|
"external_link": detailed_version.external_link,
|
||||||
"original_currency": detailed_version.original_currency,
|
|
||||||
}
|
}
|
||||||
common_properties["bill_details"] = details
|
common_properties["bill_details"] = details
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ down_revision = "cb038f79982e"
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from ihatemoney.currency_convertor import CurrencyConverter
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
|
@ -23,7 +22,7 @@ def upgrade():
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"original_currency",
|
"original_currency",
|
||||||
sa.String(length=3),
|
sa.String(length=3),
|
||||||
server_default=CurrencyConverter.no_currency,
|
server_default="",
|
||||||
nullable=True,
|
nullable=True,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -42,7 +41,7 @@ def upgrade():
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"default_currency",
|
"default_currency",
|
||||||
sa.String(length=3),
|
sa.String(length=3),
|
||||||
server_default=CurrencyConverter.no_currency,
|
server_default="",
|
||||||
nullable=True,
|
nullable=True,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,7 +20,6 @@ from sqlalchemy.sql import func
|
||||||
from sqlalchemy_continuum import make_versioned, version_class
|
from sqlalchemy_continuum import make_versioned, version_class
|
||||||
from sqlalchemy_continuum.plugins import FlaskPlugin
|
from sqlalchemy_continuum.plugins import FlaskPlugin
|
||||||
|
|
||||||
from ihatemoney.currency_convertor import CurrencyConverter
|
|
||||||
from ihatemoney.monkeypath_continuum import PatchedTransactionFactory
|
from ihatemoney.monkeypath_continuum import PatchedTransactionFactory
|
||||||
from ihatemoney.utils import generate_password_hash, get_members, same_bill
|
from ihatemoney.utils import generate_password_hash, get_members, same_bill
|
||||||
from ihatemoney.versioning import (
|
from ihatemoney.versioning import (
|
||||||
|
@ -86,7 +85,6 @@ class Project(db.Model):
|
||||||
members = db.relationship("Person", backref="project")
|
members = db.relationship("Person", backref="project")
|
||||||
|
|
||||||
query_class = ProjectQuery
|
query_class = ProjectQuery
|
||||||
default_currency = db.Column(db.String(3))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _to_serialize(self):
|
def _to_serialize(self):
|
||||||
|
@ -96,7 +94,6 @@ class Project(db.Model):
|
||||||
"contact_email": self.contact_email,
|
"contact_email": self.contact_email,
|
||||||
"logging_preference": self.logging_preference.value,
|
"logging_preference": self.logging_preference.value,
|
||||||
"members": [],
|
"members": [],
|
||||||
"default_currency": self.default_currency,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
balance = self.balance
|
balance = self.balance
|
||||||
|
@ -130,16 +127,14 @@ class Project(db.Model):
|
||||||
total_weight = sum(ower.weight for ower in bill.owers)
|
total_weight = sum(ower.weight for ower in bill.owers)
|
||||||
|
|
||||||
if bill.bill_type == BillType.EXPENSE:
|
if bill.bill_type == BillType.EXPENSE:
|
||||||
should_receive[bill.payer.id] += bill.converted_amount
|
should_receive[bill.payer.id] += bill.amount
|
||||||
for ower in bill.owers:
|
for ower in bill.owers:
|
||||||
should_pay[ower.id] += (
|
should_pay[ower.id] += ower.weight * bill.amount / total_weight
|
||||||
ower.weight * bill.converted_amount / total_weight
|
|
||||||
)
|
|
||||||
|
|
||||||
if bill.bill_type == BillType.REIMBURSEMENT:
|
if bill.bill_type == BillType.REIMBURSEMENT:
|
||||||
should_receive[bill.payer.id] += bill.converted_amount
|
should_receive[bill.payer.id] += bill.amount
|
||||||
for ower in bill.owers:
|
for ower in bill.owers:
|
||||||
should_receive[ower.id] -= bill.converted_amount
|
should_receive[ower.id] -= bill.amount
|
||||||
|
|
||||||
for person in self.members:
|
for person in self.members:
|
||||||
balance = should_receive[person.id] - should_pay[person.id]
|
balance = should_receive[person.id] - should_pay[person.id]
|
||||||
|
@ -183,7 +178,7 @@ class Project(db.Model):
|
||||||
monthly = defaultdict(lambda: defaultdict(float))
|
monthly = defaultdict(lambda: defaultdict(float))
|
||||||
for bill in self.get_bills_unordered().all():
|
for bill in self.get_bills_unordered().all():
|
||||||
if bill.bill_type == BillType.EXPENSE:
|
if bill.bill_type == BillType.EXPENSE:
|
||||||
monthly[bill.date.year][bill.date.month] += bill.converted_amount
|
monthly[bill.date.year][bill.date.month] += bill.amount
|
||||||
return monthly
|
return monthly
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -204,7 +199,6 @@ class Project(db.Model):
|
||||||
"ower": transaction["ower"].name,
|
"ower": transaction["ower"].name,
|
||||||
"receiver": transaction["receiver"].name,
|
"receiver": transaction["receiver"].name,
|
||||||
"amount": round(transaction["amount"], 2),
|
"amount": round(transaction["amount"], 2),
|
||||||
"currency": transaction["currency"],
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return pretty_transactions
|
return pretty_transactions
|
||||||
|
@ -217,7 +211,6 @@ class Project(db.Model):
|
||||||
"ower": members[ower_id],
|
"ower": members[ower_id],
|
||||||
"receiver": members[receiver_id],
|
"receiver": members[receiver_id],
|
||||||
"amount": amount,
|
"amount": amount,
|
||||||
"currency": self.default_currency,
|
|
||||||
}
|
}
|
||||||
for ower_id, amount, receiver_id in settle_plan
|
for ower_id, amount, receiver_id in settle_plan
|
||||||
]
|
]
|
||||||
|
@ -228,16 +221,6 @@ class Project(db.Model):
|
||||||
"""return if the project do have bills or not"""
|
"""return if the project do have bills or not"""
|
||||||
return self.get_bills_unordered().count() > 0
|
return self.get_bills_unordered().count() > 0
|
||||||
|
|
||||||
def has_multiple_currencies(self):
|
|
||||||
"""Returns True if multiple currencies are used"""
|
|
||||||
# It would be more efficient to do the counting in the database,
|
|
||||||
# but this is called very rarely so we can tolerate if it's a bit
|
|
||||||
# slow. And doing this in Python is much more readable, see #784.
|
|
||||||
nb_currencies = len(
|
|
||||||
set(bill.original_currency for bill in self.get_bills_unordered())
|
|
||||||
)
|
|
||||||
return nb_currencies > 1
|
|
||||||
|
|
||||||
def get_bills_unordered(self):
|
def get_bills_unordered(self):
|
||||||
"""Base query for bill list"""
|
"""Base query for bill list"""
|
||||||
# The subqueryload option allows to pre-load data from the
|
# The subqueryload option allows to pre-load data from the
|
||||||
|
@ -344,7 +327,6 @@ class Project(db.Model):
|
||||||
"what": bill.what,
|
"what": bill.what,
|
||||||
"bill_type": bill.bill_type.value,
|
"bill_type": bill.bill_type.value,
|
||||||
"amount": round(bill.amount, 2),
|
"amount": round(bill.amount, 2),
|
||||||
"currency": bill.original_currency,
|
|
||||||
"date": str(bill.date),
|
"date": str(bill.date),
|
||||||
"payer_name": Person.query.get(bill.payer_id).name,
|
"payer_name": Person.query.get(bill.payer_id).name,
|
||||||
"payer_weight": Person.query.get(bill.payer_id).weight,
|
"payer_weight": Person.query.get(bill.payer_id).weight,
|
||||||
|
@ -353,41 +335,6 @@ class Project(db.Model):
|
||||||
)
|
)
|
||||||
return pretty_bills
|
return pretty_bills
|
||||||
|
|
||||||
def switch_currency(self, new_currency):
|
|
||||||
if new_currency == self.default_currency:
|
|
||||||
return
|
|
||||||
# Update converted currency
|
|
||||||
if new_currency == CurrencyConverter.no_currency:
|
|
||||||
if self.has_multiple_currencies():
|
|
||||||
raise ValueError(f"Can't unset currency of project {self.id}")
|
|
||||||
|
|
||||||
for bill in self.get_bills_unordered():
|
|
||||||
# We are removing the currency, and we already checked that all bills
|
|
||||||
# had the same currency: it means that we can simply strip the currency
|
|
||||||
# without converting the amounts. We basically ignore the current default_currency
|
|
||||||
|
|
||||||
# Reset converted amount in case it was different from the original amount
|
|
||||||
bill.converted_amount = bill.amount
|
|
||||||
# Strip currency
|
|
||||||
bill.original_currency = CurrencyConverter.no_currency
|
|
||||||
db.session.add(bill)
|
|
||||||
else:
|
|
||||||
for bill in self.get_bills_unordered():
|
|
||||||
if bill.original_currency == CurrencyConverter.no_currency:
|
|
||||||
# Bills that were created without currency will be set to the new currency
|
|
||||||
bill.original_currency = new_currency
|
|
||||||
bill.converted_amount = bill.amount
|
|
||||||
else:
|
|
||||||
# Convert amount for others, without touching original_currency
|
|
||||||
bill.converted_amount = CurrencyConverter().exchange_currency(
|
|
||||||
bill.amount, bill.original_currency, new_currency
|
|
||||||
)
|
|
||||||
db.session.add(bill)
|
|
||||||
|
|
||||||
self.default_currency = new_currency
|
|
||||||
db.session.add(self)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
def import_bills(self, bills: list):
|
def import_bills(self, bills: list):
|
||||||
"""Import bills from a list of dictionaries"""
|
"""Import bills from a list of dictionaries"""
|
||||||
# Add members not already in the project
|
# Add members not already in the project
|
||||||
|
@ -416,10 +363,8 @@ class Project(db.Model):
|
||||||
date=parse(b["date"]),
|
date=parse(b["date"]),
|
||||||
bill_type=b["bill_type"],
|
bill_type=b["bill_type"],
|
||||||
external_link="",
|
external_link="",
|
||||||
original_currency=b["currency"],
|
|
||||||
owers=Person.query.get_by_names(b["owers"], self),
|
owers=Person.query.get_by_names(b["owers"], self),
|
||||||
payer_id=id_dict[b["payer_name"]],
|
payer_id=id_dict[b["payer_name"]],
|
||||||
project_default_currency=self.default_currency,
|
|
||||||
what=b["what"],
|
what=b["what"],
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -527,7 +472,6 @@ class Project(db.Model):
|
||||||
name="demonstration",
|
name="demonstration",
|
||||||
password=generate_password_hash("demo"),
|
password=generate_password_hash("demo"),
|
||||||
contact_email="demo@notmyidea.org",
|
contact_email="demo@notmyidea.org",
|
||||||
default_currency="XXX",
|
|
||||||
)
|
)
|
||||||
db.session.add(project)
|
db.session.add(project)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -554,10 +498,8 @@ class Project(db.Model):
|
||||||
Bill(
|
Bill(
|
||||||
amount=amount,
|
amount=amount,
|
||||||
bill_type=bill_type,
|
bill_type=bill_type,
|
||||||
original_currency=project.default_currency,
|
|
||||||
owers=[members[name] for name in owers],
|
owers=[members[name] for name in owers],
|
||||||
payer_id=members[payer].id,
|
payer_id=members[payer].id,
|
||||||
project_default_currency=project.default_currency,
|
|
||||||
what=what,
|
what=what,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -689,22 +631,15 @@ class Bill(db.Model):
|
||||||
bill_type = db.Column(db.Enum(BillType))
|
bill_type = db.Column(db.Enum(BillType))
|
||||||
external_link = 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"))
|
archive = db.Column(db.Integer, db.ForeignKey("archive.id"))
|
||||||
|
|
||||||
currency_helper = CurrencyConverter()
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
amount: float,
|
amount: float,
|
||||||
date: datetime.datetime = None,
|
date: datetime.datetime = None,
|
||||||
external_link: str = "",
|
external_link: str = "",
|
||||||
original_currency: str = "",
|
|
||||||
owers: list = [],
|
owers: list = [],
|
||||||
payer_id: int = None,
|
payer_id: int = None,
|
||||||
project_default_currency: str = "",
|
|
||||||
what: str = "",
|
what: str = "",
|
||||||
bill_type: str = "Expense",
|
bill_type: str = "Expense",
|
||||||
):
|
):
|
||||||
|
@ -712,14 +647,10 @@ class Bill(db.Model):
|
||||||
self.amount = amount
|
self.amount = amount
|
||||||
self.date = date
|
self.date = date
|
||||||
self.external_link = external_link
|
self.external_link = external_link
|
||||||
self.original_currency = original_currency
|
|
||||||
self.owers = owers
|
self.owers = owers
|
||||||
self.payer_id = payer_id
|
self.payer_id = payer_id
|
||||||
self.what = what
|
self.what = what
|
||||||
self.bill_type = BillType(bill_type)
|
self.bill_type = BillType(bill_type)
|
||||||
self.converted_amount = self.currency_helper.exchange_currency(
|
|
||||||
self.amount, self.original_currency, project_default_currency
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _to_serialize(self):
|
def _to_serialize(self):
|
||||||
|
@ -733,8 +664,6 @@ class Bill(db.Model):
|
||||||
"what": self.what,
|
"what": self.what,
|
||||||
"bill_type": self.bill_type.value,
|
"bill_type": self.bill_type.value,
|
||||||
"external_link": self.external_link,
|
"external_link": self.external_link,
|
||||||
"original_currency": self.original_currency,
|
|
||||||
"converted_amount": self.converted_amount,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def pay_each_default(self, amount):
|
def pay_each_default(self, amount):
|
||||||
|
@ -759,7 +688,7 @@ class Bill(db.Model):
|
||||||
"""Warning: this is slow, if you need to compute this for many bills, do
|
"""Warning: this is slow, if you need to compute this for many bills, do
|
||||||
it differently (see balance_full function)
|
it differently (see balance_full function)
|
||||||
"""
|
"""
|
||||||
return self.pay_each_default(self.converted_amount)
|
return self.pay_each_default(self.amount)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -5,18 +5,16 @@ import warnings
|
||||||
|
|
||||||
from babel.dates import LOCALTZ
|
from babel.dates import LOCALTZ
|
||||||
from flask import Flask, g, render_template, request, session
|
from flask import Flask, g, render_template, request, session
|
||||||
from flask_babel import Babel, format_currency
|
from flask_babel import Babel
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
from flask_migrate import Migrate, stamp, upgrade
|
from flask_migrate import Migrate, stamp, upgrade
|
||||||
from flask_talisman import Talisman
|
from flask_talisman import Talisman
|
||||||
from jinja2 import pass_context
|
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
import pytz
|
import pytz
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
|
||||||
from ihatemoney import default_settings
|
from ihatemoney import default_settings
|
||||||
from ihatemoney.api.v1 import api as apiv1
|
from ihatemoney.api.v1 import api as apiv1
|
||||||
from ihatemoney.currency_convertor import CurrencyConverter
|
|
||||||
from ihatemoney.models import db
|
from ihatemoney.models import db
|
||||||
from ihatemoney.utils import (
|
from ihatemoney.utils import (
|
||||||
IhmJSONEncoder,
|
IhmJSONEncoder,
|
||||||
|
@ -176,9 +174,6 @@ def create_app(
|
||||||
# Configure the a, root="main"pplication
|
# Configure the a, root="main"pplication
|
||||||
setup_database(app)
|
setup_database(app)
|
||||||
|
|
||||||
# Setup Currency Cache
|
|
||||||
CurrencyConverter()
|
|
||||||
|
|
||||||
mail = Mail()
|
mail = Mail()
|
||||||
mail.init_app(app)
|
mail.init_app(app)
|
||||||
app.mail = mail
|
app.mail = mail
|
||||||
|
@ -220,25 +215,6 @@ def create_app(
|
||||||
else:
|
else:
|
||||||
Babel(app, default_timezone=default_timezone, locale_selector=get_locale)
|
Babel(app, default_timezone=default_timezone, locale_selector=get_locale)
|
||||||
|
|
||||||
# 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
|
|
||||||
@pass_context
|
|
||||||
def currency(context, number, currency=None, *args, **kwargs):
|
|
||||||
if currency is None:
|
|
||||||
currency = context.get("g").project.default_currency
|
|
||||||
"""
|
|
||||||
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["currency"] = currency
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ input(form.default_currency) }}
|
|
||||||
{{ input(form.current_password) }}
|
{{ input(form.current_password) }}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn btn-primary">{{ _("Save changes") }}</button>
|
<button class="btn btn-primary">{{ _("Save changes") }}</button>
|
||||||
|
@ -198,9 +197,6 @@
|
||||||
|
|
||||||
<details class="mb-3">
|
<details class="mb-3">
|
||||||
<summary class="mb-2">{{ _("More options") }}</summary>
|
<summary class="mb-2">{{ _("More options") }}</summary>
|
||||||
{% if g.project.default_currency != "XXX" %}
|
|
||||||
{{ input(form.original_currency, inline=True, class="form-control custom-select") }}
|
|
||||||
{% endif %}
|
|
||||||
{{ input(form.external_link, inline=True) }}
|
{{ input(form.external_link, inline=True) }}
|
||||||
</details>
|
</details>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
|
@ -97,7 +97,7 @@
|
||||||
<summary>{% if before %} {{ _("Details of the bill (before the change)") }} {% else %} {{ _("Details of the bill") }} {% endif %}</summary>
|
<summary>{% if before %} {{ _("Details of the bill (before the change)") }} {% else %} {{ _("Details of the bill") }} {% endif %}</summary>
|
||||||
{{ _("Date:") }} {{ details.date|em_surround }}.
|
{{ _("Date:") }} {{ details.date|em_surround }}.
|
||||||
{{ _("Payer:") }} {{ details.payer|em_surround }}.
|
{{ _("Payer:") }} {{ details.payer|em_surround }}.
|
||||||
{{ _("Amount:") }} {{ details.amount|currency(details.original_currency)|em_surround }}.
|
{{ _("Amount:") }} {{ details.amount|em_surround }}.
|
||||||
{{ _("Owers:") }} {{ owers_list_str }}.
|
{{ _("Owers:") }} {{ owers_list_str }}.
|
||||||
{% if details.external_link %}
|
{% if details.external_link %}
|
||||||
{{ _("External link:") }}
|
{{ _("External link:") }}
|
||||||
|
@ -229,10 +229,6 @@
|
||||||
{{ bill_property_change(event, _("Amount")) }}
|
{{ bill_property_change(event, _("Amount")) }}
|
||||||
{% elif event.prop_changed == "date" %}
|
{% elif event.prop_changed == "date" %}
|
||||||
{{ bill_property_change(event, _("Date")) }}
|
{{ bill_property_change(event, _("Date")) }}
|
||||||
{% elif event.prop_changed == "original_currency" %}
|
|
||||||
{{ bill_property_change(event, _("Currency")) }}
|
|
||||||
{% elif event.prop_changed == "converted_amount" %}
|
|
||||||
{{ bill_property_change(event, _("Amount in %(currency)s", currency=g.project.default_currency)) }}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans %}Bill {{ name }} modified{% endtrans %}
|
{% trans %}Bill {{ name }} modified{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{% extends "sidebar_table_layout.html" %}
|
{% extends "sidebar_table_layout.html" %}
|
||||||
|
|
||||||
{%- macro weighted_bill_amount(bill, weights, currency=bill.original_currency, amount=bill.amount) %}
|
{%- macro weighted_bill_amount(bill, weights, amount=bill.amount) %}
|
||||||
{{ amount|currency(currency) }}
|
{{ amount }}
|
||||||
{%- if weights != 1.0 %}
|
{%- if weights != 1.0 %}
|
||||||
({{ _("%(amount)s each", amount=(amount / weights)|currency(currency)) }})
|
({{ _("%(amount)s each", amount=(amount / weights)) }})
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{% endmacro -%}
|
{% endmacro -%}
|
||||||
|
|
||||||
|
@ -142,10 +142,7 @@
|
||||||
{{ bill.owers|join(', ', 'name') }}
|
{{ bill.owers|join(', ', 'name') }}
|
||||||
{%- endif %}</td>
|
{%- endif %}</td>
|
||||||
<td>
|
<td>
|
||||||
<span data-toggle="tooltip" data-placement="top"
|
|
||||||
title="{{ weighted_bill_amount(bill, weights, g.project.default_currency, bill.converted_amount) if bill.original_currency != g.project.default_currency else '' }}">
|
|
||||||
{{ weighted_bill_amount(bill, weights) }}
|
{{ weighted_bill_amount(bill, weights) }}
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="bill-actions d-flex align-items-center">
|
<td class="bill-actions d-flex align-items-center">
|
||||||
<a class="edit" href="{{ url_for(".edit_bill", bill_id=bill.id) }}" title="{{ _("edit") }}">{{ _('edit') }}</a>
|
<a class="edit" href="{{ url_for(".edit_bill", bill_id=bill.id) }}" title="{{ _("edit") }}">{{ _('edit') }}</a>
|
||||||
|
|
|
@ -10,11 +10,11 @@
|
||||||
<link>{{ url_for(".list_bills", _external=True) }}</link>
|
<link>{{ url_for(".list_bills", _external=True) }}</link>
|
||||||
{% for (weights, bill) in bills.items -%}
|
{% for (weights, bill) in bills.items -%}
|
||||||
<item>
|
<item>
|
||||||
<title>{{ bill.what }} - {{ bill.amount|currency(bill.original_currency) }}</title>
|
<title>{{ bill.what }} - {{ bill.amount | round(2) }}</title>
|
||||||
<guid isPermaLink="false">{{ bill.id }}</guid>
|
<guid isPermaLink="false">{{ bill.id }}</guid>
|
||||||
<dc:creator>{{ bill.payer }}</dc:creator>
|
<dc:creator>{{ bill.payer }}</dc:creator>
|
||||||
{% if bill.external_link %}<link>{{ bill.external_link }}</link>{% endif -%}
|
{% if bill.external_link %}<link>{{ bill.external_link }}</link>{% endif -%}
|
||||||
<description>{{ bill.date|dateformat("long") }} - {{ bill.owers|join(', ', 'name') }} : {{ (bill.amount/weights)|currency(bill.original_currency) }}</description>
|
<description>{{ bill.date|dateformat("long") }} - {{ bill.owers|join(', ', 'name') }} : {{ bill.amount/weights | round(2)}}</description>
|
||||||
<pubDate>{{ bill.creation_date.strftime("%a, %d %b %Y %T") }} +0000</pubDate>
|
<pubDate>{{ bill.creation_date.strftime("%a, %d %b %Y %T") }} +0000</pubDate>
|
||||||
</item>
|
</item>
|
||||||
{% endfor -%}
|
{% endfor -%}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<tr receiver={{bill.receiver.id}}>
|
<tr receiver={{bill.receiver.id}}>
|
||||||
<td>{{ bill.ower }}</td>
|
<td>{{ bill.ower }}</td>
|
||||||
<td>{{ bill.receiver }}</td>
|
<td>{{ bill.receiver }}</td>
|
||||||
<td>{{ bill.amount|currency }}</td>
|
<td>{{ bill.amount }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span id="settle-bill" class="ml-auto pb-2">
|
<span id="settle-bill" class="ml-auto pb-2">
|
||||||
<a href="{{ url_for('.settle', amount = bill.amount, ower_id = bill.ower.id, payer_id = bill.receiver.id) }}" class="btn btn-primary">
|
<a href="{{ url_for('.settle', amount = bill.amount, ower_id = bill.ower.id, payer_id = bill.receiver.id) }}" class="btn btn-primary">
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
<td class="balance-value {% if balance[member.id]|round(2) > 0 %}positive{% elif balance[member.id]|round(2) < 0 %}negative{% endif %}">
|
<td class="balance-value {% if balance[member.id]|round(2) > 0 %}positive{% elif balance[member.id]|round(2) < 0 %}negative{% endif %}">
|
||||||
{% if balance[member.id] | round(2) > 0 %}+{% endif %}{{ balance[member.id]|currency }}
|
{% if balance[member.id] | round(2) > 0 %}+{% endif %}{{ balance[member.id] }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
|
@ -15,8 +15,8 @@
|
||||||
{% for stat in members_stats|sort(attribute='member.name') %}
|
{% for stat in members_stats|sort(attribute='member.name') %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="d-md-none">{{ stat.member.name }}</td>
|
<td class="d-md-none">{{ stat.member.name }}</td>
|
||||||
<td>{{ stat.paid|currency }}</td>
|
<td>{{ stat.paid | round(2) }}</td>
|
||||||
<td>{{ stat.spent|currency }}</td>
|
<td>{{ stat.spent | round(2) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
{% for month in months %}
|
{% for month in months %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ month|dateformat("MMMM yyyy") }}</td>
|
<td>{{ month|dateformat("MMMM yyyy") }}</td>
|
||||||
<td>{{ monthly_stats[month.year][month.month]|currency }}</td>
|
<td>{{ monthly_stats[month.year][month.month] | round(2) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -2,8 +2,6 @@ import base64
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from ihatemoney.tests.common.help_functions import em_surround
|
from ihatemoney.tests.common.help_functions import em_surround
|
||||||
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
|
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
|
||||||
|
|
||||||
|
@ -11,9 +9,7 @@ from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
|
||||||
class TestAPI(IhatemoneyTestCase):
|
class TestAPI(IhatemoneyTestCase):
|
||||||
"""Tests the API"""
|
"""Tests the API"""
|
||||||
|
|
||||||
def api_create(
|
def api_create(self, name, id=None, password=None, contact=None):
|
||||||
self, name, id=None, password=None, contact=None, default_currency=None
|
|
||||||
):
|
|
||||||
id = id or name
|
id = id or name
|
||||||
password = password or name
|
password = password or name
|
||||||
contact = contact or f"{name}@notmyidea.org"
|
contact = contact or f"{name}@notmyidea.org"
|
||||||
|
@ -24,8 +20,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
"password": password,
|
"password": password,
|
||||||
"contact_email": contact,
|
"contact_email": contact,
|
||||||
}
|
}
|
||||||
if default_currency:
|
|
||||||
data["default_currency"] = default_currency
|
|
||||||
|
|
||||||
return self.client.post(
|
return self.client.post(
|
||||||
"/api/projects",
|
"/api/projects",
|
||||||
|
@ -90,7 +84,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"password": "raclette",
|
"password": "raclette",
|
||||||
"contact_email": "not-an-email",
|
"contact_email": "not-an-email",
|
||||||
"default_currency": "XXX",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -124,7 +117,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
"members": [],
|
"members": [],
|
||||||
"name": "raclette",
|
"name": "raclette",
|
||||||
"contact_email": "raclette@notmyidea.org",
|
"contact_email": "raclette@notmyidea.org",
|
||||||
"default_currency": "XXX",
|
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"logging_preference": 1,
|
"logging_preference": 1,
|
||||||
}
|
}
|
||||||
|
@ -136,7 +128,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
"/api/projects/raclette",
|
"/api/projects/raclette",
|
||||||
data={
|
data={
|
||||||
"contact_email": "yeah@notmyidea.org",
|
"contact_email": "yeah@notmyidea.org",
|
||||||
"default_currency": "XXX",
|
|
||||||
"password": "raclette",
|
"password": "raclette",
|
||||||
"name": "The raclette party",
|
"name": "The raclette party",
|
||||||
"project_history": "y",
|
"project_history": "y",
|
||||||
|
@ -150,7 +141,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
"/api/projects/raclette",
|
"/api/projects/raclette",
|
||||||
data={
|
data={
|
||||||
"contact_email": "yeah@notmyidea.org",
|
"contact_email": "yeah@notmyidea.org",
|
||||||
"default_currency": "XXX",
|
|
||||||
"current_password": "fromage aux patates",
|
"current_password": "fromage aux patates",
|
||||||
"password": "raclette",
|
"password": "raclette",
|
||||||
"name": "The raclette party",
|
"name": "The raclette party",
|
||||||
|
@ -165,7 +155,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
"/api/projects/raclette",
|
"/api/projects/raclette",
|
||||||
data={
|
data={
|
||||||
"contact_email": "yeah@notmyidea.org",
|
"contact_email": "yeah@notmyidea.org",
|
||||||
"default_currency": "XXX",
|
|
||||||
"current_password": "raclette",
|
"current_password": "raclette",
|
||||||
"password": "raclette",
|
"password": "raclette",
|
||||||
"name": "The raclette party",
|
"name": "The raclette party",
|
||||||
|
@ -183,7 +172,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
expected = {
|
expected = {
|
||||||
"name": "The raclette party",
|
"name": "The raclette party",
|
||||||
"contact_email": "yeah@notmyidea.org",
|
"contact_email": "yeah@notmyidea.org",
|
||||||
"default_currency": "XXX",
|
|
||||||
"members": [],
|
"members": [],
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"logging_preference": 1,
|
"logging_preference": 1,
|
||||||
|
@ -196,7 +184,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
"/api/projects/raclette",
|
"/api/projects/raclette",
|
||||||
data={
|
data={
|
||||||
"contact_email": "yeah@notmyidea.org",
|
"contact_email": "yeah@notmyidea.org",
|
||||||
"default_currency": "XXX",
|
|
||||||
"current_password": "raclette",
|
"current_password": "raclette",
|
||||||
"password": "tartiflette",
|
"password": "tartiflette",
|
||||||
"name": "The raclette party",
|
"name": "The raclette party",
|
||||||
|
@ -250,7 +237,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
"/api/projects/raclette",
|
"/api/projects/raclette",
|
||||||
data={
|
data={
|
||||||
"contact_email": "yeah@notmyidea.org",
|
"contact_email": "yeah@notmyidea.org",
|
||||||
"default_currency": "XXX",
|
|
||||||
"password": "tartiflette",
|
"password": "tartiflette",
|
||||||
"name": "The raclette party",
|
"name": "The raclette party",
|
||||||
},
|
},
|
||||||
|
@ -435,8 +421,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
"amount": 25.0,
|
"amount": 25.0,
|
||||||
"date": "2011-08-10",
|
"date": "2011-08-10",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"converted_amount": 25.0,
|
|
||||||
"original_currency": "XXX",
|
|
||||||
"external_link": "https://raclette.fr",
|
"external_link": "https://raclette.fr",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -507,8 +491,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
"amount": 25.0,
|
"amount": 25.0,
|
||||||
"date": "2011-09-10",
|
"date": "2011-09-10",
|
||||||
"external_link": "https://raclette.fr",
|
"external_link": "https://raclette.fr",
|
||||||
"converted_amount": 25.0,
|
|
||||||
"original_currency": "XXX",
|
|
||||||
"id": 1,
|
"id": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -588,8 +570,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
"date": "2011-08-10",
|
"date": "2011-08-10",
|
||||||
"id": id,
|
"id": id,
|
||||||
"external_link": "",
|
"external_link": "",
|
||||||
"original_currency": "XXX",
|
|
||||||
"converted_amount": expected_amount,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
got = json.loads(req.data.decode("utf-8"))
|
got = json.loads(req.data.decode("utf-8"))
|
||||||
|
@ -624,169 +604,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
)
|
)
|
||||||
self.assertStatus(400, req)
|
self.assertStatus(400, req)
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Currency conversion is broken")
|
|
||||||
def test_currencies(self):
|
|
||||||
# check /currencies for list of supported currencies
|
|
||||||
resp = self.client.get("/api/currencies")
|
|
||||||
assert 200 == resp.status_code
|
|
||||||
assert "XXX" in json.loads(resp.data.decode("utf-8"))
|
|
||||||
|
|
||||||
# create project with a default currency
|
|
||||||
resp = self.api_create("raclette", default_currency="EUR")
|
|
||||||
assert 201 == resp.status_code
|
|
||||||
|
|
||||||
# get information about it
|
|
||||||
resp = self.client.get(
|
|
||||||
"/api/projects/raclette", headers=self.get_auth("raclette")
|
|
||||||
)
|
|
||||||
|
|
||||||
assert 200 == resp.status_code
|
|
||||||
expected = {
|
|
||||||
"members": [],
|
|
||||||
"name": "raclette",
|
|
||||||
"contact_email": "raclette@notmyidea.org",
|
|
||||||
"default_currency": "EUR",
|
|
||||||
"id": "raclette",
|
|
||||||
"logging_preference": 1,
|
|
||||||
}
|
|
||||||
decoded_resp = json.loads(resp.data.decode("utf-8"))
|
|
||||||
assert decoded_resp == expected
|
|
||||||
|
|
||||||
# Add participants
|
|
||||||
self.api_add_member("raclette", "zorglub")
|
|
||||||
self.api_add_member("raclette", "jeanne")
|
|
||||||
self.api_add_member("raclette", "quentin")
|
|
||||||
|
|
||||||
# Add a bill without explicit currency
|
|
||||||
req = self.client.post(
|
|
||||||
"/api/projects/raclette/bills",
|
|
||||||
data={
|
|
||||||
"date": "2011-08-10",
|
|
||||||
"what": "fromage",
|
|
||||||
"payer": "1",
|
|
||||||
"payed_for": ["1", "2"],
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": "25",
|
|
||||||
"external_link": "https://raclette.fr",
|
|
||||||
},
|
|
||||||
headers=self.get_auth("raclette"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# should return the id
|
|
||||||
self.assertStatus(201, req)
|
|
||||||
assert req.data.decode("utf-8") == "1\n"
|
|
||||||
|
|
||||||
# get this bill details
|
|
||||||
req = self.client.get(
|
|
||||||
"/api/projects/raclette/bills/1", headers=self.get_auth("raclette")
|
|
||||||
)
|
|
||||||
|
|
||||||
# compare with the added info
|
|
||||||
self.assertStatus(200, req)
|
|
||||||
expected = {
|
|
||||||
"what": "fromage",
|
|
||||||
"payer_id": 1,
|
|
||||||
"owers": [
|
|
||||||
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
|
|
||||||
{"activated": True, "id": 2, "name": "jeanne", "weight": 1},
|
|
||||||
],
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": 25.0,
|
|
||||||
"date": "2011-08-10",
|
|
||||||
"id": 1,
|
|
||||||
"converted_amount": 25.0,
|
|
||||||
"original_currency": "EUR",
|
|
||||||
"external_link": "https://raclette.fr",
|
|
||||||
}
|
|
||||||
|
|
||||||
got = json.loads(req.data.decode("utf-8"))
|
|
||||||
assert (
|
|
||||||
datetime.date.today()
|
|
||||||
== datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date()
|
|
||||||
)
|
|
||||||
del got["creation_date"]
|
|
||||||
assert expected == got
|
|
||||||
|
|
||||||
# Change bill amount and currency
|
|
||||||
req = self.client.put(
|
|
||||||
"/api/projects/raclette/bills/1",
|
|
||||||
data={
|
|
||||||
"date": "2011-08-10",
|
|
||||||
"what": "fromage",
|
|
||||||
"payer": "1",
|
|
||||||
"payed_for": ["1", "2"],
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": "30",
|
|
||||||
"external_link": "https://raclette.fr",
|
|
||||||
"original_currency": "CAD",
|
|
||||||
},
|
|
||||||
headers=self.get_auth("raclette"),
|
|
||||||
)
|
|
||||||
self.assertStatus(200, req)
|
|
||||||
|
|
||||||
# Check result
|
|
||||||
req = self.client.get(
|
|
||||||
"/api/projects/raclette/bills/1", headers=self.get_auth("raclette")
|
|
||||||
)
|
|
||||||
self.assertStatus(200, req)
|
|
||||||
expected_amount = self.converter.exchange_currency(30.0, "CAD", "EUR")
|
|
||||||
expected = {
|
|
||||||
"what": "fromage",
|
|
||||||
"payer_id": 1,
|
|
||||||
"owers": [
|
|
||||||
{"activated": True, "id": 1, "name": "zorglub", "weight": 1.0},
|
|
||||||
{"activated": True, "id": 2, "name": "jeanne", "weight": 1.0},
|
|
||||||
],
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": 30.0,
|
|
||||||
"date": "2011-08-10",
|
|
||||||
"id": 1,
|
|
||||||
"converted_amount": expected_amount,
|
|
||||||
"original_currency": "CAD",
|
|
||||||
"external_link": "https://raclette.fr",
|
|
||||||
}
|
|
||||||
|
|
||||||
got = json.loads(req.data.decode("utf-8"))
|
|
||||||
del got["creation_date"]
|
|
||||||
assert expected == got
|
|
||||||
|
|
||||||
# Add a bill with yet another currency
|
|
||||||
req = self.client.post(
|
|
||||||
"/api/projects/raclette/bills",
|
|
||||||
data={
|
|
||||||
"date": "2011-09-10",
|
|
||||||
"what": "Pierogi",
|
|
||||||
"payer": "1",
|
|
||||||
"payed_for": ["2", "3"],
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": "80",
|
|
||||||
"original_currency": "PLN",
|
|
||||||
},
|
|
||||||
headers=self.get_auth("raclette"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# should return the id
|
|
||||||
self.assertStatus(201, req)
|
|
||||||
assert req.data.decode("utf-8") == "2\n"
|
|
||||||
|
|
||||||
# Try to remove default project currency, it should fail
|
|
||||||
req = self.client.put(
|
|
||||||
"/api/projects/raclette",
|
|
||||||
data={
|
|
||||||
"contact_email": "yeah@notmyidea.org",
|
|
||||||
"default_currency": "XXX",
|
|
||||||
"current_password": "raclette",
|
|
||||||
"password": "raclette",
|
|
||||||
"name": "The raclette party",
|
|
||||||
},
|
|
||||||
headers=self.get_auth("raclette"),
|
|
||||||
)
|
|
||||||
self.assertStatus(400, req)
|
|
||||||
assert "This project cannot be set" in req.data.decode("utf-8")
|
|
||||||
assert "because it contains bills in multiple currencies" in req.data.decode(
|
|
||||||
"utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_statistics(self):
|
def test_statistics(self):
|
||||||
# create a project
|
# create a project
|
||||||
self.api_create("raclette")
|
self.api_create("raclette")
|
||||||
|
@ -896,8 +713,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
"date": "2011-08-10",
|
"date": "2011-08-10",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"external_link": "",
|
"external_link": "",
|
||||||
"converted_amount": 25.0,
|
|
||||||
"original_currency": "XXX",
|
|
||||||
}
|
}
|
||||||
got = json.loads(req.data.decode("utf-8"))
|
got = json.loads(req.data.decode("utf-8"))
|
||||||
assert (
|
assert (
|
||||||
|
@ -940,7 +755,6 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"name": "raclette",
|
"name": "raclette",
|
||||||
"logging_preference": 1,
|
"logging_preference": 1,
|
||||||
"default_currency": "XXX",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.assertStatus(200, req)
|
self.assertStatus(200, req)
|
||||||
|
|
|
@ -4,11 +4,9 @@ import re
|
||||||
from urllib.parse import unquote, urlparse, urlunparse
|
from urllib.parse import unquote, urlparse, urlunparse
|
||||||
|
|
||||||
from flask import session, url_for
|
from flask import session, url_for
|
||||||
import pytest
|
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
from ihatemoney import models
|
from ihatemoney import models
|
||||||
from ihatemoney.currency_convertor import CurrencyConverter
|
|
||||||
from ihatemoney.tests.common.help_functions import extract_link
|
from ihatemoney.tests.common.help_functions import extract_link
|
||||||
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
|
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
|
||||||
from ihatemoney.utils import generate_password_hash
|
from ihatemoney.utils import generate_password_hash
|
||||||
|
@ -182,7 +180,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"contact_email": "zorglub@notmyidea.org",
|
"contact_email": "zorglub@notmyidea.org",
|
||||||
"current_password": "raclette",
|
"current_password": "raclette",
|
||||||
"password": "didoudida",
|
"password": "didoudida",
|
||||||
"default_currency": "XXX",
|
|
||||||
},
|
},
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
|
@ -259,7 +256,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"password": "party",
|
"password": "party",
|
||||||
"contact_email": "raclette@notmyidea.org",
|
"contact_email": "raclette@notmyidea.org",
|
||||||
"default_currency": "USD",
|
|
||||||
},
|
},
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
|
@ -287,7 +283,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"id": "raclette", # already used !
|
"id": "raclette", # already used !
|
||||||
"password": "party",
|
"password": "party",
|
||||||
"contact_email": "raclette@notmyidea.org",
|
"contact_email": "raclette@notmyidea.org",
|
||||||
"default_currency": "USD",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -305,7 +300,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"password": "party",
|
"password": "party",
|
||||||
"contact_email": "raclette@notmyidea.org",
|
"contact_email": "raclette@notmyidea.org",
|
||||||
"default_currency": "USD",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -326,7 +320,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"password": "party",
|
"password": "party",
|
||||||
"contact_email": "raclette@notmyidea.org",
|
"contact_email": "raclette@notmyidea.org",
|
||||||
"default_currency": "USD",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -345,7 +338,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"password": "party",
|
"password": "party",
|
||||||
"contact_email": "raclette@notmyidea.org",
|
"contact_email": "raclette@notmyidea.org",
|
||||||
"default_currency": "USD",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -963,6 +955,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
result[self.get_project("raclette").members[0].id] = 8.12
|
result[self.get_project("raclette").members[0].id] = 8.12
|
||||||
result[self.get_project("raclette").members[1].id] = 0.0
|
result[self.get_project("raclette").members[1].id] = 0.0
|
||||||
result[self.get_project("raclette").members[2].id] = -8.12
|
result[self.get_project("raclette").members[2].id] = -8.12
|
||||||
|
|
||||||
# Since we're using floating point to store currency, we can have some
|
# Since we're using floating point to store currency, we can have some
|
||||||
# rounding issues that prevent test from working.
|
# rounding issues that prevent test from working.
|
||||||
# However, we should obtain the same values as the theoretical ones if we
|
# However, we should obtain the same values as the theoretical ones if we
|
||||||
|
@ -979,7 +972,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"contact_email": "zorglub@notmyidea.org",
|
"contact_email": "zorglub@notmyidea.org",
|
||||||
"password": "didoudida",
|
"password": "didoudida",
|
||||||
"logging_preference": LoggingMode.ENABLED.value,
|
"logging_preference": LoggingMode.ENABLED.value,
|
||||||
"default_currency": "USD",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# It should fail if we don't provide the current password
|
# It should fail if we don't provide the current password
|
||||||
|
@ -988,7 +980,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
project = self.get_project("raclette")
|
project = self.get_project("raclette")
|
||||||
assert project.name != new_data["name"]
|
assert project.name != new_data["name"]
|
||||||
assert project.contact_email != new_data["contact_email"]
|
assert project.contact_email != new_data["contact_email"]
|
||||||
assert project.default_currency != new_data["default_currency"]
|
|
||||||
assert not check_password_hash(project.password, new_data["password"])
|
assert not check_password_hash(project.password, new_data["password"])
|
||||||
|
|
||||||
# It should fail if we provide the wrong current password
|
# It should fail if we provide the wrong current password
|
||||||
|
@ -998,7 +989,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
project = self.get_project("raclette")
|
project = self.get_project("raclette")
|
||||||
assert project.name != new_data["name"]
|
assert project.name != new_data["name"]
|
||||||
assert project.contact_email != new_data["contact_email"]
|
assert project.contact_email != new_data["contact_email"]
|
||||||
assert project.default_currency != new_data["default_currency"]
|
|
||||||
assert not check_password_hash(project.password, new_data["password"])
|
assert not check_password_hash(project.password, new_data["password"])
|
||||||
|
|
||||||
# It should work if we give the current private code
|
# It should work if we give the current private code
|
||||||
|
@ -1008,7 +998,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
project = self.get_project("raclette")
|
project = self.get_project("raclette")
|
||||||
assert project.name == new_data["name"]
|
assert project.name == new_data["name"]
|
||||||
assert project.contact_email == new_data["contact_email"]
|
assert project.contact_email == new_data["contact_email"]
|
||||||
assert project.default_currency == new_data["default_currency"]
|
|
||||||
assert check_password_hash(project.password, new_data["password"])
|
assert check_password_hash(project.password, new_data["password"])
|
||||||
|
|
||||||
# Editing a project with a wrong email address should fail
|
# Editing a project with a wrong email address should fail
|
||||||
|
@ -1055,7 +1044,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
|
|
||||||
def test_statistics(self):
|
def test_statistics(self):
|
||||||
# Output is checked with the USD sign
|
# Output is checked with the USD sign
|
||||||
self.post_project("raclette", default_currency="USD")
|
self.post_project("raclette")
|
||||||
|
|
||||||
# add participants
|
# add participants
|
||||||
self.client.post("/raclette/members/add", data={"name": "zorglub", "weight": 2})
|
self.client.post("/raclette/members/add", data={"name": "zorglub", "weight": 2})
|
||||||
|
@ -1117,18 +1106,18 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
response = self.client.get("/raclette/statistics")
|
response = self.client.get("/raclette/statistics")
|
||||||
regex = r"<td class=\"d-md-none\">{}</td>\s*<td>{}</td>\s*<td>{}</td>"
|
regex = r"<td class=\"d-md-none\">{}</td>\s*<td>{}</td>\s*<td>{}</td>"
|
||||||
assert re.search(
|
assert re.search(
|
||||||
regex.format("zorglub", r"\$20\.00", r"\$31\.67"),
|
regex.format("zorglub", r"20\.0", r"31\.67"),
|
||||||
response.data.decode("utf-8"),
|
response.data.decode("utf-8"),
|
||||||
)
|
)
|
||||||
assert re.search(
|
assert re.search(
|
||||||
regex.format("jeanne", r"\$20\.00", r"\$5\.83"),
|
regex.format("jeanne", r"20\.0", r"5\.83"),
|
||||||
response.data.decode("utf-8"),
|
response.data.decode("utf-8"),
|
||||||
)
|
)
|
||||||
assert re.search(
|
assert re.search(
|
||||||
regex.format("tata", r"\$0\.00", r"\$2\.50"), response.data.decode("utf-8")
|
regex.format("tata", r"0", r"2\.5"), response.data.decode("utf-8")
|
||||||
)
|
)
|
||||||
assert re.search(
|
assert re.search(
|
||||||
regex.format("pépé", r"\$0\.00", r"\$0\.00"), response.data.decode("utf-8")
|
regex.format("pépé", r"0", r"0"), response.data.decode("utf-8")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check that the order of participants in the sidebar table is the
|
# Check that the order of participants in the sidebar table is the
|
||||||
|
@ -1556,225 +1545,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
member = models.Person.query.filter(models.Person.id == 1).one_or_none()
|
member = models.Person.query.filter(models.Person.id == 1).one_or_none()
|
||||||
assert member is None
|
assert member is None
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Currency conversion is broken")
|
|
||||||
def test_currency_switch(self):
|
|
||||||
# A project should be editable
|
|
||||||
self.post_project("raclette")
|
|
||||||
|
|
||||||
# add participants
|
|
||||||
self.client.post("/raclette/members/add", data={"name": "zorglub"})
|
|
||||||
self.client.post("/raclette/members/add", data={"name": "jeanne"})
|
|
||||||
self.client.post("/raclette/members/add", data={"name": "tata"})
|
|
||||||
|
|
||||||
# create bills
|
|
||||||
self.client.post(
|
|
||||||
"/raclette/add",
|
|
||||||
data={
|
|
||||||
"date": "2016-12-31",
|
|
||||||
"what": "fromage à raclette",
|
|
||||||
"payer": 1,
|
|
||||||
"payed_for": [1, 2, 3],
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": "10.0",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.post(
|
|
||||||
"/raclette/add",
|
|
||||||
data={
|
|
||||||
"date": "2016-12-31",
|
|
||||||
"what": "red wine",
|
|
||||||
"payer": 2,
|
|
||||||
"payed_for": [1, 3],
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": "20",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.post(
|
|
||||||
"/raclette/add",
|
|
||||||
data={
|
|
||||||
"date": "2017-01-01",
|
|
||||||
"what": "refund",
|
|
||||||
"payer": 3,
|
|
||||||
"payed_for": [2],
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": "13.33",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
project = self.get_project("raclette")
|
|
||||||
|
|
||||||
# First all converted_amount should be the same as amount, with no currency
|
|
||||||
for bill in project.get_bills():
|
|
||||||
assert bill.original_currency == CurrencyConverter.no_currency
|
|
||||||
assert bill.amount == bill.converted_amount
|
|
||||||
|
|
||||||
# Then, switch to EUR, all bills must have been changed to this currency
|
|
||||||
project.switch_currency("EUR")
|
|
||||||
for bill in project.get_bills():
|
|
||||||
assert bill.original_currency == "EUR"
|
|
||||||
assert bill.amount == bill.converted_amount
|
|
||||||
|
|
||||||
# Add a bill in EUR, the current default currency
|
|
||||||
self.client.post(
|
|
||||||
"/raclette/add",
|
|
||||||
data={
|
|
||||||
"date": "2017-01-01",
|
|
||||||
"what": "refund from EUR",
|
|
||||||
"payer": 3,
|
|
||||||
"payed_for": [2],
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": "20",
|
|
||||||
"original_currency": "EUR",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
last_bill = project.get_bills().first()
|
|
||||||
assert last_bill.converted_amount == last_bill.amount
|
|
||||||
|
|
||||||
# Erase all currencies
|
|
||||||
project.switch_currency(CurrencyConverter.no_currency)
|
|
||||||
for bill in project.get_bills():
|
|
||||||
assert bill.original_currency == CurrencyConverter.no_currency
|
|
||||||
assert bill.amount == bill.converted_amount
|
|
||||||
|
|
||||||
# Let's go back to EUR to test conversion
|
|
||||||
project.switch_currency("EUR")
|
|
||||||
# This is a bill in CAD
|
|
||||||
self.client.post(
|
|
||||||
"/raclette/add",
|
|
||||||
data={
|
|
||||||
"date": "2017-01-01",
|
|
||||||
"what": "Poutine",
|
|
||||||
"payer": 3,
|
|
||||||
"payed_for": [2],
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": "18",
|
|
||||||
"original_currency": "CAD",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
last_bill = project.get_bills().first()
|
|
||||||
expected_amount = self.converter.exchange_currency(
|
|
||||||
last_bill.amount, "CAD", "EUR"
|
|
||||||
)
|
|
||||||
assert last_bill.converted_amount == expected_amount
|
|
||||||
|
|
||||||
# Switch to USD. Now, NO bill should be in USD, since they already had a currency
|
|
||||||
project.switch_currency("USD")
|
|
||||||
for bill in project.get_bills():
|
|
||||||
assert bill.original_currency != "USD"
|
|
||||||
expected_amount = self.converter.exchange_currency(
|
|
||||||
bill.amount, bill.original_currency, "USD"
|
|
||||||
)
|
|
||||||
assert bill.converted_amount == expected_amount
|
|
||||||
|
|
||||||
# Switching back to no currency must fail
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
project.switch_currency(CurrencyConverter.no_currency)
|
|
||||||
|
|
||||||
# It also must fails with a nice error using the form
|
|
||||||
resp = self.client.post(
|
|
||||||
"/raclette/edit",
|
|
||||||
data={
|
|
||||||
"name": "demonstration",
|
|
||||||
"password": "demo",
|
|
||||||
"contact_email": "demo@notmyidea.org",
|
|
||||||
"project_history": "y",
|
|
||||||
"default_currency": CurrencyConverter.no_currency,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
# A user displayed error should be generated, and its currency should be the same.
|
|
||||||
self.assertStatus(200, resp)
|
|
||||||
assert '<p class="alert alert-danger">' in resp.data.decode("utf-8")
|
|
||||||
assert self.get_project("raclette").default_currency == "USD"
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Currency conversion is broken")
|
|
||||||
def test_currency_switch_to_bill_currency(self):
|
|
||||||
# Default currency is 'XXX', but we should start from a project with a currency
|
|
||||||
self.post_project("raclette", default_currency="USD")
|
|
||||||
|
|
||||||
# add participants
|
|
||||||
self.client.post("/raclette/members/add", data={"name": "zorglub"})
|
|
||||||
self.client.post("/raclette/members/add", data={"name": "jeanne"})
|
|
||||||
|
|
||||||
# Bill with a different currency than project's default
|
|
||||||
self.client.post(
|
|
||||||
"/raclette/add",
|
|
||||||
data={
|
|
||||||
"date": "2016-12-31",
|
|
||||||
"what": "fromage à raclette",
|
|
||||||
"payer": 1,
|
|
||||||
"payed_for": [1, 2],
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": "10.0",
|
|
||||||
"original_currency": "EUR",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
project = self.get_project("raclette")
|
|
||||||
|
|
||||||
bill = project.get_bills().first()
|
|
||||||
assert (
|
|
||||||
self.converter.exchange_currency(bill.amount, "EUR", "USD")
|
|
||||||
== bill.converted_amount
|
|
||||||
)
|
|
||||||
|
|
||||||
# And switch project to the currency from the bill we created
|
|
||||||
project.switch_currency("EUR")
|
|
||||||
bill = project.get_bills().first()
|
|
||||||
assert bill.converted_amount == bill.amount
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Currency conversion is broken")
|
|
||||||
def test_currency_switch_to_no_currency(self):
|
|
||||||
# Default currency is 'XXX', but we should start from a project with a currency
|
|
||||||
self.post_project("raclette", default_currency="USD")
|
|
||||||
|
|
||||||
# add participants
|
|
||||||
self.client.post("/raclette/members/add", data={"name": "zorglub"})
|
|
||||||
self.client.post("/raclette/members/add", data={"name": "jeanne"})
|
|
||||||
|
|
||||||
# Bills with a different currency than project's default
|
|
||||||
self.client.post(
|
|
||||||
"/raclette/add",
|
|
||||||
data={
|
|
||||||
"date": "2016-12-31",
|
|
||||||
"what": "fromage à raclette",
|
|
||||||
"payer": 1,
|
|
||||||
"payed_for": [1, 2],
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": "10.0",
|
|
||||||
"original_currency": "EUR",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.post(
|
|
||||||
"/raclette/add",
|
|
||||||
data={
|
|
||||||
"date": "2017-01-01",
|
|
||||||
"what": "aspirine",
|
|
||||||
"payer": 2,
|
|
||||||
"payed_for": [1, 2],
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": "5.0",
|
|
||||||
"original_currency": "EUR",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
project = self.get_project("raclette")
|
|
||||||
|
|
||||||
for bill in project.get_bills_unordered():
|
|
||||||
assert (
|
|
||||||
self.converter.exchange_currency(bill.amount, "EUR", "USD")
|
|
||||||
== bill.converted_amount
|
|
||||||
)
|
|
||||||
|
|
||||||
# And switch project to no currency: amount should be equal to what was submitted
|
|
||||||
project.switch_currency(CurrencyConverter.no_currency)
|
|
||||||
no_currency_bills = [
|
|
||||||
(bill.amount, bill.converted_amount) for bill in project.get_bills()
|
|
||||||
]
|
|
||||||
assert no_currency_bills == [(5.0, 5.0), (10.0, 10.0)]
|
|
||||||
|
|
||||||
def test_amount_is_null(self):
|
def test_amount_is_null(self):
|
||||||
self.post_project("raclette")
|
self.post_project("raclette")
|
||||||
|
|
||||||
|
@ -1791,7 +1561,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
"bill_type": "Expense",
|
"bill_type": "Expense",
|
||||||
"amount": "0",
|
"amount": "0",
|
||||||
"original_currency": "XXX",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1836,7 +1605,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
"bill_type": "Expense",
|
"bill_type": "Expense",
|
||||||
"amount": "9347242149381274732472348728748723473278472843.12",
|
"amount": "9347242149381274732472348728748723473278472843.12",
|
||||||
"original_currency": "EUR",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert '<p class="alert alert-danger">' in resp.data.decode("utf-8")
|
assert '<p class="alert alert-danger">' in resp.data.decode("utf-8")
|
||||||
|
@ -1870,7 +1638,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"""
|
"""
|
||||||
Tests that the RSS feed output content is expected.
|
Tests that the RSS feed output content is expected.
|
||||||
"""
|
"""
|
||||||
self.post_project("raclette", default_currency="EUR")
|
self.post_project("raclette")
|
||||||
self.client.post("/raclette/members/add", data={"name": "george"})
|
self.client.post("/raclette/members/add", data={"name": "george"})
|
||||||
self.client.post("/raclette/members/add", data={"name": "peter"})
|
self.client.post("/raclette/members/add", data={"name": "peter"})
|
||||||
self.client.post("/raclette/members/add", data={"name": "steven"})
|
self.client.post("/raclette/members/add", data={"name": "steven"})
|
||||||
|
@ -1883,7 +1651,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1, 2, 3],
|
"payed_for": [1, 2, 3],
|
||||||
"amount": "12",
|
"amount": "12",
|
||||||
"original_currency": "EUR",
|
|
||||||
"bill_type": "Expense",
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1895,7 +1662,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
"amount": "15",
|
"amount": "15",
|
||||||
"original_currency": "EUR",
|
|
||||||
"bill_type": "Expense",
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1907,7 +1673,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
"original_currency": "EUR",
|
|
||||||
"bill_type": "Expense",
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1925,23 +1690,23 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
<atom:link href="http://localhost/raclette/feed/{token}.xml" rel="self" type="application/rss+xml" />
|
<atom:link href="http://localhost/raclette/feed/{token}.xml" rel="self" type="application/rss+xml" />
|
||||||
<link>http://localhost/raclette/</link>
|
<link>http://localhost/raclette/</link>
|
||||||
<item>
|
<item>
|
||||||
<title>fromage à raclette - €12.00</title>
|
<title>fromage à raclette - 12.0</title>
|
||||||
<guid isPermaLink="false">1</guid>
|
<guid isPermaLink="false">1</guid>
|
||||||
<dc:creator>george</dc:creator>
|
<dc:creator>george</dc:creator>
|
||||||
<description>December 31, 2016 - george, peter, steven : €4.00</description>
|
<description>December 31, 2016 - george, peter, steven : 4.0</description>
|
||||||
"""
|
"""
|
||||||
in content
|
in content
|
||||||
)
|
)
|
||||||
|
|
||||||
assert """<title>charcuterie - €15.00</title>""" in content
|
assert "<title>charcuterie - 15.0</title>" in content
|
||||||
assert """<title>vin blanc - €10.00</title>""" in content
|
assert "<title>vin blanc - 10.0</title>" in content
|
||||||
|
|
||||||
def test_rss_feed_history_disabled(self):
|
def test_rss_feed_history_disabled(self):
|
||||||
"""
|
"""
|
||||||
Tests that RSS feeds is correctly rendered even if the project
|
Tests that RSS feeds is correctly rendered even if the project
|
||||||
history is disabled.
|
history is disabled.
|
||||||
"""
|
"""
|
||||||
self.post_project("raclette", default_currency="EUR", project_history=False)
|
self.post_project("raclette", project_history=False)
|
||||||
self.client.post("/raclette/members/add", data={"name": "george"})
|
self.client.post("/raclette/members/add", data={"name": "george"})
|
||||||
self.client.post("/raclette/members/add", data={"name": "peter"})
|
self.client.post("/raclette/members/add", data={"name": "peter"})
|
||||||
self.client.post("/raclette/members/add", data={"name": "steven"})
|
self.client.post("/raclette/members/add", data={"name": "steven"})
|
||||||
|
@ -1954,7 +1719,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1, 2, 3],
|
"payed_for": [1, 2, 3],
|
||||||
"amount": "12",
|
"amount": "12",
|
||||||
"original_currency": "EUR",
|
|
||||||
"bill_type": "Expense",
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1966,7 +1730,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
"amount": "15",
|
"amount": "15",
|
||||||
"original_currency": "EUR",
|
|
||||||
"bill_type": "Expense",
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1978,7 +1741,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
"original_currency": "EUR",
|
|
||||||
"bill_type": "Expense",
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1988,8 +1750,8 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
resp = self.client.get(f"/raclette/feed/{token}.xml")
|
resp = self.client.get(f"/raclette/feed/{token}.xml")
|
||||||
|
|
||||||
content = resp.data.decode()
|
content = resp.data.decode()
|
||||||
assert """<title>charcuterie - €15.00</title>""" in content
|
assert """<title>charcuterie - 15.0</title>""" in content
|
||||||
assert """<title>vin blanc - €10.00</title>""" in content
|
assert """<title>vin blanc - 10.0</title>""" in content
|
||||||
|
|
||||||
def test_rss_if_modified_since_header(self):
|
def test_rss_if_modified_since_header(self):
|
||||||
# Project creation
|
# Project creation
|
||||||
|
@ -2036,7 +1798,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
"amount": "12",
|
"amount": "12",
|
||||||
"original_currency": "XXX",
|
|
||||||
"bill_type": "Expense",
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
|
@ -2094,7 +1855,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
"amount": "12",
|
"amount": "12",
|
||||||
"bill_type": "Expense",
|
"bill_type": "Expense",
|
||||||
"original_currency": "XXX",
|
|
||||||
},
|
},
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
|
@ -2175,7 +1935,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"contact_email": "zorglub@notmyidea.org",
|
"contact_email": "zorglub@notmyidea.org",
|
||||||
"current_password": "raclette",
|
"current_password": "raclette",
|
||||||
"password": "didoudida",
|
"password": "didoudida",
|
||||||
"default_currency": "XXX",
|
|
||||||
},
|
},
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from ihatemoney import models
|
||||||
from ihatemoney.utils import generate_password_hash
|
from ihatemoney.utils import generate_password_hash
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("client", "converter")
|
@pytest.mark.usefixtures("client")
|
||||||
class BaseTestCase:
|
class BaseTestCase:
|
||||||
SECRET_KEY = "TEST SESSION"
|
SECRET_KEY = "TEST SESSION"
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||||
|
@ -29,7 +29,6 @@ class BaseTestCase:
|
||||||
self,
|
self,
|
||||||
id,
|
id,
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
default_currency="XXX",
|
|
||||||
name=None,
|
name=None,
|
||||||
password=None,
|
password=None,
|
||||||
project_history=True,
|
project_history=True,
|
||||||
|
@ -45,7 +44,6 @@ class BaseTestCase:
|
||||||
"id": id,
|
"id": id,
|
||||||
"password": password,
|
"password": password,
|
||||||
"contact_email": f"{id}@notmyidea.org",
|
"contact_email": f"{id}@notmyidea.org",
|
||||||
"default_currency": default_currency,
|
|
||||||
"project_history": project_history,
|
"project_history": project_history,
|
||||||
},
|
},
|
||||||
follow_redirects=follow_redirects,
|
follow_redirects=follow_redirects,
|
||||||
|
@ -59,7 +57,7 @@ class BaseTestCase:
|
||||||
)
|
)
|
||||||
assert ("/{id}/edit" in str(resp.response)) == (not success)
|
assert ("/{id}/edit" in str(resp.response)) == (not success)
|
||||||
|
|
||||||
def create_project(self, id, default_currency="XXX", name=None, password=None):
|
def create_project(self, id, name=None, password=None):
|
||||||
name = name or str(id)
|
name = name or str(id)
|
||||||
password = password or id
|
password = password or id
|
||||||
project = models.Project(
|
project = models.Project(
|
||||||
|
@ -67,7 +65,6 @@ class BaseTestCase:
|
||||||
name=name,
|
name=name,
|
||||||
password=generate_password_hash(password),
|
password=generate_password_hash(password),
|
||||||
contact_email=f"{id}@notmyidea.org",
|
contact_email=f"{id}@notmyidea.org",
|
||||||
default_currency=default_currency,
|
|
||||||
)
|
)
|
||||||
models.db.session.add(project)
|
models.db.session.add(project)
|
||||||
models.db.session.commit()
|
models.db.session.commit()
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from jinja2 import FileSystemBytecodeCache
|
from jinja2 import FileSystemBytecodeCache
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ihatemoney.babel_utils import compile_catalogs
|
from ihatemoney.babel_utils import compile_catalogs
|
||||||
from ihatemoney.currency_convertor import CurrencyConverter
|
|
||||||
from ihatemoney.run import create_app, db
|
from ihatemoney.run import create_app, db
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,21 +41,3 @@ def client(app: Flask, request: pytest.FixtureRequest):
|
||||||
request.cls.client = client
|
request.cls.client = client
|
||||||
|
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def converter(request: pytest.FixtureRequest):
|
|
||||||
# Add dummy data to CurrencyConverter for all tests (since it's a singleton)
|
|
||||||
mock_data = {
|
|
||||||
"USD": 1,
|
|
||||||
"EUR": 0.8,
|
|
||||||
"CAD": 1.2,
|
|
||||||
"PLN": 4,
|
|
||||||
CurrencyConverter.no_currency: 1,
|
|
||||||
}
|
|
||||||
converter = CurrencyConverter()
|
|
||||||
converter.get_rates = MagicMock(return_value=mock_data)
|
|
||||||
# Also add it to an attribute to make tests clearer
|
|
||||||
request.cls.converter = converter
|
|
||||||
|
|
||||||
yield converter
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ def demo(client):
|
||||||
"id": "demo",
|
"id": "demo",
|
||||||
"password": "demo",
|
"password": "demo",
|
||||||
"contact_email": "demo@notmyidea.org",
|
"contact_email": "demo@notmyidea.org",
|
||||||
"default_currency": "XXX",
|
|
||||||
"project_history": True,
|
"project_history": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -43,7 +42,6 @@ class TestHistory(IhatemoneyTestCase):
|
||||||
"contact_email": "demo@notmyidea.org",
|
"contact_email": "demo@notmyidea.org",
|
||||||
"current_password": current_password,
|
"current_password": current_password,
|
||||||
"password": "demo",
|
"password": "demo",
|
||||||
"default_currency": "XXX",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if logging_preference != LoggingMode.DISABLED:
|
if logging_preference != LoggingMode.DISABLED:
|
||||||
|
@ -93,7 +91,6 @@ class TestHistory(IhatemoneyTestCase):
|
||||||
"current_password": "demo",
|
"current_password": "demo",
|
||||||
"password": "123456",
|
"password": "123456",
|
||||||
"project_history": "y",
|
"project_history": "y",
|
||||||
"default_currency": "USD", # Currency changed from default
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True)
|
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True)
|
||||||
|
@ -114,7 +111,7 @@ class TestHistory(IhatemoneyTestCase):
|
||||||
assert resp.data.decode("utf-8").index("Project renamed ") < resp.data.decode(
|
assert resp.data.decode("utf-8").index("Project renamed ") < resp.data.decode(
|
||||||
"utf-8"
|
"utf-8"
|
||||||
).index("Project private code changed")
|
).index("Project private code changed")
|
||||||
assert resp.data.decode("utf-8").count("<td> -- </td>") == 5
|
assert resp.data.decode("utf-8").count("<td> -- </td>") == 4
|
||||||
assert "127.0.0.1" not in resp.data.decode("utf-8")
|
assert "127.0.0.1" not in resp.data.decode("utf-8")
|
||||||
|
|
||||||
def test_project_privacy_edit(self):
|
def test_project_privacy_edit(self):
|
||||||
|
@ -184,7 +181,6 @@ class TestHistory(IhatemoneyTestCase):
|
||||||
"contact_email": "demo2@notmyidea.org",
|
"contact_email": "demo2@notmyidea.org",
|
||||||
"current_password": "demo",
|
"current_password": "demo",
|
||||||
"password": "123456",
|
"password": "123456",
|
||||||
"default_currency": "USD",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Keep privacy settings where they were
|
# Keep privacy settings where they were
|
||||||
|
@ -307,7 +303,7 @@ class TestHistory(IhatemoneyTestCase):
|
||||||
)
|
)
|
||||||
assert "Nothing to list" not in resp.data.decode("utf-8")
|
assert "Nothing to list" not in resp.data.decode("utf-8")
|
||||||
assert "Some entries below contain IP addresses," in resp.data.decode("utf-8")
|
assert "Some entries below contain IP addresses," in resp.data.decode("utf-8")
|
||||||
assert resp.data.decode("utf-8").count("127.0.0.1") == 12
|
assert resp.data.decode("utf-8").count("127.0.0.1") == 10
|
||||||
assert resp.data.decode("utf-8").count("<td> -- </td>") == 1
|
assert resp.data.decode("utf-8").count("<td> -- </td>") == 1
|
||||||
|
|
||||||
# Generate more operations to confirm additional IP info isn't recorded
|
# Generate more operations to confirm additional IP info isn't recorded
|
||||||
|
@ -315,8 +311,8 @@ class TestHistory(IhatemoneyTestCase):
|
||||||
|
|
||||||
resp = self.client.get("/demo/history")
|
resp = self.client.get("/demo/history")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.data.decode("utf-8").count("127.0.0.1") == 12
|
assert resp.data.decode("utf-8").count("127.0.0.1") == 10
|
||||||
assert resp.data.decode("utf-8").count("<td> -- </td>") == 7
|
assert resp.data.decode("utf-8").count("<td> -- </td>") == 6
|
||||||
|
|
||||||
# Ensure we can't clear IP data with a GET or with a password-less POST
|
# Ensure we can't clear IP data with a GET or with a password-less POST
|
||||||
resp = self.client.get("/demo/strip_ip_addresses")
|
resp = self.client.get("/demo/strip_ip_addresses")
|
||||||
|
@ -326,8 +322,8 @@ class TestHistory(IhatemoneyTestCase):
|
||||||
|
|
||||||
resp = self.client.get("/demo/history")
|
resp = self.client.get("/demo/history")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.data.decode("utf-8").count("127.0.0.1") == 12
|
assert resp.data.decode("utf-8").count("127.0.0.1") == 10
|
||||||
assert resp.data.decode("utf-8").count("<td> -- </td>") == 7
|
assert resp.data.decode("utf-8").count("<td> -- </td>") == 6
|
||||||
|
|
||||||
# Clear IP Data
|
# Clear IP Data
|
||||||
resp = self.client.post(
|
resp = self.client.post(
|
||||||
|
@ -350,7 +346,7 @@ class TestHistory(IhatemoneyTestCase):
|
||||||
"utf-8"
|
"utf-8"
|
||||||
)
|
)
|
||||||
assert resp.data.decode("utf-8").count("127.0.0.1") == 0
|
assert resp.data.decode("utf-8").count("127.0.0.1") == 0
|
||||||
assert resp.data.decode("utf-8").count("<td> -- </td>") == 19
|
assert resp.data.decode("utf-8").count("<td> -- </td>") == 16
|
||||||
|
|
||||||
def test_logs_for_common_actions(self):
|
def test_logs_for_common_actions(self):
|
||||||
# adds a member to this project
|
# adds a member to this project
|
||||||
|
@ -638,7 +634,6 @@ class TestHistory(IhatemoneyTestCase):
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
"bill_type": "Expense",
|
"bill_type": "Expense",
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
"original_currency": "EUR",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -81,50 +81,9 @@ class CommonTestCase(object):
|
||||||
for d in range(len(self.data)):
|
for d in range(len(self.data)):
|
||||||
self.data[d]["currency"] = currencies[d]
|
self.data[d]["currency"] = currencies[d]
|
||||||
|
|
||||||
def test_import_currencies_in_empty_project_with_currency(self):
|
def test_import_single_currency_in_empty_project(self):
|
||||||
# Import JSON with currencies in an empty project with a default currency
|
# Import JSON with a single currency in an empty project
|
||||||
|
# It should work by stripping the currency from bills.
|
||||||
self.post_project("raclette", default_currency="EUR")
|
|
||||||
self.login("raclette")
|
|
||||||
|
|
||||||
project = self.get_project("raclette")
|
|
||||||
|
|
||||||
self.populate_data_with_currencies(["EUR", "CAD", "EUR"])
|
|
||||||
self.import_project("raclette", self.generate_form_data(self.data))
|
|
||||||
|
|
||||||
bills = project.get_pretty_bills()
|
|
||||||
|
|
||||||
# Check if all bills have been added
|
|
||||||
assert len(bills) == len(self.data)
|
|
||||||
|
|
||||||
# Check if name of bills are ok
|
|
||||||
b = [e["what"] for e in bills]
|
|
||||||
b.sort()
|
|
||||||
ref = [e["what"] for e in self.data]
|
|
||||||
ref.sort()
|
|
||||||
|
|
||||||
assert b == ref
|
|
||||||
|
|
||||||
# Check if other informations in bill are ok
|
|
||||||
for d in self.data:
|
|
||||||
for b in bills:
|
|
||||||
if b["what"] == d["what"]:
|
|
||||||
assert b["payer_name"] == d["payer_name"]
|
|
||||||
assert b["amount"] == d["amount"]
|
|
||||||
assert b["currency"] == d["currency"]
|
|
||||||
assert b["payer_weight"] == d["payer_weight"]
|
|
||||||
assert b["date"] == d["date"]
|
|
||||||
assert b["bill_type"] == d["bill_type"]
|
|
||||||
list_project = [ower for ower in b["owers"]]
|
|
||||||
list_project.sort()
|
|
||||||
list_json = [ower for ower in d["owers"]]
|
|
||||||
list_json.sort()
|
|
||||||
assert list_project == list_json
|
|
||||||
|
|
||||||
def test_import_single_currency_in_empty_project_without_currency(self):
|
|
||||||
# Import JSON with a single currency in an empty project with no
|
|
||||||
# default currency. It should work by stripping the currency from
|
|
||||||
# bills.
|
|
||||||
|
|
||||||
self.post_project("raclette")
|
self.post_project("raclette")
|
||||||
self.login("raclette")
|
self.login("raclette")
|
||||||
|
@ -153,8 +112,6 @@ class CommonTestCase(object):
|
||||||
if b["what"] == d["what"]:
|
if b["what"] == d["what"]:
|
||||||
assert b["payer_name"] == d["payer_name"]
|
assert b["payer_name"] == d["payer_name"]
|
||||||
assert b["amount"] == d["amount"]
|
assert b["amount"] == d["amount"]
|
||||||
# Currency should have been stripped
|
|
||||||
assert b["currency"] == "XXX"
|
|
||||||
assert b["payer_weight"] == d["payer_weight"]
|
assert b["payer_weight"] == d["payer_weight"]
|
||||||
assert b["date"] == d["date"]
|
assert b["date"] == d["date"]
|
||||||
assert b["bill_type"] == d["bill_type"]
|
assert b["bill_type"] == d["bill_type"]
|
||||||
|
@ -182,51 +139,7 @@ class CommonTestCase(object):
|
||||||
# Check that there are no bills
|
# Check that there are no bills
|
||||||
assert len(bills) == 0
|
assert len(bills) == 0
|
||||||
|
|
||||||
def test_import_no_currency_in_empty_project_with_currency(self):
|
def test_import_no_currency_in_empty_project(self):
|
||||||
# Import JSON without currencies (from ihatemoney < 5) in an empty
|
|
||||||
# project with a default currency.
|
|
||||||
|
|
||||||
self.post_project("raclette", default_currency="EUR")
|
|
||||||
self.login("raclette")
|
|
||||||
|
|
||||||
project = self.get_project("raclette")
|
|
||||||
|
|
||||||
self.import_project("raclette", self.generate_form_data(self.data))
|
|
||||||
|
|
||||||
bills = project.get_pretty_bills()
|
|
||||||
|
|
||||||
# Check if all bills have been added
|
|
||||||
assert len(bills) == len(self.data)
|
|
||||||
|
|
||||||
# Check if name of bills are ok
|
|
||||||
b = [e["what"] for e in bills]
|
|
||||||
b.sort()
|
|
||||||
ref = [e["what"] for e in self.data]
|
|
||||||
ref.sort()
|
|
||||||
|
|
||||||
assert b == ref
|
|
||||||
|
|
||||||
# Check if other informations in bill are ok
|
|
||||||
for d in self.data:
|
|
||||||
for b in bills:
|
|
||||||
if b["what"] == d["what"]:
|
|
||||||
assert b["payer_name"] == d["payer_name"]
|
|
||||||
assert b["amount"] == d["amount"]
|
|
||||||
# All bills are converted to default project currency
|
|
||||||
assert b["currency"] == "EUR"
|
|
||||||
assert b["payer_weight"] == d["payer_weight"]
|
|
||||||
assert b["date"] == d["date"]
|
|
||||||
assert b["bill_type"] == d["bill_type"]
|
|
||||||
list_project = [ower for ower in b["owers"]]
|
|
||||||
list_project.sort()
|
|
||||||
list_json = [ower for ower in d["owers"]]
|
|
||||||
list_json.sort()
|
|
||||||
assert list_project == list_json
|
|
||||||
|
|
||||||
def test_import_no_currency_in_empty_project_without_currency(self):
|
|
||||||
# Import JSON without currencies (from ihatemoney < 5) in an empty
|
|
||||||
# project with no default currency.
|
|
||||||
|
|
||||||
self.post_project("raclette")
|
self.post_project("raclette")
|
||||||
self.login("raclette")
|
self.login("raclette")
|
||||||
|
|
||||||
|
@ -253,7 +166,6 @@ class CommonTestCase(object):
|
||||||
if b["what"] == d["what"]:
|
if b["what"] == d["what"]:
|
||||||
assert b["payer_name"] == d["payer_name"]
|
assert b["payer_name"] == d["payer_name"]
|
||||||
assert b["amount"] == d["amount"]
|
assert b["amount"] == d["amount"]
|
||||||
assert b["currency"] == "XXX"
|
|
||||||
assert b["payer_weight"] == d["payer_weight"]
|
assert b["payer_weight"] == d["payer_weight"]
|
||||||
assert b["date"] == d["date"]
|
assert b["date"] == d["date"]
|
||||||
assert b["bill_type"] == d["bill_type"]
|
assert b["bill_type"] == d["bill_type"]
|
||||||
|
@ -288,8 +200,6 @@ class CommonTestCase(object):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.populate_data_with_currencies(["XXX", "XXX", "XXX"])
|
|
||||||
|
|
||||||
self.import_project("raclette", self.generate_form_data(self.data))
|
self.import_project("raclette", self.generate_form_data(self.data))
|
||||||
|
|
||||||
bills = project.get_pretty_bills()
|
bills = project.get_pretty_bills()
|
||||||
|
@ -311,7 +221,6 @@ class CommonTestCase(object):
|
||||||
if b["what"] == d["what"]:
|
if b["what"] == d["what"]:
|
||||||
assert b["payer_name"] == d["payer_name"]
|
assert b["payer_name"] == d["payer_name"]
|
||||||
assert b["amount"] == d["amount"]
|
assert b["amount"] == d["amount"]
|
||||||
assert b["currency"] == d["currency"]
|
|
||||||
assert b["payer_weight"] == d["payer_weight"]
|
assert b["payer_weight"] == d["payer_weight"]
|
||||||
assert b["date"] == d["date"]
|
assert b["date"] == d["date"]
|
||||||
assert b["bill_type"] == d["bill_type"]
|
assert b["bill_type"] == d["bill_type"]
|
||||||
|
@ -406,7 +315,6 @@ class TestExport(IhatemoneyTestCase):
|
||||||
"bill_type": "Reimbursement",
|
"bill_type": "Reimbursement",
|
||||||
"what": "refund",
|
"what": "refund",
|
||||||
"amount": 13.33,
|
"amount": 13.33,
|
||||||
"currency": "XXX",
|
|
||||||
"payer_name": "tata",
|
"payer_name": "tata",
|
||||||
"payer_weight": 1.0,
|
"payer_weight": 1.0,
|
||||||
"owers": ["jeanne"],
|
"owers": ["jeanne"],
|
||||||
|
@ -416,7 +324,6 @@ class TestExport(IhatemoneyTestCase):
|
||||||
"bill_type": "Expense",
|
"bill_type": "Expense",
|
||||||
"what": "red wine",
|
"what": "red wine",
|
||||||
"amount": 200.0,
|
"amount": 200.0,
|
||||||
"currency": "XXX",
|
|
||||||
"payer_name": "jeanne",
|
"payer_name": "jeanne",
|
||||||
"payer_weight": 1.0,
|
"payer_weight": 1.0,
|
||||||
"owers": ["zorglub", "tata"],
|
"owers": ["zorglub", "tata"],
|
||||||
|
@ -426,7 +333,6 @@ class TestExport(IhatemoneyTestCase):
|
||||||
"bill_type": "Expense",
|
"bill_type": "Expense",
|
||||||
"what": "\xe0 raclette",
|
"what": "\xe0 raclette",
|
||||||
"amount": 10.0,
|
"amount": 10.0,
|
||||||
"currency": "XXX",
|
|
||||||
"payer_name": "zorglub",
|
"payer_name": "zorglub",
|
||||||
"payer_weight": 2.0,
|
"payer_weight": 2.0,
|
||||||
"owers": ["zorglub", "jeanne", "tata", "p\xe9p\xe9"],
|
"owers": ["zorglub", "jeanne", "tata", "p\xe9p\xe9"],
|
||||||
|
@ -437,10 +343,10 @@ class TestExport(IhatemoneyTestCase):
|
||||||
# generate csv export of bills
|
# generate csv export of bills
|
||||||
resp = self.client.get("/raclette/export/bills.csv")
|
resp = self.client.get("/raclette/export/bills.csv")
|
||||||
expected = [
|
expected = [
|
||||||
"date,what,bill_type,amount,currency,payer_name,payer_weight,owers",
|
"date,what,bill_type,amount,payer_name,payer_weight,owers",
|
||||||
"2017-01-01,refund,Reimbursement,XXX,13.33,tata,1.0,jeanne",
|
"2017-01-01,refund,Reimbursement,13.33,tata,1.0,jeanne",
|
||||||
'2016-12-31,red wine,Expense,XXX,200.0,jeanne,1.0,"zorglub, tata"',
|
'2016-12-31,red wine,Expense,200.0,jeanne,1.0,"zorglub, tata"',
|
||||||
'2016-12-31,à raclette,Expense,10.0,XXX,zorglub,2.0,"zorglub, jeanne, tata, pépé"',
|
'2016-12-31,à raclette,Expense,10.0,zorglub,2.0,"zorglub, jeanne, tata, pépé"',
|
||||||
]
|
]
|
||||||
received_lines = resp.data.decode("utf-8").split("\n")
|
received_lines = resp.data.decode("utf-8").split("\n")
|
||||||
|
|
||||||
|
@ -452,14 +358,12 @@ class TestExport(IhatemoneyTestCase):
|
||||||
expected = [
|
expected = [
|
||||||
{
|
{
|
||||||
"amount": 2.00,
|
"amount": 2.00,
|
||||||
"currency": "XXX",
|
|
||||||
"receiver": "jeanne",
|
"receiver": "jeanne",
|
||||||
"ower": "p\xe9p\xe9",
|
"ower": "p\xe9p\xe9",
|
||||||
},
|
},
|
||||||
{"amount": 55.34, "currency": "XXX", "receiver": "jeanne", "ower": "tata"},
|
{"amount": 55.34, "receiver": "jeanne", "ower": "tata"},
|
||||||
{
|
{
|
||||||
"amount": 127.33,
|
"amount": 127.33,
|
||||||
"currency": "XXX",
|
|
||||||
"receiver": "jeanne",
|
"receiver": "jeanne",
|
||||||
"ower": "zorglub",
|
"ower": "zorglub",
|
||||||
},
|
},
|
||||||
|
@ -471,10 +375,10 @@ class TestExport(IhatemoneyTestCase):
|
||||||
resp = self.client.get("/raclette/export/transactions.csv")
|
resp = self.client.get("/raclette/export/transactions.csv")
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
"amount,currency,receiver,ower",
|
"amount,receiver,ower",
|
||||||
"2.0,XXX,jeanne,pépé",
|
"2.0,jeanne,pépé",
|
||||||
"55.34,XXX,jeanne,tata",
|
"55.34,jeanne,tata",
|
||||||
"127.33,XXX,jeanne,zorglub",
|
"127.33,jeanne,zorglub",
|
||||||
]
|
]
|
||||||
received_lines = resp.data.decode("utf-8").split("\n")
|
received_lines = resp.data.decode("utf-8").split("\n")
|
||||||
|
|
||||||
|
@ -485,179 +389,8 @@ class TestExport(IhatemoneyTestCase):
|
||||||
resp = self.client.get("/raclette/export/transactions.wrong")
|
resp = self.client.get("/raclette/export/transactions.wrong")
|
||||||
assert resp.status_code == 404
|
assert resp.status_code == 404
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Currency conversion is broken")
|
|
||||||
def test_export_with_currencies(self):
|
|
||||||
self.post_project("raclette", default_currency="EUR")
|
|
||||||
|
|
||||||
# add participants
|
|
||||||
self.client.post("/raclette/members/add", data={"name": "zorglub", "weight": 2})
|
|
||||||
self.client.post("/raclette/members/add", data={"name": "jeanne"})
|
|
||||||
self.client.post("/raclette/members/add", data={"name": "tata"})
|
|
||||||
self.client.post("/raclette/members/add", data={"name": "pépé"})
|
|
||||||
|
|
||||||
# create bills
|
|
||||||
self.client.post(
|
|
||||||
"/raclette/add",
|
|
||||||
data={
|
|
||||||
"date": "2016-12-31",
|
|
||||||
"what": "à raclette",
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"payer": 1,
|
|
||||||
"payed_for": [1, 2, 3, 4],
|
|
||||||
"amount": "10.0",
|
|
||||||
"original_currency": "EUR",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.post(
|
|
||||||
"/raclette/add",
|
|
||||||
data={
|
|
||||||
"date": "2016-12-31",
|
|
||||||
"what": "poutine from Québec",
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"payer": 2,
|
|
||||||
"payed_for": [1, 3],
|
|
||||||
"amount": "100",
|
|
||||||
"original_currency": "CAD",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.post(
|
|
||||||
"/raclette/add",
|
|
||||||
data={
|
|
||||||
"date": "2017-01-01",
|
|
||||||
"what": "refund",
|
|
||||||
"bill_type": "Reimbursement",
|
|
||||||
"payer": 3,
|
|
||||||
"payed_for": [2],
|
|
||||||
"amount": "13.33",
|
|
||||||
"original_currency": "EUR",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# generate json export of bills
|
|
||||||
resp = self.client.get("/raclette/export/bills.json")
|
|
||||||
expected = [
|
|
||||||
{
|
|
||||||
"date": "2017-01-01",
|
|
||||||
"what": "refund",
|
|
||||||
"bill_type": "Reimbursement",
|
|
||||||
"amount": 13.33,
|
|
||||||
"currency": "EUR",
|
|
||||||
"payer_name": "tata",
|
|
||||||
"payer_weight": 1.0,
|
|
||||||
"owers": ["jeanne"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2016-12-31",
|
|
||||||
"what": "poutine from Qu\xe9bec",
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": 100.0,
|
|
||||||
"currency": "CAD",
|
|
||||||
"payer_name": "jeanne",
|
|
||||||
"payer_weight": 1.0,
|
|
||||||
"owers": ["zorglub", "tata"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2016-12-31",
|
|
||||||
"what": "fromage \xe0 raclette",
|
|
||||||
"bill_type": "Expense",
|
|
||||||
"amount": 10.0,
|
|
||||||
"currency": "EUR",
|
|
||||||
"payer_name": "zorglub",
|
|
||||||
"payer_weight": 2.0,
|
|
||||||
"owers": ["zorglub", "jeanne", "tata", "p\xe9p\xe9"],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
assert json.loads(resp.data.decode("utf-8")) == expected
|
|
||||||
|
|
||||||
# generate csv export of bills
|
|
||||||
resp = self.client.get("/raclette/export/bills.csv")
|
|
||||||
expected = [
|
|
||||||
"date,what,bill_type,amount,currency,payer_name,payer_weight,owers",
|
|
||||||
"2017-01-01,refund,Reimbursement,13.33,EUR,tata,1.0,jeanne",
|
|
||||||
'2016-12-31,poutine from Québec,Expense,100.0,CAD,jeanne,1.0,"zorglub, tata"',
|
|
||||||
'2016-12-31,à raclette,Expense,10.0,EUR,zorglub,2.0,"zorglub, jeanne, tata, pépé"',
|
|
||||||
]
|
|
||||||
received_lines = resp.data.decode("utf-8").split("\n")
|
|
||||||
|
|
||||||
for i, line in enumerate(expected):
|
|
||||||
assert set(line.split(",")) == set(received_lines[i].strip("\r").split(","))
|
|
||||||
|
|
||||||
# generate json export of transactions (in EUR!)
|
|
||||||
resp = self.client.get("/raclette/export/transactions.json")
|
|
||||||
expected = [
|
|
||||||
{
|
|
||||||
"amount": 2.00,
|
|
||||||
"currency": "EUR",
|
|
||||||
"receiver": "jeanne",
|
|
||||||
"ower": "p\xe9p\xe9",
|
|
||||||
},
|
|
||||||
{"amount": 10.89, "currency": "EUR", "receiver": "jeanne", "ower": "tata"},
|
|
||||||
{
|
|
||||||
"amount": 38.45,
|
|
||||||
"currency": "EUR",
|
|
||||||
"receiver": "jeanne",
|
|
||||||
"ower": "zorglub",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
assert json.loads(resp.data.decode("utf-8")) == expected
|
|
||||||
|
|
||||||
# generate csv export of transactions
|
|
||||||
resp = self.client.get("/raclette/export/transactions.csv")
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
"amount,currency,receiver,ower",
|
|
||||||
"2.0,EUR,jeanne,pépé",
|
|
||||||
"10.89,EUR,jeanne,tata",
|
|
||||||
"38.45,EUR,jeanne,zorglub",
|
|
||||||
]
|
|
||||||
received_lines = resp.data.decode("utf-8").split("\n")
|
|
||||||
|
|
||||||
for i, line in enumerate(expected):
|
|
||||||
assert set(line.split(",")) == set(received_lines[i].strip("\r").split(","))
|
|
||||||
|
|
||||||
# Change project currency to CAD
|
|
||||||
project = self.get_project("raclette")
|
|
||||||
project.switch_currency("CAD")
|
|
||||||
|
|
||||||
# generate json export of transactions (now in CAD!)
|
|
||||||
resp = self.client.get("/raclette/export/transactions.json")
|
|
||||||
expected = [
|
|
||||||
{
|
|
||||||
"amount": 3.00,
|
|
||||||
"currency": "CAD",
|
|
||||||
"receiver": "jeanne",
|
|
||||||
"ower": "p\xe9p\xe9",
|
|
||||||
},
|
|
||||||
{"amount": 16.34, "currency": "CAD", "receiver": "jeanne", "ower": "tata"},
|
|
||||||
{
|
|
||||||
"amount": 57.67,
|
|
||||||
"currency": "CAD",
|
|
||||||
"receiver": "jeanne",
|
|
||||||
"ower": "zorglub",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
assert json.loads(resp.data.decode("utf-8")) == expected
|
|
||||||
|
|
||||||
# generate csv export of transactions
|
|
||||||
resp = self.client.get("/raclette/export/transactions.csv")
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
"amount,currency,receiver,ower",
|
|
||||||
"3.0,CAD,jeanne,pépé",
|
|
||||||
"16.34,CAD,jeanne,tata",
|
|
||||||
"57.67,CAD,jeanne,zorglub",
|
|
||||||
]
|
|
||||||
received_lines = resp.data.decode("utf-8").split("\n")
|
|
||||||
|
|
||||||
for i, line in enumerate(expected):
|
|
||||||
assert set(line.split(",")) == set(received_lines[i].strip("\r").split(","))
|
|
||||||
|
|
||||||
def test_export_escape_formulae(self):
|
def test_export_escape_formulae(self):
|
||||||
self.post_project("raclette", default_currency="EUR")
|
self.post_project("raclette")
|
||||||
|
|
||||||
# add participants
|
# add participants
|
||||||
self.client.post("/raclette/members/add", data={"name": "zorglub"})
|
self.client.post("/raclette/members/add", data={"name": "zorglub"})
|
||||||
|
@ -672,15 +405,14 @@ class TestExport(IhatemoneyTestCase):
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
"amount": "10.0",
|
"amount": "10.0",
|
||||||
"original_currency": "EUR",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# generate csv export of bills
|
# generate csv export of bills
|
||||||
resp = self.client.get("/raclette/export/bills.csv")
|
resp = self.client.get("/raclette/export/bills.csv")
|
||||||
expected = [
|
expected = [
|
||||||
"date,what,bill_type,amount,currency,payer_name,payer_weight,owers",
|
"date,what,bill_type,amount,payer_name,payer_weight,owers",
|
||||||
"2016-12-31,'=COS(36),Expense,10.0,EUR,zorglub,1.0,zorglub",
|
"2016-12-31,'=COS(36),Expense,10.0,zorglub,1.0,zorglub",
|
||||||
]
|
]
|
||||||
received_lines = resp.data.decode("utf-8").split("\n")
|
received_lines = resp.data.decode("utf-8").split("\n")
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ from sqlalchemy import orm
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
from ihatemoney import models
|
from ihatemoney import models
|
||||||
from ihatemoney.currency_convertor import CurrencyConverter
|
|
||||||
from ihatemoney.manage import (
|
from ihatemoney.manage import (
|
||||||
delete_project,
|
delete_project,
|
||||||
generate_config,
|
generate_config,
|
||||||
|
@ -376,7 +375,6 @@ class TestCaptcha(IhatemoneyTestCase):
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"password": "party",
|
"password": "party",
|
||||||
"contact_email": "raclette@notmyidea.org",
|
"contact_email": "raclette@notmyidea.org",
|
||||||
"default_currency": "USD",
|
|
||||||
"captcha": "éùüß",
|
"captcha": "éùüß",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -391,7 +389,6 @@ class TestCaptcha(IhatemoneyTestCase):
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"password": "party",
|
"password": "party",
|
||||||
"contact_email": "raclette@notmyidea.org",
|
"contact_email": "raclette@notmyidea.org",
|
||||||
"default_currency": "USD",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert len(models.Project.query.all()) == 0
|
assert len(models.Project.query.all()) == 0
|
||||||
|
@ -403,7 +400,6 @@ class TestCaptcha(IhatemoneyTestCase):
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"password": "party",
|
"password": "party",
|
||||||
"contact_email": "raclette@notmyidea.org",
|
"contact_email": "raclette@notmyidea.org",
|
||||||
"default_currency": "USD",
|
|
||||||
"captcha": "nope",
|
"captcha": "nope",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -416,7 +412,6 @@ class TestCaptcha(IhatemoneyTestCase):
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"password": "party",
|
"password": "party",
|
||||||
"contact_email": "raclette@notmyidea.org",
|
"contact_email": "raclette@notmyidea.org",
|
||||||
"default_currency": "USD",
|
|
||||||
"captcha": "euro",
|
"captcha": "euro",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -435,37 +430,3 @@ class TestCaptcha(IhatemoneyTestCase):
|
||||||
)
|
)
|
||||||
assert resp.status_code == 201
|
assert resp.status_code == 201
|
||||||
assert len(models.Project.query.all()) == 1
|
assert len(models.Project.query.all()) == 1
|
||||||
|
|
||||||
|
|
||||||
class TestCurrencyConverter:
|
|
||||||
converter = CurrencyConverter()
|
|
||||||
mock_data = {
|
|
||||||
"USD": 1,
|
|
||||||
"EUR": 0.8,
|
|
||||||
"CAD": 1.2,
|
|
||||||
"PLN": 4,
|
|
||||||
CurrencyConverter.no_currency: 1,
|
|
||||||
}
|
|
||||||
converter.get_rates = MagicMock(return_value=mock_data)
|
|
||||||
|
|
||||||
def test_only_one_instance(self):
|
|
||||||
one = id(CurrencyConverter())
|
|
||||||
two = id(CurrencyConverter())
|
|
||||||
assert one == two
|
|
||||||
|
|
||||||
def test_get_currencies(self):
|
|
||||||
currencies = self.converter.get_currencies()
|
|
||||||
for currency in ["USD", "EUR", "CAD", "PLN", CurrencyConverter.no_currency]:
|
|
||||||
assert currency in currencies
|
|
||||||
|
|
||||||
def test_exchange_currency(self):
|
|
||||||
result = self.converter.exchange_currency(100, "USD", "EUR")
|
|
||||||
assert result == 80.0
|
|
||||||
|
|
||||||
def test_failing_remote(self):
|
|
||||||
rates = {}
|
|
||||||
with patch("requests.Response.json", new=lambda _: {}):
|
|
||||||
# we need a non-patched converter, but it seems that MagickMock
|
|
||||||
# is mocking EVERY instance of the class method. Too bad.
|
|
||||||
rates = CurrencyConverter.get_rates(self.converter)
|
|
||||||
assert rates == {CurrencyConverter.no_currency: 1}
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ import smtplib
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from babel import Locale
|
from babel import Locale
|
||||||
from babel.numbers import get_currency_name, get_currency_symbol
|
|
||||||
from flask import current_app, flash, redirect, render_template
|
from flask import current_app, flash, redirect, render_template
|
||||||
from flask_babel import get_locale, lazy_gettext as _
|
from flask_babel import get_locale, lazy_gettext as _
|
||||||
from flask_limiter import Limiter
|
from flask_limiter import Limiter
|
||||||
|
@ -309,7 +308,6 @@ def same_bill(bill1, bill2):
|
||||||
"payer_name",
|
"payer_name",
|
||||||
"payer_weight",
|
"payer_weight",
|
||||||
"amount",
|
"amount",
|
||||||
"currency",
|
|
||||||
"date",
|
"date",
|
||||||
"owers",
|
"owers",
|
||||||
]
|
]
|
||||||
|
@ -410,21 +408,6 @@ def localize_list(items, surround_with_em=True):
|
||||||
return output_str.format(start_object=wrapped_items.pop())
|
return output_str.format(start_object=wrapped_items.pop())
|
||||||
|
|
||||||
|
|
||||||
def render_localized_currency(code, detailed=True):
|
|
||||||
# We cannot use CurrencyConvertor.no_currency here because of circular dependencies
|
|
||||||
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):
|
def render_localized_template(template_name_prefix, **context):
|
||||||
"""Like render_template(), but selects the right template according to the
|
"""Like render_template(), but selects the right template according to the
|
||||||
current user language. Fallback to English if a template for the
|
current user language. Fallback to English if a template for the
|
||||||
|
|
|
@ -40,7 +40,6 @@ from sqlalchemy_continuum import Operation
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
from ihatemoney.currency_convertor import CurrencyConverter
|
|
||||||
from ihatemoney.emails import send_creation_email
|
from ihatemoney.emails import send_creation_email
|
||||||
from ihatemoney.forms import (
|
from ihatemoney.forms import (
|
||||||
AdminAuthenticationForm,
|
AdminAuthenticationForm,
|
||||||
|
@ -448,7 +447,6 @@ def edit_project():
|
||||||
edit_form.ip_recording.data = True
|
edit_form.ip_recording.data = True
|
||||||
|
|
||||||
edit_form.contact_email.data = g.project.contact_email
|
edit_form.contact_email.data = g.project.contact_email
|
||||||
edit_form.default_currency.data = g.project.default_currency
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"edit_project.html",
|
"edit_project.html",
|
||||||
|
@ -479,7 +477,6 @@ def import_project():
|
||||||
attr = [
|
attr = [
|
||||||
"amount",
|
"amount",
|
||||||
"bill_type",
|
"bill_type",
|
||||||
"currency",
|
|
||||||
"date",
|
"date",
|
||||||
"owers",
|
"owers",
|
||||||
"payer_name",
|
"payer_name",
|
||||||
|
@ -488,28 +485,22 @@ def import_project():
|
||||||
]
|
]
|
||||||
currencies = set()
|
currencies = set()
|
||||||
for b in bills:
|
for b in bills:
|
||||||
if b.get("currency", "") in ["", "XXX"]:
|
if "currency" in b.keys():
|
||||||
b["currency"] = g.project.default_currency
|
currencies.add(b["currency"])
|
||||||
|
del b["currency"]
|
||||||
for a in attr:
|
for a in attr:
|
||||||
if a not in b:
|
if a not in b:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
_("Missing attribute: %(attribute)s", attribute=a)
|
_("Missing attribute: %(attribute)s", attribute=a)
|
||||||
)
|
)
|
||||||
currencies.add(b["currency"])
|
|
||||||
|
|
||||||
# Additional checks if project has no default currency
|
if len(currencies) > 1:
|
||||||
if g.project.default_currency == CurrencyConverter.no_currency:
|
|
||||||
# If bills have currencies, they must be consistent
|
|
||||||
if len(currencies - {CurrencyConverter.no_currency}) >= 2:
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
_(
|
_(
|
||||||
"Cannot add bills in multiple currencies to a project without default "
|
"Cannot add bills in multiple currencies to a project without default "
|
||||||
"currency"
|
"currency"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Strip currency from bills (since it's the same for every bill)
|
|
||||||
for b in bills:
|
|
||||||
b["currency"] = CurrencyConverter.no_currency
|
|
||||||
|
|
||||||
g.project.import_bills(bills)
|
g.project.import_bills(bills)
|
||||||
|
|
||||||
|
@ -863,7 +854,6 @@ def settle(amount, ower_id, payer_id):
|
||||||
date=datetime.datetime.today(),
|
date=datetime.datetime.today(),
|
||||||
owers=[Person.query.get(payer_id)],
|
owers=[Person.query.get(payer_id)],
|
||||||
payer_id=ower_id,
|
payer_id=ower_id,
|
||||||
project_default_currency=g.project.default_currency,
|
|
||||||
bill_type=BillType.REIMBURSEMENT,
|
bill_type=BillType.REIMBURSEMENT,
|
||||||
what=_("Settlement"),
|
what=_("Settlement"),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue